Skip to content

Imprint

The imprint subsystem implements history-dependent coupling modulation — a computational model of learning and memory in coupled oscillator networks. Oscillators that have been consistently active accumulate an imprint \(m_k\) that modulates their coupling strength, phase lag, and bifurcation parameter. This creates a memory effect: frequently synchronised oscillator pairs develop stronger coupling over time, analogous to Hebbian synaptic strengthening in neural systems.

Why this module exists

Imprint provides bounded adaptation instead of unbounded recalibration. It lets the system retain useful coupling history in a form that remains auditable through release-time surfaces.

From an operations perspective, this matters when:

  • recurring synchronization structures should be reinforced without replacing the base topology;
  • transient disturbances should not cause permanent control changes unless replayed and validated;
  • and policy handoff teams need an interpretable memory signal with attribution.

The module is designed to be explainable: every modulation path is explicit and each state update is constrained by saturation and decay checks.

Theory

The imprint model draws from three theoretical traditions:

  1. Hebbian plasticity (Hebb 1949): "neurons that fire together wire together." In phase oscillator terms, oscillators with correlated phases strengthen their mutual coupling.

  2. Exponential trace models (Friston 2005, Philos. Trans. R. Soc. B 360:815-836): The free energy framework uses exponential accumulation with decay to model synaptic efficacy changes.

  3. Imprint dynamics in the SCPN model: the 15+1 layer architecture uses imprints as the mechanism by which L16 (the cybernetic closure) shapes the lower layers' coupling topology over time.

Dynamics

The imprint variable \(m_k\) for oscillator \(k\) evolves as:

\[m_k(t + \Delta t) = m_k(t) \cdot e^{-\lambda \Delta t} + E_k \cdot \Delta t\]

where:

  • \(\lambda\) — decay rate (how fast the memory fades without reinforcement)
  • \(E_k\) — exposure (input from the current oscillator activity)
  • \(\Delta t\) — timestep

The result is clipped to \([0, m_{\text{sat}}]\) where \(m_{\text{sat}}\) is the saturation threshold, preventing runaway accumulation.

Decay rate interpretation:

\(\lambda\) Timescale Analogy
0.0 Permanent Long-term potentiation
0.01 ~100 steps Working memory
0.1 ~10 steps Sensory adaptation
1.0 ~1 step No memory (reactive)

Modulation Channels

The imprint modulates three UPDE parameters:

Coupling (\(K_{nm}\))

\[K_{nm}^{\prime} = K_{nm} \cdot (1 + m_k)\]

Rows of the coupling matrix are scaled by \((1 + m_k)\), so oscillators with high imprint couple more strongly to all their neighbours.

Phase Lag (\(\alpha_{ij}\))

\[\alpha_{ij}^{\prime} = \alpha_{ij} + (m_i - m_j)\]

The antisymmetric offset breaks phase-lag symmetry toward observed phase relationships. If oscillator \(i\) has higher imprint than \(j\), the lag shifts to favour the pattern that produced the imprint.

Bifurcation (\(\mu_k\))

\[\mu_k^{\prime} = \mu_k \cdot (1 + m_k)\]

For Stuart-Landau dynamics, the bifurcation parameter controls the distance from the Hopf bifurcation. Higher imprint pushes the oscillator further into the limit-cycle regime.

Amplitude coupling excluded

The amplitude coupling topology \(K_{nm}^r\) is not modulated by imprint. It is fixed by the binding specification. This is intentional: amplitude coupling represents physical connectivity (e.g. axon tracts), which does not change on the timescale of imprint dynamics.

Usage

from scpn_phase_orchestrator.imprint.state import ImprintState
from scpn_phase_orchestrator.imprint.update import ImprintModel

# Create model: slow decay, saturation at 5.0
model = ImprintModel(decay_rate=0.01, saturation=5.0)

# Initial state: no imprint
state = ImprintState(m_k=np.zeros(n), last_update=0.0)

# Update with exposure from oscillator activity
exposure = compute_exposure(phases, knm)  # domain-specific
state = model.update(state, exposure, dt=0.01)

# Modulate coupling for next UPDE step
knm_modulated = model.modulate_coupling(knm, state)
alpha_modulated = model.modulate_lag(alpha, state)

