bmtool 0.7.7__py3-none-any.whl → 0.7.8.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.

Potentially problematic release.


This version of bmtool might be problematic. Click here for more details.

bmtool/synapses.py CHANGED
@@ -173,6 +173,7 @@ class SynapseTuner:
173
173
  self.other_vars_to_record = other_vars_to_record or []
174
174
  self.ispk = None
175
175
  self.input_mode = False # Add input_mode attribute
176
+ self.last_figure = None # Store reference to last generated figure
176
177
 
177
178
  # Store original slider_vars for connection switching
178
179
  self.original_slider_vars = slider_vars or list(self.synaptic_props.keys())
@@ -1045,7 +1046,9 @@ class SynapseTuner:
1045
1046
  for j in range(num_vars_to_plot, len(axs)):
1046
1047
  fig.delaxes(axs[j])
1047
1048
 
1048
- # plt.tight_layout()
1049
+ #plt.tight_layout()
1050
+ fig.suptitle(f"Connection: {self.current_connection}")
1051
+ self.last_figure = plt.gcf()
1049
1052
  plt.show()
1050
1053
 
1051
1054
  def _set_drive_train(self, freq=50.0, delay=250.0):
@@ -1148,7 +1151,7 @@ class SynapseTuner:
1148
1151
 
1149
1152
  def _calc_ppr_induction_recovery(self, amp, normalize_by_trial=True, print_math=True):
1150
1153
  """
1151
- Calculates paired-pulse ratio, induction, and recovery metrics from response amplitudes.
1154
+ Calculates paired-pulse ratio, induction, recovery, and simple PPR metrics from response amplitudes.
1152
1155
 
1153
1156
  Parameters:
1154
1157
  -----------
@@ -1163,13 +1166,15 @@ class SynapseTuner:
1163
1166
  --------
1164
1167
  tuple
1165
1168
  A tuple containing:
1166
- - ppr: Paired-pulse ratio (2nd pulse / 1st pulse)
1169
+ - ppr: Paired-pulse ratio (2nd pulse - 1st pulse) normalized by 90th percentile amplitude
1167
1170
  - induction: Measure of facilitation/depression during initial pulses
1168
1171
  - recovery: Measure of recovery after the delay period
1172
+ - simple_ppr: Simple paired-pulse ratio (2nd pulse / 1st pulse)
1169
1173
 
1170
1174
  Notes:
1171
1175
  ------
1172
- - PPR > 1 indicates facilitation, PPR < 1 indicates depression
1176
+ - PPR > 0 indicates facilitation, PPR < 0 indicates depression
1177
+ - Simple PPR > 1 indicates facilitation, Simple PPR < 1 indicates depression
1173
1178
  - Induction > 0 indicates facilitation, Induction < 0 indicates depression
1174
1179
  - Recovery compares the response after delay to the initial pulses
1175
1180
  """
@@ -1190,34 +1195,44 @@ class SynapseTuner:
1190
1195
  f"Short Term Plasticity Results for {self.train_freq}Hz with {self.train_delay} Delay"
1191
1196
  )
1192
1197
  print("=" * 40)
1193
- print("PPR: Above 0 is facilitating, below 0 is depressing.")
1194
- print("Induction: Above 0 is facilitating, below 0 is depressing.")
1195
- print("Recovery: A measure of how fast STP decays.\n")
1198
+ print("Simple PPR: Above 1 is facilitating, below 1 is depressing")
1199
+ print("PPR: Above 0 is facilitating, below 0 is depressing.")
1200
+ print("Induction: Above 0 is facilitating, below 0 is depressing.")
1201
+ print("Recovery: A measure of how fast STP decays.\n")
1202
+
1203
+ # Simple PPR Calculation: Avg 2nd pulse / Avg 1st pulse
1204
+ simple_ppr = np.mean(amp[:, 1:2]) / np.mean(amp[:, 0:1])
1205
+ print("Simple Paired Pulse Ratio (PPR)")
1206
+ print(" Calculation: Avg 2nd pulse / Avg 1st pulse")
1207
+ print(
1208
+ f" Values: {np.mean(amp[:, 1:2]):.3f} / {np.mean(amp[:, 0:1]):.3f} = {simple_ppr:.3f}\n"
1209
+ )
1196
1210
 
1197
1211
  # PPR Calculation: (Avg 2nd pulse - Avg 1st pulse) / 90th percentile amplitude
1198
1212
  ppr = (np.mean(amp[:, 1:2]) - np.mean(amp[:, 0:1])) / percentile_90
1199
1213
  print("Paired Pulse Response (PPR)")
1200
- print("Calculation: (Avg 2nd pulse - Avg 1st pulse) / 90th percentile amplitude")
1214
+ print(" Calculation: (Avg 2nd pulse - Avg 1st pulse) / 90th percentile amplitude")
1201
1215
  print(
1202
- f"Values: ({np.mean(amp[:, 1:2]):.3f} - {np.mean(amp[:, 0:1]):.3f}) / {percentile_90:.3f} = {ppr:.3f}\n"
1216
+ f" Values: ({np.mean(amp[:, 1:2]):.3f} - {np.mean(amp[:, 0:1]):.3f}) / {percentile_90:.3f} = {ppr:.3f}\n"
1203
1217
  )
1218
+
1204
1219
 
1205
1220
  # Induction Calculation: (Avg (6th, 7th, 8th pulses) - Avg 1st pulse) / 90th percentile amplitude
1206
1221
  induction = (np.mean(amp[:, 5:8]) - np.mean(amp[:, :1])) / percentile_90
1207
1222
  print("Induction")
1208
- print("Calculation: (Avg(6th, 7th, 8th pulses) - Avg 1st pulse) / 90th percentile amplitude")
1223
+ print(" Calculation: (Avg(6th, 7th, 8th pulses) - Avg 1st pulse) / 90th percentile amplitude")
1209
1224
  print(
1210
- f"Values: {np.mean(amp[:, 5:8]):.3f} - {np.mean(amp[:, :1]):.3f} / {percentile_90:.3f} = {induction:.3f}\n"
1225
+ f" Values: {np.mean(amp[:, 5:8]):.3f} - {np.mean(amp[:, :1]):.3f} / {percentile_90:.3f} = {induction:.3f}\n"
1211
1226
  )
1212
1227
 
1213
1228
  # Recovery Calculation: (Avg (9th, 10th, 11th, 12th pulses) - Avg (1st, 2nd, 3rd, 4th pulses)) / 90th percentile amplitude
1214
1229
  recovery = (np.mean(amp[:, 8:12]) - np.mean(amp[:, :4])) / percentile_90
1215
1230
  print("Recovery")
1216
1231
  print(
1217
- "Calculation: (Avg(9th, 10th, 11th, 12th pulses) - Avg(1st to 4th pulses)) / 90th percentile amplitude"
1232
+ " Calculation: (Avg(9th, 10th, 11th, 12th pulses) - Avg(1st to 4th pulses)) / 90th percentile amplitude"
1218
1233
  )
1219
1234
  print(
1220
- f"Values: {np.mean(amp[:, 8:12]):.3f} - {np.mean(amp[:, :4]):.3f} / {percentile_90:.3f} = {recovery:.3f}\n"
1235
+ f" Values: {np.mean(amp[:, 8:12]):.3f} - {np.mean(amp[:, :4]):.3f} / {percentile_90:.3f} = {recovery:.3f}\n"
1221
1236
  )
1222
1237
 
1223
1238
  print("=" * 40 + "\n")
