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.
- bluecellulab/analysis/inject_sequence.py +4 -4
- bluecellulab/cell/injector.py +1 -1
- bluecellulab/stimulus/factory.py +289 -361
- bluecellulab/stimulus/stimulus.py +782 -0
- {bluecellulab-2.6.42.dist-info → bluecellulab-2.6.43.dist-info}/METADATA +1 -1
- {bluecellulab-2.6.42.dist-info → bluecellulab-2.6.43.dist-info}/RECORD +10 -9
- {bluecellulab-2.6.42.dist-info → bluecellulab-2.6.43.dist-info}/WHEEL +1 -1
- {bluecellulab-2.6.42.dist-info → bluecellulab-2.6.43.dist-info}/AUTHORS.txt +0 -0
- {bluecellulab-2.6.42.dist-info → bluecellulab-2.6.43.dist-info}/LICENSE +0 -0
- {bluecellulab-2.6.42.dist-info → bluecellulab-2.6.43.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|