scalcs 1.1.0__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.
- scalcs/cjumps.py +835 -0
- scalcs/firstlatency.py +854 -0
- scalcs/gui/SCALCS.ICO +0 -0
- scalcs/gui/__init__.py +4 -0
- scalcs/gui/burstmenu.py +181 -0
- scalcs/gui/dca2.gif +0 -0
- scalcs/gui/helpmenu.py +77 -0
- scalcs/gui/jumpmenu.py +415 -0
- scalcs/gui/mechmenu.py +635 -0
- scalcs/gui/myqtcommon.py +307 -0
- scalcs/gui/plotwindow.py +94 -0
- scalcs/gui/savemenu.py +54 -0
- scalcs/gui/scalcsmenu.py +314 -0
- scalcs/mechanism.py +949 -0
- scalcs/pdfs.py +181 -0
- scalcs/popen.py +245 -0
- scalcs/qmatlib.py +886 -0
- scalcs/samples/Flip_a1GlyRWT_Burz2004.yaml +989 -0
- scalcs/samples/__init__.py +0 -0
- scalcs/samples/samples.py +492 -0
- scalcs/scalcsio.py +1180 -0
- scalcs/scalcslib.py +1638 -0
- scalcs/scburst.py +700 -0
- scalcs/sccurves.py +597 -0
- scalcs/scplotlib.py +34 -0
- scalcs/scsim.py +470 -0
- scalcs/version.py +3 -0
- scalcs-1.1.0.dist-info/METADATA +116 -0
- scalcs-1.1.0.dist-info/RECORD +32 -0
- scalcs-1.1.0.dist-info/WHEEL +5 -0
- scalcs-1.1.0.dist-info/licenses/LICENSE.MD +339 -0
- scalcs-1.1.0.dist-info/top_level.txt +1 -0
scalcs/cjumps.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"""Macroscopic responses to concentration jumps.
|
|
2
|
+
|
|
3
|
+
This module calculates the time course of open probability and state
|
|
4
|
+
occupancies when the agonist (or any effector) concentration is changed as
|
|
5
|
+
a function of time. The calculation uses the Q-matrix formalism of
|
|
6
|
+
Colquhoun & Hawkes (1977, 1981) and is the basis for simulating
|
|
7
|
+
concentration-clamp or fast-perfusion experiments.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
Pulse dataclasses (one per concentration profile shape)
|
|
12
|
+
ErfPulse Realistic jump: error-function rise and fall. This is the
|
|
13
|
+
profile used in fast-perfusion ('rcj') experiments.
|
|
14
|
+
SquarePulse Ideal square step.
|
|
15
|
+
InstExpPulse Instantaneous rise, exponential decay.
|
|
16
|
+
PairedSquarePulse Two square pulses separated by a gap.
|
|
17
|
+
|
|
18
|
+
Result dataclasses
|
|
19
|
+
JumpResult Time-course output of solve(). Supports 4-tuple unpacking
|
|
20
|
+
``t, c, Popen, P = result`` for backward compatibility.
|
|
21
|
+
RelaxationResult Analytical on/off time constants and amplitudes returned by
|
|
22
|
+
relaxation_taus().
|
|
23
|
+
|
|
24
|
+
Functions
|
|
25
|
+
solve(mec, pulse, reclen, step, method='ode')
|
|
26
|
+
Compute the macroscopic open-probability time course.
|
|
27
|
+
|
|
28
|
+
relaxation_taus(mec, pulse)
|
|
29
|
+
Analytical on/off relaxation time constants for a SquarePulse.
|
|
30
|
+
|
|
31
|
+
jump_summary(mec, pulse, gamma=30e-12, Vm=-80e-3)
|
|
32
|
+
All analytical jump properties as a plain dict — no formatting.
|
|
33
|
+
|
|
34
|
+
printout(mec, pulse, gamma=30e-12, Vm=-80e-3)
|
|
35
|
+
Human-readable report built from jump_summary().
|
|
36
|
+
|
|
37
|
+
Private helpers (prefixed ``_``)
|
|
38
|
+
_dPdt, _P_t, _coefficient_calc, _solve_ode, _solve_matrix
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
``relaxation_taus``, ``jump_summary`` and ``printout`` require a
|
|
43
|
+
``SquarePulse`` because the analytical expressions for on/off relaxation are
|
|
44
|
+
derived for ideal concentration steps. Pass an ``ErfPulse`` and a
|
|
45
|
+
``TypeError`` is raised with a clear message.
|
|
46
|
+
|
|
47
|
+
References
|
|
48
|
+
----------
|
|
49
|
+
Colquhoun D & Hawkes AG (1977) Relaxation and fluctuations of membrane
|
|
50
|
+
currents that flow through drug-operated channels.
|
|
51
|
+
Proc R Soc Lond B 199, 231-262.
|
|
52
|
+
|
|
53
|
+
Colquhoun D & Hawkes AG (1981) On the stochastic properties of single ion
|
|
54
|
+
channels. Proc R Soc Lond B 211, 205-235.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import math
|
|
60
|
+
from dataclasses import dataclass, field
|
|
61
|
+
from typing import Union
|
|
62
|
+
|
|
63
|
+
import numpy as np
|
|
64
|
+
import scipy.integrate as scpi
|
|
65
|
+
from scipy.special import erf
|
|
66
|
+
|
|
67
|
+
from scalcs import qmatlib as qml
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ===========================================================================
|
|
71
|
+
# Pulse profile dataclasses
|
|
72
|
+
# ===========================================================================
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ErfPulse:
|
|
76
|
+
"""Realistic concentration jump with error-function rise and fall.
|
|
77
|
+
|
|
78
|
+
This profile matches the waveform produced by fast perfusion ('rcj')
|
|
79
|
+
experiments in which the solution exchange is not instantaneous. The
|
|
80
|
+
concentration is::
|
|
81
|
+
|
|
82
|
+
c(t) = cb + cmax/2 * [erf((t - centre + width/2) / rise)
|
|
83
|
+
- erf((t - centre - width/2) / decay)]
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
cmax : float
|
|
88
|
+
Peak concentration (M). Must be > 0.
|
|
89
|
+
width : float
|
|
90
|
+
Pulse half-width (s). Must be > 0.
|
|
91
|
+
cb : float
|
|
92
|
+
Background (baseline) concentration (M). Default 0.
|
|
93
|
+
centre : float
|
|
94
|
+
Time of the pulse centre (s). Default 10 ms.
|
|
95
|
+
rise : float
|
|
96
|
+
10–90 % rise time constant for the error function (s). Default 200 µs.
|
|
97
|
+
decay : float
|
|
98
|
+
90–10 % decay time constant for the error function (s). Default 200 µs.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
cmax: float
|
|
102
|
+
width: float
|
|
103
|
+
cb: float = 0.0
|
|
104
|
+
centre: float = 10e-3
|
|
105
|
+
rise: float = 200e-6
|
|
106
|
+
decay: float = 200e-6
|
|
107
|
+
|
|
108
|
+
def __post_init__(self):
|
|
109
|
+
if self.cmax <= 0:
|
|
110
|
+
raise ValueError(f"ErfPulse: cmax must be > 0, got {self.cmax}")
|
|
111
|
+
if self.width <= 0:
|
|
112
|
+
raise ValueError(f"ErfPulse: width must be > 0, got {self.width}")
|
|
113
|
+
if self.rise <= 0:
|
|
114
|
+
raise ValueError(f"ErfPulse: rise must be > 0, got {self.rise}")
|
|
115
|
+
if self.decay <= 0:
|
|
116
|
+
raise ValueError(f"ErfPulse: decay must be > 0, got {self.decay}")
|
|
117
|
+
|
|
118
|
+
def profile(self, t: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
119
|
+
"""Concentration at time(s) *t* (M).
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
t : float or ndarray
|
|
124
|
+
Time in seconds.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
c : float or ndarray
|
|
129
|
+
Concentration in M. Same shape as *t*.
|
|
130
|
+
"""
|
|
131
|
+
conc = (self.cmax * 0.5 *
|
|
132
|
+
(erf((t - self.centre + self.width / 2.) / self.rise) -
|
|
133
|
+
erf((t - self.centre - self.width / 2.) / self.decay)))
|
|
134
|
+
return conc + self.cb
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class SquarePulse:
|
|
139
|
+
"""Ideal square concentration step.
|
|
140
|
+
|
|
141
|
+
The concentration profile is::
|
|
142
|
+
|
|
143
|
+
c(t) = cmax if prepulse < t <= prepulse + width
|
|
144
|
+
cb otherwise
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
cmax : float
|
|
149
|
+
Pulse concentration (M). Must be > 0.
|
|
150
|
+
width : float
|
|
151
|
+
Pulse duration (s). Must be > 0.
|
|
152
|
+
cb : float
|
|
153
|
+
Background concentration (M). Default 0.
|
|
154
|
+
prepulse : float
|
|
155
|
+
Time before the pulse starts (s). Default 5 ms.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
cmax: float
|
|
159
|
+
width: float
|
|
160
|
+
cb: float = 0.0
|
|
161
|
+
prepulse: float = 5e-3
|
|
162
|
+
|
|
163
|
+
def __post_init__(self):
|
|
164
|
+
if self.cmax <= 0:
|
|
165
|
+
raise ValueError(f"SquarePulse: cmax must be > 0, got {self.cmax}")
|
|
166
|
+
if self.width <= 0:
|
|
167
|
+
raise ValueError(f"SquarePulse: width must be > 0, got {self.width}")
|
|
168
|
+
|
|
169
|
+
def profile(self, t: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
170
|
+
"""Concentration at time(s) *t* (M)."""
|
|
171
|
+
if np.isscalar(t):
|
|
172
|
+
conc = (self.cmax
|
|
173
|
+
if self.prepulse < t <= self.prepulse + self.width
|
|
174
|
+
else 0.0)
|
|
175
|
+
else:
|
|
176
|
+
t = np.asarray(t)
|
|
177
|
+
conc = np.where(
|
|
178
|
+
(t > self.prepulse) & (t <= self.prepulse + self.width),
|
|
179
|
+
self.cmax, 0.0)
|
|
180
|
+
return conc + self.cb
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class InstExpPulse:
|
|
185
|
+
"""Concentration pulse with instantaneous rise and exponential decay.
|
|
186
|
+
|
|
187
|
+
The profile is::
|
|
188
|
+
|
|
189
|
+
c(t) = cb if t <= prepulse
|
|
190
|
+
cb + cmax * exp(-(t-prepulse) / tdec) if t > prepulse
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
cmax : float
|
|
195
|
+
Peak concentration immediately after the step (M). Must be > 0.
|
|
196
|
+
tdec : float
|
|
197
|
+
Exponential decay time constant (s). Must be > 0.
|
|
198
|
+
cb : float
|
|
199
|
+
Background concentration (M). Default 0.
|
|
200
|
+
prepulse : float
|
|
201
|
+
Time before the step (s). Default 5 ms.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
cmax: float
|
|
205
|
+
tdec: float
|
|
206
|
+
cb: float = 0.0
|
|
207
|
+
prepulse: float = 5e-3
|
|
208
|
+
|
|
209
|
+
def __post_init__(self):
|
|
210
|
+
if self.cmax <= 0:
|
|
211
|
+
raise ValueError(f"InstExpPulse: cmax must be > 0, got {self.cmax}")
|
|
212
|
+
if self.tdec <= 0:
|
|
213
|
+
raise ValueError(f"InstExpPulse: tdec must be > 0, got {self.tdec}")
|
|
214
|
+
|
|
215
|
+
def profile(self, t: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
216
|
+
"""Concentration at time(s) *t* (M)."""
|
|
217
|
+
if np.isscalar(t):
|
|
218
|
+
conc = (self.cmax * math.exp(-(t - self.prepulse) / self.tdec)
|
|
219
|
+
if t > self.prepulse else 0.0)
|
|
220
|
+
else:
|
|
221
|
+
t = np.asarray(t, dtype=float)
|
|
222
|
+
conc = np.where(
|
|
223
|
+
t > self.prepulse,
|
|
224
|
+
self.cmax * np.exp(-(t - self.prepulse) / self.tdec),
|
|
225
|
+
0.0)
|
|
226
|
+
return conc + self.cb
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class PairedSquarePulse:
|
|
231
|
+
"""Two square concentration pulses separated by a gap.
|
|
232
|
+
|
|
233
|
+
The profile is::
|
|
234
|
+
|
|
235
|
+
c(t) = cmax if prepulse < t <= prepulse + width
|
|
236
|
+
cmax if prepulse + width + inter < t
|
|
237
|
+
<= prepulse + 2*width + inter
|
|
238
|
+
cb otherwise
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
cmax : float
|
|
243
|
+
Pulse concentration (M). Must be > 0.
|
|
244
|
+
width : float
|
|
245
|
+
Duration of each pulse (s). Must be > 0.
|
|
246
|
+
inter : float
|
|
247
|
+
Gap between the two pulses (s). Must be >= 0.
|
|
248
|
+
cb : float
|
|
249
|
+
Background concentration (M). Default 0.
|
|
250
|
+
prepulse : float
|
|
251
|
+
Time before the first pulse (s). Default 5 ms.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
cmax: float
|
|
255
|
+
width: float
|
|
256
|
+
inter: float
|
|
257
|
+
cb: float = 0.0
|
|
258
|
+
prepulse: float = 5e-3
|
|
259
|
+
|
|
260
|
+
def __post_init__(self):
|
|
261
|
+
if self.cmax <= 0:
|
|
262
|
+
raise ValueError(f"PairedSquarePulse: cmax must be > 0, got {self.cmax}")
|
|
263
|
+
if self.width <= 0:
|
|
264
|
+
raise ValueError(f"PairedSquarePulse: width must be > 0, got {self.width}")
|
|
265
|
+
if self.inter < 0:
|
|
266
|
+
raise ValueError(f"PairedSquarePulse: inter must be >= 0, got {self.inter}")
|
|
267
|
+
|
|
268
|
+
def profile(self, t: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
269
|
+
"""Concentration at time(s) *t* (M)."""
|
|
270
|
+
p, w, g = self.prepulse, self.width, self.inter
|
|
271
|
+
if np.isscalar(t):
|
|
272
|
+
in_p1 = p < t <= p + w
|
|
273
|
+
in_p2 = p + w + g < t <= p + 2 * w + g
|
|
274
|
+
conc = self.cmax if (in_p1 or in_p2) else 0.0
|
|
275
|
+
else:
|
|
276
|
+
t = np.asarray(t, dtype=float)
|
|
277
|
+
in_p1 = (t > p) & (t <= p + w)
|
|
278
|
+
in_p2 = (t > p + w + g) & (t <= p + 2 * w + g)
|
|
279
|
+
conc = np.where(in_p1 | in_p2, self.cmax, 0.0)
|
|
280
|
+
return conc + self.cb
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Convenience type alias used in type hints below
|
|
284
|
+
AnyPulse = Union[ErfPulse, SquarePulse, InstExpPulse, PairedSquarePulse]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ===========================================================================
|
|
288
|
+
# Result dataclasses
|
|
289
|
+
# ===========================================================================
|
|
290
|
+
|
|
291
|
+
@dataclass
|
|
292
|
+
class JumpResult:
|
|
293
|
+
"""Output of :func:`solve`.
|
|
294
|
+
|
|
295
|
+
Attributes
|
|
296
|
+
----------
|
|
297
|
+
t : ndarray, shape (n,)
|
|
298
|
+
Time axis (s).
|
|
299
|
+
c : ndarray, shape (n,)
|
|
300
|
+
Concentration profile (M) as seen by the mechanism.
|
|
301
|
+
Popen : ndarray, shape (n,)
|
|
302
|
+
Open probability at each time point.
|
|
303
|
+
P : ndarray, shape (k, n)
|
|
304
|
+
Occupancy of every state (rows) at every time point (columns).
|
|
305
|
+
pulse : AnyPulse
|
|
306
|
+
The pulse object used to generate this result.
|
|
307
|
+
|
|
308
|
+
Notes
|
|
309
|
+
-----
|
|
310
|
+
The result supports 4-tuple unpacking for backward compatibility::
|
|
311
|
+
|
|
312
|
+
t, c, Popen, P = solve(mec, pulse, reclen, step)
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
t: np.ndarray
|
|
316
|
+
c: np.ndarray
|
|
317
|
+
Popen: np.ndarray
|
|
318
|
+
P: np.ndarray
|
|
319
|
+
pulse: AnyPulse
|
|
320
|
+
|
|
321
|
+
def __iter__(self):
|
|
322
|
+
"""Yield (t, c, Popen, P) so the result can be unpacked as a 4-tuple."""
|
|
323
|
+
yield self.t
|
|
324
|
+
yield self.c
|
|
325
|
+
yield self.Popen
|
|
326
|
+
yield self.P
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass
|
|
330
|
+
class RelaxationResult:
|
|
331
|
+
"""Output of :func:`relaxation_taus`.
|
|
332
|
+
|
|
333
|
+
Attributes
|
|
334
|
+
----------
|
|
335
|
+
tau_on_weighted : float
|
|
336
|
+
Amplitude-weighted mean on-relaxation time constant (s).
|
|
337
|
+
tau_off_weighted : float
|
|
338
|
+
Amplitude-weighted mean off-relaxation time constant (s).
|
|
339
|
+
tau_on : ndarray, shape (k-1,)
|
|
340
|
+
Individual on-relaxation time constants (s), sorted fastest first.
|
|
341
|
+
tau_off : ndarray, shape (k-1,)
|
|
342
|
+
Individual off-relaxation time constants (s), sorted fastest first.
|
|
343
|
+
ampl_on : ndarray, shape (k-1,)
|
|
344
|
+
Relative amplitudes for each on-relaxation component.
|
|
345
|
+
ampl_off : ndarray, shape (k-1,)
|
|
346
|
+
Relative amplitudes for each off-relaxation component.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
tau_on_weighted: float
|
|
350
|
+
tau_off_weighted: float
|
|
351
|
+
tau_on: np.ndarray
|
|
352
|
+
tau_off: np.ndarray
|
|
353
|
+
ampl_on: np.ndarray
|
|
354
|
+
ampl_off: np.ndarray
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ===========================================================================
|
|
358
|
+
# Private numerical core
|
|
359
|
+
# ===========================================================================
|
|
360
|
+
|
|
361
|
+
def _dPdt(P, t, mec, pulse):
|
|
362
|
+
"""Rate of change of state occupancies: dP/dt = P · Q(c(t)).
|
|
363
|
+
|
|
364
|
+
Used as the right-hand side for scipy.odeint.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
P : ndarray, shape (k,)
|
|
369
|
+
Current state occupancies.
|
|
370
|
+
t : float
|
|
371
|
+
Current time (s).
|
|
372
|
+
mec : Mechanism
|
|
373
|
+
The mechanism (Q matrix updated via set_eff).
|
|
374
|
+
pulse : AnyPulse
|
|
375
|
+
Concentration profile; supplies c(t) via pulse.profile(t).
|
|
376
|
+
"""
|
|
377
|
+
mec.set_eff('c', float(pulse.profile(t)))
|
|
378
|
+
return np.dot(P, mec.Q)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _P_t(t, eigs, w):
|
|
382
|
+
"""State occupancies at time *t* from eigendecomposition.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
t : float
|
|
387
|
+
Time elapsed since the step (s).
|
|
388
|
+
eigs : ndarray, shape (k,)
|
|
389
|
+
Eigenvalues of Q.
|
|
390
|
+
w : ndarray, shape (k, k)
|
|
391
|
+
Weighted spectral components from :func:`_coefficient_calc`.
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
Pt : ndarray, shape (k,)
|
|
396
|
+
State occupancies at time *t*.
|
|
397
|
+
"""
|
|
398
|
+
Pt = np.zeros(eigs.shape)
|
|
399
|
+
for i in range(eigs.size):
|
|
400
|
+
Pt[i] = np.sum(w[:, i] * np.exp(eigs * t))
|
|
401
|
+
return Pt
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _coefficient_calc(k, A, p_occup):
|
|
405
|
+
"""Weighted spectral components p · A_n for each spectral matrix A_n.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
k : int
|
|
410
|
+
Number of states.
|
|
411
|
+
A : ndarray, shape (k, k, k)
|
|
412
|
+
Spectral matrices of Q.
|
|
413
|
+
p_occup : ndarray, shape (k,)
|
|
414
|
+
State occupancies at the start of the interval.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
w : ndarray, shape (k, k)
|
|
419
|
+
Element w[n, i] = (p · A_n)[i].
|
|
420
|
+
"""
|
|
421
|
+
w = np.zeros((k, k))
|
|
422
|
+
for n in range(k):
|
|
423
|
+
w[n, :] = np.dot(p_occup, A[n, :, :])
|
|
424
|
+
return w
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _solve_ode(mec, pulse, reclen, step):
|
|
428
|
+
"""Solve by ODE integration (scipy.odeint).
|
|
429
|
+
|
|
430
|
+
Accurate for smoothly varying concentration profiles (e.g. ErfPulse).
|
|
431
|
+
Also works for piecewise profiles, though the matrix method may be faster
|
|
432
|
+
for those.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
mec : Mechanism
|
|
437
|
+
pulse : AnyPulse
|
|
438
|
+
reclen : float Recording length (s).
|
|
439
|
+
step : float Sampling interval (s).
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
t, c, Popen, P : as described in JumpResult.
|
|
444
|
+
"""
|
|
445
|
+
t = np.arange(0, reclen, step)
|
|
446
|
+
mec.set_eff('c', pulse.cb)
|
|
447
|
+
P0 = qml.pinf(mec.Q)
|
|
448
|
+
Pt = scpi.odeint(_dPdt, P0, t, args=(mec, pulse),
|
|
449
|
+
atol=1e-8, rtol=1e-6)
|
|
450
|
+
P = Pt.transpose()
|
|
451
|
+
Popen = np.sum(P[:mec.kA], axis=0)
|
|
452
|
+
c = pulse.profile(t)
|
|
453
|
+
return t, c, Popen, P
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _solve_matrix(mec, pulse, reclen, step):
|
|
457
|
+
"""Solve step-wise using the Q-matrix eigendecomposition.
|
|
458
|
+
|
|
459
|
+
At each time step the concentration is treated as constant and the
|
|
460
|
+
occupancies are propagated analytically via P(t+dt) = P(t) · exp(Q·dt).
|
|
461
|
+
Accurate for profiles that are piecewise constant (SquarePulse,
|
|
462
|
+
PairedSquarePulse) and useful as an independent check against _solve_ode.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
mec : Mechanism
|
|
467
|
+
pulse : AnyPulse
|
|
468
|
+
reclen : float
|
|
469
|
+
step : float
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
t, c, Popen, P : as described in JumpResult.
|
|
474
|
+
"""
|
|
475
|
+
t = np.arange(0, reclen, step)
|
|
476
|
+
c = pulse.profile(t)
|
|
477
|
+
mec.set_eff('c', pulse.cb)
|
|
478
|
+
pi = qml.pinf(mec.Q)
|
|
479
|
+
Pt = np.array([pi.copy()])
|
|
480
|
+
|
|
481
|
+
for i in range(1, t.shape[0]):
|
|
482
|
+
mec.set_eff('c', float(c[i]))
|
|
483
|
+
eigenvals, A = qml.eigs_sorted(mec.Q)
|
|
484
|
+
w = _coefficient_calc(mec.k, A, pi)
|
|
485
|
+
pi = _P_t(step, eigenvals, w)
|
|
486
|
+
Pt = np.append(Pt, [pi.copy()], axis=0)
|
|
487
|
+
|
|
488
|
+
P = Pt.transpose()
|
|
489
|
+
Popen = np.sum(P[:mec.kA], axis=0)
|
|
490
|
+
return t, c, Popen, P
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ===========================================================================
|
|
494
|
+
# Public API
|
|
495
|
+
# ===========================================================================
|
|
496
|
+
|
|
497
|
+
def solve(mec, pulse: AnyPulse, reclen: float, step: float,
|
|
498
|
+
method: str = 'ode') -> JumpResult:
|
|
499
|
+
"""Compute the macroscopic open-probability response to a concentration pulse.
|
|
500
|
+
|
|
501
|
+
Starting from the equilibrium state at the background concentration
|
|
502
|
+
``pulse.cb``, the state occupancies are evolved forward in time according
|
|
503
|
+
to dP/dt = P · Q(c(t)).
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
mec : Mechanism
|
|
508
|
+
The ion-channel mechanism (must have set_eff, Q, kA, k attributes).
|
|
509
|
+
pulse : ErfPulse | SquarePulse | InstExpPulse | PairedSquarePulse
|
|
510
|
+
Concentration profile. The pulse's ``.cb`` attribute sets the initial
|
|
511
|
+
equilibrium condition.
|
|
512
|
+
reclen : float
|
|
513
|
+
Total recording length (s).
|
|
514
|
+
step : float
|
|
515
|
+
Sampling interval (s). ``reclen / step`` time points are returned.
|
|
516
|
+
method : {'ode', 'matrix'}
|
|
517
|
+
Numerical method:
|
|
518
|
+
|
|
519
|
+
``'ode'``
|
|
520
|
+
Scipy ODE integrator (``odeint``). Robust for all pulse shapes,
|
|
521
|
+
especially smooth profiles like :class:`ErfPulse`.
|
|
522
|
+
``'matrix'``
|
|
523
|
+
Step-wise Q-matrix eigendecomposition. Each time step is treated
|
|
524
|
+
as a constant-concentration interval. Useful as an independent
|
|
525
|
+
numerical check; may be slower.
|
|
526
|
+
|
|
527
|
+
Returns
|
|
528
|
+
-------
|
|
529
|
+
JumpResult
|
|
530
|
+
Named result with attributes ``t``, ``c``, ``Popen``, ``P``, ``pulse``.
|
|
531
|
+
Supports 4-tuple unpacking: ``t, c, Popen, P = result``.
|
|
532
|
+
|
|
533
|
+
Raises
|
|
534
|
+
------
|
|
535
|
+
ValueError
|
|
536
|
+
If *method* is not ``'ode'`` or ``'matrix'``.
|
|
537
|
+
|
|
538
|
+
Examples
|
|
539
|
+
--------
|
|
540
|
+
>>> from scalcs.samples.samples import CH82
|
|
541
|
+
>>> from scalcs.cjumps import ErfPulse, solve
|
|
542
|
+
>>> mec = CH82()
|
|
543
|
+
>>> pulse = ErfPulse(cmax=1e-6, width=10e-3)
|
|
544
|
+
>>> result = solve(mec, pulse, reclen=50e-3, step=5e-6)
|
|
545
|
+
>>> t, c, Popen, P = result # backward-compatible unpacking
|
|
546
|
+
"""
|
|
547
|
+
if method == 'ode':
|
|
548
|
+
t, c, Popen, P = _solve_ode(mec, pulse, reclen, step)
|
|
549
|
+
elif method == 'matrix':
|
|
550
|
+
t, c, Popen, P = _solve_matrix(mec, pulse, reclen, step)
|
|
551
|
+
else:
|
|
552
|
+
raise ValueError(
|
|
553
|
+
f"solve: unknown method {method!r}. Choose 'ode' or 'matrix'.")
|
|
554
|
+
return JumpResult(t=t, c=c, Popen=Popen, P=P, pulse=pulse)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def relaxation_taus(mec, pulse: SquarePulse) -> RelaxationResult:
|
|
558
|
+
"""Analytical on/off relaxation time constants for a square concentration step.
|
|
559
|
+
|
|
560
|
+
For a SquarePulse the on-relaxation is governed by the eigenvalues of Q at
|
|
561
|
+
``pulse.cmax`` (starting from equilibrium at ``pulse.cb``), and the
|
|
562
|
+
off-relaxation by the eigenvalues of Q at ``pulse.cb`` (starting from the
|
|
563
|
+
occupancies at the end of the pulse).
|
|
564
|
+
|
|
565
|
+
Parameters
|
|
566
|
+
----------
|
|
567
|
+
mec : Mechanism
|
|
568
|
+
pulse : SquarePulse
|
|
569
|
+
Must be a :class:`SquarePulse`. Passing any other pulse type raises
|
|
570
|
+
``TypeError`` because the analytical expressions assume an ideal step.
|
|
571
|
+
|
|
572
|
+
Returns
|
|
573
|
+
-------
|
|
574
|
+
RelaxationResult
|
|
575
|
+
Attributes: ``tau_on_weighted``, ``tau_off_weighted`` (floats),
|
|
576
|
+
``tau_on``, ``tau_off``, ``ampl_on``, ``ampl_off`` (ndarrays, k-1).
|
|
577
|
+
|
|
578
|
+
Raises
|
|
579
|
+
------
|
|
580
|
+
TypeError
|
|
581
|
+
If *pulse* is not a :class:`SquarePulse`.
|
|
582
|
+
|
|
583
|
+
Notes
|
|
584
|
+
-----
|
|
585
|
+
This replaces the old ``weighted_taus()`` which returned only 2 scalars,
|
|
586
|
+
breaking the ``sccurves`` call that expected 4 return values.
|
|
587
|
+
"""
|
|
588
|
+
if not isinstance(pulse, SquarePulse):
|
|
589
|
+
raise TypeError(
|
|
590
|
+
"relaxation_taus() requires a SquarePulse (ideal step). "
|
|
591
|
+
f"Got {type(pulse).__name__}. "
|
|
592
|
+
"For non-square profiles use solve() and inspect the Popen trace.")
|
|
593
|
+
|
|
594
|
+
# Equilibrium at background concentration
|
|
595
|
+
mec.set_eff('c', pulse.cb)
|
|
596
|
+
eigs0, A0 = qml.eigs_sorted(mec.Q)
|
|
597
|
+
P0 = qml.pinf(mec.Q)
|
|
598
|
+
|
|
599
|
+
# Equilibrium at pulse concentration
|
|
600
|
+
mec.set_eff('c', pulse.cmax)
|
|
601
|
+
eigsInf, Ainf = qml.eigs_sorted(mec.Q)
|
|
602
|
+
|
|
603
|
+
# On-relaxation: step from cb → cmax
|
|
604
|
+
w_on = _coefficient_calc(mec.k, Ainf, P0)
|
|
605
|
+
ampl_on_raw = np.sum(w_on[:, :mec.kA], axis=1) # shape (k,)
|
|
606
|
+
max_ampl_on = np.max(np.abs(ampl_on_raw))
|
|
607
|
+
rel_ampl_on = ampl_on_raw / max_ampl_on # normalised
|
|
608
|
+
tau_on = -1.0 / eigsInf[:-1] # k-1 time constants (s)
|
|
609
|
+
# The dominant component typically has a negative relative amplitude
|
|
610
|
+
# (it drives occupancy toward the new equilibrium). Negating gives the
|
|
611
|
+
# conventional positive weighted time constant (matches DCPROGS HJC_JUMP).
|
|
612
|
+
tau_on_weighted = float(np.sum(-rel_ampl_on[:-1] * tau_on))
|
|
613
|
+
|
|
614
|
+
# Occupancies at end of pulse
|
|
615
|
+
Pt = _P_t(pulse.width, eigsInf, w_on)
|
|
616
|
+
|
|
617
|
+
# Off-relaxation: step from cmax → cb
|
|
618
|
+
w_off = _coefficient_calc(mec.k, A0, Pt)
|
|
619
|
+
ampl_off_raw = np.sum(w_off[:, :mec.kA], axis=1)
|
|
620
|
+
max_ampl_off = np.max(np.abs(ampl_off_raw))
|
|
621
|
+
rel_ampl_off = ampl_off_raw / max_ampl_off
|
|
622
|
+
tau_off = -1.0 / eigs0[:-1]
|
|
623
|
+
tau_off_weighted = float(np.sum(rel_ampl_off[:-1] * tau_off))
|
|
624
|
+
|
|
625
|
+
return RelaxationResult(
|
|
626
|
+
tau_on_weighted=tau_on_weighted,
|
|
627
|
+
tau_off_weighted=tau_off_weighted,
|
|
628
|
+
tau_on=tau_on,
|
|
629
|
+
tau_off=tau_off,
|
|
630
|
+
ampl_on=rel_ampl_on[:-1],
|
|
631
|
+
ampl_off=rel_ampl_off[:-1],
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def jump_summary(mec, pulse: SquarePulse,
|
|
636
|
+
gamma: float = 30e-12,
|
|
637
|
+
Vm: float = -80e-3) -> dict:
|
|
638
|
+
"""Analytical properties of a square concentration jump.
|
|
639
|
+
|
|
640
|
+
Computes all quantities needed for ``printout()`` and returns them as a
|
|
641
|
+
plain dict so that each quantity is individually testable.
|
|
642
|
+
|
|
643
|
+
Parameters
|
|
644
|
+
----------
|
|
645
|
+
mec : Mechanism
|
|
646
|
+
pulse : SquarePulse
|
|
647
|
+
Must be a :class:`SquarePulse`; raises ``TypeError`` otherwise.
|
|
648
|
+
gamma : float
|
|
649
|
+
Single-channel conductance (S). Default 30 pS.
|
|
650
|
+
Vm : float
|
|
651
|
+
Transmembrane voltage (V). Default −80 mV.
|
|
652
|
+
|
|
653
|
+
Returns
|
|
654
|
+
-------
|
|
655
|
+
dict with keys:
|
|
656
|
+
|
|
657
|
+
``p0`` ndarray — equilibrium occupancies before pulse.
|
|
658
|
+
``pinf`` ndarray — equilibrium occupancies at cmax.
|
|
659
|
+
``pt`` ndarray — occupancies at end of pulse.
|
|
660
|
+
``eigenvalues_on`` ndarray, shape (k-1,) — eigenvalues during on-phase (s⁻¹).
|
|
661
|
+
``eigenvalues_off`` ndarray, shape (k-1,) — eigenvalues during off-phase (s⁻¹).
|
|
662
|
+
``amplitudes_on`` ndarray, shape (k-1,) — current amplitudes, on-phase (A).
|
|
663
|
+
``amplitudes_off`` ndarray, shape (k-1,) — current amplitudes, off-phase (A).
|
|
664
|
+
``rel_amplitudes_on`` ndarray — normalised (dimensionless).
|
|
665
|
+
``rel_amplitudes_off`` ndarray — normalised.
|
|
666
|
+
``areas_on`` ndarray, shape (k-1,) — charge under each on component (C).
|
|
667
|
+
``areas_off`` ndarray, shape (k-1,) — charge under each off component (C).
|
|
668
|
+
``tau_on_weighted`` float — weighted on time constant (s).
|
|
669
|
+
``tau_off_weighted`` float — weighted off time constant (s).
|
|
670
|
+
``gamma`` float — conductance used (S).
|
|
671
|
+
``Vm`` float — voltage used (V).
|
|
672
|
+
|
|
673
|
+
Raises
|
|
674
|
+
------
|
|
675
|
+
TypeError
|
|
676
|
+
If *pulse* is not a :class:`SquarePulse`.
|
|
677
|
+
"""
|
|
678
|
+
if not isinstance(pulse, SquarePulse):
|
|
679
|
+
raise TypeError(
|
|
680
|
+
"jump_summary() requires a SquarePulse. "
|
|
681
|
+
f"Got {type(pulse).__name__}.")
|
|
682
|
+
|
|
683
|
+
# --- equilibrium before pulse ---
|
|
684
|
+
mec.set_eff('c', pulse.cb)
|
|
685
|
+
P0 = qml.pinf(mec.Q)
|
|
686
|
+
eigs0, A0 = qml.eigs_sorted(mec.Q)
|
|
687
|
+
|
|
688
|
+
# --- equilibrium at peak concentration ---
|
|
689
|
+
mec.set_eff('c', pulse.cmax)
|
|
690
|
+
Pinf = qml.pinf(mec.Q)
|
|
691
|
+
eigsInf, Ainf = qml.eigs_sorted(mec.Q)
|
|
692
|
+
|
|
693
|
+
# --- on-relaxation ---
|
|
694
|
+
w_on = _coefficient_calc(mec.k, Ainf, P0)
|
|
695
|
+
ampl_on_raw = np.sum(w_on[:, :mec.kA], axis=1) # shape (k,)
|
|
696
|
+
cur_on = ampl_on_raw * gamma * Vm # current (A)
|
|
697
|
+
max_ampl_on = np.max(np.abs(ampl_on_raw))
|
|
698
|
+
rel_ampl_on = ampl_on_raw / max_ampl_on
|
|
699
|
+
tau_on = -1.0 / eigsInf[:-1] # k-1 time constants (s)
|
|
700
|
+
area_on = -cur_on[:-1] / eigsInf[:-1] # charge (C)
|
|
701
|
+
tau_on_weighted = float(np.sum(-rel_ampl_on[:-1] * tau_on))
|
|
702
|
+
|
|
703
|
+
# --- occupancies at end of pulse ---
|
|
704
|
+
Pt = _P_t(pulse.width, eigsInf, w_on)
|
|
705
|
+
|
|
706
|
+
# --- off-relaxation ---
|
|
707
|
+
w_off = _coefficient_calc(mec.k, A0, Pt)
|
|
708
|
+
ampl_off_raw = np.sum(w_off[:, :mec.kA], axis=1)
|
|
709
|
+
cur_off = ampl_off_raw * gamma * Vm
|
|
710
|
+
max_ampl_off = np.max(np.abs(ampl_off_raw))
|
|
711
|
+
rel_ampl_off = ampl_off_raw / max_ampl_off
|
|
712
|
+
tau_off = -1.0 / eigs0[:-1]
|
|
713
|
+
area_off = np.zeros(mec.k - 1)
|
|
714
|
+
for i in range(mec.k - 1):
|
|
715
|
+
area_off[i] = -1000.0 * cur_off[i] / eigs0[i] # in pC·ms (×1000)
|
|
716
|
+
tau_off_weighted = float(np.sum(rel_ampl_off[:-1] * tau_off))
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
'p0': P0,
|
|
720
|
+
'pinf': Pinf,
|
|
721
|
+
'pt': Pt,
|
|
722
|
+
'eigenvalues_on': eigsInf[:-1],
|
|
723
|
+
'eigenvalues_off': eigs0[:-1],
|
|
724
|
+
'amplitudes_on': cur_on[:-1],
|
|
725
|
+
'amplitudes_off': cur_off[:-1],
|
|
726
|
+
'rel_amplitudes_on': rel_ampl_on[:-1],
|
|
727
|
+
'rel_amplitudes_off': rel_ampl_off[:-1],
|
|
728
|
+
'areas_on': area_on,
|
|
729
|
+
'areas_off': area_off,
|
|
730
|
+
'tau_on_weighted': tau_on_weighted,
|
|
731
|
+
'tau_off_weighted': tau_off_weighted,
|
|
732
|
+
'gamma': gamma,
|
|
733
|
+
'Vm': Vm,
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def printout(mec, pulse: SquarePulse,
|
|
738
|
+
gamma: float = 30e-12,
|
|
739
|
+
Vm: float = -80e-3) -> str:
|
|
740
|
+
"""Human-readable summary of concentration jump properties.
|
|
741
|
+
|
|
742
|
+
Produces the same analytical printout as the original ``cjumps.printout``
|
|
743
|
+
but with configurable single-channel conductance and voltage (no more
|
|
744
|
+
hard-coded values).
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
mec : Mechanism
|
|
749
|
+
pulse : SquarePulse
|
|
750
|
+
Must be a :class:`SquarePulse`; raises ``TypeError`` otherwise.
|
|
751
|
+
gamma : float
|
|
752
|
+
Single-channel conductance (S). Default 30 pS.
|
|
753
|
+
Vm : float
|
|
754
|
+
Transmembrane voltage (V). Default −80 mV.
|
|
755
|
+
|
|
756
|
+
Returns
|
|
757
|
+
-------
|
|
758
|
+
str
|
|
759
|
+
Formatted text report.
|
|
760
|
+
|
|
761
|
+
Raises
|
|
762
|
+
------
|
|
763
|
+
TypeError
|
|
764
|
+
If *pulse* is not a :class:`SquarePulse`.
|
|
765
|
+
"""
|
|
766
|
+
if not isinstance(pulse, SquarePulse):
|
|
767
|
+
raise TypeError(
|
|
768
|
+
"printout() requires a SquarePulse. "
|
|
769
|
+
f"Got {type(pulse).__name__}. "
|
|
770
|
+
"For other profiles, use solve() to obtain the Popen time course.")
|
|
771
|
+
|
|
772
|
+
s = jump_summary(mec, pulse, gamma=gamma, Vm=Vm)
|
|
773
|
+
k = mec.k
|
|
774
|
+
|
|
775
|
+
out = ('\n*******************************************\n'
|
|
776
|
+
'CONCENTRATION JUMPS\n')
|
|
777
|
+
|
|
778
|
+
out += ('\nEquilibrium occupancies before t=0, at concentration = '
|
|
779
|
+
'{:.5g} mM:\n'.format(pulse.cb * 1000))
|
|
780
|
+
for i in range(k):
|
|
781
|
+
out += ' p0({:d}) = {:.5g}\n'.format(i + 1, s['p0'][i])
|
|
782
|
+
|
|
783
|
+
out += ('\nEquilibrium occupancies at peak concentration = '
|
|
784
|
+
'{:.5g} mM:\n'.format(pulse.cmax * 1000))
|
|
785
|
+
for i in range(k):
|
|
786
|
+
out += ' pinf({:d}) = {:.5g}\n'.format(i + 1, s['pinf'][i])
|
|
787
|
+
|
|
788
|
+
out += '\nOccupancies at end of {:.5g} ms pulse:\n'.format(pulse.width * 1000)
|
|
789
|
+
for i in range(k):
|
|
790
|
+
out += ' pt({:d}) = {:.5g}\n'.format(i + 1, s['pt'][i])
|
|
791
|
+
|
|
792
|
+
out += ('\nSingle-channel conductance: {:.5g} pS\n'
|
|
793
|
+
'Transmembrane voltage: {:.5g} mV\n'.format(
|
|
794
|
+
gamma * 1e12, Vm * 1e3))
|
|
795
|
+
|
|
796
|
+
out += ('\nON-RELAXATION (ideal step {:s} → {:s} mM):\n'
|
|
797
|
+
' {:>4s} {:>14s} {:>10s} {:>12s} {:>12s}\n'.format(
|
|
798
|
+
'{:.5g}'.format(pulse.cb * 1000),
|
|
799
|
+
'{:.5g}'.format(pulse.cmax * 1000),
|
|
800
|
+
'Comp', 'Eigenvalue', 'Tau (ms)',
|
|
801
|
+
'Ampl (t=0, pA)', 'Rel. ampl.'))
|
|
802
|
+
for i in range(k - 1):
|
|
803
|
+
out += (' {:>4d} {:>14.5g} {:>10.5g} {:>12.5g} {:>12.5g}\n'.format(
|
|
804
|
+
i + 1,
|
|
805
|
+
s['eigenvalues_on'][i],
|
|
806
|
+
-1000.0 / s['eigenvalues_on'][i],
|
|
807
|
+
s['amplitudes_on'][i] * 1e12, # pA
|
|
808
|
+
s['rel_amplitudes_on'][i]))
|
|
809
|
+
out += (' Weighted on tau = {:.5g} ms\n'.format(s['tau_on_weighted'] * 1000))
|
|
810
|
+
out += (' Total current at t=0 = {:.5g} pA\n'.format(
|
|
811
|
+
np.sum(s['amplitudes_on']) * 1e12))
|
|
812
|
+
cur_on_eq = s['amplitudes_on'] # last component is equilibrium term
|
|
813
|
+
out += (' Total area (on) = {:.5g} pC\n'.format(
|
|
814
|
+
np.sum(s['areas_on']) * 1e3)) # stored in C, convert to pC
|
|
815
|
+
|
|
816
|
+
out += ('\nOFF-RELAXATION (ideal step {:s} → {:s} mM):\n'
|
|
817
|
+
' {:>4s} {:>14s} {:>10s} {:>12s} {:>12s}\n'.format(
|
|
818
|
+
'{:.5g}'.format(pulse.cmax * 1000),
|
|
819
|
+
'{:.5g}'.format(pulse.cb * 1000),
|
|
820
|
+
'Comp', 'Eigenvalue', 'Tau (ms)',
|
|
821
|
+
'Ampl (t=0, pA)', 'Rel. ampl.'))
|
|
822
|
+
for i in range(k - 1):
|
|
823
|
+
out += (' {:>4d} {:>14.5g} {:>10.5g} {:>12.5g} {:>12.5g}\n'.format(
|
|
824
|
+
i + 1,
|
|
825
|
+
s['eigenvalues_off'][i],
|
|
826
|
+
-1000.0 / s['eigenvalues_off'][i],
|
|
827
|
+
s['amplitudes_off'][i] * 1e12,
|
|
828
|
+
s['rel_amplitudes_off'][i]))
|
|
829
|
+
out += (' Weighted off tau = {:.5g} ms\n'.format(s['tau_off_weighted'] * 1000))
|
|
830
|
+
out += (' Total current at t=0 = {:.5g} pA\n'.format(
|
|
831
|
+
np.sum(s['amplitudes_off']) * 1e12))
|
|
832
|
+
out += (' Total area (off) = {:.5g} pC\n'.format(
|
|
833
|
+
np.sum(s['areas_off'])))
|
|
834
|
+
|
|
835
|
+
return out
|