Skip to content

Subcritical Hopf normal form

The subcritical Hopf normal form, where a stable focus coexists with a stable limit cycle.

  • Name: subcritical_hopf_form
  • Field: mathematics/dynamical_systems
  • Formalism: ordinary differential equations
  • References: Bertin 2021

Factory

@register_model(
    field='mathematics/dynamical_systems',
    tags=('bifurcation', 'normal form', 'ode', 'limit cycle', 'bistability', 'hysteresis'),
    references=('Bertin 2021',),
    defaults={'until': 50.0, 'dt': 0.1},
)
def subcritical_hopf_form(
    x: float = 0.1,
    y: float = 0.0,
    *,
    alpha: float = -0.1,
    omega: float = 1.0,
    q: float = 0.0,
    gamma: float = 1.0,
    s: float = 0.0,
) -> System:
    """Return the subcritical Hopf normal form, where a stable focus coexists with a stable limit cycle.

    The complex amplitude `z = x + iy` follows `z' = (alpha + iω) z + (1 + iq) |z|² z - (gamma + is) |z|⁴ z`, so its
    modulus follows `r' = alpha r + r³ - gamma r⁵`. The focus `z = 0` is stable for `alpha < 0`. The stable limit cycle
    of radius `r² = (1 + √(1 + 4 alpha gamma)) / (2 gamma)` exists for `alpha > -1 / (4 gamma)`, and an unstable cycle
    separates the two attractors while `alpha < 0`. A slow sweep of `alpha` up and down therefore jumps to the cycle at
    `alpha = 0` and falls back to the focus at `alpha = -1 / (4 gamma)`, a hysteresis loop.

    Args:
        x:
            The initial real part of `z`.

        y:
            The initial imaginary part of `z`.

        alpha:
            The bifurcation parameter `alpha`, the linear growth rate.

        omega:
            The angular frequency `ω` at small amplitude.

        q:
            The nonlinear frequency shift `q` of the cubic term.

        gamma:
            The stabilizing quintic coefficient `gamma`, positive.

        s:
            The nonlinear frequency shift `s` of the quintic term.

    Returns:
        system:
            The model, with the states `form.x` and `form.y`.

    Examples:
        >>> from skcomplex.models.mathematics.dynamical_systems import subcritical_hopf_form
        >>> subcritical_hopf_form(alpha=0.1).elements[0].alpha
        0.1
    """
    return System(
        elements=[SubcriticalHopfElement('form', x=x, y=y, alpha=alpha, omega=omega, q=q, gamma=gamma, s=s)],
    )

Components

class SubcriticalHopfElement(Element):
    """Subcritical Hopf normal form `z' = (alpha + i omega) z + (1 + i q) |z|^2 z - (gamma + i s) |z|^4 z`.

    The complex amplitude is `z = x + i y`.

    Args:
        label:
            The label of the element.

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

        x:
            The initial real part of `z`.

        y:
            The initial imaginary part of `z`.

        alpha:
            The bifurcation parameter, the linear growth rate.

        omega:
            The angular frequency at small amplitude.

        q:
            The nonlinear frequency shift of the cubic term.

        gamma:
            The stabilizing quintic coefficient, positive.

        s:
            The nonlinear frequency shift of the quintic term.

    Examples:
        >>> from skcomplex.models.mathematics.dynamical_systems import SubcriticalHopfElement
        >>> SubcriticalHopfElement('form', alpha=0.1).alpha
        0.1
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[object, StateSpec()] = 0.1,
        y: Annotated[object, StateSpec()] = 0.0,
        alpha: Annotated[object, ParameterSpec()] = -0.1,
        omega: Annotated[object, ParameterSpec()] = 1.0,
        q: Annotated[object, ParameterSpec()] = 0.0,
        gamma: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        s: Annotated[object, ParameterSpec()] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.y = y
        self.alpha = alpha
        self.omega = omega
        self.q = q
        self.gamma = gamma
        self.s = s

    def _get_rates(self, s: SimpleNamespace) -> tuple[Expression, Expression]:
        """Return the rates of the real and the imaginary part of the amplitude."""
        radius = s.x**2 + s.y**2
        real = s.alpha + radius - s.gamma * radius**2
        imaginary = s.omega + s.q * radius - s.s * radius**2
        return real * s.x - imaginary * s.y, imaginary * s.x + real * s.y

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the rates of the real and the imaginary part of the amplitude.

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

        Returns:
            laws:
                The laws of the element.
        """
        real, imaginary = self._get_rates(s)
        return [RateLaw(s.x, real), RateLaw(s.y, imaginary)]

Equations

form.x: diff(x_form(t), t) == -((x_form(t)^2 + y_form(t)^2)^2*gamma_form - x_form(t)^2 - y_form(t)^2 - alpha_form)*x_form(t) + ((x_form(t)^2 + y_form(t)^2)^2*s_form - (x_form(t)^2 + y_form(t)^2)*q_form - omega_form)*y_form(t)
form.y: diff(y_form(t), t) == -((x_form(t)^2 + y_form(t)^2)^2*s_form - (x_form(t)^2 + y_form(t)^2)*q_form - omega_form)*x_form(t) - ((x_form(t)^2 + y_form(t)^2)^2*gamma_form - x_form(t)^2 - y_form(t)^2 - alpha_form)*y_form(t)