dendrotweaks 0.4.4__py3-none-any.whl → 0.4.6__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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.4.4"
1
+ __version__ = "0.4.6"
2
2
 
3
3
  from dendrotweaks.model import Model
4
4
  from dendrotweaks.simulators import NeuronSimulator
@@ -8,4 +8,5 @@ from dendrotweaks.analysis.ephys_analysis import plot_passive_properties
8
8
  from dendrotweaks.analysis.ephys_analysis import calculate_voltage_attenuation
9
9
  from dendrotweaks.analysis.ephys_analysis import plot_voltage_attenuation
10
10
  from dendrotweaks.analysis.ephys_analysis import calculate_dendritic_nonlinearity
11
- from dendrotweaks.analysis.ephys_analysis import plot_dendritic_nonlinearity
11
+ from dendrotweaks.analysis.ephys_analysis import plot_dendritic_nonlinearity
12
+ from dendrotweaks.analysis.ephys_analysis import calculate_sag_ratio
@@ -38,6 +38,11 @@ def get_somatic_data(model):
38
38
  def calculate_input_resistance(model):
39
39
  """
40
40
  Calculate the input resistance of the neuron model.
41
+
42
+ This function determines the input resistance by calculating the ratio
43
+ between the voltage change and the injected current. The voltage change
44
+ is measured as the difference between the membrane potential at the onset
45
+ and offset of the current injection.
41
46
 
42
47
  Parameters
43
48
  ----------
@@ -52,13 +57,11 @@ def calculate_input_resistance(model):
52
57
 
53
58
  v, t, dt, iclamp = get_somatic_data(model)
54
59
 
55
- v_min = np.min(v)
56
-
57
60
  amp = iclamp.amp
58
- start_ts = iclamp.delay / dt
59
- end_ts = int((iclamp.delay + iclamp.dur) / dt)
61
+ start_ts = int(iclamp.delay / dt)
62
+ stop_ts = int((iclamp.delay + iclamp.dur) / dt)
60
63
  v_onset = v[int(start_ts)]
61
- v_offset = v[int(end_ts)]
64
+ v_offset = v[int(stop_ts)]
62
65
 
63
66
  R_in = (v_onset - v_offset) / amp
64
67
  print(f"Input resistance: {R_in:.2f} MOhm")
@@ -71,44 +74,61 @@ def calculate_input_resistance(model):
71
74
  }
72
75
 
73
76
 
74
- def _exp_decay(t, A, tau):
75
- return A * np.exp(-t / tau)
76
-
77
+ def _double_exp_decay(t, A1, tau1, A2, tau2):
78
+ return A1 * np.exp(-t / tau1) + A2 * np.exp(-t / tau2)
77
79
 
78
80
  def calculate_time_constant(model):
79
81
  """
80
- Calculate the membrane time constant of the neuron model.
82
+ Estimate the passive membrane time constant (τm) by fitting
83
+ a double exponential to the somatic voltage decay and selecting
84
+ the slowest τ component.
81
85
 
82
86
  Parameters
83
87
  ----------
84
88
  model : Model
85
- The neuron model.
89
+ The neuron model (assumes Ih is disabled).
86
90
 
87
91
  Returns
88
92
  -------
89
93
  dict
90
- A dictionary containing the time constant and the exponential fit.
94
+ A dictionary with τm (slowest), both τs, and fit details.
91
95
  """
92
96
  v, t, dt, iclamp = get_somatic_data(model)
93
97
 
94
98
  start_ts = int(iclamp.delay / dt)
95
99
  stop_ts = int((iclamp.delay + iclamp.dur) / dt)
96
100
  min_ts = np.argmin(v[start_ts:stop_ts]) + start_ts
97
- v_min = np.min(v[start_ts: min_ts])
101
+ v_min = v[min_ts]
98
102
  v_decay = v[start_ts: min_ts] - v_min
99
103
  t_decay = t[start_ts: min_ts] - t[start_ts]
100
- popt, _ = curve_fit(_exp_decay, t_decay, v_decay, p0=[1, 100])
101
- tau = popt[1]
102
- A = popt[0]
103
- print(f"Membrane time constant: {tau:.2f} ms")
104
+
105
+ # Fit double exponential
106
+ try:
107
+ popt, _ = curve_fit(
108
+ _double_exp_decay, t_decay, v_decay,
109
+ p0=[1, 10, 0.5, 100],
110
+ bounds=(0, [np.inf, 1000, np.inf, 1000])
111
+ )
112
+ A1, tau1, A2, tau2 = popt
113
+ tau_slowest = max(tau1, tau2)
114
+ except RuntimeError:
115
+ print("Fit failed. Could not estimate time constant.")
116
+ return None
117
+
118
+ print(f"Time constant {tau_slowest:.2f} ms. Estimated from double exp fit (slowest component)")
119
+
104
120
  return {
105
- 'time_constant': tau,
106
- 'A': A,
121
+ 'time_constant': tau_slowest,
107
122
  'start_time': start_ts * dt,
123
+ 'tau1': tau1,
124
+ 'tau2': tau2,
125
+ 'A1': A1,
126
+ 'A2': A2,
108
127
  'decay_time': t_decay,
109
128
  'decay_voltage': v_decay
110
129
  }
111
130
 
131
+
112
132
  def calculate_passive_properties(model):
113
133
  """
