Skip to content

Ott-Antonsen reduction

The Ott-Antonsen equation for the order parameter of infinitely many Kuramoto oscillators.

  • Name: ott_antonsen
  • Field: physics/synchronization
  • Formalism: ordinary differential equations
  • References: Ott and Antonsen 2008; Bick et al. 2020

Factory

@register_model(
    field='physics/synchronization',
    tags=('synchronization', 'ode', 'mean field', 'dimension reduction'),
    references=('Ott and Antonsen 2008', 'Bick et al. 2020'),
    defaults={'until': 30.0, 'dt': 0.1},
)
def ott_antonsen(
    r: float = 0.1,
    *,
    K: float = 2.0,
    Delta: float = 0.5,
    omega: float = 0.0,
) -> System:
    """Return the Ott-Antonsen equation for the order parameter of infinitely many Kuramoto oscillators.

    For natural frequencies with a Lorentzian distribution of center `omega` and half width `Delta`, the complex order
    parameter `z = x + i y` follows exactly `z' = (i omega - Delta) z + (K / 2)(1 - |z|²) z`, a Stuart-Landau
    oscillator with the growth rate `K / 2 - Delta` and the saturation `K / 2`. Its modulus settles at
    `sqrt(1 - 2 Delta / K)` above the critical coupling `K_c = 2 Delta`, and at 0 below it.

    Args:
        r:
            The initial modulus of the order parameter.

        K:
            The coupling strength.

        Delta:
            The half width of the distribution of natural frequencies.

        omega:
            The center of the distribution of natural frequencies.

    Returns:
        system:
            The model, with the real part `field.x` and the imaginary part `field.y` of the order parameter.

    Examples:
        >>> from skcomplex.models.physics.synchronization import ott_antonsen
        >>> ott_antonsen(K=3.0).elements[0].kappa
        1.5
    """
    return System(elements=[StuartLandauElement('field', x=r, y=0.0, mu=K / 2 - Delta, omega=omega, kappa=K / 2)])

Components

class StuartLandauElement(Element):
    """Stuart-Landau oscillator `z' = tau ((mu + i omega) z - kappa (1 + i beta) |z|^2 z)` of `z = x + i y`.

    It is the normal form of a supercritical Hopf bifurcation. For `mu > 0` the oscillator settles on the limit cycle
    of radius `sqrt(mu / kappa)`, which it turns at the frequency `tau (omega - beta mu)`, and for `mu < 0` it decays to
    the fixed point 0. The twist `beta` makes the frequency depend on the amplitude, and the time scale `tau` speeds
    up or slows down the whole flow.

    Args:
        label:
            The label of the oscillator or the group.

        visual:
            How the oscillator is drawn, or `None` for `default_visual_`.

        x:
            The initial real part of `z`.

        y:
            The initial imaginary part of `z`.

        mu:
            The growth rate of the fixed point 0, the bifurcation parameter.

        omega:
            The frequency of the linear rotation.

        beta:
            The twist, the shear of the frequency with the amplitude.

        kappa:
            The strength of the cubic saturation.

        tau:
            The time scale.

    Attributes:
        default_visual_:
            The visual of the oscillator when it sets none.

    Examples:
        >>> import math
        >>> from skcomplex.core.system import System
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.physics.common.oscillators import StuartLandauElement
        >>> system = System(elements=[StuartLandauElement('oscillator', x=0.1, mu=4.0)])
        >>> trajectory = Simulation(system, dt=0.5).check().prepare().run(until=10.0).trajectory_
        >>> round(math.hypot(trajectory.values('oscillator.x')[-1], trajectory.values('oscillator.y')[-1]), 4)
        2.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[float, StateSpec()] = 1.0,
        y: Annotated[float, StateSpec()] = 0.0,
        mu: Annotated[float, ParameterSpec()] = 1.0,
        omega: Annotated[float, ParameterSpec()] = 1.0,
        beta: Annotated[float, ParameterSpec()] = 0.0,
        kappa: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.y = y
        self.mu = mu
        self.omega = omega
        self.beta = beta
        self.kappa = kappa
        self.tau = tau

    def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the rates of the real and imaginary parts of `z`.

        Args:
            namespace:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The rates of `x` and `y`.
        """
        s = namespace
        saturation = s.kappa * (s.x**2 + s.y**2)
        return [
            RateLaw(s.x, s.tau * (s.mu * s.x - s.omega * s.y - saturation * (s.x - s.beta * s.y))),
            RateLaw(s.y, s.tau * (s.mu * s.y + s.omega * s.x - saturation * (s.y + s.beta * s.x))),
        ]

Equations

field.x: diff(x_field(t), t) == ((x_field(t)^2 + y_field(t)^2)*(beta_field*y_field(t) - x_field(t))*kappa_field + mu_field*x_field(t) - omega_field*y_field(t))*tau_field
field.y: diff(y_field(t), t) == -((beta_field*x_field(t) + y_field(t))*(x_field(t)^2 + y_field(t)^2)*kappa_field - omega_field*x_field(t) - mu_field*y_field(t))*tau_field