siibra 0.4a76__py3-none-any.whl → 0.5a1__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/commons.py +3 -2
- siibra/configuration/configuration.py +6 -2
- siibra/configuration/factory.py +48 -27
- siibra/explorer/__init__.py +1 -0
- siibra/explorer/url.py +162 -0
- siibra/explorer/util.py +65 -0
- siibra/features/anchor.py +36 -9
- siibra/features/connectivity/__init__.py +6 -2
- siibra/features/connectivity/functional_connectivity.py +21 -0
- siibra/features/connectivity/regional_connectivity.py +91 -86
- siibra/features/dataset/ebrains.py +1 -1
- siibra/features/feature.py +331 -35
- siibra/features/tabular/bigbrain_intensity_profile.py +5 -2
- siibra/features/tabular/cell_density_profile.py +3 -1
- siibra/features/tabular/cortical_profile.py +14 -10
- siibra/features/tabular/gene_expression.py +0 -2
- siibra/features/tabular/layerwise_bigbrain_intensities.py +3 -2
- siibra/features/tabular/receptor_density_profile.py +7 -1
- siibra/features/tabular/regional_timeseries_activity.py +81 -102
- siibra/features/tabular/tabular.py +21 -9
- siibra/livequeries/bigbrain.py +11 -22
- siibra/locations/__init__.py +65 -1
- siibra/locations/boundingbox.py +0 -16
- siibra/locations/location.py +13 -0
- siibra/locations/pointset.py +1 -3
- siibra/retrieval/cache.py +5 -3
- siibra/retrieval/datasets.py +27 -27
- siibra/volumes/neuroglancer.py +6 -9
- {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/METADATA +1 -1
- {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/RECORD +34 -31
- {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/WHEEL +1 -1
- {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/LICENSE +0 -0
- {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/top_level.txt +0 -0
siibra/features/feature.py
CHANGED
|
@@ -20,10 +20,11 @@ from ..commons import logger, InstanceTable, siibra_tqdm, __version__
|
|
|
20
20
|
from ..core import concept
|
|
21
21
|
from ..core import space, region, parcellation
|
|
22
22
|
|
|
23
|
-
from typing import Union, TYPE_CHECKING, List, Dict, Type, Tuple, BinaryIO
|
|
23
|
+
from typing import Union, TYPE_CHECKING, List, Dict, Type, Tuple, BinaryIO, Any, Iterator
|
|
24
24
|
from hashlib import md5
|
|
25
25
|
from collections import defaultdict
|
|
26
26
|
from zipfile import ZipFile
|
|
27
|
+
from abc import ABC
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
29
30
|
from ..retrieval.datasets import EbrainsDataset
|
|
@@ -42,6 +43,10 @@ class NotFoundException(Exception):
|
|
|
42
43
|
pass
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
class ParseCompoundFeatureIdException(Exception):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
45
50
|
_README_TMPL = """
|
|
46
51
|
Downloaded from siibra toolsuite.
|
|
47
52
|
siibra-python version: {version}
|
|
@@ -120,7 +125,7 @@ class Feature:
|
|
|
120
125
|
# allows subclasses to implement lazy loading of an anchor
|
|
121
126
|
return self._anchor_cached
|
|
122
127
|
|
|
123
|
-
def __init_subclass__(cls, configuration_folder=None, category=None, do_not_index=False):
|
|
128
|
+
def __init_subclass__(cls, configuration_folder=None, category=None, do_not_index=False, **kwargs):
|
|
124
129
|
|
|
125
130
|
# Feature.SUBCLASSES serves as an index where feature class inheritance is cached. When users
|
|
126
131
|
# queries a branch on the hierarchy, all children will also be queried. There are usecases where
|
|
@@ -144,7 +149,7 @@ class Feature:
|
|
|
144
149
|
cls.category = category
|
|
145
150
|
if category is not None:
|
|
146
151
|
cls.CATEGORIZED[category].add(cls.__name__, cls)
|
|
147
|
-
return super().__init_subclass__()
|
|
152
|
+
return super().__init_subclass__(**kwargs)
|
|
148
153
|
|
|
149
154
|
@classmethod
|
|
150
155
|
def _get_subclasses(cls):
|
|
@@ -257,6 +262,8 @@ class Feature:
|
|
|
257
262
|
This allows all classes in the __mro__ to have the opportunity to append files
|
|
258
263
|
of interest.
|
|
259
264
|
"""
|
|
265
|
+
if isinstance(self, Compoundable) and "README.md" in fh.namelist():
|
|
266
|
+
return
|
|
260
267
|
ebrains_page = "\n".join(
|
|
261
268
|
{ds.ebrains_page for ds in self.datasets if getattr(ds, "ebrains_page", None)}
|
|
262
269
|
)
|
|
@@ -295,8 +302,11 @@ class Feature:
|
|
|
295
302
|
"""
|
|
296
303
|
Export as a zip archive.
|
|
297
304
|
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
filelike: str or path
|
|
308
|
+
Filelike to write the zip file. User is responsible to ensure the
|
|
309
|
+
correct extension (.zip) is set.
|
|
300
310
|
"""
|
|
301
311
|
fh = ZipFile(filelike, "w")
|
|
302
312
|
self._export(fh)
|
|
@@ -333,22 +343,12 @@ class Feature:
|
|
|
333
343
|
if not hasattr(feat.__class__, '_live_queries'):
|
|
334
344
|
raise EncodeLiveQueryIdException(f"generate_livequery_featureid can only be used on live queries, but {feat.__class__.__name__} is not.")
|
|
335
345
|
|
|
336
|
-
encoded_c =
|
|
337
|
-
if isinstance(concept, space.Space):
|
|
338
|
-
encoded_c.append(f"s:{concept.id}")
|
|
339
|
-
elif isinstance(concept, parcellation.Parcellation):
|
|
340
|
-
encoded_c.append(f"p:{concept.id}")
|
|
341
|
-
elif isinstance(concept, region.Region):
|
|
342
|
-
encoded_c.append(f"p:{concept.parcellation.id}")
|
|
343
|
-
encoded_c.append(f"r:{concept.name}")
|
|
344
|
-
|
|
345
|
-
if len(encoded_c) == 0:
|
|
346
|
-
raise EncodeLiveQueryIdException("no concept is encoded")
|
|
346
|
+
encoded_c = Feature._encode_concept(concept)
|
|
347
347
|
|
|
348
|
-
return f"lq0::{feat.__class__.__name__}::{
|
|
348
|
+
return f"lq0::{feat.__class__.__name__}::{encoded_c}::{feat.id}"
|
|
349
349
|
|
|
350
350
|
@classmethod
|
|
351
|
-
def deserialize_query_context(
|
|
351
|
+
def deserialize_query_context(cls, feature_id: str) -> Tuple[Type['Feature'], concept.AtlasConcept, str]:
|
|
352
352
|
"""
|
|
353
353
|
Deserialize id into query context.
|
|
354
354
|
|
|
@@ -360,41 +360,69 @@ class Feature:
|
|
|
360
360
|
|
|
361
361
|
clsname, *concepts, fid = rest
|
|
362
362
|
|
|
363
|
-
Features =
|
|
363
|
+
Features = cls._parse_featuretype(clsname)
|
|
364
364
|
|
|
365
365
|
if len(Features) == 0:
|
|
366
366
|
raise ParseLiveQueryIdException(f"classname {clsname!r} could not be parsed correctly. {feature_id!r}")
|
|
367
367
|
F = Features[0]
|
|
368
368
|
|
|
369
|
+
concept = cls._decode_concept(concepts)
|
|
370
|
+
|
|
371
|
+
return (F, concept, fid)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _encode_concept(concept: concept.AtlasConcept):
|
|
375
|
+
encoded_c = []
|
|
376
|
+
if isinstance(concept, space.Space):
|
|
377
|
+
encoded_c.append(f"s:{concept.id}")
|
|
378
|
+
elif isinstance(concept, parcellation.Parcellation):
|
|
379
|
+
encoded_c.append(f"p:{concept.id}")
|
|
380
|
+
elif isinstance(concept, region.Region):
|
|
381
|
+
encoded_c.append(f"p:{concept.parcellation.id}")
|
|
382
|
+
encoded_c.append(f"r:{concept.name}")
|
|
383
|
+
|
|
384
|
+
if len(encoded_c) == 0:
|
|
385
|
+
raise EncodeLiveQueryIdException("no concept is encoded")
|
|
386
|
+
|
|
387
|
+
return '::'.join(encoded_c)
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
def _decode_concept(cls, concepts: List[str]) -> concept.AtlasConcept:
|
|
391
|
+
# choose exception to divert try-except correctly
|
|
392
|
+
if issubclass(cls, CompoundFeature):
|
|
393
|
+
exception = ParseCompoundFeatureIdException
|
|
394
|
+
else:
|
|
395
|
+
exception = ParseLiveQueryIdException
|
|
396
|
+
|
|
369
397
|
concept = None
|
|
370
398
|
for c in concepts:
|
|
371
399
|
if c.startswith("s:"):
|
|
372
400
|
if concept is not None:
|
|
373
|
-
raise
|
|
401
|
+
raise exception("Conflicting spec.")
|
|
374
402
|
concept = space.Space.registry()[c.replace("s:", "")]
|
|
375
403
|
if c.startswith("p:"):
|
|
376
404
|
if concept is not None:
|
|
377
|
-
raise
|
|
405
|
+
raise exception("Conflicting spec.")
|
|
378
406
|
concept = parcellation.Parcellation.registry()[c.replace("p:", "")]
|
|
379
407
|
if c.startswith("r:"):
|
|
380
408
|
if concept is None:
|
|
381
|
-
raise
|
|
409
|
+
raise exception("region has been encoded, but parcellation has not been populated in the encoding, {feature_id!r}")
|
|
382
410
|
if not isinstance(concept, parcellation.Parcellation):
|
|
383
|
-
raise
|
|
411
|
+
raise exception("region has been encoded, but previous encoded concept is not parcellation")
|
|
384
412
|
concept = concept.get_region(c.replace("r:", ""))
|
|
385
|
-
if concept is None:
|
|
386
|
-
raise ParseLiveQueryIdException(f"concept was not populated: {feature_id!r}")
|
|
387
413
|
|
|
388
|
-
|
|
414
|
+
if concept is None:
|
|
415
|
+
raise ParseLiveQueryIdException("concept was not populated in feature id")
|
|
416
|
+
return concept
|
|
389
417
|
|
|
390
418
|
@classmethod
|
|
391
|
-
def
|
|
392
|
-
ftypes = {
|
|
419
|
+
def _parse_featuretype(cls, feature_type: str) -> List[Type['Feature']]:
|
|
420
|
+
ftypes = sorted({
|
|
393
421
|
feattype
|
|
394
422
|
for FeatCls, feattypes in cls.SUBCLASSES.items()
|
|
395
423
|
if all(w.lower() in FeatCls.__name__.lower() for w in feature_type.split())
|
|
396
424
|
for feattype in feattypes
|
|
397
|
-
}
|
|
425
|
+
}, key=lambda t: t.__name__)
|
|
398
426
|
if len(ftypes) > 1:
|
|
399
427
|
return [ft for ft in ftypes if getattr(ft, 'category')]
|
|
400
428
|
else:
|
|
@@ -422,7 +450,12 @@ class Feature:
|
|
|
422
450
|
return live_instances
|
|
423
451
|
|
|
424
452
|
@classmethod
|
|
425
|
-
def match(
|
|
453
|
+
def match(
|
|
454
|
+
cls,
|
|
455
|
+
concept: Union[region.Region, parcellation.Parcellation, space.Space],
|
|
456
|
+
feature_type: Union[str, Type['Feature'], list],
|
|
457
|
+
**kwargs
|
|
458
|
+
) -> List['Feature']:
|
|
426
459
|
"""
|
|
427
460
|
Retrieve data features of the desired modality.
|
|
428
461
|
|
|
@@ -448,7 +481,7 @@ class Feature:
|
|
|
448
481
|
if isinstance(feature_type, str):
|
|
449
482
|
# feature type given as a string. Decode the corresponding class.
|
|
450
483
|
# Some string inputs, such as connectivity, may hit multiple matches.
|
|
451
|
-
ftype_candidates = cls.
|
|
484
|
+
ftype_candidates = cls._parse_featuretype(feature_type)
|
|
452
485
|
if len(ftype_candidates) == 0:
|
|
453
486
|
raise ValueError(
|
|
454
487
|
f"feature_type {str(feature_type)} did not match with any "
|
|
@@ -483,10 +516,16 @@ class Feature:
|
|
|
483
516
|
|
|
484
517
|
live_instances = feature_type.livequery(concept, **kwargs)
|
|
485
518
|
|
|
486
|
-
|
|
519
|
+
results = list(dict.fromkeys(preconfigured_instances + live_instances))
|
|
520
|
+
return CompoundFeature.compound(results, concept)
|
|
487
521
|
|
|
488
522
|
@classmethod
|
|
489
|
-
def
|
|
523
|
+
def _get_instance_by_id(cls, feature_id: str, **kwargs):
|
|
524
|
+
try:
|
|
525
|
+
return CompoundFeature._get_instance_by_id(feature_id, **kwargs)
|
|
526
|
+
except ParseCompoundFeatureIdException:
|
|
527
|
+
pass
|
|
528
|
+
|
|
490
529
|
try:
|
|
491
530
|
F, concept, fid = cls.deserialize_query_context(feature_id)
|
|
492
531
|
return [
|
|
@@ -495,12 +534,21 @@ class Feature:
|
|
|
495
534
|
if f.id == fid or f.id == feature_id
|
|
496
535
|
][0]
|
|
497
536
|
except ParseLiveQueryIdException:
|
|
498
|
-
|
|
537
|
+
candidates = [
|
|
499
538
|
inst
|
|
500
539
|
for Cls in Feature.SUBCLASSES[Feature]
|
|
501
540
|
for inst in Cls.get_instances()
|
|
502
541
|
if inst.id == feature_id
|
|
503
|
-
]
|
|
542
|
+
]
|
|
543
|
+
if len(candidates) == 0:
|
|
544
|
+
raise NotFoundException(f"No feature instance wth {feature_id} found.")
|
|
545
|
+
if len(candidates) == 1:
|
|
546
|
+
return candidates[0]
|
|
547
|
+
else:
|
|
548
|
+
raise RuntimeError(
|
|
549
|
+
f"Multiple feature instance match {feature_id}",
|
|
550
|
+
[c.name for c in candidates]
|
|
551
|
+
)
|
|
504
552
|
except IndexError:
|
|
505
553
|
raise NotFoundException
|
|
506
554
|
|
|
@@ -563,3 +611,251 @@ class Feature:
|
|
|
563
611
|
return getattr(self.inst, __name)
|
|
564
612
|
|
|
565
613
|
return ProxyFeature(feature, fid)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class Compoundable(ABC):
|
|
617
|
+
"""
|
|
618
|
+
Base class for structures which allow compounding.
|
|
619
|
+
Determines the necessary grouping and compounding attributes.
|
|
620
|
+
"""
|
|
621
|
+
_filter_attrs = [] # the attributes to filter this instance of feature
|
|
622
|
+
_compound_attrs = [] # `compound_key` has to be created from `filter_attributes`
|
|
623
|
+
|
|
624
|
+
def __init_subclass__(cls, **kwargs):
|
|
625
|
+
assert len(cls._filter_attrs) > 0, "All compoundable classes have to have `_filter_attrs` defined."
|
|
626
|
+
assert len(cls._compound_attrs) > 0, "All compoundable classes have to have `_compound_attrs` defined."
|
|
627
|
+
assert all(attr in cls._filter_attrs for attr in cls._compound_attrs), "`_compound_attrs` must be a subset of `_filter_attrs`."
|
|
628
|
+
return super().__init_subclass__(**kwargs)
|
|
629
|
+
|
|
630
|
+
def __init__(self):
|
|
631
|
+
assert all(hasattr(self, attr) for attr in self._filter_attrs), "`_filter_attrs` can only consist of the attributes of the class."
|
|
632
|
+
|
|
633
|
+
@property
|
|
634
|
+
def filter_attributes(self) -> Dict[str, Any]:
|
|
635
|
+
"""
|
|
636
|
+
Attributes that help distinguish or combine features of the same type
|
|
637
|
+
among others.
|
|
638
|
+
"""
|
|
639
|
+
return {attr: getattr(self, attr) for attr in self._filter_attrs}
|
|
640
|
+
|
|
641
|
+
@property
|
|
642
|
+
def _compound_key(self) -> Tuple[Any]:
|
|
643
|
+
"""
|
|
644
|
+
A tuple of values that define the basis for compounding elements of
|
|
645
|
+
the same type.
|
|
646
|
+
"""
|
|
647
|
+
return tuple([self.filter_attributes[attr] for attr in self._compound_attrs])
|
|
648
|
+
|
|
649
|
+
@property
|
|
650
|
+
def _element_index(self) -> Any:
|
|
651
|
+
"""
|
|
652
|
+
Unique index of this compoundable feature as a subfeature of the
|
|
653
|
+
Compound. Should be hashable.
|
|
654
|
+
"""
|
|
655
|
+
index = [
|
|
656
|
+
self.filter_attributes[attr]
|
|
657
|
+
for attr in self._filter_attrs
|
|
658
|
+
if attr not in self._compound_attrs
|
|
659
|
+
]
|
|
660
|
+
return index[0] if len(index) == 1 else tuple(index)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class CompoundFeature(Feature):
|
|
664
|
+
"""
|
|
665
|
+
A compound aggregating mutliple features of the same type, forming its
|
|
666
|
+
elements. The anatomical anchors and data of the features is merged.
|
|
667
|
+
Features need to subclass "Compoundable" to allow aggregation
|
|
668
|
+
into a compound feature.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
def __init__(
|
|
672
|
+
self,
|
|
673
|
+
elements: List['Feature'],
|
|
674
|
+
queryconcept: Union[region.Region, parcellation.Parcellation, space.Space]
|
|
675
|
+
):
|
|
676
|
+
"""
|
|
677
|
+
A compound of several features of the same type with an anchor created
|
|
678
|
+
as a sum of adjoinable anchors.
|
|
679
|
+
"""
|
|
680
|
+
self._feature_type = elements[0].__class__
|
|
681
|
+
assert all(isinstance(f, self._feature_type) for f in elements), NotImplementedError("Cannot compound features of different types.")
|
|
682
|
+
self.category = elements[0].category # same feature types have the same category
|
|
683
|
+
assert issubclass(self._feature_type, Compoundable), NotImplementedError(f"Cannot compound {self._feature_type}.")
|
|
684
|
+
|
|
685
|
+
modality = elements[0].modality
|
|
686
|
+
assert all(f.modality == modality for f in elements), NotImplementedError("Cannot compound features of different modalities.")
|
|
687
|
+
|
|
688
|
+
compound_keys = {element._compound_key for element in elements}
|
|
689
|
+
assert len(compound_keys) == 1, ValueError(
|
|
690
|
+
"Only features with identical compound_key can be aggregated."
|
|
691
|
+
)
|
|
692
|
+
self._compounding_attributes = {
|
|
693
|
+
attr: elements[0].filter_attributes[attr]
|
|
694
|
+
for attr in elements[0]._compound_attrs
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
self._elements = {f._element_index: f for f in elements}
|
|
698
|
+
assert len(self._elements) == len(elements), RuntimeError(
|
|
699
|
+
"Element indices should be unique to each element within a CompoundFeature."
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
Feature.__init__(
|
|
703
|
+
self,
|
|
704
|
+
modality=modality,
|
|
705
|
+
description="\n".join({f.description for f in elements}),
|
|
706
|
+
anchor=sum([f.anchor for f in elements]),
|
|
707
|
+
datasets=list(dict.fromkeys([ds for f in elements for ds in f.datasets]))
|
|
708
|
+
)
|
|
709
|
+
self._queryconcept = queryconcept
|
|
710
|
+
|
|
711
|
+
def __getattr__(self, attr: str) -> Any:
|
|
712
|
+
"""Expose compounding attributes explicitly."""
|
|
713
|
+
if attr in self._compounding_attributes:
|
|
714
|
+
return self._compounding_attributes[attr]
|
|
715
|
+
else:
|
|
716
|
+
raise AttributeError(f"{self._feature_type.__name__} has no attribute {attr}.")
|
|
717
|
+
|
|
718
|
+
def __dir__(self):
|
|
719
|
+
return super().__dir__() + list(self._compounding_attributes.keys())
|
|
720
|
+
|
|
721
|
+
@property
|
|
722
|
+
def elements(self):
|
|
723
|
+
"""Features that make up the compound feature."""
|
|
724
|
+
return list(self._elements.values())
|
|
725
|
+
|
|
726
|
+
@property
|
|
727
|
+
def indices(self):
|
|
728
|
+
"""Unique indices to features making up the compound feature."""
|
|
729
|
+
return list(self._elements.keys())
|
|
730
|
+
|
|
731
|
+
@property
|
|
732
|
+
def feature_type(self) -> Type:
|
|
733
|
+
"""Feature type of the elements forming the CompoundFeature."""
|
|
734
|
+
return self._feature_type
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def name(self) -> str:
|
|
738
|
+
"""Returns a short human-readable name of this feature."""
|
|
739
|
+
groupby = ', '.join([
|
|
740
|
+
f"{v} {k}" for k, v in self._compounding_attributes.items()
|
|
741
|
+
])
|
|
742
|
+
return (
|
|
743
|
+
f"{self.__class__.__name__} of {len(self)} "
|
|
744
|
+
f"{self.feature_type.__name__} features grouped by ({groupby})"
|
|
745
|
+
f" anchored at {self.anchor}"
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
@property
|
|
749
|
+
def id(self) -> str:
|
|
750
|
+
return "::".join((
|
|
751
|
+
"cf0",
|
|
752
|
+
f"{self._feature_type.__name__}",
|
|
753
|
+
self._encode_concept(self._queryconcept),
|
|
754
|
+
self.datasets[0].id if self.datasets else "nodsid",
|
|
755
|
+
md5(self.name.encode("utf-8")).hexdigest()
|
|
756
|
+
))
|
|
757
|
+
|
|
758
|
+
def __iter__(self) -> Iterator['Feature']:
|
|
759
|
+
"""Iterate over subfeatures"""
|
|
760
|
+
return self.elements.__iter__()
|
|
761
|
+
|
|
762
|
+
def __len__(self):
|
|
763
|
+
"""Number of subfeatures making the CompoundFeature"""
|
|
764
|
+
return len(self._elements)
|
|
765
|
+
|
|
766
|
+
def __getitem__(self, index: Any):
|
|
767
|
+
"""Get the nth element in the compound."""
|
|
768
|
+
return self.elements[index]
|
|
769
|
+
|
|
770
|
+
def get_element(self, index: Any):
|
|
771
|
+
"""Get the element with its unique index in the compound."""
|
|
772
|
+
try:
|
|
773
|
+
return self._elements[index]
|
|
774
|
+
except Exception:
|
|
775
|
+
raise IndexError(f"No feature with index '{index}' in this compound.")
|
|
776
|
+
|
|
777
|
+
@classmethod
|
|
778
|
+
def compound(
|
|
779
|
+
cls,
|
|
780
|
+
features: List['Feature'],
|
|
781
|
+
queryconcept: Union[region.Region, parcellation.Parcellation, space.Space]
|
|
782
|
+
) -> List['CompoundFeature']:
|
|
783
|
+
"""
|
|
784
|
+
Compound features of the same the same type based on their `_compound_key`.
|
|
785
|
+
|
|
786
|
+
If there are features that are not of type `Compoundable`, they are
|
|
787
|
+
returned as is.
|
|
788
|
+
|
|
789
|
+
Parameters
|
|
790
|
+
----------
|
|
791
|
+
features: List[Feature]
|
|
792
|
+
Feature instances to be compounded.
|
|
793
|
+
queryconcept:
|
|
794
|
+
AtlasConcept used for the query.
|
|
795
|
+
|
|
796
|
+
Returns
|
|
797
|
+
-------
|
|
798
|
+
List[CompoundFeature | Feature]
|
|
799
|
+
"""
|
|
800
|
+
non_compound_features = []
|
|
801
|
+
grouped_features = defaultdict(list)
|
|
802
|
+
for f in features:
|
|
803
|
+
if isinstance(f, Compoundable):
|
|
804
|
+
grouped_features[f._compound_key].append(f)
|
|
805
|
+
continue
|
|
806
|
+
non_compound_features.append(f)
|
|
807
|
+
return non_compound_features + [
|
|
808
|
+
cls(fts, queryconcept)
|
|
809
|
+
for fts in grouped_features.values() if fts
|
|
810
|
+
]
|
|
811
|
+
|
|
812
|
+
@classmethod
|
|
813
|
+
def _get_instance_by_id(cls, feature_id: str, **kwargs):
|
|
814
|
+
"""
|
|
815
|
+
Use the feature id to obtain the same feature instance.
|
|
816
|
+
|
|
817
|
+
Parameters
|
|
818
|
+
----------
|
|
819
|
+
feature_id : str
|
|
820
|
+
|
|
821
|
+
Returns
|
|
822
|
+
-------
|
|
823
|
+
CompoundFeature
|
|
824
|
+
|
|
825
|
+
Raises
|
|
826
|
+
------
|
|
827
|
+
ParseCompoundFeatureIdException
|
|
828
|
+
If no or multiple matches are found. Or id is not fitting to
|
|
829
|
+
compound features.
|
|
830
|
+
"""
|
|
831
|
+
if not feature_id.startswith("cf0::"):
|
|
832
|
+
raise ParseCompoundFeatureIdException("CompoundFeature id must start with cf0::")
|
|
833
|
+
cf_version, clsname, *queryconcept, dsid, fid = feature_id.split("::")
|
|
834
|
+
assert cf_version == "cf0"
|
|
835
|
+
candidates = [
|
|
836
|
+
f
|
|
837
|
+
for f in Feature.match(
|
|
838
|
+
concept=cls._decode_concept(queryconcept),
|
|
839
|
+
feature_type=clsname
|
|
840
|
+
)
|
|
841
|
+
if f.id == fid or f.id == feature_id
|
|
842
|
+
]
|
|
843
|
+
if candidates:
|
|
844
|
+
if len(candidates) == 1:
|
|
845
|
+
return candidates[0]
|
|
846
|
+
else:
|
|
847
|
+
raise ParseCompoundFeatureIdException(
|
|
848
|
+
f"The query with id '{feature_id}' have resulted multiple instances.")
|
|
849
|
+
else:
|
|
850
|
+
raise ParseCompoundFeatureIdException
|
|
851
|
+
|
|
852
|
+
def _export(self, fh: ZipFile):
|
|
853
|
+
super()._export(fh)
|
|
854
|
+
for idx, element in siibra_tqdm(self._elements.items(), desc="Exporting elements", unit="element"):
|
|
855
|
+
if '/' in str(idx):
|
|
856
|
+
logger.warning(f"'/' will be replaced with ' ' of the file for element with index {idx}")
|
|
857
|
+
filename = '/'.join([
|
|
858
|
+
str(i).replace('/', ' ')
|
|
859
|
+
for i in (idx if isinstance(idx, tuple) else [idx])
|
|
860
|
+
])
|
|
861
|
+
fh.writestr(f"{self.feature_type.__name__}/{filename}.csv", element.data.to_csv())
|
|
@@ -29,10 +29,13 @@ class BigBrainIntensityProfile(
|
|
|
29
29
|
"cortical layers: Cortical and laminar thickness gradients diverge in sensory and "
|
|
30
30
|
"motor cortices. PLoS Biology, 18(4), e3000678. "
|
|
31
31
|
"http://dx.doi.org/10.1371/journal.pbio.3000678'."
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"The data is taken from the tutorial at "
|
|
33
|
+
"https://github.com/kwagstyl/cortical_layers_tutorial. Each vertex is "
|
|
34
|
+
"assigned to the regional map when queried."
|
|
34
35
|
)
|
|
35
36
|
|
|
37
|
+
_filter_attrs = cortical_profile.CorticalProfile._filter_attrs + ["location"]
|
|
38
|
+
|
|
36
39
|
def __init__(
|
|
37
40
|
self,
|
|
38
41
|
regionname: str,
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
from .. import anchor as _anchor
|
|
17
16
|
from . import cortical_profile
|
|
18
17
|
|
|
18
|
+
from .. import anchor as _anchor
|
|
19
19
|
from ...commons import PolyLine, logger, create_key
|
|
20
20
|
from ...retrieval import requests
|
|
21
21
|
|
|
@@ -43,6 +43,8 @@ class CellDensityProfile(
|
|
|
43
43
|
|
|
44
44
|
BIGBRAIN_VOLUMETRIC_SHRINKAGE_FACTOR = 1.931
|
|
45
45
|
|
|
46
|
+
_filter_attrs = cortical_profile.CorticalProfile._filter_attrs + ["section", "patch"]
|
|
47
|
+
|
|
46
48
|
@classmethod
|
|
47
49
|
def CELL_READER(cls, b):
|
|
48
50
|
return pd.read_csv(BytesIO(b[2:]), delimiter=" ", header=0).astype(
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
16
|
from . import tabular
|
|
17
|
+
from ..feature import Compoundable
|
|
17
18
|
|
|
18
19
|
from .. import anchor as _anchor
|
|
19
20
|
|
|
@@ -23,7 +24,7 @@ from textwrap import wrap
|
|
|
23
24
|
import numpy as np
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
class CorticalProfile(tabular.Tabular):
|
|
27
|
+
class CorticalProfile(tabular.Tabular, Compoundable):
|
|
27
28
|
"""
|
|
28
29
|
Represents a 1-dimensional profile of measurements along cortical depth,
|
|
29
30
|
measured at relative depths between 0 representing the pial surface,
|
|
@@ -43,6 +44,9 @@ class CorticalProfile(tabular.Tabular):
|
|
|
43
44
|
LAYERS = {0: "0", 1: "I", 2: "II", 3: "III", 4: "IV", 5: "V", 6: "VI", 7: "WM"}
|
|
44
45
|
BOUNDARIES = list(zip(list(LAYERS.keys())[:-1], list(LAYERS.keys())[1:]))
|
|
45
46
|
|
|
47
|
+
_filter_attrs = ["modality"]
|
|
48
|
+
_compound_attrs = ["modality"]
|
|
49
|
+
|
|
46
50
|
def __init__(
|
|
47
51
|
self,
|
|
48
52
|
description: str,
|
|
@@ -169,18 +173,18 @@ class CorticalProfile(tabular.Tabular):
|
|
|
169
173
|
"matplotlib", "plotly", or others supported by pandas DataFrame
|
|
170
174
|
plotting backend.
|
|
171
175
|
**kwargs
|
|
172
|
-
Keyword arguments are passed on to the plot command.
|
|
173
|
-
|
|
176
|
+
Keyword arguments are passed on to the plot command. 'layercolor'
|
|
177
|
+
can be used to specify a color for cortical layer shading.
|
|
174
178
|
"""
|
|
175
179
|
wrapwidth = kwargs.pop("textwrap") if "textwrap" in kwargs else 40
|
|
176
180
|
kwargs["title"] = kwargs.get("title", "\n".join(wrap(self.name, wrapwidth)))
|
|
181
|
+
layercolor = kwargs.pop("layercolor", "gray")
|
|
177
182
|
|
|
178
|
-
if backend == "matplotlib":
|
|
183
|
+
if backend == "matplotlib":
|
|
179
184
|
kwargs["xlabel"] = kwargs.get("xlabel", "Cortical depth")
|
|
180
185
|
kwargs["ylabel"] = kwargs.get("ylabel", self.unit)
|
|
181
186
|
kwargs["grid"] = kwargs.get("grid", True)
|
|
182
187
|
kwargs["ylim"] = kwargs.get("ylim", (0, max(self._values)))
|
|
183
|
-
layercolor = kwargs.pop("layercolor") if "layercolor" in kwargs else "black"
|
|
184
188
|
axs = self.data.plot(*args, **kwargs, backend=backend)
|
|
185
189
|
|
|
186
190
|
if self.boundaries_mapped:
|
|
@@ -194,22 +198,22 @@ class CorticalProfile(tabular.Tabular):
|
|
|
194
198
|
ha="center",
|
|
195
199
|
)
|
|
196
200
|
if i % 2 == 0:
|
|
197
|
-
axs.axvspan(d1, d2, color=layercolor, alpha=0.
|
|
201
|
+
axs.axvspan(d1, d2, color=layercolor, alpha=0.3)
|
|
198
202
|
|
|
199
203
|
axs.set_title(axs.get_title(), fontsize="medium")
|
|
200
204
|
return axs
|
|
201
205
|
elif backend == "plotly":
|
|
202
206
|
kwargs["title"] = kwargs["title"].replace("\n", "<br>")
|
|
203
207
|
kwargs["labels"] = {
|
|
204
|
-
"index": kwargs.pop("xlabel", "Cortical depth"),
|
|
205
|
-
"value": kwargs.pop("ylabel", self.unit)
|
|
208
|
+
"index": kwargs.pop("xlabel", None) or kwargs.pop("index", "Cortical depth"),
|
|
209
|
+
"value": kwargs.pop("ylabel", None) or kwargs.pop("value", self.unit)
|
|
206
210
|
}
|
|
207
211
|
fig = self.data.plot(*args, **kwargs, backend=backend)
|
|
208
212
|
if self.boundaries_mapped:
|
|
209
213
|
bvals = list(self.boundary_positions.values())
|
|
210
214
|
for i, (d1, d2) in enumerate(list(zip(bvals[:-1], bvals[1:]))):
|
|
211
215
|
fig.add_vrect(
|
|
212
|
-
x0=d1, x1=d2, line_width=0, fillcolor=
|
|
216
|
+
x0=d1, x1=d2, line_width=0, fillcolor=layercolor,
|
|
213
217
|
opacity=0.2 if i % 2 == 0 else 0.0,
|
|
214
218
|
label=dict(text=self.LAYERS[i + 1], textposition="bottom center")
|
|
215
219
|
)
|
|
@@ -218,7 +222,7 @@ class CorticalProfile(tabular.Tabular):
|
|
|
218
222
|
yaxis_range=(0, max(self._values)),
|
|
219
223
|
title=dict(
|
|
220
224
|
automargin=True, yref="container", xref="container",
|
|
221
|
-
pad=dict(t=
|
|
225
|
+
pad=dict(t=40), xanchor="left", yanchor="top"
|
|
222
226
|
)
|
|
223
227
|
)
|
|
224
228
|
return fig
|
|
@@ -32,8 +32,9 @@ class LayerwiseBigBrainIntensities(
|
|
|
32
32
|
"cortical layers: Cortical and laminar thickness gradients diverge in sensory and "
|
|
33
33
|
"motor cortices. PLoS Biology, 18(4), e3000678. "
|
|
34
34
|
"http://dx.doi.org/10.1371/journal.pbio.3000678'."
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"The data is taken from the tutorial at "
|
|
36
|
+
"https://github.com/kwagstyl/cortical_layers_tutorial. Each vertex is "
|
|
37
|
+
"assigned to the regional map when queried."
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
def __init__(
|
|
@@ -34,6 +34,8 @@ class ReceptorDensityProfile(
|
|
|
34
34
|
"to the border between layer VI and the white matter."
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
_filter_attrs = cortical_profile.CorticalProfile._filter_attrs + ["receptor"]
|
|
38
|
+
|
|
37
39
|
def __init__(
|
|
38
40
|
self,
|
|
39
41
|
receptor: str,
|
|
@@ -47,7 +49,7 @@ class ReceptorDensityProfile(
|
|
|
47
49
|
cortical_profile.CorticalProfile.__init__(
|
|
48
50
|
self,
|
|
49
51
|
description=self.DESCRIPTION,
|
|
50
|
-
modality=
|
|
52
|
+
modality="Receptor density",
|
|
51
53
|
anchor=anchor,
|
|
52
54
|
datasets=datasets,
|
|
53
55
|
)
|
|
@@ -73,6 +75,10 @@ class ReceptorDensityProfile(
|
|
|
73
75
|
vocabularies.RECEPTOR_SYMBOLS[self.type]['receptor']['name'],
|
|
74
76
|
)
|
|
75
77
|
|
|
78
|
+
@property
|
|
79
|
+
def name(self):
|
|
80
|
+
return super().name + f" for {self.type}"
|
|
81
|
+
|
|
76
82
|
@property
|
|
77
83
|
def neurotransmitter(self):
|
|
78
84
|
return "{} ({})".format(
|