use crate::error::{SpecialError, SpecialResult};
use crate::painleve::equations::painleve_system;
use crate::painleve::types::{PainleveConfig, PainleveSolution};
const C: [f64; 7] = [0.0, 1.0 / 5.0, 3.0 / 10.0, 4.0 / 5.0, 8.0 / 9.0, 1.0, 1.0];
const A: [[f64; 7]; 7] = [
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[1.0 / 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[3.0 / 40.0, 9.0 / 40.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[44.0 / 45.0, -56.0 / 15.0, 32.0 / 9.0, 0.0, 0.0, 0.0, 0.0],
[
19372.0 / 6561.0,
-25360.0 / 2187.0,
64448.0 / 6561.0,
-212.0 / 729.0,
0.0,
0.0,
0.0,
],
[
9017.0 / 3168.0,
-355.0 / 33.0,
46732.0 / 5247.0,
49.0 / 176.0,
-5103.0 / 18656.0,
0.0,
0.0,
],
[
35.0 / 384.0,
0.0,
500.0 / 1113.0,
125.0 / 192.0,
-2187.0 / 6784.0,
11.0 / 84.0,
0.0,
],
];
const B5: [f64; 7] = [
35.0 / 384.0,
0.0,
500.0 / 1113.0,
125.0 / 192.0,
-2187.0 / 6784.0,
11.0 / 84.0,
0.0,
];
const B4: [f64; 7] = [
5179.0 / 57600.0,
0.0,
7571.0 / 16695.0,
393.0 / 640.0,
-92097.0 / 339200.0,
187.0 / 2100.0,
1.0 / 40.0,
];
const SAFETY: f64 = 0.9;
const MAX_FACTOR: f64 = 5.0;
const MIN_FACTOR: f64 = 0.2;
const MIN_STEP_RATIO: f64 = 1e-15;
pub fn solve_painleve(config: &PainleveConfig) -> SpecialResult<PainleveSolution> {
let dt_total = config.t_end - config.t_start;
if dt_total.abs() < 1e-30 {
return Err(SpecialError::ValueError(
"Integration interval has zero length".to_string(),
));
}
let direction = dt_total.signum();
let abs_span = dt_total.abs();
let min_step = abs_span * MIN_STEP_RATIO;
let mut h = direction * abs_span.min(0.01);
let mut t = config.t_start;
let mut y = config.y0;
let mut dy = config.dy0;
let mut t_values = vec![t];
let mut y_values = vec![y];
let mut dy_values = vec![dy];
let mut poles: Vec<f64> = Vec::new();
let mut steps = 0usize;
while (config.t_end - t) * direction > min_step * 0.5 {
if steps >= config.max_steps {
return Ok(PainleveSolution {
t_values,
y_values,
dy_values,
poles,
converged: false,
steps_taken: steps,
});
}
if (t + h - config.t_end) * direction > 0.0 {
h = config.t_end - t;
}
match rk45_step(&config.equation, t, y, dy, h, config.tolerance) {
Ok((y_new, dy_new, err, h_new)) => {
if err <= 1.0 {
t += h;
y = y_new;
dy = dy_new;
steps += 1;
t_values.push(t);
y_values.push(y);
dy_values.push(dy);
if y.abs() > config.pole_threshold {
poles.push(t);
return Ok(PainleveSolution {
t_values,
y_values,
dy_values,
poles,
converged: false,
steps_taken: steps,
});
}
h = direction * h_new.abs().min(abs_span).max(min_step);
} else {
let factor = (SAFETY * (1.0 / err).powf(0.2)).clamp(MIN_FACTOR, 1.0);
h *= factor;
if h.abs() < min_step {
return Err(SpecialError::ConvergenceError(format!(
"Step size underflow at t={t}: h={h}"
)));
}
}
}
Err(_e) => {
h *= 0.5;
if h.abs() < min_step {
poles.push(t + h);
return Ok(PainleveSolution {
t_values,
y_values,
dy_values,
poles,
converged: false,
steps_taken: steps,
});
}
}
}
}
Ok(PainleveSolution {
t_values,
y_values,
dy_values,
poles,
converged: true,
steps_taken: steps,
})
}
fn rk45_step(
eq: &crate::painleve::types::PainleveEquation,
t: f64,
y: f64,
dy: f64,
h: f64,
tol: f64,
) -> SpecialResult<(f64, f64, f64, f64)> {
let mut ky = [0.0f64; 7];
let mut kdy = [0.0f64; 7];
let (f0y, f0dy) = painleve_system(eq, t, y, dy)?;
ky[0] = f0y;
kdy[0] = f0dy;
for i in 1..7 {
let ti = t + C[i] * h;
let mut yi = y;
let mut dyi = dy;
for j in 0..i {
yi += h * A[i][j] * ky[j];
dyi += h * A[i][j] * kdy[j];
}
let (fy, fdy) = painleve_system(eq, ti, yi, dyi)?;
ky[i] = fy;
kdy[i] = fdy;
}
let mut y5 = y;
let mut dy5 = dy;
for i in 0..7 {
y5 += h * B5[i] * ky[i];
dy5 += h * B5[i] * kdy[i];
}
let mut y4 = y;
let mut dy4 = dy;
for i in 0..7 {
y4 += h * B4[i] * ky[i];
dy4 += h * B4[i] * kdy[i];
}
let err_y = (y5 - y4).abs();
let err_dy = (dy5 - dy4).abs();
let scale_y = tol * (1.0 + y.abs());
let scale_dy = tol * (1.0 + dy.abs());
let err = (err_y / scale_y).max(err_dy / scale_dy);
let h_new = if err < 1e-30 {
h.abs() * MAX_FACTOR
} else {
let factor = (SAFETY * (1.0 / err).powf(0.2)).clamp(MIN_FACTOR, MAX_FACTOR);
h.abs() * factor
};
Ok((y5, dy5, err, h_new))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::painleve::types::PainleveEquation;
#[test]
fn test_solve_pi_short_interval() {
let config = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 0.0,
t_end: 0.5,
y0: 0.0,
dy0: 0.0,
tolerance: 1e-10,
max_steps: 100_000,
pole_threshold: 1e10,
};
let sol = solve_painleve(&config);
assert!(sol.is_ok());
let sol = sol.expect("solver should succeed");
assert!(sol.converged);
assert!(sol.steps_taken > 0);
assert!(sol.t_values.len() > 2);
let y_end = sol.y_values[sol.y_values.len() - 1];
assert!(y_end.abs() < 1.0, "y(0.5) should be small, got {y_end}");
}
#[test]
fn test_solve_pi_convergence() {
let config_coarse = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 0.0,
t_end: 0.3,
y0: 0.0,
dy0: 0.0,
tolerance: 1e-6,
max_steps: 100_000,
pole_threshold: 1e10,
};
let config_fine = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 0.0,
t_end: 0.3,
y0: 0.0,
dy0: 0.0,
tolerance: 1e-12,
max_steps: 100_000,
pole_threshold: 1e10,
};
let sol_c = solve_painleve(&config_coarse).expect("coarse solver failed");
let sol_f = solve_painleve(&config_fine).expect("fine solver failed");
assert!(
sol_f.steps_taken >= sol_c.steps_taken,
"fine={} coarse={}",
sol_f.steps_taken,
sol_c.steps_taken
);
assert!(sol_c.converged);
assert!(sol_f.converged);
}
#[test]
fn test_solve_pii_alpha_zero() {
let config = PainleveConfig {
equation: PainleveEquation::PII { alpha: 0.0 },
t_start: -2.0,
t_end: 2.0,
y0: 0.0,
dy0: 0.1,
tolerance: 1e-10,
max_steps: 100_000,
pole_threshold: 1e10,
};
let sol = solve_painleve(&config).expect("solver failed");
assert!(sol.steps_taken > 0);
}
#[test]
fn test_solve_zero_interval_error() {
let config = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 1.0,
t_end: 1.0,
y0: 0.0,
dy0: 0.0,
..PainleveConfig::default()
};
let sol = solve_painleve(&config);
assert!(sol.is_err());
}
#[test]
fn test_solve_backward_integration() {
let config = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 0.5,
t_end: 0.0,
y0: 0.001,
dy0: 0.0,
tolerance: 1e-10,
max_steps: 100_000,
pole_threshold: 1e10,
};
let sol = solve_painleve(&config).expect("backward solver failed");
assert!(sol.converged);
let t_last = sol.t_values[sol.t_values.len() - 1];
assert!(
(t_last - 0.0).abs() < 1e-6,
"should reach t=0, got {t_last}"
);
}
#[test]
fn test_pi_pole_detection() {
let config = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 0.0,
t_end: 10.0,
y0: 1.0,
dy0: 1.0,
tolerance: 1e-8,
max_steps: 100_000,
pole_threshold: 1e6,
};
let sol = solve_painleve(&config).expect("solver failed");
assert!(sol.steps_taken > 0);
}
#[test]
fn test_step_size_adaptation() {
let config = PainleveConfig {
equation: PainleveEquation::PI,
t_start: 0.0,
t_end: 1.0,
y0: 0.0,
dy0: 0.0,
tolerance: 1e-10,
max_steps: 100_000,
pole_threshold: 1e10,
};
let sol = solve_painleve(&config).expect("solver failed");
assert!(sol.t_values.len() >= 3);
let dt1 = sol.t_values[1] - sol.t_values[0];
let dt_last = sol.t_values[sol.t_values.len() - 1] - sol.t_values[sol.t_values.len() - 2];
assert!(
sol.steps_taken >= 2,
"should take multiple steps, took {}",
sol.steps_taken
);
let _ = dt1;
let _ = dt_last;
}
}