Ott-Antonsen Mean-Field Reduction
July 28, 2026 · View on GitHub
1. Mathematical Formalism
The Ott-Antonsen Ansatz
For an infinite population of globally coupled Kuramoto oscillators with Lorentzian (Cauchy) natural frequency distribution:
where is the centre frequency and is the half-width at half-maximum, the exact low-dimensional dynamics of the Kuramoto order parameter are given by:
This is the Ott-Antonsen reduction — a single complex ODE that captures the exact macroscopic dynamics of the infinite- Kuramoto model.
Decomposition of the ODE
Separating real and imaginary parts :
The three terms have clear physical meanings:
- : Damping from frequency spread (wider distribution → faster decay)
- : Rotation at the mean frequency
- : Cubic self-coupling from the Lorentzian closure (Ott & Antonsen 2008, Eq. 9)
Steady-State Solution
Setting in the polar representation:
This is exact for the Lorentzian distribution. The transition at is continuous (second-order), with the universal Kuramoto scaling near the transition.
Critical Coupling
This is the Dörfler-Bullo criterion for globally coupled oscillators with Lorentzian spread. Larger frequency spread (larger ) requires stronger coupling to achieve synchronisation.
Integration Method
RK4 (Runge-Kutta 4th order) on the complex ODE:
where are the standard RK4 stages applied to .
2. Theoretical Context
Why Mean-Field Reduction?
Simulating oscillators costs per step (all-to-all coupling). For with 1000 steps, that is $10^{11}N$.
This is not an approximation: for Lorentzian and , the reduction is exact. For finite , it provides a mean-field prediction that converges as $1/\sqrt{N}$.
Domain of Validity
The Ott-Antonsen ansatz applies when:
- Coupling is global (all-to-all): for all
- Frequency distribution is Lorentzian (or can be well approximated)
- No noise (deterministic dynamics)
- No phase frustration ()
For non-Lorentzian distributions, the ansatz is no longer exact but remains a useful approximation. For Gaussian , the error is typically in for moderate coupling.
Historical Context
- Kuramoto (1975): Original model, heuristic self-consistency for
- Strogatz (2000): Mean-field theory for finite
- Ott & Antonsen (2008): Exact low-dimensional reduction for Lorentzian
- Ott & Antonsen (2009): Extension to multivariate coupling
- Marvel, Mirollo & Strogatz (2009): Identified the OA manifold as the Möbius group (Watanabe-Strogatz theory)
- Pikovsky & Rosenblum (2008): Analogous reduction via circular cumulants
The Ott-Antonsen result is considered one of the most significant theoretical advances in coupled oscillator theory since Kuramoto's original work.
Applications in SPO
The OttAntonsenReduction serves as a fast diagnostic:
- Before running expensive N-oscillator simulations, predict whether the given will produce synchronisation
- Estimate the steady-state for parameter sweeps in seconds rather than hours
- Validate N-oscillator results against the exact mean-field solution
- Provide initial conditions for the full simulation (seed phases near the predicted )
Lorentzian Fitting
The predict_from_oscillators() method automatically estimates
from a set of measured frequencies:
- (robust central tendency)
- (the IQR of a Lorentzian equals $2\Delta$)
This enables one-call diagnostics: pass frequencies, get predicted .
3. Pipeline Position
Oscillators.extract() ──→ ω₁, ω₂, ..., ωₙ
│
↓
┌── OttAntonsenReduction(ω₀, Δ, K, dt) ──┐
│ │
│ Input: z₀ (seed), n_steps │
│ Param: ω₀, Δ (from g(ω)), K, dt │
│ Method: RK4 (Rust/Mojo/Julia/Go/Python)│
│ Output: OAState(z, R, ψ, K_c) │
│ │
│ OR: predict_from_oscillators(ω, K) │
│ → fit Lorentzian → run → OAState │
│ │
└──────────────────────────────────────────┘
│
↓
R_predicted vs R_measured (from UPDEEngine)
→ validation / parameter guidance
Input Contracts
| Parameter | Type | Range | Source |
|---|---|---|---|
omega_0 | float | any | Median of natural frequencies |
delta | float | Half-width of Lorentzian fit | |
K | float | finite real | Coupling strength; negative values model repulsive coupling |
dt | float | Integration time step (default 0.01) | |
z0 | complex | $ | z_0 |
n_steps | int | Number of RK4 steps | |
omegas | real FloatArray | finite, non-empty, one-dimensional | Empirical sample for predict_from_oscillators(); boolean, complex, and numeric-string aliases are rejected before fitting |
Output Contract
| Field | Type | Range | Meaning |
|---|---|---|---|
z | complex | $ | z |
R | float | $ | |
psi | float | , mean phase | |
K_c | float | Critical coupling $2\Delta$ |
Accelerator Output Contract
Direct Go, Julia, Mojo, and Rust-backed public dispatch outputs must preserve
the same Ott-Antonsen state invariant before an OAState is published. The
backend tuple must contain finite non-boolean real scalars, the complex state
must remain inside the OA unit disk, R must equal abs(z), and psi must
match atan2(Im(z), Re(z)) for non-zero radius. Loader or runtime
unavailability still falls through to the Python floor, but physics-contract
faults propagate as validation errors. The Rust kernel runner validates its
scalar inputs before conversion, and the optional Rust steady-state helper must
return a finite real scalar inside [0, 1] before the public result is emitted.
4. Features
- Exact reduction for Lorentzian — not an approximation
- RK4 integration — 4th-order accurate on the complex ODE
- Rust FFI acceleration — 38-96x speedup over Python loop
- Mojo subprocess boundary — accepts exactly four finite scalar records
from the compiled OA runner before constructing an
OAState - Public backend output replay — optional backend outputs are checked for
R == |z|,psi == atan2(Im(z), Re(z)), and non-boolean finite scalars before publication - Typed empirical fitting boundary — measured-frequency samples reject boolean, complex, and numeric-string aliases before Lorentzian fitting
- Analytical steady-state —
steady_state_R()returns exact - Automatic Lorentzian fitting —
predict_from_oscillators()fits from measured frequencies - Critical coupling —
K_cproperty returns $2\Delta$ - Single-step and batch —
step()for interactive,run()for batch - Immutable state — returns
OAStatedataclass, engine is reusable
5. Usage Examples
Basic: Predict R for Given Parameters
from scpn_phase_orchestrator.upde.reduction import OttAntonsenReduction
oa = OttAntonsenReduction(omega_0=0.0, delta=0.5, K=3.0, dt=0.01)
print(f"K_c = {oa.K_c:.2f}") # 1.0
print(f"R_ss = {oa.steady_state_R():.4f}") # sqrt(1 - 2*0.5/3.0) = 0.8165
state = oa.run(z0=complex(0.01, 0.0), n_steps=2000)
print(f"R (numerical) = {state.R:.4f}") # Should match R_ss
Fast Diagnostic from Oscillator Frequencies
import numpy as np
from scpn_phase_orchestrator.upde.reduction import OttAntonsenReduction
# Given N oscillator frequencies (e.g. from EEG channels)
omegas = np.random.default_rng(42).standard_cauchy(100) * 0.3 + 1.0
oa = OttAntonsenReduction(omega_0=0, delta=0.1, K=1.0, dt=0.01)
prediction = oa.predict_from_oscillators(omegas, K=2.0)
print(f"Predicted R = {prediction.R:.4f}")
print(f"K_c = {prediction.K_c:.4f}")
print(f"Supercritical: {2.0 > prediction.K_c}")
Parameter Sweep
import numpy as np
from scpn_phase_orchestrator.upde.reduction import OttAntonsenReduction
delta = 0.5
K_values = np.linspace(0, 5, 100)
for K in K_values:
oa = OttAntonsenReduction(omega_0=0, delta=delta, K=K, dt=0.01)
R_analytical = oa.steady_state_R()
state = oa.run(complex(0.01, 0.0), 3000)
print(f"K={K:.2f}: R_analytical={R_analytical:.4f}, R_numerical={state.R:.4f}")
Validation Against Full Simulation
import numpy as np
from scpn_phase_orchestrator.upde.reduction import OttAntonsenReduction
from scpn_phase_orchestrator.upde.engine import UPDEEngine
from scpn_phase_orchestrator.upde.order_params import compute_order_parameter
N = 1000
K = 3.0
delta = 0.5
# OA prediction (milliseconds)
oa = OttAntonsenReduction(omega_0=0, delta=delta, K=K, dt=0.01)
R_oa = oa.steady_state_R()
# Full simulation (seconds)
omegas = np.random.default_rng(42).standard_cauchy(N) * delta
knm = np.full((N, N), K / N); np.fill_diagonal(knm, 0.0)
phases = np.random.default_rng(42).uniform(0, 2 * np.pi, N)
engine = UPDEEngine(N, dt=0.01)
for _ in range(5000):
phases = engine.step(phases, omegas, knm, 0.0, 0.0, np.zeros((N, N)))
R_sim, _ = compute_order_parameter(phases)
print(f"OA prediction: R = {R_oa:.4f}")
print(f"Full sim (N={N}): R = {R_sim:.4f}")
# Error: typically < 0.05 for N >= 100
6. Technical Reference
Class: OttAntonsenReduction
::: scpn_phase_orchestrator.upde.reduction
Dataclass: OAState
@dataclass
class OAState:
z: complex # Mean-field z = R·exp(iψ)
R: float # |z|, order parameter
psi: float # arg(z), mean phase
K_c: float # Critical coupling 2Δ
Rust Engine Functions
// RK4 integration of OA ODE, returns (z_re, z_im, R, psi)
pub fn oa_run(z_re, z_im, omega_0, delta, k_coupling, dt, n_steps) -> (f64, f64, f64, f64)
// Analytical steady-state R
pub fn steady_state_r_oa(delta, k_coupling) -> f64
// Fit Lorentzian (omega_0, delta) from frequency array
pub fn fit_lorentzian(omegas: &[f64]) -> (f64, f64)
Auto-Select Logic
All three functions (run, steady_state_R, predict_from_oscillators)
use Rust when available. The RK4 loop in Rust is 38-96x faster because
the Python path executes a Python-level loop with complex arithmetic
per step.
7. Performance Benchmarks
Measured on Intel Core i5-11600K @ 3.90 GHz, 32 GB DDR4-2400. ω₀ = 0, Δ = 1, K = 4, z₀ = 0.01. Averaged over 100-1000 iterations.
| Steps | Python (ms) | Rust (ms) | Speedup |
|---|---|---|---|
| 100 | 0.203 | 0.0053 | 38.1x |
| 500 | 1.836 | 0.0192 | 95.8x |
| 2,000 | 7.183 | 0.0763 | 94.2x |
| 10,000 | 20.811 | 0.3641 | 57.2x |
Why 38-96x Speedup?
The OA reduction is a scalar complex ODE. Each RK4 step requires 4 evaluations of , each involving:
- 1 complex multiply ()
- 2 complex multiply-adds
- ~10 floating-point operations total
In Python, each step crosses the Python interpreter boundary for every arithmetic operation (complex + float overhead). In Rust, the entire loop runs in native code with no interpreter overhead. The 38-96x range reflects the interplay between Python interpreter overhead (dominant at high step counts → ~96x) and FFI call overhead (dominant at low step counts → ~38x). For the typical use case (500-2000 steps), the speedup is ~95x.
Memory Usage
Negligible: 2 floats (re, im) for state, 8 floats for RK4 stages. No heap allocation in the inner loop.
Test Coverage
- Rust tests: 9 (reduction module in spo-engine)
- Steady-state subcritical/supercritical, convergence, subcritical decay, rotation with ω₀, fit_lorentzian, zero steps, psi angle, critical coupling
- Python tests: 12 (
tests/test_ott_antonsen.py)- Critical coupling, below/above/at K_c, step finite, run convergence, run decay, negative delta raises, predict_from_oscillators, predict identical omegas, OAState fields, pipeline wiring
- Source lines: 208 (Rust) + 99 (Python) = 307 total
8. Citations
-
Ott, E. & Antonsen, T. M. (2008). "Low dimensional behavior of large systems of globally coupled oscillators." Chaos 18(3):037113. DOI: 10.1063/1.2930766
-
Ott, E. & Antonsen, T. M. (2009). "Long time evolution of phase oscillator systems." Chaos 19(2):023117. DOI: 10.1063/1.3136851
-
Marvel, S. A., Mirollo, R. E., & Strogatz, S. H. (2009). "Identical phase oscillators with global sinusoidal coupling evolve by Möbius group action." Chaos 19(4):043104. DOI: 10.1063/1.3247089
-
Pikovsky, A. & Rosenblum, M. (2008). "Partially integrable dynamics of hierarchical populations of coupled oscillators." Physical Review Letters 101(26):264103. DOI: 10.1103/PhysRevLett.101.264103
-
Strogatz, S. H. (2000). "From Kuramoto to Crawford: exploring the onset of synchronization in populations of coupled oscillators." Physica D 143(1-4):1-20. DOI: 10.1016/S0167-2789(00)00094-4
-
Dörfler, F. & Bullo, F. (2014). "Synchronization in complex networks of phase oscillators: A survey." Automatica 50(6):1539-1564. DOI: 10.1016/j.automatica.2014.04.012
-
Kuramoto, Y. (1975). "Self-entrainment of a population of coupled non-linear oscillators." In Int. Symp. Mathematical Problems in Theoretical Physics, Lecture Notes in Physics 39:420-422. Springer.
-
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:137-185. DOI: 10.1103/RevModPhys.77.137
Edge Cases and Limitations
|z₀| ≥ 1
The OA manifold requires . If seeded with , the cubic term drives back below 1, but the transient may be non-physical. Always seed with (e.g., 0.01).
Δ = 0 (Identical Oscillators)
When , — any positive coupling produces synchronisation. The steady-state . The OA ODE becomes — pure rotation plus cubic growth. Starting from , exponentially with rate .
Non-Lorentzian Distributions
For Gaussian :
- The OA ansatz is not exact
- Using (FWHM matching) gives accurate to for moderate coupling
- For bimodal distributions, the OA ansatz does not apply
Numerical Stability
The RK4 integrator is unconditionally stable for . For typical parameters (, ), this is always satisfied. If , reduce accordingly.
Finite- Corrections
For oscillators (not ):
- fluctuates around with variance
- The transition at is rounded: for all (finite-size precursors)
- The
predict_from_oscillatorsmethod accounts for the Lorentzian fitting uncertainty but not the finite- fluctuations
Derivation of the Ott-Antonsen Ansatz
Step 1: Continuity Equation
For oscillators with density on , the continuity equation reads:
where is the velocity field from the Kuramoto model with mean-field coupling.
Step 2: Fourier Expansion
The density admits a Fourier series in :
Substituting into the continuity equation yields an infinite hierarchy of ODEs for the Fourier coefficients .
Step 3: The Ansatz
Ott and Antonsen (2008) observed that if we restrict to the manifold:
then the infinite hierarchy collapses to a single ODE for :
where is the Kuramoto order parameter, defined by:
Step 4: Lorentzian Closure
For , the integral over can be evaluated by closing the contour in the lower half-plane, picking up the single pole at :
This yields the closed ODE:
The key insight is that the Lorentzian distribution has a single pole in the lower half-plane, which makes the contour integral exact. Distributions with multiple poles (e.g., bimodal) or no poles (e.g., Gaussian, which has an essential singularity) break the closure.
Rust Implementation Details
oa_deriv — Inner Loop Kernel
The derivative function oa_deriv computes both real and imaginary
parts of without heap allocation:
fn oa_deriv(re: f64, im: f64, omega_0: f64, delta: f64, half_k: f64) -> (f64, f64) {
let abs_sq = re * re + im * im;
let lin_re = -delta * re + omega_0 * im;
let lin_im = -delta * im - omega_0 * re;
let cubic_factor = half_k * (1.0 - abs_sq);
(lin_re + cubic_factor * re, lin_im + cubic_factor * im)
}
Each RK4 step calls oa_deriv 4 times (k1-k4), for a total of
~40 floating-point operations per step. The Rust compiler inlines
this function and uses SSE2/AVX for the multiply-adds.
fit_lorentzian — Robust Estimation
The Lorentzian fitting uses order statistics rather than maximum-likelihood estimation:
- Median for : breakdown point 50%, robust to outliers
- IQR/2 for : exploits the exact relationship for a Cauchy distribution
The sort is , but is the number of oscillators (typically ), so the cost is negligible compared to the integration.
FFI Transfer
The oa_run function returns a tuple (f64, f64, f64, f64), which
PyO3 converts to a Python tuple with zero-copy semantics (scalar
values only, no array allocation). The fit_lorentzian function
accepts a PyReadonlyArray1<f64> reference, avoiding any data copy
from NumPy to Rust.
Troubleshooting
Issue: R Does Not Converge
Symptom: After many steps, oscillates or drifts instead of settling to .
Diagnosis: Check whether . In the rotating frame, converges while rotates at frequency . The complex traces a circle of radius , so converges even though itself does not settle to a fixed point.
If truly oscillates, verify that (RK4 stability bound). For and , is safe.
Issue: predict_from_oscillators Returns R ≈ 0 for Clearly Coupled System
Diagnosis: The Lorentzian fit via IQR assumes unimodal, heavy-tailed . For bimodal or Gaussian distributions, the estimated may be too large, yielding .
Solution: Manually set from known distribution parameters rather than using the automatic fitting.
Issue: R_ss = 0 Despite Supercritical K
Diagnosis: The steady_state_R function requires K > 2Δ.
If you pass K as the total coupling but the per-pair coupling
is , you need to use the per-pair value multiplied by
(i.e., the effective global coupling strength).
Issue: Mismatch Between OA and Full Simulation
Diagnosis: Common causes:
- Non-Lorentzian frequency distribution (OA is approximate)
- Finite- fluctuations ( deviates by )
- Phase frustration () — OA assumes zero frustration
- Non-global coupling (sparse ) — OA assumes all-to-all
For cases 1-2, the mismatch should decrease with larger and Lorentzian-like distributions. For cases 3-4, the OA reduction is not applicable.
Integration with Other SPO Modules
With UPDEEngine
The OttAntonsenReduction validates full simulations:
# Quick check: will this parameter set synchronise?
oa = OttAntonsenReduction(omega_0=0, delta=0.5, K=3.0)
if oa.steady_state_R() > 0.5:
# Worth running the expensive N-oscillator simulation
engine = UPDEEngine(n_oscillators=256, dt=0.01)
# ... run full sim ...
With SSGF Costs
The predicted can initialise the SSGF sync deficit cost:
This provides a baseline expectation for the geometry optimisation. If the full simulation yields , the geometry is suboptimal and the SSGF engine should increase the learning rate.
With RegimeManager
The OA reduction enables fast regime classification:
| Regime | ||
|---|---|---|
| Incoherent | 0 | |
| $0.5 - 1.0$ | Subcritical | 0 |
| $1.0 - 1.5$ | Onset | $0 - 0.58$ |
| $1.5 - 3.0$ | Partial sync | $0.58 - 0.82$ |
| Full sync |
The supervisor can use these thresholds to decide whether to engage geometry control or whether the current coupling is sufficient.