Pipeline integration

UPDEEngine.step() ──→ phases ──→ compute_exposure()
                                 ImprintModel.update()
                                 ImprintState (m_k updated)
                      ┌─────────────────┼──────────────────┐
                      ↓                 ↓                  ↓
              modulate_coupling  modulate_lag     modulate_mu
              K_nm' = K·(1+m)   α' = α+(Δm)     μ' = μ·(1+m)
                      │                 │                  │
                      ↓                 ↓                  ↓
              UPDEEngine.step(K_nm', ..., α', ...)  ← next cycle

ImprintState (frozen dataclass)

Field Type Default Description
m_k NDArray required Imprint values per oscillator
last_update float required Timestamp of last update
attribution dict[str, float] {} Source attribution weights

The attribution dict tracks which input sources contributed to the imprint (e.g., {"eeg_alpha": 0.6, "emg_burst": 0.4}). This supports explainability: when the imprint modulates coupling, the attribution records which signals caused the modulation.

Validation

ImprintModel.__init__ validates: - decay_rate >= 0 (non-negative; 0 = permanent memory) - saturation > 0 (positive; prevents runaway accumulation)

ImprintModel

ImprintModel(decay_rate: float, saturation: float)
Method Signature Description
update (state, exposure, dt) → ImprintState Exponential update + decay
modulate_coupling (knm, state) → NDArray K' = K · (1 + m_k)
modulate_lag (alpha, state) → NDArray α' = α + (m_i - m_j)
modulate_mu (mu, state) → NDArray μ' = μ · (1 + m_k)

Performance: update(n=64) < 5 ms.

API Reference

State

state

Immutable per-oscillator imprint state containers.

ImprintState stores the L9 memory vector, its last update timestamp, and optional attribution weights. Validation is intentionally performed by the update/model layer so serialized historical states can still be inspected before being accepted into active dynamics.

Classes

ImprintState dataclass

ImprintState(
    m_k: FloatArray,
    last_update: float,
    attribution: dict[str, float] = dict(),
)

L9 memory imprint per oscillator: accumulation vector, timestamp, attribution.

Model

update

Validated imprint accumulation and modulation rules.

ImprintModel decays existing memory, accumulates non-negative exposure, clips to a configured saturation ceiling, and applies the resulting vector to K, alpha, or mu surfaces. All public methods reject boolean, non-numeric, non-finite, negative-state, or shape-mismatched inputs before returning arrays for downstream dynamics.

Classes

ImprintModel

ImprintModel(decay_rate: float, saturation: float)

Exponential exposure accumulation with decay and saturation.

m_k(t+dt) = m_k(t) * exp(-decay_rate * dt) + exposure * dt, clipped to [0, saturation].

Modulates: K (phase coupling), alpha (lag), mu (bifurcation). Does NOT modulate knm_r (amplitude coupling strength) — amplitude coupling topology is fixed by the binding spec, not learned.

Source code in src/scpn_phase_orchestrator/imprint/update.py
def __init__(self, decay_rate: float, saturation: float):
    decay_rate = _finite_real(decay_rate, "decay_rate")
    saturation = _finite_real(saturation, "saturation")
    if decay_rate < 0.0:
        raise ValueError(f"decay_rate must be non-negative, got {decay_rate}")
    if saturation <= 0.0:
        raise ValueError(f"saturation must be positive, got {saturation}")
    self._decay_rate = decay_rate
    self._saturation = saturation
Methods:
update
update(
    state: ImprintState, exposure: FloatArray, dt: float
) -> ImprintState

Decay existing imprint, add new exposure, clip to saturation.

Parameters

state : ImprintState The state mapping. exposure : FloatArray Driver exposure value. dt : float Integration step size.

Returns

ImprintState Decay existing imprint, add new exposure, clip to saturation.

Raises

ValueError If the inputs are invalid or inconsistent.

