"""
This module defines the NeuronModel and PointNeuronModel classes.
The NeuronModel class creates a multicompartmental neuron model by connecting
individual compartments and merging their equations, parameters, and custom
events. This model can then be used for creating a population of neurons through
Brian's NeuronGroup. This class also contains useful methods for managing model
properties and for automating the initialization of custom events and simulation
parameters.
The PointNeuronModel is like a NeuronModel but for point-neuron (single-compartment)
models.
"""
import pprint as pp
from copy import deepcopy
from typing import List, Optional, Tuple, Union
import numpy as np
from brian2 import NeuronGroup, Synapses, defaultclock
from brian2.units import Quantity, ms, pA
from .compartment import Compartment, Dendrite, Soma
from .ephysproperties import EphysProperties
from .equations import library_point
from .utils import (DimensionlessCompartmentError, DuplicateEquationsError,
get_logger)
logger = get_logger(__name__)
[docs]
class NeuronModel:
"""
Creates a multicompartmental neuron model by connecting individual
compartments and merging their equations, parameters and custom events.This
model can then be used for creating a population of neurons through Brian's
:doc:`NeuronGroup <brian2:reference/brian2.groups.neurongroup.NeuronGroup>`.
This class also contains useful methods for managing model properties and
for automating the initialization of custom events and simulation parameters.
.. tip::
Dendrify aims to facilitate the development of reduced,
**few-compartmental** I&F models that help us study how key dendritic
properties may affect network-level functions. It is not designed to
substitute morphologically and biophysically detailed neuron models,
commonly used for highly-accurate, single-cell simulations. If you are
interested in the latter category of models, please see Brian's
:doc:`SpatialNeuron
<brian2:reference/brian2.spatialneuron.spatialneuron.SpatialNeuron>`.
Parameters
----------
connections : list[tuple[Compartment, Compartment, str | Quantity]]
A description of how the various compartments belonging to the same
neuron model should be connected.
cm : ~brian2.units.fundamentalunits.Quantity, optional
Specific capacitance (usually μF / cm^2).
gl : ~brian2.units.fundamentalunits.Quantity, optional
Specific leakage conductance (usually μS / cm^2).
r_axial : ~brian2.units.fundamentalunits.Quantity, optional
Axial resistance (usually Ohm * cm).
v_rest : ~brian2.units.fundamentalunits.Quantity, optional
Resting membrane voltage.
scale_factor : float, optional
A global area scale factor, by default ``1.0``.
spine_factor : float, optional
A dendritic area scale factor to account for spines, by default ``1.0``.
Warning
-------
Parameters set here affect all model compartments and can override any
compartment-specific parameters.
Example
-------
>>> # Valid format: [*(x, y, z)], where
>>> # x -> Soma or Dendrite object
>>> # y -> Soma or Dendrite object other than x
>>> # z -> 'half_cylinders' or 'cylinder_ + name' or brian2.nS unit
>>> # (by default 'half_cylinders')
>>> soma = Soma(...)
>>> prox = Dendrite(...)
>>> dist = Dendrite(...)
>>> connections = [(soma, prox, 15*nS), (prox, dist, 10*nS)]
>>> model = NeuronModel(connections)
"""
def __init__(
self,
connections: List[Tuple[Compartment,
Compartment,
Union[str, Quantity, None]]],
cm: Optional[Quantity] = None,
gl: Optional[Quantity] = None,
r_axial: Optional[Quantity] = None,
v_rest: Optional[Quantity] = None,
scale_factor: Optional[float] = None,
spine_factor: Optional[float] = None,
):
self._compartments = None
self._extra_equations = None
self._extra_params = None
self._graph = None
self._parse_compartments(connections)
self._set_properties(cm=cm, gl=gl,
r_axial=r_axial,
v_rest=v_rest,
scale_factor=scale_factor,
spine_factor=spine_factor)
def __str__(self):
equations = self.equations
parameters = pp.pformat(self.parameters)
events = pp.pformat(self.events, width=120)
event_names = pp.pformat(self.event_names)
txt = (f"\nOBJECT\n{6*'-'}\n{self.__class__}\n\n\n"
f"EQUATIONS\n{9*'-'}\n{equations}\n\n\n"
f"PARAMETERS\n{10*'-'}\n{parameters}\n\n\n"
f"EVENTS\n{6*'-'}\n{event_names}\n\n\n"
f"EVENT CONDITIONS\n{16*'-'}\n{events}\n\n\n"
)
return txt
def _parse_compartments(self, comp_list):
# Ensure that all compartments have a unique names
ids, names = [], []
for tup in comp_list:
for i in tup:
if isinstance(i, Compartment):
ids.append(id(i))
names.append(i.name)
if len(set(ids)) != len(set(names)):
raise ValueError(
("Please make sure that all compartments included to a single "
"NeuronModel have unique names.")
)
# Start parsing
self._compartments = []
self._graph = []
# Copy compartments to avoid modifying the original objects
copied_list = self._copy_compartments(comp_list)
for tup in copied_list:
pre, post = tup[0], tup[1]
# Store graph-like representation for debugging or visualization
self._graph.append((pre.name, post.name))
# Include all compartments in a list for easy access
if pre not in self._compartments:
self._compartments.append(pre)
if post not in self._compartments:
self._compartments.append(post)
# Call the connect method from the Compartment class
if len(tup) == 2:
pre.connect(post)
else:
pre.connect(post, g=tup[2])
# Check if all compartments are dimensionless or not
is_dimensionless = [i.dimensionless for i in self._compartments]
if True in is_dimensionless and False in is_dimensionless:
raise DimensionlessCompartmentError(
"When creating a NeuronModel, either all of its\n"
"compartments must be dimensionless or none of them. "
"To resolve this issue, you\n"
"can perform one of the following:\n\n"
"1. Discard these parameters [length, diameter, cm,"
"gl, r_axial]\n if you want to create dimensionless "
"compartments.\n\n"
"2. Discard these parameters [cm_abs, gl_abs] if you want to\n"
" create compartments with physical dimensions."
)
def _copy_compartments(self, comp_list):
error_msg = (
"\n\nValid format: [*(x, y, z)]\n"
f"{26*'-'}\n"
"x -> Soma or Dendrite object.\n"
"y -> Soma or Dendrite object other than x.\n"
"z -> 'half_cylinders' or 'cylinder_ + name' or conductance unit.\n"
" (default: 'half_cylinders' if left blank).\n\n"
"Example:\n"
"[(comp1, comp2), (comp2, comp3, 10*nS)] \n"
)
used = {} # Keep track of copied compartments to avoid duplicates
new_list = []
for tup in comp_list:
# Ensure that users provide correct format
if len(tup) < 2 or len(tup) > 3:
raise ValueError(
f"Invalid number of arguments provided. {error_msg}"
)
# Ensure that users do not use objects that make no sense
if not (isinstance(tup[0], Compartment) and
isinstance(tup[1], Compartment)):
raise TypeError(
f"Invalid compartment type provided. {error_msg}"
)
# Prohibit self connections
if tup[0] is tup[1]:
raise ValueError(
f"ERROR: Cannot connect '{tup[0].name}' to itself. "
f"{error_msg}"
)
if tup[0].name in used:
pre = used[tup[0].name]
else:
pre = deepcopy(tup[0])
used[pre.name] = pre
if tup[1].name in used:
post = used[tup[1].name]
else:
post = deepcopy(tup[1])
used[post.name] = post
if len(tup) == 2:
new_tup = (pre, post)
elif len(tup) == 3:
new_tup = (pre, post, tup[2])
new_list.append(new_tup)
return new_list
def _set_properties(self, cm=None, gl=None, r_axial=None, v_rest=None,
scale_factor=None, spine_factor=None):
params = {'cm': cm, 'gl': gl, 'r_axial': r_axial,
'scale_factor': scale_factor, 'spine_factor': spine_factor}
for comp in self._compartments:
# update v_rest for all compartments if provided
if v_rest:
comp._ephys_object.v_rest = v_rest
# prohibit dimensionless compartments from taking area-related params
if comp.dimensionless and any(params.values()):
raise DimensionlessCompartmentError(
f"The dimensionless compartment '{comp.name}' cannot take "
"the \nfollowing parameters: "
"[cm, gl, r_axial, scale_factor, spine_factor]."
)
# update all other params if provided
if not comp.dimensionless and any(params.values()):
for param, value in params.items():
# omit spine_factor for Soma
if param == 'spine_factor' and isinstance(comp, Soma):
continue
if value:
setattr(comp._ephys_object, param, value)
# make sure to initialize area factors if not provided
if not value and param in ['scale_factor', 'spine_factor']:
setattr(comp._ephys_object, param, 1.0)
[docs]
def config_dspikes(self, event_name: str,
threshold: Union[Quantity, None] = None,
duration_rise: Union[Quantity, None] = None,
duration_fall: Union[Quantity, None] = None,
reversal_rise: Union[Quantity, str, None] = None,
reversal_fall: Union[Quantity, str, None] = None,
offset_fall: Union[Quantity, None] = None,
refractory: Union[Quantity, None] = None
):
"""
Configure the parameters for dendritic spiking.
Parameters
----------
event_name : str
A unique name referring to a specific dSpike type.
threshold : ~brian2.units.fundamentalunits.Quantity, optional
The membrane voltage threshold for dendritic spiking.
duration_rise : ~brian2.units.fundamentalunits.Quantity, optional
The duration of g_rise staying open.
duration_fall : ~brian2.units.fundamentalunits.Quantity, optional
The duration of g_fall staying open.
reversal_rise : (~brian2.units.fundamentalunits.Quantity, str), optional
The reversal potential of the channel that is activated during the rise
(depolarization) phase.
reversal_fall : (~brian2.units.fundamentalunits.Quantity, str), optional
The reversal potential of the channel that is activated during the fall
(repolarization) phase.
offset_fall : ~brian2.units.fundamentalunits.Quantity, optional
The delay for the activation of g_rise.
refractory : ~brian2.units.fundamentalunits.Quantity, optional
The time interval required before dSpike can be activated again.
"""
for comp in self._compartments:
if isinstance(comp, Dendrite) and comp._dspike_params:
event_id = f"{event_name}_{comp.name}"
if event_id in comp._dspike_params:
dt = defaultclock.dt
d = {f"Vth_{event_id}": threshold,
f"duration_rise_{event_id}": comp._timestep(duration_rise, dt),
f"duration_fall_{event_id}": comp._timestep(duration_fall, dt),
f"E_rise_{event_name}": comp._ionic_param(reversal_rise),
f"E_fall_{event_name}": comp._ionic_param(reversal_fall),
f"offset_fall_{event_id}": comp._timestep(offset_fall, dt),
f"refractory_{event_id}": comp._timestep(refractory, dt)}
comp._dspike_params[event_id].update(d)
[docs]
def make_neurongroup(self,
N: int,
method: str = 'euler',
threshold: Optional[str] = None,
reset: Optional[str] = None,
second_reset: Optional[str] = None,
spike_width: Optional[Quantity] = None,
refractory: Union[Quantity, str, bool] = False,
init_rest: bool = True,
init_events: bool = True,
show: bool = False,
**kwargs
) -> Union[NeuronGroup, Tuple]:
"""
Returns a Brian2 NeuronGroup object from a NeuronModel. If a second
reset is provided, it also returns a Synapses object to implement
somatic action potentials with a more realistic shape which also unlocks
dendritic backpropagation. This method can also take all parameters that
are accepted by Brian's
:doc:`NeuronGroup <brian2:reference/brian2.groups.neurongroup.NeuronGroup>`.
Parameters
----------
N : int
The number of neurons in the group.
method : str, optional
The numerical integration method. Either a string with the name of a
registered method (e.g. "euler") or a function that receives an
`Equations` object and returns the corresponding abstract code, by
default ``'euler'``.
threshold : str, optional
The condition which produces spikes. Should be a single line boolean
expression.
reset : str, optional
The (possibly multi-line) string with the code to execute on reset.
refractory : (Quantity, str), optional
Either the length of the refractory period (e.g. ``2*ms``), a string
expression that evaluates to the length of the refractory period
after each spike (e.g. ``'(1 + rand())*ms'``), or a string expression
evaluating to a boolean value, given the condition under which the
neuron stays refractory after a spike (e.g. ``'v > -20*mV'``).
second_reset : str, optional
Option to include a second reset for more realistic somatic spikes.
spike_width : Quantity, optional
The time interval between the two resets.
init_rest : bool, optional
Option to automatically initialize the voltages of all compartments
at the specified resting potentials, by default True.
init_events : bool, optional
Option to automatically initialize all custom events that required
for dendritic spiking, by default True.
show : bool, optional
Option to print the automatically initialized parameters, by default
False.
**kwargs: optional
All other parameters accepted by Brian's NeuronGroup.
Returns
-------
Union[NeuronGroup, Tuple]
If no second reset is added, it returns a NeuronGroup object.
Otherwise, it returns a tuple of (NeuronGroup, Synapses) objects.
"""
group = NeuronGroup(N,
method=method,
threshold=threshold,
reset=reset,
refractory=refractory,
model=self.equations,
events=self.events,
namespace=self.parameters,
**kwargs)
if init_rest:
for comp in self._compartments:
if show:
print(
f"V_{comp.name} = {repr(comp._ephys_object.v_rest)}")
setattr(group, f'V_{comp.name}', comp._ephys_object.v_rest)
if init_events:
if self.event_actions:
for event, action in self.event_actions.items():
if show:
print(f"run_on_event('{event}', '{action}')")
group.run_on_event(event, action, order='before_groups')
ap_reset = None
if any([second_reset, spike_width]):
txt = (
"If you wish to have a more realistic action potential shape, "
"please provide \nvalid values for both [second_reset] and "
"[spike_width]."
)
if not all([second_reset, spike_width]):
raise ValueError(txt)
try:
ap_reset = Synapses(group, group,
on_pre=second_reset,
delay=spike_width,
namespace=self.parameters)
ap_reset.connect(j='i')
except Exception:
raise ValueError(txt)
return (group, ap_reset) if ap_reset else group
[docs]
def add_params(self, params_dict: dict):
"""
Adds extra/custom parameters to a NeuronModel.
Parameters
----------
params_dict : dict
A dictionary of parameters.
"""
if not self._extra_params:
self._extra_params = {}
self._extra_params.update(params_dict)
[docs]
def add_equations(self, eqs: str):
"""
Allows adding custom equations.
Parameters
----------
eqs : str
A string of Brian-compatible equations.
"""
if not self._extra_equations:
self._extra_equations = f"{eqs}"
else:
if eqs not in self._extra_equations:
self._extra_equations += f"\n{eqs}"
else:
logger.warning(
"The equations you are trying to add already exist in the model."
)
[docs]
def replace_equations(self, eqs_old: str, eqs_new: str):
"""
Replaces existing equations with custom ones.
Parameters
----------
eqs_old : str
The existing equations to be replaced.
eqs_new : str
The custom equations.
"""
eqs_found = False
for comp in self._compartments:
if eqs_old in comp._equations:
comp._equations = comp._equations.replace(eqs_old, eqs_new)
eqs_found = True
if not eqs_found:
logger.warning(
"The equations to be replaced are not found in the model."
)
[docs]
def as_graph(self, figsize: list = [6, 4], fontsize: int = 10, fontcolor: str = 'white',
scale_nodes: float = 1, color_soma: str = '#4C6C92',
color_dendrites: str = '#A7361C', alpha: float = 1,
scale_edges: float = 1, seed: Optional[int] = None):
"""
Plots a graph-like representation of a NeuronModel using the
:doc:`Graph <networkx:reference/classes/graph>` class and the
:doc:`Fruchterman-Reingold force-directed algorithm
<networkx:reference/generated/networkx.drawing.layout.spring_layout>`
from `Networkx <https://networkx.org/>`_.
Parameters
----------
fontsize : int, optional
The size in pt of each node's name, by default ``10``.
fontcolor : str, optional
The color of each node's name, by default ``'white'``.
scale_nodes : float, optional
Percentage change in node size, by default ``1``.
color_soma : str, optional
Somatic node color, by default ``'#4C6C92'``.
color_dendrites : str, optional
Dendritic nodes color, by default ``'#A7361C'``.
alpha : float, optional
Nodes color opacity, by default ``1``.
scale_edges : float, optional
The percentage change in edges length, by default ``1``.
seed : int, optional
Set the random state for deterministic node layouts, by default
``None``.
.
"""
import matplotlib.pyplot as plt
import networkx as nx
# Separate soma from dendrites
soma, dendrites = [], []
for comp in self._compartments:
target = soma if isinstance(comp, Soma) else dendrites
target.append(comp.name)
# Make graph
G = nx.Graph()
G.add_edges_from(self._graph)
# Visualize it
fig, ax = plt.subplots(figsize=figsize)
for d in ['right', 'top', 'left', 'bottom']:
ax.spines[d].set_visible(False)
pos = nx.spring_layout(G, fixed=soma, pos={soma[0]: (0, 0)},
k=0.05*scale_edges, iterations=100,
seed=seed)
nx.draw_networkx_nodes(G, pos, nodelist=dendrites,
node_color=color_dendrites,
node_size=1200*scale_nodes, margins=0.1,
ax=ax, alpha=alpha)
nx.draw_networkx_nodes(G, pos, nodelist=soma, node_color=color_soma,
node_size=1200*scale_nodes, ax=ax, alpha=alpha)
nx.draw_networkx_edges(G, pos, alpha=0.5, width=1, ax=ax)
nx.draw_networkx_labels(G, pos, ax=ax, font_color=fontcolor,
font_size=fontsize)
ax.set_title('Model graph', weight='bold')
fig.tight_layout()
plt.show()
@property
def equations(self) -> str:
"""
Returns a string containing all model equations.
Returns
-------
str
All model equations.
"""
all_eqs = [i._equations for i in self._compartments]
if self._extra_equations:
all_eqs.append(self._extra_equations)
return '\n\n'.join(all_eqs)
@property
def parameters(self) -> dict:
"""
Returns a dictionary containing all model parameters.
Returns
-------
dict
All model parameters.
"""
d = {}
for i in self._compartments:
d.update(i.parameters)
if self._extra_params:
d.update(self._extra_params)
return d
@property
def events(self) -> dict:
"""
Returns a dictionary containing all model custom events for dendritic
spiking.
Returns
-------
dict
All model custom events for dendritic spiking.
"""
d_out = {}
dendrites = [i for i in self._compartments if isinstance(i, Dendrite)]
all_events = [i._events for i in dendrites if i._events]
for d in all_events:
d_out.update(d)
return d_out
@property
def event_names(self) -> list:
"""
Returns a list of all event names for dendritic spiking.
Returns
-------
list
All event names for dendritic spiking
"""
return list(self.events.keys())
@property
def event_actions(self) -> dict:
"""
Returns a dictionary containing all event actions for dendritic
spiking.
Returns
-------
list
All event actions for dendritic spiking
"""
d_out = {}
dendrites = [i for i in self._compartments if isinstance(i, Dendrite)]
all_actions = [i._event_actions for i in dendrites if i._event_actions]
for d in all_actions:
d_out.update(d)
return d_out
[docs]
class PointNeuronModel:
"""
Like a :class:`.NeuronModel` but for point-neuron (single-compartment)
models.
Parameters
----------
model : str, optional
A keyword for accessing Dendrify's library models. Custom models can
also be provided but they should be in the same formattable structure as
the library models. Available options: ``'leakyIF'`` (default),
``'adaptiveIF'``, ``'adex'``.
length : ~brian2.units.fundamentalunits.Quantity, optional
The point neuron's length.
diameter : ~brian2.units.fundamentalunits.Quantity, optional
The point neuron's diameter.
cm : ~brian2.units.fundamentalunits.Quantity, optional
Specific capacitance (usually μF / cm^2).
gl : ~brian2.units.fundamentalunits.Quantity, optional
Specific leakage conductance (usually μS / cm^2).
cm_abs : ~brian2.units.fundamentalunits.Quantity, optional
Absolute capacitance (usually pF).
gl_abs : ~brian2.units.fundamentalunits.Quantity, optional
Absolute leakage conductance (usually nS).
v_rest : ~brian2.units.fundamentalunits.Quantity, optional
Resting membrane voltage.
"""
def __init__(
self,
model: str = 'leakyIF',
length: Optional[Quantity] = None,
diameter: Optional[Quantity] = None,
cm: Optional[Quantity] = None,
gl: Optional[Quantity] = None,
cm_abs: Optional[Quantity] = None,
gl_abs: Optional[Quantity] = None,
v_rest: Optional[Quantity] = None,
):
self._equations = None
self._params = None
self._synapses = None
self._extra_equations = None
self._extra_params = None
# Add membrane equations:
self._create_equations(model)
# Keep track of electrophysiological properties:
self._ephys_object = EphysProperties(
name=None,
length=length,
diameter=diameter,
cm=cm,
gl=gl,
cm_abs=cm_abs,
gl_abs=gl_abs,
v_rest=v_rest,
)
def __str__(self):
equations = self.equations
parameters = pp.pformat(self.parameters)
user = pp.pformat(self._ephys_object.__dict__)
txt = (f"\nOBJECT\n{6*'-'}\n{self.__class__}\n\n\n"
f"EQUATIONS\n{9*'-'}\n{equations}\n\n\n"
f"PARAMETERS\n{10*'-'}\n{parameters}\n\n\n"
f"USER PARAMETERS\n{15*'-'}\n{user}")
return txt
def _create_equations(self, model: str):
"""
Adds equations to a compartment.
Parameters
----------
model : str
"""
# Pick a model template or provide a custom model:
if model in library_point:
self._equations = library_point[model]
else:
logger.warning(("The model you provided is not found. The default "
"'passive' membrane model will be used instead."))
self._equations = library_point['passive']
[docs]
def synapse(self,
channel: str,
tag: str,
g: Optional[Quantity] = None,
t_rise: Optional[Quantity] = None,
t_decay: Optional[Quantity] = None,
scale_g: bool = False):
"""
Adds synaptic currents equations and parameters. When only the decay
time constant ``t_decay`` is provided, the synaptic model assumes an
instantaneous rise of the synaptic conductance followed by an exponential
decay. When both the rise ``t_rise`` and decay ``t_decay`` constants are
provided, synapses are modelled as a sum of two exponentials. For more
information see:
`Modeling Synapses by Arnd Roth & Mark C. W. van Rossum
<https://doi.org/10.7551/mitpress/9780262013277.003.0007>`_
Parameters
----------
channel : str
Synaptic channel type. Available options: ``'AMPA'``, ``'NMDA'``,
``'GABA'``.
tag : str
A unique name to distinguish synapses of the same type.
g : :class:`~brian2.units.fundamentalunits.Quantity`
Maximum synaptic conductance
t_rise : :class:`~brian2.units.fundamentalunits.Quantity`
Rise time constant
t_decay : :class:`~brian2.units.fundamentalunits.Quantity`
Decay time constant
scale_g : bool, optional
Option to add a normalization factor to scale the maximum
conductance at 1 when synapses are modelled as a difference of
exponentials (have both rise and decay kinetics), by default
``False``.
Examples
--------
>>> neuron = PointNeuronModel(...)
>>> # adding an AMPA synapse with instant rise & exponential decay:
>>> neuron.synapse('AMPA', tag='X', g=1*nS, t_decay=5*ms)
>>> # same channel, different conductance & source:
>>> neuron.synapse('AMPA', tag='Y', g=2*nS, t_decay=5*ms)
>>> # different channel with both rise & decay kinetics:
>>> neuron.synapse('NMDA', tag='X' g=1*nS, t_rise=5*ms, t_decay=50*ms)
"""
synapse_id = "_".join([channel, tag])
if self._synapses:
# Check if this synapse already exists
if synapse_id in self._synapses:
raise DuplicateEquationsError(
f"The equations of '{channel}_{tag}' have already been "
f"added. \nPlease use a different "
f"combination of [channel, tag] when calling the synapse() "
"method \nmultiple times on a single compartment. You might"
" also see this error if you are using \nJupyter/iPython "
"which store variable values in memory. Try cleaning all "
"variables or \nrestart the kernel before running your "
"code. If this problem persists, please report it \n"
"by creating a new issue here: "
"https://github.com/Poirazi-Lab/dendrify/issues."
)
else:
self._synapses = []
# Switch to rise/decay equations if t_rise & t_decay are provided
key = f"{channel}_rd" if all([t_rise, t_decay]) else channel
current_name = f'I_{channel}_{tag}'
current_eqs = library_point[key].format(tag)
to_replace = '= I_ext'
self._equations = self._equations.replace(
to_replace,
f'{to_replace} + {current_name}'
)
self._equations += '\n'+current_eqs
if not self._params:
self._params = {}
weight = f"w_{channel}_{tag}"
self._params[weight] = 1.0
# If user provides a value for g, then add it to _params
if g:
self._params[f'g_{channel}_{tag}'] = g
if t_rise:
self._params[f't_{channel}_rise_{tag}'] = t_rise
if t_decay:
self._params[f't_{channel}_decay_{tag}'] = t_decay
if scale_g:
if all([t_rise, t_decay, g]):
norm_factor = Compartment.g_norm_factor(t_rise, t_decay)
self._params[f'g_{channel}_{tag}'] *= norm_factor
self._synapses.append(synapse_id)
[docs]
def noise(self, tau: Quantity = 20*ms, sigma: Quantity = 1*pA,
mean: Quantity = 0*pA):
"""
Adds a stochastic noise current. For more information see the Noise
section: of :doc:`brian2:user/models`
Parameters
----------
tau : :class:`~brian2.units.fundamentalunits.Quantity`, optional
Time constant of the Gaussian noise, by default ``20*ms``
sigma : :class:`~brian2.units.fundamentalunits.Quantity`, optional
Standard deviation of the Gaussian noise, by default ``3*pA``
mean : :class:`~brian2.units.fundamentalunits.Quantity`, optional
Mean of the Gaussian noise, by default ``0*pA``
"""
noise_current = 'I_noise'
if noise_current in self.equations:
raise DuplicateEquationsError(
f"The equations of '{noise_current}' have already been "
f"added to the model. \nYou might be seeing this error if "
"you are using Jupyter/iPython "
"which store variable values \nin memory. Try cleaning all "
"variables or restart the kernel before running your "
"code. If this \nproblem persists, please report it "
"by creating a new issue here:\n"
"https://github.com/Poirazi-Lab/dendrify/issues."
)
noise_eqs = library_point['noise']
to_change = '= I_ext'
self._equations = self._equations.replace(
to_change,
f'{to_change} + {noise_current}'
)
self._equations = f"{self._equations}\n{noise_eqs}"
# Add _params:
if not self._params:
self._params = {}
self._params['tau_noise'] = tau
self._params['sigma_noise'] = sigma
self._params['mean_noise'] = mean
[docs]
def make_neurongroup(self, N: int, **kwargs) -> NeuronGroup:
"""
Create a NeuronGroup object with the specified number of neurons.
Parameters:
N (int): The number of neurons in the group.
**kwargs: Additional keyword arguments to be passed to the NeuronGroup constructor.
Returns:
NeuronGroup: The created NeuronGroup object.
"""
group = NeuronGroup(N, model=self.equations,
namespace=self.parameters,
**kwargs)
setattr(group, 'V', self._ephys_object.v_rest)
return group
[docs]
def add_params(self, params_dict: dict):
"""
Allows specifying extra/custom parameters.
Parameters
----------
params_dict : dict
A dictionary of parameters.
"""
if not self._extra_params:
self._extra_params = {}
self._extra_params.update(params_dict)
[docs]
def add_equations(self, eqs: str):
"""
Allows adding custom equations.
Parameters
----------
eqs : str
A string of Brian-compatible equations.
"""
if not self._extra_equations:
self._extra_equations = f"{eqs}"
else:
self._extra_equations += f"\n{eqs}"
@property
def parameters(self) -> dict:
"""
Returns all the parameters that have been generated for a single
compartment.
Returns
-------
dict
"""
d_out = {}
if self._params:
d_out.update(self._params)
if self._extra_params:
d_out.update(self._extra_params)
if self._ephys_object:
d_out.update(self._ephys_object.parameters)
return d_out
@property
def area(self) -> Quantity:
"""
Returns a compartment's surface area (open cylinder) based on its length
and diameter.
Returns
-------
:class:`~brian2.units.fundamentalunits.Quantity`
"""
return self._ephys_object.area
@property
def capacitance(self) -> Quantity:
"""
Returns a compartment's absolute capacitance.
Returns
-------
:class:`~brian2.units.fundamentalunits.Quantity`
"""
return self._ephys_object.capacitance
@property
def g_leakage(self) -> Quantity:
"""
A compartment's absolute leakage conductance.
Returns
-------
:class:`~brian2.units.fundamentalunits.Quantity`
"""
return self._ephys_object.g_leakage
@property
def equations(self) -> str:
"""
Returns all differential equations that describe a single compartment
and the mechanisms that have been added to it.
Returns
-------
str
"""
if self._extra_equations:
return f"{self._equations}\n\n{self._extra_equations}"
return self._equations
@staticmethod
def g_norm_factor(trise: Quantity, tdecay: Quantity):
tpeak = (tdecay*trise / (tdecay-trise)) * np.log(tdecay/trise)
factor = (((tdecay*trise) / (tdecay-trise))
* (-np.exp(-tpeak/trise) + np.exp(-tpeak/tdecay))
/ ms)
return 1/factor
@property
def dimensionless(self) -> bool:
"""
Checks if a compartment has been flagged as dimensionless.
Returns
-------
bool
"""
return bool(self._ephys_object._dimensionless)