Skip to content

Commit

Permalink
doc: add more documentation
Browse files Browse the repository at this point in the history
2nd release, after some bugfixes and small refactor
  • Loading branch information
jnsbck committed Sep 6, 2023
1 parent 6fff163 commit 1a925d9
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 26 deletions.
176 changes: 162 additions & 14 deletions ephyspy/features/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,55 @@


class BaseFeature(ABC):
r"""Base class for all electrophysiological features.
This class defines the interface for all electrophysiological features.
All sweep features should inherit from this class, and must implement a
`_compute` and `_data_init` method. The `_compute` method should return the
feature value and optionally save diagnostic information for later debugging to
`self._diagnostics`. The `_data_init` method should be used to set the
`self.data` attribute, and add the feature to the `self.data.features`.
The description of the feature should contain a short description of the
feature, and a list of dependencies. The dependencies should be listed
as a comma separated list of feature names. It is parsed and can be displayed
but has no functional use for now. Furthermore, the units of the feature
should be specified. If the feature is unitless, the units should be set to "/".
The docstring should have the following format:
'''<Some Text>
description: <Short description of the feature>.
depends on: <Comma separated list of dependencies>.
units: <Units of the feature>.
<Some more text>'''
`BaseFeature`s can also implement a _plot method, that displays the diagnostic
information or the feature itself. If the feature cannot be displayed in a V(t)
or I(t) plot, instead the `plot` method should be overwritten directly. This
is because `plot` wraps `_plot` adds additional functionality ot it.
"""

