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
siibra/core/region.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");
@@ -13,35 +13,31 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  """Representation of a brain region."""
16
- from . import concept, space as _space, parcellation as _parcellation
17
- from .relation_qualification import Qualification as RegionRelationship, RelationAssignment
18
16
 
19
- from ..locations import boundingbox, point, pointset
20
- from ..volumes import parcellationmap
17
+ from . import concept, structure, space as _space, parcellation as _parcellation
18
+ from .assignment import Qualification, AnatomicalAssignment
21
19
 
20
+ from ..retrieval.cache import cache_user_fn
21
+ from ..locations import location, pointcloud, boundingbox as _boundingbox
22
+ from ..volumes import parcellationmap, volume
22
23
  from ..commons import (
23
24
  logger,
24
25
  MapType,
25
- affine_scaling,
26
26
  create_key,
27
27
  clear_name,
28
- siibra_tqdm,
29
28
  InstanceTable,
30
- SIIBRA_DEFAULT_MAPTYPE,
31
- SIIBRA_DEFAULT_MAP_THRESHOLD
32
29
  )
30
+ from ..exceptions import NoMapAvailableError, SpaceWarpingFailedError
33
31
 
34
- import numpy as np
35
32
  import re
36
33
  import anytree
37
- from typing import List, Set, Union, Iterable, Dict, Callable
38
- from nibabel import Nifti1Image
34
+ from typing import List, Union, Iterable, Dict, Callable, Tuple
39
35
  from difflib import SequenceMatcher
40
- from dataclasses import dataclass, field
41
36
  from ebrains_drive import BucketApiClient
42
37
  import json
43
- from functools import wraps
38
+ from functools import wraps, reduce
44
39
  from concurrent.futures import ThreadPoolExecutor
40
+ from functools import lru_cache
45
41
 
46
42
 
47
43
  REGEX_TYPE = type(re.compile("test"))
@@ -49,20 +45,7 @@ REGEX_TYPE = type(re.compile("test"))
49
45
  THRESHOLD_STATISTICAL_MAPS = None
50
46
 
51
47
 
52
- @dataclass
53
- class SpatialPropCmpt:
54
- centroid: point.Point
55
- volume: int
56
-
57
-
58
- @dataclass
59
- class SpatialProp:
60
- cog: SpatialPropCmpt = None
61
- components: List[SpatialPropCmpt] = field(default_factory=list)
62
- space: _space.Space = None
63
-
64
-
65
- class Region(anytree.NodeMixin, concept.AtlasConcept):
48
+ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
66
49
  """
67
50
  Representation of a region with name and more optional attributes
68
51
  """
@@ -70,6 +53,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
70
53
  _regex_re = re.compile(r'^\/(?P<expression>.+)\/(?P<flags>[a-zA-Z]*)$')
71
54
  _accepted_flags = "aiLmsux"
72
55
 
56
+ _GETMAP_CACHE = {}
57
+ _GETMAP_CACHE_MAX_ENTRIES = 1
58
+
73
59
  def __init__(
74
60
  self,
75
61
  name: str,
@@ -82,6 +68,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
82
68
  datasets: list = [],
83
69
  rgb: str = None,
84
70
  spec=None,
71
+ prerelease: bool = False,
85
72
  ):
86
73
  """
87
74
  Constructs a new Region object.
@@ -119,7 +106,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
119
106
  modality=modality,
120
107
  publications=publications,
121
108
  datasets=datasets,
122
- spec=spec
109
+ spec=spec,
110
+ prerelease=prerelease,
123
111
  )
124
112
 
125
113
  # anytree node will take care to use this appropriately
@@ -131,8 +119,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
131
119
  else tuple(int(rgb[p:p + 2], 16) for p in [1, 3, 5])
132
120
  )
133
121
  self._supported_spaces = None # computed on 1st call of self.supported_spaces
134
- self._CACHED_REGION_SEARCHES = {}
135
122
  self._str_aliases = None
123
+ self.find = lru_cache(maxsize=3)(self.find)
136
124
 
137
125
  def get_related_regions(self) -> Iterable["RegionRelationAssessments"]:
138
126
  """
@@ -140,11 +128,11 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
140
128
 
141
129
  Yields
142
130
  ------
143
- RegionRelationAssessments
131
+ Qualification
144
132
 
145
133
  Example
146
134
  -------
147
- >>> region = siibra.get_region("monkey", "PG")^M
135
+ >>> region = siibra.get_region("monkey", "PG")
148
136
  >>> for assesment in region.get_related_regions():
149
137
  >>> print(assesment)
150
138
  'PG' is homologous to 'Area PGa (IPL)'
@@ -255,6 +243,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
255
243
  Parameters
256
244
  ----------
257
245
  region: Region
246
+
258
247
  Returns
259
248
  -------
260
249
  bool
@@ -274,9 +263,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
274
263
 
275
264
  Parameters
276
265
  ----------
277
- regionspec: str, regex, int, Region
266
+ regionspec: str, regex, Region
278
267
  - a string with a possibly inexact name (matched both against the name and the identifier key)
279
- - a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux)
268
+ - a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux, see at https://docs.python.org/3/library/re.html#flags)
280
269
  - a regex applied to region names
281
270
  - a Region object
282
271
  filter_children : bool, default: False
@@ -285,20 +274,18 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
285
274
  If True (requires `filter_children=True`), will return parent
286
275
  structures if all children are matched, even though the parent
287
276
  itself might not match the specification.
277
+
288
278
  Returns
289
279
  -------
290
280
  list[Region]
291
281
  list of regions matching to the regionspec
282
+
292
283
  Tip
293
284
  ---
294
285
  See example 01-003, find regions.
