dendrotweaks 0.3.1__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.
- dendrotweaks/__init__.py +10 -0
- dendrotweaks/analysis/__init__.py +11 -0
- dendrotweaks/analysis/ephys_analysis.py +482 -0
- dendrotweaks/analysis/morphometric_analysis.py +106 -0
- dendrotweaks/membrane/__init__.py +6 -0
- dendrotweaks/membrane/default_mod/AMPA.mod +65 -0
- dendrotweaks/membrane/default_mod/AMPA_NMDA.mod +100 -0
- dendrotweaks/membrane/default_mod/CaDyn.mod +54 -0
- dendrotweaks/membrane/default_mod/GABAa.mod +65 -0
- dendrotweaks/membrane/default_mod/Leak.mod +27 -0
- dendrotweaks/membrane/default_mod/NMDA.mod +72 -0
- dendrotweaks/membrane/default_mod/vecstim.mod +76 -0
- dendrotweaks/membrane/default_templates/NEURON_template.py +354 -0
- dendrotweaks/membrane/default_templates/default.py +73 -0
- dendrotweaks/membrane/default_templates/standard_channel.mod +87 -0
- dendrotweaks/membrane/default_templates/template_jaxley.py +108 -0
- dendrotweaks/membrane/default_templates/template_jaxley_new.py +108 -0
- dendrotweaks/membrane/distributions.py +324 -0
- dendrotweaks/membrane/groups.py +103 -0
- dendrotweaks/membrane/io/__init__.py +11 -0
- dendrotweaks/membrane/io/ast.py +201 -0
- dendrotweaks/membrane/io/code_generators.py +312 -0
- dendrotweaks/membrane/io/converter.py +108 -0
- dendrotweaks/membrane/io/factories.py +144 -0
- dendrotweaks/membrane/io/grammar.py +417 -0
- dendrotweaks/membrane/io/loader.py +90 -0
- dendrotweaks/membrane/io/parser.py +499 -0
- dendrotweaks/membrane/io/reader.py +212 -0
- dendrotweaks/membrane/mechanisms.py +574 -0
- dendrotweaks/model.py +1916 -0
- dendrotweaks/model_io.py +75 -0
- dendrotweaks/morphology/__init__.py +5 -0
- dendrotweaks/morphology/domains.py +100 -0
- dendrotweaks/morphology/io/__init__.py +5 -0
- dendrotweaks/morphology/io/factories.py +212 -0
- dendrotweaks/morphology/io/reader.py +66 -0
- dendrotweaks/morphology/io/validation.py +212 -0
- dendrotweaks/morphology/point_trees.py +681 -0
- dendrotweaks/morphology/reduce/__init__.py +16 -0
- dendrotweaks/morphology/reduce/reduce.py +155 -0
- dendrotweaks/morphology/reduce/reduced_cylinder.py +129 -0
- dendrotweaks/morphology/sec_trees.py +1112 -0
- dendrotweaks/morphology/seg_trees.py +157 -0
- dendrotweaks/morphology/trees.py +567 -0
- dendrotweaks/path_manager.py +261 -0
- dendrotweaks/simulators.py +235 -0
- dendrotweaks/stimuli/__init__.py +3 -0
- dendrotweaks/stimuli/iclamps.py +73 -0
- dendrotweaks/stimuli/populations.py +265 -0
- dendrotweaks/stimuli/synapses.py +203 -0
- dendrotweaks/utils.py +239 -0
- dendrotweaks-0.3.1.dist-info/METADATA +70 -0
- dendrotweaks-0.3.1.dist-info/RECORD +56 -0
- dendrotweaks-0.3.1.dist-info/WHEEL +5 -0
- dendrotweaks-0.3.1.dist-info/licenses/LICENSE +674 -0
- dendrotweaks-0.3.1.dist-info/top_level.txt +1 -0
dendrotweaks/__init__.py
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
__version__ = "0.3.1"
|
2
|
+
|
3
|
+
from dendrotweaks.model import Model
|
4
|
+
from dendrotweaks.simulators import NEURONSimulator
|
5
|
+
from dendrotweaks.membrane.distributions import Distribution
|
6
|
+
from dendrotweaks.path_manager import PathManager
|
7
|
+
from dendrotweaks.stimuli import Synapse, Population, IClamp
|
8
|
+
|
9
|
+
from dendrotweaks.utils import download_example_data
|
10
|
+
from dendrotweaks.utils import apply_dark_theme
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
from dendrotweaks.analysis.morphometric_analysis import calculate_domain_statistics
|
3
|
+
from dendrotweaks.analysis.morphometric_analysis import calculate_cell_statistics
|
4
|
+
from dendrotweaks.analysis.ephys_analysis import calculate_fI_curve, detect_somatic_spikes
|
5
|
+
from dendrotweaks.analysis.ephys_analysis import plot_fI_curve, plot_somatic_spikes
|
6
|
+
from dendrotweaks.analysis.ephys_analysis import calculate_time_constant, calculate_input_resistance, calculate_passive_properties
|
7
|
+
from dendrotweaks.analysis.ephys_analysis import plot_passive_properties
|
8
|
+
from dendrotweaks.analysis.ephys_analysis import calculate_voltage_attenuation
|
9
|
+
from dendrotweaks.analysis.ephys_analysis import plot_voltage_attenuation
|
10
|
+
from dendrotweaks.analysis.ephys_analysis import calculate_dendritic_nonlinearity
|
11
|
+
from dendrotweaks.analysis.ephys_analysis import plot_dendritic_nonlinearity
|
@@ -0,0 +1,482 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import pandas as pd
|
3
|
+
import matplotlib.pyplot as plt
|
4
|
+
|
5
|
+
from scipy.signal import find_peaks, peak_widths
|
6
|
+
from scipy.optimize import curve_fit
|
7
|
+
|
8
|
+
|
9
|
+
# =============================================================================
|
10
|
+
# PASSIVE PROPERTIES
|
11
|
+
# =============================================================================
|
12
|
+
|
13
|
+
def get_somatic_data(model):
|
14
|
+
"""
|
15
|
+
Get the somatic voltage, time, time step, and injected current.
|
16
|
+
|
17
|
+
Parameters
|
18
|
+
----------
|
19
|
+
model : Model
|
20
|
+
The neuron model.
|
21
|
+
|
22
|
+
Returns
|
23
|
+
-------
|
24
|
+
tuple
|
25
|
+
A tuple containing the voltage, time, time step, and injected current.
|
26
|
+
"""
|
27
|
+
seg = model.seg_tree.root
|
28
|
+
iclamp = model.iclamps[seg]
|
29
|
+
|
30
|
+
v = np.array(model.simulator.vs[seg])
|
31
|
+
t = np.array(model.simulator.t)
|
32
|
+
dt = model.simulator.dt
|
33
|
+
|
34
|
+
return v, t, dt, iclamp
|
35
|
+
|
36
|
+
|
37
|
+
def calculate_input_resistance(model):
|
38
|
+
"""
|
39
|
+
Calculate the input resistance of the neuron model.
|
40
|
+
|
41
|
+
Parameters
|
42
|
+
----------
|
43
|
+
model : Model
|
44
|
+
The neuron model.
|
45
|
+
|
46
|
+
Returns
|
47
|
+
-------
|
48
|
+
dict
|
49
|
+
A dictionary containing the onset and offset voltages, the input resistance, and the injected current.
|
50
|
+
"""
|
51
|
+
|
52
|
+
v, t, dt, iclamp = get_somatic_data(model)
|
53
|
+
|
54
|
+
v_min = np.min(v)
|
55
|
+
|
56
|
+
amp = iclamp.amp
|
57
|
+
start_ts = iclamp.delay / dt
|
58
|
+
end_ts = int((iclamp.delay + iclamp.dur) / dt)
|
59
|
+
v_onset = v[int(start_ts)]
|
60
|
+
v_offset = v[int(end_ts)]
|
61
|
+
|
62
|
+
R_in = (v_onset - v_offset) / amp
|
63
|
+
print(f"Input resistance: {R_in:.2f} MOhm")
|
64
|
+
|
65
|
+
return {
|
66
|
+
'onset_voltage': v_onset,
|
67
|
+
'offset_voltage': v_offset,
|
68
|
+
'input_resistance': R_in,
|
69
|
+
'current_amplitude': amp
|
70
|
+
}
|
71
|
+
|
72
|
+
|
73
|
+
def _exp_decay(t, A, tau):
|
74
|
+
return A * np.exp(-t / tau)
|
75
|
+
|
76
|
+
|
77
|
+
def calculate_time_constant(model):
|
78
|
+
"""
|
79
|
+
Calculate the membrane time constant of the neuron model.
|
80
|
+
|
81
|
+
Parameters
|
82
|
+
----------
|
83
|
+
model : Model
|
84
|
+
The neuron model.
|
85
|
+
|
86
|
+
Returns
|
87
|
+
-------
|
88
|
+
dict
|
89
|
+
A dictionary containing the time constant and the exponential fit.
|
90
|
+
"""
|
91
|
+
v, t, dt, iclamp = get_somatic_data(model)
|
92
|
+
|
93
|
+
start_ts = int(iclamp.delay / dt)
|
94
|
+
stop_ts = int((iclamp.delay + iclamp.dur) / dt)
|
95
|
+
min_ts = np.argmin(v[start_ts:stop_ts]) + start_ts
|
96
|
+
v_min = np.min(v[start_ts: min_ts])
|
97
|
+
v_decay = v[start_ts: min_ts] - v_min
|
98
|
+
t_decay = t[start_ts: min_ts] - t[start_ts]
|
99
|
+
popt, _ = curve_fit(_exp_decay, t_decay, v_decay, p0=[1, 100])
|
100
|
+
tau = popt[1]
|
101
|
+
A = popt[0]
|
102
|
+
print(f"Membrane time constant: {tau:.2f} ms")
|
103
|
+
return {
|
104
|
+
'time_constant': tau,
|
105
|
+
'A': A,
|
106
|
+
'start_time': start_ts * dt,
|
107
|
+
'decay_time': t_decay,
|
108
|
+
'decay_voltage': v_decay
|
109
|
+
}
|
110
|
+
|
111
|
+
def calculate_passive_properties(model):
|
112
|
+
"""
|
113
|
+
Calculate the passive properties of the neuron model.
|
114
|
+
|
115
|
+
Parameters
|
116
|
+
----------
|
117
|
+
model : Model
|
118
|
+
The neuron model.
|
119
|
+
|
120
|
+
Returns
|
121
|
+
-------
|
122
|
+
dict
|
123
|
+
A dictionary containing the input resistance, time constant, and the exponential fit.
|
124
|
+
"""
|
125
|
+
|
126
|
+
data_rin = calculate_input_resistance(model)
|
127
|
+
data_tau = calculate_time_constant(model)
|
128
|
+
|
129
|
+
return {**data_rin, **data_tau}
|
130
|
+
|
131
|
+
def plot_passive_properties(data, ax=None):
|
132
|
+
|
133
|
+
if ax is None:
|
134
|
+
_, ax = plt.subplots()
|
135
|
+
|
136
|
+
R_in = data['input_resistance']
|
137
|
+
tau = data['time_constant']
|
138
|
+
v_onset = data['onset_voltage']
|
139
|
+
v_offset = data['offset_voltage']
|
140
|
+
t_decay = data['decay_time']
|
141
|
+
v_decay = data['decay_voltage']
|
142
|
+
A = data['A']
|
143
|
+
start_t = data['start_time']
|
144
|
+
|
145
|
+
ax.set_title(f"R_in: {R_in:.2f} MOhm, Tau: {tau:.2f} ms")
|
146
|
+
ax.axhline(v_onset, color='gray', linestyle='--', label='V onset')
|
147
|
+
ax.axhline(v_offset, color='gray', linestyle='--', label='V offset')
|
148
|
+
|
149
|
+
# Shift the exp_decay output along the y-axis
|
150
|
+
shifted_exp_decay = _exp_decay(t_decay, A, tau) + v_offset
|
151
|
+
ax.plot(t_decay + start_t, shifted_exp_decay, color='red', label='Exp. fit', linestyle='--')
|
152
|
+
ax.legend()
|
153
|
+
|
154
|
+
|
155
|
+
# =============================================================================
|
156
|
+
# ACTIVE PROPERTIES
|
157
|
+
# =============================================================================
|
158
|
+
|
159
|
+
def detect_somatic_spikes(model, **kwargs):
|
160
|
+
"""
|
161
|
+
Detect somatic spikes in the model and calculate spike amplitudes and widths.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
dict: A dictionary containing spike metrics.
|
165
|
+
"""
|
166
|
+
seg = model.seg_tree.root
|
167
|
+
|
168
|
+
v = np.array(model.simulator.vs[seg])
|
169
|
+
t = np.array(model.simulator.t)
|
170
|
+
dt = model.simulator.dt
|
171
|
+
|
172
|
+
baseline = np.median(v)
|
173
|
+
height = kwargs.get('height', baseline)
|
174
|
+
distance = kwargs.get('distance', int(2/dt))
|
175
|
+
prominence = kwargs.get('prominence', 50)
|
176
|
+
wlen = kwargs.get('wlen', int(20/dt))
|
177
|
+
|
178
|
+
peaks, properties = find_peaks(v, height=height, distance=distance, prominence=prominence, wlen=wlen)
|
179
|
+
half_widths, _, left_bases, right_bases = peak_widths(v, peaks, rel_height=0.5)
|
180
|
+
half_widths *= dt
|
181
|
+
left_bases *= dt
|
182
|
+
right_bases *= dt
|
183
|
+
|
184
|
+
return {
|
185
|
+
'spike_times': t[peaks],
|
186
|
+
'spike_values': properties['peak_heights'],
|
187
|
+
'half_widths': half_widths,
|
188
|
+
'amplitudes': properties['prominences'],
|
189
|
+
'left_bases': left_bases,
|
190
|
+
'right_bases': right_bases,
|
191
|
+
'stimulus_duration': model.iclamps[seg].dur
|
192
|
+
}
|
193
|
+
|
194
|
+
|
195
|
+
def plot_somatic_spikes(data, ax=None, show_metrics=False):
|
196
|
+
"""Plot detected spikes on the provided axis or create a new figure.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
model: The neuron model
|
200
|
+
ax: Optional matplotlib axis for plotting
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
matplotlib.axes.Axes: The plot axis
|
204
|
+
"""
|
205
|
+
|
206
|
+
spike_times = data['spike_times']
|
207
|
+
spike_values = data['spike_values']
|
208
|
+
half_widths = data['half_widths']
|
209
|
+
amplitudes = data['amplitudes']
|
210
|
+
right_bases = data['right_bases']
|
211
|
+
left_bases = data['left_bases']
|
212
|
+
duration_ms = data['stimulus_duration']
|
213
|
+
|
214
|
+
n_spikes = len(spike_times)
|
215
|
+
|
216
|
+
if n_spikes == 0:
|
217
|
+
return
|
218
|
+
|
219
|
+
print(f"Detected {n_spikes} spikes")
|
220
|
+
print(f"Average spike half-width: {np.mean(half_widths):.2f} ms")
|
221
|
+
print(f"Average spike amplitude: {np.mean(amplitudes):.2f} mV")
|
222
|
+
print(f"Spike frequency: {n_spikes / duration_ms * 1000:.2f} Hz")
|
223
|
+
|
224
|
+
ax.plot(spike_times, spike_values, 'o', color='red')
|
225
|
+
ax.set_xlabel('Time (ms)')
|
226
|
+
ax.set_ylabel('Amplitude (mV)')
|
227
|
+
ax.set_title(f'Somatic spikes ({len(spike_times)} detected)')
|
228
|
+
|
229
|
+
if show_metrics:
|
230
|
+
for t, v, w, a, lb, rb in zip(spike_times, spike_values, half_widths, amplitudes, left_bases, right_bases):
|
231
|
+
# plot spike amplitude
|
232
|
+
ax.plot([t, t], [v, v - a], color='red', linestyle='--')
|
233
|
+
# plot spike width
|
234
|
+
ax.plot([t - 10*w/2, t + 10*w/2], [v - a/2, v - a/2], color='lawngreen', linestyle='--')
|
235
|
+
|
236
|
+
|
237
|
+
def calculate_fI_curve(model, duration=1000, min_amp=0, max_amp=1, n=5, **kwargs):
|
238
|
+
"""
|
239
|
+
Calculate the frequency-current (f-I) curve of the neuron model.
|
240
|
+
|
241
|
+
Parameters
|
242
|
+
----------
|
243
|
+
model : Model
|
244
|
+
The neuron model.
|
245
|
+
duration : int
|
246
|
+
Duration of the simulation in ms.
|
247
|
+
min_amp : float
|
248
|
+
Minimum amplitude of the current injection in nA.
|
249
|
+
max_amp : float
|
250
|
+
Maximum amplitude of the current injection in nA.
|
251
|
+
n : int
|
252
|
+
Number of amplitudes to test.
|
253
|
+
|
254
|
+
Returns
|
255
|
+
-------
|
256
|
+
dict
|
257
|
+
A dictionary containing the current amplitudes, firing rates, and voltages.
|
258
|
+
"""
|
259
|
+
|
260
|
+
seg = model.seg_tree.root
|
261
|
+
duration = duration
|
262
|
+
|
263
|
+
amps = np.round(np.linspace(min_amp, max_amp, n), 4)
|
264
|
+
iclamp = model.iclamps[seg]
|
265
|
+
rates = []
|
266
|
+
vs = {}
|
267
|
+
for amp in amps:
|
268
|
+
iclamp.amp = amp
|
269
|
+
model.simulator.run(duration)
|
270
|
+
spike_data = detect_somatic_spikes(model, **kwargs)
|
271
|
+
n_spikes = len(spike_data['spike_times'])
|
272
|
+
rate = n_spikes / iclamp.dur * 1000
|
273
|
+
rates.append(rate)
|
274
|
+
vs[amp] = model.simulator.vs[seg]
|
275
|
+
|
276
|
+
return {
|
277
|
+
'current_amplitudes': amps,
|
278
|
+
'firing_rates': rates,
|
279
|
+
'voltages': vs,
|
280
|
+
'time': model.simulator.t
|
281
|
+
}
|
282
|
+
|
283
|
+
|
284
|
+
def plot_fI_curve(data, ax=None, **kwargs):
|
285
|
+
|
286
|
+
if ax is None:
|
287
|
+
_, ax = plt.subplots(1, 2, figsize=(5, 5))
|
288
|
+
|
289
|
+
amps = data['current_amplitudes']
|
290
|
+
rates = data['firing_rates']
|
291
|
+
vs = data['voltages']
|
292
|
+
t = data['time']
|
293
|
+
|
294
|
+
for i, (amp, v) in enumerate(vs.items()):
|
295
|
+
ax[0].plot(t, np.array(v) - i*200, label=f'{amp} nA')
|
296
|
+
# ax[0].set_xlabel('Time (ms)')
|
297
|
+
# ax[0].set_ylabel('Voltage (mV)')
|
298
|
+
ax[0].set_title('Somatic spikes')
|
299
|
+
ax[0].legend()
|
300
|
+
ax[0].spines['top'].set_visible(False)
|
301
|
+
ax[0].spines['right'].set_visible(False)
|
302
|
+
ax[0].spines['bottom'].set_visible(False)
|
303
|
+
ax[0].spines['left'].set_visible(False)
|
304
|
+
ax[0].set_xticks([])
|
305
|
+
ax[0].set_yticks([])
|
306
|
+
|
307
|
+
ax[1].plot(amps, rates, color='gray', zorder=0)
|
308
|
+
for a, r in zip(amps, rates):
|
309
|
+
ax[1].scatter(a, r, s=50, edgecolor='white')
|
310
|
+
ax[1].set_xlabel('Current (nA)')
|
311
|
+
ax[1].set_ylabel('Firing rate (Hz)')
|
312
|
+
ax[1].set_title('f-I curve')
|
313
|
+
|
314
|
+
|
315
|
+
# =============================================================================
|
316
|
+
# DENDRITIC PROPERTIES
|
317
|
+
# =============================================================================
|
318
|
+
|
319
|
+
def calculate_voltage_attenuation(model):
|
320
|
+
"""
|
321
|
+
Calculate the voltage attenuation along the dendrites.
|
322
|
+
|
323
|
+
Parameters
|
324
|
+
----------
|
325
|
+
model : Model
|
326
|
+
The neuron model.
|
327
|
+
|
328
|
+
Returns
|
329
|
+
-------
|
330
|
+
dict
|
331
|
+
A dictionary containing the path distances, minimum voltages, and voltage attenuations
|
332
|
+
"""
|
333
|
+
|
334
|
+
# Assuming one stimulation site and multiple recording sites including the stimulated site
|
335
|
+
stimulated_segs = list(model.iclamps.keys())
|
336
|
+
if len(stimulated_segs) != 1:
|
337
|
+
print("Only one stimulation site is supported")
|
338
|
+
return None
|
339
|
+
recorded_segs = list(model.recordings.keys())
|
340
|
+
if len(recorded_segs) < 2:
|
341
|
+
print("At least two recording sites are required")
|
342
|
+
return None
|
343
|
+
|
344
|
+
stimulated_seg = stimulated_segs[0]
|
345
|
+
|
346
|
+
iclamp = model.iclamps[stimulated_seg]
|
347
|
+
amp = iclamp.amp
|
348
|
+
|
349
|
+
if amp >= 0:
|
350
|
+
print("Stimulus amplitude must be negative")
|
351
|
+
return None
|
352
|
+
|
353
|
+
path_distances = [seg.path_distance() for seg in recorded_segs]
|
354
|
+
|
355
|
+
start_ts = int(iclamp.delay / model.simulator.dt)
|
356
|
+
stop_ts = int((iclamp.delay + iclamp.dur) / model.simulator.dt)
|
357
|
+
|
358
|
+
voltage_at_stimulated = np.array(model.simulator.vs[stimulated_seg])[start_ts:stop_ts]
|
359
|
+
voltages = [np.array(model.simulator.vs[seg])[start_ts:stop_ts] for seg in recorded_segs]
|
360
|
+
|
361
|
+
# Calculate voltage displacement from the resting potential
|
362
|
+
delta_v_at_stimulated = voltage_at_stimulated[0] - np.min(voltage_at_stimulated)
|
363
|
+
delta_vs = [v[0] - np.min(v) for v in voltages]
|
364
|
+
|
365
|
+
min_voltages = [np.min(v) for v in voltages]
|
366
|
+
|
367
|
+
attenuation = [dv / delta_v_at_stimulated for dv in delta_vs]
|
368
|
+
|
369
|
+
return {
|
370
|
+
'path_distances': path_distances,
|
371
|
+
'min_voltages': min_voltages,
|
372
|
+
'attenuation': attenuation
|
373
|
+
}
|
374
|
+
|
375
|
+
|
376
|
+
def plot_voltage_attenuation(data, ax=None):
|
377
|
+
|
378
|
+
path_distances = data['path_distances']
|
379
|
+
attenuation = data['attenuation']
|
380
|
+
|
381
|
+
if ax is None:
|
382
|
+
_, ax = plt.subplots()
|
383
|
+
|
384
|
+
ax.plot(path_distances, attenuation, 'o-')
|
385
|
+
ax.set_ylim(-0.1, 1.1)
|
386
|
+
ax.set_xlabel('Path distance (um)')
|
387
|
+
ax.set_ylabel('Voltage attenuation')
|
388
|
+
ax.set_title('Voltage attenuation')
|
389
|
+
|
390
|
+
def calculate_dendritic_nonlinearity(model, duration=1000, max_weight=None, n=None):
|
391
|
+
"""Calculate the expected and observed voltage changes for a range of synaptic weights.
|
392
|
+
|
393
|
+
Parameters
|
394
|
+
----------
|
395
|
+
model : Model
|
396
|
+
The neuron model.
|
397
|
+
duration : int
|
398
|
+
Duration of the simulation in ms.
|
399
|
+
max_weight : int
|
400
|
+
Maximum synaptic weight to test.
|
401
|
+
|
402
|
+
Returns
|
403
|
+
-------
|
404
|
+
dict
|
405
|
+
A dictionary containing the expected and observed voltage changes.
|
406
|
+
"""
|
407
|
+
|
408
|
+
recorded_segs = list(model.recordings.keys())
|
409
|
+
seg = recorded_segs[0]
|
410
|
+
|
411
|
+
populations = [pop for pops in model.populations.values() for pop in pops.values()]
|
412
|
+
if len(populations) != 1:
|
413
|
+
print("Only one population is supported")
|
414
|
+
return None
|
415
|
+
population = populations[0]
|
416
|
+
if population.N != 1:
|
417
|
+
print("Only one synapse should be placed on the dendrite")
|
418
|
+
return None
|
419
|
+
|
420
|
+
start_ts = int(population.input_params['start'] / model.simulator.dt)
|
421
|
+
|
422
|
+
vs = {}
|
423
|
+
delta_vs = []
|
424
|
+
min_weight = 1
|
425
|
+
if max_weight is None or min_weight is None or n is None:
|
426
|
+
max_weight = population.input_params['weight']
|
427
|
+
n = max_weight + 1
|
428
|
+
|
429
|
+
weights = np.linspace(min_weight, max_weight, n, dtype=int)
|
430
|
+
weights = np.unique(weights)
|
431
|
+
|
432
|
+
for w in weights:
|
433
|
+
population.update_input_params(weight=w)
|
434
|
+
model.simulator.run(duration)
|
435
|
+
v = np.array(model.simulator.vs[seg])
|
436
|
+
v_start = v[start_ts]
|
437
|
+
v_max = np.max(v[start_ts:])
|
438
|
+
delta_v = v_max - v_start
|
439
|
+
delta_vs.append(delta_v)
|
440
|
+
vs[w] = v
|
441
|
+
unitary_delta_v = delta_vs[0]
|
442
|
+
expected_delta_vs = [w * unitary_delta_v for w in weights]
|
443
|
+
|
444
|
+
return {
|
445
|
+
'expected_response': expected_delta_vs,
|
446
|
+
'observed_response': delta_vs,
|
447
|
+
'voltages': vs,
|
448
|
+
'weights': weights,
|
449
|
+
'time': model.simulator.t
|
450
|
+
}
|
451
|
+
|
452
|
+
|
453
|
+
def plot_dendritic_nonlinearity(data, ax=None, **kwargs):
|
454
|
+
|
455
|
+
if ax is None:
|
456
|
+
_, ax = plt.subplots(1, 2, figsize=(10, 5))
|
457
|
+
|
458
|
+
expected_delta_vs = data['expected_response']
|
459
|
+
delta_vs = data['observed_response']
|
460
|
+
vs = data['voltages']
|
461
|
+
t = data['time']
|
462
|
+
|
463
|
+
for i, (weight, v) in enumerate(vs.items()):
|
464
|
+
ax[0].plot(t, np.array(v) - i*200, label=f'{weight} synapses')
|
465
|
+
ax[0].set_title('Voltage traces')
|
466
|
+
ax[0].legend()
|
467
|
+
ax[0].spines['top'].set_visible(False)
|
468
|
+
ax[0].spines['right'].set_visible(False)
|
469
|
+
ax[0].spines['bottom'].set_visible(False)
|
470
|
+
ax[0].spines['left'].set_visible(False)
|
471
|
+
ax[0].set_xticks([])
|
472
|
+
ax[0].set_yticks([])
|
473
|
+
|
474
|
+
ax[1].plot(expected_delta_vs, delta_vs, 'o-')
|
475
|
+
ax[1].plot(expected_delta_vs, expected_delta_vs, color='gray', linestyle='--')
|
476
|
+
ax[1].set_xlabel('Expected voltage change (mV)')
|
477
|
+
ax[1].set_ylabel('Observed voltage change (mV)')
|
478
|
+
ax[1].set_title('Dendritic nonlinearity')
|
479
|
+
|
480
|
+
|
481
|
+
|
482
|
+
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import pandas as pd
|
3
|
+
import matplotlib.pyplot as plt
|
4
|
+
|
5
|
+
def get_node_data(nodes):
|
6
|
+
|
7
|
+
data = {
|
8
|
+
'idx': [node.idx for node in nodes],
|
9
|
+
'depth': [node.depth for node in nodes],
|
10
|
+
'length': [node.length for node in nodes],
|
11
|
+
'diam': [node.diam for node in nodes],
|
12
|
+
'area': [node.area for node in nodes],
|
13
|
+
'n_children': [len(node.children) for node in nodes]
|
14
|
+
}
|
15
|
+
|
16
|
+
return pd.DataFrame(data)
|
17
|
+
|
18
|
+
|
19
|
+
def calculate_section_statistics(sections, param_name=None):
|
20
|
+
|
21
|
+
df = get_node_data(sections)
|
22
|
+
|
23
|
+
depth_counts = df['depth'].value_counts().sort_index()
|
24
|
+
stats = {
|
25
|
+
'N_sections': len(df),
|
26
|
+
'N_bifurcations': (df['n_children'] == 2).sum(),
|
27
|
+
'N_terminations': (df['n_children'] == 0).sum(),
|
28
|
+
'depth': {
|
29
|
+
'min': df['depth'].min(),
|
30
|
+
'max': df['depth'].max(),
|
31
|
+
'counts': depth_counts.to_dict(),
|
32
|
+
},
|
33
|
+
'diam': {
|
34
|
+
'min': np.round(df['diam'].min(), 2),
|
35
|
+
'max': np.round(df['diam'].max(), 2),
|
36
|
+
'mean': np.round(df['diam'].mean(), 2),
|
37
|
+
'std': np.round(df['diam'].std(), 2)
|
38
|
+
},
|
39
|
+
'length': {
|
40
|
+
'min': np.round(df['length'].min(), 2),
|
41
|
+
'max': np.round(df['length'].max(), 2),
|
42
|
+
'mean': np.round(df['length'].mean(), 2),
|
43
|
+
'std': np.round(df['length'].std(), 2)
|
44
|
+
},
|
45
|
+
'area': {
|
46
|
+
'min': np.round(df['area'].min(), 2),
|
47
|
+
'max': np.round(df['area'].max(), 2),
|
48
|
+
'mean': np.round(df['area'].mean(), 2),
|
49
|
+
'std': np.round(df['area'].std(), 2)
|
50
|
+
},
|
51
|
+
'total_length': np.round(df['length'].sum(), 2),
|
52
|
+
'total_area': np.round(df['area'].sum(), 2)
|
53
|
+
}
|
54
|
+
|
55
|
+
return stats
|
56
|
+
|
57
|
+
|
58
|
+
def calculate_cell_statistics(tree):
|
59
|
+
|
60
|
+
all_sections = []
|
61
|
+
for domain in tree.domains.values():
|
62
|
+
all_sections.extend(domain.sections)
|
63
|
+
|
64
|
+
return calculate_section_statistics(all_sections)
|
65
|
+
|
66
|
+
|
67
|
+
def calculate_domain_statistics(tree, domain_names=None, param_name=None):
|
68
|
+
|
69
|
+
if domain_names is None:
|
70
|
+
return calculate_cell_statistics(tree)
|
71
|
+
if not isinstance(domain_names, list):
|
72
|
+
raise ValueError("domain_names must be a list of strings")
|
73
|
+
|
74
|
+
domains = [domain for domain in tree.domains.values() if domain.name in domain_names]
|
75
|
+
|
76
|
+
stats = {}
|
77
|
+
|
78
|
+
for domain in domains:
|
79
|
+
stats[domain.name] = calculate_section_statistics(domain.sections)
|
80
|
+
|
81
|
+
return stats
|
82
|
+
|
83
|
+
|
84
|
+
def calculate_segment_statistics(model, segments):
|
85
|
+
|
86
|
+
df = model.get_node_data(segments)
|
87
|
+
|
88
|
+
stats = {
|
89
|
+
'N_segments': len(df),
|
90
|
+
'N_bifurcations': (df['n_children'] == 2).sum(),
|
91
|
+
'N_terminations': (df['n_children'] == 0).sum(),
|
92
|
+
'diam': (np.round(df['diam'].mean(), 2), np.round(df['diam'].std(), 2), np.round(df['diam'].min(), 2), np.round(df['diam'].max(), 2)),
|
93
|
+
'length': (np.round(df['length'].mean(), 2), np.round(df['length'].std(), 2), np.round(df['length'].min(), 2), np.round(df['length'].max(), 2)),
|
94
|
+
'area': (np.round(df['area'].mean(), 2), np.round(df['area'].std(), 2), np.round(df['area'].min(), 2), np.round(df['area'].max(), 2)),
|
95
|
+
'total_lenght': np.round(df['length'].sum(), 2),
|
96
|
+
'total_area': np.round(df['area'].sum(), 2)
|
97
|
+
}
|
98
|
+
|
99
|
+
|
100
|
+
def update_histogram(model, param_name, segments, **kwargs):
|
101
|
+
if param not in ['diam', 'length', 'area']:
|
102
|
+
raise ValueError(f"Invalid parameter: {param}")
|
103
|
+
values = [seg.get_param_value(param_name) for seg in segments]
|
104
|
+
hist, edges = np.histogram(values, **kwargs)
|
105
|
+
return hist, edges
|
106
|
+
|
@@ -0,0 +1,6 @@
|
|
1
|
+
from dendrotweaks.membrane.mechanisms import Mechanism, IonChannel, CaDynamics, StandardIonChannel, LeakChannel
|
2
|
+
from dendrotweaks.membrane.mechanisms import LeakChannel
|
3
|
+
from dendrotweaks.membrane.groups import SegmentGroup
|
4
|
+
from dendrotweaks.membrane.distributions import Distribution
|
5
|
+
|
6
|
+
import dendrotweaks.membrane.io as io
|
@@ -0,0 +1,65 @@
|
|
1
|
+
TITLE AMPA synapse
|
2
|
+
|
3
|
+
COMMENT
|
4
|
+
Custom AMPA synapse model developed for DendroTweaks
|
5
|
+
ENDCOMMENT
|
6
|
+
|
7
|
+
NEURON {
|
8
|
+
POINT_PROCESS AMPA
|
9
|
+
NONSPECIFIC_CURRENT i
|
10
|
+
RANGE gmax
|
11
|
+
RANGE tau_rise, tau_decay
|
12
|
+
RANGE i, g, e
|
13
|
+
}
|
14
|
+
|
15
|
+
UNITS {
|
16
|
+
(nA) = (nanoamp)
|
17
|
+
(mV) = (millivolt)
|
18
|
+
(uS) = (microsiemens)
|
19
|
+
}
|
20
|
+
|
21
|
+
PARAMETER {
|
22
|
+
gmax = 0 (uS)
|
23
|
+
e = 0 (mV)
|
24
|
+
tau_rise = 0.1 (ms)
|
25
|
+
tau_decay = 2.5 (ms)
|
26
|
+
}
|
27
|
+
|
28
|
+
ASSIGNED {
|
29
|
+
v (mV)
|
30
|
+
i (nA)
|
31
|
+
g (uS)
|
32
|
+
factor (1)
|
33
|
+
}
|
34
|
+
|
35
|
+
STATE {
|
36
|
+
A (1)
|
37
|
+
B (1)
|
38
|
+
}
|
39
|
+
|
40
|
+
BREAKPOINT {
|
41
|
+
SOLVE state METHOD cnexp
|
42
|
+
g = gmax * (B - A)
|
43
|
+
i = g * (v - e)
|
44
|
+
}
|
45
|
+
|
46
|
+
DERIVATIVE state {
|
47
|
+
A' = -A / tau_rise
|
48
|
+
B' = -B / tau_decay
|
49
|
+
}
|
50
|
+
|
51
|
+
INITIAL {
|
52
|
+
LOCAL tp
|
53
|
+
A = 0
|
54
|
+
B = 0
|
55
|
+
|
56
|
+
tp = (tau_rise * tau_decay) / (tau_decay - tau_rise) * log(tau_decay / tau_rise)
|
57
|
+
factor = -exp(-tp / tau_rise) + exp(-tp / tau_decay)
|
58
|
+
factor = 1 / factor
|
59
|
+
|
60
|
+
}
|
61
|
+
|
62
|
+
NET_RECEIVE(weight (1)) {
|
63
|
+
A = A + weight * factor
|
64
|
+
B = B + weight * factor
|
65
|
+
}
|