The following diagrams automatically generated by :ref:`pyreverse <https://pylint.readthedocs.io/en/latest/pyreverse.html>` (see :ref:`docs.yml <https://github.com/FZJ-INM1-BDA/siibra-python/blob/main/.github/workflows/docs.yml>` for details).
.. thumbnail:: _static/packages_siibra.svg :title: Package diagram :height: 100px :width: 300px :group: diagrams
.. thumbnail:: _static/classes_siibra.svg :title: Class diagram (diamond: relation between two classes via variable name, triangular: ancestor, dahsed: interface.) :height: 100px :width: 300px :group: diagrams
Since siibra creates many objects in a typical workflow, the creation of objects is following a clear structure. There are two main approaches of object creation:
Preconfigured objects, via the configuration module. This happens automatically for
- subclasses of
AtlasConcept
, if the class parameterconfiguration_folder
points to a subfolder of siibra configuration repositories. - subclasses of
Feature
, if the class parameterconfiguration_folder
points to a subfolder of siibra configuration repositories.
The implementation of these queries is centralized in
AtlasConcept.get_instances()
andFeature.get_instances()
, respectively, and relies on the registrations done inAtlasConcept.__init_subclass__()
andFeature.__init_subclass__()
.- subclasses of
Live queries, via the livequery module. This applies to subclasses of Feature, and relies on subclasses of
LiveQuery
with the class parameterFeatureType
set to the corresponding subclass ofFeature
. This triggers the query to be registered in theFeature._live_queries
list viaLiveQuery.__init_subclass__()
. Any registered live queries will then automatically be called inFeature.get_instances()
, extending the list of possible preconfigured object instances.
Note that AtlasConcept
keeps the list of instances in an InstanceTable
during
runtime, which can be accessed via AtlasConcept.registry()
for any subclass.
Instances of Feature
subclasses are just stored as a list at class level.
siibra makes frequent use of some typical design patterns.
Often, siibra implementations make use of classes from different siibra modules. It is common Python practice to do all imports at the start of a file, and not locally inside functions / classes.
Direct imports of classes from other local modules like here:
from ..core.space import Space
def my_func():
s = Space()
result in immediate creation of the class, and quickly leads to cyclic import dependencies which result in runtime errors.
This effect can often be avoided by only importing the siibra module, and deferring the class creating to a later stage in the code:
from ..core import space
def my_func():
s = space.Space()
In general, it seems a good practice to import specific classes only in the
__init__.py
files, and use module imports in other python files.
However, this rule of thumb is not yet consistently implemented and verified in siibra.
Since siibra pre-configures many objects, of which the user will typically only use a few (e.g. after filternig data features by brain regions), it is important that time and/or memory consuming operations are only executed when objects are actually requested and used. We typically solve this by implementing object properties with a lazy loading mechanism, following this scheme:
class Thing:
def __init__(self):
self._heavy_property_cached = None
@property
def heavy_property(self):
if self._heavy_property_cached is None:
# only here we do the initialization,
# and only once for the object
self._heavy_property_cached = some_heavy_computation()
return self._heavy_property_cached
Volume: is a complete 3D object, typically a complete brain.
Volume provider: is a resource that provides access to volumes. A volume can have multiple providers in different formats.
- Variant: refers to alternative representations of the same volume (e.g. inflated surface).
- If the volume has variants, they need to be listed in the configuration file.
Fragments: are individually addressable components of a volume.
- If a volume has fragments, either the user or the code needs to retrieve from multiple sources to access the complete volume.
- Fragments need to be named (e.g. left and right hemisphere), because they inevitably split the whole object into distinct anatomical parts that require semantic labeling.
Brain regions (label): are structures mapped inside a specific volume or fragment.
- The structure appears by interpreting the labels inside the volume listed in the configuration file. In special cases, a brain region could be represented by the complete volume or fragment.
Volume index: the index of the volume in case there is more than one; typically used for probability maps, where each area has a different volume.
Z: for 4D volumes, it specifies the 4th coordinate identifying an actual 3D volume. It has a similar function as the volume index, only that the volumes are concatenated in one array and share the same affine transformation.
Source type (format): the format of the volume data.
- See
SUPPORTED_FORMATS
(IMAGE_FORMATS
andSURFACE_FORMATS
) at volumes.volume.py for the currently supported formats.
- See
To access the actual image or mesh data from a volume requires explicit fetching. The process of fetching typically involves finding an appropriate volume provider, loading the data, and returning the image or mesh data. In simple volume objects however, this might only require to return already loaded image data. Images are typially returned as Nifti1Image, while meshes are returned as a dictionary including the vertices, faces and sometimes label arrays.
Fetching volumes occurs in two main stages:
Request of a volume object, such as a template, parcellation map, or other plain image volume.
The user sets the object they would like to fetch a volume from:
- a space template -> using
get_template()
which provides a volume template. - or a map -> getting the desired map by setting desired specs.
- a space template -> using
The user invokes
fetch()
method to retrieve the volume from the template or map.- template directly accesses to
volume.fetch()
fetch()
first goes throughmap.fetch()
to determine the associated volume.
- template directly accesses to
Actual retrieval of the volume data by siibra after the user asks for the volume via
fetch()
method. Whenfetch()
is invoked it accesses to corresponding volume provider based on the specifications given by volume index, fragment, z, label, variant, and format. According to the source type, the provider invokes the correct class and fetches the data accordingly.
Defaults
- Volume with several variants: the first variant listed in the configuration is fetched. The user is informed along with a list of possible variants.
- Volume with several fragments: All fragments are retrieved and combined to provide the whole volume. (This may cause some array length issues on the user end so the user should be informed. Potentially, this may be changed to fetch only the first fragment along with info and a list of options.)
Implementation Notes
- When adjusting to a new type of data or special cases, it is highly encouraged to use one of the existing parameters.
- Always inform a user when there are options available and the default is chosen.
Is the feature type class representation for the data?
- Yes: go to step 1.
- No: create feature type subclass and PR to siibra-python main.
Is the feature type already described by the schema (in siibra-python/config_schema)?
- Yes: go to step 2.
- No: create schema and PR to siibra-python main.
Create feature jsons and create a PR to siibra-configurations.
After merging the PR, create new tag on siibra-configurations.
Bump siibra-python version to match the new tag.
Each feature instance requires an anatomical anchor. This could be a parcellation
(as in RegionalConnecticity
), a region, a region and a location, or a location.
Using the anatomical anchor siibra can determine the semantic and spatial
relationship between different AtlasConcepts
.