bluecellulab 2.6.42__py3-none-any.whl → 2.6.43__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.

@@ -1,361 +1,25 @@
1
- from __future__ import annotations
2
- from abc import ABC, abstractmethod
3
- from typing import Optional
4
- import logging
5
- import matplotlib.pyplot as plt
6
- import numpy as np
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- class Stimulus(ABC):
12
- def __init__(self, dt: float) -> None:
13
- self.dt = dt
14
-
15
- @property
16
- @abstractmethod
17
- def time(self) -> np.ndarray:
18
- """Time values of the stimulus."""
19
- ...
20
-
21
- @property
22
- @abstractmethod
23
- def current(self) -> np.ndarray:
24
- """Current values of the stimulus."""
25
- ...
26
-
27
- def __len__(self) -> int:
28
- return len(self.time)
29
-
30
- @property
31
- def stimulus_time(self) -> float:
32
- return len(self) * self.dt
33
-
34
- def __repr__(self) -> str:
35
- return f"{self.__class__.__name__}(dt={self.dt})"
36
-
37
- def plot(self, ax=None, **kwargs):
38
- if ax is None:
39
- ax = plt.gca()
40
- ax.plot(self.time, self.current, **kwargs)
41
- ax.set_xlabel("Time (ms)")
42
- ax.set_ylabel("Current (nA)")
43
- ax.set_title(self.__class__.__name__)
44
- return ax
45
-
46
- def __add__(self, other: Stimulus) -> CombinedStimulus:
47
- """Override + operator to concatenate Stimulus objects."""
48
- if self.dt != other.dt:
49
- raise ValueError(
50
- "Stimulus objects must have the same dt to be concatenated"
51
- )
52
- if len(self.time) == 0:
53
- return CombinedStimulus(other.dt, other.time, other.current)
54
- elif len(other.time) == 0:
55
- return CombinedStimulus(self.dt, self.time, self.current)
56
- else:
57
- # shift other time
58
- other_time = other.time + self.time[-1] + self.dt
59
- combined_time = np.concatenate([self.time, other_time])
60
- # Concatenate the current arrays
61
- combined_current = np.concatenate([self.current, other.current])
62
- return CombinedStimulus(self.dt, combined_time, combined_current)
63
-
64
- def __eq__(self, other: object) -> bool:
65
- if not isinstance(other, Stimulus):
66
- return NotImplemented
67
- else:
68
- return (
69
- np.allclose(self.time, other.time)
70
- and np.allclose(self.current, other.current)
71
- and self.dt == other.dt
72
- )
73
-
74
-
75
- class CombinedStimulus(Stimulus):
76
- """Represents the Stimulus created by combining multiple stimuli."""
77
-
78
- def __init__(self, dt: float, time: np.ndarray, current: np.ndarray) -> None:
79
- super().__init__(dt)
80
- self._time = time
81
- self._current = current
82
-
83
- @property
84
- def time(self) -> np.ndarray:
85
- return self._time
86
-
87
- @property
88
- def current(self) -> np.ndarray:
89
- return self._current
90
-
91
-
92
- class Empty(Stimulus):
93
- """Represents empty stimulus (all zeros) that has no impact on the cell.
94
-
95
- This is required by some Stimuli that expect the cell to rest.
96
- """
97
-
98
- def __init__(self, dt: float, duration: float) -> None:
99
- super().__init__(dt)
100
- self.duration = duration
101
-
102
- @property
103
- def time(self) -> np.ndarray:
104
- return np.arange(0.0, self.duration, self.dt)
105
-
106
- @property
107
- def current(self) -> np.ndarray:
108
- return np.zeros_like(self.time)
109
-
110
-
111
- class Flat(Stimulus):
112
- def __init__(self, dt: float, duration: float, amplitude: float) -> None:
113
- super().__init__(dt)
114
- self.duration = duration
115
- self.amplitude = amplitude
116
-
117
- @property
118
- def time(self) -> np.ndarray:
119
- return np.arange(0.0, self.duration, self.dt)
120
-
121
- @property
122
- def current(self) -> np.ndarray:
123
- return np.full_like(self.time, self.amplitude)
124
-
125
-
126
- class Slope(Stimulus):
127
- def __init__(
128
- self, dt: float, duration: float, amplitude_start: float, amplitude_end: float
129
- ) -> None:
130
- super().__init__(dt)
131
- self.duration = duration
132
- self.amplitude_start = amplitude_start
133
- self.amplitude_end = amplitude_end
134
-
135
- @property
136
- def time(self) -> np.ndarray:
137
- return np.arange(0.0, self.duration, self.dt)
138
-
139
- @property
140
- def current(self) -> np.ndarray:
141
- return np.linspace(self.amplitude_start, self.amplitude_end, len(self.time))
142
-
143
-
144
- class Zap(Stimulus):
145
- def __init__(self, dt: float, duration: float, amplitude: float) -> None:
146
- super().__init__(dt)
147
- self.duration = duration
148
- self.amplitude = amplitude
149
-
150
- @property
151
- def time(self) -> np.ndarray:
152
- return np.arange(0.0, self.duration, self.dt)
153
-
154
- @property
155
- def current(self) -> np.ndarray:
156
- return self.amplitude * np.sin(
157
- 2.0 * np.pi * (1.0 + (1.0 / (5.15 - (self.time - 0.1)))) * (self.time - 0.1)
158
- )
159
-
160
-
161
- class Step(Stimulus):
162
-
163
- def __init__(self):
164
- raise NotImplementedError(
165
- "This class cannot be instantiated directly. "
166
- "Please use the class methods 'amplitude_based' "
167
- "or 'threshold_based' to create objects."
168
- )
169
-
170
- @classmethod
171
- def amplitude_based(
172
- cls,
173
- dt: float,
174
- pre_delay: float,
175
- duration: float,
176
- post_delay: float,
177
- amplitude: float,
178
- ) -> CombinedStimulus:
179
- """Create a Step stimulus from given time events and amplitude.
180
-
181
- Args:
182
- dt: The time step of the stimulus.
183
- pre_delay: The delay before the start of the step.
184
- duration: The duration of the step.
185
- post_delay: The time to wait after the end of the step.
186
- amplitude: The amplitude of the step.
187
- """
188
- return (
189
- Empty(dt, duration=pre_delay)
190
- + Flat(dt, duration=duration, amplitude=amplitude)
191
- + Empty(dt, duration=post_delay)
192
- )
193
-
194
- @classmethod
195
- def threshold_based(
196
- cls,
197
- dt: float,
198
- pre_delay: float,
199
- duration: float,
200
- post_delay: float,
201
- threshold_current: float,
202
- threshold_percentage: float,
203
- ) -> CombinedStimulus:
204
- """Creates a Step stimulus with respect to the threshold current.
205
-
206
- Args:
207
-
208
- dt: The time step of the stimulus.
209
- pre_delay: The delay before the start of the step.
210
- duration: The duration of the step.
211
- post_delay: The time to wait after the end of the step.
212
- threshold_current: The threshold current of the Cell.
213
- threshold_percentage: Percentage of desired threshold_current amplification.
214
- """
215
- amplitude = threshold_current * threshold_percentage / 100
216
- res = cls.amplitude_based(
217
- dt,
218
- pre_delay=pre_delay,
219
- duration=duration,
220
- post_delay=post_delay,
221
- amplitude=amplitude,
222
- )
223
- return res
1
+ # Copyright 2023-2024 Blue Brain Project / EPFL
2
+ # Copyright 2025 Open Brain Institute
224
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
225
7
 