114
134
  Calculate the passive properties of the neuron model.
@@ -140,7 +160,6 @@ def plot_passive_properties(data, ax=None):
140
160
  v_offset = data['offset_voltage']
141
161
  t_decay = data['decay_time']
142
162
  v_decay = data['decay_voltage']
143
- A = data['A']
144
163
  start_t = data['start_time']
145
164
 
146
165
  ax.set_title(f"R_in: {R_in:.2f} MOhm, Tau: {tau:.2f} ms")
@@ -148,8 +167,10 @@ def plot_passive_properties(data, ax=None):
148
167
  ax.axhline(v_offset, color='gray', linestyle='--', label='V offset')
149
168
 
150
169
  # Shift the exp_decay output along the y-axis
151
- shifted_exp_decay = _exp_decay(t_decay, A, tau) + v_offset
152
- ax.plot(t_decay + start_t, shifted_exp_decay, color='red', label='Exp. fit', linestyle='--')
170
+ fit_curve = _double_exp_decay(t_decay, data['A1'], data['tau1'], data['A2'], data['tau2']) + v_offset
171
+ label = f'Double exp fit (tau1 = {data["tau1"]:.1f} ms, tau2 = {data["tau2"]:.1f} ms)'
172
+
173
+ ax.plot(t_decay + start_t, fit_curve, color='red', label='Exp. fit', linestyle='--')
153
174
  ax.legend()
154
175
 
155
176
 
@@ -236,7 +257,7 @@ def plot_somatic_spikes(data, ax=None, show_metrics=False):
236
257
  ax.plot([t - 10*w/2, t + 10*w/2], [v - a/2, v - a/2], color='lawngreen', linestyle='--')
237
258
 
238
259
 