@@ -1226,8 +1241,9 @@ class SynapseTuner:
1226
1241
  ppr = (np.mean(amp[:, 1:2]) - np.mean(amp[:, 0:1])) / percentile_90
1227
1242
  induction = (np.mean(amp[:, 5:8]) - np.mean(amp[:, :1])) / percentile_90
1228
1243
  recovery = (np.mean(amp[:, 8:12]) - np.mean(amp[:, :4])) / percentile_90
1244
+ simple_ppr = np.mean(amp[:, 1:2]) / np.mean(amp[:, 0:1])
1229
1245
 
1230
- return ppr, induction, recovery
1246
+ return ppr, induction, recovery, simple_ppr
1231
1247
 
1232
1248
  def _set_syn_prop(self, **kwargs):
1233
1249
  """
@@ -1268,19 +1284,19 @@ class SynapseTuner:
1268
1284
  for i in range(3):
1269
1285
  self.vcl.amp[i] = self.conn["spec_settings"]["vclamp_amp"]
1270
1286
  self.vcl.dur[i] = vcldur[1][i]
1271
- #h.finitialize(self.cell.Vinit * mV)
1272
- #h.continuerun(self.tstop * ms)
1273
- h.run()
1287
+ h.finitialize(70 * mV)
1288
+ h.continuerun(self.tstop * ms)
1289
+ #h.run()
1274
1290
  else:
1275
- self.tstop = self.general_settings["tstart"] + self.general_settings["tdur"]
1291
+ # Continuous input mode: ensure simulation runs long enough for the full stimulation duration
1292
+ self.tstop = self.general_settings["tstart"] + self.w_duration.value + 300 # 300ms buffer time
1276
1293
  self.nstim.interval = 1000 / input_frequency
1277
1294
  self.nstim.number = np.ceil(self.w_duration.value / 1000 * input_frequency + 1)
1278
1295
  self.nstim2.number = 0
1279
- self.tstop = self.w_duration.value + self.general_settings["tstart"]
1280
1296
 
1281
- #h.finitialize(self.cell.Vinit * mV)
1282
- #h.continuerun(self.tstop * ms)
1283
- h.run()
1297
+ h.finitialize(70 * mV)
1298
+ h.continuerun(self.tstop * ms)
1299
+ #h.run()
1284
1300
 
1285
1301
  def InteractiveTuner(self):
1286
1302
  """
@@ -1375,6 +1391,52 @@ class SynapseTuner:
1375
1391
  options=durations, value=duration0, description="Duration"
1376
1392
  )
1377
1393
 
1394
+ # Save functionality widgets
1395
+ save_path_text = widgets.Text(
1396
+ value="plot.png",
1397
+ description="Save path:",
1398
+ layout=widgets.Layout(width='300px')
1399
+ )
1400
+ save_button = widgets.Button(description="Save Plot", icon="save", button_style="success")
1401
+
1402
+ def save_plot(b):
1403
+ if hasattr(self, 'last_figure') and self.last_figure is not None:
1404
+ try:
1405
+ # Create a new figure with just the first subplot (synaptic current)
1406
+ fig, ax = plt.subplots(figsize=(8, 6))
1407
+
1408
+ # Get the axes from the original figure
1409
+ original_axes = self.last_figure.get_axes()
1410
+ if len(original_axes) > 0:
1411
+ first_ax = original_axes[0]
1412
+
1413
+ # Copy the data from the first subplot
1414
+ for line in first_ax.get_lines():
1415
+ ax.plot(line.get_xdata(), line.get_ydata(),
1416
+ color=line.get_color(), label=line.get_label())
1417
+
1418
+ # Copy axis labels and title
1419
+ ax.set_xlabel(first_ax.get_xlabel())
1420
+ ax.set_ylabel(first_ax.get_ylabel())
1421
+ ax.set_title(first_ax.get_title())
1422
+ ax.set_xlim(first_ax.get_xlim())
1423
+ ax.legend()
1424
+ ax.grid(True)
1425
+
1426
+ # Save the new figure
1427
+ fig.savefig(save_path_text.value)
1428
+ plt.close(fig) # Close the temporary figure
1429
+ print(f"Synaptic current plot saved to {save_path_text.value}")
1430
+ else:
1431
+ print("No subplots found in the figure")
1432
+
1433
+ except Exception as e:
1434
+ print(f"Error saving plot: {e}")
1435
+ else:
1436
+ print("No plot to save")
1437
+
1438
+ save_button.on_click(save_plot)
1439
+
1378
1440
  def create_dynamic_sliders():
1379
1441
  """Create sliders based on current connection's parameters"""
1380
1442
  sliders = {}
@@ -1453,7 +1515,7 @@ class SynapseTuner:
1453
1515
  the network dropdown. It coordinates the complete switching process:
1454
1516
  1. Calls _switch_network() to rebuild connections for the new network
1455
1517
  2. Updates the connection dropdown options with new network's connections
1456
- 3. Recreates dynamic sliders for the new connection parameters
1518
+ 3. Recreates dynamic sliders for new connection parameters
1457
1519
  4. Refreshes the entire UI to reflect all changes
1458
1520
  """
1459
1521
  if w_network is None:
@@ -1515,8 +1577,9 @@ class SynapseTuner:
1515
1577
  else:
1516
1578
  connection_row = HBox([w_connection])
1517
1579
  slider_row = HBox([w_input_freq, self.w_delay, self.w_duration])
1580
+ save_row = HBox([save_path_text, save_button])
1518
1581
 
1519
- ui = VBox([connection_row, button_row, slider_row, slider_columns])
1582
+ ui = VBox([connection_row, button_row, slider_row, slider_columns, save_row])
1520
1583
 
1521
1584
  # Function to update UI based on input mode
1522
1585
  def update_ui(*args):
@@ -1618,6 +1681,7 @@ class SynapseTuner:
1618
1681
  Dictionary containing frequency-dependent metrics with keys:
1619
1682
  - 'frequencies': List of tested frequencies
1620
1683
  - 'ppr': Paired-pulse ratios at each frequency
1684
+ - 'simple_ppr': Simple paired-pulse ratios (2nd/1st pulse) at each frequency
1621
1685
  - 'induction': Induction values at each frequency
1622
1686
  - 'recovery': Recovery values at each frequency
1623
1687
 
@@ -1627,7 +1691,7 @@ class SynapseTuner:
1627
1691
  behavior of synapses, such as identifying facilitating vs. depressing regimes
1628
1692
  or the frequency at which a synapse transitions between these behaviors.
1629
1693
  """
1630
- results = {"frequencies": freqs, "ppr": [], "induction": [], "recovery": []}
1694
+ results = {"frequencies": freqs, "ppr": [], "induction": [], "recovery": [], "simple_ppr": []}
1631
1695
 
1632
1696
  # Store original state
1633
1697
  original_ispk = self.ispk
@@ -1635,11 +1699,12 @@ class SynapseTuner:
1635
1699
  for freq in tqdm(freqs, desc="Analyzing frequencies"):
1636
1700
  self._simulate_model(freq, delay)
1637
1701
  amp = self._response_amplitude()
1638
- ppr, induction, recovery = self._calc_ppr_induction_recovery(amp, print_math=False)
1702
+ ppr, induction, recovery, simple_ppr = self._calc_ppr_induction_recovery(amp, print_math=False)
1639
1703
 
1640
1704
  results["ppr"].append(float(ppr))
1641
1705
  results["induction"].append(float(induction))
1642
1706
  results["recovery"].append(float(recovery))