295
286
  """
296
- key = (regionspec, filter_children, find_topmost)
297
- MEM = self._CACHED_REGION_SEARCHES
298
- if key in MEM:
299
- return MEM[key]
300
-
301
287
  if isinstance(regionspec, str):
288
+ # convert the specified string into a regex for matching
302
289
  regex_match = self._regex_re.match(regionspec)
303
290
  if regex_match:
304
291
  flags = regex_match.group('flags')
@@ -306,7 +293,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
306
293
 
307
294
  for flag in flags or []: # catch if flags is nullish
308
295
  if flag not in self._accepted_flags:
309
- raise Exception(f"only accepted flag are in { self._accepted_flags }. {flag} is not within them")
296
+ raise Exception(f"only accepted flag are in {self._accepted_flags}. {flag} is not within them")
310
297
  search_regex = (f"(?{flags})" if flags else "") + expression
311
298
  regionspec = re.compile(search_regex)
312
299
 
@@ -358,18 +345,15 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
358
345
  found_regions = sorted(set(candidates), key=lambda r: r.depth)
359
346
 
360
347
  # reverse is set to True, since SequenceMatcher().ratio(), higher == better
361
- MEM[key] = (
348
+ return (
362
349
  sorted(
363
350
  found_regions,
364
351
  reverse=True,
365
352
  key=lambda region: SequenceMatcher(None, str(region), regionspec).ratio(),
366
353
  )
367
- if type(regionspec) == str
368
- else found_regions
354
+ if isinstance(regionspec, str) else found_regions
369
355
  )
370
356
 
371
- return MEM[key]
372
-
373
357
  def matches(self, regionspec):
374
358
  """
375
359
  Checks whether this region matches the given region specification.
@@ -386,53 +370,57 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
386
370
  bool
387
371
  If the regionspec matches to the Region.
388
372
  """
389
- if regionspec is None:
390
- return False
391
-
392
- def splitstr(s):
393
- return [w for w in re.split(r"[^a-zA-Z0-9.\-]", s) if len(w) > 0]
394
-
395
- if isinstance(regionspec, Region):
396
- return self == regionspec
373
+ if regionspec not in self._CACHED_MATCHES:
374
+ def splitstr(s):
375
+ return [w for w in re.split(r"[^a-zA-Z0-9.\-]", s) if len(w) > 0]
376
+
377
+ if regionspec is None:
378
+ self._CACHED_MATCHES[regionspec] = False
379
+
380
+ elif isinstance(regionspec, Region):
381
+ self._CACHED_MATCHES[regionspec] = self == regionspec
382
+
383
+ elif isinstance(regionspec, str):
384
+ # string is given, perform lazy string matching
385
+ q = regionspec.lower().strip()
386
+ if q == self.key.lower().strip():
387
+ self._CACHED_MATCHES[regionspec] = True
388
+ elif q == self.id.lower().strip():
389
+ self._CACHED_MATCHES[regionspec] = True
390
+ elif q == self.name.lower().strip():
391
+ self._CACHED_MATCHES[regionspec] = True
392
+ else:
393
+ # match if all words of the query are also included in the region name
394
+ W = splitstr(clear_name(self.name.lower()))
395
+ Q = splitstr(clear_name(regionspec))
396
+ self._CACHED_MATCHES[regionspec] = all([any(
397
+ q.lower() == w or 'v' + q.lower() == w
398
+ for w in W
399
+ ) for q in Q])
400
+
401
+ # TODO since dropping 3.6 support, maybe reimplement as re.Pattern ?
402
+ elif isinstance(regionspec, REGEX_TYPE):
403
+ # match regular expression
404
+ self._CACHED_MATCHES[regionspec] = any(regionspec.search(s) is not None for s in [self.name, self.key])
405
+
406
+ elif isinstance(regionspec, (list, tuple)):
407
+ self._CACHED_MATCHES[regionspec] = any(self.matches(_) for _ in regionspec)
397
408
 
398
- elif isinstance(regionspec, str):
399
- # string is given, perform lazy string matching
400
- q = regionspec.lower().strip()
401
- if q == self.key.lower().strip():
402
- return True
403
- elif q == self.id.lower().strip():
404
- return True
405
- elif q == self.name.lower().strip():
406
- return True
407
409
  else:
408
- # match if all words of the query are also included in the region name
409
- W = splitstr(clear_name(self.name.lower()))
410
- Q = splitstr(clear_name(regionspec))
411
- return all([any(
412
- q.lower() == w or 'v' + q.lower() == w
413
- for w in W
414
- ) for q in Q])
415
-
416
- # TODO since dropping 3.6 support, maybe reimplement as re.Pattern ?
417
- elif isinstance(regionspec, REGEX_TYPE):
418
- # match regular expression
419
- return any(regionspec.search(s) is not None for s in [self.name, self.key])
420
- elif isinstance(regionspec, (list, tuple)):
421
- return any(self.matches(_) for _ in regionspec)
422
- else:
423
- raise TypeError(
424
- f"Cannot interpret region specification of type '{type(regionspec)}'"
425
- )
410
+ raise TypeError(
411
+ f"Cannot interpret region specification of type '{type(regionspec)}'"
412
+ )
413
+
414
+ return self._CACHED_MATCHES[regionspec]
426
415
 
427
- def fetch_regional_map(
416
+ def get_regional_mask(
428
417
  self,
429
418
  space: Union[str, _space.Space],
430
- maptype: MapType = SIIBRA_DEFAULT_MAPTYPE,
431
- threshold: float = SIIBRA_DEFAULT_MAP_THRESHOLD,
432
- via_space: Union[str, _space.Space] = None
433
- ):
419
+ maptype: MapType = MapType.LABELLED,
420
+ threshold: float = 0.0,
421
+ ) -> volume.FilteredVolume:
434
422
  """
435
- Attempts to build a binary mask of this region in the given space,
423
+ Get a binary mask of this region in the given space,
436
424
  using the specified MapTypes.
437
425
 
438
426
  Parameters
@@ -441,100 +429,98 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
441
429
  The requested reference space
442
430
  maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
