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.

Files changed (34) hide show
  1. siibra/VERSION +1 -1
  2. siibra/commons.py +3 -2
  3. siibra/configuration/configuration.py +6 -2
  4. siibra/configuration/factory.py +48 -27
  5. siibra/explorer/__init__.py +1 -0
  6. siibra/explorer/url.py +162 -0
  7. siibra/explorer/util.py +65 -0
  8. siibra/features/anchor.py +36 -9
  9. siibra/features/connectivity/__init__.py +6 -2
  10. siibra/features/connectivity/functional_connectivity.py +21 -0
  11. siibra/features/connectivity/regional_connectivity.py +91 -86
  12. siibra/features/dataset/ebrains.py +1 -1
  13. siibra/features/feature.py +331 -35
  14. siibra/features/tabular/bigbrain_intensity_profile.py +5 -2
  15. siibra/features/tabular/cell_density_profile.py +3 -1
  16. siibra/features/tabular/cortical_profile.py +14 -10
  17. siibra/features/tabular/gene_expression.py +0 -2
  18. siibra/features/tabular/layerwise_bigbrain_intensities.py +3 -2
  19. siibra/features/tabular/receptor_density_profile.py +7 -1
  20. siibra/features/tabular/regional_timeseries_activity.py +81 -102
  21. siibra/features/tabular/tabular.py +21 -9
  22. siibra/livequeries/bigbrain.py +11 -22
  23. siibra/locations/__init__.py +65 -1
  24. siibra/locations/boundingbox.py +0 -16
  25. siibra/locations/location.py +13 -0
  26. siibra/locations/pointset.py +1 -3
  27. siibra/retrieval/cache.py +5 -3
  28. siibra/retrieval/datasets.py +27 -27
  29. siibra/volumes/neuroglancer.py +6 -9
  30. {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/METADATA +1 -1
  31. {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/RECORD +34 -31
  32. {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/WHEEL +1 -1
  33. {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/LICENSE +0 -0
  34. {siibra-0.4a76.dist-info → siibra-0.5a1.dist-info}/top_level.txt +0 -0
@@ -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
- Args:
299
- filelike (string or filelike): name or filehandle to write the zip file. User is responsible to ensure the correct extension (.zip) is set.
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__}::{'::'.join(encoded_c)}::{feat.id}"
348
+ return f"lq0::{feat.__class__.__name__}::{encoded_c}::{feat.id}"
349
349
 
350
350
  @classmethod
351
- def deserialize_query_context(Cls, feature_id: str) -> Tuple[Type['Feature'], concept.AtlasConcept, str]:
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 = Cls.parse_featuretype(clsname)
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 ParseLiveQueryIdException("Conflicting spec.")
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 ParseLiveQueryIdException("Conflicting spec.")
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 ParseLiveQueryIdException("region has been encoded, but parcellation has not been populated in the encoding, {feature_id!r}")
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 ParseLiveQueryIdException("region has been encoded, but previous encoded concept is not parcellation")
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
- return (F, concept, fid)
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 parse_featuretype(cls, feature_type: str) -> List[Type['Feature']]:
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(cls, concept: Union[region.Region, parcellation.Parcellation, space.Space], feature_type: Union[str, Type['Feature'], list], **kwargs) -> List['Feature']:
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.parse_featuretype(feature_type)
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
- return list(dict.fromkeys(preconfigured_instances + live_instances))
519
+ results = list(dict.fromkeys(preconfigured_instances + live_instances))
520
+ return CompoundFeature.compound(results, concept)
487
521
 
488
522
  @classmethod
489
- def get_instance_by_id(cls, feature_id: str, **kwargs):
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
- return [
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
- ][0]
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
- "Taken from the tutorial at https://github.com/kwagstyl/cortical_layers_tutorial "
33
- "and assigned to cytoarchitectonic regions of Julich-Brain."
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
- 'layercolor' can be used to specify a color for cortical layer shading.
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.1)
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="gray",
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=15), xanchor="left"
225
+ pad=dict(t=40), xanchor="left", yanchor="top"
222
226
  )
223
227
  )
224
228
  return fig
@@ -16,8 +16,6 @@
16
16
  from .. import anchor as _anchor
17
17
  from . import tabular
18
18
 
19
- from ... import logger
20
-
21
19
  import pandas as pd
22
20
  from textwrap import wrap
23
21
  from typing import List
@@ -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
- "Taken from the tutorial at https://github.com/kwagstyl/cortical_layers_tutorial "
36
- "and assigned to cytoarchitectonic regions of Julich-Brain."
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=f"{receptor} receptor density",
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(