1707
+ results["simple_ppr"].append(float(simple_ppr))
1643
1708
 
1644
1709
  # Restore original state
1645
1710
  self.ispk = original_ispk
@@ -1659,6 +1724,7 @@ class SynapseTuner:
1659
1724
  Dictionary containing frequency analysis results with keys:
1660
1725
  - 'frequencies': List of tested frequencies
1661
1726
  - 'ppr': Paired-pulse ratios at each frequency
1727
+ - 'simple_ppr': Simple paired-pulse ratios at each frequency
1662
1728
  - 'induction': Induction values at each frequency
1663
1729
  - 'recovery': Recovery values at each frequency
1664
1730
  log_plot : bool
@@ -1667,24 +1733,27 @@ class SynapseTuner:
1667
1733
  Notes:
1668
1734
  ------
1669
1735
  Creates a figure with three subplots showing:
1670
- 1. Paired-pulse ratio vs. frequency
1736
+ 1. Paired-pulse ratios (both normalized and simple) vs. frequency
1671
1737
  2. Induction vs. frequency
1672
1738
  3. Recovery vs. frequency
1673
1739
 
1674
1740
  Each plot includes a horizontal reference line at y=0 or y=1 to indicate
1675
1741
  the boundary between facilitation and depression.
1676
1742
  """
1677
- fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))
1743
+ fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))
1678
1744
 
1679
- # Plot PPR
1745
+ # Plot both PPR measures
1680
1746
  if log_plot:
1681
- ax1.semilogx(results["frequencies"], results["ppr"], "o-")
1747
+ ax1.semilogx(results["frequencies"], results["ppr"], "o-", label="Normalized PPR")
1748
+ ax1.semilogx(results["frequencies"], results["simple_ppr"], "s-", label="Simple PPR")
1682
1749
  else:
1683
- ax1.plot(results["frequencies"], results["ppr"], "o-")
1750
+ ax1.plot(results["frequencies"], results["ppr"], "o-", label="Normalized PPR")
1751
+ ax1.plot(results["frequencies"], results["simple_ppr"], "s-", label="Simple PPR")
1684
1752
  ax1.axhline(y=1, color="gray", linestyle="--", alpha=0.5)
1685
1753
  ax1.set_xlabel("Frequency (Hz)")
1686
1754
  ax1.set_ylabel("Paired Pulse Ratio")
1687
1755
  ax1.set_title("PPR vs Frequency")
1756
+ ax1.legend()
1688
1757
  ax1.grid(True)
1689
1758
 
1690
1759
  # Plot Induction
@@ -1713,6 +1782,168 @@ class SynapseTuner:
1713
1782
  plt.show()
1714
1783
 
1715
1784
 
1785
+ def generate_synaptic_table(self, stp_frequency=50.0, stp_delay=250.0, plot=True):
1786
+ """
1787
+ Generate a comprehensive table of synaptic parameters for all connections.
1788
+
1789
+ This method iterates through all available connections, runs simulations to
1790
+ characterize each synapse, and compiles the results into a pandas DataFrame.
1791
+
1792
+ Parameters:
1793
+ -----------
1794
+ stp_frequency : float, optional
1795
+ Frequency in Hz to use for STP (short-term plasticity) analysis. Default is 50.0 Hz.
1796
+ stp_delay : float, optional
1797
+ Delay in ms between pulse trains for STP analysis. Default is 250.0 ms.
1798
+ plot : bool, optional
1799
+ Whether to display the resulting table. Default is True.
1800
+
1801
+ Returns:
1802
+ --------
1803
+ pd.DataFrame
1804
+ DataFrame containing synaptic parameters for each connection with columns:
1805
+ - connection: Connection name
1806
+ - rise_time: 20-80% rise time (ms)
1807
+ - decay_time: Decay time constant (ms)
1808
+ - latency: Response latency (ms)
1809
+ - half_width: Response half-width (ms)
1810
+ - peak_amplitude: Peak synaptic current amplitude (pA)
1811
+ - baseline: Baseline current (pA)
1812
+ - ppr: Paired-pulse ratio (normalized)
1813
+ - simple_ppr: Simple paired-pulse ratio (2nd/1st pulse)
1814
+ - induction: STP induction measure
1815
+ - recovery: STP recovery measure
1816
+
1817
+ Notes:
1818
+ ------
1819
+ This method temporarily switches between connections to characterize each one,
1820
+ then restores the original connection. The STP metrics are calculated at the
1821
+ specified frequency and delay.
1822
+ """
1823
+ # Store original connection to restore later
1824
+ original_connection = self.current_connection
1825
+
1826
+ # Initialize results list
1827
+ results = []
1828
+
1829
+ print(f"Analyzing {len(self.conn_type_settings)} connections...")
1830
+
1831
+ for conn_name in tqdm(self.conn_type_settings.keys(), desc="Analyzing connections"):
1832
+ try:
1833
+ # Switch to this connection
1834
+ self._switch_connection(conn_name)
1835
+
1836
+ # Run single event analysis
1837
+ self.SingleEvent(plot_and_print=False)
1838
+
1839
+ # Get synaptic properties from the single event
1840
+ syn_props = self._get_syn_prop()
1841
+
1842
+ # Run STP analysis at specified frequency
1843
+ stp_results = self.stp_frequency_response(
1844
+ freqs=[stp_frequency],
1845
+ delay=stp_delay,
1846
+ plot=False,
1847
+ log_plot=False
1848
+ )
1849
+
1850
+ # Extract STP metrics for this frequency
1851
+ freq_idx = 0 # Only one frequency tested
1852
+ ppr = stp_results['ppr'][freq_idx]
1853
+ induction = stp_results['induction'][freq_idx]
1854
+ recovery = stp_results['recovery'][freq_idx]
1855
+ simple_ppr = stp_results['simple_ppr'][freq_idx]
1856
+
1857
+ # Compile results for this connection
1858
+ conn_results = {
1859
+ 'connection': conn_name,
1860
+ 'rise_time': float(self.rise_time),
1861
+ 'decay_time': float(self.decay_time),
1862
+ 'latency': float(syn_props.get('latency', 0)),
1863
+ 'half_width': float(syn_props.get('half_width', 0)),
1864
+ 'peak_amplitude': float(syn_props.get('amp', 0)),
1865
+ 'baseline': float(syn_props.get('baseline', 0)),
1866
+ 'ppr': float(ppr),
1867
+ 'simple_ppr': float(simple_ppr),
1868
+ 'induction': float(induction),
1869
+ 'recovery': float(recovery)
1870
+ }
1871
+
1872
+ results.append(conn_results)
1873
+
1874
+ except Exception as e:
1875
+ print(f"Warning: Failed to analyze connection '{conn_name}': {e}")
1876
+ # Add partial results if possible
1877
+ results.append({
1878
+ 'connection': conn_name,
1879
+ 'rise_time': float('nan'),
1880
+ 'decay_time': float('nan'),
1881
+ 'latency': float('nan'),
1882
+ 'half_width': float('nan'),
1883
+ 'peak_amplitude': float('nan'),
1884
+ 'baseline': float('nan'),
1885
+ 'ppr': float('nan'),
1886
+ 'simple_ppr': float('nan'),
1887
+ 'induction': float('nan'),
1888
+ 'recovery': float('nan')
1889
+ })
1890
+
1891
+ # Restore original connection
1892
+ if original_connection in self.conn_type_settings:
1893
+ self._switch_connection(original_connection)
1894
+
1895
+ # Create DataFrame
1896
+ df = pd.DataFrame(results)
1897
+
1898
+ # Set connection as index for better display
1899
+ df = df.set_index('connection')
1900
+
1901
+ if plot:
1902
+ # Display the table
1903
+ print("\nSynaptic Parameters Table:")
1904
+ print("=" * 80)
1905
+ display(df.round(4))
1906
+
1907
+ # Optional: Create a simple bar plot for key metrics
1908
+ try:
1909
+ fig, axes = plt.subplots(2, 2, figsize=(15, 10))
1910
+ fig.suptitle(f'Synaptic Parameters Across Connections (STP at {stp_frequency}Hz)', fontsize=16)
1911
+
1912
+ # Plot rise/decay times
1913
+ df[['rise_time', 'decay_time']].plot(kind='bar', ax=axes[0,0])
1914
+ axes[0,0].set_title('Rise and Decay Times')
1915
+ axes[0,0].set_ylabel('Time (ms)')
1916
+ axes[0,0].tick_params(axis='x', rotation=45)
1917
+
1918
+ # Plot PPR metrics
1919
+ df[['ppr', 'simple_ppr']].plot(kind='bar', ax=axes[0,1])
1920
+ axes[0,1].set_title('Paired-Pulse Ratios')
1921
+ axes[0,1].axhline(y=1, color='gray', linestyle='--', alpha=0.5)
1922
+ axes[0,1].tick_params(axis='x', rotation=45)
1923
+
1924
+ # Plot induction
1925
+ df['induction'].plot(kind='bar', ax=axes[1,0], color='green')
1926
+ axes[1,0].set_title('STP Induction')
1927
+ axes[1,0].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
1928
+ axes[1,0].set_ylabel('Induction')
1929
+ axes[1,0].tick_params(axis='x', rotation=45)
1930
+
1931
+ # Plot recovery
1932
+ df['recovery'].plot(kind='bar', ax=axes[1,1], color='orange')
1933
+ axes[1,1].set_title('STP Recovery')
1934
+ axes[1,1].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
1935
+ axes[1,1].set_ylabel('Recovery')
1936
+ axes[1,1].tick_params(axis='x', rotation=45)
1937
+
1938
+ plt.tight_layout()
1939
+ plt.show()
1940
+
1941
+ except Exception as e:
1942
+ print(f"Warning: Could not create plots: {e}")
1943
+
1944
+ return df
1945
+
1946
+
1716
1947
  class GapJunctionTuner:
1717
1948
  def __init__(
1718
1949
  self,
@@ -1767,6 +1998,7 @@ class GapJunctionTuner:
1767
1998
  self.config = config
1768
1999
  self.available_networks = []
1769
2000
  self.current_network = None
2001
+ self.last_figure = None
1770
2002
  if self.conn_type_settings is None and self.config is not None:
1771
2003
  self.conn_type_settings = self._build_conn_type_settings_from_config(self.config)
1772
2004
  if self.conn_type_settings is None or len(self.conn_type_settings) == 0:
@@ -2168,6 +2400,7 @@ class GapJunctionTuner:
2168
2400
  plt.xlabel("Time (ms)")
2169
2401
  plt.ylabel("Membrane Voltage (mV)")
2170
2402
  plt.legend()
2403
+ self.last_figure = plt.gcf()
2171
2404
 
2172
2405
  def coupling_coefficient(self, t, v1, v2, t_start, t_end, dt=h.dt):
2173
2406
  """
