siibra 0.5a2__py3-none-any.whl → 1.0.0a1__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 (83) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +20 -12
  3. siibra/commons.py +145 -90
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +22 -17
  6. siibra/configuration/factory.py +177 -128
  7. siibra/core/__init__.py +1 -8
  8. siibra/core/{relation_qualification.py → assignment.py} +17 -14
  9. siibra/core/atlas.py +66 -35
  10. siibra/core/concept.py +81 -39
  11. siibra/core/parcellation.py +83 -67
  12. siibra/core/region.py +569 -263
  13. siibra/core/space.py +7 -39
  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 +16 -0
  22. siibra/explorer/url.py +112 -52
  23. siibra/explorer/util.py +31 -9
  24. siibra/features/__init__.py +73 -8
  25. siibra/features/anchor.py +75 -196
  26. siibra/features/connectivity/__init__.py +1 -1
  27. siibra/features/connectivity/functional_connectivity.py +2 -2
  28. siibra/features/connectivity/regional_connectivity.py +99 -10
  29. siibra/features/connectivity/streamline_counts.py +1 -1
  30. siibra/features/connectivity/streamline_lengths.py +1 -1
  31. siibra/features/connectivity/tracing_connectivity.py +1 -1
  32. siibra/features/dataset/__init__.py +1 -1
  33. siibra/features/dataset/ebrains.py +3 -3
  34. siibra/features/feature.py +219 -110
  35. siibra/features/image/__init__.py +1 -1
  36. siibra/features/image/image.py +21 -13
  37. siibra/features/image/sections.py +1 -1
  38. siibra/features/image/volume_of_interest.py +1 -1
  39. siibra/features/tabular/__init__.py +1 -1
  40. siibra/features/tabular/bigbrain_intensity_profile.py +24 -13
  41. siibra/features/tabular/cell_density_profile.py +111 -69
  42. siibra/features/tabular/cortical_profile.py +82 -16
  43. siibra/features/tabular/gene_expression.py +117 -6
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +7 -9
  45. siibra/features/tabular/layerwise_cell_density.py +9 -24
  46. siibra/features/tabular/receptor_density_fingerprint.py +11 -6
  47. siibra/features/tabular/receptor_density_profile.py +12 -15
  48. siibra/features/tabular/regional_timeseries_activity.py +74 -18
  49. siibra/features/tabular/tabular.py +17 -8
  50. siibra/livequeries/__init__.py +1 -7
  51. siibra/livequeries/allen.py +139 -77
  52. siibra/livequeries/bigbrain.py +104 -128
  53. siibra/livequeries/ebrains.py +7 -4
  54. siibra/livequeries/query.py +1 -2
  55. siibra/locations/__init__.py +32 -25
  56. siibra/locations/boundingbox.py +153 -127
  57. siibra/locations/location.py +45 -80
  58. siibra/locations/point.py +97 -83
  59. siibra/locations/pointcloud.py +349 -0
  60. siibra/retrieval/__init__.py +1 -1
  61. siibra/retrieval/cache.py +107 -13
  62. siibra/retrieval/datasets.py +9 -14
  63. siibra/retrieval/exceptions/__init__.py +2 -1
  64. siibra/retrieval/repositories.py +147 -53
  65. siibra/retrieval/requests.py +64 -29
  66. siibra/vocabularies/__init__.py +2 -2
  67. siibra/volumes/__init__.py +7 -9
  68. siibra/volumes/parcellationmap.py +396 -253
  69. siibra/volumes/providers/__init__.py +20 -0
  70. siibra/volumes/providers/freesurfer.py +113 -0
  71. siibra/volumes/{gifti.py → providers/gifti.py} +29 -18
  72. siibra/volumes/{neuroglancer.py → providers/neuroglancer.py} +204 -92
  73. siibra/volumes/{nifti.py → providers/nifti.py} +64 -44
  74. siibra/volumes/providers/provider.py +107 -0
  75. siibra/volumes/sparsemap.py +159 -260
  76. siibra/volumes/volume.py +720 -152
  77. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/METADATA +25 -28
  78. siibra-1.0.0a1.dist-info/RECORD +84 -0
  79. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/WHEEL +1 -1
  80. siibra/locations/pointset.py +0 -198
  81. siibra-0.5a2.dist-info/RECORD +0 -74
  82. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/LICENSE +0 -0
  83. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2023
1
+ # Copyright 2018-2024
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,11 +12,13 @@
12
12
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
- """Qualification between two arbitary concepts"""
15
+ """Qualification between two BrainStructures"""
16
16
 
17
17
  from enum import Enum
18
- from typing import Dict, TypeVar, Generic
19
18
  from dataclasses import dataclass
