From 8070bce21bd1c0e4e4860b0e23b63725a4e1be73 Mon Sep 17 00:00:00 2001 From: katduecker Date: Thu, 6 Nov 2025 16:46:46 -0500 Subject: [PATCH 01/12] feat: Part 1 of many of extracting KD's v_init This is the first commit of many to come where I'm going to extract the parts of #1168 relevant to the new, proper voltage initialization. The plan is: 1. We'll separate the changes to the "v_init" stuff and make it its own PR onto `master`, 2. Change the tests that become broken as we go along, 3. Everyone reviews the changes (which will likely end up in a slightly different form from how they currently exist in #1168), 4. We merge these voltage initialization changes into `master`, 5. Then finally, we merge/rebase the voltage initialization changes (which are now on `master`) *back* into the code at #1168 . 6. Repeat the process for all code features included in the length #1168 work, until everything gets merged, piece-by-piece. For starters, this commit consists of *only* the extracted changes to `cell.py` and nothing else. Fortunately, in this case, the tests seem to pass without having to change anything. The original person who actually wrote this code was @katduecker, which is why they are indicated as author. This version of the code that was extracted from #1168 was done so by @asoplata . --- hnn_core/cell.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/hnn_core/cell.py b/hnn_core/cell.py index b1be611a8e..15e02cf979 100644 --- a/hnn_core/cell.py +++ b/hnn_core/cell.py @@ -180,7 +180,9 @@ class Section: membrane capacitance in micro-Farads. Ra : float axial resistivity in ohm-cm - end_pts : list of [x, y, z] + v : float + start value for membrane potential in millivolts. + end_pts : list of [x, y, z], optional The start and stop points of the section. Attributes @@ -201,15 +203,18 @@ class Section: membrane capacitance in micro-Farads. Ra : float axial resistivity in ohm-cm. + v : float + start value for membrane potential in millivolts. nseg : int Number of segments in the section """ - def __init__(self, L, diam, Ra, cm, end_pts=None): + def __init__(self, L, diam, Ra, cm, v=-65, end_pts=None): self._L = L self._diam = diam self._Ra = Ra self._cm = cm + self._v = v # initial voltage if end_pts is None: end_pts = list() self._end_pts = end_pts @@ -221,7 +226,7 @@ def __init__(self, L, diam, Ra, cm, end_pts=None): self.nseg = _get_nseg(self.L) def __repr__(self): - return f"L={self.L}, diam={self.diam}, cm={self.cm}, Ra={self.Ra}" + return f"L={self.L}, diam={self.diam}, cm={self.cm}, Ra={self.Ra}, v={self.v}" def __eq__(self, other): if not isinstance(other, Section): @@ -266,6 +271,7 @@ def to_dict(self): section_data["Ra"] = self.Ra section_data["end_pts"] = self.end_pts section_data["nseg"] = self.nseg + section_data["v"] = self._v # Need to solve the partial function problem # in mechs section_data["mechs"] = self.mechs @@ -288,6 +294,10 @@ def cm(self): def Ra(self): return self._Ra + @property + def v(self): + return self._v + @property def end_pts(self): return self._end_pts @@ -574,6 +584,7 @@ def _set_biophysics(self, sections): sec = self._nrn_sections[sec_name] for mech_name, p_mech in section.mechs.items(): sec.insert(mech_name) + setattr(sec, "v", section.v) for attr, val in p_mech.items(): if isinstance(val, list): seg_xs, seg_vals = val[0], val[1] @@ -642,6 +653,7 @@ def _create_sections(self, sections, cell_tree): sec.Ra = sections[sec_name].Ra sec.cm = sections[sec_name].cm sec.nseg = sections[sec_name].nseg + sec.v = sections[sec_name].v if cell_tree is None: cell_tree = dict() @@ -1062,7 +1074,7 @@ def _update_end_pts(self): # of sections. self.define_shape(("soma", 0)) - def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None): + def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None, v=None): """Change attributes of section specified by `sec_name` Parameters @@ -1077,6 +1089,8 @@ def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None): membrane capacitance in micro-Farads. Ra : float | int | None axial resistivity in ohm-cm. + v : float | int | None + start value for membrane potential in millivolts. Notes ----- @@ -1101,4 +1115,8 @@ def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None): _validate_type(Ra, (float, int), "Ra") self.sections[sec_name]._Ra = Ra + if v is not None: + _validate_type(v, (float, int), "v") + self.sections[sec_name]._v = v + self._update_end_pts() From b8953da7b1487fa3c066368c5aa99fd41ef5df8c Mon Sep 17 00:00:00 2001 From: katduecker Date: Thu, 6 Nov 2025 17:21:22 -0500 Subject: [PATCH 02/12] kd: apply cells_default.py changes [no ci] This does NOT pass tests The original person who actually wrote this code was @katduecker, which is why they are indicated as author. This version of the code that was extracted from #1168 was done so by @asoplata . --- hnn_core/cells_default.py | 88 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index 6d3abb095a..4de99f79e8 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -15,7 +15,9 @@ # units for taur: ms -def _get_dends(params, cell_type, section_names): +# KD: comment: initialize membrane potential here as it's not overridden by +# h.finitialize unless called as h.finitialize(-65) +def _get_dends(params, cell_type, section_names, v_init={"all": -65}): """Convert a flat dictionary to a nested dictionary. Returns @@ -34,22 +36,64 @@ def _get_dends(params, cell_type, section_names): # map apicaltrunk -> apical_trunk etc. middle = section_name.replace("_", "") dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] + if len(v_init) == 1: + v = v_init["all"] + else: + v = v_init[section_name] + sections[section_name] = Section( L=dend_prop["L"], diam=dend_prop["diam"], Ra=dend_prop["Ra"], cm=dend_prop["cm"], + v=v, ) return sections -def _get_pyr_soma(p_all, cell_type): +# KD: In the new model, the basal dendrites are differently tuned from the apical +# dendrites. +def _get_basal(params, cell_type, section_names, v_init={"all": -65}): + """Convert a flat dictionary to a nested dictionary. + + Returns + ------- + sections : dict + Dictionary of sections. Keys are section names + """ + prop_names = ["L", "diam", "Ra", "cm"] + sections = dict() + for section_name in section_names: + dend_prop = dict() + middle = section_name.replace("_", "") + for key in prop_names: + if key in ["Ra", "cm"]: + middle = "basal" + else: + # map apicaltrunk -> apical_trunk etc. + middle = section_name.replace("_", "") + dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] + if len(v_init) == 1: + v = v_init["all"] + else: + v = v_init[section_name] + sections[section_name] = Section( + L=dend_prop["L"], + diam=dend_prop["diam"], + Ra=dend_prop["Ra"], + cm=dend_prop["cm"], + v=v, + ) + + +def _get_pyr_soma(p_all, cell_type, v_init=-65): """Get somatic properties.""" return Section( L=p_all[f"{cell_type}_soma_L"], diam=p_all[f"{cell_type}_soma_diam"], cm=p_all[f"{cell_type}_soma_cm"], Ra=p_all[f"{cell_type}_soma_Ra"], + v=v_init, ) @@ -70,7 +114,12 @@ def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): "basal_3", ] - sections = _get_dends(p_all, cell_type="L2Pyr", section_names=section_names) + sections = _get_dends( + p_all, + cell_type="L2Pyr", + section_names=section_names, + v_init={"all": -71.46}, + ) sections["soma"] = _get_pyr_soma(p_all, "L2Pyr") end_pts = { @@ -153,8 +202,29 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): "basal_3", ] - sections = _get_dends(p_all, cell_type="L5Pyr", section_names=section_names) - sections["soma"] = _get_pyr_soma(p_all, "L5Pyr") + v_init = { + "apical_1": -71.32, + "apical_2": -69.08, + "apical_tuft": -67.30, + "apical_trunk": -72, + "soma": -72.0, + "basal_1": -72, + "basal_2": -72, + "basal_3": -72, + "apical_oblique": -72, + } + + sections = _get_dends( + p_all, + cell_type="L5Pyr", + section_names=section_names, + v_init=v_init, + ) + sections["soma"] = _get_pyr_soma( + p_all, + "L5Pyr", + v_init=-72, + ) end_pts = { "soma": [[0, 0, 0], [0, 0, 23]], @@ -230,11 +300,17 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): ) -def _get_basket_soma(cell_name): +def _get_basket_soma(cell_name, v_init=-64.9737): end_pts = [[0, 0, 0], [0, 0, 39.0]] return Section(L=39.0, diam=20.0, cm=0.85, Ra=200.0, end_pts=end_pts) +# values from Chamberland et al 2023 +def _get_interneuron_soma(cell_name, v_init=-75): + end_pts = [[0, 0, 0], [0, 0, 20.0]] + return Section(L=20.0, diam=20.0, cm=1, Ra=200.0, end_pts=end_pts) + + def _get_pyr_syn_props(p_all, cell_type): return { "ampa": { From 43c77c1ac945771228346bda87d7f7559c2ef865 Mon Sep 17 00:00:00 2001 From: katduecker Date: Thu, 6 Nov 2025 17:37:01 -0500 Subject: [PATCH 03/12] fix: add missing "v" read-in (with back-compat) This also includes regenerated networks, since doing that is necessary to allow the tests to pass as well. The original person who actually wrote this code was @katduecker, which is why they are indicated as author. This version of the code that was extracted from #1168 was done so by @asoplata . --- hnn_core/hnn_io.py | 26 ++++++++++++++----- hnn_core/param/jones2009_base.json | 19 ++++++++++++++ .../tests/assets/jones2009_3x3_drives.json | 19 ++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/hnn_core/hnn_io.py b/hnn_core/hnn_io.py index 18b2dcb759..aae55f4254 100644 --- a/hnn_core/hnn_io.py +++ b/hnn_core/hnn_io.py @@ -166,13 +166,25 @@ def _read_cell_types(cell_types_data): sections_data = cell_data["sections"] for section_name in sections_data: section_data = sections_data[section_name] - sections[section_name] = Section( - L=section_data["L"], - diam=section_data["diam"], - cm=section_data["cm"], - Ra=section_data["Ra"], - end_pts=section_data["end_pts"], - ) + if "v" in section_data.keys(): + sections[section_name] = Section( + L=section_data["L"], + diam=section_data["diam"], + cm=section_data["cm"], + Ra=section_data["Ra"], + v=section_data["v"], + end_pts=section_data["end_pts"], + ) + else: + # Yet more legacy backwards-compatibility + sections[section_name] = Section( + L=section_data["L"], + diam=section_data["diam"], + cm=section_data["cm"], + Ra=section_data["Ra"], + end_pts=section_data["end_pts"], + ) + # Set section attributes sections[section_name].syns = section_data["syns"] sections[section_name].mechs = section_data["mechs"] diff --git a/hnn_core/param/jones2009_base.json b/hnn_core/param/jones2009_base.json index 42f875636d..ff6eb6edd7 100644 --- a/hnn_core/param/jones2009_base.json +++ b/hnn_core/param/jones2009_base.json @@ -32,6 +32,7 @@ ] ], "nseg": 1, + "v": -65, "mechs": { "hh2": {} }, @@ -110,6 +111,7 @@ ] ], "nseg": 1, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -146,6 +148,7 @@ ] ], "nseg": 7, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -182,6 +185,7 @@ ] ], "nseg": 5, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -218,6 +222,7 @@ ] ], "nseg": 7, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -254,6 +259,7 @@ ] ], "nseg": 1, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -290,6 +296,7 @@ ] ], "nseg": 5, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -326,6 +333,7 @@ ] ], "nseg": 5, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -362,6 +370,7 @@ ] ], "nseg": 1, + "v": -65, "mechs": { "km": { "gbar_km": 250.0 @@ -494,6 +503,7 @@ ] ], "nseg": 1, + "v": -65, "mechs": { "hh2": {} }, @@ -570,6 +580,7 @@ ] ], "nseg": 3, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -632,6 +643,7 @@ ] ], "nseg": 13, + "v": -71.32, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -714,6 +726,7 @@ ] ], "nseg": 13, + "v": -69.08, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -796,6 +809,7 @@ ] ], "nseg": 9, + "v": -67.3, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -870,6 +884,7 @@ ] ], "nseg": 5, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -936,6 +951,7 @@ ] ], "nseg": 1, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -994,6 +1010,7 @@ ] ], "nseg": 5, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1060,6 +1077,7 @@ ] ], "nseg": 5, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1126,6 +1144,7 @@ ] ], "nseg": 1, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, diff --git a/hnn_core/tests/assets/jones2009_3x3_drives.json b/hnn_core/tests/assets/jones2009_3x3_drives.json index 89ae561d27..60ba615a9c 100644 --- a/hnn_core/tests/assets/jones2009_3x3_drives.json +++ b/hnn_core/tests/assets/jones2009_3x3_drives.json @@ -32,6 +32,7 @@ ] ], "nseg": 1, + "v": -65, "mechs": { "hh2": {} }, @@ -110,6 +111,7 @@ ] ], "nseg": 1, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -146,6 +148,7 @@ ] ], "nseg": 7, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -182,6 +185,7 @@ ] ], "nseg": 5, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -218,6 +222,7 @@ ] ], "nseg": 7, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -254,6 +259,7 @@ ] ], "nseg": 1, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -290,6 +296,7 @@ ] ], "nseg": 5, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -326,6 +333,7 @@ ] ], "nseg": 5, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -362,6 +370,7 @@ ] ], "nseg": 1, + "v": -65, "mechs": { "km": { "gbar_km": 250.0 @@ -494,6 +503,7 @@ ] ], "nseg": 1, + "v": -65, "mechs": { "hh2": {} }, @@ -570,6 +580,7 @@ ] ], "nseg": 3, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -632,6 +643,7 @@ ] ], "nseg": 13, + "v": -71.32, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -714,6 +726,7 @@ ] ], "nseg": 13, + "v": -69.08, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -796,6 +809,7 @@ ] ], "nseg": 9, + "v": -67.3, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -870,6 +884,7 @@ ] ], "nseg": 5, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -936,6 +951,7 @@ ] ], "nseg": 1, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -994,6 +1010,7 @@ ] ], "nseg": 5, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1060,6 +1077,7 @@ ] ], "nseg": 5, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1126,6 +1144,7 @@ ] ], "nseg": 1, + "v": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, From 80863e190dacc1db69ec173f75cc9ed55acc6425 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Thu, 6 Nov 2025 18:29:24 -0500 Subject: [PATCH 04/12] fix: add missing L2 soma v_init, remove old init This fixes what I think is a bug currently present in #1168, where the L2's `soma` section did not have its universal voltage-initialization value applied (but the dendrites did). Additionally, @katduecker , I think the same bug is present in your `_get_interneuron_soma()` function (which I've removed in this commit since it's not relevant to the voltage-initialization). Your `_get_interneuron_soma` takes a `v_init` value, but does not apply it in `Section` creation. This was probably missed. This also removes `NetworkBuilder.state_init()` like KD did, since it is no longer needed, as the voltage initialization is completely moved over to `cells_default.py` now. With this, I think the voltage-initialization stuff has been completely extracted from #1168. The only remaining work is to write new tests specifically for the `v_init` where it is used. --- hnn_core/cells_default.py | 27 ++++++++++----- hnn_core/network_builder.py | 33 ------------------- hnn_core/param/jones2009_base.json | 6 ++-- .../tests/assets/jones2009_3x3_drives.json | 6 ++-- 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index 4de99f79e8..a6a215acd2 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -104,6 +104,8 @@ def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): assert isinstance(override_params, dict) p_all = compare_dictionaries(p_all, override_params) + all_v_init = -71.46 + section_names = [ "apical_trunk", "apical_1", @@ -118,9 +120,15 @@ def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): p_all, cell_type="L2Pyr", section_names=section_names, - v_init={"all": -71.46}, + v_init={ + "all": all_v_init, + }, + ) + sections["soma"] = _get_pyr_soma( + p_all, + "L2Pyr", + v_init=all_v_init, ) - sections["soma"] = _get_pyr_soma(p_all, "L2Pyr") end_pts = { "soma": [[-50, 0, 765], [-50, 0, 778]], @@ -302,13 +310,14 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): def _get_basket_soma(cell_name, v_init=-64.9737): end_pts = [[0, 0, 0], [0, 0, 39.0]] - return Section(L=39.0, diam=20.0, cm=0.85, Ra=200.0, end_pts=end_pts) - - -# values from Chamberland et al 2023 -def _get_interneuron_soma(cell_name, v_init=-75): - end_pts = [[0, 0, 0], [0, 0, 20.0]] - return Section(L=20.0, diam=20.0, cm=1, Ra=200.0, end_pts=end_pts) + return Section( + L=39.0, + diam=20.0, + cm=0.85, + Ra=200.0, + v=v_init, + end_pts=end_pts, + ) def _get_pyr_syn_props(p_all, cell_type): diff --git a/hnn_core/network_builder.py b/hnn_core/network_builder.py index 3806a04e26..7279d5eb49 100644 --- a/hnn_core/network_builder.py +++ b/hnn_core/network_builder.py @@ -371,8 +371,6 @@ def _build(self): record_ca=record_ca, ) - self.state_init() - # set to record spikes, somatic voltages, and extracellular potentials self._spike_times = h.Vector() self._spike_gids = h.Vector() @@ -649,37 +647,6 @@ def aggregate_data(self, n_samples): _PC.barrier() # get all nodes to this place before continuing - def state_init(self): - """Initializes the state closer to baseline.""" - - for cell in self._cells: - seclist = h.SectionList() - seclist.wholetree(sec=cell._nrn_sections["soma"]) - src_type = self.net.gid_to_type(cell.gid) - cell_metadata = self.net.cell_types[src_type]["cell_metadata"] - # initializing segment voltages from cell_metadata - for sect in seclist: - for seg in sect: - if ( - cell_metadata.get("morpho_type") == "pyramidal" - and cell_metadata.get("layer") == "2" - ): - seg.v = -71.46 - elif ( - cell_metadata.get("morpho_type") == "pyramidal" - and cell_metadata.get("layer") == "5" - ): - if sect.name() == f"{_short_name(src_type)}_apical_1": - seg.v = -71.32 - elif sect.name() == f"{_short_name(src_type)}_apical_2": - seg.v = -69.08 - elif sect.name() == f"{_short_name(src_type)}_apical_tuft": - seg.v = -67.30 - else: - seg.v = -72.0 - elif cell_metadata.get("morpho_type") == "basket": - seg.v = -64.9737 - def _clear_neuron_objects(self): """Clear up NEURON internal gid and reference information. diff --git a/hnn_core/param/jones2009_base.json b/hnn_core/param/jones2009_base.json index ff6eb6edd7..1fb3bb12b1 100644 --- a/hnn_core/param/jones2009_base.json +++ b/hnn_core/param/jones2009_base.json @@ -32,7 +32,7 @@ ] ], "nseg": 1, - "v": -65, + "v": -64.9737, "mechs": { "hh2": {} }, @@ -370,7 +370,7 @@ ] ], "nseg": 1, - "v": -65, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -503,7 +503,7 @@ ] ], "nseg": 1, - "v": -65, + "v": -64.9737, "mechs": { "hh2": {} }, diff --git a/hnn_core/tests/assets/jones2009_3x3_drives.json b/hnn_core/tests/assets/jones2009_3x3_drives.json index 60ba615a9c..0d6dd30655 100644 --- a/hnn_core/tests/assets/jones2009_3x3_drives.json +++ b/hnn_core/tests/assets/jones2009_3x3_drives.json @@ -32,7 +32,7 @@ ] ], "nseg": 1, - "v": -65, + "v": -64.9737, "mechs": { "hh2": {} }, @@ -370,7 +370,7 @@ ] ], "nseg": 1, - "v": -65, + "v": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -503,7 +503,7 @@ ] ], "nseg": 1, - "v": -65, + "v": -64.9737, "mechs": { "hh2": {} }, From 05e5c6f53c9d6907a6e9139d1eecdea875c10c42 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Mon, 1 Dec 2025 15:20:26 -0500 Subject: [PATCH 05/12] ref: replace new symbol "v" with "v0" fix: bug in v->v0 rename This is a good reason WHY we should perform the name change in this case. yet another v->v0 fix --- hnn_core/cell.py | 36 ++++++++++++++++++++---------------- hnn_core/cells_default.py | 16 ++++++++-------- hnn_core/hnn_io.py | 4 ++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/hnn_core/cell.py b/hnn_core/cell.py index 15e02cf979..2569085c75 100644 --- a/hnn_core/cell.py +++ b/hnn_core/cell.py @@ -180,7 +180,7 @@ class Section: membrane capacitance in micro-Farads. Ra : float axial resistivity in ohm-cm - v : float + v0 : float start value for membrane potential in millivolts. end_pts : list of [x, y, z], optional The start and stop points of the section. @@ -203,18 +203,18 @@ class Section: membrane capacitance in micro-Farads. Ra : float axial resistivity in ohm-cm. - v : float - start value for membrane potential in millivolts. + v0 : float + Initial value for membrane potential in millivolts. nseg : int Number of segments in the section """ - def __init__(self, L, diam, Ra, cm, v=-65, end_pts=None): + def __init__(self, L, diam, Ra, cm, v0=-65, end_pts=None): self._L = L self._diam = diam self._Ra = Ra self._cm = cm - self._v = v # initial voltage + self._v0 = v0 if end_pts is None: end_pts = list() self._end_pts = end_pts @@ -226,7 +226,7 @@ def __init__(self, L, diam, Ra, cm, v=-65, end_pts=None): self.nseg = _get_nseg(self.L) def __repr__(self): - return f"L={self.L}, diam={self.diam}, cm={self.cm}, Ra={self.Ra}, v={self.v}" + return f"L={self.L}, diam={self.diam}, cm={self.cm}, Ra={self.Ra}, v0={self.v0}" def __eq__(self, other): if not isinstance(other, Section): @@ -271,7 +271,7 @@ def to_dict(self): section_data["Ra"] = self.Ra section_data["end_pts"] = self.end_pts section_data["nseg"] = self.nseg - section_data["v"] = self._v + section_data["v0"] = self.v0 # Need to solve the partial function problem # in mechs section_data["mechs"] = self.mechs @@ -295,8 +295,8 @@ def Ra(self): return self._Ra @property - def v(self): - return self._v + def v0(self): + return self._v0 @property def end_pts(self): @@ -584,7 +584,9 @@ def _set_biophysics(self, sections): sec = self._nrn_sections[sec_name] for mech_name, p_mech in section.mechs.items(): sec.insert(mech_name) - setattr(sec, "v", section.v) + # This is one of the two places where we are actually applying our + # initial voltage + setattr(sec, "v", section.v0) for attr, val in p_mech.items(): if isinstance(val, list): seg_xs, seg_vals = val[0], val[1] @@ -653,7 +655,9 @@ def _create_sections(self, sections, cell_tree): sec.Ra = sections[sec_name].Ra sec.cm = sections[sec_name].cm sec.nseg = sections[sec_name].nseg - sec.v = sections[sec_name].v + # This is one of the two places where we are actually applying our initial + # voltage + sec.v = sections[sec_name].v0 if cell_tree is None: cell_tree = dict() @@ -1074,7 +1078,7 @@ def _update_end_pts(self): # of sections. self.define_shape(("soma", 0)) - def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None, v=None): + def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None, v0=None): """Change attributes of section specified by `sec_name` Parameters @@ -1089,7 +1093,7 @@ def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None, v=None): membrane capacitance in micro-Farads. Ra : float | int | None axial resistivity in ohm-cm. - v : float | int | None + v0 : float | int | None start value for membrane potential in millivolts. Notes @@ -1115,8 +1119,8 @@ def modify_section(self, sec_name, L=None, diam=None, cm=None, Ra=None, v=None): _validate_type(Ra, (float, int), "Ra") self.sections[sec_name]._Ra = Ra - if v is not None: - _validate_type(v, (float, int), "v") - self.sections[sec_name]._v = v + if v0 is not None: + _validate_type(v0, (float, int), "v0") + self.sections[sec_name]._v0 = v0 self._update_end_pts() diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index a6a215acd2..8344629b5c 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -37,16 +37,16 @@ def _get_dends(params, cell_type, section_names, v_init={"all": -65}): middle = section_name.replace("_", "") dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] if len(v_init) == 1: - v = v_init["all"] + v0 = v_init["all"] else: - v = v_init[section_name] + v0 = v_init[section_name] sections[section_name] = Section( L=dend_prop["L"], diam=dend_prop["diam"], Ra=dend_prop["Ra"], cm=dend_prop["cm"], - v=v, + v0=v0, ) return sections @@ -74,15 +74,15 @@ def _get_basal(params, cell_type, section_names, v_init={"all": -65}): middle = section_name.replace("_", "") dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] if len(v_init) == 1: - v = v_init["all"] + v0 = v_init["all"] else: - v = v_init[section_name] + v0 = v_init[section_name] sections[section_name] = Section( L=dend_prop["L"], diam=dend_prop["diam"], Ra=dend_prop["Ra"], cm=dend_prop["cm"], - v=v, + v0=v0, ) @@ -93,7 +93,7 @@ def _get_pyr_soma(p_all, cell_type, v_init=-65): diam=p_all[f"{cell_type}_soma_diam"], cm=p_all[f"{cell_type}_soma_cm"], Ra=p_all[f"{cell_type}_soma_Ra"], - v=v_init, + v0=v_init, ) @@ -315,7 +315,7 @@ def _get_basket_soma(cell_name, v_init=-64.9737): diam=20.0, cm=0.85, Ra=200.0, - v=v_init, + v0=v_init, end_pts=end_pts, ) diff --git a/hnn_core/hnn_io.py b/hnn_core/hnn_io.py index aae55f4254..ba7d8b6d05 100644 --- a/hnn_core/hnn_io.py +++ b/hnn_core/hnn_io.py @@ -166,13 +166,13 @@ def _read_cell_types(cell_types_data): sections_data = cell_data["sections"] for section_name in sections_data: section_data = sections_data[section_name] - if "v" in section_data.keys(): + if "v0" in section_data.keys(): sections[section_name] = Section( L=section_data["L"], diam=section_data["diam"], cm=section_data["cm"], Ra=section_data["Ra"], - v=section_data["v"], + v0=section_data["v0"], end_pts=section_data["end_pts"], ) else: From 15f22e6e19dff9357e71a953c19f6a72f48f0b90 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Mon, 1 Dec 2025 15:55:13 -0500 Subject: [PATCH 06/12] ref: move v0 into `dend_prop` --- hnn_core/cells_default.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index 8344629b5c..1ae67cd8d0 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -36,17 +36,19 @@ def _get_dends(params, cell_type, section_names, v_init={"all": -65}): # map apicaltrunk -> apical_trunk etc. middle = section_name.replace("_", "") dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] - if len(v_init) == 1: - v0 = v_init["all"] - else: - v0 = v_init[section_name] + # v0 is handled separately since it is "newer", and will never be found in the + # `params` input. + if len(v_init) == 1: + dend_prop["v0"] = v_init["all"] + else: + dend_prop["v0"] = v_init[section_name] sections[section_name] = Section( L=dend_prop["L"], diam=dend_prop["diam"], Ra=dend_prop["Ra"], cm=dend_prop["cm"], - v0=v0, + v0=dend_prop["v0"], ) return sections @@ -73,16 +75,18 @@ def _get_basal(params, cell_type, section_names, v_init={"all": -65}): # map apicaltrunk -> apical_trunk etc. middle = section_name.replace("_", "") dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] - if len(v_init) == 1: - v0 = v_init["all"] - else: - v0 = v_init[section_name] + # v0 is handled separately since it is "newer", and will never be found in the + # `params` input. + if len(v_init) == 1: + dend_prop["v0"] = v_init["all"] + else: + dend_prop["v0"] = v_init[section_name] sections[section_name] = Section( L=dend_prop["L"], diam=dend_prop["diam"], Ra=dend_prop["Ra"], cm=dend_prop["cm"], - v0=v0, + v0=dend_prop["v0"], ) From 736cd4cbfafeac47848f784e2eca4dae8990fe13 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Mon, 1 Dec 2025 16:02:16 -0500 Subject: [PATCH 07/12] doc: single comment change --- hnn_core/cells_default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index 1ae67cd8d0..c3c5b1da33 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -53,8 +53,8 @@ def _get_dends(params, cell_type, section_names, v_init={"all": -65}): return sections -# KD: In the new model, the basal dendrites are differently tuned from the apical -# dendrites. +# KD: In the new `duecker_ET_model` model, tuning of the basal dendrites is different +# from the tuning of the apical dendrites. def _get_basal(params, cell_type, section_names, v_init={"all": -65}): """Convert a flat dictionary to a nested dictionary. From 9bb50dfe24dabfd93922ad5226c1e4660bbd7f11 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Mon, 1 Dec 2025 16:02:33 -0500 Subject: [PATCH 08/12] ref: apply v->v0 rename to network JSON files --- hnn_core/param/jones2009_base.json | 38 +++++++++---------- .../tests/assets/jones2009_3x3_drives.json | 38 +++++++++---------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/hnn_core/param/jones2009_base.json b/hnn_core/param/jones2009_base.json index 1fb3bb12b1..5a6963686e 100644 --- a/hnn_core/param/jones2009_base.json +++ b/hnn_core/param/jones2009_base.json @@ -32,7 +32,7 @@ ] ], "nseg": 1, - "v": -64.9737, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -111,7 +111,7 @@ ] ], "nseg": 1, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -148,7 +148,7 @@ ] ], "nseg": 7, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -185,7 +185,7 @@ ] ], "nseg": 5, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -222,7 +222,7 @@ ] ], "nseg": 7, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -259,7 +259,7 @@ ] ], "nseg": 1, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -296,7 +296,7 @@ ] ], "nseg": 5, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -333,7 +333,7 @@ ] ], "nseg": 5, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -370,7 +370,7 @@ ] ], "nseg": 1, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -503,7 +503,7 @@ ] ], "nseg": 1, - "v": -64.9737, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -580,7 +580,7 @@ ] ], "nseg": 3, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -643,7 +643,7 @@ ] ], "nseg": 13, - "v": -71.32, + "v0": -71.32, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -726,7 +726,7 @@ ] ], "nseg": 13, - "v": -69.08, + "v0": -69.08, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -809,7 +809,7 @@ ] ], "nseg": 9, - "v": -67.3, + "v0": -67.3, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -884,7 +884,7 @@ ] ], "nseg": 5, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -951,7 +951,7 @@ ] ], "nseg": 1, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1010,7 +1010,7 @@ ] ], "nseg": 5, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1077,7 +1077,7 @@ ] ], "nseg": 5, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1144,7 +1144,7 @@ ] ], "nseg": 1, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, diff --git a/hnn_core/tests/assets/jones2009_3x3_drives.json b/hnn_core/tests/assets/jones2009_3x3_drives.json index 0d6dd30655..78c7e4dda7 100644 --- a/hnn_core/tests/assets/jones2009_3x3_drives.json +++ b/hnn_core/tests/assets/jones2009_3x3_drives.json @@ -32,7 +32,7 @@ ] ], "nseg": 1, - "v": -64.9737, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -111,7 +111,7 @@ ] ], "nseg": 1, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -148,7 +148,7 @@ ] ], "nseg": 7, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -185,7 +185,7 @@ ] ], "nseg": 5, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -222,7 +222,7 @@ ] ], "nseg": 7, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -259,7 +259,7 @@ ] ], "nseg": 1, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -296,7 +296,7 @@ ] ], "nseg": 5, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -333,7 +333,7 @@ ] ], "nseg": 5, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -370,7 +370,7 @@ ] ], "nseg": 1, - "v": -71.46, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -503,7 +503,7 @@ ] ], "nseg": 1, - "v": -64.9737, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -580,7 +580,7 @@ ] ], "nseg": 3, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -643,7 +643,7 @@ ] ], "nseg": 13, - "v": -71.32, + "v0": -71.32, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -726,7 +726,7 @@ ] ], "nseg": 13, - "v": -69.08, + "v0": -69.08, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -809,7 +809,7 @@ ] ], "nseg": 9, - "v": -67.3, + "v0": -67.3, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -884,7 +884,7 @@ ] ], "nseg": 5, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -951,7 +951,7 @@ ] ], "nseg": 1, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1010,7 +1010,7 @@ ] ], "nseg": 5, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1077,7 +1077,7 @@ ] ], "nseg": 5, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1144,7 +1144,7 @@ ] ], "nseg": 1, - "v": -72, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, From d05f0fecf28805a3e994d7b23a72564607890fbe Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Tue, 9 Dec 2025 15:56:22 -0500 Subject: [PATCH 09/12] ref: merge _get_dends and _get_basal --- hnn_core/cells_default.py | 98 ++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index c3c5b1da33..c984a9a7d1 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -15,15 +15,61 @@ # units for taur: ms -# KD: comment: initialize membrane potential here as it's not overridden by -# h.finitialize unless called as h.finitialize(-65) -def _get_dends(params, cell_type, section_names, v_init={"all": -65}): - """Convert a flat dictionary to a nested dictionary. +def _get_dends( + params, + cell_type, + section_names, + v_init={"all": -65}, + is_basal_specific=False, +): + """Create dendritic Section objects from flat parameter dictionary. + + Extracts geometric and electrical properties (length, diameter, axial resistance, + membrane capacitance) from a flat parameter dictionary, takes initial membrane + voltage from its, and constructs Section objects for each dendritic + compartment. Handles parameter key name transformations (e.g., 'apical_trunk' -> + 'apicaltrunk') required for lookup in the parameter dictionary. + + Parameters + ---------- + params : dict + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_{section}_{property}' (e.g., 'L5Pyr_apicaltrunk_L'). + 'Ra' and 'cm' use "dend" as the middle component rather than specific + section names. This 'params' dictionary is expected to be constructed using + functions like `params_default.py::get_L2Pyr_params_default`. + cell_type : str, {'L2Pyr', 'L5Pyr'} + Cell type identifier used as prefix in parameter key lookups. + section_names : list of str + Names of dendritic sections to create (e.g., ['apical_trunk', + 'apical_1', 'basal_2']). Underscores are removed for parameter + lookups except for 'Ra' and 'cm'. + v_init : dict, default={"all": -65} + Initial membrane potential in mV. If dict contains single key "all", + that value is applied to all sections. Otherwise, keys must match + 'section_names' for section-specific initialization. + is_basal_specific : bool, default=False + Flag indicating whether or not to use the (Duecker 2025) model's custom basal + dendrite parameters. If True, this will read the 'Ra' and 'cm' parameters from + 'params' using '{cell_type}_basal_{property}' instead of the default + '{cell_type}_dend_{property}' naming scheme. Returns ------- sections : dict - Dictionary of sections. Keys are section names + Dictionary mapping section names (str) to Section objects with attributes L, + diam, Ra, and cm set from 'params', and v0 set from argument. + + Notes + ----- + - KD: This function is where the initial voltages for the dendritic sections are + set; these voltages are not overridden by `h.finitialize` unless called with a + value, e.g. `h.finitialize(-65)`. + - The 'v0' (initial voltage) parameter is handled separately from other properties as + it is a newer addition not found in legacy parameter files. + - In the (Jones et al., 2009) model, this is used to construct both apical and basal + dendrite sections. In the newer (Duecker 2025) model, this is only used for the + apical dendrite sections. """ prop_names = ["L", "diam", "Ra", "cm"] sections = dict() @@ -31,7 +77,10 @@ def _get_dends(params, cell_type, section_names, v_init={"all": -65}): dend_prop = dict() for key in prop_names: if key in ["Ra", "cm"]: - middle = "dend" + if is_basal_specific: + middle = "basal" + else: + middle = "dend" else: # map apicaltrunk -> apical_trunk etc. middle = section_name.replace("_", "") @@ -53,43 +102,6 @@ def _get_dends(params, cell_type, section_names, v_init={"all": -65}): return sections -# KD: In the new `duecker_ET_model` model, tuning of the basal dendrites is different -# from the tuning of the apical dendrites. -def _get_basal(params, cell_type, section_names, v_init={"all": -65}): - """Convert a flat dictionary to a nested dictionary. - - Returns - ------- - sections : dict - Dictionary of sections. Keys are section names - """ - prop_names = ["L", "diam", "Ra", "cm"] - sections = dict() - for section_name in section_names: - dend_prop = dict() - middle = section_name.replace("_", "") - for key in prop_names: - if key in ["Ra", "cm"]: - middle = "basal" - else: - # map apicaltrunk -> apical_trunk etc. - middle = section_name.replace("_", "") - dend_prop[key] = params[f"{cell_type}_{middle}_{key}"] - # v0 is handled separately since it is "newer", and will never be found in the - # `params` input. - if len(v_init) == 1: - dend_prop["v0"] = v_init["all"] - else: - dend_prop["v0"] = v_init[section_name] - sections[section_name] = Section( - L=dend_prop["L"], - diam=dend_prop["diam"], - Ra=dend_prop["Ra"], - cm=dend_prop["cm"], - v0=dend_prop["v0"], - ) - - def _get_pyr_soma(p_all, cell_type, v_init=-65): """Get somatic properties.""" return Section( From 020943eab8165fc6fef79d24cc5a358cc123c26a Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Tue, 9 Dec 2025 18:15:38 -0500 Subject: [PATCH 10/12] doc: add some docs for cells_default functions --- hnn_core/cells_default.py | 190 +++++++++++++++++++++++++++++++++----- 1 file changed, 165 insertions(+), 25 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index c984a9a7d1..7885fca355 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -16,38 +16,43 @@ def _get_dends( - params, - cell_type, - section_names, - v_init={"all": -65}, - is_basal_specific=False, + params, + cell_type, + section_names, + v_init={"all": -65}, + is_basal_specific=False, ): """Create dendritic Section objects from flat parameter dictionary. Extracts geometric and electrical properties (length, diameter, axial resistance, membrane capacitance) from a flat parameter dictionary, takes initial membrane - voltage from its, and constructs Section objects for each dendritic + voltage from an argument, and constructs Section objects for each dendritic compartment. Handles parameter key name transformations (e.g., 'apical_trunk' -> 'apicaltrunk') required for lookup in the parameter dictionary. + *Importantly*, "Section objects" in this context are objects of the class + `hnn_core/cell.py::Section`, NOT the "true" NEURON sections. The "true" NEURON + sections are only created later, immediately before a simulation is run, using + `NetworkBuilder._build`. + Parameters ---------- params : dict Flat dictionary containing cell parameters with keys formatted as - '{cell_type}_{section}_{property}' (e.g., 'L5Pyr_apicaltrunk_L'). - 'Ra' and 'cm' use "dend" as the middle component rather than specific - section names. This 'params' dictionary is expected to be constructed using + '{cell_type}_{section}_{property}' (e.g., 'L5Pyr_apicaltrunk_L'). 'Ra' and 'cm' + use "dend" as the middle component rather than specific section names. This + 'params' dictionary is expected to be constructed using functions like `params_default.py::get_L2Pyr_params_default`. - cell_type : str, {'L2Pyr', 'L5Pyr'} + cell_type : {'L2Pyr', 'L5Pyr'} Cell type identifier used as prefix in parameter key lookups. section_names : list of str - Names of dendritic sections to create (e.g., ['apical_trunk', - 'apical_1', 'basal_2']). Underscores are removed for parameter - lookups except for 'Ra' and 'cm'. + Names of dendritic sections to create (e.g., ['apical_trunk', 'apical_1', + 'basal_2']). Underscores are removed for parameter lookups except for 'Ra' and + 'cm'. v_init : dict, default={"all": -65} - Initial membrane potential in mV. If dict contains single key "all", - that value is applied to all sections. Otherwise, keys must match - 'section_names' for section-specific initialization. + Initial membrane potential in mV. If dict contains single key "all", that value + is applied to all sections. Otherwise, keys must match 'section_names' for + section-specific initialization. is_basal_specific : bool, default=False Flag indicating whether or not to use the (Duecker 2025) model's custom basal dendrite parameters. If True, this will read the 'Ra' and 'cm' parameters from @@ -57,16 +62,16 @@ def _get_dends( Returns ------- sections : dict - Dictionary mapping section names (str) to Section objects with attributes L, - diam, Ra, and cm set from 'params', and v0 set from argument. + Dictionary mapping section names (str) to Section objects with attributes 'L', + 'diam', 'Ra', and 'cm' set from 'params', and 'v0' set from argument. Notes ----- - KD: This function is where the initial voltages for the dendritic sections are set; these voltages are not overridden by `h.finitialize` unless called with a value, e.g. `h.finitialize(-65)`. - - The 'v0' (initial voltage) parameter is handled separately from other properties as - it is a newer addition not found in legacy parameter files. + - The 'v0' (initial voltage) parameter is handled separately from other properties + as it is a newer addition not found in legacy parameter files. - In the (Jones et al., 2009) model, this is used to construct both apical and basal dendrite sections. In the newer (Duecker 2025) model, this is only used for the apical dendrite sections. @@ -103,7 +108,46 @@ def _get_dends( def _get_pyr_soma(p_all, cell_type, v_init=-65): - """Get somatic properties.""" + """Create Pyramidal somatic Section objects from flat parameter dictionary. + + Extracts geometric and electrical properties (length, diameter, axial resistance, + membrane capacitance) from a flat parameter dictionary, takes initial membrane + voltage from an argument, and constructs a Section object for each Pyramidal soma + compartment. + + *Importantly*, "Section objects" in this context are objects of the class + `hnn_core/cell.py::Section`, NOT the "true" NEURON sections. The "true" NEURON + sections are only created later, immediately before a simulation is run, using + `NetworkBuilder._build`. + + Parameters + ---------- + p_all : dict + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_soma_{property}' (e.g., 'L5Pyr_soma_L'). This 'p_all' dictionary + is expected to be constructed using functions like + `params_default.py::get_L2Pyr_params_default`. + cell_type : {'L2Pyr', 'L5Pyr'} + Cell type identifier used as prefix in parameter key lookups. + v_init : dict, default={"all": -65} + Initial membrane potential in mV. If dict contains single key "all", that value + is applied to all sections. Otherwise, keys must match 'section_names' for + section-specific initialization. + + Returns + ------- + Section + A Section object with attributes 'L', 'diam', 'Ra', and 'cm' set from 'p_all', + and 'v0' set from argument. + + Notes + ----- + - KD: This function is where the initial voltages for the somata are set; these + voltages are not overridden by `h.finitialize` unless called with a value, + e.g. `h.finitialize(-65)`. + - The 'v0' (initial voltage) parameter is handled separately from other properties + as it is a newer addition not found in legacy parameter files. + """ return Section( L=p_all[f"{cell_type}_soma_L"], diam=p_all[f"{cell_type}_soma_diam"], @@ -113,13 +157,60 @@ def _get_pyr_soma(p_all, cell_type, v_init=-65): ) -def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): - """The geometry of the default sections in L2Pyr neuron.""" +def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0): + """Create a Cell object of the Layer 2/3 Pyramidal cell type. + + This constructs a Layer 2/3 Pyramidal cell type (i.e. 'L2Pyr') using the following + steps: + 1. "Loads" the default parameters for this celltype using + `params_default.py::get_L2Pyr_params_default`. + 2. Overrides the default parameters based on the 'override_params' argument. + 3. Creates all dendrite Section compartment objects, including initializing their + voltages. + 4. Creates the soma Section compartment object, including initializing its voltage. + 5. Programs the 'end_pts' of each Section using hard-coded values. + 6. Sets the mechanisms for each Section. In this celltype, all Sections contain the + same set of mechanisms. + 7. Sets the receiving synapse types for each Section. Somata only receive inhibitory + synapses, while dendrites receive all types. + 8. Constructs a map of the cell tree, connecting each Section appropriately. + 9. Assigns different dendritic Sections to either the 'proximal' or 'distal' groups. + 10. Sets parameters for all synaptic types. + 11. Finally, creates the Cell object with all of the above information, including + 'pos' cell position and 'gid' identifier that are set by arguments. + + *Importantly*, "Cell objects" in this context are objects of the class + `hnn_core/cell.py::Cell`, not "true" NEURON cells. Similarly, "Section objects" + in this context are objects of the class `hnn_core/cell.py::Section`, NOT the "true" + Section objects as created and used by NEURON. The "true" NEURON sections and cells + are only created later, immediately before a simulation is run, using + `NetworkBuilder._build`. + + Parameters + ---------- + override_params : dict + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_{section}_{property}' (e.g., 'L2Pyr_apicaltrunk_L'), where + key-value pairs are only provided for those values where the user wants to use + custom, non-default parameters. The default parameters can be found in + `params_default.py::get_L2Pyr_params_default`. If no overrides are desired, then + this argument should be None. + pos : tuple of (float, float, int), default=(0.0, 0.0, 0) + 3-dimensional position to place the cell at. + gid : int, default=0 + The unique, "global ID" (GID) of the cell. + + Returns + ------- + Cell + A Cell object of the Layer 2/3 Pyramidal cell type. + """ p_all = get_L2Pyr_params_default() if override_params is not None: assert isinstance(override_params, dict) p_all = compare_dictionaries(p_all, override_params) + # All sections of this cell type use the same initial membrane voltage: all_v_init = -71.46 section_names = [ @@ -207,8 +298,55 @@ def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): ) -def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): - """The geometry of the default sections in L5Pyr Neuron.""" +def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0): + """Create a Cell object of the Layer 5 Pyramidal cell type. + + This constructs a Layer 5 Pyramidal cell type (i.e. 'L5Pyr') using the following + steps: + 1. "Loads" the default parameters for this celltype using + `params_default.py::get_L5Pyr_params_default`. + 2. Overrides the default parameters based on the 'override_params' argument. + 3. Creates all dendrite Section compartment objects, including initializing their + voltages. + 4. Creates the soma Section compartment object, including initializing its voltage. + 5. Programs the 'end_pts' of each Section using hard-coded values. + 6. Sets the mechanisms for each Section. In this celltype, all Sections contain the + same set of mechanisms. + 7. Sets the receiving synapse types for each Section. Somata only receive inhibitory + synapses, while dendrites receive all types. + 8. Sets the AR current maximal conductance according to a spatial algorithm. + 9. Constructs a map of the cell tree, connecting each Section appropriately. + 10. Assigns different dendritic Sections to either the 'proximal' or 'distal' groups. + 11. Sets parameters for all synaptic types. + 12. Finally, creates the Cell object with all of the above information, including + 'pos' cell position and 'gid' identifier that are set by arguments. + + *Importantly*, "Cell objects" in this context are objects of the class + `hnn_core/cell.py::Cell`, not "true" NEURON cells. Similarly, "Section objects" + in this context are objects of the class `hnn_core/cell.py::Section`, NOT the "true" + Section objects as created and used by NEURON. The "true" NEURON sections and cells + are only created later, immediately before a simulation is run, using + `NetworkBuilder._build`. + + Parameters + ---------- + override_params : dict + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_{section}_{property}' (e.g., 'L5Pyr_apicaltrunk_L'), where + key-value pairs are only provided for those values where the user wants to use + custom, non-default parameters. The default parameters can be found in + `params_default.py::get_L5Pyr_params_default`. If no overrides are desired, then + this argument should be None. + pos : tuple of (float, float, int), default=(0.0, 0.0, 0) + 3-dimensional position to place the cell at. + gid : int, default=0 + The unique, "global ID" (GID) of the cell. + + Returns + ------- + Cell + A Cell object of the Layer 5 Pyramidal cell type. + """ p_all = get_L5Pyr_params_default() if override_params is not None: @@ -226,6 +364,7 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): "basal_3", ] + # Different sections of this cell type use different initial membrane voltages: v_init = { "apical_1": -71.32, "apical_2": -69.08, @@ -262,6 +401,7 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0.0): "basal_3": [[0, 0, -50], [106, 0, -156]], } + # AES TODO: what's up with this units comment? # units = ['pS/um^2', 'S/cm^2', 'pS/um^2', '??', 'tau', '??'] mechanisms = { "hh2": ["gkbar_hh2", "gnabar_hh2", "gl_hh2", "el_hh2"], From a1466066befc2bab7f043160e7b905918932bc63 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Wed, 10 Dec 2025 16:43:35 -0500 Subject: [PATCH 11/12] doc: add significant cells_default documentation I avoided adding docs to the functions that are expected to be changed by either other current PRs or by Katharina's model code. --- hnn_core/cells_default.py | 170 ++++++++++++++++++++++++++++++-------- 1 file changed, 135 insertions(+), 35 deletions(-) diff --git a/hnn_core/cells_default.py b/hnn_core/cells_default.py index 7885fca355..961e0dbc3b 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -129,10 +129,8 @@ def _get_pyr_soma(p_all, cell_type, v_init=-65): `params_default.py::get_L2Pyr_params_default`. cell_type : {'L2Pyr', 'L5Pyr'} Cell type identifier used as prefix in parameter key lookups. - v_init : dict, default={"all": -65} - Initial membrane potential in mV. If dict contains single key "all", that value - is applied to all sections. Otherwise, keys must match 'section_names' for - section-specific initialization. + v_init : float, default=-65 + Initial membrane potential in mV. Returns ------- @@ -196,9 +194,10 @@ def _cell_L2Pyr(override_params, pos=(0.0, 0.0, 0), gid=0): `params_default.py::get_L2Pyr_params_default`. If no overrides are desired, then this argument should be None. pos : tuple of (float, float, int), default=(0.0, 0.0, 0) - 3-dimensional position to place the cell at. + Coordinates of cell soma in xyz-space. gid : int, default=0 - The unique, "global ID" (GID) of the cell. + The unique, "global ID" (GID) of the cell. Once the GID is set, it cannot be + changed. Returns ------- @@ -338,9 +337,10 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0): `params_default.py::get_L5Pyr_params_default`. If no overrides are desired, then this argument should be None. pos : tuple of (float, float, int), default=(0.0, 0.0, 0) - 3-dimensional position to place the cell at. + Coordinates of cell soma in xyz-space. gid : int, default=0 - The unique, "global ID" (GID) of the cell. + The unique, "global ID" (GID) of the cell. Once the GID is set, it cannot be + changed. Returns ------- @@ -465,6 +465,39 @@ def _cell_L5Pyr(override_params, pos=(0.0, 0.0, 0), gid=0): def _get_basket_soma(cell_name, v_init=-64.9737): + """Create Basket somatic Section objects. + + This sets geometric and electrical properties (length, diameter, axial resistance, + membrane capacitance, and spatial end points) using hard-coded values, takes initial + membrane voltage from an argument, and constructs a Section object for each Basket + (inhibitory) soma compartment. + + These parameters are not different between Layer 2 and Layer 5 Basket cells. + + *Importantly*, "Section objects" in this context are objects of the class + `hnn_core/cell.py::Section`, NOT the "true" NEURON sections. The "true" NEURON + sections are only created later, immediately before a simulation is run, using + `NetworkBuilder._build`. + + Parameters + ---------- + cell_name : ??? + Not actually used. + v_init : float, default=-64.9737 + Initial membrane potential in mV. + + Returns + ------- + Section + A Section object with attributes 'L', 'diam', 'Ra', 'cm', and 'end_pts' set from + hard-coded values, and 'v0' set from argument. + + Notes + ----- + - KD: This function is where the initial voltages for the somata are set; these + voltages are not overridden by `h.finitialize` unless called with a value, + e.g. `h.finitialize(-65)`. + """ end_pts = [[0, 0, 0], [0, 0, 39.0]] return Section( L=39.0, @@ -477,6 +510,30 @@ def _get_basket_soma(cell_name, v_init=-64.9737): def _get_pyr_syn_props(p_all, cell_type): + """Return the default synaptic parameters for a given Pyramidal cell_type. + + Extracts the 'e', 'tau1', and 'tau2' parameters for all synapse types for a + particular cell_type. + + Parameters + ---------- + p_all : dict + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_{synapse type}_{property}' (e.g., 'L2Pyr_nmda_tau1'). This 'p_all' + dictionary is expected to be constructed using functions like + `params_default.py::get_L2Pyr_params_default`. + cell_type : {'L2Pyr', 'L5Pyr'} + Cell type identifier used as prefix in parameter key lookups. + + Returns + ------- + dict + A dictionary where the keys are the four synapse types {'ampa', 'nmda', 'gabaa', + 'gabab'}, and where the values are dictionaries whose keys are {'e', 'tau1', + 'tau2'} and whose values are the parameter values of that case from default + parameter dictionaries such as from + `params_default.py::get_L2Pyr_params_default`. + """ return { "ampa": { "e": p_all["%s_ampa_e" % cell_type], @@ -502,6 +559,17 @@ def _get_pyr_syn_props(p_all, cell_type): def _get_basket_syn_props(): + """Return the default synaptic parameters for Basket cell types. + + These parameters are not different between Layer 2 and Layer 5 Basket cells. + + Returns + ------- + dict + A dictionary where the keys are three of the four synapse types {'ampa', 'nmda', + 'gabaa'} (no 'gabab') and where the values are dictionaries whose keys are {'e', + 'tau1', 'tau2'} and whose values are hard-coded. + """ return { "ampa": {"e": 0, "tau1": 0.5, "tau2": 5.0}, "gabaa": {"e": -80, "tau1": 0.5, "tau2": 5.0}, @@ -510,23 +578,29 @@ def _get_basket_syn_props(): def _get_mechanisms(p_all, cell_type, section_names, mechanisms): - """Get mechanism + """Create dictionary of mechanism parameters for Pyramidal cells. Parameters ---------- - cell_type : str - The cell type - section_names : str - The section_names + p_all : dict + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_soma_{property}' (e.g., 'L5Pyr_soma_L'). This 'p_all' dictionary + is expected to be constructed using functions like + `params_default.py::get_L2Pyr_params_default`. + cell_type : {'L2Pyr', 'L5Pyr'} + Cell type identifier used as prefix in parameter key lookups. + section_names : list of str + Names of sections. mechanisms : dict of list - The mechanism properties to extract + The mechanism properties to extract. Dictionary whose keys are mechanism names + and whose values are a list of the parameter names to be set for that mechanism. Returns ------- mech_props : dict of dict of dict Nested dictionary of the form - sections -> mechanism -> mechanism properties - used to instantiate the mechanism in Neuron + "sections -> mechanism -> mechanism properties" + used to instantiate the mechanism in NEURON """ mech_props = dict() for sec_name in section_names: @@ -564,23 +638,40 @@ def _exp_g_at_dist(x, zero_val, exp_term, offset): def basket(cell_name, pos=(0, 0, 0), gid=None): - """Get layer 2 / layer 5 basket cells. + """Create a Cell object of the Basket cell type. + + This constructs a Basket cell type (i.e. either 'L2Basket' or 'L5Basket') using the + following steps: + 1. Sets the "proximal" or "distal" section groups depending on the layer of the + celltype. + 2. Creates the soma Section compartment object using hard-coded values in + sub-functions. + 3. Sets the receiving synaptic types and parameters for the cell, for the three + valid synaptic types ('ampa', 'gabaa', and 'nmda', but not 'gabab'). + 4. Sets the mechanism of the cell to the "hh2" mechanism only, and using the default + values. + + *Importantly*, "Cell objects" in this context are objects of the class + `hnn_core/cell.py::Cell`, not "true" NEURON cells. Similarly, "Section objects" + in this context are objects of the class `hnn_core/cell.py::Section`, NOT the "true" + Section objects as created and used by NEURON. The "true" NEURON sections and cells + are only created later, immediately before a simulation is run, using + `NetworkBuilder._build`. Parameters ---------- - cell_name : str - The name of the cell. - pos : tuple - Coordinates of cell soma in xyz-space - gid : int or None (optional) - Each cell in a network is uniquely identified by it's "global ID": GID. - The GID is an integer from 0 to n_cells, or None if the cell is not - yet attached to a network. Once the GID is set, it cannot be changed. + cell_name : {'L2_basket', 'L5_basket'} + The type of basket cell to create. + pos : tuple of (int, int, int), default=(0, 0, 0) + Coordinates of cell soma in xyz-space. + gid : int, default=None + The unique, "global ID" (GID) of the cell. Once the GID is set, it cannot be + changed. Returns ------- - cell : instance of BasketSingle - The basket cell. + Cell + A Cell object of either the Layer 2/3 or Layer 5 Basket cell type. """ if cell_name == "L2_basket": sect_loc = dict(proximal=["soma"], distal=["soma"]) @@ -608,20 +699,29 @@ def basket(cell_name, pos=(0, 0, 0), gid=None): def pyramidal(cell_name, pos=(0, 0, 0), override_params=None, gid=None): - """Pyramidal neuron. + """Create a Cell object of a Pyramidal neuron. + + This is mostly just a wrapper function for distinguishing between whether to create + a Layer 2/3 Pyramidal cell or a Layer 5 Pyramidal cell. This calls the main + functions to create the Cells, either `_cell_L5Pyr` or `_cell_L2Pyr`. Parameters ---------- - cell_name : str - 'L5Pyr' or 'L2Pyr'. The pyramidal cell type. - pos : tuple + cell_name : {'L2Pyr', 'L5Pyr'} + The type of pyramidal cell to create. + pos : tuple of (int, int, int), default=(0, 0, 0) Coordinates of cell soma in xyz-space override_params : dict or None (optional) - Parameters specific to L2 pyramidal neurons to override the default set + Flat dictionary containing cell parameters with keys formatted as + '{cell_type}_{section}_{property}' (e.g., 'L2Pyr_apicaltrunk_L'), where + key-value pairs are only provided for those values where the user wants to use + custom, non-default parameters. The default parameters can be found in either + `params_default.py::get_L2Pyr_params_default` or + `params_default.py::get_L5Pyr_params_default`. If no overrides are desired, then + this argument should be None. gid : int or None (optional) - Each cell in a network is uniquely identified by it's "global ID": GID. - The GID is an integer from 0 to n_cells, or None if the cell is not - yet attached to a network. Once the GID is set, it cannot be changed. + The unique, "global ID" (GID) of the cell. Once the GID is set, it cannot be + changed. """ if cell_name == "L2_pyramidal": return _cell_L2Pyr(override_params, pos=pos, gid=gid) From 0f168bd615d1500809b62ae9b3f49ced547eae68 Mon Sep 17 00:00:00 2001 From: "Austin E. Soplata" Date: Wed, 10 Dec 2025 17:13:17 -0500 Subject: [PATCH 12/12] test: add volt init tests for cells_default This was initially done with Claude-AI, but heavily refactored and expanded. --- hnn_core/tests/test_cells_default.py | 125 +++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/hnn_core/tests/test_cells_default.py b/hnn_core/tests/test_cells_default.py index 73e59f3055..6a6234354e 100644 --- a/hnn_core/tests/test_cells_default.py +++ b/hnn_core/tests/test_cells_default.py @@ -84,3 +84,128 @@ def test_cells_default(): with pytest.raises(ValueError, match="Unknown basket cell type"): basket(cell_name="blah") + + +def test_basket_voltage_init(): + """Test voltage initialization for Basket cell types.""" + # Current section initial voltage, for soma (only section) of both basket types + expected_basket_v0 = -64.9737 + + # Initial setup + load_custom_mechanisms() + l5b = basket(cell_name="L5_basket") + l2b = basket(cell_name="L2_basket") + + # Test initial voltages (v0) for basket cells + assert np.isclose(l5b.sections["soma"].v0, expected_basket_v0), ( + f"L5_basket soma v0={l5b.sections['soma'].v0}, expected {expected_basket_v0}" + ) + assert np.isclose(l2b.sections["soma"].v0, expected_basket_v0), ( + f"L2_basket soma v0={l2b.sections['soma'].v0}, expected {expected_basket_v0}" + ) + + # Test that v0 values are correctly applied to built NEURON sections + l2b.build() + l5b.build() + + # Initialize with finitialize() which should use the v0 values + h.finitialize() + + # All L2Pyr sections should have the same v0 + l2b_soma_v = l2b._nrn_sections["soma"](0.5).v + l5b_soma_v = l5b._nrn_sections["soma"](0.5).v + + assert np.isclose(l2b_soma_v, expected_basket_v0, atol=0.1), ( + f"L2_Basket soma initial voltage is {l2b_soma_v}, expected {expected_basket_v0}" + ) + assert np.isclose(l5b_soma_v, expected_basket_v0, atol=0.1), ( + f"L5_Basket soma initial voltage is {l5b_soma_v}, expected {expected_basket_v0}" + ) + + +def test_l2pyr_voltage_init(): + """Test voltage initialization for Layer 2/3 Pyramidal cell type.""" + # Current section initial voltage, for all sections + expected_l2pyr_v0 = -71.46 + + # Initial setup + load_custom_mechanisms() + l2pyr = pyramidal(cell_name="L2_pyramidal") + + # Test initial voltages (v0) for L2Pyr + for sec_name, sec in l2pyr.sections.items(): + assert np.isclose(sec.v0, expected_l2pyr_v0), ( + f"L2Pyr {sec_name} v0={sec.v0}, expected {expected_l2pyr_v0}" + ) + + # Test that v0 values are correctly applied to built NEURON sections + l2pyr.build(sec_name_apical="apical_trunk") + + # Initialize with finitialize() which should use the v0 values + h.finitialize() + + # All L2Pyr sections should have the same v0 + soma_v = l2pyr._nrn_sections["soma"](0.5).v + apical_1_v = l2pyr._nrn_sections["apical_1"](0.5).v + basal_1_v = l2pyr._nrn_sections["basal_1"](0.5).v + + assert np.isclose(soma_v, expected_l2pyr_v0, atol=0.1), ( + f"L2Pyr soma initial voltage is {soma_v}, expected {expected_l2pyr_v0}" + ) + assert np.isclose(apical_1_v, expected_l2pyr_v0, atol=0.1), ( + f"L2Pyr apical_1 initial voltage is {apical_1_v}, expected {expected_l2pyr_v0}" + ) + assert np.isclose(basal_1_v, expected_l2pyr_v0, atol=0.1), ( + f"L2Pyr basal_1 initial voltage is {basal_1_v}, expected {expected_l2pyr_v0}" + ) + + +def test_l5pyr_voltage_init(): + """Test voltage initialization for Layer 2/3 Pyramidal cell type.""" + # Current section initial voltages + expected_l5pyr_v0 = { + "apical_1": -71.32, + "apical_2": -69.08, + "apical_tuft": -67.30, + "apical_trunk": -72, + "soma": -72.0, + "basal_1": -72, + "basal_2": -72, + "basal_3": -72, + "apical_oblique": -72, + } + + # Initial setup + load_custom_mechanisms() + l5pyr = pyramidal(cell_name="L5_pyramidal") + + # Test initial voltages (v0) for L5Pyr - different sections have different values + for sec_name, sec in l5pyr.sections.items(): + expected_v0 = expected_l5pyr_v0[sec_name] + assert np.isclose(sec.v0, expected_v0), ( + f"L5Pyr {sec_name} v0={sec.v0}, expected {expected_v0}" + ) + + # Test that v0 values are correctly applied to built NEURON sections + l5pyr.build(sec_name_apical="apical_trunk") + + # After building, NEURON sections should have v initialized to v0 + # Check a few key sections with different v0 values + apical_2_nrn_sec = l5pyr._nrn_sections["apical_2"] + apical_tuft_nrn_sec = l5pyr._nrn_sections["apical_tuft"] + soma_nrn_sec = l5pyr._nrn_sections["soma"] + + # Initialize with finitialize() which should use the v0 values + h.finitialize() + + # Check that voltages match expected v0 values + # Note: We check at the midpoint (0.5) of each section + assert np.isclose(apical_2_nrn_sec(0.5).v, -69.08, atol=0.1), ( + f"apical_2 initial voltage is {apical_2_nrn_sec(0.5).v}, expected -69.08" + ) + assert np.isclose(apical_tuft_nrn_sec(0.5).v, -67.30, atol=0.1), ( + f"apical_tuft initial voltage is {apical_tuft_nrn_sec(0.5).v}, expected -67.30" + ) + assert np.isclose(soma_nrn_sec(0.5).v, -72.0, atol=0.1), ( + f"soma initial voltage is {soma_nrn_sec(0.5).v}, expected -72.0" + )