diff --git a/hnn_core/cell.py b/hnn_core/cell.py index b1be611a8..2569085c7 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] + 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. Attributes @@ -201,15 +203,18 @@ class Section: membrane capacitance in micro-Farads. Ra : float axial resistivity in ohm-cm. + v0 : float + Initial 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, v0=-65, end_pts=None): self._L = L self._diam = diam self._Ra = Ra self._cm = cm + self._v0 = v0 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}, v0={self.v0}" 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["v0"] = self.v0 # 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 v0(self): + return self._v0 + @property def end_pts(self): return self._end_pts @@ -574,6 +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) + # 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] @@ -642,6 +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 + # 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() @@ -1062,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): + 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 @@ -1077,6 +1093,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. + v0 : float | int | None + start value for membrane potential in millivolts. Notes ----- @@ -1101,4 +1119,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 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 6d3abb095..961e0dbc3 100644 --- a/hnn_core/cells_default.py +++ b/hnn_core/cells_default.py @@ -15,13 +15,66 @@ # units for taur: ms -def _get_dends(params, cell_type, section_names): - """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 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 + 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 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() @@ -29,37 +82,136 @@ def _get_dends(params, cell_type, section_names): 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("_", "") 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"], ) return sections -def _get_pyr_soma(p_all, cell_type): - """Get somatic properties.""" +def _get_pyr_soma(p_all, cell_type, v_init=-65): + """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 : float, default=-65 + Initial membrane potential in mV. + + 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"], cm=p_all[f"{cell_type}_soma_cm"], Ra=p_all[f"{cell_type}_soma_Ra"], + v0=v_init, ) -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) + Coordinates of cell soma in xyz-space. + gid : int, default=0 + The unique, "global ID" (GID) of the cell. Once the GID is set, it cannot be + changed. + + 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 = [ "apical_trunk", "apical_1", @@ -70,8 +222,19 @@ 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["soma"] = _get_pyr_soma(p_all, "L2Pyr") + sections = _get_dends( + p_all, + cell_type="L2Pyr", + section_names=section_names, + v_init={ + "all": all_v_init, + }, + ) + sections["soma"] = _get_pyr_soma( + p_all, + "L2Pyr", + v_init=all_v_init, + ) end_pts = { "soma": [[-50, 0, 765], [-50, 0, 778]], @@ -134,8 +297,56 @@ 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) + Coordinates of cell soma in xyz-space. + gid : int, default=0 + The unique, "global ID" (GID) of the cell. Once the GID is set, it cannot be + changed. + + Returns + ------- + Cell + A Cell object of the Layer 5 Pyramidal cell type. + """ p_all = get_L5Pyr_params_default() if override_params is not None: @@ -153,8 +364,30 @@ 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") + # Different sections of this cell type use different initial membrane voltages: + 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]], @@ -168,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"], @@ -230,12 +464,76 @@ 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): + """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, diam=20.0, cm=0.85, Ra=200.0, end_pts=end_pts) + return Section( + L=39.0, + diam=20.0, + cm=0.85, + Ra=200.0, + v0=v_init, + end_pts=end_pts, + ) 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], @@ -261,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}, @@ -269,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: @@ -323,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"]) @@ -367,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) diff --git a/hnn_core/hnn_io.py b/hnn_core/hnn_io.py index 18b2dcb75..ba7d8b6d0 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 "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"], + v0=section_data["v0"], + 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/network_builder.py b/hnn_core/network_builder.py index 3806a04e2..7279d5eb4 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 42f875636..5a6963686 100644 --- a/hnn_core/param/jones2009_base.json +++ b/hnn_core/param/jones2009_base.json @@ -32,6 +32,7 @@ ] ], "nseg": 1, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -110,6 +111,7 @@ ] ], "nseg": 1, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -146,6 +148,7 @@ ] ], "nseg": 7, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -182,6 +185,7 @@ ] ], "nseg": 5, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -218,6 +222,7 @@ ] ], "nseg": 7, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -254,6 +259,7 @@ ] ], "nseg": 1, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -290,6 +296,7 @@ ] ], "nseg": 5, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -326,6 +333,7 @@ ] ], "nseg": 5, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -362,6 +370,7 @@ ] ], "nseg": 1, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -494,6 +503,7 @@ ] ], "nseg": 1, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -570,6 +580,7 @@ ] ], "nseg": 3, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -632,6 +643,7 @@ ] ], "nseg": 13, + "v0": -71.32, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -714,6 +726,7 @@ ] ], "nseg": 13, + "v0": -69.08, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -796,6 +809,7 @@ ] ], "nseg": 9, + "v0": -67.3, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -870,6 +884,7 @@ ] ], "nseg": 5, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -936,6 +951,7 @@ ] ], "nseg": 1, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -994,6 +1010,7 @@ ] ], "nseg": 5, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1060,6 +1077,7 @@ ] ], "nseg": 5, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1126,6 +1144,7 @@ ] ], "nseg": 1, + "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 89ae561d2..78c7e4dda 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, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -110,6 +111,7 @@ ] ], "nseg": 1, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -146,6 +148,7 @@ ] ], "nseg": 7, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -182,6 +185,7 @@ ] ], "nseg": 5, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -218,6 +222,7 @@ ] ], "nseg": 7, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -254,6 +259,7 @@ ] ], "nseg": 1, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -290,6 +296,7 @@ ] ], "nseg": 5, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -326,6 +333,7 @@ ] ], "nseg": 5, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -362,6 +370,7 @@ ] ], "nseg": 1, + "v0": -71.46, "mechs": { "km": { "gbar_km": 250.0 @@ -494,6 +503,7 @@ ] ], "nseg": 1, + "v0": -64.9737, "mechs": { "hh2": {} }, @@ -570,6 +580,7 @@ ] ], "nseg": 3, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -632,6 +643,7 @@ ] ], "nseg": 13, + "v0": -71.32, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -714,6 +726,7 @@ ] ], "nseg": 13, + "v0": -69.08, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -796,6 +809,7 @@ ] ], "nseg": 9, + "v0": -67.3, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -870,6 +884,7 @@ ] ], "nseg": 5, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -936,6 +951,7 @@ ] ], "nseg": 1, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -994,6 +1010,7 @@ ] ], "nseg": 5, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1060,6 +1077,7 @@ ] ], "nseg": 5, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, @@ -1126,6 +1144,7 @@ ] ], "nseg": 1, + "v0": -72, "mechs": { "hh2": { "gkbar_hh2": 0.01, diff --git a/hnn_core/tests/test_cells_default.py b/hnn_core/tests/test_cells_default.py index 73e59f305..6a6234354 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" + )