from .core import transform, transform_record, _is_sequence
from .error_handler import catch_hoc_error, CatchRecord
_registration_queue = []
_had_pointers_wrapped = set()
def _safe_call(method):
"""
Internal decorator to defer a method to the underlying NEURON object,
unpacking all args and returning the result to the decorated method.
"""
def caller(self, *args, **kwargs):
call_result = self._safe_call(method.__name__, *args, **kwargs)
return method(self, call_result, *args, **kwargs)
return caller
[docs]class PythonHocObject:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
try:
from .interpreter import PythonHocInterpreter
except ImportError:
_registration_queue.append(cls)
return
PythonHocInterpreter.register_hoc_object(cls)
def __init__(self, interpreter, ptr):
# Initialize ourselves with a reference to our own "pointer"
# and prepare a list for other references.
self._neuron_ptr = transform(ptr)
self._references = []
self._interpreter = interpreter
super().__init__()
def __getattr__(self, attr):
# Return underlying attributes that aren't explicitly set on the wrapper
return getattr(self.__dict__["_neuron_ptr"], attr)
def __setattr__(self, attr, value):
# Set attributes on the underlying pointer, and set on self if they don't
# exist on the underlying pointer. This allows you to set arbitrary values
# on the NEURON objects as you would be able to with a real Pythonic object.
try:
setattr(self._neuron_ptr, attr, value)
except (LookupError, AttributeError) as _:
self.__dict__[attr] = value
def __call__(self, *args, **kwargs):
# Relay calls to self to the underlying pointer
return self._neuron_ptr(*args, **kwargs)
def __iter__(self):
# Create an iterator from ourselves.
ptr = self.__neuron__()
if type(ptr).__name__ == "Section":
# Iter on section isn't a full iterator.
return ptr
# Relay iteration to the underlying pointer
try:
return iter(ptr)
except TypeError:
raise
def __bool__(self):
return True
def __len__(self):
# Relay length to pointer
return len(self.__neuron__())
def __eq__(self, other):
return transform(self) is transform(other)
def __repr__(self):
ostr = object.__repr__(self)
return ostr[: ostr.rindex("at")] + "pointing to '" + str(self.__neuron__()) + "'>"
def __hash__(self):
return object.__hash__(self)
[docs] def __neuron__(self):
"""
Magic method that is called when this object is passed to NEURON.
"""
return self._neuron_ptr
[docs] def __ref__(self, obj):
"""
Magic method that is called when a strong reference needs to be stored on the
object.
"""
if obj not in self._references:
self._references.append(obj)
[docs] def __deref__(self, obj):
"""
Magic method that is called when a strong reference needs to be removed from the
object.
"""
try:
self._references.remove(obj)
return True
except ValueError:
return False
def _safe_call(self, func_name, *args, **kwargs):
"""
Unpacks all arguments to their NEURON variant and retrieves the naked
function from the HocObject then calls it.
"""
func = getattr(transform(self), func_name)
args = [transform(a) for a in args]
kwargs = {k: transform(v) for k, v in kwargs.items()}
return func(*args, **kwargs)
[docs]class Connectable:
def __init__(self):
# Prepare a dictionary that lists which other NEURON parts this is connected to
self._connections = {}
[docs]class PointerWrapper:
def __init__(self, attr):
self._attr = attr
def __get__(self, instance, owner):
if instance is None:
return owner
value = getattr(instance.__neuron__(), self._attr)
t = instance._interpreter.t
class SimulationValue(type(value)):
def __record__(v):
return getattr(instance.__neuron__(), f"_ref_{self._attr}")
def __str__(v):
return str(type(value)(v))
def __repr__(v):
return f"<{self._attr}={value} at t={t} of {instance}>"
return SimulationValue(value)
[docs]class WrapsPointers:
def __init__(self):
self._init_pointers_wrappers()
def _init_pointers_wrappers(self):
cls = type(self)
target = self.__neuron__()
hoctype = str(target).split("[")[0].split("_0x")[0]
if hoctype not in _had_pointers_wrapped:
for k in dir(target):
if not k.startswith("_"):
try:
is_ptr = str(getattr(target, f"_ref_{k}", None)).startswith(
"<pointer"
)
except:
is_ptr = False
if is_ptr:
setattr(cls, k, PointerWrapper(k))
_had_pointers_wrapped.add(hoctype)
[docs]class Section(PythonHocObject, Connectable, WrapsPointers):
def __init__(self, *args, **kwargs):
PythonHocObject.__init__(self, *args, **kwargs)
Connectable.__init__(self)
[docs] def connect(self, target, *args, **kwargs):
"""
Connect this section to another one as child section.
"""
nrn_target = transform(target)
self.__neuron__().connect(nrn_target, *args, **kwargs)
if hasattr(target, "__ref__"):
target.__ref__(self)
self.__ref__(target)
@property
def parent(self):
"""
Returns the parent of the Section, or ``None``
"""
ref = self._interpreter.SectionRef(sec=self)
return Section(self._interpreter, ref.parent) if ref.has_parent() else None
def __arc__(self):
"""
Return the default arc-position (a point in the closed interval [0, 1]
that represents the position between start and end of the Section).
Defaults to 0.5
"""
return 0.5
def __netcon__(self):
"""
Return the default pointer to connect to a NetCon.
Defaults to ``self(0.5)._ref_v``
"""
return self(self.__arc__()).__netcon__()
def __record__(self):
"""
Return the default pointer to record.
Defaults to ``self(0.5)._ref_v``
"""
return self(self.__arc__()).__record__()
def __call__(self, x, ephemeral=False, *args, **kwargs):
v = super().__call__(x, *args, **kwargs)
if type(v).__name__ != "Segment": # pragma: no cover
raise TypeError("Section call did not return a Segment.")
seg = Segment(self._interpreter, v, self)
if not ephemeral:
# By default store references to segments, but allow for them to be
# garbage collected if `ephemeral=True`
seg.__ref__(self)
self.__ref__(seg)
return seg
def __iter__(self, *args, **kwargs):
iter = super().__iter__(*args, **kwargs)
for v in iter:
if type(v).__name__ != "Segment": # pragma: no cover
raise TypeError("Section iteration did not return a Segment.")
yield Segment(self._interpreter, v, self)
[docs] def insert(self, *args, **kwargs):
"""
Insert a mechanism into the Section.
"""
# Catch nrn.Section return value, always seems to be self.
# So if Neuron doesn't raise an error, return self.
# Probably for method chaining?
self.__neuron__().insert(*args, **kwargs)
return self
[docs] def connect_points(self, target, x=None, **kwargs):
"""
Connect a Segment of this Section to a target. Usually used to connect
the membrane potential to a point process.
"""
if x is None:
x = self.__arc__()
segment = self(x)
self.push()
nc = self._interpreter.NetCon(segment, target, **kwargs)
self._interpreter.pop_section()
return nc
[docs] def set_dimensions(self, length, diameter):
"""
Set the length and diameter of the piece of cable this Section will
represent in the simulation.
"""
self.L = length
self.diam = diameter
[docs] def set_segments(self, segments):
"""
Set the number of discrete points where equations are solved during simulation.
"""
self.nseg = segments
[docs] def add_3d(self, points, diameters=None):
"""
Add new 3D points to this section xyz data.
:param points: A 2D array of xyz points.
:param diameters: A scalar or array of diameters corresponding to the points. Default value is the section diameter.
:type diameters: float or array
"""
if diameters is None:
diameters = [self.diam for _ in range(len(points))]
if not _is_sequence(diameters):
diameters = [diameters for _ in range(len(points))]
self.__neuron__().push()
for point, diameter in zip(points, diameters):
self._interpreter.pt3dadd(*point, diameter)
self._interpreter.pop_section()
@property
def points(self):
"""
Return the 3d point information associated to this section.
"""
import numpy
return numpy.column_stack(
(
[self.x3d(n) for n in range(self.n3d())],
[self.y3d(n) for n in range(self.n3d())],
[self.z3d(n) for n in range(self.n3d())],
)
)
[docs] def wholetree(self):
"""
Return the whole tree of child Sections
:rtype: List[patch.Section]
"""
return [Section(self._interpreter, s) for s in self.__neuron__().wholetree()]
[docs] def record(self, x=None):
"""
Record the Section at a certain point.
:param x: Arcpoint, defaults to ``__arc__`` if omitted.
:type x: float
"""
if x is None:
x = self.__arc__()
if not hasattr(self, "recordings"):
self.recordings = {}
if not x in self.recordings:
recorder = self._interpreter.Vector()
recorder.record(self(x))
self.recordings[x] = recorder
return recorder
return self.recordings[x]
[docs] def synapse(self, factory, *args, store=False, **kwargs):
"""
Insert a synapse into the Section.
:param factory: Callable that creates a point process, is given the
Section as first argument and passes on all other args.
:type factory: callable
:param store: Store the synapse on the Section in a ``synapses``
attribute.
:type store: bool
"""
synapse = factory(self, *args, **kwargs)
if store:
if not hasattr(self, "synapses"):
self.synapses = []
self.synapses.append(synapse)
return synapse
[docs] def iclamp(self, x=0.5, delay=0, duration=100, amplitude=0):
"""
Create a current clamp on the section.
:param x: Location along the segment from 0 to 1.
:type x: float
:param delay: Duration of the pre-step holding interval, from `0` to `delay` ms.
:type delay: float
:param duration: Duration of the step interval, from `delay` to `delay + duration` ms.
:type duration: float
:param amplitude: Can be a single value to define the current during the step
(`delay` to `delay + duration` ms), or a sequence to play after `delay` ms. This
will play 1 value of the sequence into the clamp per timestep.
:type amplitude: Union[float, List[float]]
:returns: The current clamp placed in the section.
:rtype: :class:`.objects.SEClamp`
"""
clamp = self._interpreter.IClamp(x=x, sec=self)
clamp.delay = delay
clamp.dur = duration
if _is_sequence(amplitude):
# If its a sequence play it as a vector into the clamp
dt = self._interpreter.dt
t = self._interpreter.Vector([delay + dt * i for i in range(len(amplitude))])
v = self._interpreter.Vector(amplitude, t)
v.play(clamp._ref_amp, t.__neuron__())
clamp.__ref__(v)
clamp.__ref__(t)
else:
clamp.amp = amplitude
return clamp
[docs] def vclamp(self, x=0.5, delay=0, duration=100, after=0, voltage=-70, holding=-70):
"""
Create a voltage clamp on the section.
:param x: Location along the segment from 0 to 1.
:type x: float
:param delay: Duration of the pre-step holding interval, from `0` to `delay` ms.
:type delay: float
:param duration: Duration of the step interval, from `delay` to `delay + duration` ms.
:type duration: float
:param after: Duration of the post-step holding interval, from `delay + duration`
to `delay + duration + after` ms.
:type after: float
:param voltage: Can be a single value to define the voltage during the step
(`delay` to `delay + duration` ms), or 3 values to define the pre-step, step and
post-step voltages altogether.
:type voltage: Union[float, List[float]]
:param holding: If `voltage` is a single value, `holding` is used for the pre-step
and post-step voltages.
:type holding: float
:returns: The single electrode voltage clamp placed in the section.
:rtype: :class:`.objects.SEClamp`
"""
clamp = self._interpreter.SEClamp(x=x, sec=self)
clamp.dur1 = delay
clamp.dur2 = duration
clamp.dur3 = after
try:
voltage = iter(voltage)
except TypeError:
clamp.amp1 = holding
clamp.amp2 = voltage
clamp.amp3 = holding
else:
voltage = list(voltage)
clamp.amp1 = voltage[0]
clamp.amp2 = voltage[1]
clamp.amp3 = voltage[2]
return clamp
[docs] def push(self):
"""
Return a context manager that pushes this Section onto the section stack
and takes it off when the context is exited.
"""
transform(self).push()
return _SectionStackContextManager(self)
[docs] def pop(self):
"""
Pop this section off the section stack.
"""
if self == self._interpreter.cas():
self._interpreter.pop_section()
else:
raise RuntimeError(
"Cannot pop this section as it is not on top of the section stack"
)
class _SectionStackContextManager:
def __init__(self, section):
self._section = section
def __enter__(self):
pass
def __exit__(self, *args):
self._section.pop()
[docs]class SectionRef(PythonHocObject):
@property
def child(self):
return [Section(self._interpreter, s) for s in self.__neuron__().child]
[docs]class Vector(PythonHocObject):
[docs] def record(self, target, *args, **kwargs):
nrn_target = transform_record(target)
with catch_hoc_error(CatchRecord, target=target):
self.__neuron__().record(nrn_target, *args, **kwargs)
self.__ref__(target)
return self
[docs]class IClamp(PythonHocObject, WrapsPointers):
pass
[docs]class SEClamp(PythonHocObject, WrapsPointers):
pass
[docs]class NetStim(PythonHocObject, Connectable):
def __init__(self, *args, **kwargs):
PythonHocObject.__init__(self, *args, **kwargs)
Connectable.__init__(self)
[docs]class VecStim(PythonHocObject, Connectable):
def __init__(self, *args, **kwargs):
PythonHocObject.__init__(self, *args, **kwargs)
Connectable.__init__(self)
@property
def vector(self):
return self._vector
@property
def pattern(self):
return self._pattern.copy()
[docs]class NetCon(PythonHocObject):
[docs] def record(self, vector=None):
if vector is not None:
self._neuron_ptr.record(transform(vector))
self.recorder = vector
if hasattr(vector, "__ref__"):
vector.__ref__(self)
else:
if not hasattr(self, "recorder"):
vector = self._interpreter.Vector()
self._neuron_ptr.record(transform(vector))
self.recorder = vector
if hasattr(vector, "__ref__"):
vector.__ref__(self)
return self.recorder
[docs]class Segment(PythonHocObject, Connectable, WrapsPointers):
def __init__(self, interpreter, ptr, section, **kwargs):
PythonHocObject.__init__(self, interpreter, ptr, **kwargs)
Connectable.__init__(self)
WrapsPointers.__init__(self)
self.section = section
def __netcon__(self):
return self.__neuron__()._ref_v
def __record__(self):
return self.__neuron__()._ref_v
[docs]class PointProcess(PythonHocObject, Connectable, WrapsPointers):
"""
Wrapper for all point processes (membrane and synapse mechanisms).
"""
def __init__(self, *args, **kwargs):
PythonHocObject.__init__(self, *args, **kwargs)
Connectable.__init__(self)
WrapsPointers.__init__(self)
[docs] def stimulate(self, pattern=None, weight=0.04, delay=0.0, **kwargs):
"""
Stimulate a point process.
:param pattern: Specific stimulus event times to play into the point process.
:type pattern: list[float]
:param kwargs: All keyword arguments will be passed set on the
:class:`NetStim <neuron:NetStim>`
"""
from . import connection
if pattern is None:
# No specific pattern given, create NetStim
stimulus = self._interpreter.NetStim()
for kw, value in kwargs.items():
setattr(stimulus.__neuron__(), kw, value)
else:
# Specific pattern required, create VecStim
stimulus = self._interpreter.VecStim(pattern=pattern)
self._interpreter.NetCon(stimulus, self, weight=weight, delay=delay)
return stimulus
def _get_obj_registration_queue():
return _registration_queue