Skip to content

Stuart-Landau

The Stuart-Landau oscillator z' = (μ + i ω) z - (1 + i β) |z|² z, the normal form of a Hopf bifurcation.

  • Name: stuart_landau
  • Field: physics/nonlinear_oscillators
  • Formalism: ordinary differential equations
  • References: Gros 2015; Dilão 2023

Factory

@register_model(
    field='physics/nonlinear_oscillators',
    tags=('limit cycle', 'ode', 'hopf bifurcation', 'oscillation'),
    references=('Gros 2015', 'Dilão 2023'),
    defaults={'until': 30.0, 'dt': 0.05},
)
def stuart_landau(
    x: float = 0.1,
    y: float = 0.0,
    *,
    mu: float = 1.0,
    omega: float = 1.0,
    beta: float = 0.0,
) -> System:
    """Return the Stuart-Landau oscillator `z' = (μ + i ω) z - (1 + i β) |z|² z`, the normal form of a Hopf bifurcation.

    The complex amplitude `z = x + i y` is written as two real states. For `μ > 0` every orbit except the origin
    settles on the circle of radius `sqrt(μ)`, turning at the frequency `ω - β μ`.

    Args:
        x:
            The initial real part of the amplitude.

        y:
            The initial imaginary part of the amplitude.

        mu:
            The distance from the Hopf bifurcation.

        omega:
            The linear frequency.

        beta:
            The nonlinear frequency shift.

    Returns:
        system:
            The model, with the real part `oscillator.x` and the imaginary part `oscillator.y` of the amplitude.

    Examples:
        >>> from skcomplex.models.physics.nonlinear_oscillators import stuart_landau
        >>> stuart_landau(mu=0.5).elements[0].mu
        0.5
    """
    return System(elements=[StuartLandauElement('oscillator', x=x, y=y, mu=mu, omega=omega, beta=beta)])

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

oscillator.x: diff(x_oscillator(t), t) == ((x_oscillator(t)^2 + y_oscillator(t)^2)*(beta_oscillator*y_oscillator(t) - x_oscillator(t))*kappa_oscillator + mu_oscillator*x_oscillator(t) - omega_oscillator*y_oscillator(t))*tau_oscillator
oscillator.y: diff(y_oscillator(t), t) == -((beta_oscillator*x_oscillator(t) + y_oscillator(t))*(x_oscillator(t)^2 + y_oscillator(t)^2)*kappa_oscillator - omega_oscillator*x_oscillator(t) - mu_oscillator*y_oscillator(t))*tau_oscillator