siibra 0.4a35__py3-none-any.whl → 0.4a46__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 (35) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +1 -0
  3. siibra/commons.py +38 -17
  4. siibra/configuration/configuration.py +21 -17
  5. siibra/configuration/factory.py +38 -12
  6. siibra/core/atlas.py +11 -8
  7. siibra/core/concept.py +22 -2
  8. siibra/core/parcellation.py +42 -22
  9. siibra/core/region.py +56 -95
  10. siibra/features/anchor.py +7 -4
  11. siibra/features/connectivity/functional_connectivity.py +8 -1
  12. siibra/features/connectivity/regional_connectivity.py +14 -19
  13. siibra/features/dataset/ebrains.py +1 -1
  14. siibra/features/feature.py +193 -29
  15. siibra/features/image/__init__.py +1 -1
  16. siibra/features/image/image.py +1 -0
  17. siibra/features/image/volume_of_interest.py +14 -5
  18. siibra/features/tabular/__init__.py +2 -0
  19. siibra/features/tabular/regional_timeseries_activity.py +213 -0
  20. siibra/livequeries/ebrains.py +2 -3
  21. siibra/locations/location.py +4 -3
  22. siibra/locations/pointset.py +2 -2
  23. siibra/retrieval/datasets.py +73 -3
  24. siibra/retrieval/repositories.py +17 -6
  25. siibra/retrieval/requests.py +68 -61
  26. siibra/volumes/neuroglancer.py +9 -9
  27. siibra/volumes/nifti.py +4 -5
  28. siibra/volumes/parcellationmap.py +157 -97
  29. siibra/volumes/sparsemap.py +27 -31
  30. siibra/volumes/volume.py +1 -1
  31. {siibra-0.4a35.dist-info → siibra-0.4a46.dist-info}/METADATA +2 -1
  32. {siibra-0.4a35.dist-info → siibra-0.4a46.dist-info}/RECORD +35 -34
  33. {siibra-0.4a35.dist-info → siibra-0.4a46.dist-info}/WHEEL +1 -1
  34. {siibra-0.4a35.dist-info → siibra-0.4a46.dist-info}/LICENSE +0 -0
  35. {siibra-0.4a35.dist-info → siibra-0.4a46.dist-info}/top_level.txt +0 -0
siibra/core/region.py CHANGED
@@ -20,11 +20,11 @@ from ..volumes import parcellationmap
20
20
 
21
21
  from ..commons import (
22
22
  logger,
23
- MapIndex,
24
23
  MapType,
25
24
  affine_scaling,
26
25
  create_key,
27
26
  clear_name,
27
+ siibra_tqdm,
28
28
  InstanceTable,
29
29
  SIIBRA_DEFAULT_MAPTYPE,
30
30
  SIIBRA_DEFAULT_MAP_THRESHOLD
@@ -36,12 +36,25 @@ import anytree
36
36
  from typing import List, Set, Union
37
37
  from nibabel import Nifti1Image
38
38
  from difflib import SequenceMatcher
39
+ from dataclasses import dataclass, field
39
40
 
40
41
 
41
42
  REGEX_TYPE = type(re.compile("test"))
42
43
 
43
44
  THRESHOLD_STATISTICAL_MAPS = None
44
45
 
46
+ @dataclass
47
+ class SpatialPropCmpt:
48
+ centroid: point.Point
49
+ volume: int
50
+
51
+
52
+ @dataclass
53
+ class SpatialProp:
54
+ cog:SpatialPropCmpt=None
55
+ components:List[SpatialPropCmpt]=field(default_factory=list)
56
+ space:_space.Space=None
57
+
45
58
 
46
59
  class Region(anytree.NodeMixin, concept.AtlasConcept):
47
60
  """
@@ -153,13 +166,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
153
166
  c.parent = region
154
167
  return region
155
168
 
156
- @property
157
- def labels(self):
158
- return {r.index.label for r in self if r.index.label is not None} # Potenially a BUG
159
-
160
169
  @property
161
170
  def names(self):
162
- return {r.key for r in self}
171
+ return {r.name for r in self}
163
172
 
164
173
  def __eq__(self, other):
165
174
  """
@@ -171,9 +180,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
171
180
  elif isinstance(other, str):
172
181
  return any([self.name == other, self.key == other, self.id == other])
173
182
  else:
174
- raise ValueError(
175
- f"Cannot compare object of type {type(other)} to {self.__class__.__name__}"
176
- )
183
+ return False
177
184
 
178
185
  def __hash__(self):
179
186
  return hash(self.id)
@@ -207,25 +214,24 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
207
214
 
208
215
  Parameters
209
216
  ----------
210
- regionspec: str, regex, int, Region, MapIndex
217
+ regionspec: str, regex, int, Region
211
218
  - a string with a possibly inexact name (matched both against the name and the identifier key)
212
219
  - a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux)