@@ -2202,75 +2435,649 @@ class GapJunctionTuner:
2202
2435
  return (v2[idx2] - v2[idx1]) / (v1[idx2] - v1[idx1])
2203
2436
 
2204
2437
  def InteractiveTuner(self):
2205
- w_run = widgets.Button(description="Run", icon="history", button_style="primary")
2206
- values = [i * 10**-4 for i in range(1, 1001)] # From 1e-4 to 1e-1
2207
-
2208
- # Create the SelectionSlider widget with appropriate formatting
2209
- resistance = widgets.FloatLogSlider(
2210
- value=0.001,
2211
- base=10,
2212
- min=-4, # max exponent of base
2213
- max=-1, # min exponent of base
2214
- step=0.1, # exponent step
2215
- description="Resistance: ",
2216
- continuous_update=True,
2217
- )
2438
+ """
2439
+ Sets up interactive sliders for tuning short-term plasticity (STP) parameters in a Jupyter Notebook.
2440
+
2441
+ This method creates an interactive UI with sliders for:
2442
+ - Network selection dropdown (if multiple networks available and config provided)
2443
+ - Connection type selection dropdown
2444
+ - Input frequency
2445
+ - Delay between pulse trains
2446
+ - Duration of stimulation (for continuous input mode)
2447
+ - Synaptic parameters (e.g., Use, tau_f, tau_d) based on the syn model
2448
+
2449
+ It also provides buttons for:
2450
+ - Running a single event simulation
2451
+ - Running a train input simulation
2452
+ - Toggling voltage clamp mode
2453
+ - Switching between standard and continuous input modes
2218
2454
 
2219
- output = widgets.Output()
2455
+ Network Dropdown Feature:
2456
+ ------------------------
2457
+ When the SynapseTuner is initialized with a BMTK config file containing multiple networks:
2458
+ - A network dropdown appears next to the connection dropdown
2459
+ - Users can dynamically switch between networks (e.g., 'network_to_network', 'external_to_network')
2460
+ - Switching networks rebuilds available connections and updates the connection dropdown
2461
+ - The current connection is preserved if it exists in the new network
2462
+ - If multiple networks exist but only one is specified during init, that network is used as default
2220
2463
 
2221
- ui_widgets = [w_run, resistance]
2464
+ Notes:
2465
+ ------
2466
+ Ideal for exploratory parameter tuning and interactive visualization of
2467
+ synapse behavior with different parameter values and stimulation protocols.
2468
+ The network dropdown feature enables comprehensive exploration of multi-network
2469
+ BMTK simulations without needing to reinitialize the tuner.
2470
+ """
2471
+ # Widgets setup (Sliders)
2472
+ freqs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 35, 50, 100, 200]
2473
+ delays = [125, 250, 500, 1000, 2000, 4000]
2474
+ durations = [100, 300, 500, 1000, 2000, 5000, 10000]
2475
+ freq0 = 50
2476
+ delay0 = 250
2477
+ duration0 = 300
2478
+ vlamp_status = self.vclamp
2222
2479
 
2223
- def on_button(*args):
2224
- with output:
2225
- # Clear only the output widget, not the entire cell
2226
- output.clear_output(wait=True)
2480
+ # Connection dropdown
2481
+ connection_options = sorted(list(self.conn_type_settings.keys()))
2482
+ w_connection = widgets.Dropdown(
2483
+ options=connection_options,
2484
+ value=self.current_connection,
2485
+ description="Connection:",
2486
+ style={'description_width': 'initial'}
2487
+ )
2227
2488
 
2228
- resistance_for_gap = resistance.value
2229
- print(f"Running simulation with resistance: {resistance_for_gap:0.6f} and {self.general_settings['iclamp_amp']*1000}pA current clamps")
2489
+ # Network dropdown - only shown if config was provided and multiple networks are available
2490
+ # This enables users to switch between different network datasets dynamically
2491
+ w_network = None
2492
+ if self.config is not None and len(self.available_networks) > 1:
2493
+ w_network = widgets.Dropdown(
2494
+ options=self.available_networks,
2495
+ value=self.current_network,
2496
+ description="Network:",
2497
+ style={'description_width': 'initial'}
2498
+ )
2230
2499
 
