OSCR

HCN1 is a primary HCN Pacemaker Channel in Neurons.

Code ↔ Paper

3 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 3 matches
  1. [1] § Methods › Simulations of the effects of HCN1 channels in an SCN neuron ↔ HCN_rest-singlePo.py, lines 77–187 · score 0.71 · HCN1 open probability, Brian2 simulation, neuron, leak, trace
  2. [2] § Methods › Simulations of the effects of HCN1 channels in an SCN neuron ↔ HCN_rest-singlePo.py, lines 26–75 · score 0.71 · potassium reversal potential, steady state, membrane potential, dt, HCN1
  3. [3] § Methods › Simulations of the effects of HCN1 channels in an SCN neuron ↔ HCN_rest-singlePo.py, lines 77–187 · score 0.57 · voltage ramp, membrane potential, duration, Simulations, HCN, injected

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

Python · 905 lines · 36 KB · no license · 3 matches

  1. # Copyright (c) 2026 Christoph Schmidt-Hieber
  2. # SPDX-License-Identifier: MIT
  3. # Simulation of HCN channel effects on membrane potential during voltage ramps
  4. # Code accompanying Enke et al., 2026
  5. # v2026-01-30
  6. # Simplified code to compute capacitance from specified membrane time constant (tau_m)
  7. # v2026-01-21
  8. # LLM assistance was used to:
  9. # - document function arguments and return values
  10. # - assist with plotting
  11. # - assist with exporting the data to Excel
  12. # - produce data structures for the Excel export
  13. # - produce data structures to store the results of the simulations
  14. # - make the code more readable
  15. from brian2 import *
  16. import numpy as np
  17. import pandas as pd
  18. import matplotlib.pyplot as plt
  19. import matplotlib.gridspec as gridspec
  20. from scipy.interpolate import interp1d
  21. from itertools import product
  22. from pathlib import Path
  23. import pickle
  24. print("\n".join([f"{name}: {version}" for name, version in [(name, globals()[name].__version__ if hasattr(globals()[name], '__version__') else 'N/A') for name in ['brian2', 'numpy', 'pandas', 'matplotlib', 'scipy', 'itertools', 'pathlib', 'pickle']] if name in globals()]))
  25. def compute_conductances(V_rest, E_K_param, R_in, scaling, p_rest_rep):
  26. """
  27. Compute leak and HCN conductances from the biophysical parameters.
  28. Args:
  29. V_rest: Resting membrane potential (quantity).
  30. E_K_param: Potassium reversal potential (quantity).
  31. R_in: Input resistance (quantity).
  32. scaling: Dimensionless scaling factor for HCNX.
  33. p_rest_rep: Resting open probability (float).
  34. Returns:
  35. (g_leak, g_HCN_full, g_HCNX, G_total) as siemens quantities.
  36. """
  37. # Remove units and convert to float
  38. V_r = float(V_rest / volt)
  39. E_K_val = float(E_K_param / volt)
  40. E_HCN_val = float(E_HCN / volt)
  41. G_total = float(1 / R_in / siemens)
  42. # At the beginning of the ramp we assume that the system is at
  43. # a steady state and therefore we have two conditions:
  44. # 1) dV/dt is zero, so the net sum of currents is zero.
  45. # 2) Membrane potential is at V_rest
  46. # The total conductance of the cell is given by G_total = 1/R_in
  47. # and composed of g_leak + g_HCN1_rest + g_HCNX, where:
  48. # g_HCN1_rest = p_rest_rep * g_HCN_full
  49. # g_HCNX = scaling * g_HCN_full
  50. # Therefore:
  51. # g_leak + (p_rest_rep + scaling) * g_HCN_full = G_total [eq 1]
  52. # Net sum of currents at steady state is zero, therefore:
  53. # g_leak * (V_rest - E_K) + g_HCN1_rest * (V_rest - E_HCN) + g_HCNX * (V_rest - E_HCN) = 0
  54. # We can ignore capacitive currents because dV/dt is zero at steady state, hence C*dV/dt = 0.
  55. # Rearranging yields:
  56. # g_leak * (V_rest - E_K) + (p_rest_rep + scaling) * g_HCN_full * (V_rest - E_HCN) = 0 [eq 2]
  57. # Solving equation 1 for g_leak: g_leak = G_total - (p_rest_rep + scaling) * g_HCN_full,
  58. # and substituting into equation 2 yields:
  59. # G_total * (V_rest - E_K) + (p_rest_rep + scaling) * g_HCN_full * (E_K - E_HCN) = 0
  60. # Rearranging to isolate g_HCN_full gives:
  61. # G_total * (V_rest - E_K) = (p_rest_rep + scaling) * g_HCN_full * (E_HCN - E_K)
  62. # Solving for g_HCN_full yields:
  63. # g_HCN_full = G_total * (V_rest - E_K) / ((p_rest_rep + scaling) * (E_HCN_val - E_K_val))
  64. denom = (p_rest_rep + scaling) * (E_HCN_val - E_K_val)
  65. g_HCN_full = G_total * (V_r - E_K_val) / denom
  66. g_HCNX = scaling * g_HCN_full
  67. g_HCN1_rest = p_rest_rep * g_HCN_full
  68. g_leak = G_total - g_HCN1_rest - g_HCNX
  69. return g_leak * siemens, g_HCN_full * siemens, g_HCNX * siemens, G_total * siemens
  70. def run_sim(V_rest, E_K_param, R_in, scaling,
  71. P_HCN1_timed, t_interp, sim_duration, t_start_ramp, dVdt_target,
  72. p_rest_rep,
  73. inject_ramp=False, label="sim", rep_idx=None):
  74. """
  75. Run a single Brian2 simulation and store summary/trace outputs.
  76. Args:
  77. V_rest: Resting membrane potential (quantity).
  78. E_K_param: Potassium reversal potential (quantity).
  79. R_in: Input resistance (quantity).
  80. scaling: Dimensionless scaling factor for HCNX.
  81. P_HCN1_timed: TimedArray of HCN1 open probability.
  82. t_interp: Time vector with units for ramp/current construction.
  83. sim_duration: Total simulation duration (quantity).
  84. t_start_ramp: Time when the ramp starts (quantity).
  85. dVdt_target: Target voltage ramp slope (quantity).
  86. p_rest_rep: Resting open probability (float).
  87. inject_ramp: Whether to inject the compensatory ramp current.
  88. label: Key for storing trace outputs.
  89. rep_idx: Replicate identifier for summary output.
  90. """
  91. start_scope()
  92. g_leak_run, g_HCN_full_run, g_HCNX_run, G_total_run = compute_conductances(
  93. V_rest, E_K_param, R_in, scaling, p_rest_rep
  94. )
  95. # Compute total membrane capacitance from specified membrane time constant:
  96. # C_total = tau_m / R_in (since tau = R * C -> C = tau / R)
  97. C_total = tau_m / R_in
  98. if inject_ramp:
  99. V_ramp = V_start + dVdt_target * t_interp
  100. n_hold = int(hold_duration * ms / defaultclock.dt)
  101. I_hold = np.zeros(n_hold) * amp
  102. t_ramp = t_interp[n_hold:]
  103. V_ramp_dyn = V_ramp[n_hold:]
  104. P_HCN_dyn = P_HCN1_timed(t_ramp)
  105. I_ramp = -(
  106. C_total * dVdt_target
  107. - g_leak_run * (V_ramp_dyn - E_K_param)
  108. - g_HCN_full_run * P_HCN_dyn * (V_ramp_dyn - E_HCN)
  109. - g_HCNX_run * (V_ramp_dyn - E_HCN)
  110. ) # Quantity in amp
  111. # --- FIX: concatenate unitless magnitudes, then reattach units ---
  112. I_inj_array = np.concatenate([
  113. np.asarray(I_hold / amp),
  114. np.asarray(I_ramp / amp),
  115. ]) * amp
  116. else:
  117. I_inj_array = np.zeros(len(t_interp)) * amp
  118. I_inj_timed = TimedArray(I_inj_array, dt=defaultclock.dt)
  119. eqs = '''
  120. dv/dt = (I_leak + I_HCN1 + I_HCNX + I_inj) / C : volt
  121. I_leak = -g_leak * (v - E_K) : amp
  122. I_HCN1 = -g_HCN_full * P_HCN1_timed(t) * (v - E_HCN) : amp
  123. I_HCNX = -g_HCNX * (v - E_HCN) : amp
  124. I_inj = I_inj_timed(t) : amp
  125. C : farad (constant)
  126. E_K : volt (constant)
  127. g_leak : siemens (constant)
  128. g_HCN_full : siemens (constant)
  129. g_HCNX : siemens (constant)
  130. '''
  131. neuron = NeuronGroup(1, eqs, method='euler')
  132. neuron.v = V_rest
  133. neuron.C = C_total
  134. neuron.E_K = E_K_param
  135. neuron.g_leak = g_leak_run
  136. neuron.g_HCN_full = g_HCN_full_run
  137. neuron.g_HCNX = g_HCNX_run
  138. mon = StateMonitor(neuron, ['v', 'I_HCN1', 'I_leak', 'I_inj'], record=0)
  139. run(sim_duration)
  140. time = mon.t / ms
  141. v = mon.v[0] / mV
  142. i_hcn1 = mon.I_HCN1[0] / nA
  143. i_leak = mon.I_leak[0] / nA
  144. i_inj = mon.I_inj[0] / nA
  145. p_open = P_HCN1_timed(mon.t)
  146. i_start = int(t_start_ramp / defaultclock.dt)
  147. V_max = float(np.max(v[i_start:]))
  148. summary_data.append({
  149. "replicate": rep_idx,
  150. "E_K_mV": float(E_K_param / mV),
  151. "R_in_Mohm": float(R_in / ohm) / 1e6,
  152. "scaling": float(scaling),
  153. "V_max_mV": V_max,
  154. "injected": bool(inject_ramp),
  155. "label": label
  156. })
  157. time_series_data[label] = {
  158. "time_ms": np.asarray(time),
  159. "v_mV": np.asarray(v),
  160. "i_hcn1_nA": np.asarray(i_hcn1),
  161. "i_leak_nA": np.asarray(i_leak),
  162. "i_inj_nA": np.asarray(i_inj),
  163. "p_open": np.asarray(p_open)
  164. }
  165. def closest(val, arr):
  166. """
  167. Return the element of arr closest to val.
  168. Args:
  169. val: Target value (float-like).
  170. arr: Iterable of numeric values.
  171. """
  172. arr = np.asarray(list(arr), dtype=float)
  173. return float(arr[np.argmin(np.abs(arr - float(val)))])
  174. def labels_for_noinj_condition(summary_df, E_K_mV, R_in_Mohm, scaling):
  175. """
  176. Get labels matching a non-injected condition in summary_df.
  177. Args:
  178. summary_df: DataFrame with summary data and labels.
  179. E_K_mV: Potassium reversal potential in mV (float).
  180. R_in_Mohm: Input resistance in MΩ (float).
  181. scaling: Scaling value (float).
  182. """
  183. m = (
  184. (summary_df["injected"] == False) &
  185. (summary_df["E_K_mV"] == float(E_K_mV)) &
  186. (summary_df["R_in_Mohm"] == float(R_in_Mohm)) &
  187. (summary_df["scaling"] == float(scaling))
  188. )
  189. labels = summary_df.loc[m, "label"].dropna().tolist()
  190. labels = [l for l in labels if l in time_series_data]
  191. return labels
  192. def labels_for_injected(summary_df, time_series_data):
  193. """
  194. Get labels for injected traces that exist in time_series_data.
  195. Args:
  196. summary_df: DataFrame with summary data and labels.
  197. time_series_data: Dict of trace data keyed by label.
  198. """
  199. labels = summary_df.loc[summary_df["injected"] == True, "label"].dropna().tolist()
  200. labels = [l for l in labels if l in time_series_data]
  201. if len(labels) > 0:
  202. return labels
  203. raise RuntimeError("No injected traces found in summary_df['label'] that exist in time_series_data.")
  204. def mean_trace(time_series_data, labels, key):
  205. """
  206. Compute mean trace by interpolating all traces onto the shortest common time grid
  207. over the overlapping time window.
  208. Returns (t_ref, y_mean).
  209. Args:
  210. time_series_data: Dict of trace data keyed by label.
  211. labels: Labels to include in the mean.
  212. key: Trace field to average (e.g., "v_mV").
  213. """
  214. t_list = [np.asarray(time_series_data[l]["time_ms"], dtype=float) for l in labels]
  215. t_min = max(t[0] for t in t_list)
  216. t_max = min(t[-1] for t in t_list)
  217. if not (t_max > t_min):
  218. raise RuntimeError("No overlapping time window across traces for averaging.")
  219. masks = [(t >= t_min) & (t <= t_max) for t in t_list]
  220. lengths = [int(np.sum(m)) for m in masks]
  221. idx_ref = int(np.argmin(lengths))
  222. t_ref = t_list[idx_ref][masks[idx_ref]]
  223. Y = []
  224. for lbl, t, m in zip(labels, t_list, masks):
  225. y = np.asarray(time_series_data[lbl][key], dtype=float)
  226. Y.append(np.interp(t_ref, t[m], y[m]))
  227. Y = np.vstack(Y)
  228. return t_ref, np.mean(Y, axis=0)
  229. def plot_cloud_and_mean(ax, labels, xkey, ykey,
  230. color="#000000", alpha_cloud=0.25, lw_mean=2.0,
  231. y_transform=None, label_mean=None):
  232. """
  233. Plot all individual traces with the SAME color as the mean (transparent),
  234. then plot the mean trace on top (opaque).
  235. Args:
  236. ax: Matplotlib axis to plot on.
  237. labels: Labels of traces to plot.
  238. xkey: X-axis field name (e.g., "time_ms").
  239. ykey: Y-axis field name (e.g., "v_mV").
  240. color: Trace/mean color.
  241. alpha_cloud: Alpha for individual traces.
  242. lw_mean: Line width for mean trace.
  243. y_transform: Optional transform applied to y-values.
  244. label_mean: Legend label for the mean trace.
  245. """
  246. # Cloud
  247. for lbl in labels:
  248. ts = time_series_data[lbl]
  249. x = np.asarray(ts[xkey], dtype=float)
  250. y = np.asarray(ts[ykey], dtype=float)
  251. if y_transform is not None:
  252. y = y_transform(y)
  253. ax.plot(x, y, color=color, alpha=alpha_cloud, linewidth=1.0)
  254. # Mean
  255. t_mean, y_mean = mean_trace(time_series_data, labels, ykey)
  256. if y_transform is not None:
  257. y_mean = y_transform(y_mean)
  258. ax.plot(t_mean, y_mean, color=color, alpha=1.0, linewidth=lw_mean,
  259. label=label_mean)
  260. def plot_dist(ax, x_vals, df_subset, x_col, xlabel, ylabel=None, semilogx=False,
  261. point_size=22, point_alpha=0.55, color=None):
  262. """
  263. Mean±SD and replicate points. Color optionally set (e.g. to match panel b by E_K).
  264. Args:
  265. ax: Matplotlib axis to plot on.
  266. x_vals: Ordered x-axis values.
  267. df_subset: DataFrame subset with summary values.
  268. x_col: Column name for x values in df_subset.
  269. xlabel: X-axis label.
  270. ylabel: Y-axis label (or None to skip).
  271. semilogx: Whether to use a log-scaled x-axis.
  272. point_size: Marker size for replicate points.
  273. point_alpha: Alpha for replicate points.
  274. color: Color for mean±SD and replicate points. If None, use black / gray.
  275. """
  276. line_color = color if color is not None else "black"
  277. point_color = color if color is not None else "0.35"
  278. g = (df_subset.groupby(x_col, as_index=False)
  279. .agg(V_mean=("V_max_mV", "mean"),
  280. V_sd=("V_max_mV", "std"),
  281. n=("V_max_mV", "count")))
  282. g = g.set_index(x_col).reindex(x_vals).reset_index()
  283. x = g[x_col].to_numpy(dtype=float)
  284. y = g["V_mean"].to_numpy(dtype=float)
  285. yerr = g["V_sd"].to_numpy(dtype=float)
  286. if semilogx:
  287. ax.errorbar(x, y, yerr=yerr, fmt='o-', capsize=3, color=line_color,
  288. markerfacecolor=line_color, markeredgecolor=line_color, linewidth=1.6)
  289. ax.set_xscale("log")
  290. else:
  291. ax.errorbar(x, y, yerr=yerr, fmt='o-', capsize=3, color=line_color,
  292. markerfacecolor=line_color, markeredgecolor=line_color, linewidth=1.6)
  293. # Replicate points: slightly jitter only for semilogx readability; otherwise no jitter is fine here
  294. reps = df_subset.copy()
  295. if semilogx:
  296. jitter = np.exp(np.random.normal(loc=0.0, scale=0.03, size=len(reps)))
  297. x_rep = reps[x_col].to_numpy(dtype=float) * jitter
  298. else:
  299. x_rep = reps[x_col].to_numpy(dtype=float)
  300. ax.scatter(x_rep, reps["V_max_mV"].to_numpy(dtype=float),
  301. s=point_size, color=point_color, alpha=point_alpha, edgecolors="none")
  302. ax.set_xlabel(xlabel)
  303. if ylabel is not None:
  304. ax.set_ylabel(ylabel)
  305. def plot_scaling_curves_by_EK(ax, df, rin_fixed, ek_values, scaling_order,
  306. ek_color_map,
  307. xlabel="HCN2-4 scaling", ylabel=None,
  308. point_size=28, point_alpha=0.65):
  309. """
  310. Vmax vs scaling at fixed R_in, with one curve per E_K.
  311. Replicate points:
  312. - colored by E_K
  313. - plotted at exact scaling x positions, with a tiny EK-dependent x-offset
  314. so different EK groups at the same scaling don't overlap.
  315. Args:
  316. ax: Matplotlib axis to plot on.
  317. df: DataFrame with summary data.
  318. rin_fixed: Fixed R_in value (MΩ).
  319. ek_values: Ordered E_K values (mV).
  320. scaling_order: Ordered scaling values.
  321. ek_color_map: Mapping from E_K to color.
  322. xlabel: X-axis label.
  323. ylabel: Y-axis label (or None to skip).
  324. point_size: Marker size for replicate points.
  325. point_alpha: Alpha for replicate points.
  326. """
  327. df0 = df[df["R_in_Mohm"] == float(rin_fixed)].copy()
  328. if df0.empty:
  329. raise RuntimeError(f"No data for R_in_Mohm == {rin_fixed}")
  330. # Tiny, fixed offsets per EK (in x-axis units of 'scaling')
  331. # Chosen to be visually separable but negligible relative to tick spacing.
  332. offsets = np.linspace(-0.035, 0.035, num=max(1, len(ek_values)))
  333. ek_to_dx = {float(ek): float(dx) for ek, dx in zip(ek_values, offsets)}
  334. for ek in ek_values:
  335. d = df0[df0["E_K_mV"] == float(ek)].copy()
  336. if d.empty:
  337. continue
  338. col = ek_color_map[float(ek)]
  339. dx = ek_to_dx[float(ek)]
  340. g = (d.groupby("scaling", as_index=False)
  341. .agg(V_mean=("V_max_mV", "mean"),
  342. V_sd=("V_max_mV", "std"),
  343. n=("V_max_mV", "count")))
  344. g = g.set_index("scaling").reindex(scaling_order).reset_index()
  345. x = g["scaling"].to_numpy(float) + dx
  346. y = g["V_mean"].to_numpy(float)
  347. yerr = g["V_sd"].to_numpy(float)
  348. # Mean ± SD (also offset slightly to match the replicate cloud)
  349. ax.errorbar(x, y, yerr=yerr, fmt='o-', capsize=3, linewidth=1.8,
  350. color=col, markerfacecolor=col, markeredgecolor=col,
  351. label=rf"$E_{{\mathrm{{K}}}}={ek:.1f}\,\mathrm{{mV}}$")
  352. # Replicate points: same scaling within EK, but EK-dependent dx
  353. ax.scatter(d["scaling"].to_numpy(float) + dx,
  354. d["V_max_mV"].to_numpy(float),
  355. s=point_size, color=col, alpha=point_alpha, edgecolors="none")
  356. ax.set_xlabel(xlabel)
  357. if ylabel is not None:
  358. ax.set_ylabel(ylabel)
  359. # === Constants ===
  360. # Membrane time constant (tau_m) used to compute total capacitance C_total = tau_m / R_in
  361. tau_m = 30 * ms
  362. E_HCN = 0 * mV
  363. V_start = -81 * mV
  364. V_end = -53 * mV
  365. hold_duration = 50 # ms
  366. sim_dt = 10.0 # µs
  367. defaultclock.dt = sim_dt * us
  368. # === Plotting config ===
  369. # Fixed parameter defaults used in plotting and summaries
  370. EK_DEFAULT = -95.0 # mV
  371. RIN_DEFAULT = 100.0 # MΩ
  372. SC_DEFAULT = 0.0
  373. # Colors (Matplotlib default cycle colors by explicit hex)
  374. COL_V = "#1f77b4" # blue
  375. COL_P = "#ff7f0e" # orange
  376. COL_HCN = "#2ca02c" # green
  377. COL_LEAK = "#d62728" # red
  378. COL_INJ = "#9467bd" # purple
  379. # For panel b (curves by EK): use 3 distinct colors, consistent for mean+replicates
  380. EK_COLOR_MAP = None # filled after we know EK values
  381. # === Simulation cache ===
  382. USE_SIM_CACHE = True
  383. SIM_CACHE_PATH = Path("sim_cache.pkl")
  384. # === Load HCN1 open probability data (time + 11 p-columns) ===
  385. file_name = "mHCN1_single_20260203.xlsx"
  386. df = pd.read_excel(file_name)
  387. iend = 1338
  388. # time column (shared)
  389. time_col = pd.to_numeric(df.iloc[:iend, 0], errors="coerce")
  390. replicates = []
  391. eps = 0.0 # threshold to define "non-zero"
  392. N_rest = 10 # first N non-zero points used to estimate p_rest per replicate
  393. for col_idx in range(1, 12): # columns 1..11
  394. p_col = pd.to_numeric(df.iloc[:iend, col_idx], errors="coerce")
  395. valid = (~time_col.isna()) & (~p_col.isna())
  396. t_vals = time_col[valid].to_numpy(dtype=float)
  397. p_vals = p_col[valid].to_numpy(dtype=float)
  398. # Replicate-specific p_rest from first N non-zero points
  399. n_take = min(N_rest, len(p_vals))
  400. p_rest_rep = float(np.mean(p_vals[:n_take])) # or np.median(p_vals[:n_take]) for robustness
  401. # Re-zero time so that dynamic starts at hold_duration cleanly
  402. t_vals = t_vals - t_vals[0]
  403. replicates.append((t_vals, p_vals, col_idx-1, p_rest_rep))
  404. # === Parameter grid derived from explicit value lists ===
  405. V_rest_values = [-81 * mV]
  406. E_K_values = [-95 * mV, -92.5 * mV, -90 * mV]
  407. R_in_values = [1e7 * ohm, 1e8 * ohm, 1e9 * ohm]
  408. scaling_values = [0.0, 0.5, 1.0, 1.5, 2.0]
  409. param_grid = [
  410. {"V_rest": V_rest, "E_K_param": E_K, "R_in": R_in, "scaling": scaling}
  411. for (V_rest, E_K, R_in, scaling) in product(V_rest_values, E_K_values, R_in_values, scaling_values)
  412. ]
  413. # === Run and store simulation results ===
  414. summary_data = []
  415. time_series_data = {}
  416. if USE_SIM_CACHE and SIM_CACHE_PATH.exists():
  417. with SIM_CACHE_PATH.open("rb") as f:
  418. cached = pickle.load(f)
  419. summary_data = cached.get("summary_data", [])
  420. time_series_data = cached.get("time_series_data", {})
  421. else:
  422. # === Run all simulations for each replicate ===
  423. for t_vals, p_vals, rep_idx, p_rest_rep in replicates:
  424. # Interpolate HCN open probability for this replicate
  425. t_hold = np.arange(0, hold_duration, sim_dt / 1000.0)
  426. p_hold = np.full_like(t_hold, p_rest_rep)
  427. t_dynamic = t_vals + hold_duration
  428. p_combined = np.concatenate((p_hold, p_vals))
  429. t_combined = np.concatenate((t_hold, t_dynamic))
  430. # float time bases in ms
  431. t_interp_ms = np.arange(0, t_combined[-1] + sim_dt/1000.0, sim_dt/1000.0)
  432. p_interp = interp1d(
  433. t_combined, p_combined, kind="linear", fill_value="extrapolate"
  434. )(t_interp_ms)
  435. # TimedArray expects values sampled at defaultclock.dt; dt already set in us
  436. P_HCN1_timed = TimedArray(p_interp, dt=defaultclock.dt)
  437. # Keep a quantity time vector for Brian2 computations that expect units
  438. t_interp = t_interp_ms * ms
  439. t_start_ramp = hold_duration * ms
  440. t_end_ramp = t_dynamic[-1] * ms
  441. ramp_duration = t_end_ramp - t_start_ramp
  442. dVdt_target = (V_end - V_start) / ramp_duration
  443. sim_duration = t_end_ramp
  444. # Baseline runs: exhaustive parameter grid
  445. for i, params in enumerate(param_grid):
  446. run_sim(
  447. **params,
  448. P_HCN1_timed=P_HCN1_timed,
  449. t_interp=t_interp,
  450. sim_duration=sim_duration,
  451. t_start_ramp=t_start_ramp,
  452. dVdt_target=dVdt_target,
  453. p_rest_rep=p_rest_rep,
  454. inject_ramp=False,
  455. label=f"rep{rep_idx}_grid{i}",
  456. rep_idx=rep_idx
  457. )
  458. # Injected ramp run
  459. run_sim(
  460. V_rest=-81 * mV, E_K_param=EK_DEFAULT * mV, R_in=1e8 * ohm, scaling=SC_DEFAULT,
  461. P_HCN1_timed=P_HCN1_timed,
  462. t_interp=t_interp,
  463. sim_duration=sim_duration,
  464. t_start_ramp=t_start_ramp,
  465. dVdt_target=dVdt_target,
  466. p_rest_rep=p_rest_rep,
  467. inject_ramp=True,
  468. label=f"rep{rep_idx}_injected",
  469. rep_idx=rep_idx
  470. )
  471. with SIM_CACHE_PATH.open("wb") as f:
  472. pickle.dump(
  473. {"summary_data": summary_data, "time_series_data": time_series_data},
  474. f,
  475. protocol=pickle.HIGHEST_PROTOCOL,
  476. )
  477. summary_df = pd.DataFrame(summary_data)
  478. # Normalize legacy column names if needed
  479. rename_map = {}
  480. if "E_K" in summary_df.columns and "E_K_mV" not in summary_df.columns:
  481. rename_map["E_K"] = "E_K_mV"
  482. if "R_in" in summary_df.columns and "R_in_Mohm" not in summary_df.columns:
  483. rename_map["R_in"] = "R_in_Mohm"
  484. if "V_max" in summary_df.columns and "V_max_mV" not in summary_df.columns:
  485. rename_map["V_max"] = "V_max_mV"
  486. summary_df = summary_df.rename(columns=rename_map)
  487. df_no_inj = summary_df[summary_df["injected"] == False].copy()
  488. df_inj = summary_df[summary_df["injected"] == True].copy()
  489. # -----------------------------------------------------------------------------
  490. # Collect labels for traces
  491. # -----------------------------------------------------------------------------
  492. # Panels a and d use (E_K, R_in, scaling) = (EK_DEFAULT, RIN_DEFAULT, SC_DEFAULT)
  493. # (= -95 mV, 100 MΩ, 0 by default).
  494. labels_noinj = labels_for_noinj_condition(summary_df, EK_DEFAULT, RIN_DEFAULT, SC_DEFAULT)
  495. if len(labels_noinj) == 0:
  496. raise RuntimeError(
  497. "Requested non-injected trace condition not found in summary_data/time_series_data:\n"
  498. f"E_K={EK_DEFAULT} mV, R_in={RIN_DEFAULT} MΩ, scaling={SC_DEFAULT}"
  499. )
  500. labels_inj = labels_for_injected(summary_df, time_series_data)
  501. # -----------------------------------------------------------------------------
  502. # Reference values for parameter sweeps (panels b and c)
  503. # -----------------------------------------------------------------------------
  504. E_K_vals = sorted(df_no_inj["E_K_mV"].unique())
  505. R_in_vals = sorted(df_no_inj["R_in_Mohm"].unique())
  506. sc_vals = sorted(df_no_inj["scaling"].unique())
  507. E_K_ref_for_rin = closest(EK_DEFAULT, E_K_vals)
  508. sc_ref_for_rin = closest(SC_DEFAULT, sc_vals)
  509. df_rin = df_no_inj[(df_no_inj["scaling"] == sc_ref_for_rin) & (df_no_inj["E_K_mV"] == E_K_ref_for_rin)].copy()
  510. rin_order = sorted(df_rin["R_in_Mohm"].unique())
  511. ek_values = sorted(df_no_inj["E_K_mV"].unique())
  512. scaling_order = sorted(df_no_inj["scaling"].unique())
  513. # Map EK->color (3 curves)
  514. # Use Matplotlib default qualitative colors for clean separation
  515. ek_palette = ["#1f77b4", "#ff7f0e", "#2ca02c"] # blue, orange, green
  516. EK_COLOR_MAP = {float(ek): ek_palette[i % len(ek_palette)] for i, ek in enumerate(ek_values)}
  517. # -----------------------------------------------------------------------------
  518. # Figure layout:
  519. # Top row: a (left) and d (right), aligned, each is a 3-row trace stack
  520. # Bottom row: b (left, scaling curves by E_K) and c (right, R_in sweep)
  521. # -----------------------------------------------------------------------------
  522. fig = plt.figure(figsize=(6, 10)) # wider, less tall (since a and c are side-by-side)
  523. gs_outer = gridspec.GridSpec(
  524. 2, 2, figure=fig,
  525. height_ratios=[3.2, 1.55],
  526. width_ratios=[1.0, 1.0],
  527. hspace=0.55, wspace=0.28
  528. )
  529. # Subgrids for trace stacks
  530. gs_a = gridspec.GridSpecFromSubplotSpec(
  531. 3, 1, subplot_spec=gs_outer[0, 0],
  532. height_ratios=[1.35, 1.15, 1.05],
  533. hspace=0.14
  534. )
  535. gs_d = gridspec.GridSpecFromSubplotSpec(
  536. 3, 1, subplot_spec=gs_outer[0, 1],
  537. height_ratios=[1.35, 1.15, 1.05],
  538. hspace=0.14
  539. )
  540. # Panels b and c in bottom row
  541. gs_bc = gridspec.GridSpecFromSubplotSpec(
  542. 1, 2, subplot_spec=gs_outer[1, :],
  543. width_ratios=[2.0, 1.0],
  544. wspace=0.35
  545. )
  546. # Axes creation
  547. ax_a1 = fig.add_subplot(gs_a[0, 0])
  548. ax_a2 = fig.add_subplot(gs_a[1, 0], sharex=ax_a1)
  549. ax_a3 = fig.add_subplot(gs_a[2, 0], sharex=ax_a1)
  550. ax_d1 = fig.add_subplot(gs_d[0, 0], sharex=ax_a1, sharey=ax_a1)
  551. ax_d2 = fig.add_subplot(gs_d[1, 0], sharex=ax_a1, sharey=ax_a2)
  552. ax_d3 = fig.add_subplot(gs_d[2, 0], sharex=ax_a1, sharey=ax_a3)
  553. ax_b1 = fig.add_subplot(gs_bc[0, 0])
  554. ax_c1 = fig.add_subplot(gs_bc[0, 1], sharey=ax_b1)
  555. fig.subplots_adjust(left=0.08, right=0.98, top=0.96, bottom=0.10)
  556. # Hide x tick labels on upper axes in stacks
  557. for ax in (ax_a1, ax_a2, ax_d1, ax_d2):
  558. ax.tick_params(bottom=False, labelbottom=False)
  559. # -----------------------------------------------------------------------------
  560. # Panel a: Non-injected traces (colored clouds + colored means)
  561. # -----------------------------------------------------------------------------
  562. plot_cloud_and_mean(ax_a1, labels_noinj, "time_ms", "p_open",
  563. color=COL_P, alpha_cloud=0.20, lw_mean=2.3)
  564. ax_a1.set_ylabel(r"$P_{\mathrm{open}}$")
  565. plot_cloud_and_mean(ax_a2, labels_noinj, "time_ms", "v_mV",
  566. color=COL_V, alpha_cloud=0.20, lw_mean=2.3)
  567. ax_a2.set_ylabel(r"$V$ (mV)")
  568. # Currents: two colors (no patterns)
  569. plot_cloud_and_mean(ax_a3, labels_noinj, "time_ms", "i_hcn1_nA",
  570. color=COL_HCN, alpha_cloud=0.18, lw_mean=2.2,
  571. y_transform=lambda y: -y, label_mean="HCN1 (mean)")
  572. plot_cloud_and_mean(ax_a3, labels_noinj, "time_ms", "i_leak_nA",
  573. color=COL_LEAK, alpha_cloud=0.18, lw_mean=2.2,
  574. y_transform=lambda y: -y, label_mean="Leak (mean)")
  575. ax_a3.set_ylabel("Current (nA)")
  576. ax_a3.set_xlabel("Time (ms)")
  577. ax_a3.legend(frameon=False, fontsize=9, loc="upper right")
  578. # -----------------------------------------------------------------------------
  579. # Panel d: Injected traces (colored clouds + colored means)
  580. # -----------------------------------------------------------------------------
  581. plot_cloud_and_mean(ax_d1, labels_inj, "time_ms", "p_open",
  582. color=COL_P, alpha_cloud=0.20, lw_mean=2.3)
  583. ax_d1.set_ylabel(r"$P_{\mathrm{open}}$")
  584. plot_cloud_and_mean(ax_d2, labels_inj, "time_ms", "v_mV",
  585. color=COL_V, alpha_cloud=0.20, lw_mean=2.3)
  586. ax_d2.set_ylabel(r"$V$ (mV)")
  587. plot_cloud_and_mean(ax_d3, labels_inj, "time_ms", "i_hcn1_nA",
  588. color=COL_HCN, alpha_cloud=0.18, lw_mean=2.2,
  589. y_transform=lambda y: -y, label_mean="HCN1 (mean)")
  590. plot_cloud_and_mean(ax_d3, labels_inj, "time_ms", "i_leak_nA",
  591. color=COL_LEAK, alpha_cloud=0.18, lw_mean=2.2,
  592. y_transform=lambda y: -y, label_mean="Leak (mean)")
  593. plot_cloud_and_mean(ax_d3, labels_inj, "time_ms", "i_inj_nA",
  594. color=COL_INJ, alpha_cloud=0.18, lw_mean=2.2,
  595. y_transform=lambda y: -y, label_mean="Injected (mean)")
  596. ax_d3.set_ylabel("Current (nA)")
  597. ax_d3.set_xlabel("Time (ms)")
  598. ax_d3.legend(frameon=False, fontsize=9, loc="upper right")
  599. # -----------------------------------------------------------------------------
  600. # Panel b: Scaling curves by E_K
  601. # -----------------------------------------------------------------------------
  602. plot_scaling_curves_by_EK(
  603. ax_b1,
  604. df=df_no_inj,
  605. rin_fixed=RIN_DEFAULT,
  606. ek_values=ek_values,
  607. scaling_order=scaling_order,
  608. ek_color_map=EK_COLOR_MAP,
  609. xlabel="HCN2-4 scaling",
  610. ylabel=r"Max $V$ (mV)",
  611. point_size=34, # larger
  612. point_alpha=0.60 # less transparent
  613. )
  614. # -----------------------------------------------------------------------------
  615. # Panel c: R_in parameter sweep (same E_K, scaling as panels a/d → match panel b color)
  616. # -----------------------------------------------------------------------------
  617. col_c = EK_COLOR_MAP[float(E_K_ref_for_rin)]
  618. plot_dist(
  619. ax_c1, rin_order, df_rin, "R_in_Mohm",
  620. xlabel=r"$R_{\mathrm{in}}$ (M$\Omega$)",
  621. ylabel=None,
  622. semilogx=True,
  623. point_size=34,
  624. point_alpha=0.65,
  625. color=col_c
  626. )
  627. ax_c1.tick_params(labelleft=False)
  628. ax_b1.set_title(rf"$R_{{\mathrm{{in}}}} = {RIN_DEFAULT:.0f}\,\mathrm{{M\Omega}}$", fontsize=10, pad=4)
  629. ax_b1.legend(frameon=False, fontsize=9, loc="best")
  630. # Remove x-axis line and ticks to emphasize categorical nature of scaling values
  631. # ax_b1.spines["bottom"].set_visible(False)
  632. # ax_b1.tick_params(bottom=False, labelbottom=True) # Keep labels, remove ticks
  633. ax_c1.set_title("mean ± SD (replicates)", fontsize=10, pad=4)
  634. # -----------------------------------------------------------------------------
  635. # Styling & panel labels
  636. # -----------------------------------------------------------------------------
  637. for ax in (ax_a1, ax_a2, ax_a3, ax_d1, ax_d2, ax_d3, ax_c1, ax_b1):
  638. ax.spines["top"].set_visible(False)
  639. ax.spines["right"].set_visible(False)
  640. ax.grid(False)
  641. # Panel letters aligned to the top of each block
  642. bbox_a = ax_a1.get_position()
  643. bbox_d = ax_d1.get_position()
  644. bbox_b = ax_b1.get_position()
  645. bbox_c = ax_c1.get_position()
  646. fig.text(bbox_a.x0 - 0.06, bbox_a.y1, "a", fontsize=13, fontweight="bold", va="top", ha="left")
  647. fig.text(bbox_d.x0 - 0.06, bbox_d.y1, "d", fontsize=13, fontweight="bold", va="top", ha="left")
  648. fig.text(bbox_b.x0 - 0.06, bbox_b.y1, "b", fontsize=13, fontweight="bold", va="top", ha="left")
  649. fig.text(bbox_c.x0 - 0.06, bbox_c.y1, "c", fontsize=13, fontweight="bold", va="top", ha="left")
  650. # -----------------------------------------------------------------------------
  651. # Export data to Excel
  652. # -----------------------------------------------------------------------------
  653. excel_file = "figure_data.xlsx"
  654. with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
  655. # Panel a: Non-injected traces
  656. # Get mean traces
  657. t_mean_a, v_mean_a = mean_trace(time_series_data, labels_noinj, "v_mV")
  658. _, p_mean_a = mean_trace(time_series_data, labels_noinj, "p_open")
  659. _, i_hcn1_mean_a = mean_trace(time_series_data, labels_noinj, "i_hcn1_nA")
  660. _, i_leak_mean_a = mean_trace(time_series_data, labels_noinj, "i_leak_nA")
  661. # Create DataFrame with mean traces
  662. df_panel_a = pd.DataFrame({
  663. "Time_ms": t_mean_a,
  664. "Voltage_mean_mV": v_mean_a,
  665. "P_open_mean": p_mean_a,
  666. "I_HCN1_mean_nA": -i_hcn1_mean_a, # Apply negative transform as in plot
  667. "I_leak_mean_nA": -i_leak_mean_a # Apply negative transform as in plot
  668. })
  669. # Add individual replicate traces (interpolated to mean time grid)
  670. for i, label in enumerate(labels_noinj):
  671. ts = time_series_data[label]
  672. t_orig = np.asarray(ts["time_ms"], dtype=float)
  673. df_panel_a[f"Voltage_rep{i+1}_mV"] = np.interp(t_mean_a, t_orig, np.asarray(ts["v_mV"], dtype=float))
  674. df_panel_a[f"P_open_rep{i+1}"] = np.interp(t_mean_a, t_orig, np.asarray(ts["p_open"], dtype=float))
  675. df_panel_a[f"I_HCN1_rep{i+1}_nA"] = -np.interp(t_mean_a, t_orig, np.asarray(ts["i_hcn1_nA"], dtype=float))
  676. df_panel_a[f"I_leak_rep{i+1}_nA"] = -np.interp(t_mean_a, t_orig, np.asarray(ts["i_leak_nA"], dtype=float))
  677. df_panel_a.to_excel(writer, sheet_name="Panel a", index=False)
  678. # Panel b: Scaling curves by E_K
  679. # Find max number of replicates across all E_K and scaling combinations
  680. df_panel_b_temp = df_no_inj[(df_no_inj["R_in_Mohm"] == float(RIN_DEFAULT))].copy()
  681. max_reps_b = int(df_panel_b_temp.groupby(["E_K_mV", "scaling"])["V_max_mV"].count().max()) if not df_panel_b_temp.empty else 0
  682. df_panel_b_list = []
  683. for ek in ek_values:
  684. d = df_no_inj[(df_no_inj["E_K_mV"] == float(ek)) &
  685. (df_no_inj["R_in_Mohm"] == float(RIN_DEFAULT))].copy()
  686. if d.empty:
  687. continue
  688. g_b = (d.groupby("scaling", as_index=False)
  689. .agg(V_max_mean_mV=("V_max_mV", "mean"),
  690. V_max_sd_mV=("V_max_mV", "std"),
  691. n_replicates=("V_max_mV", "count")))
  692. g_b = g_b.set_index("scaling").reindex(scaling_order).reset_index()
  693. g_b["E_K_mV"] = ek
  694. # Create replicate columns
  695. for rep_idx in range(1, max_reps_b + 1):
  696. col_name = f"V_max_rep{rep_idx}_mV"
  697. g_b[col_name] = np.nan
  698. # Add individual replicate values
  699. for sc_val in scaling_order:
  700. reps = d[d["scaling"] == sc_val]["V_max_mV"].values
  701. for j, rep_val in enumerate(reps):
  702. col_name = f"V_max_rep{j+1}_mV"
  703. g_b.loc[g_b["scaling"] == sc_val, col_name] = rep_val
  704. df_panel_b_list.append(g_b)
  705. if df_panel_b_list:
  706. df_panel_b = pd.concat(df_panel_b_list, ignore_index=True)
  707. # Reorder columns: E_K, scaling, then statistics and replicates
  708. cols = ["E_K_mV", "scaling", "V_max_mean_mV", "V_max_sd_mV", "n_replicates"]
  709. rep_cols = [c for c in df_panel_b.columns if c.startswith("V_max_rep")]
  710. df_panel_b = df_panel_b[cols + rep_cols]
  711. df_panel_b.to_excel(writer, sheet_name="Panel b", index=False)
  712. # Panel c: R_in parameter sweep
  713. g_c = (df_rin.groupby("R_in_Mohm", as_index=False)
  714. .agg(V_max_mean_mV=("V_max_mV", "mean"),
  715. V_max_sd_mV=("V_max_mV", "std"),
  716. n_replicates=("V_max_mV", "count")))
  717. g_c = g_c.set_index("R_in_Mohm").reindex(rin_order).reset_index()
  718. # Find max number of replicates to create all columns
  719. max_reps = int(df_rin.groupby("R_in_Mohm")["V_max_mV"].count().max())
  720. # Add individual replicate values
  721. df_panel_c = g_c.copy()
  722. for rep_idx in range(1, max_reps + 1):
  723. col_name = f"V_max_rep{rep_idx}_mV"
  724. df_panel_c[col_name] = np.nan
  725. for rin_val in rin_order:
  726. reps = df_rin[df_rin["R_in_Mohm"] == rin_val]["V_max_mV"].values
  727. for j, rep_val in enumerate(reps):
  728. col_name = f"V_max_rep{j+1}_mV"
  729. df_panel_c.loc[df_panel_c["R_in_Mohm"] == rin_val, col_name] = rep_val
  730. df_panel_c.to_excel(writer, sheet_name="Panel c", index=False)
  731. # Panel d: Injected traces
  732. # Get mean traces
  733. t_mean_d, v_mean_d = mean_trace(time_series_data, labels_inj, "v_mV")
  734. _, p_mean_d = mean_trace(time_series_data, labels_inj, "p_open")
  735. _, i_hcn1_mean_d = mean_trace(time_series_data, labels_inj, "i_hcn1_nA")
  736. _, i_leak_mean_d = mean_trace(time_series_data, labels_inj, "i_leak_nA")
  737. _, i_inj_mean_d = mean_trace(time_series_data, labels_inj, "i_inj_nA")
  738. # Create DataFrame with mean traces
  739. df_panel_d = pd.DataFrame({
  740. "Time_ms": t_mean_d,
  741. "Voltage_mean_mV": v_mean_d,
  742. "P_open_mean": p_mean_d,
  743. "I_HCN1_mean_nA": -i_hcn1_mean_d, # Apply negative transform as in plot
  744. "I_leak_mean_nA": -i_leak_mean_d, # Apply negative transform as in plot
  745. "I_injected_mean_nA": -i_inj_mean_d # Apply negative transform as in plot
  746. })
  747. # Add individual replicate traces (interpolated to mean time grid)
  748. for i, label in enumerate(labels_inj):
  749. ts = time_series_data[label]
  750. t_orig = np.asarray(ts["time_ms"], dtype=float)
  751. df_panel_d[f"Voltage_rep{i+1}_mV"] = np.interp(t_mean_d, t_orig, np.asarray(ts["v_mV"], dtype=float))
  752. df_panel_d[f"P_open_rep{i+1}"] = np.interp(t_mean_d, t_orig, np.asarray(ts["p_open"], dtype=float))
  753. df_panel_d[f"I_HCN1_rep{i+1}_nA"] = -np.interp(t_mean_d, t_orig, np.asarray(ts["i_hcn1_nA"], dtype=float))
  754. df_panel_d[f"I_leak_rep{i+1}_nA"] = -np.interp(t_mean_d, t_orig, np.asarray(ts["i_leak_nA"], dtype=float))
  755. df_panel_d[f"I_injected_rep{i+1}_nA"] = -np.interp(t_mean_d, t_orig, np.asarray(ts["i_inj_nA"], dtype=float))
  756. df_panel_d.to_excel(writer, sheet_name="Panel d", index=False)
  757. print(f"Data exported to {excel_file}")
  758. # -----------------------------------------------------------------------------
  759. # Save
  760. # -----------------------------------------------------------------------------
  761. fig.savefig("figure_combined.svg", format="svg", bbox_inches="tight")
  762. fig.savefig("figure_combined.pdf", format="pdf", bbox_inches="tight")
  763. plt.show()