443
431
  The type of map to be used ('labelled' or 'statistical')
444
- threshold: float, optional
432
+ threshold: float, default: 0.0
445
433
  When fetching a statistical map, use this threshold to convert
446
- it to a binary mask
447
- via_space: Space or str
448
- If specified, fetch the map in this space first, and then perform
449
- a linear warping from there to the requested space.
450
-
451
- Tip
452
- ---
453
- You might want to use this if a map in the requested space
454
- is not available.
455
-
456
- Note
457
- ----
458
- This linear warping is an affine approximation of the
459
- nonlinear deformation, computed from the warped corner points
460
- of the bounding box (see siibra.locations.BoundingBox.estimate_affine()).
461
- It does not require voxel resampling, just replaces the affine
462
- matrix, but is less accurate than a full nonlinear warping,
463
- which is currently not supported in siibra-python for images.
434
+ it to a binary mask.
435
+
464
436
  Returns
465
437
  -------
466
- Nifti1Image
438
+ Volume (use fetch() to get a NiftiImage)
467
439
  """
468
440
  if isinstance(maptype, str):
469
441
  maptype = MapType[maptype.upper()]
470
- result = None
471
442
 
472
- # prepare space instances
443
+ threshold_info = "" if maptype == MapType.LABELLED else f"(threshold: {threshold}) "
444
+ name = f"Mask {threshold_info}of '{self.name} ({self.parcellation})' in "
445
+ try:
446
+ regional_map = self.get_regional_map(space=space, maptype=maptype)
447
+ if maptype == MapType.LABELLED:
448
+ assert threshold == 0.0, f"threshold can only be set for {MapType.STATISTICAL} maps."
449
+ result = regional_map
450
+ result._boundingbox = None
451
+ if maptype == MapType.STATISTICAL:
452
+ result = volume.FilteredVolume(
453
+ parent_volume=regional_map,
454
+ threshold=threshold
455
+ )
456
+ if threshold == 0.0:
457
+ result._boundingbox = regional_map._boundingbox
458
+ name += f"'{result.space}'"
459
+ except NoMapAvailableError:
460
+ # This region is not mapped directly in any map in the registry.
461
+ # Try building a map from the child regions
462
+ if (len(self.children) > 0) and all(c.mapped_in_space(space) for c in self.children):
463
+ logger.info(f"{self.name} is not mapped in {space}. Merging the masks of its {len(self.children)} child regions.")
464
+ child_volumes = [
465
+ child.get_regional_mask(space=space, maptype=maptype, threshold=threshold)
466
+ for child in self.children
467
+ ]
468
+ result = volume.FilteredVolume(
469
+ volume.merge(child_volumes),
470
+ label=1
471
+ )
472
+ name += f"'{result.space}' (built by merging the mask {threshold_info} of its decendants)"
473
+ result._name = name
474
+ return result
475
+
476
+ def get_regional_map(
477
+ self,
478
+ space: Union[str, _space.Space],
479
+ maptype: MapType = MapType.LABELLED,
480
+ ) -> Union[volume.FilteredVolume, volume.Volume, volume.Subvolume]:
481
+ """
482
+ Get a volume reprsenting this region in the given space and MapType.
483
+
484
+ Note
485
+ ----
486
+ If a region is not mapped in any of the `Map`s in the registry, then
487
+ siibra will get the maps of its children recursively and merge them.
488
+ If no map is available this way as well, an exception is raised.
489
+
490
+ Parameters
491
+ ----------
492
+ space: Space or str
493
+ The requested reference space
494
+ maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
495
+ The type of map to be used ('labelled' or 'statistical')
496
+
497
+ Returns
498
+ -------
499
+ Volume (use fetch() to get a NiftiImage)
500
+ """
501
+ if isinstance(maptype, str):
502
+ maptype = MapType[maptype.upper()]
503
+
504
+ # prepare space instance
473
505
  if isinstance(space, str):
474
506
  space = _space.Space.get_instance(space)
475
- fetch_space = space if via_space is None else via_space
476
- if isinstance(fetch_space, str):
477
- fetch_space = _space.Space.get_instance(fetch_space)
478
507
 
508
+ # see if we find a map supporting the requested region
479
509
  for m in parcellationmap.Map.registry():
480
510
  if (
481
- m.space.matches(fetch_space)
511
+ m.space.matches(space)
482
512
  and m.parcellation == self.parcellation
483
513
  and m.provides_image
484
514
  and m.maptype == maptype
485
515
  and self.name in m.regions
486
516
  ):
487
- result = m.fetch(region=self, format='image')
488
- if (maptype == MapType.STATISTICAL) and (threshold is not None):
489
- logger.info(f"Thresholding statistical map at {threshold}")
490
- result = Nifti1Image(
491
- (result.get_fdata() > threshold).astype('uint8'),
492
- result.affine
493
- )
494
- break
517
+ return m.get_volume(region=self)
495
518
  else:
496
- # all children are mapped instead
497
- dataobj = None
498
- affine = None
499
- if all(c.mapped_in_space(fetch_space) for c in self.children):
500
- for c in siibra_tqdm(
501
- self.children,
502
- desc=f"Building mask of {self.name}",
503
- leave=False,
504
- unit=" child region"
505
- ):
506
- mask = c.fetch_regional_map(fetch_space, maptype, threshold)
507
- if dataobj is None:
508
- dataobj = np.asanyarray(mask.dataobj)
509
- affine = mask.affine
510
- else:
511
- if np.linalg.norm(mask.affine - affine) > 1e-12:
512
- raise NotImplementedError(
513
- f"Child regions of {self.name} have different voxel spaces "
514
- "and the aggregated subtree mask is not supported. "
515
- f"Try fetching masks of the children: {self.children}"
516
- )
517
- updates = mask.get_fdata() > dataobj
518
- dataobj[updates] = mask.get_fdata()[updates]
519
- if dataobj is not None:
520
- result = Nifti1Image(dataobj, affine)
521
-
522
- if result is None:
523
- raise RuntimeError(f"Cannot build mask for {self.name} from {maptype} maps in {fetch_space}")
524
-
525
- if via_space is not None:
526
- # fetch used an intermediate reference space provided by 'via_space'.
527
- # We will now transform the affine to match the desired target space.
528
- bbox = boundingbox.BoundingBox.from_image(result, fetch_space)
529
- transform = bbox.estimate_affine(space)
530
- result = Nifti1Image(result.dataobj, np.dot(transform, result.affine))
531
- logger.info(
532
- f"Regional map was fetched from {fetch_space.name}, "
533
- f"then linearly corrected to match {space.name}."
519
+ raise NoMapAvailableError(
520
+ f"{self.name} is not mapped in {space} as a {str(maptype)} map."
521
+ " Please try getting the children or getting the mask."
534
522
  )
535
523
 
536
- return result
537
-
538
524
  def mapped_in_space(self, space, recurse: bool = True) -> bool:
539
525
  """