239
- def calculate_fI_curve(model, duration=1000, min_amp=0, max_amp=1, n=5, **kwargs):
260
+ def calculate_fI_curve(model, duration=1000, prerun_time=0, min_amp=0, max_amp=1, n=5, **kwargs):
240
261
  """
241
262
  Calculate the frequency-current (f-I) curve of the neuron model.
242
263
 
@@ -268,7 +289,7 @@ def calculate_fI_curve(model, duration=1000, min_amp=0, max_amp=1, n=5, **kwargs
268
289
  vs = {}
269
290
  for amp in amps:
270
291
  iclamp.amp = amp
271
- model.simulator.run(duration)
292
+ model.run(duration=duration, prerun_time=prerun_time)
272
293
  spike_data = detect_somatic_spikes(model, **kwargs)
273
294
  n_spikes = len(spike_data['spike_times'])
274
295
  rate = n_spikes / iclamp.dur * 1000
@@ -283,7 +304,22 @@ def calculate_fI_curve(model, duration=1000, min_amp=0, max_amp=1, n=5, **kwargs
283
304
  }
284
305
 
285
306
 
286
- def plot_fI_curve(data, ax=None, **kwargs):
307
+ def plot_fI_curve(data, ax=None, vshift=200, **kwargs):
308
+ """
309
+ Plot the f-I curve and somatic voltage traces.
310
+
311
+ Parameters
312
+ ----------
313
+ data : dict
314
+ A dictionary containing the current amplitudes, firing rates, and voltages.
315
+ Can be obtained from `calculate_fI_curve`.
316
+ ax : matplotlib.axes.Axes, optional
317
+ The axes to plot on. If two axes are provided, the first will show the somatic voltage traces and the second will show the f-I curve.
318
+ If a single axis is provided, it will show the f-I curve only.
319
+ If None, a new figure will be created.
320
+ vshift : int, optional
321
+ The vertical shift for the somatic voltage traces. Default is 200.
322
+ """
287
323
 
288
324
  if ax is None:
289
325
  _, ax = plt.subplots(1, 2, figsize=(5, 5))
@@ -293,25 +329,20 @@ def plot_fI_curve(data, ax=None, **kwargs):
293
329
  vs = data['voltages']
294
330
  t = data['time']
295
331
 
296
- for i, (amp, v) in enumerate(vs.items()):
297
- ax[0].plot(t, np.array(v) - i*200, label=f'{amp} nA')
298
- # ax[0].set_xlabel('Time (ms)')
299
- # ax[0].set_ylabel('Voltage (mV)')
300
- ax[0].set_title('Somatic spikes')
301
- ax[0].legend()
302
- ax[0].spines['top'].set_visible(False)
303
- ax[0].spines['right'].set_visible(False)
304
- ax[0].spines['bottom'].set_visible(False)
305
- ax[0].spines['left'].set_visible(False)
306
- ax[0].set_xticks([])
307
- ax[0].set_yticks([])
332
+ if isinstance(ax, (list, np.ndarray)):
333
+ for i, (amp, v) in enumerate(vs.items()):
334
+ ax[0].plot(t, np.array(v) - i*vshift, label=f'{amp} nA')
335
+ ax[0].set_title('Somatic spikes')
336
+ ax[0].legend()
337
+ ax[0].axis('off')
338
+ ax = ax[1]
308
339
 
309
- ax[1].plot(amps, rates, color='gray', zorder=0)
340
+ ax.plot(amps, rates, color='gray', zorder=0)
310
341
  for a, r in zip(amps, rates):
311
- ax[1].scatter(a, r, s=50, edgecolor='white')
312
- ax[1].set_xlabel('Current (nA)')
313
- ax[1].set_ylabel('Firing rate (Hz)')
314
- ax[1].set_title('f-I curve')
342
+ ax.plot(a, r, 'o', zorder=1)
343
+ ax.set_xlabel('Current (nA)')
344
+ ax.set_ylabel('Firing rate (Hz)')
345
+ ax.set_title('f-I curve')
315
346
 
316
347
 
317
348
  # =============================================================================
@@ -365,16 +396,18 @@ def calculate_voltage_attenuation(model):
365
396
 
366
397
 
367
398
  # Calculate voltage displacement from the resting potential
368
- delta_v_at_stimulated = voltage_at_stimulated[0] - np.min(voltage_at_stimulated)
369
- delta_vs = [v[0] - np.min(v) for v in voltages]
399
+ delta_v_at_stimulated = voltage_at_stimulated[0] - voltage_at_stimulated[-2]# np.min(voltage_at_stimulated)
400
+ delta_vs = [v[0] - v[-2] for v in voltages] # np.min(v) for v in voltages]
370
401
 
371
402
  min_voltages = [np.min(v) for v in voltages]
403
+ end_voltages = [v[-2] for v in voltages]
372
404
 
373
405
  attenuation = [dv / delta_v_at_stimulated for dv in delta_vs]
374
406
 
375
407
  return {
376
408
  'path_distances': path_distances,
377
409
  'min_voltages': min_voltages,
410
+ 'end_voltages': end_voltages,
378
411
  'attenuation': attenuation
379
412
  }
380
413
 
@@ -393,7 +426,7 @@ def plot_voltage_attenuation(data, ax=None):
393
426
  ax.set_ylabel('Voltage attenuation')
394
427
  ax.set_title('Voltage attenuation')
395
428
 
396
- def calculate_dendritic_nonlinearity(model, duration=1000, max_weight=None, n=None):
429
+ def calculate_dendritic_nonlinearity(model, duration=1000, prerun_time=0, max_weight=None, n=None):
397
430
  """Calculate the expected and observed voltage changes for a range of synaptic weights.
398
431
 
399
432
  Parameters
