Skip to content

Van der Pol ice ages

Glacial cycles as a Van der Pol relaxation oscillator, optionally paced by orbital forcing.

  • Name: van_der_pol_ice_ages
  • Field: physics/climate
  • Formalism: ordinary differential equations
  • References: De Saedeleer, Crucifix, and Wieczorek 2013; Datseris and Parlitz 2022

Factory

@register_model(
    field='physics/climate',
    tags=('ode', 'limit cycle', 'relaxation oscillator', 'ice ages', 'paleoclimate'),
    references=('De Saedeleer, Crucifix, and Wieczorek 2013', 'Datseris and Parlitz 2022'),
    defaults={'until': 400.0, 'dt': 0.5},
)
def van_der_pol_ice_ages(
    x: float = 0.0,
    y: float = 1.0,
    *,
    alpha: float = 30.0,
    beta: float = 0.75,
    gamma: float = 0.0,
    tau: float = 35.09,
) -> System:
    """Return glacial cycles as a Van der Pol relaxation oscillator, optionally paced by orbital forcing.

    Time is in thousands of years. The slow variable `x` stands for ice volume and the fast variable `y` for the climate
    state, with `tau x' = -(y + beta + gamma F(t))` and `tau y' = -alpha (y^3/3 - y - x)`, where
    `F(t) = cos(2 pi t/23) + cos(2 pi t/41)` stands for precession and obliquity. For `|beta| < 1` the unforced
    oscillator has a relaxation limit cycle of slow ice build-up and fast terminations, and `gamma` sets how strongly
    the orbit paces it.

    Args:
        x:
            The initial ice volume.

        y:
            The initial climate state.

        alpha:
            The time-scale separation of the fast climate state.

        beta:
            The asymmetry of the cycle, between build-up and termination.

        gamma:
            The strength of the orbital forcing.

        tau:
            The time scale, in thousands of years.

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

    Examples:
        >>> from skcomplex.models.physics.climate import van_der_pol_ice_ages
        >>> van_der_pol_ice_ages(gamma=0.4).elements[0].gamma
        0.4
    """
    return System(
        elements=[VanDerPolIceAgesElement('climate', x=x, y=y, alpha=alpha, beta=beta, gamma=gamma, tau=tau)],
    )

Components

class VanDerPolIceAgesElement(Element):
    """Climate of glacial cycles as a Van der Pol relaxation oscillator, a slow ice volume and a fast climate state.

    With time in thousands of years, `tau x' = -(y + beta + gamma F(t))` and `tau y' = -alpha (y^3/3 - y - x)`, where
    `F(t) = cos(2 pi t/23) + cos(2 pi t/41)` stands for precession and obliquity.

    Args:
        label:
            The label of the climate.

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

        x:
            The initial ice volume.

        y:
            The initial climate state.

        alpha:
            The time-scale separation of the fast climate state.

        beta:
            The asymmetry of the cycle, between build-up and termination.

        gamma:
            The strength of the orbital forcing.

        tau:
            The time scale, in thousands of years.

    Examples:
        >>> from skcomplex.models.physics.climate import VanDerPolIceAgesElement
        >>> VanDerPolIceAgesElement('climate').alpha
        30.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[float, StateSpec()] = 0.0,
        y: Annotated[float, StateSpec()] = 1.0,
        alpha: Annotated[float, ParameterSpec()] = 30.0,
        beta: Annotated[float, ParameterSpec()] = 0.75,
        gamma: Annotated[float, ParameterSpec()] = 0.0,
        tau: Annotated[float, ParameterSpec()] = 35.09,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.y = y
        self.alpha = alpha
        self.beta = beta
        self.gamma = gamma
        self.tau = tau

    def _get_orbital_forcing(self, s: SimpleNamespace) -> Expression:
        """Return the orbital forcing, the sum of the cycles of precession and obliquity.

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

        Returns:
            forcing:
                The orbital forcing.
        """
        return cos(2 * pi * s.t / 23) + cos(2 * pi * s.t / 41)

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the slow ice volume and the fast climate of the relaxation oscillator under orbital forcing.

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

        Returns:
            laws:
                The rate laws of the ice volume and the climate state.
        """
        forcing = self._get_orbital_forcing(s)
        return [
            RateLaw(s.x, -(s.y + s.beta + s.gamma * forcing) / s.tau),
            RateLaw(s.y, -s.alpha * (s.y**3 / 3 - s.y - s.x) / s.tau),
        ]

Equations

climate.x: diff(x_climate(t), t) == -(gamma_climate*(cos(2/23*pi*t) + cos(2/41*pi*t)) + beta_climate + y_climate(t))/tau_climate
climate.y: diff(y_climate(t), t) == -1/3*(y_climate(t)^3 - 3*x_climate(t) - 3*y_climate(t))*alpha_climate/tau_climate