OSCR

Limit-Cycle Proliferation Under Parametric Delayed Feedback in a Conductance-Based Neuron: Bifurcation Landscape, Orbit Catalog, and Capacity Analysis

Code ↔ Paper

36 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 36 matches
  1. [1] § 3. Experimental Pipeline › 3.3. PS1: Write Protocol ↔ experiments/PS1_WriteProtocol.ipynb, lines 775–869 · score 0.90 · orbits achieve lock, Gate PS G1, orbit switch lock, median settling, ISI error, G1d
  2. [2] § 3. Experimental Pipeline › 3.4. PS2: Read Protocol and Noise Robustness ↔ experiments/PS2_ReadProtocol.ipynb, lines 764–876 · score 0.88 · Gate PS G2, noisy accuracy, jitter accuracy, Clean accuracy, observation window, G2d
  3. [3] § 2. Mathematical Framework › 2.2. Stability and the Orbit Landscape ↔ experiments/PS0b_FloquetValidation.ipynb, lines 468–548 · score 0.81 · trivial Floquet multipliers, QR iteration, phase shift, periodic orbit, history, delay
  4. [4] § 4. Results › 4.1. PS0: The HH–DFC Orbit Catalog ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 522–667 · score 0.76 · hierarchical clustering, qualitative categories, ISI separation, distinct orbit, linkage, map
  5. [5] § 3. Experimental Pipeline › 3.2. PS0: Dense Orbit Catalog ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 669–739 · score 0.76 · pairwise ISI separability, Gate PS G0, qualitative categories, distinct orbit, Catalog, PS0
  6. [6] § 2. Mathematical Framework › 2.1. Hodgkin–Huxley Neuron with Delayed Feedback Control ↔ experiments/PS0b_FloquetValidation.ipynb, lines 715–843 · score 0.74 · Floquet multiplier validation, Floquet convergence, Period quality, ps0b floquetvalidation, trivial multiplier, empirical stability
  7. [7] § 3. Experimental Pipeline › 3.5. PS3: Full Write–Read–Erase Demonstration ↔ experiments/PS3_FullDemo_n_100.ipynb, lines 786–876 · score 0.73 · erase verification, Gate PS G3, G3b, G3a, G3e, G3c
  8. [8] § 3. Experimental Pipeline › 3.5. PS3: Full Write–Read–Erase Demonstration ↔ experiments/PS3_FullDemo_n_20.ipynb, lines 786–863 · score 0.73 · erase verification, Gate PS G3, G3b, G3a, G3e, G3c
  9. [9] § 3. Experimental Pipeline › 3.2. PS0: Dense Orbit Catalog ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 320–403 · score 0.72 · fingerprint vectors, quasi periodic, ISI CV, silent, cluster, chaotic
  10. [10] § 3. Experimental Pipeline › 3.1. Gate Threshold Rationale ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 669–739 · score 0.72 · ISI separability, PS G0, pairwise ISI separation, distinct orbit, catalog, PS0
  11. [11] § 4. Results › 4.3. PS2: Read Protocol and Noise Robustness ↔ experiments/PS2_ReadProtocol.ipynb, lines 764–876 · score 0.70 · Gate PS G2, jitter accuracy, clean accuracy, G2d, G2a, G2b
  12. [12] § 2. Mathematical Framework › 2.1. Hodgkin–Huxley Neuron with Delayed Feedback Control ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 44–108 · score 0.70 · potassium activation, sodium activation, gating variables
  13. [13] § 3. Experimental Pipeline › 3.7. PS5: Maximum Capacity from All 207 Orbit Types ↔ experiments/PS5_MaxCapacity.ipynb, lines 573–653 · score 0.70 · pairwise confusion matrix, greedy maximum subset, capacity curve, PS5, windows, selection
  14. [14] § 4. Results › 4.2. PS1: Write Protocol Performance ↔ experiments/PS1_WriteProtocol.ipynb, lines 775–869 · score 0.67 · Median settling, orbits achieve, Lock rate, target ISI, G1c, G1a
  15. [15] § 4. Results › 4.2. PS1: Write Protocol Performance ↔ experiments/PS1_WriteProtocol.ipynb, lines 871–987 · score 0.66 · ISI relative error, Switch lock rate, Median settling, switching matrix, PS1, Protocol
  16. [16] § 4. Results › 4.1. PS0: The HH–DFC Orbit Catalog ↔ experiments/PS0b_FloquetValidation.ipynb, lines 468–548 · score 0.65 · QR iteration, period quality, Floquet multipliers, trivial, Validation, transient
  17. [17] § 3. Experimental Pipeline › 3.4. PS2: Read Protocol and Noise Robustness ↔ experiments/PS2_ReadProtocol.ipynb, lines 1083–1183 · score 0.65 · Noise robustness, observation window, fingerprint templates, calibrated, jitter, transient
  18. [18] § 4. Results › 4.6. PS5: Maximum Capacity ↔ experiments/PS5_MaxCapacity.ipynb, lines 278–351 · score 0.62 · pre simulation, ISI windows, confusion matrix, caching, PS5, Capacity
  19. [19] § 4. Results › 4.6. PS5: Maximum Capacity ↔ experiments/PS5_MaxCapacity.ipynb, lines 573–653 · score 0.62 · Gate PS G5, greedy maximum subset, Capacity curve, PS5, accuracy, orbits
  20. [20] § 3. Experimental Pipeline › 3.5. PS3: Full Write–Read–Erase Demonstration ↔ experiments/PS3_FullDemo_n_100.ipynb, lines 536–630 · score 0.58 · PS1 switching matrix, clean simulation, alphabet, sequence, transient, symbol
  21. [21] § 3. Experimental Pipeline › 3.5. PS3: Full Write–Read–Erase Demonstration ↔ experiments/PS3_FullDemo_n_20.ipynb, lines 536–630 · score 0.58 · PS1 switching matrix, clean simulation, alphabet, sequence, transient, symbol
  22. [22] § 4. Results › 4.5. PS4: Rate Coding Baseline Comparison ↔ experiments/PS4_RateBaseline.ipynb, lines 346–413 · score 0.57 · HH Rate capacity, HH Rate baseline, Capacity comparison, PS4, bar, HH DFC
  23. [23] § 4. Results › 4.5. PS4: Rate Coding Baseline Comparison ↔ experiments/PS4_RateBaseline.ipynb, lines 438–534 · score 0.57 · Voltage traces, stable limit cycles, ISI series, PS4, Baseline, cm2
  24. [24] § 4. Results › 4.3. PS2: Read Protocol and Noise Robustness ↔ experiments/PS2_ReadProtocol.ipynb, lines 878–924 · score 0.56 · max band, observation window, classification accuracy, calibration, PS2, min
  25. [25] § 3. Experimental Pipeline › 3.2. PS0: Dense Orbit Catalog ↔ experiments/PS0_SecondaryIbias.ipynb, lines 626–752 · score 0.56 · 5.9–56.9 ms, 5.9 ms, dense, space, 200 ms, catalog
  26. [26] § 4. Results › 4.5. PS4: Rate Coding Baseline Comparison ↔ experiments/PS5_MaxCapacity.ipynb, lines 498–571 · score 0.56 · capacity curve, Capacity comparison, HH Rate, bar, HH DFC, PS4
  27. [27] § 2. Mathematical Framework › 2.1. Hodgkin–Huxley Neuron with Delayed Feedback Control ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 110–232 · score 0.56 · warm started, delay buffer, RC3, voltage, transients, neuron
  28. [28] § 4. Results › 4.5. PS4: Rate Coding Baseline Comparison ↔ experiments/PS4_RateBaseline.ipynb, lines 303–344 · score 0.55 · Gate PS G4, topological diversity, ISI range, DFC orbits, PS4, Baseline
  29. [29] § 3. Experimental Pipeline › 3.5. PS3: Full Write–Read–Erase Demonstration ↔ experiments/PS5_MaxCapacity.ipynb, lines 278–351 · score 0.55 · evenly spacing, ISI window, capacity, accuracy, Phase, orbits
  30. [30] § 3. Experimental Pipeline › 3.3. PS1: Write Protocol ↔ experiments/PS1_WriteProtocol.ipynb, lines 316–358 · score 0.55 · minimum pairwise ISI, maximizes, greedy, PS1, protocol, cataloged
  31. [31] § 2. Mathematical Framework › 2.3. Orbit Library and Memory Formalism ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 522–667 · score 0.54 · burst_p, qualitative categories, triplet, doublet, tonic, ISIs
  32. [32] § 4. Results › 4.5. PS4: Rate Coding Baseline Comparison ↔ experiments/PS4_RateBaseline.ipynb, lines 346–413 · score 0.54 · Firing rate, HH rate, ISI CV, Hz, PS4, threshold
  33. [33] § 2. Mathematical Framework › 2.3. Orbit Library and Memory Formalism ↔ experiments/PS0_SecondaryIbias.ipynb, lines 382–458 · score 0.53 · burst_p, qualitative categories, triplet, doublet, tonic, ISIs
  34. [34] § Appendix A. HH Rate Functions and Parameters ↔ experiments/PS0_OrbitCatalogue.ipynb, lines 110–232 · score 0.53 · warm start, delay buffer, RC3, voltage, neuron, DFC
  35. [35] § 2. Mathematical Framework › 2.4. ISI-Based Orbit Classification: POLD ↔ experiments/PS2_ReadProtocol.ipynb, lines 411–512 · score 0.51 · ISI sequence, floored, segment, correlation, zero, score
  36. [36] § 4. Results › 4.2. PS1: Write Protocol Performance ↔ experiments/PS1_WriteProtocol.ipynb, lines 1009–1109 · score 0.50 · Gate PS G1, orbit switching matrix, settling, PS1, Protocol, delay

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,038 lines · 35 KB · MIT · 8 matches

  1. # %% [markdown]
  2. # # PS0 — Dense Orbit Catalogue
  3. # ## Option C: HH Delay-Directed Orbit Selection
  4. # ### HHSMC Project — Corrected Pipeline — February 2026
  5. #
  6. # **Purpose:** Build comprehensive catalogue of all stable periodic orbits accessible via (K, τ) parameter switching at fixed I_bias.
  7. #
  8. # **Prerequisite:** ExpF0v3 → Gate G0 FAILED → Track B (Option C)
  9. # - Strong chaos found (λ₁ up to 0.218 ms⁻¹) but only 2–4 ISI clusters
  10. # - 75.1% periodic (5,711/7,600 pts) is the opportunity
  11. #
  12. # **Gate PS-G0:** ≥15 distinct orbit types, ≥3 qualitative categories
  13. # %% [markdown]
  14. # ## CELL 1 — Setup and Imports
  15. # %%
  16. import numpy as np
  17. from numba import njit
  18. import matplotlib.pyplot as plt
  19. import matplotlib.colors as mcolors
  20. from matplotlib.colors import ListedColormap
  21. import json, os, time, warnings
  22. from datetime import datetime
  23. from scipy.cluster.hierarchy import fcluster, linkage
  24. from scipy.spatial.distance import pdist, squareform
  25. from collections import Counter
  26. # --- Google Drive Mount ---
  27. try:
  28. from google.colab import drive
  29. drive.mount('/content/drive')
  30. OUTPUT_DIR = '/content/drive/My Drive/HHSMC/full_study/PS0_orbit_catalogue'
  31. ON_COLAB = True
  32. except ImportError:
  33. OUTPUT_DIR = './PS0_results'
  34. ON_COLAB = False
  35. os.makedirs(OUTPUT_DIR, exist_ok=True)
  36. print(f"Output directory: {OUTPUT_DIR}")
  37. print(f"Timestamp: {datetime.now().isoformat()}")
  38. print(f"Phase PS0 — Dense Orbit Catalogue for Option C")
  39. # %% [markdown]
  40. # ## CELL 2 — HH Model (V-shifted convention, rest = 0)
  41. # %%
  42. # --- Fixed biophysical parameters ---
  43. C_M = 1.0 # μF/cm²
  44. G_NA = 120.0 # mS/cm²
  45. G_K = 36.0 # mS/cm²
  46. G_L = 0.3 # mS/cm²
  47. E_NA = 115.0 # mV (V-shifted)
  48. E_K = -12.0 # mV (V-shifted)
  49. E_L = 10.6 # mV (V-shifted)
  50. DT = 0.01 # ms integration step
  51. @njit
  52. def alpha_m(V):
  53. """Sodium activation rate with L'Hôpital limit."""
  54. x = 25.0 - V
  55. if abs(x) < 1e-7:
  56. return 1.0
  57. return 0.1 * x / (np.exp(x / 10.0) - 1.0)
  58. @njit
  59. def beta_m(V):
  60. return 4.0 * np.exp(-V / 18.0)
  61. @njit
  62. def alpha_h(V):
  63. return 0.07 * np.exp(-V / 20.0)
  64. @njit
  65. def beta_h(V):
  66. return 1.0 / (np.exp((30.0 - V) / 10.0) + 1.0)
  67. @njit
  68. def alpha_n(V):
  69. """Potassium activation rate with L'Hôpital limit."""
  70. x = 10.0 - V
  71. if abs(x) < 1e-7:
  72. return 0.1
  73. return 0.01 * x / (np.exp(x / 10.0) - 1.0)
  74. @njit
  75. def beta_n(V):
  76. return 0.125 * np.exp(-V / 80.0)
  77. @njit
  78. def hh_rhs(V, m, h, n, I_total):
  79. """Right-hand side of HH equations."""
  80. I_Na = G_NA * m*m*m * h * (V - E_NA)
  81. I_K = G_K * n*n*n*n * (V - E_K)
  82. I_L = G_L * (V - E_L)
  83. dV = (I_total - I_Na - I_K - I_L) / C_M
  84. dm = alpha_m(V) * (1.0 - m) - beta_m(V) * m
  85. dh = alpha_h(V) * (1.0 - h) - beta_h(V) * h
  86. dn = alpha_n(V) * (1.0 - n) - beta_n(V) * n
  87. return dV, dm, dh, dn
  88. @njit
  89. def hh_steady_state(V):
  90. """Steady-state gating variables at voltage V."""
  91. am = alpha_m(V); bm = beta_m(V)
  92. ah = alpha_h(V); bh = beta_h(V)
  93. an = alpha_n(V); bn = beta_n(V)
  94. return am/(am+bm), ah/(ah+bh), an/(an+bn)
  95. # %% [markdown]
  96. # ## CELL 3 — Simulation Engine (HH + DFC with RK4)
  97. # %%
  98. @njit
  99. def simulate_hh_dfc_full(I_bias, K, tau_ms, T_total_ms, dt=0.01,
  100. T_transient_ms=0.0):
  101. """
  102. Simulate HH neuron with Pyragas DFC. Returns spike times, ISIs,
  103. and the full voltage trace for pattern analysis.
  104. Corrections applied:
  105. - RC3: Warm-started delay buffer (free-run for τ ms)
  106. - DFC recomputed at each RK4 substep
  107. - L'Hôpital limits in all rate functions
  108. """
  109. n_steps = int(T_total_ms / dt)
  110. buf_size = max(int(tau_ms / dt), 1)
  111. # Initial conditions at rest
  112. V = 0.0
  113. m, h, n = hh_steady_state(V)
  114. # --- Warm-start delay buffer (RC3 fix) ---
  115. V_buf = np.zeros(buf_size)
  116. for ws in range(buf_size):
  117. I_total = I_bias # No DFC during warmup
  118. dV1, dm1, dh1, dn1 = hh_rhs(V, m, h, n, I_total)
  119. V2 = V + 0.5*dt*dV1; m2 = m + 0.5*dt*dm1
  120. h2 = h + 0.5*dt*dh1; n2 = n + 0.5*dt*dn1
  121. dV2, dm2, dh2, dn2 = hh_rhs(V2, m2, h2, n2, I_total)
  122. V3 = V + 0.5*dt*dV2; m3 = m + 0.5*dt*dm2
  123. h3 = h + 0.5*dt*dh2; n3 = n + 0.5*dt*dn2
  124. dV3, dm3, dh3, dn3 = hh_rhs(V3, m3, h3, n3, I_total)
  125. V4 = V + dt*dV3; m4 = m + dt*dm3
  126. h4 = h + dt*dh3; n4 = n + dt*dn3
  127. dV4, dm4, dh4, dn4 = hh_rhs(V4, m4, h4, n4, I_total)
  128. V = V + (dt/6.0)*(dV1+2*dV2+2*dV3+dV4)
  129. m = min(max(m + (dt/6.0)*(dm1+2*dm2+2*dm3+dm4), 0.0), 1.0)
  130. h = min(max(h + (dt/6.0)*(dh1+2*dh2+2*dh3+dh4), 0.0), 1.0)
  131. n = min(max(n + (dt/6.0)*(dn1+2*dn2+2*dn3+dn4), 0.0), 1.0)
  132. V_buf[ws % buf_size] = V
  133. buf_idx = 0
  134. # --- Main simulation with DFC ---
  135. max_spikes = int(T_total_ms / 2) + 100
  136. spike_times_raw = np.empty(max_spikes)
  137. n_spikes_raw = 0
  138. V_prev = V
  139. # Store voltage trace (subsampled: every 10 steps = 0.1 ms)
  140. subsample = 10
  141. n_trace = n_steps // subsample + 1
  142. V_trace = np.empty(n_trace)
  143. trace_idx = 0
  144. for step in range(n_steps):
  145. # Read delayed voltage
  146. V_delayed = V_buf[buf_idx]
  147. # DFC control
  148. I_ctrl = K * (V_delayed - V)
  149. I_total = I_bias + I_ctrl
  150. # RK4 with DFC recomputed at each substep
  151. dV1, dm1, dh1, dn1 = hh_rhs(V, m, h, n, I_total)
  152. Vk2 = V + 0.5*dt*dV1
  153. I_ctrl_2 = K * (V_delayed - Vk2)
  154. mk2 = m + 0.5*dt*dm1; hk2 = h + 0.5*dt*dh1; nk2 = n + 0.5*dt*dn1
  155. dV2, dm2, dh2, dn2 = hh_rhs(Vk2, mk2, hk2, nk2, I_bias + I_ctrl_2)
  156. Vk3 = V + 0.5*dt*dV2
  157. I_ctrl_3 = K * (V_delayed - Vk3)
  158. mk3 = m + 0.5*dt*dm2; hk3 = h + 0.5*dt*dh2; nk3 = n + 0.5*dt*dn2
  159. dV3, dm3, dh3, dn3 = hh_rhs(Vk3, mk3, hk3, nk3, I_bias + I_ctrl_3)
  160. Vk4 = V + dt*dV3
  161. I_ctrl_4 = K * (V_delayed - Vk4)
  162. mk4 = m + dt*dm3; hk4 = h + dt*dh3; nk4 = n + dt*dn3
  163. dV4, dm4, dh4, dn4 = hh_rhs(Vk4, mk4, hk4, nk4, I_bias + I_ctrl_4)
  164. V_new = V + (dt/6.0)*(dV1 + 2*dV2 + 2*dV3 + dV4)
  165. m_new = min(max(m + (dt/6.0)*(dm1 + 2*dm2 + 2*dm3 + dm4), 0.0), 1.0)
  166. h_new = min(max(h + (dt/6.0)*(dh1 + 2*dh2 + 2*dh3 + dh4), 0.0), 1.0)
  167. n_new = min(max(n + (dt/6.0)*(dn1 + 2*dn2 + 2*dn3 + dn4), 0.0), 1.0)
  168. # Update delay buffer
  169. V_buf[buf_idx] = V_new
  170. buf_idx = (buf_idx + 1) % buf_size
  171. # Spike detection: rising threshold crossing at V = 0 mV
  172. if V_prev <= 0.0 and V_new > 0.0:
  173. if n_spikes_raw < max_spikes:
  174. spike_times_raw[n_spikes_raw] = step * dt
  175. n_spikes_raw += 1
  176. # Subsample voltage
  177. if step % subsample == 0 and trace_idx < n_trace:
  178. V_trace[trace_idx] = V_new
  179. trace_idx += 1
  180. V_prev = V_new
  181. V = V_new; m = m_new; h = h_new; n = n_new
  182. # Extract spikes in analysis window
  183. spike_times = spike_times_raw[:n_spikes_raw]
  184. V_trace = V_trace[:trace_idx]
  185. # Filter to analysis window
  186. if T_transient_ms > 0:
  187. mask = spike_times >= T_transient_ms
  188. spike_times_analysis = spike_times[mask]
  189. else:
  190. spike_times_analysis = spike_times
  191. if len(spike_times_analysis) >= 2:
  192. isi_array = np.diff(spike_times_analysis)
  193. else:
  194. isi_array = np.empty(0)
  195. return spike_times_analysis, isi_array, V_trace
  196. # %% [markdown]
  197. # ## CELL 4 — ISI Pattern Analysis Functions
  198. # %%
  199. @njit
  200. def isi_cv(isi_array):
  201. """Coefficient of variation of ISI series."""
  202. if len(isi_array) < 3:
  203. return -1.0
  204. mu = np.mean(isi_array)
  205. if mu < 1e-10:
  206. return -1.0
  207. return np.std(isi_array) / mu
  208. def detect_pattern_period(isis, max_period_spikes=12):
  209. """
  210. Detect repeating ISI pattern via autocorrelation.
  211. For periodic orbits with complex patterns (e.g., doublets, triplets),
  212. the ISI series has a repeating pattern: [a, b, a, b, ...] for doublets,
  213. [a, b, c, a, b, c, ...] for triplets, etc.
  214. Returns: (pattern_length, pattern_isis, confidence)
  215. pattern_length: number of ISIs in one pattern repeat (1=tonic)
  216. pattern_isis: the repeating ISI sequence
  217. confidence: normalized autocorrelation at pattern period
  218. """
  219. if len(isis) < 6:
  220. return 1, isis[:1] if len(isis) > 0 else np.array([0.0]), 0.0
  221. isis = np.array(isis, dtype=np.float64)
  222. n = len(isis)
  223. # Try pattern lengths 1 to max_period_spikes
  224. best_len = 1
  225. best_conf = 0.0
  226. best_pattern = isis[:1]
  227. for p in range(1, min(max_period_spikes + 1, n // 3 + 1)):
  228. # Check if ISI[i] ≈ ISI[i+p] for all i
  229. n_compare = min(n - p, 3 * p) # Compare at least 3 repetitions
  230. if n_compare < p:
  231. continue
  232. diffs = np.zeros(n_compare)
  233. for i in range(n_compare):
  234. diffs[i] = abs(isis[i] - isis[i + p])
  235. # Median absolute deviation from pattern repeat
  236. med_diff = np.median(diffs)
  237. med_isi = np.median(isis)
  238. if med_isi > 0:
  239. relative_error = med_diff / med_isi
  240. else:
  241. continue
  242. # Confidence: 1 - relative_error (capped at 0)
  243. conf = max(0.0, 1.0 - relative_error * 10.0)
  244. if conf > best_conf and conf > 0.8:
  245. best_len = p
  246. best_conf = conf
  247. # Extract pattern: average over repetitions
  248. pattern = np.zeros(p)
  249. count = 0
  250. for rep in range(n // p):
  251. start = rep * p
  252. if start + p <= n:
  253. pattern += isis[start:start+p]
  254. count += 1
  255. if count > 0:
  256. pattern /= count
  257. best_pattern = pattern
  258. # If best_len == 1, verify it's truly tonic (not just defaulting)
  259. if best_len == 1 and len(isis) >= 3:
  260. cv = np.std(isis) / np.mean(isis) if np.mean(isis) > 0 else 999
  261. if cv < 0.02:
  262. best_conf = 1.0
  263. best_pattern = np.array([np.mean(isis)])
  264. return best_len, best_pattern, best_conf
  265. def classify_dynamics(isis, spike_times, V_trace, T_analysis_ms):
  266. """
  267. Classify the dynamics at a (K, τ) point.
  268. Returns: dict with classification and fingerprint
  269. """
  270. result = {
  271. 'class': 'unknown',
  272. 'n_spikes': len(spike_times),
  273. 'isi_cv': -1.0,
  274. 'isi_mean': 0.0,
  275. 'isi_std': 0.0,
  276. 'firing_rate': 0.0,
  277. 'pattern_length': 0,
  278. 'pattern_isis': [],
  279. 'pattern_period_ms': 0.0,
  280. 'fingerprint': np.zeros(6),
  281. }
  282. # Silent
  283. if len(spike_times) < 5:
  284. result['class'] = 'silent'
  285. return result
  286. # Check for depolarization block (V stays high)
  287. if V_trace is not None and len(V_trace) > 100:
  288. last_quarter = V_trace[3*len(V_trace)//4:]
  289. if np.mean(last_quarter) > 30.0 and np.std(last_quarter) < 5.0:
  290. result['class'] = 'depol_block'
  291. return result
  292. cv = isi_cv(isis)
  293. result['isi_cv'] = cv
  294. result['isi_mean'] = float(np.mean(isis))
  295. result['isi_std'] = float(np.std(isis))
  296. result['firing_rate'] = len(spike_times) / (T_analysis_ms / 1000.0)
  297. if cv < 0:
  298. result['class'] = 'insufficient'
  299. return result
  300. # Classify by ISI variability
  301. if cv < 0.02:
  302. # Low variability: tonic or complex periodic
  303. p_len, p_isis, p_conf = detect_pattern_period(isis)
  304. result['pattern_length'] = p_len
  305. result['pattern_isis'] = p_isis.tolist()
  306. result['pattern_period_ms'] = float(np.sum(p_isis))
  307. if p_len == 1:
  308. result['class'] = 'tonic'
  309. else:
  310. result['class'] = f'periodic_p{p_len}'
  311. elif cv < 0.10:
  312. # Medium variability: could be complex periodic with jitter
  313. p_len, p_isis, p_conf = detect_pattern_period(isis)
  314. result['pattern_length'] = p_len
  315. result['pattern_isis'] = p_isis.tolist()
  316. result['pattern_period_ms'] = float(np.sum(p_isis))
  317. if p_conf > 0.7:
  318. result['class'] = f'periodic_p{p_len}'
  319. else:
  320. result['class'] = 'quasi_periodic'
  321. elif cv < 0.15:
  322. result['class'] = 'quasi_periodic'
  323. else:
  324. result['class'] = 'chaotic'
  325. # Build fingerprint vector for clustering
  326. # [mean_ISI, ISI_CV, pattern_length, pattern_period, firing_rate, ISI_range]
  327. isi_range = float(np.max(isis) - np.min(isis)) if len(isis) > 0 else 0.0
  328. result['fingerprint'] = np.array([
  329. result['isi_mean'],
  330. result['isi_cv'],
  331. float(result['pattern_length']),
  332. result['pattern_period_ms'],
  333. result['firing_rate'],
  334. isi_range,
  335. ])
  336. return result
  337. # %% [markdown]
  338. # ## CELL 5 — Dense Orbit Sweep
  339. # %%
  340. def run_dense_sweep(I_bias, K_range, tau_range,
  341. T_sim=3000.0, T_transient=500.0):
  342. """
  343. Dense sweep of (K, τ) plane at fixed I_bias.
  344. Parameters
  345. ----------
  346. I_bias : float — Fixed bias current (μA/cm²)
  347. K_range : array — DFC gain values
  348. tau_range : array — DFC delay values (ms)
  349. T_sim : float — Total simulation time (ms)
  350. T_transient : float — Transient to discard (ms)
  351. Returns
  352. -------
  353. results_grid : 2D array of classification dicts
  354. summary : dict with aggregate statistics
  355. """
  356. n_K = len(K_range)
  357. n_tau = len(tau_range)
  358. total = n_K * n_tau
  359. T_analysis = T_sim - T_transient
  360. print(f"Dense Sweep: {n_K} × {n_tau} = {total} grid points")
  361. print(f" I_bias = {I_bias:.1f} μA/cm²")
  362. print(f" K: [{K_range[0]:.3f}, {K_range[-1]:.3f}] ({n_K} values)")
  363. print(f" τ: [{tau_range[0]:.1f}, {tau_range[-1]:.1f}] ms ({n_tau} values)")
  364. print(f" T_sim = {T_sim:.0f} ms, T_transient = {T_transient:.0f} ms")
  365. # JIT warmup
  366. print("JIT warmup...", end=" ", flush=True)
  367. _ = simulate_hh_dfc_full(10.0, 0.5, 50.0, 200.0, DT, 100.0)
  368. print("done.")
  369. # Storage
  370. results_grid = [[None]*n_tau for _ in range(n_K)]
  371. class_grid = np.full((n_K, n_tau), -1, dtype=np.int32)
  372. isi_mean_grid = np.full((n_K, n_tau), np.nan)
  373. cv_grid = np.full((n_K, n_tau), np.nan)
  374. fr_grid = np.full((n_K, n_tau), np.nan)
  375. # Class encoding
  376. class_map = {
  377. 'silent': 0, 'depol_block': 1, 'tonic': 2,
  378. 'quasi_periodic': 3, 'chaotic': 4, 'insufficient': 5,
  379. 'unknown': 6,
  380. }
  381. # Periodic patterns: 10 + pattern_length
  382. # e.g., periodic_p2 → 12, periodic_p3 → 13
  383. t0 = time.time()
  384. done = 0
  385. for ik, K_val in enumerate(K_range):
  386. for it, tau_val in enumerate(tau_range):
  387. spk, isis, V_trace = simulate_hh_dfc_full(
  388. I_bias, K_val, tau_val, T_sim, DT, T_transient)
  389. result = classify_dynamics(isis, spk, V_trace, T_analysis)
  390. result['K'] = float(K_val)
  391. result['tau_ms'] = float(tau_val)
  392. results_grid[ik][it] = result
  393. # Encode class for grid
  394. cls = result['class']
  395. if cls in class_map:
  396. class_grid[ik, it] = class_map[cls]
  397. elif cls.startswith('periodic_p'):
  398. p = int(cls.split('_p')[1])
  399. class_grid[ik, it] = 10 + p
  400. else:
  401. class_grid[ik, it] = 6
  402. isi_mean_grid[ik, it] = result['isi_mean']
  403. cv_grid[ik, it] = result['isi_cv']
  404. fr_grid[ik, it] = result['firing_rate']
  405. done += 1
  406. if done % 500 == 0:
  407. elapsed = time.time() - t0
  408. rate = done / elapsed
  409. eta = (total - done) / rate
  410. print(f" [{done}/{total}] {elapsed:.0f}s elapsed, "
  411. f"~{eta:.0f}s remaining", flush=True)
  412. elapsed = time.time() - t0
  413. print(f"Dense sweep complete in {elapsed:.1f}s ({elapsed/60:.1f} min)")
  414. # --- Summary statistics ---
  415. class_counts = Counter()
  416. for ik in range(n_K):
  417. for it in range(n_tau):
  418. cls = results_grid[ik][it]['class']
  419. class_counts[cls] += 1
  420. print(f"\nClassification summary:")
  421. for cls, count in sorted(class_counts.items(), key=lambda x: -x[1]):
  422. pct = 100.0 * count / total
  423. print(f" {cls:20s}: {count:5d} ({pct:5.1f}%)")
  424. summary = {
  425. 'I_bias': I_bias,
  426. 'n_K': n_K,
  427. 'n_tau': n_tau,
  428. 'total_points': total,
  429. 'T_sim': T_sim,
  430. 'T_transient': T_transient,
  431. 'class_counts': dict(class_counts),
  432. 'elapsed_s': elapsed,
  433. }
  434. return results_grid, class_grid, isi_mean_grid, cv_grid, fr_grid, summary
  435. # %% [markdown]
  436. # ## CELL 6 — Orbit Fingerprint Clustering
  437. # %%
  438. def cluster_orbits(results_grid, K_range, tau_range,
  439. min_isi_separation=2.0, linkage_method='average'):
  440. """
  441. Cluster periodic orbits by ISI fingerprint.
  442. For all periodic points, compute pairwise distances in fingerprint
  443. space and cluster using agglomerative hierarchical clustering.
  444. Parameters
  445. ----------
  446. min_isi_separation : float — minimum ISI mean difference (ms) between
  447. distinct orbit types
  448. Returns
  449. -------
  450. orbit_types : list of dicts, one per distinct orbit type
  451. orbit_assignments : dict mapping (ik, it) → type_id
  452. """
  453. n_K = len(K_range)
  454. n_tau = len(tau_range)
  455. # Collect all periodic points
  456. periodic_points = []
  457. for ik in range(n_K):
  458. for it in range(n_tau):
  459. r = results_grid[ik][it]
  460. cls = r['class']
  461. if cls == 'tonic' or cls.startswith('periodic_p'):
  462. periodic_points.append({
  463. 'ik': ik, 'it': it,
  464. 'K': r['K'], 'tau_ms': r['tau_ms'],
  465. 'class': cls,
  466. 'isi_mean': r['isi_mean'],
  467. 'isi_cv': r['isi_cv'],
  468. 'pattern_length': r['pattern_length'],
  469. 'pattern_period_ms': r['pattern_period_ms'],
  470. 'pattern_isis': r['pattern_isis'],
  471. 'firing_rate': r['firing_rate'],
  472. 'fingerprint': r['fingerprint'],
  473. })
  474. n_periodic = len(periodic_points)
  475. print(f"\nOrbit clustering: {n_periodic} periodic points")
  476. if n_periodic == 0:
  477. print(" No periodic points found!")
  478. return [], {}
  479. # Build fingerprint matrix
  480. # Use: [isi_mean, pattern_period, pattern_length]
  481. # Weight ISI mean most heavily (primary distinguishing feature)
  482. fp_matrix = np.zeros((n_periodic, 3))
  483. for i, p in enumerate(periodic_points):
  484. fp_matrix[i, 0] = p['isi_mean'] # weight 1.0
  485. fp_matrix[i, 1] = p['pattern_period_ms'] * 0.5 # weight 0.5
  486. fp_matrix[i, 2] = p['pattern_length'] * 5.0 # weight 5.0 (categorical)
  487. # Pairwise distances
  488. if n_periodic > 1:
  489. dists = pdist(fp_matrix, metric='euclidean')
  490. Z = linkage(dists, method=linkage_method)
  491. # Cut at threshold such that clusters differ by at least min_isi_separation
  492. labels = fcluster(Z, t=min_isi_separation, criterion='distance')
  493. else:
  494. labels = np.array([1])
  495. n_clusters = len(set(labels))
  496. print(f" Found {n_clusters} orbit clusters (threshold={min_isi_separation} ms)")
  497. # Build orbit type catalogue
  498. orbit_types = []
  499. orbit_assignments = {}
  500. for cid in sorted(set(labels)):
  501. members = [periodic_points[i] for i in range(n_periodic)
  502. if labels[i] == cid]
  503. # Representative: point closest to cluster centroid in ISI mean
  504. isi_means = [m['isi_mean'] for m in members]
  505. centroid_isi = np.mean(isi_means)
  506. best_idx = np.argmin([abs(m['isi_mean'] - centroid_isi) for m in members])
  507. rep = members[best_idx]
  508. # Determine qualitative category
  509. p_len = rep['pattern_length']
  510. if p_len == 1:
  511. category = 'tonic'
  512. elif p_len == 2:
  513. category = 'doublet'
  514. elif p_len == 3:
  515. category = 'triplet'
  516. else:
  517. category = f'burst_p{p_len}'
  518. orbit_type = {
  519. 'type_id': int(cid),
  520. 'category': category,
  521. 'pattern_length': p_len,
  522. 'isi_mean': float(centroid_isi),
  523. 'isi_std': float(np.std(isi_means)),
  524. 'pattern_isis': rep['pattern_isis'],
  525. 'pattern_period_ms': rep['pattern_period_ms'],
  526. 'firing_rate': rep['firing_rate'],
  527. 'n_members': len(members),
  528. 'representative_K': rep['K'],
  529. 'representative_tau': rep['tau_ms'],
  530. 'K_range': [float(min(m['K'] for m in members)),
  531. float(max(m['K'] for m in members))],
  532. 'tau_range': [float(min(m['tau_ms'] for m in members)),
  533. float(max(m['tau_ms'] for m in members))],
  534. }
  535. orbit_types.append(orbit_type)
  536. # Record assignments
  537. for m in members:
  538. orbit_assignments[(m['ik'], m['it'])] = int(cid)
  539. # Sort by ISI mean
  540. orbit_types.sort(key=lambda x: x['isi_mean'])
  541. # Print catalogue
  542. print(f"\n{'='*80}")
  543. print(f"ORBIT TYPE CATALOGUE — {len(orbit_types)} distinct types")
  544. print(f"{'='*80}")
  545. print(f"{'ID':>4} {'Category':>10} {'PLen':>5} {'ISI_mean':>9} "
  546. f"{'ISI_std':>8} {'Period':>8} {'FR(Hz)':>8} {'Members':>8} "
  547. f"{'K*':>6} {'τ*':>8}")
  548. print("-"*80)
  549. for ot in orbit_types:
  550. print(f"{ot['type_id']:>4} {ot['category']:>10} "
  551. f"{ot['pattern_length']:>5} "
  552. f"{ot['isi_mean']:>9.2f} {ot['isi_std']:>8.3f} "
  553. f"{ot['pattern_period_ms']:>8.2f} {ot['firing_rate']:>8.1f} "
  554. f"{ot['n_members']:>8} {ot['representative_K']:>6.3f} "
  555. f"{ot['representative_tau']:>8.1f}")
  556. # Count categories
  557. cats = Counter(ot['category'] for ot in orbit_types)
  558. print(f"\nQualitative categories: {dict(cats)}")
  559. return orbit_types, orbit_assignments
  560. # %% [markdown]
  561. # ## CELL 7 — Gate PS-G0 Evaluation
  562. # %%
  563. def evaluate_gate_PS_G0(orbit_types, min_types=15, min_categories=3):
  564. """
  565. Gate PS-G0: Enough orbit diversity for meaningful memory?
  566. Requirements:
  567. - ≥15 distinct orbit types
  568. - Pairwise ISI mean difference > 2 ms for distinguishable pairs
  569. - At least 3 qualitative categories
  570. """
  571. print("\n" + "="*70)
  572. print("GATE PS-G0 — ORBIT DIVERSITY ASSESSMENT")
  573. print("="*70)
  574. n_types = len(orbit_types)
  575. categories = set(ot['category'] for ot in orbit_types)
  576. n_categories = len(categories)
  577. # Pairwise ISI separability
  578. n_separable_pairs = 0
  579. n_total_pairs = 0
  580. min_separation = float('inf')
  581. for i in range(n_types):
  582. for j in range(i+1, n_types):
  583. diff = abs(orbit_types[i]['isi_mean'] - orbit_types[j]['isi_mean'])
  584. n_total_pairs += 1
  585. if diff > 2.0:
  586. n_separable_pairs += 1
  587. min_separation = min(min_separation, diff)
  588. # Gate checks
  589. gate_types = n_types >= min_types
  590. gate_categories = n_categories >= min_categories
  591. gate_all = gate_types and gate_categories
  592. print(f"\n Orbit types found: {n_types} (gate: ≥{min_types})"
  593. f" {'✓ PASS' if gate_types else '✗ FAIL'}")
  594. print(f" Categories: {n_categories} {dict(Counter(ot['category'] for ot in orbit_types))}"
  595. f" (gate: ≥{min_categories})"
  596. f" {'✓ PASS' if gate_categories else '✗ FAIL'}")
  597. print(f" Separable pairs (Δ ISI > 2ms): {n_separable_pairs}/{n_total_pairs}")
  598. print(f" Minimum ISI separation: {min_separation:.3f} ms")
  599. if gate_all:
  600. print(f"\n ✓✓✓ GATE PS-G0: PASS — Proceed to Phase PS1 ✓✓✓")
  601. print(f" Orbit library has sufficient diversity for orbit-coded memory.")
  602. else:
  603. if not gate_types:
  604. print(f"\n ✗ Insufficient orbit types. Try denser grid or secondary I_bias values.")
  605. if not gate_categories:
  606. print(f"\n ✗ Insufficient qualitative categories.")
  607. print(f"\n ✗✗✗ GATE PS-G0: FAIL — Consider adjustments ✗✗✗")
  608. gate_result = {
  609. 'decision': 'PASS' if gate_all else 'FAIL',
  610. 'n_types': n_types,
  611. 'n_categories': n_categories,
  612. 'categories': list(categories),
  613. 'gate_types': gate_types,
  614. 'gate_categories': gate_categories,
  615. 'n_separable_pairs': n_separable_pairs,
  616. 'n_total_pairs': n_total_pairs,
  617. 'min_separation_ms': float(min_separation) if min_separation < float('inf') else 0.0,
  618. }
  619. print("="*70)
  620. return gate_result
  621. # %% [markdown]
  622. # ## CELL 8 — Visualization
  623. # %%
  624. def plot_bifurcation_map(class_grid, K_range, tau_range, I_bias, save_dir=None):
  625. """Bifurcation map: dynamics classification across (K, τ) plane."""
  626. fig, ax = plt.subplots(1, 1, figsize=(14, 8))
  627. # Custom colormap
  628. # 0:silent(black), 1:depol(gray), 2:tonic(blue), 3:quasi(yellow),
  629. # 4:chaotic(red), 5:insufficient(white), 6:unknown(white),
  630. # 10+p: periodic_p (greens)
  631. unique_classes = sorted(set(class_grid.flatten()))
  632. colors_dict = {
  633. 0: '#1a1a2e', 1: '#4a4a4a', 2: '#2196F3', 3: '#FFC107',
  634. 4: '#F44336', 5: '#EEEEEE', 6: '#EEEEEE',
  635. }
  636. # Periodic patterns: gradient from green to purple
  637. periodic_colors = ['#4CAF50', '#66BB6A', '#81C784', '#A5D6A7',
  638. '#00BCD4', '#26C6DA', '#4DD0E1', '#80DEEA',
  639. '#7E57C2', '#9575CD', '#B39DDB', '#CE93D8']
  640. # Map class codes to sequential indices for colormapping
  641. class_to_idx = {}
  642. color_list = []
  643. idx = 0
  644. for c in unique_classes:
  645. class_to_idx[c] = idx
  646. if c in colors_dict:
  647. color_list.append(colors_dict[c])
  648. elif c >= 10:
  649. p = c - 10
  650. color_list.append(periodic_colors[min(p - 1, len(periodic_colors) - 1)])
  651. else:
  652. color_list.append('#CCCCCC')
  653. idx += 1
  654. # Remap grid
  655. plot_grid = np.zeros_like(class_grid, dtype=float)
  656. for ik in range(len(K_range)):
  657. for it in range(len(tau_range)):
  658. plot_grid[ik, it] = class_to_idx.get(class_grid[ik, it], 0)
  659. cmap = ListedColormap(color_list)
  660. im = ax.pcolormesh(tau_range, K_range, plot_grid,
  661. cmap=cmap, shading='auto',
  662. vmin=-0.5, vmax=len(color_list)-0.5)
  663. ax.set_xlabel('τ (ms)', fontsize=13)
  664. ax.set_ylabel('K (DFC gain)', fontsize=13)
  665. ax.set_title(f'Bifurcation Map — HH + DFC at I_bias = {I_bias:.1f} μA/cm²',
  666. fontsize=14)
  667. ax.set_xscale('log')
  668. # Legend
  669. import matplotlib.patches as mpatches
  670. class_names = {
  671. 0: 'Silent', 1: 'Depol. Block', 2: 'Tonic', 3: 'Quasi-periodic',
  672. 4: 'Chaotic', 5: 'Insufficient',
  673. }
  674. handles = []
  675. for c in unique_classes:
  676. if c in class_names:
  677. name = class_names[c]
  678. elif c >= 10:
  679. name = f'Periodic p{c-10}'
  680. else:
  681. continue
  682. color = color_list[class_to_idx[c]]
  683. handles.append(mpatches.Patch(color=color, label=name))
  684. ax.legend(handles=handles, loc='upper right', fontsize=9,
  685. ncol=2, framealpha=0.9)
  686. plt.tight_layout()
  687. if save_dir:
  688. plt.savefig(os.path.join(save_dir, 'PS0_bifurcation_map.png'),
  689. dpi=300, bbox_inches='tight')
  690. plt.show()
  691. def plot_isi_landscape(isi_mean_grid, cv_grid, K_range, tau_range,
  692. I_bias, save_dir=None):
  693. """ISI mean and CV landscape plots."""
  694. fig, axes = plt.subplots(1, 2, figsize=(18, 7))
  695. # ISI mean
  696. ax = axes[0]
  697. isi_plot = np.copy(isi_mean_grid)
  698. isi_plot[isi_plot <= 0] = np.nan
  699. im = ax.pcolormesh(tau_range, K_range, isi_plot,
  700. cmap='viridis', shading='auto')
  701. ax.set_xlabel('τ (ms)', fontsize=12)
  702. ax.set_ylabel('K', fontsize=12)
  703. ax.set_title(f'Mean ISI (ms) — I_bias={I_bias:.1f}', fontsize=13)
  704. ax.set_xscale('log')
  705. plt.colorbar(im, ax=ax, label='Mean ISI (ms)')
  706. # ISI CV
  707. ax = axes[1]
  708. cv_plot = np.copy(cv_grid)
  709. cv_plot[cv_plot < 0] = np.nan
  710. im = ax.pcolormesh(tau_range, K_range, cv_plot,
  711. cmap='hot', shading='auto', vmin=0, vmax=0.5)
  712. ax.set_xlabel('τ (ms)', fontsize=12)
  713. ax.set_ylabel('K', fontsize=12)
  714. ax.set_title(f'ISI CV — I_bias={I_bias:.1f}', fontsize=13)
  715. ax.set_xscale('log')
  716. plt.colorbar(im, ax=ax, label='ISI CV')
  717. plt.tight_layout()
  718. if save_dir:
  719. plt.savefig(os.path.join(save_dir, 'PS0_isi_landscape.png'),
  720. dpi=300, bbox_inches='tight')
  721. plt.show()
  722. def plot_orbit_catalogue(orbit_types, save_dir=None):
  723. """Visualize the orbit type catalogue."""
  724. if not orbit_types:
  725. print("No orbit types to plot.")
  726. return
  727. n = len(orbit_types)
  728. fig, axes = plt.subplots(1, 3, figsize=(18, 6))
  729. # 1. ISI mean distribution
  730. ax = axes[0]
  731. isi_means = [ot['isi_mean'] for ot in orbit_types]
  732. categories = [ot['category'] for ot in orbit_types]
  733. cat_colors = {'tonic': '#2196F3', 'doublet': '#4CAF50',
  734. 'triplet': '#FF9800', 'burst_p4': '#9C27B0'}
  735. colors = [cat_colors.get(c, '#607D8B') for c in categories]
  736. ax.barh(range(n), isi_means, color=colors, edgecolor='black', alpha=0.8)
  737. ax.set_yticks(range(n))
  738. ax.set_yticklabels([f"T{ot['type_id']} ({ot['category']})"
  739. for ot in orbit_types], fontsize=8)
  740. ax.set_xlabel('Mean ISI (ms)', fontsize=11)
  741. ax.set_title('Orbit Types by ISI Mean', fontsize=12)
  742. ax.invert_yaxis()
  743. # 2. Pairwise separation matrix
  744. ax = axes[1]
  745. sep_matrix = np.zeros((n, n))
  746. for i in range(n):
  747. for j in range(n):
  748. sep_matrix[i, j] = abs(isi_means[i] - isi_means[j])
  749. im = ax.imshow(sep_matrix, cmap='YlOrRd', aspect='auto')
  750. ax.set_xlabel('Orbit Type', fontsize=11)
  751. ax.set_ylabel('Orbit Type', fontsize=11)
  752. ax.set_title('Pairwise ISI Separation (ms)', fontsize=12)
  753. plt.colorbar(im, ax=ax, label='|Δ ISI| (ms)')
  754. # 3. Parameter space coverage
  755. ax = axes[2]
  756. for ot in orbit_types:
  757. color = cat_colors.get(ot['category'], '#607D8B')
  758. ax.scatter(ot['representative_tau'], ot['representative_K'],
  759. c=color, s=80, edgecolors='black', linewidth=0.5, zorder=5)
  760. ax.annotate(f"T{ot['type_id']}", (ot['representative_tau'], ot['representative_K']),
  761. fontsize=7, ha='center', va='bottom')
  762. ax.set_xlabel('τ (ms)', fontsize=11)
  763. ax.set_ylabel('K', fontsize=11)
  764. ax.set_title('Representative Points in (K, τ) Space', fontsize=12)
  765. ax.set_xscale('log')
  766. plt.tight_layout()
  767. if save_dir:
  768. plt.savefig(os.path.join(save_dir, 'PS0_orbit_catalogue.png'),
  769. dpi=300, bbox_inches='tight')
  770. plt.show()
  771. # %% [markdown]
  772. # ## CELL 9 — JSON Serialization Helper
  773. # %%
  774. def clean_for_json(obj):
  775. """Convert numpy types for JSON serialization."""
  776. if isinstance(obj, dict):
  777. return {k: clean_for_json(v) for k, v in obj.items()}
  778. elif isinstance(obj, list):
  779. return [clean_for_json(v) for v in obj]
  780. elif isinstance(obj, (np.integer,)):
  781. return int(obj)
  782. elif isinstance(obj, (np.floating,)):
  783. return float(obj)
  784. elif isinstance(obj, np.ndarray):
  785. return obj.tolist()
  786. elif isinstance(obj, (np.bool_,)):
  787. return bool(obj)
  788. return obj
  789. # %% [markdown]
  790. # ## CELL 10 — MAIN EXECUTION
  791. # %%
  792. if __name__ == '__main__' or True: # Always run in Colab
  793. print("="*70)
  794. print("PS0 — DENSE ORBIT CATALOGUE")
  795. print("Option C: HH Delay-Directed Orbit Selection")
  796. print("="*70)
  797. # ---- Define parameter grid ----
  798. # Fixed I_bias from ExpF0v3 pilot (standard HH spiking regime)
  799. I_BIAS = 10.0 # μA/cm²
  800. # Dense (K, τ) grid — 10× denser than ExpF0v3
  801. K_range = np.arange(0.0, 2.02, 0.02) # 101 values: 0.00, 0.02, ..., 2.00
  802. tau_range = np.geomspace(1.0, 200.0, 100) # 100 values, log-spaced
  803. # Include K=0 as baseline (no DFC → natural tonic firing)
  804. print(f"\nGrid: {len(K_range)} × {len(tau_range)} "
  805. f"= {len(K_range)*len(tau_range)} points")
  806. print(f"I_bias = {I_BIAS:.1f} μA/cm²\n")
  807. # ==== DENSE SWEEP ====
  808. print("\n" + "="*50)
  809. print("PHASE PS0-A — Dense Parameter Sweep")
  810. print("="*50)
  811. (results_grid, class_grid, isi_mean_grid, cv_grid, fr_grid,
  812. summary) = run_dense_sweep(I_BIAS, K_range, tau_range,
  813. T_sim=3000.0, T_transient=500.0)
  814. # Save raw results
  815. np.savez_compressed(os.path.join(OUTPUT_DIR, 'PS0_grid_data.npz'),
  816. class_grid=class_grid,
  817. isi_mean_grid=isi_mean_grid,
  818. cv_grid=cv_grid,
  819. fr_grid=fr_grid,
  820. K_range=K_range,
  821. tau_range=tau_range)
  822. print(f"Grid data saved to {OUTPUT_DIR}")
  823. # ==== VISUALIZATION ====
  824. print("\n" + "="*50)
  825. print("PHASE PS0-B — Bifurcation Analysis")
  826. print("="*50)
  827. plot_bifurcation_map(class_grid, K_range, tau_range, I_BIAS,
  828. save_dir=OUTPUT_DIR)
  829. plot_isi_landscape(isi_mean_grid, cv_grid, K_range, tau_range,
  830. I_BIAS, save_dir=OUTPUT_DIR)
  831. # ==== ORBIT CLUSTERING ====
  832. print("\n" + "="*50)
  833. print("PHASE PS0-C — Orbit Fingerprint Clustering")
  834. print("="*50)
  835. orbit_types, orbit_assignments = cluster_orbits(
  836. results_grid, K_range, tau_range,
  837. min_isi_separation=2.0)
  838. plot_orbit_catalogue(orbit_types, save_dir=OUTPUT_DIR)
  839. # Save orbit catalogue
  840. with open(os.path.join(OUTPUT_DIR, 'PS0_orbit_types.json'), 'w') as f:
  841. json.dump(clean_for_json(orbit_types), f, indent=2)
  842. with open(os.path.join(OUTPUT_DIR, 'PS0_summary.json'), 'w') as f:
  843. json.dump(clean_for_json(summary), f, indent=2)
  844. print(f"\nOrbit catalogue saved to {OUTPUT_DIR}")
  845. # ==== GATE PS-G0 ====
  846. print("\n")
  847. gate_result = evaluate_gate_PS_G0(orbit_types, min_types=15, min_categories=3)
  848. with open(os.path.join(OUTPUT_DIR, 'gate_PS_G0_result.json'), 'w') as f:
  849. json.dump(clean_for_json(gate_result), f, indent=2)
  850. # ==== SECONDARY I_BIAS SWEEPS (if needed) ====
  851. if gate_result['decision'] == 'FAIL':
  852. print("\n" + "="*50)
  853. print("SECONDARY SWEEPS — Additional I_bias values")
  854. print("="*50)
  855. print(" Primary sweep at I_bias=10.0 insufficient.")
  856. print(" Running secondary sweeps at I_bias = 7.0, 8.5, 12.0, 15.0")
  857. print(" (Set SECONDARY_SWEEPS = True and re-run this cell)")
  858. # ==== FINAL SUMMARY ====
  859. print(f"\n{'='*70}")
  860. print(f"PS0 COMPLETE — ALL RESULTS SAVED")
  861. print(f"{'='*70}")
  862. print(f"Output directory: {OUTPUT_DIR}")
  863. print(f"Files:")
  864. print(f" PS0_grid_data.npz — Raw classification grid")
  865. print(f" PS0_orbit_types.json — Orbit type catalogue")
  866. print(f" PS0_summary.json — Sweep summary statistics")
  867. print(f" gate_PS_G0_result.json — Gate decision")
  868. print(f" PS0_bifurcation_map.png — (K, τ) dynamics heatmap")
  869. print(f" PS0_isi_landscape.png — ISI mean and CV landscapes")
  870. print(f" PS0_orbit_catalogue.png — Orbit types visualization")
  871. print(f"\nGate PS-G0: {gate_result['decision']}")
  872. if gate_result['decision'] == 'PASS':
  873. print(f" → PROCEED TO PHASE PS1 (Write Protocol & Settling Time)")
  874. else:
  875. print(f" → Run secondary I_bias sweeps or adjust clustering threshold")

PS0_OrbitCatalogue.ipynb at commit 465b302, under MIT · at the source

Overview

Authors: Mohammad O Alhawarat1, Ayman J Alnsour1, Mohammed A F Al-Husainy1, Khalil M Abdelnaby1,2
  1. Faculty of Information Technology, Al-Ahliyya Amman University, Amman 19328, Jordan; (A.J.A.); (M.A.F.A.-H.); (K.M.A.)
  2. Systems and Computers Engineering Department, Faculty of Engineering, Al-Azhar University, Nasr City, Cairo 11765, Egypt
Journal: Entropy (Basel, Switzerland), volume 28, issue 6, article 678
Dates: received 17 April 2026; accepted 4 June 2026; published online 11 June 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI · PMCID PMC13298313
Status: code verified
Categories: computational modeling (no new data) (modality), none (in silico) (organism)
Keywords: Hodgkin–Huxley model, parametric delayed feedback, limit-cycle multiplicity, orbit-coded memory, bifurcation landscape, autaptic feedback, neuromorphic computing
Citations: not cited yet (Europe PMC); 89 references in the paper

Abstract

We show that a single Hodgkin–Huxley (HH) neuron with Pyragas-type delayed feedback control (DFC) can store multiple symbols as stable periodic orbits, where the specific orbit is selected by tuning the DFC gain K and time delay τ. Sweeping the (K,τ) parameter plane at fixed bias current Ibias = 10.0 μA/cm2 reveals 207 orbit types across 12 topological categories, with inter-spike interval (ISI) means from 5.9 to 56.9 ms. We establish: (i) a write protocol that reliably locks orbits with 13.9 ms median settling time; (ii) a novel Pattern-Oriented Limit-cycle Decoder (POLD) that reads orbits at 100% accuracy from only five observed ISIs (1200 trials across 12 orbits; Wilson 95% CI: 99.7–100%); (iii) a complete single-symbol write–read–erase (W–R–E) cycle with 100% read accuracy, 92% erase verification, and no decay over hold durations up to 50 s; and (iv) a fully validated 12-symbol memory capacity with a read-discriminable upper bound of 67 symbols (11.2× over rate coding; write viability confirmed only for the conservative 12-symbol subset). Reliable orbit addressing needs delay precision of ±2%, which constitutes a write-precision specification and not a fundamental capacity limit. These findings show that parametric delayed feedback is a viable mechanism for limit-cycle-based information storage in conductance-based spiking neurons. The biological interpretation is analogical, not direct: the ±2% delay-precision requirement exceeds what has been demonstrated for biological autaptic variability, and the orbit-coded memory framing is best understood as a computational proof-of-principle aimed at neuromorphic engineering, not as a claim about biological working memory.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 36 matches between paragraphs and lines of code.

malhawarat/HH-DFC-OrbitMemory

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 465b302c206d7fcb197fc7c26ab2b35914991471, 4 July 2026
Languages: Jupyter (9)
Size: 11 files, 9 scripts
Software Heritage: not archived
Found in: “Supplementary Materials”
Holds: README, license file, 9 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (9 files), Numba (9 files), NumPy (9 files), SciPy (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
11 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 9 scripts, each with its path and the digest of its content;
  • 36 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability Statement

All simulation code (six Jupyter notebooks, PS0–PS5), processed output data, and the complete orbit catalog are available in a public GitHub repository at https://github.com/malhawarat/HH-DFC-OrbitMemory (accessed on 1 June 2026). Raw simulation outputs exceeding the repository size limit are available from the corresponding author upon reasonable request.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 7 keywords, 80 references.

Cite

This paper

Alhawarat, M. O., Alnsour, A. J., Al-Husainy, M. A. F., & Abdelnaby, K. M. (2026). Limit-Cycle Proliferation Under Parametric Delayed Feedback in a Conductance-Based Neuron: Bifurcation Landscape, Orbit Catalog, and Capacity Analysis. Entropy (Basel, Switzerland), 28(6), 678.

BibTeX

@article{alhawarat2026limit,
author = {Alhawarat, Mohammad O and Alnsour, Ayman J and Al-Husainy, Mohammed A F and Abdelnaby, Khalil M},
title = {{Limit-Cycle Proliferation Under Parametric Delayed Feedback in a Conductance-Based Neuron: Bifurcation Landscape, Orbit Catalog, and Capacity Analysis}},
journal = {Entropy (Basel, Switzerland)},
year = {2026},
month = jun,
volume = {28},
number = {6},
pages = {678},
publisher = {Multidisciplinary Digital Publishing Institute (MDPI)},
issn = {1099-4300},
pmcid = {PMC13298313}
}

RIS

TY - JOUR
AU - Alhawarat, Mohammad O
AU - Alnsour, Ayman J
AU - Al-Husainy, Mohammed A F
AU - Abdelnaby, Khalil M
TI - Limit-Cycle Proliferation Under Parametric Delayed Feedback in a Conductance-Based Neuron: Bifurcation Landscape, Orbit Catalog, and Capacity Analysis
T2 - Entropy (Basel, Switzerland)
J2 - Entropy (Basel)
PY - 2026
DA - 2026/06/01
VL - 28
IS - 6
SP - 678
SN - 1099-4300
PB - Multidisciplinary Digital Publishing Institute (MDPI)
LA - en
ER -

CSL-JSON

{
"id": "pmcid:PMC13298313",
"type": "article-journal",
"title": "Limit-Cycle Proliferation Under Parametric Delayed Feedback in a Conductance-Based Neuron: Bifurcation Landscape, Orbit Catalog, and Capacity Analysis",
"container-title": "Entropy (Basel, Switzerland)",
"author": [
{
"family": "Alhawarat",
"given": "Mohammad O"
},
{
"family": "Alnsour",
"given": "Ayman J"
},
{
"family": "Al-Husainy",
"given": "Mohammed A F"
},
{
"family": "Abdelnaby",
"given": "Khalil M"
}
],
"container-title-short": "Entropy (Basel)",
"volume": "28",
"issue": "6",
"page": "678",
"PMCID": "PMC13298313",
"ISSN": "1099-4300",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pcbi.1014577 [code]
Combining sampling and attractor dynamics in spiking models of head direction systems.
Journal: PLoS computational biology
In common: SciPy, Matplotlib, NumPy, computational modeling (no new data), 4 references
[2] doi:10.1038/s41467-026-75704-3 [code]
A minimal model of working memory in neural systems and neuromorphic circuits.
Journal: Nature communications
In common: Numba, SciPy, Matplotlib, 1 other tool, computational modeling (no new data), 2 references
[3] doi:10.1038/s41467-026-74460-8 [code]
Spike-based alignment learning solves the weight transport problem.
Journal: Nature communications
In common: Numba, SciPy, Matplotlib, 1 other tool, computational modeling (no new data), 2 references
[4] doi:10.1371/journal.pcbi.1014585 [code]
Decoding behavior with minimal and interpretable agent models.
Journal: PLoS computational biology
In common: Numba, SciPy, Matplotlib, 1 other tool, computational modeling (no new data), 2 references
[5] doi:10.1007/s00422-026-01048-2 [code]
A quality measure for repeating multiple-unit spike patterns.
Journal: Biological cybernetics
In common: Matplotlib, NumPy, none (in silico), 3 references
[6] doi:10.1038/s41467-026-75924-7 [code]
Data-driven reduced modeling of neural dynamics.
Journal: Nature communications
In common: SciPy, Matplotlib, NumPy, none (in silico), computational modeling (no new data), 2 references
[7] doi:10.1021/acs.chemrev.5c00878
Self-Oscillatory Neuron-like Devices for Unconventional Computing Applications.
Journal: Chemical reviews
In common: 4 references
[8] doi:10.1038/s41467-026-74243-1 [code]
Spiking neural network decoders of finger forces from high-density intramuscular microelectrode arrays.
Journal: Nature communications
In common: SciPy, Matplotlib, NumPy, 2 references
[9] doi: [code]
Going deeper with morphologically detailed neural networks by simulation-based gradient propagation
Journal: Frontiers in computational neuroscience
In common: SciPy, Matplotlib, NumPy, none (in silico), computational modeling (no new data), 1 reference
[10] doi:10.1371/journal.pcbi.1014458 [code]
Neuronal excitability and parameter variability in the Hodgkin-Huxley model.
Journal: PLoS computational biology
In common: SciPy, Matplotlib, NumPy, none (in silico), computational modeling (no new data), 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.