@@ -437,7 +470,7 @@ def calculate_dendritic_nonlinearity(model, duration=1000, max_weight=None, n=No
437
470
 
438
471
  for w in weights:
439
472
  population.update_input_params(weight=w)
440
- model.simulator.run(duration)
473
+ model.run(duration=duration, prerun_time=prerun_time)
441
474
  v = np.array(model.simulator.recordings['v'][seg])
442
475
  v_start = v[start_ts]
443
476
  v_max = np.max(v[start_ts:])
@@ -456,7 +489,22 @@ def calculate_dendritic_nonlinearity(model, duration=1000, max_weight=None, n=No
456
489
  }
457
490
 
458
491
 
459
- def plot_dendritic_nonlinearity(data, ax=None, **kwargs):
492
+ def plot_dendritic_nonlinearity(data, ax=None, vshift=200, **kwargs):
493
+ """
494
+ Plot the dendritic nonlinearity based on expected and observed voltage changes.
495
+
496
+ Parameters
497
+ ----------
498
+ data : dict
499
+ A dictionary containing the expected and observed voltage changes.
500
+ Can be obtained from `calculate_dendritic_nonlinearity`.
501
+ ax : matplotlib.axes.Axes, optional
502
+ The axes to plot on. If two axes are provided, the first will show the voltage traces and the second will show the dendritic nonlinearity.
503
+ If a single axis is provided, it will show the dendritic nonlinearity only.
504
+ If None, a new figure will be created.
505
+ vshift : int, optional
506
+ The vertical shift for the voltage traces.
507
+ """
460
508
 
461
509
  if ax is None:
462
510
  _, ax = plt.subplots(1, 2, figsize=(10, 5))
@@ -466,23 +514,53 @@ def plot_dendritic_nonlinearity(data, ax=None, **kwargs):
466
514
  vs = data['voltages']
467
515
  t = data['time']
468
516
 
469
- for i, (weight, v) in enumerate(vs.items()):
470
- ax[0].plot(t, np.array(v) - i*200, label=f'{weight} synapses')
471
- ax[0].set_title('Voltage traces')
472
- ax[0].legend()
473
- ax[0].spines['top'].set_visible(False)
474
- ax[0].spines['right'].set_visible(False)
475
- ax[0].spines['bottom'].set_visible(False)
476
- ax[0].spines['left'].set_visible(False)
477
- ax[0].set_xticks([])
478
- ax[0].set_yticks([])
479
-
480
- ax[1].plot(expected_delta_vs, delta_vs, 'o-')
481
- ax[1].plot(expected_delta_vs, expected_delta_vs, color='gray', linestyle='--')
482
- ax[1].set_xlabel('Expected voltage change (mV)')
483
- ax[1].set_ylabel('Observed voltage change (mV)')
484
- ax[1].set_title('Dendritic nonlinearity')
517
+ if isinstance(ax, (list, np.ndarray)):
518
+ for i, (weight, v) in enumerate(vs.items()):
519
+ ax[0].plot(t, np.array(v) - i*vshift, label=f'{weight} synapses')
520
+ ax[0].set_title('Voltage traces')
521
+ ax[0].legend()
522
+ ax[0].axis('off')
523
+ ax = ax[1]
524
+
525
+ ax.plot(expected_delta_vs, delta_vs, zorder=1)
526
+ ax.plot(expected_delta_vs, expected_delta_vs, color='gray', linestyle='--', zorder=0)
527
+ for ep, ob in zip(expected_delta_vs, delta_vs):
528
+ ax.plot(ep, ob, 'o', zorder=2)
529
+ ax.set_xlabel('Expected voltage change (mV)')
530
+ ax.set_ylabel('Observed voltage change (mV)')
531
+ ax.set_title('Dendritic nonlinearity')
485
532
 
486
533
 
534
+ def calculate_sag_ratio(model):
535
+ """
536
+ Calculate the sag ratio of the neuron model.
537
+
538
+ Parameters
539
+ ----------
540
+ model : Model
541
+ The neuron model.
487
542
 
543
+ Returns
544
+ -------
545
+ dict
546
+ A dictionary containing the sag ratio and intermediate values.
547
+ """
548
+ v, t, dt, iclamp = get_somatic_data(model)
549
+
550
+ start_ts = int(iclamp.delay / dt)
551
+ stop_ts = int((iclamp.delay + iclamp.dur) / dt)
552
+ min_ts = np.argmin(v[start_ts:stop_ts]) + start_ts
553
+ v_min = np.min(v[start_ts: min_ts])
554
+
555
+ a = v[stop_ts] - v_min
556
+ b = v[start_ts] - v_min
557
+
558
+ sag_ratio = a / b if b != 0 else np.nan
559
+
560
+ print(f"Sag ratio: {a:.2f}/{b:.2f} = {sag_ratio:.2f}")
561
+ return {
562
+ 'a': a,
563
+ 'b': b,
564
+ 'sag_ratio': sag_ratio,
565
+ }
488
566
 
@@ -1,6 +1,7 @@
1
1
  : Vector stream of events
2
2
 
3
3
  NEURON {
4
+ THREADSAFE
4
5
  ARTIFICIAL_CELL VecStim
5
6
  RANGE delay
6
7
  }
@@ -12,13 +13,8 @@ ASSIGNED {
12
13
  delay
13
14
  }
14
15
 
15
- PARAMETER {
16
- :delay = 0.0
17
- }
18
-
19
16
  INITIAL {
20
17
  index = 0
21
- :delay = 0
22
18
  element()
23
19
  if (index > 0) {
24
20
  net_send(delay + etime - t, 1)
@@ -35,12 +31,6 @@ NET_RECEIVE (w) {
35
31
  }
36
32
  }
37
33
 
38
- VERBATIM
39
- extern double* vector_vec();
40
- extern int vector_capacity();
41
- extern void* vector_arg();
42
- ENDVERBATIM
43
-
44
34
  PROCEDURE element() {
45
35
  VERBATIM
46
36
  { void* vv; int i, size; double* px;
@@ -0,0 +1,131 @@
1
+ # This Python channel class was automatically generated from a MOD file
2
+ # using DendroTweaks toolbox, dendrotweaks.dendrites.gr
3
+
4
+
5
+ from jaxley.channels import Channel
6
+ from jaxley.solver_gate import exponential_euler
7
+ import jax.numpy as np
8
+
9
+ class {{ class_name }}(Channel):
10
+ """
11
+ {{ title }}
12
+ """
13
+
14
+ def __init__(self, name="{{ class_name }}"):
15
+ self.current_is_in_mA_per_cm2 = True
16
+ super().__init__(name=name)
17
+ self.channel_params = {
18
+ {% for param, value in channel_params.items() -%}
19
+ "{{ param }}_{{ class_name }}": {{ value }}
20
+ {%- if not loop.last -%},
21
+ {%- endif %}
22
+ {% endfor -%}
23
+ }
24
+ self.channel_states = {
25
+ {% for state in state_vars -%}
26
+ "{{ state }}_{{class_name}}": 0.0
27
+ {%- if not loop.last %},
28
+ {%- endif %}
29
+ {% endfor -%}
30
+ }
31
+ self._state_powers = {
32
+ {% for state, power in state_vars.items() -%}
33
+ "{{ state }}_{{class_name}}": {{ power }}
34
+ {%- if not loop.last %},
35
+ {%- endif %}
36
+ {% endfor -%}
37
+ }
38
+ self.ion = "{{ ion }}"
39
+ self.current_name = "i_{{ ion }}"
40
+
41
+ self.independent_var_name = "{{ independent_var_name }}"
42
+ self.tadj = 1
43
+
44
+ def set_tadj(self, temperature):
45
+ """
46
+ Set the temperature adjustment factor for the channel kinetics.
47
+
48
+ Parameters
49
+ ----------
50
+ temperature : float
51
+ The temperature in degrees Celsius.
52
+
53
+ Notes
54
+ -----
55
+ The temperature adjustment factor is calculated as:
56
+ tadj = q10 ** ((temperature - reference_temp) / 10)
57
+ where q10 is the temperature coefficient and reference_temp is the
58
+ temperature at which the channel kinetics were measured.
59
+ """
60
+ q10 = self.channel_params.get(f"q10_{{ class_name }}")
61
+ reference_temp = self.channel_params.get(f"temp_{{ class_name }}")
62
+ if q10 is None or reference_temp is None:
63
+ self.tadj = 1
64
+ print(f"Warning: q10 or reference temperature not set for {self.name}. Using default tadj = 1.")
65
+ else:
66
+ self.tadj = q10 ** ((temperature - reference_temp) / 10)
67
+
68
+ def __getitem__(self, item):
69
+ return self.channel_params[item]
70
+
71
+ def __setitem__(self, item, value):
72
+ self.channel_params[item] = value
73
+
74
+ {% for function in functions %}
75
+ {{ function['signature'] }}
76
+ {%- for param in function['params'] -%}
77
+ {{ param }} = self.channel_params.get("{{ param }}_{{ class_name }}", 1)
78
+ {% endfor %}
79
+ {{ function['body'] }}
80
+ {% if not loop.last %}
81
+ {% endif %}{% endfor -%}
82
+ {% for procedure in procedures %}
83
+ {{ procedure['signature'] }}
84
+ {% for param in procedure['params'] -%}
85
+ {{ param }} = self.channel_params.get("{{ param }}_{{ class_name }}", 1)
86
+ {% endfor %}
87
+ {{ procedure['body'] }}
88
+ {%- if not loop.last %}
89
+ {% endif %}{% endfor %}
90
+
91
+ def update_states(self, states, dt, v, params):
92
+ {% for state, state_params in state_vars.items() -%}
93
+ {{state}} = states['{{ state }}_{{class_name}}']
94
+ {%- if not loop.last %}
95
+ {%- endif %}
96
+ {% endfor -%}
97
+ {{- procedure_calls}}
98
+ {% for state in state_vars.keys() %}new_{{state}} = exponential_euler({{state}}, dt, {{state}}Inf, {{state}}Tau){% if not loop.last %}
99
+ {% endif %}{% endfor %}
100
+ return {
101
+ {% for state in state_vars -%}
102
+ "{{ state }}_{{class_name}}": new_{{state}}
103
+ {%- if not loop.last %},
104
+ {%- endif %}
105
+ {% endfor -%}
106
+ }
107
+
108
+ def compute_current(self, states, v, params):
109
+ {% for state in state_vars.keys() -%}
110
+ {{state}} = states['{{ state }}_{{class_name}}']
111
+ {%- if not loop.last %}
112
+ {%- endif %}
113
+ {% endfor -%}
114
+ gbar = params["gbar_{{class_name}}"]
115
+ # E = params["E_{{ ion }}"]
116
+ E = {{ E_ion }}
117
+ {{ procedure_calls}}
118
+ g = self.tadj * gbar *{% for state, power in state_vars.items()%} {{state}}**{{power['power']}} {% if not loop.last %}*{% endif %}{% endfor %}
119
+ return g * (v - E)
120
+
121
+ def init_state(self, states, v, params, delta_t):
122
+ {{ procedure_calls}}
123
+ return {
124
+ {% for state in state_vars.keys() -%}
125
+ "{{ state }}_{{class_name}}": {{state}}Inf
126
+ {%- if not loop.last %},
127
+ {%- endif %}
128
+ {% endfor -%}
129
+ }
130
+
131
+
@@ -131,16 +131,16 @@ def gaussian(distance: float, amplitude: float, mean: float, std: float) -> floa
131
131
  return amplitude * exp(-((distance - mean) ** 2) / (2 * std ** 2))
132
132
 
133
133
 
134
- def step(distance: float, max_value: float, min_value: float, start: float, end: float) -> float:
134
+ def step(distance: float, start: float, end: float, min_value: float, max_value: float) -> float:
135
135
  """
136
136
  Step distribution function.
137
137
 
138
138
  Args:
139
139
  distance (float): The distance parameter.
140
- min_value (float): The minimum value parameter.
141
- max_value (float): The maximum value parameter.
142
140
  start (float): The start parameter.
143
141
  end (float): The end parameter.
142
+ min_value (float): The minimum value parameter.
143
+ max_value (float): The maximum value parameter.
144
144
 
145
145
  Returns:
146
146
  The result of the step equation: min_value if distance < start, max_value if distance > end, and a linear interpolation between min_value and max_value if start <= distance <= end.
@@ -94,6 +94,10 @@ class MODFileConverter():
94
94
  self.reader.read_file(path_to_mod_file)
95
95
  self.reader.preprocess()
96
96
  blocks = self.reader.get_blocks(verbose)
97
+ if blocks.get('KINETIC'):
98
+ raise NotImplementedError(
99
+ "Conversion aborted: MOD files containing KINETIC blocks are not supported by DendroTweaks."
100
+ )
97
101
 
98
102
  if verbose: print(f"\nPARSING")
99
103
  self.parser.parse(blocks, verbose)
@@ -575,4 +575,14 @@ class CaDynamics(Mechanism):
575
575
  'gamma': 0.05,
576
576
  'kt': 0.0,
577
577
  'kd': 0.0
578
- }
578
+ }
579
+
580
+
581
+ class FallbackChannel(IonChannel):
582
+ """
583
+ Fallback channel class in case of import failure.
584
+ """
585
+ def __init__(self, name):
586
+ super().__init__(name=name)
587
+ self.params = {'gbar': 0.0}
588
+ self.range_params = {'gbar': 0.0}