19
+ from typing import Dict, Generic, TypeVar, TYPE_CHECKING
20
+ if TYPE_CHECKING:
21
+ from .structure import BrainStructure
20
22
 
21
23
  T = TypeVar("T")
22
24
 
@@ -43,14 +45,14 @@ class Qualification(Enum):
43
45
  Qualification.CONTAINS: 'contains',
44
46
  Qualification.APPROXIMATE: 'approximates to',
45
47
  Qualification.HOMOLOGOUS: 'is homologous to',
46
- Qualification.OTHER_VERSION: 'is another version of',
48
+ Qualification.OTHER_VERSION: 'is another version of'
47
49
  }
48
- assert self in transl, f"{str(self)} verb cannot be found!"
50
+ assert self in transl, f"{str(self)} verb cannot be found."
49
51
  return transl[self]
50
52
 
51
53
  def invert(self):
52
54
  """
53
- Return a MatchPrecision object with the inverse meaning
55
+ Return qualification with the inverse meaning
54
56
  """
55
57
  inverses = {
56
58
  Qualification.EXACT: Qualification.EXACT,
@@ -61,7 +63,7 @@ class Qualification(Enum):
61
63
  Qualification.HOMOLOGOUS: Qualification.HOMOLOGOUS,
62
64
  Qualification.OTHER_VERSION: Qualification.OTHER_VERSION,
63
65
  }
64
- assert self in inverses, f"{str(self)} inverses cannot be found"
66
+ assert self in inverses, f"{str(self)} inverses cannot be found."
65
67
  return inverses[self]
66
68
 
67
69
  def __str__(self):
@@ -79,9 +81,10 @@ class Qualification(Enum):
79
81
 
80
82
 
81
83
  @dataclass
82
- class RelationAssignment(Generic[T]):
83
- query_structure: T
84
- assigned_structure: T
84
+ class AnatomicalAssignment(Generic[T]):
85
+ """Represents a qualified assignment between anatomical structures."""
86
+ query_structure: "BrainStructure"
87
+ assigned_structure: "BrainStructure"
85
88
  qualification: Qualification
86
89
  explanation: str = ""
87
90
 
@@ -94,14 +97,14 @@ class RelationAssignment(Generic[T]):
94
97
  return msg if self.explanation == "" else f"{msg} - {self.explanation}"
95
98
 
96
99
  def invert(self):
97
- return RelationAssignment(
100
+ return AnatomicalAssignment(
98
101
  self.assigned_structure,
99
102
  self.query_structure,
100
103
  self.qualification.invert(),
101
104
  self.explanation
102
105
  )
103
106
 
104
- def __lt__(self, other: 'RelationAssignment'):
105
- if not isinstance(other, RelationAssignment):
106
- raise ValueError(f"Cannot compare RelationAssignment with instances of '{type(other)}'")
107
+ def __lt__(self, other: 'AnatomicalAssignment'):
108
+ if not isinstance(other, AnatomicalAssignment):
109
+ raise ValueError(f"Cannot compare AnatomicalAssignment with instances of '{type(other)}'")
107
110
  return self.qualification.value < other.qualification.value
siibra/core/atlas.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
1
+ # Copyright 2018-2024
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,14 +30,15 @@ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
30
30
  spaces, as well as common functionalities of those.
31
31
  """
32
32
 
33
- def __init__(self, identifier: str, name: str, species: Species):
33
+ def __init__(self, identifier: str, name: str, species: Species, **kwargs):
34
34
  """Construct an empty atlas object with a name and identifier."""
35
35
 
36
36
  concept.AtlasConcept.__init__(
37
37
  self,
38
38
  identifier=identifier,
39
39
  name=name,
40
- species=species
40
+ species=species,
41
+ **kwargs
41
42
  )
42
43
  self._parcellation_ids: List[str] = []
43
44
  self._space_ids: List[str] = []
@@ -64,9 +65,20 @@ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
64
65
  matchfunc=_parcellation.Parcellation.match,
65
66
  )
66
67
 
67
- def get_parcellation(self, parcellation=None):
68
- """Returns a valid parcellation object defined by the atlas.
69
- If no specification is provided, the default is returned."""
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
+ """
70
82
 
71
83
  if parcellation is None:
72
84
  parcellation_obj = self.parcellations[self._parcellation_ids[0]]
@@ -80,12 +92,19 @@ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
80
92
 
81
93
  return self.parcellations[parcellation]
82
94
 
83
- def get_space(self, space=None):
84
- """Returns a valid reference space object defined by the atlas.
85
- If no specification is provided, the default is returned.
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
86
104
 
87
- Parameters:
88
- space: Space, or string specification of a space
105
+ Returns
106
+ -------
107
+ Space
89
108
  """
90
109
  if space is None:
91
110
  space_obj = self.spaces[self._space_ids[0]]
@@ -105,12 +124,13 @@ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
105
124
  parcellation: _parcellation.Parcellation = None,
106
125
  maptype: MapType = MapType.LABELLED,
107
126
  ):