Source code in src/scpn_phase_orchestrator/imprint/update.py
def update(
    self, state: ImprintState, exposure: FloatArray, dt: float
) -> ImprintState:
    """Decay existing imprint, add new exposure, clip to saturation.

    Parameters
    ----------
    state : ImprintState
        The state mapping.
    exposure : FloatArray
        Driver exposure value.
    dt : float
        Integration step size.

    Returns
    -------
    ImprintState
        Decay existing imprint, add new exposure, clip to saturation.

    Raises
    ------
    ValueError
        If the inputs are invalid or inconsistent.
    """
    m_k, last_update = _validated_state(state)
    dt = _finite_real(dt, "dt")
    if dt <= 0.0:
        raise ValueError("dt must be positive")
    exposure = _finite_vector(
        exposure, "exposure", length=m_k.shape[0], non_negative=True
    )
    decayed = m_k * np.exp(-self._decay_rate * dt)
    m_new = np.clip(decayed + exposure * dt, 0.0, self._saturation)
    return ImprintState(
        m_k=m_new,
        last_update=last_update + dt,
        attribution=state.attribution.copy(),
    )
modulate_coupling
modulate_coupling(
    knm: FloatArray, imprint: ImprintState
) -> FloatArray

Scale Knm rows by (1 + m_k).

Parameters

knm : FloatArray Coupling matrix K_nm, shape (N, N). imprint : ImprintState The imprint state.

Returns

FloatArray Scale Knm rows by (1 + m_k).

Source code in src/scpn_phase_orchestrator/imprint/update.py
def modulate_coupling(self, knm: FloatArray, imprint: ImprintState) -> FloatArray:
    """Scale Knm rows by (1 + m_k).

    Parameters
    ----------
    knm : FloatArray
        Coupling matrix ``K_nm``, shape ``(N, N)``.
    imprint : ImprintState
        The imprint state.

    Returns
    -------
    FloatArray
        Scale Knm rows by (1 + m_k).
    """
    m_k, _ = _validated_state(imprint)
    knm = _finite_square_matrix(knm, "knm", size=m_k.shape[0])
    result: FloatArray = knm * (1.0 + m_k)[:, np.newaxis]
    return result
modulate_lag
modulate_lag(
    alpha: FloatArray, imprint: ImprintState
) -> FloatArray

Shift phase lags by antisymmetric imprint offset.

Parameters

alpha : FloatArray Phase-lag matrix alpha, shape (N, N). imprint : ImprintState The imprint state.

Returns

FloatArray Shift phase lags by antisymmetric imprint offset.

Source code in src/scpn_phase_orchestrator/imprint/update.py
def modulate_lag(self, alpha: FloatArray, imprint: ImprintState) -> FloatArray:
    """Shift phase lags by antisymmetric imprint offset.

    Parameters
    ----------
    alpha : FloatArray
        Phase-lag matrix ``alpha``, shape ``(N, N)``.
    imprint : ImprintState
        The imprint state.

    Returns
    -------
    FloatArray
        Shift phase lags by antisymmetric imprint offset.
    """
    m_k, _ = _validated_state(imprint)
    alpha = _finite_square_matrix(alpha, "alpha", size=m_k.shape[0])
    offset = m_k[:, np.newaxis] - m_k[np.newaxis, :]
    result: FloatArray = alpha + offset
    return result
modulate_mu
modulate_mu(
    mu: FloatArray, imprint: ImprintState
) -> FloatArray

Scale bifurcation parameter: μ_k * (1 + m_k).

Parameters

mu : FloatArray Bifurcation parameter μ, shape (N,). imprint : ImprintState The imprint state.

Returns

FloatArray Scale bifurcation parameter: μ_k * (1 + m_k).

Source code in src/scpn_phase_orchestrator/imprint/update.py
def modulate_mu(self, mu: FloatArray, imprint: ImprintState) -> FloatArray:
    """Scale bifurcation parameter: μ_k * (1 + m_k).

    Parameters
    ----------
    mu : FloatArray
        Bifurcation parameter ``μ``, shape ``(N,)``.
    imprint : ImprintState
        The imprint state.

    Returns
    -------
    FloatArray
        Scale bifurcation parameter: μ_k * (1 + m_k).
    """
    m_k, _ = _validated_state(imprint)
    mu = _finite_vector(mu, "mu", length=m_k.shape[0])
    result: FloatArray = mu * (1.0 + m_k)
    return result