def __init__(
self,
data: Optional[EphysSweep] = None,
compute_at_init: bool = True,
name: Optional[str] = None,
):
r"""
Args:
data: EphysSweep object.
Can also be passed later with `__call__`.
compute_at_init: If True, compute the feature at initialization.
Otherwise the feature is only copmuted when `__call__` or
`get_value` is called. This can be useful when instantiating
many features at once, and waiting with the computation until
the features are actually needed.
name: Custom name of the feature. If None, the name of the feature
class is used.
"""
self.name = self.__class__.__name__.lower() if name is None else name
self.name = (
self.name.replace("sweep_", "")
Expand All @@ -64,10 +107,30 @@ def __init__(
self.units = "" if self.units == "/" else self.units

@abstractmethod
def _data_init(self, data: EphysSweep):
def _data_init(self, data: Union[EphysSweep, EphysSweepSet]):
"""Initialize the feature with a EphysSweep or EphysSweepSet object.
This method is called at initialization and when the feature is
called with a new data object. It should be used to set the `self.data`
attribute, and add the feature to the `self.data.features` dictionary.
It can further be used to add any pre-existing / pre-computed features
stored in `self.data.features` to the class attributes (`_value`,
`_diagnostics`, etc.).
Args:
data: EphysSweep object.
"""
self.data = data

def ensure_correct_hyperparams(self):
"""Ensure that parameters passed with the data are used in computation.
Both EphysSweep and EphysSweepSet can come with metadata attached. This
metadata can be used to set default values for hyperparameters of
features. This method ensures that these hyperparameters are used in
computation. It should be called in `_data_init` after setting the
`self.data` attribute.
"""
metadata = self.data.metadata
new_defaults = {kw: v for kw, v in metadata.items() if kw in self.__dict__}
if len(new_defaults) > 0:
Expand All @@ -89,7 +152,7 @@ def _compute(
"""Compute the feature.
All computation that is neccesary to yield the value of the feature should
be defined here. This is the core method of SweepFeature and all other
be defined here. This is the core method of BaseFeature and all other
functionality interacts with this method.
Alongside computing the value of the corresponding feature, this method
Expand Down Expand Up @@ -124,13 +187,26 @@ def recompute(self) -> float:
The value of the feature."""
return self.get_value(recompute=True, store_diagnostics=True)

def get_diagnostics(self, recompute=False):
def get_diagnostics(self, recompute: bool = False) -> Dict[str, Any]:
"""Get diagnostic information about how a feature was computed.
This method returns any intermediary results obtained during computation
of the feature that has been stored in `_diagnostics`. If the feature
is not yet computed, it will be computed first.
Args:
recompute: If True, recompute the feature even if it is already
computed.
Returns:
A dictionary with diagnostic information about the feature computation.
"""
if recompute or self._diagnostics is None:
self.get_value(recompute=recompute, store_diagnostics=True)
return self._diagnostics

@property
def diagnostics(self):
def diagnostics(self) -> Dict[str, Any]:
return self.get_diagnostics()

def _update_diagnostics(self, dct: Dict[str, Any]):
Expand Down Expand Up @@ -172,7 +248,7 @@ def get_value(
return self._value

@property
def value(self):
def value(self) -> Any:
return self.get_value()

@value.setter
Expand Down Expand Up @@ -224,14 +300,14 @@ def plot(
) -> Axes:
"""Adds additional kwargs and functionality to `BaseFeature`._plot`.
Before calling `SweepFeature._plot`, this function checks if the feature
Before calling `BaseFeature._plot`, this function checks if the feature
is a stimulus feature and if so, ensures the feature is plotteed onto
the stimulus axis. Additionally along with every feature, the sweep
can be plotted. Same goes for the stimulus.
If no axis is provided one is created.
This function can be (and should be overwritten) if the feature cannot
be plotted on top of the unterlying sweep.
be displayed on top of the unterlying sweep.
Args:
self (BaseFeature): Feature to plot. Needs to have a `plot` method.
Expand Down Expand Up @@ -311,22 +387,89 @@ def _plot(self, *args, ax: Optional[Axes] = None, **kwargs) -> Axes:


class SpikeFeature(BaseFeature):
r"""Base class for all spike level electrophysiological features.
All spike features should inherit from this class, and must implement the
`_compute` method. The `_compute` method should return the feature value
and optionally save diagnostic information for later debugging to
`self._diagnostics`.
Compared to `SweepFeature`, `SpikeFeature` behaves slightly differently.
Firstly, since spike features are computed on the spike level, results come
in the form of a vector, where each entry corresponds to a spike. Similar to
before this vector is stored in the `_value` attribute. However, because the
handling the spike features is left to the AllenSDK's `process_spikes`, they
`SpikeFeature` just provides an interface to the `_spikes_df` attribute of
the underlying `EphysSweep` object. Secondly, the spike features in the
AllenSDK are defined in a functional manner. This means the `__call__` method
of `SpikeFeature` provides the required functional interface to be able to
compute spike features with `EphysSweep.process_spikes`, while being able to
provide additional functionality to the spike feature class.
Currently, no diagnostics or recursive feature lookup is supported for spike
features! For now this class mainly just acts as a feature function.
The description of the feature should contain a short description of the
feature, and a list of dependencies. The dependencies should be listed
as a comma separated list of feature names. It is parsed and can be displayed
but has no functional use for now. Furthermore, the units of the feature
should be specified. If the feature is unitless, the units should be set to "/".
The docstring should have the following format:
'''<Some Text>
description: <Short description of the feature>.
depends on: <Comma separated list of dependencies>.
units: <Units of the feature>.
<Some more text>'''
All computed features are added to the underlying `EphysSweep`
object, and can be accessed via `lookup_spike_feature`. The methods will
first check if the feature is already computed, and if not, instantiate and
compute it. Any dependencies already computed will be reused, unless
`recompute=True` is passed.
`SpikeFeature`s can also implement a _plot method, the feature. If the
feature cannot be displayed in a V(t) or I(t) plot, instead the `plot` method
should be overwritten directly. This is because `plot` wraps `_plot` adds
additional functionality ot it.
"""

def __init__(
self,
data: Optional[EphysSweep] = None,
compute_at_init: bool = True,
name: Optional[str] = None,
):
r"""
Args:
data: EphysSweep object.
Can also be passed later with `__call__`.
compute_at_init: If True, compute the feature at initialization.
Otherwise the feature is only copmuted when `__call__` or
`get_value` is called. This can be useful when instantiating
many features at once, and waiting with the computation until
the features are actually needed.
name: Custom name of the feature. If None, the name of the feature
class is used.
"""
super().__init__(data, compute_at_init, name)

def _data_init(self, data: EphysSweep):
"""Initialize the feature with a EphysSweep object.
Sets self.data and ensures correct hyperparameters.
Args:
data: EphysSweep object.
"""
self.data = data
if data is not None:
assert isinstance(data, EphysSweep), "data must be EphysSweep"
self.type = type(data).__name__
self.ensure_correct_hyperparams()
if hasattr(self.data, "_spikes_df"):
spike_features = self.data._spikes_df

def lookup_spike_feature(
self, feature_name: str, recompute: bool = False
Expand Down Expand Up @@ -368,11 +511,11 @@ def __str__(self):
@abstractmethod
def _compute(
self, recompute: bool = False, store_diagnostics: bool = True
) -> float:
) -> ndarray:
"""Compute the feature.
All computation that is neccesary to yield the value of the feature should
be defined here. This is the core method of SweepFeature and all other
be defined here. This is the core method of SpikeFeature and all other
functionality interacts with this method.
Alongside computing the value of the corresponding feature, this method
Expand All @@ -383,6 +526,9 @@ def _compute(
be accessed with `get_diagnostics` or via the `diagnostics` property and
updated with `_update_diagnostics`.
When `__call__` is called `_compute` can be thought of as a function
that takes in data (`EphysSweep`) and returns a vector of features.
Args:
recompute: If True, recompute the feature even if it is already
computed.
Expand All @@ -397,7 +543,10 @@ def _compute(
# save diagnostics using _update_diagnostics
return

def get_diagnostics(self, recompute=False):
def get_diagnostics(self, recompute: bool = False):
"""Overwrite get_diagnostics to return None.
Diagnostics is currently not supported for spike features."""
# No diagnostics for spike features for now!
return None

Expand Down Expand Up @@ -439,8 +588,7 @@ def __call__(
class SweepFeature(BaseFeature):
r"""Base class for all sweep level electrophysiological features.
This class defines the interface for all electrophysiological features.
All features should inherit from this class, and must implement the
All sweep features should inherit from this class, and must implement the
`_compute` method. The `_compute` method should return the feature value
and optionally save diagnostic information for later debugging to
`self._diagnostics`.
Expand Down
21 changes: 19 additions & 2 deletions ephyspy/features/spike_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Optional
from typing import Dict, Optional

import numpy as np
from matplotlib.axes import Axes
Expand All @@ -26,7 +26,24 @@
from ephyspy.utils import fwhm, has_spike_feature, is_spike_feature, scatter_spike_ft


def available_spike_features(compute_at_init=False, store_diagnostics=False):
def available_spike_features(
compute_at_init: bool = False, store_diagnostics: bool = False
) -> Dict[str, SpikeFeature]:
"""Return a dictionary of all implemented spike features.
Looks for all classes that inherit from SpikeFeature and returns a dictionary
of all available features. If compute_at_init is True, the features are
computed at initialization.
Args:
compute_at_init (bool, optional): If True, the features are computed at
initialization. Defaults to False.
store_diagnostics (bool, optional): If True, the features are computed
with diagnostics. Defaults to False.
Returns:
dict[str, SpikeFeature]: Dictionary of all available spike features.
"""
all_features = fetch_available_fts()
features = {ft.__name__.lower(): ft for ft in all_features if is_spike_feature(ft)}
features = {k.replace("spike_", ""): v for k, v in features.items()}
Expand Down
21 changes: 19 additions & 2 deletions ephyspy/features/sweep_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from __future__ import annotations

import warnings
from typing import Callable, Optional
from typing import Callable, Dict, Optional

import numpy as np
from matplotlib.pyplot import Axes
Expand Down Expand Up @@ -48,7 +48,24 @@
)


def available_sweep_features(compute_at_init=False, store_diagnostics=False):
def available_sweep_features(
compute_at_init: bool = False, store_diagnostics: bool = False
) -> Dict[str, SweepFeature]:
"""Return a dictionary of all implemented sweep features.
Looks for all classes that inherit from SweepFeature and returns a dictionary
of all available features. If compute_at_init is True, the features are
computed at initialization.
Args:
compute_at_init (bool, optional): If True, the features are computed at
initialization. Defaults to False.
store_diagnostics (bool, optional): If True, the features are computed
with diagnostics. Defaults to False.
Returns:
dict[str, SweepFeature]: Dictionary of all available spike features.
"""
all_features = fetch_available_fts()
features = {ft.__name__.lower(): ft for ft in all_features if is_sweep_feature(ft)}
features = {k.replace("sweep_", ""): v for k, v in features.items()}
Expand Down
21 changes: 19 additions & 2 deletions ephyspy/features/sweepset_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from __future__ import annotations

# ransac = linear_model.RANSACRegressor()
from typing import Optional
from typing import Dict, Optional

import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -36,7 +36,24 @@
from ephyspy.utils import is_sweepset_feature


def available_sweepset_features(compute_at_init=False, store_diagnostics=False):
def available_sweepset_features(
compute_at_init: bool = False, store_diagnostics: bool = False
) -> Dict[str, SweepSetFeature]:
"""Return a dictionary of all implemented sweepset features.
Looks for all classes that inherit from SweepSetFeature and returns a dictionary
of all available features. If compute_at_init is True, the features are
computed at initialization.
Args:
compute_at_init (bool, optional): If True, the features are computed at
initialization. Defaults to False.
store_diagnostics (bool, optional): If True, the features are computed
with diagnostics. Defaults to False.
Returns:
dict[str, SweepSetFeature]: Dictionary of all available spike features.
"""
all_features = fetch_available_fts()
features = {
ft.__name__.lower(): ft for ft in all_features if is_sweepset_feature(ft)
Expand Down
Loading

0 comments on commit 1a925d9

Please sign in to comment.