Skip to content

Integrate-and-fire

The leaky integrate-and-fire neuron, tau v' = -(v - rest) + R I, which spikes and resets at a threshold.

  • Name: integrate_and_fire
  • Field: biology/neuroscience
  • Formalism: ordinary differential equations
  • References: Lapicque 1907; Gerstner et al. 2014

Factory

@register_model(
    field='biology/neuroscience',
    tags=('neuron', 'spiking', 'events'),
    references=('Lapicque 1907', 'Gerstner et al. 2014'),
    defaults={'until': 100.0, 'dt': 0.1},
)
def integrate_and_fire(
    v: float = -65.0,
    *,
    rest: float = -65.0,
    threshold: float = -50.0,
    reset: float = -65.0,
    resistance: float = 10.0,
    tau: float = 10.0,
    I: float = 2.0,
) -> System:
    """Return the leaky integrate-and-fire neuron, `tau v' = -(v - rest) + R I`, which spikes and resets at a threshold.

    With `R I` above `threshold - rest` it fires periodically with period `tau ln(R I / (R I - threshold + rest))`
    when `reset = rest`.

    Args:
        v:
            The initial membrane potential in mV.

        rest:
            The resting potential in mV.

        threshold:
            The potential at which the neuron spikes in mV.

        reset:
            The potential after a spike in mV.

        resistance:
            The membrane resistance in MΩ.

        tau:
            The membrane time constant in ms.

        I:
            The input current in nA.

    Returns:
        system:
            The model, with the membrane potential `neuron.v` and its spikes recorded as `spike` events.

    Examples:
        >>> from skcomplex.models.biology.neuroscience import integrate_and_fire
        >>> from skcomplex.engine.runtime import Simulation
        >>> trajectory = Simulation(integrate_and_fire(), dt=0.1).check().prepare().run(until=50.0).trajectory_
        >>> round(trajectory.events[0].time, 2)
        13.86
    """
    neuron = LeakyIntegrateAndFireElement('neuron', v=v, rest=rest, resistance=resistance, tau=tau, I=I)
    spike = SpikeResetInteraction('spike', target='neuron', threshold=threshold, after=reset)
    return System(elements=[neuron], interactions=[spike])

Components

class LeakyIntegrateAndFireElement(Element):
    """Leaky integrate-and-fire neuron, whose potential relaxes to `rest + resistance I` over the time `tau`.

    The potential follows `tau v' = -(v - rest) + resistance I`. A spike reset fires the neuron. Units are mV, ms, MΩ,
    and nA.

    Args:
        label:
            The label of the neuron.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        v:
            The initial potential.

        rest:
            The resting potential.

        resistance:
            The membrane resistance.

        tau:
            The membrane time constant.

        I:
            The input current.

    Examples:
        >>> from skcomplex.models.biology.neuroscience import LeakyIntegrateAndFireElement
        >>> LeakyIntegrateAndFireElement('neuron').tau
        10.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        v: Annotated[float, StateSpec()] = -65.0,
        rest: Annotated[float, ParameterSpec()] = -65.0,
        resistance: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
        tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
        I: Annotated[float, ParameterSpec()] = 2.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.v = v
        self.rest = rest
        self.resistance = resistance
        self.tau = tau
        self.I = I

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the leaky integration of the input current.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return [RateLaw(s.v, (-(s.v - s.rest) + s.resistance * s.I) / s.tau)]

class SpikeResetInteraction(CrossingInteraction):
    """Spike of a neuron, which resets its potential when it crosses a threshold upwards and kicks a second state.

    When the potential `v` rises through `threshold`, it is set to `after`, and the state `kicked`, such as a recovery
    variable or a synaptic trace, grows by `kick`. Every spike is an event labelled by the interaction.

    Args:
        label:
            The label of the interaction and of its events.

        target:
            The path of the neuron.

        state:
            The name of the potential.

        threshold:
            The potential of a spike.

        after:
            The potential after a spike.

        kicked:
            The name of the state a spike increments, or `None` for none.

        kick:
            The increment of the kicked state at a spike.

    Raises:
        ValidationError:
            The direction is not -1, 0, or 1, or the guard or a new value is not a function.

    Examples:
        >>> from skcomplex.core.elements import CompartmentElement
        >>> from skcomplex.core.interactions import RateInteraction
        >>> from skcomplex.core.system import System
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.biology.neuroscience import SpikeResetInteraction
        >>> drive = RateInteraction('drive', target='neuron', state='n', rate=1.0)
        >>> spike = SpikeResetInteraction('spike', target='neuron', state='n', threshold=1.0, after=0.0)
        >>> system = System(elements=[CompartmentElement('neuron')], interactions=[drive, spike])
        >>> trajectory = Simulation(system, dt=0.25).check().prepare().run(until=2.1).trajectory_
        >>> [round(event.time, 6) for event in trajectory.events]
        [1.0, 2.0]
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        state: str = 'v',
        threshold: Annotated[float, ParameterSpec()] = 1.0,
        after: Annotated[float, ParameterSpec()] = 0.0,
        kicked: str | None = None,
        kick: Annotated[float, ParameterSpec()] = 0.0,
    ) -> None:
        super().__init__(
            label,
            target=target,
            guard=self._get_guard,
            reset=self._get_resets(state, kicked),
            direction=1,
        )
        self.state = state
        self.threshold = threshold
        self.after = after
        self.kicked = kicked
        self.kick = kick

    def _get_guard(self, s: SimpleNamespace) -> Expression:
        """Return the potential above the threshold."""
        return getattr(s.target, self.state) - s.threshold

    def _get_resets(self, state: str, kicked: str | None) -> dict[str, Callable[[SimpleNamespace], object]]:
        """Return the new value of the potential and of the kicked state after a spike."""
        resets: dict[str, Callable[[SimpleNamespace], object]] = {state: lambda s: s.after}
        if kicked is not None:
            resets[kicked] = lambda s: getattr(s.target, kicked) + s.kick
        return resets

Equations

neuron.v: diff(v_neuron(t), t) == (I_neuron*resistance_neuron + rest_neuron - v_neuron(t))/tau_neuron