540
526
  Verifies wether this region is defined by an explicit map in the given space.
@@ -565,13 +551,15 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
565
551
  return False
566
552
 
567
553
  @property
568
- def supported_spaces(self) -> Set[_space.Space]:
554
+ def supported_spaces(self) -> List[_space.Space]:
569
555
  """
570
556
  The set of spaces for which a mask could be extracted.
571
557
  Overwrites the corresponding method of AtlasConcept.
572
558
  """
573
559
  if self._supported_spaces is None:
574
- self._supported_spaces = {s for s in _space.Space.registry() if self.mapped_in_space(s)}
560
+ self._supported_spaces = sorted(
561
+ {s for s in _space.Space.registry() if self.mapped_in_space(s)}
562
+ )
575
563
  return self._supported_spaces
576
564
 
577
565
  def supports_space(self, space: _space.Space):
@@ -587,11 +575,92 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
587
575
  elements={s.key: s for s in self.supported_spaces},
588
576
  )
589
577
 
590
- def __str__(self):
591
- return self.name
578
+ def __contains__(self, other: Union[location.Location, 'Region']) -> bool:
579
+ if isinstance(other, Region):
580
+ return len(self.find(other)) > 0
581
+ else:
582
+ try:
583
+ regionmap = self.get_regional_mask(space=other.space)
584
+ return regionmap.__contains__(other)
585
+ except NoMapAvailableError:
586
+ return False
587
+
588
+ def assign(self, other: structure.BrainStructure) -> AnatomicalAssignment:
589
+ """
590
+ Compute assignment of a location to this region.
591
+
592
+ Two cases:
593
+ 1) other is location -> get region map, call regionmap.assign(other)
594
+ 2) other is region -> just do a semantic check for the regions
595
+
596
+ Parameters
597
+ ----------
598
+ other : Location or Region
592
599
 
593
- def __repr__(self):
594
- return self.name
600
+ Returns
601
+ -------
602
+ AnatomicalAssignment or None
603
+ None if there is no Qualification found.
604
+ """
605
+ if (self, other) in self._ASSIGNMENT_CACHE:
606
+ return self._ASSIGNMENT_CACHE[self, other]
607
+ if (other, self) in self._ASSIGNMENT_CACHE:
608
+ if self._ASSIGNMENT_CACHE[other, self] is None:
609
+ return None
610
+ return self._ASSIGNMENT_CACHE[other, self].invert()
611
+
612
+ if isinstance(other, (location.Location, volume.Volume)):
613
+ if self.mapped_in_space(other.space):
614
+ regionmap = self.get_regional_mask(other.space)
615
+ self._ASSIGNMENT_CACHE[self, other] = regionmap.assign(other)
616
+ return self._ASSIGNMENT_CACHE[self, other]
617
+
618
+ if isinstance(other, _boundingbox.BoundingBox): # volume.intersection(bbox) gets boundingbox anyway
619
+ try:
620
+ regionbbox_otherspace = self.get_boundingbox(other.space, restrict_space=False)
621
+ if regionbbox_otherspace is not None:
622
+ self._ASSIGNMENT_CACHE[self, other] = regionbbox_otherspace.assign(other)
623
+ return self._ASSIGNMENT_CACHE[self, other]
624
+ except Exception as e:
625
+ logger.debug(e)
626
+
627
+ assignment_result = None
628
+ for targetspace in self.supported_spaces:
629
+ try:
630
+ other_warped = other.warp(targetspace)
631
+ regionmap = self.get_regional_mask(targetspace)
632
+ assignment_result = regionmap.assign(other_warped)
633
+ except SpaceWarpingFailedError:
634
+ try:
635
+ regionbbox_targetspace = self.get_boundingbox(
636
+ targetspace, restrict_space=True
637
+ )
638
+ if regionbbox_targetspace is None:
639
+ continue
640
+ regionbbox_warped = regionbbox_targetspace.warp(other.space)
641
+ except SpaceWarpingFailedError:
642
+ continue
643
+ assignment_result = regionbbox_warped.assign(other)
644
+ except Exception as e:
645
+ logger.debug(e)
646
+ continue
647
+ break
648
+ self._ASSIGNMENT_CACHE[self, other] = assignment_result
649
+ else: # other is a Region
650
+ assert isinstance(other, Region)
651
+ if self == other:
652
+ qualification = Qualification.EXACT
653
+ elif self.__contains__(other):
654
+ qualification = Qualification.CONTAINS
655
+ elif other.__contains__(self):
656
+ qualification = Qualification.CONTAINED
657
+ else:
658
+ qualification = None
659
+ if qualification is None:
660
+ self._ASSIGNMENT_CACHE[self, other] = None
661
+ else:
662
+ self._ASSIGNMENT_CACHE[self, other] = AnatomicalAssignment(self, other, qualification)
663
+ return self._ASSIGNMENT_CACHE[self, other]
595
664
 
