bluecellulab 2.6.42__py3-none-any.whl → 2.6.44__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 bluecellulab might be problematic. Click here for more details.

@@ -0,0 +1,782 @@
1
+ # Copyright 2023-2024 Blue Brain Project / EPFL
2
+ # Copyright 2025 Open Brain Institute
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import annotations
17
+ from abc import ABC, abstractmethod
18
+ from typing import Optional
19
+ import logging
20
+ import matplotlib.pyplot as plt
21
+ import numpy as np
22
+ from bluecellulab.cell.stimuli_generator import get_relative_shotnoise_params
23
+ from bluecellulab.exceptions import BluecellulabError
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class Stimulus(ABC):
29
+ def __init__(self, dt: float) -> None:
30
+ self.dt = dt
31
+
32
+ @property
33
+ @abstractmethod
34
+ def time(self) -> np.ndarray:
35
+ """Time values of the stimulus."""
36
+ ...
37
+
38
+ @property
39
+ @abstractmethod
40
+ def current(self) -> np.ndarray:
41
+ """Current values of the stimulus."""
42
+ ...
43
+
44
+ def __len__(self) -> int:
45
+ return len(self.time)
46
+
47
+ @property
48
+ def stimulus_time(self) -> float:
49
+ return len(self) * self.dt
50
+
51
+ def __repr__(self) -> str:
52
+ return f"{self.__class__.__name__}(dt={self.dt})"
53
+
54
+ def plot(self, ax=None, **kwargs):
55
+ if ax is None:
56
+ ax = plt.gca()
57
+ ax.plot(self.time, self.current, **kwargs)
58
+ ax.set_xlabel("Time (ms)")
59
+ ax.set_ylabel("Current (nA)")
60
+ ax.set_title(self.__class__.__name__)
61
+ return ax
62
+
63
+ def __add__(self, other: Stimulus) -> CombinedStimulus:
64
+ """Override + operator to concatenate Stimulus objects."""
65
+ if self.dt != other.dt:
66
+ raise ValueError(
67
+ "Stimulus objects must have the same dt to be concatenated"
68
+ )
69
+ if len(self.time) == 0:
70
+ return CombinedStimulus(other.dt, other.time, other.current)
71
+ elif len(other.time) == 0:
72
+ return CombinedStimulus(self.dt, self.time, self.current)
73
+ else:
74
+ # shift other time
75
+ other_time = other.time + self.time[-1] + self.dt
76
+ combined_time = np.concatenate([self.time, other_time])
77
+ # Concatenate the current arrays
78
+ combined_current = np.concatenate([self.current, other.current])
79
+ return CombinedStimulus(self.dt, combined_time, combined_current)
80
+
81
+ def __eq__(self, other: object) -> bool:
82
+ if not isinstance(other, Stimulus):
83
+ return NotImplemented
84
+ else:
85
+ return (
86
+ np.allclose(self.time, other.time)
87
+ and np.allclose(self.current, other.current)
88
+ and self.dt == other.dt
89
+ )
90
+
91
+
92
+ class CombinedStimulus(Stimulus):
93
+ """Represents the Stimulus created by combining multiple stimuli."""
94
+
95
+ def __init__(self, dt: float, time: np.ndarray, current: np.ndarray) -> None:
96
+ super().__init__(dt)
97
+ self._time = time
98
+ self._current = current
99
+
100
+ @property
101
+ def time(self) -> np.ndarray:
102
+ return self._time
103
+
104
+ @property
105
+ def current(self) -> np.ndarray:
106
+ return self._current
107
+
108
+
109
+ class Empty(Stimulus):
110
+ """Represents empty stimulus (all zeros) that has no impact on the cell.
111
+
112
+ This is required by some Stimuli that expect the cell to rest.
113
+ """
114
+
115
+ def __init__(self, dt: float, duration: float) -> None:
116
+ super().__init__(dt)
117
+ self.duration = duration
118
+
119
+ @property
120
+ def time(self) -> np.ndarray:
121
+ return np.arange(0.0, self.duration, self.dt)
122
+
123
+ @property
124
+ def current(self) -> np.ndarray:
125
+ return np.zeros_like(self.time)
126
+
127
+
128
+ class Flat(Stimulus):
129
+ def __init__(self, dt: float, duration: float, amplitude: float) -> None:
130
+ super().__init__(dt)
131
+ self.duration = duration
132
+ self.amplitude = amplitude
133
+
134
+ @property
135
+ def time(self) -> np.ndarray:
136
+ return np.arange(0.0, self.duration, self.dt)
137
+
138
+ @property
139
+ def current(self) -> np.ndarray:
140
+ return np.full_like(self.time, self.amplitude)
141
+
142
+
143
+ class Slope(Stimulus):
144
+ def __init__(
145
+ self, dt: float, duration: float, amplitude_start: float, amplitude_end: float
146
+ ) -> None:
147
+ super().__init__(dt)
148
+ self.duration = duration
149
+ self.amplitude_start = amplitude_start
150
+ self.amplitude_end = amplitude_end
151
+
152
+ @property
153
+ def time(self) -> np.ndarray:
154
+ return np.arange(0.0, self.duration, self.dt)
155
+
156
+ @property
157
+ def current(self) -> np.ndarray:
158
+ return np.linspace(self.amplitude_start, self.amplitude_end, len(self.time))
159
+
160
+
161
+ class Zap(Stimulus):
162
+ def __init__(self, dt: float, duration: float, amplitude: float) -> None:
163
+ super().__init__(dt)
164
+ self.duration = duration
165
+ self.amplitude = amplitude
166
+
167
+ @property
168
+ def time(self) -> np.ndarray:
169
+ return np.arange(0.0, self.duration, self.dt)
170
+
171
+ @property
172
+ def current(self) -> np.ndarray:
173
+ return self.amplitude * np.sin(
174
+ 2.0 * np.pi * (1.0 + (1.0 / (5.15 - (self.time - 0.1)))) * (self.time - 0.1)
175
+ )
176
+
177
+
178
+ class OUProcess(Stimulus):
179
+ """Generates an Ornstein-Uhlenbeck noise signal."""
180
+
181
+ def __init__(self, dt: float, duration: float, tau: float, sigma: float, mean: float, seed: Optional[int] = None):
182
+ super().__init__(dt) # Ensure proper Stimulus initialization
183
+ self.duration = duration
184
+ self.tau = tau
185
+ self.sigma = sigma
186
+ self.mean = mean
187
+ self.seed = seed
188
+
189
+ # Generate OU noise upon initialization
190
+ self._time, self._current = self._generate_ou_noise()
191
+
192
+ @property
193
+ def time(self) -> np.ndarray:
194
+ """Returns the time array for the stimulus duration."""
195
+ return self._time
196
+
197
+ @property
198
+ def current(self) -> np.ndarray:
199
+ """Returns the Ornstein-Uhlenbeck noise signal."""
200
+ return self._current
201
+
202
+ def _generate_ou_noise(self):
203
+ """Generates an Ornstein-Uhlenbeck noise signal."""
204
+ from bluecellulab.cell.stimuli_generator import gen_ornstein_uhlenbeck
205
+ from bluecellulab.rngsettings import RNGSettings
206
+ import neuron
207
+
208
+ rng_settings = RNGSettings.get_instance()
209
+ rng = neuron.h.Random()
210
+
211
+ if rng_settings.mode == "Random123":
212
+ seed1, seed2, seed3 = 2997, 291204, self.seed if self.seed else 123
213
+ rng.Random123(seed1, seed2, seed3)
214
+ else:
215
+ raise ValueError("Ornstein-Uhlenbeck stimulus requires Random123 RNG mode.")
216
+
217
+ # Generate noise signal
218
+ time, current = gen_ornstein_uhlenbeck(self.tau, self.sigma, self.mean, self.duration, self.dt, rng)
219
+ return time, current
220
+
221
+
222
+ class ShotNoiseProcess(Stimulus):
223
+ """Generates a shot noise signal, modeling discrete synaptic events
224
+ occurring at random intervals."""
225
+
226
+ def __init__(
227
+ self, dt: float, duration: float, rate: float, mean: float, sigma: float,
228
+ rise_time: float, decay_time: float, seed: Optional[int] = None
229
+ ):
230
+ super().__init__(dt)
231
+ self.duration = duration
232
+ self.rate = rate
233
+ self.mean = mean
234
+ self.sigma = sigma
235
+ self.rise_time = rise_time
236
+ self.decay_time = decay_time
237
+ self.seed = seed
238
+
239
+ # Generate shot noise signal
240
+ self._time, self._current = self._generate_shot_noise()
241
+
242
+ @property
243
+ def time(self) -> np.ndarray:
244
+ return self._time
245
+
246
+ @property
247
+ def current(self) -> np.ndarray:
248
+ return self._current
249
+
250
+ def _generate_shot_noise(self):
251
+ """Generates the shot noise time and current vectors."""
252
+ from bluecellulab.cell.stimuli_generator import gen_shotnoise_signal
253
+ from bluecellulab.rngsettings import RNGSettings
254
+ import neuron
255
+
256
+ rng_settings = RNGSettings.get_instance()
257
+ rng = neuron.h.Random()
258
+
259
+ if rng_settings.mode == "Random123":
260
+ seed1, seed2, seed3 = 2997, 19216, self.seed if self.seed else 123
261
+ rng.Random123(seed1, seed2, seed3)
262
+ else:
263
+ raise ValueError("Shot noise stimulus requires Random123 RNG mode.")
264
+
265
+ variance = self.sigma ** 2
266
+ tvec, svec = gen_shotnoise_signal(
267
+ self.decay_time,
268
+ self.rise_time,
269
+ self.rate,
270
+ self.mean,
271
+ variance,
272
+ self.duration,
273
+ self.dt,
274
+ rng=rng
275
+ )
276
+
277
+ return np.array(tvec.to_python()), np.array(svec.to_python())
278
+
279
+
280
+ class StepNoiseProcess(Stimulus):
281
+ """Generates step noise: A step current with noise variations."""
282
+
283
+ def __init__(
284
+ self,
285
+ dt: float,
286
+ duration: float,
287
+ step_interval: float,
288
+ mean: float,
289
+ sigma: float,
290
+ seed: Optional[int] = None,
291
+ ):
292
+ super().__init__(dt)
293
+ self.duration = duration
294
+ self.step_interval = step_interval
295
+ self.mean = mean
296
+ self.sigma = sigma
297
+ self.seed = seed
298
+
299
+ # Generate step noise signal
300
+ self._time, self._current = self._generate_step_noise()
301
+
302
+ @property
303
+ def time(self) -> np.ndarray:
304
+ return self._time
305
+
306
+ @property
307
+ def current(self) -> np.ndarray:
308
+ return self._current
309
+
310
+ def _generate_step_noise(self):
311
+ """Generates the step noise time and current vectors using NEURON’s
312
+ random generator."""
313
+ from neuron import h
314
+ from bluecellulab.rngsettings import RNGSettings
315
+
316
+ # Get NEURON RNG settings
317
+ rng_settings = RNGSettings.get_instance()
318
+ rng = h.Random()
319
+
320
+ if rng_settings.mode == "Random123":
321
+ seed1, seed2, seed3 = 2997, 19216, self.seed if self.seed else 123
322
+ rng.Random123(seed1, seed2, seed3)
323
+ else:
324
+ raise ValueError("StepNoise stimulus requires Random123 RNG mode.")
325
+
326
+ num_steps = int(self.duration / self.step_interval)
327
+
328
+ # Generate noise using NEURON's normal distribution function
329
+ amplitudes = [self.mean + rng.normal(0, self.sigma) for _ in range(num_steps)]
330
+
331
+ # Construct stimulus
332
+ time_values = []
333
+ current_values = []
334
+ time = 0
335
+
336
+ for amp in amplitudes:
337
+ time_values.append(time)
338
+ current_values.append(amp)
339
+ time += self.step_interval
340
+
341
+ return np.array(time_values), np.array(current_values)
342
+
343
+
344
+ class Step(Stimulus):
345
+
346
+ def __init__(self):
347
+ raise NotImplementedError(
348
+ "This class cannot be instantiated directly. "
349
+ "Please use the class methods 'amplitude_based' "
350
+ "or 'threshold_based' to create objects."
351
+ )
352
+
353
+ @classmethod
354
+ def amplitude_based(
355
+ cls,
356
+ dt: float,
357
+ pre_delay: float,
358
+ duration: float,
359
+ post_delay: float,
360
+ amplitude: float,
361
+ ) -> CombinedStimulus:
362
+ """Create a Step stimulus from given time events and amplitude.
363
+
364
+ Args:
365
+ dt: The time step of the stimulus.
366
+ pre_delay: The delay before the start of the step.
367
+ duration: The duration of the step.
368
+ post_delay: The time to wait after the end of the step.
369
+ amplitude: The amplitude of the step.
370
+ """
371
+ return (
372
+ Empty(dt, duration=pre_delay)
373
+ + Flat(dt, duration=duration, amplitude=amplitude)
374
+ + Empty(dt, duration=post_delay)
375
+ )
376
+
377
+ @classmethod
378
+ def threshold_based(
379
+ cls,
380
+ dt: float,
381
+ pre_delay: float,
382
+ duration: float,
383
+ post_delay: float,
384
+ threshold_current: float,
385
+ threshold_percentage: float,
386
+ ) -> CombinedStimulus:
387
+ """Creates a Step stimulus with respect to the threshold current.
388
+
389
+ Args:
390
+
391
+ dt: The time step of the stimulus.
392
+ pre_delay: The delay before the start of the step.
393
+ duration: The duration of the step.
394
+ post_delay: The time to wait after the end of the step.
395
+ threshold_current: The threshold current of the Cell.
396
+ threshold_percentage: Percentage of desired threshold_current amplification.
397
+ """
398
+ amplitude = threshold_current * threshold_percentage / 100
399
+ res = cls.amplitude_based(
400
+ dt,
401
+ pre_delay=pre_delay,
402
+ duration=duration,
403
+ post_delay=post_delay,
404
+ amplitude=amplitude,
405
+ )
406
+ return res
407
+
408
+
409
+ class Ramp(Stimulus):
410
+
411
+ def __init__(self):
412
+ raise NotImplementedError(
413
+ "This class cannot be instantiated directly. "
414
+ "Please use the class methods 'amplitude_based' "
415
+ "or 'threshold_based' to create objects."
416
+ )
417
+
418
+ @classmethod
419
+ def amplitude_based(
420
+ cls,
421
+ dt: float,
422
+ pre_delay: float,
423
+ duration: float,
424
+ post_delay: float,
425
+ amplitude: float,
426
+ ) -> CombinedStimulus:
427
+ """Create a Ramp stimulus from given time events and amplitudes.
428
+
429
+ Args:
430
+ dt: The time step of the stimulus.
431
+ pre_delay: The delay before the start of the ramp.
432
+ duration: The duration of the ramp.
433
+ post_delay: The time to wait after the end of the ramp.
434
+ amplitude: The final amplitude of the ramp.
435
+ """
436
+ return (
437
+ Empty(dt, duration=pre_delay)
438
+ + Slope(
439
+ dt,
440
+ duration=duration,
441
+ amplitude_start=0.0,
442
+ amplitude_end=amplitude,
443
+ )
444
+ + Empty(dt, duration=post_delay)
445
+ )
446
+
447
+ @classmethod
448
+ def threshold_based(
449
+ cls,
450
+ dt: float,
451
+ pre_delay: float,
452
+ duration: float,
453
+ post_delay: float,
454
+ threshold_current: float,
455
+ threshold_percentage: float,
456
+ ) -> CombinedStimulus:
457
+ """Creates a Ramp stimulus with respect to the threshold current.
458
+
459
+ Args:
460
+
461
+ dt: The time step of the stimulus.
462
+ pre_delay: The delay before the start of the ramp.
463
+ duration: The duration of the ramp.
464
+ post_delay: The time to wait after the end of the ramp.
465
+ threshold_current: The threshold current of the Cell.
466
+ threshold_percentage: Percentage of desired threshold_current amplification.
467
+ """
468
+ amplitude = threshold_current * threshold_percentage / 100
469
+ res = cls.amplitude_based(
470
+ dt,
471
+ pre_delay=pre_delay,
472
+ duration=duration,
473
+ post_delay=post_delay,
474
+ amplitude=amplitude,
475
+ )
476
+ return res
477
+
478
+
479
+ class DelayedZap(Stimulus):
480
+
481
+ def __init__(self):
482
+ raise NotImplementedError(
483
+ "This class cannot be instantiated directly. "
484
+ "Please use the class methods 'amplitude_based' "
485
+ "or 'threshold_based' to create objects."
486
+ )
487
+
488
+ @classmethod
489
+ def amplitude_based(
490
+ cls,
491
+ dt: float,
492
+ pre_delay: float,
493
+ duration: float,
494
+ post_delay: float,
495
+ amplitude: float,
496
+ ) -> CombinedStimulus:
497
+ """Create a DelayedZap stimulus from given time events and amplitude.
498
+
499
+ Args:
500
+ dt: The time step of the stimulus.
501
+ pre_delay: The delay before the start of the step.
502
+ duration: The duration of the step.
503
+ post_delay: The time to wait after the end of the step.
504
+ amplitude: The amplitude of the step.
505
+ """
506
+ return (
507
+ Empty(dt, duration=pre_delay)
508
+ + Zap(dt, duration=duration, amplitude=amplitude)
509
+ + Empty(dt, duration=post_delay)
510
+ )
511
+
512
+ @classmethod
513
+ def threshold_based(
514
+ cls,
515
+ dt: float,
516
+ pre_delay: float,
517
+ duration: float,
518
+ post_delay: float,
519
+ threshold_current: float,
520
+ threshold_percentage: float,
521
+ ) -> CombinedStimulus:
522
+ """Creates a SineSpec stimulus with respect to the threshold current.
523
+
524
+ Args:
525
+
526
+ dt: The time step of the stimulus.
527
+ pre_delay: The delay before the start of the step.
528
+ duration: The duration of the step.
529
+ post_delay: The time to wait after the end of the step.
530
+ threshold_current: The threshold current of the Cell.
531
+ threshold_percentage: Percentage of desired threshold_current amplification.
532
+ """
533
+ amplitude = threshold_current * threshold_percentage / 100
534
+ res = cls.amplitude_based(
535
+ dt,
536
+ pre_delay=pre_delay,
537
+ duration=duration,
538
+ post_delay=post_delay,
539
+ amplitude=amplitude,
540
+ )
541
+ return res
542
+
543
+
544
+ class OrnsteinUhlenbeck(Stimulus):
545
+ """Factory-compatible Ornstein-Uhlenbeck noise stimulus."""
546
+
547
+ def __init__(self):
548
+ """Prevents direct instantiation of the class."""
549
+ raise NotImplementedError(
550
+ "This class cannot be instantiated directly. "
551
+ "Please use 'amplitude_based' or 'threshold_based' methods."
552
+ )
553
+
554
+ @classmethod
555
+ def amplitude_based(
556
+ cls,
557
+ dt: float,
558
+ pre_delay: float,
559
+ duration: float,
560
+ post_delay: float,
561
+ tau: float,
562
+ sigma: float,
563
+ mean: float,
564
+ seed: Optional[int] = None,
565
+ ) -> CombinedStimulus:
566
+ """Create an Ornstein-Uhlenbeck stimulus from given time events and
567
+ amplitude."""
568
+ return (
569
+ Empty(dt, duration=pre_delay)
570
+ + OUProcess(dt, duration, tau, sigma, mean, seed)
571
+ + Empty(dt, duration=post_delay)
572
+ )
573
+
574
+ @classmethod
575
+ def threshold_based(
576
+ cls,
577
+ dt: float,
578
+ pre_delay: float,
579
+ duration: float,
580
+ post_delay: float,
581
+ mean_percent: float,
582
+ sigma_percent: float,
583
+ threshold_current: float,
584
+ tau: float,
585
+ seed: Optional[int] = None,
586
+ ) -> CombinedStimulus:
587
+ """Creates an Ornstein-Uhlenbeck stimulus with respect to the threshold
588
+ current."""
589
+ sigma = sigma_percent / 100 * threshold_current
590
+ if sigma <= 0:
591
+ raise BluecellulabError(f"Calculated standard deviation (sigma) must be positive, but got {sigma}. Ensure sigma_percent and threshold_current are both positive.")
592
+
593
+ mean = mean_percent / 100 * threshold_current
594
+ if mean < 0 and abs(mean) > 2 * sigma:
595
+ logger.warning("Relative Ornstein-Uhlenbeck signal is mostly zero.")
596
+
597
+ return cls.amplitude_based(
598
+ dt,
599
+ pre_delay=pre_delay,
600
+ duration=duration,
601
+ post_delay=post_delay,
602
+ tau=tau,
603
+ sigma=sigma,
604
+ mean=mean,
605
+ seed=seed,
606
+ )
607
+
608
+
609
+ class ShotNoise(Stimulus):
610
+ """Factory-compatible Shot Noise Stimulus."""
611
+
612
+ def __init__(self):
613
+ """Prevents direct instantiation."""
614
+ raise NotImplementedError(
615
+ "This class cannot be instantiated directly. "
616
+ "Please use 'amplitude_based' or 'threshold_based' methods."
617
+ )
618
+
619
+ @classmethod
620
+ def amplitude_based(
621
+ cls,
622
+ dt: float,
623
+ pre_delay: float,
624
+ duration: float,
625
+ post_delay: float,
626
+ rate: float,
627
+ mean: float,
628
+ sigma: float,
629
+ rise_time: float,
630
+ decay_time: float,
631
+ seed: Optional[int] = None,
632
+ ) -> CombinedStimulus:
633
+ """Creates a shot noise stimulus with a specified amplitude.
634
+
635
+ Args:
636
+ dt: Time step of the stimulus.
637
+ pre_delay: Delay before the noise starts.
638
+ duration: Duration of the noise signal.
639
+ post_delay: Delay after the noise ends.
640
+ rate: Frequency of synaptic-like events.
641
+ mean: Mean amplitude of the events.
642
+ sigma: Standard deviation of event amplitudes.
643
+ rise_time: Time constant for the event's rise phase.
644
+ decay_time: Time constant for the event's decay phase.
645
+ seed: Random seed for reproducibility.
646
+ """
647
+ return (
648
+ Empty(dt, duration=pre_delay)
649
+ + ShotNoiseProcess(dt, duration, rate, mean, sigma, rise_time, decay_time, seed)
650
+ + Empty(dt, duration=post_delay)
651
+ )
652
+
653
+ @classmethod
654
+ def threshold_based(
655
+ cls,
656
+ dt: float,
657
+ pre_delay: float,
658
+ duration: float,
659
+ post_delay: float,
660
+ rise_time: float,
661
+ decay_time: float,
662
+ mean_percent: float,
663
+ sigma_percent: float,
664
+ threshold_current: float,
665
+ relative_skew: float = 0.5,
666
+ seed: Optional[int] = None,
667
+ ) -> CombinedStimulus:
668
+ """Creates a shot noise stimulus based on a neuron's threshold current.
669
+
670
+ Args:
671
+ dt: Time step of the stimulus.
672
+ pre_delay: Delay before the noise starts.
673
+ duration: Duration of the noise signal.
674
+ post_delay: Delay after the noise ends.
675
+ rise_time: Rise time constant of events.
676
+ decay_time: Decay time constant of events.
677
+ mean_percent: Mean value as a percentage of the threshold current.
678
+ sigma_percent: Standard deviation as a percentage of the threshold current.
679
+ threshold_current: Baseline threshold current.
680
+ relative_skew: Skew factor affecting noise distribution.
681
+ seed: Random seed for reproducibility.
682
+ """
683
+ _mean = mean_percent / 100 * threshold_current
684
+ sd = sigma_percent / 100 * threshold_current
685
+
686
+ rate, mean, sigma = get_relative_shotnoise_params(
687
+ _mean, sd, decay_time, rise_time, relative_skew
688
+ )
689
+
690
+ return cls.amplitude_based(
691
+ dt,
692
+ pre_delay=pre_delay,
693
+ duration=duration,
694
+ post_delay=post_delay,
695
+ rate=rate,
696
+ mean=mean,
697
+ sigma=sigma,
698
+ rise_time=rise_time,
699
+ decay_time=decay_time,
700
+ seed=seed,
701
+ )
702
+
703
+
704
+ class StepNoise(Stimulus):
705
+ """Factory-compatible Step Noise Stimulus."""
706
+
707
+ def __init__(self):
708
+ """Prevents direct instantiation."""
709
+ raise NotImplementedError(
710
+ "This class cannot be instantiated directly. "
711
+ "Please use 'amplitude_based' or 'threshold_based' methods."
712
+ )
713
+
714
+ @classmethod
715
+ def amplitude_based(
716
+ cls,
717
+ dt: float,
718
+ pre_delay: float,
719
+ duration: float,
720
+ post_delay: float,
721
+ step_interval: float,
722
+ mean: float,
723
+ sigma: float,
724
+ seed: Optional[int] = None,
725
+ ) -> CombinedStimulus:
726
+ """Creates a step noise stimulus with a specified amplitude.
727
+
728
+ Args:
729
+ dt: Time step of the stimulus.
730
+ pre_delay: Delay before the step noise starts.
731
+ duration: Duration of the noise signal.
732
+ post_delay: Delay after the step noise ends.
733
+ step_interval: Interval at which noise amplitude changes.
734
+ mean: Mean amplitude of step noise.
735
+ sigma: Standard deviation of step noise.
736
+ seed: Random seed for reproducibility.
737
+ """
738
+ return (
739
+ Empty(dt, duration=pre_delay)
740
+ + StepNoiseProcess(dt, duration, step_interval, mean, sigma, seed)
741
+ + Empty(dt, duration=post_delay)
742
+ )
743
+
744
+ @classmethod
745
+ def threshold_based(
746
+ cls,
747
+ dt: float,
748
+ pre_delay: float,
749
+ duration: float,
750
+ post_delay: float,
751
+ step_interval: float,
752
+ mean_percent: float,
753
+ sigma_percent: float,
754
+ threshold_current: float,
755
+ seed: Optional[int] = None,
756
+ ) -> CombinedStimulus:
757
+ """Creates a step noise stimulus relative to the threshold current.
758
+
759
+ Args:
760
+ dt: Time step of the stimulus.
761
+ pre_delay: Delay before the step noise starts.
762
+ duration: Duration of the noise signal.
763
+ post_delay: Delay after the step noise ends.
764
+ step_interval: Interval at which noise amplitude changes.
765
+ mean_percent: Mean current as a percentage of threshold current.
766
+ sigma_percent: Standard deviation as a percentage of threshold current.
767
+ threshold_current: Baseline threshold current.
768
+ seed: Random seed for reproducibility.
769
+ """
770
+ mean = mean_percent / 100 * threshold_current
771
+ sigma = sigma_percent / 100 * threshold_current
772
+
773
+ return cls.amplitude_based(
774
+ dt,
775
+ pre_delay=pre_delay,
776
+ duration=duration,
777
+ post_delay=post_delay,
778
+ step_interval=step_interval,
779
+ mean=mean,
780
+ sigma=sigma,
781
+ seed=seed,
782
+ )