213
220
  - a regex applied to region names
214
- - an integer (interpreted as a labelindex)
215
221
  - a Region object
216
- - a full MapIndex object
217
222
  filter_children : bool, default: False
218
223
  If True, children of matched parents will not be returned
219
224
  find_topmost : bool, default: True
220
- If True, will return parent structures if all children are matched,
221
- even though the parent itself might not match the specification.
225
+ If True (requires `filter_children=True`), will return parent
226
+ structures if all children are matched, even though the parent
227
+ itself might not match the specification.
222
228
  Returns
223
229
  -------
224
230
  list[Region]
225
231
  list of regions matching to the regionspec
226
232
  Tip
227
233
  ---
228
- See example 01-003 find regions
234
+ See example 01-003, find regions.
229
235
  """
230
236
  key = (regionspec, filter_children, find_topmost)
231
237
  MEM = self._CACHED_REGION_SEARCHES
@@ -244,40 +250,19 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
244
250
  search_regex = (f"(?{flags})" if flags else "") + expression
245
251
  regionspec = re.compile(search_regex)
246
252
 
247
- if regionspec in self.names:
248
- # key is given, this gives us an exact region
249
- match = anytree.search.find_by_attr(self, regionspec, name="key")
250
- MEM[key] = [] if match is None else [match]
251
- return list(MEM[key])
252
-
253
253
  candidates = list(
254
- set(anytree.search.findall(self, lambda node: node.matches(regionspec)))
254
+ anytree.search.findall(self, lambda node: node.matches(regionspec))
255
255
  )
256
256
 
257
257
  if len(candidates) > 1 and filter_children:
258
-
259
258
  filtered = []
260
259
  for region in candidates:
261
260
  children_included = [c for c in region.children if c in candidates]
262
- # if the parcellation index matches only approximately,
263
- # while a child has an exact matching index, use the child.
264
261
  if len(children_included) > 0:
265
- if not (
266
- isinstance(regionspec, MapIndex)
267
- and (region.index != regionspec)
268
- and any(c.index == regionspec for c in children_included)
269
- ):
270
- filtered.append(region)
262
+ filtered.append(region)
271
263
  else:
272
264
  if region.parent not in candidates:
273
265
  filtered.append(region)
274
- else:
275
- if (
276
- isinstance(regionspec, MapIndex)
277
- and (region.index == regionspec)
278
- and (region.parent.index != regionspec)
279
- ):
280
- filtered.append(region)
281
266
 
282
267
  # find any non-matched regions of which all children are matched
283
268
  if find_topmost:
@@ -295,7 +280,10 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
295
280
  else:
296
281
  # filter child regions again
297
282
  filtered += complete_parents
298
- candidates = [r for r in filtered if r.parent not in filtered]
283
+ candidates = [
284
+ r for r in filtered
285
+ if (r.parent not in filtered) or r == regionspec
286
+ ]
299
287
  else:
300
288
  candidates = filtered
301
289
 
@@ -307,12 +295,12 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
307
295
  else:
308
296
  candidates = list(candidates)
309
297
 
310
- found_regions = sorted(candidates, key=lambda r: r.depth)
298
+ found_regions = sorted(set(candidates), key=lambda r: r.depth)
311
299
 
312
300
  # reverse is set to True, since SequenceMatcher().ratio(), higher == better
313
301
  MEM[key] = (
314
302
  sorted(
315
- set(found_regions),
303
+ found_regions,
316
304
  reverse=True,
317
305
  key=lambda region: SequenceMatcher(None, str(region), regionspec).ratio(),
318
306
  )
@@ -429,14 +417,12 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
429
417
  fetch_space = _space.Space.get_instance(fetch_space)
430
418
 
431
419
  for m in parcellationmap.Map.registry():
432
- if all(
433
- [
434
- m.space.matches(fetch_space),
435
- m.parcellation == self.parcellation,
436
- m.provides_image,
437
- m.maptype == maptype,
438
- self.name in m.regions
439
- ]
420
+ if (
421
+ m.space.matches(fetch_space) and
422
+ m.parcellation == self.parcellation and
423
+ m.provides_image and
424
+ m.maptype == maptype and
425
+ self.name in m.regions
440
426
  ):
441
427
  result = m.fetch(region=self, format='image')
442
428
  if (maptype == MapType.STATISTICAL) and (threshold is not None):
@@ -451,7 +437,12 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
451
437
  dataobj = None
452
438
  affine = None
453
439
  if all(c.mapped_in_space(fetch_space) for c in self.children):
454
- for c in self.children:
440
+ for c in siibra_tqdm(
441
+ self.children,
442
+ desc=f"Building mask of {self.name}",
443
+ leave=False,
444
+ unit=" child region"
445
+ ):
455
446
  mask = c.fetch_regional_map(fetch_space, maptype, threshold)
456
447
  if dataobj is None:
457
448
  dataobj = np.asanyarray(mask.dataobj)
@@ -536,41 +527,6 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
536
527
  elements={s.key: s for s in self.supported_spaces},
537
528
  )
538
529
 
539
- def __getitem__(self, labelindex):
540
- """
541
- Given an integer label index, return the corresponding region.
542
-
543
- If multiple matches are found, return the unique parent if possible.
544
- Otherwise, returns None.
545
-
546
- Parameters
547
- ----------
548
- regionlabel: int
549
- Label index of the desired region.
550
- Returns
551
- -------
552
- Region
553
- """
554
- if not isinstance(labelindex, int):
555
- raise TypeError(
556
- "Index access into the regiontree expects label indices of integer type"
557
- )
558
-
559
- # first test this head node
560
- if self.index.label == labelindex:
561
- return self
562
-
563
- # Consider children, and return the one with smallest depth
564
- matches = list(
565
- filter(lambda x: x is not None, [c[labelindex] for c in self.children])
566
- )
567
- if matches:
568
- parentmatches = [m for m in matches if m.parent not in matches]
569
- if len(parentmatches) == 1:
570
- return parentmatches[0]
571
-
572
- return None
573
-
574
530
  def __str__(self):
575
531
  return self.name
576
532
 
@@ -626,7 +582,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
626
582
  maptype=maptype,
627
583
  threshold=threshold_statistical,
628
584
  )
629
- logger.warn(
585
+ logger.warning(
630
586
  f"No bounding box for {self.name} defined in {spaceobj.name}, "
631
587
  f"will warp the bounding box from {other_space.name} instead."
632
588
  )
@@ -657,7 +613,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
657
613
  """
