Source code for scpn_fusion.phase.knm

# SPDX-License-Identifier: AGPL-3.0-or-later
# Commercial license available
# © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
# © Code 2020–2026 Miroslav Šotek. All rights reserved.
# ORCID: 0009-0009-3560-0851
# Contact: www.anulum.li | protoscience@anulum.li
# SCPN Fusion Core — Knm Coupling Matrix (Paper 27)
"""
Paper 27 Knm specification.

K[n, m] encodes coupling from source layer n to target layer m.
Diagonal: intra-layer synchronisation strength.
Off-diagonal: inter-layer bidirectional causality (bottom-up / top-down).
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Sequence

import numpy as np
from numpy.typing import NDArray

FloatArray = NDArray[np.float64]

# Canonical 16-layer natural frequencies (rad/s).
# Paper 27, Table 1 — SCPN Kuramoto calibration.
OMEGA_N_16 = np.array(
    [
        1.329,
        2.610,
        0.844,
        1.520,
        0.710,
        3.780,
        1.055,
        0.625,
        2.210,
        1.740,
        0.480,
        3.210,
        0.915,
        1.410,
        2.830,
        0.991,
    ],
    dtype=np.float64,
)


[docs] @dataclass(frozen=True) class KnmSpec: """Paper 27 coupling specification. K : (L, L) coupling matrix. K[n, m] = source n -> target m. alpha : (L, L) Sakaguchi phase-lag (optional). zeta : (L,) per-layer global-driver gain ζ_m (optional). """ K: FloatArray alpha: FloatArray | None = None zeta: FloatArray | None = None layer_names: Sequence[str] | None = None def __post_init__(self) -> None: """Validate that the coupling matrix K is square (L, L).""" K = np.asarray(self.K, dtype=np.float64) if K.ndim != 2 or K.shape[0] != K.shape[1]: raise ValueError("K must be square (L, L)") L = K.shape[0] if self.alpha is not None: a = np.asarray(self.alpha, dtype=np.float64) if a.shape != (L, L): raise ValueError(f"alpha shape {a.shape} != ({L}, {L})") if self.zeta is not None: z = np.asarray(self.zeta, dtype=np.float64) if z.shape != (L,): raise ValueError(f"zeta shape {z.shape} != ({L},)") if self.layer_names is not None and len(self.layer_names) != L: raise ValueError("layer_names length must equal L") @property def L(self) -> int: """Number of Kuramoto layers in this specification. Returns ------- int Number of sources/targets (matrix width and height) for ``K``. """ return int(np.asarray(self.K).shape[0])
[docs] def build_knm_paper27( L: int = 16, K_base: float = 0.45, K_alpha: float = 0.3, zeta_uniform: float = 0.0, ) -> KnmSpec: """Build the canonical Paper 27 Knm with exponential distance decay. ``K[i, j] = K_base * exp(-K_alpha * abs(i - j))``; ``diag(K)`` is kept for intra-layer sync, unlike the inter-oscillator Knm which zeros the diagonal. K_base=0.45 and K_alpha=0.3 from Paper 27 §3.2, Eq. 12. Calibration anchors from Paper 27, Table 2. Cross-hierarchy boosts from Paper 27 §4.3. """ idx = np.arange(L) dist = np.abs(idx[:, None] - idx[None, :]) K = K_base * np.exp(-K_alpha * dist) # Calibration anchors — Paper 27 Table 2 anchors = [(0, 1, 0.302), (1, 2, 0.201), (2, 3, 0.252), (3, 4, 0.154)] for i, j, val in anchors: if i < L and j < L: K[i, j] = val K[j, i] = val # Cross-hierarchy boosts — Paper 27 §4.3 if L >= 16: K[0, 15] = max(K[0, 15], 0.05) K[15, 0] = max(K[15, 0], 0.05) if L >= 7: K[4, 6] = max(K[4, 6], 0.15) K[6, 4] = max(K[6, 4], 0.15) zeta = np.full(L, zeta_uniform, dtype=np.float64) if zeta_uniform != 0.0 else None return KnmSpec(K=K, zeta=zeta)