226
- class Ramp(Stimulus):
8
+ # http://www.apache.org/licenses/LICENSE-2.0
227
9
 
228
- def __init__(self):
229
- raise NotImplementedError(
230
- "This class cannot be instantiated directly. "
231
- "Please use the class methods 'amplitude_based' "
232
- "or 'threshold_based' to create objects."
233
- )
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.
234
15
 
235
- @classmethod
236
- def amplitude_based(
237
- cls,
238
- dt: float,
239
- pre_delay: float,
240
- duration: float,
241
- post_delay: float,
242
- amplitude: float,
243
- ) -> CombinedStimulus:
244
- """Create a Ramp stimulus from given time events and amplitudes.
245
-
246
- Args:
247
- dt: The time step of the stimulus.
248
- pre_delay: The delay before the start of the ramp.
249
- duration: The duration of the ramp.
250
- post_delay: The time to wait after the end of the ramp.
251
- amplitude: The final amplitude of the ramp.
252
- """
253
- return (
254
- Empty(dt, duration=pre_delay)
255
- + Slope(
256
- dt,
257
- duration=duration,
258
- amplitude_start=0.0,
259
- amplitude_end=amplitude,
260
- )
261
- + Empty(dt, duration=post_delay)
262
- )
263
-
264
- @classmethod
265
- def threshold_based(
266
- cls,
267
- dt: float,
268
- pre_delay: float,
269
- duration: float,
270
- post_delay: float,
271
- threshold_current: float,
272
- threshold_percentage: float,
273
- ) -> CombinedStimulus:
274
- """Creates a Ramp stimulus with respect to the threshold current.
275
-
276
- Args:
277
-
278
- dt: The time step of the stimulus.
279
- pre_delay: The delay before the start of the ramp.
280
- duration: The duration of the ramp.
281
- post_delay: The time to wait after the end of the ramp.
282
- threshold_current: The threshold current of the Cell.
283
- threshold_percentage: Percentage of desired threshold_current amplification.
284
- """
285
- amplitude = threshold_current * threshold_percentage / 100
286
- res = cls.amplitude_based(
287
- dt,
288
- pre_delay=pre_delay,
289
- duration=duration,
290
- post_delay=post_delay,
291
- amplitude=amplitude,
292
- )
293
- return res
294
-
295
-
296
- class DelayedZap(Stimulus):
297
-
298
- def __init__(self):
299
- raise NotImplementedError(
300
- "This class cannot be instantiated directly. "
301
- "Please use the class methods 'amplitude_based' "
302
- "or 'threshold_based' to create objects."
303
- )
304
-
305
- @classmethod
306
- def amplitude_based(
307
- cls,
308
- dt: float,
309
- pre_delay: float,
310
- duration: float,
311
- post_delay: float,
312
- amplitude: float,
313
- ) -> CombinedStimulus:
314
- """Create a DelayedZap stimulus from given time events and amplitude.
315
-
316
- Args:
317
- dt: The time step of the stimulus.
318
- pre_delay: The delay before the start of the step.
319
- duration: The duration of the step.
320
- post_delay: The time to wait after the end of the step.
321
- amplitude: The amplitude of the step.
322
- """
323
- return (
324
- Empty(dt, duration=pre_delay)
325
- + Zap(dt, duration=duration, amplitude=amplitude)
326
- + Empty(dt, duration=post_delay)
327
- )
328
-
329
- @classmethod
330
- def threshold_based(
331
- cls,
332
- dt: float,
333
- pre_delay: float,
334
- duration: float,
335
- post_delay: float,
336
- threshold_current: float,
337
- threshold_percentage: float,
338
- ) -> CombinedStimulus:
339
- """Creates a SineSpec stimulus with respect to the threshold current.
340
-
341
- Args:
16
+ from __future__ import annotations
17
+ from typing import Optional
18
+ import logging
19
+ from bluecellulab.stimulus.stimulus import DelayedZap, Empty, Ramp, Slope, Step, StepNoise, Stimulus, OrnsteinUhlenbeck, ShotNoise
20
+ from bluecellulab.stimulus.circuit_stimulus_definitions import Stimulus as CircuitStimulus
342
21
 
343
- dt: The time step of the stimulus.
344
- pre_delay: The delay before the start of the step.
345
- duration: The duration of the step.
346
- post_delay: The time to wait after the end of the step.
347
- threshold_current: The threshold current of the Cell.
348
- threshold_percentage: Percentage of desired threshold_current amplification.
349
- """
350
- amplitude = threshold_current * threshold_percentage / 100
351
- res = cls.amplitude_based(
352
- dt,
353
- pre_delay=pre_delay,
354
- duration=duration,
355
- post_delay=post_delay,
356
- amplitude=amplitude,
357
- )
358
- return res
22
+ logger = logging.getLogger(__name__)
359
23
 
