Skip to content

UPDE Numerics

Equation

dtheta_i/dt = omega_i
            + sum_j K_ij sin(theta_j - theta_i - alpha_ij)
            + zeta sin(Psi - theta_i)

Integration Methods

Euler (default)

theta(t+dt) = theta(t) + dt * f(theta(t))

First-order. Sufficient when dt satisfies the stability condition.

RK4

k1 = f(theta)
k2 = f(theta + dt/2 * k1)
k3 = f(theta + dt/2 * k2)
k4 = f(theta + dt * k3)
theta(t+dt) = theta(t) + dt/6 * (k1 + 2*k2 + 2*k3 + k4)

Fourth-order. Use for stiff systems or when accuracy matters more than speed. 4x the derivative evaluations per step.

Stability Condition

CFL-like bound:

dt < 1 / (max(omega) + N * max(K) + zeta)

Where N is the number of oscillators. The coupling term contributes up to N * max(K) to the effective frequency. Exceeding this bound causes phase jumps that break the wrapping invariant.

The binding spec sample_period_s sets dt. Validate at initialisation.

Phase Wrapping

After every step: theta = theta % (2*pi). This is the ONLY place wrapping occurs. Intermediate computations (derivative, scratch arrays) operate on unwrapped differences.

Scratch Array Pre-Allocation

UPDEEngine.__init__ allocates:

Array Shape Purpose
_phase_diff (N, N) theta_j - theta_i - alpha_ij
_sin_diff (N, N) sin(phase_diff)
_scratch_dtheta (N,) derivative accumulator

All operations use out= parameter to avoid allocation during stepping. For RK4, k1-k3 are copied since the scratch arrays are reused.

Numerical Considerations

  • sin(theta_j - theta_i) handles wrap-around implicitly: sin(5.9 - 0.1) = sin(5.8) ≈ sin(-0.48).
  • Double precision (float64) throughout. No single-precision paths.
  • Order parameter R = |mean(exp(i*theta))| computed via complex arithmetic, not via trigonometric identities.