HCN_rest-singlePo.py, no license · at the source

Overview

Authors: Uta Enke1, Andrea Schweinitz1, Debanjan Tewari1, Christian Sattler1, Ralf Schmauder1, Christoph Schmidt-Hieber2, Klaus Benndorf1
  1. Institut für Physiologie II, Universitätsklinikum Jena, Friedrich-Schiller-Universität Jena, Jena, Germany
  2. Institut für Physiologie I, Universitätsklinikum Jena, Friedrich-Schiller-Universität Jena, Jena, Germany
Journal: Nature communications, volume 17, issue 1, article 3745
Dates: received 21 August 2025; accepted 10 April 2026; published online 23 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-72257-3 · PMID 42026039 · PMCID PMC13106851 · OpenAlex W7155356039
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: intracellular / patch clamp (modality), human (organism), cellular / molecular (subfield)
Keywords: Neurophysiology, Single-molecule biophysics, Single-channel recording, Patch clamp, Ion channels
MeSH: Biological Clocks*, Cyclic Nucleotide-Gated Cation Channels*, Hyperpolarization-Activated Cyclic Nucleotide-Gated Channels*, Neurons*, Potassium Channels*, Action Potentials, Animals, Humans, Ion Channel Gating, Patch-Clamp Techniques (* major topic)
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Deutsche Forschungsgemeinschaft (German Research Foundation) (RU 2518 P02)
Citations: not cited yet (Europe PMC); 49 references in the paper

