Mackey-Glass — Parameter Sweep
Delay-parameter study showing the periodic → chaotic → hyperchaotic transition in Mackey-Glass.
- advanced
Companion to the basic mackey_glass example. This sweeps the delay
parameter τ ∈ {6, 17, 30} to demonstrate the bifurcation cascade:
τ = 6— periodic (limit cycle)τ = 17— chaotic (the classical chaos value)τ = 30— hyperchaotic
Useful as a baseline for any work on attractor reconstruction, Lyapunov exponent estimation, or chaos-based forecasting.
Run it:
cargo run --release --example dde_mackey_glass Source
numra/examples/dde_mackey_glass.rs on GitHub
·
Related book chapter
use numra::dde::{DdeOptions, DdeSolver, DdeSystem, MethodOfSteps};
/// Mackey-Glass DDE system parameterized for easy reuse.
struct MackeyGlass {
beta: f64,
gamma: f64,
n: f64,
tau: f64,
}
impl DdeSystem<f64> for MackeyGlass {
fn dim(&self) -> usize {
1
}
fn delays(&self) -> Vec<f64> {
vec![self.tau]
}
fn rhs(&self, _t: f64, y: &[f64], y_delayed: &[&[f64]], dydt: &mut [f64]) {
let y_tau = y_delayed[0][0]; // y(t - tau)
dydt[0] = self.beta * y_tau / (1.0 + y_tau.powf(self.n)) - self.gamma * y[0];
}
}
/// Solve the Mackey-Glass equation and return summary statistics.
fn solve_and_analyze(tau: f64, t_final: f64) -> Result<(f64, f64, f64, usize), String> {
let system = MackeyGlass {
beta: 2.0,
gamma: 1.0,
n: 9.65,
tau,
};
let history = |_t: f64| vec![0.5];
let options = DdeOptions::default()
.rtol(1e-6)
.atol(1e-9)
.max_steps(200_000);
let result = MethodOfSteps::solve(&system, 0.0, t_final, &history, &options)?;
// Analyze only the second half of the trajectory (after transients)
let half = result.t.len() / 2;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for i in half..result.y.len() {
let val = result.y[i];
if val < y_min {
y_min = val;
}
if val > y_max {
y_max = val;
}
}
let y_final = result.y_final().unwrap()[0];
let n_steps = result.stats.n_accept;
Ok((y_min, y_max, y_final, n_steps))
}
fn main() {
println!("=== Mackey-Glass DDE: Parameter Study ===\n");
println!("Equation: y'(t) = beta*y(t-tau)/(1+y(t-tau)^n) - gamma*y(t)");
println!("Fixed parameters: beta=2, gamma=1, n=9.65");
println!("History: y(t) = 0.5 for t <= 0\n");
// Study how the delay tau affects dynamics
let delays = [2.0, 6.0, 10.0, 17.0, 23.0, 30.0];
let t_final = 500.0;
println!(
"Solving for {} different delay values (tau)...\n",
delays.len()
);
println!(
"{:>6} | {:>8} | {:>8} | {:>8} | {:>8} | {:>12}",
"tau", "y_min", "y_max", "range", "y_final", "steps"
);
println!(
"{:-<6}|{:-<9}|{:-<9}|{:-<9}|{:-<9}|{:-<13}",
"", "", "", "", "", ""
);
let mut results_data: Vec<(f64, f64, f64, f64)> = Vec::new();
for &tau in &delays {
match solve_and_analyze(tau, t_final) {
Ok((y_min, y_max, y_final, n_steps)) => {
let range = y_max - y_min;
println!(
"{:>6.1} | {:>8.4} | {:>8.4} | {:>8.4} | {:>8.4} | {:>12}",
tau, y_min, y_max, range, y_final, n_steps
);
results_data.push((tau, y_min, y_max, range));
}
Err(e) => {
println!("{:>6.1} | FAILED: {}", tau, e);
}
}
}
println!();
// Classify dynamics based on range
println!("=== Dynamics Classification ===\n");
for &(tau, _y_min, _y_max, range) in &results_data {
let classification = if range < 0.01 {
"Stable equilibrium"
} else if range < 0.3 {
"Small oscillations"
} else if range < 0.6 {
"Periodic (limit cycle)"
} else if range < 1.0 {
"Quasi-periodic / Chaotic"
} else {
"Strongly chaotic"
};
println!(
" tau = {:>5.1}: range = {:.4} => {}",
tau, range, classification
);
}
println!();
// Detailed trajectory for the classic chaotic case (tau=17)
println!("=== Detailed Trajectory for tau = 17 (classic chaos) ===\n");
let system = MackeyGlass {
beta: 2.0,
gamma: 1.0,
n: 9.65,
tau: 17.0,
};
let history = |_t: f64| vec![0.5];
let options = DdeOptions::default()
.rtol(1e-8)
.atol(1e-10)
.max_steps(200_000);
let result = MethodOfSteps::solve(&system, 0.0, t_final, &history, &options)
.expect("Failed to solve with tau=17");
println!("Solver statistics:");
println!(" Time points: {}", result.len());
println!(" Accepted steps: {}", result.stats.n_accept);
println!(" Rejected steps: {}", result.stats.n_reject);
println!();
// Sample trajectory
println!("Trajectory samples (tau=17):");
println!(" {:>10} | {:>12}", "t", "y(t)");
println!(" {:-<10}|{:-<13}", "", "");
let sample_times = [
0.0, 25.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0, 450.0, 500.0,
];
let mut sample_idx = 0;
for (i, &t) in result.t.iter().enumerate() {
if sample_idx < sample_times.len() && t >= sample_times[sample_idx] {
let y = result.y_at(i)[0];
println!(" {:>10.1} | {:>12.6}", t, y);
sample_idx += 1;
}
}
// Sensitivity to initial history
println!("\n=== Sensitivity to Initial History ===\n");
println!("Comparing y(0)=0.5 vs y(0)=0.51 at t=500 (tau=17):\n");
let history_b = |_t: f64| vec![0.51];
let result_b = MethodOfSteps::solve(&system, 0.0, t_final, &history_b, &options)
.expect("Failed to solve perturbed system");
let y_a = result.y_final().unwrap()[0];
let y_b = result_b.y_final().unwrap()[0];
println!(" y(500) with history=0.50: {:.8}", y_a);
println!(" y(500) with history=0.51: {:.8}", y_b);
println!(" Difference: {:.8}", (y_a - y_b).abs());
println!();
println!("A large difference from a small initial perturbation is a hallmark");
println!("of chaotic dynamics (sensitive dependence on initial conditions).");
println!();
println!("=== Done ===");
}