MHRN-Space / src /core /neuron_models.py
ThomasHeisig's picture
Sync GitHub main 4645f4137666463023fa501984a14336dd2fc666 (part 11)
31226fd verified
Raw History Blame Contribute Delete
5.73 kB
"""Versioned, deterministic single-neuron dynamics models for MHRN.
The registry in this module is deliberately small. A model is added only when
its transition equations, spike rule, reset rule and version are explicit.
This keeps model selection a scientific treatment rather than an untracked
implementation detail.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Final
class NeuronModel(str, Enum):
"""Implemented membrane-dynamics model families."""
IZHIKEVICH = "izhikevich-2003"
LEAKY_INTEGRATE_AND_FIRE = "lif-current-v1"
@dataclass(frozen=True, slots=True)
class NeuronModelDescriptor:
"""Stable provenance metadata for one implemented dynamics model."""
model: NeuronModel
version: str
family: str
state_variables: tuple[str, ...]
integration_rule: str
spike_rule: str
reset_rule: str
canonical: bool
citation: str
def to_dict(self) -> dict[str, object]:
return {
"model": self.model.value,
"version": self.version,
"family": self.family,
"state_variables": list(self.state_variables),
"integration_rule": self.integration_rule,
"spike_rule": self.spike_rule,
"reset_rule": self.reset_rule,
"canonical": self.canonical,
"citation": self.citation,
}
MODEL_REGISTRY: Final[dict[NeuronModel, NeuronModelDescriptor]] = {
NeuronModel.IZHIKEVICH: NeuronModelDescriptor(
model=NeuronModel.IZHIKEVICH,
version="mhrn-1.0",
family="quadratic integrate-and-fire with recovery variable",
state_variables=("v", "u"),
integration_rule="two half Euler steps for v, one Euler step for u",
spike_rule="v >= 30 mV plus optional adaptive threshold offset",
reset_rule="v = c; u = u + d",
canonical=True,
citation=(
"E. M. Izhikevich, Simple model of spiking neurons, "
"IEEE Transactions on Neural Networks 14(6), 2003, "
"doi:10.1109/TNN.2003.820440"
),
),
NeuronModel.LEAKY_INTEGRATE_AND_FIRE: NeuronModelDescriptor(
model=NeuronModel.LEAKY_INTEGRATE_AND_FIRE,
version="mhrn-1.0",
family="current-based leaky integrate-and-fire",
state_variables=("v",),
integration_rule="forward Euler membrane leak/current integration",
spike_rule="v >= configured lif_threshold plus optional adaptation",
reset_rule="v = configured lif_reset",
canonical=False,
citation="Reference current-based LIF implementation; model parameters are persisted.",
),
}
_MODEL_ALIASES: Final[dict[str, NeuronModel]] = {
"izhikevich": NeuronModel.IZHIKEVICH,
"izhikevich-2003": NeuronModel.IZHIKEVICH,
"izh": NeuronModel.IZHIKEVICH,
"lif": NeuronModel.LEAKY_INTEGRATE_AND_FIRE,
"leaky-integrate-and-fire": NeuronModel.LEAKY_INTEGRATE_AND_FIRE,
"lif-current-v1": NeuronModel.LEAKY_INTEGRATE_AND_FIRE,
}
def coerce_neuron_model(value: NeuronModel | str) -> NeuronModel:
"""Resolve a model enum or documented alias to a stable model identifier."""
if isinstance(value, NeuronModel):
return value
key = str(value).strip().lower()
try:
return _MODEL_ALIASES[key]
except KeyError as exc:
supported = ", ".join(model.value for model in NeuronModel)
raise ValueError(
f"Unknown neuron model {value!r}; supported: {supported}"
) from exc
def get_model_descriptor(value: NeuronModel | str) -> NeuronModelDescriptor:
"""Return immutable provenance metadata for an implemented model."""
return MODEL_REGISTRY[coerce_neuron_model(value)]
def available_neuron_models() -> tuple[NeuronModelDescriptor, ...]:
"""Return model descriptors in stable enum order."""
return tuple(MODEL_REGISTRY[model] for model in NeuronModel)
def integrate_membrane(
model: NeuronModel | str,
*,
v: float,
u: float,
input_current: float,
dt_ms: float,
a: float,
b: float,
lif_resting_potential: float,
lif_tau_m_ms: float,
lif_resistance: float,
) -> tuple[float, float]:
"""Advance only membrane-model state by one deterministic time step."""
selected = coerce_neuron_model(model)
if selected is NeuronModel.IZHIKEVICH:
half_dt = 0.5 * dt_ms
v += half_dt * (0.04 * v * v + 5.0 * v + 140.0 - u + input_current)
v += half_dt * (0.04 * v * v + 5.0 * v + 140.0 - u + input_current)
u += dt_ms * a * (b * v - u)
return v, u
dv = ((lif_resting_potential - v) + lif_resistance * input_current) * (
dt_ms / lif_tau_m_ms
)
return v + dv, u
def spike_threshold(
model: NeuronModel | str,
*,
izhikevich_threshold: float,
lif_threshold: float,
) -> float:
"""Return the non-adaptive spike threshold for a model."""
selected = coerce_neuron_model(model)
if selected is NeuronModel.IZHIKEVICH:
return izhikevich_threshold
return lif_threshold
def apply_spike_reset(
model: NeuronModel | str,
*,
v: float,
u: float,
c: float,
d: float,
lif_reset: float,
) -> tuple[float, float]:
"""Apply the model-specific state transition immediately after a spike."""
selected = coerce_neuron_model(model)
if selected is NeuronModel.IZHIKEVICH:
return c, u + d
return lif_reset, u
__all__ = [
"MODEL_REGISTRY",
"NeuronModel",
"NeuronModelDescriptor",
"apply_spike_reset",
"available_neuron_models",
"coerce_neuron_model",
"get_model_descriptor",
"integrate_membrane",
"spike_threshold",
]