Melcher25
Melcher25(
var_name='bistable',
sigma=0.2,
gamma=1.5,
alpha=0.0,
b0=0.625,
q0=-9.0,
q1=12.0,
tau=0.902,
state_variables=None,
diagnostic_variables=None,
*args,
**kwargs,
)Two-equation bistable Itô SDE for stochastic Dansgaard-Oeschger transitions.
Models the meridional buoyancy gradient Δb and buoyancy flux B as a coupled stochastic system that switches between stadial (weak AMOC) and interstadial (strong AMOC) states:
d(db)/dt = -B - |q0 + q1*(db - b0)| * (db - b0) + sigma * dW
dB/dt = (db + alpha*B - gamma) / tau + sigma * dW
Parameters
var_name : str = 'bistable'-
Label for model output. Default is ‘bistable’.
sigma : float = 0.2-
Noise amplitude applied to both state variables. Range [0.15, 0.4]. Default is 0.2.
gamma : float or callable orcc.core.Forcing= 1.5-
External forcing proportional to atmospheric CO2. Bistable behaviour occurs near γ ≈ 1.5; range [0.8, 3.2]. If callable, must follow the signature contract in
contracts/signal_model_contract.md:(t),(t, state), or(t, state, model)with the first argument namedtortime. Default is 1.5. alpha : float or callable orcc.core.Forcing= 0.0-
Southern Ocean AABW coupling; controls the asymmetry between stadial and interstadial durations. Range [-1.0, 0.0]. If callable, must follow the same signature contract as gamma. Default is 0.0.
b0 : float = 0.625-
Reference buoyancy gradient that sets the centre of the bistable potential well. Default is 0.625 (calibrated from NGRIP, Melcher et al. 2025). Changing this value shifts both fixed points and may destroy bistability if moved far from the calibrated value.
q0 : float = -9.0-
Constant term in the nonlinear restoring force. Default is -9.0 (calibrated from NGRIP). Together with
q1it controls the depth and separation of the two potential wells; large departures from the default can eliminate one well and collapse the system to monostable behaviour. q1 : float = 12.0-
Linear coefficient in the nonlinear restoring force. Default is 12.0 (calibrated from NGRIP). Must be positive for the restoring force to be bounded; values far from the default alter the transition rates.
tau : float = 0.902-
Relaxation time-scale of the buoyancy-flux equation (in model time units). Default is 0.902 (calibrated from NGRIP). Very small values make B relax almost instantaneously and can suppress oscillations; very large values slow the recovery from transitions.
Notes
The parameters b0, q0, q1, and tau are calibrated from NGRIP data (Melcher et al. 2025). Their defaults are the suggested values that reproduce realistic Dansgaard-Oeschger statistics. You may change them to explore sensitivity or to fit a different dataset, but be aware that combinations far from the defaults can suppress bistability entirely.
All seven parameters (sigma, gamma, alpha, b0, q0, q1, tau) are registered in param_values and are therefore inspectable via model.doc('parameters') and model.list('parameters'). sigma, gamma, and alpha additionally support time-varying values via callables or Forcing objects.
This model sets uses_post_history = True: state variables are derived from the full solved trajectory. After every integrate() call, the states diagnostic variable is auto-populated with the hysteresis classification, and model.stadial_threshold / model.interstadial_threshold are set to the Jacobian-derived thresholds. Use method='heun_maruyama' for strong order 1.0 accuracy (preferred over 'euler_maruyama' for this additive-noise model).
See also
cc.core.CCModel : Base class. classify_bistable_states : Reclassify a Δb signal post-hoc without re-running the SDE.
Examples
Generate a synthetic DO record and inspect states
import numpy as np
import matplotlib.pyplot as plt
from climatecritters.model_critters.melcher25 import (
Melcher25, classify_bistable_states
)
model = Melcher25(sigma=0.2, gamma=1.5, alpha=-0.4)
output = model.integrate(
t_span=(0, 599.88), y0=[1.0, 0.0],
method='heun_maruyama', dt=0.012,
kwargs={'random_seed': 42, 'si': 0.12},
)
# Δb time series as a pyleoclim Series — ready to plot or analyse
ts = output.to_pyleo('db')
ts.plot()
plt.savefig('docs/reference/figures/Melcher25_example.png',
dpi=150, bbox_inches='tight')
# Stadial/interstadial classification is computed automatically
states = output.diagnostic_variables['states'] # 1 = cold, 0 = warm
print('stadial threshold:', model.stadial_threshold)
print('interstadial threshold:', model.interstadial_threshold)
# Count DO events (interstadial onsets)
n_events = int(np.sum(np.diff(states) == -1))
print('number of DO events:', n_events)
Add a time-varying forcing (e.g. a slow CO2 ramp across a glacial cycle):
import numpy as np
import matplotlib.pyplot as plt
from climatecritters.model_critters.melcher25 import Melcher25
t_arr = np.linspace(0, 599.88, 5000)
gamma_ramp = np.linspace(0.8, 3.2, 5000) # γ increases over time
model = Melcher25(
sigma=0.2,
gamma=lambda t: float(np.interp(t, t_arr, gamma_ramp)),
alpha=0.0,
)
output = model.integrate(
t_span=(t_arr[0], t_arr[-1]), y0=[1.0, 0.0],
method='heun_maruyama', dt=0.012,
kwargs={'random_seed': 0, 'si': 0.12},
)
ts = output.to_pyleo('db')
ts.plot()
plt.savefig('docs/reference/figures/Melcher25_ramp_example.png',
dpi=150, bbox_inches='tight')
References
Melcher et al. (2025), Clim. Past, 21, 115-132. https://cp.copernicus.org/articles/21/115/2025/
Methods
| Name | Description |
|---|---|
| compute_stability_thresholds | Compute stadial and interstadial thresholds via Jacobian stability analysis. |
| dydt | Evaluate the deterministic drift of the bistable SDE at time t and state y. |
| populate_diagnostics_from_history | Classify states and compute thresholds from the full solved trajectory. |
| sde_noise | Return the diffusion coefficient vector at time t and state y. |
compute_stability_thresholds
Melcher25.compute_stability_thresholds(alpha)Compute stadial and interstadial thresholds via Jacobian stability analysis.
Parameters
Returns
Notes
The lower threshold is the bifurcation point where trace(J) = 0 (Eq. 7, Melcher et al. 2025):
b_low = b0 + (-q0 - alpha/tau) / (2*q1)
The upper threshold is the non-differentiable point of the drift:
b_nd = b0 - q0/q1
which is independent of alpha.
dydt
Melcher25.dydt(t, y)Evaluate the deterministic drift of the bistable SDE at time t and state y.
Called by the solver at each timestep. Because this model sets uses_post_history = True, state is not accumulated here as a side effect.
Parameters
Returns
dydt : list of float-
Time-derivatives [d(db)/dt, dB/dt].
populate_diagnostics_from_history
Melcher25.populate_diagnostics_from_history(time, history)Classify states and compute thresholds from the full solved trajectory.
Called automatically by CCModel.post_integrate after every integrate() call. Populates diagnostic_variables['states'] with the hysteresis classification and sets self.stadial_threshold / self.interstadial_threshold.
Parameters
time : array -like-
Time axis of the solution.
history : ndarray of shape (n_times, 2)-
State trajectory [db, B] at each time step.
sde_noise
Melcher25.sde_noise(t, y)Return the diffusion coefficient vector at time t and state y.
Called by SDE solvers (euler_maruyama, heun_maruyama, milstein) to scale the Wiener increments. Noise is additive (state-independent), which means heun_maruyama achieves strong order 1.0 for this model.
Parameters
Returns
diffusion : ndarray of shape (2,)-
Diffusion coefficient for each state variable.