596
665
  def tree2str(self):
597
666
  """Render region-tree as a string"""
@@ -605,13 +674,16 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
605
674
  """Prints the tree representation of the region"""
606
675
  print(self.tree2str())
607
676
 
608
- def get_bounding_box(
677
+ def get_boundingbox(
609
678
  self,
610
679
  space: _space.Space,
611
680
  maptype: MapType = MapType.LABELLED,
612
- threshold_statistical=None,
613
- ):
614
- """Compute the bounding box of this region in the given space.
681
+ threshold_statistical: float = 0.0,
682
+ restrict_space: bool = True,
683
+ **fetch_kwargs
684
+ ) -> Union[_boundingbox.BoundingBox, None]:
685
+ """
686
+ Compute the bounding box of this region in the given space.
615
687
 
616
688
  Parameters
617
689
  ----------
@@ -621,40 +693,61 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
621
693
  Type of map to build ('labelled' will result in a binary mask,
622
694
  'statistical' attempts to build a statistical mask, possibly by
623
695
  elementwise maximum of statistical maps of children)
624
- threshold_statistical: float, or None
625
- if not None, masks will be preferably constructed by thresholding
626
- statistical maps with the given value.
696
+ threshold_statistical: float, default: 0.0
697
+ When masking a statistical map, use this threshold to convert
698
+ it to a binary mask before finding its bounding box.
699
+ restrict_space: bool, default: False
700
+ If True, it will not try to fetch maps from other spaces and warp
701
+ its boundingbox to requested space.
702
+
627
703
  Returns
628
704
  -------
629
705
  BoundingBox
630
706
  """
631
707
  spaceobj = _space.Space.get_instance(space)
632
708
  try:
633
- mask = self.fetch_regional_map(
709
+ mask = self.get_regional_mask(
634
710
  spaceobj, maptype=maptype, threshold=threshold_statistical
635
711
  )
636
- return boundingbox.BoundingBox.from_image(mask, space=spaceobj)
712
+ return mask.get_boundingbox(
713
+ clip=True,
714
+ background=0.0,
715
+ **fetch_kwargs
716
+ )
637
717
  except (RuntimeError, ValueError):
718
+ if restrict_space:
719
+ return None
638
720
  for other_space in self.parcellation.spaces - spaceobj:
639
721
  try:
640
- mask = self.fetch_regional_map(
722
+ mask = self.get_regional_mask(
641
723
  other_space,
642
724
  maptype=maptype,
643
725
  threshold=threshold_statistical,
644
726
  )
645
- logger.warning(
646
- f"No bounding box for {self.name} defined in {spaceobj.name}, "
647
- f"will warp the bounding box from {other_space.name} instead."
648
- )
649
- bbox = boundingbox.BoundingBox.from_image(mask, space=other_space)
727
+ bbox = mask.get_boundingbox(clip=True, background=0.0, **fetch_kwargs)
650
728
  if bbox is not None:
651
- return bbox.warp(spaceobj)
729
+ try:
730
+ bbox_warped = bbox.warp(spaceobj)
731
+ except SpaceWarpingFailedError:
732
+ continue
733
+ logger.debug(
734
+ f"No bounding box for {self.name} defined in {spaceobj.name}, "
735
+ f"warped the bounding box from {other_space.name} instead."
736
+ )
737
+ return bbox_warped
652
738
  except RuntimeError:
653
739
  continue
654
740
  logger.error(f"Could not compute bounding box for {self.name}.")
655
741
  return None
656
742
 
657
- def compute_centroids(self, space: _space.Space) -> pointset.PointSet:
743
+ def compute_centroids(
744
+ self,
745
+ space: _space.Space,
746
+ maptype: MapType = MapType.LABELLED,
747
+ threshold_statistical: float = 0.0,
748
+ split_components: bool = True,
749
+ **fetch_kwargs,
750
+ ) -> pointcloud.PointCloud:
658
751
  """
659
752
  Compute the centroids of the region in the given space.
660
753
 
@@ -662,18 +755,33 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
662
755
  ----------
663
756
  space: Space
664
757
  reference space in which the computation will be performed
758
+ maptype: MapType, default: MapType.LABELLED
759
+ Type of map to build ('labelled' will result in a binary mask,
760
+ 'statistical' attempts to build a statistical mask, possibly by
761
+ elementwise maximum of statistical maps of children)
762
+ threshold_statistical: float, default: 0.0
763
+ When masking a statistical map, use this threshold to convert
764
+ it to a binary mask before finding its centroids.
765
+
665
766
  Returns
666
767
  -------
667
- PointSet
668
- Found centroids (as Point objects) in a PointSet
768
+ PointCloud
769
+ Found centroids (as Point objects) in a PointCloud
770
+
669
771
  Note
670
772
  ----
671
773
  A region can generally have multiple centroids if it has multiple
672
774
  connected components in the map.
673
775
  """
674
- props = self.spatial_props(space)
675
- return pointset.PointSet(
676
- [c.centroid for c in props.components],
776
+ props = self.spatial_props(
777
+ space=space,
778
+ maptype=maptype,
779
+ threshold_statistical=threshold_statistical,
780
+ split_components=split_components,
781
+ **fetch_kwargs,
782
+ )
783
+ return pointcloud.PointCloud(
784
+ [c.centroid for c in props],
677
785
  space=space
678
786
  )
679
787
 
@@ -681,8 +789,10 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
681
789
  self,
682
790
  space: _space.Space,
683
791
  maptype: MapType = MapType.LABELLED,
684
- threshold_statistical=None,
685
- ) -> SpatialProp:
792
+ threshold_statistical: float = 0.0,
793
+ split_components: bool = True,
794
+ **fetch_kwargs,
795
+ ):
686
796
  """
687
797
  Compute spatial properties for connected components of this region in the given space.
688
798
 
@@ -694,59 +804,34 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
694
804
  Type of map to build ('labelled' will result in a binary mask,
695
805
  'statistical' attempts to build a statistical mask, possibly by
696
806
  elementwise maximum of statistical maps of children)
697
- threshold_statistical: float, or None
807
+ threshold_statistical: float, default: 0.0
698
808
  if not None, masks will be preferably constructed by thresholding
699
809
  statistical maps with the given value.
700
810
 
701
811
  Returns
702
812
  -------
703
- Dict
704
- Dictionary of region's spatial properties
813
+ List
814
+ List of region's component spatial properties
705
815
  """
706
- from skimage import measure
707
-
708
816
  if not isinstance(space, _space.Space):
709
817
  space = _space.Space.get_instance(space)
710
818
 
711
- result = SpatialProp(space=space)
712
-
713
- if not self.mapped_in_space(space):
714
- logger.warning(
819
+ # build binary mask of the image
820
+ try:
821
+ region_vol = self.get_regional_mask(
822
+ space, maptype=maptype, threshold=threshold_statistical
823
+ )
824
+ except NoMapAvailableError:
825
+ raise ValueError(
715
826
  f"Spatial properties of {self.name} cannot be computed in {space.name}. "
716
827
  "This region is only mapped in these spaces: "
717
828
  f"{', '.join(s.name for s in self.supported_spaces)}"
718
829
  )
719
- return result
720
830
 
721
- # build binary mask of the image
722
- pimg = self.fetch_regional_map(
723
- space, maptype=maptype, threshold=threshold_statistical
831
+ return region_vol.compute_spatial_props(
832
+ split_components=split_components, **fetch_kwargs
724
833
  )
725
834
 
726
- # determine scaling factor from voxels to cube mm
727
- scale = affine_scaling(pimg.affine)
728
-
729
- # compute properties of labelled volume
730
- A = np.asarray(pimg.get_fdata(), dtype=np.int32).squeeze()
731
- C = measure.label(A)
732
-
733
- # compute spatial properties of each connected component
734
- for label in range(1, C.max() + 1):
735
- nonzero = np.c_[np.nonzero(C == label)]
736
- result.components.append(
737
- SpatialPropCmpt(
738
- centroid=point.Point(
739
- np.dot(pimg.affine, np.r_[nonzero.mean(0), 1])[:3], space=space
740
- ),
741
- volume=nonzero.shape[0] * scale,
742
- )
743
- )
744
-
745
- # sort by volume
746
- result.components.sort(key=lambda cmp: cmp.volume, reverse=True)
747
-
748
- return result
749
-
750
835
  def __iter__(self):
751
836
  """
752
837
  Returns an iterator that goes through all regions in this subtree
@@ -754,6 +839,110 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
754
839
  """
755
840
  return anytree.PreOrderIter(self)
756
841
 
842
+ def intersection(self, other: "location.Location") -> "location.Location":
843
+ """Use this region for filtering a location object."""
844
+
845
+ if self.supports_space(other.space):
846
+ try:
847
+ volume = self.get_regional_mask(other.space)
848
+ if volume is not None:
849
+ return volume.intersection(other)
850
+ except NotImplementedError:
851
+ intersections = [child.intersection(other) for child in self.children]
852
+ return reduce(lambda a, b: a.union(b), intersections)
853
+
854
+ for space in self.supported_spaces:
855
+ if space.provides_image:
856
+ try:
857
+ volume = self.get_regional_mask(space)
858
+ if volume is not None:
859
+ intersection = volume.intersection(other)
860
+ logger.info(f"Warped {other} to {space} to find the intersection.")
861
+ return intersection
862
+ except SpaceWarpingFailedError:
863
+ continue
864
+
865
+ return None
866
+
867
+
868
+ @cache_user_fn
869
+ def _get_related_regions_str(pe_id: str) -> Tuple[Tuple[str, str, str, str], ...]:
870
+ logger.info("LONG CALC...", pe_id)
871
+ return_val = []
872
+ region_relation_assessments = RegionRelationAssessments.translate_pes(pe_id, pe_id)
873
+ for asgmt in region_relation_assessments:
874
+ assert isinstance(asgmt, RegionRelationAssessments), f"Expecting type to be of RegionRelationAssessments, but is {type(asgmt)}"
875
+ assert isinstance(asgmt.assigned_structure, Region), f"Expecting assigned structure to be of type Region, but is {type(asgmt.assigned_structure)}"
876
+ return_val.append((
877
+ asgmt.assigned_structure.parcellation.id,
878
+ asgmt.assigned_structure.name,
879
+ asgmt.qualification.name,
880
+ asgmt.explanation
881
+ ))
882
+ return tuple(return_val)
883
+
884
+
885
+ def get_peid_from_region(region: Region) -> str:
886
+ """
887
+ Given a region, obtain the Parcellation Entity ID.
888
+
889
+ Parameters
890
+ ----------
891
+ region : Region
892
+
893
+ Returns
894
+ -------
895
+ str
896
+ """
897
+ if region._spec:
898
+ region_peid = region._spec.get("ebrains", {}).get("openminds/ParcellationEntity")
899
+ if region_peid:
900
+ return region_peid
901
+ # In some cases (e.g. Julich Brain, PE is defined on the parent leaf nodes)
902
+ if region.parent and region.parent._spec:
903
+ parent_peid = region.parent._spec.get("ebrains", {}).get("openminds/ParcellationEntity")
904
+ if parent_peid:
905
+ return parent_peid
906
+ return None
907
+
908
+
909
+ def get_related_regions(region: Region) -> Iterable["RegionRelationAssessments"]:
910
+ """
911
+ Get assements on relations of a region to others defined on EBRAINS.
912
+
913
+ Parameters
914
+ ----------
915
+ region: Region
916
+
917
+ Yields
918
+ ------
919
+ Qualification
920
+
921
+ Example
922
+ -------
923
+ >>> region = siibra.get_region("monkey", "PG")
924
+ >>> for assesment in siibra.core.region.get_related_regions(region):
925
+ >>> print(assesment)
926
+ 'PG' is homologous to 'Area PGa (IPL)'
927
+ 'PG' is homologous to 'Area PGa (IPL) left'
928
+ 'PG' is homologous to 'Area PGa (IPL) right'
929
+ 'PG' is homologous to 'Area PGa (IPL)'
930
+ 'PG' is homologous to 'Area PGa (IPL) left'
931
+ 'PG' is homologous to 'Area PGa (IPL) right'
932
+ 'PG' is homologous to 'Area PGa (IPL)'
933
+ 'PG' is homologous to 'Area PGa (IPL) right'
934
+ 'PG' is homologous to 'Area PGa (IPL) left'
935
+ """
936
+ logger.info("get related region called")
937
+ pe_id = get_peid_from_region(region)
938
+ if not pe_id:
939
+ return []
940
+
941
+ for parc_id, region_name, qual, explanation in _get_related_regions_str(pe_id):
942
+ parc = _parcellation.Parcellation.get_instance(parc_id)
943
+ found_region = parc.get_region(region_name)
944
+ yield RegionRelationAssessments(region, found_region, qual, explanation)
945
+
757
946
 
758
947
  _get_reg_relation_asmgt_types: Dict[str, Callable] = {}
759
948
 
@@ -769,12 +958,33 @@ def _register_region_reference_type(ebrain_type: str):
769
958
  return outer
770
959
 
771
960
 
772
- class RegionRelationAssessments(RelationAssignment[Region]):
961
+ class RegionRelationAssessments(AnatomicalAssignment[Region]):
962
+ """
963
+ A collection of methods on finding related regions and the quantification
964
+ of the relationship.
965
+ """
773
966
 
774
967
  anony_client = BucketApiClient()
775
968
 
776
969
  @staticmethod
777
- def get_uuid(long_id: Union[str, Dict]):
970
+ def get_uuid(long_id: Union[str, Dict]) -> str:
971
+ """
972
+ Returns the uuid portion of either a fully formed openminds id, or get
973
+ the 'id' property first, and extract the uuid portion of the id.
974
+
975
+ Parameters
976
+ ----------
977
+ long_id: str, dict[str, str]
978
+
979
+ Returns
980
+ -------
981
+ str
982
+
983
+ Raises
984
+ ------
985
+ AssertionError
986
+ RuntimeError
987
+ """
778
988
  if isinstance(long_id, str):
779
989
  pass
780
990
  elif isinstance(long_id, dict):
@@ -788,6 +998,22 @@ class RegionRelationAssessments(RelationAssignment[Region]):
788
998
 
789
999
  @staticmethod
790
1000
  def parse_id_arg(_id: Union[str, List[str]]) -> List[str]:
1001
+ """
1002
+ Normalizes the ebrains id property. The ebrains id field can be either
1003
+ a str or list[str]. This method normalizes it to always be list[str].
1004
+
1005
+ Parameters
1006
+ ----------
1007
+ _id: strl, list[str]
1008
+
1009
+ Returns
1010
+ -------
1011
+ list[str]
1012
+
1013
+ Raises
1014
+ ------
1015
+ RuntimeError
1016
+ """
791
1017
  if isinstance(_id, list):
792
1018
  assert all(isinstance(_i, str) for _i in _id), "all instances of pev should be str"
793
1019
  elif isinstance(_id, str):
@@ -798,11 +1024,34 @@ class RegionRelationAssessments(RelationAssignment[Region]):
798
1024
 
799
1025
  @classmethod
800
1026
  def get_object(cls, obj: str):
1027
+ """
1028
+ Gets given a object (path), loads the content and serializes to json.
1029
+ Relative to the bucket 'reference-atlas-data'.
1030
+
1031
+ Parameters
1032
+ ----------
1033
+ obj: str
1034
+
1035
+ Returns
1036
+ -------
1037
+ dict
1038
+ """
801
1039
  bucket = cls.anony_client.buckets.get_bucket("reference-atlas-data")
802
1040
  return json.loads(bucket.get_file(obj).get_content())
803
1041
 
804
1042
  @classmethod
805
1043
  def get_snapshot_factory(cls, type_str: str):
1044
+ """
1045
+ Factory method for given type.
1046
+
1047
+ Parameters
1048
+ ----------
1049
+ type_str: str
1050
+
1051
+ Returns
1052
+ -------
1053
+ Callable[[str|list[str]], dict]
1054
+ """
806
1055
  def get_objects(_id: Union[str, List[str]]):
807
1056
  _id = cls.parse_id_arg(_id)
808
1057
  with ThreadPoolExecutor() as ex:
@@ -815,7 +1064,19 @@ class RegionRelationAssessments(RelationAssignment[Region]):
815
1064
 
816
1065
  @classmethod
817
1066
  def parse_relationship_assessment(cls, src: "Region", assessment):
1067
+ """
1068
+ Given a region, and the fetched assessment json, yield
1069
+ RegionRelationAssignment object.
818
1070
 
1071
+ Parameters
1072
+ ----------
1073
+ src: Region
1074
+ assessment: dict
1075
+
1076
+ Returns
1077
+ -------
1078
+ Iterable[RegionRelationAssessments]
1079
+ """
819
1080
  all_regions = [
820
1081
  region
821
1082
  for p in _parcellation.Parcellation.registry()
@@ -836,7 +1097,11 @@ class RegionRelationAssessments(RelationAssignment[Region]):
836
1097
  ]
837
1098
 
838
1099
  for found_target in found_targets:
839
- yield cls(query_structure=src, assigned_structure=found_target, qualification=RegionRelationship.parse_relation_assessment(overlap))
1100
+ yield cls(
1101
+ query_structure=src,
1102
+ assigned_structure=found_target,
1103
+ qualification=Qualification.parse_relation_assessment(overlap)
1104
+ )
840
1105
 
841
1106
  if "https://openminds.ebrains.eu/sands/ParcellationEntity" in target.get("type"):
842
1107
  pev_uuids = [
@@ -846,28 +1111,47 @@ class RegionRelationAssessments(RelationAssignment[Region]):
846
1111
  ]
847
1112
  for reg in all_regions:
848
1113
  if reg in pev_uuids:
849
- yield cls(query_structure=src, assigned_structure=reg, qualification=RegionRelationship.parse_relation_assessment(overlap))
1114
+ yield cls(
1115
+ query_structure=src,
1116
+ assigned_structure=reg,
1117
+ qualification=Qualification.parse_relation_assessment(overlap)
1118
+ )
850
1119
 
851
1120
  @classmethod
852
1121
  @_register_region_reference_type("openminds/CustomAnatomicalEntity")
853
1122
  def translate_cae(cls, src: "Region", _id: Union[str, List[str]]):
1123
+ """Register how CustomAnatomicalEntity should be parsed
1124
+
1125
+ Parameters
1126
+ ----------
1127
+ src: Region
1128
+ _id: str|list[str]
1129
+
1130
+ Returns
1131
+ -------
1132
+ Iterable[RegionRelationAssessments]
1133
+ """
854
1134
  caes = cls.get_snapshot_factory("CustomAnatomicalEntity")(_id)
855
1135
  for cae in caes:
856
1136
  for ass in cae.get("relationAssessment", []):
857
1137
  yield from cls.parse_relationship_assessment(src, ass)
858
1138
 
859
1139
  @classmethod
860
- @_register_region_reference_type("openminds/ParcellationEntityVersion")
861
- def translate_pevs(cls, src: "Region", _id: Union[str, List[str]]):
862
- pe_uuids = [
863
- uuid for uuid in
864
- {
865
- cls.get_uuid(pe)
866
- for pev in cls.get_snapshot_factory("ParcellationEntityVersion")(_id)
867
- for pe in pev.get("isVersionOf")
868
- }
869
- ]
870
- pes = cls.get_snapshot_factory("ParcellationEntity")(pe_uuids)
1140
+ @_register_region_reference_type("openminds/ParcellationEntity")
1141
+ def translate_pes(cls, src: "Region", _id: Union[str, List[str]]):
1142
+ """
1143
+ Register how ParcellationEntity should be parsed
1144
+
1145
+ Parameters
1146
+ ----------
1147
+ src: Region
1148
+ _id: str|list[str]
1149
+
1150
+ Returns
1151
+ -------
1152
+ Iterable[RegionRelationAssessments]
1153
+ """
1154
+ pes = cls.get_snapshot_factory("ParcellationEntity")(_id)
871
1155
 
872
1156
  all_regions = [
873
1157
  region
@@ -876,30 +1160,15 @@ class RegionRelationAssessments(RelationAssignment[Region]):
876
1160
  ]
877
1161
 
878
1162
  for pe in pes:
879
-
880
- # other versions
881
- has_versions = pe.get("hasVersion", [])
882
- for has_version in has_versions:
883
- uuid = cls.get_uuid(has_version)
884
-
885
- # ignore if uuid is referring to src region
886
- if uuid == src:
1163
+ for region in all_regions:
1164
+ if region is src:
887
1165
  continue
888
-
889
- found_targets = [
890
- region
891
- for region in all_regions
892
- if region == uuid
893
- ]
894
- if len(found_targets) == 0:
895
- logger.warning(f"other version with uuid {uuid} not found")
896
- continue
897
-
898
- for found_target in found_targets:
1166
+ region_peid = get_peid_from_region(region)
1167
+ if region_peid and (region_peid in pe.get("id")):
899
1168
  yield cls(
900
1169
  query_structure=src,
901
- assigned_structure=found_target,
902
- qualification=RegionRelationship.OTHER_VERSION
1170
+ assigned_structure=region,
1171
+ qualification=Qualification.OTHER_VERSION
903
1172
  )
904
1173
 
905
1174
  # homologuous
@@ -907,8 +1176,45 @@ class RegionRelationAssessments(RelationAssignment[Region]):
907
1176
  for relation in relations:
908
1177
  yield from cls.parse_relationship_assessment(src, relation)
909
1178
 
1179
+ @classmethod
1180
+ @_register_region_reference_type("openminds/ParcellationEntityVersion")
1181
+ def translate_pevs(cls, src: "Region", _id: Union[str, List[str]]):
1182
+ """
1183
+ Register how ParcellationEntityVersion should be parsed
1184
+
1185
+ Parameters
1186
+ ----------
1187
+ src: Region
1188
+ _id: str|list[str]
1189
+
1190
+ Returns
1191
+ -------
1192
+ Iterable[RegionRelationAssessments]
1193
+ """
1194
+ pe_uuids = [
1195
+ uuid for uuid in
1196
+ {
1197
+ cls.get_uuid(pe)
1198
+ for pev in cls.get_snapshot_factory("ParcellationEntityVersion")(_id)
1199
+ for pe in pev.get("isVersionOf")
1200
+ }
1201
+ ]
1202
+ yield from cls.translate_pes(src, pe_uuids)
1203
+
910
1204
  @classmethod
911
1205
  def parse_from_region(cls, region: "Region") -> Iterable["RegionRelationAssessments"]:
1206
+ """
1207
+ Main entry on how related regions should be retrieved. Given a region,
1208
+ retrieves all RegionRelationAssessments
1209
+
1210
+ Parameters
1211
+ ----------
1212
+ region: Region
1213
+
1214
+ Returns
1215
+ -------
1216
+ Iterable[RegionRelationAssessments]
1217
+ """
912
1218
  if not region._spec:
913
1219
  return None
914
1220
  for ebrain_type, ebrain_ref in region._spec.get("ebrains", {}).items():