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.
- siibra/VERSION +1 -0
- siibra/__init__.py +164 -0
- siibra/commons.py +823 -0
- siibra/configuration/__init__.py +17 -0
- siibra/configuration/configuration.py +189 -0
- siibra/configuration/factory.py +589 -0
- siibra/core/__init__.py +16 -0
- siibra/core/assignment.py +110 -0
- siibra/core/atlas.py +239 -0
- siibra/core/concept.py +308 -0
- siibra/core/parcellation.py +387 -0
- siibra/core/region.py +1223 -0
- siibra/core/space.py +131 -0
- siibra/core/structure.py +111 -0
- siibra/exceptions.py +63 -0
- siibra/experimental/__init__.py +19 -0
- siibra/experimental/contour.py +61 -0
- siibra/experimental/cortical_profile_sampler.py +57 -0
- siibra/experimental/patch.py +98 -0
- siibra/experimental/plane3d.py +256 -0
- siibra/explorer/__init__.py +17 -0
- siibra/explorer/url.py +222 -0
- siibra/explorer/util.py +87 -0
- siibra/features/__init__.py +117 -0
- siibra/features/anchor.py +224 -0
- siibra/features/connectivity/__init__.py +33 -0
- siibra/features/connectivity/functional_connectivity.py +57 -0
- siibra/features/connectivity/regional_connectivity.py +494 -0
- siibra/features/connectivity/streamline_counts.py +27 -0
- siibra/features/connectivity/streamline_lengths.py +27 -0
- siibra/features/connectivity/tracing_connectivity.py +30 -0
- siibra/features/dataset/__init__.py +17 -0
- siibra/features/dataset/ebrains.py +90 -0
- siibra/features/feature.py +970 -0
- siibra/features/image/__init__.py +27 -0
- siibra/features/image/image.py +115 -0
- siibra/features/image/sections.py +26 -0
- siibra/features/image/volume_of_interest.py +88 -0
- siibra/features/tabular/__init__.py +24 -0
- siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
- siibra/features/tabular/cell_density_profile.py +298 -0
- siibra/features/tabular/cortical_profile.py +322 -0
- siibra/features/tabular/gene_expression.py +257 -0
- siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
- siibra/features/tabular/layerwise_cell_density.py +95 -0
- siibra/features/tabular/receptor_density_fingerprint.py +192 -0
- siibra/features/tabular/receptor_density_profile.py +110 -0
- siibra/features/tabular/regional_timeseries_activity.py +294 -0
- siibra/features/tabular/tabular.py +139 -0
- siibra/livequeries/__init__.py +19 -0
- siibra/livequeries/allen.py +352 -0
- siibra/livequeries/bigbrain.py +197 -0
- siibra/livequeries/ebrains.py +145 -0
- siibra/livequeries/query.py +49 -0
- siibra/locations/__init__.py +91 -0
- siibra/locations/boundingbox.py +454 -0
- siibra/locations/location.py +115 -0
- siibra/locations/point.py +344 -0
- siibra/locations/pointcloud.py +349 -0
- siibra/retrieval/__init__.py +27 -0
- siibra/retrieval/cache.py +233 -0
- siibra/retrieval/datasets.py +389 -0
- siibra/retrieval/exceptions/__init__.py +27 -0
- siibra/retrieval/repositories.py +769 -0
- siibra/retrieval/requests.py +659 -0
- siibra/vocabularies/__init__.py +45 -0
- siibra/vocabularies/gene_names.json +29176 -0
- siibra/vocabularies/receptor_symbols.json +210 -0
- siibra/vocabularies/region_aliases.json +460 -0
- siibra/volumes/__init__.py +23 -0
- siibra/volumes/parcellationmap.py +1279 -0
- siibra/volumes/providers/__init__.py +20 -0
- siibra/volumes/providers/freesurfer.py +113 -0
- siibra/volumes/providers/gifti.py +165 -0
- siibra/volumes/providers/neuroglancer.py +736 -0
- siibra/volumes/providers/nifti.py +266 -0
- siibra/volumes/providers/provider.py +107 -0
- siibra/volumes/sparsemap.py +468 -0
- siibra/volumes/volume.py +892 -0
- siibra-1.0.0a1.dist-info/LICENSE +201 -0
- siibra-1.0.0a1.dist-info/METADATA +160 -0
- siibra-1.0.0a1.dist-info/RECORD +84 -0
- siibra-1.0.0a1.dist-info/WHEEL +5 -0
- 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
|