siibra 1.0a1__1-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of siibra might be problematic. Click here for more details.

Files changed (84) hide show
  1. siibra/VERSION +1 -0
  2. siibra/__init__.py +164 -0
  3. siibra/commons.py +823 -0
  4. siibra/configuration/__init__.py +17 -0
  5. siibra/configuration/configuration.py +189 -0
  6. siibra/configuration/factory.py +589 -0
  7. siibra/core/__init__.py +16 -0
  8. siibra/core/assignment.py +110 -0
  9. siibra/core/atlas.py +239 -0
  10. siibra/core/concept.py +308 -0
  11. siibra/core/parcellation.py +387 -0
  12. siibra/core/region.py +1223 -0
  13. siibra/core/space.py +131 -0
  14. siibra/core/structure.py +111 -0
  15. siibra/exceptions.py +63 -0
  16. siibra/experimental/__init__.py +19 -0
  17. siibra/experimental/contour.py +61 -0
  18. siibra/experimental/cortical_profile_sampler.py +57 -0
  19. siibra/experimental/patch.py +98 -0
  20. siibra/experimental/plane3d.py +256 -0
  21. siibra/explorer/__init__.py +17 -0
  22. siibra/explorer/url.py +222 -0
  23. siibra/explorer/util.py +87 -0
  24. siibra/features/__init__.py +117 -0
  25. siibra/features/anchor.py +224 -0
  26. siibra/features/connectivity/__init__.py +33 -0
  27. siibra/features/connectivity/functional_connectivity.py +57 -0
  28. siibra/features/connectivity/regional_connectivity.py +494 -0
  29. siibra/features/connectivity/streamline_counts.py +27 -0
  30. siibra/features/connectivity/streamline_lengths.py +27 -0
  31. siibra/features/connectivity/tracing_connectivity.py +30 -0
  32. siibra/features/dataset/__init__.py +17 -0
  33. siibra/features/dataset/ebrains.py +90 -0
  34. siibra/features/feature.py +970 -0
  35. siibra/features/image/__init__.py +27 -0
  36. siibra/features/image/image.py +115 -0
  37. siibra/features/image/sections.py +26 -0
  38. siibra/features/image/volume_of_interest.py +88 -0
  39. siibra/features/tabular/__init__.py +24 -0
  40. siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
  41. siibra/features/tabular/cell_density_profile.py +298 -0
  42. siibra/features/tabular/cortical_profile.py +322 -0
  43. siibra/features/tabular/gene_expression.py +257 -0
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
  45. siibra/features/tabular/layerwise_cell_density.py +95 -0
  46. siibra/features/tabular/receptor_density_fingerprint.py +192 -0
  47. siibra/features/tabular/receptor_density_profile.py +110 -0
  48. siibra/features/tabular/regional_timeseries_activity.py +294 -0
  49. siibra/features/tabular/tabular.py +139 -0
  50. siibra/livequeries/__init__.py +19 -0
  51. siibra/livequeries/allen.py +352 -0
  52. siibra/livequeries/bigbrain.py +197 -0
  53. siibra/livequeries/ebrains.py +145 -0
  54. siibra/livequeries/query.py +49 -0
  55. siibra/locations/__init__.py +91 -0
  56. siibra/locations/boundingbox.py +454 -0
  57. siibra/locations/location.py +115 -0
  58. siibra/locations/point.py +344 -0
  59. siibra/locations/pointcloud.py +349 -0
  60. siibra/retrieval/__init__.py +27 -0
  61. siibra/retrieval/cache.py +233 -0
  62. siibra/retrieval/datasets.py +389 -0
  63. siibra/retrieval/exceptions/__init__.py +27 -0
  64. siibra/retrieval/repositories.py +769 -0
  65. siibra/retrieval/requests.py +659 -0
  66. siibra/vocabularies/__init__.py +45 -0
  67. siibra/vocabularies/gene_names.json +29176 -0
  68. siibra/vocabularies/receptor_symbols.json +210 -0
  69. siibra/vocabularies/region_aliases.json +460 -0
  70. siibra/volumes/__init__.py +23 -0
  71. siibra/volumes/parcellationmap.py +1279 -0
  72. siibra/volumes/providers/__init__.py +20 -0
  73. siibra/volumes/providers/freesurfer.py +113 -0
  74. siibra/volumes/providers/gifti.py +165 -0
  75. siibra/volumes/providers/neuroglancer.py +736 -0
  76. siibra/volumes/providers/nifti.py +266 -0
  77. siibra/volumes/providers/provider.py +107 -0
  78. siibra/volumes/sparsemap.py +468 -0
  79. siibra/volumes/volume.py +892 -0
  80. siibra-1.0.0a1.dist-info/LICENSE +201 -0
  81. siibra-1.0.0a1.dist-info/METADATA +160 -0
  82. siibra-1.0.0a1.dist-info/RECORD +84 -0
  83. siibra-1.0.0a1.dist-info/WHEEL +5 -0
  84. siibra-1.0.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,110 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Qualification between two BrainStructures"""
16
+
17
+ from enum import Enum
18
+ from dataclasses import dataclass
19
+ from typing import Dict, Generic, TypeVar, TYPE_CHECKING
20
+ if TYPE_CHECKING:
21
+ from .structure import BrainStructure
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ class Qualification(Enum):
27
+ EXACT = 1
28
+ OVERLAPS = 2
29
+ CONTAINED = 3
30
+ CONTAINS = 4
31
+ APPROXIMATE = 5
32
+ HOMOLOGOUS = 6
33
+ OTHER_VERSION = 7
34
+
35
+ @property
36
+ def verb(self):
37
+ """
38
+ a string that can be used as a verb in a sentence
39
+ for producing human-readable messages.
40
+ """
41
+ transl = {
42
+ Qualification.EXACT: 'coincides with',
43
+ Qualification.OVERLAPS: 'overlaps with',
44
+ Qualification.CONTAINED: 'is contained in',
45
+ Qualification.CONTAINS: 'contains',
46
+ Qualification.APPROXIMATE: 'approximates to',
47
+ Qualification.HOMOLOGOUS: 'is homologous to',
48
+ Qualification.OTHER_VERSION: 'is another version of'
49
+ }
50
+ assert self in transl, f"{str(self)} verb cannot be found."
51
+ return transl[self]
52
+
53
+ def invert(self):
54
+ """
55
+ Return qualification with the inverse meaning
56
+ """
57
+ inverses = {
58
+ Qualification.EXACT: Qualification.EXACT,
59
+ Qualification.OVERLAPS: Qualification.OVERLAPS,
60
+ Qualification.CONTAINED: Qualification.CONTAINS,
61
+ Qualification.CONTAINS: Qualification.CONTAINED,
62
+ Qualification.APPROXIMATE: Qualification.APPROXIMATE,
63
+ Qualification.HOMOLOGOUS: Qualification.HOMOLOGOUS,
64
+ Qualification.OTHER_VERSION: Qualification.OTHER_VERSION,
65
+ }
66
+ assert self in inverses, f"{str(self)} inverses cannot be found."
67
+ return inverses[self]
68
+
69
+ def __str__(self):
70
+ return f"{self.__class__.__name__}={self.name.lower()}"
71
+
72
+ def __repr__(self):
73
+ return str(self)
74
+
75
+ @staticmethod
76
+ def parse_relation_assessment(spec: Dict):
77
+ name = spec.get("name")
78
+ if name == "is homologous to":
79
+ return Qualification.HOMOLOGOUS
80
+ raise Exception(f"Cannot parse spec: {spec}")
81
+
82
+
83
+ @dataclass
84
+ class AnatomicalAssignment(Generic[T]):
85
+ """Represents a qualified assignment between anatomical structures."""
86
+ query_structure: "BrainStructure"
87
+ assigned_structure: "BrainStructure"
88
+ qualification: Qualification
89
+ explanation: str = ""
90
+
91
+ @property
92
+ def is_exact(self):
93
+ return self.qualification == Qualification.EXACT
94
+
95
+ def __str__(self):
96
+ msg = f"'{self.query_structure}' {self.qualification.verb} '{self.assigned_structure}'"
97
+ return msg if self.explanation == "" else f"{msg} - {self.explanation}"
98
+
99
+ def invert(self):
100
+ return AnatomicalAssignment(
101
+ self.assigned_structure,
102
+ self.query_structure,
103
+ self.qualification.invert(),
104
+ self.explanation
105
+ )
106
+
107
+ def __lt__(self, other: 'AnatomicalAssignment'):
108
+ if not isinstance(other, AnatomicalAssignment):
109
+ raise ValueError(f"Cannot compare AnatomicalAssignment with instances of '{type(other)}'")
110
+ return self.qualification.value < other.qualification.value
siibra/core/atlas.py ADDED
@@ -0,0 +1,239 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Provides reference systems for brains."""
16
+ from . import concept, space as _space, parcellation as _parcellation
17
+
18
+ from ..commons import MapType, logger, InstanceTable, Species
19
+
20
+ from typing import List
21
+
22
+
23
+ VERSION_BLACKLIST_WORDS = ["beta", "rc", "alpha"]
24
+
25
+
26
+ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
27
+ """
28
+ Main class for an atlas, providing access to feasible
29
+ combinations of available parcellations and reference
30
+ spaces, as well as common functionalities of those.
31
+ """
32
+
33
+ def __init__(self, identifier: str, name: str, species: Species, **kwargs):
34
+ """Construct an empty atlas object with a name and identifier."""
35
+
36
+ concept.AtlasConcept.__init__(
37
+ self,
38
+ identifier=identifier,
39
+ name=name,
40
+ species=species,
41
+ **kwargs
42
+ )
43
+ self._parcellation_ids: List[str] = []
44
+ self._space_ids: List[str] = []
45
+
46
+ def _register_space(self, space_id: str):
47
+ self._space_ids.append(space_id)
48
+
49
+ def _register_parcellation(self, parcellation_id: str):
50
+ self._parcellation_ids.append(parcellation_id)
51
+
52
+ @property
53
+ def spaces(self):
54
+ """Access a registry of reference spaces supported by this atlas."""
55
+ return InstanceTable[_space.Space](
56
+ elements={s.key: s for s in _space.Space.registry() if s.id in self._space_ids},
57
+ matchfunc=_space.Space.match,
58
+ )
59
+
60
+ @property
61
+ def parcellations(self):
62
+ """Access a registry of parcellations supported by this atlas."""
63
+ return InstanceTable[_parcellation.Parcellation](
64
+ elements={p.key: p for p in _parcellation.Parcellation.registry() if p.id in self._parcellation_ids},
65
+ matchfunc=_parcellation.Parcellation.match,
66
+ )
67
+
68
+ def get_parcellation(self, parcellation=None) -> "_parcellation.Parcellation":
69
+ """
70
+ Returns a valid parcellation object defined by the atlas. If no
71
+ specification is provided, the default is returned.
72
+
73
+ Parameters
74
+ ----------
75
+ parcellation: str, Parcellation
76
+ specification of a parcellation or a parcellation object
77
+
78
+ Returns
79
+ -------
80
+ Parcellation
81
+ """
82
+
83
+ if parcellation is None:
84
+ parcellation_obj = self.parcellations[self._parcellation_ids[0]]
85
+ if len(self._parcellation_ids) > 1:
86
+ logger.info(f"No parcellation specified, using default: '{parcellation_obj.name}'.")
87
+ return parcellation_obj
88
+
89
+ if isinstance(parcellation, _parcellation.Parcellation):
90
+ assert parcellation in self.parcellations
91
+ return parcellation
92
+
93
+ return self.parcellations[parcellation]
94
+
95
+ def get_space(self, space=None) -> "_space.Space":
96
+ """
97
+ Returns a valid reference space object defined by the atlas. If no
98
+ specification is provided, the default is returned.
99
+
100
+ Parameters
101
+ ----------
102
+ space: str, Space
103
+ specification of a space or a space object
104
+
105
+ Returns
106
+ -------
107
+ Space
108
+ """
109
+ if space is None:
110
+ space_obj = self.spaces[self._space_ids[0]]
111
+ if len(self._space_ids) > 1:
112
+ logger.info(f"No space specified, using default '{space_obj.name}'.")
113
+ return space_obj
114
+
115
+ if isinstance(space, _space.Space):
116
+ assert space in self.spaces
117
+ return space
118
+
119
+ return self.spaces[space]
120
+
121
+ def get_map(
122
+ self,
123
+ space: _space.Space = None,
124
+ parcellation: _parcellation.Parcellation = None,
125
+ maptype: MapType = MapType.LABELLED,
126
+ ):
127
+ """
128
+ Returns a parcellation map in the given space.
129
+
130
+ Parameters
131
+ ----------
132
+
133
+ space: Space
134
+ The requested reference space. If None, the default is used.
135
+ parcellation: Parcellation
136
+ The requested parcellation. If None, the default is used.
137
+ maptype: MapType
138
+ Type of the map (labelled or statistical/probabilistic)
139
+
140
+ Returns
141
+ -------
142
+ ParcellationMap
143
+ """
144
+ parc_obj = self.get_parcellation(parcellation)
145
+ space_obj = self.get_space(space)
146
+ return parc_obj.get_map(space=space_obj, maptype=maptype)
147
+
148
+ def get_region(self, region, parcellation=None):
149
+ """
150
+ Returns a valid Region object matching the given specification.
151
+
152
+ Parameters
153
+ ----------
154
+ region : str or Region
155
+ Key, approximate name, id or instance of a brain region
156
+ parcellation : str or Parcellation
157
+ Key, approximate name, id or instance of a brain parcellation.
158
+ If None, the default is used.
159
+ """
160
+ return self.get_parcellation(parcellation).get_region(region)
161
+
162
+ def get_template(self, space: _space.Space = None, variant: str = None):
163
+ """
164
+ Returns the reference template in the desired reference space.
165
+ If no reference space is given, the default from `Atlas.space()` is used.
166
+
167
+ Parameters
168
+ ----------
169
+ space: Space
170
+ The desired reference space
171
+ variant: str (optional)
172
+ Some templates are provided in different variants, e.g.
173
+ freesurfer is available as either white matter, pial or
174
+ inflated surface for left and right hemispheres (6 variants).
175
+ This field could be used to request a specific variant.
176
+ Per default, the first found variant is returned.
177
+ """
178
+ return self.get_space(space).get_template(variant=variant)
179
+
180
+ def get_voi(self, space: _space.Space, point1: tuple, point2: tuple):
181
+ """Get a volume of interest spanned by two points in the given reference space.
182
+
183
+ Parameters
184
+ ----------
185
+ space: Space, str
186
+ The target reference space, or a string specification of the space
187
+ point1: Tuple
188
+ A 3D coordinate given in this reference space
189
+ point2: Tuple
190
+ Another 3D coordinate given in this reference space
191
+
192
+ Returns
193
+ -------
194
+ BoundingBox
195
+ """
196
+ return self.get_template(space).get_boundingbox(point1, point2)
197
+
198
+ def find_regions(
199
+ self,
200
+ regionspec: str,
201
+ all_versions: bool = False,
202
+ filter_children: bool = True,
203
+ find_topmost: bool = False
204
+ ):
205
+ """
206
+ Find regions with the given specification in all parcellations offered
207
+ by the atlas.
208
+
209
+ Parameters
210
+ ----------
211
+ regionspec: str, regex
212
+ - a string with a possibly inexact name (matched both against the name and the identifier key)
213
+ - a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux, see at https://docs.python.org/3/library/re.html#flags)
214
+ - a regex applied to region names
215
+ all_versions : Bool, default: False
216
+ If True, matched regions for all versions of a parcellation are returned.
217
+ filter_children : bool, default: True
218
+ If False, children of matched parents will be returned.
219
+ find_topmost : bool, default: False
220
+ If True (requires `filter_children=True`), will return parent
221
+ structures if all children are matched, even though the parent
222
+ itself might not match the specification.
223
+
224
+ Returns
225
+ -------
226
+ list[Region]
227
+ list of regions matching to the regionspec
228
+ """
229
+ result = []
230
+ for p in self.parcellations:
231
+ if p.is_newest_version or all_versions:
232
+ result.extend(
233
+ p.find(
234
+ regionspec=regionspec,
235
+ filter_children=filter_children,
236
+ find_topmost=find_topmost
237
+ )
238
+ )
239
+ return result
siibra/core/concept.py ADDED
@@ -0,0 +1,308 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Parent class to siibra main concepts."""
16
+ from ..commons import (
17
+ create_key,
18
+ clear_name,
19
+ logger,
20
+ InstanceTable,
21
+ Species,
22
+ TypePublication
23
+ )
24
+ from ..retrieval import cache
25
+
26
+ import re
27
+ from typing import TypeVar, Type, Union, List, TYPE_CHECKING, Dict
28
+
29
+ T = TypeVar("T", bound="AtlasConcept")
30
+ _REGISTRIES: Dict[Type[T], InstanceTable[T]] = {}
31
+
32
+
33
+ @cache.Warmup.register_warmup_fn(is_factory=True)
34
+ def _atlas_concept_warmup():
35
+ return [cls.registry for cls in _REGISTRIES]
36
+
37
+
38
+ if TYPE_CHECKING:
39
+ from ..retrieval.datasets import EbrainsDataset
40
+ TypeDataset = EbrainsDataset
41
+
42
+
43
+ def get_registry(subclass_name: str):
44
+ subclasses = {c.__name__: c for c in _REGISTRIES}
45
+ if subclass_name in subclasses:
46
+ return subclasses[subclass_name].registry()
47
+ else:
48
+ logger.warn(f"No registry for atlas concepts named {subclass_name}")
49
+ return None
50
+
51
+
52
+ class AtlasConcept:
53
+ """
54
+ Parent class encapsulating commonalities of the basic siibra concept like atlas, parcellation, space, region.
55
+ These concepts have an id, name, and key, and they are bootstrapped from metadata stored in an online resources.
56
+ Typically, they are linked with one or more datasets that can be retrieved from the same or another online resource,
57
+ providing data files or additional metadata descriptions on request.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ identifier: str,
63
+ name: str,
64
+ species: Union[str, Species],
65
+ shortname: str = None,
66
+ description: str = None,
67
+ modality: str = "",
68
+ publications: List[TypePublication] = [],
69
+ datasets: List['TypeDataset'] = [],
70
+ spec=None,
71
+ prerelease: bool = False,
72
+ ):
73
+ """
74
+ Construct a new atlas concept base object.
75
+
76
+ Parameters
77
+ ----------
78
+ identifier : str
79
+ Unique identifier of the parcellation
80
+ name : str
81
+ Human-readable name of the parcellation
82
+ species: Species or string
83
+ Specification of the species
84
+ shortname: str
85
+ Shortform of human-readable name (optional)
86
+ description: str
87
+ Textual description of the parcellation
88
+ modality : str or None
89
+ Specification of the modality underlying this concept
90
+ datasets : list
91
+ list of datasets corresponding to this concept
92
+ publications: list
93
+ List of publications, each a dictionary with "doi" and/or "citation" fields
94
+ spec: dict, default: None
95
+ The preconfigured specification.
96
+ """
97
+ self._id = identifier
98
+ self.name = name if not prerelease else f"[PRERELEASE] {name}"
99
+ self._species_cached = None if species is None \
100
+ else Species.decode(species) # overwritable property implementation below
101
+ self.shortname = shortname
102
+ self.modality = modality
103
+ self._description = description
104
+ self._publications = publications
105
+ self.datasets = datasets
106
+ self._spec = spec
107
+ self._CACHED_MATCHES = {} # we cache match() function results
108
+ self._prerelease = prerelease
109
+
110
+ @property
111
+ def description(self):
112
+ if self._description:
113
+ return self._description
114
+ for ds in self.datasets:
115
+ if ds.description:
116
+ return ds.description
117
+ return ''
118
+
119
+ @property
120
+ def LICENSE(self) -> str:
121
+ licenses = []
122
+ for ds in self.datasets:
123
+ if ds.LICENSE is None or ds.LICENSE == "No license information is found.":
124
+ continue
125
+ if isinstance(ds.LICENSE, str):
126
+ licenses.append(ds.LICENSE)
127
+ if isinstance(ds.LICENSE, list):
128
+ licenses.extend(ds.LICENSE)
129
+ if len(licenses) == 0:
130
+ logger.warning("No license information is found.")
131
+ return ""
132
+ if len(licenses) > 1:
133
+ logger.info("Found multiple licenses corresponding to datasets.")
134
+ return '\n'.join(licenses)
135
+
136
+ @property
137
+ def urls(self) -> List[str]:
138
+ """The list of URLs (including DOIs) associated with this atlas concept."""
139
+ return [
140
+ url.get("url")
141
+ for ds in self.datasets
142
+ for url in ds.urls
143
+ ]
144
+
145
+ @property
146
+ def authors(self):
147
+ return [
148
+ contributer['name']
149
+ for ds in self.datasets
150
+ for contributer in ds.contributors
151
+ ]
152
+
153
+ @property
154
+ def publications(self) -> List[TypePublication]:
155
+ return [
156
+ *self._publications,
157
+ *[
158
+ {'citation': f"Dataset name: {ds.name}", 'url': url.get("url")}
159
+ for ds in self.datasets
160
+ for url in ds.urls
161
+ ]
162
+ ]
163
+
164
+ @property
165
+ def species(self) -> Species:
166
+ # Allow derived classes to implement a lazy loader (e.g. in Map)
167
+ if self._species_cached is None:
168
+ raise RuntimeError(f"No species defined for {self}.")
169
+ return self._species_cached
170
+
171
+ @classmethod
172
+ def registry(cls: Type[T]) -> InstanceTable[T]:
173
+ if cls._configuration_folder is None:
174
+ return None
175
+ if _REGISTRIES[cls] is None:
176
+ from ..configuration import Configuration
177
+ conf = Configuration()
178
+ # visit the configuration to provide a cleanup function
179
+ # in case the user changes the configuration during runtime.
180
+ Configuration.register_cleanup(cls.clear_registry)
181
+ assert cls._configuration_folder in conf.folders
182
+ objects = conf.build_objects(cls._configuration_folder)
183
+ logger.debug(f"Built {len(objects)} preconfigured {cls.__name__} objects.")
184
+ assert len(objects) > 0
185
+ assert all([hasattr(o, 'key') for o in objects])
186
+
187
+ # TODO Map.registry() returns InstanceTable that contains two different types, SparseMap and Map
188
+ # Since we take the objects[0].__class__.match, if the first element happen to be SparseMap, this could result.
189
+ # Code to reproduce:
190
+ """
191
+ import siibra
192
+ r = siibra.volumes.Map.registry()
193
+ """
194
+ if len({o.__class__ for o in objects}) > 1:
195
+ logger.warning(
196
+ f"{cls.__name__} registry contains multiple classes: "
197
+ f"{', '.join(list({o.__class__.__name__ for o in objects}))}"
198
+ )
199
+ assert hasattr(objects[0].__class__, "match") and callable(objects[0].__class__.match)
200
+ _REGISTRIES[cls] = InstanceTable(
201
+ elements={o.key: o for o in objects},
202
+ matchfunc=objects[0].__class__.match
203
+ )
204
+ return _REGISTRIES[cls]
205
+
206
+ @classmethod
207
+ def clear_registry(cls):
208
+ _REGISTRIES[cls] = None
209
+
210
+ @classmethod
211
+ def get_instance(cls, spec: str):
212
+ """
213
+ Parameters
214
+ ----------
215
+ spec: str
216
+ Specification of the class the instance is requested.
217
+ Returns
218
+ -------
219
+ an instance of this class matching the given specification from its
220
+ registry if possible, otherwise None.
221
+ Raises
222
+ ------
223
+ IndexError
224
+ If spec cannot match any instance
225
+ """
226
+ if cls.registry() is not None:
227
+ return cls.registry().get(spec)
228
+
229
+ @property
230
+ def id(self):
231
+ # allows derived classes to assign the id dynamically
232
+ return self._id
233
+
234
+ @property
235
+ def key(self):
236
+ return create_key(self.name)
237
+
238
+ def __init_subclass__(cls, configuration_folder: str = None):
239
+ """
240
+ This method is called whenever AtlasConcept gets subclassed
241
+ (see https://docs.python.org/3/reference/datamodel.html)
242
+ """
243
+ cls._configuration_folder = configuration_folder
244
+ _REGISTRIES[cls] = None
245
+ return super().__init_subclass__()
246
+
247
+ def __str__(self):
248
+ return self.name
249
+
250
+ def __repr__(self):
251
+ return f"<{self.__class__.__name__}(identifier='{self.id}', name='{self.name}', species='{self.species}')>"
252
+
253
+ def matches(self, spec) -> bool:
254
+ """
255
+ Parameters
256
+ ----------
257
+ spec: str
258
+ Specification checked within the concept name, key or id
259
+
260
+ Returns
261
+ -------
262
+ bool
263
+ Whether the given specification matches the name, key or id of the concept.
264
+ """
265
+ if spec not in self._CACHED_MATCHES:
266
+ self._CACHED_MATCHES[spec] = False
267
+ if isinstance(spec, self.__class__) and (spec == self):
268
+ self._CACHED_MATCHES[spec] = True
269
+ elif isinstance(spec, str):
270
+ if spec == self.key:
271
+ self._CACHED_MATCHES[spec] = True
272
+ elif spec == self.id:
273
+ self._CACHED_MATCHES[spec] = True
274
+ else:
275
+ # match the name
276
+ words = [w for w in re.split("[ -]", spec)]
277
+ squeezedname = clear_name(self.name.lower()).replace(" ", "")
278
+ self._CACHED_MATCHES[spec] = any(
279
+ [
280
+ all(w.lower() in squeezedname for w in words),
281
+ spec.replace(" ", "") in squeezedname,
282
+ ]
283
+ )
284
+ return self._CACHED_MATCHES[spec]
285
+
286
+ @classmethod
287
+ def match(cls, obj, spec) -> bool:
288
+ """Match a given object specification. """
289
+ assert isinstance(obj, cls)
290
+ return obj.matches(spec)
291
+
292
+ def __gt__(self, other: 'AtlasConcept'):
293
+ """
294
+ Compare this atlas concept with other atlas concepts of the same kind
295
+ with it's name.
296
+ """
297
+ if self.__class__ is not other.__class__:
298
+ raise ValueError("Cannot compare different atlas concept types.")
299
+ return self.name > other.name
300
+
301
+ def __lt__(self, other: 'AtlasConcept'):
302
+ """
303
+ Compare this atlas concept with other atlas concepts of the same kind
304
+ with it's name.
305
+ """
306
+ if self.__class__ is not other.__class__:
307
+ raise ValueError("Cannot compare different atlas concept types.")
308
+ return self.name < other.name