From 2d8ec362f43ef86b0b1d927ff199fe6b8447265f Mon Sep 17 00:00:00 2001 From: Robert Hu Date: Thu, 2 Oct 2025 12:02:34 -0400 Subject: [PATCH 01/14] Added code for subcellular analysis (GW/OT), and updated dependencies. Started from updated main branch. --- requirements.txt | 1 + src/cajal/subcellular.py | 623 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 624 insertions(+) create mode 100644 src/cajal/subcellular.py diff --git a/requirements.txt b/requirements.txt index 9780c69..a54c600 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,6 +37,7 @@ pexpect==4.9.0 pillow==11.0 platformdirs==4.2.2 potpourri3d==1.1.0 +POT==0.9.5 pox==0.3.4 ppft==1.7.6.8 prompt_toolkit==3.0.47 diff --git a/src/cajal/subcellular.py b/src/cajal/subcellular.py new file mode 100644 index 0000000..5277778 --- /dev/null +++ b/src/cajal/subcellular.py @@ -0,0 +1,623 @@ +import numpy as np +import pandas as pd +import os +import skimage as ski +import matplotlib.pyplot as plt +from tqdm import tqdm +import pickle +import copy +from scipy.spatial.distance import cdist, pdist, squareform +from multiprocessing import Pool, cpu_count +import itertools as it +import ot +import warnings + +from .sample_seg import cell_boundaries +from .gw_cython import gw_cython_core + + +def make_cell_image(gw_ot_cell, channels): + # Load GW_OT_Cell object if path specified + if isinstance(gw_ot_cell, str): + with open(gw_ot_cell, 'rb') as file: + gw_ot_cell = pickle.load(file) + coords = gw_ot_cell.coords + coords[:,0] = coords[:,0] - coords[:,0].min() + coords[:,1] = coords[:,1] - coords[:,1].min() + # make new (n_channel + 1) x cell_width x cell_len image array + cell_image = np.zeros((coords[:,0].max()+1, coords[:,1].max()+1, len(channels)+1)) + for coord_i in range(len(coords)): + i,j = coords[coord_i] + cell_image[i,j,0] = 1 # store segmentation mask + for channel_i in range(len(channels)): # store channel pixel intensities + channel = channels[channel_i] + if channel == 'nucleus': + cell_image[i,j,channel_i+1] = gw_ot_cell.nucleus[coord_i] + else: + cell_image[i,j,channel_i+1] = gw_ot_cell.intensities[channel][coord_i] + for channel_i in range(len(channels)): + cell_image[:,:,channel_i+1] -= cell_image[:,:,channel_i+1].min() + cell_image[:,:,channel_i+1] /= cell_image[:,:,channel_i+1].max() + return(cell_image) + + +def to_shape(a, shape): + + z_, y_, x_ = shape + z, y, x = a.shape + z_pad = (z_-z) + y_pad = (y_-y) + x_pad = (x_-x) + return np.pad(a,((z_pad//2, z_pad//2 + z_pad%2), + (y_pad//2, y_pad//2 + y_pad%2), + (x_pad//2, x_pad//2 + x_pad%2)), + mode = 'constant') + + +def make_cell_image_for_plot(image, mask_alpha=0.2): + im = np.zeros((image.shape[0], image.shape[1], 3)) + mask = image[:,:,0].copy() + for channel_i in range(1, image.shape[2]): + # add transparent cell mask to channel + im[:,:,channel_i-1] = image[:,:,channel_i] + (mask * mask_alpha) + # rescale channel + im[:,:,channel_i-1] = im[:,:,channel_i-1] / im[:,:,channel_i-1].max() + # add mask to empty channels if any + for channel_i in range(image.shape[2], 4): + im[:,:,channel_i-1] = im[:,:,channel_i-1] + (mask * mask_alpha) + # reorder channel color ordering to blue, red, green + im = im[:,:,[1,2,0]] + return(im) + + +def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha = 0.2): + """ + Plots a cell image with the specified channels. + + Args: + gw_ot_cell: The cell object or file path. + channels: A list of channels to plot. + make_square: Whether to make the plot square. + ax: The axes to plot on. + mask_alpha: The alpha value for the mask. + + Returns: + The axes object with the plotted image if ax is specified, else returns None. + """ + if len(channels) > 3: + raise ValueError("Only up to 3 channels can be plotted.") + image = make_cell_image(gw_ot_cell, channels) + image = make_cell_image_for_plot(image, mask_alpha=mask_alpha) + if make_square: + max_dim = max(image.shape[0], image.shape[1]) + image = to_shape(image, (max_dim, max_dim, image.shape[2])) + if ax: + return(ax.imshow(image)) + else: + plt.imshow(image) + + +def rescale_mask_to_pixel_count(mask, target_pixels, max_iter=20, tolerance=0.01): + """ + Rescale a binary mask to achieve a target number of non-zero pixels using skimage.transform.resize. + + Args: + mask: 2D binary numpy array (0s and 1s) + target_pixels: Desired number of non-zero pixels + max_iter: Maximum number of scaling iterations (default: 20) + tolerance: Acceptable relative error (default: 0.01 for 1%) + + Returns: + Rescaled mask with pixel count close to target, maintaining binary values + """ + current_pixels = np.count_nonzero(mask) + + # If mask is already close enough, return it + if abs(current_pixels - target_pixels) / target_pixels < tolerance: + return mask + + # Initial scale factor estimate (area scales with square of linear dimensions) + scale_factor = np.sqrt(target_pixels / current_pixels) + + for _ in range(max_iter): + # Calculate new dimensions (maintaining aspect ratio) + h, w = mask.shape + new_h = max(1, int(h * scale_factor)) + new_w = max(1, int(w * scale_factor)) + + # Resize the mask using skimage.transform.resize + resized_mask = ski.transform.resize(mask.astype(float), + (new_h, new_w), + order=0, # nearest neighbor interpolation + preserve_range=True, + anti_aliasing=False) + + # Binarize the result (threshold at 0.5 to maintain binary nature) + resized_mask = (resized_mask > 0.5).astype(np.uint8) + + # Count current non-zero pixels + current_pixels = np.count_nonzero(resized_mask) + + # Check if we're within tolerance + if abs(current_pixels - target_pixels) / target_pixels < tolerance: + return resized_mask + + # Update scale factor based on current error + scale_factor *= np.sqrt(target_pixels / current_pixels) + + # Return the best result if max iterations reached + return resized_mask + + +def compute_geodesic_dmat(mask_coords): + """ + Compute geodesic distance matrix for given coordinates within a binary mask. + """ + mask_coords[:,0] = mask_coords[:,0] - mask_coords[:,0].min() + mask_coords[:,1] = mask_coords[:,1] - mask_coords[:,1].min() + cell_mask = np.zeros((mask_coords[:,0].max()+1, mask_coords[:,1].max()+1)) + cell_mask[mask_coords[:,0], mask_coords[:,1]] = 1 + # Initialize MCP_Geometric with the mask (cost=1 for foreground, inf for background) + cost_array = np.where(cell_mask > 0, 1, np.inf) + mcp = ski.graph.MCP_Geometric(cost_array) + # Compute geodesic distances from each pixel to all others + N = len(mask_coords) + geodesic_dmat = np.zeros((N, N)) + for i, start in enumerate(mask_coords): + costs, traceback = mcp.find_costs([tuple(start)]) + geodesic_dmat[i] = costs[mask_coords[:,0], mask_coords[:,1]] + return(geodesic_dmat) + + +class GW_OT_Cell: + """ + Represents a cell in the GW-OT framework. + + Args: + coords: list of (x, y) tuples for each pixel in the cell + boundary_coords: list of (x, y) tuples sampled from the cell boundary + intensities: dict mapping channel names to pixel intensity arrays + nucleus: array or list indicating nuclear identity for each cell pixel + """ + def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, metric='euclidean'): + self.coords = coords + self.boundary_coords = boundary_coords + if metric is None: + self.coord_dmat = None + self.boundary_coord_dmat = None + elif metric == 'geodesic': + self.coord_dmat = compute_geodesic_dmat(self.coords) + self.boundary_coord_dmat = None + # if boundary_coords is not None: + # warnings.warn("Geodesic distance matrix cannot be computed for cell boundary coordinates, ignoring.") + else: + self.coord_dmat = squareform(pdist(coords, metric=metric)) + self.boundary_coord_dmat = squareform(pdist(boundary_coords, metric=metric)) if boundary_coords is not None else None + self.intensities = intensities if intensities is not None else {} + self.nucleus = nucleus + self.size = len(coords) + + def copy(self): + copy = GW_OT_Cell( + coords=self.coords.copy(), + boundary_coords=self.boundary_coords.copy() if self.boundary_coords is not None else None, + intensities=copy.deepcopy(self.intensities), + nucleus=self.nucleus.copy() if self.nucleus is not None else None + ) + copy.coord_dmat = self.coord_dmat.copy() if self.coord_dmat is not None else None + copy.boundary_coord_dmat = self.boundary_coord_dmat.copy() if self.boundary_coords is not None else None + copy.size = self.size + return copy + + +def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_factor=None, ds_target_size=None, + filter_border_cells=True, n_boundary_points=100, save_path=None, return_objects=True): + """ + Create a list of GW_OT_Cell objects, each representing a cell in the image. + Args: + image: 3D numpy array (H x W x C) representing the image + channels: List of channel names corresponding to the last dimension of the image + cell_mask_image: 2D numpy array (H x W) with integer labels for each cell (0 for background) + nucleus_mask_image: Optional 2D numpy array (H x W) with integer labels for nuclei (0 for background) + ds_factor: Optional downsampling factor (integer). If provided, downsample by this factor. + ds_target_size: Optional target size (integer). If provided, downsample to achieve this number of pixels per cell. + filter_border_cells: If True, exclude cells touching the image border. + n_boundary_points: If provided, sample this many points from the cell boundary and include in the dictionary. + save_path: Optional path to save the processed cell objects. + return_objects: If True, return the list of GW_OT_Cell objects. + Returns: + If return_objects is True, return the list of GW_OT_Cell objects; otherwise, return None. + """ + cell_inds = np.unique(cell_mask_image) + cell_inds = cell_inds[cell_inds > 0] # Remove background (0) + + gw_ot_cells = [] + for cell_ind in cell_inds: + cell_mask = (cell_mask_image == cell_ind).astype(np.uint8) + nuc_mask = (nucleus_mask_image == cell_ind).astype(np.uint8) if nucleus_mask_image is not None else None + # Filter out cells touching the border + if filter_border_cells: + if np.any(cell_mask[0, :]) or np.any(cell_mask[-1, :]) or np.any(cell_mask[:, 0]) or np.any(cell_mask[:, -1]): + continue + # Downsample image and masks if necessary + if ds_factor is not None: # downsample by a factor + image_ds = ski.transform.resize(image, (image.shape[0] // ds_factor, image.shape[1] // ds_factor, image.shape[2]), order=1, preserve_range=True).astype(np.uint8) + cell_mask_ds = ski.transform.resize(cell_mask, (cell_mask.shape[0] // ds_factor, cell_mask.shape[1] // ds_factor), order=0, anti_aliasing=False, preserve_range=True).astype(np.uint8) + nuc_mask_ds = ski.transform.resize(nuc_mask, (nuc_mask.shape[0] // ds_factor, nuc_mask.shape[1] // ds_factor), order=0, anti_aliasing=False, preserve_range=True).astype(np.uint8) if nuc_mask is not None else None + elif ds_target_size is not None: # downsample to a target size + cell_mask_ds = rescale_mask_to_pixel_count(cell_mask, target_pixels=ds_target_size) + nuc_mask_ds = ski.transform.resize(nuc_mask, (cell_mask_ds.shape[0], cell_mask_ds.shape[1]), order=0, anti_aliasing=False, preserve_range=True).astype(np.uint8) if nuc_mask is not None else None + image_ds = ski.transform.resize(image, (cell_mask_ds.shape[0], cell_mask_ds.shape[1], image.shape[2]), order=1, preserve_range=True).astype(np.uint8) + else: + image_ds = image + cell_mask_ds = cell_mask + nuc_mask_ds = nuc_mask + # Create a cell object + # Sample points from cell boundary (if specified) + cell_boundary_pts = None + if n_boundary_points is not None: + _, cell_boundary_pts = cell_boundaries(np.pad(cell_mask, 1), n_sample=n_boundary_points)[0] # pad to avoid border issues + cell_boundary_pts = cell_boundary_pts - 1 # remove padding + gw_ot_cell = GW_OT_Cell(coords=np.array(np.where(cell_mask_ds)).T, boundary_coords=cell_boundary_pts) + if nucleus_mask_image is not None: + gw_ot_cell.nucleus = nuc_mask_ds[np.where(cell_mask_ds)] + if gw_ot_cell.nucleus.sum() == 0: # filter cells without segmented nuclei + continue + for channel in channels: + gw_ot_cell.intensities[channel] = image_ds[np.where(cell_mask_ds)][:,channels.index(channel)] + # Normalize the channels (to sum to 1) + for channel in channels: + gw_ot_cell.intensities[channel] = gw_ot_cell.intensities[channel] / np.sum(gw_ot_cell.intensities[channel]) + if return_objects: + gw_ot_cells.append(gw_ot_cell) + if save_path is not None: + if not os.path.isdir(save_path): # Create directory if it doesn't exist + os.makedirs(save_path) + with open(os.path.join(save_path, 'cell_'+str(cell_ind).zfill(4)+'.pickle'), 'wb') as file: + pickle.dump(gw_ot_cell, file) + if return_objects: + return gw_ot_cells + else: + return None + + +def _init_gw_pool(cell_objects: list, points: str): + # list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects + global _CELL_OBJECTS + _CELL_OBJECTS = cell_objects + # set of points to use for distance computation ('boundary' or 'full') + global _POINTS + _POINTS = points + + +def _gw_index(p: tuple[int, int]): + """ + Compute Gromov-Wasserstein distance between two cells given their indices. + Args: + p: tuple of two indices (i, j) representing the cells to compare + Returns: + tuple of (i, j, coupling_mat, gw_dist) where: + i, j: indices of the cells + coupling_mat: numpy array representing the coupling matrix + gw_dist: Gromov-Wasserstein distance between the two cells + """ + i, j = p + # load GW_OT_Cell objects if path specified + if isinstance(_CELL_OBJECTS[i], str): + _CELL_OBJECTS[i] = pickle.load(open(_CELL_OBJECTS[i], 'rb')) + if isinstance(_CELL_OBJECTS[j], str): + _CELL_OBJECTS[j] = pickle.load(open(_CELL_OBJECTS[j], 'rb')) + if _POINTS == 'boundary': + A = _CELL_OBJECTS[i].boundary_coord_dmat + B = _CELL_OBJECTS[j].boundary_coord_dmat + elif _POINTS == 'full': + A = _CELL_OBJECTS[i].coord_dmat + B = _CELL_OBJECTS[j].coord_dmat + n_A = A.shape[0] + n_B = B.shape[0] + a = np.repeat(1/n_A, n_A) + b = np.repeat(1/n_B, n_B) + a_dot_dist = A@a + b_dot_dist = B@b + a_cell_constant = ((A * A)@a)@a + b_cell_constant = ((B * B)@b)@b + + coupling_mat, gw_dist = gw_cython_core( + A, + a, + a_dot_dist, + a_cell_constant, + B, + b, + b_dot_dist, + b_cell_constant, + ) + + return (i, j, coupling_mat, gw_dist) + + +def gw_pairwise_parallel(cell_objects, points='boundary', num_processes=4, chunksize=20, n_approx_anchors=None, initial_anchor=0): + """ + Compute pairwise Gromov-Wasserstein distances between cells in parallel. + Args: + cell_objects: list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects + points: which points to use for the distance computation ('boundary' or 'full') + num_processes: number of parallel processes to use (default: 4) + chunksize: number of pairs to process in each chunk (default: 20) + n_approx_anchors: number of anchors to use for triangle inequality approximation of GW distances + initial_anchor: index of the first anchor cell (default: None, which means the first cell is used) + Returns: + gw_dmat: numpy array of shape (N, N) containing pairwise Gromov-Wasserstein distances + """ + N = len(cell_objects) + # Compute all pairwise GW distances + if n_approx_anchors is None: + index_pairs = it.combinations(iter(range(N)), 2) + total_num_pairs = int((N * (N - 1)) / 2) + with Pool( + initializer=_init_gw_pool, initargs=(cell_objects,points,), processes=num_processes + ) as pool: + res = pool.imap_unordered(_gw_index, index_pairs, chunksize=chunksize) + gw_dmat = np.zeros((N,N)) + for i, j, coupling_mat, gw_dist in tqdm(res, total=total_num_pairs, position=0, leave=True): + gw_dmat[i,j] = gw_dist + gw_dmat[j,i] = gw_dist + # Approximate GW distances using triangle inequality + else: + anchor_ind = initial_anchor + all_anchor_gw_dists = np.zeros((n_approx_anchors,N)) + for i_anchor in range(n_approx_anchors): + anchor_gw_dists = np.zeros(N) + index_pairs = it.product(iter(range(N)), [anchor_ind]) + total_num_pairs = N + with Pool( + initializer=_init_gw_pool, initargs=(cell_objects,points,), processes=num_processes + ) as pool: + res = pool.imap_unordered(_gw_index, index_pairs, chunksize=chunksize) + for i, j, coupling_mat, gw_dist in tqdm(res, total=total_num_pairs): + anchor_gw_dists[i] = gw_dist + all_anchor_gw_dists[i_anchor,:] = anchor_gw_dists + anchor_ind = np.argmax(all_anchor_gw_dists[:i_anchor+1,:].min(axis=0)) # next anchor + gw_dmat = np.zeros((N,N)) + for i,j in it.combinations(range(N), 2): + d = min(all_anchor_gw_dists[:,i] + all_anchor_gw_dists[:,j]) + gw_dmat[i,j] = d + gw_dmat[j,i] = d + return gw_dmat + + +def find_centroid(distance_matrix): + """ + Find the centroid of a set of points given a distance matrix. + """ + sum_distances = np.sum(distance_matrix, axis=1) + centroid_index = np.argmin(sum_distances) + return centroid_index + + +def _init_fgw_map_pool(cell_objects: list, channels: list, compartment_specific: bool, method, + fused_channel: str, fused_cost: float, fused_param: float, unbalanced_param: float): + global _CELL_OBJECTS # + _CELL_OBJECTS = cell_objects # list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects + global _CHANNELS + _CHANNELS = channels # which channels to compute protein OT for + global _COMPARTMENT_SPECIFIC + _COMPARTMENT_SPECIFIC = compartment_specific # whether to do compartment-specific mapping (nuclear/cytoplasm) + global _METHOD + _METHOD = method # method for morphology mapping: 'fused' or 'fused_unbalanced' + global _FUSED_CHANNEL + _FUSED_CHANNEL = fused_channel # channel to use for fused GW morphology mapping + global _FUSED_COST + _FUSED_COST = fused_cost # cost for fused GW morphology mapping + global _FUSED_PARAM + _FUSED_PARAM = fused_param # parameter for fused/unbalanced GW morphology mapping + global _UNBALANCED_PARAM + _UNBALANCED_PARAM = unbalanced_param # parameter for fused unbalanced GW mapping`` + + +# compute morphology fGW and map protein distribution from one cell to another +def _fgw_map_index(p: tuple[int, int]): + i, j = p + # load GW_OT_Cell objects if path specified + if isinstance(_CELL_OBJECTS[i], str): + _CELL_OBJECTS[i] = pickle.load(open(_CELL_OBJECTS[i], 'rb')) + if isinstance(_CELL_OBJECTS[j], str): + _CELL_OBJECTS[j] = pickle.load(open(_CELL_OBJECTS[j], 'rb')) + A = _CELL_OBJECTS[i].coord_dmat + B = _CELL_OBJECTS[j].coord_dmat + n_A = A.shape[0] + n_B = B.shape[0] + + # dictionary to store fGW morphology and OT protein distances + mapped_distbs = np.zeros((len(_CHANNELS),n_B)) + + if _COMPARTMENT_SPECIFIC: + # rescaling probabilities to allow fused GW to map nucleus to nucleus, cytoplasm to cytoplasm + n_pixel_nuc_i = _CELL_OBJECTS[i].nucleus.sum() + n_pixel_cyto_i = n_A - n_pixel_nuc_i + n_pixel_nuc_j = _CELL_OBJECTS[j].nucleus.sum() + n_pixel_cyto_j = n_B - n_pixel_nuc_j + + # rescale uniform distribution in cell i to have same nuclear/cytoplasm ration as cell j + a = np.zeros(n_A) + a[_CELL_OBJECTS[i].nucleus==1] = 0.5 / n_pixel_nuc_i + a[_CELL_OBJECTS[i].nucleus==0] = 0.5 / n_pixel_cyto_i + b = np.zeros(n_B) + b[_CELL_OBJECTS[j].nucleus==1] = 0.5 / n_pixel_nuc_j + b[_CELL_OBJECTS[j].nucleus==0] = 0.5 / n_pixel_cyto_j + else: + a = np.repeat(1/n_A, n_A) + b = np.repeat(1/n_B, n_B) + + # fGW morphology mapping + alpha = _FUSED_PARAM + cost = _FUSED_COST + rho = _UNBALANCED_PARAM + if _FUSED_CHANNEL == 'nucleus': + cost_matrix = cdist(_CELL_OBJECTS[i].nucleus[:,np.newaxis], _CELL_OBJECTS[j].nucleus[:,np.newaxis],) * cost + else: + cost_matrix = cdist(_CELL_OBJECTS[i].intensities[_FUSED_CHANNEL][:,np.newaxis], _CELL_OBJECTS[j].intensities[_FUSED_CHANNEL][:,np.newaxis],) * cost + if _METHOD == 'fused': + coupling_mat, log = ot.gromov.fused_gromov_wasserstein(M=cost_matrix, C1=A, C2=B, p=a, q=b, alpha=alpha, log=True) + gw_dist = log['fgw_dist'] + elif _METHOD == 'fused_unbalanced': + coupling_mat, coupling_mat_2, log = ot.gromov.fused_unbalanced_gromov_wasserstein(M=cost_matrix, Cx=A, Cy=B, wx=a, wy=b, alpha=alpha, + reg_marginals=rho, max_iter=20, log=True) + gw_dist = log['fugw_cost'] + + if _COMPARTMENT_SPECIFIC: + # find nuclear pixels after mapping + mapped_nucleus = _CELL_OBJECTS[i].nucleus.dot(coupling_mat * n_A) + mapped_nucleus_thresh = ( np.quantile(mapped_nucleus, 0.9) + np.quantile(mapped_nucleus, 0.1) ) / 2 + mapped_is_nucleus = mapped_nucleus > mapped_nucleus_thresh + n_pixel_nuc_mapped = mapped_is_nucleus.sum() + if _METHOD == 'fused_unbalanced': + mapped_cyto = np.repeat(1, n_A).dot(coupling_mat * n_A) + mapped_cyto[mapped_is_nucleus] = 0 + mapped_cyto_thresh = ( np.quantile(mapped_cyto, 0.7) + np.quantile(mapped_cyto, 0.1) ) / 2 + mapped_is_cyto = mapped_cyto > mapped_cyto_thresh + n_pixel_cyto_mapped = mapped_is_cyto.sum() + mapping_cyto = np.repeat(1, n_B).dot(coupling_mat.T * n_B) + mapping_cyto[_CELL_OBJECTS[i].nucleus==1] = 0 + mapping_cyto_thresh = ( np.quantile(mapping_cyto, 0.7) + np.quantile(mapping_cyto, 0.1) ) / 2 + mapping_is_cyto = mapping_cyto > mapping_cyto_thresh + n_pixel_cyto_mapping = mapped_is_cyto.sum() + else: + n_pixel_cyto_mapped = n_B - n_pixel_nuc_mapped + + # mapping cell A's distribution on cell B + for k in range(len(_CHANNELS)): + channel = _CHANNELS[k] + + if channel == 'nucleus': + a = _CELL_OBJECTS[i].nucleus.dot(coupling_mat * n_A) + else: + a = _CELL_OBJECTS[i].intensities[channel].dot(coupling_mat * n_A) + + if _COMPARTMENT_SPECIFIC: + # rescale based on protein distribution in nucleus/cytoplasm + if _METHOD == 'fused_unbalanced': + if a[mapped_is_nucleus].sum() != 0: + a[mapped_is_nucleus] = a[mapped_is_nucleus] / a[mapped_is_nucleus].sum() * a[_CELL_OBJECTS[j].nucleus==1].sum() + if a[~mapped_is_nucleus].sum() != 0: + a[~mapped_is_nucleus] = a[~mapped_is_nucleus] / n_pixel_cyto_mapped * n_pixel_cyto_mapping + else: + if a[mapped_is_nucleus].sum() != 0: + a[mapped_is_nucleus] = a[mapped_is_nucleus] / a[mapped_is_nucleus].sum() * _CELL_OBJECTS[i].intensities[channel][_CELL_OBJECTS[i].nucleus==1].sum() + if a[~mapped_is_nucleus].sum() != 0: + a[~mapped_is_nucleus] = a[~mapped_is_nucleus] / a[~mapped_is_nucleus].sum() * _CELL_OBJECTS[i].intensities[channel][_CELL_OBJECTS[i].nucleus==0].sum() + # then rescale based on number nuclear/cytoplasm pixels + a[mapped_is_nucleus] *= n_pixel_nuc_j/n_B*n_A/n_pixel_nuc_i + a[~mapped_is_nucleus] *= n_pixel_cyto_j/n_B*n_A/n_pixel_cyto_i + + # final normalization + a = a / a.sum() + + mapped_distbs[k,:] = a + + return (i, j, gw_dist, mapped_distbs) + + +def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_specific=True, method='fused', + fused_channel='protein', fused_cost=10, fused_param=0.1, unbalanced_param=70, parallel=True, + num_processes=4, chunksize=20): + """ + Map protein distributions from all cells to a target cell using fused Gromov-Wasserstein morphology mapping in parallel. + Args: + cell_objects: list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects + channels: list of channel names to map + target_cell_ind: index of the target cell to map to + compartment_specific: whether to do compartment-specific mapping (nuclear/cytoplasm) + method: method for morphology mapping ('fused' or 'fused_unbalanced') + fused_channel: channel to use for fused GW morphology mapping + fused_cost: cost for fused GW morphology mapping + fused_param: parameter for fused/unbalanced GW morphology mapping + unbalanced_param: parameter for fused unbalanced GW mapping + num_processes: number of parallel processes to use (default: 4) + chunksize: number of pairs to process in each chunk (default: 20) + Returns: + mapped_distbs: numpy array of shape (N, len(channels), n_target_pixels) containing mapped protein distributions + from each cell to the target cell + """ + print('Mapping cells to target cell:') + N = len(cell_objects) + index_pairs = [(i, target_cell_ind) for i in range(N)] + total_num_pairs = N - 1 + if parallel: + # Parallelized + with Pool( + initializer=_init_fgw_map_pool, initargs=(cell_objects, channels, compartment_specific, method, + fused_channel, fused_cost, fused_param, unbalanced_param), + processes=num_processes + ) as pool: + res = pool.imap_unordered(_fgw_map_index, index_pairs, chunksize=chunksize) + target_cell_object = cell_objects[target_cell_ind] + # load target GW_OT_Cell object if path specified + if isinstance(target_cell_object, str): + target_cell_object = pickle.load(open(target_cell_object, 'rb')) + mapped_distbs = np.zeros((len(channels),N,target_cell_object.coord_dmat.shape[0])) + for i, j, gw_dist, mapped_distb in tqdm(res, total=total_num_pairs, position=0, leave=True): + mapped_distbs[:,i,:] = mapped_distb + else: + # Non-parallelized + _init_fgw_map_pool(cell_objects, channels, compartment_specific, method, fused_channel, fused_cost, fused_param, unbalanced_param) + mapped_distbs = np.zeros((len(channels),N,cell_objects[target_cell_ind].coord_dmat.shape[0])) + for p in tqdm(index_pairs): + i, j, gw_dist, mapped_distb = _fgw_map_index(p) + mapped_distbs[:,i,:] = mapped_distb + return mapped_distbs + + +def _init_gw_mapped_ot_pool(cell_object: GW_OT_Cell, mapped_cell_dists: np.ndarray): + global _CELL_OBJECT + _CELL_OBJECT = cell_object # GW_OT_Cell object + global _MAPPED_CELL_DISTS + _MAPPED_CELL_DISTS = mapped_cell_dists # numpy array storing mapped cell protein distributions + + +def _gw_mapped_ot_index(p: tuple[int, int]): + global _CELL_OBJECT + i, j = p + if isinstance(_CELL_OBJECT, str): + _CELL_OBJECT = pickle.load(open(_CELL_OBJECT, 'rb')) + n_channels = _MAPPED_CELL_DISTS.shape[0] + ot_dists = np.zeros(n_channels) + + # protein OT + for channel_i in range(n_channels): + a = _MAPPED_CELL_DISTS[channel_i,i,:] + b = _MAPPED_CELL_DISTS[channel_i,j,:] + coupling_mat_prot, emd_dict = ot.emd(a, b, _CELL_OBJECT.coord_dmat, log=True) + gw_dist_prot = emd_dict['cost'] + ot_dists[channel_i] = gw_dist_prot + + return (i, j, ot_dists) + + +def gw_mapped_ot_pairwise_parallel(cell_object, mapped_cell_dists, num_processes=4, chunksize=20): + """ + Compute pairwise Gromov-Wasserstein distances between cells with mapped protein distributions in parallel. + Args: + cell_object: GW_OT_Cell object for target cell or path to GW_OT_Cell object for target cell + mapped_cell_dists: numpy array of shape (N, len(channels), n_target_pixels) containing mapped protein distributions + from each cell to the target cell + num_processes: number of parallel processes to use (default: 4) + chunksize: number of pairs to process in each chunk (default: 20) + Returns: + ot_dmats: numpy array of shape (len(channels), N, N) containing pairwise Gromov-Wasserstein distances + for each channel between cells with mapped protein distributions + """ + print('Computing pairwise OT distances:') + N = mapped_cell_dists.shape[1] + index_pairs = it.combinations(iter(range(N)), 2) # cell pairs to compute fGW / OT for + total_num_pairs = int((N * (N - 1)) / 2) # total number of cell pairs to compute (for progress bar) + with Pool( + initializer=_init_gw_mapped_ot_pool, initargs=(cell_object,mapped_cell_dists,), processes=num_processes + ) as pool: + res = pool.imap(_gw_mapped_ot_index, index_pairs, chunksize=chunksize) + # store OT distances in dictionary of matricies + ot_dmats = np.zeros((mapped_cell_dists.shape[0],N,N)) + for i, j, ot_dists in tqdm(res, total=total_num_pairs, position=0, leave=True): + ot_dmats[:,i,j] = ot_dists + ot_dmats[:,j,i] = ot_dists + return(ot_dmats) \ No newline at end of file From caf2e44b61ac08581c8ddf402fc8886cfab55c59 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Wed, 22 Oct 2025 18:53:31 +0000 Subject: [PATCH 02/14] Added code for deep GW-OT analysis, and normal GW-OT tutorial --- docs/notebooks/Example_6.ipynb | 4890 ++++++++++++++++++++++++++++++++ src/cajal/subcellular_dl.py | 1703 +++++++++++ 2 files changed, 6593 insertions(+) create mode 100644 docs/notebooks/Example_6.ipynb create mode 100644 src/cajal/subcellular_dl.py diff --git a/docs/notebooks/Example_6.ipynb b/docs/notebooks/Example_6.ipynb new file mode 100644 index 0000000..2762283 --- /dev/null +++ b/docs/notebooks/Example_6.ipynb @@ -0,0 +1,4890 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dca49492", + "metadata": {}, + "source": [ + "# Tutorial 6: Quantifying variation in subcellular protein localization (GW-OT)" + ] + }, + { + "cell_type": "markdown", + "id": "28d6ce35", + "metadata": {}, + "source": [ + "To demonstrate the functionality of GW-OT, we will perform an analysis on immunoflourescence data from the Human Protein Atlas. We will working with a small subset of X cells from X images, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/63tquyl5b6psiczrgihdn/hpa_images_metadata.zip?rlkey=7iz9cl5u35bvfupip6f0iicf3&st=ocpnazb7&dl=0)." + ] + }, + { + "cell_type": "markdown", + "id": "c27d59b5", + "metadata": {}, + "source": [ + "First, we will process the cell images, sample points from the cell boundary for morphological analysis and storing the subcellular protein information for localization analysis. We assume that cell segmentation has been performed on each image. Nuclear segmentation is optional, but can improve compartmental specificity in the localization analysis. \n", + "\n", + "The processed `GW_OT cell` objects can be kept in memory for faster analysis, or be written to files in cases where avaliable memory is insufficient. All functions that take `GW_OT cell` objects as input, can also take in the paths to the saved `GW_OT cell` objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be4cb3a2", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "from tqdm import tqdm\n", + "import skimage as ski\n", + "from cajal.subcellular import *" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1ce056f", + "metadata": {}, + "outputs": [], + "source": [ + "# change to path to where data is located\n", + "data_path = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/'\n", + "\n", + "# load image metadata\n", + "image_metadata = pd.read_csv(os.path.join(data_path, 'image_metadata.csv'), index_col=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e03a6861", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 60/60 [05:51<00:00, 5.86s/it]\n" + ] + } + ], + "source": [ + "# create list to store cell objects\n", + "cell_objects = []\n", + "cell_metadata = pd.DataFrame(columns=image_metadata.columns)\n", + "for i in tqdm(range(image_metadata.shape[0])):\n", + " im_path = os.path.join(data_path, 'images', image_metadata.iloc[i]['image_file'])\n", + " # load image\n", + " im = ski.io.imread(im_path)\n", + " channels = ['microtubules', 'protein', 'DNA'] # names of channels in image\n", + " # load cell and nuclear segmentation masks\n", + " im_cell_mask = ski.io.imread(im_path.replace('blue_red_green.jpg','predictedmask.png'))\n", + " im_nuc_mask = ski.io.imread(im_path.replace('blue_red_green.jpg','predictednucmask.png'))\n", + " # create cell objects from image\n", + " image_cell_objects = process_image(im, channels, im_cell_mask, im_nuc_mask, ds_target_size=1000)\n", + " cell_objects.extend(image_cell_objects)\n", + " # save metadata for each cell\n", + " n_image_cells = len(image_cell_objects)\n", + " cell_metadata = pd.concat([cell_metadata, image_metadata.iloc[i:i+1].reset_index(drop=True).loc[np.repeat(0, n_image_cells)]], ignore_index=True)" + ] + }, + { + "cell_type": "markdown", + "id": "363d4b4a", + "metadata": {}, + "source": [ + "To capture the morphological variation between cells, we compute the Gromov-Wasserstein distance between each pair of cells, using points sampled from each cell boundary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d451e68e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 69378/69378 [30:15<00:00, 38.22it/s] \n" + ] + } + ], + "source": [ + "# gw_dmat = gw_pairwise_parallel(cell_object_paths, num_processes=cpu_count(), chunksize=20)\n", + "gw_dmat = gw_pairwise_parallel(cell_objects, num_processes=cpu_count(), chunksize=20) " + ] + }, + { + "cell_type": "markdown", + "id": "7c526dd3", + "metadata": {}, + "source": [ + "We can the cluster the cells based on the computed Gromov-Wasserstein morphology space to identify groups of cells that display similar morphologies. We can also use UMAP to embed the morphology space into 2 dimensions for visualization." + ] + }, + { + "cell_type": "markdown", + "id": "28b35ff8", + "metadata": {}, + "source": [ + "Cells in the same cluster should have similar morphologies. For example, we can visualize some cells from cluster 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2603a863", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\n", + "\u001b[1;31mClick here for more info. \n", + "\n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], + "source": [ + "cells_to_plot = [122, 177, 188] # Example cells from cluster 1\n", + "fig, axes = plt.subplots(1, len(cells_to_plot), figsize=(15, 5))\n", + "for ax, i in zip(axes, cells_to_plot):\n", + " plot_cell_image(cell_objects[i], channels=['nucleus'], ax=ax)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "45b44dca", + "metadata": {}, + "source": [ + "Before we quantify the variation in subcellular localization patterns, we must first map the protein localization patterns of each cell to an anchor cell. We recommend choosing centroid cell in in the morphology space, the most morphologically 'average' cell, as the anchor.\n", + "\n", + "There are two approaches for mapping to the anchor cell: Fused Gromov-Wasserstein and Fused Unbalanced Gromov-Wasserstein. Fused Gromov-Wasserstein performs a full cell to cell mapping, which is appropriate for datasets with relatively simple cell morphologies. Fused Unbalanced Gromov-Wasserstein allows for partial cell to cell mappings. This is useful in datasets with more complex cell morphologies (i.e neurons) where certain cell structures may be present in one cell, but missing in others.\n", + "\n", + "The \"Fused\" variant of Gromov-Wasserstein enables the mappings between cells to consider additional staining or segmentation information. By default, we choose the segmented nucleus mask to inform to mapping to better align cellular structures, but other stains can be used as well. The `fused_cost` and `fused_param` parameters control how much this additional information is considered in the mapping. Higher values of `fused_cost` and lower values of `fused_param` give greater weight to this additional information. In practive we've found the cell mappings to be more sensitive to changes in the `fused_cost` value, as opposed to the `fused_param` value.\n", + "\n", + "Finally, we have an option to perform a 'compartment-specific' mapping. We define this as enforcing a strict mapping of the nuclear regions of one cell to the nuclear regions of the other cell, and the same for the non-nuclear regions. This is more important in the full (non-unbalanced) mapping case, where large differences in nucleus size can result in poor alignment of the cellular compartments after mapping." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59478b5f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapping cells to target cell:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "373it [19:59, 3.22s/it] \n" + ] + } + ], + "source": [ + "# We choose the morphological centroid cell as the anchor cell to map to\n", + "target_cell_ind = find_centroid(gw_dmat)\n", + "\n", + "channels_to_map = ['protein'] # which distributions to quantify variation in localization patterns for\n", + "# Mapping all cells to anchor cell\n", + "mapped_distbs = map_to_cell_parallel(cell_objects, \n", + " channels_to_map, \n", + " target_cell_ind, # cell to map to\n", + " method='fused', # 'fused' for full mapping, 'fused' for partial mapping\n", + " fused_channel='nucleus', # addition info to consider for mapping\n", + " fused_cost=1000, fused_param=0.1, # controls weight of additional info\n", + " compartment_specific=True, # enforces strict mapping of nucleus to nucleus\n", + " num_processes=cpu_count(), chunksize=1) # parallelization parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1efce506", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapping cells to target cell:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "10it [00:48, 4.82s/it] \n" + ] + } + ], + "source": [ + "mapped_distbs = map_to_cell_parallel(cell_objects[:10], \n", + " channels_to_map, \n", + " 0, # cell to map to\n", + " method='fused', # 'fused' for full mapping, 'fused' for partial mapping\n", + " fused_channel='nucleus', # addition info to consider for mapping\n", + " fused_cost=1000, fused_param=0.1, # controls weight of additional info\n", + " compartment_specific=True, # enforces strict mapping of nucleus to nucleus\n", + " num_processes=cpu_count(), chunksize=1) # parallelization parameters" + ] + }, + { + "cell_type": "markdown", + "id": "00434624", + "metadata": {}, + "source": [ + "We can visualize some examples of the mapped to localalization patterns to see whether the mapping parameters need adjustment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe14f406", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABcgAAAPmCAYAAADQQXwHAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAA2VBJREFUeJzs3Xl4VOX9//9XEsgkkSyEJYskgIBA2CxRIVUpAhpQESUuuKKlohaogFYbP25QNVStohbi8uELthpRFLRqhQIfCVqBSoSCUKlQKCgkKDYL2wST8/vDX0ZGEs59JjOZSeb5uK65ruTc77nP+ywz98w997lPhGVZlgAAAAAAAAAACDORwU4AAAAAAAAAAIBgoIMcAAAAAAAAABCW6CAHAAAAAAAAAIQlOsgBAAAAAAAAAGGJDnIAAAAAAAAAQFiigxwAAAAAAAAAEJboIAcAAAAAAAAAhCU6yAEAAAAAAAAAYYkOcgAAAAAAAABAWKKDHGhCDz30kCIiInx67oIFCxQREaFdu3b5N6nj7Nq1SxEREVqwYEHA1nEy9e2fLl266KabbgpKPgCA8ENbfXK01QCAUEB7fXK014AzdJADBrZs2aLrr79ep556qlwul9LT03Xddddpy5YtwU4tqMrKynTXXXepV69eiouL0ymnnKLs7Gw9/PDDKi8vb/J8/vznP2vgwIGKiYlRZmamHnzwQX333XdNngcAoOnRVtcvlNrqgwcPaurUqerUqZNcLpd69+6twsLCemOXL1+uc889V3FxcWrbtq2uuOKKgHZkAACaBu11/UKpvZ42bZoGDhyo5ORkxcXFqXfv3nrooYd08OBBr7iDBw/qwQcf1MiRI5WcnGz7g8A///lPjRw5Um3atFFycrJuuOEGff311wHeGsBMq2AnAIS6xYsX65prrlFycrImTJigrl27ateuXZo3b57eeOMNLVy4UJdffrlRXffdd59+85vf+JTHDTfcoHHjxsnlcvn0fH/75JNPdNFFF+ngwYO6/vrrlZ2dLUlav369Zs2apdWrV+uvf/1rk+Xz/vvv67LLLtPQoUP17LPPavPmzXr44Ye1f//+Br98AwBaBtrq+oVSW11TU6Pc3FytX79ekyZNUo8ePbRs2TL98pe/1H//+1/de++9nth3331XY8aM0cCBAzVr1ixVVlbq6aef1rnnnqsNGzaoQ4cOTZIzAMC/aK/rF0rtdV0+5513nm6++WbFxMRow4YNmjVrllasWKHVq1crMvL7sbbffPONZs6cqczMTA0YMECrVq1qsM4vv/xSQ4YMUWJioh599FEdPHhQTzzxhDZv3qy///3vio6ObqKtAxpgAWjQ9u3brbi4OKtXr17W/v37vcq+/vprq1evXtYpp5xi7dix46T1HDx4MJBp+s3OnTstSdb8+fNPGvff//7XOvXUU62UlBTrn//85wnlpaWl1m9/+1vH63/wwQetH78tde7c2Ro/frztc7OysqwBAwZYx44d8yz7n//5HysiIqLeHAEALQNtdf1Cra1+/fXXLUnWvHnzvJbn5eVZMTExVllZmWdZVlaW1b17d8vtdnuWbdy40YqMjLSmT5/uOGcAQPDRXtcv1NrrhjzxxBOWJGvNmjWeZUePHrX27dtnWZZlffLJJyfd3ttvv92KjY21/vOf/3iWLV++3JJkPf/88z7lBPgTU6wAJ/H444/r8OHDeuGFF04YrdS+fXs9//zzOnTokB577DHP8rq5vrZu3aprr71Wbdu21bnnnutVdrwjR47oV7/6ldq3b6/4+Hhdeuml+uqrrxQREaGHHnrIE1ffPGldunTRJZdcoo8++khnn322YmJidNppp+mPf/yj1zq+/fZb3XXXXerXr5/atGmjhIQEjRo1Sv/4xz982i/PP/+8vvrqKz355JPq1avXCeUpKSm67777vJa9//77Ou+883TKKacoPj5eF198sd8uo9u6dau2bt2qiRMnqlWrHy6M+eUvfynLsvTGG2/4ZT0AgNBDW12/UGurP/zwQ0nSuHHjvJaPGzdOR48e1dtvvy3p+/2wdetWXX755V6jyQYMGKDevXtr4cKFfskHANC0aK/rF2rtdUO6dOkiSV7TvbhcLqWmpho9/80339Qll1yizMxMz7IRI0bo9NNP1+uvv+7PVAGf0EEOnMQ777yjLl266Lzzzqu3fMiQIerSpYvee++9E8quvPJKHT58WI8++qhuueWWBtdx00036dlnn9VFF12k3/3ud4qNjdXFF19snOP27dt1xRVX6IILLtDvf/97tW3bVjfddJNXA/nvf/9bb731li655BI9+eST+vWvf63NmzfrZz/7mfbu3Wu8rjp//vOfFRsbqyuuuMIo/k9/+pMuvvhitWnTRr/73e90//33a+vWrTr33HP9Mp/ohg0bJElnnnmm1/L09HR16tTJUw4AaHloq+sXam212+1WVFTUCZdQx8XFSZJKSko8cZIUGxt7Qh1xcXHau3evSktLG50PAKBp0V7XL9Ta6zrfffedvvnmG+3du1d//etfdd999yk+Pl5nn32247q++uor7d+//4Tv65J09tln830dIYE5yIEGVFRUaO/evRozZsxJ4/r3768///nPqqqqUnx8vGf5gAEDVFRUdNLnfvrpp3r99dc1depUPfXUU5K+H/V88803G/8CvW3bNq1evdrzQeOqq65SRkaG5s+fryeeeEKS1K9fP/3rX//yzBUmfT/vWq9evTRv3jzdf//9Ruuq889//lOnn3660TxhBw8e1K9+9Sv94he/0AsvvOBZPn78ePXs2VOPPvqo13Jf7Nu3T5KUlpZ2QllaWppPH1QAAKGPtrphodZW9+zZUzU1NVq7dq1n9J/0w8jyr776StL3I+WSkpL0t7/9zev5Bw4c0NatWz2xpiPWAADBR3vdsFBrr+usX79eOTk5nv979uypP//5z0pOTnZcl9339W+//VZutztk5oRHeGIEOdCAqqoqSfJqmOtTV15ZWem1/LbbbrNdx9KlSyV933Afb8qUKcZ5ZmVlef0K36FDB/Xs2VP//ve/PctcLpenAa+pqdGBAwfUpk0b9ezZU59++qnxuupUVlba7pc6y5cvV3l5ua655hp98803nkdUVJQGDRqkDz74wPH6f+zIkSOSVG+DGhMT4ykHALQstNUNC7W2+tprr1ViYqJ+/vOfa/ny5dq1a5deeOEFzZ07V9IPbXlkZKRuvfVWrVy5Uvn5+friiy9UUlKiq666StXV1V6xAIDmgfa6YaHWXtfJysrS8uXL9dZbb+nuu+/WKaecooMHD/pUl9339eNjgGBhBDnQgLpGqq4xb0hDjX3Xrl1t1/Gf//xHkZGRJ8R2797dOM/j5/Cq07ZtW/33v//1/F9bW6unn35ac+fO1c6dO1VTU+Mpa9eunfG66iQkJNjulzpffPGFJGnYsGEN1tVYdZdh112WfbyjR4/We5k2AKD5o61uWKi11ampqfrzn/+sG264QRdeeKGn3meffVbjx49XmzZtPLEzZ87UN998o8cee0yzZs2SJF144YWaMGGCnnvuOa9YAEDoo71uWKi118fXNWLECEnSmDFjVFRU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9frrrys2NjaImSFYTnZOTJgwIYiZIZDGjRvn+btfv37q37+/unXrplWrVmn48OFBzMw/wvGzTkPbPHHiRM/f/fr1U1pamoYPH64dO3aoW7duTZ2mX4Tj566GtjkrK6tFHuNwRZsc2ppb29lS2sLm3r61lDarubdDLbGPIWSnWGnfvr2ioqJOuMNpWVmZUlNTg5RVy1O3L9nPvpk8ebLeffddffDBB+rUqZNneWpqqqqrq1VeXu4Vz341Ex0dre7duys7O1sFBQUaMGCAnn76afarj0pKSrR//34NHDhQrVq1UqtWrVRcXKxnnnlGrVq1UkpKCvvVT5KSknT66adr+/btnK+iLZe8z4lwwOeK75122mlq3759izju4fhZp6Ftrs+gQYMkqVkf63D83NXQNtenJRxjJ1py290S2uSW3M6GctvZUtrCltC+tZQ2q7m3Qy2xjyFkO8ijo6OVnZ2tlStXepbV1tZq5cqVXvPyoHG6du2q1NRUr/1cWVmpdevWsZ9PwrIsTZ48WUuWLNH//d//qWvXrl7l2dnZat26tdd+3bZtm3bv3s1+9UFtba3cbjf71UfDhw/X5s2btXHjRs/jzDPP1HXXXef5m/3qHwcPHtSOHTuUlpbG+Sracsn7nAgHfK743pdffqkDBw406+Mejp917La5Phs3bpSkZn2sfywcP3fVbXN9WuIxPpmW3Ha3hDa5Jbezodh2tpS2sCW3by2lzWpu7VCL7GMI6i1CbSxcuNByuVzWggULrK1bt1oTJ060kpKSrNLS0mCn1qxUVVVZGzZssDZs2GBJsp588klrw4YN1n/+8x/Lsixr1qxZVlJSkvX2229bmzZtssaMGWN17drVOnLkSJAzD1233367lZiYaK1atcrat2+f53H48GFPzG233WZlZmZa//d//2etX7/eysnJsXJycoKYdfPwm9/8xiouLrZ27txpbdq0yfrNb35jRUREWH/9618ty2K/+suP74zNfvXNnXfeaa1atcrauXOn9be//c0aMWKE1b59e2v//v2WZbFfLSv82nK7c6IlCMfPFSfb5qqqKuuuu+6y1qxZY+3cudNasWKFNXDgQKtHjx7W0aNHg526z8Lxs47dNm/fvt2aOXOmtX79emvnzp3W22+/bZ122mnWkCFDgpy578Lxc9fJtrklHmNftJS2u7m2yS2lnW0JbWdLaQtbSvvWUtqsltoONfc+hpDuILcsy3r22WetzMxMKzo62jr77LOttWvXBjulZueDDz6wJJ3wGD9+vGVZllVbW2vdf//9VkpKiuVyuazhw4db27ZtC27SIa6+/SnJmj9/vifmyJEj1i9/+Uurbdu2VlxcnHX55Zdb+/btC17SzcTPf/5zq3PnzlZ0dLTVoUMHa/jw4Z4Gz7LYr/7y48aL/eqbq6++2kpLS7Oio6OtU0891br66qut7du3e8rZr98Lp7bc7pxoCcLxc8XJtvnw4cPWhRdeaHXo0MFq3bq11blzZ+uWW25pdh1JPxaOn3Xstnn37t3WkCFDrOTkZMvlclndu3e3fv3rX1sVFRXBTbwRwvFz18m2uSUeY1+1hLa7ubbJLaWdbQltZ0tpC1tK+9ZS2qyW2g419z6GCMuyLP+MRQcAAAAAAAAAoPkI2TnIAQAAAAAAAAAIJDrIAQAAAAAAAABhiQ5yAAAAAAAAAEBYooMcAAAAAAAAABCW6CAHAAAAAAAAAIQlOsgBAAAAAAAAAGGJDnIAAAAAAAAAQFiigxwAAAAAAAAAEJboIAfgZejQoRo6dGhQ1r1r1y5FRERowYIFnmUPPfSQIiIigpIPAAChivYaAIDQRlsNNB90kAMNWLBggSIiIhQREaGPPvrohHLLspSRkaGIiAhdcsklQcgw+GpqajR//nwNHTpUycnJcrlc6tKli26++WatX7++yfP56quvdNVVVykpKUkJCQkaM2aM/v3vfzd5HgCApkN7bS/U2uuFCxdq4MCBiomJUYcOHTRhwgR98803J8SVlZXp5ptvVseOHRUbG6uBAwdq0aJFTZ4vAKBxaKvthVJbvWTJEuXm5io9PV0ul0udOnXSFVdcoc8+++yE2Ndee03XX3+9evTooYiIiJP+IOB2u3XPPfcoPT1dsbGxGjRokJYvXx7ALQHM0UEO2IiJiVFRUdEJy4uLi/Xll1/K5XIFIavgO3LkiC655BL9/Oc/l2VZuvfee1VYWKgbb7xRa9as0dlnn60vv/yyyfI5ePCgzj//fBUXF+vee+/VjBkztGHDBv3sZz/TgQMHmiwPAEBw0F7XL9Ta68LCQl1zzTVKTk7Wk08+qVtuuUULFy7U8OHDdfToUU9cZWWlzj33XL355pu69dZb9cQTTyg+Pl5XXXVVvccZABD6aKvrF2pt9ebNm9W2bVvdcccdmjt3rm6//XZt2LBBZ599tv7xj394xRYWFurtt99WRkaG2rZte9J6b7rpJj355JO67rrr9PTTTysqKkoXXXRRvT+aAE2tVbATAELdRRddpEWLFumZZ55Rq1Y/vGSKioqUnZ1d74incPDrX/9aS5cu1VNPPaWpU6d6lT344IN66qmnmjSfuXPn6osvvtDf//53nXXWWZKkUaNGqW/fvvr973+vRx99tEnzAQA0Ldrr+oVSe11dXa17771XQ4YM0fLlyz2Xef/0pz/V6NGj9eKLL2rKlCmSpOeff17bt2/XypUrNWzYMEnS7bffrsGDB+vOO+/UFVdcoejo6CbLHQDQeLTV9QultlqSHnjggROW/eIXv1CnTp1UWFio5557zrP8T3/6k0499VRFRkaqb9++Ddb597//XQsXLtTjjz+uu+66S5J04403qm/fvrr77rv18ccf+39DAAcYQQ7YuOaaa3TgwAGvS3+qq6v1xhtv6Nprr633OU888YR++tOfql27doqNjVV2drbeeOONE+IiIiI0efJkvfLKK+rZs6diYmKUnZ2t1atXe8XVzRX2+eef66qrrlJCQoLatWunO+64w2u0VZ2XX35Z2dnZio2NVXJyssaNG6c9e/acEPfCCy+oW7duio2N1dlnn60PP/zQaJ98+eWXev7553XBBRec0IBLUlRUlO666y516tTJs+yrr77Sz3/+c6WkpMjlcqlPnz76f//v/xmtz8Qbb7yhs846y9M5Lkm9evXS8OHD9frrr/ttPQCA0ER7faJQa68/++wzlZeX6+qrr/aaA/WSSy5RmzZttHDhQs+yDz/8UB06dPB0jktSZGSkrrrqKpWWlqq4uNgvOQEAmg5t9YlCra1uSMeOHRUXF6fy8nKv5RkZGYqMtO9afOONNxQVFaWJEyd6lsXExGjChAlas2ZNvfsUaEp0kAM2unTpopycHL366queZe+//74qKio0bty4ep/z9NNP6yc/+YlmzpypRx99VK1atdKVV16p995774TY4uJiTZ06Vddff71mzpypAwcOaOTIkfXO73XVVVfp6NGjKigo0EUXXaRnnnnGq4GRpEceeUQ33nijevTooSeffFJTp07VypUrNWTIEK/GbN68ebr11luVmpqqxx57TOecc44uvfRSo4bp/fff13fffacbbrjBNlb6fg7RwYMHa8WKFZo8ebKefvppde/eXRMmTNDs2bON6jiZ2tpabdq0SWeeeeYJZWeffbZ27NihqqqqRq8HABC6aK9PFGrttdvtliTFxsaeUBYbG6sNGzaotrbWE1tfXFxcnCSppKSk0fkAAJoWbfWJQq2tPl55ebm+/vprbd68Wb/4xS9UWVmp4cOH+1TXhg0bdPrppyshIcFr+dlnny1J2rhxY2PTBRrHAlCv+fPnW5KsTz75xPrDH/5gxcfHW4cPH7Ysy7KuvPJK6/zzz7csy7I6d+5sXXzxxV7PrYurU11dbfXt29caNmyY13JJliRr/fr1nmX/+c9/rJiYGOvyyy/3LHvwwQctSdall17q9fxf/vKXliTrH//4h2VZlrVr1y4rKirKeuSRR7ziNm/ebLVq1cqzvLq62urYsaN1xhlnWG632xP3wgsvWJKsn/3sZyfdN9OmTbMkWRs2bDhpXJ0JEyZYaWlp1jfffOO1fNy4cVZiYqJnf+3cudOSZM2fP/+EbT+Zr7/+2pJkzZw584SyOXPmWJKszz//3ChXAEDzQnvdsFBsryMiIqwJEyZ4Lf/88889+7hu3VOmTLEiIyOtXbt2nZCLJGvy5MlG2wQACD7a6oaFWlt9vJ49e3r2a5s2baz77rvPqqmpaTC+T58+DW5vnz59TjhmlmVZW7ZssSRZzz33nHFeQCAwghwwcNVVV+nIkSN69913VVVVpXfffbfBS8Ak75FR//3vf1VRUaHzzjtPn3766QmxOTk5ys7O9vyfmZmpMWPGaNmyZaqpqfGKnTRpktf/dfN0/uUvf5EkLV68WLW1tbrqqqv0zTffeB6pqanq0aOHPvjgA0nS+vXrtX//ft12221e83fedNNNSkxMtN0flZWVkqT4+HjbWMuy9Oabb2r06NGyLMsrr9zcXFVUVNS7X5w4cuSIJNV7U5eYmBivGABAy0V77S3U2uv27dvrqquu0ksvvaTf//73+ve//60PP/xQV199tVq3bi3ph/b6F7/4haKionTVVVfp448/1o4dO1RQUKAlS5Z4xQEAmhfaam+h1lYfb/78+Vq6dKnmzp2r3r1768iRIyfsR1NHjhzh+zpCGjfpBAx06NBBI0aMUFFRkQ4fPqyamhpdccUVDca/++67evjhh7Vx40bP5cSSvObbrNOjR48Tlp1++uk6fPiwvv76a6WmpjYY261bN0VGRmrXrl2SpC+++EKWZdVbpyTPl8///Oc/9dbXunVrnXbaaQ1uV526y6JMpi35+uuvVV5erhdeeEEvvPBCvTH79++3redk6j40Hb+v69TNI1ffZdoAgJaF9tpbqLXX0vc33zxy5Ijuuusuz026rr/+enXr1k2LFy9WmzZtJEn9+/dXUVGRbrvtNp1zzjmSpNTUVM2ePVu33367Jw4A0LzQVnsLxba6Tk5OjufvcePGqXfv3pK+nxfeqdjYWL6vI6TRQQ4Yuvbaa3XLLbeotLRUo0aNUlJSUr1xH374oS699FINGTJEc+fOVVpamlq3bq358+erqKjIrzn9+ENBbW2tIiIi9P777ysqKuqEeH99mezVq5ckafPmzTrjjDNOGls3l+j111+v8ePH1xvTv3//RuWTnJwsl8ulffv2nVBWtyw9Pb1R6wAANA+01z8ItfZakhITE/X2229r9+7d2rVrlzp37qzOnTvrpz/9qTp06OB1vK644gpdeuml+sc//qGamhoNHDhQq1atkvR9hwcAoHmirf5BKLbV9Wnbtq2GDRumV155xacO8rS0NH311VcnLOf7OkIFHeSAocsvv1y33nqr1q5dq9dee63BuDfffFMxMTFatmyZ1yVE8+fPrzf+iy++OGHZv/71L8XFxalDhw4nxHbt2tXz//bt21VbW6suXbpI+v5Xb8uy1LVr15N+cezcubOnvmHDhnmWHzt2TDt37tSAAQMafK4kjRo1SlFRUXr55ZdtbybSoUMHxcfHq6amRiNGjDhprK8iIyPVr18/rV+//oSydevW6bTTTjO6ZA0A0PzRXv8g1Nrr42VmZiozM1PS9zcBKykpUV5e3glx0dHROuusszz/r1ixQpKaJEcAQGDQVv8glNvqHzty5IgqKip8eu4ZZ5yhDz74QJWVlV436ly3bp2nHAgm5iAHDLVp00aFhYV66KGHNHr06AbjoqKiFBER4TU3165du/TWW2/VG79mzRqvecL27Nmjt99+WxdeeOEJv1TPmTPH6/9nn31W0veNqiSNHTtWUVFRmjFjhizL8oq1LEsHDhyQJJ155pnq0KGDnnvuOVVXV3tiFixY4HU37oZkZGTolltu0V//+ldPDserra3V73//e3355ZeKiopSXl6e3nzzzXrvHv7111/brs/EFVdcoU8++cSrk3zbtm36v//7P1155ZV+WQcAIPTRXv8gFNvr+uTn5+u7777TtGnTThr3xRdf6LnnntMll1zCCHIAaMZoq38Qim11fdO07Nq1SytXrtSZZ57pU51XXHGFampqvKaGcbvdmj9/vgYNGqSMjAyf8wX8gRHkgAMNXcZ0vIsvvlhPPvmkRo4cqWuvvVb79+/XnDlz1L17d23atOmE+L59+yo3N1e/+tWv5HK5NHfuXEnSjBkzTojduXOnLr30Uo0cOVJr1qzRyy+/rGuvvdbzq3S3bt308MMPKz8/X7t27dJll12m+Ph47dy5U0uWLNHEiRN11113qXXr1nr44Yd16623atiwYbr66qu1c+dOzZ8/32ieNEn6/e9/rx07duhXv/qVFi9erEsuuURt27bV7t27tWjRIn3++ecaN26cJGnWrFn64IMPNGjQIN1yyy3KysrSt99+q08//VQrVqzQt99+a7TOk/nlL3+pF198URdffLFnG5/8/9r792i76vJe/H82uWwISXYIl1xKoAgIBgjWKDFV0QoVqMfhBTu02l/R40+/eoJflXpq02GLejwNx55hrT2IPUcO6Lcira1o9VuhFiW0FShEOVyiKdBYYiFBqbkQyM5lz98f/RmNBPI8yZpZe+35eo2xxyA7b575mXOuNZ+5nr2y9kc/GnPmzInf/M3fPOD6AAwO/fonxlu/vuyyy+Kee+6JJUuWxOTJk+OLX/xi/M3f/E18+MMf3uOd4hERCxcujF/91V+N4447LtauXRtXXHFFzJ49Oz75yU8e8DoA6C+9+ifGW68+44wz4pxzzolnP/vZccQRR8R9990XV155ZezYsSMuu+yyPbI333xz3HzzzRHx7wP6rVu3xoc//OGIiDj77LPj7LPPjoiIJUuWxK/+6q/G8uXL45FHHomTTjopPv3pT8f3vve9uPLKKw94zXDAGmCvrrrqqiYimttvv/1pc8cff3zz8pe/fI/vXXnllc3JJ5/cDA8PN6eeempz1VVXNZdeemnzs0+5iGiWLVvW/Omf/unu/C/8wi803/jGN/bI/fj/Xb16dfPa1762mTFjRnPEEUc0F198cfPEE088aU1/+Zd/2bzwhS9sDj/88Obwww9vTj311GbZsmXNmjVr9sh94hOfaE444YRmeHi4ee5zn9vcfPPNzYtf/OLmxS9+ceoY7dy5s/nUpz7VvOhFL2pGRkaa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7979vUE7R/02UXr4oPfvCD1cDz+49PDxfX4i9PBBOEf9poePH3q4Hn4wTbQePtH6d4Qe3sZ5GvcD8j/7sz+LSy65JC699NL41re+FWeeeWacd9558cgjj/R7afvttNNOi4cffnj319///d/3e0lpW7dujTPPPDMuv/zyvf79Rz7ykfj4xz8en/zkJ+O2226Lww8/PM4777zYtm3bQV5pzr72JyLi/PPP3+N8fe5znzuIK6xZuXJlLFu2LG699db42te+Fjt27IiXvexlsXXr1t2Z97znPfHlL385Pv/5z8fKlSvjoYceite85jV9XPVTy+xPRMRb3/rWPc7RRz7ykT6teN+OPfbYuOyyy2LVqlVxxx13xEtf+tJ45StfGffee29EDNb5idj3/kQM1vn5abfffnv8yZ/8SSxatGiP7w/aOeqnidbDB7l/R+jhevjBpYeP7/MToYcPwjnqJz18fNHD9fCDaaL18InWvyP08FbOUzPOnXXWWc2yZct2/3nXrl3N/PnzmxUrVvRxVfvv0ksvbc4888x+L6MnIqK57rrrdv95bGysmTt3bvMHf/AHu7+3cePGZnh4uPnc5z7XhxXW/Oz+NE3TXHTRRc0rX/nKvqynFx555JEmIpqVK1c2TfPv52PKlCnN5z//+d2Z73znO01ENLfccku/lpn2s/vTNE3z4he/uHnXu97Vv0X1wBFHHNF86lOfGvjz82M/3p+mGdzzs2XLlubkk09uvva1r+2xDxPlHB0sE6mHT6T+3TR6+CDQwweDHj7+6OG9oYePX3r4+KeHj38TrX83jR5+oMb1O8i3b98eq1atinPPPXf39w455JA499xz45Zbbunjyg7MfffdF/Pnz49nPOMZ8cY3vjEefPDBfi+pJ9auXRvr16/f43yNjIzEkiVLBvp83XTTTXHMMcfEKaecEu94xzvi0Ucf7feS0jZt2hQREbNnz46IiFWrVsWOHTv2OEennnpqHHfccQNxjn52f37ss5/9bBx11FFx+umnx/Lly+Pxxx/vx/LKdu3aFddee21s3bo1li5dOvDn52f358cG8fwsW7YsXv7yl+9xLiIG/zl0ME3EHj5R+3eEHj4e6eHjmx4+funhB04PHyx6+Pijh49fE61/R+jhvTpPk3tSpSU//OEPY9euXTFnzpw9vj9nzpz47ne/26dVHZglS5bE1VdfHaeccko8/PDD8cEPfjBe9KIXxT333BMzZszo9/IOyPr16yMi9nq+fvx3g+b888+P17zmNXHCCSfEAw88EL/zO78TF1xwQdxyyy0xadKkfi/vaY2NjcW73/3ueMELXhCnn356RPz7OZo6dWrMmjVrj+wgnKO97U9ExBve8IY4/vjjY/78+XHXXXfF+973vlizZk184Qtf6ONqn97dd98dS5cujW3btsX06dPjuuuui4ULF8add945kOfnqfYnYjDPz7XXXhvf+ta34vbbb3/S3w3yc+hgm2g9fCL37wg9fLzRw8dvj9DDx/f50cN7Qw8fLHr4+KKHj88eMdH6d4QeHtHb8zSuB+QT0QUXXLD7vxctWhRLliyJ448/Pv78z/883vKWt/RxZezN61//+t3/fcYZZ8SiRYvixBNPjJtuuinOOeecPq5s35YtWxb33HPPwH2+3lN5qv1529vetvu/zzjjjJg3b16cc8458cADD8SJJ554sJeZcsopp8Sdd94ZmzZtir/4i7+Iiy66KFauXNnvZe23p9qfhQsXDtz5WbduXbzrXe+Kr33ta3HooYf2ezmMI/r34NHDxw89fPzSw+kCPXzw6OHjx0Tp4ROtf0fo4b02rj9i5aijjopJkyY96beSbtiwIebOndunVfXWrFmz4pnPfGbcf//9/V7KAfvxOZnI5+sZz3hGHHXUUeP+fF188cXxla98Jb7xjW/Escceu/v7c+fOje3bt8fGjRv3yI/3c/RU+7M3S5YsiYgY1+do6tSpcdJJJ8XixYtjxYoVceaZZ8Yf/dEfDez5ear92Zvxfn5WrVoVjzzySDznOc+JyZMnx+TJk2PlypXx8Y9/PCZPnhxz5swZyHPUDxO9h0+k/h2hh48nevj47RERenjE+D0/enjv6OGDRQ8fP/Tw8dsjJlr/jtDDI3p7nsb1gHzq1KmxePHiuPHGG3d/b2xsLG688cY9PldnkD322GPxwAMPxLx58/q9lAN2wgknxNy5c/c4X5s3b47bbrttwpyv73//+/Hoo4+O2/PVNE1cfPHFcd1118XXv/71OOGEE/b4+8WLF8eUKVP2OEdr1qyJBx98cFyeo33tz97ceeedERHj9hztzdjYWIyOjg7c+XkqP96fvRnv5+ecc86Ju+++O+68887dX8997nPjjW984+7/ngjn6GCY6D18IvXvCD18PNDDx3+P2Bs9fPzQw3tHDx8senj/6eHjv0f8rInWvyP08APWk1/12aJrr722GR4ebq6++upm9erVzdve9rZm1qxZzfr16/u9tP3ym7/5m81NN93UrF27tvmHf/iH5txzz22OOuqo5pFHHun30lK2bNnSfPvb326+/e1vNxHRfPSjH22+/e1vN//yL//SNE3TXHbZZc2sWbOaL33pS81dd93VvPKVr2xOOOGE5oknnujzyvfu6fZny5YtzXvf+97mlltuadauXdv87d/+bfOc5zynOfnkk5tt27b1e+l79Y53vKMZGRlpbrrppubhhx/e/fX444/vzrz97W9vjjvuuObrX/96c8cddzRLly5tli5d2sdVP7V97c/999/ffOhDH2ruuOOOZu3atc2XvvSl5hnPeEZz9tln93nlT+23f/u3m5UrVzZr165t7rrrrua3f/u3m6GhoeZv/uZvmqYZrPPTNE+/P4N4fvbmZ38D+KCdo36aSD180Pt30+jhevjBpYeP7/PTNHr4IJyjftLDxxc9XA8/mCZaD59o/btp9PA2ztO4H5A3TdP88R//cXPcccc1U6dObc4666zm1ltv7feS9tvrXve6Zt68ec3UqVObn/u5n2te97rXNffff3+/l5X2jW98o4mIJ31ddNFFTdM0zdjYWPO7v/u7zZw5c5rh4eHmnHPOadasWdPfRT+Np9ufxx9/vHnZy17WHH300c2UKVOa448/vnnrW986rm8K97YvEdFcddVVuzNPPPFE85/+039qjjjiiGbatGnNq1/96ubhhx/u36Kfxr7258EHH2zOPvvsZvbs2c3w8HBz0kknNf/5P//nZtOmTf1d+NP4j//xPzbHH398M3Xq1Oboo49uzjnnnN2NuWkG6/w0zdPvzyCen7352cY8aOeo3yZKDx/0/t00ergefnDp4eP7/DSNHj4I56jf9PDxQw/Xww+midbDJ1r/bho9vI3zNNQ0TbP/7z8HAAAAAIDBNK4/gxwAAAAAANpiQA4AAAAAQCcZkAMAAAAA0EkG5AAAAAAAdJIBOQAAAAAAnWRADgAAAABAJxmQAwAAAADQSQbkAAAAAAB0kgE5AAAAAACdZEAOAAAAAEAnGZADAAAAANBJBuQAAAAAAHTS/w8SWNqBUDCJdQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cells_to_plot = [0, 99, 310]\n", + "fig, axes = plt.subplots(2, len(cells_to_plot), figsize=(15, 10))\n", + "\n", + "for idx, i in enumerate(cells_to_plot):\n", + " # Plotting original cell\n", + " plot_cell_image(cell_objects[i], channels=['nucleus', 'protein'], ax=axes[0, idx])\n", + " axes[0, idx].set_title(f'Original Cell {i}')\n", + " \n", + " # Plotting mapped cell\n", + " mapped_cell_object = cell_objects[target_cell_ind].copy()\n", + " for j, channel in enumerate(channels_to_map):\n", + " mapped_cell_object.intensities[channel] = mapped_distbs[j][i]\n", + " plot_cell_image(mapped_cell_object, channels=['nucleus', 'protein'], ax=axes[1, idx])\n", + " axes[1, idx].set_title(f'Mapped Cell {i}')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f0a8a95", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapping cells to target cell:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "373it [19:59, 3.22s/it] \n" + ] + } + ], + "source": [ + "# We choose the morphological centroid cell as the anchor cell to map to\n", + "target_cell_ind = find_centroid(gw_dmat)\n", + "\n", + "channels_to_map = ['protein'] # which distributions to quantify variation in localization patterns for\n", + "# Mapping all cells to anchor cell\n", + "mapped_distbs = map_to_cell_parallel(cell_objects, \n", + " channels_to_map, \n", + " target_cell_ind, # cell to map to\n", + " method='fused', # 'fused' for full mapping, 'fused' for partial mapping\n", + " fused_channel='nucleus', # addition info to consider for mapping\n", + " fused_cost=1000, fused_param=0.1, # controls weight of additional info\n", + " compartment_specific=True, # enforces strict mapping of nucleus to nucleus\n", + " num_processes=cpu_count(), chunksize=1) # parallelization parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc3ab932", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapping cells to target cell:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "10it [00:48, 4.82s/it] \n" + ] + } + ], + "source": [ + "mapped_distbs = map_to_cell_parallel(cell_objects[:10], \n", + " channels_to_map, \n", + " 0, # cell to map to\n", + " method='fused', # 'fused' for full mapping, 'fused' for partial mapping\n", + " fused_channel='nucleus', # addition info to consider for mapping\n", + " fused_cost=1000, fused_param=0.1, # controls weight of additional info\n", + " compartment_specific=True, # enforces strict mapping of nucleus to nucleus\n", + " num_processes=cpu_count(), chunksize=1) # parallelization parameters" + ] + }, + { + "cell_type": "markdown", + "id": "56480efc", + "metadata": {}, + "source": [ + "We can visualize some examples of the mapped to localalization patterns to see whether the mapping parameters need adjustment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90dde1e3", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABcgAAAPmCAYAAADQQXwHAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAA2VBJREFUeJzs3Xl4VOX9//9XEsgkkSyEJYskgIBA2CxRIVUpAhpQESUuuKKlohaogFYbP25QNVStohbi8uELthpRFLRqhQIfCVqBSoSCUKlQKCgkKDYL2wST8/vDX0ZGEs59JjOZSeb5uK65ruTc77nP+ywz98w997lPhGVZlgAAAAAAAAAACDORwU4AAAAAAAAAAIBgoIMcAAAAAAAAABCW6CAHAAAAAAAAAIQlOsgBAAAAAAAAAGGJDnIAAAAAAAAAQFiigxwAAAAAAAAAEJboIAcAAAAAAAAAhCU6yAEAAAAAAAAAYYkOcgAAAAAAAABAWKKDHGhCDz30kCIiInx67oIFCxQREaFdu3b5N6nj7Nq1SxEREVqwYEHA1nEy9e2fLl266KabbgpKPgCA8ENbfXK01QCAUEB7fXK014AzdJADBrZs2aLrr79ep556qlwul9LT03Xddddpy5YtwU4tqMrKynTXXXepV69eiouL0ymnnKLs7Gw9/PDDKi8vb/J8/vznP2vgwIGKiYlRZmamHnzwQX333XdNngcAoOnRVtcvlNrqgwcPaurUqerUqZNcLpd69+6twsLCemOXL1+uc889V3FxcWrbtq2uuOKKgHZkAACaBu11/UKpvZ42bZoGDhyo5ORkxcXFqXfv3nrooYd08OBBr7iDBw/qwQcf1MiRI5WcnGz7g8A///lPjRw5Um3atFFycrJuuOEGff311wHeGsBMq2AnAIS6xYsX65prrlFycrImTJigrl27ateuXZo3b57eeOMNLVy4UJdffrlRXffdd59+85vf+JTHDTfcoHHjxsnlcvn0fH/75JNPdNFFF+ngwYO6/vrrlZ2dLUlav369Zs2apdWrV+uvf/1rk+Xz/vvv67LLLtPQoUP17LPPavPmzXr44Ye1f//+Br98AwBaBtrq+oVSW11TU6Pc3FytX79ekyZNUo8ePbRs2TL98pe/1H//+1/de++9nth3331XY8aM0cCBAzVr1ixVVlbq6aef1rnnnqsNGzaoQ4cOTZIzAMC/aK/rF0rtdV0+5513nm6++WbFxMRow4YNmjVrllasWKHVq1crMvL7sbbffPONZs6cqczMTA0YMECrVq1qsM4vv/xSQ4YMUWJioh599FEdPHhQTzzxhDZv3qy///3vio6ObqKtAxpgAWjQ9u3brbi4OKtXr17W/v37vcq+/vprq1evXtYpp5xi7dix46T1HDx4MJBp+s3OnTstSdb8+fNPGvff//7XOvXUU62UlBTrn//85wnlpaWl1m9/+1vH63/wwQetH78tde7c2Ro/frztc7OysqwBAwZYx44d8yz7n//5HysiIqLeHAEALQNtdf1Cra1+/fXXLUnWvHnzvJbn5eVZMTExVllZmWdZVlaW1b17d8vtdnuWbdy40YqMjLSmT5/uOGcAQPDRXtcv1NrrhjzxxBOWJGvNmjWeZUePHrX27dtnWZZlffLJJyfd3ttvv92KjY21/vOf/3iWLV++3JJkPf/88z7lBPgTU6wAJ/H444/r8OHDeuGFF04YrdS+fXs9//zzOnTokB577DHP8rq5vrZu3aprr71Wbdu21bnnnutVdrwjR47oV7/6ldq3b6/4+Hhdeuml+uqrrxQREaGHHnrIE1ffPGldunTRJZdcoo8++khnn322YmJidNppp+mPf/yj1zq+/fZb3XXXXerXr5/atGmjhIQEjRo1Sv/4xz982i/PP/+8vvrqKz355JPq1avXCeUpKSm67777vJa9//77Ou+883TKKacoPj5eF198sd8uo9u6dau2bt2qiRMnqlWrHy6M+eUvfynLsvTGG2/4ZT0AgNBDW12/UGurP/zwQ0nSuHHjvJaPGzdOR48e1dtvvy3p+/2wdetWXX755V6jyQYMGKDevXtr4cKFfskHANC0aK/rF2rtdUO6dOkiSV7TvbhcLqWmpho9/80339Qll1yizMxMz7IRI0bo9NNP1+uvv+7PVAGf0EEOnMQ777yjLl266Lzzzqu3fMiQIerSpYvee++9E8quvPJKHT58WI8++qhuueWWBtdx00036dlnn9VFF12k3/3ud4qNjdXFF19snOP27dt1xRVX6IILLtDvf/97tW3bVjfddJNXA/nvf/9bb731li655BI9+eST+vWvf63NmzfrZz/7mfbu3Wu8rjp//vOfFRsbqyuuuMIo/k9/+pMuvvhitWnTRr/73e90//33a+vWrTr33HP9Mp/ohg0bJElnnnmm1/L09HR16tTJUw4AaHloq+sXam212+1WVFTUCZdQx8XFSZJKSko8cZIUGxt7Qh1xcXHau3evSktLG50PAKBp0V7XL9Ta6zrfffedvvnmG+3du1d//etfdd999yk+Pl5nn32247q++uor7d+//4Tv65J09tln830dIYE5yIEGVFRUaO/evRozZsxJ4/r3768///nPqqqqUnx8vGf5gAEDVFRUdNLnfvrpp3r99dc1depUPfXUU5K+H/V88803G/8CvW3bNq1evdrzQeOqq65SRkaG5s+fryeeeEKS1K9fP/3rX//yzBUmfT/vWq9evTRv3jzdf//9Ruuq889//lOnn3660TxhBw8e1K9+9Sv94he/0AsvvOBZPn78ePXs2VOPPvqo13Jf7Nu3T5KUlpZ2QllaWppPH1QAAKGPtrphodZW9+zZUzU1NVq7dq1n9J/0w8jyr776StL3I+WSkpL0t7/9zev5Bw4c0NatWz2xpiPWAADBR3vdsFBrr+usX79eOTk5nv979uypP//5z0pOTnZcl9339W+//VZutztk5oRHeGIEOdCAqqoqSfJqmOtTV15ZWem1/LbbbrNdx9KlSyV933Afb8qUKcZ5ZmVlef0K36FDB/Xs2VP//ve/PctcLpenAa+pqdGBAwfUpk0b9ezZU59++qnxuupUVlba7pc6y5cvV3l5ua655hp98803nkdUVJQGDRqkDz74wPH6f+zIkSOSVG+DGhMT4ykHALQstNUNC7W2+tprr1ViYqJ+/vOfa/ny5dq1a5deeOEFzZ07V9IPbXlkZKRuvfVWrVy5Uvn5+friiy9UUlKiq666StXV1V6xAIDmgfa6YaHWXtfJysrS8uXL9dZbb+nuu+/WKaecooMHD/pUl9339eNjgGBhBDnQgLpGqq4xb0hDjX3Xrl1t1/Gf//xHkZGRJ8R2797dOM/j5/Cq07ZtW/33v//1/F9bW6unn35ac+fO1c6dO1VTU+Mpa9eunfG66iQkJNjulzpffPGFJGnYsGEN1tVYdZdh112WfbyjR4/We5k2AKD5o61uWKi11ampqfrzn/+sG264QRdeeKGn3meffVbjx49XmzZtPLEzZ87UN998o8cee0yzZs2SJF144YWaMGGCnnvuOa9YAEDoo71uWKi118fXNWLECEnSmDFjVFRU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9frrrys2NjaImSFYTnZOTJgwIYiZIZDGjRvn+btfv37q37+/unXrplWrVmn48OFBzMw/wvGzTkPbPHHiRM/f/fr1U1pamoYPH64dO3aoW7duTZ2mX4Tj566GtjkrK6tFHuNwRZsc2ppb29lS2sLm3r61lDarubdDLbGPIWSnWGnfvr2ioqJOuMNpWVmZUlNTg5RVy1O3L9nPvpk8ebLeffddffDBB+rUqZNneWpqqqqrq1VeXu4Vz341Ex0dre7duys7O1sFBQUaMGCAnn76afarj0pKSrR//34NHDhQrVq1UqtWrVRcXKxnnnlGrVq1UkpKCvvVT5KSknT66adr+/btnK+iLZe8z4lwwOeK75122mlq3759izju4fhZp6Ftrs+gQYMkqVkf63D83NXQNtenJRxjJ1py290S2uSW3M6GctvZUtrCltC+tZQ2q7m3Qy2xjyFkO8ijo6OVnZ2tlStXepbV1tZq5cqVXvPyoHG6du2q1NRUr/1cWVmpdevWsZ9PwrIsTZ48WUuWLNH//d//qWvXrl7l2dnZat26tdd+3bZtm3bv3s1+9UFtba3cbjf71UfDhw/X5s2btXHjRs/jzDPP1HXXXef5m/3qHwcPHtSOHTuUlpbG+Sracsn7nAgHfK743pdffqkDBw406+Mejp917La5Phs3bpSkZn2sfywcP3fVbXN9WuIxPpmW3Ha3hDa5Jbezodh2tpS2sCW3by2lzWpu7VCL7GMI6i1CbSxcuNByuVzWggULrK1bt1oTJ060kpKSrNLS0mCn1qxUVVVZGzZssDZs2GBJsp588klrw4YN1n/+8x/Lsixr1qxZVlJSkvX2229bmzZtssaMGWN17drVOnLkSJAzD1233367lZiYaK1atcrat2+f53H48GFPzG233WZlZmZa//d//2etX7/eysnJsXJycoKYdfPwm9/8xiouLrZ27txpbdq0yfrNb35jRUREWH/9618ty2K/+suP74zNfvXNnXfeaa1atcrauXOn9be//c0aMWKE1b59e2v//v2WZbFfLSv82nK7c6IlCMfPFSfb5qqqKuuuu+6y1qxZY+3cudNasWKFNXDgQKtHjx7W0aNHg526z8Lxs47dNm/fvt2aOXOmtX79emvnzp3W22+/bZ122mnWkCFDgpy578Lxc9fJtrklHmNftJS2u7m2yS2lnW0JbWdLaQtbSvvWUtqsltoONfc+hpDuILcsy3r22WetzMxMKzo62jr77LOttWvXBjulZueDDz6wJJ3wGD9+vGVZllVbW2vdf//9VkpKiuVyuazhw4db27ZtC27SIa6+/SnJmj9/vifmyJEj1i9/+Uurbdu2VlxcnHX55Zdb+/btC17SzcTPf/5zq3PnzlZ0dLTVoUMHa/jw4Z4Gz7LYr/7y48aL/eqbq6++2kpLS7Oio6OtU0891br66qut7du3e8rZr98Lp7bc7pxoCcLxc8XJtvnw4cPWhRdeaHXo0MFq3bq11blzZ+uWW25pdh1JPxaOn3Xstnn37t3WkCFDrOTkZMvlclndu3e3fv3rX1sVFRXBTbwRwvFz18m2uSUeY1+1hLa7ubbJLaWdbQltZ0tpC1tK+9ZS2qyW2g419z6GCMuyLP+MRQcAAAAAAAAAoPkI2TnIAQAAAAAAAAAIJDrIAQAAAAAAAABhiQ5yAAAAAAAAAEBYooMcAAAAAAAAABCW6CAHAAAAAAAAAIQlOsgBAAAAAAAAAGGJDnIAAAAAAAAAQFiigxwAAAAAAAAAEJboIAfgZejQoRo6dGhQ1r1r1y5FRERowYIFnmUPPfSQIiIigpIPAAChivYaAIDQRlsNNB90kAMNWLBggSIiIhQREaGPPvrohHLLspSRkaGIiAhdcsklQcgw+GpqajR//nwNHTpUycnJcrlc6tKli26++WatX7++yfP56quvdNVVVykpKUkJCQkaM2aM/v3vfzd5HgCApkN7bS/U2uuFCxdq4MCBiomJUYcOHTRhwgR98803J8SVlZXp5ptvVseOHRUbG6uBAwdq0aJFTZ4vAKBxaKvthVJbvWTJEuXm5io9PV0ul0udOnXSFVdcoc8+++yE2Ndee03XX3+9evTooYiIiJP+IOB2u3XPPfcoPT1dsbGxGjRokJYvXx7ALQHM0UEO2IiJiVFRUdEJy4uLi/Xll1/K5XIFIavgO3LkiC655BL9/Oc/l2VZuvfee1VYWKgbb7xRa9as0dlnn60vv/yyyfI5ePCgzj//fBUXF+vee+/VjBkztGHDBv3sZz/TgQMHmiwPAEBw0F7XL9Ta68LCQl1zzTVKTk7Wk08+qVtuuUULFy7U8OHDdfToUU9cZWWlzj33XL355pu69dZb9cQTTyg+Pl5XXXVVvccZABD6aKvrF2pt9ebNm9W2bVvdcccdmjt3rm6//XZt2LBBZ599tv7xj394xRYWFurtt99WRkaG2rZte9J6b7rpJj355JO67rrr9PTTTysqKkoXXXRRvT+aAE2tVbATAELdRRddpEWLFumZZ55Rq1Y/vGSKioqUnZ1d74incPDrX/9aS5cu1VNPPaWpU6d6lT344IN66qmnmjSfuXPn6osvvtDf//53nXXWWZKkUaNGqW/fvvr973+vRx99tEnzAQA0Ldrr+oVSe11dXa17771XQ4YM0fLlyz2Xef/0pz/V6NGj9eKLL2rKlCmSpOeff17bt2/XypUrNWzYMEnS7bffrsGDB+vOO+/UFVdcoejo6CbLHQDQeLTV9QultlqSHnjggROW/eIXv1CnTp1UWFio5557zrP8T3/6k0499VRFRkaqb9++Ddb597//XQsXLtTjjz+uu+66S5J04403qm/fvrr77rv18ccf+39DAAcYQQ7YuOaaa3TgwAGvS3+qq6v1xhtv6Nprr633OU888YR++tOfql27doqNjVV2drbeeOONE+IiIiI0efJkvfLKK+rZs6diYmKUnZ2t1atXe8XVzRX2+eef66qrrlJCQoLatWunO+64w2u0VZ2XX35Z2dnZio2NVXJyssaNG6c9e/acEPfCCy+oW7duio2N1dlnn60PP/zQaJ98+eWXev7553XBBRec0IBLUlRUlO666y516tTJs+yrr77Sz3/+c6WkpMjlcqlPnz76f//v/xmtz8Qbb7yhs846y9M5Lkm9evXS8OHD9frrr/ttPQCA0ER7faJQa68/++wzlZeX6+qrr/aaA/WSSy5RmzZttHDhQs+yDz/8UB06dPB0jktSZGSkrrrqKpWWlqq4uNgvOQEAmg5t9YlCra1uSMeOHRUXF6fy8nKv5RkZGYqMtO9afOONNxQVFaWJEyd6lsXExGjChAlas2ZNvfsUaEp0kAM2unTpopycHL366queZe+//74qKio0bty4ep/z9NNP6yc/+YlmzpypRx99VK1atdKVV16p995774TY4uJiTZ06Vddff71mzpypAwcOaOTIkfXO73XVVVfp6NGjKigo0EUXXaRnnnnGq4GRpEceeUQ33nijevTooSeffFJTp07VypUrNWTIEK/GbN68ebr11luVmpqqxx57TOecc44uvfRSo4bp/fff13fffacbbrjBNlb6fg7RwYMHa8WKFZo8ebKefvppde/eXRMmTNDs2bON6jiZ2tpabdq0SWeeeeYJZWeffbZ27NihqqqqRq8HABC6aK9PFGrttdvtliTFxsaeUBYbG6sNGzaotrbWE1tfXFxcnCSppKSk0fkAAJoWbfWJQq2tPl55ebm+/vprbd68Wb/4xS9UWVmp4cOH+1TXhg0bdPrppyshIcFr+dlnny1J2rhxY2PTBRrHAlCv+fPnW5KsTz75xPrDH/5gxcfHW4cPH7Ysy7KuvPJK6/zzz7csy7I6d+5sXXzxxV7PrYurU11dbfXt29caNmyY13JJliRr/fr1nmX/+c9/rJiYGOvyyy/3LHvwwQctSdall17q9fxf/vKXliTrH//4h2VZlrVr1y4rKirKeuSRR7ziNm/ebLVq1cqzvLq62urYsaN1xhlnWG632xP3wgsvWJKsn/3sZyfdN9OmTbMkWRs2bDhpXJ0JEyZYaWlp1jfffOO1fNy4cVZiYqJnf+3cudOSZM2fP/+EbT+Zr7/+2pJkzZw584SyOXPmWJKszz//3ChXAEDzQnvdsFBsryMiIqwJEyZ4Lf/88889+7hu3VOmTLEiIyOtXbt2nZCLJGvy5MlG2wQACD7a6oaFWlt9vJ49e3r2a5s2baz77rvPqqmpaTC+T58+DW5vnz59TjhmlmVZW7ZssSRZzz33nHFeQCAwghwwcNVVV+nIkSN69913VVVVpXfffbfBS8Ak75FR//3vf1VRUaHzzjtPn3766QmxOTk5ys7O9vyfmZmpMWPGaNmyZaqpqfGKnTRpktf/dfN0/uUvf5EkLV68WLW1tbrqqqv0zTffeB6pqanq0aOHPvjgA0nS+vXrtX//ft12221e83fedNNNSkxMtN0flZWVkqT4+HjbWMuy9Oabb2r06NGyLMsrr9zcXFVUVNS7X5w4cuSIJNV7U5eYmBivGABAy0V77S3U2uv27dvrqquu0ksvvaTf//73+ve//60PP/xQV199tVq3bi3ph/b6F7/4haKionTVVVfp448/1o4dO1RQUKAlS5Z4xQEAmhfaam+h1lYfb/78+Vq6dKnmzp2r3r1768iRIyfsR1NHjhzh+zpCGjfpBAx06NBBI0aMUFFRkQ4fPqyamhpdccUVDca/++67evjhh7Vx40bP5cSSvObbrNOjR48Tlp1++uk6fPiwvv76a6WmpjYY261bN0VGRmrXrl2SpC+++EKWZdVbpyTPl8///Oc/9dbXunVrnXbaaQ1uV526y6JMpi35+uuvVV5erhdeeEEvvPBCvTH79++3redk6j40Hb+v69TNI1ffZdoAgJaF9tpbqLXX0vc33zxy5Ijuuusuz026rr/+enXr1k2LFy9WmzZtJEn9+/dXUVGRbrvtNp1zzjmSpNTUVM2ePVu33367Jw4A0LzQVnsLxba6Tk5OjufvcePGqXfv3pK+nxfeqdjYWL6vI6TRQQ4Yuvbaa3XLLbeotLRUo0aNUlJSUr1xH374oS699FINGTJEc+fOVVpamlq3bq358+erqKjIrzn9+ENBbW2tIiIi9P777ysqKuqEeH99mezVq5ckafPmzTrjjDNOGls3l+j111+v8ePH1xvTv3//RuWTnJwsl8ulffv2nVBWtyw9Pb1R6wAANA+01z8ItfZakhITE/X2229r9+7d2rVrlzp37qzOnTvrpz/9qTp06OB1vK644gpdeuml+sc//qGamhoNHDhQq1atkvR9hwcAoHmirf5BKLbV9Wnbtq2GDRumV155xacO8rS0NH311VcnLOf7OkIFHeSAocsvv1y33nqr1q5dq9dee63BuDfffFMxMTFatmyZ1yVE8+fPrzf+iy++OGHZv/71L8XFxalDhw4nxHbt2tXz//bt21VbW6suXbpI+v5Xb8uy1LVr15N+cezcubOnvmHDhnmWHzt2TDt37tSAAQMafK4kjRo1SlFRUXr55ZdtbybSoUMHxcfHq6amRiNGjDhprK8iIyPVr18/rV+//oSydevW6bTTTjO6ZA0A0PzRXv8g1Nrr42VmZiozM1PS9zcBKykpUV5e3glx0dHROuusszz/r1ixQpKaJEcAQGDQVv8glNvqHzty5IgqKip8eu4ZZ5yhDz74QJWVlV436ly3bp2nHAgm5iAHDLVp00aFhYV66KGHNHr06AbjoqKiFBER4TU3165du/TWW2/VG79mzRqvecL27Nmjt99+WxdeeOEJv1TPmTPH6/9nn31W0veNqiSNHTtWUVFRmjFjhizL8oq1LEsHDhyQJJ155pnq0KGDnnvuOVVXV3tiFixY4HU37oZkZGTolltu0V//+ldPDserra3V73//e3355ZeKiopSXl6e3nzzzXrvHv7111/brs/EFVdcoU8++cSrk3zbtm36v//7P1155ZV+WQcAIPTRXv8gFNvr+uTn5+u7777TtGnTThr3xRdf6LnnntMll1zCCHIAaMZoq38Qim11fdO07Nq1SytXrtSZZ57pU51XXHGFampqvKaGcbvdmj9/vgYNGqSMjAyf8wX8gRHkgAMNXcZ0vIsvvlhPPvmkRo4cqWuvvVb79+/XnDlz1L17d23atOmE+L59+yo3N1e/+tWv5HK5NHfuXEnSjBkzTojduXOnLr30Uo0cOVJr1qzRyy+/rGuvvdbzq3S3bt308MMPKz8/X7t27dJll12m+Ph47dy5U0uWLNHEiRN11113qXXr1nr44Yd16623atiwYbr66qu1c+dOzZ8/32ieNEn6/e9/rx07duhXv/qVFi9erEsuuURt27bV7t27tWjRIn3++ecaN26cJGnWrFn64IMPNGjQIN1yyy3KysrSt99+q08//VQrVqzQt99+a7TOk/nlL3+pF198URdffLFnG5/8/9r792i76vJe/H82uWwISXYIl1xKoAgIBgjWKDFV0QoVqMfhBTu02l/R40+/eoJflXpq02GLejwNx55hrT2IPUcO6Lcira1o9VuhFiW0FShEOVyiKdBYYiFBqbkQyM5lz98f/RmNBPI8yZpZe+35eo2xxyA7b575mXOuNZ+5nr2y9kc/GnPmzInf/M3fPOD6AAwO/fonxlu/vuyyy+Kee+6JJUuWxOTJk+OLX/xi/M3f/E18+MMf3uOd4hERCxcujF/91V+N4447LtauXRtXXHFFzJ49Oz75yU8e8DoA6C+9+ifGW68+44wz4pxzzolnP/vZccQRR8R9990XV155ZezYsSMuu+yyPbI333xz3HzzzRHx7wP6rVu3xoc//OGIiDj77LPj7LPPjoiIJUuWxK/+6q/G8uXL45FHHomTTjopPv3pT8f3vve9uPLKKw94zXDAGmCvrrrqqiYimttvv/1pc8cff3zz8pe/fI/vXXnllc3JJ5/cDA8PN6eeempz1VVXNZdeemnzs0+5iGiWLVvW/Omf/unu/C/8wi803/jGN/bI/fj/Xb16dfPa1762mTFjRnPEEUc0F198cfPEE088aU1/+Zd/2bzwhS9sDj/88Obwww9vTj311GbZsmXNmjVr9sh94hOfaE444YRmeHi4ee5zn9vcfPPNzYtf/OLmxS9+ceoY7dy5s/nUpz7VvOhFL2pGRkaa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7979vUE7R/02UXr4oPfvCD1cDz+49PDxfX4i9PBBOEf9poePH3q4Hn4wTbQePtH6d4Qe3sZ5GvcD8j/7sz+LSy65JC699NL41re+FWeeeWacd9558cgjj/R7afvttNNOi4cffnj319///d/3e0lpW7dujTPPPDMuv/zyvf79Rz7ykfj4xz8en/zkJ+O2226Lww8/PM4777zYtm3bQV5pzr72JyLi/PPP3+N8fe5znzuIK6xZuXJlLFu2LG699db42te+Fjt27IiXvexlsXXr1t2Z97znPfHlL385Pv/5z8fKlSvjoYceite85jV9XPVTy+xPRMRb3/rWPc7RRz7ykT6teN+OPfbYuOyyy2LVqlVxxx13xEtf+tJ45StfGffee29EDNb5idj3/kQM1vn5abfffnv8yZ/8SSxatGiP7w/aOeqnidbDB7l/R+jhevjBpYeP7/MToYcPwjnqJz18fNHD9fCDaaL18InWvyP08FbOUzPOnXXWWc2yZct2/3nXrl3N/PnzmxUrVvRxVfvv0ksvbc4888x+L6MnIqK57rrrdv95bGysmTt3bvMHf/AHu7+3cePGZnh4uPnc5z7XhxXW/Oz+NE3TXHTRRc0rX/nKvqynFx555JEmIpqVK1c2TfPv52PKlCnN5z//+d2Z73znO01ENLfccku/lpn2s/vTNE3z4he/uHnXu97Vv0X1wBFHHNF86lOfGvjz82M/3p+mGdzzs2XLlubkk09uvva1r+2xDxPlHB0sE6mHT6T+3TR6+CDQwweDHj7+6OG9oYePX3r4+KeHj38TrX83jR5+oMb1O8i3b98eq1atinPPPXf39w455JA499xz45Zbbunjyg7MfffdF/Pnz49nPOMZ8cY3vjEefPDBfi+pJ9auXRvr16/f43yNjIzEkiVLBvp83XTTTXHMMcfEKaecEu94xzvi0Ucf7feS0jZt2hQREbNnz46IiFWrVsWOHTv2OEennnpqHHfccQNxjn52f37ss5/9bBx11FFx+umnx/Lly+Pxxx/vx/LKdu3aFddee21s3bo1li5dOvDn52f358cG8fwsW7YsXv7yl+9xLiIG/zl0ME3EHj5R+3eEHj4e6eHjmx4+funhB04PHyx6+Pijh49fE61/R+jhvTpPk3tSpSU//OEPY9euXTFnzpw9vj9nzpz47ne/26dVHZglS5bE1VdfHaeccko8/PDD8cEPfjBe9KIXxT333BMzZszo9/IOyPr16yMi9nq+fvx3g+b888+P17zmNXHCCSfEAw88EL/zO78TF1xwQdxyyy0xadKkfi/vaY2NjcW73/3ueMELXhCnn356RPz7OZo6dWrMmjVrj+wgnKO97U9ExBve8IY4/vjjY/78+XHXXXfF+973vlizZk184Qtf6ONqn97dd98dS5cujW3btsX06dPjuuuui4ULF8add945kOfnqfYnYjDPz7XXXhvf+ta34vbbb3/S3w3yc+hgm2g9fCL37wg9fLzRw8dvj9DDx/f50cN7Qw8fLHr4+KKHj88eMdH6d4QeHtHb8zSuB+QT0QUXXLD7vxctWhRLliyJ448/Pv78z/883vKWt/RxZezN61//+t3/fcYZZ8SiRYvixBNPjJtuuinOOeecPq5s35YtWxb33HPPwH2+3lN5qv1529vetvu/zzjjjJg3b16cc8458cADD8SJJ554sJeZcsopp8Sdd94ZmzZtir/4i7+Iiy66KFauXNnvZe23p9qfhQsXDtz5WbduXbzrXe+Kr33ta3HooYf2ezmMI/r34NHDxw89fPzSw+kCPXzw6OHjx0Tp4ROtf0fo4b02rj9i5aijjopJkyY96beSbtiwIebOndunVfXWrFmz4pnPfGbcf//9/V7KAfvxOZnI5+sZz3hGHHXUUeP+fF188cXxla98Jb7xjW/Escceu/v7c+fOje3bt8fGjRv3yI/3c/RU+7M3S5YsiYgY1+do6tSpcdJJJ8XixYtjxYoVceaZZ8Yf/dEfDez5ear92Zvxfn5WrVoVjzzySDznOc+JyZMnx+TJk2PlypXx8Y9/PCZPnhxz5swZyHPUDxO9h0+k/h2hh48nevj47RERenjE+D0/enjv6OGDRQ8fP/Tw8dsjJlr/jtDDI3p7nsb1gHzq1KmxePHiuPHGG3d/b2xsLG688cY9PldnkD322GPxwAMPxLx58/q9lAN2wgknxNy5c/c4X5s3b47bbrttwpyv73//+/Hoo4+O2/PVNE1cfPHFcd1118XXv/71OOGEE/b4+8WLF8eUKVP2OEdr1qyJBx98cFyeo33tz97ceeedERHj9hztzdjYWIyOjg7c+XkqP96fvRnv5+ecc86Ju+++O+68887dX8997nPjjW984+7/ngjn6GCY6D18IvXvCD18PNDDx3+P2Bs9fPzQw3tHDx8senj/6eHjv0f8rInWvyP08APWk1/12aJrr722GR4ebq6++upm9erVzdve9rZm1qxZzfr16/u9tP3ym7/5m81NN93UrF27tvmHf/iH5txzz22OOuqo5pFHHun30lK2bNnSfPvb326+/e1vNxHRfPSjH22+/e1vN//yL//SNE3TXHbZZc2sWbOaL33pS81dd93VvPKVr2xOOOGE5oknnujzyvfu6fZny5YtzXvf+97mlltuadauXdv87d/+bfOc5zynOfnkk5tt27b1e+l79Y53vKMZGRlpbrrppubhhx/e/fX444/vzrz97W9vjjvuuObrX/96c8cddzRLly5tli5d2sdVP7V97c/999/ffOhDH2ruuOOOZu3atc2XvvSl5hnPeEZz9tln93nlT+23f/u3m5UrVzZr165t7rrrrua3f/u3m6GhoeZv/uZvmqYZrPPTNE+/P4N4fvbmZ38D+KCdo36aSD180Pt30+jhevjBpYeP7/PTNHr4IJyjftLDxxc9XA8/mCZaD59o/btp9PA2ztO4H5A3TdP88R//cXPcccc1U6dObc4666zm1ltv7feS9tvrXve6Zt68ec3UqVObn/u5n2te97rXNffff3+/l5X2jW98o4mIJ31ddNFFTdM0zdjYWPO7v/u7zZw5c5rh4eHmnHPOadasWdPfRT+Np9ufxx9/vHnZy17WHH300c2UKVOa448/vnnrW986rm8K97YvEdFcddVVuzNPPPFE85/+039qjjjiiGbatGnNq1/96ubhhx/u36Kfxr7258EHH2zOPvvsZvbs2c3w8HBz0kknNf/5P//nZtOmTf1d+NP4j//xPzbHH398M3Xq1Oboo49uzjnnnN2NuWkG6/w0zdPvzyCen7352cY8aOeo3yZKDx/0/t00ergefnDp4eP7/DSNHj4I56jf9PDxQw/Xww+midbDJ1r/bho9vI3zNNQ0TbP/7z8HAAAAAIDBNK4/gxwAAAAAANpiQA4AAAAAQCcZkAMAAAAA0EkG5AAAAAAAdJIBOQAAAAAAnWRADgAAAABAJxmQAwAAAADQSQbkAAAAAAB0kgE5AAAAAACdZEAOAAAAAEAnGZADAAAAANBJBuQAAAAAAHTS/w8SWNqBUDCJdQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cells_to_plot = [0, 99, 310]\n", + "fig, axes = plt.subplots(2, len(cells_to_plot), figsize=(15, 10))\n", + "\n", + "for idx, i in enumerate(cells_to_plot):\n", + " # Plotting original cell\n", + " plot_cell_image(cell_objects[i], channels=['nucleus', 'protein'], ax=axes[0, idx])\n", + " axes[0, idx].set_title(f'Original Cell {i}')\n", + " \n", + " # Plotting mapped cell\n", + " mapped_cell_object = cell_objects[target_cell_ind].copy()\n", + " for j, channel in enumerate(channels_to_map):\n", + " mapped_cell_object.intensities[channel] = mapped_distbs[j][i]\n", + " plot_cell_image(mapped_cell_object, channels=['nucleus', 'protein'], ax=axes[1, idx])\n", + " axes[1, idx].set_title(f'Mapped Cell {i}')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a459d477", + "metadata": {}, + "source": [ + "After mapping all cells to the anchor cell, we compute the optimal transport (Wasserstein) distance to measure the difference in protein localization patterns between cells. Similar to the Gromov-Wasserstein morphology space, we can cluster cells based on the optimal transport localization space to identify groups of cells with similar protein localization patterns, and visualize the space with UMAP." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4917bb17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing pairwise OT distances:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 69378/69378 [20:54<00:00, 55.29it/s] \n" + ] + } + ], + "source": [ + "ot_dmats = gw_mapped_ot_pairwise_parallel(cell_objects[target_cell_ind], mapped_distbs, num_processes=cpu_count(), chunksize=20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "696ee320", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/umap/umap_.py:1780: UserWarning:\n", + "\n", + "using precomputed metric; inverse_transform will be unavailable\n", + "\n", + "/opt/conda/lib/python3.10/site-packages/plotly/express/_core.py:1992: FutureWarning:\n", + "\n", + "When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", + "\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{hovertext}

color=1
x=%{x}
y=%{y}", + "hovertext": [ + "cell_0", + "cell_2", + "cell_4", + "cell_6", + "cell_7", + "cell_8", + "cell_10", + "cell_11", + "cell_12", + "cell_13", + "cell_14", + "cell_16", + "cell_19", + "cell_20", + "cell_22", + "cell_23", + "cell_24", + "cell_25", + "cell_27", + "cell_28", + "cell_29", + "cell_31", + "cell_33", + "cell_35", + "cell_37", + "cell_38", + "cell_39", + "cell_41", + "cell_42", + "cell_43", + "cell_44", + "cell_47", + "cell_48", + "cell_51", + "cell_53", + "cell_55", + "cell_58", + "cell_60", + "cell_61", + "cell_62", + "cell_64", + "cell_65", + "cell_66", + "cell_68", + "cell_71", + "cell_72", + "cell_73", + "cell_74", + "cell_75", + "cell_76", + "cell_78", + "cell_91", + "cell_93", + "cell_95", + "cell_97", + "cell_136", + "cell_140", + "cell_142", + "cell_144", + "cell_145", + "cell_158", + "cell_162", + "cell_166", + "cell_167", + "cell_175", + "cell_176", + "cell_219", + "cell_253", + "cell_256", + "cell_262", + "cell_272", + "cell_273", + "cell_301" + ], + "legendgroup": "1", + "marker": { + "color": "#1F77B4", + "symbol": "circle" + }, + "mode": "markers", + "name": "1", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 10.470212936401367, + 12.312568664550781, + 12.284958839416504, + 11.013801574707031, + 11.075864791870117, + 11.517743110656738, + 11.250957489013672, + 11.468009948730469, + 11.692370414733887, + 10.716691017150879, + 11.46733283996582, + 12.315851211547852, + 11.25301742553711, + 11.631410598754883, + 11.236824035644531, + 11.964887619018555, + 12.309128761291504, + 11.374908447265625, + 11.650925636291504, + 12.029847145080566, + 11.864778518676758, + 12.271783828735352, + 12.267718315124512, + 12.208593368530273, + 11.614533424377441, + 11.813565254211426, + 11.328191757202148, + 12.266615867614746, + 11.84893798828125, + 12.076929092407227, + 11.760852813720703, + 10.281558990478516, + 10.683618545532227, + 11.117523193359375, + 11.16075325012207, + 10.487278938293457, + 11.64605712890625, + 11.977656364440918, + 11.149227142333984, + 11.74874496459961, + 11.40621280670166, + 11.974194526672363, + 11.77653694152832, + 12.3076753616333, + 10.67529296875, + 12.303462028503418, + 11.24199104309082, + 11.621973037719727, + 11.582865715026855, + 11.592204093933105, + 11.583460807800293, + 11.083187103271484, + 12.001856803894043, + 12.064848899841309, + 11.531425476074219, + 10.988033294677734, + 11.710485458374023, + 10.125171661376953, + 9.907941818237305, + 10.13366985321045, + 10.60818099975586, + 11.986207008361816, + 10.851330757141113, + 11.885449409484863, + 11.892702102661133, + 11.179452896118164, + 12.354615211486816, + 10.74283218383789, + 10.472368240356445, + 10.926989555358887, + 10.817597389221191, + 11.050715446472168, + 10.393438339233398 + ], + "xaxis": "x", + "y": [ + 7.33117151260376, + 8.375398635864258, + 7.855964660644531, + 7.39954137802124, + 7.643902778625488, + 7.103420257568359, + 6.951264381408691, + 8.590084075927734, + 9.220922470092773, + 7.009788513183594, + 7.479423999786377, + 8.541096687316895, + 9.079501152038574, + 8.162571907043457, + 7.48162841796875, + 8.796451568603516, + 8.88451099395752, + 8.459766387939453, + 7.386898994445801, + 8.004698753356934, + 7.842724800109863, + 7.739343166351318, + 8.262476921081543, + 9.184324264526367, + 7.689973831176758, + 7.425989151000977, + 8.443625450134277, + 7.7019147872924805, + 7.726999759674072, + 7.500425815582275, + 7.274848937988281, + 7.219523906707764, + 7.431251049041748, + 9.183481216430664, + 8.774727821350098, + 7.540390968322754, + 7.331007957458496, + 7.45502233505249, + 6.866971969604492, + 7.894918441772461, + 7.443665504455566, + 8.133285522460938, + 7.953441143035889, + 9.155420303344727, + 8.609997749328613, + 8.419486045837402, + 7.263685703277588, + 7.33095645904541, + 8.800616264343262, + 7.4456915855407715, + 8.780095100402832, + 8.757272720336914, + 8.575485229492188, + 8.976503372192383, + 8.741171836853027, + 9.189842224121094, + 8.413444519042969, + 7.051916122436523, + 7.342107772827148, + 7.417575359344482, + 7.990440368652344, + 8.00399112701416, + 6.773506164550781, + 8.533093452453613, + 8.859456062316895, + 6.902346611022949, + 8.565699577331543, + 7.253220081329346, + 7.201398849487305, + 8.053874015808105, + 9.229771614074707, + 9.145679473876953, + 7.159358501434326 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=5
x=%{x}
y=%{y}", + "hovertext": [ + "cell_1", + "cell_3", + "cell_5", + "cell_15", + "cell_17", + "cell_18", + "cell_26", + "cell_30", + "cell_36", + "cell_40", + "cell_50", + "cell_52", + "cell_56", + "cell_57", + "cell_59", + "cell_63", + "cell_67", + "cell_70", + "cell_84", + "cell_92", + "cell_96", + "cell_98", + "cell_99", + "cell_100", + "cell_104", + "cell_105", + "cell_106", + "cell_130", + "cell_137", + "cell_138", + "cell_147", + "cell_148", + "cell_156", + "cell_157", + "cell_159", + "cell_160", + "cell_161", + "cell_163", + "cell_164", + "cell_165", + "cell_169", + "cell_171", + "cell_172", + "cell_174", + "cell_181", + "cell_188", + "cell_192", + "cell_222", + "cell_232", + "cell_234", + "cell_260" + ], + "legendgroup": "5", + "marker": { + "color": "#FF7F0E", + "symbol": "circle" + }, + "mode": "markers", + "name": "5", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 11.747814178466797, + 12.064518928527832, + 11.840521812438965, + 12.267498970031738, + 12.147038459777832, + 12.216019630432129, + 12.024085998535156, + 11.696830749511719, + 11.604247093200684, + 11.593334197998047, + 10.578858375549316, + 11.883559226989746, + 11.195067405700684, + 11.565237998962402, + 10.922109603881836, + 12.37385082244873, + 11.372662544250488, + 12.119458198547363, + 11.421720504760742, + 10.925214767456055, + 10.36055850982666, + 10.446563720703125, + 10.664886474609375, + 10.22600269317627, + 11.17844009399414, + 11.300833702087402, + 10.49958610534668, + 10.450248718261719, + 11.161641120910645, + 11.290375709533691, + 10.804224967956543, + 10.380353927612305, + 11.554306030273438, + 11.103714942932129, + 11.590129852294922, + 11.822622299194336, + 12.213976860046387, + 12.251840591430664, + 12.105546951293945, + 12.289101600646973, + 10.598264694213867, + 11.25814151763916, + 11.343453407287598, + 10.801051139831543, + 11.49448299407959, + 11.171337127685547, + 9.67757511138916, + 11.203398704528809, + 10.798874855041504, + 10.841958999633789, + 11.134123802185059 + ], + "xaxis": "x", + "y": [ + 10.567832946777344, + 10.209301948547363, + 10.536479949951172, + 9.799760818481445, + 10.12877368927002, + 9.717034339904785, + 9.966899871826172, + 10.726451873779297, + 10.07335090637207, + 10.128976821899414, + 9.770319938659668, + 10.564817428588867, + 11.312215805053711, + 9.910209655761719, + 9.962122917175293, + 9.553116798400879, + 11.533130645751953, + 10.125333786010742, + 11.463017463684082, + 9.861875534057617, + 9.56574535369873, + 11.268634796142578, + 11.485913276672363, + 11.012574195861816, + 11.487122535705566, + 11.543986320495605, + 11.359087944030762, + 11.444477081298828, + 9.809798240661621, + 11.211044311523438, + 11.318399429321289, + 10.390735626220703, + 10.801411628723145, + 11.246253967285156, + 11.124070167541504, + 10.421768188476562, + 10.141022682189941, + 9.988823890686035, + 10.561238288879395, + 9.586380004882812, + 11.269648551940918, + 11.518125534057617, + 11.282392501831055, + 11.163555145263672, + 11.396036148071289, + 9.7876558303833, + 10.542497634887695, + 11.513497352600098, + 9.779924392700195, + 9.870909690856934, + 9.754999160766602 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=3
x=%{x}
y=%{y}", + "hovertext": [ + "cell_9", + "cell_21", + "cell_54", + "cell_69", + "cell_77", + "cell_79", + "cell_82", + "cell_85", + "cell_89", + "cell_94", + "cell_102", + "cell_128", + "cell_131", + "cell_133", + "cell_134", + "cell_141", + "cell_146", + "cell_152", + "cell_153", + "cell_170", + "cell_177", + "cell_182", + "cell_191", + "cell_198", + "cell_199", + "cell_204", + "cell_208", + "cell_212", + "cell_221", + "cell_226", + "cell_227", + "cell_231", + "cell_233", + "cell_236", + "cell_241", + "cell_243", + "cell_244", + "cell_247", + "cell_248", + "cell_252", + "cell_254", + "cell_259", + "cell_261", + "cell_271", + "cell_274", + "cell_276", + "cell_286", + "cell_288", + "cell_295", + "cell_296", + "cell_297", + "cell_299", + "cell_311", + "cell_312", + "cell_314", + "cell_315", + "cell_316", + "cell_317", + "cell_324", + "cell_358" + ], + "legendgroup": "3", + "marker": { + "color": "#2CA02C", + "symbol": "circle" + }, + "mode": "markers", + "name": "3", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 10.844043731689453, + 10.066186904907227, + 8.121711730957031, + 9.412109375, + 8.111811637878418, + 8.955904006958008, + 10.099164962768555, + 10.236702919006348, + 9.05978775024414, + 10.014548301696777, + 6.505732536315918, + 8.59902286529541, + 9.48098373413086, + 10.114336013793945, + 8.441006660461426, + 9.067895889282227, + 10.213714599609375, + 7.9023661613464355, + 5.922726154327393, + 7.92042350769043, + 9.144457817077637, + 7.054023265838623, + 8.23034381866455, + 8.893445014953613, + 10.14044189453125, + 8.014692306518555, + 8.886664390563965, + 7.463397979736328, + 7.12321138381958, + 6.124226093292236, + 6.031777381896973, + 7.8968071937561035, + 8.391080856323242, + 10.015118598937988, + 9.610279083251953, + 7.963976860046387, + 8.17613410949707, + 8.345664024353027, + 10.31667709350586, + 6.5018229484558105, + 5.779963970184326, + 10.09521484375, + 10.343782424926758, + 10.042535781860352, + 10.392885208129883, + 10.27789306640625, + 9.997753143310547, + 7.991713047027588, + 8.10283374786377, + 10.316632270812988, + 9.835476875305176, + 9.4730224609375, + 8.855951309204102, + 9.382893562316895, + 9.77592945098877, + 9.874587059020996, + 9.094463348388672, + 8.361153602600098, + 9.347718238830566, + 5.972364902496338 + ], + "xaxis": "x", + "y": [ + 8.4461030960083, + 9.200824737548828, + 9.385358810424805, + 7.435500621795654, + 9.28831958770752, + 9.574503898620605, + 8.64625358581543, + 9.494376182556152, + 9.4609956741333, + 8.643878936767578, + 8.373583793640137, + 10.22214412689209, + 7.6977033615112305, + 9.112919807434082, + 9.428910255432129, + 9.71088981628418, + 9.646464347839355, + 9.598580360412598, + 9.06125545501709, + 9.315159797668457, + 10.246822357177734, + 9.326196670532227, + 9.370452880859375, + 10.092228889465332, + 8.480729103088379, + 9.179167747497559, + 9.7933349609375, + 9.790886878967285, + 9.414827346801758, + 9.188652992248535, + 8.544243812561035, + 8.870431900024414, + 9.942936897277832, + 8.375882148742676, + 7.815301895141602, + 9.440760612487793, + 9.94061279296875, + 9.971707344055176, + 9.429641723632812, + 8.383771896362305, + 8.178888320922852, + 9.015743255615234, + 8.8228178024292, + 8.666848182678223, + 9.175257682800293, + 9.0098237991333, + 8.521554946899414, + 9.103172302246094, + 10.916104316711426, + 8.724075317382812, + 8.450115203857422, + 7.9202775955200195, + 10.031404495239258, + 10.158611297607422, + 8.269808769226074, + 9.457282066345215, + 10.566201210021973, + 9.998526573181152, + 7.717197418212891, + 8.599685668945312 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=2
x=%{x}
y=%{y}", + "hovertext": [ + "cell_32", + "cell_34", + "cell_45", + "cell_46", + "cell_80", + "cell_81", + "cell_83", + "cell_87", + "cell_90", + "cell_101", + "cell_112", + "cell_113", + "cell_115", + "cell_120", + "cell_121", + "cell_122", + "cell_127", + "cell_135", + "cell_139", + "cell_143", + "cell_149", + "cell_150", + "cell_151", + "cell_155", + "cell_168", + "cell_179", + "cell_180", + "cell_186", + "cell_189", + "cell_190", + "cell_196", + "cell_197", + "cell_201", + "cell_202", + "cell_206", + "cell_207", + "cell_209", + "cell_211", + "cell_213", + "cell_216", + "cell_218", + "cell_220", + "cell_223", + "cell_229", + "cell_230", + "cell_235", + "cell_246", + "cell_250", + "cell_255", + "cell_275", + "cell_287", + "cell_291", + "cell_292", + "cell_300", + "cell_313", + "cell_320", + "cell_322", + "cell_361", + "cell_364", + "cell_369" + ], + "legendgroup": "2", + "marker": { + "color": "#D62728", + "symbol": "circle" + }, + "mode": "markers", + "name": "2", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 8.199105262756348, + 6.664831161499023, + 6.700311660766602, + 6.343005657196045, + 9.605731964111328, + 5.576918125152588, + 7.368834018707275, + 8.703349113464355, + 8.41206169128418, + 8.016773223876953, + 9.50320816040039, + 6.412990570068359, + 8.229869842529297, + 6.481535911560059, + 9.348567962646484, + 7.570054054260254, + 5.8002190589904785, + 5.744693756103516, + 8.638879776000977, + 8.070985794067383, + 6.272864818572998, + 6.171213150024414, + 7.215863227844238, + 8.107453346252441, + 7.9924635887146, + 5.479817867279053, + 8.433005332946777, + 5.473900318145752, + 6.895977973937988, + 8.579727172851562, + 7.876437187194824, + 7.183394432067871, + 5.571967601776123, + 8.23187255859375, + 6.689271450042725, + 8.480426788330078, + 8.408916473388672, + 7.915027618408203, + 5.880353927612305, + 5.588372230529785, + 8.57016658782959, + 8.648880004882812, + 5.590453147888184, + 5.821910858154297, + 7.477540493011475, + 8.390219688415527, + 7.340928554534912, + 6.239269256591797, + 9.62497329711914, + 8.277377128601074, + 8.63513469696045, + 5.796451091766357, + 6.15939998626709, + 9.125432968139648, + 6.608971118927002, + 6.744550704956055, + 6.2651472091674805, + 6.119339942932129, + 5.6807098388671875, + 5.4191179275512695 + ], + "xaxis": "x", + "y": [ + 7.334995746612549, + 6.936014652252197, + 6.965074062347412, + 7.11490535736084, + 9.846455574035645, + 7.1071062088012695, + 7.46168851852417, + 7.93528938293457, + 7.807597637176514, + 7.612594127655029, + 10.055404663085938, + 7.9679694175720215, + 7.961348056793213, + 7.610311985015869, + 9.993404388427734, + 7.421642780303955, + 7.23611307144165, + 7.294541358947754, + 7.731958389282227, + 7.233391284942627, + 7.456475734710693, + 8.00796890258789, + 7.518858909606934, + 7.289413928985596, + 7.464017868041992, + 7.262912273406982, + 7.2448410987854, + 7.083313941955566, + 7.355195045471191, + 7.344755172729492, + 7.29884147644043, + 7.494061470031738, + 7.206907272338867, + 7.283708095550537, + 7.632297039031982, + 7.757523536682129, + 8.17127513885498, + 7.573615550994873, + 7.204403400421143, + 7.31341028213501, + 8.371613502502441, + 7.278628826141357, + 7.174847602844238, + 7.4548821449279785, + 7.909790515899658, + 7.999366760253906, + 7.433365821838379, + 7.873532772064209, + 7.285638332366943, + 7.342884063720703, + 7.385688781738281, + 7.497030258178711, + 7.872297763824463, + 7.412783145904541, + 7.7408366203308105, + 7.7426252365112305, + 7.918805122375488, + 7.80131721496582, + 7.159327030181885, + 7.372076988220215 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=4
x=%{x}
y=%{y}", + "hovertext": [ + "cell_49", + "cell_86", + "cell_88", + "cell_103", + "cell_107", + "cell_108", + "cell_109", + "cell_110", + "cell_111", + "cell_116", + "cell_117", + "cell_118", + "cell_119", + "cell_123", + "cell_124", + "cell_125", + "cell_129", + "cell_132", + "cell_154", + "cell_173", + "cell_183", + "cell_184", + "cell_187", + "cell_193", + "cell_194", + "cell_203", + "cell_205", + "cell_210", + "cell_217", + "cell_228", + "cell_240", + "cell_249", + "cell_251", + "cell_257", + "cell_258", + "cell_263", + "cell_284", + "cell_294", + "cell_310", + "cell_319", + "cell_326", + "cell_339", + "cell_340", + "cell_343", + "cell_347", + "cell_350", + "cell_351", + "cell_352", + "cell_356", + "cell_360", + "cell_362", + "cell_368", + "cell_370" + ], + "legendgroup": "4", + "marker": { + "color": "#9467BD", + "symbol": "circle" + }, + "mode": "markers", + "name": "4", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 8.792957305908203, + 9.771020889282227, + 8.111828804016113, + 10.551321029663086, + 7.334377288818359, + 7.512739658355713, + 7.225539684295654, + 7.23398494720459, + 7.602049350738525, + 8.415926933288574, + 8.695684432983398, + 9.391413688659668, + 8.61044979095459, + 8.571767807006836, + 10.289719581604004, + 9.380416870117188, + 8.570059776306152, + 9.31619930267334, + 8.835460662841797, + 8.39517879486084, + 9.052550315856934, + 7.930122375488281, + 9.493639945983887, + 10.106727600097656, + 8.337416648864746, + 8.134517669677734, + 9.564823150634766, + 8.223367691040039, + 9.31398868560791, + 8.53930950164795, + 7.381310939788818, + 8.93163013458252, + 8.030320167541504, + 6.583729267120361, + 9.901187896728516, + 7.684980392456055, + 7.3445048332214355, + 7.500270843505859, + 7.859353542327881, + 8.66342830657959, + 8.51186752319336, + 7.962340354919434, + 9.412398338317871, + 7.3680572509765625, + 7.5125837326049805, + 9.67270565032959, + 9.116009712219238, + 8.934961318969727, + 11.073728561401367, + 9.277174949645996, + 9.565858840942383, + 7.4820122718811035, + 9.112516403198242 + ], + "xaxis": "x", + "y": [ + 11.721299171447754, + 11.314684867858887, + 11.407580375671387, + 11.606562614440918, + 10.656499862670898, + 10.366463661193848, + 10.384355545043945, + 10.080385208129883, + 11.30085563659668, + 10.278273582458496, + 11.15866470336914, + 11.27371597290039, + 11.38850212097168, + 11.93083667755127, + 11.713282585144043, + 11.3212251663208, + 10.616705894470215, + 11.670025825500488, + 11.509689331054688, + 11.87523365020752, + 11.74281120300293, + 11.637688636779785, + 11.629831314086914, + 11.557605743408203, + 11.327455520629883, + 11.533363342285156, + 11.448644638061523, + 11.56425952911377, + 11.643001556396484, + 12.002367973327637, + 11.237997055053711, + 11.807448387145996, + 11.91479206085205, + 9.93568229675293, + 11.719674110412598, + 11.008150100708008, + 10.831694602966309, + 10.681164741516113, + 11.559565544128418, + 11.62997055053711, + 11.910080909729004, + 11.488192558288574, + 12.004965782165527, + 10.77297592163086, + 10.818303108215332, + 11.836057662963867, + 11.363900184631348, + 11.62457275390625, + 11.685964584350586, + 11.854164123535156, + 11.920244216918945, + 11.331361770629883, + 11.9354887008667 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=0
x=%{x}
y=%{y}", + "hovertext": [ + "cell_114", + "cell_126", + "cell_178", + "cell_185", + "cell_195", + "cell_200", + "cell_214", + "cell_215", + "cell_224", + "cell_225", + "cell_237", + "cell_238", + "cell_239", + "cell_242", + "cell_245", + "cell_264", + "cell_265", + "cell_266", + "cell_267", + "cell_268", + "cell_269", + "cell_270", + "cell_277", + "cell_278", + "cell_279", + "cell_280", + "cell_281", + "cell_282", + "cell_283", + "cell_285", + "cell_289", + "cell_290", + "cell_293", + "cell_298", + "cell_302", + "cell_303", + "cell_304", + "cell_305", + "cell_306", + "cell_307", + "cell_308", + "cell_309", + "cell_318", + "cell_321", + "cell_323", + "cell_325", + "cell_327", + "cell_328", + "cell_329", + "cell_330", + "cell_331", + "cell_332", + "cell_333", + "cell_334", + "cell_335", + "cell_336", + "cell_337", + "cell_338", + "cell_341", + "cell_342", + "cell_344", + "cell_345", + "cell_346", + "cell_348", + "cell_349", + "cell_353", + "cell_354", + "cell_355", + "cell_357", + "cell_359", + "cell_363", + "cell_365", + "cell_366", + "cell_367", + "cell_371", + "cell_372" + ], + "legendgroup": "0", + "marker": { + "color": "#8C564B", + "symbol": "circle" + }, + "mode": "markers", + "name": "0", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 5.8254241943359375, + 6.312600612640381, + 7.4073591232299805, + 5.5496439933776855, + 6.587268352508545, + 6.415332794189453, + 5.402957439422607, + 5.634885787963867, + 5.412581443786621, + 5.480430603027344, + 5.923130035400391, + 4.660431861877441, + 4.750604152679443, + 5.676141738891602, + 6.9678497314453125, + 5.845112323760986, + 4.730572700500488, + 5.364740371704102, + 5.6140618324279785, + 5.58400821685791, + 5.626848220825195, + 6.026407241821289, + 4.661167621612549, + 4.845733642578125, + 6.269335746765137, + 6.569248676300049, + 4.694128513336182, + 5.913049221038818, + 4.75978422164917, + 6.9230732917785645, + 6.047796249389648, + 6.43698263168335, + 6.868078231811523, + 6.228044509887695, + 4.831230163574219, + 4.7045416831970215, + 5.938592433929443, + 5.495119571685791, + 5.022512435913086, + 4.881829261779785, + 6.051345348358154, + 5.975616931915283, + 5.391265869140625, + 5.728851795196533, + 5.717605113983154, + 6.81803035736084, + 6.168428897857666, + 6.318421840667725, + 5.614867210388184, + 5.778199195861816, + 5.653630256652832, + 6.127173900604248, + 6.019142150878906, + 5.603560924530029, + 5.99357271194458, + 5.24373722076416, + 6.886112213134766, + 5.429830551147461, + 5.813126087188721, + 6.325909614562988, + 6.5907793045043945, + 5.358606338500977, + 5.31993293762207, + 5.788165092468262, + 7.344539642333984, + 6.769246578216553, + 5.575586795806885, + 6.623497009277344, + 6.2303290367126465, + 5.447906970977783, + 7.3655853271484375, + 5.552432060241699, + 5.909779071807861, + 5.980591297149658, + 5.426873683929443, + 6.0437188148498535 + ], + "xaxis": "x", + "y": [ + 9.319472312927246, + 11.60047435760498, + 9.9579496383667, + 7.996298313140869, + 11.07923412322998, + 11.820281982421875, + 8.979147911071777, + 10.918229103088379, + 10.264891624450684, + 7.8346991539001465, + 11.048379898071289, + 10.001415252685547, + 10.235962867736816, + 9.327349662780762, + 11.078413963317871, + 10.565495491027832, + 10.074840545654297, + 8.80063247680664, + 10.688979148864746, + 8.635754585266113, + 8.716435432434082, + 10.811123847961426, + 10.050178527832031, + 10.319920539855957, + 11.122275352478027, + 10.861050605773926, + 10.088911056518555, + 11.370647430419922, + 10.005218505859375, + 11.350404739379883, + 11.336915969848633, + 10.390603065490723, + 11.310484886169434, + 9.756113052368164, + 10.019303321838379, + 10.080672264099121, + 10.09730052947998, + 8.764248847961426, + 9.83413028717041, + 9.872381210327148, + 9.617083549499512, + 10.9186372756958, + 8.531365394592285, + 8.633432388305664, + 10.969356536865234, + 11.489310264587402, + 10.530739784240723, + 11.73331069946289, + 8.403346061706543, + 11.075582504272461, + 11.138618469238281, + 10.732989311218262, + 10.433423042297363, + 9.8084135055542, + 10.914801597595215, + 10.086227416992188, + 11.159547805786133, + 10.681574821472168, + 8.927404403686523, + 11.69886302947998, + 11.474774360656738, + 9.330952644348145, + 8.596227645874023, + 10.771533012390137, + 11.562420845031738, + 10.67807388305664, + 8.098641395568848, + 11.60422134399414, + 11.645100593566895, + 8.42159366607666, + 11.64996337890625, + 10.978010177612305, + 10.883095741271973, + 9.500628471374512, + 10.904341697692871, + 11.054499626159668 + ], + "yaxis": "y" + } + ], + "layout": { + "legend": { + "title": { + "text": "color" + }, + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "rgb(36,36,36)" + }, + "error_y": { + "color": "rgb(36,36,36)" + }, + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "rgb(36,36,36)", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "rgb(36,36,36)" + }, + "baxis": { + "endlinecolor": "rgb(36,36,36)", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "rgb(36,36,36)" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.6 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "rgb(237,237,237)" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "rgb(217,217,217)" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "rgb(103,0,31)" + ], + [ + 0.1, + "rgb(178,24,43)" + ], + [ + 0.2, + "rgb(214,96,77)" + ], + [ + 0.3, + "rgb(244,165,130)" + ], + [ + 0.4, + "rgb(253,219,199)" + ], + [ + 0.5, + "rgb(247,247,247)" + ], + [ + 0.6, + "rgb(209,229,240)" + ], + [ + 0.7, + "rgb(146,197,222)" + ], + [ + 0.8, + "rgb(67,147,195)" + ], + [ + 0.9, + "rgb(33,102,172)" + ], + [ + 1, + "rgb(5,48,97)" + ] + ], + "sequential": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "sequentialminus": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ] + }, + "colorway": [ + "#1F77B4", + "#FF7F0E", + "#2CA02C", + "#D62728", + "#9467BD", + "#8C564B", + "#E377C2", + "#7F7F7F", + "#BCBD22", + "#17BECF" + ], + "font": { + "color": "rgb(36,36,36)" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "white", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "white", + "polar": { + "angularaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "bgcolor": "white", + "radialaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "yaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "zaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + } + }, + "shapedefaults": { + "fillcolor": "black", + "line": { + "width": 0 + }, + "opacity": 0.3 + }, + "ternary": { + "aaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "baxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "bgcolor": "white", + "caxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside", + "title": { + "standoff": 15 + }, + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "yaxis": { + "automargin": true, + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside", + "title": { + "standoff": 15 + }, + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "x" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "y" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Compute UMAP representation of the OT localization space\n", + "reducer = umap.UMAP(metric=\"precomputed\", random_state=1)\n", + "embedding = reducer.fit_transform(ot_dmats[0])\n", + "\n", + "# Cluster cells based on the OT localization space using the leiden algorithm\n", + "clusters = cajal.utilities.leiden_clustering(ot_dmats[0], resolution=0.005, seed=1)\n", + "\n", + "# Visualize the OT localization space\n", + "plotly.express.scatter(x=embedding[:,0],\n", + " y=embedding[:,1],\n", + " template=\"simple_white\",\n", + " hover_name=[\"cell_\" + str(i) for i in range(ot_dmats[0].shape[0])],\n", + " color = [str(c) for c in clusters]\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "13f8e5cb", + "metadata": {}, + "source": [ + "We can visualize some example cells and their protein distributions from cluster 0." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "839bec6f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cells_to_plot = [335, 270, 309] # Example cells from cluster 0\n", + "fig, axes = plt.subplots(1, len(cells_to_plot), figsize=(15, 5))\n", + "for ax, i in zip(axes, cells_to_plot):\n", + " plot_cell_image(cell_objects[i], channels=['nucleus', 'protein'], ax=ax)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ed7eb1dc", + "metadata": {}, + "source": [ + "We can also color the UMAP representation of the optimal transport localization space by the localization pattern annotations from the Human Protein Atlas. As expected, the annotated localization patterns separate in the localization space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77c4d262", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/plotly/express/_core.py:1992: FutureWarning:\n", + "\n", + "When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", + "\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{hovertext}

color=cytosol
x=%{x}
y=%{y}", + "hovertext": [ + "cell_0", + "cell_1", + "cell_2", + "cell_3", + "cell_4", + "cell_5", + "cell_6", + "cell_7", + "cell_8", + "cell_9", + "cell_10", + "cell_11", + "cell_12", + "cell_13", + "cell_14", + "cell_15", + "cell_16", + "cell_17", + "cell_18", + "cell_19", + "cell_20", + "cell_21", + "cell_22", + "cell_23", + "cell_24", + "cell_25", + "cell_26", + "cell_27", + "cell_28", + "cell_29", + "cell_30", + "cell_31", + "cell_32", + "cell_33", + "cell_34", + "cell_35", + "cell_36", + "cell_37", + "cell_38", + "cell_39", + "cell_40", + "cell_41", + "cell_42", + "cell_43", + "cell_44", + "cell_45", + "cell_46", + "cell_47", + "cell_48", + "cell_49", + "cell_50", + "cell_51", + "cell_52", + "cell_53", + "cell_54", + "cell_55", + "cell_56", + "cell_57", + "cell_58", + "cell_59", + "cell_60", + "cell_61", + "cell_62", + "cell_63", + "cell_64", + "cell_65", + "cell_66", + "cell_67", + "cell_68", + "cell_69", + "cell_70", + "cell_71", + "cell_72", + "cell_73", + "cell_74", + "cell_75", + "cell_76", + "cell_77", + "cell_78", + "cell_79", + "cell_80", + "cell_81", + "cell_82", + "cell_83", + "cell_84", + "cell_85", + "cell_86" + ], + "legendgroup": "cytosol", + "marker": { + "color": "#1F77B4", + "symbol": "circle" + }, + "mode": "markers", + "name": "cytosol", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 10.470212936401367, + 11.747814178466797, + 12.312568664550781, + 12.064518928527832, + 12.284958839416504, + 11.840521812438965, + 11.013801574707031, + 11.075864791870117, + 11.517743110656738, + 10.844043731689453, + 11.250957489013672, + 11.468009948730469, + 11.692370414733887, + 10.716691017150879, + 11.46733283996582, + 12.267498970031738, + 12.315851211547852, + 12.147038459777832, + 12.216019630432129, + 11.25301742553711, + 11.631410598754883, + 10.066186904907227, + 11.236824035644531, + 11.964887619018555, + 12.309128761291504, + 11.374908447265625, + 12.024085998535156, + 11.650925636291504, + 12.029847145080566, + 11.864778518676758, + 11.696830749511719, + 12.271783828735352, + 8.199105262756348, + 12.267718315124512, + 6.664831161499023, + 12.208593368530273, + 11.604247093200684, + 11.614533424377441, + 11.813565254211426, + 11.328191757202148, + 11.593334197998047, + 12.266615867614746, + 11.84893798828125, + 12.076929092407227, + 11.760852813720703, + 6.700311660766602, + 6.343005657196045, + 10.281558990478516, + 10.683618545532227, + 8.792957305908203, + 10.578858375549316, + 11.117523193359375, + 11.883559226989746, + 11.16075325012207, + 8.121711730957031, + 10.487278938293457, + 11.195067405700684, + 11.565237998962402, + 11.64605712890625, + 10.922109603881836, + 11.977656364440918, + 11.149227142333984, + 11.74874496459961, + 12.37385082244873, + 11.40621280670166, + 11.974194526672363, + 11.77653694152832, + 11.372662544250488, + 12.3076753616333, + 9.412109375, + 12.119458198547363, + 10.67529296875, + 12.303462028503418, + 11.24199104309082, + 11.621973037719727, + 11.582865715026855, + 11.592204093933105, + 8.111811637878418, + 11.583460807800293, + 8.955904006958008, + 9.605731964111328, + 5.576918125152588, + 10.099164962768555, + 7.368834018707275, + 11.421720504760742, + 10.236702919006348, + 9.771020889282227 + ], + "xaxis": "x", + "y": [ + 7.33117151260376, + 10.567832946777344, + 8.375398635864258, + 10.209301948547363, + 7.855964660644531, + 10.536479949951172, + 7.39954137802124, + 7.643902778625488, + 7.103420257568359, + 8.4461030960083, + 6.951264381408691, + 8.590084075927734, + 9.220922470092773, + 7.009788513183594, + 7.479423999786377, + 9.799760818481445, + 8.541096687316895, + 10.12877368927002, + 9.717034339904785, + 9.079501152038574, + 8.162571907043457, + 9.200824737548828, + 7.48162841796875, + 8.796451568603516, + 8.88451099395752, + 8.459766387939453, + 9.966899871826172, + 7.386898994445801, + 8.004698753356934, + 7.842724800109863, + 10.726451873779297, + 7.739343166351318, + 7.334995746612549, + 8.262476921081543, + 6.936014652252197, + 9.184324264526367, + 10.07335090637207, + 7.689973831176758, + 7.425989151000977, + 8.443625450134277, + 10.128976821899414, + 7.7019147872924805, + 7.726999759674072, + 7.500425815582275, + 7.274848937988281, + 6.965074062347412, + 7.11490535736084, + 7.219523906707764, + 7.431251049041748, + 11.721299171447754, + 9.770319938659668, + 9.183481216430664, + 10.564817428588867, + 8.774727821350098, + 9.385358810424805, + 7.540390968322754, + 11.312215805053711, + 9.910209655761719, + 7.331007957458496, + 9.962122917175293, + 7.45502233505249, + 6.866971969604492, + 7.894918441772461, + 9.553116798400879, + 7.443665504455566, + 8.133285522460938, + 7.953441143035889, + 11.533130645751953, + 9.155420303344727, + 7.435500621795654, + 10.125333786010742, + 8.609997749328613, + 8.419486045837402, + 7.263685703277588, + 7.33095645904541, + 8.800616264343262, + 7.4456915855407715, + 9.28831958770752, + 8.780095100402832, + 9.574503898620605, + 9.846455574035645, + 7.1071062088012695, + 8.64625358581543, + 7.46168851852417, + 11.463017463684082, + 9.494376182556152, + 11.314684867858887 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=golgi apparatus
x=%{x}
y=%{y}", + "hovertext": [ + "cell_87", + "cell_88", + "cell_89", + "cell_90", + "cell_91", + "cell_92", + "cell_93", + "cell_94", + "cell_95", + "cell_96", + "cell_97", + "cell_98", + "cell_99", + "cell_100", + "cell_101", + "cell_102", + "cell_103", + "cell_104", + "cell_105", + "cell_106", + "cell_107", + "cell_108", + "cell_109", + "cell_110", + "cell_111", + "cell_112", + "cell_113", + "cell_114", + "cell_115", + "cell_116", + "cell_117", + "cell_118", + "cell_119", + "cell_120", + "cell_121", + "cell_122", + "cell_123", + "cell_124", + "cell_125", + "cell_126", + "cell_127", + "cell_128", + "cell_129", + "cell_130", + "cell_131", + "cell_132", + "cell_133", + "cell_134", + "cell_135", + "cell_136", + "cell_137", + "cell_138", + "cell_139", + "cell_140", + "cell_141", + "cell_142", + "cell_143", + "cell_144", + "cell_145", + "cell_146", + "cell_147", + "cell_148", + "cell_149", + "cell_150", + "cell_151", + "cell_152", + "cell_153", + "cell_154" + ], + "legendgroup": "golgi apparatus", + "marker": { + "color": "#FF7F0E", + "symbol": "circle" + }, + "mode": "markers", + "name": "golgi apparatus", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 8.703349113464355, + 8.111828804016113, + 9.05978775024414, + 8.41206169128418, + 11.083187103271484, + 10.925214767456055, + 12.001856803894043, + 10.014548301696777, + 12.064848899841309, + 10.36055850982666, + 11.531425476074219, + 10.446563720703125, + 10.664886474609375, + 10.22600269317627, + 8.016773223876953, + 6.505732536315918, + 10.551321029663086, + 11.17844009399414, + 11.300833702087402, + 10.49958610534668, + 7.334377288818359, + 7.512739658355713, + 7.225539684295654, + 7.23398494720459, + 7.602049350738525, + 9.50320816040039, + 6.412990570068359, + 5.8254241943359375, + 8.229869842529297, + 8.415926933288574, + 8.695684432983398, + 9.391413688659668, + 8.61044979095459, + 6.481535911560059, + 9.348567962646484, + 7.570054054260254, + 8.571767807006836, + 10.289719581604004, + 9.380416870117188, + 6.312600612640381, + 5.8002190589904785, + 8.59902286529541, + 8.570059776306152, + 10.450248718261719, + 9.48098373413086, + 9.31619930267334, + 10.114336013793945, + 8.441006660461426, + 5.744693756103516, + 10.988033294677734, + 11.161641120910645, + 11.290375709533691, + 8.638879776000977, + 11.710485458374023, + 9.067895889282227, + 10.125171661376953, + 8.070985794067383, + 9.907941818237305, + 10.13366985321045, + 10.213714599609375, + 10.804224967956543, + 10.380353927612305, + 6.272864818572998, + 6.171213150024414, + 7.215863227844238, + 7.9023661613464355, + 5.922726154327393, + 8.835460662841797 + ], + "xaxis": "x", + "y": [ + 7.93528938293457, + 11.407580375671387, + 9.4609956741333, + 7.807597637176514, + 8.757272720336914, + 9.861875534057617, + 8.575485229492188, + 8.643878936767578, + 8.976503372192383, + 9.56574535369873, + 8.741171836853027, + 11.268634796142578, + 11.485913276672363, + 11.012574195861816, + 7.612594127655029, + 8.373583793640137, + 11.606562614440918, + 11.487122535705566, + 11.543986320495605, + 11.359087944030762, + 10.656499862670898, + 10.366463661193848, + 10.384355545043945, + 10.080385208129883, + 11.30085563659668, + 10.055404663085938, + 7.9679694175720215, + 9.319472312927246, + 7.961348056793213, + 10.278273582458496, + 11.15866470336914, + 11.27371597290039, + 11.38850212097168, + 7.610311985015869, + 9.993404388427734, + 7.421642780303955, + 11.93083667755127, + 11.713282585144043, + 11.3212251663208, + 11.60047435760498, + 7.23611307144165, + 10.22214412689209, + 10.616705894470215, + 11.444477081298828, + 7.6977033615112305, + 11.670025825500488, + 9.112919807434082, + 9.428910255432129, + 7.294541358947754, + 9.189842224121094, + 9.809798240661621, + 11.211044311523438, + 7.731958389282227, + 8.413444519042969, + 9.71088981628418, + 7.051916122436523, + 7.233391284942627, + 7.342107772827148, + 7.417575359344482, + 9.646464347839355, + 11.318399429321289, + 10.390735626220703, + 7.456475734710693, + 8.00796890258789, + 7.518858909606934, + 9.598580360412598, + 9.06125545501709, + 11.509689331054688 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=mitochondria
x=%{x}
y=%{y}", + "hovertext": [ + "cell_155", + "cell_156", + "cell_157", + "cell_158", + "cell_159", + "cell_160", + "cell_161", + "cell_162", + "cell_163", + "cell_164", + "cell_165", + "cell_166", + "cell_167", + "cell_168", + "cell_169", + "cell_170", + "cell_171", + "cell_172", + "cell_173", + "cell_174", + "cell_175", + "cell_176", + "cell_177", + "cell_178", + "cell_179", + "cell_180", + "cell_181", + "cell_182", + "cell_183", + "cell_184", + "cell_185", + "cell_186", + "cell_187", + "cell_188", + "cell_189", + "cell_190", + "cell_191", + "cell_192", + "cell_193", + "cell_194", + "cell_195", + "cell_196", + "cell_197", + "cell_198", + "cell_199", + "cell_200", + "cell_201", + "cell_202", + "cell_203", + "cell_204", + "cell_205", + "cell_206", + "cell_207", + "cell_208", + "cell_209", + "cell_210", + "cell_211", + "cell_212", + "cell_213", + "cell_214", + "cell_215", + "cell_216", + "cell_217", + "cell_218", + "cell_219", + "cell_220", + "cell_221", + "cell_222", + "cell_223" + ], + "legendgroup": "mitochondria", + "marker": { + "color": "#2CA02C", + "symbol": "circle" + }, + "mode": "markers", + "name": "mitochondria", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 8.107453346252441, + 11.554306030273438, + 11.103714942932129, + 10.60818099975586, + 11.590129852294922, + 11.822622299194336, + 12.213976860046387, + 11.986207008361816, + 12.251840591430664, + 12.105546951293945, + 12.289101600646973, + 10.851330757141113, + 11.885449409484863, + 7.9924635887146, + 10.598264694213867, + 7.92042350769043, + 11.25814151763916, + 11.343453407287598, + 8.39517879486084, + 10.801051139831543, + 11.892702102661133, + 11.179452896118164, + 9.144457817077637, + 7.4073591232299805, + 5.479817867279053, + 8.433005332946777, + 11.49448299407959, + 7.054023265838623, + 9.052550315856934, + 7.930122375488281, + 5.5496439933776855, + 5.473900318145752, + 9.493639945983887, + 11.171337127685547, + 6.895977973937988, + 8.579727172851562, + 8.23034381866455, + 9.67757511138916, + 10.106727600097656, + 8.337416648864746, + 6.587268352508545, + 7.876437187194824, + 7.183394432067871, + 8.893445014953613, + 10.14044189453125, + 6.415332794189453, + 5.571967601776123, + 8.23187255859375, + 8.134517669677734, + 8.014692306518555, + 9.564823150634766, + 6.689271450042725, + 8.480426788330078, + 8.886664390563965, + 8.408916473388672, + 8.223367691040039, + 7.915027618408203, + 7.463397979736328, + 5.880353927612305, + 5.402957439422607, + 5.634885787963867, + 5.588372230529785, + 9.31398868560791, + 8.57016658782959, + 12.354615211486816, + 8.648880004882812, + 7.12321138381958, + 11.203398704528809, + 5.590453147888184 + ], + "xaxis": "x", + "y": [ + 7.289413928985596, + 10.801411628723145, + 11.246253967285156, + 7.990440368652344, + 11.124070167541504, + 10.421768188476562, + 10.141022682189941, + 8.00399112701416, + 9.988823890686035, + 10.561238288879395, + 9.586380004882812, + 6.773506164550781, + 8.533093452453613, + 7.464017868041992, + 11.269648551940918, + 9.315159797668457, + 11.518125534057617, + 11.282392501831055, + 11.87523365020752, + 11.163555145263672, + 8.859456062316895, + 6.902346611022949, + 10.246822357177734, + 9.9579496383667, + 7.262912273406982, + 7.2448410987854, + 11.396036148071289, + 9.326196670532227, + 11.74281120300293, + 11.637688636779785, + 7.996298313140869, + 7.083313941955566, + 11.629831314086914, + 9.7876558303833, + 7.355195045471191, + 7.344755172729492, + 9.370452880859375, + 10.542497634887695, + 11.557605743408203, + 11.327455520629883, + 11.07923412322998, + 7.29884147644043, + 7.494061470031738, + 10.092228889465332, + 8.480729103088379, + 11.820281982421875, + 7.206907272338867, + 7.283708095550537, + 11.533363342285156, + 9.179167747497559, + 11.448644638061523, + 7.632297039031982, + 7.757523536682129, + 9.7933349609375, + 8.17127513885498, + 11.56425952911377, + 7.573615550994873, + 9.790886878967285, + 7.204403400421143, + 8.979147911071777, + 10.918229103088379, + 7.31341028213501, + 11.643001556396484, + 8.371613502502441, + 8.565699577331543, + 7.278628826141357, + 9.414827346801758, + 11.513497352600098, + 7.174847602844238 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nucleoli
x=%{x}
y=%{y}", + "hovertext": [ + "cell_224", + "cell_225", + "cell_226", + "cell_227", + "cell_228", + "cell_229", + "cell_230", + "cell_231", + "cell_232", + "cell_233", + "cell_234", + "cell_235", + "cell_236", + "cell_237", + "cell_238", + "cell_239", + "cell_240", + "cell_241", + "cell_242", + "cell_243", + "cell_244", + "cell_245", + "cell_246", + "cell_247", + "cell_248", + "cell_249", + "cell_250", + "cell_251", + "cell_252", + "cell_253", + "cell_254", + "cell_255", + "cell_256", + "cell_257", + "cell_258", + "cell_259", + "cell_260", + "cell_261", + "cell_262", + "cell_263", + "cell_264", + "cell_265", + "cell_266", + "cell_267", + "cell_268", + "cell_269", + "cell_270", + "cell_271", + "cell_272", + "cell_273", + "cell_274", + "cell_275", + "cell_276", + "cell_277", + "cell_278", + "cell_279", + "cell_280", + "cell_281", + "cell_282", + "cell_283", + "cell_284", + "cell_285", + "cell_286", + "cell_287", + "cell_288", + "cell_289", + "cell_290", + "cell_291", + "cell_292", + "cell_293", + "cell_294" + ], + "legendgroup": "nucleoli", + "marker": { + "color": "#D62728", + "symbol": "circle" + }, + "mode": "markers", + "name": "nucleoli", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 5.412581443786621, + 5.480430603027344, + 6.124226093292236, + 6.031777381896973, + 8.53930950164795, + 5.821910858154297, + 7.477540493011475, + 7.8968071937561035, + 10.798874855041504, + 8.391080856323242, + 10.841958999633789, + 8.390219688415527, + 10.015118598937988, + 5.923130035400391, + 4.660431861877441, + 4.750604152679443, + 7.381310939788818, + 9.610279083251953, + 5.676141738891602, + 7.963976860046387, + 8.17613410949707, + 6.9678497314453125, + 7.340928554534912, + 8.345664024353027, + 10.31667709350586, + 8.93163013458252, + 6.239269256591797, + 8.030320167541504, + 6.5018229484558105, + 10.74283218383789, + 5.779963970184326, + 9.62497329711914, + 10.472368240356445, + 6.583729267120361, + 9.901187896728516, + 10.09521484375, + 11.134123802185059, + 10.343782424926758, + 10.926989555358887, + 7.684980392456055, + 5.845112323760986, + 4.730572700500488, + 5.364740371704102, + 5.6140618324279785, + 5.58400821685791, + 5.626848220825195, + 6.026407241821289, + 10.042535781860352, + 10.817597389221191, + 11.050715446472168, + 10.392885208129883, + 8.277377128601074, + 10.27789306640625, + 4.661167621612549, + 4.845733642578125, + 6.269335746765137, + 6.569248676300049, + 4.694128513336182, + 5.913049221038818, + 4.75978422164917, + 7.3445048332214355, + 6.9230732917785645, + 9.997753143310547, + 8.63513469696045, + 7.991713047027588, + 6.047796249389648, + 6.43698263168335, + 5.796451091766357, + 6.15939998626709, + 6.868078231811523, + 7.500270843505859 + ], + "xaxis": "x", + "y": [ + 10.264891624450684, + 7.8346991539001465, + 9.188652992248535, + 8.544243812561035, + 12.002367973327637, + 7.4548821449279785, + 7.909790515899658, + 8.870431900024414, + 9.779924392700195, + 9.942936897277832, + 9.870909690856934, + 7.999366760253906, + 8.375882148742676, + 11.048379898071289, + 10.001415252685547, + 10.235962867736816, + 11.237997055053711, + 7.815301895141602, + 9.327349662780762, + 9.440760612487793, + 9.94061279296875, + 11.078413963317871, + 7.433365821838379, + 9.971707344055176, + 9.429641723632812, + 11.807448387145996, + 7.873532772064209, + 11.91479206085205, + 8.383771896362305, + 7.253220081329346, + 8.178888320922852, + 7.285638332366943, + 7.201398849487305, + 9.93568229675293, + 11.719674110412598, + 9.015743255615234, + 9.754999160766602, + 8.8228178024292, + 8.053874015808105, + 11.008150100708008, + 10.565495491027832, + 10.074840545654297, + 8.80063247680664, + 10.688979148864746, + 8.635754585266113, + 8.716435432434082, + 10.811123847961426, + 8.666848182678223, + 9.229771614074707, + 9.145679473876953, + 9.175257682800293, + 7.342884063720703, + 9.0098237991333, + 10.050178527832031, + 10.319920539855957, + 11.122275352478027, + 10.861050605773926, + 10.088911056518555, + 11.370647430419922, + 10.005218505859375, + 10.831694602966309, + 11.350404739379883, + 8.521554946899414, + 7.385688781738281, + 9.103172302246094, + 11.336915969848633, + 10.390603065490723, + 7.497030258178711, + 7.872297763824463, + 11.310484886169434, + 10.681164741516113 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nucleoplasm
x=%{x}
y=%{y}", + "hovertext": [ + "cell_295", + "cell_296", + "cell_297", + "cell_298", + "cell_299", + "cell_300", + "cell_301", + "cell_302", + "cell_303", + "cell_304", + "cell_305", + "cell_306", + "cell_307", + "cell_308", + "cell_309", + "cell_310", + "cell_311", + "cell_312", + "cell_313", + "cell_314", + "cell_315", + "cell_316", + "cell_317", + "cell_318", + "cell_319", + "cell_320", + "cell_321", + "cell_322", + "cell_323", + "cell_324", + "cell_325", + "cell_326", + "cell_327", + "cell_328", + "cell_329", + "cell_330", + "cell_331", + "cell_332", + "cell_333", + "cell_334", + "cell_335", + "cell_336", + "cell_337", + "cell_338", + "cell_339", + "cell_340", + "cell_341", + "cell_342", + "cell_343", + "cell_344", + "cell_345", + "cell_346", + "cell_347", + "cell_348", + "cell_349", + "cell_350", + "cell_351", + "cell_352", + "cell_353", + "cell_354", + "cell_355", + "cell_356", + "cell_357", + "cell_358", + "cell_359", + "cell_360", + "cell_361", + "cell_362", + "cell_363", + "cell_364", + "cell_365", + "cell_366", + "cell_367", + "cell_368", + "cell_369", + "cell_370", + "cell_371", + "cell_372" + ], + "legendgroup": "nucleoplasm", + "marker": { + "color": "#9467BD", + "symbol": "circle" + }, + "mode": "markers", + "name": "nucleoplasm", + "orientation": "v", + "showlegend": true, + "type": "scatter", + "x": [ + 8.10283374786377, + 10.316632270812988, + 9.835476875305176, + 6.228044509887695, + 9.4730224609375, + 9.125432968139648, + 10.393438339233398, + 4.831230163574219, + 4.7045416831970215, + 5.938592433929443, + 5.495119571685791, + 5.022512435913086, + 4.881829261779785, + 6.051345348358154, + 5.975616931915283, + 7.859353542327881, + 8.855951309204102, + 9.382893562316895, + 6.608971118927002, + 9.77592945098877, + 9.874587059020996, + 9.094463348388672, + 8.361153602600098, + 5.391265869140625, + 8.66342830657959, + 6.744550704956055, + 5.728851795196533, + 6.2651472091674805, + 5.717605113983154, + 9.347718238830566, + 6.81803035736084, + 8.51186752319336, + 6.168428897857666, + 6.318421840667725, + 5.614867210388184, + 5.778199195861816, + 5.653630256652832, + 6.127173900604248, + 6.019142150878906, + 5.603560924530029, + 5.99357271194458, + 5.24373722076416, + 6.886112213134766, + 5.429830551147461, + 7.962340354919434, + 9.412398338317871, + 5.813126087188721, + 6.325909614562988, + 7.3680572509765625, + 6.5907793045043945, + 5.358606338500977, + 5.31993293762207, + 7.5125837326049805, + 5.788165092468262, + 7.344539642333984, + 9.67270565032959, + 9.116009712219238, + 8.934961318969727, + 6.769246578216553, + 5.575586795806885, + 6.623497009277344, + 11.073728561401367, + 6.2303290367126465, + 5.972364902496338, + 5.447906970977783, + 9.277174949645996, + 6.119339942932129, + 9.565858840942383, + 7.3655853271484375, + 5.6807098388671875, + 5.552432060241699, + 5.909779071807861, + 5.980591297149658, + 7.4820122718811035, + 5.4191179275512695, + 9.112516403198242, + 5.426873683929443, + 6.0437188148498535 + ], + "xaxis": "x", + "y": [ + 10.916104316711426, + 8.724075317382812, + 8.450115203857422, + 9.756113052368164, + 7.9202775955200195, + 7.412783145904541, + 7.159358501434326, + 10.019303321838379, + 10.080672264099121, + 10.09730052947998, + 8.764248847961426, + 9.83413028717041, + 9.872381210327148, + 9.617083549499512, + 10.9186372756958, + 11.559565544128418, + 10.031404495239258, + 10.158611297607422, + 7.7408366203308105, + 8.269808769226074, + 9.457282066345215, + 10.566201210021973, + 9.998526573181152, + 8.531365394592285, + 11.62997055053711, + 7.7426252365112305, + 8.633432388305664, + 7.918805122375488, + 10.969356536865234, + 7.717197418212891, + 11.489310264587402, + 11.910080909729004, + 10.530739784240723, + 11.73331069946289, + 8.403346061706543, + 11.075582504272461, + 11.138618469238281, + 10.732989311218262, + 10.433423042297363, + 9.8084135055542, + 10.914801597595215, + 10.086227416992188, + 11.159547805786133, + 10.681574821472168, + 11.488192558288574, + 12.004965782165527, + 8.927404403686523, + 11.69886302947998, + 10.77297592163086, + 11.474774360656738, + 9.330952644348145, + 8.596227645874023, + 10.818303108215332, + 10.771533012390137, + 11.562420845031738, + 11.836057662963867, + 11.363900184631348, + 11.62457275390625, + 10.67807388305664, + 8.098641395568848, + 11.60422134399414, + 11.685964584350586, + 11.645100593566895, + 8.599685668945312, + 8.42159366607666, + 11.854164123535156, + 7.80131721496582, + 11.920244216918945, + 11.64996337890625, + 7.159327030181885, + 10.978010177612305, + 10.883095741271973, + 9.500628471374512, + 11.331361770629883, + 7.372076988220215, + 11.9354887008667, + 10.904341697692871, + 11.054499626159668 + ], + "yaxis": "y" + } + ], + "layout": { + "legend": { + "title": { + "text": "color" + }, + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "rgb(36,36,36)" + }, + "error_y": { + "color": "rgb(36,36,36)" + }, + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "rgb(36,36,36)", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "rgb(36,36,36)" + }, + "baxis": { + "endlinecolor": "rgb(36,36,36)", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "rgb(36,36,36)" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.6 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "rgb(237,237,237)" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "rgb(217,217,217)" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "rgb(103,0,31)" + ], + [ + 0.1, + "rgb(178,24,43)" + ], + [ + 0.2, + "rgb(214,96,77)" + ], + [ + 0.3, + "rgb(244,165,130)" + ], + [ + 0.4, + "rgb(253,219,199)" + ], + [ + 0.5, + "rgb(247,247,247)" + ], + [ + 0.6, + "rgb(209,229,240)" + ], + [ + 0.7, + "rgb(146,197,222)" + ], + [ + 0.8, + "rgb(67,147,195)" + ], + [ + 0.9, + "rgb(33,102,172)" + ], + [ + 1, + "rgb(5,48,97)" + ] + ], + "sequential": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "sequentialminus": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ] + }, + "colorway": [ + "#1F77B4", + "#FF7F0E", + "#2CA02C", + "#D62728", + "#9467BD", + "#8C564B", + "#E377C2", + "#7F7F7F", + "#BCBD22", + "#17BECF" + ], + "font": { + "color": "rgb(36,36,36)" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "white", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "white", + "polar": { + "angularaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "bgcolor": "white", + "radialaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "yaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "zaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + } + }, + "shapedefaults": { + "fillcolor": "black", + "line": { + "width": 0 + }, + "opacity": 0.3 + }, + "ternary": { + "aaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "baxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "bgcolor": "white", + "caxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside", + "title": { + "standoff": 15 + }, + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "yaxis": { + "automargin": true, + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside", + "title": { + "standoff": 15 + }, + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "x" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "y" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plotly.express.scatter(x=embedding[:,0],\n", + " y=embedding[:,1],\n", + " template=\"simple_white\",\n", + " hover_name=np.array([\"cell_\" + str(i) for i in range(ot_dmats[0].shape[0])]),\n", + " color = np.array([str(c) for c in cell_metadata['locations']])\n", + " )" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/cajal/subcellular_dl.py b/src/cajal/subcellular_dl.py new file mode 100644 index 0000000..024a0e1 --- /dev/null +++ b/src/cajal/subcellular_dl.py @@ -0,0 +1,1703 @@ +import os +import random +import itertools as it +import numpy as np +import skimage as ski +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +import torchvision.models as models +import torchvision.transforms.functional as TF +import torchvision.transforms.v2 as transforms +from torch.utils.data import Dataset, DataLoader +from tqdm import tqdm +import pickle + +from .subcellular import make_cell_image, to_shape + +def resize_cell_image(image, target_shape): + """ + Resize a 3-channel cell image with appropriate interpolation for each channel. + + Uses bilinear interpolation for probability channels and nearest neighbor + for binary mask channels to preserve the discrete nature of segmentation masks. + + Parameters + ---------- + image : numpy.ndarray + Input image of shape (H, W, 3) where channel 0 is probability/intensity, + channels 1 and 2 are binary masks. + target_shape : tuple of int + Target shape as (height, width) for the resized image. + + Returns + ------- + numpy.ndarray + Resized image of shape (target_shape[0], target_shape[1], 3) with + appropriate interpolation applied to each channel. + """ + out = np.zeros((target_shape[0], target_shape[1], image.shape[2]), dtype=image.dtype) + # Probability channel (bilinear) + out[:,:,0] = ski.transform.resize(image[:,:,0], target_shape, order=1, preserve_range=True, anti_aliasing=True) + # Binary channels (nearest neighbor) + out[:,:,1] = ski.transform.resize(image[:,:,1], target_shape, order=0, preserve_range=True, anti_aliasing=False) + out[:,:,2] = ski.transform.resize(image[:,:,2], target_shape, order=0, preserve_range=True, anti_aliasing=False) + return out + + +def major_axis_pca_with_center(mask, center): + """ + Compute the major axis of a binary mask using PCA with a specified center point. + + Performs principal component analysis on the mask pixels relative to a given + center point to determine the major axis of the shape. The axis orientation + is normalized to point toward the side with more mass distribution. + + Parameters + ---------- + mask : numpy.ndarray + 2D binary mask where non-zero values indicate the region of interest. + center : tuple of float + Center point as (y, x) coordinates to use for PCA computation + (e.g., nucleus centroid). + + Returns + ------- + tuple + major_axis_vector : numpy.ndarray + Unit vector representing the major axis direction as [dy, dx]. + angle : float + Angle in radians relative to the x-axis, with consistent orientation + toward the side with more mass. + """ + yx = np.argwhere(mask > 0) + yx_centered = yx - np.array(center) # center at nucleus + if len(yx_centered) < 2: + return np.array([1, 0]), 0.0 # default axis + cov = np.cov(yx_centered, rowvar=False) + eigvals, eigvecs = np.linalg.eig(cov) + major_axis = eigvecs[:, np.argmax(eigvals)] + # Ensure consistent orientation: major axis points toward side with more mass + projections = yx_centered @ major_axis + if projections.sum() < 0: + major_axis = -major_axis + angle = np.arctan2(major_axis[1], major_axis[0]) # angle w.r.t. x-axis + return major_axis, angle + + +def align_image(image, center='cell', cell_mask_channel=1, nucleus_channel=2): + """ + Center and align a 3-channel cell image based on morphological features. + + Centers the image based on the centroid of the largest labeled object and + rotates it to align the major axis horizontally. The image is padded as + needed and trimmed to remove empty borders. + + Parameters + ---------- + image : numpy.ndarray + Input image of shape (H, W, 3) containing cell imaging data. + center : str, optional + Centering method: 'cell' to center on cell mask, 'nucleus' to center + on nucleus mask. Default is 'cell'. + cell_mask_channel : int, optional + Index of the cell mask channel. Default is 1. + nucleus_channel : int, optional + Index of the nucleus mask channel. Default is 2. + + Returns + ------- + numpy.ndarray + Centered and rotated image, possibly larger than input due to padding + and rotation operations. + """ + # Center based on largest labeled cell/nucleus object + if center == 'cell': + mask = image[..., cell_mask_channel] + elif center == 'nucleus': + mask = image[..., nucleus_channel] + else: + raise ValueError("center must be 'cell' or 'nucleus'") + labeled_objects = ski.measure.label(mask > 0) + object_regions = ski.measure.regionprops(labeled_objects) + if not object_regions: + print(f'No {center}s found, returning original image.') + return image.copy() + largest_object = max(object_regions, key=lambda x: x.area) + cy, cx = largest_object.centroid # (y, x) + h, w = image.shape[:2] + pad_top = int(max(0, (h - cy) - cy)) + pad_bottom = int(max(0, cy - (h - cy))) + pad_left = int(max(0, (w - cx) - cx)) + pad_right = int(max(0, cx - (w - cx))) + padded = np.pad(image, ((pad_top, pad_bottom), (pad_left, pad_right), (0,0)), mode='constant') + # pad image to make it square + if padded.shape[0] != padded.shape[1]: + size = max(padded.shape[:2]) + pad_h = (size - padded.shape[0]) // 2 + pad_w = (size - padded.shape[1]) // 2 + padded = np.pad(padded, ((pad_h, size-padded.shape[0]-pad_h), (pad_w, size-padded.shape[1]-pad_w), (0,0)), mode='constant') + # add extra padding to ensure mask is not cropped after rotation + pad_extra = int(padded.shape[0] / 2 * (np.sqrt(2) - 1)) + padded = np.pad(padded, ((pad_extra, pad_extra), (pad_extra, pad_extra), (0,0)), mode='constant') + # New centroid after padding + new_cy = padded.shape[0]//2 + new_cx = padded.shape[1]//2 + # Rotation based on largest labeled cell object + cell_mask = padded[..., cell_mask_channel] + # Use centroid of largest nucleus for centering, but largest cell for rotation + major_axis, angle = major_axis_pca_with_center(cell_mask, (new_cy, new_cx)) + angle_deg = -np.degrees(angle) + rotated_channels = [] + for c in range(padded.shape[2]): + order = 0 if np.array_equal(np.unique(padded[...,c]), [0,1]) else 1 + rotated = ski.transform.rotate(padded[...,c], angle=angle_deg, center=(new_cy, new_cx), order=order, preserve_range=True) + rotated_channels.append(rotated) + result = np.stack(rotated_channels, axis=-1) + # trim empty borders + mask = result[..., cell_mask_channel] + if np.any(mask > 0): + min_y, min_x = np.array(np.where(mask > 0)).min(axis=1) + max_y, max_x = np.array(np.where(mask > 0)).max(axis=1) + trim_len = np.min([min_y, min_x, result.shape[0]-max_y, result.shape[1]-max_x]) + if trim_len > 0: + result = result[trim_len:result.shape[0]-trim_len, trim_len:result.shape[1]-trim_len, :] + return result + + +def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped_channel_distributions, channel, center='cell', rescale=True, shape=(64, 64)): + """ + Generate training data from GW_OT_Cell objects for neural network training. + + Creates paired cell images and their corresponding mapped versions for training + deep learning models. Images are aligned, normalized, and saved as numpy arrays. + + Parameters + ---------- + save_path : str + Directory path to save processed images. Cell images saved to + '/cell_images' and mapped images to '/mapped_cell_images'. + cell_objects : list + List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. + reference_cell_object : GW_OT_Cell or str + Reference cell object or path to pickled reference cell object used + as template for mapped distributions. + mapped_channel_distributions : numpy.ndarray + Array of mapped protein distributions for each cell. + channel : str + Channel name to use for image processing. + center : str, optional + Centering method for image alignment: 'cell' or 'nucleus'. Default is 'cell'. + rescale : bool, optional + Whether to rescale images to a fixed size. Default is True. + shape : tuple of int, optional + Target shape (height, width) for resizing images. Default is (64, 64). + + Returns + ------- + None + Images are saved to disk as .npy files. + """ + if not rescale: + max_size = 0 + for cell_object in cell_objects: + # Load GW_OT_Cell object if path specified + if isinstance(cell_object, str): + pickle.load(open(cell_object, 'rb')) + cell_image = make_cell_image(cell_object, ['nucleus', channel]) + cell_image = to_shape(cell_image, (max(cell_image.shape[:2]), max(cell_image.shape[:2]), 3)) + cell_image = cell_image[:,:,[1,0,2]] + cell_image = align_image(cell_image, center=center) + max_size = max(max_size, max(cell_image.shape[:2])) + + for i, cell_object in enumerate(cell_objects): + # Load GW_OT_Cell object if path specified + if isinstance(cell_object, str): + pickle.load(open(cell_object, 'rb')) + # make image array from cell object + cell_image = make_cell_image(cell_object, ['nucleus', channel]) + mapped_cell_object = reference_cell_object.copy() + mapped_cell_object.intensities[channel] = mapped_channel_distributions[i] + mapped_cell_image = make_cell_image(mapped_cell_object, ['nucleus', channel]) + # pad image to square + cell_image = to_shape(cell_image, (max(cell_image.shape[:2]), max(cell_image.shape[:2]), 3)) + mapped_cell_image = to_shape(mapped_cell_image, (max(mapped_cell_image.shape[:2]), max(mapped_cell_image.shape[:2]), 3)) + # reorder channels: channel, binary cell mask, binary nucleus mask + cell_image = cell_image[:,:,[1,0,2]] + mapped_cell_image = mapped_cell_image[:,:,[1,0,2]] + # align image + cell_image = align_image(cell_image, center=center) + mapped_cell_image = align_image(mapped_cell_image, center=center) + # resize image + if not rescale: + cell_image = to_shape(cell_image, (max_size, max_size, 3)) + mapped_cell_image = to_shape(mapped_cell_image, (max_size, max_size, 3)) + cell_image = resize_cell_image(cell_image, shape) + mapped_cell_image = resize_cell_image(mapped_cell_image, shape) + # make cell_images & mapped_cell_images directories if they don't exist + if not os.path.exists(os.path.join(save_path, 'cell_images')): + os.makedirs(os.path.join(save_path, 'cell_images')) + if not os.path.exists(os.path.join(save_path, 'mapped_cell_images')): + os.makedirs(os.path.join(save_path, 'mapped_cell_images')) + # save image + np.save(os.path.join(save_path, 'cell_images', f'cell_{i}.npy'), cell_image) + np.save(os.path.join(save_path, 'mapped_cell_images', f'mapped_cell_{i}.npy'), mapped_cell_image) + + +class EfficientNetFeatureExtractor(nn.Module): + """ + Feature extractor using EfficientNet backbone for cell image embeddings. + + Adapts a pretrained EfficientNet model to extract fixed-size feature + embeddings from cell images. Handles variable input channel numbers + and resizes inputs to match EfficientNet requirements. + + Parameters + ---------- + embedding_size : int, optional + Size of the output embedding vector. Default is 50. + input_channels : int, optional + Number of input channels in the cell images. Default is 3. + efficientnet_type : str, optional + Type of EfficientNet architecture to use. Default is 'efficientnet_b0'. + pretrained : bool, optional + Whether to use pretrained ImageNet weights. Default is True. + """ + def __init__(self, embedding_size=50, input_channels=3, efficientnet_type='efficientnet_b0', pretrained=True): + super().__init__() + # Load a pretrained EfficientNet + efficientnet = getattr(models, efficientnet_type)(pretrained=pretrained) + # Determine expected input size from model metadata if available + if hasattr(efficientnet, 'default_cfg') and 'input_size' in efficientnet.default_cfg: + self.efficientnet_input_size = efficientnet.default_cfg['input_size'][-1] + else: + self.efficientnet_input_size = 224 # Fallback for older torchvision + if input_channels != 3: + efficientnet.features[0][0] = nn.Conv2d(input_channels, efficientnet.features[0][0].out_channels, + kernel_size=efficientnet.features[0][0].kernel_size, + stride=efficientnet.features[0][0].stride, + padding=efficientnet.features[0][0].padding, + bias=False) + self.features = efficientnet.features + self.avgpool = efficientnet.avgpool + self.fc = nn.Linear(efficientnet.classifier[1].in_features, embedding_size) + + def forward(self, x): + """ + Forward pass through the feature extractor. + + Parameters + ---------- + x : torch.Tensor + Input tensor of shape (batch_size, channels, height, width). + + Returns + ------- + torch.Tensor + Feature embedding tensor of shape (batch_size, embedding_size). + """ + # Always resize input to expected EfficientNet input size + if x.shape[2] != self.efficientnet_input_size or x.shape[3] != self.efficientnet_input_size: + x = F.interpolate(x, size=(self.efficientnet_input_size, self.efficientnet_input_size), mode='bilinear', align_corners=False) + x = self.features(x) + x = self.avgpool(x) + x = torch.flatten(x, 1) + x = self.fc(x) + return x + + +class UNetDecoder(nn.Module): + """ + U-Net style decoder for reconstructing images from feature embeddings. + + Implements a decoder network that reconstructs images from compressed + feature embeddings using transposed convolutions and skip connections. + The architecture progressively upsamples from a compact representation + back to full image resolution. + + Parameters + ---------- + embedding_size : int, optional + Size of the input embedding vector. Default is 50. + image_size : int, optional + Target output image size (assumed square). Default is 64. + out_channels : int, optional + Number of output channels in the reconstructed image. Default is 1. + """ + def __init__(self, embedding_size=50, image_size=64, out_channels=1): + super().__init__() + self.image_size = image_size + self.fc = nn.Linear(embedding_size, 128 * (image_size // 8) * (image_size // 8)) + # Encoder/decoder blocks + self.up1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2) + self.conv1 = nn.Sequential( + nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(), + nn.Conv2d(64, 64, 3, padding=1), nn.ReLU() + ) + self.up2 = nn.ConvTranspose2d(64, 32, kernel_size=2, stride=2) + self.conv2 = nn.Sequential( + nn.Conv2d(32, 32, 3, padding=1), nn.ReLU(), + nn.Conv2d(32, 32, 3, padding=1), nn.ReLU() + ) + self.up3 = nn.ConvTranspose2d(32, 16, kernel_size=2, stride=2) + self.conv3 = nn.Sequential( + nn.Conv2d(16, 16, 3, padding=1), nn.ReLU(), + nn.Conv2d(16, 16, 3, padding=1), nn.ReLU() + ) + self.final = nn.Conv2d(16, out_channels, 1) + + def forward(self, x): + """ + Forward pass through the U-Net decoder. + + Parameters + ---------- + x : torch.Tensor + Input embedding tensor of shape (batch_size, embedding_size). + + Returns + ------- + torch.Tensor + Reconstructed image tensor of shape (batch_size, out_channels, + image_size, image_size) with softmax-normalized probability + distributions. + """ + # x: (batch, embedding_size) + x = self.fc(x) + x = x.view(x.size(0), 128, self.image_size // 8, self.image_size // 8) + x = self.up1(x) + x = self.conv1(x) + x = self.up2(x) + x = self.conv2(x) + x = self.up3(x) + x = self.conv3(x) + x = self.final(x) + # Output: (batch, out_channels, H, W) + x = torch.softmax(x.view(x.size(0), -1), dim=1).view(x.size(0), 1, self.image_size, self.image_size) + return x + + +class dGWOTNetwork(nn.Module): + """ + Deep Gromov-Wasserstein Optimal Transport Network. + + A complete neural network architecture that combines feature extraction, + distance computation, and image reconstruction in a multi-task learning + framework. Designed for learning embeddings that preserve Gromov-Wasserstein + distances between cell morphologies while enabling reconstruction of + protein distributions. + + Parameters + ---------- + input_channels : int, optional + Number of input image channels. Default is 3. + embedding_size : int, optional + Dimensionality of the feature embedding space. Default is 50. + image_size : int, optional + Size of input/output images (assumed square). Default is 64. + """ + def __init__(self, input_channels=3, embedding_size=50, image_size=64): + super().__init__() + self.feature_extractor = EfficientNetFeatureExtractor( + embedding_size=embedding_size, + input_channels=input_channels, + efficientnet_type='efficientnet_b4', + pretrained=True + ) + # Only decode the first/protein/probability channel + self.feature_decoder = UNetDecoder(embedding_size, image_size) + + def forward(self, x1, x2, return_embedding=False): + """ + Forward pass through the complete dGWOT network. + + Processes two input images through feature extraction, computes their + embedding distance, and reconstructs both images. Optionally returns + the intermediate feature embeddings. + + Parameters + ---------- + x1, x2 : torch.Tensor + Input image tensors of shape (batch_size, channels, height, width). + return_embedding : bool, optional + Whether to return intermediate feature embeddings. Default is False. + + Returns + ------- + tuple + If return_embedding is False: + distance : torch.Tensor + Squared Euclidean distance between embeddings of shape + (batch_size, 1). + uf1, uf2 : torch.Tensor + Reconstructed images of shape (batch_size, 1, height, width). + + If return_embedding is True: + distance : torch.Tensor + Squared Euclidean distance between embeddings. + uf1, uf2 : torch.Tensor + Reconstructed images. + feat1, feat2 : torch.Tensor + Feature embeddings of shape (batch_size, embedding_size). + """ + # Extract features from both images + feat1 = self.feature_extractor(x1) + feat2 = self.feature_extractor(x2) + # Compute Euclidean distance in embedding space + distance = torch.sum((feat1 - feat2) ** 2, dim=1, keepdim=True) + # Reconstruct both images from their embeddings + uf1 = self.feature_decoder(feat1) + uf2 = self.feature_decoder(feat2) + # Return: distance, reconstruction1, reconstruction2, copy1, copy2 + if return_embedding: + return distance, uf1, uf2, feat1, feat2 + else: + return distance, uf1, uf2 + + +class PretrainPairedDataset(Dataset): + """ + PyTorch Dataset for pretraining with paired input and target images. + + Loads pairs of numpy arrays for pretraining tasks where each input image + has a corresponding target image. Handles channel dimension reordering + and applies optional transforms. + + Parameters + ---------- + input_files : list of str + List of file paths to input image numpy arrays. + target_files : list of str + List of file paths to target image numpy arrays. Must have same + length as input_files. + transform : callable, optional + Optional transform to apply to both input and target images. + Default is None. + + Raises + ------ + AssertionError + If input_files and target_files have different lengths. + """ + def __init__(self, input_files, target_files, transform=None): + self.input_files = input_files + self.target_files = target_files + assert len(self.input_files) == len(self.target_files), 'Input and target directories must have the same number of images.' + self.transform = transform + + def __len__(self): + return len(self.input_files) + + def __getitem__(self, idx): + input_img = np.load(self.input_files[idx]) + target_img = np.load(self.target_files[idx]) + # Ensure shape is (C, H, W) for torch + if input_img.ndim == 2: + input_img = input_img[np.newaxis, ...] + elif input_img.ndim == 3 and input_img.shape[0] != 3 and input_img.shape[-1] == 3: + input_img = np.transpose(input_img, (2, 0, 1)) + if target_img.ndim == 2: + target_img = target_img[np.newaxis, ...] + elif target_img.ndim == 3 and target_img.shape[0] != 3 and target_img.shape[-1] == 3: + target_img = np.transpose(target_img, (2, 0, 1)) + if self.transform: + input_img = self.transform(input_img) + target_img = self.transform(target_img) + input_img = torch.from_numpy(input_img).float() + target_img = torch.from_numpy(target_img).float() + return input_img, target_img + + +def pretrain_model(input_files, target_files, model, save_path=None, model_name="pretrained_model", + batch_size=64, epochs=10, lr=1e-3, device=None, return_model=True): + """ + Pretrain a model using paired input and target images. + + Performs pretraining of a neural network model using reconstruction loss + between input images and their corresponding targets. Uses KL divergence + loss for probability distributions. + + Parameters + ---------- + input_files : list of str + List of file paths to input image numpy arrays. + target_files : list of str + List of file paths to target image numpy arrays. + model : torch.nn.Module + Neural network model to pretrain. Must have a forward method that + takes two identical inputs and returns reconstructions. + save_path : str, optional + Directory path to save the pretrained model. If None, model is not saved. + Default is None. + model_name : str, optional + Name prefix for saved model files. Default is "pretrained_model". + batch_size : int, optional + Batch size for training. Default is 64. + epochs : int, optional + Number of training epochs. Default is 10. + lr : float, optional + Learning rate for the Adam optimizer. Default is 1e-3. + device : torch.device, optional + Device to run training on. If None, automatically selects GPU + if available. Default is None. + return_model : bool, optional + Whether to return the trained model. If False, returns None. + Default is True. + + Returns + ------- + torch.nn.Module or None + The pretrained model if return_model is True, otherwise None. + """ + dataset = PretrainPairedDataset(input_files, target_files) + loader = DataLoader(dataset, batch_size=batch_size, shuffle=True) + if device is None: + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + model = model.to(device) + optimizer = optim.Adam(model.parameters(), lr=lr) + + # Store config for saving (if it's a dGWOTNetwork) + config = None + if hasattr(model, 'feature_extractor') and hasattr(model, 'feature_decoder'): + # Try to extract config from dGWOTNetwork + try: + input_channels = model.feature_extractor.feature_extractor.features[0][0].in_channels if hasattr(model.feature_extractor, 'feature_extractor') else 3 + embedding_size = model.feature_extractor.fc.out_features + image_size = model.feature_decoder.image_size + config = { + 'input_channels': input_channels, + 'embedding_size': embedding_size, + 'image_size': image_size + } + except: + print("Warning: Could not extract model config for saving") + + # Create save directory if specified + if save_path is not None: + os.makedirs(save_path, exist_ok=True) + + model.train() + best_loss = float('inf') + + for epoch in range(epochs): + total_loss = 0 + progress_bar = tqdm(loader, desc=f"Epoch {epoch+1}/{epochs}") + for input_batch, target_batch in progress_bar: + input_batch = input_batch.float() + target_batch = target_batch.float() + if input_batch.ndim == 4 and input_batch.shape[-1] == 3: + input_batch = input_batch.permute(0, 3, 1, 2) + if target_batch.ndim == 4 and target_batch.shape[-1] == 3: + target_batch = target_batch.permute(0, 3, 1, 2) + input_batch = input_batch.to(device) + target_batch = target_batch.to(device) + optimizer.zero_grad() + _, recon, _ = model(input_batch, input_batch) + # Only reconstruct the first channel (probability/protein) of the target + target = target_batch[:, 0:1, :, :] + pred = recon[:, 0:1, :, :] + loss = kullback_leibler_divergence_loss(target, pred) + loss.backward() + optimizer.step() + total_loss += loss.item() * input_batch.size(0) + progress_bar.set_postfix({'loss': loss.item()}) + + epoch_loss = total_loss / len(dataset) + print(f"Epoch {epoch+1}/{epochs}, Loss: {epoch_loss:.6f}") + + # Save best model if save_path is provided + if save_path is not None and epoch_loss < best_loss: + best_loss = epoch_loss + if config is not None: + # Save with config + torch.save({ + 'state_dict': model.state_dict(), + 'config': config, + 'epoch': epoch + 1, + 'loss': epoch_loss + }, os.path.join(save_path, f'{model_name}_best.pth')) + else: + # Save without config (backward compatibility) + torch.save(model.state_dict(), os.path.join(save_path, f'{model_name}_best.pth')) + print(f' → New best model saved (loss: {epoch_loss:.6f})') + + # Save final model if save_path is provided + if save_path is not None: + if config is not None: + torch.save({ + 'state_dict': model.state_dict(), + 'config': config, + 'epoch': epochs, + 'loss': epoch_loss + }, os.path.join(save_path, f'{model_name}_final.pth')) + else: + torch.save(model.state_dict(), os.path.join(save_path, f'{model_name}_final.pth')) + print(f'Saved pretrained model to {save_path}/{model_name}_final.pth') + + return model if return_model else None + + +def kullback_leibler_divergence_loss(y_true, y_pred): + """ + Compute Kullback-Leibler divergence loss for probability distributions. + + Measures how well a predicted probability distribution matches a target + distribution. Used for reconstruction quality assessment when dealing + with normalized protein distributions. + + Parameters + ---------- + y_true : torch.Tensor + Target probability distribution tensor. + y_pred : torch.Tensor + Predicted probability distribution tensor. + + Returns + ------- + torch.Tensor + Mean KL divergence loss across the batch. + + Notes + ----- + The KL divergence is computed as: KL(P||Q) = sum(P * log(P/Q)) + Values are clamped to avoid log(0) numerical issues. + """ + epsilon = 1e-8 + + # Clamp values to avoid log(0) + y_true = torch.clamp(y_true, epsilon, 1.0) + y_pred = torch.clamp(y_pred, epsilon, 1.0) + + # Flatten tensors for computation + y_true_flat = y_true.view(y_true.size(0), -1) + y_pred_flat = y_pred.view(y_pred.size(0), -1) + + # Compute KL divergence: KL(P||Q) = sum(P * log(P/Q)) + kl_div = torch.sum(y_true_flat * torch.log(y_true_flat / y_pred_flat), dim=1) + return torch.mean(kl_div) + + +def sparsity_constraint_loss(embeddings, sparsity_target=0.1): + """ + Apply KL divergence sparsity constraint to hidden unit activations. + + Encourages sparse representations by penalizing deviations from a target + sparsity level. This regularization helps prevent overfitting and promotes + more interpretable feature representations. + + Parameters + ---------- + embeddings : torch.Tensor + Hidden unit activations of shape (batch_size, embedding_dim). + sparsity_target : float, optional + Desired average activation level for each hidden unit. Default is 0.1. + + Returns + ------- + torch.Tensor + Scalar sparsity loss computed as the sum of KL divergences across + all embedding dimensions. + + Notes + ----- + Activations are passed through sigmoid to ensure they're in (0,1) range + before computing the sparsity constraint. + """ + epsilon = 1e-8 + # Apply sigmoid to ensure activations are in (0,1) + activations = torch.sigmoid(embeddings) + rho_hat = torch.mean(activations, dim=0) # (embedding_dim,) + rho = torch.full_like(rho_hat, sparsity_target) + kl = rho * torch.log((rho + epsilon) / (rho_hat + epsilon)) + \ + (1 - rho) * torch.log((1 - rho + epsilon) / (1 - rho_hat + epsilon)) + return torch.sum(kl) + + +def reconstruction_loss(x, uf): + """ + Compute multi-channel reconstruction loss for cell images. + + Applies appropriate loss functions for different channel types: + probability distributions use KL divergence, while binary masks + use binary cross-entropy loss. + + Parameters + ---------- + x : torch.Tensor + Original image tensor of shape (batch_size, 3, height, width). + uf : torch.Tensor + Reconstructed image tensor of same shape as x. + + Returns + ------- + torch.Tensor + Combined reconstruction loss across all channels. + + Notes + ----- + - Channel 0: Probability distribution (KL divergence loss) + - Channel 1: Binary cell mask (Binary cross-entropy loss) + - Channel 2: Binary nucleus mask (Binary cross-entropy loss) + """ + # Split channels + x_prob, x_mask1, x_mask2 = x[:, 0:1, :, :], x[:, 1:2, :, :], x[:, 2:3, :, :] + uf_prob, uf_mask1, uf_mask2 = uf[:, 0:1, :, :], uf[:, 1:2, :, :], uf[:, 2:3, :, :] + + # Probability channel: KL divergence + kl = kullback_leibler_divergence_loss(x_prob, uf_prob) + # Binary mask channels: BCE loss + bce1 = F.binary_cross_entropy(uf_mask1, x_mask1) + bce2 = F.binary_cross_entropy(uf_mask2, x_mask2) + return kl + bce1 + bce2 + + +def get_random_pairs(indices, n_pairs): + """ + Generate a random subset of unique pairs from a list of indices. + + Creates all possible unique pairs from the input indices and randomly + samples a specified number of them. Useful for creating training pairs + from a dataset without exhaustive pairwise combinations. + + Parameters + ---------- + indices : array-like + List or array of indices to create pairs from. + n_pairs : int + Number of pairs to randomly sample. If larger than the total + possible pairs, returns all possible pairs. + + Returns + ------- + numpy.ndarray + Array of shape (n_pairs, 2) containing randomly selected index pairs. + """ + all_pairs = np.array(list(it.combinations(indices, 2))) + if n_pairs > len(all_pairs): + n_pairs = len(all_pairs) + pair_inds = np.random.choice(len(all_pairs), n_pairs, replace=False) + return all_pairs[pair_inds] + + +class IndexedImageDataset(Dataset): + """ + PyTorch Dataset for loading individual cell images by index. + + Simple dataset for loading cell images by index, useful for extracting + embeddings from unique images in a PairedDataset without loading duplicates. + + Parameters + ---------- + image_dir : str + Path to directory containing cell image .npy files with naming + convention 'cell_{index}.npy'. + indices : list of int + List of cell indices to load. + transform : callable, optional + Transform function to apply to all images. Default is None. + """ + def __init__(self, image_dir, indices, transform=None): + self.image_dir = image_dir + self.indices = indices + self.transform = transform + + def __len__(self): + return len(self.indices) + + def __getitem__(self, idx): + index = self.indices[idx] + img_path = os.path.join(self.image_dir, f"cell_{index}.npy") + image = np.load(img_path) + + if self.transform is not None: + image = self.transform(image) + + return image + + +class PairedDataset(Dataset): + """ + PyTorch Dataset for loading paired cell images with distance labels. + + Loads pairs of cell images and their mapped counterparts from numpy files + for training distance-based models. Supports data augmentation and lazy + loading for memory efficiency. + + Parameters + ---------- + image_dir : str + Path to directory containing cell image .npy files with naming + convention 'cell_{index}.npy'. + mapped_image_dir : str + Path to directory containing mapped cell image .npy files with naming + convention 'mapped_cell_{index}.npy'. + distances : list of float + Distance values corresponding to each image pair for supervised learning. + image_pairs : list of tuple + List of (index1, index2) tuples specifying which images to pair. + transform : callable, optional + Transform function to apply to all images. Default is None. + augment_transform : callable, optional + Additional augmentation transform for data augmentation. Default is None. + n_augment : int, optional + Number of augmented copies to create for each pair. Default is 1. + + Notes + ----- + The dataset expects file naming conventions: + - Cell images: 'cell_{index}.npy' + - Mapped images: 'mapped_cell_{index}.npy' + """ + def __init__(self, image_dir, mapped_image_dir, distances, image_pairs, transform=None, augment_transform=None, n_augment=1): + # Store directory paths. Listing all files is no longer needed. + self.image_dir = image_dir + self.mapped_image_dir = mapped_image_dir + + # The rest of the logic remains the same + self.distances = [distance for distance in distances for _ in range(n_augment)] + self.image_pairs = [image_pair for image_pair in image_pairs for _ in range(n_augment)] + self.transform = transform + self.augment_transform = augment_transform + + def __getitem__(self, index): + # Get the integer indices for the pair + ind_1, ind_2 = self.image_pairs[index] + + # Dynamically construct the filenames using the indices + img_path_1 = os.path.join(self.image_dir, f"cell_{ind_1}.npy") + img_path_2 = os.path.join(self.image_dir, f"cell_{ind_2}.npy") + mapped_img_path_1 = os.path.join(self.mapped_image_dir, f"mapped_cell_{ind_1}.npy") + mapped_img_path_2 = os.path.join(self.mapped_image_dir, f"mapped_cell_{ind_2}.npy") + + # Load the NumPy arrays from the .npy files + image_1 = np.load(img_path_1) + image_2 = np.load(img_path_2) + mapped_image_cell_1 = np.load(mapped_img_path_1) + mapped_image_cell_2 = np.load(mapped_img_path_2) + + # Apply the general transform if it exists + if self.transform is not None: + image_1 = self.transform(image_1) + image_2 = self.transform(image_2) + mapped_image_cell_1 = self.transform(mapped_image_cell_1) + mapped_image_cell_2 = self.transform(mapped_image_cell_2) + + # Apply the augmentation transform if it exists + if self.augment_transform is not None: + image_1 = self.augment_transform(image_1) + image_2 = self.augment_transform(image_2) + + distance = self.distances[index] + + return image_1, image_2, mapped_image_cell_1, mapped_image_cell_2, distance + + def __len__(self): + # The length is determined by the number of pairs, which is correct + return len(self.image_pairs) + + +class RandomHorizontalRescale(object): + """ + Data augmentation transform that randomly rescales image width. + + Randomly rescales the horizontal axis of cell images to achieve uniform + distribution of cell mask widths. Uses appropriate interpolation methods + for different channel types (bilinear for intensity, nearest for masks). + + Parameters + ---------- + min_relative_width : float, optional + Minimum relative width of the cell mask as fraction of image width. + Default is 0.1. + max_relative_width : float, optional + Maximum relative width of the cell mask as fraction of image width. + Default is 1.0. + + Notes + ----- + - Channel 0: Resized with bilinear interpolation (intensity/probability) + - Other channels: Resized with nearest neighbor interpolation (binary masks) + The transform maintains the original image width by padding or cropping after rescaling. + """ + def __init__(self, min_relative_width=0.1, max_relative_width=1.0): + assert 0 < min_relative_width <= max_relative_width <= 1.0 + self.min_relative_width = min_relative_width + self.max_relative_width = max_relative_width + + def __call__(self, image): + c, h, w = image.shape + mask = image[1] # channel 1 is the segmentation mask + mask_inds = (mask > 0).nonzero(as_tuple=False) + min_x = mask_inds[:, 1].min().item() + max_x = mask_inds[:, 1].max().item() + mask_width = max_x - min_x + 1 + + # Compute allowed min/max mask widths in pixels + min_mask_width = int(self.min_relative_width * w) + max_mask_width = int(self.max_relative_width * w) + min_mask_width = max(1, min_mask_width) + max_mask_width = max(min_mask_width, max_mask_width) + + # Sample target mask width + target_mask_width = random.randint(min_mask_width, max_mask_width) + scale = target_mask_width / mask_width + + # Compute new width for the whole image + new_w = int(round(w * scale)) + new_w = max(1, new_w) + + # Resize each channel separately, with bilinear for channel 0, nearest for others + resized = [] + for i in range(c): + channel = image[i].unsqueeze(0) + if i == 0: + resized_channel = TF.resize(channel, [h, new_w], interpolation=TF.InterpolationMode.BILINEAR) + else: + resized_channel = TF.resize(channel, [h, new_w], interpolation=TF.InterpolationMode.NEAREST) + resized.append(resized_channel.squeeze(0)) + image_rescaled = torch.stack(resized, dim=0) + + # Pad or crop to original width + if new_w < w: + pad = (w - new_w) // 2 + image_rescaled = TF.pad(image_rescaled, (pad, 0, w - new_w - pad, 0)) + elif new_w > w: + crop = (new_w - w) // 2 + image_rescaled = image_rescaled[:, :, crop:crop + w] + return image_rescaled + + +def train_dGWOT(train_dataset, valid_dataset, test_dataset, save_path, dataset_name, embedding_size=50, + image_shape=(64,64), batch_size=100, epochs=100, + device=None, learning_rate=0.001, dist_weight=1.0, + early_stopping=True, patience=3, weight_decay=1e-5, + lr_gamma=0.95, sparsity_weight=0.0, sparsity_target=0.05, + pretrained_path=None, show_loss_components=False): + """ + Train the Deep Gromov-Wasserstein Optimal Transport model. + + Trains a dGWOT network using multi-task learning with distance prediction + and image reconstruction objectives. Supports early stopping, learning rate + scheduling, and optional sparsity constraints. + + Parameters + ---------- + train_dataset, valid_dataset, test_dataset : Dataset + PyTorch datasets for training, validation, and testing. + save_path : str + Directory path to save the trained model and checkpoints. + dataset_name : str + Name prefix for saved model files. + embedding_size : int, optional + Dimensionality of the feature embedding space. Default is 50. + image_shape : tuple of int, optional + Shape of input images as (height, width). Default is (64, 64). + batch_size : int, optional + Batch size for training. Default is 100. + epochs : int, optional + Maximum number of training epochs. Default is 100. + device : torch.device, optional + Device for training. If None, automatically selects GPU if available. + learning_rate : float, optional + Initial learning rate for Adam optimizer. Default is 0.001. + dist_weight : float, optional + Weight for distance loss vs reconstruction loss in total loss. Default is 1.0. + early_stopping : bool, optional + Whether to use early stopping based on validation loss. Default is True. + patience : int, optional + Number of epochs to wait for improvement before stopping. Default is 3. + weight_decay : float, optional + L2 regularization weight for optimizer. Default is 1e-5. + lr_gamma : float, optional + Decay factor for exponential learning rate scheduler. Default is 0.95. + sparsity_weight : float, optional + Weight for sparsity constraint loss. Default is 0.0 (disabled). + sparsity_target : float, optional + Target sparsity level for hidden activations. Default is 0.05. + pretrained_path : str, optional + Path to pretrained model weights to initialize from. Default is None. + show_loss_components : bool, optional + Whether to display individual loss components (distance, reconstruction, + sparsity) during training. Default is False. + + Returns + ------- + tuple + model : torch.nn.Module + Trained dGWOT model. + train_losses : list of float + Training loss history. + val_losses : list of float + Validation loss history. + """ + # Setup device + if device is None: + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"Training on device: {device}") + + train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) + valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False) + test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) + + input_channels = 3 + image_size = image_shape[0] + model = dGWOTNetwork(input_channels, embedding_size, image_size).to(device) + + # Store config for saving + config = { + 'input_channels': input_channels, + 'embedding_size': embedding_size, + 'image_size': image_size + } + + # Load pretrained weights if provided + if pretrained_path is not None and os.path.exists(pretrained_path): + print(f"Loading pretrained weights from {pretrained_path}") + checkpoint = torch.load(pretrained_path, map_location=device) + if isinstance(checkpoint, dict) and 'state_dict' in checkpoint: + model.load_state_dict(checkpoint['state_dict'], strict=False) + else: + model.load_state_dict(checkpoint, strict=False) + + # Training setup + optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=weight_decay) + scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=lr_gamma) + best_val_loss = float('inf') + patience_counter = 0 + + # Create model directory + if not os.path.exists(save_path): + os.makedirs(save_path) + + print(f"Starting training for {epochs} epochs...") + + train_losses = [] + val_losses = [] + # Training loop + for epoch in range(epochs): + # Training phase + model.train() + train_loss = 0.0 + train_samples = 0 + train_iter = tqdm(enumerate(train_loader), total=len(train_loader), desc=f"Epoch {epoch+1}/{epochs}", leave=False) + for batch_idx, (x1, x2, mapped_x1, mapped_x2, target_dist) in train_iter: + x1, x2, mapped_x1, mapped_x2, target_dist = x1.to(device), x2.to(device), mapped_x1.to(device), mapped_x2.to(device), target_dist.to(device) + x1 = torch.clamp(x1, 0.0, 1.0) + x2 = torch.clamp(x2, 0.0, 1.0) + mapped_x1 = torch.clamp(mapped_x1, 0.0, 1.0) + mapped_x2 = torch.clamp(mapped_x2, 0.0, 1.0) + optimizer.zero_grad() + # Forward pass (with embeddings) + distance, uf1, uf2, emb1, emb2 = model(x1, x2, return_embedding=True) + dist_loss = F.mse_loss(distance.squeeze(), target_dist) + mapped_x1_prob = mapped_x1[:, 0:1, :, :] + mapped_x2_prob = mapped_x2[:, 0:1, :, :] + kl1 = kullback_leibler_divergence_loss(mapped_x1_prob, uf1) + kl2 = kullback_leibler_divergence_loss(mapped_x2_prob, uf2) + recon_loss = kl1 + kl2 + # KL sparsity constraint + sparsity_loss = sparsity_constraint_loss(emb1, sparsity_target) + sparsity_constraint_loss(emb2, sparsity_target) + total_loss = dist_weight * dist_loss + recon_loss + sparsity_weight * sparsity_loss + total_loss.backward() + optimizer.step() + batch_size_actual = x1.size(0) + train_loss += total_loss.item() * batch_size_actual + train_samples += batch_size_actual + # Validation phase + model.eval() + val_loss = 0.0 + val_samples = 0 + with torch.no_grad(): + for x1, x2, mapped_x1, mapped_x2, target_dist in valid_loader: + x1, x2, mapped_x1, mapped_x2, target_dist = x1.to(device), x2.to(device), mapped_x1.to(device), mapped_x2.to(device), target_dist.to(device) + distance, uf1, uf2 = model(x1, x2) + dist_loss = F.mse_loss(distance.squeeze(), target_dist) + mapped_x1_prob = mapped_x1[:, 0:1, :, :] + mapped_x2_prob = mapped_x2[:, 0:1, :, :] + kl1 = kullback_leibler_divergence_loss(mapped_x1_prob, uf1) + kl2 = kullback_leibler_divergence_loss(mapped_x2_prob, uf2) + recon_loss = kl1 + kl2 + total_loss = dist_weight * dist_loss + recon_loss + batch_size_actual = x1.size(0) + val_loss += total_loss.item() * batch_size_actual + val_samples += batch_size_actual + # Step the learning rate scheduler + scheduler.step() + # Calculate average losses per pair + train_loss /= train_samples + val_loss /= val_samples + train_losses.append(train_loss) + val_losses.append(val_loss) + print(f'Epoch {epoch+1:3d}/{epochs}: Train Loss: {train_loss:.6f}, ' + f'Val Loss: {val_loss:.6f}') + + if show_loss_components: + # check ranges of losses (FOR TESTING) + print(f"Distance Loss: {dist_loss.item()}, Reconstruction Loss: {recon_loss.item()}, Sparsity Loss: {sparsity_loss.item()}") + + # Early stopping and model saving + if val_loss < best_val_loss: + best_val_loss = val_loss + patience_counter = 0 + + # Save best model with config + torch.save({ + 'state_dict': model.state_dict(), + 'config': config + }, f'{save_path}/{dataset_name}_best.pth') + print(f' → New best model saved (val_loss: {val_loss:.6f})') + else: + patience_counter += 1 + if early_stopping and patience_counter >= patience: + print(f'Early stopping at epoch {epoch+1} (patience: {patience})') + break + + # Final test evaluation + print("\nEvaluating on test set...") + model.eval() + test_loss = 0.0 + test_samples = 0 + + with torch.no_grad(): + for x1, x2, mapped_x1, mapped_x2, target_dist in test_loader: + x1, x2, mapped_x1, mapped_x2, target_dist = x1.to(device), x2.to(device), mapped_x1.to(device), mapped_x2.to(device), target_dist.to(device) + distance, uf1, uf2 = model(x1, x2) + dist_loss = F.mse_loss(distance, target_dist) + mapped_x1_prob = mapped_x1[:, 0:1, :, :] + mapped_x2_prob = mapped_x2[:, 0:1, :, :] + kl1 = kullback_leibler_divergence_loss(mapped_x1_prob, uf1) + kl2 = kullback_leibler_divergence_loss(mapped_x2_prob, uf2) + recon_loss = kl1 + kl2 + total_loss = dist_weight * dist_loss + recon_loss + batch_size_actual = x1.size(0) + test_loss += total_loss.item() * batch_size_actual + test_samples += batch_size_actual + + test_loss /= test_samples + print(f'Final Test Loss: {test_loss:.6f}') + + # Save final model with config + print("\nSaving model...") + torch.save({ + 'state_dict': model.state_dict(), + 'config': config + }, f'{save_path}/{dataset_name}_final.pth') + print(f'Saved DWE model to {save_path}/{dataset_name}_final.pth') + + return model, train_losses, val_losses + + +def load_dGWOT_model(checkpoint_path, device=None): + """ + Load a dGWOT model from a checkpoint containing state dict and config. + + Parameters + ---------- + checkpoint_path : str + Path to the checkpoint file containing both state_dict and config. + device : torch.device, optional + Device to load the model on. If None, uses GPU if available. + + Returns + ------- + torch.nn.Module + Loaded dGWOT model ready for inference or further training. + """ + if device is None: + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + # Load checkpoint + checkpoint = torch.load(checkpoint_path, map_location=device) + + # Handle different checkpoint formats + if isinstance(checkpoint, dict): + if 'config' in checkpoint and 'state_dict' in checkpoint: + # New format with config + config = checkpoint['config'] + state_dict = checkpoint['state_dict'] + + # Create model with saved config + model = dGWOTNetwork(**config) + model.load_state_dict(state_dict) + + print(f"Loaded dGWOT model from {checkpoint_path}") + print(f"Config: {config}") + + elif 'state_dict' in checkpoint: + # Old format with just state_dict + print("Warning: Loading checkpoint without config. You'll need to specify model parameters manually.") + return checkpoint['state_dict'] + else: + # Assume checkpoint is a bare state_dict + print("Warning: Loading bare state_dict. You'll need to specify model parameters manually.") + return checkpoint + else: + print("Warning: Unknown checkpoint format.") + return checkpoint + + # Move to device + model = model.to(device) + + return model + + +def extract_embeddings(model, data, batch_size=64, device=None): + """ + Extract latent embeddings from a trained dGWOT model. + + Processes input images through the feature extractor to obtain latent + embeddings. Supports lists/arrays of images, PyTorch datasets, and PairedDatasets. + + Parameters + ---------- + model : torch.nn.Module + Trained dGWOT model with a feature_extractor attribute. + data : list, numpy.ndarray, torch.utils.data.Dataset, or PairedDataset + Input data to extract embeddings from. Can be: + - List of numpy arrays with shape (H, W, C) + - Single numpy array with shape (N, H, W, C) + - PyTorch Dataset where __getitem__ returns images + - PairedDataset (extracts embeddings for unique images only) + batch_size : int, optional + Batch size for processing. Default is 64. + device : torch.device, optional + Device to run computation on. If None, uses model's current device. + + Returns + ------- + numpy.ndarray + Extracted embeddings of shape (N, embedding_size) where N is the + number of input images. + """ + if device is None: + device = next(model.parameters()).device + + model.eval() + embeddings = [] + + # Case 1: PairedDataset - extract embeddings for unique images only + if isinstance(data, PairedDataset): + # Get all unique image indices from the dataset pairs + all_indices = set() + for pair in data.image_pairs: + all_indices.update(pair) + all_indices = sorted(list(all_indices)) + + print(f"Extracting embeddings for {len(all_indices)} unique images from PairedDataset...") + + # Create dataset for unique images + unique_image_dataset = IndexedImageDataset( + data.image_dir, + all_indices, + transform=data.transform + ) + + # Process the unique image dataset + loader = DataLoader(unique_image_dataset, batch_size=batch_size, shuffle=False) + with torch.no_grad(): + for batch in tqdm(loader, desc="Extracting embeddings"): + images = batch + images = images.to(device) + if images.dtype != torch.float32: + images = images.float() + + # Extract features using the model's feature extractor + feats = model.feature_extractor(images) + embeddings.append(feats.cpu().numpy()) + + return np.concatenate(embeddings, axis=0) + + # Case 2: General PyTorch Dataset + elif hasattr(data, '__getitem__') and hasattr(data, '__len__') and isinstance(data, Dataset): + loader = DataLoader(data, batch_size=batch_size, shuffle=False) + with torch.no_grad(): + for batch in tqdm(loader, desc="Extracting embeddings"): + # Handle different dataset return formats + if isinstance(batch, (list, tuple)): + # If dataset returns multiple items, take the first (assumed to be images) + images = batch[0] + else: + images = batch + + images = images.to(device) + if images.dtype != torch.float32: + images = images.float() + + # Extract features using the model's feature extractor + feats = model.feature_extractor(images) + embeddings.append(feats.cpu().numpy()) + + return np.concatenate(embeddings, axis=0) + + # Case 2: List or numpy array of images + # Convert list to numpy array if needed + if isinstance(data, list): + data = np.stack(data, axis=0) + + # Ensure data is numpy array with shape (N, H, W, C) + if data.ndim == 3: + data = data[np.newaxis, ...] # Add batch dimension + + n_images = data.shape[0] + + # Process in batches + with torch.no_grad(): + for i in tqdm(range(0, n_images, batch_size), desc="Extracting embeddings"): + batch_end = min(i + batch_size, n_images) + batch_images = data[i:batch_end] + + # Convert to torch tensor and reorder dimensions (N, H, W, C) -> (N, C, H, W) + if batch_images.shape[-1] in [1, 3]: # Channels last + batch_tensor = torch.from_numpy(batch_images).permute(0, 3, 1, 2).float() + else: # Assume channels first already + batch_tensor = torch.from_numpy(batch_images).float() + + batch_tensor = batch_tensor.to(device) + + # Extract features + feats = model.feature_extractor(batch_tensor) + embeddings.append(feats.cpu().numpy()) + + return np.concatenate(embeddings, axis=0) + + +def predict_distances(model, paired_dataset, batch_size=64, device=None): + """ + Predict distances in latent space for a PairedDataset. + + Extracts embeddings for all unique images in the dataset and computes + pairwise Euclidean distances in the embedding space. This provides + predictions that can be compared against ground truth distances. + + Parameters + ---------- + model : torch.nn.Module + Trained dGWOT model with a feature_extractor attribute. + paired_dataset : PairedDataset + Dataset containing paired images with known distances. + batch_size : int, optional + Batch size for processing embeddings. Default is 64. + device : torch.device, optional + Device to run computation on. If None, uses model's current device. + + Returns + ------- + numpy.ndarray + Array of predicted distances of shape (len(paired_dataset),) + corresponding to each pair in the dataset. + """ + if device is None: + device = next(model.parameters()).device + + # Get all unique image indices from the dataset + all_indices = set() + for pair in paired_dataset.image_pairs: + all_indices.update(pair) + all_indices = sorted(list(all_indices)) + + print(f"Extracting embeddings for {len(all_indices)} unique images...") + + # Create dataset for unique images + unique_image_dataset = IndexedImageDataset( + paired_dataset.image_dir, + all_indices, + transform=paired_dataset.transform + ) + + # Extract embeddings for all unique images + embeddings = extract_embeddings(model, unique_image_dataset, batch_size=batch_size, device=device) + + # Create mapping from image index to embedding index + index_to_embedding = {img_idx: emb_idx for emb_idx, img_idx in enumerate(all_indices)} + + # Compute distances for each pair in the dataset + predicted_distances = [] + + print(f"Computing distances for {len(paired_dataset.image_pairs)} pairs...") + + for pair in tqdm(paired_dataset.image_pairs, desc="Computing pairwise distances"): + idx1, idx2 = pair + + # Get embedding indices + emb_idx1 = index_to_embedding[idx1] + emb_idx2 = index_to_embedding[idx2] + + # Get embeddings + emb1 = embeddings[emb_idx1] + emb2 = embeddings[emb_idx2] + + # Compute squared Euclidean distance (matching model's training objective) + distance = np.sum((emb1 - emb2) ** 2) + predicted_distances.append(distance) + + return np.array(predicted_distances) + + +def plot_distance_predictions(model, paired_dataset, batch_size=64, device=None, figsize=(8, 8), + return_plot=False, title=None, alpha=0.6, s=20): + """ + Plot predicted vs true distances for a PairedDataset. + + Creates a scatter plot comparing model predictions against ground truth + distances with a diagonal reference line and correlation metrics. + + Parameters + ---------- + model : torch.nn.Module + Trained dGWOT model with a feature_extractor attribute. + paired_dataset : PairedDataset + Dataset containing paired images with known distances. + batch_size : int, optional + Batch size for processing embeddings. Default is 64. + device : torch.device, optional + Device to run computation on. If None, uses model's current device. + figsize : tuple, optional + Figure size as (width, height). Default is (8, 8). + return_plot : bool, optional + Whether to return the matplotlib figure and axes objects. Default is False. + title : str, optional + Custom title for the plot. If None, uses default with correlation metrics. + alpha : float, optional + Transparency of scatter points. Default is 0.6. + s : int, optional + Size of scatter points. Default is 20. + + Returns + ------- + None or tuple + If return_plot is False: displays the plot and returns None. + If return_plot is True: returns (fig, ax) matplotlib objects. + """ + # Get predictions + predicted_distances = predict_distances(model, paired_dataset, batch_size=batch_size, device=device) + true_distances = np.array(paired_dataset.distances) + + try: + import matplotlib.pyplot as plt + import seaborn as sns + + # Create the plot + fig, ax = plt.subplots(figsize=figsize) + + # Scatter plot + sns.scatterplot(x=true_distances, y=predicted_distances, alpha=alpha, s=s, ax=ax) + + # Add diagonal reference line + min_val = min(true_distances.min(), predicted_distances.min()) + max_val = max(true_distances.max(), predicted_distances.max()) + ax.plot([min_val, max_val], [min_val, max_val], 'r--', alpha=0.8, linewidth=2, label='Perfect prediction') + + # Labels and title + ax.set_xlabel('True Distances') + ax.set_ylabel('Predicted Distances') + + if title: + ax.set_title(title) + + # Grid and legend + ax.grid(True, alpha=0.3) + ax.legend() + + if return_plot: + return fig, ax + else: + plt.show() + return None + + except ImportError: + print("Matplotlib/Seaborn not available for plotting") + return None if not return_plot else (None, None) + + +def plot_reconstruction_comparison(model, paired_dataset, num_images=5, device=None, figsize=None, seed=None): + """ + Plot comparison of original mapped protein images vs model reconstructions. + + Randomly selects different individual images from the dataset, shows the original mapped + protein images in the top row and their reconstructions from the model in + the bottom row. + + Parameters + ---------- + model : torch.nn.Module + Trained dGWOT model with reconstruction capabilities. + paired_dataset : PairedDataset + Dataset containing paired images for reconstruction. + num_images : int, optional + Number of image pairs to display. Default is 5. + device : torch.device, optional + Device to run model on. If None, uses model's current device. + figsize : tuple, optional + Figure size as (width, height). If None, automatically calculated + based on number of images. + seed : int, optional + Random seed for reproducible image selection. Default is None. + + Returns + ------- + None + Displays the plot using matplotlib. + """ + if device is None: + device = next(model.parameters()).device + + # Set random seed if provided + if seed is not None: + np.random.seed(seed) + + # Get unique image indices from the dataset pairs + all_image_indices = set() + for pair in paired_dataset.image_pairs: + all_image_indices.update(pair) + all_image_indices = list(all_image_indices) + + # Randomly select unique image indices + selected_image_indices = np.random.choice(all_image_indices, size=min(num_images, len(all_image_indices)), replace=False) + + # Set up the plot + if figsize is None: + figsize = (3 * num_images, 6) + + try: + import matplotlib.pyplot as plt + + fig, axes = plt.subplots(2, num_images, figsize=figsize) + if num_images == 1: + axes = axes.reshape(2, 1) + + model.eval() + + with torch.no_grad(): + for i, img_idx in enumerate(selected_image_indices): + # Load the specific image directly by constructing the path + img_path = os.path.join(paired_dataset.image_dir, f"cell_{img_idx}.npy") + mapped_img_path = os.path.join(paired_dataset.mapped_image_dir, f"mapped_cell_{img_idx}.npy") + + # Load the numpy arrays + image = np.load(img_path) + mapped_image = np.load(mapped_img_path) + + # Apply transforms if they exist + if paired_dataset.transform is not None: + image = paired_dataset.transform(image) + mapped_image = paired_dataset.transform(mapped_image) + + # Convert to tensor and add batch dimension + if isinstance(image, np.ndarray): + # Convert from (H, W, C) to (1, C, H, W) + image_tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0).float().to(device) + else: + # Already a tensor, just add batch dimension and ensure correct device + image_tensor = image.unsqueeze(0).to(device) + + # Get reconstruction from model + _, reconstruction, _ = model(image_tensor, image_tensor) + + # Extract protein channel (channel 0) + if isinstance(mapped_image, np.ndarray): + original_protein = mapped_image[:, :, 0] # Protein channel + else: + original_protein = mapped_image[0].cpu().numpy() # Protein channel + + reconstructed_protein = reconstruction[0, 0].cpu().numpy() # First batch, first (protein) channel + + # Plot original protein (top row) + im1 = axes[0, i].imshow(original_protein, cmap='viridis') + axes[0, i].set_title(f'Original Cell {img_idx}') + axes[0, i].axis('off') + + # Plot reconstructed protein (bottom row) + im2 = axes[1, i].imshow(reconstructed_protein, cmap='viridis') + axes[1, i].set_title(f'Reconstructed Cell {img_idx}') + axes[1, i].axis('off') + + plt.tight_layout() + plt.show() + + except ImportError: + print("Matplotlib not available for plotting") + return + + except Exception as e: + print(f"Error creating plot: {e}") + return + + +def generate_dataset_split_pairs(indices, n_pairs, proportions=None, seed=None): + """ + Generate cell pairs for dGWOT dataset splits. + + Creates stratified cell pairs for deep learning model training. Supports both + random sampling across all cells and stratified sampling from predefined groups + to ensure balanced representation across train/validation/test splits. + + Parameters + ---------- + indices : list or array-like + List of available cell indices corresponding to processed cell images. + n_pairs : list of int + N-length list specifying number of pairs to generate for each dataset split. + Typically [n_train_pairs, n_val_pairs, n_test_pairs]. + proportions : list of float, optional + N-length list of proportions that sum to 1.0 for stratified sampling. + If provided, cell indices are split into N groups according to these + proportions, and pairs are drawn only within each group. This ensures + train/val/test sets use disjoint cell populations. If None, all pairs + are drawn randomly from all available cells. Default is None. + seed : int, optional + Random seed for reproducible dataset splits. Default is None. + + Returns + ------- + list of numpy.ndarray + N-length list where each element is a 2D array of shape (n_pairs, 2) + containing cell index pairs for each dataset split (train, val, test). + """ + if seed is not None: + np.random.seed(seed) + + indices = np.array(indices) + n_groups = len(n_pairs) # Number of dataset splits (train, val, test) + + if proportions is not None: + if len(proportions) != n_groups: + raise ValueError(f"Length of proportions ({len(proportions)}) must match length of n_pairs ({n_groups})") + + if not np.isclose(sum(proportions), 1.0, rtol=1e-5): + raise ValueError(f"Proportions must sum to 1.0, got {sum(proportions)}") + + # Split cell indices into disjoint groups for train/val/test + np.random.shuffle(indices) # Randomize cell order first + n_total = len(indices) + + group_indices = [] + start_idx = 0 + + for i, prop in enumerate(proportions[:-1]): # Handle all but last group + group_size = int(np.round(prop * n_total)) + end_idx = start_idx + group_size + group_indices.append(indices[start_idx:end_idx]) + start_idx = end_idx + + # Last group gets remaining indices + group_indices.append(indices[start_idx:]) + + # Generate pairs within each disjoint cell group (train, val, test) + paired_arrays = [] + for i, group_inds in enumerate(group_indices): + n_pairs_for_group = n_pairs[i] + if len(group_inds) < 2: + raise ValueError(f"Dataset split {i} has only {len(group_inds)} cells, need at least 2 to generate pairs") + + pairs = get_random_pairs(group_inds, n_pairs_for_group) + paired_arrays.append(pairs) + + else: + # Generate all pairs randomly from all cells (overlapping populations) + paired_arrays = [] + for n_pairs_for_group in n_pairs: + pairs = get_random_pairs(indices, n_pairs_for_group) + paired_arrays.append(pairs) + + return paired_arrays \ No newline at end of file From debc4ceeb17e218587892ce7d40748dd6c2458ea Mon Sep 17 00:00:00 2001 From: robertkhu Date: Wed, 22 Oct 2025 19:07:15 +0000 Subject: [PATCH 03/14] Fixed plotly rendering in tutorial 6 --- docs/notebooks/Example_6.ipynb | 4328 +------------------------------- 1 file changed, 57 insertions(+), 4271 deletions(-) diff --git a/docs/notebooks/Example_6.ipynb b/docs/notebooks/Example_6.ipynb index 2762283..8e94c14 100644 --- a/docs/notebooks/Example_6.ipynb +++ b/docs/notebooks/Example_6.ipynb @@ -13,7 +13,7 @@ "id": "28d6ce35", "metadata": {}, "source": [ - "To demonstrate the functionality of GW-OT, we will perform an analysis on immunoflourescence data from the Human Protein Atlas. We will working with a small subset of X cells from X images, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/63tquyl5b6psiczrgihdn/hpa_images_metadata.zip?rlkey=7iz9cl5u35bvfupip6f0iicf3&st=ocpnazb7&dl=0)." + "To demonstrate the functionality of GW-OT, we will perform an analysis on immunoflourescence data from the Human Protein Atlas. We will working with a small subset of 373 cells from 70 images, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/63tquyl5b6psiczrgihdn/hpa_images_metadata.zip?rlkey=7iz9cl5u35bvfupip6f0iicf3&st=ocpnazb7&dl=0)." ] }, { @@ -35,6 +35,7 @@ "source": [ "import os\n", "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", "from tqdm import tqdm\n", "import skimage as ski\n", "from cajal.subcellular import *" @@ -447,7 +448,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "696ee320", "metadata": {}, "outputs": [ @@ -467,2152 +468,34 @@ }, { "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hovertemplate": "%{hovertext}

color=1
x=%{x}
y=%{y}", - "hovertext": [ - "cell_0", - "cell_2", - "cell_4", - "cell_6", - "cell_7", - "cell_8", - "cell_10", - "cell_11", - "cell_12", - "cell_13", - "cell_14", - "cell_16", - "cell_19", - "cell_20", - "cell_22", - "cell_23", - "cell_24", - "cell_25", - "cell_27", - "cell_28", - "cell_29", - "cell_31", - "cell_33", - "cell_35", - "cell_37", - "cell_38", - "cell_39", - "cell_41", - "cell_42", - "cell_43", - "cell_44", - "cell_47", - "cell_48", - "cell_51", - "cell_53", - "cell_55", - "cell_58", - "cell_60", - "cell_61", - "cell_62", - "cell_64", - "cell_65", - "cell_66", - "cell_68", - "cell_71", - "cell_72", - "cell_73", - "cell_74", - "cell_75", - "cell_76", - "cell_78", - "cell_91", - "cell_93", - "cell_95", - "cell_97", - "cell_136", - "cell_140", - "cell_142", - "cell_144", - "cell_145", - "cell_158", - "cell_162", - "cell_166", - "cell_167", - "cell_175", - "cell_176", - "cell_219", - "cell_253", - "cell_256", - "cell_262", - "cell_272", - "cell_273", - "cell_301" - ], - "legendgroup": "1", - "marker": { - "color": "#1F77B4", - "symbol": "circle" - }, - "mode": "markers", - "name": "1", - "orientation": "v", - "showlegend": true, - "type": "scatter", - "x": [ - 10.470212936401367, - 12.312568664550781, - 12.284958839416504, - 11.013801574707031, - 11.075864791870117, - 11.517743110656738, - 11.250957489013672, - 11.468009948730469, - 11.692370414733887, - 10.716691017150879, - 11.46733283996582, - 12.315851211547852, - 11.25301742553711, - 11.631410598754883, - 11.236824035644531, - 11.964887619018555, - 12.309128761291504, - 11.374908447265625, - 11.650925636291504, - 12.029847145080566, - 11.864778518676758, - 12.271783828735352, - 12.267718315124512, - 12.208593368530273, - 11.614533424377441, - 11.813565254211426, - 11.328191757202148, - 12.266615867614746, - 11.84893798828125, - 12.076929092407227, - 11.760852813720703, - 10.281558990478516, - 10.683618545532227, - 11.117523193359375, - 11.16075325012207, - 10.487278938293457, - 11.64605712890625, - 11.977656364440918, - 11.149227142333984, - 11.74874496459961, - 11.40621280670166, - 11.974194526672363, - 11.77653694152832, - 12.3076753616333, - 10.67529296875, - 12.303462028503418, - 11.24199104309082, - 11.621973037719727, - 11.582865715026855, - 11.592204093933105, - 11.583460807800293, - 11.083187103271484, - 12.001856803894043, - 12.064848899841309, - 11.531425476074219, - 10.988033294677734, - 11.710485458374023, - 10.125171661376953, - 9.907941818237305, - 10.13366985321045, - 10.60818099975586, - 11.986207008361816, - 10.851330757141113, - 11.885449409484863, - 11.892702102661133, - 11.179452896118164, - 12.354615211486816, - 10.74283218383789, - 10.472368240356445, - 10.926989555358887, - 10.817597389221191, - 11.050715446472168, - 10.393438339233398 - ], - "xaxis": "x", - "y": [ - 7.33117151260376, - 8.375398635864258, - 7.855964660644531, - 7.39954137802124, - 7.643902778625488, - 7.103420257568359, - 6.951264381408691, - 8.590084075927734, - 9.220922470092773, - 7.009788513183594, - 7.479423999786377, - 8.541096687316895, - 9.079501152038574, - 8.162571907043457, - 7.48162841796875, - 8.796451568603516, - 8.88451099395752, - 8.459766387939453, - 7.386898994445801, - 8.004698753356934, - 7.842724800109863, - 7.739343166351318, - 8.262476921081543, - 9.184324264526367, - 7.689973831176758, - 7.425989151000977, - 8.443625450134277, - 7.7019147872924805, - 7.726999759674072, - 7.500425815582275, - 7.274848937988281, - 7.219523906707764, - 7.431251049041748, - 9.183481216430664, - 8.774727821350098, - 7.540390968322754, - 7.331007957458496, - 7.45502233505249, - 6.866971969604492, - 7.894918441772461, - 7.443665504455566, - 8.133285522460938, - 7.953441143035889, - 9.155420303344727, - 8.609997749328613, - 8.419486045837402, - 7.263685703277588, - 7.33095645904541, - 8.800616264343262, - 7.4456915855407715, - 8.780095100402832, - 8.757272720336914, - 8.575485229492188, - 8.976503372192383, - 8.741171836853027, - 9.189842224121094, - 8.413444519042969, - 7.051916122436523, - 7.342107772827148, - 7.417575359344482, - 7.990440368652344, - 8.00399112701416, - 6.773506164550781, - 8.533093452453613, - 8.859456062316895, - 6.902346611022949, - 8.565699577331543, - 7.253220081329346, - 7.201398849487305, - 8.053874015808105, - 9.229771614074707, - 9.145679473876953, - 7.159358501434326 - ], - "yaxis": "y" - }, - { - "hovertemplate": "%{hovertext}

color=5
x=%{x}
y=%{y}", - "hovertext": [ - "cell_1", - "cell_3", - "cell_5", - "cell_15", - "cell_17", - "cell_18", - "cell_26", - "cell_30", - "cell_36", - "cell_40", - "cell_50", - "cell_52", - "cell_56", - "cell_57", - "cell_59", - "cell_63", - "cell_67", - "cell_70", - "cell_84", - "cell_92", - "cell_96", - "cell_98", - "cell_99", - "cell_100", - "cell_104", - "cell_105", - "cell_106", - "cell_130", - "cell_137", - "cell_138", - "cell_147", - "cell_148", - "cell_156", - "cell_157", - "cell_159", - "cell_160", - "cell_161", - "cell_163", - "cell_164", - "cell_165", - "cell_169", - "cell_171", - "cell_172", - "cell_174", - "cell_181", - "cell_188", - "cell_192", - "cell_222", - "cell_232", - "cell_234", - "cell_260" - ], - "legendgroup": "5", - "marker": { - "color": "#FF7F0E", - "symbol": "circle" - }, - "mode": "markers", - "name": "5", - "orientation": "v", - "showlegend": true, - "type": "scatter", - "x": [ - 11.747814178466797, - 12.064518928527832, - 11.840521812438965, - 12.267498970031738, - 12.147038459777832, - 12.216019630432129, - 12.024085998535156, - 11.696830749511719, - 11.604247093200684, - 11.593334197998047, - 10.578858375549316, - 11.883559226989746, - 11.195067405700684, - 11.565237998962402, - 10.922109603881836, - 12.37385082244873, - 11.372662544250488, - 12.119458198547363, - 11.421720504760742, - 10.925214767456055, - 10.36055850982666, - 10.446563720703125, - 10.664886474609375, - 10.22600269317627, - 11.17844009399414, - 11.300833702087402, - 10.49958610534668, - 10.450248718261719, - 11.161641120910645, - 11.290375709533691, - 10.804224967956543, - 10.380353927612305, - 11.554306030273438, - 11.103714942932129, - 11.590129852294922, - 11.822622299194336, - 12.213976860046387, - 12.251840591430664, - 12.105546951293945, - 12.289101600646973, - 10.598264694213867, - 11.25814151763916, - 11.343453407287598, - 10.801051139831543, - 11.49448299407959, - 11.171337127685547, - 9.67757511138916, - 11.203398704528809, - 10.798874855041504, - 10.841958999633789, - 11.134123802185059 - ], - "xaxis": "x", - "y": [ - 10.567832946777344, - 10.209301948547363, - 10.536479949951172, - 9.799760818481445, - 10.12877368927002, - 9.717034339904785, - 9.966899871826172, - 10.726451873779297, - 10.07335090637207, - 10.128976821899414, - 9.770319938659668, - 10.564817428588867, - 11.312215805053711, - 9.910209655761719, - 9.962122917175293, - 9.553116798400879, - 11.533130645751953, - 10.125333786010742, - 11.463017463684082, - 9.861875534057617, - 9.56574535369873, - 11.268634796142578, - 11.485913276672363, - 11.012574195861816, - 11.487122535705566, - 11.543986320495605, - 11.359087944030762, - 11.444477081298828, - 9.809798240661621, - 11.211044311523438, - 11.318399429321289, - 10.390735626220703, - 10.801411628723145, - 11.246253967285156, - 11.124070167541504, - 10.421768188476562, - 10.141022682189941, - 9.988823890686035, - 10.561238288879395, - 9.586380004882812, - 11.269648551940918, - 11.518125534057617, - 11.282392501831055, - 11.163555145263672, - 11.396036148071289, - 9.7876558303833, - 10.542497634887695, - 11.513497352600098, - 9.779924392700195, - 9.870909690856934, - 9.754999160766602 - ], - "yaxis": "y" - }, - { - "hovertemplate": "%{hovertext}

color=3
x=%{x}
y=%{y}", - "hovertext": [ - "cell_9", - "cell_21", - "cell_54", - "cell_69", - "cell_77", - "cell_79", - "cell_82", - "cell_85", - "cell_89", - "cell_94", - "cell_102", - "cell_128", - "cell_131", - "cell_133", - "cell_134", - "cell_141", - "cell_146", - "cell_152", - "cell_153", - "cell_170", - "cell_177", - "cell_182", - "cell_191", - "cell_198", - "cell_199", - "cell_204", - "cell_208", - "cell_212", - "cell_221", - "cell_226", - "cell_227", - "cell_231", - "cell_233", - "cell_236", - "cell_241", - "cell_243", - "cell_244", - "cell_247", - "cell_248", - "cell_252", - "cell_254", - "cell_259", - "cell_261", - "cell_271", - "cell_274", - "cell_276", - "cell_286", - "cell_288", - "cell_295", - "cell_296", - "cell_297", - "cell_299", - "cell_311", - "cell_312", - "cell_314", - "cell_315", - "cell_316", - "cell_317", - "cell_324", - "cell_358" - ], - "legendgroup": "3", - "marker": { - "color": "#2CA02C", - "symbol": "circle" - }, - "mode": "markers", - "name": "3", - "orientation": "v", - "showlegend": true, - "type": "scatter", - "x": [ - 10.844043731689453, - 10.066186904907227, - 8.121711730957031, - 9.412109375, - 8.111811637878418, - 8.955904006958008, - 10.099164962768555, - 10.236702919006348, - 9.05978775024414, - 10.014548301696777, - 6.505732536315918, - 8.59902286529541, - 9.48098373413086, - 10.114336013793945, - 8.441006660461426, - 9.067895889282227, - 10.213714599609375, - 7.9023661613464355, - 5.922726154327393, - 7.92042350769043, - 9.144457817077637, - 7.054023265838623, - 8.23034381866455, - 8.893445014953613, - 10.14044189453125, - 8.014692306518555, - 8.886664390563965, - 7.463397979736328, - 7.12321138381958, - 6.124226093292236, - 6.031777381896973, - 7.8968071937561035, - 8.391080856323242, - 10.015118598937988, - 9.610279083251953, - 7.963976860046387, - 8.17613410949707, - 8.345664024353027, - 10.31667709350586, - 6.5018229484558105, - 5.779963970184326, - 10.09521484375, - 10.343782424926758, - 10.042535781860352, - 10.392885208129883, - 10.27789306640625, - 9.997753143310547, - 7.991713047027588, - 8.10283374786377, - 10.316632270812988, - 9.835476875305176, - 9.4730224609375, - 8.855951309204102, - 9.382893562316895, - 9.77592945098877, - 9.874587059020996, - 9.094463348388672, - 8.361153602600098, - 9.347718238830566, - 5.972364902496338 - ], - "xaxis": "x", - "y": [ - 8.4461030960083, - 9.200824737548828, - 9.385358810424805, - 7.435500621795654, - 9.28831958770752, - 9.574503898620605, - 8.64625358581543, - 9.494376182556152, - 9.4609956741333, - 8.643878936767578, - 8.373583793640137, - 10.22214412689209, - 7.6977033615112305, - 9.112919807434082, - 9.428910255432129, - 9.71088981628418, - 9.646464347839355, - 9.598580360412598, - 9.06125545501709, - 9.315159797668457, - 10.246822357177734, - 9.326196670532227, - 9.370452880859375, - 10.092228889465332, - 8.480729103088379, - 9.179167747497559, - 9.7933349609375, - 9.790886878967285, - 9.414827346801758, - 9.188652992248535, - 8.544243812561035, - 8.870431900024414, - 9.942936897277832, - 8.375882148742676, - 7.815301895141602, - 9.440760612487793, - 9.94061279296875, - 9.971707344055176, - 9.429641723632812, - 8.383771896362305, - 8.178888320922852, - 9.015743255615234, - 8.8228178024292, - 8.666848182678223, - 9.175257682800293, - 9.0098237991333, - 8.521554946899414, - 9.103172302246094, - 10.916104316711426, - 8.724075317382812, - 8.450115203857422, - 7.9202775955200195, - 10.031404495239258, - 10.158611297607422, - 8.269808769226074, - 9.457282066345215, - 10.566201210021973, - 9.998526573181152, - 7.717197418212891, - 8.599685668945312 - ], - "yaxis": "y" - }, - { - "hovertemplate": "%{hovertext}

color=2
x=%{x}
y=%{y}", - "hovertext": [ - "cell_32", - "cell_34", - "cell_45", - "cell_46", - "cell_80", - "cell_81", - "cell_83", - "cell_87", - "cell_90", - "cell_101", - "cell_112", - "cell_113", - "cell_115", - "cell_120", - "cell_121", - "cell_122", - "cell_127", - "cell_135", - "cell_139", - "cell_143", - "cell_149", - "cell_150", - "cell_151", - "cell_155", - "cell_168", - "cell_179", - "cell_180", - "cell_186", - "cell_189", - "cell_190", - "cell_196", - "cell_197", - "cell_201", - "cell_202", - "cell_206", - "cell_207", - "cell_209", - "cell_211", - "cell_213", - "cell_216", - "cell_218", - "cell_220", - "cell_223", - "cell_229", - "cell_230", - "cell_235", - "cell_246", - "cell_250", - "cell_255", - "cell_275", - "cell_287", - "cell_291", - "cell_292", - "cell_300", - "cell_313", - "cell_320", - "cell_322", - "cell_361", - "cell_364", - "cell_369" - ], - "legendgroup": "2", - "marker": { - "color": "#D62728", - "symbol": "circle" - }, - "mode": "markers", - "name": "2", - "orientation": "v", - "showlegend": true, - "type": "scatter", - "x": [ - 8.199105262756348, - 6.664831161499023, - 6.700311660766602, - 6.343005657196045, - 9.605731964111328, - 5.576918125152588, - 7.368834018707275, - 8.703349113464355, - 8.41206169128418, - 8.016773223876953, - 9.50320816040039, - 6.412990570068359, - 8.229869842529297, - 6.481535911560059, - 9.348567962646484, - 7.570054054260254, - 5.8002190589904785, - 5.744693756103516, - 8.638879776000977, - 8.070985794067383, - 6.272864818572998, - 6.171213150024414, - 7.215863227844238, - 8.107453346252441, - 7.9924635887146, - 5.479817867279053, - 8.433005332946777, - 5.473900318145752, - 6.895977973937988, - 8.579727172851562, - 7.876437187194824, - 7.183394432067871, - 5.571967601776123, - 8.23187255859375, - 6.689271450042725, - 8.480426788330078, - 8.408916473388672, - 7.915027618408203, - 5.880353927612305, - 5.588372230529785, - 8.57016658782959, - 8.648880004882812, - 5.590453147888184, - 5.821910858154297, - 7.477540493011475, - 8.390219688415527, - 7.340928554534912, - 6.239269256591797, - 9.62497329711914, - 8.277377128601074, - 8.63513469696045, - 5.796451091766357, - 6.15939998626709, - 9.125432968139648, - 6.608971118927002, - 6.744550704956055, - 6.2651472091674805, - 6.119339942932129, - 5.6807098388671875, - 5.4191179275512695 - ], - "xaxis": "x", - "y": [ - 7.334995746612549, - 6.936014652252197, - 6.965074062347412, - 7.11490535736084, - 9.846455574035645, - 7.1071062088012695, - 7.46168851852417, - 7.93528938293457, - 7.807597637176514, - 7.612594127655029, - 10.055404663085938, - 7.9679694175720215, - 7.961348056793213, - 7.610311985015869, - 9.993404388427734, - 7.421642780303955, - 7.23611307144165, - 7.294541358947754, - 7.731958389282227, - 7.233391284942627, - 7.456475734710693, - 8.00796890258789, - 7.518858909606934, - 7.289413928985596, - 7.464017868041992, - 7.262912273406982, - 7.2448410987854, - 7.083313941955566, - 7.355195045471191, - 7.344755172729492, - 7.29884147644043, - 7.494061470031738, - 7.206907272338867, - 7.283708095550537, - 7.632297039031982, - 7.757523536682129, - 8.17127513885498, - 7.573615550994873, - 7.204403400421143, - 7.31341028213501, - 8.371613502502441, - 7.278628826141357, - 7.174847602844238, - 7.4548821449279785, - 7.909790515899658, - 7.999366760253906, - 7.433365821838379, - 7.873532772064209, - 7.285638332366943, - 7.342884063720703, - 7.385688781738281, - 7.497030258178711, - 7.872297763824463, - 7.412783145904541, - 7.7408366203308105, - 7.7426252365112305, - 7.918805122375488, - 7.80131721496582, - 7.159327030181885, - 7.372076988220215 - ], - "yaxis": "y" - }, - { - "hovertemplate": "%{hovertext}

color=4
x=%{x}
y=%{y}", - "hovertext": [ - "cell_49", - "cell_86", - "cell_88", - "cell_103", - "cell_107", - "cell_108", - "cell_109", - "cell_110", - "cell_111", - "cell_116", - "cell_117", - "cell_118", - "cell_119", - "cell_123", - "cell_124", - "cell_125", - "cell_129", - "cell_132", - "cell_154", - "cell_173", - "cell_183", - "cell_184", - "cell_187", - "cell_193", - "cell_194", - "cell_203", - "cell_205", - "cell_210", - "cell_217", - "cell_228", - "cell_240", - "cell_249", - "cell_251", - "cell_257", - "cell_258", - "cell_263", - "cell_284", - "cell_294", - "cell_310", - "cell_319", - "cell_326", - "cell_339", - "cell_340", - "cell_343", - "cell_347", - "cell_350", - "cell_351", - "cell_352", - "cell_356", - "cell_360", - "cell_362", - "cell_368", - "cell_370" - ], - "legendgroup": "4", - "marker": { - "color": "#9467BD", - "symbol": "circle" - }, - "mode": "markers", - "name": "4", - "orientation": "v", - "showlegend": true, - "type": "scatter", - "x": [ - 8.792957305908203, - 9.771020889282227, - 8.111828804016113, - 10.551321029663086, - 7.334377288818359, - 7.512739658355713, - 7.225539684295654, - 7.23398494720459, - 7.602049350738525, - 8.415926933288574, - 8.695684432983398, - 9.391413688659668, - 8.61044979095459, - 8.571767807006836, - 10.289719581604004, - 9.380416870117188, - 8.570059776306152, - 9.31619930267334, - 8.835460662841797, - 8.39517879486084, - 9.052550315856934, - 7.930122375488281, - 9.493639945983887, - 10.106727600097656, - 8.337416648864746, - 8.134517669677734, - 9.564823150634766, - 8.223367691040039, - 9.31398868560791, - 8.53930950164795, - 7.381310939788818, - 8.93163013458252, - 8.030320167541504, - 6.583729267120361, - 9.901187896728516, - 7.684980392456055, - 7.3445048332214355, - 7.500270843505859, - 7.859353542327881, - 8.66342830657959, - 8.51186752319336, - 7.962340354919434, - 9.412398338317871, - 7.3680572509765625, - 7.5125837326049805, - 9.67270565032959, - 9.116009712219238, - 8.934961318969727, - 11.073728561401367, - 9.277174949645996, - 9.565858840942383, - 7.4820122718811035, - 9.112516403198242 - ], - "xaxis": "x", - "y": [ - 11.721299171447754, - 11.314684867858887, - 11.407580375671387, - 11.606562614440918, - 10.656499862670898, - 10.366463661193848, - 10.384355545043945, - 10.080385208129883, - 11.30085563659668, - 10.278273582458496, - 11.15866470336914, - 11.27371597290039, - 11.38850212097168, - 11.93083667755127, - 11.713282585144043, - 11.3212251663208, - 10.616705894470215, - 11.670025825500488, - 11.509689331054688, - 11.87523365020752, - 11.74281120300293, - 11.637688636779785, - 11.629831314086914, - 11.557605743408203, - 11.327455520629883, - 11.533363342285156, - 11.448644638061523, - 11.56425952911377, - 11.643001556396484, - 12.002367973327637, - 11.237997055053711, - 11.807448387145996, - 11.91479206085205, - 9.93568229675293, - 11.719674110412598, - 11.008150100708008, - 10.831694602966309, - 10.681164741516113, - 11.559565544128418, - 11.62997055053711, - 11.910080909729004, - 11.488192558288574, - 12.004965782165527, - 10.77297592163086, - 10.818303108215332, - 11.836057662963867, - 11.363900184631348, - 11.62457275390625, - 11.685964584350586, - 11.854164123535156, - 11.920244216918945, - 11.331361770629883, - 11.9354887008667 - ], - "yaxis": "y" - }, - { - "hovertemplate": "%{hovertext}

color=0
x=%{x}
y=%{y}", - "hovertext": [ - "cell_114", - "cell_126", - "cell_178", - "cell_185", - "cell_195", - "cell_200", - "cell_214", - "cell_215", - "cell_224", - "cell_225", - "cell_237", - "cell_238", - "cell_239", - "cell_242", - "cell_245", - "cell_264", - "cell_265", - "cell_266", - "cell_267", - "cell_268", - "cell_269", - "cell_270", - "cell_277", - "cell_278", - "cell_279", - "cell_280", - "cell_281", - "cell_282", - "cell_283", - "cell_285", - "cell_289", - "cell_290", - "cell_293", - "cell_298", - "cell_302", - "cell_303", - "cell_304", - "cell_305", - "cell_306", - "cell_307", - "cell_308", - "cell_309", - "cell_318", - "cell_321", - "cell_323", - "cell_325", - "cell_327", - "cell_328", - "cell_329", - "cell_330", - "cell_331", - "cell_332", - "cell_333", - "cell_334", - "cell_335", - "cell_336", - "cell_337", - "cell_338", - "cell_341", - "cell_342", - "cell_344", - "cell_345", - "cell_346", - "cell_348", - "cell_349", - "cell_353", - "cell_354", - "cell_355", - "cell_357", - "cell_359", - "cell_363", - "cell_365", - "cell_366", - "cell_367", - "cell_371", - "cell_372" - ], - "legendgroup": "0", - "marker": { - "color": "#8C564B", - "symbol": "circle" - }, - "mode": "markers", - "name": "0", - "orientation": "v", - "showlegend": true, - "type": "scatter", - "x": [ - 5.8254241943359375, - 6.312600612640381, - 7.4073591232299805, - 5.5496439933776855, - 6.587268352508545, - 6.415332794189453, - 5.402957439422607, - 5.634885787963867, - 5.412581443786621, - 5.480430603027344, - 5.923130035400391, - 4.660431861877441, - 4.750604152679443, - 5.676141738891602, - 6.9678497314453125, - 5.845112323760986, - 4.730572700500488, - 5.364740371704102, - 5.6140618324279785, - 5.58400821685791, - 5.626848220825195, - 6.026407241821289, - 4.661167621612549, - 4.845733642578125, - 6.269335746765137, - 6.569248676300049, - 4.694128513336182, - 5.913049221038818, - 4.75978422164917, - 6.9230732917785645, - 6.047796249389648, - 6.43698263168335, - 6.868078231811523, - 6.228044509887695, - 4.831230163574219, - 4.7045416831970215, - 5.938592433929443, - 5.495119571685791, - 5.022512435913086, - 4.881829261779785, - 6.051345348358154, - 5.975616931915283, - 5.391265869140625, - 5.728851795196533, - 5.717605113983154, - 6.81803035736084, - 6.168428897857666, - 6.318421840667725, - 5.614867210388184, - 5.778199195861816, - 5.653630256652832, - 6.127173900604248, - 6.019142150878906, - 5.603560924530029, - 5.99357271194458, - 5.24373722076416, - 6.886112213134766, - 5.429830551147461, - 5.813126087188721, - 6.325909614562988, - 6.5907793045043945, - 5.358606338500977, - 5.31993293762207, - 5.788165092468262, - 7.344539642333984, - 6.769246578216553, - 5.575586795806885, - 6.623497009277344, - 6.2303290367126465, - 5.447906970977783, - 7.3655853271484375, - 5.552432060241699, - 5.909779071807861, - 5.980591297149658, - 5.426873683929443, - 6.0437188148498535 - ], - "xaxis": "x", - "y": [ - 9.319472312927246, - 11.60047435760498, - 9.9579496383667, - 7.996298313140869, - 11.07923412322998, - 11.820281982421875, - 8.979147911071777, - 10.918229103088379, - 10.264891624450684, - 7.8346991539001465, - 11.048379898071289, - 10.001415252685547, - 10.235962867736816, - 9.327349662780762, - 11.078413963317871, - 10.565495491027832, - 10.074840545654297, - 8.80063247680664, - 10.688979148864746, - 8.635754585266113, - 8.716435432434082, - 10.811123847961426, - 10.050178527832031, - 10.319920539855957, - 11.122275352478027, - 10.861050605773926, - 10.088911056518555, - 11.370647430419922, - 10.005218505859375, - 11.350404739379883, - 11.336915969848633, - 10.390603065490723, - 11.310484886169434, - 9.756113052368164, - 10.019303321838379, - 10.080672264099121, - 10.09730052947998, - 8.764248847961426, - 9.83413028717041, - 9.872381210327148, - 9.617083549499512, - 10.9186372756958, - 8.531365394592285, - 8.633432388305664, - 10.969356536865234, - 11.489310264587402, - 10.530739784240723, - 11.73331069946289, - 8.403346061706543, - 11.075582504272461, - 11.138618469238281, - 10.732989311218262, - 10.433423042297363, - 9.8084135055542, - 10.914801597595215, - 10.086227416992188, - 11.159547805786133, - 10.681574821472168, - 8.927404403686523, - 11.69886302947998, - 11.474774360656738, - 9.330952644348145, - 8.596227645874023, - 10.771533012390137, - 11.562420845031738, - 10.67807388305664, - 8.098641395568848, - 11.60422134399414, - 11.645100593566895, - 8.42159366607666, - 11.64996337890625, - 10.978010177612305, - 10.883095741271973, - 9.500628471374512, - 10.904341697692871, - 11.054499626159668 - ], - "yaxis": "y" - } - ], - "layout": { - "legend": { - "title": { - "text": "color" - }, - "tracegroupgap": 0 - }, - "margin": { - "t": 60 - }, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "rgb(36,36,36)" - }, - "error_y": { - "color": "rgb(36,36,36)" - }, - "marker": { - "line": { - "color": "white", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "white", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "rgb(36,36,36)", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "rgb(36,36,36)" - }, - "baxis": { - "endlinecolor": "rgb(36,36,36)", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "rgb(36,36,36)" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "line": { - "color": "white", - "width": 0.6 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - }, - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "rgb(237,237,237)" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "rgb(217,217,217)" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 1, - "tickcolor": "rgb(36,36,36)", - "ticks": "outside" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "rgb(103,0,31)" - ], - [ - 0.1, - "rgb(178,24,43)" - ], - [ - 0.2, - "rgb(214,96,77)" - ], - [ - 0.3, - "rgb(244,165,130)" - ], - [ - 0.4, - "rgb(253,219,199)" - ], - [ - 0.5, - "rgb(247,247,247)" - ], - [ - 0.6, - "rgb(209,229,240)" - ], - [ - 0.7, - "rgb(146,197,222)" - ], - [ - 0.8, - "rgb(67,147,195)" - ], - [ - 0.9, - "rgb(33,102,172)" - ], - [ - 1, - "rgb(5,48,97)" - ] - ], - "sequential": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "sequentialminus": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ] - }, - "colorway": [ - "#1F77B4", - "#FF7F0E", - "#2CA02C", - "#D62728", - "#9467BD", - "#8C564B", - "#E377C2", - "#7F7F7F", - "#BCBD22", - "#17BECF" - ], - "font": { - "color": "rgb(36,36,36)" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "white", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "white", - "polar": { - "angularaxis": { - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside" - }, - "bgcolor": "white", - "radialaxis": { - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "white", - "gridcolor": "rgb(232,232,232)", - "gridwidth": 2, - "linecolor": "rgb(36,36,36)", - "showbackground": true, - "showgrid": false, - "showline": true, - "ticks": "outside", - "zeroline": false, - "zerolinecolor": "rgb(36,36,36)" - }, - "yaxis": { - "backgroundcolor": "white", - "gridcolor": "rgb(232,232,232)", - "gridwidth": 2, - "linecolor": "rgb(36,36,36)", - "showbackground": true, - "showgrid": false, - "showline": true, - "ticks": "outside", - "zeroline": false, - "zerolinecolor": "rgb(36,36,36)" - }, - "zaxis": { - "backgroundcolor": "white", - "gridcolor": "rgb(232,232,232)", - "gridwidth": 2, - "linecolor": "rgb(36,36,36)", - "showbackground": true, - "showgrid": false, - "showline": true, - "ticks": "outside", - "zeroline": false, - "zerolinecolor": "rgb(36,36,36)" - } - }, - "shapedefaults": { - "fillcolor": "black", - "line": { - "width": 0 - }, - "opacity": 0.3 - }, - "ternary": { - "aaxis": { - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside" - }, - "baxis": { - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside" - }, - "bgcolor": "white", - "caxis": { - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside", - "title": { - "standoff": 15 - }, - "zeroline": false, - "zerolinecolor": "rgb(36,36,36)" - }, - "yaxis": { - "automargin": true, - "gridcolor": "rgb(232,232,232)", - "linecolor": "rgb(36,36,36)", - "showgrid": false, - "showline": true, - "ticks": "outside", - "title": { - "standoff": 15 - }, - "zeroline": false, - "zerolinecolor": "rgb(36,36,36)" - } - } - }, - "xaxis": { - "anchor": "y", - "domain": [ - 0, - 1 - ], - "title": { - "text": "x" - } - }, - "yaxis": { - "anchor": "x", - "domain": [ - 0, - 1 - ], - "title": { - "text": "y" - } - } - } - }, "text/html": [ - "
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.io as pio\n", + "\n", + "# Choose the adequate plotly renderer for visualizing plotly graphs in your system\n", + "pio.renderers.default = 'notebook_connected'\n", + "# pio.renderers.default = 'iframe'\n", + "\n", + "import cajal.utilities\n", + "import umap\n", + "import plotly.express\n", + "\n", + "# Compute UMAP representation of the GW morphology space\n", + "reducer = umap.UMAP(metric=\"precomputed\", random_state=1)\n", + "embedding = reducer.fit_transform(gw_dmat)\n", + "\n", + "# Cluster cells based on the GW morphology space using the leiden algorithm\n", + "gw_clusters = cajal.utilities.leiden_clustering(gw_dmat, resolution=0.003, seed=1)\n", + "\n", + "# Visualize the GW morphology space\n", + "plotly.express.scatter(x=embedding[:,0],\n", + " y=embedding[:,1],\n", + " template=\"simple_white\",\n", + " hover_name=[\"cell_\" + str(i) for i in range(gw_dmat.shape[0])],\n", + " color = [str(c) for c in gw_clusters]\n", + " )" + ] + }, { "cell_type": "markdown", "id": "28b35ff8", @@ -134,7 +238,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "2603a863", "metadata": {}, "outputs": [ @@ -147,20 +251,6 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", - "\n", - "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", - "\n", - "\u001b[1;31mClick here for more info. \n", - "\n", - "\u001b[1;31mView Jupyter log for further details." - ] } ], "source": [ @@ -187,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "59478b5f", "metadata": {}, "outputs": [ @@ -224,7 +314,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "1efce506", "metadata": {}, "outputs": [ @@ -264,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "fe14f406", "metadata": {}, "outputs": [ @@ -301,7 +391,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "9f0a8a95", "metadata": {}, "outputs": [ @@ -336,38 +426,6 @@ " num_processes=cpu_count(), chunksize=1) # parallelization parameters" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc3ab932", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mapping cells to target cell:\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "10it [00:48, 4.82s/it] \n" - ] - } - ], - "source": [ - "mapped_distbs = map_to_cell_parallel(cell_objects[:10], \n", - " channels_to_map, \n", - " 0, # cell to map to\n", - " method='fused', # 'fused' for full mapping, 'fused' for partial mapping\n", - " fused_channel='nucleus', # addition info to consider for mapping\n", - " fused_cost=1000, fused_param=0.1, # controls weight of additional info\n", - " compartment_specific=True, # enforces strict mapping of nucleus to nucleus\n", - " num_processes=cpu_count(), chunksize=1) # parallelization parameters" - ] - }, { "cell_type": "markdown", "id": "56480efc", @@ -378,7 +436,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "90dde1e3", "metadata": {}, "outputs": [ @@ -423,7 +481,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "4917bb17", "metadata": {}, "outputs": [ @@ -448,7 +506,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "696ee320", "metadata": {}, "outputs": [ @@ -525,29 +583,19 @@ } ], "source": [ - "import plotly.io as pio\n", - "\n", - "# Choose the adequate plotly renderer for visualizing plotly graphs in your system\n", - "pio.renderers.default = 'notebook_connected'\n", - "# pio.renderers.default = 'iframe'\n", - "\n", - "import cajal.utilities\n", - "import umap\n", - "import plotly.express\n", - "\n", "# Compute UMAP representation of the OT localization space\n", "reducer = umap.UMAP(metric=\"precomputed\", random_state=1)\n", "embedding = reducer.fit_transform(ot_dmats[0])\n", "\n", "# Cluster cells based on the OT localization space using the leiden algorithm\n", - "clusters = cajal.utilities.leiden_clustering(ot_dmats[0], resolution=0.005, seed=1)\n", + "ot_clusters = cajal.utilities.leiden_clustering(ot_dmats[0], resolution=0.005, seed=1)\n", "\n", "# Visualize the OT localization space\n", "plotly.express.scatter(x=embedding[:,0],\n", " y=embedding[:,1],\n", " template=\"simple_white\",\n", " hover_name=[\"cell_\" + str(i) for i in range(ot_dmats[0].shape[0])],\n", - " color = [str(c) for c in clusters]\n", + " color = [str(c) for c in ot_clusters]\n", " )" ] }, @@ -561,7 +609,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "839bec6f", "metadata": {}, "outputs": [ @@ -594,7 +642,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 15, "id": "77c4d262", "metadata": {}, "outputs": [ @@ -650,6 +698,233 @@ " color = np.array([str(c) for c in cell_metadata['locations']])\n", " )" ] + }, + { + "cell_type": "markdown", + "id": "8b7a9703", + "metadata": {}, + "source": [ + "## Utilizing multiple anchor cells" + ] + }, + { + "cell_type": "markdown", + "id": "0930f006", + "metadata": {}, + "source": [ + "One characteristic of the GW-OT algorithm is that the OT localization space is dependent on the choice of anchor cell to map to. While in practice we've observed that choosing centroid cell based on the GW morphology space results in informative localization spaces, one may want their analysis to be more robust to the choice of anchor cell. To address this, we suggest mapping the protein distributions of each cell to multiple anchor cells, and integrating the resulting OT localization spaces. One natural way to select a set of anchor cells is to first cluster the GW morphology space and select the centroid cell of each morphological cluster, thus utilizing a broad range of cellular morphologies in constructing each localization space. Note, since this approach involves repeating the GW-based mapping and OT computations per each anchor cell, it will substatially increase the runtime of the analysis. " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "a0d31aeb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Find centroid cell for each GW morphology cluster\n", + "cluster_centroids = []\n", + "for c in np.unique(gw_clusters):\n", + " cluster_inds = np.where(gw_clusters == c)[0]\n", + " # subset the GW distance matrix to only the cells in the cluster\n", + " gw_dmat_cluster = gw_dmat[np.ix_(cluster_inds, cluster_inds)]\n", + " # find the centroid cell in the cluster\n", + " cluster_centroid_idx = find_centroid(gw_dmat_cluster)\n", + " cluster_centroid = cluster_inds[cluster_centroid_idx]\n", + " cluster_centroids.append(cluster_centroid)\n", + "\n", + "# Visualize centroid cells for each GW morphology cluster\n", + "fig, axes = plt.subplots(1, len(cluster_centroids), figsize=(15, 5))\n", + "for ax, i in zip(axes, cluster_centroids):\n", + " plot_cell_image(cell_objects[i], channels=['nucleus'], ax=ax)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60e26557", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapping cells to target cell:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "373it [27:13, 4.38s/it] " + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing pairwise OT distances:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "100%|█████████████████████████████████████████████████████████████████████████████| 69378/69378 [27:58<00:00, 41.32it/s]\n" + ] + }, + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_293_mapped_distbs.pickle'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn [21], line 18\u001b[0m\n\u001b[1;32m 15\u001b[0m centroid_mapped_distbs\u001b[38;5;241m.\u001b[39mappend(mapped_distbs)\n\u001b[1;32m 16\u001b[0m centroid_ot_dmats\u001b[38;5;241m.\u001b[39mappend(ot_dmats)\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mtarget_cell_ind\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m_mapped_distbs.pickle\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 19\u001b[0m mapped_distbs \u001b[38;5;241m=\u001b[39m pickle\u001b[38;5;241m.\u001b[39mload(f)\n\u001b[1;32m 20\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(target_cell_ind) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_ot_dmats.pickle\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrb\u001b[39m\u001b[38;5;124m'\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n", + "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/IPython/core/interactiveshell.py:282\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 275\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 276\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 277\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 278\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 279\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 280\u001b[0m )\n\u001b[0;32m--> 282\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_293_mapped_distbs.pickle'" + ] + } + ], + "source": [ + "centroid_mapped_distbs = []\n", + "centroid_ot_dmats = []\n", + "for target_cell_ind in cluster_centroids[1:]:\n", + " # Mapping all cells to anchor cell\n", + " mapped_distbs = map_to_cell_parallel(cell_objects, \n", + " channels_to_map, \n", + " target_cell_ind, # cell to map to\n", + " method='fused', # 'fused' for full mapping, 'fused' for partial mapping\n", + " fused_channel='nucleus', # addition info to consider for mapping\n", + " fused_cost=1000, fused_param=0.1, # controls weight of additional info\n", + " compartment_specific=True, # enforces strict mapping of nucleus to nucleus\n", + " num_processes=cpu_count(), chunksize=1) # parallelization parameters\n", + " # Compute OT distance matrix for the mapped protein distributions\n", + " ot_dmats = gw_mapped_ot_pairwise_parallel(cell_objects[target_cell_ind], mapped_distbs, num_processes=cpu_count(), chunksize=20)\n", + " centroid_mapped_distbs.append(mapped_distbs)\n", + " centroid_ot_dmats.append(ot_dmats)\n", + "\n", + " out_dir = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/'\n", + " for target_cell_ind, mapped_distbs, ot_dmats in zip(cluster_centroids, centroid_mapped_distbs, centroid_ot_dmats):\n", + " with open(os.path.join(out_dir, f'anchor_{target_cell_ind}_mapped_distbs.pickle'), 'wb') as f:\n", + " pickle.dump(mapped_distbs, f, protocol=pickle.HIGHEST_PROTOCOL)\n", + " with open(os.path.join(out_dir, f'anchor_{target_cell_ind}_ot_dmats.pickle'), 'wb') as f:\n", + " pickle.dump(ot_dmats, f, protocol=pickle.HIGHEST_PROTOCOL)" + ] + }, + { + "cell_type": "markdown", + "id": "3c0b69e4", + "metadata": {}, + "source": [ + "Having computed the OT localization spaces for each cluster centroid, we will now build a consolidated space that integrates information from each localization space. For this purpose, we use the Weighted Nearest Neighbors (WNN) algorithm introduced in:\n", + "\n", + "\\- Hao, Y. et al. [Integrated analysis of multimodal single-cell data.](https://www.sciencedirect.com/science/article/pii/S0092867421005833) Cell 184, 3573-3587 (2021).\n", + "\n", + "To do this, we construct instances of the `Modality` class for each input. The input to `cajal.wnn.wnn()` is a list of Modality objects and a number of nearest neighbors to consider in each space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd369efe", + "metadata": {}, + "outputs": [], + "source": [ + "import cajal.wnn\n", + "\n", + "# Extract protein OT dmat from each centroid\n", + "centroid_protein_ot_dmats = [ot_dmats[0] for ot_dmats in centroid_ot_dmats]\n", + "# Integrate OT localization spaces from each centroid cell using WNN\n", + "integrated_space = 1-cajal.wnn.wnn(centroid_protein_ot_dmats, 5)" + ] + }, + { + "cell_type": "markdown", + "id": "4e979675", + "metadata": {}, + "source": [ + "The similarity function returned by the weighted nearest neighbors algorithm is asymmetric. For this reason, the term \"space\" here is somewhat imprecise. To visualize the consolidated space using UMAP, it is therefore convenient to symmetrize the matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64fe8dd2", + "metadata": {}, + "outputs": [], + "source": [ + "def symmetrize(a):\n", + " a = a.copy()\n", + " a[a == 0] = np.max(a)\n", + " b = a + a.T\n", + " b = np.minimum(a,b)\n", + " b = np.minimum(b,a.T)\n", + " d=np.zeros(a.shape[0],dtype=int)\n", + " b[d,d]=0\n", + " return np.array(b)\n", + "\n", + "wnn_dmat = symmetrize(integrated_space)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64dd0a71", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute UMAP representation of the OT localization space\n", + "reducer = umap.UMAP(metric=\"precomputed\", random_state=1)\n", + "embedding = reducer.fit_transform(wnn_dmat)\n", + "\n", + "# Visualize the OT localization space\n", + "plotly.express.scatter(x=embedding[:,0],\n", + " y=embedding[:,1],\n", + " template=\"simple_white\",\n", + " hover_name=[\"cell_\" + str(i) for i in range(wnn_dmat.shape[0])],\n", + " color = [str(c) for c in cell_metadata['locations']]\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9cd5977e", + "metadata": {}, + "outputs": [], + "source": [ + "# load\n", + "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/cell_objects.pickle', 'rb') as f:\n", + " cell_objects = pickle.load(f)\n", + "\n", + "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/gw_dmat.pickle', 'rb') as f:\n", + " gw_dmat = pickle.load(f)\n", + "\n", + "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/target_cell_ind.pickle', 'rb') as f:\n", + " target_cell_ind = pickle.load(f)\n", + "\n", + "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/mapped_distbs.pickle', 'rb') as f:\n", + " mapped_distbs = pickle.load(f)\n", + "\n", + "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/ot_dmats.pickle', 'rb') as f:\n", + " ot_dmats = pickle.load(f)\n", + "\n", + "cell_metadata = pd.read_csv('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/cell_metadata.csv', index_col=0)" + ] } ], "metadata": { diff --git a/docs/notebooks/Example_7.ipynb b/docs/notebooks/Example_7.ipynb new file mode 100644 index 0000000..48ba304 --- /dev/null +++ b/docs/notebooks/Example_7.ipynb @@ -0,0 +1,7829 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "65da9095", + "metadata": {}, + "source": [ + "# Tutorial 7: Quantifying subcellular protein localization in very large datasets (dGW-OT)" + ] + }, + { + "cell_type": "markdown", + "id": "1b6d513c", + "metadata": {}, + "source": [ + "The Fused Gromov-Wasserstein mapping between two cells with 1000 points takes around 3 s to compute, and the Wasserstein distance between the two mapped distributions takes aroud 18 ms. While number of Fused Gromov-Wasserstein mapping computations scales linearly with the number of cells, the number of Wasserstein computations of mapped distributions scales quadratically, which can result in very long runtimes in datasets with 100s of thousands of cells. For these large datasets, we've provided a deep learning framework, deep Gromow-Wasserstein Optimal Transport (dGW-OT) to reduce the necessary computation. This approach enables users to compute the GW-OT mappings and distances for only a subset of cells, and train a deep learning model to predict the mappings and distances for the remaining cells. \n", + "\n", + "We will demonstrate this approach on a dataset of 16,787 neurons with simulated subcellular protein distributions. For this analysis, we assume that the image data has already been processed into GW-OT cell objects, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/mb1wx32lfqiqpu3mkhni9/sim_neuron_cell_objects.zip?rlkey=113rcvxp1qgpp0wbih63phu5t&dl=0)." + ] + }, + { + "cell_type": "markdown", + "id": "dd0b2ff4", + "metadata": {}, + "source": [ + "First, we must convert the cell objects, as well as the mapped subcellular protein distributions, into cell-specific images that the dGW-OT model can take as input. The `make_NN_training_data` function creates two directories (`cell_images` and `mapped_cell_images`) to store the cell and mapped cell images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ca74919", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/simulated_neurons/cell_objects' # path to saved cell objects\n", + "cell_object_paths = [os.path.join(data_path, fname) for fname in os.listdir(data_path)]\n", + "anchor_ind = 658 # index of anchor cell (which other cells are mapped to)\n", + "anchor_cell_obj_path = cell_object_paths[anchor_ind]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ddb8254", + "metadata": {}, + "outputs": [], + "source": [ + "cell_image_path = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/simulated_neurons'\n", + "make_NN_training_data(save_path=cell_image_path, # path to save cell images\n", + " cell_objects=cell_object_paths,\n", + " reference_cell_object=anchor_cell_obj_path,\n", + " mapped_channel_distributions=mapped_distbs[0], # using the mapped protein distribution\n", + " channel='protein', # this should match the mapped distributions used\n", + " center='nucleus', # center='cell' when using Fused GW mappings, center='nucleus' when using Unbalanced Fused GW mappings\n", + " rescale=False) # rescale=True when using Fused GW mappings, rescale=False when using Unbalanced Fused GW mappings" + ] + }, + { + "cell_type": "markdown", + "id": "1b03bd80", + "metadata": {}, + "source": [ + "To avoid the model overfitting to the training dataset, we split our data into a training, validation, and test set. Since, the dGW-OT model does not need to trained on every pair of training cells, the Wasserstein distances between the mapped protein distributions are only computed for a subset of pairs. In practice, we've observed good model performance when training on around 10,000 cells and 30,000 cell pairs. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b18b026f", + "metadata": {}, + "outputs": [], + "source": [ + "# Generate train/val/test dataset cell pairs\n", + "train_pairs, val_pairs, test_pairs = generate_dataset_split_pairs(indices=list(range(len(cell_object_paths))), \n", + " n_pairs=[35000, 10000, 5000], # number of cell pairs in train/val/test sets\n", + " proportions=[0.7, 0.2, 0.1]) # proportion of cells in train/val/test sets\n", + "\n", + "# Store unique indices in each set\n", + "train_inds = np.unique(train_pairs)\n", + "val_inds = np.unique(val_pairs)\n", + "test_inds = np.unique(test_pairs)\n", + "\n", + "# Compute GW-mapped OT distances for all pairs in train/val/test sets\n", + "train_ot_dists = gw_mapped_ot_pairwise_parallel(cell_object_paths[anchor_ind], mapped_distbs, \n", + " num_processes=12, chunksize=20, index_pairs=train_pairs)[0]\n", + "val_ot_dists = gw_mapped_ot_pairwise_parallel(cell_object_paths[anchor_ind], mapped_distbs, \n", + " num_processes=12, chunksize=20, index_pairs=val_pairs)[0]\n", + "test_ot_dists = gw_mapped_ot_pairwise_parallel(cell_object_paths[anchor_ind], mapped_distbs, \n", + " num_processes=12, chunksize=20, index_pairs=test_pairs)[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ccd1918", + "metadata": {}, + "outputs": [], + "source": [ + "# Create PairedDataset objects for train/val/test sets for training dGW-OT model\n", + "train_data = PairedDataset(\n", + " image_dir = cell_image_path, \n", + " mapped_image_dir = mapped_cell_image_path,\n", + " distances = train_ot_dists.astype('float32'), \n", + " image_pairs = train_pairs,\n", + " transform = transforms.Compose([transforms.ToImage(),\n", + " transforms.ToDtype(torch.float32)]),\n", + ")\n", + "\n", + "val_data = PairedDataset(\n", + " image_dir = cell_image_path, \n", + " mapped_image_dir = mapped_cell_image_path,\n", + " distances = val_ot_dists.astype('float32'), \n", + " image_pairs = val_pairs,\n", + " transform = transforms.Compose([transforms.ToImage(),\n", + " transforms.ToDtype(torch.float32)]),\n", + ")\n", + "\n", + "test_data = PairedDataset(\n", + " image_dir = cell_image_path, \n", + " mapped_image_dir = mapped_cell_image_path,\n", + " distances = test_ot_dists.astype('float32'), \n", + " image_pairs = test_pairs,\n", + " transform = transforms.Compose([transforms.ToImage(),\n", + " transforms.ToDtype(torch.float32)]),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "112f9ac4", + "metadata": {}, + "source": [ + "We initialize the dGW-OT model and begin the two-stage training process. First, during pretraining, the model learns the Fused (Unbalanced) Gromov-Wasserstein mapping operation. More specifically, for each cell, the model learns to predict the subcellular protein distribution after mapping to the anchor cell.\n", + "\n", + "The dGW-OT model pretraining took around 24 hours running on a Nvidia RTX 4500 Ada." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e95eaaf0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=EfficientNet_B4_Weights.IMAGENET1K_V1`. You can also use `weights=EfficientNet_B4_Weights.DEFAULT` to get the most up-to-date weights.\n", + " warnings.warn(msg)\n", + "Epoch 1/50: 100%|██████████| 2099/2099 [31:43<00:00, 1.10it/s, loss=0.319]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/50, Loss: 0.395456\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 2/50: 100%|██████████| 2099/2099 [32:10<00:00, 1.09it/s, loss=0.21] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 2/50, Loss: 0.321780\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 3/50: 100%|██████████| 2099/2099 [26:44<00:00, 1.31it/s, loss=0.203] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 3/50, Loss: 0.287171\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 4/50: 100%|██████████| 2099/2099 [26:33<00:00, 1.32it/s, loss=0.0909]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 4/50, Loss: 0.214656\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 5/50: 100%|██████████| 2099/2099 [25:30<00:00, 1.37it/s, loss=0.111] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 5/50, Loss: 0.186027\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 6/50: 100%|██████████| 2099/2099 [25:34<00:00, 1.37it/s, loss=0.814] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 6/50, Loss: 0.162329\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 7/50: 100%|██████████| 2099/2099 [25:44<00:00, 1.36it/s, loss=0.133] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 7/50, Loss: 0.147380\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 8/50: 100%|██████████| 2099/2099 [25:36<00:00, 1.37it/s, loss=0.166] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 8/50, Loss: 0.132804\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 9/50: 100%|██████████| 2099/2099 [25:40<00:00, 1.36it/s, loss=0.112] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 9/50, Loss: 0.125499\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 10/50: 100%|██████████| 2099/2099 [25:41<00:00, 1.36it/s, loss=0.201] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 10/50, Loss: 0.116555\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 11/50: 100%|██████████| 2099/2099 [25:15<00:00, 1.39it/s, loss=0.14] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 11/50, Loss: 0.114798\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 12/50: 100%|██████████| 2099/2099 [25:30<00:00, 1.37it/s, loss=0.0448]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 12/50, Loss: 0.107108\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 13/50: 100%|██████████| 2099/2099 [25:31<00:00, 1.37it/s, loss=0.0551]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 13/50, Loss: 0.103031\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 14/50: 100%|██████████| 2099/2099 [25:38<00:00, 1.36it/s, loss=0.252] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 14/50, Loss: 0.098223\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 15/50: 100%|██████████| 2099/2099 [26:35<00:00, 1.32it/s, loss=0.0921]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 15/50, Loss: 0.094357\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 16/50: 100%|██████████| 2099/2099 [25:32<00:00, 1.37it/s, loss=0.134] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 16/50, Loss: 0.093203\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 17/50: 100%|██████████| 2099/2099 [25:32<00:00, 1.37it/s, loss=0.0605]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 17/50, Loss: 0.090122\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 18/50: 100%|██████████| 2099/2099 [25:29<00:00, 1.37it/s, loss=0.0553]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 18/50, Loss: 0.086902\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 19/50: 100%|██████████| 2099/2099 [25:34<00:00, 1.37it/s, loss=0.0733]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 19/50, Loss: 0.083662\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 20/50: 100%|██████████| 2099/2099 [25:40<00:00, 1.36it/s, loss=0.0487]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 20/50, Loss: 0.082714\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 21/50: 100%|██████████| 2099/2099 [25:36<00:00, 1.37it/s, loss=0.104] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 21/50, Loss: 0.081393\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 22/50: 100%|██████████| 2099/2099 [25:43<00:00, 1.36it/s, loss=0.0671]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 22/50, Loss: 0.078136\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 23/50: 100%|██████████| 2099/2099 [25:38<00:00, 1.36it/s, loss=0.115] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 23/50, Loss: 0.077529\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 24/50: 100%|██████████| 2099/2099 [25:29<00:00, 1.37it/s, loss=0.0573]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 24/50, Loss: 0.077468\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 25/50: 100%|██████████| 2099/2099 [25:39<00:00, 1.36it/s, loss=0.0407]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 25/50, Loss: 0.074328\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 26/50: 100%|██████████| 2099/2099 [25:49<00:00, 1.35it/s, loss=0.0787]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 26/50, Loss: 0.071749\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 27/50: 100%|██████████| 2099/2099 [25:39<00:00, 1.36it/s, loss=0.0695]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 27/50, Loss: 0.070086\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 28/50: 100%|██████████| 2099/2099 [31:35<00:00, 1.11it/s, loss=0.0704]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 28/50, Loss: 0.068668\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 29/50: 100%|██████████| 2099/2099 [32:49<00:00, 1.07it/s, loss=0.0388]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 29/50, Loss: 0.064706\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 30/50: 100%|██████████| 2099/2099 [32:25<00:00, 1.08it/s, loss=0.0637]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 30/50, Loss: 0.063919\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 31/50: 100%|██████████| 2099/2099 [32:08<00:00, 1.09it/s, loss=0.0571]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 31/50, Loss: 0.061969\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 32/50: 100%|██████████| 2099/2099 [32:03<00:00, 1.09it/s, loss=0.0344]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 32/50, Loss: 0.062507\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 33/50: 100%|██████████| 2099/2099 [31:52<00:00, 1.10it/s, loss=0.0394]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 33/50, Loss: 0.059359\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 34/50: 100%|██████████| 2099/2099 [32:10<00:00, 1.09it/s, loss=0.0489]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 34/50, Loss: 0.058132\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 35/50: 100%|██████████| 2099/2099 [31:55<00:00, 1.10it/s, loss=0.0799]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 35/50, Loss: 0.057911\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 36/50: 100%|██████████| 2099/2099 [32:38<00:00, 1.07it/s, loss=0.0397]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 36/50, Loss: 0.056321\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 37/50: 100%|██████████| 2099/2099 [32:11<00:00, 1.09it/s, loss=0.0427]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 37/50, Loss: 0.054310\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 38/50: 100%|██████████| 2099/2099 [28:48<00:00, 1.21it/s, loss=0.0631]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 38/50, Loss: 0.056188\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 39/50: 100%|██████████| 2099/2099 [30:51<00:00, 1.13it/s, loss=0.0767] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 39/50, Loss: 0.052495\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 40/50: 100%|██████████| 2099/2099 [30:20<00:00, 1.15it/s, loss=0.0486] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 40/50, Loss: 0.050635\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 41/50: 100%|██████████| 2099/2099 [30:57<00:00, 1.13it/s, loss=0.0269]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 41/50, Loss: 0.049588\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 42/50: 100%|██████████| 2099/2099 [27:57<00:00, 1.25it/s, loss=0.0557] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 42/50, Loss: 0.050044\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 43/50: 100%|██████████| 2099/2099 [28:45<00:00, 1.22it/s, loss=0.0321]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 43/50, Loss: 0.047658\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 44/50: 100%|██████████| 2099/2099 [32:27<00:00, 1.08it/s, loss=0.0464]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 44/50, Loss: 0.047649\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 45/50: 100%|██████████| 2099/2099 [32:20<00:00, 1.08it/s, loss=0.0456]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 45/50, Loss: 0.046697\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 46/50: 100%|██████████| 2099/2099 [33:05<00:00, 1.06it/s, loss=0.0484]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 46/50, Loss: 0.045813\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 47/50: 100%|██████████| 2099/2099 [32:50<00:00, 1.06it/s, loss=0.06] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 47/50, Loss: 0.046260\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 48/50: 100%|██████████| 2099/2099 [31:56<00:00, 1.10it/s, loss=0.031] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 48/50, Loss: 0.044359\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 49/50: 100%|██████████| 2099/2099 [33:13<00:00, 1.05it/s, loss=0.06] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 49/50, Loss: 0.043685\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 50/50: 100%|██████████| 2099/2099 [44:40<00:00, 1.28s/it, loss=0.0741]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 50/50, Loss: 0.044747\n" + ] + } + ], + "source": [ + "model = dGWOTNetwork(embedding_size=50, image_size=image_shape[0])\n", + "model = pretrain_model(train_data, model, batch_size=8, epochs=50, lr=1e-3, device='cuda')\n", + "torch.save(model.state_dict(), \"/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/pretrained_sim_neuron.pth\")" + ] + }, + { + "cell_type": "markdown", + "id": "bd50f6b6", + "metadata": {}, + "source": [ + "Next, during training, the model learns to extract features that preserve the GW-OT distances between cells in the feature space, in addition to predicting the mapped protein distributions.\n", + "\n", + "The model is optimized with respect to two main loss components. The distance loss measures how well the the GW-OT distances are preserved in the model's latent feature space, while the reconstruction loss measures accuractely the model predicts the mapped protein distributions. The `dist_weight` parameter controls the relative weighting of these loss components during training. Ideally, the relatively contribution of both losses, which can be viewed by setting `show_loss_components = True`, should be around the same order of magnitude.\n", + "\n", + "To avoid overfitting, we apply L1 regularization (adjusted by `weight_decay`) and L2 regularization (adjusted by `sparsity_weight`, and `sparsity_target`). If the dGW-OT model is overfitting, you could experiment with increasing the `weight_decay` and `sparsity_weight`, or decreasing `sparsity_target`, to further regularize the model to resolve the issue.\n", + "\n", + "The dGW-OT model training took around 48 hours running on a Nvidia RTX 4500 Ada." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6704bb14", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training on device: cuda\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading pretrained weights from /home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/pretrained_sim_neuron.pth\n", + "Starting training for 25 epochs...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1/25: Train Loss: 141.416748, Val Loss: 1.089571\n", + " → New best model saved (val_loss: 1.089571)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 2/25: Train Loss: 0.840245, Val Loss: 0.695685\n", + " → New best model saved (val_loss: 0.695685)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 3/25: Train Loss: 0.717471, Val Loss: 0.754261\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 4/25: Train Loss: 0.792152, Val Loss: 0.692320\n", + " → New best model saved (val_loss: 0.692320)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 5/25: Train Loss: 0.745411, Val Loss: 0.646097\n", + " → New best model saved (val_loss: 0.646097)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 6/25: Train Loss: 0.722791, Val Loss: 0.721418\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 7/25: Train Loss: 0.638340, Val Loss: 0.677450\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 8/25: Train Loss: 0.588748, Val Loss: 0.703082\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 9/25: Train Loss: 0.543514, Val Loss: 0.692757\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 10/25: Train Loss: 0.513111, Val Loss: 0.669878\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 11/25: Train Loss: 0.472981, Val Loss: 0.640772\n", + " → New best model saved (val_loss: 0.640772)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 12/25: Train Loss: 0.449233, Val Loss: 0.549593\n", + " → New best model saved (val_loss: 0.549593)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 13/25: Train Loss: 0.430085, Val Loss: 0.570916\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 14/25: Train Loss: 0.401759, Val Loss: 0.550535\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 15/25: Train Loss: 0.391115, Val Loss: 0.569693\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 16/25: Train Loss: 0.377396, Val Loss: 0.542184\n", + " → New best model saved (val_loss: 0.542184)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 17/25: Train Loss: 0.361147, Val Loss: 0.554994\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 18/25: Train Loss: 0.347979, Val Loss: 0.631908\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 19/25: Train Loss: 0.335832, Val Loss: 0.513285\n", + " → New best model saved (val_loss: 0.513285)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 20/25: Train Loss: 0.316852, Val Loss: 0.553770\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 21/25: Train Loss: 0.311076, Val Loss: 0.506876\n", + " → New best model saved (val_loss: 0.506876)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 22/25: Train Loss: 0.297790, Val Loss: 0.498590\n", + " → New best model saved (val_loss: 0.498590)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 23/25: Train Loss: 0.293798, Val Loss: 0.523204\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 24/25: Train Loss: 0.282792, Val Loss: 0.474511\n", + " → New best model saved (val_loss: 0.474511)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 25/25: Train Loss: 0.274882, Val Loss: 0.466883\n", + " → New best model saved (val_loss: 0.466883)\n", + "\n", + "Evaluating on test set...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/cajal/subcellular_dl.py:1128: UserWarning: Using a target size (torch.Size([8])) that is different to the input size (torch.Size([8, 1])). This will likely lead to incorrect results due to broadcasting. Please ensure they have the same size.\n", + " with torch.no_grad():\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final Test Loss: 10.465317\n", + "\n", + "Saving model...\n", + "Saved DWE model to /home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models//sim_neuron_final.pth\n" + ] + } + ], + "source": [ + "# Train the DGWOTE model with the prepared datasets\n", + "models, train_losses, val_losses = train_dGWOT(\n", + " train_data, val_data, test_data,\n", + " save_path='/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/',\n", + " dataset_name='sim_neuron',\n", + " embedding_size=50, # 50-dimensional embeddings\n", + " image_shape=(256, 256), # Input image shape\n", + " device='cuda', # Use GPU if available\n", + " batch_size=8, # Batch size for training\n", + " epochs=25, # Number of epochs\n", + " learning_rate=0.001, # Adam learning rate\n", + " dist_weight=0.1, # Distance weight (vs reconstruction loss)\n", + " early_stopping=False, # Disable early stopping\n", + " weight_decay=1e-4, # L2 regularization weight\n", + " lr_gamma=0.95, # Learning rate decay factor\n", + " sparsity_weight=1e-3, # Sparsity weight for the embedding loss\n", + " sparsity_target=0.1, # Target sparsity for the embedding loss\n", + " pretrained_path=\"/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/pretrained_sim_neuron.pth\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5a5a11d0", + "metadata": {}, + "source": [ + "The `train_dGWOT` function saves two versions of the model, the best performing model based on performance on validation dataset (`_best.pth`), and the final model after all training epochs (`_final.pth`). Here, we load the best model based on validation loss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cff2c062", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=EfficientNet_B4_Weights.IMAGENET1K_V1`. You can also use `weights=EfficientNet_B4_Weights.DEFAULT` to get the most up-to-date weights.\n", + " warnings.warn(msg)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded dGWOT model from /home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/sim_neuron_best.pth\n", + "Config: {'input_channels': 3, 'embedding_size': 50, 'image_size': 256}\n" + ] + } + ], + "source": [ + "# load best model\n", + "model = load_dGWOT_model('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/sim_neuron_best.pth')" + ] + }, + { + "cell_type": "markdown", + "id": "fefc2421", + "metadata": {}, + "source": [ + "To evaluate model performance, we can look at how well the true GW-OT distances are preserved in the dGW-OT feature space. We can also look at how well the model predicted the mapped protein distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7cf85e3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Extracting embeddings for 1670 unique images...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Extracting embeddings: 0%| | 0/27 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_distance_predictions(model, test_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8fcfd80", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABb4AAAJSCAYAAAAMOtMPAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAA0b9JREFUeJzs3XmcHHW1///3p5ZeZstuEpZAgLBFFgHDYrgBLoKyiCigIEsEQRYX+IHKVRBFQVHky/2qoNzrjX4R7mURAa+iBkRB2TdFQNmXkH2ZfXqpqvP7o7o7M5mZZBKSTGfyej4eA5nq6u7qYXKoOnU+5zgzMwEAAAAAAAAAMEJ4w30AAAAAAAAAAACsTyS+AQAAAAAAAAAjColvAAAAAAAAAMCIQuIbAAAAAAAAADCikPgGAAAAAAAAAIwoJL4BAAAAAAAAACMKiW8AAAAAAAAAwIhC4hsAAAAAAAAAMKKQ+AYAAAAAAAAAjCgkvtejr33ta3LOrdNzf/rTn8o5p9dff339HlQvr7/+upxz+ulPf7rB3mN1Bvr5bLvttpo9e/awHA8A4taaELeA4UWMWj1iFDC8iFGrR4wCNh7i0eoRjzZfJL4lPffcczr55JO15ZZbKpvNaostttAnPvEJPffcc8N9aMNq0aJFuuiii7TzzjuroaFBjY2N2nvvvfXNb35Tra2tG/VYbrnlFp188smaNm2anHM66KCDBt23WCzqS1/6krbYYgvl83ntu+++mjt3br/9yuWyvv71r2u77bZTNpvVdtttp29+85uKoqjPfo8//rg+85nPaPr06WpsbNSUKVN0wgkn6MUXX+z3mo899pjOPfdc7b333grDcJ3/xwOsCXFrYPUUtySpo6NDX/ziFzV16lRls1ltueWWOu6449Td3V3bZ8GCBbr44ot18MEHq7m5Wc45/fGPfxz0NR966CHNnDlTDQ0NmjRpkj73uc+ps7Oz335DjYXAhkCMGlg9xajOzk6df/752mqrrZTNZrXLLrvo+uuv77ffQQcdJOfcgF9hGPbZ94ILLtBee+2lsWPHqqGhQbvssou+9rWvDRijnnzySX3gAx9QS0uLmpubddhhh+mZZ57ZUB8X6IMYNbB6ilG9vfLKK8rlcnLO6Yknnuj3eGtrq8466yxNmDBBjY2NOvjgg/XUU0/1269QKOhb3/qWdt11VzU0NGjLLbfU8ccf3++/ezUJONDXwoULN9jnxOaJeDSweopHa5OP6u2KK66Qc07vfve7B3y8VCrpyiuv1M4776xcLqeJEyfqyCOP1Lx582r7dHZ26rLLLtMHPvABjR07do03D5Ik0fXXX68999xT+Xxe48aN0yGHHKK//vWva/ORR5RguA9guN1xxx068cQTNXbsWJ1xxhmaOnWqXn/9df3kJz/R7bffrv/5n//RscceO6TXuuSSS3TxxRev03Gccsop+vjHP65sNrtOz1/fHn/8cR1xxBHq7OzUySefrL333luS9MQTT+jb3/62HnjgAf3+97/faMdz/fXX68knn9R73/teLVu2bLX7zp49W7fffrvOP/98TZs2TT/96U91xBFH6P7779fMmTNr+5188sm67bbbdPrpp2ufffbRI488oksvvVRvvvmmbrjhhtp+V111lf7yl7/o+OOP1+67766FCxfqBz/4gfbaay898sgjfYLYb37zG/3nf/6ndt99d2233XYDJseBd4q4NbB6i1ttbW2aNWuW5s2bp7POOks77LCDlixZogcffFDFYlENDQ2SpH/+85+66qqrNG3aNO222256+OGHB33NZ555Rv/6r/+qXXbZRddcc43mzZunq6++Wi+99JLuueeePvsONRYC6xsxamD1FKPiONbhhx+uJ554Quedd56mTZum3/3udzr33HO1YsUKffnLX67t+5WvfEWf+tSn+jy/q6tLZ599tg477LA+2x9//HEdeOCB+uQnP6lcLqenn35a3/72t3XvvffqgQcekOelNTdPPfWUZs6cqa233lqXXXaZkiTRddddp1mzZumxxx7TTjvttOF/CNhsEaMGVk8xalUXXHCBgiBQsVjs91iSJDryyCP117/+VV/4whc0fvx4XXfddTrooIP05JNPatq0abV9P/GJT+juu+/WmWeeqb322kvz58/XD3/4Q+2///569tlntc022/R57csvv1xTp07ts2306NEb5DNi80Q8Gli9xaO1yUdVzZs3T1deeaUaGxsHfLxcLuvII4/UQw89pDPPPFO77767VqxYoUcffVRtbW3aaqutJElLly7V5ZdfrilTpmiPPfZYbYGUJJ1++um66aabdOqpp+ozn/mMurq69PTTT2vx4sVr9ZlHFNuMvfzyy9bQ0GA777yzLV68uM9jS5YssZ133tkaGxvtlVdeWe3rdHZ2bsjDXG9ee+01k2Rz5sxZ7X4rVqywLbfc0iZOnGgvvPBCv8cXLlxo3/jGN9b6/S+77DJb9Vdum222sdNOO22Nz33zzTctjmMzM5s+fbrNmjVrwP0effRRk2Tf/e53a9t6enps++23t/3337+27bHHHjNJdumll/Z5/oUXXmjOOfvrX/9a2/aXv/zFisVin/1efPFFy2az9olPfKLP9oULF1p3d7eZmZ133nn9Pi/wThG3BlaPceucc86x0aNH26uvvrra/drb223ZsmVmZnbbbbeZJLv//vsH3PeDH/ygTZ482dra2mrb/uM//sMk2e9+97vatqHGQmB9I0YNrN5i1K233mqS7Cc/+Umf7R/96Ectl8vZokWLVvv8G2+80STZTTfdtMZjvPrqq02SPfzww7VtRxxxhI0ZM8aWLl1a2zZ//nxramqyj3zkI2t8TWBdEaMGVm8xqrff/va3lslk7JJLLjFJ9vjjj/d5/JZbbjFJdtttt9W2LV682EaPHm0nnnhibdu8efNMkl100UV9nv+HP/zBJNk111xT2zZnzpwB3wtYn4hHA6vHeDTUfFRvH/vYx+yQQw6xWbNm2fTp0/s9ftVVV1kYhvboo4+u9nUKhYItWLDAzMwef/zx1f4Mq/HwjjvuWOPxbU4261Yn3/3ud9Xd3a0bbrhBEyZM6PPY+PHj9eMf/1hdXV36zne+U9te7Qv0/PPP66STTtKYMWNqlXMD9Qzq6enR5z73OY0fP17Nzc360Ic+pLffflvOOX3ta1+r7TdQT6Vtt91WRx11lP785z9rxowZyuVy2m677fT//t//6/Mey5cv10UXXaTddttNTU1Namlp0Qc/+MF1Xsrw4x//WG+//bauueYa7bzzzv0enzhxoi655JI+2+655x4deOCBamxsVHNzs4488sj1ujRn6623rlUJrc7tt98u3/d11lln1bblcjmdccYZevjhh/XWW29Jkh588EFJ0sc//vE+z//4xz8uM9Mtt9xS23bAAQcok8n02W/atGmaPn26XnjhhT7bJ06cqHw+v3YfDlgLxK2B1Vvcam1t1Zw5c3TWWWdp6tSpKpVKA1YpSVJzc7PGjh27xtdsb2/X3LlzdfLJJ6ulpaW2/dRTT1VTU5NuvfXW2rahxkJgfSNGDazeYtTqzoMKhYLuuuuu1T7/5ptvVmNjo4455pg1vte2224rSX2WJT/44IM69NBDNW7cuNq2yZMna9asWfrf//3fAVujAOsDMWpg9Rajqsrlsj7/+c/r85//vLbffvsB97n99ts1ceJEfeQjH6ltmzBhgk444QTdddddtfOvjo6O2mfpbfLkyZI06DVcR0eH4jh+x58FWBXxaGD1GI+Gmo+qeuCBB3T77bfr2muvHfDxJEn07//+7zr22GM1Y8YMRVHUpxVmb9lsVpMmTRrS+15zzTWaMWOGjj32WCVJoq6uriEf80i2WSe+f/WrX2nbbbfVgQceOODj//Iv/6Jtt91Wv/71r/s9dvzxx6u7u1tXXnmlzjzzzEHfY/bs2fr+97+vI444QldddZXy+byOPPLIIR/jyy+/rOOOO07vf//79b3vfU9jxozR7Nmz+/wlfvXVV3XnnXfqqKOO0jXXXKMvfOELevbZZzVr1izNnz9/yO9Vdffddyufz+u4444b0v433nijjjzySDU1Nemqq67SpZdequeff14zZ87coMMRBvL0009rxx137JMUkqQZM2ZIUq13ZPUEaNUTnGrrgSeffHK172NmWrRokcaPH78+DhsYMuLWwOotbv35z39WoVDQDjvsoOOOO04NDQ3K5/N63/vet849bJ999llFUaR99tmnz/ZMJqM999xTTz/9dG3bUGMhsL4RowZWbzGqWCzK9/1+N/aHch60ZMkSzZ07Vx/+8IcHXL4bRZGWLl2q+fPn6/e//70uueQSNTc31+JP9f0HSjI1NDSoVCrp73//+7p+NGC1iFEDq7cYVXXttddqxYoV/ZJcvT399NPaa6+9+iWlZsyYoe7u7lrrye23315bbbWVvve97+lXv/qV5s2bp8cee0xnn322pk6d2u9GoCQdfPDBamlpUUNDgz70oQ/ppZdeWm+fDSAeDaxe49FQxXGsz372s/rUpz6l3XbbbcB9nn/+ec2fP1+77767zjrrLDU2NqqxsVG777677r///nV63/b2dj322GN673vfqy9/+csaNWqUmpqatN122/UpkNosDXfJ+XBpbW01SXbMMcesdr8PfehDJsna29vNbOXyiN7LpqpWXTrx5JNPmiQ7//zz++w3e/Zsk2SXXXZZbVt1OdVrr71W27bNNtuYJHvggQdq2xYvXmzZbNYuvPDC2rZCoVBbdlH12muvWTabtcsvv7zPNg1hacmYMWNsjz32WO0+VR0dHTZ69Gg788wz+2xfuHChjRo1qs/29bHUzWz1S0umT59uhxxySL/tzz33nEmyH/3oR2Zm9otf/MIk2Y033thnvx/96Ecmyd797nev9hiqS3xXXSLcG61OsL4RtwZXb3HrmmuuMUk2btw4mzFjht1000123XXX2cSJE23MmDE2f/78AZ+3ulYn1cd6/2yrjj/+eJs0aVLt+6HGQmB9IkYNrt5i1Pe+9z2TZA8++GCf7RdffLFJsqOOOmrQ537/+983Sfab3/xmwMcffvhhk1T72mmnnfrFtN1228123HFHi6Kotq1YLNqUKVNMkt1+++2rPX5gXRCjBldvMcrMbMGCBdbc3Gw//vGPzWzw9iONjY12+umn93v+r3/9a5Nkv/3tb2vbHn30Udt+++37xKi999671kag6pZbbrHZs2fbz372M/vlL39pl1xyiTU0NNj48ePtzTffXOOxA2tCPBpcPcaj3tbU6uQHP/iBjRo1qta+ZqBWJ3fccUftWnHatGk2Z84cmzNnjk2bNs0ymUyf1ru9ra7VyVNPPVV7zYkTJ9p1111nN910k82YMcOcc3bPPfes1eccSTbbiu/qUqfm5ubV7ld9vL29vc/2s88+e43v8dvf/laSdO655/bZ/tnPfnbIx7nrrrv2uQM4YcIE7bTTTnr11Vdr27LZbO0OdxzHWrZsmZqamrTTTjsNOM16Tdrb29f4c6maO3euWltbdeKJJ2rp0qW1L9/3te+++67z3ap11dPTM+BAhlwuV3tcko444ghts802uuiii3THHXfojTfe0K233qqvfOUrCoKgtt9A/vGPf+i8887T/vvvr9NOO23DfBBgAMStwdVb3Kou03fO6b777tNJJ52kc845R3feeadWrFihH/7wh2v9mtW4NFiM6x23hhoLgfWJGDW4eotRJ510kkaNGqXTTz9dc+fO1euvv64bbrhB1113naTVx4ibb75ZEyZM0Pvf//4BH9911101d+5c3XnnnfriF7+oxsbGfq1Lzj33XL344os644wz9Pzzz+vvf/+7Tj31VC1YsGCN7w+sK2LU4OotRknSl770JW233Xb9huuuam3OecaMGaM999xTF198se68805dffXVev3113X88cerUCjU9jvhhBM0Z84cnXrqqfrwhz+sb3zjG/rd736nZcuW6Yorrlgvnw+bN+LR4OoxHg3VsmXL9NWvflWXXnppv/Y1vVXPizo6OnTfffdp9uzZmj17tu69916ZWZ/2NkNVfc1ly5bprrvu0jnnnKOTTjpJ9913n8aNG6dvfvOb6/ahRoBguA9guFT/IlUDzmAGC0irTnceyBtvvCHP8/rtu8MOOwz5OKdMmdJv25gxY7RixYra99X+QNddd51ee+21Pj3IevdOHKqWlpY1/lyqqsu9DjnkkEFfa2PK5/MD9tGtnshUl9Xmcjn9+te/1gknnKCPfvSjktKA/Z3vfEdXXHGFmpqaBnz9hQsX6sgjj9SoUaNqPXSBjYW4Nbh6i1vVWHP00Uf3iSf77befpk6dqoceemidX3OwGNe7bcBQYyGwPhGjBldvMWrSpEm6++67dcopp+iwww6rve73v/99nXbaaYOeB7366qt6+OGH9ZnPfEZBMPBlREtLiw499FBJ0jHHHKObb75ZxxxzjJ566intsccektIL9rfeekvf/e539bOf/UyStM8+++iLX/zias/DgHeCGDW4eotRjzzyiG688Ubdd999a+yrO9Rznra2Nh144IH6whe+oAsvvLC23z777KODDjpIc+bM0TnnnDPo+8ycOVP77ruv7r333nX5SEAfxKPB1Vs8WhuXXHKJxo4du8abC9W49L73vU9bb711bfuUKVM0c+bMd3StOHXqVO2777617U1NTTr66KP185//XFEUDXr+NpJtfp+4YtSoUZo8ebL+9re/rXa/v/3tb9pyyy37/YXZWEmDwRKrZlb785VXXqlLL71Up59+ur7xjW9o7Nix8jxP559/vpIkWev33HnnnfXMM8+oVCr16/24qurr33jjjQM23N/Yf6kmT56st99+u9/2agXRFltsUds2ffp0/f3vf9fzzz+vFStWaNddd1U+n9cFF1ygWbNm9XuNtrY2ffCDH1Rra6sefPDBPq8FbAzErcHVW9yqxodVByhJ0rve9a4+J4tDVR2+VI1nvS1YsKBPTFqbWAisL8SowdVbjJLS3qGvvvqqnn32WXV1dWmPPfao9eLccccdB3zOzTffLEn6xCc+MeT3+chHPqJTTjlF//M//1NLfEvSFVdcoYsuukjPPfecRo0apd12201f/vKXV/v+wDtBjBpcvcWoL37xizrwwAM1derUWo/epUuXSkrPZd58881aQm7y5MmDnhtJK895fvGLX2jRokX60Ic+1Ge/WbNmqaWlRX/5y19Wm/iW0gF3//znP9/RZwMk4tHq1Fs8GqqXXnpJN9xwg6699to+vc0LhYLK5bJef/11tbS0aOzYsWu8Vuw9u2mo1vSa5XJZXV1dGjVq1Fq/9qZus018S9JRRx2l//iP/9Cf//zn2iTc3h588EG9/vrr+vSnP71Or7/NNtsoSRK99tprmjZtWm37yy+/vM7HPJDbb79dBx98sH7yk5/02d7a2rpOwxePPvpoPfzww/rFL36hE088cbX7Vqdrv+td76pV9wynPffcU/fff7/a29v7/M/h0UcfrT3em3NO06dPr33/m9/8RkmS9PsshUJBRx99tF588UXde++92nXXXTfchwBWg7g1sHqLW3vvvbckDZh8nj9//oATytfk3e9+t4Ig0BNPPKETTjihtr1UKumZZ57ps21tYyGwvhCjBlZvMarK9/0+8aBayTjY+958883afvvttd9++w35PYrFopIkUVtbW7/HxowZ0+f35N5779VWW221TjESGApi1MDqLUa9+eabeuONNwasav3Qhz6kUaNGqbW1VVJ6TvPggw8qSZI+1eGPPvqoGhoaajfSFi1aJEl9qlGlNIEXx7GiKFrjcb366qurbV8ArA3i0cDqLR4N1dtvv60kSfS5z31On/vc5/o9PnXqVH3+85/Xtddeq912201hGA56rbgucWaLLbbQpEmTBn3NXC435BYyI81m2+Nbkr7whS8on8/r05/+tJYtW9bnseXLl+vss89WQ0ODvvCFL6zT6x9++OGSVOuXWPX9739/3Q54EL7v97njJkm33XbbgL/wQ3H22Wdr8uTJuvDCC2tTsHtbvHhxrT/Q4YcfrpaWFl155ZUql8v99l2yZMk6HcO6Ou644xTHsW644YbatmKxqDlz5mjffffts4xkVT09Pbr00ks1efLkPgE2jmN97GMf08MPP6zbbrtN+++//wb9DMDqELcGVm9xa6eddtIee+yhu+66q1ahJEm///3v9dZbbw3aG3d1Ro0apUMPPVQ///nP+yz/u/HGG9XZ2anjjz++tu2dxELgnSBGDazeYtRAlixZoquuukq77777gBePTz/9tF544QWddNJJAz6/tbV1wOP9z//8T0lpO4HVueWWW/T444/r/PPPX2NrA2BdEaMGVm8x6oYbbtAvf/nLPl/V1gFXX321brrpptq+xx13nBYtWqQ77rijtm3p0qW67bbbdPTRR9f6f1cT4P/zP//T573uvvtudXV16T3vec9qP8NvfvMbPfnkk/rABz7wjj8fIBGPBlNv8Wio3v3ud/eLW7/85S81ffp0TZkyRb/85S91xhlnSEpb1xxxxBF66KGH9I9//KP2Gi+88IIeeuihdbpWlKSPfexjeuuttzR37tzatqVLl+quu+7SIYccstmeX23WFd/Tpk3Tz372M33iE5/QbrvtpjPOOKO2nOonP/mJli5dqv/+7/+u3UVaW3vvvbc++tGP6tprr9WyZcu033776U9/+lPtL69zbr18jqOOOkqXX365PvnJT+qAAw7Qs88+q5tuuknbbbfdOr3emDFj9Mtf/lJHHHGE9txzT5188sm16sWnnnpK//3f/11L/ra0tOj666/XKaecor322ksf//jHNWHCBL355pv69a9/rfe97336wQ9+8I4/4wMPPKAHHnhAUhq8urq6asHuX/7lX/Qv//IvkqR9991Xxx9/vP7t3/5Nixcv1g477KCf/exntf+mvZ1wwgnaYosttOuuu6q9vV3/9V//pVdffVW//vWv+9wJu/DCC3X33Xfr6KOP1vLly/Xzn/+8z+ucfPLJtT+/8cYbuvHGGyVJTzzxhCTVjnObbbbRKaec8o5/Fti8EbcGVo9x6//8n/+j97///Zo5c6Y+/elPq62tTddcc4123HHHfktpq3Hiueeek5Qms//85z9LSnvFVV1xxRU64IADNGvWLJ111lmaN2+evve97+mwww7rcyG2NrEQWJ+IUQOrxxg1a9Ys7b///tphhx20cOFC3XDDDers7NT//u//DnhhVE00Ddbm5I9//KM+97nP6bjjjtO0adNUKpX04IMP6o477tA+++zT53zpgQce0OWXX67DDjtM48aN0yOPPKI5c+boAx/4gD7/+c+/488GDIYYNbB6i1HV2QO9VSu8Z82a1edG2nHHHaf99ttPn/zkJ/X8889r/Pjxuu666xTHsb7+9a/X9jv66KM1ffp0XX755XrjjTe033776eWXX9YPfvADTZ48uZaQkqQDDjhA73nPe7TPPvto1KhReuqpp/Rf//Vf2nrrrWstmYB3ing0sHqLR9LQ8lHjx4/Xhz/84X7PvfbaayWp32NXXnml7rvvPh1yyCG1CvH/+3//r8aOHdsvzvzgBz9Qa2trrYXKr371K82bN09SOqy02r7k3/7t33Trrbfqox/9qP6//+//06hRo/SjH/1I5XJZV1555Tv+OWyyDPa3v/3NTjzxRJs8ebKFYWiTJk2yE0880Z599tl++1522WUmyZYsWTLoY711dXXZeeedZ2PHjrWmpib78Ic/bP/85z9Nkn3729+u7TdnzhyTZK+99lpt2zbbbGNHHnlkv/eZNWuWzZo1q/Z9oVCwCy+80CZPnmz5fN7e97732cMPP9xvv9dee80k2Zw5c4b0c5k/f75dcMEFtuOOO1oul7OGhgbbe++97YorrrC2trY++95///12+OGH26hRoyyXy9n2229vs2fPtieeeGK1P59tttnGTjvttDUeS/W5A31ddtllffbt6emxiy66yCZNmmTZbNbe+9732m9/+9t+r3nVVVfZzjvvbLlczsaMGWMf+tCH7Omnn+6336xZswZ971U/z/333z/ofr3/WwDvFHFrYPUUt8zM5s6da/vtt5/lcjkbO3asnXLKKbZgwYJ++w01xpiZPfjgg3bAAQdYLpezCRMm2HnnnWft7e399htqLAQ2BGLUwOopRl1wwQW23XbbWTabtQkTJthJJ51kr7zyyoD7xnFsW265pe21116Dvt7LL79sp556qm233XaWz+ctl8vZ9OnT7bLLLrPOzs5++x522GE2fvx4y2aztvPOO9u3vvUtKxaLazxuYH0gRg2snmLUqqo/r8cff7zfY8uXL7czzjjDxo0bZw0NDTZr1qxB96t+vmw2a+PHj7ePf/zj9uqrr/bZ7ytf+YrtueeeNmrUKAvD0KZMmWLnnHOOLVy4cK2PG1gT4tHA6ikerU0+alWzZs2y6dOnD/jYk08+aYceeqg1NjZac3OzHXPMMfbiiy/222+bbbYZ9P17/zczM3vllVfs2GOPtZaWFsvn83bIIYfYY489tsbPOJI5s1XWJGCDe+aZZ/Se97xHP//5z9dqOBAADBfiFoB6RowCUM+IUQDqBfEIm5vNs8HLRtTT09Nv27XXXivP82rtOQCgnhC3ANQzYhSAekaMAlAviEfAZt7je2P4zne+oyeffFIHH3ywgiDQPffco3vuuUdnnXUWw8UA1CXiFoB6RowCUM+IUQDqBfEIkGh1soHNnTtXX//61/X888+rs7NTU6ZM0Smn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AAAwopD4BgAAAAAAAACMKCS+AQAAAAAAAAAjColvAAAAAAAAAMCIQuIbG8Trr78u55x++tOfDsv7f+1rX5Nzrs+2bbfdVrNnzx6W4wFQ/4hbAOoZMQpAPSNGAagXxCP0VleJ75/+9KdyztW+giDQlltuqdmzZ+vtt98e7sNb76677rph+4tYT8cgSYsWLdJFF12knXfeWQ0NDWpsbNTee++tb37zm2ptbd2ox3LLLbfo5JNP1rRp0+Sc00EHHTTovsViUV/60pe0xRZbKJ/Pa99999XcuXP77Vcul/X1r39d2223nbLZrLbbbjt985vfVBRFffZ7/PHH9ZnPfEbTp09XY2OjpkyZohNOOEEvvvhiv9d87LHHdO6552rvvfdWGIb9Ais2DuLW5nkMUn3FLUnq6OjQF7/4RU2dOlXZbFZbbrmljjvuOHV3d9f2WbBggS6++GIdfPDBam5ulnNOf/zjHwd9zYceekgzZ85UQ0ODJk2apM997nPq7Ozst99QYyE2PmLU5nkMUn3FqM7OTp1//vnaaqutlM1mtcsuu+j666/vt99BBx3U5/e191cYhn32veCCC7TXXntp7Nixamho0C677KKvfe1rA8aoJ598Uh/4wAfU0tKi5uZmHXbYYXrmmWc21MfFWiBGbZ7HINVXjOrtlVdeUS6Xk3NOTzzxRL/HW1tbddZZZ2nChAlqbGzUwQcfrKeeeqrffoVCQd/61re06667qqGhQVtuuaWOP/54Pffcc332W/XvQO+vhQsXbrDPif6IR5vnMUj1FY/WJh/V2xVXXCHnnN797ncP+HipVNKVV16pnXfeWblcThMnTtSRRx6pefPm1fbp7OzUZZddpg984AMaO3bsGm8eJEmi66+/Xnvuuafy+bzGjRunQw45RH/961/X5iNvcMFwH8BALr/8ck2dOlWFQkGPPPKIfvrTn+rPf/6z/v73vyuXyw334a031113ncaPHz+sd33q4Rgef/xxHXHEEers7NTJJ5+svffeW5L0xBNP6Nvf/rYeeOAB/f73v99ox3P99dfrySef1Hvf+14tW7ZstfvOnj1bt99+u84//3xNmzZNP/3pT3XEEUfo/vvv18yZM2v7nXzyybrtttt0+umna5999tEjjzyiSy+9VG+++aZuuOGG2n5XXXWV/vKXv+j444/X7rvvroULF+oHP/iB9tprLz3yyCN9gthvfvMb/ed//qd23313bbfddgMmx7HxELc2r2Oot7jV1tamWbNmad68eTrrrLO0ww47aMmSJXrwwQdVLBbV0NAgSfrnP/+pq666StOmTdNuu+2mhx9+eNDXfOaZZ/Sv//qv2mWXXXTNNddo3rx5uvrqq/XSSy/pnnvu6bPvUGMhhg8xavM6hnqKUXEc6/DDD9cTTzyh8847T9OmTdPvfvc7nXvuuVqxYoW+/OUv1/b9yle+ok996lN9nt/V1aWzzz5bhx12WJ/tjz/+uA488EB98pOfVC6X09NPP61vf/vbuvfee/XAAw/I89L6nqeeekozZ87U1ltvrcsuu0xJkui6667TrFmz9Nhjj2mnnXba8D8ErBExavM6hnqKUau64IILFASBisViv8eSJNGRRx6pv/71r/rCF76g8ePH67rrrtNBBx2kJ598UtOmTavt+4lPfEJ33323zjzzTO21116aP3++fvjDH2r//ffXs88+q2222abPa1f/DvQ2evToDfIZsXrEo83rGOotHq1NPqpq3rx5uvLKK9XY2Djg4+VyWUceeaQeeughnXnmmdp99921YsUKPfroo2pra9NWW20lSVq6dKkuv/xyTZkyRXvsscdqC6Qk6fTTT9dNN92kU089VZ/5zGfU1dWlp59+WosXL16rz7zBWR2ZM2eOSbLHH3+8z/YvfelLJsluueWWYTqyDWP69Ok2a9asIe3b2dk57MewNl577TWTZHPmzFntfitWrLAtt9zSJk6caC+88EK/xxcuXGjf+MY31vr9L7vsMlv113ubbbax0047bY3PffPNNy2OYzNb/c/n0UcfNUn23e9+t7atp6fHtt9+e9t///1r2x577DGTZJdeemmf51944YXmnLO//vWvtW1/+ctfrFgs9tnvxRdftGw2a5/4xCf6bF+4cKF1d3ebmdl5553X7/Ni4yBuDY64tXbeSdw655xzbPTo0fbqq6+udr/29nZbtmyZmZnddtttJsnuv//+Aff94Ac/aJMnT7a2trbatv/4j/8wSfa73/2utm2osRDDgxg1OGLU2lnXGHXrrbeaJPvJT37SZ/tHP/pRy+VytmjRotU+/8YbbzRJdtNNN63xGK+++mqTZA8//HBt2xFHHGFjxoyxpUuX1rbNnz/fmpqa7CMf+cgaXxMbFjFqcMSotfNOzqOqfvvb31omk7FLLrlkwN/LW265xSTZbbfdVtu2ePFiGz16tJ144om1bfPmzTNJdtFFF/V5/h/+8AeTZNdcc01t22B/B7DxEY8GRzxaOxsjH9Xbxz72MTvkkENs1qxZNn369H6PX3XVVRaGoT366KOrfZ1CoWALFiwwM7PHH398tT/Dajy844471nh8w62uWp0M5sADD5SULjvq7R//+IeOO+44jR07VrlcTvvss4/uvvvufs9vbW3VBRdcoG233VbZbFZbbbWVTj31VC1durS2z+LFi3XGGWdo4sSJyuVy2mOPPfSzn/2sz+tU+wRdffXVuuGGG7T99tsrm83qve99rx5//PE++y5cuFCf/OQna0s6J0+erGOOOUavv/66pLS/z3PPPac//elPtaU01SUM1SU2f/rTn3TuuefqXe96V+0OzOzZs7Xtttv2+4wD9RCSpJ///OeaMWOGGhoaNGbMGP3Lv/xL7W7V6o6h+nM7//zztfXWWyubzWqHHXbQVVddpSRJ+v18Z8+erVGjRmn06NE67bTThrwc5Mc//rHefvttXXPNNdp55537PT5x4kRdcsklfbbdc889OvDAA9XY2Kjm5mYdeeSR/ZaMvRNbb711rUpodW6//Xb5vq+zzjqrti2Xy+mMM87Qww8/rLfeekuS9OCDD0qSPv7xj/d5/sc//nGZmW655ZbatgMOOECZTKbPftOmTdP06dP1wgsv9Nk+ceJE5fP5tftw2GiIW8St3jZk3GptbdWcOXN01llnaerUqSqVSgNWKUlSc3Ozxo4du8bXbG9v19y5c3XyySerpaWltv3UU09VU1OTbr311tq2ocZC1BdiFDGqtw0Zo1Z3HlQoFHTXXXet9vk333yzGhsbdcwxx6zxvar/HXv/rB588EEdeuihGjduXG3b5MmTNWvWLP3v//7vgK1RMPyIUcSo3jb09Z+UVkR+/vOf1+c//3ltv/32A+5z++23a+LEifrIRz5S2zZhwgSdcMIJuuuuu2rnXx0dHbXP0tvkyZMladBruI6ODsVx/I4/C9Yv4hHxqLd6yUdVPfDAA7r99tt17bXXDvh4kiT693//dx177LGaMWOGoijq0wqzt2w2q0mTJg3pfa+55hrNmDFDxx57rJIkUVdX15CPeWOry1Ynq6r+5RwzZkxt23PPPaf3ve992nLLLXXxxRersbFRt956qz784Q/rF7/4hY499lhJaY+aAw88UC+88IJOP/107bXXXlq6dKnuvvtuzZs3T+PHj1dPT48OOuggvfzyy/rMZz6jqVOn6rbbbtPs2bPV2tqqz3/+832O5+abb1ZHR4c+/elPyzmn73znO/rIRz6iV199tdZ/8KMf/aiee+45ffazn9W2226rxYsXa+7cuXrzzTe17bbb6tprr9VnP/tZNTU16Stf+Yqk/v9jPPfcczVhwgR99atfXadfoq9//ev62te+pgMOOECXX365MpmMHn30Uf3hD3/QYYcdttpj6O7u1qxZs/T222/r05/+tKZMmaKHHnpI//Zv/6YFCxbU/lKZmY455hj9+c9/1tlnn61ddtlFv/zlL3XaaacN6Rjvvvtu5fN5HXfccUPa/8Ybb9Rpp52mww8/XFdddZW6u7t1/fXXa+bMmXr66acHDMIbytNPP60dd9yxT1JIkmbMmCEpbROw9dZb106AVj3BqbYeePLJJ1f7PmamRYsWafr06evr0LERELeIW1UbOm79+c9/VqFQ0A477KDjjjtOd955p5Ik0f77768f/vCH2nPPPdf6NZ999llFUaR99tmnz/ZMJqM999xTTz/9dG3bUGMh6gsxihhVtaFjVLFYlO/7/W7s9z4POvPMMwd87pIlSzR37lx97GMfG3D5bhRFam1tValU0t///nddcsklam5ursWf6vsPlGRqaGioPW+//fZ7Jx8RGwAxihhVtbGu/6699lqtWLFCl1xyie64444B93n66ae111579UtKzZgxQzfccINefPFF7bbbbtp+++211VZb6Xvf+5522mknvec979H8+fNrs1hWvREoSQcffLA6OzuVyWR0+OGH63vf+16f1ikYPsQj4lFVPeWjpLSd3Gc/+1l96lOf0m677TbgPs8//7zmz5+v3XffXWeddZZ+9rOfqVQqabfddtO///u/6+CDD17r921vb6/NnPvyl7+s73//++rs7NTUqVP17W9/WyeccMI7/Wjr1zBWm/dTXVpy77332pIlS+ytt96y22+/3SZMmGDZbNbeeuut2r7/+q//arvttpsVCoXatiRJ7IADDrBp06bVtn31q18dtPw+SRIzM7v22mtNkv385z+vPVYqlWz//fe3pqYma29vN7OVyyXGjRtny5cvr+171113mST71a9+ZWbpcgmtsux7IIMtW6j+HGbOnGlRFPV57LTTTrNtttmm33NWXUrx0ksvmed5duyxx9aWSaz6uVd3DN/4xjessbHRXnzxxT7bL774YvN93958800zM7vzzjtNkn3nO9+p7RNFkR144IFDWloyZswY22OPPVa7T1VHR4eNHj3azjzzzD7bFy5caKNGjeqzfX0sdTNb/dKS6dOn2yGHHNJv+3PPPWeS7Ec/+pGZmf3iF78wSXbjjTf22e9HP/qRSbJ3v/vdqz2G6hLfVZcI90ark+FD3Or7cyBu9bUx4tY111xT+288Y8YMu+mmm+y6666ziRMn2pgxY2z+/PkDPm91rU6qjz3wwAP9Hjv++ONt0qRJte+HGgsxPIhRfX8OxKi+NkaM+t73vmeS7MEHH+yz/eKLLzZJdtRRRw363O9///smyX7zm98M+PjDDz9skmpfO+20U7+Ytttuu9mOO+7Y5797sVi0KVOmmCS7/fbbV3v82LCIUX1/DsSovjbW9d+CBQusubnZfvzjH5vZ4C0vGhsb7fTTT+/3/F//+tcmyX7729/Wtj366KO2/fbb94lRe++9d62NQNUtt9xis2fPtp/97Gf2y1/+0i655BJraGiw8ePH137m2DiIR31/DsSjvuotH2Vm9oMf/MBGjRplixcvNjMbsNXJHXfcUfu9mTZtms2ZM8fmzJlj06ZNs0wm06f1bm+ra3Xy1FNP1V5z4sSJdt1119lNN91kM2bMMOec3XPPPWv1OTe0umx1cuihh2rChAnaeuutddxxx6mxsVF33313bXnF8uXL9Yc//EEnnHCCOjo6tHTpUi1dulTLli3T4Ycfrpdeeqk2dfcXv/iF9thjj9odt96qSzF+85vfaNKkSTrxxBNrj4VhqM997nPq7OzUn/70pz7P+9jHPtbnbl916curr74qKa3szWQy+uMf/6gVK1as88/hzDPPlO/76/TcasXfV7/61X53pAdagrKq2267TQceeKDGjBlT+/kuXbpUhx56qOI41gMPPCAp/dkFQaBzzjmn9lzf9/XZz352SMfZ3t6u5ubmIe07d+5ctba26sQTT+xzTL7va99999X9998/pNdZX3p6epTNZvttrw686OnpkSQdccQR2mabbXTRRRfpjjvu0BtvvKFbb71VX/nKVxQEQW2/gfzjH//Qeeedp/3333/Idy0xPIhbKeJWXxsjblWX6TvndN999+mkk07SOeecozvvvFMrVqzQD3/4w7V+zWpcGizG9Y5bQ42FGF7EqBQxqq+NEaNOOukkjRo1Sqeffrrmzp2r119/XTfccIOuu+46SauPETfffLMmTJig97///QM+vuuuu2ru3Lm688479cUvflGNjY39Wpece+65evHFF3XGGWfo+eef19///nedeuqpWrBgwRrfHxsPMSpFjOprY13/felLX9J2223Xb7juqtbmnGfMmDHac889dfHFF+vOO+/U1Vdfrddff13HH3+8CoVCbb8TTjhBc+bM0amnnqoPf/jD+sY3vqHf/e53WrZsma644or18vmwdohHKeJRX/WWj1q2bJm++tWv6tJLL9WECRMG3a96XtTR0aH77rtPs2fP1uzZs3XvvffKzPSd73xnrd+7+prLli3TXXfdpXPOOUcnnXSS7rvvPo0bN07f/OY31+1DbSB12erkhz/8oXbccUe1tbXpv/7rv/TAAw/0+R/Myy+/LDPTpZdeqksvvXTA11i8eLG23HJLvfLKK/roRz+62vd74403NG3atH5/IXfZZZfa471NmTKlz/fVoFMNKtlsVldddZUuvPBCTZw4Ufvtt5+OOuoonXrqqUPulyOp31TntfHKK6/I8zztuuuu6/T8l156SX/7298G/QtUndL6xhtvaPLkyWpqaurz+E477TSk92lpaan1QBvKMUnSIYccMuhrbUz5fH7APrrVE5nqstpcLqdf//rXOuGEE2q/i9lsVt/5znd0xRVX9PvZVS1cuFBHHnmkRo0aVeuhi/pF3EoRt/ofk7Rh41Y11hx99NF9PtN+++2nqVOn6qGHHlrn1xwsxvVuGzDUWIjhRYxKEaP6H5O0YWPUpEmTdPfdd+uUU07RYYcdVnvd73//+zrttNMGPQ969dVX9fDDD+szn/mMgmDgS5aWlhYdeuihkqRjjjlGN998s4455hg99dRT2mOPPSRJZ599tt566y1997vfrfVL3WefffTFL35xtedh2LiIUSliVP9jkjZsjHrkkUd044036r777ltjX92hnvO0tbXpwAMP1Be+8AVdeOGFtf322WcfHXTQQZozZ06fRN2qZs6cqX333Vf33nvvunwkvEPEoxTxqP8xSfWTj7rkkks0duzYNSb5q3Hpfe97X5/2k1OmTNHMmTPf0bXi1KlTte+++9a2NzU16eijj9bPf/5zRVE06PnbxlYfR7GKGTNm1PqKfvjDH9bMmTN10kkn6Z///KeamppqzewvuugiHX744QO+xg477LDBjm+wBKSZ1f58/vnn6+ijj9add96p3/3ud7r00kv1rW99S3/4wx/0nve85/9v79+DLLvK+/7/86y19z7n9FUzmhkhcREgGfhxCdg4AWNsOcTGDjHEFyCUY2wcVxynnOAQbMfElaJCApQrdkLFJNhUAkmZOL5g9DWJXYmJf+RrYhOHqy9AzEWAEJJGmltPX85l772e7x9rn9PdmhlpZiTN6TnzflU1o+nu6Tk9GpbW+uxnPc9F/T7nCwsu9HTskR6CkVLSt33bt+mnfuqnzvvxpzzlKY/I7/O0pz1Nn/zkJzWZTM7p/Xi+1yTlvkrnW7Cv9P+pbrzxxtmT3L2mFUQ33XTT7H3PeMYz9Gd/9mf69Kc/rdOnT+vpT3+6BoOBXve61+m2224752tsbGzor/7Vv6ozZ87oQx/60L6vhYOJdStj3Tr3NUmP7ro1XR8e2JdPko4dO3ZZlR7T4UvT9Wyve+65Z9+adClrIeaHNSpjjTr3NUmP/t7qm7/5m3XHHXfoT//0T7W9va1nP/vZuvvuuyVd+Pv+lV/5FUnS3/ybf/Oif5/v+Z7v0atf/Wr96q/+6iz4lqQ3v/nN+omf+Al96lOf0vr6up71rGfpH//jf/ygvz+uLNaojDXq3NckPbpr1E/91E/pm77pm/SkJz1p1st5OnTwnnvu0Z133jkLGm+88cYL7o2k3T3Pb/7mb+r48eN62ctetu/zbrvtNq2trekP/uAPHjT4lvKAuz//8z9/WN8bLg/rUcZ6dO5rkg5GHvW5z31O73znO/W2t71ttp+S8kO4uq71pS99SWtrazp8+PBDnhX3zm66WA/1Neu61vb2ttbX1y/5az8aDmTwvVeMUW9961v1l//yX9bb3/52/fRP/7Se/OQnS8rXP6ZVHhdyyy236M/+7M8e9HNuvvlm/cmf/IlSSvuesv3f//t/Zx+/HLfccote//rX6/Wvf70+97nP6TnPeY5+/ud/Xu95z3skXdwVjwc6dOjQeSfUPvAp4C233KKUkj796U8/6GCzC72GW265RVtbWw/553vzzTfr937v97S1tbXvKdvF/kf6pS99qT784Q/rN3/zN/dd7bnQa5Ly/5Ee6nVdCc95znP0wQ9+UGfPnt33dO+P/uiPZh/fy8z2Daj8nd/5HaWUzvleRqORXvrSl+qzn/2s/sf/+B+X/ZQU88O6tR/r1qO7bj33uc+VpPOGz3ffffd5J5Q/lGc+85kqikIf/ehH9w0nmUwm+uQnP7nvfZe6FmL+WKP2Y426MnurGOO+P7dpJeOFft9f+ZVf0S233HJJgyfH47FSStrY2DjnY4cOHdILX/jCfb//4x73uMtaI/HoYo3ajzXq0V2j7rzzTn35y18+b3Xry172Mq2vr8/+/J/znOfoQx/60Dl/b/7oj/5IS0tLszDu+PHjks4NA91dbduqaZqHfF133HHHg7YvwJXBerQf69HByKO++tWvKqWk1772tXrta197zsef9KQn6cd//Mf1tre9Tc961rNUluUFz4qXs87cdNNNesxjHnPBr9nv9y+6hcyVcCB7fD/Qt3zLt+gv/aW/pLe97W0ajUY6duyYvuVbvkW/9Eu/dN4nrvfff//sn7/3e79Xf/zHf6zbb7/9nM+bPhF7yUteonvvvVe/9mu/NvtY0zT6hV/4Ba2srJy3IvfB7Ozs7OvbJeX/k6yuru67GrW8vHzeRePB3HLLLdrY2NCf/MmfzN53zz33nPP9fdd3fZdCCHrTm940ezI1tfdJ4IVewytf+Up9+MMf1n//7//9nI+dOXNm9h/rl7zkJWqaRu94xztmH2/bVr/wC79wUd/Pj/7oj+rGG2/U61//en32s5895+P33XffrD/Qt3/7t2ttbU1vectbVNf1OZ+799/7lfDyl79cbdvqne985+x94/FY7373u/W85z1v3zWSBxoOh/on/+Sf6MYbb9y3wLZtq7/xN/6GPvzhD+s3fuM39A3f8A2P6veARw/r1v6vw7r16K1bT33qU/XsZz9bv/VbvzWrUJKk3/3d39VXvvKVC/bGfTDr6+v61m/9Vr3nPe/Zd/3vl3/5l7W1taVXvOIVs/c9nLUQ88Matf/rsEZd2b3V/fffr5/92Z/VX/gLf+G8h8dPfOIT+sxnPqPv+77vO++vP3PmzHlf77/7d/9OkmaVehfya7/2a/rIRz6if/AP/sFDtjbAfLBG7f86rFGP3hr1zne+U7fffvu+t2nrgJ/7uZ/Tf/pP/2n2uS9/+ct1/Phxve9975u978SJE/qN3/gNvfSlL521w5gG4L/6q7+67/d6//vfr+3t7X0Vt+f7Hn7nd35HH/vYx/Qd3/EdD/v7w8PHerT/67AezT+PeuYzn3nOunX77bfrGc94hp7whCfo9ttv1w//8A9LklZXV/WSl7xEf/iHfzh7mCJJn/nMZ/SHf/iHl3VWlHKv+a985Sv6wAc+MHvfiRMn9Fu/9Vt60YtedKD2Vwe+4nvqJ3/yJ/WKV7xC/+E//Af96I/+qP7Nv/k3euELX6hnPetZ+tt/+2/ryU9+so4fP64Pf/jDuuuuu/THf/zHs1/33ve+V694xSv0t/7W39Jzn/tcnTp1Su9///v1i7/4i3r2s5+tH/mRH9Ev/dIv6TWveY0+9rGP6YlPfKLe+9736g/+4A/0tre97ZKfVHz2s5/VX/krf0WvfOUr9fSnP11FUej222/X8ePH9apXvWr2ec997nP1jne8Q//8n/9z3XrrrTp27NgF+wVNvepVr9I/+kf/SN/93d+t1772tdrZ2dE73vEOPeUpT9HHP/7x2efdeuut+pmf+Rn9s3/2z/RN3/RN+p7v+R71ej195CMf0U033aS3vvWtD/oafvInf1Lvf//79Z3f+Z16zWteo+c+97na3t7Wn/7pn+q9732vvvSlL+nIkSN66Utfqm/8xm/UT//0T+tLX/qSnv70p+t973vfeSttzufQoUO6/fbb9ZKXvETPec5z9P3f//2z6sWPf/zj+s//+T/Pwt+1tTW94x3v0Ktf/Wp93dd9nV71qlfp6NGjuvPOO/Xbv/3b+sZv/Ea9/e1vv6R/V+fz+7//+7NhCffff7+2t7dni903f/M365u/+ZslSc973vP0ile8Qm94wxt033336dZbb9V//I//UV/60pf07//9v9/3NV/5ylfqpptu0tOf/nSdPXtW73rXu3THHXfot3/7t/f9/Xr961+v97///XrpS1+qU6dOzZ7GTn3/93//7J+//OUv65d/+ZclSR/96EclafY6b775Zr361a9+2H8WeHhYtzLWrUd/3fpX/+pf6du+7dv0whe+UH/n7/wdbWxs6F/+y3+ppzzlKedcpZ2uE5/61Kck5TD7f/2v/yUp94qbevOb36wXvOAFuu222/QjP/Ijuuuuu/TzP//zevGLX7zvIHYpayEOFtaojDXq0V+jbrvtNn3DN3yDbr31Vt1777165zvfqa2tLf3X//pfz3swmgZNF2pz8j//5//Ua1/7Wr385S/X13zN12gymehDH/qQ3ve+9+nrv/7r9+2Xfv/3f19vetOb9OIXv1jXX3+9/vf//t9697vfre/4ju/Qj//4jz/s7w2PHtaojDXq0V2jprMH9poGcbfddtu+B2kvf/nL9fznP18/9EM/pE9/+tM6cuSI/u2//bdq21b/9J/+09nnvfSlL9UznvEMvelNb9KXv/xlPf/5z9fnP/95vf3tb9eNN944C6Qk6QUveIG+9mu/Vl//9V+v9fV1ffzjH9e73vUuPf7xj5+1ZML8sR5lrEcHI486cuSIvuu7vuucX/u2t71Nks752Fve8hb93u/9nl70ohfNKsT/9b/+1zp8+PA568zb3/52nTlzZtZC5b/8l/+iu+66S5L09//+35+1L3nDG96gX//1X9f3fu/36h/+w3+o9fV1/eIv/qLqutZb3vKWh/3n8IjyA+Td7363S/KPfOQj53ysbVu/5ZZb/JZbbvGmadzd/Qtf+IL/wA/8gD/mMY/xsiz9sY99rH/nd36nv/e97933a0+ePOl/7+/9PX/sYx/rVVX54x73OP/BH/xBP3HixOxzjh8/7j/0Qz/kR44c8aqq/FnPepa/+93v3vd1vvjFL7ok/xf/4l+c8/ok+Rvf+EZ3dz9x4oT/2I/9mD/taU/z5eVlX19f9+c973n+67/+6/t+zb333ut/7a/9NV9dXXVJfttttz3kn4O7++/+7u/6M5/5TK+qyp/61Kf6e97zHn/jG9/o5/vX+a53vcu/9mu/1nu9nh86dMhvu+02/8AHPvCQr8HdfXNz09/whjf4rbfe6lVV+ZEjR/wFL3iB/9zP/ZxPJpN9f76vfvWrfW1tzdfX1/3Vr361f+ITn3BJ5/wZXsjdd9/tr3vd6/wpT3mK9/t9X1pa8uc+97n+5je/2Tc2NvZ97gc/+EH/9m//dl9fX/d+v++33HKLv+Y1r/GPfvSjs88535/HzTff7D/4gz/4kK9l+mvP9zb9dzw1HA79J37iJ/wxj3mM93o9/4t/8S/6f/tv/+2cr/mzP/uz/rSnPc37/b4fOnTIX/ayl/knPvGJcz7vtttuu+Dv/cDv54Mf/OAFP2/vv0c8uli3bnvIPwd31q1He91yd//ABz7gz3/+873f7/vhw4f91a9+td9zzz3nfN7FrjHu7h/60If8BS94gff7fT969Kj/2I/9mJ89e/acz7vYtRBXHmvUbQ/55+DOGvVor1Gve93r/MlPfrL3ej0/evSof9/3fZ9/4QtfOO/ntm3rj33sY/3rvu7rLvj1Pv/5z/sP/MAP+JOf/GQfDAbe7/f9Gc94hr/xjW/0ra2tcz73xS9+sR85csR7vZ4/7WlP87e+9a0+Ho8f8nXj0ccaddtD/jm4s0ZdiX3UXg/27+PUqVP+wz/8w3799df70tKS33bbbRf8vOn31+v1/MiRI/6qV73K77jjjn2f9zM/8zP+nOc8x9fX170sS3/CE57gf/fv/l2/9957L/l14+FhPbrtIf8c3FmPDlIe9UC33XabP+MZzzjvxz72sY/5t37rt/ry8rKvrq76X//rf90/+9nPnvN5N9988wV//y9+8Yv7PvcLX/iCf/d3f7evra35YDDwF73oRf5//s//ecjv8Uoz9z33DAAAAAAAAAAAuModnKYrAAAAAAAAAAA8Agi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUIqL/cRvC694NF8HgKvUB9JvzPslSGKNAnB+rFEADjLWKAAHGWsUgIPsYtYoKr4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC6WY9wvAAWYmKwpZWUqSvK7ldT3nFwUAHTNZjFKMkkxqannbzvtVAcAus91/dp/f6wCA8zHr1imTPLFOAThYWKPwCCD4xvmZKaytKSwvSWX+a+LDkTQay9UtNk0rH0/kqWUBAnBlmSmsrMiWBrKikMzk47E0nsjlMpm8aVijAMxPCLIQd8PvlOR7D22sSwDmyMoy76FCvgTubSu10zXK5cmlREEBgPmwopAVpRTyPmq2RnV5FGsULhbBN84VQq7yPnZYzXXLeTPkrjBuZJNG5p4PccORdPKMfHubwxuAK6dbo+z6Q/L1ZaUYcvV33cqaViblnw9H0olT8i3WKABXmJmsKGXF3uDbpbQ3+E7ypmF9AnBlmclCyAVOvZ6sq6j0lHaDb0/yupEPR/K2nuZMAPDo69YkGwxkVSV12yhrU95HSbkAvG7ko7E8NaxReFAE39gnLC/J1telQU9puZ8Xne5A5tPDm7vkLqsLWRFlZqwzAK6IMBjI1lZlg77SykAeQ64CMJNXUV7G/IkmmSdZVcnCMB/mAODRZiaLRV6DYjdKZxpsm2TBZoczT2HfPgsAHm1WVgpLA6kscqDUFQ9I1mVL3QO6NkkWpKaRt41IlQBcETEq9HpSjLkYc7Zv8lyQ2VV/y6c/D1IysUbhwRB8Q5JyVVJZ5NB7bVnezxsh87yEWBd2d6WU+Z1VKV23rlCW8u2d/LStbeb7jQBYSLtr1Jq0vqLUq+RF3vx4sG4TZHKz3aqA0Few6/MB7+yW0vY2cwoAPDq6qm6LRdfiZFoooLwmTau+p70quz1VDsnbfF3XeUAH4NExnYkSVldky8vyXrlnz2S74ZLn4NvaJKtKqSwUe5V8PFEaT2grAODRYSYLUdarZL3enpB7zzo1DafyL8i36nqVLIbc4rKlvSXOj+AbUggKayuywUC+siTvV7MqSp8+PDOTwu6ZzKLkFmXWywF5Wci2dnKw1BB+A3gEhaCwsixb2rNGlVEewyz09pA3RT4NmExSLyr1CoWlSnGpr7DRVzp1Wj6ZsCkC8IiyadVRiPvmWUqu2WZq+oHpzTmTPIR8ljOXJyNUAvDI6+aiqN+XVpaUBnkfNb0xt09yWZvkbQ6/VZWyXiWNxgo7Q6WdHYlB4gAeYVZVuZVlVUl728Tt5dJuhZNLirLSdiu/65oWcjgvgm/koSZrq0rLA3kV91x50yxAyktHfspm7nLvnrhZXmimVQTmnnt+syEC8AixopDWV+XLA6VeMQu9FUypsByAxz3Bd9htv2Tusn5QGpSKg1LBTOnEyRx+A8AjwSzvgULIrQG6wXCzj+09vLnvu41rJrmCFLxrHee7/SsB4BFgVSUdXpcv9eX9Uqkq5MW0eCB/zvRol4NvlzVJoU2yOsl7Ofy2slCQCL8BPLJCUOj3parqCgn2tIF74B6q+9Fd+fO6t1kBgkT4jXMQfF/jLEaFpSWpV0rlbug9DY9m/zy9lrt3EUqe37r3WdNtiiaT3E+XxQbAwzRbo/rVbuhdBHkMOfQu8485ADd5mFZ9Tw9wUmhdqQiS92TbA9l2P2+ICJcAPBLMcuBtuXp798nbeaop97YW2FP5nQfL+f5WKADwMFlRKqwsy5f6Sks9pUGhVIXdfZPtFg5Y1+rE2rx3CnVSmCRZ3SrEkIsJxhPZmLMegEeIWe7lXRa7A8FN3YFOXaHlnr1RV0BgnrMon7Yk8Hz7zmPMD+ZYn7AHwfc1zno92cqSUlXOwiIP1lVP5vYBHnTOk7bZxii5rOkObqmUVpcVikK+syPfGdJPF8DDYr2etLo7d8CLoFSGfGgrg1JlaktTW5m8MKWoPbdUJEtSaFzF2OWhlNuain6leGpTfvqM0vb2PL89AFe7aU/KsDtfYNaTsvv4zEMdwizIoroQPO0e8ADgMlmvyjd7V/pqB4XaQVTTM6Vyt2BA0u48AnV7p9oV6qA4TgqToBiD8uW6NYWylHZ2lLZ3OOsBeFgshJxJFcX+kDtMFyfbnZViXXVB8t3igabN+yZJ8qhQVXKz3Z7fFDpBBN/XthBkvUo+6HdXRII8hNnVt2kAnkpTM8hVTKGRQp1kSfmpWuqqAdTNabLu60pS07IZAnD5QpD1e9JSP7czmYbevai2F9T2TE0vh97NkpTKaf9cyWN+k0txYmp3XG1pSj1T6kVVwRTqWiL4BvAwWAhS3NOL8jwV3rPrtynlQFs69wqvuyyXK3X5k8kTFUsAHoYYZcvLSoeW1SwVapai2n5Q0++C7yB5kFIhtQNJMoWJK46lWJusccXKVIy7lnJBCiEodFWZxlkPwMNhJiurPEg3hNx9IHRtd2PobqSYVBZqlytJUhg3skkjtblAwEKQ2ja3i+se4Nme23NO8A0RfF/TQlXlgZZFnA2H8yIoFUHeXX+rl4PG1wWNrzN5lIodqdwMKnc8VwK0nvvDTb+mup66dSmV5fy+OQBXvbxG9fONlCIolVGpimoGQfVSUNs3TVakZsVUr7pSTLLWZG0XfBd5qFwcS8W2qdySUhnlZgrjnqqzA1oKALh80zknewfEzVqZSJLlGShFkQ9zyWWpldp04Sok965lCv1zATw8YWlJum5FzWpPzXLePzVLQU1fanumVEjNwNVclxQOT+Qm+ZlSxamo6qypGOaiAi9cKeZbwUV37gt1Ixv1pK2tOX+XAK5WFgtZVeZq72mRQIw5n4pBXkS1K5VG11caHs0P3HobrXqnGpVnJ7JxI9WtbFLP2p94q3yDpZtBp6aZ97eJA4Dg+xpm/b68V+VKymBS7K68lblf7mQt6uwTTf7EsfqHR+qXjba3ejp795L6d0f1TuX2ATHsDY1cwV2qSqnf64Y88ZQNwKXLa1RvX4uTtp9D73rFNFk1jY4l6fqxwqBVv2yV2qDJuFBqQ86z26A0tq41Sj7kyYLCpFQ8O5DFmPt9A8ClCkEKcbc6qevdbdMQPIY8qKlX5cOX++w2nNV1/mdP+3uCpyST71Y5AcBlsuUltWtLavu7ld7NQLMf6+uSnnjLvfruJ3xSz145rUEo9bGt6/X/3HWr7vj8TUp3lyqGucjJbdp2oOhaXfZko35eAxMP6gBcOisKqZyG3l0HgiJKZQ6/J4f7OvO0UuEZQx2+8ayWy4mOn1zT/Z9b19r/jVq6e6K4M8lfa/o11dUQyKWmqxqnyOmaR/B9jbKqysF0Wcz6JXk0pSIPO2mroJ0bgiZPqLVy4476g0m+ZdKT0sBV31BodKpUeW+pwXFXb0O7q01SngLe9BTWV+XDkXwyYcEBcNGsqqRBX+qV+YFckft5NwNTs5R/rNdc7UqSR1MYlmqHxazSu7c0Vq9Xq22ihsNKzTAqLQW1vZiv66aoMFnSYOMm2akz8q0deUsADuAidS1MLOxWetsD252EmCu+lQPt2bXcGLvDXZt7U7btbk/v2dcIMnN5HqrCHgrAJQmDgbS2onapVKqC2srU9rq3Skql1LthR0997N36mkN3a7Xc0cR7OhprfUtZ62uuP647TtygO+66QfVXeuqd6ob4SgptkdugLPcVD60rbW1z1gNwaWLMWVSc9vbuQu8iystCqYraenyp4dNa3XTLhq5f21Qw1+EVSetJW48f6OxdlZa/UGnly2OVpzXbh+VO4J4HZralvG4oxrzGEXxfi8wUBoPc33tvm5OYW5y0VVC9EjS8sVV5ZKSialWnqJSCJNdgZaxidajxdaU245risFAx7qoA3GVtlDeFrFfJlpfzld6mydN1AeChmCksLcn6PaUiyuPuA7mmZ2r6yhVLKy43yc4WCkOTJVMqXT5Iatdd1p9oaWmsWLSqB1GpCZpUPU1UKjSmOClVPO6QquTSeELwDeDiTa/kzoYvdWaDmYJUxPy+tj13UGUwmRVdsbfnQ1pKXfsl5dA7mEwht0UBgItlJltfU1ruKfW6G2+lKVVSW0qpJzWrSc+68R49ZfVe7ajUp8dHNPZSLulYb1NPOHZSN6+fUB1NX9h8nIqdIEuSNXk/FqqgMKhkqyuyuuGsB+CSWJlbnFicVnt3fb3LqNQvNFkrtfVEaeWxW1pdGkmSJimqKFodPryplbWhTl23qi1fU+9MpWK7zllUSrnYIARZLKQy/9xbigiuZQTf1yKzXO0d4+6E3Okgy2hq+6bhUVM8NtHK6khl0apuo8xc0ZLK2GpQ1hqUtdojpSb3rareMrlMloJC47I6yOso65WyqpKNxhzcAFycEKRBX152fXH3DNpNXSumduBKZV5r4o4pjkzm+UBmrakNUt0vtdIfa2l5omiuoKST1arO+KrGdaE4DioPVyo2lhXObEij0by/cwBXCQsxD1SSdtucTEPvrgXK7OPTau5pRfesIjz3APfkOeyWy90k8679ie1+Poc1ABfJYpRWluS9It+aC1KKUiryzKa274pHxnrqobv1mGpDQy91ql1R40FVaNWzWv3Q6JbBfXra4bt1x3U3qDndkyUp1KY0MaVxUCoLxV4p6/c46wG4eGaystgdDm4m74Zbegxqlgpt31iqeNxQRw5taqmcyCSFbhB4EZKsbNRfH2t0tNZkPap3slRoXWqTrE2ylHI73+kQ8uleDNckgu9rkIWQD1rdwcstV3ynIodK9XLQ+PGNlg8PtVTViiGpCEltCgrmqmKjfmzUrxrpkOnO65dUb0aZm0IbFGqX1VFh0l1fqUpZEeUM/QZwESxEWVnM5g94NzguFfnwpiC1PUnJFMemUJssKWdELlmSvDZ5Y+rFVtf3tzWItaqQ165xXWg4XNZkJ6rYiarW+6oGA2nj7Jy/cwBXjVlfb+vanHQVS9OBl0XX+9t9fw/vfaYHsrAnGN8Tjnc9w+n1DeBSWJHPX9N9VIo58M7ht9SsJH3d476iJyyfVJJplErlOMnUeFBpJpPrhmJLf2HtXn3k6IbuO3VUoTGFsdSWpljm+SteFgq9Xj5bzvsbB3B1mO6bbPfnCqHrQBBUr0Zt39rq0LFtrfZG6oemW18KNZ7nOIXQan0wVDpSaXj9dRrcX6iqW6mNubVc2+a2cbEbmMlMp2sawfc1yN3l0+FJkmTqqgFy39zR9dLhJ2xo/bothZC3Qepu67pM0ZIGRa3VYqy0ajp+bKTx2WWFiclaKdRBcZIXLYsxt1QpS2k8zk/aAODBTIe9zSoA8qFNlg9sbU9K/SRLJqsla5V7e+dSAEmSJVPTRLXJtFaOdKx3VqW1WgoTnR33dddWT83ZoHrLNFkvVa0uy05VuUclADwkn1V4254AXDHOQie5uoOX65wA23aDc4tR7i55krnlqm9Zbnfi+XDowdlDAbgonpLMu6GUMQ/2TlHyQmqXXGvHtvSSY5/WE3ondToNFOTqh0m39kiFWhWWtGK1nrR0Wk85dq9OnFhXu9VT6EltlYulvLDcj7cqZFUpjQPrFICH5i53757v7xYNKAa1S4WGR6IOPfmUbjp8WqvlWFVoc9htriBX40HRkpbiRKP1nk4/xjU+XqjYbBTaQmpSN0+lnbWlMwtycSvlWkXwfS3yacP/6QAlzcKltmeqV6T1lR2t94ZqPc4WmWhJZi65qQyNeqHRem+o6w5t6d5DfTUbhUIjtWNTmlYBxLyA2bQyCgAuhuW5A9M2J9Oruh7zQCaPOfC2veVFwfPn5AVO7SRqPClVWquj5ab6odYg1Lp7+Trdv7Kq8XKpZhDULAV5VVKtBOCi2d5Kb8t9KachtqZvKU2fyE1/0Z4qbnXhtyS5LIV8zTdNq761p1L8yn9/AK5uHvLsptzeJIfUHqV24Fpb29H/b+m0VoptDetCbQgK7gq2G1q7pFamQ3FLT1m7R3966HHaPF4pDrsgvbDZrTyb3lzhrAfgItk07J4F37ndSduLmqyajq0Odbi/oyo0it2g7543qkOjOuU2vEGulZWhwrGJRocqDY5HWdPKiiA1eQC5z2avzPs7xjwRfF+DLDzw//wmt65iMuRwyUzqhUatJ7lMhSX1YqNoaRaE92Otfpzo6MqmTqytqR0UaidSmlYBxGl1U3d6o6cSgIsxDZFmoXfIgffskKVc2T1dr/bMlputZSa5m+o2SC4diju6Pm6pUqNjvbO6c+mQhoMltf2gVEqSy1uqlABchPMc1rS31Ynlg5ZNg+y4p+J72gtce350SSHtaZtis3Z0AHCpchC9p2ige0uF5MFlJpUyrYZGy6FWnaKCuiInuZLnjVXjpqUw0U3Vhg4t7+hMdUhe5Mrx6W3hfXMIOOsBuBhd0YDt20ftrlmS1HhQYa36oVZhSYW1CpbXp3Eq1CooyDUZbOm+Q9vaOtRTs5Tb7Xodd782iTdE8H1N8pQDHnPP+xP3WdWkJZc1pqYNKkOrnjVKbqpCq35oFK3tzmyuXmhUWaPR0obuWj2sjeWB4tDytPDpZmgWsLPgALhI3l3p78KfPJQpVyrlB3Qujy4vXaEwJffcHqDb20z7gOcvFeSSlsJEjy02VFmru3vXaa0/0n2DVqmKSuWeTRcAPJQu4NnX4sS0/+ehG/o9/TWWZ6rsb3eyG4BbaqU25AA8sRYBuHye8nA3dc/cvHuTSUqmnUmls8n0WLnWwlgTD6oVZxXfbkGlWiVJhZKOlkMdGWzpC70kjzG3TQmiihLAZerCJ99ztW0697uV4kQa14VKa3VdsdMN3W1UWKtWpkkqVHshyTUIE525blmfObSuejmq3O6GZAZ7wPLEYnUtI/i+Fk3/P9+FS5ZcSi5LuXVAnEgbOwO1KWi5GkmSCkuqrMl9lSypUFIVGq3GkfpLte49vK6NQ6tKZ6sumNLu9bc9T/EA4CGdEw5pFmjnKqP8bi9cqXCFZFLrs8/LbQK63rjuKiypb0mHYq1WOzpcbGu5mihWbW6b0h3eLARanQC4fPuC8N3K730f37sf6sJwa5Pcc1DlKUmWckV5Snk9c/ZPAC7B3n3UA1omWStt7vT0ye1juqm3oSWrtRyidroBl5IU1Ki06TA507FiR49fPqGPrz5Rfl+cfT2fPdDTuXs3ALiQPbm35UrM3I7XXda44si1uTmQJen6YkuDUKuwVtGSWg9qY77RG7qfjw9F3XnDUY2vX1H/VFB8YGGCuEl3rSP4vgbZ9FpJ8t2qJfdc7Z2kMDadPr2izaN9He1tqRcamXVDLiVFc/Ws1nIc63Dclkfp6NJZWb/d/yBt7+LC9TcAF8u6Xrd714zugJW6qm+5SdHllctb251dsCckt5hUFa2W4kR9a1RKCspBeBlahSIpBeXNVzf0FwAe0jTU3p2Wsr99yfSdoWuJkj+w5yZc/gSfhkVtV4TQtHkYZgiylOQx5HXNWZsAXLzZbKU0DZPU7XUkS6bJdqX//8mn6dal+/WUwX1asYlaM409RwPBkoqu7YkklZbUj7ViTLlQKkn7KgW6fRRnPQAXZV8HEt+zhuTwuxhJZ+5d0taT+lpZG+twsaWovBdqFNW6qbJWfauVFHSqt6yllbHO9lfPzaNmmRTr07WM4Psa5CnJ61pWVbNNkPZsYGLt8jOltoYDVdc1uqHamE3PrbsN0VIY62ixpaNxU8ebVZ0dLqnZrFS2eTNkexYvpbQbsgPAQ/C2kdW1PPVnmxefVnxPMySXVLg8tErRZRPL7QFMSqVL/VZLK2MdWz6r68stldbIJEWTcgOC3cNgaJXXKIJvABfF5W2bB1lKe9qbhP3BdhGlIsxanMxmn8zWNZOCZJOkWHeBdwhdQB6mpVBz+h4BXK28rhV2RoqTvkLre85m+Zzm46gvnDyizx85oif3T+tQrBVN2k6NJor5xpxcpbmCpLvrVX1u40ZNTvU0mOR90/Rr2rQ9HWc9ABfLk7xpZeX514w4cvXujbr/7Lp0g+sxcVt9S0pyDT1q5IUKtboutBp6T5vjvjbuX9byfUmhTvvXIpeceXPXPILva5F7Dr7bdl/Ft9RtYFopjEybo76aFHS0OKvlMNZW6muUSpmkQZjo5nJTh8JYnxse1f1bawrbQdbmPuHTQMnSnoVnOvgEAB5MSvLRJK8fkmZ932z3p5Jk0RX6rcKS5HWQN7l8u6iSlpdGevLh+/X867+oZw7u1rE41HIotOmu0loVluRuUurCdKPVCYCL5JLa/QcrmwXfQR67H8sor6I8mlIRdofB7f4iyV1RjcIo/xqLIV9tSZ6rvmdBOVd0AVwcb1tpZ6QwbhSaUqHx7oxmObBuTKOdSnfsHNXx1Xu1Xp3RWmhUu6nxqFbSyCv13XTWXZ/ceoy+eOKoio2Yg/TGu6+Z8lo43a9x1gNwMVz5hltK5xZiuxQaV7kp3XdmVfeO1vW1g/t1NDbaTFGtklyNGo8qLMjSsr5y9oia4z1VZ/Y8hPPpb5Qnrvh0yDhr1DWJ4Pta5C7VTV5spv/H7yoArHWF2hRHpjMnlvX5lWM6bFs62tvUmXZJtUf1YqNN72nFCw1D0ifOPEF3nzikODSFiSs0ebGyJi881ial8y1qAHAhk4nU5HXDQ96yzK7rJkmeB5bEmNTr1Yohb3BiSFqtxnrcyhl9w3Vf1Dcuf1lH41ADC6qskLtr4lF1ikpNUNFK1rjUtnI2QgAu1uxafzc9TsoDLWOUYpQXQV4Gtb2oVAalKiiVDwi+JYU6KTRBXgRZEaU25l7fXYi+O0STbRSASzAeyyaNQu0KtSvWpjiRUm0KE6kdB33q5OO0Eke6d/ke9WKtE82ydlIlmTRsSq3HkSTTH9z7NTp1fE3VjhTHUpy44jgpTJKsSZz1AFwyb1uZp9291DSs9vxgLU5cZ76ypv+z/jVaqlsd65/VqcmStr1UMtNO3dcNxUhjVfrzux6n3v1BoWlkbTe/bhZ+UzgAgu9rljdtnvi9t893mxTqoDiRym1p8uUlfWL0ZH3uMcd0ZH1LE4+KIWm5nGinrnR9uaVD5Y4+cueTNT7RV69RDr3rXAlgTZLVrXw8kU8mEj0qAVwMd/lkkiuJZrdRPFcqtflWijWmNAlqiyivGq31h1qpJlotR7qhf1a39O/X1y3do1vKiSqrFGQau+l0Mp1plzVqS6UmKI6k/slGduqs0ng8528cwNXCPc1uy8mUQ+oYpSJKZZT3SqUqdoF3UNMPavum1O28p31yowXFMsirQt6k3bkr07kDKT+U48EcgIvmLh91wfekVZwExZGrKNQ9gJNkUV/+0jF95eQh/fbKWEu9iUZ1IZcphqTRsMo9dItGp766rureQsXQZ285+G5lkyaf9UZjznoALprPKr5zUC2pu+2W34qRa+WzhT519hZ94qYnytZr+XahZJJXSWEzaqWaqFiuNfrMmtZOpvMP2fUkb1t521DtfQ0j+L5GeVNL44nU60ltmUPq7tpaqD0/za9McSNqU6va3B4o9hoVVasiJrXJdG9alcZR4xN9lSPLoXfbvTW5AkCTWr4zlDfNvL9lAFeRNB4rDkcK9WA2eNfS9OGa5YdrafrczhTMZ6H34/undGvvPt1UbKuw0NVjBk08aSv1dLYZaGtSScOgasM1+OqWfGt73t8ygKtJSvI27W/BbSZFkxexC72j2n5UW5rqZdNkzdQO8syCUOfKyTJKoY2yxlWkpND1y7Wm7a7k6py2KgDwUNJwqLi5rbjSU1FFtVW+cZK6AeHWmtLE1U4qbW+U2ip9X2GkTUyTxrRTS9VGULnlKoa5924cJ4VxK5u0u2e91M7tewVwFWpbed1IVZuLMdskSw8YoJtc5bZJdxdqTkdZkCzkfVScmEZeKDSuwclcGJVbymn/wN1uvh37qGsbwfe1yl0+HkvDUioLWa9QaFJ3Fc4Vx6bYk+LY5NtBrRdqlkxtE1QXnteQcZRtFSq2g4qhKY49tzqpXaFOskkjjcby4YihcQAuTUrS9rbC2b6KfqFmKSo0kk1vljSm1OSBlrlQIJ/W+qHW9XFLx4odrVgr90Ktkra90fE26t5mTfePV3V6a1nl6ajB8UbhnpNqh8M5f8MArjptK2+a3JfbcqjksQu9+1HNIGiyElSvmCZr0mRdaleSPEphYopb1oVRQVLRzUbxvH8K3cktpRwocWADcCnc5We3FKtKikGpyAPDPYRcSFCb2p4pjLugKHhXCd41cEo5WIpDqdzJoXcxdBWjtFvtPa6l4YizHoDL4pNaVkykGGVFzAUFTe5KYG0uXcrrlVSYqa0kxe4ZnUtxIhU7Jkt5/XLL77eu75K7d9Xe7KOudQTf17A0mSgMhwpVKR9UsiZPwQ2TMKv6LkYmLyQvTG2ISm5KhUtJsklQGOcNU5hIcZQXn1Cn3eB7OORaCYDLkrZ2FMpSsV8qrJcKTVCoc5sTayRrgrwOSm1Q3XZ9uxUkSY1Lm5408olauTZToS/Wa7pjdEx3bh/S1sZAg5PS8p07Smc3ObABuGTeNlIdZMW0f4lJMQ+1bPpRzVJQvWwaHzKNDyelQ43K1YmKslXbBDWblUb9Ul6YTEHWRFldysaNbBxyy8umYX0CcFnSzo7imUKxKlT0Cnko5GYKramduNqxKZXdLJVo8r0V390tu2LkisP8YzFMiqM91d7jSQ6+OesBuAze1EojU4hRVlWyafBdJ1mKufI7SbI899uD5GHaBjOvWXn9ynmVh+7mnVluR9fmWynso0DwfS1LKfdkG45kg76sKhTqIld81640MaWRlGZXRkypNXnMgwLCxBRGplArB997B53UrTQaK20PWWgAXBZvavnmlmx5SXFnoLAU8/DdWmq7difemNo2aNwU2mkq7TSVhl5pK1U63U5UWlKjoPubZX12fIM+s/UYffX0ddLJUkt3Nyq+dFxtXc/7WwVwNXKX17W8KGZ7HS+C2l5QMwiql4Mma6bxoSQ/OtF1R7Z109oZHRlsK7npq5vX6a7+YY00UByZyk2Txy48Dzb7+gBwWdpWaWtbod9TsdyTFznZDm3It05KKRXTMEm5jHIaficp1lIce3fGc8Vhq2LYKowb2SS3zUw7nPUAXKbpPmoykepaaoquz7cUWp+10dV0ieluppi6kQK2u375tAx873LUtEq0OYEIvq953tRKZ88quCvEIO8VsjpvhkLpioWUijwkwFop1TkIn145CRPrKr1dcSLFSVLcqRU2R9LOUD5hWByAy5fGY9k9x9Vzl+yI2l5PbT9f0Q21KTVBbR01qQsN61KbTU8n6xXdHcaaKKpvtUZe6quTQ7pjeFR3b1ynnbuXtfpF09JdQ7WnTnNgA3D5UlIaDhUkWVXltpJFUNsz1QOpWXGl61odPXpWt15/v56yclyPq04rmOsL/aP6cJK+vHWD0n2lJOVqp0kjTRqqvQE8bF5PlE6eUmhale0RWeortIVCafLClLpqb1kXHHXBtyXJmlwMFSauOMmV3mFUKwwn0nZ31qsnc/3+AFzlUlLa2VEw5crvfpV7fbe53YmlnD2ZK88n6NqY5A2XujZxms2EkrusbuWTOu+jWuYPgOAbyhN1fTSWjSayUU+xCEplkEfP196i551Qyn11Paj759zaJI48937bSSrO1iruPi2d3VKiZy6Ah8s9VwFsbKrYWlO5VSqVUW2VH8qlypR6QZM6atgU2qz7OlmvqAqttlNPhbXabnv6/PCYPnP6MTp557qO/b/S6ufOKBw/pZZQCcDD1Q1OsraVtUlKeUicF6a27ypXJnr8+ml9/fqX9bWDO3VzuSWTdFOxoc26p1On1jTu5+A7jGrp1Fn59rZ8NJrv9wVgIXhdy7e3FbaWVRS5pWWogrwIXXsAyW03/J7NG2j3zG6qk8KkUdgeSyc35MMhaxSAR8a0E0HTyJo2tzppXKHxPN+pzcMrQyOl7gGdTUPvtpsBVXctd4cT6cxZ+fYOaxRmCL4hKVcD2NlNhRhk7l3wbfKYJIW88WlNbSmpm7cUGinUrmKUw+9ip1VxZig/vaG0vU2VEoBHhnvuU3nvGQ3MZGmgVBa5Uqk0eRFVF6W2i57OlAMNilW5TGeLvoJcG81Ad24e1vHj1yl8rtTan5yQ33k3LU4APGK8aaStLYVeqaJXKK5Hhaa7LdcGtclUWqPDsdWxUCmaabNodLjYVlU0mqTcRiBsjuSbm0ojhsUBeIS4y8dj2akNhTbJVgay5UqpjLN+uDn0tq6HwG7wbU3q5kC1slEtbQ2VNjfl4zHtAwA8Yrxp5FvbsrJQiEGhXyhMguLE1db5tq8HKSj/mOc9aRaQx0lSHDUKWyP52W4fxRqFDsE3JOWq7/bMGdlopDA+pNJdoelLXnaXSULeADXddTjPfZfiRCq3k3objar7txVOns2V3hzWADyCfDJR+5WvKm5sannrBlm7plDniUweg5oyahQrnYpLkqSdptKgmKhOUfdtruq+r16n3qd7OvyJbem+k/nABgCPlJTUntlQqGsVKalfHlIq+mp7QTvrhe7ePKTPrdygG8qzavyEJOkz4zXdsX1UWxsDVaddvRND2YnTasdj9lEAHlHetmpPn5YNhwrXrStOVhT6pbwscuV36EJvy/MFrPXca7fNs5tsPJF2RtLmdp49QKAE4JGUktLmpqyuFepGRTDJBkqlqe3l274epRTyp08rvkMjFUNXuVkrntqRNjaVJhPWKOxD8I19fDRSe8+9svvuV3n9YYWbrlc4uqR6rVDTz/ffPHaTdNtc6d07OVHvS6ekr96n1HS9lADgUZDOnpX+fFtLd5RaesKN2nra9dp8UqmhomqXtk1yNw2bUkVI2hlW2rljVYc/VOu6P/qSfHtbaWt73t8GgAWVtnekL9+l4u7jWr/pBlVPPSJZTyerNX0k3qzttqevWb5PtQf9ycbj9OdfuknxUz0d+sSG4qfuzKE3/SgBPEp8NFJ7fCw7cVJhbU22vipf7svLQgq7Fd95wFySNa20M85h0tlNeWp5MAfgUeOjkdp7j8tOnFJ15LDC449ItqRUxtyaKeYlSl1v71i7qjMTlV8+Kb/nPiX6euM8CL5xLnd50yidOq1Y1+oPr5c9bk1+qJQ8V3y75StwxU5SsTGWnd1WO+Y6CYAroG2V2lZ25z1aHdUqt47odNvXtkU1Jm0n02hSyszlp0qtfCFo6Ytbao/fl9co1ikAj6aU8q2Su49raVxLzTE1g76O967TsCn15ZXDSm46fnpN47uXdeh4UnFqJ9+YY30C8GjrznrtxobCZCIbr8hWV+RVsa/ViVKS6kYajuSjkbyhRRyAK8A9D+Y9cVJF02hpckQprqkto1I0helAy1aKY1exXct2RlR644IIvnFBXtdKG5sKFlQOCnlcUb3SXTMJ2p2uG0KuEACAK8hHI6Xj96sXg5ZXj6qtKo0U1bSmuh+l4IqT7vrudI1iMwTgCvHxWOnESQ3uKLV+6JjOxJ42JkGba325m9JWod5WUDFqZG0SqxOAK6ptlXZ2FNxlIchSX4oxf8xz8M3aBGBevK6Vzmwoxqj+WqW2vyQFU9vLg3ilXJSZiiAPYb4vFgcawTcelKdWvrOjeP9Zlb1CqRxIFpSi5UoAk1I/yvs9WYy0OQFwRXldy05taPmLfbVL16ntRXkISin3gVNrapZM9Xqlst9X2tmZ90sGcA3xppGdOKOVzw2Ueus6U5SaTKI8usIkn9qaflTqV+yjAFx57vLJRDq7lXvm9iu5WS5umt6SM5NiyMVOtDkBcAV528o3t1Tds6RmpVQzqJQKk0IOv9vS1KyWKpf77KNwQQTfeHDu+ert3WNZFVUMqlw9WeYPW1LeBPV7sqWB/OzmXF8ugGtMSkobG7LP7Kg3uFm99aPywtR4UCpc1ubWTO16XzpySLqT4BvAFZSS0tmzCp8eqd+/WUsrRxTGUe2gGx1em7wMaq8bqFxdkZ8+M9/XC+Da4i6va3lT5xbftioru4gg5eDbzKRY5FCJ4BvAlZSS0va27ItfUTkwVatHlYqgtsoP6DxIqQzy5b7C8pJ84+y8XzEOIIJvXBx3hc2hio2RvBio9Tjr8+1mUhllVTXvVwngGuVtq979Yy3fWytVpSTL1QDK61SqgtISaxSA+fCmUf+eHfnhsaSe6pVcrRTGXdu4Isr6vXm/TADXsrqRj8cy91zYJM9T5KZV37QSADAn3rYqTg/VOzVR2+9JppxDmZSqIF8qZf2+RPCN8+C/XrhILo3HCjtjhXFSaFyh9VzxbcrDUEqeowCYE3eF7ZHKMxOV21IcSWGS3yyZ2n5Uu1TO+1UCuFa5y7aGqk6O1D+TVG25ip08lMlayYsgZx8FYF48t4/TpJY3bW5pMh1w6bm9pRF8A5gbl+2MVZwdK45c1kjW5ttzqTC1g1Lep8gJ58d/vXBxXLn/23CsMGlkdZK1yhsimbyM8l61OxAFAK4ol3ZGiptjFcOUQ+8mv8mlVJmala4SwBjGC+BKc2k0Vjg7UrnZ5tB75IqT/KHUi/KlHvsoAHPjbZvD76aRmlZqcwCe25t0Fd/soQDMg0sajhS2urNe092Yk5QKqVmKSsvso3B+BN+4aD6p86Ft0spalyWfLTYegzToKSwvcw0OwJXnko9GCpsjxVF3K6VRfkAnqS2l5lBf4YajspLKbwBXmCu3ENgZKw7bWegdmjwvxWNQWuoprq6yjwIwH57kbQ68PbXyNsnbrvJbuf2JESoBmJNciDlRqFPuPKDc49ujqe0FpdW+4vo6+yicg78RuHgpScOxbGukOGpkjecA3F0eTF6VsuUlrsEBmAuva9nGjnonxqo2PVd9113Vt0mpX6g5vCwraCcA4MrzppFtbKm6f1v907XKraRyJylOuv1UEaWVZYIlAPPhyqH3tOI7dW/eJUxmUohUfQOYC28ahbM7Kk+PVW53HQhccpO8MKVBJa2vcNbDOUgocUl8MpZt7SiM6lnVt1J3zSQGqSqlyEIDYD5sZ6Ty5I6qs63ipKv6nlVUmtrlUtarOLQBmAsfjRVOb6nYGKvcblUMk4pRUmi74XFlwTVdAHPjbSuf1Dn8Tim/Tau+3ef98gBc60YjxdPbKraa2a05SUoxz3RKyz2Cb5yDvxG4NMnz5qd1WZu6x2tSvv5m8qqQFVE+MTZHAK68lKSmVai73m8pr0PW5uUq9aNUVfkKXNvO+cUCuOakJNWNwrhRHLW5VZyk0KRctVQEWQhyYx8FYE68G2qZ9hQJeH6/BWN9AjA3npKsSV0RphRaSdO2J4UpDUrFsshnvZTm+lpxcFDxjUsTo1REmbpAKeVWJ0rKm6CqlE1DJQC40mKQYpQlUxz7bMDlrNd3P0qDvqygzzeAOQgmmcmapDhqFcet4qTNQ8OT5/WrLNlHAZgv99zjO3l+89SF3catOQBzY7GQykLmUqhd1kqhC8E9mtp+Iev1aL+LffjbgEvTVVPapJkNuVTX51smeRGl5SWFXm/erxTAtahtZeNaxU6jYsdz+D1xxdoV2jxArr1uWWF5MO9XCuBalFxqGtm4VhjVCuNGYdTtqZokM5P6PQWG8AKYi1ztnYdcdq1O3GetTlye1ynCbwDzkFppkm/NFcNuTkqTW++6Sd4r5OurskF/3q8UBwitTnBJvK6VNjZlda2wsyxbXZIPKnkwmSsH4E2TN0sAcIWl0UThvlMqxxPFM2uqjy6rXi2VitwDLtSuMO56VwLAFeZ1rbS5JZvUsqW+bHlJ6k33US7VDfsoAPPjkqdWajxvnGLYDbpds5YntDoBMA9pNFG4/5TK8VhxY0310RXV66XStKQ3uayu5Q37KOwi+MalSUk+HsmbWqFpZDHkvt4Wu01Q93nshQDMQ9sobW/LxmOFSaMyBqUqanrByVrfv1YBwJXkLp9M5HWtUNd5OLhMVsTdvrqsTwDmyV1qW7klmaI8hN0Kb3c5oTeAedl31qtVFlFtP8p7odtTiX0UzkGrE1y6aVX3eCKNJ7tBkpR7V5aFrIzzfY0Arl0pyeta2tmRbY8UJrl3rqV8PderMvfQBYB56AJubxppMpHVjaxpZG3X5zsEKbCPAjBnnvt7T9uczKq9aXMCYJ5mZ72RbGuoWCeFtJtH+aCSKs562EXwjcvm0/B777Rck1SWeXAcmyIA8+Ke16edkUKTZG2+sSuXvIp5MxQJlgDMjzeNfFJLTZN76bYpB0whyCJbdAAHwHSo5TT0Vj7ucc4DMFfdDToNxwqTJHXnPAWT9ytZr8dZDzPsqnHZrCxkvSpXeXebHzeTeqW0vqqwvjbnVwjgmmUmVZXU78kt987Nt1Ok1C+VjqwrHL1+3q8SwLUshLyHmrY4Sa2UkiwGWb+nMGAIL4A529PiZLe3N8MtAczZ9Kw36OU5KalraSlTWqrkR9YVDh+a96vEAUGPb1yeGGVLS9LqsrzrS2l1K5s0eUNUlrKqmverBHCNsqKQrS4rXbcsjyZrXWGSFEdtrqisylwJAADzYCYr97RdalO+ujsdammWg3EAmBcz5ZBbylfmJN/bPNeMIZcA5sKKQra2rPa6FXkRZMkVx0lxp84BOHkU9iD4xqXrDmtaGsgHvVyt1LpsNJE2tqQYpDbJR+N5v1IA16IQZFUlX11Su5RvpYTaFbcmiic388ebVtremfcrBXCtCkEWY76G6577fddNvrbbhUle1/N+lQCwO8zSlVuf7H5gLq8HwDVu71lvuZKbcoHT5ljFic2cT9WNtDOc9yvFAUHwjUtmMcqWBtJSX17kaiRrGml7qHT6TK5Q6gZgAsCVZjFKK0vylcGsAiCMGoXTW/J77peFoOQpb4gA4Eozy3upIvee9Da3OfHJWGk4ykN4p+1PAGDevPufWasTAJif2VlveZBv9iYpDvNZL917fw6+E2c97CL4xqUrS1lX7e3BZG2SjWr59o58TJU3gPmyqsptmAb5eps1SXF7LNvYVtrZEUc2AHNlQVYUUoizIMnrbtBl27JGATggpn28Cb0BHBzTs15aqmSSrN5/1gMeiOAbl6arUlJVyGPXe7JJ0nAkH3KVBMCcmUlFIa+KfCPFXTZJss2RtLU971cHADlLmg6GS55bBzRNbncCAAfGtMUJoTeAA2J61utNz3rKs+Y46+FBMDUHl869O6i5zCXrrpE4V0kAHASeZG2SkncTvpNsPMm9cwHgIPA9FZRtN9SSYAnAQcO6BOCg8SRrPb8llzWc9fDgCL5xadzlqZWaNgdL8vzULXZDmgBg3tr8MM6aNCtWUgx5iBwAzNu0f7envK8iWAIAAHho7lLTSpO6y6M6gbMeLozgG5euaaXxRKpb0YgSwIHiLm9q2WiiMG5yFYA7axWAg8Nd3qY81JLQG8BB5b7/RwA4ALxtZKOJbNzkqm/WKDwEenzj0nXXci0lefLc47tu5E0971cGALkVU9PIupspVidpMpHXrFEADoKur3dKcrNc/Z0IwQEcQKxLAA6alKu+rW5lbZdHcdbDg6DiG5cuhDzcsoh5QFMMUlXJqt68XxkA5DWqLOUx5krvYFKvp9BjjQJwEFhuEzfr8z19t831VQEAABx4IUhlIRUx76NC4KyHB0XwjUuXUn7ClrqeSsGkqpRV1XxfFwBIu2tU2+arb5bXKLEZAnAQzIaEazf8nobhAAAAuLCUZrd7Z8UDZSGRR+ECCL5xWTx1hzZpt+o78tcJwAHgLrVtHnI53QwFk7FGATggXC731AXgtBIAAAC4KF3rXTWpm+ckznp4UPzNwKULQTZtdSKTq6tQ4uAG4CAIQSqK/DBuz7rkrFEADhKX5GnP2kTFNwAAwIPae9ZLrmmlE2c9XAjBNy5dStKkltWtJJe1SdoZyYejeb8yAJC3rXwykeru+lvT5jVqZzjvlwYAWcoDLvMNuiRPreTtvF8VAADAgeZtKx9PZJNallLOpbY56+HCCL5xyTwled1IdS1rUm4p0DT5RwCYN/e8Pk1qWZvyOlU3eZ0CgAPBd9uc7O35DQAAgAvrznqaNPmc17T55+RRuACCb1y6lOT1JIffkiTL100YygTgIEhJPqnlk1q2dxYBaxSAg8Jdrun1XKfLCQAAwMVISV7X8noiTXt8G0PCcWEE37gsXrfSZJKHCsQg9UoZU3QBHBDeNNJ4kisAzKSKNQrAAZPS/kpvDmwAAAAPKZ/1alnbSsGkspSV5bxfFg4ogm9cnpR75trWUOYu9Xuyfn/erwoAspSk4QPWqAFrFIADxF3uiRspAAAAl6I762lzmIsxe5Ws15v3q8IBRfCNy2MmlYXUq3Kh0riW1/W8XxUAZGZ52ndV5j5w49z6BAAODDNJlgdcOg2+AQAALsrsrFfk23OTCXkULojgG5fFykIa9OVVIWvz0zbf2Zn3ywIASZIVhdSvpCJKTZJ2hqxRAA6oPUMuAQAA8KAsRllVSjHmoZY7I/loNO+XhQOK4BuXLgRZWeWFRpLGE/nOTu6zBADzFoJsb5+38UQ+HLJGAQAAAMDVzCwXOZVFd7N3Ih+NOOvhgop5vwBchVJSGg4Vtnp5wdkeymsWGQAHxHSN2q4UYpRv79DmBMDB4y4p7flnAAAAPCh3pfFYYWeoEKJ8OKLNCR4UFd+4dGayqpL1e1KvkpYHu9XfADBv0zWqV8l7pbTEGgUAAAAAi8CKIt/uLQup19u96QucB8E3LpnFqDDoS/2eJM9TdBOVSgAOiBDyVO9eT3Llvm9tmverAgAAAAA8HCHunvUkKaX8BlwAwTcuXVHk0Lss8tC4SZ0XGrN5vzIAyMNOepUsBqlppHEtZ40CcBAx1BIAAOCiWRG6s17MxU11w1kPD4rgG5fEYiHr9/PTtRjy26AnW17KT90AYJ5ilFWVVJZyC5LljVEYDLgCBwAAAABXqxhlZZWLMU1SMFlVKvT7ef4ccB4E37g0ZZF7e5dFbiEgSUWRn7gRKgGYM+uCbwtB5i6TpCJKVckaBQAAAABXKQtBVpa52lvKmVSMUlkSfOOC+JuBS5NSbh3QtLnau02y4Vg+GksNk3QBzFlK8qaR2lbWhjx/YDKRxhOmfQMAAADA1cpd3rb5rBdCPvvVtXw8zmdA4DwIvnFp3HeHB9StNBrLN7eUhiM5wTeAeXPtrlNNK59M5DtDNkMAAAAAcBVzd9k0j2qaHHoPR/LxRN5y1sP5EXzj0hSF1A2N88lEfnZTvr3DIgPgYIhBVhSyEPJGaHtHaThk0jcAAAAAXMUsxty+0kzeNPKdodJoxFkPD4oe37h4IcimfXJdudp7h9AbwAFhlnu7FTFfexuP2QgBAAAAwNXOLPf2LnJ/b59MlMZjznp4SFR84+JN2we0be6tNJ7k/koAcEC4u6xNcpdUN2yEAAAAAOBq5y5PSdamnE1x1sNFouIbF889V3rvviO/DwAOAt+zJtme9wEAAAAArn6zXIo8CheH4BsXL8Tc43v6Fgsp8FcIwAERwuz6m8WY1yezh/51AAAAAICDa3rWm76FyFkPF4XUEhfNqlJWlZIsXytpap6wATgwLMbc41vKw06ahjUKAAAAAK5ye/t7q23zrDnOergI9PjGxXOXkkttIx+N5eMJCw2Ag8N9t8f3ZCLV9bxfEQAAAADg4Zq2tUxJXte5GBO4CFR84+K5S55y+D3Nu7laAuAgmQ7h5aEcAAAAACwEnxZiTgNwjnu4SATfuHizXkpBKguFQV9WVfT5BnAwTB/EmWRFlFWVrCx5QAcAAAAAVzHbM7/JYsyteIuCsx4eEq1OcHFCkJXdwhKCrCrlISjEKN8ZKo1GVFgCmB8zKXZDLS1IMch6YfZgjn7fAAAAAHCVmp71gkkh5iA8BGky4ayHB0WpLh6amawoZGWxW91tln/e60llmRcgAJiX6ZTvsFv1rRjy2kUlAAAAAABcnfaG3pIk2z3rxchZDw+Kim88NAu5pUlR5N65bZsXlhBk7pKZXCw0AObErNsIha6/d5qtUbtLE2sUAAAAAFxVZmc9yz2+lSTZA1ructbDhRF84yHN+ieFsH96blVJwfKQASYLAJiXbjNkZvKmnT2cs7KQZFx7AwAAAICr0TT4lsnb7qwnSUWRz38ukUfhwRB848F1/bwVozwl+aSWj0b5Q5JUlVJKXfgNAFfYdCMUY16HmkZeN7sP/YuCh3MAAAAAcLXZe7PXPffyrhup6zngRSHJOerhQRF84/xMsxYnVlV5oalr+Xgsr+tc/e2eW50AwDxMK72nPbxTkjeNPHUV3+5UewMAAADA1eaBZ722zUVO5zvrmQi/cUEE3zgvi4WsLGW9SirzXxOvm9zqRMofK7qBlgRLAK60EGQh5nWoG67rbStvkyTtqwwAAAAAAFwlLMi64ZWKMXevTCm3OpFmN36BixEe+lNwzTHL03F7Paks8/tSkpom/zgdJNA9dfPZ+wHgytgXelvXx7tt82DLvWvUdCAvaxQAAAAAHHgWbF/oLVc+z80qvI2zHi4aFd84l7tckk0Xk+59s8VF3gXhrbxtcvBNVSWAKy2ELuCWukVr/xrVtvsqAwAAAAAAB53tOdtZV9y0R1f0xFkPF4PgG+c3fWrmLlnQvoUnJXldy5PnBYinawCuME9pNmPAZHLTvgd13rZScrno8w0AAAAAVwtPSUoum3YzsT15FGc9XCKCb5yXt628qWUxSFF5MdkzPMDbtmsrwCIDYA68lbdtHrS792bK7OMud9YoAAAAALi6JCm1Umu53Yk7Zz1cNoJvnF9KSjsjBXdZfyDFIKvK3BQ+JXlyedvk8BsArjSXfDKR3GVVlYddFt1/0ro16pwrcQAAAACAg80lr2tJuy14bTrM0p2zHi4Jwy3xIDz3zZ0+RSsKWb/XhUzG0zUAB0vYnfydW32zRgEAAADAVWlvpXeMnPVwWQi+cWEW9g8UsCCFuBt609sbwDztHcA7/XkIMgtiGwQAAAAAV6m9fb33vM/MOOvhkhB848KmgyvdpenSklp53cibZq4vDQDO6fUm5TYnbcODOQAAAAC4Wk3PenvPdd7Nm+Osh0tA8I0L66onNXvA5lLT5l5LLDQA5m3v039ptjHy2QM7AAAAAMBV5zxnPeesh8tA8I1LE4IshHMXIQA4CLrrbwAAAACAq9i01QlBNx4Ggm8AwNXtnJYnxsM5AAAAALianXPOIwDHpSP4xoWltNs/qVtffO/7AGCe3KX0gM2Quzy1VAUAAAAAwNXKfTfnnhY1uTjn4ZIV834BOOCaPMzSQveMpGly8A0AB4B7kto0W6Po+QYAAAAAV7/ZWW8WfHPWw6Uj+MaDi0EWYx5yKXXDLmkhAOBgMAuyGLo1yukDBwAAAAALwaQwPd/N+7XgakWrE1xY6ELvGHeDJMIkAAeFmdSF3gy0BAAAAIAFYSYLls95nPXwMBB848LMchVlmF4rccJvAAeHmaaDLH1vv2/WKAAAAAC4uu29zeuJqm9cFoJvXFjbyutaavYMiuNpG4CDIiXpnGG7rFEAAAAAcFVzl7cpFzgBDwPBNy5s2upk1k9pT+U3AMyb7fZ8s+k/S6xRAAAAAHA161qdzG70ms0iKeBSMNwSF2RFIRVFDpM8V1Z62877ZQGAJMlilMVit9VJm/JaBQAAAAC4eoU9s5zc5cnlKVHkhEtGxTfOzyyHSiFo+ljNm1Zqmge0FQCAOXnADAKfPpxjMwQAAAAAV6081HJPZOmJLAqXhYpvnMssV3tP25x0T9W8aeRNM+9XBwDS9MGc7bn+dk6/bwAAAADAVSUEycLsqCdR7Y3LR8U3zsvKUlbEWahE6A3gwDDLLU5C95+wabU3oTcAAAAAXNVmBU6SJM/FTZz1cJkIvnFe3rZS8lklpdPiBMBB4b5/PXJR7Q0AAAAAC8DdJXn+sStyotobl4vgGxeUb5TsqfZmoQFwYHTrkbs8Ue0NAAAAAAuHFid4mAi+ca7pYEszeUpd9TehEoADwmxfmxM2QwAAAACwAMzyYEvleXP09sbDRfCN/cxyP6VpqNS2VHsDOFgs5HVKuS0TV98AAAAAYFF0/b27VifAw0Hwjf3c5Wm6sLDAADiI9q5NdsHPAgAAAABchdyJpPCIIPjGuWzPP4QgC3HPRF0AmD+fboTMZBbIvwEAAABgkdjsf4DLRvCNc5hZDrq7tidWRFkk/AZwQMxaMpksmBRMMv5zBgAAAABXPVOXSYXurEcWhctHUoBzubo+Sp4XmbKUVVWu/AaAeZv2enPvBl3ycA4AAAAAFsKe3t5m3Qw6znq4TMW8XwAOHk+tNB7Lmz1Bd0pyT/N7UQAw5UmprmcDLuWeW58AAAAAAK5e7vK2lSztht0pMeQSl43gG+dylzeN1DTnvB8A5s4lta08dQ/jWJsAAAAAYDHsqfgGHi6Cb5wfiwyAg451CgAAAAAAXAA9vgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC4XgGwAAAAAAAACwUAi+AQAAAAAAAAALheAbAAAAAAAAALBQCL4BAAAAAAAAAAuF4BsAAAAAAAAAsFAIvgEAAAAAAAAAC8Xc3ef9IgAAAAAAAAAAeKRQ8Q0AAAAAAAAAWCgE3wAAAAAAAACAhULwDQAAAAAAAABYKATfAAAAAAAAAICFQvANAAAAAAAAAFgoBN8AAAAAAAAAgIVC8A0AAAAAAAAAWCgE3wAAAAAAAACAhULwDQAAAAAAAABYKP8fVXg6wMbhZPMAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_reconstruction_comparison(model, test_data)" + ] + }, + { + "cell_type": "markdown", + "id": "8f88d0ee", + "metadata": {}, + "source": [ + "Finally, we can use the model to extract features that capture variation in subcellular protein localization for datasets of arbitrary size without any additional training or Wasserstein computations. We can perform the same analyses with this feature space as we would with the GW-OT localization space including clustering, visualization, etc." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edaa7b88", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Extracting embeddings for 1670 unique images from PairedDataset...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Extracting embeddings: 100%|██████████| 27/27 [01:18<00:00, 2.92s/it]\n" + ] + }, + { + "data": { + "text/plain": [ + "(1670, 50)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "embeddings = extract_embeddings(model, test_data)\n", + "embeddings.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "827f1b72", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/plotly/express/_core.py:1992: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", + " sf: grouped.get_group(s if len(s) > 1 else s[0])\n" + ] + }, + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{hovertext}

color=plasma membrane
x=%{x}
y=%{y}", + "hovertext": [ + "1893_J14_31_5", + "1893_J14_31_9", + "1893_J14_33_4", + "203_B4_1_1", + "203_B4_1_7", + "204_B4_1_5", + "104_D9_1_8", + "121_A8_2_4", + "127_C11_1_10", + "127_C11_2_2", + "128_C11_1_4", + "128_C11_1_9", + "128_C11_2_4", + "128_C11_2_5", + "129_C11_2_8", + "628_F4_1_14", + "628_F4_2_5", + "628_F4_2_10", + "630_F4_1_12", + "609_F2_5_8", + "143_H4_2_9", + "144_H4_1_11", + "145_H4_2_6", + "175_G2_2_7", + "176_G2_1_6", + "808_F1_6_5", + "821_D4_1_2", + "821_D4_2_7", + "821_D4_2_15", + "855_F1_1_11", + "108_B8_1_11", + "643_B2_1_12", + "643_B2_3_8", + "644_B2_1_6", + "644_B2_3_2", + "644_B2_3_4", + "644_B2_3_5", + "645_B2_2_4", + "645_B2_2_6", + "645_B2_2_7", + "645_B2_2_9", + "645_B2_2_10", + "645_B2_2_11", + "87_B8_1_13", + "87_B8_2_4", + "945_B2_1_2", + "945_B2_1_4", + "945_B2_1_5", + "945_D3_1_3", + "945_D3_3_4", + "946_B2_2_3", + "946_B2_2_6", + "946_D3_4_4", + "953_A2_4_9", + "953_A2_5_9", + "953_B2_2_7", + "634_C6_1_2", + "634_C6_1_6", + "634_C6_1_11", + "635_C6_4_3", + "639_C6_3_4", + "639_C6_9_5", + "639_C6_9_8", + "522_F6_4_4", + "527_F6_1_7", + "529_F6_1_2", + "529_F6_2_7", + "33_C8_2_7", + "34_C8_1_5", + "924_A5_2_7", + "924_A5_2_8", + "924_A5_2_18", + "126_G6_2_9", + "761_A9_1_6", + "769_A9_1_8", + "769_A9_2_5", + "769_A9_2_14", + "192_F11_1_6", + "194_F11_1_7", + "403_F9_1_14", + "403_F9_1_17", + "406_F9_2_9", + "408_F9_2_6", + "408_F9_2_7", + "411_E2_2_6", + "411_E2_2_9", + "415_E2_1_1", + "415_E2_1_6", + "416_E2_1_7", + "416_E2_2_11", + "26_E9_2_3", + "26_E9_2_4", + "27_E9_2_4", + "27_E9_2_5", + "27_E9_2_7", + "28_E9_1_4", + "330_F5_2_3", + "331_F5_1_2", + "331_F5_1_3", + "534_A7_1_2", + "616_C6_1_3", + "616_C6_2_11", + "616_C6_2_13", + "619_C6_3_6", + "619_C6_3_10", + "619_C6_3_12" + ], + "legendgroup": "plasma membrane", + "marker": { + "color": "#1F77B4", + "symbol": "circle" + }, + "mode": "markers", + "name": "plasma membrane", + "showlegend": true, + "type": "scattergl", + "x": [ + 11.437403678894043, + 0.9753666520118713, + 5.412958145141602, + 0.7044373750686646, + 0.644619882106781, + 3.7471165657043457, + 4.061347961425781, + 3.6586601734161377, + 11.79686450958252, + 5.832003116607666, + 3.871305227279663, + 8.839500427246094, + 9.447354316711426, + 3.0408411026000977, + 11.343116760253906, + 5.21724796295166, + 1.2729097604751587, + 9.691274642944336, + -1.1111730337142944, + 1.4131866693496704, + 6.445054531097412, + 1.3725026845932007, + -1.776132583618164, + -2.4633305072784424, + 1.8864562511444092, + 5.897722244262695, + -2.440277099609375, + -2.5847575664520264, + 4.721423149108887, + 2.2710928916931152, + -0.6730172038078308, + -1.6783615350723267, + -2.481581687927246, + 0.4589780867099762, + 3.0481154918670654, + 5.033864498138428, + 9.840714454650879, + 1.7739332914352417, + -2.128037929534912, + 3.1001789569854736, + 1.602852702140808, + 2.958256721496582, + 4.912827014923096, + -0.8501579165458679, + 8.404081344604492, + 3.6023354530334473, + 2.0455422401428223, + 3.0278706550598145, + -2.5447676181793213, + 2.9879989624023438, + 5.803518295288086, + 3.060000419616699, + -0.9152354598045349, + -1.149532675743103, + 2.999992847442627, + -0.13651371002197266, + 1.4880332946777344, + 2.567767858505249, + -0.7363086938858032, + 0.8363190293312073, + 3.3264000415802, + 6.880521774291992, + 5.91978645324707, + 9.405186653137207, + -1.8751944303512573, + -0.35842785239219666, + -1.4501771926879883, + -1.1379073858261108, + -0.2031283974647522, + -1.5081257820129395, + -1.6545953750610352, + 1.8209785223007202, + 1.37326979637146, + -1.9959094524383545, + -1.641442060470581, + 1.2992384433746338, + -2.4385926723480225, + 7.88951301574707, + -0.22781488299369812, + 9.566537857055664, + 0.32222241163253784, + 1.1269505023956299, + -0.8502129912376404, + 3.35174560546875, + 10.244566917419434, + 1.8939008712768555, + 3.230133533477783, + 3.6231608390808105, + 8.631754875183105, + 0.6324849128723145, + 8.0845365524292, + 1.249221920967102, + 4.006199836730957, + 0.2157762497663498, + -0.09391152113676071, + 10.655365943908691, + 1.9586269855499268, + 8.84089183807373, + -0.8341182470321655, + 2.0201961994171143, + -2.3033456802368164, + 3.3454809188842773, + 9.393486022949219, + -0.4702509939670563, + 4.0983662605285645, + 9.199991226196289 + ], + "xaxis": "x", + "y": [ + 7.936084270477295, + 9.704239845275879, + 9.141460418701172, + 10.250493049621582, + 10.151857376098633, + 9.1812744140625, + 10.091902732849121, + 10.03331184387207, + 8.355684280395508, + 10.822183609008789, + 10.873313903808594, + 7.967275619506836, + 8.068228721618652, + 10.985326766967773, + 7.822197914123535, + 7.809582233428955, + 9.267227172851562, + 7.858602046966553, + 9.878408432006836, + 10.777179718017578, + 8.023723602294922, + 10.406922340393066, + 10.146177291870117, + 9.866507530212402, + 10.324658393859863, + 9.35986614227295, + 9.886747360229492, + 9.872200012207031, + 9.926318168640137, + 11.072028160095215, + 10.521811485290527, + 9.934243202209473, + 9.8511962890625, + 10.352774620056152, + 8.713736534118652, + 7.866868495941162, + 11.119586944580078, + 8.64672565460205, + 9.831222534179688, + 8.546686172485352, + 8.909188270568848, + 8.339441299438477, + 7.897364139556885, + 9.708395004272461, + 8.057068824768066, + 8.93982219696045, + 8.533744812011719, + 9.264866828918457, + 9.82258129119873, + 8.92735481262207, + 8.466211318969727, + 8.489449501037598, + 10.478937149047852, + 9.831676483154297, + 7.913784027099609, + 9.86153507232666, + 9.814777374267578, + 8.598983764648438, + 10.406044960021973, + 10.563523292541504, + 10.901388168334961, + 8.758687019348145, + 7.985221862792969, + 7.465484142303467, + 10.177242279052734, + 10.123150825500488, + 9.815028190612793, + 10.274921417236328, + 9.921756744384766, + 10.005097389221191, + 10.114779472351074, + 8.876811981201172, + 9.927469253540039, + 9.839347839355469, + 10.184244155883789, + 10.469877243041992, + 9.827385902404785, + 8.090365409851074, + 10.330046653747559, + 8.025254249572754, + 10.313115119934082, + 10.115166664123535, + 9.337325096130371, + 9.881591796875, + 7.873258590698242, + 8.86505126953125, + 8.711174964904785, + 10.431783676147461, + 7.816223621368408, + 9.694914817810059, + 8.116555213928223, + 10.471688270568848, + 8.965024948120117, + 10.340896606445312, + 10.102538108825684, + 7.805060863494873, + 10.220816612243652, + 8.126763343811035, + 10.019498825073242, + 10.809520721435547, + 9.81965160369873, + 10.378925323486328, + 8.554923057556152, + 9.500971794128418, + 10.193597793579102, + 7.524752616882324 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=vesicles
x=%{x}
y=%{y}", + "hovertext": [ + "1894_C2_2_6", + "1894_C2_3_6", + "1894_C2_3_8", + "1894_C2_3_11", + "570_F10_1_4", + "570_F10_1_9", + "100_F5_1_5", + "100_F5_1_14", + "101_F5_1_12", + "101_F5_2_7", + "101_F5_2_18", + "658_F1_5_1", + "659_F1_4_5", + "659_F1_6_10", + "59_G12_1_3", + "59_G12_2_10", + "60_G12_1_4", + "61_G12_1_11", + "61_G12_2_12", + "68_G12_1_4", + "68_G12_2_2", + "68_G12_2_7", + "69_G12_1_3", + "69_G12_2_3", + "69_G12_2_8", + "91_G12_2_11", + "548_C9_3_9", + "548_C9_4_10", + "461_F11_2_6", + "461_F11_2_8", + "462_F11_1_4", + "462_F11_1_8", + "464_F11_1_8", + "464_F11_1_10", + "464_F11_1_11", + "464_F11_2_3", + "464_F11_2_11", + "562_D3_3_7", + "568_D3_1_10", + "568_D3_2_2", + "568_D3_2_5", + "575_D3_2_10", + "837_D1_1_7", + "215_C9_2_1", + "217_C9_1_5", + "217_C9_1_13", + "610_B10_1_11", + "610_B10_2_13", + "617_B10_1_2", + "617_B10_2_4", + "617_B10_2_9", + "180_H3_2_16", + "181_H3_2_9", + "189_E8_1_1", + "189_E8_1_2", + "189_E8_1_4", + "611_F6_1_4", + "618_F6_1_6", + "618_F6_3_7", + "57_E8_1_14", + "57_E8_2_10", + "58_E8_1_6", + "58_E8_2_10", + "1_H3_1_6", + "2_H3_1_12", + "2_H3_2_3", + "2_H3_2_4", + "234_G5_1_8", + "234_G5_2_2", + "234_G5_2_4", + "535_G5_1_5", + "62_G12_1_6", + "62_G12_1_12", + "62_G12_2_2", + "62_G12_2_4", + "62_G12_2_5", + "63_G12_2_3", + "93_G12_1_2", + "93_G12_1_6", + "421_F7_3_7", + "421_F7_3_14", + "421_F7_3_15", + "421_F7_4_14", + "427_F7_1_12", + "282_B11_2_9", + "283_B11_1_7", + "215_D12_1_5", + "216_D12_1_10", + "609_A7_8_12", + "615_G5_1_10", + "615_G5_1_12", + "615_G5_2_11", + "449_C6_3_21", + "449_C6_5_5", + "452_C6_3_5", + "452_C6_3_7", + "452_C6_3_8", + "455_C6_3_7", + "455_C6_4_4", + "20_A6_1_5", + "20_A6_1_7", + "20_A6_1_10", + "20_A6_2_5", + "21_A6_1_6", + "21_A6_2_6", + "22_A6_1_3", + "22_A6_2_8", + "20_F2_2_10", + "21_F2_2_3", + "21_F2_2_4", + "154_G9_1_3", + "154_G9_1_6", + "198_F9_2_6", + "105_G9_1_11", + "107_G9_1_7", + "107_G9_2_9", + "480_E5_1_7", + "510_E5_1_17", + "510_E5_1_22", + "510_E5_2_9" + ], + "legendgroup": "vesicles", + "marker": { + "color": "#FF7F0E", + "symbol": "circle" + }, + "mode": "markers", + "name": "vesicles", + "showlegend": true, + "type": "scattergl", + "x": [ + 9.285935401916504, + 8.188887596130371, + -2.1349825859069824, + 1.8502135276794434, + 3.700629711151123, + 5.424103736877441, + 3.513672113418579, + 6.7465643882751465, + 4.378493785858154, + -0.7859390377998352, + 10.192152976989746, + -1.03993558883667, + 10.872058868408203, + -0.035113196820020676, + 6.049213886260986, + 1.3545780181884766, + 5.305659770965576, + 1.8879388570785522, + 5.596472263336182, + 1.9471548795700073, + 11.907737731933594, + 9.179183006286621, + -1.1680008172988892, + 2.7808775901794434, + 3.60530948638916, + 10.39724349975586, + 7.479644775390625, + 10.161272048950195, + 7.378576278686523, + 4.451239585876465, + 5.005497455596924, + 3.140004873275757, + -0.6802276372909546, + 0.11550435423851013, + 9.235568046569824, + 9.834880828857422, + 1.7885063886642456, + 6.955780506134033, + -0.4366675615310669, + 7.296996116638184, + 7.347056865692139, + 3.147331714630127, + 8.8035306930542, + 9.553465843200684, + 1.432139277458191, + 11.767281532287598, + 6.615468502044678, + 9.383200645446777, + 6.807214736938477, + 0.9641739130020142, + 8.051589012145996, + -1.6589112281799316, + 4.783672332763672, + 3.41520357131958, + -1.7560995817184448, + -0.16280552744865417, + 7.946258068084717, + 3.0204873085021973, + 5.899518013000488, + 6.751859188079834, + 1.0665112733840942, + 7.366313934326172, + 0.9462012052536011, + 2.8204233646392822, + 2.9834346771240234, + -1.9932167530059814, + 6.607781887054443, + 2.2607932090759277, + 10.324588775634766, + 11.332099914550781, + 2.415109395980835, + 2.2832024097442627, + 2.387169122695923, + 6.7409257888793945, + 5.995389461517334, + 4.461305618286133, + 8.65954875946045, + 3.7143654823303223, + 3.5918898582458496, + 5.593886375427246, + 6.837454795837402, + 5.100203514099121, + 1.936654806137085, + 3.2464439868927, + 5.79866361618042, + 10.950220108032227, + -0.8656067848205566, + 7.19631290435791, + 2.841010093688965, + -0.5591171383857727, + 6.586352825164795, + 2.9887638092041016, + -0.14628253877162933, + 9.166753768920898, + 4.695462703704834, + 0.8004719614982605, + 9.159929275512695, + 4.934075355529785, + 4.509697914123535, + 10.878599166870117, + 5.082061290740967, + 7.3030290603637695, + -0.2727987766265869, + 1.6556165218353271, + -1.7240053415298462, + 2.7053751945495605, + 3.8205296993255615, + 4.844299793243408, + 2.3640060424804688, + 9.004129409790039, + 2.428449869155884, + 0.309872567653656, + 3.7069642543792725, + 3.058600902557373, + 6.140074253082275, + 7.282569885253906, + 11.224671363830566, + 8.972673416137695, + -0.39150699973106384, + 0.8405913710594177 + ], + "xaxis": "x", + "y": [ + 7.289881706237793, + 8.7659912109375, + 9.7770357131958, + 8.270730972290039, + 9.527826309204102, + 8.027181625366211, + 10.424854278564453, + 7.803881645202637, + 10.145621299743652, + 9.74114990234375, + 7.690700054168701, + 9.220887184143066, + 7.420437335968018, + 9.686607360839844, + 8.78044319152832, + 10.188528060913086, + 10.924087524414062, + 8.53693675994873, + 8.495870590209961, + 8.49335765838623, + 8.478896141052246, + 7.377534866333008, + 10.36387825012207, + 10.6628999710083, + 8.468499183654785, + 7.4372758865356445, + 7.719824314117432, + 7.274416446685791, + 7.724620819091797, + 8.947203636169434, + 7.821033477783203, + 8.868184089660645, + 9.957353591918945, + 9.710670471191406, + 7.826048851013184, + 7.364233016967773, + 10.413451194763184, + 9.131030082702637, + 9.233217239379883, + 7.816628456115723, + 10.327174186706543, + 9.030776023864746, + 10.590176582336426, + 8.165312767028809, + 10.444474220275879, + 8.099729537963867, + 8.736586570739746, + 8.048969268798828, + 8.896136283874512, + 10.299905776977539, + 8.038553237915039, + 9.955303192138672, + 8.838457107543945, + 9.309002876281738, + 9.471121788024902, + 9.140958786010742, + 9.467670440673828, + 9.211688995361328, + 9.847558975219727, + 7.894697189331055, + 10.316429138183594, + 7.698606491088867, + 9.82711124420166, + 9.387619972229004, + 9.48202896118164, + 9.602668762207031, + 9.099685668945312, + 9.294034004211426, + 7.508839130401611, + 7.840555191040039, + 9.198125839233398, + 8.043542861938477, + 8.16727066040039, + 7.905535697937012, + 9.071355819702148, + 8.35134220123291, + 10.681903839111328, + 10.253159523010254, + 9.983001708984375, + 9.079646110534668, + 8.83973503112793, + 8.378059387207031, + 10.316120147705078, + 9.405381202697754, + 9.751474380493164, + 8.695728302001953, + 9.660832405090332, + 8.615999221801758, + 8.052789688110352, + 10.503029823303223, + 11.108725547790527, + 9.361307144165039, + 10.352855682373047, + 7.784286975860596, + 8.839875221252441, + 10.362573623657227, + 8.25741958618164, + 8.546942710876465, + 8.920453071594238, + 8.173662185668945, + 7.828895568847656, + 7.738155364990234, + 10.17492389678955, + 9.707717895507812, + 10.170764923095703, + 8.01125717163086, + 9.750064849853516, + 8.711328506469727, + 9.283551216125488, + 7.891744613647461, + 8.5425443649292, + 10.230094909667969, + 10.961301803588867, + 8.805376052856445, + 9.735586166381836, + 10.058271408081055, + 7.829433441162109, + 7.636054992675781, + 9.765263557434082, + 10.243302345275879 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=peroxisomes
x=%{x}
y=%{y}", + "hovertext": [ + "376_A2_1_8", + "378_A2_1_7", + "378_A2_2_9", + "378_A2_2_10", + "383_A2_1_2", + "383_A2_2_1", + "383_A2_2_9", + "17_H7_2_4", + "17_H7_2_7", + "18_H7_2_6", + "19_H7_2_8", + "537_C1_1_7", + "540_C1_1_14", + "540_C1_1_16", + "319_B8_3_3", + "319_B8_3_12", + "319_H7_1_5", + "319_H7_1_9", + "319_H7_2_5", + "319_H7_2_6", + "320_H7_1_8", + "340_B8_1_17", + "340_B8_2_5", + "340_H7_2_3", + "221_C12_1_7", + "221_C12_1_19", + "221_C12_2_9", + "221_C12_2_10", + "234_G8_2_3", + "234_G8_2_7", + "234_G8_2_8", + "234_H8_1_6", + "234_H8_1_8", + "234_H8_2_3", + "234_H8_2_6", + "235_G8_1_12", + "235_G8_1_13", + "189_G2_1_3", + "189_G2_2_9", + "190_G2_1_9", + "190_G2_2_6", + "191_G2_1_3", + "191_G2_2_4", + "599_G11_1_12", + "601_G11_1_5", + "601_G11_2_7", + "603_G11_1_7", + "603_G11_2_12", + "616_F1_3_7", + "616_F1_4_8", + "619_F1_3_10", + "239_G8_1_4", + "240_G8_1_4", + "240_G8_2_12", + "241_G8_1_7", + "241_G8_2_5", + "241_G8_2_12" + ], + "legendgroup": "peroxisomes", + "marker": { + "color": "#2CA02C", + "symbol": "circle" + }, + "mode": "markers", + "name": "peroxisomes", + "showlegend": true, + "type": "scattergl", + "x": [ + 6.472165584564209, + 6.869111061096191, + 7.188961029052734, + 5.055633068084717, + 4.901391983032227, + 6.13953971862793, + 4.824241638183594, + 7.997182846069336, + 3.030001640319824, + -2.524125099182129, + 10.038616180419922, + 10.060962677001953, + -0.9788990616798401, + 7.207653999328613, + 4.925546169281006, + 7.4718828201293945, + -0.09779885411262512, + 8.963629722595215, + -1.4146732091903687, + -0.973852276802063, + -0.7711370587348938, + 1.4009915590286255, + -2.228506088256836, + 2.2348575592041016, + 0.07671178132295609, + 0.11310240626335144, + 2.6619460582733154, + 4.26826286315918, + 3.438533067703247, + 2.182927131652832, + 4.272791385650635, + 4.407793045043945, + -2.2373194694519043, + 10.332476615905762, + -0.2822438180446625, + 10.175993919372559, + 8.974346160888672, + 4.766129493713379, + 7.3680548667907715, + 9.267241477966309, + 9.489072799682617, + 1.8968555927276611, + 7.168770790100098, + -0.1053372323513031, + 4.911126136779785, + 2.3288686275482178, + 6.365429401397705, + 3.949941635131836, + 4.285129547119141, + 7.563950538635254, + -2.3498873710632324, + 5.872560024261475, + 5.297318935394287, + 11.096592903137207, + 6.80879545211792, + 2.5467498302459717, + 5.174282073974609 + ], + "xaxis": "x", + "y": [ + 8.732595443725586, + 7.8702898025512695, + 9.99589729309082, + 10.012714385986328, + 9.741880416870117, + 10.489699363708496, + 8.776191711425781, + 7.933918476104736, + 9.450212478637695, + 9.738481521606445, + 7.9263410568237305, + 7.82438325881958, + 9.68112850189209, + 8.965191841125488, + 10.045562744140625, + 8.392416000366211, + 10.033467292785645, + 7.905089855194092, + 10.093085289001465, + 9.654531478881836, + 10.514399528503418, + 9.307766914367676, + 9.947361946105957, + 8.702811241149902, + 10.144081115722656, + 9.988424301147461, + 9.030776977539062, + 9.965315818786621, + 9.975924491882324, + 8.980236053466797, + 9.639779090881348, + 10.104620933532715, + 9.934484481811523, + 7.43812894821167, + 9.33027458190918, + 7.191001892089844, + 7.391987323760986, + 8.47756290435791, + 9.2401123046875, + 8.046232223510742, + 7.262880802154541, + 8.487102508544922, + 7.812093257904053, + 10.635218620300293, + 7.869960784912109, + 9.690948486328125, + 8.399792671203613, + 11.35405445098877, + 10.388195991516113, + 9.872344017028809, + 9.909306526184082, + 10.12047004699707, + 11.057857513427734, + 8.65942668914795, + 10.089378356933594, + 9.019412994384766, + 10.62861156463623 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=mitochondria
x=%{x}
y=%{y}", + "hovertext": [ + "56_E12_1_11", + "57_E12_2_5", + "58_E12_1_4", + "58_E12_2_9", + "71_E5_1_3", + "71_E5_2_7", + "72_E5_1_6", + "72_E5_2_7", + "73_E5_1_16", + "73_E5_2_14", + "73_E5_2_16", + "567_E5_1_6", + "567_E5_1_7", + "567_E5_1_8", + "567_E5_1_9", + "562_C4_1_2", + "575_C4_2_5", + "575_C4_2_6", + "435_H3_1_8", + "435_H3_2_13", + "445_H3_1_8", + "445_H3_2_2", + "445_H3_2_12", + "448_H3_2_6", + "448_H3_2_15", + "636_C10_1_8", + "636_C10_1_14", + "636_C10_2_13", + "636_C10_2_15", + "637_C10_1_7", + "637_C10_1_9", + "778_H9_5_7", + "380_G5_1_3", + "382_G5_1_9", + "397_G5_2_12", + "188_C11_1_7", + "188_C11_1_10", + "188_C11_2_8", + "189_G4_1_3", + "189_G4_2_7", + "190_G4_1_5", + "190_G4_1_10", + "190_G4_2_5", + "191_G4_1_1", + "284_F11_1_9", + "284_F11_2_2", + "284_F11_2_6", + "284_F11_2_10", + "286_F11_2_10", + "286_F11_2_16", + "563_F8_1_3", + "563_F8_1_4", + "563_F8_2_1", + "563_F8_2_2", + "566_F8_2_11", + "17_B2_1_9", + "17_B2_2_4", + "149_B7_1_5", + "150_B7_2_6", + "151_B7_1_3", + "192_B8_1_2", + "192_B8_1_6", + "193_B8_1_13", + "193_B8_2_7", + "430_A3_2_6", + "432_A3_1_9", + "432_A3_3_2", + "441_A3_1_3", + "570_H4_3_7", + "570_H4_3_11", + "108_E5_1_2", + "108_E5_1_3", + "85_E5_1_4", + "85_E5_2_3", + "85_E5_2_4", + "85_E5_2_5", + "87_E5_2_8", + "222_F11_1_2", + "222_F11_1_5", + "222_F11_2_6", + "222_F11_2_11", + "222_F11_2_15", + "248_F11_1_4", + "270_D12_1_9", + "270_D12_1_14", + "271_D12_1_16", + "271_D12_2_1", + "272_D12_1_12", + "1912_B11_17_cr5c8627fbe4f4e_3", + "1912_B11_17_cr5c8627fbe4f4e_7", + "35_H11_1_2", + "36_H11_2_10", + "37_H11_1_2", + "14_A3_2_3", + "16_A3_2_9", + "43_A1_2_1", + "1199_G11_5_7", + "1199_G11_5_8", + "1199_G11_5_12", + "1199_G11_5_13", + "146_E8_1_2", + "146_E8_2_9", + "147_E8_2_3", + "74_G1_1_9", + "75_G1_1_4", + "75_G1_1_6", + "75_G1_2_7", + "76_G1_1_4", + "76_G1_1_11", + "76_G1_2_9", + "582_B10_1_8", + "582_B10_2_5" + ], + "legendgroup": "mitochondria", + "marker": { + "color": "#D62728", + "symbol": "circle" + }, + "mode": "markers", + "name": "mitochondria", + "showlegend": true, + "type": "scattergl", + "x": [ + 8.876245498657227, + 2.592487096786499, + 6.91989803314209, + 7.123671531677246, + 4.439266204833984, + 4.445375442504883, + -2.008667230606079, + 10.68436336517334, + 4.824851036071777, + 7.1045241355896, + 2.5305356979370117, + 6.350950717926025, + -2.0021302700042725, + 4.325897216796875, + 10.036189079284668, + 4.099804878234863, + 6.744747161865234, + 7.263899326324463, + 3.008392095565796, + -2.6602959632873535, + 0.8147668838500977, + 5.470724582672119, + 3.329603433609009, + 7.558781147003174, + 5.122820854187012, + 10.487521171569824, + 8.172492980957031, + 12.123702049255371, + 7.198514938354492, + 5.918031215667725, + 2.890904188156128, + 11.37468433380127, + 6.347843647003174, + 1.974286675453186, + 2.551262617111206, + 11.069531440734863, + 1.3419978618621826, + 2.02968430519104, + 2.03627610206604, + 4.750566482543945, + 10.364418983459473, + 4.623544692993164, + 11.74278450012207, + 8.99223804473877, + 8.174579620361328, + 4.8072710037231445, + -0.2908617854118347, + 4.289394378662109, + 5.733397960662842, + 7.214886665344238, + 11.099199295043945, + -0.12719126045703888, + 2.1853928565979004, + 2.9407875537872314, + 2.308027744293213, + 2.604381561279297, + -0.9825682044029236, + 7.268889904022217, + 2.054063320159912, + 6.805116653442383, + 9.154326438903809, + 10.378458976745605, + 3.3709988594055176, + 2.823060989379883, + -0.07893073558807373, + 3.6117122173309326, + 5.212403774261475, + 4.8054585456848145, + 2.685934066772461, + 1.4873015880584717, + 4.126687049865723, + 6.108668327331543, + -1.5714633464813232, + 10.567143440246582, + 9.957620620727539, + 6.78408145904541, + 7.35112190246582, + 3.6057562828063965, + 11.41600227355957, + 3.4389665126800537, + 4.265722751617432, + 2.6232917308807373, + 2.0049502849578857, + -0.8300260901451111, + 4.765443801879883, + 7.496279716491699, + 2.9706203937530518, + 3.281561851501465, + 7.206207752227783, + 10.750850677490234, + -1.1239618062973022, + 11.153931617736816, + 6.692214012145996, + 4.238689422607422, + 4.4721150398254395, + 6.815250873565674, + 4.306057929992676, + 4.341485023498535, + 2.4668378829956055, + -2.594738721847534, + 4.7967119216918945, + 2.793553113937378, + 5.6059136390686035, + 8.938620567321777, + 3.874255895614624, + 10.577744483947754, + 2.7244932651519775, + 1.6260573863983154, + 6.062060832977295, + 7.27086067199707, + 4.487593650817871, + 4.668821334838867 + ], + "xaxis": "x", + "y": [ + 7.506082534790039, + 8.167549133300781, + 7.8048577308654785, + 7.733267307281494, + 8.527247428894043, + 10.18327522277832, + 9.64476203918457, + 7.50034236907959, + 8.592729568481445, + 9.94688892364502, + 8.113268852233887, + 9.129034996032715, + 9.891816139221191, + 9.080912590026855, + 7.237348556518555, + 8.366143226623535, + 9.707283973693848, + 9.07557201385498, + 8.784873008728027, + 9.820612907409668, + 9.968594551086426, + 8.815163612365723, + 10.811614990234375, + 9.025642395019531, + 8.56679916381836, + 7.644589424133301, + 9.378528594970703, + 9.02999210357666, + 11.10313606262207, + 8.405207633972168, + 9.096006393432617, + 8.080013275146484, + 9.413065910339355, + 8.6026611328125, + 8.081436157226562, + 7.625970363616943, + 9.073594093322754, + 9.181245803833008, + 9.13595962524414, + 11.729829788208008, + 10.4729642868042, + 9.357112884521484, + 8.135985374450684, + 7.658276081085205, + 9.30790901184082, + 8.603901863098145, + 10.4024658203125, + 10.998247146606445, + 10.777832984924316, + 9.980413436889648, + 7.649085521697998, + 9.729503631591797, + 8.674214363098145, + 8.663740158081055, + 9.393211364746094, + 8.380891799926758, + 9.293384552001953, + 9.956907272338867, + 9.513540267944336, + 9.206315994262695, + 7.516440391540527, + 7.715972423553467, + 9.214858055114746, + 9.072473526000977, + 10.285920143127441, + 8.715740203857422, + 8.29838752746582, + 8.624533653259277, + 8.398615837097168, + 10.236687660217285, + 9.85976791381836, + 8.899551391601562, + 9.356600761413574, + 7.453137397766113, + 7.311354637145996, + 9.899303436279297, + 9.285529136657715, + 9.634527206420898, + 7.90163516998291, + 10.662906646728516, + 9.868156433105469, + 9.340903282165527, + 9.280007362365723, + 9.200821876525879, + 9.141107559204102, + 9.022119522094727, + 8.858643531799316, + 9.359962463378906, + 9.102997779846191, + 7.569579601287842, + 9.283219337463379, + 8.651147842407227, + 11.231049537658691, + 8.426332473754883, + 11.140820503234863, + 8.266864776611328, + 8.106871604919434, + 9.885316848754883, + 8.192584991455078, + 9.849528312683105, + 11.788207054138184, + 8.671976089477539, + 8.27782154083252, + 7.4269561767578125, + 8.388533592224121, + 7.609335422515869, + 9.161142349243164, + 9.5056791305542, + 9.179010391235352, + 9.918281555175781, + 9.848281860351562, + 11.748169898986816 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=golgi apparatus
x=%{x}
y=%{y}", + "hovertext": [ + "125_A11_2_5", + "126_A11_1_10", + "126_A11_1_14", + "126_A11_2_8", + "924_F5_2_7", + "924_F5_2_12", + "932_F5_5_4", + "932_F5_5_15", + "402_G10_5_8", + "366_A6_3_1", + "366_A6_3_7", + "370_A6_1_9", + "370_A6_2_5", + "370_A6_2_6", + "373_A6_2_6", + "373_A6_2_7", + "460_F7_1_10", + "460_F7_2_4", + "465_F7_1_5", + "465_F7_2_12", + "467_F7_1_14", + "467_F7_2_5", + "221_C7_1_7", + "221_C7_1_12", + "221_C7_2_7", + "221_C7_2_10", + "776_F3_1_9", + "899_F3_1_12", + "502_C6_2_7", + "502_C6_2_9", + "557_C6_1_7", + "557_C6_2_8", + "557_C6_2_10", + "557_C6_2_12", + "183_F8_1_8", + "183_F8_2_2", + "242_F8_3_8", + "242_F8_4_4", + "301_G12_2_12", + "342_G12_1_5", + "342_G12_1_9", + "397_D6_3_12", + "563_E12_2_4", + "563_E12_2_5", + "566_E12_4_2", + "566_E12_5_2", + "566_E12_5_5", + "569_E12_1_9", + "569_E12_5_2", + "563_H2_2_7", + "566_H2_2_8", + "569_H2_1_5", + "569_H2_2_14", + "88_C1_1_9", + "88_C1_2_6", + "88_C1_2_9", + "88_C1_2_10", + "89_C1_2_7", + "89_C1_2_12", + "90_C1_1_3", + "90_C1_1_9", + "1001_F7_1_11", + "433_B5_1_2", + "433_B5_2_11", + "440_B5_1_3", + "440_B5_2_9", + "973_A8_1_10", + "973_A8_2_18", + "992_F7_2_19", + "295_F7_1_9", + "296_F7_2_20", + "297_F7_2_5", + "377_F10_1_6", + "377_F10_2_3", + "377_F10_2_12", + "379_F10_1_9", + "379_F10_2_15", + "390_F10_2_5", + "551_A9_2_7", + "551_A9_2_8", + "554_A9_2_3", + "560_A9_2_4", + "921_A12_1_9", + "921_A12_1_12", + "931_A12_1_10", + "620_A4_1_8", + "620_A4_5_5", + "620_A4_5_6", + "620_A4_5_7", + "620_A4_5_9", + "623_A4_3_9", + "627_A4_1_11", + "911_C10_1_7", + "912_C10_3_3", + "912_C10_3_9", + "918_A8_1_10", + "918_A8_2_7", + "486_F3_1_7", + "490_F3_1_3", + "490_F3_2_3", + "509_F3_1_11", + "509_F3_1_22", + "509_F3_2_10", + "509_F3_2_20", + "509_F3_2_22", + "509_F3_2_23", + "666_E7_1_9", + "666_E7_1_15", + "666_E7_2_9", + "669_E7_1_2", + "669_E7_2_7", + "671_E7_2_9", + "41_G10_1_5", + "41_G10_1_7", + "41_G10_2_9", + "42_G10_1_5", + "43_G10_2_4", + "43_G10_2_6", + "1244_G6_2_7", + "1244_G6_3_4", + "1244_G6_3_6", + "198_H8_1_4", + "198_H8_2_11" + ], + "legendgroup": "golgi apparatus", + "marker": { + "color": "#9467BD", + "symbol": "circle" + }, + "mode": "markers", + "name": "golgi apparatus", + "showlegend": true, + "type": "scattergl", + "x": [ + 5.004769325256348, + 6.885021209716797, + 2.5892746448516846, + 4.113199710845947, + 1.5546205043792725, + 11.063040733337402, + 11.625768661499023, + 5.366086959838867, + -0.6872038245201111, + 11.372458457946777, + 5.6419572830200195, + 9.882997512817383, + 10.720466613769531, + 6.625646114349365, + 6.331587314605713, + 0.6520422101020813, + 8.500747680664062, + 1.4282920360565186, + 4.633726596832275, + 3.6322455406188965, + 0.44843366742134094, + 5.692556858062744, + 1.8695635795593262, + 4.854382514953613, + 7.116418838500977, + 8.344352722167969, + 3.586064338684082, + 7.9069743156433105, + 4.28118371963501, + 7.883293151855469, + 6.260671138763428, + 7.074736595153809, + 6.772404670715332, + 11.87542724609375, + 9.616998672485352, + -0.19140402972698212, + 1.8278220891952515, + 11.13999080657959, + 4.78341817855835, + 7.318698883056641, + 4.397075176239014, + 6.60101842880249, + 9.862390518188477, + 5.766507148742676, + 9.529487609863281, + 0.051704928278923035, + 1.430942177772522, + 9.626619338989258, + 4.420607566833496, + 7.392518997192383, + 6.884763240814209, + 2.9589645862579346, + -1.6828659772872925, + 6.226981163024902, + 9.230490684509277, + 6.3324151039123535, + 4.440446376800537, + -0.4694593846797943, + 8.809886932373047, + 5.745002746582031, + 5.855489730834961, + 1.5564297437667847, + 4.605833053588867, + 5.538403034210205, + 3.033667802810669, + 0.5195502638816833, + 1.4377572536468506, + 5.498476028442383, + 3.7187488079071045, + 3.8171751499176025, + 9.619446754455566, + 4.329771041870117, + 4.624619960784912, + 7.876868724822998, + 11.729756355285645, + 6.482407093048096, + 4.316767692565918, + 5.708987712860107, + 11.590185165405273, + 7.847529411315918, + 3.056454658508301, + 10.861926078796387, + -0.43476107716560364, + 7.443984031677246, + 6.187744140625, + -2.542734146118164, + 5.505887508392334, + 12.234068870544434, + 9.688319206237793, + 4.874911308288574, + 6.314250469207764, + 4.226566314697266, + 1.1826478242874146, + 6.0188493728637695, + 4.232884883880615, + 7.381350517272949, + 3.790065050125122, + 6.248597621917725, + 8.648795127868652, + 9.368646621704102, + 1.7166513204574585, + -1.0323700904846191, + 11.690174102783203, + 11.734078407287598, + 11.297609329223633, + 10.098816871643066, + 12.624245643615723, + 6.4781999588012695, + 7.4839186668396, + 5.504943370819092, + 8.867866516113281, + 4.2560505867004395, + 6.360586643218994, + 2.758881092071533, + 9.936935424804688, + 9.382822036743164, + 2.249675989151001, + 10.382281303405762, + 6.372960567474365, + 5.175435543060303, + 6.775574684143066, + 8.826420783996582, + 5.570595741271973 + ], + "xaxis": "x", + "y": [ + 8.863319396972656, + 7.86146354675293, + 7.949977397918701, + 8.081941604614258, + 10.633055686950684, + 7.616848945617676, + 8.072561264038086, + 8.551000595092773, + 10.400092124938965, + 7.775071620941162, + 10.486204147338867, + 7.589285373687744, + 7.468497276306152, + 8.710862159729004, + 9.796310424804688, + 9.28459644317627, + 8.510796546936035, + 9.847847938537598, + 8.648054122924805, + 8.568385124206543, + 9.685846328735352, + 9.293923377990723, + 9.446496963500977, + 10.384177207946777, + 10.308466911315918, + 10.655710220336914, + 9.149362564086914, + 8.168458938598633, + 11.16461181640625, + 8.206631660461426, + 9.159958839416504, + 8.489871978759766, + 9.709561347961426, + 9.708334922790527, + 8.345226287841797, + 9.957003593444824, + 9.456904411315918, + 9.155204772949219, + 11.763157844543457, + 9.850730895996094, + 11.077397346496582, + 8.231725692749023, + 7.4063029289245605, + 9.78343391418457, + 8.270895004272461, + 9.608264923095703, + 9.810025215148926, + 7.25045919418335, + 9.857603073120117, + 9.17324161529541, + 10.120267868041992, + 9.927001953125, + 9.926275253295898, + 9.27032470703125, + 7.254623889923096, + 9.363168716430664, + 8.60366439819336, + 10.406930923461914, + 7.659491062164307, + 8.587879180908203, + 8.58338737487793, + 10.284642219543457, + 8.81346321105957, + 11.273054122924805, + 10.824094772338867, + 9.56298828125, + 9.849698066711426, + 9.768850326538086, + 11.127902030944824, + 11.12601089477539, + 8.600180625915527, + 10.29735279083252, + 9.463370323181152, + 8.188559532165527, + 8.388129234313965, + 8.93686580657959, + 9.919442176818848, + 9.764117240905762, + 8.112099647521973, + 8.629714965820312, + 9.285490989685059, + 8.687845230102539, + 10.175190925598145, + 7.818667888641357, + 9.282398223876953, + 9.755931854248047, + 9.680213928222656, + 9.040457725524902, + 8.351082801818848, + 11.76307201385498, + 9.074119567871094, + 8.464370727539062, + 9.946414947509766, + 9.71793270111084, + 10.32390022277832, + 8.892991065979004, + 10.101558685302734, + 10.355133056640625, + 9.8796968460083, + 7.43780517578125, + 9.139591217041016, + 10.028444290161133, + 8.668807029724121, + 8.012223243713379, + 8.686506271362305, + 7.576552391052246, + 8.080733299255371, + 8.967719078063965, + 9.24197769165039, + 9.911789894104004, + 7.354523658752441, + 10.026108741760254, + 9.16253662109375, + 7.855747222900391, + 7.608037948608398, + 7.956940650939941, + 8.128968238830566, + 10.463541030883789, + 10.678622245788574, + 8.651168823242188, + 10.006348609924316, + 7.589323997497559, + 8.86194133758545 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=actin filaments
x=%{x}
y=%{y}", + "hovertext": [ + "624_C6_1_5", + "624_C6_1_10", + "516_F6_2_3", + "516_F6_2_10", + "519_F6_7_2", + "519_F6_7_5", + "519_F6_7_12", + "556_F6_1_8", + "556_F6_1_9", + "556_F6_2_14", + "404_E3_5_12", + "407_E3_1_3", + "407_E3_1_4", + "407_E3_1_9", + "407_E3_1_10", + "410_E3_1_10", + "410_E3_4_10", + "411_B4_2_4", + "411_B4_2_11", + "411_B4_2_16", + "411_B4_3_5", + "415_B4_1_11", + "415_B4_2_2", + "583_F3_1_2", + "583_F3_1_4", + "598_F3_2_9", + "598_F3_2_12", + "598_F3_3_5", + "296_E6_2_6", + "296_E6_2_7", + "32_B6_1_7", + "33_B6_1_5", + "33_B6_1_9", + "33_B6_1_13", + "33_B6_2_12", + "34_B6_2_5", + "34_B6_2_10", + "34_B6_2_13", + "620_C5_1_11", + "620_C5_3_10", + "623_C5_4_7", + "627_C5_2_8", + "627_C5_2_15", + "813_B4_3_2", + "813_B4_3_3", + "475_E2_1_5", + "475_E2_1_7", + "477_E2_1_5", + "477_E2_1_9", + "477_E2_2_11", + "477_E2_2_13", + "479_E2_3_5", + "479_E2_3_10", + "757_E10_5_12", + "757_E10_7_14", + "769_E10_2_11", + "474_A12_2_8", + "474_A12_3_7", + "510_A12_2_3", + "510_A12_2_7", + "510_A12_2_9", + "510_A12_2_10", + "143_F2_1_3", + "143_F2_2_4", + "144_F2_1_6", + "145_F2_1_6", + "145_F2_2_10", + "616_F12_1_8", + "616_F12_2_7", + "616_F12_2_9", + "619_F12_1_5", + "619_F12_1_10" + ], + "legendgroup": "actin filaments", + "marker": { + "color": "#8C564B", + "symbol": "circle" + }, + "mode": "markers", + "name": "actin filaments", + "showlegend": true, + "type": "scattergl", + "x": [ + -0.051113713532686234, + -0.3435332477092743, + 5.0222697257995605, + 10.331456184387207, + 6.394937038421631, + 2.817870616912842, + 6.886628150939941, + 6.957649230957031, + 2.257040023803711, + -0.20112694799900055, + 3.4199514389038086, + 9.902653694152832, + 10.442671775817871, + 11.46597957611084, + 10.358049392700195, + 0.5125899314880371, + 3.6177830696105957, + 5.471142768859863, + 6.270131587982178, + 4.804708480834961, + -1.2990058660507202, + -1.4311481714248657, + 2.7070982456207275, + 11.526765823364258, + 4.160569667816162, + -1.592599868774414, + 9.404735565185547, + 0.5261075496673584, + 10.244186401367188, + -0.5476545095443726, + 3.653048276901245, + 4.7813591957092285, + -1.0907959938049316, + 3.370614767074585, + 0.2707626223564148, + 3.567871570587158, + 4.9233269691467285, + 4.275055885314941, + 4.538545608520508, + 1.699578881263733, + -1.0350489616394043, + 2.707329750061035, + 3.6574594974517822, + 7.553219318389893, + 3.2026891708374023, + 3.144845724105835, + -0.8374470472335815, + -1.6496148109436035, + 7.4375810623168945, + 10.336009979248047, + 6.328372001647949, + 1.7112127542495728, + 2.8879940509796143, + 2.018975257873535, + 1.6146135330200195, + 0.4613816738128662, + 2.9811699390411377, + 3.550312042236328, + 1.972017526626587, + 2.6542248725891113, + 3.5887928009033203, + 3.7342910766601562, + 5.1894426345825195, + -2.5406365394592285, + -2.4606575965881348, + 5.385213375091553, + 0.23866920173168182, + 11.233489990234375, + 6.903780460357666, + 2.9462454319000244, + 9.701461791992188, + -2.634294271469116 + ], + "xaxis": "x", + "y": [ + 9.81507682800293, + 9.916971206665039, + 7.769481182098389, + 7.403113842010498, + 7.979330539703369, + 8.073902130126953, + 8.694353103637695, + 7.574337482452393, + 9.50735855102539, + 9.38923168182373, + 10.940258026123047, + 8.17578411102295, + 10.377908706665039, + 7.956019878387451, + 7.511343955993652, + 9.587531089782715, + 10.204992294311523, + 9.024825096130371, + 8.610971450805664, + 8.763811111450195, + 10.375728607177734, + 10.2976655960083, + 8.055310249328613, + 8.047897338867188, + 9.942087173461914, + 10.263806343078613, + 8.48245906829834, + 10.113971710205078, + 7.828304290771484, + 10.216398239135742, + 9.249751091003418, + 11.78860092163086, + 9.16310977935791, + 6.163523197174072, + 9.789214134216309, + 11.069075584411621, + 8.012373924255371, + 11.216279983520508, + 8.99679946899414, + 9.368553161621094, + 9.406460762023926, + 10.845699310302734, + 9.456710815429688, + 7.714722633361816, + 7.865786552429199, + 8.907540321350098, + 9.552569389343262, + 9.521617889404297, + 7.75559139251709, + 10.441733360290527, + 8.429061889648438, + 10.12252140045166, + 9.314643859863281, + 8.633573532104492, + 10.829501152038574, + 9.371220588684082, + 9.279938697814941, + 8.476906776428223, + 9.33568286895752, + 9.337800025939941, + 10.44753360748291, + 10.901453971862793, + 9.791974067687988, + 9.830583572387695, + 9.857592582702637, + 9.046849250793457, + 9.854467391967773, + 7.663816928863525, + 7.989266395568848, + 7.98513126373291, + 7.636916160583496, + 9.900033950805664 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=microtubules
x=%{x}
y=%{y}", + "hovertext": [ + "795_C9_7_5", + "795_C9_8_8", + "799_C9_1_5", + "394_A7_1_2", + "395_A7_2_6", + "399_A7_1_8", + "399_A7_2_6", + "411_G5_1_5", + "411_G5_1_7", + "415_G5_1_5", + "415_G5_2_7", + "416_G5_1_6", + "416_G5_2_4", + "416_G5_2_5", + "497_B1_1_12", + "497_B1_1_23", + "507_B1_1_14", + "918_B3_2_6", + "966_G6_1_6", + "197_B5_1_6", + "197_B5_1_11", + "197_B5_1_12", + "197_B5_2_2", + "151_F10_1_1", + "151_F10_1_5", + "151_F10_2_8", + "366_A7_1_5", + "366_A7_2_1", + "373_A7_2_10", + "652_D10_1_4", + "187_G7_1_3", + "187_G7_2_6", + "188_G7_3_5", + "563_C12_1_4", + "569_C12_2_13", + "569_C12_2_15", + "413_B2_1_7", + "413_B2_1_11", + "417_B2_1_9", + "481_B4_1_6", + "481_B4_1_10", + "487_B4_3_6", + "487_B4_3_7", + "491_B4_2_10", + "189_C11_2_6", + "191_C11_1_11", + "191_C11_2_6", + "563_B12_1_4", + "563_B12_2_7", + "569_B12_1_9", + "661_G2_1_3", + "661_G2_1_4", + "662_G2_1_8", + "662_G2_2_1", + "662_G2_2_9", + "662_G2_2_13", + "670_G2_1_7", + "670_G2_2_6", + "822_B12_1_6", + "822_B12_2_6", + "831_G9_1_8", + "831_G9_1_11", + "621_C12_3_6", + "673_E1_1_4", + "673_E1_2_6", + "673_E1_2_8", + "218_D9_2_9", + "218_D9_2_15", + "219_D9_2_6", + "220_D9_1_4", + "418_A11_2_8", + "418_A11_2_19", + "424_A11_1_7", + "424_A11_2_8", + "429_A11_2_6", + "263_E5_1_4", + "263_E5_2_8", + "263_E5_2_9", + "263_E5_2_10", + "263_E5_2_11", + "263_E5_2_15", + "263_E5_2_16", + "264_E5_2_7", + "277_E5_2_6" + ], + "legendgroup": "microtubules", + "marker": { + "color": "#E377C2", + "symbol": "circle" + }, + "mode": "markers", + "name": "microtubules", + "showlegend": true, + "type": "scattergl", + "x": [ + 7.80588960647583, + 6.711369037628174, + 3.0636415481567383, + 8.039419174194336, + 10.275031089782715, + 1.7174198627471924, + -2.495785713195801, + 1.2521352767944336, + 4.156789302825928, + -0.7419219613075256, + -1.105910301208496, + 3.770939350128174, + -2.4856207370758057, + -0.1851118505001068, + 8.998716354370117, + 0.8127406239509583, + 7.721927165985107, + -0.3615245223045349, + 0.8290137648582458, + 1.560547113418579, + 10.060449600219727, + -0.8983007669448853, + 6.435068130493164, + 1.5114468336105347, + 11.305437088012695, + 5.688301086425781, + 5.018712520599365, + 2.4154443740844727, + 5.449938774108887, + 0.8927267789840698, + 1.626383900642395, + 5.597720623016357, + 10.51032829284668, + -2.1094210147857666, + 5.577792167663574, + 5.256476402282715, + -1.804337501525879, + 0.6922854781150818, + 9.164814949035645, + 11.05317211151123, + 6.645759105682373, + 8.212655067443848, + -1.4229531288146973, + -1.0687475204467773, + -1.5924274921417236, + 5.634369373321533, + 5.787140369415283, + -1.2577248811721802, + -1.5316545963287354, + 3.5333383083343506, + 7.958313941955566, + 9.822720527648926, + 5.768285751342773, + -0.8591622710227966, + 0.006367618218064308, + 5.70219612121582, + -2.6235251426696777, + 2.086392641067505, + 1.7709888219833374, + -0.038378093391656876, + 1.3616269826889038, + 6.923687934875488, + 2.138993501663208, + 3.0413897037506104, + 7.885809898376465, + 2.8308277130126953, + 4.6334710121154785, + -2.582110643386841, + 1.847437858581543, + 0.15101473033428192, + 3.7457406520843506, + 7.389935493469238, + 10.797651290893555, + 0.9668118953704834, + 1.8321282863616943, + 9.939204216003418, + 8.821638107299805, + -0.3046288788318634, + 3.349409818649292, + 12.05246639251709, + 11.354207992553711, + 7.225831508636475, + -0.5577960014343262, + 0.20251964032649994 + ], + "xaxis": "x", + "y": [ + 8.039447784423828, + 9.260540008544922, + 10.784685134887695, + 7.981029033660889, + 7.587905406951904, + 10.654318809509277, + 9.903830528259277, + 10.032915115356445, + 9.05367660522461, + 9.401101112365723, + 9.454922676086426, + 9.183558464050293, + 9.836512565612793, + 10.353898048400879, + 7.515796184539795, + 10.0004301071167, + 8.174359321594238, + 9.273420333862305, + 9.66981029510498, + 9.865777969360352, + 7.302517890930176, + 9.935626029968262, + 8.056742668151855, + 10.334508895874023, + 7.783709526062012, + 8.334988594055176, + 7.997478485107422, + 9.292506217956543, + 10.96531867980957, + 9.964659690856934, + 8.976839065551758, + 8.707195281982422, + 7.954761505126953, + 9.555830955505371, + 8.694043159484863, + 7.832960605621338, + 10.227999687194824, + 9.879170417785645, + 7.394522666931152, + 8.289298057556152, + 8.9433012008667, + 7.913244247436523, + 10.034280776977539, + 9.14828109741211, + 10.332475662231445, + 8.790081977844238, + 10.717884063720703, + 9.540560722351074, + 10.03397274017334, + 8.463295936584473, + 8.097739219665527, + 8.479972839355469, + 8.200474739074707, + 10.313556671142578, + 9.981703758239746, + 8.435460090637207, + 9.853832244873047, + 9.411123275756836, + 8.847495079040527, + 9.300243377685547, + 10.532662391662598, + 7.772002696990967, + 9.324620246887207, + 9.270539283752441, + 8.119784355163574, + 8.910429000854492, + 8.99204158782959, + 9.860812187194824, + 8.855253219604492, + 10.294961929321289, + 8.594392776489258, + 7.754316329956055, + 7.370981693267822, + 10.115278244018555, + 8.85879898071289, + 7.734009742736816, + 8.173809051513672, + 9.424324035644531, + 9.195332527160645, + 7.453793048858643, + 7.80363655090332, + 7.695418357849121, + 10.511618614196777, + 9.5834379196167 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=cytosol
x=%{x}
y=%{y}", + "hovertext": [ + "104_F7_1_4", + "35_G12_2_4", + "35_G12_2_6", + "36_G12_1_2", + "36_G12_1_4", + "36_G12_1_6", + "37_G12_2_1", + "821_B5_1_3", + "323_B9_1_2", + "323_B9_1_12", + "323_B9_1_16", + "323_B9_2_2", + "326_B9_1_8", + "404_A2_1_12", + "404_A2_2_9", + "404_A2_2_17", + "407_A2_2_5", + "410_A2_1_5", + "410_A2_2_8", + "410_A2_2_13", + "68_F1_1_5", + "68_F1_1_12", + "68_F1_2_2", + "68_F1_2_8", + "69_F1_2_5", + "69_F1_2_7", + "69_F1_2_9", + "505_H6_1_5", + "505_H6_2_14", + "508_H6_1_7", + "508_H6_2_3", + "508_H6_2_13", + "508_H6_2_20", + "754_E6_3_8", + "754_E6_3_11", + "754_E6_4_14", + "813_E6_1_3", + "534_B3_1_5", + "534_B3_2_4", + "552_B3_1_5", + "295_E11_2_4", + "295_E11_2_6", + "295_E11_2_7", + "297_E11_1_6", + "10_G10_2_6", + "10_G10_2_9", + "10_G10_2_11", + "10_G10_2_15", + "10_G10_2_16", + "11_G10_1_8", + "15_G10_1_7", + "79_F2_1_2", + "79_F2_1_7", + "80_F2_1_6", + "80_F2_1_7", + "81_F2_2_15", + "629_E12_1_5", + "629_E12_1_8", + "631_E12_1_14", + "631_E12_2_11", + "181_F10_1_4", + "181_F10_2_6", + "182_F10_1_8", + "573_H11_4_7", + "589_H11_3_3", + "589_H11_3_4", + "589_H11_3_10", + "709_H11_1_7", + "709_H11_1_8", + "709_H11_1_13", + "709_H11_2_12", + "59_C7_1_9", + "59_C7_1_14", + "60_C7_1_1", + "60_C7_1_4", + "60_C7_2_8", + "61_C7_1_10", + "504_H6_2_5", + "555_H6_1_5", + "555_H6_2_8", + "976_H6_2_3", + "976_H6_2_16", + "1200_E11_3_9", + "1200_E11_4_17", + "131_D9_1_5", + "131_D9_2_8", + "132_D9_1_9", + "132_D9_1_11", + "132_D9_2_4", + "164_D9_1_3", + "164_D9_1_9", + "164_D9_2_3", + "164_D9_2_7", + "1773_E11_7_8", + "6_A9_1_6", + "6_A9_1_7", + "6_A9_2_6", + "6_A9_2_7", + "754_B4_1_13", + "758_B4_2_5", + "758_B4_3_3", + "490_F5_1_5", + "490_F5_2_5", + "509_F5_2_23", + "913_G7_1_8", + "914_G7_4_3", + "914_G7_4_5", + "919_G7_2_8", + "319_G1_2_7", + "340_G1_2_6", + "340_G1_2_21", + "340_G1_2_23", + "281_C8_1_9", + "281_C8_2_7", + "281_C8_2_12", + "282_C8_2_2", + "282_C8_2_3", + "282_C8_2_5", + "282_C8_2_6", + "283_C8_1_11", + "283_C8_1_13", + "283_C8_2_12", + "508_C1_1_11", + "508_C1_1_12", + "548_C1_2_4", + "611_C11_1_17", + "611_C11_2_14", + "614_C11_2_20", + "618_C11_2_5", + "94_A12_1_6", + "94_A12_2_10", + "539_A7_2_3", + "539_A7_3_7", + "552_A7_1_6", + "652_C9_2_2", + "652_C9_2_9", + "656_C9_3_6", + "5_H11_1_4", + "6_H11_1_11", + "1009_E3_2_9", + "1029_E3_2_3", + "428_B6_3_1", + "428_B6_4_6", + "440_B6_1_10", + "440_B6_4_6", + "860_B7_1_5", + "860_B7_1_6", + "860_B7_2_7", + "995_E3_2_11", + "995_E3_2_24", + "239_G1_1_8", + "239_G1_1_17", + "239_G1_2_5", + "239_G1_2_13", + "239_G1_2_19", + "240_G1_1_10", + "240_G1_2_7", + "240_G1_2_9", + "241_G1_2_10" + ], + "legendgroup": "cytosol", + "marker": { + "color": "#7F7F7F", + "symbol": "circle" + }, + "mode": "markers", + "name": "cytosol", + "showlegend": true, + "type": "scattergl", + "x": [ + -1.8024123907089233, + 4.451461315155029, + 5.554102420806885, + 11.124267578125, + 2.312922239303589, + 9.652252197265625, + 8.778571128845215, + -1.3600499629974365, + -1.335456371307373, + 7.443792343139648, + 1.9072165489196777, + 10.143373489379883, + 4.074429988861084, + 8.308758735656738, + 7.55338191986084, + 7.164207935333252, + 2.3305938243865967, + -0.2691679298877716, + 8.665098190307617, + 6.522381782531738, + 4.726909637451172, + 2.7915217876434326, + 3.274899482727051, + 2.1014389991760254, + 4.981446743011475, + -2.4730026721954346, + 1.7339131832122803, + -1.66078519821167, + 7.188760280609131, + 1.2140663862228394, + -2.3221852779388428, + -0.1465684473514557, + -0.3740013539791107, + 7.736973762512207, + 2.4519684314727783, + 1.2494360208511353, + 9.15802001953125, + 1.690032720565796, + 3.997241973876953, + 5.305149078369141, + 6.423083305358887, + 4.941730976104736, + 7.329478740692139, + 3.461712598800659, + 1.5590189695358276, + 7.282429218292236, + 1.2699123620986938, + 2.5298080444335938, + 8.143815040588379, + -1.0032477378845215, + 7.395720481872559, + 9.118342399597168, + -0.5033261179924011, + -1.0939444303512573, + 0.03565198928117752, + 5.048367500305176, + 2.3554723262786865, + 3.184877395629883, + 5.083219051361084, + -0.01192645262926817, + -0.12788037955760956, + 7.478898048400879, + 7.11168909072876, + 9.43112850189209, + 11.134060859680176, + 1.9467740058898926, + 7.546279430389404, + -0.7457696795463562, + 10.568892478942871, + 1.8510558605194092, + 2.1384620666503906, + 2.517458438873291, + -1.1648414134979248, + 3.5587780475616455, + 0.26146408915519714, + 6.409632682800293, + 5.85659122467041, + 5.66884708404541, + 0.31512686610221863, + 8.53899097442627, + 3.980095148086548, + -0.07047638297080994, + 5.692490100860596, + 4.172157287597656, + 2.26041316986084, + 4.655416488647461, + -2.3866090774536133, + 3.265900135040283, + -1.5875344276428223, + -0.7942299246788025, + 2.0432045459747314, + 0.011703758500516415, + 3.5113565921783447, + 6.293914794921875, + 4.408410549163818, + -0.016115786507725716, + 9.830148696899414, + 4.0782318115234375, + 7.72549295425415, + 5.093253135681152, + -0.5087053775787354, + 10.188730239868164, + 1.7562775611877441, + -1.635428547859192, + -0.6363064646720886, + 3.0202219486236572, + -1.240703821182251, + -0.6934219002723694, + 6.327619552612305, + 9.955622673034668, + 10.300748825073242, + 4.115579128265381, + 0.18165627121925354, + 9.022111892700195, + 0.512808084487915, + -1.069522500038147, + 9.822710037231445, + 9.360747337341309, + -1.7606308460235596, + 9.766042709350586, + 5.599393844604492, + 3.471282720565796, + 4.556669235229492, + 7.49871826171875, + 1.1108161211013794, + -1.0210896730422974, + 9.338658332824707, + -0.7244977355003357, + 2.578871011734009, + 6.5443315505981445, + 9.93696403503418, + 5.633239269256592, + -1.6291919946670532, + -0.6725142002105713, + 3.088209629058838, + 1.9178423881530762, + 10.648760795593262, + 7.567183017730713, + 10.699491500854492, + 3.3385825157165527, + 8.862885475158691, + 2.711973190307617, + 11.85015869140625, + 10.408352851867676, + 3.616262197494507, + 3.915334463119507, + -0.980894923210144, + 4.928647994995117, + 1.9619731903076172, + 1.696612000465393, + -0.9016175866127014, + 7.414289474487305, + -1.8858392238616943, + 8.331531524658203, + -0.9031376242637634, + -2.42097806930542, + -0.9828623533248901, + -2.3421082496643066, + 0.672944188117981 + ], + "xaxis": "x", + "y": [ + 9.508915901184082, + 7.902645587921143, + 10.984152793884277, + 7.662893772125244, + 9.096430778503418, + 7.916247844696045, + 7.939950466156006, + 10.073538780212402, + 10.177257537841797, + 8.784046173095703, + 8.846030235290527, + 7.914827346801758, + 11.092183113098145, + 9.417011260986328, + 9.625894546508789, + 9.074568748474121, + 9.73112678527832, + 9.924982070922852, + 7.679642200469971, + 10.327933311462402, + 8.115706443786621, + 9.40397834777832, + 9.416913986206055, + 9.194377899169922, + 11.761091232299805, + 9.738021850585938, + 8.467219352722168, + 9.81246566772461, + 8.496408462524414, + 9.844138145446777, + 9.848121643066406, + 10.209026336669922, + 9.3840913772583, + 9.333038330078125, + 9.48116683959961, + 10.494721412658691, + 7.450821876525879, + 9.49586296081543, + 8.636772155761719, + 7.847829818725586, + 10.700910568237305, + 8.995564460754395, + 9.524800300598145, + 9.816413879394531, + 10.501399040222168, + 9.948592185974121, + 9.970736503601074, + 8.428391456604004, + 7.978897571563721, + 9.61203384399414, + 9.619368553161621, + 7.386185169219971, + 9.352662086486816, + 9.61865520477295, + 9.582316398620605, + 9.416678428649902, + 9.830875396728516, + 8.745105743408203, + 7.925664901733398, + 10.079793930053711, + 9.35986042022705, + 7.807250022888184, + 8.791913032531738, + 7.810709476470947, + 7.69212007522583, + 8.758685111999512, + 8.3303804397583, + 9.68980884552002, + 7.593451499938965, + 8.886625289916992, + 8.855127334594727, + 10.856640815734863, + 10.006922721862793, + 10.9592866897583, + 10.080716133117676, + 8.816914558410645, + 10.742476463317871, + 8.547178268432617, + 9.606854438781738, + 7.809178352355957, + 10.122838973999023, + 9.368858337402344, + 9.66519546508789, + 10.30241870880127, + 9.3541259765625, + 8.136218070983887, + 9.861291885375977, + 9.433431625366211, + 9.621241569519043, + 9.373429298400879, + 8.982512474060059, + 10.287497520446777, + 8.531920433044434, + 8.652302742004395, + 8.87519645690918, + 10.56347370147705, + 8.32929801940918, + 9.905038833618164, + 10.039565086364746, + 8.002765655517578, + 10.562239646911621, + 7.581142425537109, + 10.74317455291748, + 9.51015853881836, + 10.489933013916016, + 9.32044506072998, + 10.011491775512695, + 9.157191276550293, + 8.183904647827148, + 7.242562294006348, + 7.733373641967773, + 8.5224027633667, + 10.235504150390625, + 7.446998119354248, + 10.679693222045898, + 9.214256286621094, + 7.520055770874023, + 7.5556817054748535, + 10.068808555603027, + 7.999142646789551, + 11.597563743591309, + 10.469236373901367, + 10.057270050048828, + 8.30539321899414, + 9.91469955444336, + 10.271504402160645, + 7.880212783813477, + 10.508559226989746, + 9.366129875183105, + 8.63044548034668, + 7.803362846374512, + 8.87388801574707, + 10.04879093170166, + 9.552556991577148, + 8.902053833007812, + 8.737015724182129, + 7.309913158416748, + 8.248804092407227, + 7.430069446563721, + 9.314717292785645, + 7.793577671051025, + 9.476190567016602, + 8.18304443359375, + 8.63797664642334, + 9.045339584350586, + 9.562917709350586, + 9.277194023132324, + 10.182193756103516, + 10.856216430664062, + 9.054398536682129, + 10.06442928314209, + 8.481124877929688, + 9.903305053710938, + 7.830940246582031, + 9.743711471557617, + 9.894811630249023, + 9.841387748718262, + 9.921014785766602, + 9.168588638305664 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nuclear speckles
x=%{x}
y=%{y}", + "hovertext": [ + "605_F9_1_10", + "605_F9_2_10", + "606_F9_2_14", + "490_H3_1_2", + "490_H3_1_5", + "490_H3_2_4", + "509_H3_3_6", + "509_H3_3_9", + "776_C1_2_3", + "776_C1_2_8", + "776_C1_2_13", + "789_C1_8_14", + "899_C1_1_12", + "899_C1_3_9", + "1245_G5_2_3", + "1245_G5_2_6", + "620_B1_4_4", + "623_B1_1_9", + "623_B1_1_15", + "440_C6_2_4", + "440_C6_2_5", + "440_C6_3_12", + "975_B10_1_3", + "976_B10_1_6", + "976_B10_2_11", + "980_B10_1_7", + "980_B10_1_13", + "592_B5_1_1", + "592_B5_1_4", + "592_B5_2_5", + "592_B5_2_10", + "791_G11_2_4", + "791_G11_2_8", + "794_G11_2_8", + "798_G11_1_11", + "798_G11_2_9", + "798_G11_2_12", + "1015_E12_1_10", + "7_C7_2_2", + "8_C7_1_5", + "8_C7_1_9", + "791_F11_10_13", + "791_F11_4_8", + "798_F11_1_8", + "798_F11_1_12", + "798_F11_1_13", + "23_E7_2_5", + "24_E7_1_3", + "24_E7_2_2", + "25_E7_1_5", + "231_G5_1_7", + "231_G5_1_9", + "231_G5_1_11", + "523_E4_1_3", + "523_E4_1_4", + "523_E4_1_10", + "523_E4_2_6", + "371_A2_1_11", + "372_A2_1_5", + "372_A2_2_1", + "374_A2_2_3", + "975_D6_1_5", + "976_D6_3_5", + "976_D6_4_6", + "980_D6_1_9", + "980_D6_2_1", + "980_D6_2_8", + "522_C1_1_3", + "522_C1_2_7", + "529_C1_1_17", + "529_C1_2_5", + "563_E1_1_3", + "563_E1_1_6", + "563_E1_2_2", + "566_E1_2_9", + "569_E1_3_13", + "565_F1_1_12", + "570_F1_1_3", + "570_F1_1_8", + "584_F1_1_6", + "584_F1_1_11", + "7_F4_1_5", + "7_F4_2_5", + "7_F4_2_11", + "8_F4_2_4", + "9_F4_2_5", + "9_F4_2_9", + "1158_G12_2_8", + "924_F4_2_9", + "924_F4_2_25", + "932_F4_2_9", + "94_E2_1_4", + "94_E2_2_5", + "95_E2_1_9", + "96_E2_1_8", + "971_F4_2_7", + "526_E7_1_3", + "526_E7_1_5", + "526_E7_2_15", + "528_E7_1_7", + "528_E7_1_8", + "528_E7_1_11", + "528_E7_2_4", + "528_E7_2_8", + "545_E7_1_3", + "545_E7_2_9", + "545_E7_2_19", + "564_A9_2_2", + "572_A9_1_3", + "572_A9_1_7" + ], + "legendgroup": "nuclear speckles", + "marker": { + "color": "#BCBD22", + "symbol": "circle" + }, + "mode": "markers", + "name": "nuclear speckles", + "showlegend": true, + "type": "scattergl", + "x": [ + 12.07348918914795, + 12.258650779724121, + 5.731505393981934, + 11.482514381408691, + -0.4047311544418335, + 8.722942352294922, + 11.792634963989258, + 12.00670051574707, + 11.891463279724121, + 7.324677467346191, + -0.26100438833236694, + 9.842408180236816, + 9.540069580078125, + 7.439480781555176, + 11.802841186523438, + 10.884795188903809, + 9.331859588623047, + -0.7661015391349792, + 9.321563720703125, + 10.946555137634277, + 9.75358772277832, + 7.663273811340332, + 10.749035835266113, + 11.914824485778809, + 9.485074043273926, + 7.325840950012207, + 7.576280117034912, + 1.9041752815246582, + 7.144676685333252, + 11.047569274902344, + 12.152562141418457, + 5.284160137176514, + 10.854405403137207, + 10.60406494140625, + 11.122819900512695, + 11.83301067352295, + 10.956655502319336, + 2.8143234252929688, + 12.126448631286621, + 6.283559322357178, + 5.098883152008057, + 5.083515644073486, + 8.83227252960205, + 10.928166389465332, + 12.049291610717773, + 2.70245099067688, + 4.199507236480713, + 3.893756866455078, + 11.941826820373535, + 0.24276675283908844, + 11.917436599731445, + 9.356618881225586, + 11.67520523071289, + 1.6802159547805786, + 8.932186126708984, + 1.6010422706604004, + 9.514856338500977, + 11.564970016479492, + 9.404121398925781, + 9.225342750549316, + 8.868645668029785, + -0.498170405626297, + 3.154674768447876, + 1.7926476001739502, + 10.85551929473877, + 6.625469207763672, + 7.4571332931518555, + 8.604262351989746, + 3.74592924118042, + 12.127912521362305, + 4.903397560119629, + 1.3218611478805542, + 11.832420349121094, + 10.96159839630127, + 8.82496452331543, + 10.649346351623535, + 12.113605499267578, + 11.590597152709961, + 12.00750732421875, + 12.004422187805176, + 10.856193542480469, + 5.20032262802124, + 10.226353645324707, + 8.34816837310791, + 6.356680393218994, + 6.742821216583252, + 6.329071044921875, + 9.521099090576172, + 9.4801025390625, + 6.0064616203308105, + 10.664175987243652, + 9.47519302368164, + 10.751021385192871, + 10.567959785461426, + 10.875974655151367, + 7.4667067527771, + 9.643583297729492, + 9.277566909790039, + -1.1593974828720093, + 11.534015655517578, + 10.973685264587402, + 10.952529907226562, + 10.227410316467285, + 10.63571834564209, + 10.072620391845703, + 10.689868927001953, + 5.539884567260742, + 4.346114635467529, + 6.169941425323486, + 5.638885021209717 + ], + "xaxis": "x", + "y": [ + 9.769668579101562, + 9.302553176879883, + 11.489947319030762, + 9.579075813293457, + 10.426056861877441, + 11.285606384277344, + 8.690122604370117, + 9.511303901672363, + 9.577609062194824, + 10.701563835144043, + 9.75223445892334, + 11.241864204406738, + 10.975647926330566, + 11.429719924926758, + 9.515569686889648, + 10.873017311096191, + 10.307027816772461, + 9.24410629272461, + 10.37912368774414, + 11.170185089111328, + 11.38102912902832, + 11.376238822937012, + 11.208006858825684, + 9.822360038757324, + 11.449458122253418, + 11.220849990844727, + 10.937773704528809, + 9.854731559753418, + 11.295073509216309, + 8.746092796325684, + 9.31459903717041, + 11.70824909210205, + 11.041420936584473, + 11.055721282958984, + 11.002989768981934, + 10.194511413574219, + 11.071070671081543, + 10.801637649536133, + 9.535472869873047, + 10.848339080810547, + 11.685586929321289, + 11.695258140563965, + 11.247696876525879, + 10.99527645111084, + 9.752099990844727, + 11.140605926513672, + 11.385055541992188, + 11.434615135192871, + 9.440120697021484, + 10.191890716552734, + 9.66280746459961, + 8.113954544067383, + 9.159473419189453, + 10.145263671875, + 10.391977310180664, + 10.403448104858398, + 8.612196922302246, + 9.016134262084961, + 11.216992378234863, + 10.394493103027344, + 11.307125091552734, + 10.34500789642334, + 10.802389144897461, + 10.751440048217773, + 8.679640769958496, + 11.542872428894043, + 10.87038803100586, + 10.691458702087402, + 10.96208667755127, + 9.47337818145752, + 10.784551620483398, + 10.255051612854004, + 9.84312629699707, + 10.83326530456543, + 11.327277183532715, + 11.2515869140625, + 9.943229675292969, + 10.346728324890137, + 10.032685279846191, + 10.061532020568848, + 11.170510292053223, + 10.332603454589844, + 9.043111801147461, + 11.377455711364746, + 10.69517993927002, + 10.952410697937012, + 10.835789680480957, + 11.396109580993652, + 10.942700386047363, + 9.125511169433594, + 11.207005500793457, + 10.477555274963379, + 11.116769790649414, + 11.082820892333984, + 11.20341968536377, + 11.516622543334961, + 11.361526489257812, + 11.3812255859375, + 10.499716758728027, + 9.898343086242676, + 11.00788688659668, + 11.18524169921875, + 11.371463775634766, + 11.24754524230957, + 11.343498229980469, + 11.220648765563965, + 11.60568904876709, + 11.475564002990723, + 11.500349044799805, + 11.641995429992676 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nucleoplasm
x=%{x}
y=%{y}", + "hovertext": [ + "608_F9_2_6", + "975_H1_1_6", + "975_H1_1_8", + "975_H1_2_6", + "976_H1_1_12", + "976_H1_1_18", + "976_H1_1_22", + "976_H1_4_8", + "980_H1_1_18", + "980_H1_2_10", + "486_H3_1_8", + "486_H3_2_6", + "542_A5_1_4", + "542_A5_2_7", + "544_A5_2_23", + "571_A5_1_4", + "221_H4_1_5", + "221_H4_1_7", + "221_H4_2_10", + "222_H4_1_5", + "222_H4_2_6", + "222_H4_2_12", + "222_H4_2_14", + "248_H4_1_3", + "248_H4_1_5", + "248_H4_1_6", + "248_H4_1_10", + "554_C2_2_13", + "554_C2_2_16", + "560_C2_4_5", + "2_B4_2_4", + "402_E5_1_3", + "402_E5_2_4", + "402_E5_2_6", + "405_E5_3_12", + "405_E5_3_18", + "409_E5_1_10", + "474_C8_1_5", + "474_C8_2_7", + "510_C8_1_13", + "728_H2_1_3", + "728_H2_1_13", + "319_C6_2_3", + "319_C6_2_7", + "319_C6_2_9", + "319_C6_2_12", + "894_B7_1_1", + "907_B7_1_8", + "907_B7_1_12", + "944_D6_1_11", + "952_D6_2_6", + "177_D8_1_12", + "827_A4_1_7", + "827_A4_1_8", + "827_A4_2_8", + "827_A4_2_14", + "827_A4_2_17", + "829_A4_2_7", + "411_D8_1_3", + "411_D8_2_4", + "415_D8_1_10", + "415_D8_2_9", + "416_D8_2_3", + "416_D8_2_6", + "416_D8_2_7", + "416_D8_2_9", + "416_D8_2_12", + "955_E9_1_11", + "972_E9_1_8", + "972_E9_2_4", + "972_E9_2_10", + "301_D8_2_9", + "342_D8_1_5", + "946_A6_3_2", + "564_H9_2_11", + "572_H9_1_8", + "572_H9_2_8", + "587_H9_2_7", + "587_H9_2_10", + "831_C7_2_7", + "944_G9_1_11", + "626_C5_2_2", + "632_C5_3_6", + "632_C5_3_15", + "633_C5_1_11", + "633_C5_2_5", + "633_C5_2_9", + "920_G8_1_12", + "920_G8_1_14", + "920_G8_2_14", + "920_G8_2_15", + "941_G9_1_2", + "941_G9_1_10", + "620_G12_1_11", + "620_G12_2_4", + "623_G12_2_3", + "623_G12_2_7", + "627_G12_1_9", + "627_G12_1_11", + "627_G12_2_12", + "230_G5_2_20", + "520_E4_1_8", + "255_H11_1_2", + "255_H11_2_5", + "256_H11_2_6", + "256_H11_2_19", + "154_F9_1_4", + "263_H6_1_4", + "263_H6_2_4", + "263_H6_2_6", + "264_H6_2_10", + "264_H6_2_13", + "277_H6_2_4", + "277_H6_2_5", + "911_C9_1_8", + "911_C9_2_3", + "912_C9_1_4", + "912_C9_2_4", + "912_C9_2_9", + "918_D7_3_11", + "918_D7_3_17", + "951_A2_1_6", + "951_A2_1_11", + "951_A2_1_13", + "975_D4_1_5", + "975_D4_2_8", + "976_D4_1_4", + "976_D4_1_11", + "976_D4_1_13", + "976_D4_2_3", + "976_D4_2_4", + "976_D4_2_9", + "976_D4_2_10", + "980_D4_1_12", + "980_D4_2_21", + "809_H5_1_5", + "809_H5_1_8", + "809_H5_2_11", + "819_H5_2_3", + "258_D6_1_5", + "258_D6_1_10", + "259_D6_1_9", + "259_D6_2_4", + "259_D6_2_7", + "259_D6_2_11", + "260_D6_1_11", + "260_D6_2_10", + "507_B9_2_2", + "511_B9_1_9", + "511_B9_1_11", + "511_B9_1_12", + "236_C4_1_6", + "237_C4_1_3", + "237_C4_1_4", + "237_C4_2_8", + "268_C4_2_9", + "62_A1_2_5", + "62_A1_2_11", + "62_A1_2_13", + "62_A1_2_17", + "93_A1_2_4" + ], + "legendgroup": "nucleoplasm", + "marker": { + "color": "#17BECF", + "symbol": "circle" + }, + "mode": "markers", + "name": "nucleoplasm", + "showlegend": true, + "type": "scattergl", + "x": [ + 10.984966278076172, + 8.073525428771973, + 10.601151466369629, + 7.441102027893066, + 5.127652645111084, + 1.7473933696746826, + -0.3599754571914673, + 2.770860433578491, + 10.6112060546875, + 7.019987106323242, + 4.1411356925964355, + 10.363632202148438, + 7.926427841186523, + 10.362897872924805, + 1.7487398386001587, + 9.156259536743164, + 2.8661158084869385, + 10.490422248840332, + 9.221872329711914, + 7.106359481811523, + 6.777750015258789, + 10.659823417663574, + 8.821587562561035, + 8.808148384094238, + 10.443461418151855, + 10.686222076416016, + 9.075057983398438, + 4.259021759033203, + 3.6375386714935303, + 10.493456840515137, + 6.2044758796691895, + 8.656903266906738, + 4.7217116355896, + 7.67918062210083, + 7.271231651306152, + 9.142016410827637, + 3.1148581504821777, + 5.6097025871276855, + 5.91672420501709, + 6.149641990661621, + 4.915364742279053, + 0.4376066327095032, + 5.251420021057129, + 6.46743106842041, + 10.588942527770996, + 10.904836654663086, + 5.269296646118164, + 4.432221412658691, + 8.492021560668945, + 10.849034309387207, + 2.8038315773010254, + 4.524566173553467, + 11.405352592468262, + 9.099053382873535, + 11.424493789672852, + 12.02547550201416, + 12.230738639831543, + 9.371925354003906, + 2.8917253017425537, + 7.417837142944336, + 4.254062652587891, + 5.941372394561768, + 5.88485050201416, + 5.095426082611084, + 5.703062057495117, + 1.451463222503662, + 7.356603145599365, + 4.550978183746338, + 7.418269157409668, + 7.437565326690674, + 11.752007484436035, + 2.5107789039611816, + 4.509511470794678, + 9.183511734008789, + 11.952510833740234, + 7.260678768157959, + 9.278373718261719, + 8.166519165039062, + 3.053792953491211, + 6.220591068267822, + 3.86327862739563, + 6.192848205566406, + 11.386683464050293, + 12.031464576721191, + 9.562540054321289, + 7.172672271728516, + 11.945749282836914, + 7.068431854248047, + 1.260867953300476, + 6.767836093902588, + 6.531129837036133, + 7.12057638168335, + 5.975885391235352, + 10.798945426940918, + 9.558353424072266, + 9.203119277954102, + 9.257644653320312, + 8.00302791595459, + 7.102749824523926, + 12.256002426147461, + 8.157100677490234, + 9.13269329071045, + 8.34151840209961, + 6.952149391174316, + 10.846931457519531, + -1.1770893335342407, + 3.4477133750915527, + 6.76606559753418, + 11.070500373840332, + 10.420467376708984, + 9.36457633972168, + 10.93181324005127, + 11.962579727172852, + 7.544419765472412, + 9.255876541137695, + 12.120798110961914, + 9.071754455566406, + 10.264166831970215, + 9.964442253112793, + 4.145314693450928, + 10.944267272949219, + 7.7115702629089355, + 9.02327823638916, + 6.065130710601807, + 10.888262748718262, + 5.613483428955078, + 12.116403579711914, + 12.085001945495605, + 8.938956260681152, + 10.300929069519043, + 10.254083633422852, + 8.46902084350586, + 8.785200119018555, + 9.424691200256348, + 11.64520263671875, + 11.571700096130371, + 9.216139793395996, + 7.539426803588867, + 5.289574146270752, + 5.978673934936523, + 9.56047248840332, + 3.0805141925811768, + 9.509605407714844, + 10.905204772949219, + 8.877875328063965, + 3.9541573524475098, + 4.212759494781494, + 1.6434650421142578, + 6.014175891876221, + 11.81765079498291, + 5.7166290283203125, + 10.235824584960938, + 9.988836288452148, + 12.271153450012207, + 9.359338760375977, + 10.802840232849121, + 5.58129358291626, + 5.053167343139648, + 11.393550872802734, + 10.70861530303955, + 5.989745616912842 + ], + "xaxis": "x", + "y": [ + 10.656658172607422, + 11.18799877166748, + 10.856574058532715, + 11.44901180267334, + 11.629622459411621, + 10.141706466674805, + 10.326932907104492, + 11.163639068603516, + 8.577914237976074, + 11.202803611755371, + 11.431288719177246, + 8.897784233093262, + 10.777013778686523, + 8.757115364074707, + 10.037969589233398, + 10.378239631652832, + 11.159039497375488, + 8.996428489685059, + 10.407123565673828, + 11.209020614624023, + 10.958918571472168, + 8.058340072631836, + 11.037464141845703, + 11.1759614944458, + 11.124183654785156, + 10.990623474121094, + 11.050283432006836, + 11.445941925048828, + 11.252389907836914, + 8.858786582946777, + 11.310880661010742, + 10.045780181884766, + 10.47917652130127, + 10.691255569458008, + 10.799641609191895, + 10.416866302490234, + 10.8339204788208, + 10.151741981506348, + 11.161153793334961, + 10.8136568069458, + 11.20238208770752, + 10.2774019241333, + 9.544167518615723, + 10.656082153320312, + 8.673077583312988, + 8.642745018005371, + 10.137826919555664, + 9.065380096435547, + 11.219986915588379, + 8.91156005859375, + 10.705818176269531, + 9.090436935424805, + 9.463476181030273, + 11.382898330688477, + 10.480552673339844, + 10.009493827819824, + 9.64395523071289, + 10.823891639709473, + 9.20860481262207, + 9.860846519470215, + 11.427290916442871, + 9.020516395568848, + 11.223712921142578, + 10.761098861694336, + 11.18014907836914, + 9.849919319152832, + 10.834057807922363, + 11.583465576171875, + 11.404841423034668, + 11.484086990356445, + 9.392868041992188, + 10.26281452178955, + 8.97150707244873, + 10.296004295349121, + 9.24005126953125, + 11.328397750854492, + 10.45443058013916, + 10.914872169494629, + 11.203351020812988, + 11.531599998474121, + 10.612454414367676, + 10.716697692871094, + 10.481507301330566, + 10.04078483581543, + 11.038707733154297, + 11.00247573852539, + 10.200936317443848, + 11.360563278198242, + 10.730998039245605, + 11.422957420349121, + 11.127883911132812, + 11.43701457977295, + 11.410134315490723, + 11.13198184967041, + 11.285154342651367, + 11.345113754272461, + 11.341779708862305, + 11.284598350524902, + 11.175896644592285, + 9.614200592041016, + 10.498095512390137, + 10.6130952835083, + 10.969415664672852, + 11.455740928649902, + 11.138276100158691, + 10.079330444335938, + 10.939187049865723, + 11.090421676635742, + 11.015386581420898, + 11.12269115447998, + 10.387526512145996, + 11.13922119140625, + 10.118760108947754, + 10.968926429748535, + 8.20037841796875, + 9.487682342529297, + 11.161566734313965, + 11.150999069213867, + 11.20274829864502, + 11.388879776000977, + 8.734782218933105, + 11.375699043273926, + 11.217055320739746, + 10.880629539489746, + 10.830223083496094, + 11.652658462524414, + 9.245227813720703, + 9.567502975463867, + 10.921148300170898, + 11.088330268859863, + 11.289122581481934, + 10.845087051391602, + 11.407930374145508, + 8.506103515625, + 9.140424728393555, + 9.921130180358887, + 11.287715911865234, + 11.482010841369629, + 11.404109001159668, + 10.588408470153809, + 8.88369369506836, + 11.213618278503418, + 8.445860862731934, + 10.524004936218262, + 8.114008903503418, + 11.191266059875488, + 11.074394226074219, + 10.183130264282227, + 10.753056526184082, + 8.92208480834961, + 10.322144508361816, + 11.35918140411377, + 11.272193908691406, + 9.536117553710938, + 11.37548542022705, + 11.089654922485352, + 10.5033540725708, + 11.29785442352295, + 8.805676460266113, + 8.884170532226562, + 11.368857383728027 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=endoplasmic reticulum
x=%{x}
y=%{y}", + "hovertext": [ + "526_G11_1_6", + "526_G11_1_7", + "528_G11_1_13", + "545_G11_1_6", + "545_G11_1_11", + "545_G11_2_20", + "104_G6_1_9", + "104_G6_2_2", + "104_G6_2_3", + "104_G6_2_5", + "69_A5_2_13", + "91_A5_2_5", + "91_A5_2_7", + "1123_D6_2_4", + "172_G6_2_2", + "634_G1_3_19", + "639_G1_1_4", + "639_G1_3_7", + "1899_F12_31_6", + "1899_F12_32_5", + "1899_F12_32_14", + "1899_F12_32_15", + "2108_H7_2_11", + "2108_H7_2_13", + "703_F3_1_3", + "703_F3_1_9", + "703_F3_2_12", + "708_F3_1_12", + "708_F3_2_5", + "75_E10_1_7", + "758_E6_1_7", + "758_E6_2_4", + "133_H10_1_7", + "133_H10_2_4", + "125_F5_1_3", + "125_F5_1_5", + "165_F5_2_7", + "1894_H7_1_6", + "1894_H7_1_11", + "1894_H7_3_7", + "258_F9_1_4", + "258_F9_1_14", + "258_F9_2_14", + "259_F9_1_3", + "259_F9_2_9", + "260_F9_2_12", + "703_G8_1_9", + "703_G8_1_14", + "708_G8_1_11", + "121_G3_2_5", + "123_G3_1_7", + "123_G3_2_5", + "172_G3_1_1", + "172_G3_2_4", + "172_G3_2_9", + "641_A8_2_5", + "642_A8_3_12", + "105_H9_2_12", + "107_H9_2_12", + "160_H9_1_6", + "160_H9_2_6", + "160_H9_2_7", + "431_G4_4_12", + "437_G4_2_10", + "437_G4_2_13", + "102_C8_1_7", + "103_C8_1_7", + "103_C8_1_10", + "103_C8_2_7", + "48_C4_1_10", + "48_C4_2_7", + "48_C4_2_9", + "49_C4_2_4", + "47_H7_1_6", + "47_H7_2_4", + "48_H7_2_3", + "527_C9_1_2", + "527_C9_1_9", + "527_C9_3_3", + "529_C9_1_14", + "529_C9_2_4", + "529_C9_2_12", + "97_B12_1_7", + "99_B12_1_6", + "99_B12_2_5", + "99_B12_2_10", + "39_H9_1_5", + "40_H9_1_3", + "40_H9_1_7", + "40_H9_2_3" + ], + "legendgroup": "endoplasmic reticulum", + "marker": { + "color": "#1F77B4", + "symbol": "circle" + }, + "mode": "markers", + "name": "endoplasmic reticulum", + "showlegend": true, + "type": "scattergl", + "x": [ + 3.2447547912597656, + 2.9184703826904297, + 6.675962924957275, + 4.58085298538208, + -1.0868974924087524, + 6.6522064208984375, + 10.166969299316406, + 9.1868896484375, + 7.177773952484131, + 0.07045882940292358, + 0.5087297558784485, + -1.7863540649414062, + 2.561910629272461, + 3.6921324729919434, + 1.165887713432312, + 9.452832221984863, + 2.588078022003174, + 6.975824356079102, + 7.195924282073975, + 7.359630584716797, + -0.9562220573425293, + 10.40177059173584, + 7.1021881103515625, + 6.9552507400512695, + 2.0751757621765137, + 6.109659671783447, + 2.624572277069092, + 9.788066864013672, + 4.165965557098389, + 6.495021820068359, + 5.8981032371521, + 7.313299179077148, + 3.8879599571228027, + 4.181728363037109, + 5.148833751678467, + 8.433150291442871, + 6.64546012878418, + 2.234600305557251, + 7.133554935455322, + 4.738037109375, + 6.800375938415527, + 1.7412147521972656, + -0.16198743879795074, + 7.305188179016113, + 0.9637420773506165, + 6.039333343505859, + 2.10451078414917, + 5.706045627593994, + 7.4676666259765625, + 5.7704620361328125, + 5.598147392272949, + 12.167831420898438, + 6.628224849700928, + 12.493654251098633, + 7.512380123138428, + 3.445263385772705, + 9.66183090209961, + 10.436975479125977, + -2.3260107040405273, + 7.205696105957031, + 5.727794170379639, + 4.644102573394775, + 6.590414524078369, + -1.020456075668335, + 3.339718818664551, + 4.483584880828857, + -0.8045490980148315, + 7.671443462371826, + 6.833191394805908, + -0.8016027808189392, + 6.9017791748046875, + 1.6875706911087036, + 10.606059074401855, + 11.119812965393066, + 11.000222206115723, + 7.025929927825928, + 9.090808868408203, + 10.315220832824707, + 0.1049470603466034, + 2.0042178630828857, + 4.636541843414307, + 6.974136829376221, + 5.765756607055664, + 6.513875961303711, + 5.82921838760376, + 5.900471210479736, + 3.7823245525360107, + 5.430930137634277, + -1.0363892316818237, + 4.3196845054626465 + ], + "xaxis": "x", + "y": [ + 8.442706108093262, + 9.027788162231445, + 9.651198387145996, + 8.531401634216309, + 10.120037078857422, + 8.819242477416992, + 7.826521873474121, + 8.08011245727539, + 10.299992561340332, + 10.53884506225586, + 10.197161674499512, + 9.77857780456543, + 8.988310813903809, + 10.414640426635742, + 10.470003128051758, + 7.272236347198486, + 9.087246894836426, + 7.9927239418029785, + 8.651765823364258, + 8.609716415405273, + 9.450906753540039, + 7.840949058532715, + 7.715696334838867, + 9.010802268981934, + 9.602347373962402, + 9.627220153808594, + 9.391681671142578, + 7.2248215675354, + 10.258728981018066, + 9.362950325012207, + 10.932474136352539, + 8.300395011901855, + 11.1378173828125, + 10.225829124450684, + 9.795682907104492, + 9.14382266998291, + 10.311723709106445, + 9.63627815246582, + 8.74305534362793, + 8.529475212097168, + 8.071136474609375, + 10.601019859313965, + 9.991802215576172, + 8.099489212036133, + 10.052708625793457, + 9.553077697753906, + 8.6648530960083, + 10.508125305175781, + 8.963942527770996, + 8.433778762817383, + 9.670740127563477, + 7.404077529907227, + 9.773726463317871, + 7.455489635467529, + 7.711250305175781, + 8.129379272460938, + 7.819215297698975, + 7.184724807739258, + 9.752079963684082, + 10.223308563232422, + 9.038124084472656, + 8.581615447998047, + 9.555394172668457, + 9.305290222167969, + 7.915450096130371, + 7.984615325927734, + 9.927027702331543, + 8.158071517944336, + 9.223053932189941, + 10.197752952575684, + 9.995200157165527, + 8.632176399230957, + 7.646537780761719, + 8.76187515258789, + 9.245253562927246, + 7.906305313110352, + 7.745706558227539, + 7.1676530838012695, + 10.413887023925781, + 8.491966247558594, + 8.586467742919922, + 8.698546409606934, + 9.416695594787598, + 10.146071434020996, + 8.716909408569336, + 8.948263168334961, + 11.29104232788086, + 9.67658805847168, + 10.364914894104004, + 9.538354873657227 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nucleoli
x=%{x}
y=%{y}", + "hovertext": [ + "505_C9_1_6", + "505_C9_1_7", + "505_C9_1_8", + "505_C9_1_11", + "505_C9_1_12", + "505_C9_1_17", + "611_F2_3_4", + "611_F2_3_13", + "614_F2_2_8", + "618_F2_1_3", + "618_F2_2_6", + "618_F2_2_10", + "618_F2_2_11", + "618_F2_2_12", + "149_G3_1_8", + "193_A10_1_6", + "193_A10_1_12", + "194_A10_1_7", + "194_A10_1_11", + "625_D11_2_4", + "625_D11_2_10", + "631_D11_1_12", + "631_D11_2_6", + "631_D11_2_7", + "631_D11_2_10", + "133_C9_1_5", + "133_C9_2_5", + "135_C9_1_2", + "1130_H2_1_7", + "1130_H2_2_9", + "408_C5_2_5", + "418_A1_3_7", + "418_A1_3_10", + "429_A1_1_4", + "429_A1_1_10", + "429_A1_2_9", + "125_D10_1_7", + "139_F7_1_4", + "139_F7_1_6", + "139_F7_2_4", + "166_F7_1_4", + "166_F7_2_3", + "604_G1_1_7", + "404_A11_1_5", + "404_A11_1_6", + "404_A11_2_14", + "407_A11_1_13", + "407_A11_2_1", + "410_A11_1_12", + "566_H6_1_2", + "566_H6_1_3", + "566_H6_1_9", + "566_H6_2_4", + "566_H6_2_12", + "569_H6_2_4", + "760_E2_1_2", + "775_E2_2_5", + "1238_B6_1_4", + "1238_B6_1_5", + "1238_B6_4_9", + "1244_E3_11_10", + "1247_E3_1_12", + "1888_H3_4_9", + "471_F10_1_4", + "471_F10_1_10", + "471_F10_1_14", + "471_F10_1_15", + "471_F10_3_2", + "471_F10_3_6", + "563_G6_2_7", + "566_G6_1_2", + "566_G6_1_4", + "566_G6_2_3", + "569_G6_2_4", + "569_G6_2_8", + "562_F7_2_5", + "568_F7_3_6", + "826_H5_2_6", + "826_H5_2_12", + "826_H5_2_25", + "102_G10_1_2", + "102_G10_2_5", + "103_G10_1_3", + "103_G10_2_5", + "104_G10_1_3", + "104_G10_2_4", + "164_F5_2_5", + "164_F5_2_8", + "271_E9_1_7", + "271_E9_1_8", + "271_E9_2_5", + "271_E9_2_6", + "271_E9_2_8", + "565_C8_2_13", + "584_C8_2_2", + "25_A4_2_3", + "8_H11_2_4", + "8_H11_2_6", + "9_H11_1_1" + ], + "legendgroup": "nucleoli", + "marker": { + "color": "#FF7F0E", + "symbol": "circle" + }, + "mode": "markers", + "name": "nucleoli", + "showlegend": true, + "type": "scattergl", + "x": [ + 5.307034015655518, + 2.2012832164764404, + 3.695546865463257, + 3.6101341247558594, + 1.461788296699524, + 9.236124038696289, + 11.36357593536377, + 9.595953941345215, + 10.810388565063477, + 6.967933654785156, + 1.1884777545928955, + 1.0686877965927124, + 10.806251525878906, + 11.62773323059082, + 5.347238063812256, + 10.020282745361328, + 6.667942047119141, + 2.8861379623413086, + 9.220315933227539, + 5.212121486663818, + 5.4374680519104, + 6.713620185852051, + 4.980778217315674, + 3.549438238143921, + 6.610246658325195, + 8.573939323425293, + 9.877596855163574, + 11.522047996520996, + 1.5197639465332031, + 9.431501388549805, + 3.790909767150879, + 4.915558815002441, + -0.6567122340202332, + 7.826632499694824, + 1.6202281713485718, + 3.883378505706787, + 4.524829387664795, + 6.727118492126465, + 5.17396354675293, + 9.509746551513672, + 4.345511436462402, + 6.862484455108643, + 8.076075553894043, + 12.017398834228516, + 11.5883150100708, + 10.452630043029785, + 12.184310913085938, + 9.42901611328125, + -2.6048970222473145, + 5.444400787353516, + 5.552539825439453, + 3.801384210586548, + 9.63230037689209, + 3.3852038383483887, + 6.489726543426514, + 9.621232986450195, + 8.090157508850098, + 4.335958957672119, + 4.369500160217285, + 1.279406189918518, + 11.12339973449707, + 7.733190059661865, + 1.9515550136566162, + 1.2634094953536987, + 5.534433364868164, + 1.2223244905471802, + 0.5669420957565308, + 2.8198423385620117, + 10.758598327636719, + 9.512332916259766, + 11.854557991027832, + 1.525325894355774, + 6.600167751312256, + 11.09082317352295, + 8.608187675476074, + 8.9635591506958, + 5.694570064544678, + 2.072519540786743, + 6.553169250488281, + 2.709378719329834, + 9.626931190490723, + 3.6699540615081787, + 2.6494152545928955, + 5.812168121337891, + 2.505359649658203, + 10.378584861755371, + 5.8731865882873535, + 6.859099388122559, + 8.902178764343262, + 5.629794120788574, + -1.1933177709579468, + 6.076942443847656, + 4.888103008270264, + 8.76037883758545, + 10.901312828063965, + 10.137368202209473, + 9.22635269165039, + 9.259835243225098, + 11.280158042907715 + ], + "xaxis": "x", + "y": [ + 9.244851112365723, + 10.878959655761719, + 9.761214256286621, + 10.530945777893066, + 10.286314964294434, + 8.066060066223145, + 8.247003555297852, + 8.3203125, + 8.704936981201172, + 10.619508743286133, + 9.815614700317383, + 10.640349388122559, + 8.436751365661621, + 8.253320693969727, + 11.284725189208984, + 8.928315162658691, + 10.915999412536621, + 8.759121894836426, + 10.159628868103027, + 10.664199829101562, + 10.879578590393066, + 10.970808029174805, + 11.35784912109375, + 10.67605972290039, + 11.115056037902832, + 11.173011779785156, + 11.12653636932373, + 9.768047332763672, + 8.648331642150879, + 10.625454902648926, + 11.09659481048584, + 10.471290588378906, + 10.419303894042969, + 8.075080871582031, + 10.125783920288086, + 10.628018379211426, + 10.871357917785645, + 8.696772575378418, + 10.890265464782715, + 8.535173416137695, + 10.959013938903809, + 11.07613468170166, + 10.215214729309082, + 9.453015327453613, + 9.507352828979492, + 10.44007682800293, + 7.352478504180908, + 8.652122497558594, + 9.82018756866455, + 11.668636322021484, + 10.88727855682373, + 10.603824615478516, + 8.86241340637207, + 10.89153003692627, + 11.37387752532959, + 8.725813865661621, + 11.068503379821777, + 10.882287979125977, + 10.832447052001953, + 10.601212501525879, + 9.199073791503906, + 11.37430477142334, + 9.04110336303711, + 10.707232475280762, + 9.185870170593262, + 9.98963737487793, + 10.276545524597168, + 10.54478931427002, + 8.804612159729004, + 10.927525520324707, + 8.342634201049805, + 10.017916679382324, + 10.329926490783691, + 8.797359466552734, + 9.27536678314209, + 11.196864128112793, + 11.560546875, + 10.281991004943848, + 10.979039192199707, + 10.183389663696289, + 11.088523864746094, + 10.5687837600708, + 11.071355819702148, + 9.999702453613281, + 11.04114818572998, + 11.075127601623535, + 10.975343704223633, + 11.287925720214844, + 9.765018463134766, + 11.249611854553223, + 10.445730209350586, + 10.891724586486816, + 10.867637634277344, + 8.29462718963623, + 8.901382446289062, + 8.75167179107666, + 10.403427124023438, + 10.524951934814453, + 10.532386779785156 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nucleoli fibrillar center
x=%{x}
y=%{y}", + "hovertext": [ + "508_C9_1_20", + "604_G4_1_5", + "607_G4_1_2", + "609_G4_1_3", + "809_D1_7_6", + "826_D1_2_3", + "412_H6_1_4", + "412_H6_1_9", + "412_H6_2_4", + "419_H6_1_13", + "471_H6_2_6", + "471_H6_2_9", + "471_H6_2_11", + "471_H6_2_19", + "471_H6_2_20", + "651_H1_2_3", + "652_H1_1_3", + "652_H1_1_6", + "656_H1_1_6", + "484_E6_1_16", + "484_E6_2_3", + "484_E6_2_8", + "492_E6_2_4", + "450_F3_3_18", + "719_C10_1_4", + "719_C10_1_6", + "764_C10_1_9", + "1819_H5_18_cr59f6d6c051e08_3", + "1819_H5_18_cr59f6d6c051e08_5", + "1819_H5_4_cr59f6d6c0517f9_2", + "1819_H5_4_cr59f6d6c0517f9_11", + "402_D4_1_7", + "402_D4_1_11", + "402_D4_2_6", + "405_D4_2_1", + "405_D4_2_10", + "409_D4_1_1", + "409_D4_1_2", + "409_D4_2_6", + "575_F7_3_9", + "412_G12_2_7", + "419_G12_1_10", + "419_G12_2_20", + "471_G12_1_6", + "471_G12_1_9", + "471_G12_2_12", + "433_G10_2_5", + "569_A11_1_8", + "569_A11_1_9", + "569_A11_1_11", + "569_A11_2_7", + "569_A11_2_13", + "107_A12_1_1", + "107_A12_2_7", + "160_A12_1_4", + "667_C2_2_3", + "144_D11_1_8", + "144_D11_1_9", + "144_D11_2_1", + "144_D11_2_4", + "144_D11_2_5", + "23_A4_1_4", + "23_A4_1_6", + "24_A4_1_8" + ], + "legendgroup": "nucleoli fibrillar center", + "marker": { + "color": "#2CA02C", + "symbol": "circle" + }, + "mode": "markers", + "name": "nucleoli fibrillar center", + "showlegend": true, + "type": "scattergl", + "x": [ + 9.63404655456543, + 10.623029708862305, + 11.051946640014648, + 5.4707136154174805, + 11.429574012756348, + 8.150724411010742, + 11.569767951965332, + 4.19325590133667, + 8.06488037109375, + 4.503082752227783, + 4.740408420562744, + 11.424897193908691, + 9.302176475524902, + 9.996280670166016, + 5.498170852661133, + -0.436117559671402, + 3.343661069869995, + 5.027379512786865, + 5.042795658111572, + 6.086508274078369, + 1.870320439338684, + 6.934201240539551, + 1.764568567276001, + 0.3037610948085785, + 10.077630996704102, + 7.841760635375977, + 12.265966415405273, + 6.817480087280273, + 8.173808097839355, + -2.085279703140259, + 8.39441967010498, + 5.0827317237854, + 6.781822681427002, + 7.6320929527282715, + 7.145431995391846, + 10.284096717834473, + 9.923644065856934, + 9.991412162780762, + 4.430325508117676, + 3.53239107131958, + 5.792045593261719, + 6.870211124420166, + 10.796614646911621, + 4.3686747550964355, + 11.795412063598633, + 6.2166852951049805, + 9.399028778076172, + 7.375187397003174, + 8.840457916259766, + 9.058771133422852, + 10.830655097961426, + 9.974544525146484, + 8.529434204101562, + 2.6785807609558105, + 8.5991849899292, + 6.159163475036621, + 8.064249992370605, + 8.946314811706543, + 10.865194320678711, + 9.544049263000488, + 6.6627936363220215, + 1.579803705215454, + 8.143524169921875, + 6.509160995483398 + ], + "xaxis": "x", + "y": [ + 8.728517532348633, + 8.770746231079102, + 9.11677074432373, + 10.373712539672852, + 9.237421035766602, + 10.26722240447998, + 9.459091186523438, + 11.120741844177246, + 10.939696311950684, + 10.99378776550293, + 9.126243591308594, + 9.010720252990723, + 9.425095558166504, + 8.716423988342285, + 11.614737510681152, + 10.041977882385254, + 10.406542778015137, + 8.059087753295898, + 9.730274200439453, + 10.465351104736328, + 10.812211036682129, + 8.875877380371094, + 10.020499229431152, + 9.797350883483887, + 11.307815551757812, + 11.42276668548584, + 9.355462074279785, + 11.096529006958008, + 9.877994537353516, + 10.070211410522461, + 10.313555717468262, + 9.577936172485352, + 10.8699369430542, + 7.856522083282471, + 11.355256080627441, + 8.103291511535645, + 9.05104923248291, + 8.869852066040039, + 11.032855033874512, + 11.107758522033691, + 11.58067798614502, + 11.130584716796875, + 10.526925086975098, + 10.857772827148438, + 8.913965225219727, + 10.759373664855957, + 11.272541999816895, + 11.442437171936035, + 11.108317375183105, + 10.335371017456055, + 10.842527389526367, + 11.220292091369629, + 10.026568412780762, + 9.58740520477295, + 10.71532917022705, + 11.01673698425293, + 11.337628364562988, + 10.21743106842041, + 11.06139087677002, + 10.9774169921875, + 11.031354904174805, + 10.851637840270996, + 10.097594261169434, + 8.745205879211426 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=centrosome
x=%{x}
y=%{y}", + "hovertext": [ + "411_C12_3_4", + "411_C12_3_13", + "415_C12_1_12", + "415_C12_2_2", + "416_C12_1_5", + "416_C12_1_6", + "416_C12_2_12", + "1894_E3_1_4", + "776_B2_1_7", + "776_B2_1_8", + "776_B2_1_13", + "776_B2_5_9", + "776_B2_5_14", + "899_B2_1_8", + "899_B2_1_11", + "899_B2_2_2", + "2108_C12_5_5", + "640_A4_1_10", + "640_A4_1_13", + "640_A4_2_6", + "641_A4_3_9", + "641_A4_3_10", + "642_A4_1_4", + "642_A4_1_7", + "642_A4_8_10", + "430_H4_2_6", + "432_H4_3_4", + "432_H4_3_10", + "516_D8_3_10", + "519_D8_4_12", + "556_D8_2_10", + "295_G10_1_7", + "295_G10_2_7", + "295_G10_2_11", + "296_G10_1_7", + "296_G10_2_6", + "297_G10_1_6", + "221_F4_1_9", + "221_F4_2_5", + "793_E4_4_2", + "793_E4_4_9", + "793_E4_5_11", + "845_E4_9_13", + "281_B11_2_14", + "757_B3_1_10", + "757_B3_1_14", + "757_B3_1_15", + "757_B3_2_7", + "757_B3_2_18", + "761_B3_3_7", + "769_B3_1_13", + "769_B3_2_9", + "769_B3_2_12", + "899_E11_2_5", + "112_C8_1_7", + "112_C8_2_8", + "272_D7_1_3", + "272_D7_1_9", + "272_D7_2_14" + ], + "legendgroup": "centrosome", + "marker": { + "color": "#D62728", + "symbol": "circle" + }, + "mode": "markers", + "name": "centrosome", + "showlegend": true, + "type": "scattergl", + "x": [ + 2.7901041507720947, + 0.874021589756012, + 8.516054153442383, + 4.91292142868042, + 3.4567291736602783, + 8.75755500793457, + 1.0790436267852783, + 2.26839017868042, + 4.938488483428955, + 4.133512496948242, + -1.1102535724639893, + 3.5276830196380615, + -0.062249138951301575, + -1.182023048400879, + 0.06675713509321213, + 4.64168643951416, + 0.20393314957618713, + 3.788853645324707, + 4.931152820587158, + 5.1454691886901855, + 8.186762809753418, + 10.12415885925293, + -0.6874390840530396, + 8.80850601196289, + 5.936117649078369, + 5.048835754394531, + 10.392433166503906, + 5.0215301513671875, + 11.059093475341797, + 10.99343204498291, + 4.884566783905029, + -1.0599615573883057, + 0.19211210310459137, + -0.5866060853004456, + 10.631531715393066, + 9.003168106079102, + 10.223150253295898, + -1.362527847290039, + 7.4354023933410645, + 7.5122575759887695, + 0.648832380771637, + 8.46804428100586, + -1.2074236869812012, + 3.4489316940307617, + -0.6104220151901245, + 8.035677909851074, + 9.761763572692871, + -0.7032376527786255, + 8.075616836547852, + 7.090182304382324, + -0.9466643929481506, + 1.9249573945999146, + 2.074481964111328, + 0.16279160976409912, + 4.347227573394775, + 6.874521732330322, + 1.7617931365966797, + 3.709890365600586, + 5.1867265701293945 + ], + "xaxis": "x", + "y": [ + 8.645325660705566, + 9.793307304382324, + 7.825340747833252, + 8.018016815185547, + 9.249263763427734, + 8.013355255126953, + 10.445072174072266, + 8.802763938903809, + 8.352803230285645, + 9.570595741271973, + 9.865367889404297, + 9.495330810546875, + 10.131620407104492, + 9.84259033203125, + 10.330339431762695, + 8.959420204162598, + 10.025975227355957, + 9.848116874694824, + 7.829399585723877, + 8.102012634277344, + 9.685673713684082, + 7.893614292144775, + 10.3157377243042, + 7.569931983947754, + 8.61505126953125, + 7.848857402801514, + 7.6770501136779785, + 9.221097946166992, + 8.731588363647461, + 8.437544822692871, + 10.487589836120605, + 10.419699668884277, + 10.257688522338867, + 10.455410957336426, + 8.810300827026367, + 7.993426322937012, + 7.692299842834473, + 10.144434928894043, + 7.731456279754639, + 7.934937477111816, + 9.484111785888672, + 7.73773193359375, + 9.814582824707031, + 10.187749862670898, + 9.57421875, + 8.025893211364746, + 11.124062538146973, + 9.917794227600098, + 8.237277030944824, + 10.387939453125, + 9.82646369934082, + 8.636519432067871, + 8.639321327209473, + 9.622383117675781, + 10.104522705078125, + 8.529229164123535, + 10.113980293273926, + 11.294637680053711, + 8.061869621276855 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nuclear bodies
x=%{x}
y=%{y}", + "hovertext": [ + "431_C1_2_4", + "431_C1_2_13", + "437_C1_3_2", + "437_C1_3_6", + "342_D6_1_7", + "819_E10_2_5", + "819_E10_2_10", + "826_E10_2_7", + "826_E10_2_12", + "834_A3_1_11", + "834_A3_2_8", + "856_F5_1_1", + "856_F5_1_7", + "271_H4_2_10", + "583_A12_2_5", + "598_A12_1_5", + "598_A12_2_6", + "602_A12_1_2", + "542_B4_1_9", + "542_B4_2_6", + "542_B4_2_12", + "544_B4_1_17", + "544_B4_2_15", + "571_B4_1_2", + "571_B4_2_8", + "523_A9_1_4", + "382_C11_2_14", + "397_C11_3_4", + "62_D3_2_8", + "93_D3_1_4", + "93_D3_2_6", + "303_D8_1_6", + "303_D8_5_3", + "921_B10_4_8", + "923_B10_1_7", + "923_B10_2_9", + "931_B10_2_5", + "931_B10_2_7", + "944_G11_1_3", + "944_G11_1_9", + "947_G11_2_9", + "899_H7_1_8", + "899_H7_2_6", + "183_C1_1_13", + "183_C1_2_12", + "185_C1_1_5", + "185_C1_1_8", + "185_C1_2_7", + "242_C1_2_9", + "793_H11_2_4", + "793_H11_2_9", + "793_H11_2_15", + "800_H11_2_6", + "257_H11_2_2", + "257_H11_2_6", + "257_H11_2_7", + "190_A4_1_4", + "190_A4_1_16", + "43_H10_2_3", + "468_F1_2_11", + "468_F1_2_24", + "468_F1_3_4", + "468_F1_3_6", + "468_F1_3_13", + "473_F1_4_4", + "473_F1_4_6", + "63_A1_1_12", + "59_G9_2_2", + "59_G9_2_10", + "59_G9_2_11", + "61_G9_2_7" + ], + "legendgroup": "nuclear bodies", + "marker": { + "color": "#9467BD", + "symbol": "circle" + }, + "mode": "markers", + "name": "nuclear bodies", + "showlegend": true, + "type": "scattergl", + "x": [ + 6.819511413574219, + -1.4979426860809326, + 6.714967727661133, + 10.15843391418457, + 10.982986450195312, + 7.552469730377197, + 4.405271530151367, + 2.126934289932251, + 8.636754035949707, + 1.006922960281372, + 1.1956366300582886, + 7.551413059234619, + 10.815673828125, + 1.11006760597229, + 9.769607543945312, + 10.736916542053223, + 8.468669891357422, + 0.8967605233192444, + 6.415238380432129, + -0.9088709354400635, + 11.284372329711914, + 2.919825315475464, + 2.955153465270996, + 8.658828735351562, + 5.957705974578857, + 11.767608642578125, + 3.0754308700561523, + 1.5623754262924194, + 6.832459449768066, + 3.9559390544891357, + 0.9555965662002563, + 12.151062965393066, + 9.497099876403809, + 6.656554698944092, + 6.161899566650391, + 3.4917244911193848, + 5.035382270812988, + 3.5406405925750732, + -0.9288233518600464, + 1.7397575378417969, + 9.492664337158203, + 12.072614669799805, + 4.4753594398498535, + 3.9321999549865723, + -0.5812107920646667, + 4.828789710998535, + 4.343875885009766, + 11.386178970336914, + 8.141541481018066, + 10.042104721069336, + 4.3508195877075195, + 4.860072135925293, + 8.342652320861816, + 11.919004440307617, + 9.743101119995117, + 9.722825050354004, + 10.064361572265625, + 7.599611759185791, + 10.777355194091797, + 1.8635993003845215, + 8.719890594482422, + 5.87231969833374, + 5.513407230377197, + 10.214001655578613, + 6.5034894943237305, + 10.840243339538574, + 7.310122013092041, + 4.905212879180908, + 11.744564056396484, + 4.0952301025390625, + 8.75599193572998 + ], + "xaxis": "x", + "y": [ + 10.629657745361328, + 10.146395683288574, + 8.651334762573242, + 8.756651878356934, + 7.739330291748047, + 10.792143821716309, + 10.731819152832031, + 10.688824653625488, + 8.159095764160156, + 10.685564041137695, + 10.630619049072266, + 11.45444393157959, + 8.790694236755371, + 10.140074729919434, + 8.752631187438965, + 8.812499046325684, + 10.804101943969727, + 10.397555351257324, + 10.586000442504883, + 9.837142944335938, + 8.470909118652344, + 9.103888511657715, + 11.081582069396973, + 11.222649574279785, + 10.667492866516113, + 8.563155174255371, + 11.150300025939941, + 9.27762222290039, + 11.046632766723633, + 11.409279823303223, + 10.576905250549316, + 9.23837661743164, + 8.175479888916016, + 10.968902587890625, + 11.544849395751953, + 10.460160255432129, + 9.973970413208008, + 10.823179244995117, + 9.820189476013184, + 10.238256454467773, + 10.967262268066406, + 9.555807113647461, + 11.607996940612793, + 11.208882331848145, + 9.512435913085938, + 9.685239791870117, + 11.44731330871582, + 8.822015762329102, + 8.301166534423828, + 8.850359916687012, + 11.318218231201172, + 11.320903778076172, + 10.360037803649902, + 9.096857070922852, + 11.328948974609375, + 11.357829093933105, + 8.985947608947754, + 8.007308006286621, + 10.893662452697754, + 8.737584114074707, + 8.287182807922363, + 11.084639549255371, + 11.536459922790527, + 8.904513359069824, + 10.56672191619873, + 8.668307304382324, + 10.88458251953125, + 10.676512718200684, + 8.298717498779297, + 11.429472923278809, + 10.341072082519531 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=nuclear membrane
x=%{x}
y=%{y}", + "hovertext": [ + "467_A7_1_5", + "467_A7_1_10", + "467_A7_1_13", + "415_E12_1_8", + "415_E12_2_2", + "416_E12_1_13", + "416_E12_1_17", + "404_B8_1_16", + "404_B8_2_9", + "407_B8_2_5", + "410_B8_1_11", + "410_B8_2_6", + "667_G6_1_8", + "667_G6_1_9", + "673_G6_1_7", + "673_G6_2_5", + "138_D12_1_7", + "166_D12_2_7", + "1238_C5_4_1", + "537_D5_2_10", + "553_D5_3_13", + "537_G2_1_5", + "540_G2_1_9", + "553_G2_1_6", + "553_G2_1_12", + "553_G2_2_12", + "553_G2_2_13", + "553_G2_2_15", + "21_C1_1_9", + "21_C1_1_10", + "22_C1_1_7", + "22_C1_1_11", + "21_H8_1_4", + "22_H8_2_5", + "20_B6_1_6", + "20_B6_1_7", + "20_B6_1_10", + "20_B6_2_4", + "20_B6_2_5", + "21_B6_1_3", + "22_B6_1_7", + "921_H7_4_9", + "923_H7_1_4", + "923_H7_1_10", + "923_H7_2_9", + "923_H7_2_12", + "931_H7_1_9", + "931_H7_1_11", + "931_H7_2_7", + "128_D5_1_16", + "128_D5_2_7", + "128_D5_2_13", + "941_G2_1_19", + "941_G2_2_1", + "941_G2_2_9", + "941_G2_2_11", + "221_G2_1_12", + "222_G2_1_9", + "301_F9_2_3", + "301_F9_2_7", + "342_F9_1_15", + "342_F9_2_12", + "394_C4_3_6", + "395_C4_1_10", + "395_C4_2_10", + "399_C4_1_7", + "399_C4_2_14" + ], + "legendgroup": "nuclear membrane", + "marker": { + "color": "#8C564B", + "symbol": "circle" + }, + "mode": "markers", + "name": "nuclear membrane", + "showlegend": true, + "type": "scattergl", + "x": [ + 1.0769258737564087, + 2.237205743789673, + 9.433876991271973, + 4.117219924926758, + 6.688831329345703, + 6.604763507843018, + 6.010365009307861, + 8.741850852966309, + 8.509101867675781, + 8.97963809967041, + 4.7867841720581055, + 6.898395538330078, + 7.27094030380249, + 7.097294330596924, + 9.740988731384277, + 10.915390014648438, + 11.7156982421875, + 9.941634178161621, + 8.420010566711426, + 10.18942642211914, + 9.063899040222168, + 10.582364082336426, + 11.727190971374512, + 8.810046195983887, + 7.337896823883057, + 8.899704933166504, + 11.956937789916992, + 12.200250625610352, + 6.004112243652344, + 9.070633888244629, + 11.657057762145996, + 9.922208786010742, + 3.3051559925079346, + 12.087153434753418, + 9.383602142333984, + 11.964122772216797, + 11.915939331054688, + 12.08342170715332, + 11.990525245666504, + 11.016666412353516, + 10.979894638061523, + 3.7488372325897217, + 10.433528900146484, + 5.1994476318359375, + 10.87480354309082, + 9.497450828552246, + 11.85411262512207, + 10.445111274719238, + 9.30124282836914, + 10.46670150756836, + 2.272946834564209, + 1.939941167831421, + 4.396029472351074, + 9.95264720916748, + 6.2784647941589355, + 6.419801235198975, + 6.8658833503723145, + -1.2691106796264648, + 10.32048511505127, + 9.00498104095459, + 11.381840705871582, + 8.532036781311035, + 5.76626443862915, + 11.59349250793457, + 8.627525329589844, + 5.37863302230835, + 8.305771827697754 + ], + "xaxis": "x", + "y": [ + 10.102506637573242, + 9.855990409851074, + 8.112512588500977, + 11.296436309814453, + 8.993387222290039, + 11.139505386352539, + 10.17568588256836, + 10.103300094604492, + 10.111981391906738, + 10.214899063110352, + 11.716001510620117, + 10.318156242370605, + 10.286762237548828, + 10.416485786437988, + 11.013307571411133, + 9.198184967041016, + 8.997018814086914, + 9.319828033447266, + 10.56431770324707, + 8.994754791259766, + 11.292583465576172, + 11.166237831115723, + 9.97184944152832, + 11.34990406036377, + 10.774373054504395, + 11.232536315917969, + 9.70197582244873, + 9.620158195495605, + 10.159516334533691, + 9.69892692565918, + 9.243497848510742, + 11.13408374786377, + 10.926000595092773, + 9.092622756958008, + 11.390235900878906, + 10.05028247833252, + 9.937987327575684, + 9.58938217163086, + 10.022841453552246, + 9.076828002929688, + 11.074780464172363, + 11.146273612976074, + 10.97065258026123, + 9.496623992919922, + 10.808326721191406, + 11.136975288391113, + 9.320352554321289, + 11.125741004943848, + 10.522614479064941, + 8.793424606323242, + 9.51340103149414, + 9.735759735107422, + 8.818861961364746, + 11.051640510559082, + 10.781630516052246, + 10.206765174865723, + 10.483786582946777, + 10.358619689941406, + 11.162014961242676, + 11.259156227111816, + 9.488568305969238, + 11.417133331298828, + 10.57093334197998, + 9.118217468261719, + 11.465967178344727, + 9.97842788696289, + 10.458306312561035 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=projection
x=%{x}
y=%{y}", + "hovertext": [ + "104_F7_2_7", + "104_F7_2_9", + "104_F7_2_11", + "405_E10_1_10", + "405_E10_2_15", + "35_G12_2_3", + "404_A2_1_11", + "407_A2_1_5", + "407_A2_2_7", + "410_A2_2_10", + "69_F1_1_10", + "508_H6_2_20", + "754_E6_4_12", + "813_E6_2_4", + "534_B3_2_10", + "297_E11_1_4", + "297_E11_2_14", + "15_G10_2_5", + "79_F2_1_3", + "631_E12_1_8", + "631_E12_1_11", + "180_F10_1_9", + "180_F10_2_10", + "180_F10_2_11", + "181_F10_2_9", + "573_H11_4_4", + "589_H11_1_10", + "60_C7_1_7", + "976_H6_2_15", + "1200_E11_3_6", + "1200_E11_4_16", + "1773_E11_7_8", + "5_A9_2_4", + "5_A9_2_5", + "754_B4_1_11", + "486_F5_2_13", + "919_G7_2_5", + "340_G1_2_12", + "281_C8_2_9", + "282_C8_2_10", + "508_C1_1_7", + "548_C1_1_1", + "548_C1_2_7", + "611_C11_2_11", + "539_A7_3_13", + "652_C9_1_3", + "652_C9_1_11", + "4_H11_2_6", + "1029_E3_3_8", + "440_B6_4_7", + "860_B7_2_12", + "995_E3_2_21", + "241_G1_2_10" + ], + "legendgroup": "projection", + "marker": { + "color": "#E377C2", + "symbol": "circle" + }, + "mode": "markers", + "name": "projection", + "showlegend": true, + "type": "scattergl", + "x": [ + 2.8220162391662598, + 2.0947189331054688, + 2.3121414184570312, + 2.8480238914489746, + 2.948303699493408, + 2.929265022277832, + 3.200376510620117, + 2.261394500732422, + 3.048814296722412, + 2.8830606937408447, + 3.2838196754455566, + 2.3122212886810303, + 2.1796975135803223, + 2.859790086746216, + 2.1846511363983154, + 3.115233898162842, + 2.902467966079712, + 2.08587646484375, + 2.8921871185302734, + 3.0978469848632812, + 2.852065086364746, + 2.884228467941284, + 2.811978340148926, + 2.277855157852173, + 2.2817561626434326, + 2.3404386043548584, + 3.1054906845092773, + 2.3040971755981445, + 3.0140440464019775, + 2.869255781173706, + 2.2352135181427, + 2.8557589054107666, + 3.077486991882324, + 2.17663311958313, + 3.104400396347046, + 2.2963125705718994, + 3.081216812133789, + 2.3057804107666016, + 2.260838508605957, + 2.975579023361206, + 2.3348441123962402, + 2.8215689659118652, + 1.9969513416290283, + 2.343165397644043, + 2.137347936630249, + 2.0755209922790527, + 2.8152575492858887, + 2.1925032138824463, + 2.071396827697754, + 3.305856227874756, + 3.2211828231811523, + 2.200394868850708, + 2.300123453140259 + ], + "xaxis": "x", + "y": [ + 5.458002090454102, + 3.743023157119751, + 4.0146098136901855, + 5.511794090270996, + 5.641158580780029, + 5.611515522003174, + 5.975940227508545, + 3.944054365158081, + 5.772857666015625, + 5.553534030914307, + 6.083166599273682, + 3.989496946334839, + 3.8444862365722656, + 5.520756721496582, + 3.8507704734802246, + 5.894106864929199, + 5.583302021026611, + 3.735743522644043, + 5.571044445037842, + 5.861843109130859, + 5.519857406616211, + 5.572367191314697, + 5.430802822113037, + 3.9519436359405518, + 3.9599406719207764, + 4.0327677726745605, + 5.9076056480407715, + 4.012452602386475, + 5.761802673339844, + 5.543085098266602, + 3.912943124771118, + 5.50507926940918, + 5.856686592102051, + 3.85214900970459, + 5.865994930267334, + 3.980992078781128, + 5.82665491104126, + 3.943847179412842, + 3.9366719722747803, + 5.698197364807129, + 4.017688274383545, + 5.4394001960754395, + 3.6312921047210693, + 4.026690483093262, + 3.795874834060669, + 3.719465732574463, + 5.5655670166015625, + 3.856088876724243, + 3.7189526557922363, + 6.131337642669678, + 6.014585971832275, + 3.865633010864258, + 3.981794595718384 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{hovertext}

color=soma
x=%{x}
y=%{y}", + "hovertext": [ + "104_F7_2_10", + "409_E10_1_9", + "36_G12_2_3", + "821_B5_1_7", + "323_B9_2_6", + "326_B9_1_1", + "404_A2_2_7", + "68_F1_2_8", + "69_F1_2_13", + "69_F1_2_16", + "505_H6_1_16", + "505_H6_2_16", + "508_H6_1_11", + "508_H6_2_13", + "534_B3_2_4", + "534_B3_2_7", + "552_B3_1_9", + "552_B3_2_3", + "295_E11_1_6", + "81_F2_1_8", + "631_E12_2_14", + "180_F10_1_12", + "182_F10_1_14", + "589_H11_3_3", + "589_H11_3_9", + "709_H11_1_13", + "709_H11_1_14", + "60_C7_2_6", + "60_C7_2_10", + "1200_E11_4_17", + "132_D9_2_7", + "164_D9_1_9", + "1773_E11_7_1", + "754_B4_2_2", + "754_B4_2_6", + "486_F5_1_18", + "486_F5_2_7", + "490_F5_1_2", + "509_F5_1_16", + "509_F5_2_14", + "281_C8_2_14", + "282_C8_1_8", + "283_C8_1_9", + "283_C8_1_12", + "283_C8_1_13", + "548_C1_1_12", + "611_C11_2_14", + "614_C11_2_20", + "614_C11_3_3", + "552_A7_1_15", + "552_A7_2_13", + "652_C9_1_6", + "652_C9_2_10", + "656_C9_1_8", + "656_C9_3_5", + "4_H11_1_12", + "5_H11_1_6", + "1029_E3_2_3", + "440_B6_4_12", + "995_E3_1_9", + "995_E3_1_22", + "239_G1_2_9", + "240_G1_1_6" + ], + "legendgroup": "soma", + "marker": { + "color": "#7F7F7F", + "symbol": "circle" + }, + "mode": "markers", + "name": "soma", + "showlegend": true, + "type": "scattergl", + "x": [ + 12.474445343017578, + 12.09831428527832, + 12.642560958862305, + 10.95546817779541, + 12.414810180664062, + 12.076699256896973, + 11.15566635131836, + 10.332839965820312, + 12.562597274780273, + 12.524617195129395, + 12.537501335144043, + 12.170047760009766, + 12.449235916137695, + 12.616276741027832, + 12.673643112182617, + 12.601920127868652, + 12.379720687866211, + 12.40365982055664, + 12.056737899780273, + 10.576614379882812, + 12.267507553100586, + 12.462041854858398, + 11.588532447814941, + 12.553101539611816, + 12.273642539978027, + 12.122650146484375, + 12.066981315612793, + 10.917231559753418, + 12.100083351135254, + 12.416119575500488, + 10.22594165802002, + 12.299478530883789, + 12.235836029052734, + 10.495287895202637, + 12.492718696594238, + 12.490102767944336, + 10.913877487182617, + 12.634825706481934, + 12.524006843566895, + 10.658376693725586, + 12.328232765197754, + 12.601805686950684, + 12.582365036010742, + 12.494547843933105, + 9.013721466064453, + 12.502442359924316, + 12.568574905395508, + 12.455399513244629, + 11.968633651733398, + 12.454617500305176, + 12.569085121154785, + 12.485071182250977, + 12.577160835266113, + 12.066126823425293, + 12.032176971435547, + 12.53004264831543, + 11.91303825378418, + 11.896278381347656, + 12.415629386901855, + 12.121464729309082, + 11.814303398132324, + 11.309557914733887, + 12.111992835998535 + ], + "xaxis": "x", + "y": [ + 7.992579936981201, + 7.627177715301514, + 8.075285911560059, + 7.644983768463135, + 8.492981910705566, + 7.4276251792907715, + 7.422135829925537, + 7.232869625091553, + 8.073230743408203, + 7.977344036102295, + 8.160232543945312, + 7.399251937866211, + 7.4225687980651855, + 8.045621871948242, + 8.062952995300293, + 8.046911239624023, + 7.538491725921631, + 8.014336585998535, + 7.361385345458984, + 7.252135276794434, + 7.477945327758789, + 7.532444000244141, + 7.360785961151123, + 7.52120304107666, + 7.8504133224487305, + 7.319384574890137, + 7.388730049133301, + 8.645696640014648, + 7.906698226928711, + 7.625060081481934, + 7.162146091461182, + 7.628461837768555, + 8.372908592224121, + 7.679476737976074, + 7.441669940948486, + 7.609679698944092, + 7.43280029296875, + 7.8058366775512695, + 7.4821977615356445, + 7.30881404876709, + 7.943845272064209, + 8.101383209228516, + 7.521078586578369, + 7.666330814361572, + 8.034710884094238, + 8.158194541931152, + 7.965065002441406, + 8.304408073425293, + 8.16307258605957, + 7.495872974395752, + 7.706000328063965, + 8.158994674682617, + 7.574252605438232, + 7.6840128898620605, + 7.537256717681885, + 7.680474758148193, + 7.614544868469238, + 8.027783393859863, + 7.516109943389893, + 7.409327030181885, + 7.805696487426758, + 7.7866339683532715, + 7.521279811859131 + ], + "yaxis": "y" + } + ], + "layout": { + "legend": { + "title": { + "text": "color" + }, + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "rgb(36,36,36)" + }, + "error_y": { + "color": "rgb(36,36,36)" + }, + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "rgb(36,36,36)", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "rgb(36,36,36)" + }, + "baxis": { + "endlinecolor": "rgb(36,36,36)", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "rgb(36,36,36)" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.6 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + }, + "colorscale": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "rgb(237,237,237)" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "rgb(217,217,217)" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 1, + "tickcolor": "rgb(36,36,36)", + "ticks": "outside" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "rgb(103,0,31)" + ], + [ + 0.1, + "rgb(178,24,43)" + ], + [ + 0.2, + "rgb(214,96,77)" + ], + [ + 0.3, + "rgb(244,165,130)" + ], + [ + 0.4, + "rgb(253,219,199)" + ], + [ + 0.5, + "rgb(247,247,247)" + ], + [ + 0.6, + "rgb(209,229,240)" + ], + [ + 0.7, + "rgb(146,197,222)" + ], + [ + 0.8, + "rgb(67,147,195)" + ], + [ + 0.9, + "rgb(33,102,172)" + ], + [ + 1, + "rgb(5,48,97)" + ] + ], + "sequential": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ], + "sequentialminus": [ + [ + 0, + "#440154" + ], + [ + 0.1111111111111111, + "#482878" + ], + [ + 0.2222222222222222, + "#3e4989" + ], + [ + 0.3333333333333333, + "#31688e" + ], + [ + 0.4444444444444444, + "#26828e" + ], + [ + 0.5555555555555556, + "#1f9e89" + ], + [ + 0.6666666666666666, + "#35b779" + ], + [ + 0.7777777777777778, + "#6ece58" + ], + [ + 0.8888888888888888, + "#b5de2b" + ], + [ + 1, + "#fde725" + ] + ] + }, + "colorway": [ + "#1F77B4", + "#FF7F0E", + "#2CA02C", + "#D62728", + "#9467BD", + "#8C564B", + "#E377C2", + "#7F7F7F", + "#BCBD22", + "#17BECF" + ], + "font": { + "color": "rgb(36,36,36)" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "white", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "white", + "polar": { + "angularaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "bgcolor": "white", + "radialaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "yaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "zaxis": { + "backgroundcolor": "white", + "gridcolor": "rgb(232,232,232)", + "gridwidth": 2, + "linecolor": "rgb(36,36,36)", + "showbackground": true, + "showgrid": false, + "showline": true, + "ticks": "outside", + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + } + }, + "shapedefaults": { + "fillcolor": "black", + "line": { + "width": 0 + }, + "opacity": 0.3 + }, + "ternary": { + "aaxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "baxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + }, + "bgcolor": "white", + "caxis": { + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside", + "title": { + "standoff": 15 + }, + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + }, + "yaxis": { + "automargin": true, + "gridcolor": "rgb(232,232,232)", + "linecolor": "rgb(36,36,36)", + "showgrid": false, + "showline": true, + "ticks": "outside", + "title": { + "standoff": 15 + }, + "zeroline": false, + "zerolinecolor": "rgb(36,36,36)" + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "x" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "y" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import umap as umap\n", + "import plotly.express\n", + "\n", + "# Compute UMAP representation\n", + "reducer = umap.UMAP(random_state=1)\n", + "umap_coords = reducer.fit_transform(embeddings)\n", + "\n", + "plotly.express.scatter(x=umap_coords[:,0],\n", + " y=umap_coords[:,1],\n", + " template=\"simple_white\",\n", + " hover_name=cell_metadata.iloc[np.unique(test_pairs)]['hpa_crop_id'],\n", + " color=cell_metadata.iloc[np.unique(test_pairs)]['hpa_locations'])" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/subcellular_dl.rst b/docs/subcellular_dl.rst new file mode 100644 index 0000000..b2b909d --- /dev/null +++ b/docs/subcellular_dl.rst @@ -0,0 +1,14 @@ +Subcellular Protein Localization Deep Learning +=================================================== +.. autofunction:: cajal.subcellular_dl.make_NN_training_data +.. autoclass:: cajal.subcellular_dl.dGWOTNetwork +.. autoclass:: cajal.subcellular_dl.PairedDataset +.. autoclass:: cajal.subcellular_dl.RandomHorizontalRescale +.. autofunction:: cajal.subcellular_dl.generate_dataset_split_pairs +.. autofunction:: cajal.subcellular_dl.pretrain_model +.. autofunction:: cajal.subcellular_dl.train_dGWOT +.. autofunction:: cajal.subcellular_dl.load_dGWOT_model +.. autofunction:: cajal.subcellular_dl.extract_embeddings +.. autofunction:: cajal.subcellular_dl.predict_distances +.. autofunction:: cajal.subcellular_dl.plot_distance_predictions +.. autofunction:: cajal.subcellular_dl.plot_reconstruction_comparison \ No newline at end of file From c376403d465a152abf73e101edbca8b47b0e7507 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Thu, 23 Oct 2025 19:35:09 +0000 Subject: [PATCH 08/14] Updated docstrings in subcellular.py and subcellular_dl.py to work with sphinx --- src/cajal/subcellular.py | 590 +++++++++++++++++++++++-------- src/cajal/subcellular_dl.py | 677 ++++++++++++++++-------------------- 2 files changed, 751 insertions(+), 516 deletions(-) diff --git a/src/cajal/subcellular.py b/src/cajal/subcellular.py index 5277778..81587a2 100644 --- a/src/cajal/subcellular.py +++ b/src/cajal/subcellular.py @@ -11,12 +11,30 @@ import itertools as it import ot import warnings +import copy from .sample_seg import cell_boundaries from .gw_cython import gw_cython_core def make_cell_image(gw_ot_cell, channels): + """Create a multi-channel image array from a GW_OT_Cell object. + + This function generates a 3D image array where the first channel contains + the cell segmentation mask and subsequent channels contain normalized + intensity values for the specified channels. + + :param gw_ot_cell: Either a GW_OT_Cell object or a path to a pickled GW_OT_Cell object. + :type gw_ot_cell: GW_OT_Cell or str + :param channels: List of channel names to include in the image. Each channel should + correspond to a key in the GW_OT_Cell's intensities dictionary or + be 'nucleus' for nuclear segmentation. + :type channels: list[str] + :return: 3D array of shape (height, width, len(channels)+1) where the first + channel (index 0) contains the segmentation mask and subsequent + channels contain normalized intensity values (0-1 range). + :rtype: numpy.ndarray + """ # Load GW_OT_Cell object if path specified if isinstance(gw_ot_cell, str): with open(gw_ot_cell, 'rb') as file: @@ -42,7 +60,21 @@ def make_cell_image(gw_ot_cell, channels): def to_shape(a, shape): - + """Pad a 3D array to match a target shape. + + Centers the input array within the target shape by adding symmetric + padding with zeros. This is useful for standardizing array dimensions + for visualization or analysis. + + :param a: numpy.ndarray + Input 3D array to be padded. + :param shape: tuple of int + Target shape as (z, y, x) dimensions. + :return: numpy.ndarray + Padded array with the specified target shape, centered with + zero-padding. + :rtype: numpy.ndarray + """ z_, y_, x_ = shape z, y, x = a.shape z_pad = (z_-z) @@ -55,6 +87,21 @@ def to_shape(a, shape): def make_cell_image_for_plot(image, mask_alpha=0.2): + """Prepare a cell image for visualization by creating an RGB composite. + + Converts a multi-channel cell image into an RGB format suitable for + plotting. Adds a transparent cell mask overlay and reorders channels + to blue-red-green color scheme. + + :param image: Multi-channel cell image with shape (height, width, channels). + Channel 0 should contain the cell segmentation mask. + :type image: numpy.ndarray + :param mask_alpha: Transparency level for the cell mask overlay, by default 0.2. + :type mask_alpha: float + :return: RGB image array with shape (height, width, 3) suitable for + visualization with channel ordering as blue, red, green. + :rtype: numpy.ndarray + """ im = np.zeros((image.shape[0], image.shape[1], 3)) mask = image[:,:,0].copy() for channel_i in range(1, image.shape[2]): @@ -70,19 +117,33 @@ def make_cell_image_for_plot(image, mask_alpha=0.2): return(im) -def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha = 0.2): - """ - Plots a cell image with the specified channels. - - Args: - gw_ot_cell: The cell object or file path. - channels: A list of channels to plot. - make_square: Whether to make the plot square. - ax: The axes to plot on. - mask_alpha: The alpha value for the mask. - - Returns: - The axes object with the plotted image if ax is specified, else returns None. +def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha=0.2): + """Plot a cell image with the specified channels as an RGB composite. + + Creates a visualization of cell image data by combining multiple channels + into an RGB representation with an optional transparent mask overlay. + + :param gw_ot_cell: GW_OT_Cell or str + Either a GW_OT_Cell object or a path to a pickled GW_OT_Cell object. + :type gw_ot_cell: GW_OT_Cell or str + :param channels: list of str + List of channel names to plot. Maximum of 3 channels allowed. + Each channel should correspond to a key in the GW_OT_Cell's + intensities dictionary or be 'nucleus' for nuclear segmentation. + :type channels: list[str] + :param make_square: bool, optional + Whether to pad the image to make it square, by default True. + :type make_square: bool + :param ax: matplotlib.axes.Axes, optional + The matplotlib axes object to plot on. If None, uses current axes. + :type ax: matplotlib.axes.Axes or None + :param mask_alpha: float, optional + Transparency level for the cell mask overlay, by default 0.2. + :type mask_alpha: float + :return: matplotlib.image.AxesImage or None + Returns the AxesImage object if ax is provided, otherwise None. + :rtype: matplotlib.image.AxesImage or None + :raises ValueError: If more than 3 channels are specified for plotting. """ if len(channels) > 3: raise ValueError("Only up to 3 channels can be plotted.") @@ -98,17 +159,25 @@ def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha def rescale_mask_to_pixel_count(mask, target_pixels, max_iter=20, tolerance=0.01): - """ - Rescale a binary mask to achieve a target number of non-zero pixels using skimage.transform.resize. - - Args: - mask: 2D binary numpy array (0s and 1s) - target_pixels: Desired number of non-zero pixels - max_iter: Maximum number of scaling iterations (default: 20) - tolerance: Acceptable relative error (default: 0.01 for 1%) - - Returns: - Rescaled mask with pixel count close to target, maintaining binary values + """Rescale a binary mask to achieve a target number of non-zero pixels. + + Iteratively adjusts the scale factor to resize a binary mask until the + number of non-zero pixels is close to the target value. Uses skimage's + resize function with nearest neighbor interpolation to maintain binary + nature of the mask. + + :param mask : numpy.ndarray + 2D binary numpy array containing 0s and 1s. + :param target_pixels : int + Desired number of non-zero pixels in the rescaled mask. + :param max_iter : int, optional + Maximum number of scaling iterations, by default 20. + :param tolerance : float, optional + Acceptable relative error as a fraction, by default 0.01 (1%). + :return: numpy.ndarray + Rescaled binary mask with pixel count close to target, maintaining + binary values (0s and 1s). + :rtype: numpy.ndarray """ current_pixels = np.count_nonzero(mask) @@ -150,8 +219,19 @@ def rescale_mask_to_pixel_count(mask, target_pixels, max_iter=20, tolerance=0.01 def compute_geodesic_dmat(mask_coords): - """ - Compute geodesic distance matrix for given coordinates within a binary mask. + """Compute geodesic distance matrix for given coordinates within a binary mask. + + Calculates the shortest path distances between all pairs of coordinates + that lie within a connected binary mask region. Uses the MCP_Geometric + algorithm from scikit-image to compute geodesic distances. + + :param mask_coords : numpy.ndarray + 2D array of shape (N, 2) containing (x, y) coordinates of pixels + within the binary mask. + :return: numpy.ndarray + Symmetric distance matrix of shape (N, N) where element (i, j) + contains the geodesic distance between coordinates i and j. + :rtype: numpy.ndarray """ mask_coords[:,0] = mask_coords[:,0] - mask_coords[:,0].min() mask_coords[:,1] = mask_coords[:,1] - mask_coords[:,1].min() @@ -170,14 +250,42 @@ def compute_geodesic_dmat(mask_coords): class GW_OT_Cell: - """ - Represents a cell in the GW-OT framework. - - Args: - coords: list of (x, y) tuples for each pixel in the cell - boundary_coords: list of (x, y) tuples sampled from the cell boundary - intensities: dict mapping channel names to pixel intensity arrays - nucleus: array or list indicating nuclear identity for each cell pixel + """A cell representation for Gromov-Wasserstein Optimal Transport analysis. + + This class encapsulates cell morphology and intensity information needed + for Gromov-Wasserstein distance computations and optimal transport analysis + between cells. + + :param coords : numpy.ndarray + Array of shape (N, 2) containing (x, y) coordinates for each pixel + in the cell. + :param boundary_coords : numpy.ndarray, optional + Array of shape (M, 2) containing (x, y) coordinates sampled from + the cell boundary. Default is None. + :param intensities : dict, optional + Dictionary mapping channel names (str) to intensity arrays (numpy.ndarray) + of length N, where N is the number of cell pixels. Default is None. + :param nucleus : numpy.ndarray, optional + Array of length N indicating nuclear identity (0 or 1) for each + cell pixel. Default is None. + :param metric : str, optional + Distance metric for computing coordinate distance matrices. Options + are 'euclidean', 'geodesic', or None. Default is 'euclidean'. + + :ivar coords: Cell pixel coordinates. + :vartype coords: numpy.ndarray + :ivar boundary_coords: Cell boundary coordinates. + :vartype boundary_coords: numpy.ndarray or None + :ivar coord_dmat: Distance matrix between all cell pixel coordinates. + :vartype coord_dmat: numpy.ndarray or None + :ivar boundary_coord_dmat: Distance matrix between boundary coordinates. + :vartype boundary_coord_dmat: numpy.ndarray or None + :ivar intensities: Channel intensity information. + :vartype intensities: dict + :ivar nucleus: Nuclear segmentation information. + :vartype nucleus: numpy.ndarray or None + :ivar size: Number of pixels in the cell. + :vartype size: int """ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, metric='euclidean'): self.coords = coords @@ -187,7 +295,7 @@ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, self.boundary_coord_dmat = None elif metric == 'geodesic': self.coord_dmat = compute_geodesic_dmat(self.coords) - self.boundary_coord_dmat = None + self.boundary_coord_dmat = squareform(pdist(boundary_coords, metric=metric)) if boundary_coords is not None else None # if boundary_coords is not None: # warnings.warn("Geodesic distance matrix cannot be computed for cell boundary coordinates, ignoring.") else: @@ -198,35 +306,66 @@ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, self.size = len(coords) def copy(self): - copy = GW_OT_Cell( + """Return a deep copy of the GW_OT_Cell instance. + + Returns + ------- + GW_OT_Cell + A new GW_OT_Cell instance with copied data from the current object. + All arrays and dictionaries are deep-copied to avoid shared references. + """ + obj_copy = GW_OT_Cell( coords=self.coords.copy(), boundary_coords=self.boundary_coords.copy() if self.boundary_coords is not None else None, intensities=copy.deepcopy(self.intensities), nucleus=self.nucleus.copy() if self.nucleus is not None else None ) - copy.coord_dmat = self.coord_dmat.copy() if self.coord_dmat is not None else None - copy.boundary_coord_dmat = self.boundary_coord_dmat.copy() if self.boundary_coords is not None else None - copy.size = self.size - return copy + obj_copy.coord_dmat = self.coord_dmat.copy() if self.coord_dmat is not None else None + obj_copy.boundary_coord_dmat = self.boundary_coord_dmat.copy() if self.boundary_coord_dmat is not None else None + obj_copy.size = self.size + return obj_copy def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_factor=None, ds_target_size=None, filter_border_cells=True, n_boundary_points=100, save_path=None, return_objects=True): - """ - Create a list of GW_OT_Cell objects, each representing a cell in the image. - Args: - image: 3D numpy array (H x W x C) representing the image - channels: List of channel names corresponding to the last dimension of the image - cell_mask_image: 2D numpy array (H x W) with integer labels for each cell (0 for background) - nucleus_mask_image: Optional 2D numpy array (H x W) with integer labels for nuclei (0 for background) - ds_factor: Optional downsampling factor (integer). If provided, downsample by this factor. - ds_target_size: Optional target size (integer). If provided, downsample to achieve this number of pixels per cell. - filter_border_cells: If True, exclude cells touching the image border. - n_boundary_points: If provided, sample this many points from the cell boundary and include in the dictionary. - save_path: Optional path to save the processed cell objects. - return_objects: If True, return the list of GW_OT_Cell objects. - Returns: - If return_objects is True, return the list of GW_OT_Cell objects; otherwise, return None. + """Create GW_OT_Cell objects from segmented microscopy images. + + Processes a multi-channel microscopy image with cell and nuclear segmentation + masks to create a list of GW_OT_Cell objects suitable for Gromov-Wasserstein + optimal transport analysis. + + :param image : numpy.ndarray + 3D numpy array of shape (H, W, C) representing the multi-channel image. + :param channels : list of str + List of channel names corresponding to the last dimension of the image. + :param cell_mask_image : numpy.ndarray + 2D numpy array of shape (H, W) with integer labels for each cell + (0 for background). + :param nucleus_mask_image : numpy.ndarray, optional + 2D numpy array of shape (H, W) with integer labels for nuclei + (0 for background). Default is None. + :param ds_factor : int, optional + Downsampling factor. If provided, downsample by this factor. + Default is None. + :param ds_target_size : int, optional + Target number of pixels per cell after downsampling. If provided, + downsample to achieve this pixel count. Default is None. + :param filter_border_cells : bool, optional + If True, exclude cells touching the image border. Default is True. + :param n_boundary_points : int, optional + Number of points to sample from the cell boundary. If None, boundary + sampling is skipped. Default is 100. + :param save_path : str, optional + Directory path to save the processed cell objects as pickle files. + If None, objects are not saved. Default is None. + :param return_objects : bool, optional + If True, return the list of GW_OT_Cell objects. Default is True. + + :return + ------- + list of GW_OT_Cell or None + If return_objects is True, returns a list of GW_OT_Cell objects; + otherwise returns None. """ cell_inds = np.unique(cell_mask_image) cell_inds = cell_inds[cell_inds > 0] # Remove background (0) @@ -282,6 +421,17 @@ def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_ def _init_gw_pool(cell_objects: list, points: str): + """Initialize global variables for parallel Gromov-Wasserstein computation. + + This function sets up shared state for multiprocessing workers computing + pairwise Gromov-Wasserstein distances. + + :param cell_objects : list + List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. + :param points : str + Type of points to use for distance computation. Either 'boundary' + for boundary coordinates or 'full' for all cell coordinates. + """ # list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects global _CELL_OBJECTS _CELL_OBJECTS = cell_objects @@ -291,15 +441,12 @@ def _init_gw_pool(cell_objects: list, points: str): def _gw_index(p: tuple[int, int]): - """ - Compute Gromov-Wasserstein distance between two cells given their indices. - Args: - p: tuple of two indices (i, j) representing the cells to compare - Returns: - tuple of (i, j, coupling_mat, gw_dist) where: - i, j: indices of the cells - coupling_mat: numpy array representing the coupling matrix - gw_dist: Gromov-Wasserstein distance between the two cells + """Worker that computes the GW coupling and distance for a pair of cells. + + :param p: tuple (i, j) of cell indices + :type p: tuple[int, int] + :return: (i, j, coupling_mat, gw_dist) + :rtype: tuple[int, int, numpy.ndarray, float] """ i, j = p # load GW_OT_Cell objects if path specified @@ -335,19 +482,26 @@ def _gw_index(p: tuple[int, int]): return (i, j, coupling_mat, gw_dist) - def gw_pairwise_parallel(cell_objects, points='boundary', num_processes=4, chunksize=20, n_approx_anchors=None, initial_anchor=0): - """ - Compute pairwise Gromov-Wasserstein distances between cells in parallel. - Args: - cell_objects: list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects - points: which points to use for the distance computation ('boundary' or 'full') - num_processes: number of parallel processes to use (default: 4) - chunksize: number of pairs to process in each chunk (default: 20) - n_approx_anchors: number of anchors to use for triangle inequality approximation of GW distances - initial_anchor: index of the first anchor cell (default: None, which means the first cell is used) - Returns: - gw_dmat: numpy array of shape (N, N) containing pairwise Gromov-Wasserstein distances + """Compute pairwise Gromov-Wasserstein distances (optionally in parallel). + + Calculates the Gromov-Wasserstein distance matrix for a colloection of cells + using either exact computation or traiangle inequality approximation with anchors. + + :param cell_objects: list of GW_OT_Cell objects or file paths + :type cell_objects: list + :param points: 'boundary' or 'full' (default: 'boundary') + :type points: str + :param num_processes: number of parallel processes to use + :type num_processes: int + :param chunksize: chunk size for parallel imap + :type chunksize: int + :param n_approx_anchors: number of anchors for approximation (None = exact) + :type n_approx_anchors: int or None + :param initial_anchor: initial anchor index for approximation + :type initial_anchor: int + :return: symmetric GW distance matrix of shape (N, N) + :rtype: numpy.ndarray """ N = len(cell_objects) # Compute all pairwise GW distances @@ -387,8 +541,12 @@ def gw_pairwise_parallel(cell_objects, points='boundary', num_processes=4, chunk def find_centroid(distance_matrix): - """ - Find the centroid of a set of points given a distance matrix. + """Return the index of the centroid point (minimizes sum of distances). + + :param distance_matrix: square symmetric pairwise distance matrix + :type distance_matrix: numpy.ndarray + :return: index of centroid point + :rtype: int """ sum_distances = np.sum(distance_matrix, axis=1) centroid_index = np.argmin(sum_distances) @@ -396,7 +554,29 @@ def find_centroid(distance_matrix): def _init_fgw_map_pool(cell_objects: list, channels: list, compartment_specific: bool, method, - fused_channel: str, fused_cost: float, fused_param: float, unbalanced_param: float): + fused_channel: str, fused_cost: float, fused_param: float, unbalanced_param: float, + nuclear_fraction: float = 0.2): + """Initialize global state for parallel fused GW mapping workers. + + :param cell_objects: list of GW_OT_Cell objects or paths + :type cell_objects: list + :param channels: channel names to compute OT for + :type channels: list[str] + :param compartment_specific: whether to perform compartment-specific mapping + :type compartment_specific: bool + :param method: mapping method ('fused' or 'fused_unbalanced') + :type method: str + :param fused_channel: channel name used for fused mapping + :type fused_channel: str + :param fused_cost: fused channel cost multiplier + :type fused_cost: float + :param fused_param: alpha parameter for fused GW + :type fused_param: float + :param unbalanced_param: regularization for unbalanced fused GW + :type unbalanced_param: float + :param nuclear_fraction: fraction considered nuclear during mapping + :type nuclear_fraction: float + """ global _CELL_OBJECTS # _CELL_OBJECTS = cell_objects # list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects global _CHANNELS @@ -412,11 +592,19 @@ def _init_fgw_map_pool(cell_objects: list, channels: list, compartment_specific: global _FUSED_PARAM _FUSED_PARAM = fused_param # parameter for fused/unbalanced GW morphology mapping global _UNBALANCED_PARAM - _UNBALANCED_PARAM = unbalanced_param # parameter for fused unbalanced GW mapping`` + _UNBALANCED_PARAM = unbalanced_param # parameter for fused unbalanced GW mapping + global _NUC_FRAC + _NUC_FRAC = nuclear_fraction # fraction of cell considered as nucleus for compartment-specific -# compute morphology fGW and map protein distribution from one cell to another def _fgw_map_index(p: tuple[int, int]): + """Worker that computes fused GW mapping and maps protein distributions. + + :param p: tuple (i, j) of source and target cell indices + :type p: tuple[int, int] + :return: (i, j, gw_dist, mapped_distbs) + :rtype: tuple[int, int, float, numpy.ndarray] + """ i, j = p # load GW_OT_Cell objects if path specified if isinstance(_CELL_OBJECTS[i], str): @@ -440,11 +628,11 @@ def _fgw_map_index(p: tuple[int, int]): # rescale uniform distribution in cell i to have same nuclear/cytoplasm ration as cell j a = np.zeros(n_A) - a[_CELL_OBJECTS[i].nucleus==1] = 0.5 / n_pixel_nuc_i - a[_CELL_OBJECTS[i].nucleus==0] = 0.5 / n_pixel_cyto_i + a[_CELL_OBJECTS[i].nucleus==1] = _NUC_FRAC / n_pixel_nuc_i + a[_CELL_OBJECTS[i].nucleus==0] = (1 - _NUC_FRAC) / n_pixel_cyto_i b = np.zeros(n_B) - b[_CELL_OBJECTS[j].nucleus==1] = 0.5 / n_pixel_nuc_j - b[_CELL_OBJECTS[j].nucleus==0] = 0.5 / n_pixel_cyto_j + b[_CELL_OBJECTS[j].nucleus==1] = _NUC_FRAC / n_pixel_nuc_j + b[_CELL_OBJECTS[j].nucleus==0] = (1 - _NUC_FRAC) / n_pixel_cyto_j else: a = np.repeat(1/n_A, n_A) b = np.repeat(1/n_B, n_B) @@ -453,17 +641,21 @@ def _fgw_map_index(p: tuple[int, int]): alpha = _FUSED_PARAM cost = _FUSED_COST rho = _UNBALANCED_PARAM - if _FUSED_CHANNEL == 'nucleus': - cost_matrix = cdist(_CELL_OBJECTS[i].nucleus[:,np.newaxis], _CELL_OBJECTS[j].nucleus[:,np.newaxis],) * cost + if i == j: + gw_dist = 0 + coupling_mat = np.eye(n_A) else: - cost_matrix = cdist(_CELL_OBJECTS[i].intensities[_FUSED_CHANNEL][:,np.newaxis], _CELL_OBJECTS[j].intensities[_FUSED_CHANNEL][:,np.newaxis],) * cost - if _METHOD == 'fused': - coupling_mat, log = ot.gromov.fused_gromov_wasserstein(M=cost_matrix, C1=A, C2=B, p=a, q=b, alpha=alpha, log=True) - gw_dist = log['fgw_dist'] - elif _METHOD == 'fused_unbalanced': - coupling_mat, coupling_mat_2, log = ot.gromov.fused_unbalanced_gromov_wasserstein(M=cost_matrix, Cx=A, Cy=B, wx=a, wy=b, alpha=alpha, - reg_marginals=rho, max_iter=20, log=True) - gw_dist = log['fugw_cost'] + if _FUSED_CHANNEL == 'nucleus': + cost_matrix = cdist(_CELL_OBJECTS[i].nucleus[:,np.newaxis], _CELL_OBJECTS[j].nucleus[:,np.newaxis],) * cost + else: + cost_matrix = cdist(_CELL_OBJECTS[i].intensities[_FUSED_CHANNEL][:,np.newaxis], _CELL_OBJECTS[j].intensities[_FUSED_CHANNEL][:,np.newaxis],) * cost + if _METHOD == 'fused': + coupling_mat, log = ot.gromov.fused_gromov_wasserstein(M=cost_matrix, C1=A, C2=B, p=a, q=b, alpha=alpha, log=True) + gw_dist = log['fgw_dist'] + elif _METHOD == 'fused_unbalanced': + coupling_mat, coupling_mat_2, log = ot.gromov.fused_unbalanced_gromov_wasserstein(M=cost_matrix, Cx=A, Cy=B, wx=a, wy=b, alpha=alpha, + reg_marginals=rho, max_iter=20, log=True) + gw_dist = log['fugw_cost'] if _COMPARTMENT_SPECIFIC: # find nuclear pixels after mapping @@ -518,26 +710,80 @@ def _fgw_map_index(p: tuple[int, int]): return (i, j, gw_dist, mapped_distbs) +def map_to_cell(cell_object_from, cell_object_to, channels, compartment_specific=True, method='fused', + fused_channel='protein', fused_cost=10, fused_param=0.1, unbalanced_param=70, nuclear_fraction=0.2): + """Map protein distributions from one cell onto another via Fused Gromov-Wasserstein. + + :param cell_object_from: source ``GW_OT_Cell`` + :type cell_object_from: GW_OT_Cell + :param cell_object_to: target ``GW_OT_Cell`` + :type cell_object_to: GW_OT_Cell + :param channels: list of channel names to map + :type channels: list[str] + :param compartment_specific: whether to use compartment-specific mapping + :type compartment_specific: bool + :param method: 'fused' or 'fused_unbalanced' + :type method: str + :param fused_channel: channel name to use for fused morphology cost + :type fused_channel: str + :param fused_cost: fused channel cost multiplier + :type fused_cost: float + :param fused_param: alpha parameter for fused GW + :type fused_param: float + :param unbalanced_param: regularization for unbalanced GW + :type unbalanced_param: float + :param nuclear_fraction: nuclear fraction for compartment scaling + :type nuclear_fraction: float + :return: mapped distributions with shape (len(channels), n_target_pixels) + :rtype: numpy.ndarray + """ + mapped_distbs = map_to_cell_parallel( + cell_objects=[cell_object_from, cell_object_to], + channels=channels, + target_cell_ind=1, + compartment_specific=compartment_specific, + method=method, + fused_channel=fused_channel, + fused_cost=fused_cost, + fused_param=fused_param, + unbalanced_param=unbalanced_param, + nuclear_fraction=nuclear_fraction, + parallel=False + ) + return mapped_distbs[:,0,:] + + def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_specific=True, method='fused', fused_channel='protein', fused_cost=10, fused_param=0.1, unbalanced_param=70, parallel=True, - num_processes=4, chunksize=20): - """ - Map protein distributions from all cells to a target cell using fused Gromov-Wasserstein morphology mapping in parallel. - Args: - cell_objects: list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects - channels: list of channel names to map - target_cell_ind: index of the target cell to map to - compartment_specific: whether to do compartment-specific mapping (nuclear/cytoplasm) - method: method for morphology mapping ('fused' or 'fused_unbalanced') - fused_channel: channel to use for fused GW morphology mapping - fused_cost: cost for fused GW morphology mapping - fused_param: parameter for fused/unbalanced GW morphology mapping - unbalanced_param: parameter for fused unbalanced GW mapping - num_processes: number of parallel processes to use (default: 4) - chunksize: number of pairs to process in each chunk (default: 20) - Returns: - mapped_distbs: numpy array of shape (N, len(channels), n_target_pixels) containing mapped protein distributions - from each cell to the target cell + nuclear_fraction=0.2, num_processes=4, chunksize=20): + """Map protein distributions from all cells to a single target cell. + + :param cell_objects: list of GW_OT_Cell objects or file paths + :type cell_objects: list + :param channels: channel names to map + :type channels: list[str] + :param target_cell_ind: index of the target cell + :type target_cell_ind: int + :param compartment_specific: whether to use compartment-specific mapping + :type compartment_specific: bool + :param method: mapping method ('fused' or 'fused_unbalanced') + :type method: str + :param fused_channel: channel used for fused cost + :type fused_channel: str + :param fused_cost: cost multiplier for fused channel + :type fused_cost: float + :param fused_param: alpha parameter for fused GW + :type fused_param: float + :param unbalanced_param: regularization for unbalanced GW + :type unbalanced_param: float + :param parallel: whether to run in parallel + :type parallel: bool + :param num_processes: number of processes for parallel execution + :type num_processes: int + :param chunksize: chunk size for parallel map + :type chunksize: int + :return: array of mapped distributions with shape (len(channels), N, n_target_pixels) + :rtype: numpy.ndarray """ print('Mapping cells to target cell:') N = len(cell_objects) @@ -547,7 +793,7 @@ def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_sp # Parallelized with Pool( initializer=_init_fgw_map_pool, initargs=(cell_objects, channels, compartment_specific, method, - fused_channel, fused_cost, fused_param, unbalanced_param), + fused_channel, fused_cost, fused_param, unbalanced_param, nuclear_fraction), processes=num_processes ) as pool: res = pool.imap_unordered(_fgw_map_index, index_pairs, chunksize=chunksize) @@ -560,7 +806,8 @@ def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_sp mapped_distbs[:,i,:] = mapped_distb else: # Non-parallelized - _init_fgw_map_pool(cell_objects, channels, compartment_specific, method, fused_channel, fused_cost, fused_param, unbalanced_param) + _init_fgw_map_pool(cell_objects, channels, compartment_specific, method, fused_channel, fused_cost, + fused_param, unbalanced_param, nuclear_fraction) mapped_distbs = np.zeros((len(channels),N,cell_objects[target_cell_ind].coord_dmat.shape[0])) for p in tqdm(index_pairs): i, j, gw_dist, mapped_distb = _fgw_map_index(p) @@ -569,6 +816,13 @@ def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_sp def _init_gw_mapped_ot_pool(cell_object: GW_OT_Cell, mapped_cell_dists: np.ndarray): + """Initialize global state for OT computation on mapped distributions. + + :param cell_object: target ``GW_OT_Cell`` containing coordinate distance matrix + :type cell_object: GW_OT_Cell + :param mapped_cell_dists: mapped distributions array (n_channels, N, n_target_pixels) + :type mapped_cell_dists: numpy.ndarray + """ global _CELL_OBJECT _CELL_OBJECT = cell_object # GW_OT_Cell object global _MAPPED_CELL_DISTS @@ -576,6 +830,13 @@ def _init_gw_mapped_ot_pool(cell_object: GW_OT_Cell, mapped_cell_dists: np.ndarr def _gw_mapped_ot_index(p: tuple[int, int]): + """Worker that computes OT distances between two mapped distributions. + + :param p: tuple (i, j) of indices + :type p: tuple[int, int] + :return: (i, j, ot_dists) where ot_dists is an array per channel + :rtype: tuple[int, int, numpy.ndarray] + """ global _CELL_OBJECT i, j = p if isinstance(_CELL_OBJECT, str): @@ -594,30 +855,79 @@ def _gw_mapped_ot_index(p: tuple[int, int]): return (i, j, ot_dists) -def gw_mapped_ot_pairwise_parallel(cell_object, mapped_cell_dists, num_processes=4, chunksize=20): - """ - Compute pairwise Gromov-Wasserstein distances between cells with mapped protein distributions in parallel. - Args: - cell_object: GW_OT_Cell object for target cell or path to GW_OT_Cell object for target cell - mapped_cell_dists: numpy array of shape (N, len(channels), n_target_pixels) containing mapped protein distributions - from each cell to the target cell - num_processes: number of parallel processes to use (default: 4) - chunksize: number of pairs to process in each chunk (default: 20) - Returns: - ot_dmats: numpy array of shape (len(channels), N, N) containing pairwise Gromov-Wasserstein distances - for each channel between cells with mapped protein distributions +def gw_mapped_ot_pairwise_parallel(cell_object, mapped_cell_dists, num_processes=4, chunksize=20, index_pairs=None, n_approx_anchors=None, initial_anchor=0): + """Compute pairwise OT distances between mapped protein distributions. + + Calculates pairwise optimal transport distances for protein distribution after + mapping to a common cell morphology. + + :param cell_object: target ``GW_OT_Cell`` or path to pickled object + :type cell_object: GW_OT_Cell or str + :param mapped_cell_dists: array of mapped distributions (len(channels), N, n_target_pixels) + :type mapped_cell_dists: numpy.ndarray + :param num_processes: number of processes for parallel execution + :type num_processes: int + :param chunksize: chunk size for parallel imap + :type chunksize: int + :param index_pairs: optional iterable of (i, j) index pairs to compute + :type index_pairs: iterable[tuple[int, int]] or None + :param n_approx_anchors: number of anchors for triangle inequality approximation + :type n_approx_anchors: int or None + :param initial_anchor: initial anchor index + :type initial_anchor: int + :return: if ``index_pairs`` is None returns array (len(channels), N, N), else (len(channels), len(index_pairs)) + :rtype: numpy.ndarray """ print('Computing pairwise OT distances:') N = mapped_cell_dists.shape[1] - index_pairs = it.combinations(iter(range(N)), 2) # cell pairs to compute fGW / OT for - total_num_pairs = int((N * (N - 1)) / 2) # total number of cell pairs to compute (for progress bar) - with Pool( - initializer=_init_gw_mapped_ot_pool, initargs=(cell_object,mapped_cell_dists,), processes=num_processes - ) as pool: - res = pool.imap(_gw_mapped_ot_index, index_pairs, chunksize=chunksize) - # store OT distances in dictionary of matricies - ot_dmats = np.zeros((mapped_cell_dists.shape[0],N,N)) - for i, j, ot_dists in tqdm(res, total=total_num_pairs, position=0, leave=True): - ot_dmats[:,i,j] = ot_dists - ot_dmats[:,j,i] = ot_dists - return(ot_dmats) \ No newline at end of file + # Compute all pairwise OT distances or specific specific pairs + if n_approx_anchors is None: + if index_pairs is None: + index_pairs = list(it.combinations(range(N), 2)) + total_num_pairs = int((N * (N - 1)) / 2) + output_full_matrix = True + else: + index_pairs = list(index_pairs) + total_num_pairs = len(index_pairs) + output_full_matrix = False + + with Pool( + initializer=_init_gw_mapped_ot_pool, initargs=(cell_object, mapped_cell_dists,), processes=num_processes + ) as pool: + res = pool.imap(_gw_mapped_ot_index, index_pairs, chunksize=chunksize) + if output_full_matrix: + ot_dmats = np.zeros((mapped_cell_dists.shape[0], N, N)) + for i, j, ot_dists in tqdm(res, total=total_num_pairs, position=0, leave=True): + ot_dmats[:, i, j] = ot_dists + ot_dmats[:, j, i] = ot_dists + return ot_dmats + else: + ot_dists_arr = np.zeros((mapped_cell_dists.shape[0], total_num_pairs)) + for idx, (i, j, ot_dists) in enumerate(tqdm(res, total=total_num_pairs, position=0, leave=True)): + ot_dists_arr[:, idx] = ot_dists + return ot_dists_arr + # Approximate OT distances using triangle inequality + else: + if index_pairs is not None: + raise ValueError("index_pairs cannot be specified when using triangle inequality approximation.") + anchor_ind = initial_anchor + all_anchor_ot_dists = np.zeros((mapped_cell_dists.shape[0], n_approx_anchors, N)) + for i_anchor in range(n_approx_anchors): + anchor_ot_dists = np.zeros((mapped_cell_dists.shape[0], N)) + index_pairs = it.product(iter(range(N)), [anchor_ind]) + total_num_pairs = N + with Pool( + initializer=_init_gw_mapped_ot_pool, initargs=(cell_object, mapped_cell_dists,), processes=num_processes + ) as pool: + res = pool.imap(_gw_mapped_ot_index, index_pairs, chunksize=chunksize) + for i, j, ot_dists in tqdm(res, total=total_num_pairs, position=0, leave=True): + anchor_ot_dists[:, i] = ot_dists + all_anchor_ot_dists[:,i_anchor,:] = anchor_ot_dists + anchor_ind = np.argmax(all_anchor_ot_dists.mean(axis=0)[:i_anchor+1,:].min(axis=0)) # next anchor + ot_dmats = np.zeros((mapped_cell_dists.shape[0], N, N)) + for channel_i in range(mapped_cell_dists.shape[0]): + for i,j in it.combinations(range(N), 2): + d = min(all_anchor_ot_dists[channel_i,i] + all_anchor_ot_dists[channel_i,j]) + ot_dmats[channel_i, i, j] = d + ot_dmats[channel_i, j, i] = d + return ot_dmats \ No newline at end of file diff --git a/src/cajal/subcellular_dl.py b/src/cajal/subcellular_dl.py index 024a0e1..f76263d 100644 --- a/src/cajal/subcellular_dl.py +++ b/src/cajal/subcellular_dl.py @@ -23,19 +23,14 @@ def resize_cell_image(image, target_shape): Uses bilinear interpolation for probability channels and nearest neighbor for binary mask channels to preserve the discrete nature of segmentation masks. - Parameters - ---------- - image : numpy.ndarray - Input image of shape (H, W, 3) where channel 0 is probability/intensity, + :param image: Input image of shape (H, W, 3) where channel 0 is probability/intensity, channels 1 and 2 are binary masks. - target_shape : tuple of int - Target shape as (height, width) for the resized image. - - Returns - ------- - numpy.ndarray - Resized image of shape (target_shape[0], target_shape[1], 3) with + :type image: numpy.ndarray + :param target_shape: Target shape as (height, width) for the resized image. + :type target_shape: tuple of int + :returns: Resized image of shape (target_shape[0], target_shape[1], 3) with appropriate interpolation applied to each channel. + :rtype: numpy.ndarray """ out = np.zeros((target_shape[0], target_shape[1], image.shape[2]), dtype=image.dtype) # Probability channel (bilinear) @@ -54,22 +49,13 @@ def major_axis_pca_with_center(mask, center): center point to determine the major axis of the shape. The axis orientation is normalized to point toward the side with more mass distribution. - Parameters - ---------- - mask : numpy.ndarray - 2D binary mask where non-zero values indicate the region of interest. - center : tuple of float - Center point as (y, x) coordinates to use for PCA computation + :param mask: 2D binary mask where non-zero values indicate the region of interest. + :type mask: numpy.ndarray + :param center: Center point as (y, x) coordinates to use for PCA computation (e.g., nucleus centroid). - - Returns - ------- - tuple - major_axis_vector : numpy.ndarray - Unit vector representing the major axis direction as [dy, dx]. - angle : float - Angle in radians relative to the x-axis, with consistent orientation - toward the side with more mass. + :type center: tuple of float + :returns: Tuple containing the major axis unit vector and the angle in radians. + :rtype: tuple (major_axis_vector: numpy.ndarray, angle: float) """ yx = np.argwhere(mask > 0) yx_centered = yx - np.array(center) # center at nucleus @@ -94,23 +80,18 @@ def align_image(image, center='cell', cell_mask_channel=1, nucleus_channel=2): rotates it to align the major axis horizontally. The image is padded as needed and trimmed to remove empty borders. - Parameters - ---------- - image : numpy.ndarray - Input image of shape (H, W, 3) containing cell imaging data. - center : str, optional - Centering method: 'cell' to center on cell mask, 'nucleus' to center + :param image: Input image of shape (H, W, 3) containing cell imaging data. + :type image: numpy.ndarray + :param center: Centering method: 'cell' to center on cell mask, 'nucleus' to center on nucleus mask. Default is 'cell'. - cell_mask_channel : int, optional - Index of the cell mask channel. Default is 1. - nucleus_channel : int, optional - Index of the nucleus mask channel. Default is 2. - - Returns - ------- - numpy.ndarray - Centered and rotated image, possibly larger than input due to padding + :type center: str + :param cell_mask_channel: Index of the cell mask channel. Default is 1. + :type cell_mask_channel: int + :param nucleus_channel: Index of the nucleus mask channel. Default is 2. + :type nucleus_channel: int + :returns: Centered and rotated image, possibly larger than input due to padding and rotation operations. + :rtype: numpy.ndarray """ # Center based on largest labeled cell/nucleus object if center == 'cell': @@ -173,31 +154,26 @@ def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped Creates paired cell images and their corresponding mapped versions for training deep learning models. Images are aligned, normalized, and saved as numpy arrays. - Parameters - ---------- - save_path : str - Directory path to save processed images. Cell images saved to + :param save_path: Directory path to save processed images. Cell images saved to '/cell_images' and mapped images to '/mapped_cell_images'. - cell_objects : list - List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. - reference_cell_object : GW_OT_Cell or str - Reference cell object or path to pickled reference cell object used + :type save_path: str + :param cell_objects: List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. + :type cell_objects: list + :param reference_cell_object: Reference cell object or path to pickled reference cell object used as template for mapped distributions. - mapped_channel_distributions : numpy.ndarray - Array of mapped protein distributions for each cell. - channel : str - Channel name to use for image processing. - center : str, optional - Centering method for image alignment: 'cell' or 'nucleus'. Default is 'cell'. - rescale : bool, optional - Whether to rescale images to a fixed size. Default is True. - shape : tuple of int, optional - Target shape (height, width) for resizing images. Default is (64, 64). - - Returns - ------- - None - Images are saved to disk as .npy files. + :type reference_cell_object: GW_OT_Cell or str + :param mapped_channel_distributions: Array of mapped protein distributions for each cell. + :type mapped_channel_distributions: numpy.ndarray + :param channel: Channel name to use for image processing. + :type channel: str + :param center: Centering method for image alignment: 'cell' or 'nucleus'. Default is 'cell'. + :type center: str + :param rescale: Whether to rescale images to a fixed size. Default is True. + :type rescale: bool + :param shape: Target shape (height, width) for resizing images. Default is (64, 64). + :type shape: tuple of int + :returns: None. Images are saved to disk as .npy files. + :rtype: None """ if not rescale: max_size = 0 @@ -253,16 +229,14 @@ class EfficientNetFeatureExtractor(nn.Module): embeddings from cell images. Handles variable input channel numbers and resizes inputs to match EfficientNet requirements. - Parameters - ---------- - embedding_size : int, optional - Size of the output embedding vector. Default is 50. - input_channels : int, optional - Number of input channels in the cell images. Default is 3. - efficientnet_type : str, optional - Type of EfficientNet architecture to use. Default is 'efficientnet_b0'. - pretrained : bool, optional - Whether to use pretrained ImageNet weights. Default is True. + :param embedding_size: Size of the output embedding vector. Default is 50. + :type embedding_size: int, optional + :param input_channels: Number of input channels in the cell images. Default is 3. + :type input_channels: int, optional + :param efficientnet_type: Type of EfficientNet architecture to use. Default is 'efficientnet_b0'. + :type efficientnet_type: str, optional + :param pretrained: Whether to use pretrained ImageNet weights. Default is True. + :type pretrained: bool, optional """ def __init__(self, embedding_size=50, input_channels=3, efficientnet_type='efficientnet_b0', pretrained=True): super().__init__() @@ -316,14 +290,12 @@ class UNetDecoder(nn.Module): The architecture progressively upsamples from a compact representation back to full image resolution. - Parameters - ---------- - embedding_size : int, optional - Size of the input embedding vector. Default is 50. - image_size : int, optional - Target output image size (assumed square). Default is 64. - out_channels : int, optional - Number of output channels in the reconstructed image. Default is 1. + :param embedding_size: Size of the input embedding vector. Default is 50. + :type embedding_size: int, optional + :param image_size: Target output image size (assumed square). Default is 64. + :type image_size: int, optional + :param out_channels: Number of output channels in the reconstructed image. Default is 1. + :type out_channels: int, optional """ def __init__(self, embedding_size=50, image_size=64, out_channels=1): super().__init__() @@ -388,14 +360,12 @@ class dGWOTNetwork(nn.Module): distances between cell morphologies while enabling reconstruction of protein distributions. - Parameters - ---------- - input_channels : int, optional - Number of input image channels. Default is 3. - embedding_size : int, optional - Dimensionality of the feature embedding space. Default is 50. - image_size : int, optional - Size of input/output images (assumed square). Default is 64. + :param input_channels: Number of input image channels. Default is 3. + :type input_channels: int, optional + :param embedding_size: Dimensionality of the feature embedding space. Default is 50. + :type embedding_size: int, optional + :param image_size: Size of input/output images (assumed square). Default is 64. + :type image_size: int, optional """ def __init__(self, input_channels=3, embedding_size=50, image_size=64): super().__init__() @@ -464,27 +434,25 @@ class PretrainPairedDataset(Dataset): has a corresponding target image. Handles channel dimension reordering and applies optional transforms. - Parameters - ---------- - input_files : list of str - List of file paths to input image numpy arrays. - target_files : list of str - List of file paths to target image numpy arrays. Must have same + :param input_files: List of file paths to input image numpy arrays. + :type input_files: list of str + :param target_files: List of file paths to target image numpy arrays. Must have same length as input_files. - transform : callable, optional - Optional transform to apply to both input and target images. + :type target_files: list of str + :param transform: Optional transform to apply to both input and target images. Default is None. + :type transform: callable, optional + :param augment_transform: Additional augmentation transform for data augmentation. Default is None. + :type augment_transform: callable, optional - Raises - ------ - AssertionError - If input_files and target_files have different lengths. + :raises AssertionError: If input_files and target_files have different lengths. """ - def __init__(self, input_files, target_files, transform=None): - self.input_files = input_files - self.target_files = target_files + def __init__(self, input_files, target_files, transform=None, augment_transform=None, n_augment=1): + self.input_files = [f for f in input_files for _ in range(n_augment)] + self.target_files = [f for f in target_files for _ in range(n_augment)] assert len(self.input_files) == len(self.target_files), 'Input and target directories must have the same number of images.' self.transform = transform + self.augment_transform = augment_transform def __len__(self): return len(self.input_files) @@ -504,52 +472,65 @@ def __getitem__(self, idx): if self.transform: input_img = self.transform(input_img) target_img = self.transform(target_img) + if self.augment_transform: + input_img = self.augment_transform(input_img) + target_img = self.augment_transform(target_img) input_img = torch.from_numpy(input_img).float() target_img = torch.from_numpy(target_img).float() return input_img, target_img -def pretrain_model(input_files, target_files, model, save_path=None, model_name="pretrained_model", +def pretrain_model(paired_dataset, model, save_path=None, model_name="pretrained_model", batch_size=64, epochs=10, lr=1e-3, device=None, return_model=True): """ - Pretrain a model using paired input and target images. - - Performs pretraining of a neural network model using reconstruction loss - between input images and their corresponding targets. Uses KL divergence - loss for probability distributions. - - Parameters - ---------- - input_files : list of str - List of file paths to input image numpy arrays. - target_files : list of str - List of file paths to target image numpy arrays. - model : torch.nn.Module - Neural network model to pretrain. Must have a forward method that + Pretrain a model using paired input and target images provided as a PairedDataset. + + This function accepts a `PairedDataset` object and first converts it into a + `PretrainPairedDataset` by collecting the unique image indices referenced in + the paired dataset. For each unique image index `i`, the input path is + '/cell_i.npy' and the target path is + '/mapped_cell_i.npy'. After conversion the rest of the + training loop is identical to the previous implementation. + + :param paired_dataset: Dataset containing paired indices and directory information. Must have + attributes `image_dir`, `mapped_image_dir` and `image_pairs`. + :type paired_dataset: PairedDataset + :param model: Neural network model to pretrain. Must have a forward method that takes two identical inputs and returns reconstructions. - save_path : str, optional - Directory path to save the pretrained model. If None, model is not saved. + :type model: torch.nn.Module + :param save_path: Directory path to save the pretrained model. If None, model is not saved. Default is None. - model_name : str, optional - Name prefix for saved model files. Default is "pretrained_model". - batch_size : int, optional - Batch size for training. Default is 64. - epochs : int, optional - Number of training epochs. Default is 10. - lr : float, optional - Learning rate for the Adam optimizer. Default is 1e-3. - device : torch.device, optional - Device to run training on. If None, automatically selects GPU + :type save_path: str, optional + :param model_name: Name prefix for saved model files. Default is "pretrained_model". + :type model_name: str, optional + :param batch_size: Batch size for training. Default is 64. + :type batch_size: int, optional + :param epochs: Number of training epochs. Default is 10. + :type epochs: int, optional + :param lr: Learning rate for the Adam optimizer. Default is 1e-3. + :type lr: float, optional + :param device: Device to run training on. If None, automatically selects GPU if available. Default is None. - return_model : bool, optional - Whether to return the trained model. If False, returns None. + :type device: torch.device, optional + :param return_model: Whether to return the trained model. If False, returns None. Default is True. + :type return_model: bool, optional - Returns - ------- - torch.nn.Module or None - The pretrained model if return_model is True, otherwise None. + :returns: The pretrained model if return_model is True, otherwise None. + :rtype: torch.nn.Module or None """ + # Convert PairedDataset to lists of input/target files (unique images) + if not isinstance(paired_dataset, PairedDataset): + raise TypeError("paired_dataset must be an instance of PairedDataset") + + all_indices = set() + for pair in paired_dataset.image_pairs: + all_indices.update(pair) + all_indices = sorted(list(all_indices)) + + input_files = [os.path.join(paired_dataset.image_dir, f"cell_{idx}.npy") for idx in all_indices] + target_files = [os.path.join(paired_dataset.mapped_image_dir, f"mapped_cell_{idx}.npy") for idx in all_indices] + dataset = PretrainPairedDataset(input_files, target_files) loader = DataLoader(dataset, batch_size=batch_size, shuffle=True) if device is None: @@ -646,22 +627,17 @@ def kullback_leibler_divergence_loss(y_true, y_pred): distribution. Used for reconstruction quality assessment when dealing with normalized protein distributions. - Parameters - ---------- - y_true : torch.Tensor - Target probability distribution tensor. - y_pred : torch.Tensor - Predicted probability distribution tensor. + :param y_true: Target probability distribution tensor. + :type y_true: torch.Tensor + :param y_pred: Predicted probability distribution tensor. + :type y_pred: torch.Tensor + :returns: Mean KL divergence loss across the batch. + :rtype: torch.Tensor - Returns - ------- - torch.Tensor - Mean KL divergence loss across the batch. + .. note:: - Notes - ----- - The KL divergence is computed as: KL(P||Q) = sum(P * log(P/Q)) - Values are clamped to avoid log(0) numerical issues. + The KL divergence is computed as: KL(P||Q) = sum(P * log(P/Q)) + Values are clamped to avoid log(0) numerical issues. """ epsilon = 1e-8 @@ -686,23 +662,18 @@ def sparsity_constraint_loss(embeddings, sparsity_target=0.1): sparsity level. This regularization helps prevent overfitting and promotes more interpretable feature representations. - Parameters - ---------- - embeddings : torch.Tensor - Hidden unit activations of shape (batch_size, embedding_dim). - sparsity_target : float, optional - Desired average activation level for each hidden unit. Default is 0.1. - - Returns - ------- - torch.Tensor - Scalar sparsity loss computed as the sum of KL divergences across + :param embeddings: Hidden unit activations of shape (batch_size, embedding_dim). + :type embeddings: torch.Tensor + :param sparsity_target: Desired average activation level for each hidden unit. Default is 0.1. + :type sparsity_target: float, optional + :returns: Scalar sparsity loss computed as the sum of KL divergences across all embedding dimensions. + :rtype: torch.Tensor + + .. note:: - Notes - ----- - Activations are passed through sigmoid to ensure they're in (0,1) range - before computing the sparsity constraint. + Activations are passed through sigmoid to ensure they're in (0,1) range + before computing the sparsity constraint. """ epsilon = 1e-8 # Apply sigmoid to ensure activations are in (0,1) @@ -722,23 +693,18 @@ def reconstruction_loss(x, uf): probability distributions use KL divergence, while binary masks use binary cross-entropy loss. - Parameters - ---------- - x : torch.Tensor - Original image tensor of shape (batch_size, 3, height, width). - uf : torch.Tensor - Reconstructed image tensor of same shape as x. + :param x: Original image tensor of shape (batch_size, 3, height, width). + :type x: torch.Tensor + :param uf: Reconstructed image tensor of same shape as x. + :type uf: torch.Tensor + :returns: Combined reconstruction loss across all channels. + :rtype: torch.Tensor - Returns - ------- - torch.Tensor - Combined reconstruction loss across all channels. - - Notes - ----- - - Channel 0: Probability distribution (KL divergence loss) - - Channel 1: Binary cell mask (Binary cross-entropy loss) - - Channel 2: Binary nucleus mask (Binary cross-entropy loss) + .. note:: + + - Channel 0: Probability distribution (KL divergence loss) + - Channel 1: Binary cell mask (Binary cross-entropy loss) + - Channel 2: Binary nucleus mask (Binary cross-entropy loss) """ # Split channels x_prob, x_mask1, x_mask2 = x[:, 0:1, :, :], x[:, 1:2, :, :], x[:, 2:3, :, :] @@ -760,18 +726,13 @@ def get_random_pairs(indices, n_pairs): samples a specified number of them. Useful for creating training pairs from a dataset without exhaustive pairwise combinations. - Parameters - ---------- - indices : array-like - List or array of indices to create pairs from. - n_pairs : int - Number of pairs to randomly sample. If larger than the total + :param indices: List or array of indices to create pairs from. + :type indices: array-like + :param n_pairs: Number of pairs to randomly sample. If larger than the total possible pairs, returns all possible pairs. - - Returns - ------- - numpy.ndarray - Array of shape (n_pairs, 2) containing randomly selected index pairs. + :type n_pairs: int + :returns: Array of shape (n_pairs, 2) containing randomly selected index pairs. + :rtype: numpy.ndarray """ all_pairs = np.array(list(it.combinations(indices, 2))) if n_pairs > len(all_pairs): @@ -787,15 +748,13 @@ class IndexedImageDataset(Dataset): Simple dataset for loading cell images by index, useful for extracting embeddings from unique images in a PairedDataset without loading duplicates. - Parameters - ---------- - image_dir : str - Path to directory containing cell image .npy files with naming + :param image_dir: Path to directory containing cell image .npy files with naming convention 'cell_{index}.npy'. - indices : list of int - List of cell indices to load. - transform : callable, optional - Transform function to apply to all images. Default is None. + :type image_dir: str + :param indices: List of cell indices to load. + :type indices: list of int + :param transform: Transform function to apply to all images. Default is None. + :type transform: callable, optional """ def __init__(self, image_dir, indices, transform=None): self.image_dir = image_dir @@ -824,30 +783,28 @@ class PairedDataset(Dataset): for training distance-based models. Supports data augmentation and lazy loading for memory efficiency. - Parameters - ---------- - image_dir : str - Path to directory containing cell image .npy files with naming + :param image_dir: Path to directory containing cell image .npy files with naming convention 'cell_{index}.npy'. - mapped_image_dir : str - Path to directory containing mapped cell image .npy files with naming + :type image_dir: str + :param mapped_image_dir: Path to directory containing mapped cell image .npy files with naming convention 'mapped_cell_{index}.npy'. - distances : list of float - Distance values corresponding to each image pair for supervised learning. - image_pairs : list of tuple - List of (index1, index2) tuples specifying which images to pair. - transform : callable, optional - Transform function to apply to all images. Default is None. - augment_transform : callable, optional - Additional augmentation transform for data augmentation. Default is None. - n_augment : int, optional - Number of augmented copies to create for each pair. Default is 1. - - Notes - ----- - The dataset expects file naming conventions: - - Cell images: 'cell_{index}.npy' - - Mapped images: 'mapped_cell_{index}.npy' + :type mapped_image_dir: str + :param distances: Distance values corresponding to each image pair for supervised learning. + :type distances: list of float + :param image_pairs: List of (index1, index2) tuples specifying which images to pair. + :type image_pairs: list of tuple + :param transform: Transform function to apply to all images. Default is None. + :type transform: callable, optional + :param augment_transform: Additional augmentation transform for data augmentation. Default is None. + :type augment_transform: callable, optional + :param n_augment: Number of augmented copies to create for each pair. Default is 1. + :type n_augment: int, optional + + .. note:: + + The dataset expects file naming conventions: + - Cell images: 'cell_{index}.npy' + - Mapped images: 'mapped_cell_{index}.npy' """ def __init__(self, image_dir, mapped_image_dir, distances, image_pairs, transform=None, augment_transform=None, n_augment=1): # Store directory paths. Listing all files is no longer needed. @@ -905,20 +862,18 @@ class RandomHorizontalRescale(object): distribution of cell mask widths. Uses appropriate interpolation methods for different channel types (bilinear for intensity, nearest for masks). - Parameters - ---------- - min_relative_width : float, optional - Minimum relative width of the cell mask as fraction of image width. + :param min_relative_width: Minimum relative width of the cell mask as fraction of image width. Default is 0.1. - max_relative_width : float, optional - Maximum relative width of the cell mask as fraction of image width. + :type min_relative_width: float, optional + :param max_relative_width: Maximum relative width of the cell mask as fraction of image width. Default is 1.0. + :type max_relative_width: float, optional + + .. note:: - Notes - ----- - - Channel 0: Resized with bilinear interpolation (intensity/probability) - - Other channels: Resized with nearest neighbor interpolation (binary masks) - The transform maintains the original image width by padding or cropping after rescaling. + - Channel 0: Resized with bilinear interpolation (intensity/probability) + - Other channels: Resized with nearest neighbor interpolation (binary masks) + The transform maintains the original image width by padding or cropping after rescaling. """ def __init__(self, min_relative_width=0.1, max_relative_width=1.0): assert 0 < min_relative_width <= max_relative_width <= 1.0 @@ -981,55 +936,50 @@ def train_dGWOT(train_dataset, valid_dataset, test_dataset, save_path, dataset_n and image reconstruction objectives. Supports early stopping, learning rate scheduling, and optional sparsity constraints. - Parameters - ---------- - train_dataset, valid_dataset, test_dataset : Dataset - PyTorch datasets for training, validation, and testing. - save_path : str - Directory path to save the trained model and checkpoints. - dataset_name : str - Name prefix for saved model files. - embedding_size : int, optional - Dimensionality of the feature embedding space. Default is 50. - image_shape : tuple of int, optional - Shape of input images as (height, width). Default is (64, 64). - batch_size : int, optional - Batch size for training. Default is 100. - epochs : int, optional - Maximum number of training epochs. Default is 100. - device : torch.device, optional - Device for training. If None, automatically selects GPU if available. - learning_rate : float, optional - Initial learning rate for Adam optimizer. Default is 0.001. - dist_weight : float, optional - Weight for distance loss vs reconstruction loss in total loss. Default is 1.0. - early_stopping : bool, optional - Whether to use early stopping based on validation loss. Default is True. - patience : int, optional - Number of epochs to wait for improvement before stopping. Default is 3. - weight_decay : float, optional - L2 regularization weight for optimizer. Default is 1e-5. - lr_gamma : float, optional - Decay factor for exponential learning rate scheduler. Default is 0.95. - sparsity_weight : float, optional - Weight for sparsity constraint loss. Default is 0.0 (disabled). - sparsity_target : float, optional - Target sparsity level for hidden activations. Default is 0.05. - pretrained_path : str, optional - Path to pretrained model weights to initialize from. Default is None. - show_loss_components : bool, optional - Whether to display individual loss components (distance, reconstruction, + :param train_dataset: PyTorch datasets for training, validation, and testing. + :type train_dataset: Dataset + :param valid_dataset: Validation dataset. + :type valid_dataset: Dataset + :param test_dataset: Test dataset. + :type test_dataset: Dataset + :param save_path: Directory path to save the trained model and checkpoints. + :type save_path: str + :param dataset_name: Name prefix for saved model files. + :type dataset_name: str + :param embedding_size: Dimensionality of the feature embedding space. Default is 50. + :type embedding_size: int, optional + :param image_shape: Shape of input images as (height, width). Default is (64, 64). + :type image_shape: tuple of int, optional + :param batch_size: Batch size for training. Default is 100. + :type batch_size: int, optional + :param epochs: Maximum number of training epochs. Default is 100. + :type epochs: int, optional + :param device: Device for training. If None, automatically selects GPU if available. + :type device: torch.device, optional + :param learning_rate: Initial learning rate for Adam optimizer. Default is 0.001. + :type learning_rate: float, optional + :param dist_weight: Weight for distance loss vs reconstruction loss in total loss. Default is 1.0. + :type dist_weight: float, optional + :param early_stopping: Whether to use early stopping based on validation loss. Default is True. + :type early_stopping: bool, optional + :param patience: Number of epochs to wait for improvement before stopping. Default is 3. + :type patience: int, optional + :param weight_decay: L2 regularization weight for optimizer. Default is 1e-5. + :type weight_decay: float, optional + :param lr_gamma: Decay factor for exponential learning rate scheduler. Default is 0.95. + :type lr_gamma: float, optional + :param sparsity_weight: Weight for sparsity constraint loss. Default is 0.0 (disabled). + :type sparsity_weight: float, optional + :param sparsity_target: Target sparsity level for hidden activations. Default is 0.05. + :type sparsity_target: float, optional + :param pretrained_path: Path to pretrained model weights to initialize from. Default is None. + :type pretrained_path: str, optional + :param show_loss_components: Whether to display individual loss components (distance, reconstruction, sparsity) during training. Default is False. + :type show_loss_components: bool, optional - Returns - ------- - tuple - model : torch.nn.Module - Trained dGWOT model. - train_losses : list of float - Training loss history. - val_losses : list of float - Validation loss history. + :returns: Tuple containing the trained model, training loss history, and validation loss history. + :rtype: tuple (model: torch.nn.Module, train_losses: list of float, val_losses: list of float) """ # Setup device if device is None: @@ -1192,17 +1142,12 @@ def load_dGWOT_model(checkpoint_path, device=None): """ Load a dGWOT model from a checkpoint containing state dict and config. - Parameters - ---------- - checkpoint_path : str - Path to the checkpoint file containing both state_dict and config. - device : torch.device, optional - Device to load the model on. If None, uses GPU if available. - - Returns - ------- - torch.nn.Module - Loaded dGWOT model ready for inference or further training. + :param checkpoint_path: Path to the checkpoint file containing both state_dict and config. + :type checkpoint_path: str + :param device: Device to load the model on. If None, uses GPU if available. + :type device: torch.device, optional + :returns: Loaded dGWOT model ready for inference or further training. + :rtype: torch.nn.Module """ if device is None: device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') @@ -1249,26 +1194,21 @@ def extract_embeddings(model, data, batch_size=64, device=None): Processes input images through the feature extractor to obtain latent embeddings. Supports lists/arrays of images, PyTorch datasets, and PairedDatasets. - Parameters - ---------- - model : torch.nn.Module - Trained dGWOT model with a feature_extractor attribute. - data : list, numpy.ndarray, torch.utils.data.Dataset, or PairedDataset - Input data to extract embeddings from. Can be: + :param model: Trained dGWOT model with a feature_extractor attribute. + :type model: torch.nn.Module + :param data: Input data to extract embeddings from. Can be: - List of numpy arrays with shape (H, W, C) - - Single numpy array with shape (N, H, W, C) + - Single numpy array with shape (N, H, W, C) - PyTorch Dataset where __getitem__ returns images - PairedDataset (extracts embeddings for unique images only) - batch_size : int, optional - Batch size for processing. Default is 64. - device : torch.device, optional - Device to run computation on. If None, uses model's current device. - - Returns - ------- - numpy.ndarray - Extracted embeddings of shape (N, embedding_size) where N is the + :type data: list, numpy.ndarray, torch.utils.data.Dataset, or PairedDataset + :param batch_size: Batch size for processing. Default is 64. + :type batch_size: int, optional + :param device: Device to run computation on. If None, uses model's current device. + :type device: torch.device, optional + :returns: Extracted embeddings of shape (N, embedding_size) where N is the number of input images. + :rtype: numpy.ndarray """ if device is None: device = next(model.parameters()).device @@ -1370,22 +1310,17 @@ def predict_distances(model, paired_dataset, batch_size=64, device=None): pairwise Euclidean distances in the embedding space. This provides predictions that can be compared against ground truth distances. - Parameters - ---------- - model : torch.nn.Module - Trained dGWOT model with a feature_extractor attribute. - paired_dataset : PairedDataset - Dataset containing paired images with known distances. - batch_size : int, optional - Batch size for processing embeddings. Default is 64. - device : torch.device, optional - Device to run computation on. If None, uses model's current device. - - Returns - ------- - numpy.ndarray - Array of predicted distances of shape (len(paired_dataset),) + :param model: Trained dGWOT model with a feature_extractor attribute. + :type model: torch.nn.Module + :param paired_dataset: Dataset containing paired images with known distances. + :type paired_dataset: PairedDataset + :param batch_size: Batch size for processing embeddings. Default is 64. + :type batch_size: int, optional + :param device: Device to run computation on. If None, uses model's current device. + :type device: torch.device, optional + :returns: Array of predicted distances of shape (len(paired_dataset),) corresponding to each pair in the dataset. + :rtype: numpy.ndarray """ if device is None: device = next(model.parameters()).device @@ -1442,32 +1377,27 @@ def plot_distance_predictions(model, paired_dataset, batch_size=64, device=None, Creates a scatter plot comparing model predictions against ground truth distances with a diagonal reference line and correlation metrics. - Parameters - ---------- - model : torch.nn.Module - Trained dGWOT model with a feature_extractor attribute. - paired_dataset : PairedDataset - Dataset containing paired images with known distances. - batch_size : int, optional - Batch size for processing embeddings. Default is 64. - device : torch.device, optional - Device to run computation on. If None, uses model's current device. - figsize : tuple, optional - Figure size as (width, height). Default is (8, 8). - return_plot : bool, optional - Whether to return the matplotlib figure and axes objects. Default is False. - title : str, optional - Custom title for the plot. If None, uses default with correlation metrics. - alpha : float, optional - Transparency of scatter points. Default is 0.6. - s : int, optional - Size of scatter points. Default is 20. - - Returns - ------- - None or tuple - If return_plot is False: displays the plot and returns None. + :param model: Trained dGWOT model with a feature_extractor attribute. + :type model: torch.nn.Module + :param paired_dataset: Dataset containing paired images with known distances. + :type paired_dataset: PairedDataset + :param batch_size: Batch size for processing embeddings. Default is 64. + :type batch_size: int, optional + :param device: Device to run computation on. If None, uses model's current device. + :type device: torch.device, optional + :param figsize: Figure size as (width, height). Default is (8, 8). + :type figsize: tuple, optional + :param return_plot: Whether to return the matplotlib figure and axes objects. Default is False. + :type return_plot: bool, optional + :param title: Custom title for the plot. If None, uses default with correlation metrics. + :type title: str, optional + :param alpha: Transparency of scatter points. Default is 0.6. + :type alpha: float, optional + :param s: Size of scatter points. Default is 20. + :type s: int, optional + :returns: If return_plot is False: displays the plot and returns None. If return_plot is True: returns (fig, ax) matplotlib objects. + :rtype: None or tuple """ # Get predictions predicted_distances = predict_distances(model, paired_dataset, batch_size=batch_size, device=device) @@ -1510,34 +1440,29 @@ def plot_distance_predictions(model, paired_dataset, batch_size=64, device=None, return None if not return_plot else (None, None) -def plot_reconstruction_comparison(model, paired_dataset, num_images=5, device=None, figsize=None, seed=None): +def plot_reconstruction_comparison(model, paired_dataset, n_cells=5, device=None, figsize=None, seed=None): """ - Plot comparison of original mapped protein images vs model reconstructions. + Plot comparison of original mapped protein distributions vs model reconstructions. - Randomly selects different individual images from the dataset, shows the original mapped - protein images in the top row and their reconstructions from the model in + Randomly selects cell images from the dataset, shows the original mapped + protein distributions in the top row and their reconstructions from the model in the bottom row. - Parameters - ---------- - model : torch.nn.Module - Trained dGWOT model with reconstruction capabilities. - paired_dataset : PairedDataset - Dataset containing paired images for reconstruction. - num_images : int, optional - Number of image pairs to display. Default is 5. - device : torch.device, optional - Device to run model on. If None, uses model's current device. - figsize : tuple, optional - Figure size as (width, height). If None, automatically calculated + :param model: Trained dGWOT model with reconstruction capabilities. + :type model: torch.nn.Module + :param paired_dataset: Dataset containing paired images for reconstruction. + :type paired_dataset: PairedDataset + :param n_cells: Number of image pairs to display. Default is 5. + :type n_cells: int, optional + :param device: Device to run model on. If None, uses model's current device. + :type device: torch.device, optional + :param figsize: Figure size as (width, height). If None, automatically calculated based on number of images. - seed : int, optional - Random seed for reproducible image selection. Default is None. - - Returns - ------- - None - Displays the plot using matplotlib. + :type figsize: tuple, optional + :param seed: Random seed for reproducible image selection. Default is None. + :type seed: int, optional + :returns: None. Displays the plot using matplotlib. + :rtype: None """ if device is None: device = next(model.parameters()).device @@ -1553,17 +1478,17 @@ def plot_reconstruction_comparison(model, paired_dataset, num_images=5, device=N all_image_indices = list(all_image_indices) # Randomly select unique image indices - selected_image_indices = np.random.choice(all_image_indices, size=min(num_images, len(all_image_indices)), replace=False) + selected_image_indices = np.random.choice(all_image_indices, size=min(n_cells, len(all_image_indices)), replace=False) # Set up the plot if figsize is None: - figsize = (3 * num_images, 6) + figsize = (3 * n_cells, 6) try: import matplotlib.pyplot as plt - fig, axes = plt.subplots(2, num_images, figsize=figsize) - if num_images == 1: + fig, axes = plt.subplots(2, n_cells, figsize=figsize) + if n_cells == 1: axes = axes.reshape(2, 1) model.eval() From 1ce6ed4bd6e7229ddb3e9ca063df5a0d98941263 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Thu, 23 Oct 2025 19:48:39 +0000 Subject: [PATCH 09/14] Added torch and torchvision to requirements.txt --- requirements.txt | 138 ++++++++++++++++++++++++----------------------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/requirements.txt b/requirements.txt index a54c600..fa1d6ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,68 +1,70 @@ -asttokens==2.4.1 -comm==0.2.2 -contourpy==1.0.6 -cycler==0.11.0 -Cython==3.0.10 -debugpy==1.8.1 -decorator==5.1.1 -dill==0.3.8 -executing==2.0.1 -fonttools==4.43.0 -igraph==0.10.2 -imageio==2.33.0 -ipykernel==6.29.4 -ipython==8.18.1 -ipywidgets==8.0.2 -jedi==0.19.1 -joblib==1.2.0 -jupyter_client==8.6.2 -jupyter_core==5.7.2 -jupyterlab-widgets==3.0.3 -kiwisolver==1.4.4 -lazy_loader==0.4 -leidenalg==0.10.2 -llvmlite==0.43.0 -matplotlib==3.9.0 -matplotlib-inline==0.1.7 -mpltern==1.0.4 -multiprocess==0.70.16 -nest-asyncio==1.6.0 -networkx==2.8.8 -numpy==2.0.0 -packaging==24.1 -pandas==2.2.2 -parso==0.8.4 -pathos==0.3.2 -pexpect==4.9.0 -pillow==11.0 -platformdirs==4.2.2 -potpourri3d==1.1.0 -POT==0.9.5 -pox==0.3.4 -ppft==1.7.6.8 -prompt_toolkit==3.0.47 -psutil==6.0.0 -ptyprocess==0.7.0 -pure-eval==0.2.2 -Pygments==2.18.0 -pyparsing==3.1.2 -python-dateutil==2.9.0.post0 -python-louvain==0.16 -pytz==2024.1 -pyzmq==26.0.3 -scikit-dimension==0.3.4 -scikit-image==0.24.0 -scikit-learn==1.5.0 -scipy==1.14.1 -six==1.16.0 -stack-data==0.6.3 -texttable==1.6.4 -threadpoolctl==3.1.0 -tifffile==2022.10.10 -tornado==6.4.2 -tqdm==4.66.4 -traitlets==5.14.3 -trimesh==3.16.1 -tzdata==2024.1 -wcwidth==0.2.13 -widgetsnbextension==4.0.3 +asttokens==2.4.1 +comm==0.2.2 +contourpy==1.0.6 +cycler==0.11.0 +Cython==3.0.10 +debugpy==1.8.1 +decorator==5.1.1 +dill==0.3.8 +executing==2.0.1 +fonttools==4.43.0 +igraph==0.10.2 +imageio==2.33.0 +ipykernel==6.29.4 +ipython==8.18.1 +ipywidgets==8.0.2 +jedi==0.19.1 +joblib==1.2.0 +jupyter_client==8.6.2 +jupyter_core==5.7.2 +jupyterlab-widgets==3.0.3 +kiwisolver==1.4.4 +lazy_loader==0.4 +leidenalg==0.10.2 +llvmlite==0.43.0 +matplotlib==3.9.0 +matplotlib-inline==0.1.7 +mpltern==1.0.4 +multiprocess==0.70.16 +nest-asyncio==1.6.0 +networkx==2.8.8 +numpy==2.0.0 +packaging==24.1 +pandas==2.2.2 +parso==0.8.4 +pathos==0.3.2 +pexpect==4.9.0 +pillow==11.0 +platformdirs==4.2.2 +potpourri3d==1.1.0 +POT==0.9.5 +pox==0.3.4 +ppft==1.7.6.8 +prompt_toolkit==3.0.47 +psutil==6.0.0 +ptyprocess==0.7.0 +pure-eval==0.2.2 +Pygments==2.18.0 +pyparsing==3.1.2 +python-dateutil==2.9.0.post0 +python-louvain==0.16 +pytz==2024.1 +pyzmq==26.0.3 +scikit-dimension==0.3.4 +scikit-image==0.24.0 +scikit-learn==1.5.0 +scipy==1.14.1 +six==1.16.0 +stack-data==0.6.3 +texttable==1.6.4 +threadpoolctl==3.1.0 +tifffile==2022.10.10 +tornado==6.4.2 +torch==2.1.1 +torchvision==0.16.1 +tqdm==4.66.4 +traitlets==5.14.3 +trimesh==3.16.1 +tzdata==2024.1 +wcwidth==0.2.13 +widgetsnbextension==4.0.3 From cb69a2d7f1e5e3e9ca461b4fbcec9f89eb487c74 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Thu, 23 Oct 2025 20:17:17 +0000 Subject: [PATCH 10/14] Fixed docstrings for GW_OT_Cell, process_image, generate_dataset_split_pairs --- src/cajal/subcellular.py | 83 +++++++++++++++---------------------- src/cajal/subcellular_dl.py | 25 +++++------ 2 files changed, 44 insertions(+), 64 deletions(-) diff --git a/src/cajal/subcellular.py b/src/cajal/subcellular.py index 81587a2..d83bf37 100644 --- a/src/cajal/subcellular.py +++ b/src/cajal/subcellular.py @@ -123,27 +123,30 @@ def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha= Creates a visualization of cell image data by combining multiple channels into an RGB representation with an optional transparent mask overlay. - :param gw_ot_cell: GW_OT_Cell or str - Either a GW_OT_Cell object or a path to a pickled GW_OT_Cell object. - :type gw_ot_cell: GW_OT_Cell or str - :param channels: list of str - List of channel names to plot. Maximum of 3 channels allowed. - Each channel should correspond to a key in the GW_OT_Cell's - intensities dictionary or be 'nucleus' for nuclear segmentation. + :param image: 3D numpy array of shape (H, W, C) representing the multi-channel image. + :type image: numpy.ndarray + :param channels: List of channel names corresponding to the last dimension of the image. :type channels: list[str] - :param make_square: bool, optional - Whether to pad the image to make it square, by default True. - :type make_square: bool - :param ax: matplotlib.axes.Axes, optional - The matplotlib axes object to plot on. If None, uses current axes. - :type ax: matplotlib.axes.Axes or None - :param mask_alpha: float, optional - Transparency level for the cell mask overlay, by default 0.2. - :type mask_alpha: float - :return: matplotlib.image.AxesImage or None - Returns the AxesImage object if ax is provided, otherwise None. - :rtype: matplotlib.image.AxesImage or None - :raises ValueError: If more than 3 channels are specified for plotting. + :param cell_mask_image: 2D numpy array of shape (H, W) with integer labels for each cell (0 for background). + :type cell_mask_image: numpy.ndarray + :param nucleus_mask_image: 2D numpy array of shape (H, W) with integer labels for nuclei (0 for background). Default is None. + :type nucleus_mask_image: numpy.ndarray or None + :param ds_factor: Downsampling factor. If provided, downsample by this factor. Default is None. + :type ds_factor: int or None + :param ds_target_size: Target number of pixels per cell after downsampling. If provided, downsample to achieve this pixel count. Default is None. + :type ds_target_size: int or None + :param filter_border_cells: If True, exclude cells touching the image border. Default is True. + :type filter_border_cells: bool + :param n_boundary_points: Number of points to sample from the cell boundary. If None, boundary sampling is skipped. Default is 100. + :type n_boundary_points: int or None + :param save_path: Directory path to save the processed cell objects as pickle files. If None, objects are not saved. Default is None. + :type save_path: str or None + :param return_objects: If True, return the list of GW_OT_Cell objects. Default is True. + :type return_objects: bool + + :return: + If return_objects is True, returns a list of GW_OT_Cell objects; otherwise returns None. + :rtype: list[GW_OT_Cell] or None """ if len(channels) > 3: raise ValueError("Only up to 3 channels can be plotted.") @@ -249,6 +252,7 @@ def compute_geodesic_dmat(mask_coords): return(geodesic_dmat) + class GW_OT_Cell: """A cell representation for Gromov-Wasserstein Optimal Transport analysis. @@ -256,36 +260,17 @@ class GW_OT_Cell: for Gromov-Wasserstein distance computations and optimal transport analysis between cells. - :param coords : numpy.ndarray - Array of shape (N, 2) containing (x, y) coordinates for each pixel - in the cell. - :param boundary_coords : numpy.ndarray, optional - Array of shape (M, 2) containing (x, y) coordinates sampled from - the cell boundary. Default is None. - :param intensities : dict, optional - Dictionary mapping channel names (str) to intensity arrays (numpy.ndarray) + :param coords : Array of shape (N, 2) containing (x, y) coordinates for each pixel in the cell. + :type coords: numpy.ndarray + :param boundary_coords : Array of shape (M, 2) containing (x, y) coordinates sampled from the cell boundary. Default is None. + :type boundary_coords: numpy.ndarray or None + :param intensities : Dictionary mapping channel names (str) to intensity arrays (numpy.ndarray) of length N, where N is the number of cell pixels. Default is None. - :param nucleus : numpy.ndarray, optional - Array of length N indicating nuclear identity (0 or 1) for each - cell pixel. Default is None. - :param metric : str, optional - Distance metric for computing coordinate distance matrices. Options - are 'euclidean', 'geodesic', or None. Default is 'euclidean'. - - :ivar coords: Cell pixel coordinates. - :vartype coords: numpy.ndarray - :ivar boundary_coords: Cell boundary coordinates. - :vartype boundary_coords: numpy.ndarray or None - :ivar coord_dmat: Distance matrix between all cell pixel coordinates. - :vartype coord_dmat: numpy.ndarray or None - :ivar boundary_coord_dmat: Distance matrix between boundary coordinates. - :vartype boundary_coord_dmat: numpy.ndarray or None - :ivar intensities: Channel intensity information. - :vartype intensities: dict - :ivar nucleus: Nuclear segmentation information. - :vartype nucleus: numpy.ndarray or None - :ivar size: Number of pixels in the cell. - :vartype size: int + :type intensities: dict or None + :param nucleus : Array of length N indicating nuclear identity (0 or 1) for each cell pixel. Default is None. + :type nucleus: numpy.ndarray or None + :param metric : Distance metric for computing coordinate distance matrices. Options are 'euclidean', 'geodesic', or None. Default is 'euclidean'. + :type metric: str or None """ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, metric='euclidean'): self.coords = coords diff --git a/src/cajal/subcellular_dl.py b/src/cajal/subcellular_dl.py index f76263d..aa1ce1e 100644 --- a/src/cajal/subcellular_dl.py +++ b/src/cajal/subcellular_dl.py @@ -1557,27 +1557,22 @@ def generate_dataset_split_pairs(indices, n_pairs, proportions=None, seed=None): random sampling across all cells and stratified sampling from predefined groups to ensure balanced representation across train/validation/test splits. - Parameters - ---------- - indices : list or array-like - List of available cell indices corresponding to processed cell images. - n_pairs : list of int - N-length list specifying number of pairs to generate for each dataset split. + :param indices: List of available cell indices corresponding to processed cell images. + :type indices: list or array-like + :param n_pairs: N-length list specifying number of pairs to generate for each dataset split. Typically [n_train_pairs, n_val_pairs, n_test_pairs]. - proportions : list of float, optional - N-length list of proportions that sum to 1.0 for stratified sampling. + :type n_pairs: list of int + :param proportions: N-length list of proportions that sum to 1.0 for stratified sampling. If provided, cell indices are split into N groups according to these proportions, and pairs are drawn only within each group. This ensures train/val/test sets use disjoint cell populations. If None, all pairs are drawn randomly from all available cells. Default is None. - seed : int, optional - Random seed for reproducible dataset splits. Default is None. - - Returns - ------- - list of numpy.ndarray - N-length list where each element is a 2D array of shape (n_pairs, 2) + :type proportions: list of float, optional + :param seed: Random seed for reproducible dataset splits. Default is None. + :type seed: int, optional + :returns: N-length list where each element is a 2D array of shape (n_pairs, 2) containing cell index pairs for each dataset split (train, val, test). + :rtype: list of numpy.ndarray """ if seed is not None: np.random.seed(seed) From 86e464530078375acf56341f856a52c84aad62a3 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Thu, 23 Oct 2025 20:28:54 +0000 Subject: [PATCH 11/14] Fixed docstrings for GW_OT_Cell, process_image --- src/cajal/subcellular.py | 42 ++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/cajal/subcellular.py b/src/cajal/subcellular.py index d83bf37..701625b 100644 --- a/src/cajal/subcellular.py +++ b/src/cajal/subcellular.py @@ -260,16 +260,16 @@ class GW_OT_Cell: for Gromov-Wasserstein distance computations and optimal transport analysis between cells. - :param coords : Array of shape (N, 2) containing (x, y) coordinates for each pixel in the cell. + :param coords: Array of shape (N, 2) containing (x, y) coordinates for each pixel in the cell. :type coords: numpy.ndarray - :param boundary_coords : Array of shape (M, 2) containing (x, y) coordinates sampled from the cell boundary. Default is None. + :param boundary_coords: Array of shape (M, 2) containing (x, y) coordinates sampled from the cell boundary. Default is None. :type boundary_coords: numpy.ndarray or None - :param intensities : Dictionary mapping channel names (str) to intensity arrays (numpy.ndarray) + :param intensities: Dictionary mapping channel names (str) to intensity arrays (numpy.ndarray) of length N, where N is the number of cell pixels. Default is None. :type intensities: dict or None - :param nucleus : Array of length N indicating nuclear identity (0 or 1) for each cell pixel. Default is None. + :param nucleus: Array of length N indicating nuclear identity (0 or 1) for each cell pixel. Default is None. :type nucleus: numpy.ndarray or None - :param metric : Distance metric for computing coordinate distance matrices. Options are 'euclidean', 'geodesic', or None. Default is 'euclidean'. + :param metric: Distance metric for computing coordinate distance matrices. Options are 'euclidean', 'geodesic', or None. Default is 'euclidean'. :type metric: str or None """ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, metric='euclidean'): @@ -319,38 +319,34 @@ def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_ masks to create a list of GW_OT_Cell objects suitable for Gromov-Wasserstein optimal transport analysis. - :param image : numpy.ndarray + :param image: numpy.ndarray 3D numpy array of shape (H, W, C) representing the multi-channel image. - :param channels : list of str + :param channels: list of str List of channel names corresponding to the last dimension of the image. - :param cell_mask_image : numpy.ndarray + :param cell_mask_image: numpy.ndarray 2D numpy array of shape (H, W) with integer labels for each cell (0 for background). - :param nucleus_mask_image : numpy.ndarray, optional + :param nucleus_mask_image: numpy.ndarray, optional 2D numpy array of shape (H, W) with integer labels for nuclei (0 for background). Default is None. - :param ds_factor : int, optional + :param ds_factor: int, optional Downsampling factor. If provided, downsample by this factor. Default is None. - :param ds_target_size : int, optional + :param ds_target_size: int, optional Target number of pixels per cell after downsampling. If provided, downsample to achieve this pixel count. Default is None. - :param filter_border_cells : bool, optional + :param filter_border_cells: bool, optional If True, exclude cells touching the image border. Default is True. - :param n_boundary_points : int, optional + :param n_boundary_points: int, optional Number of points to sample from the cell boundary. If None, boundary sampling is skipped. Default is 100. - :param save_path : str, optional + :param save_path: str, optional Directory path to save the processed cell objects as pickle files. If None, objects are not saved. Default is None. - :param return_objects : bool, optional + :param return_objects: bool, optional If True, return the list of GW_OT_Cell objects. Default is True. - - :return - ------- - list of GW_OT_Cell or None - If return_objects is True, returns a list of GW_OT_Cell objects; - otherwise returns None. + :return: If return_objects is True, returns a list of GW_OT_Cell objects; otherwise returns None. + :rtype: list[GW_OT_Cell] or None """ cell_inds = np.unique(cell_mask_image) cell_inds = cell_inds[cell_inds > 0] # Remove background (0) @@ -411,9 +407,9 @@ def _init_gw_pool(cell_objects: list, points: str): This function sets up shared state for multiprocessing workers computing pairwise Gromov-Wasserstein distances. - :param cell_objects : list + :param cell_objects: list List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. - :param points : str + :param points: str Type of points to use for distance computation. Either 'boundary' for boundary coordinates or 'full' for all cell coordinates. """ From 94bff4c323c88d557893025ba6cbe0ba1b4b5399 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Mon, 1 Jun 2026 15:23:43 +0000 Subject: [PATCH 12/14] Changed name to CellAligner, adjusted corresponding functions, documentation, and tutorials --- docs/notebooks/Example_6.ipynb | 148 ++++------ docs/notebooks/Example_7.ipynb | 45 +-- docs/subcellular.rst | 6 +- docs/subcellular_dl.rst | 6 +- src/cajal/subcellular.py | 217 +++++++------- src/cajal/subcellular_dl.py | 502 ++++++++++++++++++++++----------- 6 files changed, 529 insertions(+), 395 deletions(-) diff --git a/docs/notebooks/Example_6.ipynb b/docs/notebooks/Example_6.ipynb index 8c22b74..ccfbd4e 100644 --- a/docs/notebooks/Example_6.ipynb +++ b/docs/notebooks/Example_6.ipynb @@ -5,15 +5,7 @@ "id": "dca49492", "metadata": {}, "source": [ - "# Tutorial 6: Quantifying variation in subcellular protein localization (GW-OT)" - ] - }, - { - "cell_type": "markdown", - "id": "28d6ce35", - "metadata": {}, - "source": [ - "To demonstrate the functionality of GW-OT, we will perform an analysis on immunoflourescence data from the Human Protein Atlas. We will working with a small subset of 373 cells from 70 images, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/63tquyl5b6psiczrgihdn/hpa_images_metadata.zip?rlkey=7iz9cl5u35bvfupip6f0iicf3&st=ocpnazb7&dl=0)." + "# Tutorial 6: Quantifying variation in subcellular protein localization (CellAligner)" ] }, { @@ -21,9 +13,11 @@ "id": "c27d59b5", "metadata": {}, "source": [ + "To demonstrate the functionality of GW-OT, we will perform an analysis on immunoflourescence data from the Human Protein Atlas. We will working with a small subset of 373 cells from 70 images, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/63tquyl5b6psiczrgihdn/hpa_images_metadata.zip?rlkey=7iz9cl5u35bvfupip6f0iicf3&st=ocpnazb7&dl=0).\n", + "\n", "First, we will process the cell images, sample points from the cell boundary for morphological analysis and storing the subcellular protein information for localization analysis. We assume that cell segmentation has been performed on each image. Nuclear segmentation is optional, but can improve compartmental specificity in the localization analysis. \n", "\n", - "The processed `GW_OT cell` objects can be kept in memory for faster analysis, or be written to files in cases where avaliable memory is insufficient. All functions that take `GW_OT cell` objects as input, can also take in the paths to the saved `GW_OT cell` objects." + "The processed `CellAligner_Cell` objects can be kept in memory for faster analysis, or be written to files in cases where available memory is insufficient. All functions that take `CellAligner_Cell` objects as input, can also take in the paths to the saved `CellAligner_Cell` objects." ] }, { @@ -49,7 +43,7 @@ "outputs": [], "source": [ "# change to path to where data is located\n", - "data_path = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/'\n", + "data_path = '/path/to/data/'\n", "\n", "# load image metadata\n", "image_metadata = pd.read_csv(os.path.join(data_path, 'image_metadata.csv'), index_col=0)" @@ -476,7 +470,7 @@ "id": "a459d477", "metadata": {}, "source": [ - "After mapping all cells to the anchor cell, we compute the optimal transport (Wasserstein) distance to measure the difference in protein localization patterns between cells. Similar to the Gromov-Wasserstein morphology space, we can cluster cells based on the optimal transport localization space to identify groups of cells with similar protein localization patterns, and visualize the space with UMAP." + "After mapping all cells to the anchor cell, we can compute the optimal transport (Wasserstein) distance to measure the difference in protein localization patterns between cells. Similar to the Gromov-Wasserstein morphology space, we can cluster cells based on the optimal transport localization space to identify groups of cells with similar protein localization patterns, and visualize the space with UMAP." ] }, { @@ -699,6 +693,50 @@ " )" ] }, + { + "cell_type": "markdown", + "id": "86df85ac", + "metadata": {}, + "source": [ + "# Applying existing cell image analysis methods to mapped cells" + ] + }, + { + "cell_type": "markdown", + "id": "064876c8", + "metadata": {}, + "source": [ + "In this tutorial, we use optimal transport distances to quantify differences in subcellular protein localization after mapping each cell to the anchor cell morphology. However, you can apply any existing cell image analysis method (such as CellProfiler or Cytoself) to the mapped cells. To use external tools, we can the mapped cells as individual images for downstream processing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7de842c7", + "metadata": {}, + "outputs": [], + "source": [ + "output_dir = '/path/to/save/mapped/cell/images/'\n", + "cell_image_channels = ['nucleus', 'protein']\n", + "for i in range(len(cell_objects)):\n", + " # Create a cell object for the mapped cell\n", + " mapped_cell_object = cell_objects[anchor_cell_ind].copy()\n", + " for j, channel in enumerate(channels_to_map):\n", + " mapped_cell_object.intensities[channel] = mapped_distbs[j][i]\n", + " # Generate cell image from mapped cell object\n", + " mapped_cell_image = make_cell_image(mapped_cell_object, channels=cell_image_channels)\n", + " # Write mapped cell image to file\n", + " ski.io.imsave(os.path.join(output_dir, f'mapped_cell_image_{i}.tif'), mapped_cell_image)" + ] + }, + { + "cell_type": "markdown", + "id": "c37a71ec", + "metadata": {}, + "source": [ + "Note, the cell images generated by the `make_cell_image` function are multi-channel images where the first channel is the cell segmentation mask, and subsequent channels are specified by the `channels` parameter." + ] + }, { "cell_type": "markdown", "id": "8b7a9703", @@ -712,7 +750,7 @@ "id": "0930f006", "metadata": {}, "source": [ - "One characteristic of the GW-OT algorithm is that the OT localization space is dependent on the choice of anchor cell to map to. While in practice we've observed that choosing centroid cell based on the GW morphology space results in informative localization spaces, one may want their analysis to be more robust to the choice of anchor cell. To address this, we suggest mapping the protein distributions of each cell to multiple anchor cells, and integrating the resulting OT localization spaces. One natural way to select a set of anchor cells is to first cluster the GW morphology space and select the centroid cell of each morphological cluster, thus utilizing a broad range of cellular morphologies in constructing each localization space. Note, since this approach involves repeating the GW-based mapping and OT computations per each anchor cell, it will substatially increase the runtime of the analysis. " + "One characteristic of the CellAligner algorithm is that localization analysis following the anchor cell mapping can be dependent on the choice of anchor cell to map to. While in practice we've observed that choosing centroid cell based on the GW morphology space results in informative localization analyses, one may want their analysis to be more robust to the choice of anchor cell. To address this, we suggest mapping the protein distributions of each cell to multiple anchor cells, and integrating the resulting localization spaces. One natural way to select a set of anchor cells is to first cluster the GW morphology space and select the centroid cell of each morphological cluster, thus utilizing a broad range of cellular morphologies in constructing each localization space. Here we utilize OT to construct separate localization spaces for each anchor cell. Note, since this approach involves repeating the GW-based mapping and OT computations per each anchor cell, it will substatially increase the runtime of the analysis." ] }, { @@ -756,49 +794,7 @@ "execution_count": null, "id": "60e26557", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mapping cells to target cell:\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "373it [27:13, 4.38s/it] " - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Computing pairwise OT distances:\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "100%|█████████████████████████████████████████████████████████████████████████████| 69378/69378 [27:58<00:00, 41.32it/s]\n" - ] - }, - { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_293_mapped_distbs.pickle'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn [21], line 18\u001b[0m\n\u001b[1;32m 15\u001b[0m centroid_mapped_distbs\u001b[38;5;241m.\u001b[39mappend(mapped_distbs)\n\u001b[1;32m 16\u001b[0m centroid_ot_dmats\u001b[38;5;241m.\u001b[39mappend(ot_dmats)\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mtarget_cell_ind\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m_mapped_distbs.pickle\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 19\u001b[0m mapped_distbs \u001b[38;5;241m=\u001b[39m pickle\u001b[38;5;241m.\u001b[39mload(f)\n\u001b[1;32m 20\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mstr\u001b[39m(target_cell_ind) \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m_ot_dmats.pickle\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrb\u001b[39m\u001b[38;5;124m'\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n", - "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/IPython/core/interactiveshell.py:282\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 275\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 276\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 277\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 278\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 279\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 280\u001b[0m )\n\u001b[0;32m--> 282\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/anchor_293_mapped_distbs.pickle'" - ] - } - ], + "outputs": [], "source": [ "centroid_mapped_distbs = []\n", "centroid_ot_dmats = []\n", @@ -817,7 +813,7 @@ " centroid_mapped_distbs.append(mapped_distbs)\n", " centroid_ot_dmats.append(ot_dmats)\n", "\n", - " out_dir = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/'\n", + " out_dir = '/path/to/save/anchor/mapped/distances/'\n", " for target_cell_ind, mapped_distbs, ot_dmats in zip(cluster_centroids, centroid_mapped_distbs, centroid_ot_dmats):\n", " with open(os.path.join(out_dir, f'anchor_{target_cell_ind}_mapped_distbs.pickle'), 'wb') as f:\n", " pickle.dump(mapped_distbs, f, protocol=pickle.HIGHEST_PROTOCOL)\n", @@ -899,32 +895,6 @@ " color = [str(c) for c in cell_metadata['locations']]\n", " )" ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "9cd5977e", - "metadata": {}, - "outputs": [], - "source": [ - "# load\n", - "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/cell_objects.pickle', 'rb') as f:\n", - " cell_objects = pickle.load(f)\n", - "\n", - "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/gw_dmat.pickle', 'rb') as f:\n", - " gw_dmat = pickle.load(f)\n", - "\n", - "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/target_cell_ind.pickle', 'rb') as f:\n", - " target_cell_ind = pickle.load(f)\n", - "\n", - "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/mapped_distbs.pickle', 'rb') as f:\n", - " mapped_distbs = pickle.load(f)\n", - "\n", - "with open('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/ot_dmats.pickle', 'rb') as f:\n", - " ot_dmats = pickle.load(f)\n", - "\n", - "cell_metadata = pd.read_csv('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/test_analysis_2/cell_metadata.csv', index_col=0)" - ] } ], "metadata": { @@ -932,18 +902,6 @@ "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.6" } }, "nbformat": 4, diff --git a/docs/notebooks/Example_7.ipynb b/docs/notebooks/Example_7.ipynb index 48ba304..04ba9ea 100644 --- a/docs/notebooks/Example_7.ipynb +++ b/docs/notebooks/Example_7.ipynb @@ -5,7 +5,7 @@ "id": "65da9095", "metadata": {}, "source": [ - "# Tutorial 7: Quantifying subcellular protein localization in very large datasets (dGW-OT)" + "# Tutorial 7: Quantifying subcellular protein localization in very large datasets (dCellAligner-OT)" ] }, { @@ -13,9 +13,9 @@ "id": "1b6d513c", "metadata": {}, "source": [ - "The Fused Gromov-Wasserstein mapping between two cells with 1000 points takes around 3 s to compute, and the Wasserstein distance between the two mapped distributions takes aroud 18 ms. While number of Fused Gromov-Wasserstein mapping computations scales linearly with the number of cells, the number of Wasserstein computations of mapped distributions scales quadratically, which can result in very long runtimes in datasets with 100s of thousands of cells. For these large datasets, we've provided a deep learning framework, deep Gromow-Wasserstein Optimal Transport (dGW-OT) to reduce the necessary computation. This approach enables users to compute the GW-OT mappings and distances for only a subset of cells, and train a deep learning model to predict the mappings and distances for the remaining cells. \n", + "The Fused Gromov-Wasserstein mapping between two cells with 1000 points takes around 3 s to compute, and the optimal transport (OT) distance between the two mapped distributions takes aroud 18 ms. While number of Fused Gromov-Wasserstein mapping computations scales linearly with the number of cells, the number of Wasserstein computations of mapped distributions scales quadratically, which can result in very long runtimes in datasets with 100s of thousands of cells. For these large datasets, we've provided a deep learning framework, deep CellAligner Optimal Transport (dCellAligner-OT) to reduce the necessary computation. This approach enables users to compute the CellAligner mappings and OT distances for only a subset of cells, and train a deep learning model to predict the mappings and distances for the remaining cells. \n", "\n", - "We will demonstrate this approach on a dataset of 16,787 neurons with simulated subcellular protein distributions. For this analysis, we assume that the image data has already been processed into GW-OT cell objects, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/mb1wx32lfqiqpu3mkhni9/sim_neuron_cell_objects.zip?rlkey=113rcvxp1qgpp0wbih63phu5t&dl=0)." + "We will demonstrate this approach on a dataset of 16,787 neurons with simulated subcellular protein distributions. For this analysis, we assume that the image data has already been processed into CellAligner cell objects, which can be downloaded from this [link](https://www.dropbox.com/scl/fi/mb1wx32lfqiqpu3mkhni9/sim_neuron_cell_objects.zip?rlkey=113rcvxp1qgpp0wbih63phu5t&dl=0)." ] }, { @@ -33,7 +33,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_path = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/simulated_neurons/cell_objects' # path to saved cell objects\n", + "data_path = '/path/to/saved/cell/objects'\n", "cell_object_paths = [os.path.join(data_path, fname) for fname in os.listdir(data_path)]\n", "anchor_ind = 658 # index of anchor cell (which other cells are mapped to)\n", "anchor_cell_obj_path = cell_object_paths[anchor_ind]" @@ -46,13 +46,14 @@ "metadata": {}, "outputs": [], "source": [ - "cell_image_path = '/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/simulated_neurons'\n", - "make_NN_training_data(save_path=cell_image_path, # path to save cell images\n", + "cell_image_path = '/path/to/saved/cell/images'\n", + "make_NN_training_data(save_path=cell_image_path, # path to saved cell images\n", " cell_objects=cell_object_paths,\n", " reference_cell_object=anchor_cell_obj_path,\n", " mapped_channel_distributions=mapped_distbs[0], # using the mapped protein distribution\n", " channel='protein', # this should match the mapped distributions used\n", " center='nucleus', # center='cell' when using Fused GW mappings, center='nucleus' when using Unbalanced Fused GW mappings\n", + " shape=(256,256), # shape of the output cell images\n", " rescale=False) # rescale=True when using Fused GW mappings, rescale=False when using Unbalanced Fused GW mappings" ] }, @@ -61,7 +62,7 @@ "id": "1b03bd80", "metadata": {}, "source": [ - "To avoid the model overfitting to the training dataset, we split our data into a training, validation, and test set. Since, the dGW-OT model does not need to trained on every pair of training cells, the Wasserstein distances between the mapped protein distributions are only computed for a subset of pairs. In practice, we've observed good model performance when training on around 10,000 cells and 30,000 cell pairs. " + "To avoid the model overfitting to the training dataset, we split our data into a training, validation, and test set. Since, the dCellAligner model does not need to trained on every pair of training cells, the OT distances between the mapped protein distributions are only computed for a subset of pairs. In practice, we've observed good model performance when training on around 10,000 cells and 30,000 cell pairs. " ] }, { @@ -131,9 +132,9 @@ "id": "112f9ac4", "metadata": {}, "source": [ - "We initialize the dGW-OT model and begin the two-stage training process. First, during pretraining, the model learns the Fused (Unbalanced) Gromov-Wasserstein mapping operation. More specifically, for each cell, the model learns to predict the subcellular protein distribution after mapping to the anchor cell.\n", + "We initialize the dCellAligner-OT model and begin the two-stage training process. First, during pretraining, the model learns the Fused (Unbalanced) Gromov-Wasserstein mapping operation. More specifically, for each cell, the model learns to predict the subcellular protein distribution after mapping to the anchor cell.\n", "\n", - "The dGW-OT model pretraining took around 24 hours running on a Nvidia RTX 4500 Ada." + "The dCellAligner-OT model pretraining took around 24 hours running on a Nvidia RTX 4500 Ada." ] }, { @@ -848,9 +849,9 @@ } ], "source": [ - "model = dGWOTNetwork(embedding_size=50, image_size=image_shape[0])\n", - "model = pretrain_model(train_data, model, batch_size=8, epochs=50, lr=1e-3, device='cuda')\n", - "torch.save(model.state_dict(), \"/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/pretrained_sim_neuron.pth\")" + "model = dCellAlignerNetwork(embedding_size=50, image_size=image_shape[0])\n", + "model = pretrain_model(train_data, model, dataset_name='sim_neuron', batch_size=8, epochs=50, lr=1e-3, \n", + " device='cuda', save_path='/path/to/save/pretrained/model/', return_model=True)" ] }, { @@ -858,13 +859,13 @@ "id": "bd50f6b6", "metadata": {}, "source": [ - "Next, during training, the model learns to extract features that preserve the GW-OT distances between cells in the feature space, in addition to predicting the mapped protein distributions.\n", + "Next, during training, the model learns to extract features that preserve the CellAligner-OT distances between cells in the feature space, in addition to predicting the mapped protein distributions.\n", "\n", - "The model is optimized with respect to two main loss components. The distance loss measures how well the the GW-OT distances are preserved in the model's latent feature space, while the reconstruction loss measures accuractely the model predicts the mapped protein distributions. The `dist_weight` parameter controls the relative weighting of these loss components during training. Ideally, the relatively contribution of both losses, which can be viewed by setting `show_loss_components = True`, should be around the same order of magnitude.\n", + "The model is optimized with respect to two main loss components. The distance loss measures how well the the CellAligner-OT distances are preserved in the model's latent feature space, while the reconstruction loss measures accurately the model predicts the mapped protein distributions. The `dist_weight` parameter controls the relative weighting of these loss components during training. Ideally, the relatively contribution of both losses, which can be viewed by setting `show_loss_components = True`, should be around the same order of magnitude.\n", "\n", "To avoid overfitting, we apply L1 regularization (adjusted by `weight_decay`) and L2 regularization (adjusted by `sparsity_weight`, and `sparsity_target`). If the dGW-OT model is overfitting, you could experiment with increasing the `weight_decay` and `sparsity_weight`, or decreasing `sparsity_target`, to further regularize the model to resolve the issue.\n", "\n", - "The dGW-OT model training took around 48 hours running on a Nvidia RTX 4500 Ada." + "The dCellAligner-OT model training took around 48 hours running on a Nvidia RTX 4500 Ada." ] }, { @@ -1273,9 +1274,9 @@ ], "source": [ "# Train the DGWOTE model with the prepared datasets\n", - "models, train_losses, val_losses = train_dGWOT(\n", + "models, train_losses, val_losses = train_dCellAligner(\n", " train_data, val_data, test_data,\n", - " save_path='/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/',\n", + " save_path='/path/to/save/fully/trained/model/',\n", " dataset_name='sim_neuron',\n", " embedding_size=50, # 50-dimensional embeddings\n", " image_shape=(256, 256), # Input image shape\n", @@ -1289,7 +1290,7 @@ " lr_gamma=0.95, # Learning rate decay factor\n", " sparsity_weight=1e-3, # Sparsity weight for the embedding loss\n", " sparsity_target=0.1, # Target sparsity for the embedding loss\n", - " pretrained_path=\"/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/pretrained_sim_neuron.pth\"\n", + " pretrained_path=\"/path/to/save/pretrained/model/sim_neuron_pretrained_best.pth\"\n", ")" ] }, @@ -1298,7 +1299,7 @@ "id": "5a5a11d0", "metadata": {}, "source": [ - "The `train_dGWOT` function saves two versions of the model, the best performing model based on performance on validation dataset (`_best.pth`), and the final model after all training epochs (`_final.pth`). Here, we load the best model based on validation loss." + "The `train_dCellAligner` function saves two versions of the model, the best performing model based on performance on validation dataset (`_best.pth`), and the final model after all training epochs (`_final.pth`). Here, we load the best model based on validation loss." ] }, { @@ -1328,7 +1329,7 @@ ], "source": [ "# load best model\n", - "model = load_dGWOT_model('/home/jovyan/e/rkhu/Projects/CAJAL_spatial/data/package_dev/dgwote/models/sim_neuron_best.pth')" + "model = load_dCellAligner_model('/path/to/save/fully/trained/model/sim_neuron_best.pth')" ] }, { @@ -1336,7 +1337,7 @@ "id": "fefc2421", "metadata": {}, "source": [ - "To evaluate model performance, we can look at how well the true GW-OT distances are preserved in the dGW-OT feature space. We can also look at how well the model predicted the mapped protein distribution." + "To evaluate model performance, we can look at how well the true CellAligner-OT distances are preserved in the dCellAligner-OT feature space. We can also look at how well the model predicted the mapped protein distribution." ] }, { @@ -1421,7 +1422,7 @@ "id": "8f88d0ee", "metadata": {}, "source": [ - "Finally, we can use the model to extract features that capture variation in subcellular protein localization for datasets of arbitrary size without any additional training or Wasserstein computations. We can perform the same analyses with this feature space as we would with the GW-OT localization space including clustering, visualization, etc." + "Finally, we can use the model to extract features that capture variation in subcellular protein localization for datasets of arbitrary size without any additional training or Wasserstein computations. We can perform the same analyses with this feature space as we would with the CellAligner-OT localization space including clustering, visualization, etc." ] }, { diff --git a/docs/subcellular.rst b/docs/subcellular.rst index 2178ced..2039a1a 100644 --- a/docs/subcellular.rst +++ b/docs/subcellular.rst @@ -1,10 +1,10 @@ Subcellular Protein Localization =================================================== -.. autoclass:: cajal.subcellular.GW_OT_Cell +.. autoclass:: cajal.subcellular.CellAligner_Cell .. autofunction:: cajal.subcellular.process_image .. autofunction:: cajal.subcellular.gw_pairwise_parallel -.. autofunction:: cajal.subcellular.map_to_cell -.. autofunction:: cajal.subcellular.map_to_cell_parallel +.. autofunction:: cajal.subcellular.map_cell_to_cell +.. autofunction:: cajal.subcellular.map_to_anchor_cell .. autofunction:: cajal.subcellular.gw_mapped_ot_pairwise_parallel .. autofunction:: cajal.subcellular.find_centroid .. autofunction:: cajal.subcellular.plot_cell_image \ No newline at end of file diff --git a/docs/subcellular_dl.rst b/docs/subcellular_dl.rst index b2b909d..9d24a2a 100644 --- a/docs/subcellular_dl.rst +++ b/docs/subcellular_dl.rst @@ -1,13 +1,13 @@ Subcellular Protein Localization Deep Learning =================================================== .. autofunction:: cajal.subcellular_dl.make_NN_training_data -.. autoclass:: cajal.subcellular_dl.dGWOTNetwork +.. autoclass:: cajal.subcellular_dl.dCellAlignerNetwork .. autoclass:: cajal.subcellular_dl.PairedDataset .. autoclass:: cajal.subcellular_dl.RandomHorizontalRescale .. autofunction:: cajal.subcellular_dl.generate_dataset_split_pairs .. autofunction:: cajal.subcellular_dl.pretrain_model -.. autofunction:: cajal.subcellular_dl.train_dGWOT -.. autofunction:: cajal.subcellular_dl.load_dGWOT_model +.. autofunction:: cajal.subcellular_dl.train_dCellAligner +.. autofunction:: cajal.subcellular_dl.load_dCellAligner_model .. autofunction:: cajal.subcellular_dl.extract_embeddings .. autofunction:: cajal.subcellular_dl.predict_distances .. autofunction:: cajal.subcellular_dl.plot_distance_predictions diff --git a/src/cajal/subcellular.py b/src/cajal/subcellular.py index 701625b..23632a5 100644 --- a/src/cajal/subcellular.py +++ b/src/cajal/subcellular.py @@ -17,29 +17,31 @@ from .gw_cython import gw_cython_core -def make_cell_image(gw_ot_cell, channels): - """Create a multi-channel image array from a GW_OT_Cell object. +def make_cell_image(cellaligner_cell, channels, make_square=False): + """Create a multi-channel image array from a CellAligner_Cell object. - This function generates a 3D image array where the first channel contains - the cell segmentation mask and subsequent channels contain normalized - intensity values for the specified channels. + This function generates a multi-channel image array where the first + channel contains the cell segmentation mask and subsequent channels + contain normalized intensity values for the specified channels. - :param gw_ot_cell: Either a GW_OT_Cell object or a path to a pickled GW_OT_Cell object. - :type gw_ot_cell: GW_OT_Cell or str + :param cellaligner_cell: Either a CellAligner_Cell object or a path to a pickled CellAligner_Cell object. + :type cellaligner_cell: CellAligner_Cell or str :param channels: List of channel names to include in the image. Each channel should - correspond to a key in the GW_OT_Cell's intensities dictionary or + correspond to a key in the CellAligner_Cell's intensities dictionary or be 'nucleus' for nuclear segmentation. :type channels: list[str] + :param make_square: If True, pad the image to a square shape before returning. + :type make_square: bool :return: 3D array of shape (height, width, len(channels)+1) where the first channel (index 0) contains the segmentation mask and subsequent channels contain normalized intensity values (0-1 range). :rtype: numpy.ndarray """ - # Load GW_OT_Cell object if path specified - if isinstance(gw_ot_cell, str): - with open(gw_ot_cell, 'rb') as file: - gw_ot_cell = pickle.load(file) - coords = gw_ot_cell.coords + # Load CellAligner_Cell object if path specified + if isinstance(cellaligner_cell, str): + with open(cellaligner_cell, 'rb') as file: + cellaligner_cell = pickle.load(file) + coords = cellaligner_cell.coords coords[:,0] = coords[:,0] - coords[:,0].min() coords[:,1] = coords[:,1] - coords[:,1].min() # make new (n_channel + 1) x cell_width x cell_len image array @@ -50,12 +52,15 @@ def make_cell_image(gw_ot_cell, channels): for channel_i in range(len(channels)): # store channel pixel intensities channel = channels[channel_i] if channel == 'nucleus': - cell_image[i,j,channel_i+1] = gw_ot_cell.nucleus[coord_i] + cell_image[i,j,channel_i+1] = cellaligner_cell.nucleus[coord_i] else: - cell_image[i,j,channel_i+1] = gw_ot_cell.intensities[channel][coord_i] + cell_image[i,j,channel_i+1] = cellaligner_cell.intensities[channel][coord_i] for channel_i in range(len(channels)): cell_image[:,:,channel_i+1] -= cell_image[:,:,channel_i+1].min() cell_image[:,:,channel_i+1] /= cell_image[:,:,channel_i+1].max() + if make_square: + max_dim = max(cell_image.shape[0], cell_image.shape[1]) + cell_image = to_shape(cell_image, (max_dim, max_dim, cell_image.shape[2])) return(cell_image) @@ -117,7 +122,7 @@ def make_cell_image_for_plot(image, mask_alpha=0.2): return(im) -def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha=0.2): +def plot_cell_image(cellaligner_cell, channels, make_square=True, ax=None, mask_alpha=0.2): """Plot a cell image with the specified channels as an RGB composite. Creates a visualization of cell image data by combining multiple channels @@ -141,20 +146,17 @@ def plot_cell_image(gw_ot_cell, channels, make_square=True, ax=None, mask_alpha= :type n_boundary_points: int or None :param save_path: Directory path to save the processed cell objects as pickle files. If None, objects are not saved. Default is None. :type save_path: str or None - :param return_objects: If True, return the list of GW_OT_Cell objects. Default is True. + :param return_objects: If True, return the list of CellAligner_Cell objects. Default is True. :type return_objects: bool :return: - If return_objects is True, returns a list of GW_OT_Cell objects; otherwise returns None. - :rtype: list[GW_OT_Cell] or None + If return_objects is True, returns a list of CellAligner_Cell objects; otherwise returns None. + :rtype: list[CellAligner_Cell] or None """ if len(channels) > 3: raise ValueError("Only up to 3 channels can be plotted.") - image = make_cell_image(gw_ot_cell, channels) + image = make_cell_image(cellaligner_cell, channels, make_square=make_square) image = make_cell_image_for_plot(image, mask_alpha=mask_alpha) - if make_square: - max_dim = max(image.shape[0], image.shape[1]) - image = to_shape(image, (max_dim, max_dim, image.shape[2])) if ax: return(ax.imshow(image)) else: @@ -253,12 +255,11 @@ def compute_geodesic_dmat(mask_coords): -class GW_OT_Cell: - """A cell representation for Gromov-Wasserstein Optimal Transport analysis. +class CellAligner_Cell: + """A cell representation for CellAligner analysis. This class encapsulates cell morphology and intensity information needed - for Gromov-Wasserstein distance computations and optimal transport analysis - between cells. + for Gromov-Wasserstein mapping and distance computations between cells. :param coords: Array of shape (N, 2) containing (x, y) coordinates for each pixel in the cell. :type coords: numpy.ndarray @@ -280,7 +281,7 @@ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, self.boundary_coord_dmat = None elif metric == 'geodesic': self.coord_dmat = compute_geodesic_dmat(self.coords) - self.boundary_coord_dmat = squareform(pdist(boundary_coords, metric=metric)) if boundary_coords is not None else None + self.boundary_coord_dmat = squareform(pdist(boundary_coords, metric='euclidean')) if boundary_coords is not None else None # if boundary_coords is not None: # warnings.warn("Geodesic distance matrix cannot be computed for cell boundary coordinates, ignoring.") else: @@ -291,15 +292,15 @@ def __init__(self, coords, boundary_coords=None, intensities=None, nucleus=None, self.size = len(coords) def copy(self): - """Return a deep copy of the GW_OT_Cell instance. + """Return a deep copy of the CellAligner_Cell instance. Returns ------- - GW_OT_Cell - A new GW_OT_Cell instance with copied data from the current object. + CellAligner_Cell + A new CellAligner_Cell instance with copied data from the current object. All arrays and dictionaries are deep-copied to avoid shared references. """ - obj_copy = GW_OT_Cell( + obj_copy = CellAligner_Cell( coords=self.coords.copy(), boundary_coords=self.boundary_coords.copy() if self.boundary_coords is not None else None, intensities=copy.deepcopy(self.intensities), @@ -311,13 +312,16 @@ def copy(self): return obj_copy +# Backward compatibility for pickle files created before the class rename. +GW_OT_Cell = CellAligner_Cell + + def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_factor=None, ds_target_size=None, filter_border_cells=True, n_boundary_points=100, save_path=None, return_objects=True): - """Create GW_OT_Cell objects from segmented microscopy images. + """Create CellAligner_Cell objects from segmented microscopy images. Processes a multi-channel microscopy image with cell and nuclear segmentation - masks to create a list of GW_OT_Cell objects suitable for Gromov-Wasserstein - optimal transport analysis. + masks to create a list of CellAligner_Cell objects suitable for CellAligner analysis. :param image: numpy.ndarray 3D numpy array of shape (H, W, C) representing the multi-channel image. @@ -344,14 +348,14 @@ def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_ Directory path to save the processed cell objects as pickle files. If None, objects are not saved. Default is None. :param return_objects: bool, optional - If True, return the list of GW_OT_Cell objects. Default is True. - :return: If return_objects is True, returns a list of GW_OT_Cell objects; otherwise returns None. - :rtype: list[GW_OT_Cell] or None + If True, return the list of CellAligner_Cell objects. Default is True. + :return: If return_objects is True, returns a list of CellAligner_Cell objects; otherwise returns None. + :rtype: list[CellAligner_Cell] or None """ cell_inds = np.unique(cell_mask_image) cell_inds = cell_inds[cell_inds > 0] # Remove background (0) - gw_ot_cells = [] + cellaligner_cells = [] for cell_ind in cell_inds: cell_mask = (cell_mask_image == cell_ind).astype(np.uint8) nuc_mask = (nucleus_mask_image == cell_ind).astype(np.uint8) if nucleus_mask_image is not None else None @@ -361,13 +365,13 @@ def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_ continue # Downsample image and masks if necessary if ds_factor is not None: # downsample by a factor - image_ds = ski.transform.resize(image, (image.shape[0] // ds_factor, image.shape[1] // ds_factor, image.shape[2]), order=1, preserve_range=True).astype(np.uint8) + image_ds = ski.transform.resize(image, (image.shape[0] // ds_factor, image.shape[1] // ds_factor, image.shape[2]), order=1, preserve_range=True) cell_mask_ds = ski.transform.resize(cell_mask, (cell_mask.shape[0] // ds_factor, cell_mask.shape[1] // ds_factor), order=0, anti_aliasing=False, preserve_range=True).astype(np.uint8) nuc_mask_ds = ski.transform.resize(nuc_mask, (nuc_mask.shape[0] // ds_factor, nuc_mask.shape[1] // ds_factor), order=0, anti_aliasing=False, preserve_range=True).astype(np.uint8) if nuc_mask is not None else None elif ds_target_size is not None: # downsample to a target size cell_mask_ds = rescale_mask_to_pixel_count(cell_mask, target_pixels=ds_target_size) nuc_mask_ds = ski.transform.resize(nuc_mask, (cell_mask_ds.shape[0], cell_mask_ds.shape[1]), order=0, anti_aliasing=False, preserve_range=True).astype(np.uint8) if nuc_mask is not None else None - image_ds = ski.transform.resize(image, (cell_mask_ds.shape[0], cell_mask_ds.shape[1], image.shape[2]), order=1, preserve_range=True).astype(np.uint8) + image_ds = ski.transform.resize(image, (cell_mask_ds.shape[0], cell_mask_ds.shape[1], image.shape[2]), order=1, preserve_range=True) else: image_ds = image cell_mask_ds = cell_mask @@ -378,25 +382,25 @@ def process_image(image, channels, cell_mask_image, nucleus_mask_image=None, ds_ if n_boundary_points is not None: _, cell_boundary_pts = cell_boundaries(np.pad(cell_mask, 1), n_sample=n_boundary_points)[0] # pad to avoid border issues cell_boundary_pts = cell_boundary_pts - 1 # remove padding - gw_ot_cell = GW_OT_Cell(coords=np.array(np.where(cell_mask_ds)).T, boundary_coords=cell_boundary_pts) + cellaligner_cell = CellAligner_Cell(coords=np.array(np.where(cell_mask_ds)).T, boundary_coords=cell_boundary_pts) if nucleus_mask_image is not None: - gw_ot_cell.nucleus = nuc_mask_ds[np.where(cell_mask_ds)] - if gw_ot_cell.nucleus.sum() == 0: # filter cells without segmented nuclei + cellaligner_cell.nucleus = nuc_mask_ds[np.where(cell_mask_ds)] + if cellaligner_cell.nucleus.sum() == 0: # filter cells without segmented nuclei continue for channel in channels: - gw_ot_cell.intensities[channel] = image_ds[np.where(cell_mask_ds)][:,channels.index(channel)] + cellaligner_cell.intensities[channel] = image_ds[np.where(cell_mask_ds)][:,channels.index(channel)] # Normalize the channels (to sum to 1) for channel in channels: - gw_ot_cell.intensities[channel] = gw_ot_cell.intensities[channel] / np.sum(gw_ot_cell.intensities[channel]) + cellaligner_cell.intensities[channel] = cellaligner_cell.intensities[channel] / np.sum(cellaligner_cell.intensities[channel]) if return_objects: - gw_ot_cells.append(gw_ot_cell) + cellaligner_cells.append(cellaligner_cell) if save_path is not None: if not os.path.isdir(save_path): # Create directory if it doesn't exist os.makedirs(save_path) with open(os.path.join(save_path, 'cell_'+str(cell_ind).zfill(4)+'.pickle'), 'wb') as file: - pickle.dump(gw_ot_cell, file) + pickle.dump(cellaligner_cell, file) if return_objects: - return gw_ot_cells + return cellaligner_cells else: return None @@ -408,12 +412,12 @@ def _init_gw_pool(cell_objects: list, points: str): pairwise Gromov-Wasserstein distances. :param cell_objects: list - List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. + List of CellAligner_Cell objects or paths to pickled CellAligner_Cell objects. :param points: str Type of points to use for distance computation. Either 'boundary' for boundary coordinates or 'full' for all cell coordinates. """ - # list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects + # list of CellAligner_Cell objects or list of paths to CellAligner_Cell objects global _CELL_OBJECTS _CELL_OBJECTS = cell_objects # set of points to use for distance computation ('boundary' or 'full') @@ -430,7 +434,7 @@ def _gw_index(p: tuple[int, int]): :rtype: tuple[int, int, numpy.ndarray, float] """ i, j = p - # load GW_OT_Cell objects if path specified + # load CellAligner_Cell objects if path specified if isinstance(_CELL_OBJECTS[i], str): _CELL_OBJECTS[i] = pickle.load(open(_CELL_OBJECTS[i], 'rb')) if isinstance(_CELL_OBJECTS[j], str): @@ -441,6 +445,8 @@ def _gw_index(p: tuple[int, int]): elif _POINTS == 'full': A = _CELL_OBJECTS[i].coord_dmat B = _CELL_OBJECTS[j].coord_dmat + else: + raise ValueError("Invalid value for _POINTS. Must be 'boundary' or 'full'.") n_A = A.shape[0] n_B = B.shape[0] a = np.repeat(1/n_A, n_A) @@ -469,7 +475,7 @@ def gw_pairwise_parallel(cell_objects, points='boundary', num_processes=4, chunk Calculates the Gromov-Wasserstein distance matrix for a colloection of cells using either exact computation or traiangle inequality approximation with anchors. - :param cell_objects: list of GW_OT_Cell objects or file paths + :param cell_objects: list of CellAligner_Cell objects or file paths :type cell_objects: list :param points: 'boundary' or 'full' (default: 'boundary') :type points: str @@ -539,7 +545,7 @@ def _init_fgw_map_pool(cell_objects: list, channels: list, compartment_specific: nuclear_fraction: float = 0.2): """Initialize global state for parallel fused GW mapping workers. - :param cell_objects: list of GW_OT_Cell objects or paths + :param cell_objects: list of CellAligner_Cell objects or paths :type cell_objects: list :param channels: channel names to compute OT for :type channels: list[str] @@ -559,7 +565,7 @@ def _init_fgw_map_pool(cell_objects: list, channels: list, compartment_specific: :type nuclear_fraction: float """ global _CELL_OBJECTS # - _CELL_OBJECTS = cell_objects # list of GW_OT_Cell objects or list of paths to GW_OT_Cell objects + _CELL_OBJECTS = cell_objects # list of CellAligner_Cell objects or list of paths to CellAligner_Cell objects global _CHANNELS _CHANNELS = channels # which channels to compute protein OT for global _COMPARTMENT_SPECIFIC @@ -587,7 +593,7 @@ def _fgw_map_index(p: tuple[int, int]): :rtype: tuple[int, int, float, numpy.ndarray] """ i, j = p - # load GW_OT_Cell objects if path specified + # load CellAligner_Cell objects if path specified if isinstance(_CELL_OBJECTS[i], str): _CELL_OBJECTS[i] = pickle.load(open(_CELL_OBJECTS[i], 'rb')) if isinstance(_CELL_OBJECTS[j], str): @@ -637,6 +643,8 @@ def _fgw_map_index(p: tuple[int, int]): coupling_mat, coupling_mat_2, log = ot.gromov.fused_unbalanced_gromov_wasserstein(M=cost_matrix, Cx=A, Cy=B, wx=a, wy=b, alpha=alpha, reg_marginals=rho, max_iter=20, log=True) gw_dist = log['fugw_cost'] + else: + raise ValueError("Invalid value for _METHOD. Must be 'fused' or 'fused_unbalanced'.") if _COMPARTMENT_SPECIFIC: # find nuclear pixels after mapping @@ -691,14 +699,14 @@ def _fgw_map_index(p: tuple[int, int]): return (i, j, gw_dist, mapped_distbs) -def map_to_cell(cell_object_from, cell_object_to, channels, compartment_specific=True, method='fused', +def map_cell_to_cell(cell_object_from, cell_object_to, channels, compartment_specific=True, method='fused', fused_channel='protein', fused_cost=10, fused_param=0.1, unbalanced_param=70, nuclear_fraction=0.2): """Map protein distributions from one cell onto another via Fused Gromov-Wasserstein. - :param cell_object_from: source ``GW_OT_Cell`` - :type cell_object_from: GW_OT_Cell - :param cell_object_to: target ``GW_OT_Cell`` - :type cell_object_to: GW_OT_Cell + :param cell_object_from: source ``CellAligner_Cell`` + :type cell_object_from: CellAligner_Cell + :param cell_object_to: target ``CellAligner_Cell`` + :type cell_object_to: CellAligner_Cell :param channels: list of channel names to map :type channels: list[str] :param compartment_specific: whether to use compartment-specific mapping @@ -718,7 +726,7 @@ def map_to_cell(cell_object_from, cell_object_to, channels, compartment_specific :return: mapped distributions with shape (len(channels), n_target_pixels) :rtype: numpy.ndarray """ - mapped_distbs = map_to_cell_parallel( + mapped_distbs = map_to_anchor_cell( cell_objects=[cell_object_from, cell_object_to], channels=channels, target_cell_ind=1, @@ -729,17 +737,18 @@ def map_to_cell(cell_object_from, cell_object_to, channels, compartment_specific fused_param=fused_param, unbalanced_param=unbalanced_param, nuclear_fraction=nuclear_fraction, - parallel=False + # parallel=False ) return mapped_distbs[:,0,:] -def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_specific=True, method='fused', - fused_channel='protein', fused_cost=10, fused_param=0.1, unbalanced_param=70, parallel=True, - nuclear_fraction=0.2, num_processes=4, chunksize=20): +def map_to_anchor_cell(cell_objects, channels, target_cell_ind, compartment_specific=True, method='fused', + fused_channel='protein', fused_cost=10, fused_param=0.1, unbalanced_param=70, nuclear_fraction=0.2, + # parallel=True, num_processes=4, chunksize=20 + ): """Map protein distributions from all cells to a single target cell. - :param cell_objects: list of GW_OT_Cell objects or file paths + :param cell_objects: list of CellAligner_Cell objects or file paths :type cell_objects: list :param channels: channel names to map :type channels: list[str] @@ -757,55 +766,57 @@ def map_to_cell_parallel(cell_objects, channels, target_cell_ind, compartment_sp :type fused_param: float :param unbalanced_param: regularization for unbalanced GW :type unbalanced_param: float + :param nuclear_fraction: probablistic fraction considered nuclear for compartment-specific mapping (should roughly correspond to fraction of nuclear pixels) + :type nuclear_fraction: float :param parallel: whether to run in parallel - :type parallel: bool - :param num_processes: number of processes for parallel execution - :type num_processes: int - :param chunksize: chunk size for parallel map - :type chunksize: int - :return: array of mapped distributions with shape (len(channels), N, n_target_pixels) - :rtype: numpy.ndarray """ + # :type parallel: bool + # :param num_processes: number of processes for parallel execution + # :type num_processes: int + # :param chunksize: chunk size for parallel map + # :type chunksize: int + # :return: array of mapped distributions with shape (len(channels), N, n_target_pixels) + # :rtype: numpy.ndarray print('Mapping cells to target cell:') N = len(cell_objects) index_pairs = [(i, target_cell_ind) for i in range(N)] total_num_pairs = N - 1 - if parallel: - # Parallelized - with Pool( - initializer=_init_fgw_map_pool, initargs=(cell_objects, channels, compartment_specific, method, - fused_channel, fused_cost, fused_param, unbalanced_param, nuclear_fraction), - processes=num_processes - ) as pool: - res = pool.imap_unordered(_fgw_map_index, index_pairs, chunksize=chunksize) - target_cell_object = cell_objects[target_cell_ind] - # load target GW_OT_Cell object if path specified - if isinstance(target_cell_object, str): - target_cell_object = pickle.load(open(target_cell_object, 'rb')) - mapped_distbs = np.zeros((len(channels),N,target_cell_object.coord_dmat.shape[0])) - for i, j, gw_dist, mapped_distb in tqdm(res, total=total_num_pairs, position=0, leave=True): - mapped_distbs[:,i,:] = mapped_distb - else: - # Non-parallelized - _init_fgw_map_pool(cell_objects, channels, compartment_specific, method, fused_channel, fused_cost, - fused_param, unbalanced_param, nuclear_fraction) - mapped_distbs = np.zeros((len(channels),N,cell_objects[target_cell_ind].coord_dmat.shape[0])) - for p in tqdm(index_pairs): - i, j, gw_dist, mapped_distb = _fgw_map_index(p) - mapped_distbs[:,i,:] = mapped_distb + # if parallel: + # # Parallelized + # with Pool( + # initializer=_init_fgw_map_pool, initargs=(cell_objects, channels, compartment_specific, method, + # fused_channel, fused_cost, fused_param, unbalanced_param, nuclear_fraction), + # processes=num_processes + # ) as pool: + # res = pool.imap_unordered(_fgw_map_index, index_pairs, chunksize=chunksize) + # target_cell_object = cell_objects[target_cell_ind] + # # load target CellAligner_Cell object if path specified + # if isinstance(target_cell_object, str): + # target_cell_object = pickle.load(open(target_cell_object, 'rb')) + # mapped_distbs = np.zeros((len(channels),N,target_cell_object.coord_dmat.shape[0])) + # for i, j, gw_dist, mapped_distb in tqdm(res, total=total_num_pairs, position=0, leave=True): + # mapped_distbs[:,i,:] = mapped_distb + # else: + # Non-parallelized + _init_fgw_map_pool(cell_objects, channels, compartment_specific, method, fused_channel, fused_cost, + fused_param, unbalanced_param, nuclear_fraction) + mapped_distbs = np.zeros((len(channels),N,cell_objects[target_cell_ind].coord_dmat.shape[0])) + for p in tqdm(index_pairs): + i, j, gw_dist, mapped_distb = _fgw_map_index(p) + mapped_distbs[:,i,:] = mapped_distb return mapped_distbs -def _init_gw_mapped_ot_pool(cell_object: GW_OT_Cell, mapped_cell_dists: np.ndarray): +def _init_gw_mapped_ot_pool(cell_object: CellAligner_Cell, mapped_cell_dists: np.ndarray): """Initialize global state for OT computation on mapped distributions. - :param cell_object: target ``GW_OT_Cell`` containing coordinate distance matrix - :type cell_object: GW_OT_Cell + :param cell_object: target ``CellAligner_Cell`` containing coordinate distance matrix + :type cell_object: CellAligner_Cell :param mapped_cell_dists: mapped distributions array (n_channels, N, n_target_pixels) :type mapped_cell_dists: numpy.ndarray """ global _CELL_OBJECT - _CELL_OBJECT = cell_object # GW_OT_Cell object + _CELL_OBJECT = cell_object # CellAligner_Cell object global _MAPPED_CELL_DISTS _MAPPED_CELL_DISTS = mapped_cell_dists # numpy array storing mapped cell protein distributions @@ -842,8 +853,8 @@ def gw_mapped_ot_pairwise_parallel(cell_object, mapped_cell_dists, num_processes Calculates pairwise optimal transport distances for protein distribution after mapping to a common cell morphology. - :param cell_object: target ``GW_OT_Cell`` or path to pickled object - :type cell_object: GW_OT_Cell or str + :param cell_object: target ``CellAligner_Cell`` or path to pickled object + :type cell_object: CellAligner_Cell or str :param mapped_cell_dists: array of mapped distributions (len(channels), N, n_target_pixels) :type mapped_cell_dists: numpy.ndarray :param num_processes: number of processes for parallel execution diff --git a/src/cajal/subcellular_dl.py b/src/cajal/subcellular_dl.py index aa1ce1e..ffb35ed 100644 --- a/src/cajal/subcellular_dl.py +++ b/src/cajal/subcellular_dl.py @@ -13,9 +13,123 @@ from torch.utils.data import Dataset, DataLoader from tqdm import tqdm import pickle - +import json from .subcellular import make_cell_image, to_shape + +CELL_IMAGE_PROCESSING_FILENAME = 'cell_image_processing.json' + + +def _write_cell_image_processing_info(save_path, max_size, rescale, shape, center): + processing_info = { + 'version': 1, + 'max_size': None if max_size is None else int(max_size), + 'rescale': bool(rescale), + 'shape': [int(shape[0]), int(shape[1])], + 'center': center, + } + with open(os.path.join(save_path, CELL_IMAGE_PROCESSING_FILENAME), 'w', encoding='utf-8') as file: + json.dump(processing_info, file, indent=2) + + +def _load_cell_image_processing_info(processing_info_path): + if processing_info_path is None: + return None + + if os.path.isdir(processing_info_path): + processing_info_path = os.path.join(processing_info_path, CELL_IMAGE_PROCESSING_FILENAME) + + if not os.path.exists(processing_info_path): + return None + + with open(processing_info_path, 'r', encoding='utf-8') as file: + processing_info = json.load(file) + + if 'shape' in processing_info and processing_info['shape'] is not None: + processing_info['shape'] = tuple(processing_info['shape']) + + return processing_info + + +def _is_cellaligner_cell_like(value): + return hasattr(value, 'coords') and hasattr(value, 'intensities') and hasattr(value, 'nucleus') + + +def _prepare_cellaligner_image(cell_object, channel, center=None, processing_info=None): + if isinstance(cell_object, str): + with open(cell_object, 'rb') as file: + cell_object = pickle.load(file) + + if channel is None: + raise ValueError('channel must be provided for CellAligner_Cell inputs') + + if center is None: + center = processing_info.get('center', 'cell') if processing_info is not None else 'cell' + + cell_image = make_cell_image(cell_object, ['nucleus', channel]) + cell_image = to_shape(cell_image, (max(cell_image.shape[:2]), max(cell_image.shape[:2]), 3)) + cell_image = cell_image[:, :, [2, 0, 1]] + cell_image = align_image(cell_image, center=center) + + if processing_info is not None: + if not processing_info.get('rescale', True): + max_size = processing_info.get('max_size') + if max_size is not None: + cell_image = to_shape(cell_image, (max_size, max_size, 3)) + + shape = processing_info.get('shape') + if shape is not None: + cell_image = resize_cell_image(cell_image, shape) + + return cell_image + + +def _load_unique_paired_dataset_images(paired_dataset): + unique_indices = sorted({index for pair in paired_dataset.image_pairs for index in pair}) + images = [np.load(os.path.join(paired_dataset.image_dir, f'cell_{index}.npy')) for index in unique_indices] + return unique_indices, images + + +def _paired_dataset_to_indexed_image_dataset(paired_dataset): + unique_indices = sorted({index for pair in paired_dataset.image_pairs for index in pair}) + transform = getattr(paired_dataset, 'transform', None) + indexed_dataset = IndexedImageDataset(paired_dataset.image_dir, unique_indices, transform=transform) + return unique_indices, indexed_dataset + + +def _batch_to_tensor(batch, device): + if isinstance(batch, (list, tuple)): + batch = batch[0] + + if isinstance(batch, torch.Tensor): + tensor = batch + if tensor.ndim == 4 and tensor.shape[-1] in [1, 3] and tensor.shape[1] not in [1, 3]: + tensor = tensor.permute(0, 3, 1, 2).contiguous() + elif tensor.ndim == 3 and tensor.shape[-1] in [1, 3]: + tensor = tensor.permute(2, 0, 1).unsqueeze(0).contiguous() + else: + array = np.asarray(batch) + if array.ndim == 3: + array = array[np.newaxis, ...] + if array.shape[-1] in [1, 3]: + tensor = torch.from_numpy(array).permute(0, 3, 1, 2) + else: + tensor = torch.from_numpy(array) + + if tensor.ndim == 3: + tensor = tensor.unsqueeze(0) + + if tensor.dtype != torch.float32: + tensor = tensor.float() + + return tensor.to(device) + + +def _as_sequence_if_object_array(value): + if isinstance(value, np.ndarray) and value.dtype == object: + return value.tolist() + return value + def resize_cell_image(image, target_shape): """ Resize a 3-channel cell image with appropriate interpolation for each channel. @@ -149,7 +263,7 @@ def align_image(image, center='cell', cell_mask_channel=1, nucleus_channel=2): def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped_channel_distributions, channel, center='cell', rescale=True, shape=(64, 64)): """ - Generate training data from GW_OT_Cell objects for neural network training. + Generate training data from CellAligner_Cell objects for neural network training. Creates paired cell images and their corresponding mapped versions for training deep learning models. Images are aligned, normalized, and saved as numpy arrays. @@ -157,11 +271,11 @@ def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped :param save_path: Directory path to save processed images. Cell images saved to '/cell_images' and mapped images to '/mapped_cell_images'. :type save_path: str - :param cell_objects: List of GW_OT_Cell objects or paths to pickled GW_OT_Cell objects. + :param cell_objects: List of CellAligner_Cell objects or paths to pickled CellAligner_Cell objects. :type cell_objects: list :param reference_cell_object: Reference cell object or path to pickled reference cell object used as template for mapped distributions. - :type reference_cell_object: GW_OT_Cell or str + :type reference_cell_object: CellAligner_Cell or str :param mapped_channel_distributions: Array of mapped protein distributions for each cell. :type mapped_channel_distributions: numpy.ndarray :param channel: Channel name to use for image processing. @@ -175,20 +289,26 @@ def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped :returns: None. Images are saved to disk as .npy files. :rtype: None """ + max_size = None + if isinstance(reference_cell_object, str): + with open(reference_cell_object, 'rb') as file: + reference_cell_object = pickle.load(file) + if not rescale: max_size = 0 for cell_object in cell_objects: - # Load GW_OT_Cell object if path specified + # Load CellAligner_Cell object if path specified if isinstance(cell_object, str): - pickle.load(open(cell_object, 'rb')) + with open(cell_object, 'rb') as file: + cell_object = pickle.load(file) cell_image = make_cell_image(cell_object, ['nucleus', channel]) cell_image = to_shape(cell_image, (max(cell_image.shape[:2]), max(cell_image.shape[:2]), 3)) - cell_image = cell_image[:,:,[1,0,2]] + cell_image = cell_image[:,:,[2,0,1]] cell_image = align_image(cell_image, center=center) max_size = max(max_size, max(cell_image.shape[:2])) for i, cell_object in enumerate(cell_objects): - # Load GW_OT_Cell object if path specified + # Load CellAligner_Cell object if path specified if isinstance(cell_object, str): pickle.load(open(cell_object, 'rb')) # make image array from cell object @@ -200,8 +320,8 @@ def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped cell_image = to_shape(cell_image, (max(cell_image.shape[:2]), max(cell_image.shape[:2]), 3)) mapped_cell_image = to_shape(mapped_cell_image, (max(mapped_cell_image.shape[:2]), max(mapped_cell_image.shape[:2]), 3)) # reorder channels: channel, binary cell mask, binary nucleus mask - cell_image = cell_image[:,:,[1,0,2]] - mapped_cell_image = mapped_cell_image[:,:,[1,0,2]] + cell_image = cell_image[:,:,[2,0,1]] + mapped_cell_image = mapped_cell_image[:,:,[2,0,1]] # align image cell_image = align_image(cell_image, center=center) mapped_cell_image = align_image(mapped_cell_image, center=center) @@ -220,6 +340,8 @@ def make_NN_training_data(save_path, cell_objects, reference_cell_object, mapped np.save(os.path.join(save_path, 'cell_images', f'cell_{i}.npy'), cell_image) np.save(os.path.join(save_path, 'mapped_cell_images', f'mapped_cell_{i}.npy'), mapped_cell_image) + _write_cell_image_processing_info(save_path, max_size=max_size, rescale=rescale, shape=shape, center=center) + class EfficientNetFeatureExtractor(nn.Module): """ @@ -350,15 +472,15 @@ def forward(self, x): return x -class dGWOTNetwork(nn.Module): +class dCellAlignerNetwork(nn.Module): """ - Deep Gromov-Wasserstein Optimal Transport Network. + Deep CellAligner Network. A complete neural network architecture that combines feature extraction, distance computation, and image reconstruction in a multi-task learning - framework. Designed for learning embeddings that preserve Gromov-Wasserstein - distances between cell morphologies while enabling reconstruction of - protein distributions. + framework. Designed for approximating CellAligner mapping to anchor cell + morphologies and learning embeddings that preserve metrics for quantifying + differences in protein localization after mapping. :param input_channels: Number of input image channels. Default is 3. :type input_channels: int, optional @@ -380,7 +502,7 @@ def __init__(self, input_channels=3, embedding_size=50, image_size=64): def forward(self, x1, x2, return_embedding=False): """ - Forward pass through the complete dGWOT network. + Forward pass through the complete Deep CellAligner network. Processes two input images through feature extraction, computes their embedding distance, and reconstructs both images. Optionally returns @@ -480,7 +602,7 @@ def __getitem__(self, idx): return input_img, target_img -def pretrain_model(paired_dataset, model, save_path=None, model_name="pretrained_model", +def pretrain_model(paired_dataset, model, dataset_name, save_path=None, batch_size=64, epochs=10, lr=1e-3, device=None, return_model=True): """ Pretrain a model using paired input and target images provided as a PairedDataset. @@ -498,11 +620,11 @@ def pretrain_model(paired_dataset, model, save_path=None, model_name="pretrained :param model: Neural network model to pretrain. Must have a forward method that takes two identical inputs and returns reconstructions. :type model: torch.nn.Module + :param dataset_name: Name prefix for saved model files. + :type dataset_name: str :param save_path: Directory path to save the pretrained model. If None, model is not saved. Default is None. :type save_path: str, optional - :param model_name: Name prefix for saved model files. Default is "pretrained_model". - :type model_name: str, optional :param batch_size: Batch size for training. Default is 64. :type batch_size: int, optional :param epochs: Number of training epochs. Default is 10. @@ -538,10 +660,10 @@ def pretrain_model(paired_dataset, model, save_path=None, model_name="pretrained model = model.to(device) optimizer = optim.Adam(model.parameters(), lr=lr) - # Store config for saving (if it's a dGWOTNetwork) + # Store config for saving (if it's a dCellAlignerNetwork) config = None if hasattr(model, 'feature_extractor') and hasattr(model, 'feature_decoder'): - # Try to extract config from dGWOTNetwork + # Try to extract config from dCellAlignerNetwork try: input_channels = model.feature_extractor.feature_extractor.features[0][0].in_channels if hasattr(model.feature_extractor, 'feature_extractor') else 3 embedding_size = model.feature_extractor.fc.out_features @@ -597,10 +719,10 @@ def pretrain_model(paired_dataset, model, save_path=None, model_name="pretrained 'config': config, 'epoch': epoch + 1, 'loss': epoch_loss - }, os.path.join(save_path, f'{model_name}_best.pth')) + }, os.path.join(save_path, f'{dataset_name}_pretrained_best.pth')) else: # Save without config (backward compatibility) - torch.save(model.state_dict(), os.path.join(save_path, f'{model_name}_best.pth')) + torch.save(model.state_dict(), os.path.join(save_path, f'{dataset_name}_pretrained_best.pth')) print(f' → New best model saved (loss: {epoch_loss:.6f})') # Save final model if save_path is provided @@ -611,10 +733,10 @@ def pretrain_model(paired_dataset, model, save_path=None, model_name="pretrained 'config': config, 'epoch': epochs, 'loss': epoch_loss - }, os.path.join(save_path, f'{model_name}_final.pth')) + }, os.path.join(save_path, f'{dataset_name}_pretrained_final.pth')) else: - torch.save(model.state_dict(), os.path.join(save_path, f'{model_name}_final.pth')) - print(f'Saved pretrained model to {save_path}/{model_name}_final.pth') + torch.save(model.state_dict(), os.path.join(save_path, f'{dataset_name}_pretrained_final.pth')) + print(f'Saved pretrained model to {save_path}/{dataset_name}_pretrained_final.pth') return model if return_model else None @@ -923,16 +1045,14 @@ def __call__(self, image): return image_rescaled -def train_dGWOT(train_dataset, valid_dataset, test_dataset, save_path, dataset_name, embedding_size=50, +def train_dCellAligner(train_dataset, valid_dataset, test_dataset, save_path, dataset_name, embedding_size=50, image_shape=(64,64), batch_size=100, epochs=100, device=None, learning_rate=0.001, dist_weight=1.0, early_stopping=True, patience=3, weight_decay=1e-5, lr_gamma=0.95, sparsity_weight=0.0, sparsity_target=0.05, pretrained_path=None, show_loss_components=False): """ - Train the Deep Gromov-Wasserstein Optimal Transport model. - - Trains a dGWOT network using multi-task learning with distance prediction + Trains a Deep CellAligner model using multi-task learning with distance prediction and image reconstruction objectives. Supports early stopping, learning rate scheduling, and optional sparsity constraints. @@ -992,7 +1112,7 @@ def train_dGWOT(train_dataset, valid_dataset, test_dataset, save_path, dataset_n input_channels = 3 image_size = image_shape[0] - model = dGWOTNetwork(input_channels, embedding_size, image_size).to(device) + model = dCellAlignerNetwork(input_channels, embedding_size, image_size).to(device) # Store config for saving config = { @@ -1138,15 +1258,15 @@ def train_dGWOT(train_dataset, valid_dataset, test_dataset, save_path, dataset_n return model, train_losses, val_losses -def load_dGWOT_model(checkpoint_path, device=None): +def load_dCellAligner_model(checkpoint_path, device=None): """ - Load a dGWOT model from a checkpoint containing state dict and config. + Load a Deep CellAligner model from a checkpoint containing state dict and config. :param checkpoint_path: Path to the checkpoint file containing both state_dict and config. :type checkpoint_path: str :param device: Device to load the model on. If None, uses GPU if available. :type device: torch.device, optional - :returns: Loaded dGWOT model ready for inference or further training. + :returns: Loaded dCellAligner model ready for inference or further training. :rtype: torch.nn.Module """ if device is None: @@ -1163,10 +1283,10 @@ def load_dGWOT_model(checkpoint_path, device=None): state_dict = checkpoint['state_dict'] # Create model with saved config - model = dGWOTNetwork(**config) + model = dCellAlignerNetwork(**config) model.load_state_dict(state_dict) - print(f"Loaded dGWOT model from {checkpoint_path}") + print(f"Loaded Deep CellAligner model from {checkpoint_path}") print(f"Config: {config}") elif 'state_dict' in checkpoint: @@ -1187,25 +1307,31 @@ def load_dGWOT_model(checkpoint_path, device=None): return model -def extract_embeddings(model, data, batch_size=64, device=None): +def extract_embeddings(model, data, batch_size=64, device=None, process_info_path=None, channel=None): """ - Extract latent embeddings from a trained dGWOT model. + Extract latent embeddings from a trained Deep CellAligner model. - Processes input images through the feature extractor to obtain latent - embeddings. Supports lists/arrays of images, PyTorch datasets, and PairedDatasets. + Processes input data through the feature extractor to obtain latent + embeddings. Supports CellAligner_Cell objects, NumPy arrays of images, + PyTorch datasets, and PairedDatasets as inputs. - :param model: Trained dGWOT model with a feature_extractor attribute. + :param model: Trained dCellAligner model with a feature_extractor attribute. :type model: torch.nn.Module :param data: Input data to extract embeddings from. Can be: - - List of numpy arrays with shape (H, W, C) - - Single numpy array with shape (N, H, W, C) - - PyTorch Dataset where __getitem__ returns images - - PairedDataset (extracts embeddings for unique images only) - :type data: list, numpy.ndarray, torch.utils.data.Dataset, or PairedDataset + - PairedDataset + - PyTorch Dataset + - List of CellAligner_Cell objects + - NumPy array with shape (N, H, W, 3) or (H, W, 3) + :type data: list, tuple, numpy.ndarray, torch.utils.data.Dataset, or PairedDataset :param batch_size: Batch size for processing. Default is 64. :type batch_size: int, optional :param device: Device to run computation on. If None, uses model's current device. :type device: torch.device, optional + :param process_info_path: Path to the saved cell image processing JSON or the directory containing it. + Used when ``data`` contains CellAligner_Cell objects. + :type process_info_path: str or None, optional + :param channel: Channel name to use when ``data`` contains CellAligner_Cell objects or file paths. + :type channel: str or None, optional :returns: Extracted embeddings of shape (N, embedding_size) where N is the number of input images. :rtype: numpy.ndarray @@ -1215,158 +1341,116 @@ def extract_embeddings(model, data, batch_size=64, device=None): model.eval() embeddings = [] + + data = _as_sequence_if_object_array(data) + + processing_info = _load_cell_image_processing_info(process_info_path) - # Case 1: PairedDataset - extract embeddings for unique images only if isinstance(data, PairedDataset): - # Get all unique image indices from the dataset pairs - all_indices = set() - for pair in data.image_pairs: - all_indices.update(pair) - all_indices = sorted(list(all_indices)) - - print(f"Extracting embeddings for {len(all_indices)} unique images from PairedDataset...") - - # Create dataset for unique images - unique_image_dataset = IndexedImageDataset( - data.image_dir, - all_indices, - transform=data.transform - ) - - # Process the unique image dataset - loader = DataLoader(unique_image_dataset, batch_size=batch_size, shuffle=False) - with torch.no_grad(): - for batch in tqdm(loader, desc="Extracting embeddings"): - images = batch - images = images.to(device) - if images.dtype != torch.float32: - images = images.float() - - # Extract features using the model's feature extractor - feats = model.feature_extractor(images) - embeddings.append(feats.cpu().numpy()) - - return np.concatenate(embeddings, axis=0) - - # Case 2: General PyTorch Dataset - elif hasattr(data, '__getitem__') and hasattr(data, '__len__') and isinstance(data, Dataset): + _, data = _paired_dataset_to_indexed_image_dataset(data) + if hasattr(data, '__getitem__') and hasattr(data, '__len__') and isinstance(data, Dataset): loader = DataLoader(data, batch_size=batch_size, shuffle=False) with torch.no_grad(): for batch in tqdm(loader, desc="Extracting embeddings"): - # Handle different dataset return formats - if isinstance(batch, (list, tuple)): - # If dataset returns multiple items, take the first (assumed to be images) - images = batch[0] - else: - images = batch - - images = images.to(device) - if images.dtype != torch.float32: - images = images.float() - - # Extract features using the model's feature extractor + images = _batch_to_tensor(batch, device) feats = model.feature_extractor(images) embeddings.append(feats.cpu().numpy()) - return np.concatenate(embeddings, axis=0) - - # Case 2: List or numpy array of images - # Convert list to numpy array if needed - if isinstance(data, list): - data = np.stack(data, axis=0) - - # Ensure data is numpy array with shape (N, H, W, C) + + if isinstance(data, (list, tuple)): + if data and (_is_cellaligner_cell_like(data[0]) or isinstance(data[0], str)): + if process_info_path is None: + raise ValueError("process_info_path must be provided when data contains CellAligner_Cell objects or file paths.") + data = [_prepare_cellaligner_image(cell_object, channel=channel, processing_info=processing_info) for cell_object in data] + data = np.stack(data, axis=0) + else: + raise TypeError('data must be a NumPy array, Dataset, PairedDataset, or a sequence of CellAligner_Cell objects') + if data.ndim == 3: - data = data[np.newaxis, ...] # Add batch dimension - + data = data[np.newaxis, ...] + n_images = data.shape[0] - - # Process in batches + with torch.no_grad(): for i in tqdm(range(0, n_images, batch_size), desc="Extracting embeddings"): batch_end = min(i + batch_size, n_images) batch_images = data[i:batch_end] - - # Convert to torch tensor and reorder dimensions (N, H, W, C) -> (N, C, H, W) - if batch_images.shape[-1] in [1, 3]: # Channels last - batch_tensor = torch.from_numpy(batch_images).permute(0, 3, 1, 2).float() - else: # Assume channels first already - batch_tensor = torch.from_numpy(batch_images).float() - - batch_tensor = batch_tensor.to(device) - - # Extract features + batch_tensor = _batch_to_tensor(batch_images, device) feats = model.feature_extractor(batch_tensor) embeddings.append(feats.cpu().numpy()) - + return np.concatenate(embeddings, axis=0) -def predict_distances(model, paired_dataset, batch_size=64, device=None): +def predict_distances(model, data, batch_size=64, device=None, process_info_path=None, channel=None): """ - Predict distances in latent space for a PairedDataset. + Predict distances from a trained Deep CellAligner model. - Extracts embeddings for all unique images in the dataset and computes - pairwise Euclidean distances in the embedding space. This provides - predictions that can be compared against ground truth distances. + Extracts embeddings for all unique images in the input data and computes + pairwise distances. Supports CellAligner_Cell objects, NumPy arrays of images, + PyTorch datasets, and PairedDatasets as inputs. - :param model: Trained dGWOT model with a feature_extractor attribute. + :param model: Trained dCellAligner model with a feature_extractor attribute. :type model: torch.nn.Module - :param paired_dataset: Dataset containing paired images with known distances. - :type paired_dataset: PairedDataset + :param data: Input data to extract embeddings from. Can be: + - PairedDataset + - PyTorch Dataset + - List of CellAligner_Cell objects + - NumPy array with shape (N, H, W, 3) or (H, W, 3) + :type data: PairedDataset, Dataset, numpy.ndarray, list, or tuple :param batch_size: Batch size for processing embeddings. Default is 64. :type batch_size: int, optional :param device: Device to run computation on. If None, uses model's current device. :type device: torch.device, optional - :returns: Array of predicted distances of shape (len(paired_dataset),) - corresponding to each pair in the dataset. + :param process_info_path: Path to the saved cell image processing JSON or the directory containing it. + Used when ``data`` contains CellAligner_Cell objects. + :type process_info_path: str or None, optional + :param channel: Channel name to use when ``data`` contains CellAligner_Cell objects or file paths. + :type channel: str or None, optional + :returns: If ``data`` is a PairedDataset, returns distances of shape ``(len(data),)`` corresponding + to each pair. Otherwise returns a square distance matrix of shape ``(N, N)`` for the + ``N`` input images, with zeros on the diagonal. :rtype: numpy.ndarray """ if device is None: device = next(model.parameters()).device - # Get all unique image indices from the dataset - all_indices = set() - for pair in paired_dataset.image_pairs: - all_indices.update(pair) - all_indices = sorted(list(all_indices)) - - print(f"Extracting embeddings for {len(all_indices)} unique images...") - - # Create dataset for unique images - unique_image_dataset = IndexedImageDataset( - paired_dataset.image_dir, - all_indices, - transform=paired_dataset.transform - ) - - # Extract embeddings for all unique images - embeddings = extract_embeddings(model, unique_image_dataset, batch_size=batch_size, device=device) - - # Create mapping from image index to embedding index - index_to_embedding = {img_idx: emb_idx for emb_idx, img_idx in enumerate(all_indices)} - - # Compute distances for each pair in the dataset - predicted_distances = [] - - print(f"Computing distances for {len(paired_dataset.image_pairs)} pairs...") - - for pair in tqdm(paired_dataset.image_pairs, desc="Computing pairwise distances"): - idx1, idx2 = pair - - # Get embedding indices - emb_idx1 = index_to_embedding[idx1] - emb_idx2 = index_to_embedding[idx2] - - # Get embeddings - emb1 = embeddings[emb_idx1] - emb2 = embeddings[emb_idx2] - - # Compute squared Euclidean distance (matching model's training objective) - distance = np.sum((emb1 - emb2) ** 2) - predicted_distances.append(distance) - - return np.array(predicted_distances) + data = _as_sequence_if_object_array(data) + processing_info = _load_cell_image_processing_info(process_info_path) + + if isinstance(data, PairedDataset): + paired_dataset = data + unique_indices, data = _paired_dataset_to_indexed_image_dataset(paired_dataset) + print(f"Extracting embeddings for {len(unique_indices)} unique images...") + embeddings = extract_embeddings(model, data, batch_size=batch_size, device=device) + + index_to_embedding = {img_idx: emb_idx for emb_idx, img_idx in enumerate(unique_indices)} + predicted_distances = [] + + print(f"Computing distances for {len(paired_dataset.image_pairs)} pairs...") + for pair in tqdm(paired_dataset.image_pairs, desc="Computing pairwise distances"): + idx1, idx2 = pair + emb1 = embeddings[index_to_embedding[idx1]] + emb2 = embeddings[index_to_embedding[idx2]] + predicted_distances.append(np.sum((emb1 - emb2) ** 2)) + + return np.array(predicted_distances) + + if isinstance(data, (list, tuple)) and data and (_is_cellaligner_cell_like(data[0]) or isinstance(data[0], str)): + if process_info_path is None: + raise ValueError("process_info_path must be provided when data contains CellAligner_Cell objects or file paths.") + data = [_prepare_cellaligner_image(cell_object, channel=channel, processing_info=processing_info) for cell_object in data] + data = np.stack(data, axis=0) + + print(f"Extracting embeddings for {len(data)} images...") + embeddings = extract_embeddings(model, data, batch_size=batch_size, device=device) + + if len(embeddings) == 0: + return np.zeros((0, 0), dtype=embeddings.dtype) + + print(f"Computing distance matrix for {len(embeddings)} embeddings:") + diff = embeddings[:, np.newaxis, :] - embeddings[np.newaxis, :, :] + return np.sum(diff ** 2, axis=2) def plot_distance_predictions(model, paired_dataset, batch_size=64, device=None, figsize=(8, 8), @@ -1377,7 +1461,7 @@ def plot_distance_predictions(model, paired_dataset, batch_size=64, device=None, Creates a scatter plot comparing model predictions against ground truth distances with a diagonal reference line and correlation metrics. - :param model: Trained dGWOT model with a feature_extractor attribute. + :param model: Trained dCellAligner model with a feature_extractor attribute. :type model: torch.nn.Module :param paired_dataset: Dataset containing paired images with known distances. :type paired_dataset: PairedDataset @@ -1448,7 +1532,7 @@ def plot_reconstruction_comparison(model, paired_dataset, n_cells=5, device=None protein distributions in the top row and their reconstructions from the model in the bottom row. - :param model: Trained dGWOT model with reconstruction capabilities. + :param model: Trained dCellAligner model with reconstruction capabilities. :type model: torch.nn.Module :param paired_dataset: Dataset containing paired images for reconstruction. :type paired_dataset: PairedDataset @@ -1547,11 +1631,91 @@ def plot_reconstruction_comparison(model, paired_dataset, n_cells=5, device=None except Exception as e: print(f"Error creating plot: {e}") return + + +def deep_map_to_anchor_cell(model, data, batch_size=64, device=None, process_info_path=None, channel=None): + """ + Approximate anchor-cell mapping with a trained dCellAligner model. + + This function mirrors the role of ``map_to_anchor_cell``, but instead of + directly computing a GW/FGW mappings to the anchor cell it uses a trained + dCellAligner model to infer the mapped single cell images. Supports + CellAligner_Cell objects, NumPy arrays of images, PyTorch datasets, and + PairedDatasets as inputs. + + :param model: Trained dCellAligner model. + :type model: torch.nn.Module + :param data: Input data to extract embeddings from. Can be: + - PairedDataset + - PyTorch Dataset + - List of CellAligner_Cell objects + - NumPy array with shape (N, H, W, 3) or (H, W, 3) + :type data: PairedDataset, Dataset, numpy.ndarray, list, or tuple + :param batch_size: Number of prepared images to process per forward pass. Default is 64. + :type batch_size: int + :param device: Device to run inference on. If None, uses the model's device. + :type device: torch.device or None + :param process_info_path: Path to the saved cell image processing JSON or the directory containing it. + Used when ``data`` contains CellAligner_Cell objects. + :type process_info_path: str or None, optional + :param channel: Channel name to use when ``data`` contains CellAligner_Cell objects or file paths. + :type channel: str or None, optional + :return: Array of mapped protein images with shape (N, H, W). + :rtype: numpy.ndarray + """ + if device is None: + device = next(model.parameters()).device + + data = _as_sequence_if_object_array(data) + processing_info = _load_cell_image_processing_info(process_info_path) + + if isinstance(data, PairedDataset): + if not hasattr(data, 'image_pairs') or not hasattr(data, 'image_dir'): + raise TypeError('PairedDataset input must define image_pairs and image_dir') + + _, data = _paired_dataset_to_indexed_image_dataset(data) + if isinstance(data, Dataset): + loader = DataLoader(data, batch_size=batch_size, shuffle=False) + mapped_images = [] + with torch.no_grad(): + for batch in tqdm(loader, desc='Mapping cells'): + batch_tensor = _batch_to_tensor(batch, device) + _, reconstructed, _ = model(batch_tensor, batch_tensor) + mapped_images.append(reconstructed[:, 0, :, :].cpu().numpy()) + + return np.concatenate(mapped_images, axis=0) + else: + prepared_images = None + if isinstance(data, np.ndarray) and data.dtype != object: + prepared_images = data + else: + if not isinstance(data, (list, tuple)): + data = [data] + else: + data = _as_sequence_if_object_array(data) + + if data and (_is_cellaligner_cell_like(data[0]) or isinstance(data[0], str)): + if process_info_path is None: + raise ValueError("process_info_path must be provided when data contains CellAligner_Cell objects or file paths.") + prepared_images = [_prepare_cellaligner_image(cell_object, channel=channel, processing_info=processing_info) for cell_object in data] + prepared_images = np.stack(prepared_images, axis=0) + else: + raise TypeError('data must be a NumPy array, Dataset, PairedDataset, or a sequence of CellAligner_Cell objects') + model.eval() + mapped_images = [] + with torch.no_grad(): + for start_i in tqdm(range(0, len(prepared_images), batch_size), desc='Mapping cells'): + batch_images = prepared_images[start_i:start_i + batch_size] + batch_tensor = _batch_to_tensor(batch_images, device) + _, reconstructed, _ = model(batch_tensor, batch_tensor) + mapped_images.append(reconstructed[:, 0, :, :].cpu().numpy()) + + return np.concatenate(mapped_images, axis=0) def generate_dataset_split_pairs(indices, n_pairs, proportions=None, seed=None): """ - Generate cell pairs for dGWOT dataset splits. + Generate cell pairs for Deep CellAligner dataset splits. Creates stratified cell pairs for deep learning model training. Supports both random sampling across all cells and stratified sampling from predefined groups From b724847e67fee2cf1bec2ba1245d32658dbcb096 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Mon, 1 Jun 2026 15:49:15 +0000 Subject: [PATCH 13/14] Fixed formatting for CellAligner tutorial --- docs/notebooks/Example_6.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notebooks/Example_6.ipynb b/docs/notebooks/Example_6.ipynb index ccfbd4e..3575356 100644 --- a/docs/notebooks/Example_6.ipynb +++ b/docs/notebooks/Example_6.ipynb @@ -698,7 +698,7 @@ "id": "86df85ac", "metadata": {}, "source": [ - "# Applying existing cell image analysis methods to mapped cells" + "## Applying existing cell image analysis methods to mapped cells" ] }, { From 3b767332d41fdabfc487e04011869ae159f8f1d1 Mon Sep 17 00:00:00 2001 From: robertkhu Date: Mon, 1 Jun 2026 16:28:56 +0000 Subject: [PATCH 14/14] Updated torch & torchvision versions to be compatible with python 3.12 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index fa1d6ad..e308ab4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,8 +60,8 @@ texttable==1.6.4 threadpoolctl==3.1.0 tifffile==2022.10.10 tornado==6.4.2 -torch==2.1.1 -torchvision==0.16.1 +torch==2.4.0 +torchvision==0.19.0 tqdm==4.66.4 traitlets==5.14.3 trimesh==3.16.1