bmtool 0.5.3__py3-none-any.whl → 0.5.5__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.
bmtool/synapses.py ADDED
@@ -0,0 +1,638 @@
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ import neuron
5
+ from neuron import h
6
+ from neuron.units import ms, mV
7
+ import matplotlib.pyplot as plt
8
+ from scipy.signal import find_peaks
9
+ from scipy.optimize import curve_fit
10
+
11
+ import ipywidgets as widgets
12
+ from IPython.display import display, clear_output
13
+ from ipywidgets import HBox, VBox
14
+
15
+ class SynapseTuner:
16
+ def __init__(self, mechanisms_dir: str, templates_dir: str, conn_type_settings: dict, connection: str,
17
+ general_settings: dict, json_folder_path: str = None, current_name: str = 'i',
18
+ other_vars_to_record: list = None, slider_vars: list = None) -> None:
19
+ """
20
+ Initialize the SynapseModule class with connection type settings, mechanisms, and template directories.
21
+
22
+ Parameters:
23
+ -----------
24
+ mechanisms_dir : str
25
+ Directory path containing the compiled mod files needed for NEURON mechanisms.
26
+ templates_dir : str
27
+ Directory path containing cell template files (.hoc or .py) loaded into NEURON.
28
+ conn_type_settings : dict
29
+ A dictionary containing connection-specific settings, such as synaptic properties and details.
30
+ connection : str
31
+ Name of the connection type to be used from the conn_type_settings dictionary.
32
+ general_settings : dict
33
+ General settings dictionary including parameters like simulation time step, duration, and temperature.
34
+ json_folder_path : str, optional
35
+ Path to folder containing JSON files with additional synaptic properties to update settings.
36
+ current_name : str, optional
37
+ Name of the synaptic current variable to be recorded (default is 'i').
38
+ other_vars_to_record : list, optional
39
+ List of additional synaptic variables to record during the simulation (e.g., 'Pr', 'Use').
40
+ slider_vars : list, optional
41
+ List of synaptic variables you would like sliders set up for the STP sliders method by default will use all parameters in spec_syn_param.
42
+
43
+ """
44
+ neuron.load_mechanisms(mechanisms_dir)
45
+ h.load_file(templates_dir)
46
+ self.conn_type_settings = conn_type_settings
47
+ if json_folder_path:
48
+ print(f"updating settings from json path {json_folder_path}")
49
+ self.update_spec_syn_param(json_folder_path)
50
+ self.general_settings = general_settings
51
+ self.conn = self.conn_type_settings[connection]
52
+ self.synaptic_props = self.conn['spec_syn_param']
53
+ self.vclamp = general_settings['vclamp']
54
+ self.current_name = current_name
55
+ self.other_vars_to_record = other_vars_to_record
56
+
57
+ if slider_vars:
58
+ self.slider_vars = {key: value for key, value in self.synaptic_props.items() if key in slider_vars} # filters dict to have only the entries that have a key in the sliders var
59
+ else:
60
+ self.slider_vars = self.synaptic_props
61
+
62
+ h.tstop = general_settings['tstart'] + general_settings['tdur']
63
+ h.dt = general_settings['dt'] # Time step (resolution) of the simulation in ms
64
+ h.steps_per_ms = 1 / h.dt
65
+ h.celsius = general_settings['celsius']
66
+
67
+ def update_spec_syn_param(self, json_folder_path):
68
+ """
69
+ Update specific synaptic parameters using JSON files located in the specified folder.
70
+
71
+ Parameters:
72
+ -----------
73
+ json_folder_path : str
74
+ Path to folder containing JSON files, where each JSON file corresponds to a connection type.
75
+ """
76
+ for conn_type, settings in self.conn_type_settings.items():
77
+ json_file_path = os.path.join(json_folder_path, f"{conn_type}.json")
78
+ if os.path.exists(json_file_path):
79
+ with open(json_file_path, 'r') as json_file:
80
+ json_data = json.load(json_file)
81
+ settings['spec_syn_param'].update(json_data)
82
+ else:
83
+ print(f"JSON file for {conn_type} not found.")
84
+
85
+
86
+ def set_up_cell(self):
87
+ """
88
+ Set up the neuron cell based on the specified connection settings.
89
+ """
90
+ self.cell = getattr(h, self.conn['spec_settings']['post_cell'])()
91
+
92
+
93
+ def set_up_synapse(self):
94
+ """
95
+ Set up the synapse on the target cell according to the synaptic parameters in `conn_type_settings`.
96
+
97
+ Notes:
98
+ ------
99
+ - `set_up_cell()` should be called before setting up the synapse.
100
+ - Synapse location, type, and properties are specified within `spec_syn_param` and `spec_settings`.
101
+ """
102
+ self.syn = getattr(h, self.conn['spec_syn_param']['level_of_detail'])(list(self.cell.all)[self.conn['spec_settings']['sec_id']](self.conn['spec_settings']['sec_x']))
103
+ for key, value in self.conn['spec_syn_param'].items():
104
+ if isinstance(value, (int, float)): # Only create sliders for numeric values
105
+ if hasattr(self.syn, key):
106
+ setattr(self.syn, key, value)
107
+ else:
108
+ print(f"Warning: {key} cannot be assigned as it does not exist in the synapse. Check your mod file or spec_syn_param.")
109
+
110
+
111
+ def set_up_recorders(self):
112
+ """
113
+ Set up recording vectors to capture simulation data.
114
+
115
+ The method sets up recorders for:
116
+ - Synaptic current specified by `current_name`
117
+ - Other specified synaptic variables (`other_vars_to_record`)
118
+ - Time, soma voltage, and voltage clamp current for all simulations.
119
+ """
120
+ self.rec_vectors = {}
121
+ for var in self.other_vars_to_record:
122
+ self.rec_vectors[var] = h.Vector()
123
+ ref_attr = f'_ref_{var}'
124
+ if hasattr(self.syn, ref_attr):
125
+ self.rec_vectors[var].record(getattr(self.syn, ref_attr))
126
+ else:
127
+ print(f"Warning: {ref_attr} not found in the syn object. Use vars() to inspect available attributes.")
128
+
129
+ # Record synaptic current
130
+ self.rec_vectors[self.current_name] = h.Vector()
131
+ ref_attr = f'_ref_{self.current_name}'
132
+ if hasattr(self.syn, ref_attr):
133
+ self.rec_vectors[self.current_name].record(getattr(self.syn, ref_attr))
134
+ else:
135
+ print("Warning: Synaptic current recorder not set up correctly.")
136
+
137
+ # Record time, synaptic events, soma voltage, and voltage clamp current
138
+ self.t = h.Vector()
139
+ self.tspk = h.Vector()
140
+ self.soma_v = h.Vector()
141
+ self.ivcl = h.Vector()
142
+
143
+ self.t.record(h._ref_t)
144
+ self.nc.record(self.tspk)
145
+ self.nc2.record(self.tspk)
146
+ self.soma_v.record(self.cell.soma[0](0.5)._ref_v)
147
+ self.ivcl.record(self.vcl._ref_i)
148
+
149
+
150
+ def SingleEvent(self):
151
+ """
152
+ Simulate a single synaptic event by delivering an input stimulus to the synapse.
153
+
154
+ The method sets up the neuron cell, synapse, stimulus, and voltage clamp,
155
+ and then runs the NEURON simulation for a single event. The single synaptic event will occur at general_settings['tstart']
156
+ Will display graphs and synaptic properies works best with a jupyter notebook
157
+ """
158
+ self.set_up_cell()
159
+ self.set_up_synapse()
160
+
161
+ # Set up the stimulus
162
+ self.nstim = h.NetStim()
163
+ self.nstim.start = self.general_settings['tstart']
164
+ self.nstim.noise = 0
165
+ self.nstim2 = h.NetStim()
166
+ self.nstim2.start = h.tstop
167
+ self.nstim2.noise = 0
168
+ self.nc = h.NetCon(self.nstim, self.syn, self.general_settings['threshold'], self.general_settings['delay'], self.general_settings['weight'])
169
+ self.nc2 = h.NetCon(self.nstim2, self.syn, self.general_settings['threshold'], self.general_settings['delay'], self.general_settings['weight'])
170
+
171
+ # Set up voltage clamp
172
+ self.vcl = h.VClamp(self.cell.soma[0](0.5))
173
+ vcldur = [[0, 0, 0], [self.general_settings['tstart'], h.tstop, 1e9]]
174
+ for i in range(3):
175
+ self.vcl.amp[i] = self.conn['spec_settings']['vclamp_amp']
176
+ self.vcl.dur[i] = vcldur[1][i]
177
+
178
+ self.set_up_recorders()
179
+
180
+ # Run simulation
181
+ h.tstop = self.general_settings['tstart'] + self.general_settings['tdur']
182
+ self.nstim.interval = self.general_settings['tdur']
183
+ self.nstim.number = 1
184
+ self.nstim2.start = h.tstop
185
+ h.run()
186
+ self.plot_model([self.general_settings['tstart'] - 5, self.general_settings['tstart'] + self.general_settings['tdur']])
187
+ syn_props = self.get_syn_prop(rise_interval=self.general_settings['rise_interval'])
188
+ for prop in syn_props.items():
189
+ print(prop)
190
+
191
+
192
+ def find_first(self, x):
193
+ """
194
+ Find the index of the first non-zero element in a given array.
195
+
196
+ Parameters:
197
+ -----------
198
+ x : np.array
199
+ The input array to search.
200
+
201
+ Returns:
202
+ --------
203
+ int
204
+ Index of the first non-zero element, or None if none exist.
205
+ """
206
+ x = np.asarray(x)
207
+ idx = np.nonzero(x)[0]
208
+ return idx[0] if idx.size else None
209
+
210
+
211
+ def get_syn_prop(self, rise_interval=(0.2, 0.8), dt=h.dt, short=False):
212
+ """
213
+ Calculate synaptic properties such as peak amplitude, latency, rise time, decay time, and half-width.
214
+
215
+ Parameters:
216
+ -----------
217
+ rise_interval : tuple of floats, optional
218
+ Fractional rise time interval to calculate (default is (0.2, 0.8)).
219
+ dt : float, optional
220
+ Time step of the simulation (default is NEURON's `h.dt`).
221
+ short : bool, optional
222
+ If True, only return baseline and sign without calculating full properties.
223
+
224
+ Returns:
225
+ --------
226
+ dict
227
+ A dictionary containing the synaptic properties: baseline, sign, peak amplitude, latency, rise time,
228
+ decay time, and half-width.
229
+ """
230
+ if self.vclamp:
231
+ isyn = self.ivcl
232
+ else:
233
+ isyn = self.rec_vectors['i']
234
+ isyn = np.asarray(isyn)
235
+ tspk = np.asarray(self.tspk)
236
+ if tspk.size:
237
+ tspk = tspk[0]
238
+
239
+ ispk = int(np.floor(tspk / dt))
240
+ baseline = isyn[ispk]
241
+ isyn = isyn[ispk:] - baseline
242
+ # print(np.abs(isyn))
243
+ # print(np.argmax(np.abs(isyn)))
244
+ # print(isyn[np.argmax(np.abs(isyn))])
245
+ # print(np.sign(isyn[np.argmax(np.abs(isyn))]))
246
+ sign = np.sign(isyn[np.argmax(np.abs(isyn))])
247
+ if short:
248
+ return {'baseline': baseline, 'sign': sign}
249
+ isyn *= sign
250
+ # print(isyn)
251
+ # peak amplitude
252
+ ipk, _ = find_peaks(isyn)
253
+ ipk = ipk[0]
254
+ peak = isyn[ipk]
255
+ # latency
256
+ istart = self.find_first(np.diff(isyn[:ipk + 1]) > 0)
257
+ latency = dt * (istart + 1)
258
+ # rise time
259
+ rt1 = self.find_first(isyn[istart:ipk + 1] > rise_interval[0] * peak)
260
+ rt2 = self.find_first(isyn[istart:ipk + 1] > rise_interval[1] * peak)
261
+ rise_time = (rt2 - rt1) * dt
262
+ # decay time
263
+ iend = self.find_first(np.diff(isyn[ipk:]) > 0)
264
+ iend = isyn.size - 1 if iend is None else iend + ipk
265
+ decay_len = iend - ipk + 1
266
+ popt, _ = curve_fit(lambda t, a, tau: a * np.exp(-t / tau), dt * np.arange(decay_len),
267
+ isyn[ipk:iend + 1], p0=(peak, dt * decay_len / 2))
268
+ decay_time = popt[1]
269
+ # half-width
270
+ hw1 = self.find_first(isyn[istart:ipk + 1] > 0.5 * peak)
271
+ hw2 = self.find_first(isyn[ipk:] < 0.5 * peak)
272
+ hw2 = isyn.size if hw2 is None else hw2 + ipk
273
+ half_width = dt * (hw2 - hw1)
274
+ output = {'baseline': baseline, 'sign': sign, 'latency': latency,
275
+ 'amp': peak, 'rise_time': rise_time, 'decay_time': decay_time, 'half_width': half_width}
276
+ return output
277
+
278
+
279
+ def plot_model(self, xlim):
280
+ """
281
+ Plots the results of the simulation, including synaptic current, soma voltage,
282
+ and any additional recorded variables.
283
+
284
+ Parameters:
285
+ -----------
286
+ xlim : tuple
287
+ A tuple specifying the limits of the x-axis for the plot (start_time, end_time).
288
+
289
+ Notes:
290
+ ------
291
+ - The function determines how many plots to generate based on the number of variables recorded.
292
+ - Synaptic current and either voltage clamp or soma voltage will always be plotted.
293
+ - If other variables are provided in `other_vars_to_record`, they are also plotted.
294
+ - The function adjusts the plot layout and removes any extra subplots that are not needed.
295
+ """
296
+ # Determine the number of plots to generate (at least 2: current and voltage)
297
+ num_vars_to_plot = 2 + (len(self.other_vars_to_record) if self.other_vars_to_record else 0)
298
+
299
+ # Set up figure based on number of plots (2x2 grid max)
300
+ num_rows = (num_vars_to_plot + 1) // 2 # This ensures we have enough rows
301
+ fig, axs = plt.subplots(num_rows, 2, figsize=(12, 7))
302
+ axs = axs.ravel()
303
+
304
+ # Plot synaptic current (always included)
305
+ axs[0].plot(self.t, 1000 * self.rec_vectors[self.current_name])
306
+ axs[0].set_ylabel('Synaptic Current (pA)')
307
+
308
+ # Plot voltage clamp or soma voltage (always included)
309
+ ispk = int(np.round(self.tspk[0] / h.dt))
310
+ if self.vclamp:
311
+ baseline = self.ivcl[ispk]
312
+ ivcl_plt = np.array(self.ivcl) - baseline
313
+ ivcl_plt[:ispk] = 0
314
+ axs[1].plot(self.t, 1000 * ivcl_plt)
315
+ axs[1].set_ylabel('VClamp Current (pA)')
316
+ else:
317
+ soma_v_plt = np.array(self.soma_v)
318
+ soma_v_plt[:ispk] = soma_v_plt[ispk]
319
+ axs[1].plot(self.t, soma_v_plt)
320
+ axs[1].set_ylabel('Soma Voltage (mV)')
321
+
322
+ # Plot any other variables from other_vars_to_record, if provided
323
+ if self.other_vars_to_record:
324
+ for i, var in enumerate(self.other_vars_to_record, start=2):
325
+ if var in self.rec_vectors:
326
+ axs[i].plot(self.t, self.rec_vectors[var])
327
+ axs[i].set_ylabel(f'{var.capitalize()}')
328
+
329
+ # Adjust the layout
330
+ for i, ax in enumerate(axs[:num_vars_to_plot]):
331
+ ax.set_xlim(*xlim)
332
+ if i >= num_vars_to_plot - 2: # Add x-label to the last row
333
+ ax.set_xlabel('Time (ms)')
334
+
335
+ # Remove extra subplots if less than 4 plots are present
336
+ if num_vars_to_plot < len(axs):
337
+ for j in range(num_vars_to_plot, len(axs)):
338
+ fig.delaxes(axs[j])
339
+
340
+ plt.tight_layout()
341
+ plt.show()
342
+
343
+
344
+ def set_drive_train(self,freq=50., delay=250.):
345
+ """
346
+ Configures trains of 12 action potentials at a specified frequency and delay period
347
+ between pulses 8 and 9.
348
+
349
+ Parameters:
350
+ -----------
351
+ freq : float, optional
352
+ Frequency of the pulse train in Hz. Default is 50 Hz.
353
+ delay : float, optional
354
+ Delay period in milliseconds between the 8th and 9th pulses. Default is 250 ms.
355
+
356
+ Returns:
357
+ --------
358
+ tstop : float
359
+ The time at which the last pulse stops.
360
+
361
+ Notes:
362
+ ------
363
+ - This function is based on experiments from the Allen Database.
364
+ """
365
+ n_init_pulse = 8
366
+ n_ending_pulse = 4
367
+ self.nstim.start = self.general_settings['tstart']
368
+ self.nstim.interval = 1000 / freq
369
+ self.nstim2.interval = 1000 / freq
370
+ self.nstim.number = n_init_pulse
371
+ self.nstim2.number = n_ending_pulse
372
+ self.nstim2.start = self.nstim.start + (n_init_pulse - 1) * self.nstim.interval + delay
373
+ tstop = self.nstim2.start + n_ending_pulse * self.nstim2.interval
374
+ return tstop
375
+
376
+
377
+ def response_amplitude(self):
378
+ """
379
+ Calculates the amplitude of the synaptic response by analyzing the recorded synaptic current.
380
+
381
+ Returns:
382
+ --------
383
+ amp : list
384
+ A list containing the peak amplitudes for each segment of the recorded synaptic current.
385
+
386
+ """
387
+ isyn = np.asarray(self.rec_vectors['i'])
388
+ tspk = np.append(np.asarray(self.tspk), h.tstop)
389
+ syn_prop = self.get_syn_prop(short=True)
390
+ # print("syn_prp[sign] = " + str(syn_prop['sign']))
391
+ isyn = (isyn - syn_prop['baseline'])
392
+ isyn *= syn_prop['sign']
393
+ # print(isyn)
394
+ ispk = np.floor((tspk + self.general_settings['delay']) / h.dt).astype(int)
395
+ amp = [isyn[ispk[i]:ispk[i + 1]].max() for i in range(ispk.size - 1)]
396
+ return amp
397
+
398
+
399
+ def find_max_amp(self, amp, normalize_by_trial=True):
400
+ """
401
+ Determines the maximum amplitude from the response data.
402
+
403
+ Parameters:
404
+ -----------
405
+ amp : array-like
406
+ Array containing the amplitudes of synaptic responses.
407
+ normalize_by_trial : bool, optional
408
+ If True, normalize the maximum amplitude within each trial. Default is True.
409
+
410
+ Returns:
411
+ --------
412
+ max_amp : float
413
+ The maximum or minimum amplitude based on the sign of the response.
414
+ """
415
+ max_amp = amp.max(axis=1 if normalize_by_trial else None)
416
+ min_amp = amp.min(axis=1 if normalize_by_trial else None)
417
+ if(abs(min_amp) > max_amp):
418
+ return min_amp
419
+ return max_amp
420
+
421
+
422
+ def induction_recovery(self,amp, normalize_by_trial=True):
423
+ """
424
+ Calculates induction and recovery metrics from the synaptic response amplitudes.
425
+
426
+ Parameters:
427
+ -----------
428
+ amp : array-like
429
+ Array containing the amplitudes of synaptic responses.
430
+ normalize_by_trial : bool, optional
431
+ If True, normalize the amplitudes within each trial. Default is True.
432
+
433
+ Returns:
434
+ --------
435
+ induction : float
436
+ The calculated induction value (difference between pulses 6-8 and 1st pulse).
437
+ recovery : float
438
+ The calculated recovery value (difference between pulses 9-12 and pulses 1-4).
439
+ maxamp : float
440
+ The maximum amplitude in the response.
441
+ """
442
+ amp = np.array(amp)
443
+ amp = amp.reshape(-1, amp.shape[-1])
444
+
445
+
446
+ maxamp = amp.max(axis=1 if normalize_by_trial else None)
447
+ induction = np.mean((amp[:, 5:8].mean(axis=1) - amp[:, :1].mean(axis=1)) / maxamp)
448
+ recovery = np.mean((amp[:, 8:12].mean(axis=1) - amp[:, :4].mean(axis=1)) / maxamp)
449
+
450
+ # maxamp = max(amp, key=lambda x: abs(x[0]))
451
+ maxamp = maxamp.max()
452
+ return induction, recovery, maxamp
453
+
454
+
455
+ def paired_pulse_ratio(self, dt=h.dt):
456
+ """
457
+ Computes the paired-pulse ratio (PPR) based on the recorded synaptic current or voltage.
458
+
459
+ Parameters:
460
+ -----------
461
+ dt : float, optional
462
+ Time step in milliseconds. Default is the NEURON simulation time step.
463
+
464
+ Returns:
465
+ --------
466
+ ppr : float
467
+ The ratio between the second and first pulse amplitudes.
468
+
469
+ Notes:
470
+ ------
471
+ - The function handles both voltage-clamp and current-clamp conditions.
472
+ - A minimum of two spikes is required to calculate PPR.
473
+ """
474
+ if self.vclamp:
475
+ isyn = self.ivcl
476
+ else:
477
+ isyn = self.rec_vectors['i']
478
+ isyn = np.asarray(isyn)
479
+ tspk = np.asarray(self.tspk)
480
+ if tspk.size < 2:
481
+ raise ValueError("Need at least two spikes.")
482
+ syn_prop = self.get_syn_prop()
483
+ isyn = (isyn - syn_prop['baseline']) * syn_prop['sign']
484
+ ispk2 = int(np.floor(tspk[1] / dt))
485
+ ipk, _ = find_peaks(isyn[ispk2:])
486
+ ipk2 = ipk[0] + ispk2
487
+ peak2 = isyn[ipk2]
488
+ return peak2 / syn_prop['amp']
489
+
490
+
491
+ def set_syn_prop(self, **kwargs):
492
+ """
493
+ Sets the synaptic parameters based on user inputs from sliders.
494
+
495
+ Parameters:
496
+ -----------
497
+ **kwargs : dict
498
+ Synaptic properties (such as weight, Use, tau_f, tau_d) as keyword arguments.
499
+ """
500
+ for key, value in kwargs.items():
501
+ setattr(self.syn, key, value)
502
+
503
+
504
+ def simulate_model(self,input_frequency, delay, vclamp=None):
505
+ """
506
+ Runs the simulation with the specified input frequency, delay, and voltage clamp settings.
507
+
508
+ Parameters:
509
+ -----------
510
+ input_frequency : float
511
+ Frequency of the input drive train in Hz.
512
+ delay : float
513
+ Delay period in milliseconds between the 8th and 9th pulses.
514
+ vclamp : bool or None, optional
515
+ Whether to use voltage clamp. If None, the current setting is used. Default is None.
516
+
517
+ """
518
+ if self.input_mode == False:
519
+ self.tstop = self.set_drive_train(input_frequency, delay)
520
+ h.tstop = self.tstop
521
+
522
+ vcldur = [[0, 0, 0], [self.general_settings['tstart'], self.tstop, 1e9]]
523
+ for i in range(3):
524
+ self.vcl.amp[i] = self.conn['spec_settings']['vclamp_amp']
525
+ self.vcl.dur[i] = vcldur[1][i]
526
+ h.finitialize(self.cell.Vinit * mV)
527
+ h.continuerun(self.tstop * ms)
528
+ else:
529
+ self.tstop = self.general_settings['tstart'] + self.general_settings['tdur']
530
+ self.nstim.interval = 1000 / input_frequency
531
+ self.nstim.number = np.ceil(self.w_duration.value / 1000 * input_frequency + 1)
532
+ self.nstim2.number = 0
533
+ self.tstop = self.w_duration.value + self.general_settings['tstart']
534
+
535
+ h.finitialize(self.cell.Vinit * mV)
536
+ h.continuerun(self.tstop * ms)
537
+
538
+
539
+ def InteractiveTuner(self):
540
+ """
541
+ Sets up interactive sliders for short-term plasticity (STP) experiments in a Jupyter Notebook.
542
+
543
+ Notes:
544
+ ------
545
+ - The sliders allow control over synaptic properties dynamically based on slider_vars.
546
+ - Additional buttons allow running the simulation and configuring voltage clamp settings.
547
+ """
548
+ # Widgets setup (Sliders)
549
+ freqs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 50, 100, 200]
550
+ delays = [125, 250, 500, 1000, 2000, 4000]
551
+ durations = [300, 500, 1000, 2000, 5000, 10000]
552
+ freq0 = 50
553
+ delay0 = 250
554
+ duration0 = 300
555
+ vlamp_status = self.vclamp
556
+
557
+ w_run = widgets.Button(description='Run', icon='history', button_style='primary')
558
+ w_vclamp = widgets.ToggleButton(value=vlamp_status, description='Voltage Clamp', icon='fast-backward', button_style='warning')
559
+ w_input_mode = widgets.ToggleButton(value=False, description='Continuous input', icon='eject', button_style='info')
560
+ w_input_freq = widgets.SelectionSlider(options=freqs, value=freq0, description='Input Freq')
561
+
562
+ # Sliders for delay and duration
563
+ self.w_delay = widgets.SelectionSlider(options=delays, value=delay0, description='Delay')
564
+ self.w_duration = widgets.SelectionSlider(options=durations, value=duration0, description='Duration')
565
+
566
+ # Generate sliders dynamically based on valid numeric entries in self.slider_vars
567
+ dynamic_sliders = {}
568
+ print("Setting up slider! The sliders ranges are set by their init value so try changing that if you dont like the slider range!")
569
+ for key, value in self.slider_vars.items():
570
+ if isinstance(value, (int, float)): # Only create sliders for numeric values
571
+ if hasattr(self.syn, key):
572
+ if value == 0:
573
+ print(f'{key} was set to zero, going to try to set a range of values, try settings the {key} to a nonzero value if you dont like the range!')
574
+ slider = widgets.FloatSlider(value=value, min=0, max=1000, step=1, description=key)
575
+ else:
576
+ slider = widgets.FloatSlider(value=value, min=0, max=value*20, step=value/5, description=key)
577
+ dynamic_sliders[key] = slider
578
+ else:
579
+ print(f"skipping slider for {key} due to not being a synaptic variable")
580
+
581
+ # Function to update UI based on input mode
582
+ def update_ui(*args):
583
+ clear_output()
584
+ display(ui)
585
+ self.vclamp = w_vclamp.value
586
+ self.input_mode = w_input_mode.value
587
+ # Update synaptic properties based on slider values
588
+ syn_props = {var: slider.value for var, slider in dynamic_sliders.items()}
589
+ self.set_syn_prop(**syn_props)
590
+ if self.input_mode == False:
591
+ self.simulate_model(w_input_freq.value, self.w_delay.value, w_vclamp.value)
592
+ else:
593
+ self.simulate_model(w_input_freq.value, self.w_duration.value, w_vclamp.value)
594
+ self.plot_model([self.general_settings['tstart'] - self.nstim.interval / 3, self.tstop])
595
+ amp = self.response_amplitude()
596
+ induction_single, recovery, maxamp = self.induction_recovery(amp)
597
+ ppr = self.paired_pulse_ratio()
598
+ print('Paired Pulse Ratio using ' + ('PSC' if self.vclamp else 'PSP') + f': {ppr:.3f}')
599
+ print('Single trial ' + ('PSC' if self.vclamp else 'PSP'))
600
+ print(f'Induction: {induction_single:.2f}; Recovery: {recovery:.2f}')
601
+ print(f'Rest Amp: {amp[0]:.2f}; Maximum Amp: {maxamp:.2f}')
602
+
603
+ # Function to switch between delay and duration sliders
604
+ def switch_slider(*args):
605
+ if w_input_mode.value:
606
+ self.w_delay.layout.display = 'none' # Hide delay slider
607
+ self.w_duration.layout.display = '' # Show duration slider
608
+ else:
609
+ self.w_delay.layout.display = '' # Show delay slider
610
+ self.w_duration.layout.display = 'none' # Hide duration slider
611
+
612
+ # Link input mode to slider switch
613
+ w_input_mode.observe(switch_slider, names='value')
614
+
615
+ # Hide the duration slider initially
616
+ self.w_duration.layout.display = 'none' # Hide duration slider
617
+
618
+ w_run.on_click(update_ui)
619
+
620
+ # Add the dynamic sliders to the UI
621
+ slider_widgets = [slider for slider in dynamic_sliders.values()]
622
+
623
+ # Divide sliders into two columns
624
+ half = len(slider_widgets) // 2
625
+ col1 = VBox(slider_widgets[:half]) # First half of sliders
626
+ col2 = VBox(slider_widgets[half:]) # Second half of sliders
627
+
628
+ # Create a two-column layout with HBox
629
+ slider_columns = HBox([col1, col2])
630
+
631
+ ui = VBox([HBox([w_run, w_vclamp, w_input_mode]), HBox([w_input_freq, self.w_delay, self.w_duration]), slider_columns])
632
+
633
+ display(ui)
634
+
635
+
636
+
637
+
638
+