Source code for ClearMap.IO.assets_specs

"""
This module contains the specifications for the assets in the pipeline.
The specifications are used to create the assets and to
determine the file names and locations.
The specs passed to the asset are
`TypeSpec` objects and `ChannelSpec` objects.

`TypeSpec`
    contain the information about the type of asset
    (i.e. the step in the pipeline, e.g. stitched, layout, cells, density, binary...)

`ChannelSpec`
    contain the information about the channel
    (i.e. the labeling of the asset, e.g. cfos, dapi, autofluorescence, gfp...
    and the type of labelling e.g. nuclei, cells, vessels, veins, arteries, axons...).
"""
import ctypes
import multiprocessing
import warnings

from ClearMap.IO.assets_constants import (EXTENSIONS, COMPRESSION_ALGORITHMS, CHECKSUM_ALGORITHMS,
                                          RESOURCE_TYPE_TO_FOLDER, DATA_CONTENT_TYPES)
from ClearMap.Utils.tag_expression import Expression
from ClearMap.Utils.utilities import validate_arg


[docs] class TypeSpec: """ A specification for a type of asset. Not a concrete asset. This has all the information for a step in the pipeline regardless of the channel. """ def __init__(self, *, resource_type: str | None = None, type_name: str | None = None, sub_types: list[str] | dict | None = None, basename: str = '', sub_folder: str = '', file_format_category: str | None = None, resource_type_to_folder: dict | None = None, relevant_pipelines: list[str] | None = None, compression_algorithms: list[str] | None = None, checksum_algorithm: str | None = None, extensions: list[str] | None = None): """ A specification for a step in the pipeline. Not a concrete asset. Parameters ---------- resource_type: str | None The type of resource. E.g. 'data', 'results', 'graphs'... This is used to determine the directory where the file should be stored. If empty, the file is stored in the root directory. If None, same as type_name. (For folder assets) type_name: str The name of the type. E.g. 'stitched', 'layout', 'background', 'resampled', 'cells', 'density', 'binary'... This is the name of the step, not necessarily the name of the file. It is used to compute the file name. sub_types: List[str] | Dict[str, SubTypeSpec] | None The subtypes of the asset. These are the substeps in the pipeline. E.g. `cleaned` in graph_cleaned.gt or `raw` for cells_raw.npy. basename: str The base name of the file. This is the name of the file without the extension and substep. It is linked to type_name but can be different. Leave empty if it is the same. file_format_category: str | None The file_type used to map to the list of possible extensions for the file. E.g. 'image', 'table', 'graph'... If None, the asset is a folder. **If extensions is not None, this is ignored.** relevant_pipelines: List[str] | None The list of pipelines that are relevant for this asset. compression_algorithms: List[str] The list of possible compression algorithms for the file (ordered by preference). checksum_algorithm: str The checksum algorithm used to check the integrity of the file. extensions: List[str] | None The list of possible extensions for the file. This should be determined by the file_type. This can be used to override the default extensions. """ self._name = None if Expression(str(basename)).tags: basename = Expression(str(basename)) self.basename = basename or type_name self.name = type_name self.resource_type_to_folder = resource_type_to_folder or RESOURCE_TYPE_TO_FOLDER if resource_type and resource_type not in self.resource_type_to_folder: raise ValueError(f'Unknown resource type: {resource_type}') self.resource_type = resource_type self.relevant_pipelines = relevant_pipelines or [] if isinstance(sub_types, (list, tuple)): sub_types = {k: None for k in sub_types} else: sub_types = sub_types or {} self.sub_types = sub_types self.sub_folder = sub_folder self._file_format_category = file_format_category if extensions: self.extensions = extensions else: self.extensions = EXTENSIONS[file_format_category] if file_format_category else None compression_algorithms = compression_algorithms or COMPRESSION_ALGORITHMS self.compression_algorithms = compression_algorithms if checksum_algorithm is None: checksum_algorithm = CHECKSUM_ALGORITHMS[0] self.checksum_algorithm = validate_arg('checksum algorithm', checksum_algorithm, CHECKSUM_ALGORITHMS) def __str__(self): return f'{self.resource_type} - {self.basename}' def __repr__(self): return f'{self.__class__.__name__}({str(self)})' @property def basename(self): return self._basename @basename.setter def basename(self, value): if isinstance(value, str) and Expression(value).tags: value = Expression(value) self._basename = value
[docs] def is_expression(self): return Expression.is_expression(self._basename) # FIXME: why not self.basename (prop)
[docs] def get_sub_type(self, sub_type_name, extensions=None, file_format_category=None, expression=None): sub_type = self.sub_types.get(sub_type_name) if sub_type is None: sub_type = self.add_sub_type(sub_type_name, extensions, file_format_category, expression) return sub_type
[docs] def add_sub_type(self, sub_type_name, extensions=None, file_format_category=None, expression=None): """ Adds or updates an asset subtype Parameters ---------- sub_type_name extensions file_format_category expression Returns ------- """ if extensions is None and file_format_category is not None: extensions = EXTENSIONS[file_format_category] sub_type = SubTypeSpec(resource_type=self.resource_type, resource_type_to_folder=self.resource_type_to_folder, relevant_pipelines=self.relevant_pipelines, type_name=f'{self.name}_{sub_type_name}', basename=expression or self.basename, compression_algorithms=self.compression_algorithms, checksum_algorithm=self.checksum_algorithm, extensions=extensions or self.extensions) self.sub_types[sub_type_name] = sub_type return sub_type
@property def name(self): return self._name @name.setter def name(self, value): self._name = value if not self.basename and value != 'raw': # Skip raw because it has no default basename self.basename = value @property def is_folder(self): return self.extensions is None @property def directory(self): """ The directory where the asset should be stored. Returns the directory based on the resource type. If empty, returns the root directory. Returns ------- str The directory where the asset should be stored. """ if self.sub_folder: # Explicit sub_folder has priority return self.sub_folder if self.resource_type == '': # Experiment root dir if empty return '' else: return self.resource_type_to_folder[self.resource_type or self.name] # If None, defaults to self.name @property def default_extension(self): """ The default extension for this asset (based on type and sub_type). Returns ------- str The default extension for this asset. """ return self.extensions[0] if self.extensions else ''
[docs] def to_dict(self): return { 'resource_type': self.resource_type, 'resource_type_to_folder': self.resource_type_to_folder, 'type_name': self.name, 'sub_types': {k: (v.to_dict() if v is not None else None) for k, v in self.sub_types.items()}, 'basename': str(self.basename), 'file_format_category': self._file_format_category, 'sub_folder': self.sub_folder, 'relevant_pipelines': self.relevant_pipelines, 'compression_algorithms': self.compression_algorithms, 'checksum_algorithm': self.checksum_algorithm, 'extensions': self.extensions }
[docs] @classmethod def from_dict(cls, data): sub_types = data.get('sub_types') if sub_types: sub_types = {k: (TypeSpec.from_dict(v) if v is not None else None) for k, v in sub_types.items()} return cls( resource_type=data.get('resource_type'), resource_type_to_folder=data.get('resource_type_to_folder'), type_name=data.get('type_name'), sub_types=sub_types, basename=data.get('basename', ''), file_format_category=data.get('file_format_category'), sub_folder=data.get('sub_folder', ''), relevant_pipelines=data.get('relevant_pipelines'), compression_algorithms=data.get('compression_algorithms'), checksum_algorithm=data.get('checksum_algorithm'), extensions=data.get('extensions') )
[docs] class SubTypeSpec(TypeSpec): """ A specification for a subtype of an asset. Not a concrete asset. This has all the information for a substep in the pipeline regardless of the channel. """ def __init__(self, **kwargs): """ A specification for a substep in the pipeline. Not a concrete asset. Parameters ---------- kwargs The same parameters as TypeSpec. """ super().__init__(**kwargs) @property def main_type(self): return self.name.split('_')[0]
# def to_dict(self): # out = super().to_dict() # out['main_type'] = self.main_type # return out # # @classmethod # def from_dict(cls, data): # return super().from_dict(data) ChannelId = str | tuple[str, ...]
[docs] class ChannelSpec: """ A specification for a channel. Not a concrete asset. This has all the information for a channel regardless of the type of asset. Attributes ---------- name: ChannelId The name of the channel. (Typically the name of the labeling, e.g. cfos, dapi, autofluorescence, gfp ...) If a tuple, the channel is a composite of the channels in the tuple. content_type: str The type of content in the channel. E.g. 'nuclei', 'cells', 'vessels'... number: int The number id of the channel. """ channel_names = [] def __init__(self, channel: ChannelId, content_type: str, channel_number: int | None = None): """ A specification for a channel. Not a concrete asset. Parameters ---------- channel: ChannelId The name of the channel. (Typically the name of the labeling, e.g. cfos, dapi, autofluorescence, gfp ...) If a tuple, the channel is a composite of the channels in the tuple content_type: str The type of content in the channel. E.g. 'nuclei', 'cells', 'vessels'... channel_number: int The number of the channel. """ if isinstance(channel, list) and all(isinstance(c, str) for c in channel): channel = tuple(channel) warnings.warn('Passing a list as channel is deprecated and will be removed in a future version.' ' Use a tuple instead.', DeprecationWarning, stacklevel=2) is_tuple = isinstance(channel, tuple) # Enforce consistency between structure and content_type if not is_tuple and content_type == 'compound': if '-' in channel: channel = tuple(channel.split('-')) else: raise ValueError( f"content_type='compound' requires a compound ChannelId (tuple), " f"got atomic {channel!r}" ) if isinstance(channel, str) and '-' in channel: channel = tuple(channel.split('-')) if channel not in ChannelSpec.channel_names: ChannelSpec.channel_names.append(channel) self.name = channel self.content_type = validate_arg('data content type', content_type, DATA_CONTENT_TYPES) self.number = len(ChannelSpec.channel_names) - 1 if channel_number is None else channel_number
[docs] @classmethod def channel_number_to_name(cls, channel_number): return cls.channel_names[channel_number]
[docs] def is_simple_channel(self): return not self.is_compound()
[docs] def has_pipeline(self): return self.content_type not in (None, 'undefined', 'no-pipeline', 'compound')
[docs] def is_compound(self): return isinstance(self.name, tuple)
[docs] def to_dict(self): return { 'name': self.name, 'content_type': self.content_type, 'number': self.number }
[docs] @classmethod def from_dict(cls, data): return cls( channel=data['name'], content_type=data['content_type'], channel_number=data['number'] )
[docs] class StateManager: """ A context manager to handle the state (e.g. debug mode). It is multiprocess safe using shared memory for the status. Attributes ---------- _status: str The current context keyword. known_contexts: list[str] The list of known context keywords. """ def __init__(self): self._status = multiprocessing.Array(ctypes.c_char, 20, lock=True) # Shared memory for multiprocessing self.known_contexts = [] def __call__(self, status=None): if status: if not isinstance(status, str): status = 'debug' self.status = status return self def __enter__(self): if not self.status: self.status = 'debug' return self def __exit__(self, exc_type, exc_val, exc_tb): self.status = '' @property def status(self): with self._status.get_lock(): val = self._status.value.decode('utf-8') if val.lower() in ('', 'none', 'null'): return None elif val.lower() == 'true': return True elif val.lower() == 'false': return False else: return val @status.setter def status(self, value): with self._status.get_lock(): # if value not in self.known_contexts: # raise ValueError(f'Unknown context: {value}') if isinstance(value, bool): value = str(value).lower() if value is None: value = '' if len(value) > 20: warnings.warn('Context keyword is too long, max 20 characters.') value = value[:20] self._status.value = value.encode('utf-8')