Thermodynamic Entropy Production Rate¶
The monitor.entropy_prod module computes the dissipation rate
of an overdamped Kuramoto network — the sum of squared instantaneous
velocities times the integration step. It is a single, non-negative
scalar that quantifies how far the oscillator ensemble is from a
frequency-locked fixed point: zero when the system is perfectly
synchronised, positive whenever any oscillator is still accelerating
or decelerating.
Formally (Acebrón et al. 2005, Rev. Mod. Phys. 77:137–185):
The factor dt converts instantaneous power to per-step
dissipation. The Sakaguchi phase lag is absent from this kernel —
if you need it, multiply K_{ij} by cos(α_{ij}) outside the
call (the angle-addition identity absorbs a constant lag into a
rotated coupling).
This module has a five-language backend chain (Rust → Mojo → Julia → Go → Python), direct-backend parity checks, local multi-backend benchmarks, and slow stability tests. Checked-in timing fields are local non-isolated regression evidence unless the benchmark metadata records CPU/core isolation and host-load controls.
1. Mathematical formalism¶
1.1 Overdamped Kuramoto dynamics¶
The overdamped limit of the Kuramoto equation reads
α is a global coupling-strength scalar; K_{ij} is the
unweighted adjacency. Separating α from K lets sweep
experiments hold the network topology fixed while varying the
coupling amplitude.
1.2 Entropy production rate¶
In a thermally-damped system the instantaneous dissipation is proportional to the squared velocity:
This is the Kuramoto-analogue of the mechanical quadratic form
P = Σ (dx/dt)². The · dt factor converts it to a per-step
cost; integrating Σ/dt over a simulation window gives the total
thermodynamic work done against the gradient flow.
1.3 Invariants and limits¶
| Limit | Value of Σ |
|---|---|
Synchronised phases + ω = 0 |
0 (fixed point) |
Fully uncoupled, equal ω |
N · ω² · dt |
Doubled α with ω = 0 |
4 × Σ (α enters squared) |
dt → 0 |
Σ → 0 linearly |
n = 0 or dt = 0 |
0 (short-circuit) |
The kernel never returns negative values — each d² · dt term is
non-negative.
1.4 Relationship to other observables¶
- Lyapunov function.
Σ = 0at the same set of configurations wheredV/dt = 0in the standard Kuramoto Lyapunov function (van Hemmen & Wreszinski 1993). - Phase diffusion. When coupling is weak,
Σ ≈ N · ⟨ω²⟩ · dtreduces to the free-diffusion rate. - Critical coupling. Near the synchronisation transition
Σshows a divergence-then-collapse signature as phases lock.
2. API¶
from scpn_phase_orchestrator.monitor.entropy_prod import (
ACTIVE_BACKEND,
AVAILABLE_BACKENDS,
entropy_production_rate,
)
2.1 entropy_production_rate¶
def entropy_production_rate(
phases: NDArray, # (N,) phases in radians
omegas: NDArray, # (N,) natural frequencies
knm: NDArray, # (N, N) coupling matrix, zero diagonal by convention
alpha: float, # global coupling strength
dt: float, # integration timestep
) -> float: ...
Returns a single non-negative float. n = 0 or dt = 0
short-circuits to 0.0; negative dt is rejected.
The public monitor and direct polyglot adapter wrappers require
finite real-valued phases, frequencies, couplings, alpha, and
dt. Complex arrays are rejected rather than projected onto their
real components, because Σ is defined on real phase velocities.
Object-dtype complex aliases are rejected at the same real-valued
boundary before float coercion, including scalar backend outputs from
direct Go/Julia/Mojo adapters.
3. Backend fallback chain¶
Resolved at import time in the order Rust → Mojo → Julia → Go →
Python. The first loader that returns without raising becomes
ACTIVE_BACKEND; remaining backends are kept in AVAILABLE_BACKENDS
for tests and benchmarks.
3.1 Loader probes¶
| Backend | Probe | Artefact |
|---|---|---|
| Rust | from spo_kernel import entropy_production_rate |
spo_kernel wheel via maturin. |
| Mojo | mojo/entropy_prod_mojo executable |
mojo build mojo/entropy_prod.mojo …. |
| Julia | juliacall + julia/entropy_prod.jl |
Julia 1.11. |
| Go | ctypes.CDLL("go/libentropy_prod.so") |
go build -buildmode=c-shared. |
| Python | Pure NumPy vectorisation | Always available. |
3.2 Parity tolerances¶
| Backend | Tolerance | Reason |
|---|---|---|
| Rust | 1e-12 |
Shared f64. |
| Julia | 1e-12 |
juliacall direct Float64. |
| Go | 1e-12 |
ctypes double*. |
| Mojo | 1e-9 |
Subprocess text round-trip. |
| Python | exact | Reference. |
All four non-Python backends measure within 1e-19 on the parity
probe — bit-equivalent under the test env. The declared tolerances
leave headroom for future toolchain upgrades.
4. Per-backend build notes¶
4.1 Rust¶
Implemented in spo-engine/src/entropy_prod.rs (pre-migration). The
Rust path uses par_iter over rows for large N and stays serial
for small N to avoid thread-pool overhead.
4.2 Julia (julia/entropy_prod.jl)¶
Plain Julia, no external packages. Single @inbounds outer loop over
i, inner accumulator over j. inv_n = α / N pre-computed
outside the loop. Empty input and non-positive dt guard matches
the Python reference.
4.3 Go (go/entropy_prod.go)¶
c-shared .so. Takes caller-owned pointers via unsafe.Slice;
writes the single result into the caller's *double buffer. Zero
allocations per call.
4.4 Mojo (mojo/entropy_prod.mojo)¶
Stdin executable with one verb (EP). The body is a direct
transliteration of the Rust / Go kernels. A single print line
outputs the scalar result. The Python bridge validates raw stdout
cardinality and rejects empty stdout, blank-line insertion,
multi-line stdout, non-scalar text, non-finite values, and negative
rates before returning to callers, so subprocess transport errors cannot
silently pass as physical entropy-production values.
4.5 Python (monitor/entropy_prod.py)¶
NumPy broadcast form:
diff = phases[np.newaxis, :] - phases[:, np.newaxis]
coupling = np.sum(knm * np.sin(diff), axis=1)
dtheta_dt = omegas + (alpha / n) * coupling
return float(np.sum(dtheta_dt ** 2) * dt)
Three passes: one sin on N² entries, one row-sum, one
square-sum. Competitive on small N.
5. Benchmarks and reference gate¶
The reference suite includes
entropy_production_polyglot_parity_gate. The gate records every declared
Rust/Mojo/Julia/Go/Python slot and accepts only when available backends match
the forced Python reference. The benchmark contract is behavioural rather than
line-coverage based:
| Contract | Reason |
|---|---|
exact formula parity with sum(dtheta_dt ** 2) * dt |
prevents backend drift from the overdamped-Kuramoto definition |
| non-negative rates | follows from the squared velocity form |
| zero fixed-point and zero-timestep limits | checks physical equilibria and timestep semantics |
| linear timestep scaling | verifies the per-step dissipation factor |
quadratic global-coupling scaling when omega = 0 |
verifies that alpha enters inside the squared velocity |
| global phase-shift invariance | protects phase-gauge symmetry |
| oscillator permutation invariance | protects index-labelling symmetry |
| public dispatch parity | ensures the fallback chain returns the validated backend output |
Run the parity gate with:
Measured on the local Ubuntu 24.04 host, 16-thread x86_64 CPU,
NumPy 2.3.4 / MKL, Julia 1.11.2, Go 1.23.4, Mojo 0.26.2,
spo_kernel release build.
Per-call wall-clock in milliseconds, one warm-up + ten measured
calls. Reproduce with
python benchmarks/entropy_prod_benchmark.py --sizes 16 64 256 1024 --calls 10.
| N | rust (ms) | mojo (ms) | julia (ms) | go (ms) | python (ms) |
|---|---|---|---|---|---|
| 16 | 0.008 | 107.65 | 0.166 | 1.438 | 0.029 |
| 64 | 0.054 | 132.98 | 0.072 | 1.099 | 0.140 |
| 256 | 1.571 | 219.38 | 1.204 | 4.179 | 1.866 |
| 1024 | 20.758 | 1857.88 | 18.851 | 24.255 | 38.081 |
Observations:
- Rust wins at every N. The serial path at small
Nstill outpaces NumPy thanks to zero allocations; the parallel path atN = 1024shaves ~45% off NumPy. - Julia is a solid second. After
juliacallwarm-up the@inboundsloop approaches Rust at largerN. - Go c-shared has a fixed ~1ms ctypes call floor; becomes
competitive only past
N = 256. - Python (NumPy) holds up well for
N ≤ 256due to the vectorisedsinand matrix ops; falls behind atN = 1024. - Mojo subprocess overhead dominates at every size — this kernel's hot path is too cheap to amortise the text round-trip. Retained for parity coverage only.
Raw JSON: python benchmarks/entropy_prod_benchmark.py --output /tmp/ep_bench.json.
6. Usage examples¶
6.1 Single snapshot¶
import numpy as np
from scpn_phase_orchestrator.monitor.entropy_prod import (
entropy_production_rate,
)
rng = np.random.default_rng(0)
N = 32
phases = rng.uniform(0, 2 * np.pi, N)
omegas = rng.normal(0.0, 0.2, N)
knm = rng.uniform(0.3, 0.9, (N, N))
np.fill_diagonal(knm, 0.0)
sigma = entropy_production_rate(phases, omegas, knm, alpha=0.6, dt=0.01)
print(f"Σ = {sigma:.6g}")
6.2 Tracking synchronisation¶
from scpn_phase_orchestrator.upde.engine import upde_run
trajectory_sigma: list[float] = []
for _ in range(50):
trajectory_sigma.append(
entropy_production_rate(phases, omegas, knm, 0.5, 0.01)
)
phases = upde_run(
phases, omegas, knm, np.zeros_like(knm),
zeta=0.0, psi=0.0, dt=0.01, n_steps=200, method="rk4",
)
import matplotlib.pyplot as plt
plt.plot(trajectory_sigma)
plt.yscale("log")
plt.ylabel("Σ per window")
6.3 Critical-coupling sweep¶
alphas = np.linspace(0.0, 2.0, 21)
sigmas = [
entropy_production_rate(phases, omegas, knm, a, 0.01)
for a in alphas
]
critical = alphas[np.argmax(np.abs(np.diff(sigmas)))]
6.4 Forcing a specific backend¶
from scpn_phase_orchestrator.monitor import entropy_prod as ep_mod
saved = ep_mod.ACTIVE_BACKEND
try:
ep_mod.ACTIVE_BACKEND = "julia"
sigma = entropy_production_rate(phases, omegas, knm, 0.5, 0.01)
finally:
ep_mod.ACTIVE_BACKEND = saved
7. Tests¶
Three files (21 tests total):
7.1 tests/test_entropy_prod_algorithm.py — 11 tests¶
TestNonNegativity— Σ ≥ 0 on random inputs.TestFixedPoint— synchronised + ω = 0 → Σ = 0; uncoupled + constant ω → Σ = N·ω²·dt.TestAnalyticalIdentity— NumPy output matches the explicit nested-loop definition to 1e-12.TestScaling— Σ is linear indt; α = 0 branch depends only onω.TestEdgeCases—n = 0anddt = 0short-circuit to 0, while negativedtfails closed.TestHypothesis— random-input finite / non-negative invariant holds across seeds + sizes.TestDispatcherSurface— Python always available; active is first.
7.2 tests/test_entropy_prod_backends.py — 7 tests¶
TestRustParity— Hypothesis sweep over random(N, seed)pairs at 1e-12.TestJuliaParity— two seeds at 1e-12.TestGoParity— Hypothesis sweep at 1e-12.TestMojoParity— two seeds at 1e-9.TestCrossBackendConsistency— iterates everyAVAILABLE_BACKENDSentry under the tolerance matrix.
7.3 tests/test_entropy_prod_stability.py — 3 tests¶
@pytest.mark.slow:
test_dissipation_falls_during_synchronisation— couples anupde_runtrajectory with the Σ measurement; Σ falls ≥ 10× over the sync horizon.test_large_N_stress_finite— 500-oscillator random network.test_alpha_sweep_monotone_in_knm_variance— withω = 0, doublingαexactly quadruples Σ.
Run all three:
pytest tests/test_entropy_prod_algorithm.py tests/test_entropy_prod_backends.py
pytest tests/test_entropy_prod_stability.py -m slow
8. Failure modes and caveats¶
8.1 Public validation¶
The public monitor validates shape, finiteness, real-valuedness, boolean aliases, and numeric-string aliases before backend dispatch. The Go / Julia / Mojo direct adapter wrappers share the same validation helper, so direct adapter tests fail closed before entering polyglot runtimes. Direct backend outputs are validated before return as finite real non-negative entropy-production scalars. Boolean aliases, numeric-string aliases, complex values, non-finite results, and negative rates fail closed at the Python boundary.
8.2 Not thread-safe on the Python fallback¶
Each call allocates the N × N difference matrix and two N-
vectors. That part is re-entrant, but writing to the result or
caching intermediate arrays externally must be guarded by the
caller.
8.3 α is a scalar, not a phase-lag matrix¶
Unlike upde.engine.upde_run and monitor.lyapunov.lyapunov_spectrum,
this kernel takes a scalar α (global coupling strength). If
you need the Sakaguchi phase-lag matrix α_{ij}, use the UPDE
engine to evolve the trajectory and extract dθ/dt separately.
8.4 Mojo subprocess cost dominates¶
At typical N ≤ 1024 the Mojo path is 20–50× slower than the
others because the subprocess fork + text round-trip is fixed-cost.
It is retained for parity guarantees; do not call it from inner
loops.
8.5 Non-symmetric knm accepted¶
The formula sums K_{ij} sin(θ_j − θ_i) row-wise; asymmetric
K produces asymmetric contributions. Some SPO use cases (e.g.
directed ecological couplings) rely on this.
9. Complexity¶
| Operation | Time | Space |
|---|---|---|
entropy_production_rate |
O(N²) |
O(N) scratch |
At N = 1024 this is ~10⁶ multiplies — ~21 ms on Rust, ~38 ms on
Python, seconds on Mojo (subprocess-bound).
10. References¶
- Acebrón, J. A., Bonilla, L. L., Pérez-Vicente, C. J., Ritort, F., Spigler, R. (2005). The Kuramoto model: A simple paradigm for synchronization phenomena. Reviews of Modern Physics 77 (1), 137–185.
- Kuramoto, Y. (1984). Chemical Oscillations, Waves, and Turbulence. Springer Series in Synergetics 19.
- Strogatz, S. H. (2000). From Kuramoto to Crawford: exploring the onset of synchronization in populations of coupled oscillators. Physica D 143, 1–20.
- van Hemmen, J. L., Wreszinski, W. F. (1993). Lyapunov function for the Kuramoto model of nonlinearly coupled oscillators. Journal of Statistical Physics 72 (1–2), 145–166.
- Seifert, U. (2012). Stochastic thermodynamics, fluctuation theorems and molecular machines. Reports on Progress in Physics 75 (12), 126001.
11. Changelog¶
- 2026-04-18 — Migrated to the AttnRes-level standard. Added
Julia / Go / Mojo ports, Python bridges, and a 5-backend
dispatcher in
monitor/entropy_prod.py. 21 new tests (11 algorithm + Hypothesis, 7 cross-backend parity, 3 long-run stability) plus the multi-backend benchmark harness. Parity ≤ 1e-19 measured across all four non-Python backends.