From 3930d5f6128372ae974a790be09d7f8c250b7f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Tue, 14 Jan 2025 14:48:40 +0100 Subject: [PATCH 1/3] add Zap and DelayedZap stimuli, and sinespec factory fct --- bluecellulab/stimulus/factory.py | 126 ++++++++++++++++++++++++++++ tests/test_stimulus/test_factory.py | 19 +++++ 2 files changed, 145 insertions(+) diff --git a/bluecellulab/stimulus/factory.py b/bluecellulab/stimulus/factory.py index 5e8a1c9..6e5ce3e 100644 --- a/bluecellulab/stimulus/factory.py +++ b/bluecellulab/stimulus/factory.py @@ -141,6 +141,23 @@ def current(self) -> np.ndarray: return np.linspace(self.amplitude_start, self.amplitude_end, len(self.time)) +class Zap(Stimulus): + def __init__(self, dt: float, duration: float, amplitude: float) -> None: + super().__init__(dt) + self.duration = duration + self.amplitude = amplitude + + @property + def time(self) -> np.ndarray: + return np.arange(0.0, self.duration, self.dt) + + @property + def current(self) -> np.ndarray: + return self.amplitude * np.sin( + 2.0 * np.pi * (1.0 + (1.0 / (5.15 - (self.time - 0.1)))) * (self.time - 0.1) + ) + + class Step(Stimulus): def __init__(self): @@ -276,6 +293,71 @@ def threshold_based( return res +class DelayedZap(Stimulus): + + def __init__(self): + raise NotImplementedError( + "This class cannot be instantiated directly. " + "Please use the class methods 'amplitude_based' " + "or 'threshold_based' to create objects." + ) + + @classmethod + def amplitude_based( + cls, + dt: float, + pre_delay: float, + duration: float, + post_delay: float, + amplitude: float, + ) -> CombinedStimulus: + """Create a DelayedYap stimulus from given time events and amplitude. + + Args: + dt: The time step of the stimulus. + pre_delay: The delay before the start of the step. + duration: The duration of the step. + post_delay: The time to wait after the end of the step. + amplitude: The amplitude of the step. + """ + return ( + Empty(dt, duration=pre_delay) + + Zap(dt, duration=duration, amplitude=amplitude) + + Empty(dt, duration=post_delay) + ) + + @classmethod + def threshold_based( + cls, + dt: float, + pre_delay: float, + duration: float, + post_delay: float, + threshold_current: float, + threshold_percentage: float, + ) -> CombinedStimulus: + """Creates a SineSpec stimulus with respect to the threshold current. + + Args: + + dt: The time step of the stimulus. + pre_delay: The delay before the start of the step. + duration: The duration of the step. + post_delay: The time to wait after the end of the step. + threshold_current: The threshold current of the Cell. + threshold_percentage: Percentage of desired threshold_current amplification. + """ + amplitude = threshold_current * threshold_percentage / 100 + res = cls.amplitude_based( + dt, + pre_delay=pre_delay, + duration=duration, + post_delay=post_delay, + amplitude=amplitude, + ) + return res + + class StimulusFactory: def __init__(self, dt: float): self.dt = dt @@ -561,3 +643,47 @@ def neg_cheops( + Empty(self.dt, duration=post_delay) ) return result + + def sinespec( + self, + threshold_current: Optional[float] = None, + threshold_percentage: Optional[float] = 60.0, + amplitude: Optional[float] = None, + pre_delay: Optional[float] = 0, + ) -> Stimulus: + """Returns the SineSpec Stimulus object, a type of Zap stimulus. + + Args: + threshold_current: The threshold current of the Cell. + threshold_percentage: Percentage of desired threshold_current amplification. + amplitude: Raw amplitude of input current. + pre_delay: delay before the start of the stimulus + """ + duration = 5000.0 + post_delay = 0 + + if amplitude is not None: + if threshold_current is not None and threshold_current != 0 and threshold_percentage is not None: + logger.info( + "amplitude, threshold_current and threshold_percentage are all set in sinespec." + " Will only keep amplitude value." + ) + return DelayedZap.amplitude_based( + self.dt, + pre_delay=pre_delay, + duration=duration, + post_delay=post_delay, + amplitude=amplitude, + ) + + if threshold_current is not None and threshold_current != 0 and threshold_percentage is not None: + return DelayedZap.threshold_based( + self.dt, + pre_delay=pre_delay, + duration=duration, + post_delay=post_delay, + threshold_current=threshold_current, + threshold_percentage=threshold_percentage, + ) + + raise TypeError("You have to give either threshold_current or amplitude") diff --git a/tests/test_stimulus/test_factory.py b/tests/test_stimulus/test_factory.py index 68a4bbf..7b00552 100644 --- a/tests/test_stimulus/test_factory.py +++ b/tests/test_stimulus/test_factory.py @@ -21,6 +21,7 @@ Step, Ramp, StimulusFactory, + Zap, ) @@ -186,6 +187,24 @@ def test_create_neg_cheops(self): with pytest.raises(TypeError, match="You have to give either threshold_current or amplitude"): self.factory.neg_cheops() + def test_create_sinespec(self): + s = self.factory.sinespec(threshold_current=1) + assert isinstance(s, CombinedStimulus) + assert s.stimulus_time == 5000.0 + + test_amp = 0.1 + s = self.factory.sinespec(amplitude=test_amp) + assert isinstance(s, CombinedStimulus) + assert s.stimulus_time == 5000.0 + assert np.max(np.abs(s.current)) <= test_amp + assert (s.current == Zap(dt=self.dt, duration=5000.0, amplitude=test_amp).current).all() + + with pytest.raises(TypeError, match="You have to give either threshold_current or amplitude"): + self.factory.sinespec(threshold_current=0.0) + with pytest.raises(TypeError, match="You have to give either threshold_current or amplitude"): + self.factory.sinespec() + + def test_combined_stimulus(): """Test combining Stimulus objects.""" From abe81cc3d855b737fce583e6f6af24aaf47a8e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Tue, 14 Jan 2025 16:08:48 +0100 Subject: [PATCH 2/3] lint fix and change pydantic version for sphinx test --- bluecellulab/stimulus/factory.py | 2 +- docs/requirements_docs.txt | 1 + tests/test_stimulus/test_factory.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bluecellulab/stimulus/factory.py b/bluecellulab/stimulus/factory.py index 6e5ce3e..6a6b1b8 100644 --- a/bluecellulab/stimulus/factory.py +++ b/bluecellulab/stimulus/factory.py @@ -649,7 +649,7 @@ def sinespec( threshold_current: Optional[float] = None, threshold_percentage: Optional[float] = 60.0, amplitude: Optional[float] = None, - pre_delay: Optional[float] = 0, + pre_delay: float = 0, ) -> Stimulus: """Returns the SineSpec Stimulus object, a type of Zap stimulus. diff --git a/docs/requirements_docs.txt b/docs/requirements_docs.txt index 0d6d642..13d6a64 100644 --- a/docs/requirements_docs.txt +++ b/docs/requirements_docs.txt @@ -2,3 +2,4 @@ sphinx>=7.0.1 sphinx-bluebrain-theme>=0.4.1 sphinx_autodoc_typehints>=1.23.2 seaborn>=0.13.2 +pydantic<=2.9.2 diff --git a/tests/test_stimulus/test_factory.py b/tests/test_stimulus/test_factory.py index 7b00552..6fadad0 100644 --- a/tests/test_stimulus/test_factory.py +++ b/tests/test_stimulus/test_factory.py @@ -205,7 +205,6 @@ def test_create_sinespec(self): self.factory.sinespec() - def test_combined_stimulus(): """Test combining Stimulus objects.""" s1 = Step.amplitude_based(0.1, 0.55, 1, 2, 3) From b1de733878f5472d1165b5131ace99a8837c49df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Tue, 14 Jan 2025 16:59:15 +0100 Subject: [PATCH 3/3] fix typo --- bluecellulab/stimulus/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluecellulab/stimulus/factory.py b/bluecellulab/stimulus/factory.py index 6a6b1b8..d452847 100644 --- a/bluecellulab/stimulus/factory.py +++ b/bluecellulab/stimulus/factory.py @@ -311,7 +311,7 @@ def amplitude_based( post_delay: float, amplitude: float, ) -> CombinedStimulus: - """Create a DelayedYap stimulus from given time events and amplitude. + """Create a DelayedZap stimulus from given time events and amplitude. Args: dt: The time step of the stimulus.