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.
- siibra/VERSION +1 -1
- siibra/__init__.py +20 -12
- siibra/commons.py +145 -90
- siibra/configuration/__init__.py +1 -1
- siibra/configuration/configuration.py +22 -17
- siibra/configuration/factory.py +177 -128
- siibra/core/__init__.py +1 -8
- siibra/core/{relation_qualification.py → assignment.py} +17 -14
- siibra/core/atlas.py +66 -35
- siibra/core/concept.py +81 -39
- siibra/core/parcellation.py +83 -67
- siibra/core/region.py +569 -263
- siibra/core/space.py +7 -39
- 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 +16 -0
- siibra/explorer/url.py +112 -52
- siibra/explorer/util.py +31 -9
- siibra/features/__init__.py +73 -8
- siibra/features/anchor.py +75 -196
- siibra/features/connectivity/__init__.py +1 -1
- siibra/features/connectivity/functional_connectivity.py +2 -2
- siibra/features/connectivity/regional_connectivity.py +99 -10
- siibra/features/connectivity/streamline_counts.py +1 -1
- siibra/features/connectivity/streamline_lengths.py +1 -1
- siibra/features/connectivity/tracing_connectivity.py +1 -1
- siibra/features/dataset/__init__.py +1 -1
- siibra/features/dataset/ebrains.py +3 -3
- siibra/features/feature.py +219 -110
- siibra/features/image/__init__.py +1 -1
- siibra/features/image/image.py +21 -13
- siibra/features/image/sections.py +1 -1
- siibra/features/image/volume_of_interest.py +1 -1
- siibra/features/tabular/__init__.py +1 -1
- siibra/features/tabular/bigbrain_intensity_profile.py +24 -13
- siibra/features/tabular/cell_density_profile.py +111 -69
- siibra/features/tabular/cortical_profile.py +82 -16
- siibra/features/tabular/gene_expression.py +117 -6
- siibra/features/tabular/layerwise_bigbrain_intensities.py +7 -9
- siibra/features/tabular/layerwise_cell_density.py +9 -24
- siibra/features/tabular/receptor_density_fingerprint.py +11 -6
- siibra/features/tabular/receptor_density_profile.py +12 -15
- siibra/features/tabular/regional_timeseries_activity.py +74 -18
- siibra/features/tabular/tabular.py +17 -8
- siibra/livequeries/__init__.py +1 -7
- siibra/livequeries/allen.py +139 -77
- siibra/livequeries/bigbrain.py +104 -128
- siibra/livequeries/ebrains.py +7 -4
- siibra/livequeries/query.py +1 -2
- siibra/locations/__init__.py +32 -25
- siibra/locations/boundingbox.py +153 -127
- siibra/locations/location.py +45 -80
- siibra/locations/point.py +97 -83
- siibra/locations/pointcloud.py +349 -0
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +107 -13
- siibra/retrieval/datasets.py +9 -14
- siibra/retrieval/exceptions/__init__.py +2 -1
- siibra/retrieval/repositories.py +147 -53
- siibra/retrieval/requests.py +64 -29
- siibra/vocabularies/__init__.py +2 -2
- siibra/volumes/__init__.py +7 -9
- siibra/volumes/parcellationmap.py +396 -253
- siibra/volumes/providers/__init__.py +20 -0
- siibra/volumes/providers/freesurfer.py +113 -0
- siibra/volumes/{gifti.py → providers/gifti.py} +29 -18
- siibra/volumes/{neuroglancer.py → providers/neuroglancer.py} +204 -92
- siibra/volumes/{nifti.py → providers/nifti.py} +64 -44
- siibra/volumes/providers/provider.py +107 -0
- siibra/volumes/sparsemap.py +159 -260
- siibra/volumes/volume.py +720 -152
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/METADATA +25 -28
- siibra-1.0.0a1.dist-info/RECORD +84 -0
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/WHEEL +1 -1
- siibra/locations/pointset.py +0 -198
- siibra-0.5a2.dist-info/RECORD +0 -74
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/LICENSE +0 -0
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/top_level.txt +0 -0
siibra/features/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
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");
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Multimodal data features types and query mechanisms."""
|
|
16
16
|
|
|
17
|
+
from typing import Union
|
|
18
|
+
from functools import partial
|
|
19
|
+
|
|
17
20
|
from . import (
|
|
18
21
|
connectivity,
|
|
19
22
|
tabular,
|
|
@@ -21,21 +24,24 @@ from . import (
|
|
|
21
24
|
dataset,
|
|
22
25
|
)
|
|
23
26
|
|
|
27
|
+
from ..commons import logger
|
|
24
28
|
|
|
25
29
|
from .feature import Feature
|
|
26
|
-
|
|
30
|
+
from ..retrieval import cache
|
|
31
|
+
from ..commons import siibra_tqdm
|
|
27
32
|
|
|
33
|
+
get = Feature._match
|
|
28
34
|
|
|
29
|
-
TYPES = Feature._get_subclasses()
|
|
35
|
+
TYPES = Feature._get_subclasses() # Feature types that can be used to query for features
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def __dir__():
|
|
33
|
-
return list(Feature.
|
|
39
|
+
return list(Feature._CATEGORIZED.keys()) + ["get", "TYPES", "render_ascii_tree"]
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
def __getattr__(attr: str):
|
|
37
|
-
if attr in Feature.
|
|
38
|
-
return Feature.
|
|
43
|
+
if attr in Feature._CATEGORIZED:
|
|
44
|
+
return Feature._CATEGORIZED[attr]
|
|
39
45
|
else:
|
|
40
46
|
hint = ""
|
|
41
47
|
if isinstance(attr, str):
|
|
@@ -46,7 +52,66 @@ def __getattr__(attr: str):
|
|
|
46
52
|
raise AttributeError(f"No such attribute: {__name__}.{attr} " + hint)
|
|
47
53
|
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
@cache.Warmup.register_warmup_fn()
|
|
56
|
+
def _warm_feature_cache_instances():
|
|
50
57
|
"""Preload preconfigured multimodal data features."""
|
|
51
58
|
for ftype in TYPES.values():
|
|
52
|
-
_ = ftype.
|
|
59
|
+
_ = ftype._get_instances()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cache.Warmup.register_warmup_fn(cache.WarmupLevel.DATA, is_factory=True)
|
|
63
|
+
def _warm_feature_cache_data():
|
|
64
|
+
return_callables = []
|
|
65
|
+
for ftype in TYPES.values():
|
|
66
|
+
instances = ftype._get_instances()
|
|
67
|
+
|
|
68
|
+
# the instances *must* be cleared, or it will impede the garbage collection, and results in memleak
|
|
69
|
+
ftype._clean_instances()
|
|
70
|
+
tally = siibra_tqdm(desc=f"Warming data {ftype.__name__}", total=len(instances))
|
|
71
|
+
for f in instances:
|
|
72
|
+
def get_data(arg):
|
|
73
|
+
tally = arg.pop("tally")
|
|
74
|
+
feature = arg.pop("feature")
|
|
75
|
+
# TODO
|
|
76
|
+
# the try catch is as a result of https://github.com/FZJ-INM1-BDA/siibra-python/issues/509
|
|
77
|
+
# sometimes f.data can fail
|
|
78
|
+
try:
|
|
79
|
+
_ = feature.data
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warn(f"Feature {feature.name} warmup failed: {str(e)}")
|
|
82
|
+
finally:
|
|
83
|
+
tally.update(1)
|
|
84
|
+
# append dictionary, so that popping the dictionary will mark the feature to be garbage collected
|
|
85
|
+
return_callables.append(partial(get_data, {"feature": f, "tally": tally}))
|
|
86
|
+
return return_callables
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_ascii_tree(class_or_classname: Union[type, str]):
|
|
90
|
+
"""
|
|
91
|
+
Print the ascii hierarchy representation of a feature type.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
class_or_classname: type, str
|
|
96
|
+
Any Feature class or string of the feature type name
|
|
97
|
+
"""
|
|
98
|
+
from anytree.importer import DictImporter
|
|
99
|
+
from anytree import RenderTree
|
|
100
|
+
Cls = TYPES[class_or_classname] if isinstance(class_or_classname, str) else class_or_classname
|
|
101
|
+
assert issubclass(Cls, Feature)
|
|
102
|
+
|
|
103
|
+
def create_treenode(feature_type):
|
|
104
|
+
return {
|
|
105
|
+
'name': feature_type.__name__,
|
|
106
|
+
'children': [
|
|
107
|
+
create_treenode(c)
|
|
108
|
+
for c in feature_type.__subclasses__()
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
D = create_treenode(Cls)
|
|
112
|
+
importer = DictImporter()
|
|
113
|
+
tree = importer.import_(D)
|
|
114
|
+
print("\n".join(
|
|
115
|
+
"%s%s" % (pre, node.name)
|
|
116
|
+
for pre, _, node in RenderTree(tree)
|
|
117
|
+
))
|
siibra/features/anchor.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
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,40 +12,41 @@
|
|
|
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
|
-
"""Handles the relation between study targets and
|
|
15
|
+
"""Handles the relation between study targets and BrainStructures."""
|
|
16
16
|
|
|
17
|
-
from ..commons import
|
|
17
|
+
from ..commons import Species, logger
|
|
18
18
|
|
|
19
|
-
from ..core.
|
|
19
|
+
from ..core.structure import BrainStructure
|
|
20
|
+
from ..core.assignment import AnatomicalAssignment, Qualification
|
|
20
21
|
from ..locations.location import Location
|
|
21
|
-
from ..
|
|
22
|
-
from ..core.parcellation import Parcellation
|
|
22
|
+
from ..core.parcellation import Parcellation, find_regions
|
|
23
23
|
from ..core.region import Region
|
|
24
24
|
from ..core.space import Space
|
|
25
|
-
from ..
|
|
25
|
+
from ..exceptions import SpaceWarpingFailedError
|
|
26
26
|
|
|
27
27
|
from ..vocabularies import REGION_ALIASES
|
|
28
28
|
|
|
29
|
-
from typing import Union, List, Dict
|
|
30
|
-
|
|
31
|
-
AnatomicalAssignment = RelationAssignment[Union[Region, Location]]
|
|
29
|
+
from typing import Union, List, Dict, Iterable
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
class AnatomicalAnchor:
|
|
35
33
|
"""
|
|
36
|
-
Anatomical anchor to an atlas region,
|
|
37
|
-
|
|
38
|
-
or both.
|
|
34
|
+
Anatomical anchor to an atlas region, a geometric primitive in an atlas
|
|
35
|
+
reference space, or both.
|
|
39
36
|
"""
|
|
40
37
|
|
|
41
|
-
_MATCH_MEMO: Dict[str, Dict[Region,
|
|
42
|
-
_MASK_MEMO = {}
|
|
38
|
+
_MATCH_MEMO: Dict[str, Dict[Region, Qualification]] = {}
|
|
43
39
|
|
|
44
|
-
def __init__(
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
species: Union[List[Species], Species, str],
|
|
43
|
+
location: Location = None,
|
|
44
|
+
region: Union[str, Region] = None
|
|
45
|
+
):
|
|
45
46
|
|
|
46
47
|
if isinstance(species, (str, Species)):
|
|
47
48
|
self.species = {Species.decode(species)}
|
|
48
|
-
elif isinstance(species,
|
|
49
|
+
elif isinstance(species, Iterable):
|
|
49
50
|
assert all(isinstance(_, Species) for _ in species)
|
|
50
51
|
self.species = set(species)
|
|
51
52
|
else:
|
|
@@ -55,7 +56,7 @@ class AnatomicalAnchor:
|
|
|
55
56
|
else:
|
|
56
57
|
self.species = {sp}
|
|
57
58
|
self._location_cached = location
|
|
58
|
-
self._assignments: Dict[
|
|
59
|
+
self._assignments: Dict[BrainStructure, List[AnatomicalAssignment]] = {}
|
|
59
60
|
self._last_matched_concept = None
|
|
60
61
|
if isinstance(region, dict):
|
|
61
62
|
self._regions_cached = region
|
|
@@ -64,8 +65,9 @@ class AnatomicalAnchor:
|
|
|
64
65
|
self._regions_cached = None
|
|
65
66
|
self._regionspec = None
|
|
66
67
|
if isinstance(region, Region):
|
|
67
|
-
self._regions_cached = {region:
|
|
68
|
+
self._regions_cached = {region: Qualification.EXACT}
|
|
68
69
|
elif isinstance(region, str):
|
|
70
|
+
# we will decode regions only when needed, see self.regions property
|
|
69
71
|
self._regionspec = region
|
|
70
72
|
else:
|
|
71
73
|
if region is not None:
|
|
@@ -73,24 +75,25 @@ class AnatomicalAnchor:
|
|
|
73
75
|
self._aliases_cached = None
|
|
74
76
|
|
|
75
77
|
@property
|
|
76
|
-
def location(self):
|
|
78
|
+
def location(self) -> Location:
|
|
77
79
|
# allow to overwrite in derived classes
|
|
78
80
|
return self._location_cached
|
|
79
81
|
|
|
80
82
|
@property
|
|
81
83
|
def parcellations(self) -> List[Parcellation]:
|
|
84
|
+
"""
|
|
85
|
+
Return any parcellation objects that regions of this anchor belong to.
|
|
86
|
+
"""
|
|
82
87
|
return list({region.root for region in self.regions})
|
|
83
88
|
|
|
84
89
|
@property
|
|
85
90
|
def space(self) -> Space:
|
|
86
91
|
# may be overriden by derived classes, e.g. in features.VolumeOfInterest
|
|
87
|
-
if self.location is None
|
|
88
|
-
return None
|
|
89
|
-
else:
|
|
90
|
-
return self.location.space
|
|
92
|
+
return None if self.location is None else self.location.space
|
|
91
93
|
|
|
92
94
|
@property
|
|
93
95
|
def region_aliases(self):
|
|
96
|
+
# return any predefined aliases for the region specified in this anchor.
|
|
94
97
|
if self._aliases_cached is None:
|
|
95
98
|
self._aliases_cached: Dict[str, Dict[str, str]] = {
|
|
96
99
|
Species.decode(species_str): region_alias_mapping
|
|
@@ -100,43 +103,52 @@ class AnatomicalAnchor:
|
|
|
100
103
|
return self._aliases_cached
|
|
101
104
|
|
|
102
105
|
@property
|
|
103
|
-
def has_region_aliases(self):
|
|
106
|
+
def has_region_aliases(self) -> bool:
|
|
104
107
|
return len(self.region_aliases) > 0
|
|
105
108
|
|
|
106
109
|
@property
|
|
107
|
-
def regions(self) -> Dict[Region,
|
|
110
|
+
def regions(self) -> Dict[Region, Qualification]:
|
|
111
|
+
"""
|
|
112
|
+
Return the list of regions associated with this anchor.
|
|
113
|
+
Decode the self._regionspec string into region objects now,
|
|
114
|
+
if applicable and called for the first time.
|
|
115
|
+
"""
|
|
108
116
|
# decoding region strings is quite compute intensive, so we cache this at the class level
|
|
109
117
|
if self._regions_cached is not None:
|
|
110
118
|
return self._regions_cached
|
|
111
119
|
|
|
112
120
|
if self._regionspec is None:
|
|
113
|
-
self._regions_cached =
|
|
121
|
+
self._regions_cached = dict()
|
|
114
122
|
return self._regions_cached
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# decode the region specification into a
|
|
124
|
+
match_key = self._regionspec + '-' + str(self.species)
|
|
125
|
+
if match_key not in self.__class__._MATCH_MEMO:
|
|
126
|
+
# decode the region specification into a dict of region objects and assignment qualifications
|
|
119
127
|
regions = {
|
|
120
|
-
|
|
121
|
-
for
|
|
122
|
-
|
|
123
|
-
if r.species == species
|
|
128
|
+
region: Qualification.EXACT
|
|
129
|
+
for region in find_regions(self._regionspec, filter_children=True, find_topmost=False)
|
|
130
|
+
if region.species in self.species
|
|
124
131
|
}
|
|
125
132
|
# add more regions from possible aliases of the region spec
|
|
126
133
|
for alt_species, aliases in self.region_aliases.items():
|
|
127
|
-
for
|
|
128
|
-
for r in
|
|
134
|
+
for alias_regionspec, qualificationspec in aliases.items():
|
|
135
|
+
for r in find_regions(alias_regionspec, filter_children=True, find_topmost=False):
|
|
129
136
|
if r.species != alt_species:
|
|
130
137
|
continue
|
|
131
|
-
if r not in
|
|
132
|
-
regions[r] =
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
if r not in regions:
|
|
139
|
+
regions[r] = Qualification[qualificationspec.upper()]
|
|
140
|
+
|
|
141
|
+
self.__class__._MATCH_MEMO[match_key] = regions
|
|
142
|
+
self._regions_cached = self.__class__._MATCH_MEMO[match_key]
|
|
135
143
|
|
|
136
144
|
return self._regions_cached
|
|
137
145
|
|
|
138
146
|
def __str__(self):
|
|
139
|
-
|
|
147
|
+
parcs = {p.id: p.name for p in self.represented_parcellations()}
|
|
148
|
+
if len(parcs) == 1 and self._regionspec in [pid for pid in parcs]:
|
|
149
|
+
region = parcs[self._regionspec] # if parcellation was anchored with the id instead of the name
|
|
150
|
+
else:
|
|
151
|
+
region = "" if self._regionspec is None else str(self._regionspec)
|
|
140
152
|
location = "" if self.location is None else str(self.location)
|
|
141
153
|
separator = " " if min(len(region), len(location)) > 0 else ""
|
|
142
154
|
if region and location:
|
|
@@ -144,174 +156,41 @@ class AnatomicalAnchor:
|
|
|
144
156
|
else:
|
|
145
157
|
return region + separator + location
|
|
146
158
|
|
|
147
|
-
def
|
|
148
|
-
return self.__str__()
|
|
149
|
-
|
|
150
|
-
def assign(self, concept: AtlasConcept):
|
|
159
|
+
def assign(self, concept: Union[BrainStructure, Space]) -> AnatomicalAssignment:
|
|
151
160
|
"""
|
|
152
|
-
Match this
|
|
161
|
+
Match this anchor to a query concept. Assignments are cached at runtime,
|
|
162
|
+
so repeated assignment with the same concept will be cheap.
|
|
153
163
|
"""
|
|
164
|
+
if isinstance(concept, Space):
|
|
165
|
+
if self.location is not None and self.location.space.matches(concept):
|
|
166
|
+
return [AnatomicalAssignment(concept, self.location, Qualification.CONTAINED)]
|
|
167
|
+
else:
|
|
168
|
+
return []
|
|
169
|
+
|
|
154
170
|
if concept not in self._assignments:
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
or self.has_region_aliases: # dramatic speedup, since decoding _regionspec is expensive
|
|
165
|
-
for r in self.regions:
|
|
166
|
-
matches.append(AnatomicalAnchor.match_regions(r, concept))
|
|
167
|
-
if len(concept.root.find(self._regionspec)) == 0:
|
|
168
|
-
# We perform the (quite expensive) location-to-region test
|
|
169
|
-
# only if this anchor's regionspec is not known to the
|
|
170
|
-
# parcellation of the query region. Otherwise we can rely
|
|
171
|
-
# on the region-to-region test.
|
|
172
|
-
if self.location is not None:
|
|
173
|
-
matches.append(AnatomicalAnchor.match_location_to_region(self.location, concept))
|
|
174
|
-
elif isinstance(concept, Location):
|
|
175
|
-
if self.location is not None:
|
|
176
|
-
matches.append(AnatomicalAnchor.match_locations(self.location, concept))
|
|
177
|
-
for region in self.regions:
|
|
178
|
-
match = AnatomicalAnchor.match_location_to_region(concept, region)
|
|
179
|
-
matches.append(None if match is None else match.invert())
|
|
180
|
-
self._assignments[concept] = sorted(m for m in matches if m is not None)
|
|
171
|
+
assignments: List[AnatomicalAssignment] = []
|
|
172
|
+
if self.location is not None:
|
|
173
|
+
try:
|
|
174
|
+
assignments.append(self.location.assign(concept))
|
|
175
|
+
except SpaceWarpingFailedError as e:
|
|
176
|
+
logger.debug(e)
|
|
177
|
+
for region in self.regions:
|
|
178
|
+
assignments.append(region.assign(concept))
|
|
179
|
+
self._assignments[concept] = sorted(a for a in assignments if a is not None)
|
|
181
180
|
|
|
182
181
|
self._last_matched_concept = concept \
|
|
183
182
|
if len(self._assignments[concept]) > 0 \
|
|
184
183
|
else None
|
|
185
|
-
|
|
186
184
|
return self._assignments[concept]
|
|
187
185
|
|
|
188
|
-
def matches(self, concept:
|
|
186
|
+
def matches(self, concept: Union[BrainStructure, Space]) -> bool:
|
|
189
187
|
return len(self.assign(concept)) > 0
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
def match_locations(cls, location1: Location, location2: Location):
|
|
193
|
-
assert all(isinstance(loc, Location) for loc in [location1, location2])
|
|
194
|
-
if (location1, location2) not in cls._MATCH_MEMO:
|
|
195
|
-
if location1 == location2:
|
|
196
|
-
res = AnatomicalAssignment(location1, location2, AssignmentQualification.EXACT)
|
|
197
|
-
elif location1.contained_in(location2):
|
|
198
|
-
res = AnatomicalAssignment(location1, location2, AssignmentQualification.CONTAINED)
|
|
199
|
-
elif location1.contains(location2):
|
|
200
|
-
res = AnatomicalAssignment(location1, location2, AssignmentQualification.CONTAINS)
|
|
201
|
-
elif location1.intersects(location2):
|
|
202
|
-
res = AnatomicalAssignment(location1, location2, AssignmentQualification.OVERLAPS)
|
|
203
|
-
else:
|
|
204
|
-
res = None
|
|
205
|
-
cls._MATCH_MEMO[location1, location2] = res
|
|
206
|
-
return cls._MATCH_MEMO[location1, location2]
|
|
207
|
-
|
|
208
|
-
@classmethod
|
|
209
|
-
def match_regions(cls, region1: Region, region2: Region):
|
|
210
|
-
assert all(isinstance(r, Region) for r in [region1, region2])
|
|
211
|
-
if (region1, region2) not in cls._MATCH_MEMO:
|
|
212
|
-
if region1 == region2:
|
|
213
|
-
res = AnatomicalAssignment(region1, region2, AssignmentQualification.EXACT)
|
|
214
|
-
elif region1 in region2:
|
|
215
|
-
res = AnatomicalAssignment(region1, region2, AssignmentQualification.CONTAINED)
|
|
216
|
-
elif region2 in region1:
|
|
217
|
-
res = AnatomicalAssignment(region1, region2, AssignmentQualification.CONTAINS)
|
|
218
|
-
else:
|
|
219
|
-
res = None
|
|
220
|
-
cls._MATCH_MEMO[region1, region2] = res
|
|
221
|
-
return cls._MATCH_MEMO[region1, region2]
|
|
222
|
-
|
|
223
|
-
@classmethod
|
|
224
|
-
def match_location_to_region(cls, location: Location, region: Region):
|
|
225
|
-
assert isinstance(location, Location)
|
|
226
|
-
assert isinstance(region, Region)
|
|
227
|
-
|
|
228
|
-
for subregion in region.children:
|
|
229
|
-
res = cls.match_location_to_region(location, subregion)
|
|
230
|
-
if res is not None:
|
|
231
|
-
return res
|
|
232
|
-
|
|
233
|
-
if (location, region) not in cls._MATCH_MEMO:
|
|
234
|
-
# compute mask of the region
|
|
235
|
-
mask = None
|
|
236
|
-
if region.mapped_in_space(location.space, recurse=False):
|
|
237
|
-
if (region, location.space) not in cls._MASK_MEMO:
|
|
238
|
-
cls._MASK_MEMO[region, location.space] = \
|
|
239
|
-
region.fetch_regional_map(space=location.space, maptype='labelled')
|
|
240
|
-
mask = cls._MASK_MEMO[region, location.space]
|
|
241
|
-
mask_space = location.space
|
|
242
|
-
expl = (
|
|
243
|
-
f"{location} was compared with the mask of query region "
|
|
244
|
-
f"'{region.name}' in {location.space.name}."
|
|
245
|
-
)
|
|
246
|
-
else:
|
|
247
|
-
for space in region.supported_spaces:
|
|
248
|
-
if not region.mapped_in_space(space, recurse=False):
|
|
249
|
-
continue
|
|
250
|
-
if not space.provides_image: # siibra does not yet match locations to surface spaces
|
|
251
|
-
continue
|
|
252
|
-
if (region, space) not in cls._MASK_MEMO:
|
|
253
|
-
cls._MASK_MEMO[region, space] = region.fetch_regional_map(space=space, maptype='labelled')
|
|
254
|
-
mask = cls._MASK_MEMO[region, space]
|
|
255
|
-
mask_space = space
|
|
256
|
-
if location.space == mask_space:
|
|
257
|
-
expl = (
|
|
258
|
-
f"{location} was compared with the mask of query region '{region.name}' "
|
|
259
|
-
f"in {mask_space}."
|
|
260
|
-
)
|
|
261
|
-
else:
|
|
262
|
-
expl = (
|
|
263
|
-
f"{location} was warped from {location.space.name} and then compared "
|
|
264
|
-
f"in this space with the mask of query region '{region.name}'."
|
|
265
|
-
)
|
|
266
|
-
if mask is not None:
|
|
267
|
-
break
|
|
268
|
-
|
|
269
|
-
if mask is None:
|
|
270
|
-
logger.debug(
|
|
271
|
-
f"'{region.name}' provides no mask in a space "
|
|
272
|
-
f"to which {location} can be warped."
|
|
273
|
-
)
|
|
274
|
-
res = None
|
|
275
|
-
else:
|
|
276
|
-
# compare mask to location
|
|
277
|
-
loc_warped = location.warp(mask_space)
|
|
278
|
-
if loc_warped is None:
|
|
279
|
-
# seems we cannot warp our location to the mask space
|
|
280
|
-
# this typically happens when the location extends outside
|
|
281
|
-
# the brain. We might still be able the warp the
|
|
282
|
-
# bounding box of the mask to the location and check.
|
|
283
|
-
# TODO in fact we should estimate an affine matrix from the warped bounding box,
|
|
284
|
-
# and resample the mask for the inverse test to be more precise.
|
|
285
|
-
bbox_mask = BoundingBox.from_image(mask, mask_space).warp(location.space)
|
|
286
|
-
return cls.match_locations(location, bbox_mask)
|
|
287
|
-
elif loc_warped.contained_in(mask):
|
|
288
|
-
res = AnatomicalAssignment(location, region, AssignmentQualification.CONTAINED, expl)
|
|
289
|
-
elif loc_warped.contains(mask):
|
|
290
|
-
res = AnatomicalAssignment(location, region, AssignmentQualification.CONTAINS, expl)
|
|
291
|
-
elif loc_warped.intersects(mask):
|
|
292
|
-
res = AnatomicalAssignment(location, region, AssignmentQualification.OVERLAPS, expl)
|
|
293
|
-
else:
|
|
294
|
-
logger.debug(
|
|
295
|
-
f"{location} does not match mask of '{region.name}' in {mask_space.name}."
|
|
296
|
-
)
|
|
297
|
-
res = None
|
|
298
|
-
cls._MATCH_MEMO[location, region] = res
|
|
299
|
-
|
|
300
|
-
# keep mask cache small
|
|
301
|
-
if len(cls._MASK_MEMO) > 3:
|
|
302
|
-
# from Python 3.6, this remove the *oldest* entry
|
|
303
|
-
cls._MASK_MEMO.pop(next(iter(cls._MASK_MEMO)))
|
|
304
|
-
|
|
305
|
-
return cls._MATCH_MEMO[location, region]
|
|
306
|
-
|
|
307
|
-
def represented_parcellations(self):
|
|
189
|
+
def represented_parcellations(self) -> List[Parcellation]:
|
|
308
190
|
"""
|
|
309
191
|
Return any parcellation objects that this anchor explicitly points to.
|
|
310
192
|
"""
|
|
311
|
-
return [
|
|
312
|
-
r for r in self.regions
|
|
313
|
-
if isinstance(r, Parcellation)
|
|
314
|
-
]
|
|
193
|
+
return [r for r in self.regions if isinstance(r, Parcellation)]
|
|
315
194
|
|
|
316
195
|
@property
|
|
317
196
|
def last_match_result(self) -> List[AnatomicalAssignment]:
|
|
@@ -340,6 +219,6 @@ class AnatomicalAnchor:
|
|
|
340
219
|
|
|
341
220
|
return AnatomicalAnchor(species, location, regions)
|
|
342
221
|
|
|
343
|
-
def __radd__(self, other):
|
|
222
|
+
def __radd__(self, other) -> 'AnatomicalAnchor':
|
|
344
223
|
# required to enable `sum`
|
|
345
224
|
return self if other == 0 else self.__add__(other)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
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");
|
|
@@ -41,7 +41,7 @@ class FunctionalConnectivity(
|
|
|
41
41
|
|
|
42
42
|
@property
|
|
43
43
|
def name(self):
|
|
44
|
-
return
|
|
44
|
+
return super().name + f", paradigm: {self.paradigm}"
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class AnatomoFunctionalConnectivity(
|