658
614
  props = self.spatial_props(space)
659
615
  return pointset.PointSet(
660
- [tuple(c["centroid"]) for c in props["components"] if "centroid" in c],
616
+ [c.centroid for c in props.components],
661
617
  space=space
662
618
  )
663
619
 
@@ -666,7 +622,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
666
622
  space: _space.Space,
667
623
  maptype: MapType = MapType.LABELLED,
668
624
  threshold_statistical=None,
669
- ):
625
+ ) -> SpatialProp:
670
626
  """
671
627
  Compute spatial properties for connected components of this region in the given space.
672
628
 
@@ -687,14 +643,15 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
687
643
  Dict
688
644
  Dictionary of region's spatial properties
689
645
  """
690
- result = {"space": space, "components": []}
691
646
  from skimage import measure
692
647
 
693
648
  if not isinstance(space, _space.Space):
694
649
  space = _space.Space.get_instance(space)
695
650
 
651
+ result = SpatialProp(space=space)
652
+
696
653
  if not self.mapped_in_space(space):
697
- logger.warn(
654
+ logger.warning(
698
655
  f"Spatial properties of {self.name} cannot be computed in {space.name}. "
699
656
  "This region is only mapped in these spaces: "
700
657
  f"{', '.join(s.name for s in self.supported_spaces)}"
@@ -715,14 +672,18 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
715
672
 
716
673
  # compute spatial properties of each connected component
717
674
  for label in range(1, C.max() + 1):
718
- props = {}
719
675
  nonzero = np.c_[np.nonzero(C == label)]
720
- props["centroid"] = point.Point(
721
- np.dot(pimg.affine, np.r_[nonzero.mean(0), 1])[:3], space=space
676
+ result.components.append(
677
+ SpatialPropCmpt(
678
+ centroid=point.Point(
679
+ np.dot(pimg.affine, np.r_[nonzero.mean(0), 1])[:3], space=space
680
+ ),
681
+ volume=nonzero.shape[0] * scale,
682
+ )
722
683
  )
723
- props["volume"] = nonzero.shape[0] * scale
724
684
 
725
- result["components"].append(props)
685
+ # sort by volume
686
+ result.components.sort(key=lambda cmp: cmp.volume, reverse=True)
726
687
 
727
688
  return result
728
689
 
siibra/features/anchor.py CHANGED
@@ -33,6 +33,7 @@ class AssignmentQualification(Enum):
33
33
  OVERLAPS = 2
34
34
  CONTAINED = 3
35
35
  CONTAINS = 4
36
+ APPROXIMATE = 5
36
37
 
37
38
  @property
38
39
  def verb(self):
@@ -45,6 +46,7 @@ class AssignmentQualification(Enum):
45
46
  'OVERLAPS': 'overlaps with',
46
47
  'CONTAINED': 'is contained in',
47
48
  'CONTAINS': 'contains',
49
+ 'APPROXIMATE': 'approximates to',
48
50
  }
49
51
  return transl[self.name]
50
52
 
@@ -57,6 +59,7 @@ class AssignmentQualification(Enum):
57
59
  "OVERLAPS": "OVERLAPS",
58
60
  "CONTAINED": "CONTAINS",
59
61
  "CONTAINS": "CONTAINED",
62
+ "APPROXIMATE": "APPROXIMATE",
60
63
  }
61
64
  return AssignmentQualification[inverses[self.name]]
62
65
 
@@ -158,10 +161,10 @@ class AnatomicalAnchor:
158
161
  @property
159
162
  def region_aliases(self):
160
163
  if self._aliases_cached is None:
161
- self._aliases_cached = {
162
- k: v
164
+ self._aliases_cached: Dict[str, Dict[str, str]] = {
165
+ species_str: region_alias_mapping
163
166
  for s in self.species
164
- for k, v in REGION_ALIASES.get(str(s), {}).get(self._regionspec, {}).items()
167
+ for species_str, region_alias_mapping in REGION_ALIASES.get(str(s), {}).get(self._regionspec, {}).items()
165
168
  }
166
169
  return self._aliases_cached
167
170
 
@@ -192,7 +195,7 @@ class AnatomicalAnchor:
192
195
  for regionspec, qualificationspec in aliases.items():
193
196
  for r in Parcellation.find_regions(regionspec, alt_species):
194
197
  if r not in self._regions_cached:
195
- regions[r] = qualificationspec
198
+ regions[r] = AssignmentQualification[qualificationspec.upper()]
196
199
  self.__class__._MATCH_MEMO[self._regionspec] = regions
197
200
  self._regions_cached = self.__class__._MATCH_MEMO[self._regionspec]
198
201
 
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
 
16
16
  from . import regional_connectivity
17
-
17
+ from hashlib import md5
18
18
 
19
19
  class FunctionalConnectivity(
20
20
  regional_connectivity.RegionalConnectivity,
@@ -26,3 +26,10 @@ class FunctionalConnectivity(
26
26
  def __init__(self, paradigm: str, **kwargs):
27
27
  regional_connectivity.RegionalConnectivity.__init__(self, **kwargs)
28
28
  self.paradigm = paradigm
29
+
30
+ # paradign is used to distinguish functional connectivity features from each other.
31
+ assert self.paradigm, f"Functional connectivity must have paradigm defined!"
32
+
33
+ @property
34
+ def id(self):
35
+ return super().id + "--" + md5(self.paradigm.encode("utf-8")).hexdigest()
@@ -18,7 +18,7 @@ from ..tabular.tabular import Tabular
18
18
 
19
19
  from .. import anchor as _anchor
20
20
 
21
- from ...commons import logger, QUIET
21
+ from ...commons import logger, QUIET, siibra_tqdm
22
22
  from ...core import region as _region
23
23
  from ...locations import pointset
24
24
  from ...retrieval.repositories import RepositoryConnector
@@ -26,7 +26,6 @@ from ...retrieval.repositories import RepositoryConnector
26
26
  from typing import Callable, Dict, Union, List
27
27
  import pandas as pd
28
28
  import numpy as np
29
- from tqdm import tqdm
30
29
 
31
30
 
32
31
  class RegionalConnectivity(Feature):
@@ -94,6 +93,11 @@ class RegionalConnectivity(Feature):
94
93
  """
95
94
  return list(self._files.keys())
96
95
 
96
+ @property
97
+ def name(self):
98
+ supername = super().name
99
+ return f"{supername} with cohort {self.cohort}"
100
+
97
101
  def get_matrix(self, subject: str = None):
98
102
  """
99
103
  Returns a matrix as a pandas dataframe.
@@ -102,8 +106,7 @@ class RegionalConnectivity(Feature):
102
106
  ----------
103
107
  subject: str, default: None
104
108
  Name of the subject (see ConnectivityMatrix.subjects for available names).
105
- If "mean" or None is given, the mean is taken in case of multiple
106
- available matrices.
109
+ If None, the mean is taken in case of multiple available matrices.
107
110
  Returns
108
111
  -------
109
112
  pd.DataFrame
@@ -119,21 +122,21 @@ class RegionalConnectivity(Feature):
119
122
  if "mean" not in self._matrices:
120
123
  all_arrays = [
121
124
  self._connector.get(fname, decode_func=self._decode_func)
122
- for fname in tqdm(
125
+ for fname in siibra_tqdm(
123
126
  self._files.values(),
124
127
  total=len(self),
125
128
  desc=f"Averaging {len(self)} connectivity matrices"
126
129
  )
127
130
  ]
128
131
  self._matrices['mean'] = self._array_to_dataframe(np.stack(all_arrays).mean(0))
129
- return self._matrices['mean']
132
+ return self._matrices['mean'].copy()
130
133
  if subject is None:
131
134
  subject = next(iter(self._files.keys()))
132
135
  if subject not in self._files:
133
136
  raise ValueError(f"Subject name '{subject}' not known, use one of: {', '.join(self._files)}")
134
137
  if subject not in self._matrices:
135
138
  self._matrices[subject] = self._load_matrix(subject)
136
- return self._matrices[subject]
139
+ return self._matrices[subject].copy()
137
140
 
138
141
  def plot_matrix(self, subject: str = None, regions: List[str] = None, logscale: bool = False, **kwargs):
139
142
  """
@@ -291,23 +294,15 @@ class RegionalConnectivity(Feature):
291
294
  for i, regionname in enumerate(self.regions)
292
295
  }
293
296
  nrows = array.shape[0]
294
- if len(indexmap) == nrows:
297
+ try:
298
+ assert len(indexmap) == nrows
295
299
  remapper = {
296
300
  label - min(indexmap.keys()): region
297
301
  for label, region in indexmap.items()
298
302
  }
299
303
  df = df.rename(index=remapper).rename(columns=remapper)
300
- else:
301
- labels = {r.index.label for r in parc.regiontree} - {None}
302
- if max(labels) - min(labels) + 1 == nrows:
303
- indexmap = {
304
- r.index.label - min(labels): r
305
- for r in parc.regiontree
306
- if r.index.label is not None
307
- }
308
- df = df.rename(index=indexmap).rename(columns=indexmap)
309
- else:
310
- logger.warn("Could not decode connectivity matrix regions.")
304
+ except:
305
+ raise RuntimeError("Could not decode connectivity matrix regions.")
311
306
  return df
312
307
 
313
308
  def _load_matrix(self, subject: str):
@@ -21,7 +21,7 @@ from .. import feature
21
21
  from ...retrieval import datasets
22
22
 
23
23
 
24
- class EbrainsDataFeature(feature.Feature, datasets.EbrainsDataset, category='dataset'):
24
+ class EbrainsDataFeature(feature.Feature, datasets.EbrainsDataset, category='other'):
25
25
 
26
26
  def __init__(
27
27
  self,