108
- """Returns a parcellation map in the given space.
127
+ """
128
+ Returns a parcellation map in the given space.
109
129
 
110
130
  Parameters
111
131
  ----------
112
132
 
113
- space : Space
133
+ space: Space
114
134
  The requested reference space. If None, the default is used.
115
135
  parcellation: Parcellation
116
136
  The requested parcellation. If None, the default is used.
@@ -160,39 +180,46 @@ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
160
180
  def get_voi(self, space: _space.Space, point1: tuple, point2: tuple):
161
181
  """Get a volume of interest spanned by two points in the given reference space.
162
182
 
163
- Args:
164
- space (Space or str): The target reference space, or a string specification of the space
165
- point1 (Tuple): A 3D coordinate given in this reference space
166
- point2 (Tuple): Another 3D coordinate given in this reference space
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
167
191
 
168
- Returns:
169
- Bounding Box
192
+ Returns
193
+ -------
194
+ BoundingBox
170
195
  """
171
- return self.get_space(space).get_bounding_box(point1, point2)
196
+ return self.get_template(space).get_boundingbox(point1, point2)
172
197
 
173
198
  def find_regions(
174
199
  self,
175
- regionspec,
176
- all_versions=False,
177
- filter_children=True,
178
- **kwargs
200
+ regionspec: str,
201
+ all_versions: bool = False,
202
+ filter_children: bool = True,
203
+ find_topmost: bool = False
179
204
  ):
180
205
  """
181
- Find regions with the given specification in all
182
- parcellations offered by the atlas. Additional kwargs
183
- are passed on to Parcellation.find().
206
+ Find regions with the given specification in all parcellations offered
207
+ by the atlas.
184
208
 
185
209
  Parameters
186
210
  ----------
187
- regionspec: str, regex, int, Region, MapIndex
211
+ regionspec: str, regex
188
212
  - a string with a possibly inexact name (matched both against the name and the identifier key)
189
- - a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux)
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)
190
214
  - a regex applied to region names
191
- - a Region object
192
215
  all_versions : Bool, default: False
193
216
  If True, matched regions for all versions of a parcellation are returned.
194
217
  filter_children : bool, default: True
195
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.
196
223
 
197
224
  Returns
198
225
  -------
@@ -200,9 +227,13 @@ class Atlas(concept.AtlasConcept, configuration_folder="atlases"):
200
227
  list of regions matching to the regionspec
201
228
  """
202
229
  result = []
203
- for p in self._parcellation_ids:
204
- parcobj = _parcellation.Parcellation.get_instance(p)
205
- if parcobj.is_newest_version or all_versions:
206
- match = parcobj.find(regionspec, filter_children=filter_children, **kwargs)
207
- result.extend(match)
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
+ )
208
239
  return result
siibra/core/concept.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
1
+ # Copyright 2018-2024
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,11 +21,18 @@ from ..commons import (
21
21
  Species,
22
22
  TypePublication
23
23
  )
24
+ from ..retrieval import cache
24
25
 
25
26
  import re
26
- from typing import TypeVar, Type, Union, List, TYPE_CHECKING
27
+ from typing import TypeVar, Type, Union, List, TYPE_CHECKING, Dict
27
28
 
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]
29
36
 
30
37
 
31
38
  if TYPE_CHECKING:
@@ -33,6 +40,15 @@ if TYPE_CHECKING:
33
40
  TypeDataset = EbrainsDataset
34
41
 
35
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
+
36
52
  class AtlasConcept:
37
53
  """
38
54
  Parent class encapsulating commonalities of the basic siibra concept like atlas, parcellation, space, region.
@@ -51,7 +67,8 @@ class AtlasConcept:
51
67
  modality: str = "",
52
68
  publications: List[TypePublication] = [],
53
69
  datasets: List['TypeDataset'] = [],
54
- spec=None
70
+ spec=None,
71
+ prerelease: bool = False,
55
72
  ):
56
73
  """
57
74
  Construct a new atlas concept base object.
@@ -78,7 +95,7 @@ class AtlasConcept:
78
95
  The preconfigured specification.
