FZJ-INM1-BDA / siibra-python

Software interfaces for interacting with brain atlases - Python client
Apache License 2.0
46 stars 8 forks source link

Experimental: feature refactor using attributes #553

Closed xgui3783 closed 1 month ago

xgui3783 commented 5 months ago

design document

in discussion with @dickscheid and @AhmetNSimsek on the refactoring of features.

(from previous meeting notes)

Pseudo code

import siibra
from abc import ABC, abstractproperty
from collections import defaultdict

cursor: "QueryCursor" = siibra.feature.get("foo", "bar")

class Intent(ABC):
    type: str="intent"

    @abstractproperty
    def key(self):
        raise NotImplementedError

    @abstractproperty
    def value(self):
        raise NotImplementedError

    def filter(self, *args, **kwargs) -> bool:
        """Filter method for intent. Subclasses can overwrite 
        on how the parent feature should filter the feature."""
        return True

class Feature:
    id: str
    name: str
    desc: str
    intents: list[Intent]

    def filter(self, *args, **kwargs) -> bool:
        """Proxy method checks all intents. Shortcircuits
                if any returns false. """
        for intent in self.intents:
            if not intent.filter(*args, **kwargs):
                return False
        return True

class QueryCursor:

    def __init__(self, subfeatures: list[Feature]):
        self.subfeatures = subfeatures
        self.cursor = 0
        self._cached_attributes = None

    def seek(self, idx: int):
        self.cursor = idx

    def next(self):
        if self.cursor >= len(self):
            raise StopIteration
        f = self.subfeatures[self.cursor]
        self.cursor += 1
        return f

    def __iter__(self):
        yield self.next()

    def __len__(self):
        return len(self.subfeatures)

    def __getitem__(self, key: slice | int):
        return self.subfeatures[key]

    @property
    def attributes(self) -> dict[str, set[str]]:
        if self._cached_attributes is None:
            return_val = defaultdict(set)
            for f in self.subfeatures:
                for intent in f.intents:
                    return_val[intent.key].add(intent.value)
            self._cached_attributes = return_val
        return self._cached_attributes

    def filter(self, *args, **kwargs) -> "QueryCursor":
        filtered_features = [f for f in self.subfeatures if f.filter(*args, **kwargs)]
        return QueryCursor(filtered_features)

coincidentally similar to entity component system

to consider:

xgui3783 commented 2 months ago

@dickscheid I pushed some changes that I feel makes sense, feel free to comment and/or ping me

xgui3783 commented 2 months ago

@dickscheid I have again added new commit, which, paired with configuration commit of 24d547c068237c4d3c808ea5f62a290797e03622 should work with VOI.

related to the over zoom discussion, I have elected to not remove VolumeProvider, but instead, use existing VolumeProvider to simplify how VoiDataAttribute fetches volume.

My reasoning is that volume provider is also used by map, and template. Rewriting it would have a lot of downstream effects. For this rewrite, we should keep the scope limited to features.

We can certain add a TODO to revisit on how volume is fetched, once we finish this rewrite.


edit:

current tally of features:


live quries

xgui3783 commented 2 months ago

todo for 2024 05 06

xgui3783 commented 1 month ago

superceded by https://github.com/FZJ-INM1-BDA/siibra-python/pull/594