2231
- try:
2232
- self.model(resistance_for_gap)
2233
- self.plot_model()
2500
+ w_run = widgets.Button(description="Run Train", icon="history", button_style="primary")
2501
+ w_single = widgets.Button(description="Single Event", icon="check", button_style="success")
2502
+ w_vclamp = widgets.ToggleButton(
2503
+ value=vlamp_status,
2504
+ description="Voltage Clamp",
2505
+ icon="fast-backward",
2506
+ button_style="warning",
2507
+ )
2508
+
2509
+ # Voltage clamp amplitude input
2510
+ default_vclamp_amp = getattr(self.conn['spec_settings'], 'vclamp_amp', -70.0)
2511
+ w_vclamp_amp = widgets.FloatText(
2512
+ value=default_vclamp_amp,
2513
+ description="V_clamp (mV):",
2514
+ step=5.0,
2515
+ style={'description_width': 'initial'},
2516
+ layout=widgets.Layout(width='150px')
2517
+ )
2518
+
2519
+ w_input_mode = widgets.ToggleButton(
2520
+ value=False, description="Continuous input", icon="eject", button_style="info"
2521
+ )
2522
+ w_input_freq = widgets.SelectionSlider(options=freqs, value=freq0, description="Input Freq")
2234
2523
 
2235
- # Convert NEURON vectors to numpy arrays
2236
- t_array = np.array(self.t_vec)
2237
- v1_array = np.array(self.soma_v_1)
2238
- v2_array = np.array(self.soma_v_2)
2524
+ # Sliders for delay and duration
2525
+ self.w_delay = widgets.SelectionSlider(options=delays, value=delay0, description="Delay")
2526
+ self.w_duration = widgets.SelectionSlider(
2527
+ options=durations, value=duration0, description="Duration"
2528
+ )
2239
2529
 
2240
- cc = self.coupling_coefficient(t_array, v1_array, v2_array, 500, 1000)
2241
- print(f"coupling_coefficient is {cc:0.4f}")
2242
- plt.show()
2530
+ # Save functionality widgets
2531
+ save_path_text = widgets.Text(
2532
+ value="plot.png",
2533
+ description="Save path:",
2534
+ layout=widgets.Layout(width='300px')
2535
+ )
2536
+ save_button = widgets.Button(description="Save Plot", icon="save", button_style="success")
2243
2537
 
2538
+ def save_plot(b):
2539
+ if hasattr(self, 'last_figure') and self.last_figure is not None:
2540
+ try:
2541
+ # Create a new figure with just the first subplot (synaptic current)
2542
+ fig, ax = plt.subplots(figsize=(8, 6))
2543
+
2544
+ # Get the axes from the original figure
2545
+ original_axes = self.last_figure.get_axes()
2546
+ if len(original_axes) > 0:
2547
+ first_ax = original_axes[0]
2548
+
2549
+ # Copy the data from the first subplot
2550
+ for line in first_ax.get_lines():
2551
+ ax.plot(line.get_xdata(), line.get_ydata(),
2552
+ color=line.get_color(), label=line.get_label())
2553
+
2554
+ # Copy axis labels and title
2555
+ ax.set_xlabel(first_ax.get_xlabel())
2556
+ ax.set_ylabel(first_ax.get_ylabel())
2557
+ ax.set_title(first_ax.get_title())
2558
+ ax.set_xlim(first_ax.get_xlim())
2559
+ ax.legend()
2560
+ ax.grid(True)
2561
+
2562
+ # Save the new figure
2563
+ fig.savefig(save_path_text.value)
2564
+ plt.close(fig) # Close the temporary figure
2565
+ print(f"Synaptic current plot saved to {save_path_text.value}")
2566
+ else:
2567
+ print("No subplots found in the figure")
2568
+
2244
2569
  except Exception as e:
2245
- print(f"Error during simulation or analysis: {e}")
2246
- import traceback
2570
+ print(f"Error saving plot: {e}")
2571
+ else:
2572
+ print("No plot to save")
2247
2573
 
2248
- traceback.print_exc()
2574
+ save_button.on_click(save_plot)
2249
2575
 
2250
- # Add connection dropdown if multiple connections exist
2251
- if len(self.conn_type_settings) > 1:
2252
- connection_dropdown = widgets.Dropdown(
2253
- options=list(self.conn_type_settings.keys()),
2254
- value=self.current_connection,
2255
- description='Connection:',
2256
- )
2257
- def on_connection_change(change):
2258
- if change['type'] == 'change' and change['name'] == 'value':
2259
- self._switch_connection(change['new'])
2260
- on_button() # Automatically rerun the simulation after switching
2261
- connection_dropdown.observe(on_connection_change)
2262
- ui_widgets.insert(0, connection_dropdown)
2576
+ def create_dynamic_sliders():
2577
+ """Create sliders based on current connection's parameters"""
2578
+ sliders = {}
2579
+ for key, value in self.slider_vars.items():
2580
+ if isinstance(value, (int, float)): # Only create sliders for numeric values
2581
+ if hasattr(self.syn, key):
2582
+ if value == 0:
2583
+ print(
2584
+ 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!"
2585
+ )
2586
+ slider = widgets.FloatSlider(
2587
+ value=value, min=0, max=1000, step=1, description=key
2588
+ )
2589
+ else:
2590
+ slider = widgets.FloatSlider(
2591
+ value=value, min=0, max=value * 20, step=value / 5, description=key
2592
+ )
2593
+ sliders[key] = slider
2594
+ else:
2595
+ print(f"skipping slider for {key} due to not being a synaptic variable")
2596
+ return sliders
2263
2597
 
2264
- ui = VBox(ui_widgets)
2598
+ # Generate sliders dynamically based on valid numeric entries in self.slider_vars
2599
+ self.dynamic_sliders = create_dynamic_sliders()
2600
+ print(
2601
+ "Setting up slider! The sliders ranges are set by their init value so try changing that if you dont like the slider range!"
2602
+ )
2265
2603
 
2266
- display(ui)
2267
- display(output)
2604
+ # Create output widget for displaying results
2605
+ output_widget = widgets.Output()
2606
+
2607
+ def run_single_event(*args):
2608
+ clear_output()
2609
+ display(ui)
2610
+ display(output_widget)
2611
+
2612
+ self.vclamp = w_vclamp.value
2613
+ # Update voltage clamp amplitude if voltage clamp is enabled
2614
+ if self.vclamp:
2615
+ # Update the voltage clamp amplitude settings
2616
+ self.conn['spec_settings']['vclamp_amp'] = w_vclamp_amp.value
2617
+ # Update general settings if they exist
2618
+ if hasattr(self, 'general_settings'):
2619
+ self.general_settings['vclamp_amp'] = w_vclamp_amp.value
2620
+ # Update synaptic properties based on slider values
2621
+ self.ispk = None
2622
+
2623
+ # Clear previous results and run simulation
2624
+ output_widget.clear_output()
2625
+ with output_widget:
2626
+ self.SingleEvent()
2268
2627
 
2269
- # Run once initially
2270
- on_button()
2271
- w_run.on_click(on_button)
2628
+ def on_connection_change(*args):
2629
+ """Handle connection dropdown change"""
2630
+ try:
2631
+ new_connection = w_connection.value
2632
+ if new_connection != self.current_connection:
2633
+ # Switch to new connection
2634
+ self._switch_connection(new_connection)
2635
+
2636
+ # Recreate dynamic sliders for new connection
2637
+ self.dynamic_sliders = create_dynamic_sliders()
2638
+
2639
+ # Update UI
2640
+ update_ui_layout()
2641
+ update_ui()
2642
+
2643
+ except Exception as e:
2644
+ print(f"Error switching connection: {e}")
2272
2645
 
