Source code for ClearMap.pipeline_orchestrators.colocalization

import math
from typing import List, Optional

import numpy as np
import pandas as pd

from ClearMap.IO.workspace2 import Workspace2

from ClearMap.Utils.exceptions import ClearMapValueError
from ClearMap.Utils.utilities import sanitize_n_processes

from ClearMap.config.config_coordinator import ConfigCoordinator

from ClearMap.Analysis.Measurements import Voxelization as voxelization
from ClearMap.Analysis.colocalization.channel import Channel as ColocalizationChannel

from ClearMap.pipeline_orchestrators.generic_orchestrators import CompoundChannelPipelineOrchestrator
from ClearMap.pipeline_orchestrators.sample_info_management import SampleManager
from ClearMap.pipeline_orchestrators.registration_orchestrator import RegistrationProcessor


[docs] class ColocalizationProcessor(CompoundChannelPipelineOrchestrator): colocalization_channels: dict[ColocalizationChannel] config_name = 'colocalization' def __init__(self, sample_manager: Optional[SampleManager] = None, config_coordinator: Optional[ConfigCoordinator] = None, channels : Optional[List[str]] = None, registration_processor: Optional[RegistrationProcessor] = None): super().__init__(config_coordinator) self.sample_manager = sample_manager self.channels: List[str] = channels self.registration_processor: Optional[RegistrationProcessor] = registration_processor self.workspace: Workspace2 | None = None self.filtered_table: Optional[pd.DataFrame] = None if channels is None: raise ClearMapValueError(f'No channels specified. Please provide a pair of channels to compare. ' f'They must match the ones in the sample_params file.') if len(channels) != 2: raise ClearMapValueError(f'Please provide exactly two channels to compare. ' f'They must match the ones in the sample_params file.') self.colocalization_channels: dict[str, ColocalizationChannel] = {} # The objects that compute the colocalization from the colocalization package self.setup_finalised: bool = False self.setup(sample_manager, channels, registration_processor)
[docs] def setup(self, sample_manager: Optional[SampleManager], channel_names: tuple[str], registration_processor: Optional[RegistrationProcessor] = None): self.channels = tuple(channel_names) if registration_processor is not None: self.registration_processor = registration_processor if sample_manager is not None: self.sample_manager = sample_manager self.workspace = sample_manager.workspace sample_id = self.sample_manager.sample_id self.workspace.ensure_pipeline('Colocalization', channel_id=self.channels, sample_id=sample_id, permute_channels=True, create_channel=True) self.finalise_setup()
[docs] def finalise_setup(self): if self.setup_finalised: return finalised_channels = {chan: False for chan in self.channels} for channel in self.channels: resolution = self.sample_manager.get_channel_resolution(channel) try: self.colocalization_channels[channel] = ColocalizationChannel( self.get('cells', channel=channel, asset_sub_type='shape').existing_path, self.get_cells_df(channel), voxel_dims=resolution) finalised_channels[channel] = True except FileNotFoundError as err: print(f'Colocalization {err}') self.setup_finalised = False self.setup_finalised = all(finalised_channels.values())
[docs] def get_cells_df(self, channel): return pd.read_feather(self.get_path('cells', channel=channel))
[docs] def compute_colocalization(self, channel_a, channel_b): # voxel_blob_diameter will also be used to compute the overlap voxel_blob_diameter = self.config['comparison']['particle_diameter'] perf_cfg = self.cfg_coordinator.get_config_view(self.config_name)['performance'] n_processes = sanitize_n_processes(perf_cfg['n_processes']) report = self.colocalization_channels[channel_a].compare(self.colocalization_channels[channel_b], blob_diameter=voxel_blob_diameter, size_min=4*voxel_blob_diameter, size_max=6*voxel_blob_diameter, # FIXME: add control for size_max processes=n_processes) report_path = self.get_path('colocalization', channel=(channel_a, channel_b), asset_sub_type='report') report.reset_index(inplace=True) # WARNING: extract the index (no drop) to a separate column to allow saving to feather report.to_feather(report_path)
[docs] def save_filtered_table(self, channel_a, channel_b): if self.filtered_table is not None: self.filtered_table.to_feather(self.get_path('colocalization', channel=(channel_a, channel_b), asset_sub_type='filtered_report'))
[docs] def plot_nearest_neighbors(self, channel_a, channel_b, parent=None): # TODO: improve with line between particles from matplotlib.colors import to_hex import pyqtgraph as pg from ClearMap.Visualization.Qt import Plot3d as q_plot_3d from ClearMap.Visualization.Qt.widgets import Scatter3D channel_a_particle_coordinates, channel_b_particle_coordinates, channel_a_no_neighbour_coordinates = self.filter_table( channel_a, channel_b) # if physical coordinates # channel_a_particle_coordinates *= self.sample_manager.config['channels'][channel_a]['resolution'] # channel_a_particle_coordinates *= self.sample_manager.config['channels'][channel_b]['resolution'] lut = ['red', 'blue', 'orange', 'yellow', 'brown', 'pink', 'cyan', 'olive', 'grey'] lut = [to_hex(col) for col in lut] n_colocalized_particles = len(channel_a_particle_coordinates) n_repeats = n_colocalized_particles / len(lut) colours = np.tile(lut, math.ceil(n_repeats))[:n_colocalized_particles] channel_a_particle_coordinates['colour'] = colours channel_a_particle_coordinates['symbol'] = '+' channel_b_particle_coordinates['colour'] = colours channel_b_particle_coordinates['symbol'] = 'd' channel_a_no_neighbour_coordinates['colour'] = to_hex('grey') channel_a_no_neighbour_coordinates['symbol'] = 'o' scatter_df = pd.concat([channel_a_particle_coordinates, channel_b_particle_coordinates, channel_a_no_neighbour_coordinates]) self.filtered_table = scatter_df channel_a_stitched = self.get_path('stitched', channel=channel_a) channel_b_stitched = self.get_path('stitched', channel=channel_b) dv = q_plot_3d.plot([[channel_a_stitched, channel_b_stitched]], title=f'Nearest Neighbours {channel_a} - {channel_b}', arrange=False, parent=parent)[0] scatter = pg.ScatterPlotItem() dv.view.addItem(scatter) dv.scatter = scatter dv.scatter_coords = Scatter3D(scatter_df, z_radius=self.machine_config['particle_plot_z_sphere_radius'], marker_size=self.config['comparison']['particle_diameter'] // 2) dv.refresh() return [dv]
[docs] def filter_table(self, channel_a, channel_b): # TODO: add options for which criteria ?/ report = pd.read_feather(self.get_path('colocalization', channel=(channel_a, channel_b), asset_sub_type='report')) # TODO: see if we cache maximum_distance = self.config['analysis']['max_particle_distance'] within_distance_mask = report['closest blob distance'].values < maximum_distance chan_a = self.colocalization_channels[channel_a] channel_a_particle_coordinates = report.loc[within_distance_mask, [f'center of bounding box {ax}' for ax in chan_a.coord_names]] channel_a_particle_coordinates.columns = list(chan_a.coord_names) channel_b_particle_coordinates = report.loc[within_distance_mask, [f'closest blob center {ax}' for ax in chan_a.coord_names]] channel_b_particle_coordinates.columns = list(chan_a.coord_names) # outside_distance_particles = report[report['closest blob distance'] >= maximum_distance] channel_a_no_neighbour_coordinates = report.loc[~within_distance_mask, [f'center of bounding box {ax}' for ax in chan_a.coord_names]] channel_a_no_neighbour_coordinates.columns = list(chan_a.coord_names) return channel_a_particle_coordinates, channel_b_particle_coordinates, channel_a_no_neighbour_coordinates
[docs] def plot_overlaps(self): pass
[docs] def voxelize_filtered_table(self, channel_a, channel_b): coordinates, voxelization_parameter = self.get_voxelization_params(channel_a, channel_b) _ = self.voxelize_unweighted(channel_a, channel_b, coordinates, voxelization_parameter)
[docs] def get_voxelization_params(self, channel_a, channel_b): voxelization_parameter = { 'radius': self.config['voxelization']['radii'], 'verbose': True } if self.workspace.debug: # Path will use debug voxelization_parameter['shape'] = self.get('cells', channel=(channel_a, channel_b), asset_sub_type='shape').shape() elif self.registration_processor.was_registered: voxelization_parameter['shape'] = self.get('atlas', channel=channel_a, asset_sub_type='annotation').shape() else: voxelization_parameter['shape'] = self.sample_manager.resampled_shape(channel_a) channel_a_particle_coordinates, _, _ = self.filter_table(channel_a, channel_b) coordinates = channel_a_particle_coordinates[list(self.colocalization_channels[channel_a].coord_names)].values return coordinates, voxelization_parameter
[docs] def voxelize_unweighted(self, channel_a, channel_b, coordinates, voxelization_parameter): """ Voxelize un weighted i.e. for cell counts Parameters ---------- channel_a: str Name of the first channel channel_b: str Name of the second channel coordinates: str, array or Source Source of point of nxd coordinates. voxelization_parameter: dict Dictionary to be passed to voxelization.voxelise (i.e. with these optional keys: shape, dtype, weights, method, radius, kernel, processes, verbose Returns ------- coordinates, counts_file_path: np.array, str """ counts_asset = self.get('density', channel=(channel_a, channel_b), asset_sub_type='counts') counts_asset.delete(missing_ok=True) self.set_watcher_step('Unweighted voxelisation') voxelization.voxelize(coordinates, sink=counts_asset.path, **voxelization_parameter) # WARNING: prange self.update_watcher_main_progress() # uncrusted_coordinates = self.remove_crust(coordinates) # WARNING: currently causing issues # density_path = self.get_path('density', channel=self.channel, asset_sub_type='counts_wcrust') # voxelization.voxelize(uncrusted_coordinates, sink=density_path, **voxelization_parameter) # WARNING: prange return coordinates, counts_asset.path