79
96
  """
80
97
  self._id = identifier
81
- self.name = name
98
+ self.name = name if not prerelease else f"[PRERELEASE] {name}"
82
99
  self._species_cached = None if species is None \
83
100
  else Species.decode(species) # overwritable property implementation below
84
101
  self.shortname = shortname
@@ -87,6 +104,8 @@ class AtlasConcept:
87
104
  self._publications = publications
88
105
  self.datasets = datasets
89
106
  self._spec = spec
107
+ self._CACHED_MATCHES = {} # we cache match() function results
108
+ self._prerelease = prerelease
90
109
 
91
110
  @property
92
111
  def description(self):
@@ -99,15 +118,29 @@ class AtlasConcept:
99
118
 
100
119
  @property
101
120
  def LICENSE(self) -> str:
102
- return '\n'.join([ds.LICENSE for ds in self.datasets])
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)
103
135
 
104
136
  @property
105
- def doi_or_url(self) -> str:
106
- return '\n'.join([
137
+ def urls(self) -> List[str]:
138
+ """The list of URLs (including DOIs) associated with this atlas concept."""
139
+ return [
107
140
  url.get("url")
108
141
  for ds in self.datasets
109
142
  for url in ds.urls
110
- ])
143
+ ]
111
144
 
112
145
  @property
113
146
  def authors(self):
@@ -139,7 +172,7 @@ class AtlasConcept:
139
172
  def registry(cls: Type[T]) -> InstanceTable[T]:
140
173
  if cls._configuration_folder is None:
141
174
  return None
142
- if cls._registry_cached is None:
175
+ if _REGISTRIES[cls] is None:
143
176
  from ..configuration import Configuration
144
177
  conf = Configuration()
145
178
  # visit the configuration to provide a cleanup function
@@ -159,17 +192,20 @@ class AtlasConcept:
159
192
  r = siibra.volumes.Map.registry()
160
193
  """
161
194
  if len({o.__class__ for o in objects}) > 1:
162
- logger.warning(f"{cls.__name__} registry contains multiple classes: {', '.join(list({o.__class__.__name__ for o in objects}))}")
195
+ logger.warning(
196
+ f"{cls.__name__} registry contains multiple classes: "
197
+ f"{', '.join(list({o.__class__.__name__ for o in objects}))}"
198
+ )
163
199
  assert hasattr(objects[0].__class__, "match") and callable(objects[0].__class__.match)
164
- cls._registry_cached = InstanceTable(
200
+ _REGISTRIES[cls] = InstanceTable(
165
201
  elements={o.key: o for o in objects},
166
202
  matchfunc=objects[0].__class__.match
167
203
  )
168
- return cls._registry_cached
204
+ return _REGISTRIES[cls]
169
205
 
170
206
  @classmethod
171
207
  def clear_registry(cls):
172
- cls._registry_cached = None
208
+ _REGISTRIES[cls] = None
173
209
 
174
210
  @classmethod
175
211
  def get_instance(cls, spec: str):
@@ -204,45 +240,51 @@ class AtlasConcept:
204
240
  This method is called whenever AtlasConcept gets subclassed
205
241
  (see https://docs.python.org/3/reference/datamodel.html)
206
242
  """
207
- cls._registry_cached = None
208
243
  cls._configuration_folder = configuration_folder
244
+ _REGISTRIES[cls] = None
209
245
  return super().__init_subclass__()
210
246
 
211
247
  def __str__(self):
212
- return f"{self.__class__.__name__}: {self.name}"
248
+ return self.name
249
+
250
+ def __repr__(self):
251
+ return f"<{self.__class__.__name__}(identifier='{self.id}', name='{self.name}', species='{self.species}')>"
213
252
 
214
- def matches(self, spec):
253
+ def matches(self, spec) -> bool:
215
254
  """
216
255
  Parameters
217
256
  ----------
218
- spec: str
219
- Specification checked within the concept name, key or id
257
+ spec: str
258
+ Specification checked within the concept name, key or id
259
+
220
260
  Returns
221
261
  -------
222
- bool
223
- Whether the given specification matches the name, key or id of the concept.
262
+ bool
263
+ Whether the given specification matches the name, key or id of the concept.
224
264
  """
225
- if isinstance(spec, self.__class__) and (spec == self):
226
- return True
227
- elif isinstance(spec, str):
228
- if spec == self.key:
229
- return True
230
- elif spec == self.id:
231
- return True
232
- else:
233
- # match the name
234
- words = [w for w in re.split("[ -]", spec)]
235
- squeezedname = clear_name(self.name.lower()).replace(" ", "")
236
- return any(
237
- [
238
- all(w.lower() in squeezedname for w in words),
239
- spec.replace(" ", "") in squeezedname,
240
- ]
241
- )
242
- return False
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]
243
285
 
244
286
  @classmethod
245
- def match(cls, obj, spec):
287
+ def match(cls, obj, spec) -> bool:
246
288
  """Match a given object specification. """
247
289
  assert isinstance(obj, cls)
248
290
  return obj.matches(spec)