2646
+ def on_network_change(*args):
2647
+ """
2648
+ Handle network dropdown change events.
2649
+
2650
+ This callback is triggered when the user selects a different network from
2651
+ the network dropdown. It coordinates the complete switching process:
2652
+ 1. Calls _switch_network() to rebuild connections for the new network
2653
+ 2. Updates the connection dropdown options with new network's connections
2654
+ 3. Recreates dynamic sliders for new connection parameters
2655
+ 4. Refreshes the entire UI to reflect all changes
2656
+ """
2657
+ if w_network is None:
2658
+ return
2659
+ try:
2660
+ new_network = w_network.value
2661
+ if new_network != self.current_network:
2662
+ # Switch to new network
2663
+ self._switch_network(new_network)
2664
+
2665
+ # Update connection dropdown options with new network's connections
2666
+ connection_options = list(self.conn_type_settings.keys())
2667
+ w_connection.options = connection_options
2668
+ if connection_options:
2669
+ w_connection.value = self.current_connection
2670
+
2671
+ # Recreate dynamic sliders for new connection
2672
+ self.dynamic_sliders = create_dynamic_sliders()
2673
+
2674
+ # Update UI
2675
+ update_ui_layout()
2676
+ update_ui()
2677
+
2678
+ except Exception as e:
2679
+ print(f"Error switching network: {e}")
2273
2680
 