360
24
 
361
25
  class StimulusFactory:
@@ -374,15 +38,39 @@ class StimulusFactory:
374
38
  )
375
39
 
376
40
  def ramp(
377
- self, pre_delay: float, duration: float, post_delay: float, amplitude: float
41
+ self,
42
+ pre_delay: float,
43
+ duration: float,
44
+ post_delay: float,
45
+ amplitude: Optional[float] = None,
46
+ threshold_current: Optional[float] = None,
47
+ threshold_percentage: Optional[float] = 220.0,
378
48
  ) -> Stimulus:
379
- return Ramp.amplitude_based(
380
- self.dt,
381
- pre_delay=pre_delay,
382
- duration=duration,
383
- post_delay=post_delay,
384
- amplitude=amplitude,
385
- )
49
+ if amplitude is not None:
50
+ if threshold_current is not None and threshold_current != 0 and threshold_percentage is not None:
51
+ logger.info(
52
+ "amplitude, threshold_current and threshold_percentage are all set in ramp."
53
+ " Will only keep amplitude value."
54
+ )
55
+ return Ramp.amplitude_based(
56
+ self.dt,
57
+ pre_delay=pre_delay,
58
+ duration=duration,
59
+ post_delay=post_delay,
60
+ amplitude=amplitude,
61
+ )
62
+
63
+ if threshold_current is not None and threshold_current != 0 and threshold_percentage is not None:
64
+ return Ramp.threshold_based(
65
+ self.dt,
66
+ pre_delay=pre_delay,
67
+ duration=duration,
68
+ post_delay=post_delay,
69
+ threshold_current=threshold_current,
70
+ threshold_percentage=threshold_percentage,
71
+ )
72
+
73
+ raise TypeError("You have to give either threshold_current or amplitude")
386
74
 