Abstract

Rhythmic activity of specialized pacemaker neurons in the brain is necessary to control alertness and circadian timing. Four HCN channels have been identified to generate the pacemaker current Ih or Iq, differing in activation speed, voltage dependence, single-channel conductance, and cAMP sensitivity. Here we show the time-resolved operation of single HCN1, HCN2 and HCN4 channels during the pacemaker depolarization using a dynamic neuronal action potential clamp at femtosiemens resolution. All channels produce a relevant open probability during pacemaker depolarization. However, only mHCN1 channels are significantly activated and deactivated in action potential cycles whereas the gating in mHCN2 and mHCN4 channels is at best barely resolvable and too slow. Simulations suggest that the role of HCN1 channels is to trigger the initial neuronal pacemaker depolarization before other depolarizing conductances take over this role. In conclusion, mHCN1 channels are the primary HCN pacemaker channels that operate as trigger channels for pacemaking.

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 3 matches between paragraphs and lines of code.

OSF 7g5ch

License: none: the authors keep all their rights
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Languages: Python (1)
Size: 4 files, 1 script
Software Heritage: not checked
Found in: “Code availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Brian 2 (1 file), Matplotlib (1 file), NumPy (1 file), pandas (1 file), SciPy (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
  • 29 September 2026: the link answers (HTTP 200)
3 files
At the source: osf.io/7g5ch

Code availability

The code of the simulations (HCN_rest-singlePo.py) is available at OSF [osf.io/7g5ch]. No manuscript-specific software was used for the analysis of measured data.

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

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;
  • 1 script, each with its path and the digest of its content;
  • 3 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

All data included in the manuscript are deposited in the Open Science Framework [osf.io/7g5ch].

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, 29 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 5 keywords, 10 MeSH terms, 1 funder, 48 references.

Cite

This paper

Enke, U., Schweinitz, A., Tewari, D., Sattler, C., Schmauder, R., Schmidt-Hieber, C., & Benndorf, K. (2026). HCN1 is a primary HCN Pacemaker Channel in Neurons. Nature communications, 17(1), 3745. https://doi.org/10.1038/s41467-026-72257-3

BibTeX

@article{enke2026hcn1,
author = {Enke, Uta and Schweinitz, Andrea and Tewari, Debanjan and Sattler, Christian and Schmauder, Ralf and Schmidt-Hieber, Christoph and Benndorf, Klaus},
title = {{HCN1 is a primary HCN Pacemaker Channel in Neurons}},
journal = {Nature communications},
year = {2026},
month = apr,
volume = {17},
number = {1},
pages = {3745},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-72257-3},
url = {https://doi.org/10.1038/s41467-026-72257-3},
pmid = {42026039},
pmcid = {PMC13106851}
}

RIS

TY - JOUR
AU - Enke, Uta
AU - Schweinitz, Andrea
AU - Tewari, Debanjan
AU - Sattler, Christian
AU - Schmauder, Ralf
AU - Schmidt-Hieber, Christoph
AU - Benndorf, Klaus
TI - HCN1 is a primary HCN Pacemaker Channel in Neurons
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/04/23
VL - 17
IS - 1
SP - 3745
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-72257-3
UR - https://doi.org/10.1038/s41467-026-72257-3
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-72257-3",
"type": "article-journal",
"title": "HCN1 is a primary HCN Pacemaker Channel in Neurons",
"container-title": "Nature communications",
"author": [
{
"family": "Enke",
"given": "Uta"
},
{
"family": "Schweinitz",
"given": "Andrea"
},
{
"family": "Tewari",
"given": "Debanjan"
},
{
"family": "Sattler",
"given": "Christian"
},
{
"family": "Schmauder",
"given": "Ralf"
},
{
"family": "Schmidt-Hieber",
"given": "Christoph"
},
{
"family": "Benndorf",
"given": "Klaus"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "3745",
"DOI": "10.1038/s41467-026-72257-3",
"PMID": "42026039",
"PMCID": "PMC13106851",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-72257-3",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
23
]
]
}
}

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.1113/ep093939 [code]
Temperature dependence of HCN channel kinetics: A systematic comparison across mammalian isoforms and species.
Journal: Experimental physiology
In common: intracellular / patch clamp, cellular / molecular, 4 references
[2] doi:10.1016/j.isci.2026.116930
HCAR1-mediated lactate signaling modulates motor behavior and regulates spontaneous firing in Purkinje cells.
Journal: iScience
In common: cellular / molecular, 4 references
[3] doi:10.1523/jneurosci.1506-25.2026 [code]
Controlling Spatio-Temporal Sequences of Neural Activity by Local Synaptic Changes.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: Brian 2, pandas, SciPy, 2 other tools, cellular / molecular, 1 reference
[4] doi:10.1002/hipo.70089 [code]
The Role of Plasticity in Replay: Stability Through Anti-Hebbian Rules.
Journal: Hippocampus
In common: Brian 2, pandas, SciPy, 2 other tools, cellular / molecular, 1 reference
[5] doi:10.1371/journal.pcbi.1014752 [code]
Hierarchical feature binding in a spiking neural network model of the primate ventral visual pathway.
Journal: PLoS computational biology
In common: Brian 2, pandas, SciPy, 2 other tools, 1 reference
[6] doi:10.1126/sciadv.aee9425 [code]
Probabilistic inference of homonymous and heteronymous recurrent inhibition in human muscles from large-scale motor neuron recordings.
Journal: Science advances
In common: Brian 2, pandas, SciPy, 2 other tools, 1 reference
[7] doi:10.1038/s41467-026-75705-2 [code]
Redundant prefrontal hemispheres adapt storage strategy to working memory demands.
Journal: Nature communications
In common: Brian 2, pandas, SciPy, 2 other tools, 1 reference
[8] doi:10.1523/jneurosci.0912-25.2026 [code]
Hierarchical Afferent Connectivity Drives Population-Wide Bursting Dynamics in a Computational Model of Human-Derived Excitatory Neuronal Networks.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: Brian 2, pandas, SciPy, 2 other tools, 1 reference
[9] 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: Brian 2, SciPy, Matplotlib, 1 other tool, 1 reference
[10] doi:10.1016/j.celrep.2026.117793 [code]
Clustered inputs engage dendritic nonlinearities and calcium signaling to support efficient place-field formation in CA1 pyramidal neurons.
Journal: Cell reports
In common: Brian 2, pandas, SciPy, 2 other tools, cellular / molecular

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.