2681
+ def update_ui_layout():
2682
+ """
2683
+ Update the UI layout with new sliders and network dropdown.
2684
+
2685
+ This function reconstructs the entire UI layout including:
2686
+ - Network dropdown (if available) and connection dropdown in the top row
2687
+ - Button controls and input mode toggles
2688
+ - Parameter sliders arranged in columns
2689
+ """
2690
+ nonlocal ui, slider_columns
2691
+
2692
+ # Add the dynamic sliders to the UI
2693
+ slider_widgets = [slider for slider in self.dynamic_sliders.values()]
2694
+
2695
+ if slider_widgets:
2696
+ half = len(slider_widgets) // 2
2697
+ col1 = VBox(slider_widgets[:half])
2698
+ col2 = VBox(slider_widgets[half:])
2699
+ slider_columns = HBox([col1, col2])
2700
+ else:
2701
+ slider_columns = VBox([])
2702
+
2703
+ # Create button row with voltage clamp controls
2704
+ if w_vclamp.value: # Show voltage clamp amplitude input when toggle is on
2705
+ button_row = HBox([w_run, w_single, w_vclamp, w_vclamp_amp, w_input_mode])
2706
+ else: # Hide voltage clamp amplitude input when toggle is off
2707
+ button_row = HBox([w_run, w_single, w_vclamp, w_input_mode])
2708
+
2709
+ # Construct the top row - include network dropdown if available
2710
+ # This creates a horizontal layout with network dropdown (if present) and connection dropdown
2711
+ if w_network is not None:
2712
+ connection_row = HBox([w_network, w_connection])
2713
+ else:
2714
+ connection_row = HBox([w_connection])
2715
+ slider_row = HBox([w_input_freq, self.w_delay, self.w_duration])
2716
+ save_row = HBox([save_path_text, save_button])
2717
+
2718
+ ui = VBox([connection_row, button_row, slider_row, slider_columns, save_row])
2719
+
2720
+ # Function to update UI based on input mode
2721
+ def update_ui(*args):
2722
+ clear_output()
2723
+ display(ui)
2724
+ display(output_widget)
2725
+
2726
+ self.vclamp = w_vclamp.value
2727
+ # Update voltage clamp amplitude if voltage clamp is enabled
2728
+ if self.vclamp:
2729
+ self.conn['spec_settings']['vclamp_amp'] = w_vclamp_amp.value
2730
+ if hasattr(self, 'general_settings'):
2731
+ self.general_settings['vclamp_amp'] = w_vclamp_amp.value
2732
+
2733
+ self.input_mode = w_input_mode.value
2734
+ syn_props = {var: slider.value for var, slider in self.dynamic_sliders.items()}
2735
+ self._set_syn_prop(**syn_props)
2736
+
2737
+ # Clear previous results and run simulation
2738
+ output_widget.clear_output()
2739
+ with output_widget:
2740
+ if not self.input_mode:
2741
+ self._simulate_model(w_input_freq.value, self.w_delay.value, w_vclamp.value)
2742
+ else:
2743
+ self._simulate_model(w_input_freq.value, self.w_duration.value, w_vclamp.value)
2744
+ amp = self._response_amplitude()
2745
+ self._plot_model(
2746
+ [self.general_settings["tstart"] - self.nstim.interval / 3, self.tstop]
2747
+ )
2748
+ _ = self._calc_ppr_induction_recovery(amp)
2749
+
2750
+ # Function to switch between delay and duration sliders
2751
+ def switch_slider(*args):
2752
+ if w_input_mode.value:
2753
+ self.w_delay.layout.display = "none" # Hide delay slider
2754
+ self.w_duration.layout.display = "" # Show duration slider
2755
+ else:
2756
+ self.w_delay.layout.display = "" # Show delay slider
2757
+ self.w_duration.layout.display = "none" # Hide duration slider
2758
+
2759
+ # Function to handle voltage clamp toggle
2760
+ def on_vclamp_toggle(*args):
2761
+ """Handle voltage clamp toggle changes to show/hide amplitude input"""
2762
+ update_ui_layout()
2763
+ clear_output()
2764
+ display(ui)
2765
+ display(output_widget)
2766
+
2767
+ # Link widgets to their callback functions
2768
+ w_connection.observe(on_connection_change, names="value")
2769
+ # Link network dropdown callback only if network dropdown was created
2770
+ if w_network is not None:
2771
+ w_network.observe(on_network_change, names="value")
2772
+ w_input_mode.observe(switch_slider, names="value")
2773
+ w_vclamp.observe(on_vclamp_toggle, names="value")
2774
+
2775
+ # Hide the duration slider initially until the user selects it
2776
+ self.w_duration.layout.display = "none" # Hide duration slider
2777
+
2778
+ w_single.on_click(run_single_event)
2779
+ w_run.on_click(update_ui)
2780
+
2781
+ # Initial UI setup
2782
+ slider_columns = VBox([])
2783
+ ui = VBox([])
2784
+ update_ui_layout()
2785
+
2786
+ display(ui)
2787
+ update_ui()
2788
+
2789
+ def stp_frequency_response(
2790
+ self,
2791
+ freqs=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 35, 50, 100, 200],
2792
+ delay=250,
2793
+ plot=True,
2794
+ log_plot=True,
2795
+ ):
2796
+ """
2797
+ Analyze synaptic response across different stimulation frequencies.
2798
+
2799
+ This method systematically tests how the synapse model responds to different
2800
+ stimulation frequencies, calculating key short-term plasticity (STP) metrics
2801
+ for each frequency.
2802
+
2803
+ Parameters:
2804
+ -----------
2805
+ freqs : list, optional
2806
+ List of frequencies to analyze (in Hz). Default covers a wide range from 1-200 Hz.
2807
+ delay : float, optional
2808
+ Delay between pulse trains in ms. Default is 250 ms.
2809
+ plot : bool, optional
2810
+ Whether to plot the results. Default is True.
2811
+ log_plot : bool, optional
2812
+ Whether to use logarithmic scale for frequency axis. Default is True.
2813
+
2814
+ Returns:
2815
+ --------
2816
+ dict
2817
+ Dictionary containing frequency-dependent metrics with keys:
2818
+ - 'frequencies': List of tested frequencies
2819
+ - 'ppr': Paired-pulse ratios at each frequency
2820
+ - 'simple_ppr': Simple paired-pulse ratios (2nd/1st pulse) at each frequency
2821
+ - 'induction': Induction values at each frequency
2822
+ - 'recovery': Recovery values at each frequency
2823
+
2824
+ Notes:
2825
+ ------
2826
+ This method is particularly useful for characterizing the frequency-dependent
2827
+ behavior of synapses, such as identifying facilitating vs. depressing regimes
2828
+ or the frequency at which a synapse transitions between these behaviors.
2829
+ """
2830
+ results = {"frequencies": freqs, "ppr": [], "induction": [], "recovery": [], "simple_ppr": []}
2831
+
2832
+ # Store original state
2833
+ original_ispk = self.ispk
2834
+
2835
+ for freq in tqdm(freqs, desc="Analyzing frequencies"):
2836
+ self._simulate_model(freq, delay)
2837
+ amp = self._response_amplitude()
2838
+ ppr, induction, recovery, simple_ppr = self._calc_ppr_induction_recovery(amp, print_math=False)
2839
+
2840
+ results["ppr"].append(float(ppr))
2841
+ results["induction"].append(float(induction))
2842
+ results["recovery"].append(float(recovery))
2843
+ results["simple_ppr"].append(float(simple_ppr))
2844
+
2845
+ # Restore original state
2846
+ self.ispk = original_ispk
2847
+
2848
+ if plot:
2849
+ self._plot_frequency_analysis(results, log_plot=log_plot)
2850
+
2851
+ return results
2852
+
2853
+ def _plot_frequency_analysis(self, results, log_plot):
2854
+ """
2855
+ Plot the frequency-dependent synaptic properties.
2856
+
2857
+ Parameters:
2858
+ -----------
2859
+ results : dict
2860
+ Dictionary containing frequency analysis results with keys:
2861
+ - 'frequencies': List of tested frequencies
2862
+ - 'ppr': Paired-pulse ratios at each frequency
2863
+ - 'simple_ppr': Simple paired-pulse ratios at each frequency
2864
+ - 'induction': Induction values at each frequency
2865
+ - 'recovery': Recovery values at each frequency
2866
+ log_plot : bool
2867
+ Whether to use logarithmic scale for frequency axis
2868
+
2869
+ Notes:
2870
+ ------
2871
+ Creates a figure with three subplots showing:
2872
+ 1. Paired-pulse ratios (both normalized and simple) vs. frequency
2873
+ 2. Induction vs. frequency
2874
+ 3. Recovery vs. frequency
2875
+
2876
+ Each plot includes a horizontal reference line at y=0 or y=1 to indicate
2877
+ the boundary between facilitation and depression.
2878
+ """
2879
+ fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))
2880
+
2881
+ # Plot both PPR measures
2882
+ if log_plot:
2883
+ ax1.semilogx(results["frequencies"], results["ppr"], "o-", label="Normalized PPR")
2884
+ ax1.semilogx(results["frequencies"], results["simple_ppr"], "s-", label="Simple PPR")
2885
+ else:
2886
+ ax1.plot(results["frequencies"], results["ppr"], "o-", label="Normalized PPR")
2887
+ ax1.plot(results["frequencies"], results["simple_ppr"], "s-", label="Simple PPR")
2888
+ ax1.axhline(y=1, color="gray", linestyle="--", alpha=0.5)
2889
+ ax1.set_xlabel("Frequency (Hz)")
2890
+ ax1.set_ylabel("Paired Pulse Ratio")
2891
+ ax1.set_title("PPR vs Frequency")
2892
+ ax1.legend()
2893
+ ax1.grid(True)
2894
+
2895
+ # Plot Induction
2896
+ if log_plot:
2897
+ ax2.semilogx(results["frequencies"], results["induction"], "o-")
2898
+ else:
2899
+ ax2.plot(results["frequencies"], results["induction"], "o-")
2900
+ ax2.axhline(y=0, color="gray", linestyle="--", alpha=0.5)
2901
+ ax2.set_xlabel("Frequency (Hz)")
2902
+ ax2.set_ylabel("Induction")
2903
+ ax2.set_title("Induction vs Frequency")
2904
+ ax2.grid(True)
2905
+
2906
+ # Plot Recovery
2907
+ if log_plot:
2908
+ ax3.semilogx(results["frequencies"], results["recovery"], "o-")
2909
+ else:
2910
+ ax3.plot(results["frequencies"], results["recovery"], "o-")
2911
+ ax3.axhline(y=0, color="gray", linestyle="--", alpha=0.5)
2912
+ ax3.set_xlabel("Frequency (Hz)")
2913
+ ax3.set_ylabel("Recovery")
2914
+ ax3.set_title("Recovery vs Frequency")
2915
+ ax3.grid(True)
2916
+
2917
+ plt.tight_layout()
2918
+ plt.show()
2919
+
2920
+ def generate_synaptic_table(self, stp_frequency=50.0, stp_delay=250.0, plot=True):
2921
+ """
2922
+ Generate a comprehensive table of synaptic parameters for all connections.
2923
+
2924
+ This method iterates through all available connections, runs simulations to
2925
+ characterize each synapse, and compiles the results into a pandas DataFrame.
2926
+
2927
+ Parameters:
2928
+ -----------
2929
+ stp_frequency : float, optional
2930
+ Frequency in Hz to use for STP (short-term plasticity) analysis. Default is 50.0 Hz.
2931
+ stp_delay : float, optional
2932
+ Delay in ms between pulse trains for STP analysis. Default is 250.0 ms.
2933
+ plot : bool, optional
2934
+ Whether to display the resulting table. Default is True.
2935
+
2936
+ Returns:
2937
+ --------
2938
+ pd.DataFrame
2939
+ DataFrame containing synaptic parameters for each connection with columns:
2940
+ - connection: Connection name
2941
+ - rise_time: 20-80% rise time (ms)
2942
+ - decay_time: Decay time constant (ms)
2943
+ - latency: Response latency (ms)
2944
+ - half_width: Response half-width (ms)
2945
+ - peak_amplitude: Peak synaptic current amplitude (pA)
2946
+ - baseline: Baseline current (pA)
2947
+ - ppr: Paired-pulse ratio (normalized)
2948
+ - simple_ppr: Simple paired-pulse ratio (2nd/1st pulse)
2949
+ - induction: STP induction measure
2950
+ - recovery: STP recovery measure
2951
+
2952
+ Notes:
2953
+ ------
2954
+ This method temporarily switches between connections to characterize each one,
2955
+ then restores the original connection. The STP metrics are calculated at the
2956
+ specified frequency and delay.
2957
+ """
2958
+ # Store original connection to restore later
2959
+ original_connection = self.current_connection
2960
+
2961
+ # Initialize results list
2962
+ results = []
2963
+
2964
+ print(f"Analyzing {len(self.conn_type_settings)} connections...")
2965
+
2966
+ for conn_name in tqdm(self.conn_type_settings.keys(), desc="Analyzing connections"):
2967
+ try:
2968
+ # Switch to this connection
2969
+ self._switch_connection(conn_name)
2970
+
2971
+ # Run single event analysis
2972
+ self.SingleEvent(plot_and_print=False)
2973
+
2974
+ # Get synaptic properties from the single event
2975
+ syn_props = self._get_syn_prop()
2976
+
2977
+ # Run STP analysis at specified frequency
2978
+ stp_results = self.stp_frequency_response(
2979
+ freqs=[stp_frequency],
2980
+ delay=stp_delay,
2981
+ plot=False,
2982
+ log_plot=False
2983
+ )
2984
+
2985
+ # Extract STP metrics for this frequency
2986
+ freq_idx = 0 # Only one frequency tested
2987
+ ppr = stp_results['ppr'][freq_idx]
2988
+ induction = stp_results['induction'][freq_idx]
2989
+ recovery = stp_results['recovery'][freq_idx]
2990
+ simple_ppr = stp_results['simple_ppr'][freq_idx]
2991
+
2992
+ # Compile results for this connection
2993
+ conn_results = {
2994
+ 'connection': conn_name,
2995
+ 'rise_time': float(self.rise_time),
2996
+ 'decay_time': float(self.decay_time),
2997
+ 'latency': float(syn_props.get('latency', 0)),
2998
+ 'half_width': float(syn_props.get('half_width', 0)),
2999
+ 'peak_amplitude': float(syn_props.get('amp', 0)),
3000
+ 'baseline': float(syn_props.get('baseline', 0)),
3001
+ 'ppr': float(ppr),
3002
+ 'simple_ppr': float(simple_ppr),
3003
+ 'induction': float(induction),
3004
+ 'recovery': float(recovery)
3005
+ }
3006
+
3007
+ results.append(conn_results)
3008
+
3009
+ except Exception as e:
3010
+ print(f"Warning: Failed to analyze connection '{conn_name}': {e}")
3011
+ # Add partial results if possible
3012
+ results.append({
3013
+ 'connection': conn_name,
3014
+ 'rise_time': float('nan'),
3015
+ 'decay_time': float('nan'),
3016
+ 'latency': float('nan'),
3017
+ 'half_width': float('nan'),
3018
+ 'peak_amplitude': float('nan'),
3019
+ 'baseline': float('nan'),
3020
+ 'ppr': float('nan'),
3021
+ 'simple_ppr': float('nan'),
3022
+ 'induction': float('nan'),
3023
+ 'recovery': float('nan')
3024
+ })
3025
+
3026
+ # Restore original connection
3027
+ if original_connection in self.conn_type_settings:
3028
+ self._switch_connection(original_connection)
3029
+
3030
+ # Create DataFrame
3031
+ df = pd.DataFrame(results)
3032
+
3033
+ # Set connection as index for better display
3034
+ df = df.set_index('connection')
3035
+
3036
+ if plot:
3037
+ # Display the table
3038
+ print("\nSynaptic Parameters Table:")
3039
+ print("=" * 80)
3040
+ display(df.round(4))
3041
+
3042
+ # Optional: Create a simple bar plot for key metrics
3043
+ try:
3044
+ fig, axes = plt.subplots(2, 2, figsize=(15, 10))
3045
+ fig.suptitle(f'Synaptic Parameters Across Connections (STP at {stp_frequency}Hz)', fontsize=16)
3046
+
3047
+ # Plot rise/decay times
3048
+ df[['rise_time', 'decay_time']].plot(kind='bar', ax=axes[0,0])
3049
+ axes[0,0].set_title('Rise and Decay Times')
3050
+ axes[0,0].set_ylabel('Time (ms)')
3051
+ axes[0,0].tick_params(axis='x', rotation=45)
3052
+
3053
+ # Plot PPR metrics
3054
+ df[['ppr', 'simple_ppr']].plot(kind='bar', ax=axes[0,1])
3055
+ axes[0,1].set_title('Paired-Pulse Ratios')
3056
+ axes[0,1].axhline(y=1, color='gray', linestyle='--', alpha=0.5)
3057
+ axes[0,1].tick_params(axis='x', rotation=45)
3058
+
3059
+ # Plot induction
3060
+ df['induction'].plot(kind='bar', ax=axes[1,0], color='green')
3061
+ axes[1,0].set_title('STP Induction')
3062
+ axes[1,0].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
3063
+ axes[1,0].set_ylabel('Induction')
3064
+ axes[1,0].tick_params(axis='x', rotation=45)
3065
+
3066
+ # Plot recovery
3067
+ df['recovery'].plot(kind='bar', ax=axes[1,1], color='orange')
3068
+ axes[1,1].set_title('STP Recovery')
3069
+ axes[1,1].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
3070
+ axes[1,1].set_ylabel('Recovery')
3071
+ axes[1,1].tick_params(axis='x', rotation=45)
3072
+
3073
+ plt.tight_layout()
3074
+ plt.show()
3075
+
3076
+ except Exception as e:
3077
+ print(f"Warning: Could not create plots: {e}")
3078
+
3079
+ return df
3080
+
2274
3081
  # optimizers!