387
75
  def ap_waveform(
388
76
  self,
@@ -687,3 +375,243 @@ class StimulusFactory:
687
375
  )
688
376
 
689
377
  raise TypeError("You have to give either threshold_current or amplitude")
378
+
379
+ def ornstein_uhlenbeck(
380
+ self,
381
+ pre_delay: float,
382
+ post_delay: float,
383
+ duration: float,
384
+ tau: float,
385
+ sigma: Optional[float] = None,
386
+ mean: Optional[float] = None,
387
+ mean_percent: Optional[float] = None,
388
+ sigma_percent: Optional[float] = None,
389
+ threshold_current: Optional[float] = None,
390
+ seed: Optional[int] = None
391
+ ) -> Stimulus:
392
+ """Creates an Ornstein-Uhlenbeck process stimulus (factory-compatible).
393
+
394
+ Args:
395
+ pre_delay: Delay before the noise starts (ms).
396
+ post_delay: Delay after the noise ends (ms).
397
+ duration: Duration of the stimulus (ms).
398
+ tau: Time constant of the noise process.
399
+ sigma: Standard deviation of the noise (used when mean is provided).
400
+ mean: Absolute mean current value (used if provided).
401
+ mean_percent: Mean current as a percentage of threshold current (used if mean is None).
402
+ sigma_percent: Standard deviation as a percentage of threshold current (used if sigma is None).
403
+ threshold_current: Reference threshold current for percentage-based calculation.
404
+ seed: Optional random seed for reproducibility.
405
+
406
+ Returns:
407
+ A `Stimulus` object (OrnsteinUhlenbeck) that can be plotted and injected.
408
+
409
+ Notes:
410
+ - If `mean` is provided, `mean_percent` is ignored.
411
+ - If `threshold_current` is not provided, threshold-based parameters cannot be used.
412
+ """
413
+ is_amplitude_based = mean is not None and sigma is not None
414
+ is_threshold_based = (
415
+ threshold_current is not None
416
+ and threshold_current != 0
417
+ and mean_percent is not None
418
+ and sigma_percent is not None
419
+ )
420
+
421
+ if is_amplitude_based:
422
+ if is_threshold_based:
423
+ logger.info(
424
+ "mean, threshold_current, and mean_percent are all set in Ornstein-Uhlenbeck."
425
+ " Using mean and ignoring threshold-based parameters."
426
+ )
427
+
428
+ return OrnsteinUhlenbeck.amplitude_based(
429
+ dt=self.dt,
430
+ pre_delay=pre_delay,
431
+ post_delay=post_delay,
432
+ duration=duration,
433
+ tau=tau,
434
+ sigma=sigma, # type: ignore[arg-type]
435
+ mean=mean, # type: ignore[arg-type]
436
+ seed=seed
437
+ )
438
+
439
+ if is_threshold_based:
440
+ return OrnsteinUhlenbeck.threshold_based(
441
+ dt=self.dt,
442
+ pre_delay=pre_delay,
443
+ post_delay=post_delay,
444
+ duration=duration,
445
+ mean_percent=mean_percent, # type: ignore[arg-type]
446
+ sigma_percent=sigma_percent, # type: ignore[arg-type]
447
+ threshold_current=threshold_current, # type: ignore[arg-type]
448
+ tau=tau,
449
+ seed=seed
450
+ )
451
+
452
+ raise TypeError("You have to give either `mean` and `sigma` or `threshold_current` and `mean_percent` and `sigma_percent`.")
453
+
454
+ def shot_noise(
455
+ self,
456
+ pre_delay: float,
457
+ post_delay: float,
458
+ duration: float,
459
+ rate: float,
460
+ rise_time: float,
461
+ decay_time: float,
462
+ mean: Optional[float] = None,
463
+ sigma: Optional[float] = None,
464
+ mean_percent: Optional[float] = None,
465
+ sigma_percent: Optional[float] = None,
466
+ relative_skew: float = 0.5,
467
+ threshold_current: Optional[float] = None,
468
+ seed: Optional[int] = None
469
+ ) -> Stimulus:
470
+ """Creates a ShotNoise instance, either with an absolute amplitude or
471
+ relative to a threshold current.
472
+
473
+ Args:
474
+ pre_delay: Delay before the noise starts (ms).
475
+ post_delay: Delay after the noise ends (ms).
476
+ duration: Duration of the stimulus (ms).
477
+ rate: Mean rate of synaptic events (Hz).
478
+ mean: Mean amplitude of events (nA), used if provided.
479
+ sigma: Standard deviation of event amplitudes.
480
+ rise_time: Rise time of synaptic events (ms).
481
+ decay_time: Decay time of synaptic events (ms).
482
+ mean_percent: Mean current as a percentage of threshold current (used if mean is None).
483
+ sigma_percent: Standard deviation as a percentage of threshold current (used if sigma is None).
484
+ relative_skew: Skew factor for the shot noise process (default: 0.5).
485
+ threshold_current: Reference threshold current for percentage-based calculation.
486
+ seed: Optional random seed for reproducibility.
487
+
488
+ Returns:
489
+ A `Stimulus` object that can be plotted and injected.
490
+
491
+ Notes:
492
+ - If `mean` is provided, `mean_percent` is ignored.
493
+ - If `threshold_current` is not provided, threshold-based parameters cannot be used.
494
+ """
495
+ is_amplitude_based = mean is not None and sigma is not None
496
+ is_threshold_based = (
497
+ threshold_current is not None
498
+ and threshold_current != 0
499
+ and mean_percent is not None
500
+ and sigma_percent is not None
501
+ )
502
+
503
+ if is_amplitude_based:
504
+ if is_threshold_based:
505
+ logger.info(
506
+ "mean, threshold_current, and mean_percent are all set in ShotNoise."
507
+ " Using mean and ignoring threshold-based parameters."
508
+ )
509
+
510
+ return ShotNoise.amplitude_based(
511
+ dt=self.dt,
512
+ pre_delay=pre_delay,
513
+ post_delay=post_delay,
514
+ duration=duration,
515
+ rate=rate,
516
+ mean=mean, # type: ignore[arg-type]
517
+ sigma=sigma, # type: ignore[arg-type]
518
+ rise_time=rise_time,
519
+ decay_time=decay_time,
520
+ seed=seed
521
+ )
522
+
523
+ if is_threshold_based:
524
+ return ShotNoise.threshold_based(
525
+ dt=self.dt,
526
+ pre_delay=pre_delay,
527
+ post_delay=post_delay,
528
+ duration=duration,
529
+ rise_time=rise_time,
530
+ decay_time=decay_time,
531
+ mean_percent=mean_percent, # type: ignore[arg-type]
532
+ sigma_percent=sigma_percent, # type: ignore[arg-type]
533
+ threshold_current=threshold_current, # type: ignore[arg-type]
534
+ relative_skew=relative_skew,
535
+ seed=seed
536
+ )
537
+
538
+ raise TypeError("You must provide either `mean` and `sigma`, or `threshold_current` and `mean_percent` and `sigma_percent` with percentage values.")
539
+
540
+ def step_noise(
541
+ self,
542
+ pre_delay: float,
543
+ post_delay: float,
544
+ duration: float,
545
+ step_interval: float,
546
+ mean: Optional[float] = None,
547
+ sigma: Optional[float] = None,
548
+ mean_percent: Optional[float] = None,
549
+ sigma_percent: Optional[float] = None,
550
+ threshold_current: Optional[float] = None,
551
+ seed: Optional[int] = None,
552
+ ) -> Stimulus:
553
+ """Creates a StepNoise instance, either with an absolute amplitude or
554
+ relative to a threshold current.
555
+
556
+ Args:
557
+ pre_delay: Delay before the step noise starts (ms).
558
+ post_delay: Delay after the step noise ends (ms).
559
+ duration: Duration of the stimulus (ms).
560
+ step_interval: Interval at which noise amplitude changes.
561
+ mean: Mean amplitude of step noise (nA), used if provided.
562
+ sigma: Standard deviation of step noise.
563
+ mean_percent: Mean current as a percentage of threshold current (used if mean is None).
564
+ sigma_percent: Standard deviation as a percentage of threshold current (used if sigma is None).
565
+ threshold_current: Reference threshold current for percentage-based calculation.
566
+ seed: Optional random seed for reproducibility.
567
+
568
+ Returns:
569
+ A `Stimulus` object that can be plotted and injected.
570
+
571
+ Notes:
572
+ - If `mean` is provided, `mean_percent` is ignored.
573
+ - If `threshold_current` is not provided, threshold-based parameters cannot be used.
574
+ """
575
+ is_amplitude_based = mean is not None and sigma is not None
576
+ is_threshold_based = (
577
+ threshold_current is not None
578
+ and threshold_current != 0
579
+ and mean_percent is not None
580
+ and sigma_percent is not None
581
+ )
582
+
583
+ if is_amplitude_based:
584
+ if is_threshold_based:
585
+ logger.info(
586
+ "mean, threshold_current, and mean_percent are all set in StepNoise."
587
+ " Using mean and ignoring threshold-based parameters."
588
+ )
589
+ return StepNoise.amplitude_based(
590
+ dt=self.dt,
591
+ pre_delay=pre_delay,
592
+ post_delay=post_delay,
593
+ duration=duration,
594
+ step_interval=step_interval,
595
+ mean=mean, # type: ignore[arg-type]
596
+ sigma=sigma, # type: ignore[arg-type]
597
+ seed=seed,
598
+ )
599
+
600
+ if is_threshold_based:
601
+ return StepNoise.threshold_based(
602
+ dt=self.dt,
603
+ pre_delay=pre_delay,
604
+ post_delay=post_delay,
605
+ duration=duration,
606
+ step_interval=step_interval,
607
+ mean_percent=mean_percent, # type: ignore[arg-type]
608
+ sigma_percent=sigma_percent, # type: ignore[arg-type]
609
+ threshold_current=threshold_current, # type: ignore[arg-type]
610
+ seed=seed,
611
+ )
612
+
613
+ raise TypeError("You must provide either `mean` and `sigma`, or `threshold_current` and `mean_percent` and `sigma_percent` with percentage values.")
614
+
615
+ def from_sonata(cls, circuit_stimulus: CircuitStimulus):
616
+ """Convert a SONATA stimulus into a factory-based stimulus."""
617
+ raise ValueError(f"Unsupported circuit stimulus type: {type(circuit_stimulus)}")