Source code for bluecellulab.synapse.synapse_types

# Copyright 2023-2024 Blue Brain Project / EPFL

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Class that represents a synapse in bluecellulab."""

from __future__ import annotations
from typing import Any, NamedTuple, Optional
import logging

import neuron
import pandas as pd

from bluecellulab.circuit import SynapseProperty
from bluecellulab.circuit.node_id import CellId
from bluecellulab.rngsettings import RNGSettings
from bluecellulab.type_aliases import HocObjectType, NeuronSection


logger = logging.getLogger(__name__)


[docs] class SynapseID(NamedTuple): """Class that represents a synapse id in bluecellulab.""" projection: str sid: int
[docs] class SynapseHocArgs(NamedTuple): """Parameters required by synapse hoc constructor.""" location: float section: NeuronSection
[docs] class Synapse: """Class that represents a synapse in bluecellulab.""" def __init__( self, cell_id: CellId, hoc_args: SynapseHocArgs, syn_id: tuple[str, int], syn_description: pd.Series, popids: tuple[int, int], post_gid: int | None, extracellular_calcium: float | None = None): """Constructor. Args: cell_id: Identifier of the postsynaptic cell. hoc_args: The synapse location and section in hoc. syn_id: A tuple containing a synapse identifier, where the string is the projection name and int is the synapse id. An empty string refers to a local connection. syn_description: Parameters of the synapse. popids: A tuple containing source and target popids used by the random number generation. post_gid: Global postsynaptic gid used for RNG seeding and synapse initialization. If None, falls back to cell_id.id for backward compatibility with standalone Cell usage. extracellular_calcium: The extracellular calcium concentration. Optional and defaults to None. """ self.persistent: list[HocObjectType] = [] self.synapseconfigure_cmds: list[str] = [] self._delay_weights: list[tuple[float, float]] = [] self._weight: Optional[float] = None self.post_cell_id = cell_id self.syn_id = SynapseID(*syn_id) self.extracellular_calcium = extracellular_calcium self.syn_description: pd.Series = self.update_syn_description(syn_description) self.hsynapse: Optional[HocObjectType] = None self.source_popid, self.target_popid = popids self.pre_gid = int(self.syn_description[SynapseProperty.PRE_GID]) self.post_gid = int(cell_id.id if post_gid is None else post_gid) self.hoc_args = hoc_args self.mech_name: str = "not-yet-defined" self.randseed3: Optional[int] = None @property def delay_weights(self) -> list[tuple[float, float]]: """Adjustments to synapse delay and weight.""" return self._delay_weights @delay_weights.setter def delay_weights(self, value: list[tuple[float, float]]) -> None: self._delay_weights = value @property def weight(self) -> float | None: """The last overridden synapse weight.""" return self._weight @weight.setter def weight(self, value: float | None) -> None: self._weight = value
[docs] def update_syn_description(self, syn_description: pd.Series) -> pd.Series: """Change data types, compute more columns needed by the simulator.""" # if the optional properties are NaN (that happens due to pandas outer join), then remove them for prop in [SynapseProperty.U_HILL_COEFFICIENT, SynapseProperty.NRRP]: if prop in syn_description and pd.isna(syn_description[prop]): syn_description.pop(prop) if SynapseProperty.NRRP in syn_description: try: int(syn_description[SynapseProperty.NRRP]) except ValueError: # delete SynapseProperty.NRRP from syn_description syn_description.pop(SynapseProperty.NRRP) if SynapseProperty.U_HILL_COEFFICIENT in syn_description: syn_description["u_scale_factor"] = self.calc_u_scale_factor( syn_description[SynapseProperty.U_HILL_COEFFICIENT], self.extracellular_calcium) else: syn_description["u_scale_factor"] = 1.0 syn_description[SynapseProperty.U_SYN] *= syn_description["u_scale_factor"] return syn_description
[docs] def apply_hoc_configuration(self, hoc_configure_params: list[str]) -> None: """Apply the list of hoc configuration commands to the synapse.""" self.synapseconfigure_cmds = [] # hoc exec synapse configure blocks for cmd in hoc_configure_params: self.synapseconfigure_cmds.append(cmd) cmd = cmd.replace('%s', '\n%(syn)s') hoc_cmd = cmd % {'syn': self.hsynapse.hname()} # type: ignore hoc_cmd = '{%s}' % hoc_cmd neuron.h(hoc_cmd)
def _set_gabaab_ampanmda_rng(self) -> None: """Setup the RNG for the gabaab and ampanmd helpers. Raises: ValueError: when rng mode is not recognised. """ rng_settings = RNGSettings.get_instance() if rng_settings.mode == "Random123": self.randseed1 = self.post_gid + 1 + 250 # convert 0-based GID to 1-based for RNG seeding (Neurodamus convention) self.randseed2 = self.syn_id.sid + 100 self.randseed3 = self.source_popid * 65536 + self.target_popid + \ rng_settings.synapse_seed + 300 self.hsynapse.setRNG( # type: ignore self.randseed1, self.randseed2, self.randseed3) else: rndd = neuron.h.Random() if rng_settings.mode == "Compatibility": self.randseed1 = self.syn_id.sid * 100000 + 100 self.randseed2 = self.post_gid + \ 250 + rng_settings.base_seed elif rng_settings.mode == "UpdatedMCell": self.randseed1 = self.syn_id.sid * 1000 + 100 self.randseed2 = self.source_popid * 16777216 + \ self.post_gid + \ 250 + rng_settings.base_seed + \ rng_settings.synapse_seed else: raise ValueError( "Synapse: unknown RNG mode: %s" % rng_settings.mode) self.randseed3 = None # Not used in this case rndd.MCellRan4(self.randseed1, self.randseed2) rndd.uniform(0, 1) self.hsynapse.setRNG(rndd) # type: ignore self.persistent.append(rndd)
[docs] def delete(self) -> None: """Delete the NEURON objects of the connection.""" if hasattr(self, 'persistent'): for persistent_object in self.persistent: del persistent_object
@staticmethod def calc_u_scale_factor(u_hill_coefficient, extracellular_calcium): if extracellular_calcium is None or u_hill_coefficient is None: return 1.0 def constrained_hill(K_half, y) -> float: """Calculates the constrained hill coefficient.""" K_half_fourth = K_half**4 y_fourth = y**4 return (K_half_fourth + 16) / 16 * y_fourth / (K_half_fourth + y_fourth) u_scale_factor = constrained_hill(u_hill_coefficient, extracellular_calcium) logger.debug( "Scaling synapse Use with u_hill_coeffient %f, " "extra_cellular_calcium %f with a factor of %f" % (u_hill_coefficient, extracellular_calcium, u_scale_factor)) return u_scale_factor @property def info_dict(self) -> dict[str, Any]: """Convert the synapse info to a dict from which it can be reconstructed.""" synapse_dict: dict[str, Any] = {} synapse_dict['synapse_id'] = self.syn_id synapse_dict['pre_cell_id'] = self.pre_gid synapse_dict['post_cell_id'] = self.post_cell_id.id synapse_dict['syn_description'] = self.syn_description.to_dict() # if keys are enum make them str synapse_dict['syn_description'] = { str(k): v for k, v in synapse_dict['syn_description'].items()} synapse_dict['post_segx'] = self.hoc_args.location synapse_dict['mech_name'] = self.mech_name synapse_dict['randseed1'] = self.randseed1 synapse_dict['randseed2'] = self.randseed2 synapse_dict['randseed3'] = self.randseed3 synapse_dict['synapseconfigure_cmds'] = self.synapseconfigure_cmds # Parameters of the mod mechanism synapse_dict['synapse_parameters'] = {} synapse_dict['synapse_parameters']['Use'] = self.hsynapse.Use # type: ignore synapse_dict['synapse_parameters']['Dep'] = self.hsynapse.Dep # type: ignore synapse_dict['synapse_parameters']['Fac'] = self.hsynapse.Fac # type: ignore synapse_dict['synapse_parameters']['extracellular_calcium'] = \ self.extracellular_calcium return synapse_dict def __del__(self) -> None: self.delete()
[docs] class GluSynapse(Synapse): def __init__(self, gid, hoc_args, syn_id, syn_description, popids, post_gid, extracellular_calcium): super().__init__(gid, hoc_args, syn_id, syn_description, popids, post_gid, extracellular_calcium) self.use_glusynapse_helper()
[docs] def use_glusynapse_helper(self) -> None: """Python implementation of the GluSynapse helper. This helper object will encapsulate the hoc actions needed to create our plastic excitatory synapse. """ self.mech_name = 'GluSynapse' self.hsynapse = neuron.h.GluSynapse( self.hoc_args.location, sec=self.hoc_args.section ) self.hsynapse.Use_d = self.syn_description["Use_d_TM"] * \ self.syn_description["u_scale_factor"] self.hsynapse.Use_p = self.syn_description["Use_p_TM"] * \ self.syn_description["u_scale_factor"] self.hsynapse.theta_d_GB = self.syn_description["theta_d"] self.hsynapse.theta_p_GB = self.syn_description["theta_p"] self.hsynapse.rho0_GB = self.syn_description["rho0_GB"] self.hsynapse.volume_CR = self.syn_description["volume_CR"] self.hsynapse.gmax_d_AMPA = self.syn_description["gmax_d_AMPA"] self.hsynapse.gmax_p_AMPA = self.syn_description["gmax_p_AMPA"] if self.hsynapse.rho0_GB > neuron.h.rho_star_GB_GluSynapse: self.hsynapse.gmax0_AMPA = self.hsynapse.gmax_p_AMPA self.hsynapse.Use = self.hsynapse.Use_p else: self.hsynapse.gmax0_AMPA = self.hsynapse.gmax_d_AMPA self.hsynapse.Use = self.hsynapse.Use_d self.hsynapse.gmax_NMDA = self.hsynapse.gmax0_AMPA * \ self.syn_description[SynapseProperty.CONDUCTANCE_RATIO] self.hsynapse.tau_d_AMPA = self.syn_description[SynapseProperty.DTC] self.hsynapse.Dep = abs(self.syn_description[SynapseProperty.D_SYN]) self.hsynapse.Fac = abs(self.syn_description[SynapseProperty.F_SYN]) if self.syn_description[SynapseProperty.NRRP] >= 0: self.hsynapse.Nrrp = self.syn_description[SynapseProperty.NRRP] self.randseed1 = self.post_gid self.randseed2 = 100000 + self.syn_id.sid rng_settings = RNGSettings.get_instance() self.randseed3 = rng_settings.synapse_seed + 200 self.hsynapse.setRNG(self.randseed1, self.randseed2, self.randseed3) self.hsynapse.synapseID = self.syn_id.sid
@property def info_dict(self): parent_dict = super().info_dict parent_dict['synapse_parameters']['tau_d_AMPA'] = self.hsynapse.tau_d_AMPA return parent_dict
[docs] class GabaabSynapse(Synapse): def __init__(self, gid, hoc_args, syn_id, syn_description, popids, post_gid, extracellular_calcium, randomize_risetime=True): super().__init__(gid, hoc_args, syn_id, syn_description, popids, post_gid, extracellular_calcium) self.use_gabaab_helper(randomize_risetime)
[docs] def use_gabaab_helper(self, randomize_gaba_risetime: bool) -> None: """Python implementation of the GABAAB helper. This helper object will encapsulate the hoc actions needed to create our typical inhibitory synapse. Args: randomize_gaba_risetime: if True then RNG code runs to set tau_r_GABAA value. Raises: ValueError: raised when the RNG mode is unknown. """ self.mech_name = 'ProbGABAAB_EMS' self.hsynapse = neuron.h.ProbGABAAB_EMS( self.hoc_args.location, sec=self.hoc_args.section ) if randomize_gaba_risetime is True: rng = neuron.h.Random() rng_settings = RNGSettings.get_instance() if rng_settings.mode == "Compatibility": rng.MCellRan4( self.syn_id.sid * 100000 + 100, self.post_gid + 250 + rng_settings.base_seed) elif rng_settings.mode == "UpdatedMCell": rng.MCellRan4( self.syn_id.sid * 1000 + 100, self.source_popid * 16777216 + self.post_gid + 250 + rng_settings.base_seed + rng_settings.synapse_seed) elif rng_settings.mode == "Random123": rng.Random123( self.post_gid + 250, self.syn_id.sid + 100, self.source_popid * 65536 + self.target_popid + rng_settings.synapse_seed + 450) else: raise ValueError( "Synapse: unknown RNG mode: %s" % rng_settings.mode) rng.lognormal(0.2, 0.1) self.hsynapse.tau_r_GABAA = rng.repick() if SynapseProperty.CONDUCTANCE_RATIO in self.syn_description: self.hsynapse.GABAB_ratio = self.syn_description[SynapseProperty.CONDUCTANCE_RATIO] self.hsynapse.tau_d_GABAA = self.syn_description[SynapseProperty.DTC] self.hsynapse.Use = abs(self.syn_description[SynapseProperty.U_SYN]) self.hsynapse.Dep = abs(self.syn_description[SynapseProperty.D_SYN]) self.hsynapse.Fac = abs(self.syn_description[SynapseProperty.F_SYN]) if SynapseProperty.NRRP in self.syn_description: self.hsynapse.Nrrp = self.syn_description[SynapseProperty.NRRP] self._set_gabaab_ampanmda_rng() self.hsynapse.synapseID = self.syn_id.sid
@property def info_dict(self): parent_dict = super().info_dict parent_dict['synapse_parameters']['tau_d_GABAA'] = self.hsynapse.tau_d_GABAA parent_dict['synapse_parameters']['tau_r_GABAA'] = self.hsynapse.tau_r_GABAA return parent_dict
[docs] class AmpanmdaSynapse(Synapse): def __init__(self, gid, hoc_args, syn_id, syn_description, popids, post_gid, extracellular_calcium): super().__init__(gid, hoc_args, syn_id, syn_description, popids, post_gid, extracellular_calcium) self.use_ampanmda_helper()
[docs] def use_ampanmda_helper(self) -> None: """Python implementation of the AMPANMDA helper. This helper object will encapsulate the hoc actions needed to create our typical excitatory synapse. """ self.mech_name = 'ProbAMPANMDA_EMS' self.hsynapse = neuron.h.ProbAMPANMDA_EMS( self.hoc_args.location, sec=self.hoc_args.section, ) self.hsynapse.tau_d_AMPA = self.syn_description[SynapseProperty.DTC] if SynapseProperty.CONDUCTANCE_RATIO in self.syn_description: self.hsynapse.NMDA_ratio = self.syn_description[SynapseProperty.CONDUCTANCE_RATIO] self.hsynapse.Use = abs(self.syn_description[SynapseProperty.U_SYN]) self.hsynapse.Dep = abs(self.syn_description[SynapseProperty.D_SYN]) self.hsynapse.Fac = abs(self.syn_description[SynapseProperty.F_SYN]) if SynapseProperty.NRRP in self.syn_description: self.hsynapse.Nrrp = self.syn_description[SynapseProperty.NRRP] self._set_gabaab_ampanmda_rng() self.hsynapse.synapseID = self.syn_id.sid
@property def info_dict(self): parent_dict = super().info_dict parent_dict['synapse_parameters']['tau_d_AMPA'] = self.hsynapse.tau_d_AMPA return parent_dict