2275
3082
 
2276
3083
 
@@ -2365,6 +3172,7 @@ class SynapseOptimizer:
2365
3172
  induction = 0
2366
3173
  ppr = 0
2367
3174
  recovery = 0
3175
+ simple_ppr = 0
2368
3176
  amp = 0
2369
3177
  rise_time = 0
2370
3178
  decay_time = 0
@@ -2388,7 +3196,7 @@ class SynapseOptimizer:
2388
3196
  if self.run_train_input:
2389
3197
  self.tuner._simulate_model(self.train_frequency, self.train_delay)
2390
3198
  amp = self.tuner._response_amplitude()
2391
- ppr, induction, recovery = self.tuner._calc_ppr_induction_recovery(
3199
+ ppr, induction, recovery, simple_ppr = self.tuner._calc_ppr_induction_recovery(
2392
3200
  amp, print_math=False
2393
3201
  )
2394
3202
  amp = self.tuner._find_max_amp(amp)
@@ -2397,6 +3205,7 @@ class SynapseOptimizer:
2397
3205
  "induction": float(induction),
2398
3206
  "ppr": float(ppr),
2399
3207
  "recovery": float(recovery),
3208
+ "simple_ppr": float(simple_ppr),
2400
3209
  "max_amplitude": float(amp),
2401
3210
  "rise_time": float(rise_time),
2402
3211
  "decay_time": float(decay_time),
@@ -2540,7 +3349,7 @@ class SynapseOptimizer:
2540
3349
  elif init_guess == "middle_guess":
2541
3350
  x0 = [(b[0] + b[1]) / 2 for b in bounds]
2542
3351
  else:
2543
- raise Exception("Pick a vaid init guess method either random or midde_guess")
3352
+ raise Exception("Pick a valid init guess method: either 'random' or 'middle_guess'")
2544
3353
  normalized_x0 = self._normalize_params(np.array(x0), param_names)
2545
3354
 
2546
3355
  # Run optimization
@@ -2646,7 +3455,7 @@ class SynapseOptimizer:
2646
3455
  self.tuner.ispk = None
2647
3456
  self.tuner.SingleEvent(plot_and_print=True)
2648
3457
 
2649
- # dataclass means just init the typehints as self.typehint. looks a bit cleaner
3458
+ # dataclass decorator automatically generates __init__ from type-annotated class variables for cleaner code
2650
3459
  @dataclass
2651
3460
  class GapOptimizationResult:
2652
3461
  """Container for gap junction optimization results"""