Skip to main content

oxilean_std/differential_equations/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
7
8/// A linear ODE dy/dt = a(t)y + b(t) with initial value.
9pub struct LinearODE {
10    /// Coefficient function a(t)
11    pub a: Box<dyn Fn(f64) -> f64>,
12    /// Forcing function b(t)
13    pub b: Box<dyn Fn(f64) -> f64>,
14    /// Initial time
15    pub t0: f64,
16    /// Initial value
17    pub y0: f64,
18}
19impl LinearODE {
20    /// Create a new linear ODE.
21    pub fn new(
22        a: impl Fn(f64) -> f64 + 'static,
23        b: impl Fn(f64) -> f64 + 'static,
24        t0: f64,
25        y0: f64,
26    ) -> Self {
27        Self {
28            a: Box::new(a),
29            b: Box::new(b),
30            t0,
31            y0,
32        }
33    }
34    /// Check whether this is a constant-coefficient autonomous ODE (b = 0).
35    pub fn is_autonomous(&self) -> bool {
36        [(self.b)(0.0), (self.b)(1.0), (self.b)(-1.0)]
37            .iter()
38            .all(|v| v.abs() < 1e-14)
39    }
40}
41/// Adams-Bashforth / Adams-Moulton multistep method (2-step as reference).
42pub struct MultistepMethod {
43    /// Step size h
44    pub h: f64,
45    /// Number of steps k (currently supports 2)
46    pub k: usize,
47}
48impl MultistepMethod {
49    /// Create a k-step multistep method.
50    pub fn new(h: f64, k: usize) -> Self {
51        Self { h, k: k.max(2) }
52    }
53    /// Solve using Adams-Bashforth 2-step (predictor) from t0 to t_end.
54    ///
55    /// Bootstrapped with one RK4 step.
56    pub fn solve_to(
57        &self,
58        f: &dyn Fn(f64, f64) -> f64,
59        t0: f64,
60        y0: f64,
61        t_end: f64,
62    ) -> Vec<(f64, f64)> {
63        let mut result = Vec::new();
64        let h = self.h;
65        let mut t = t0;
66        let mut y = y0;
67        result.push((t, y));
68        let rk4 = RungeKutta4::new(h);
69        let y1 = rk4.step(f, t, y);
70        let t1 = t + h;
71        result.push((t1, y1));
72        let mut f_prev = f(t, y);
73        let mut f_curr = f(t1, y1);
74        t = t1;
75        y = y1;
76        while t < t_end - 1e-12 {
77            let y_pred = y + h * (3.0 / 2.0 * f_curr - 1.0 / 2.0 * f_prev);
78            let t_new = t + h;
79            let f_pred = f(t_new, y_pred);
80            let y_new = y + h * (1.0 / 2.0 * f_pred + 1.0 / 2.0 * f_curr);
81            f_prev = f_curr;
82            f_curr = f(t_new, y_new);
83            t = t_new;
84            y = y_new;
85            result.push((t, y));
86        }
87        result
88    }
89}
90/// Euler method for vector ODEs.
91pub struct VectorEuler {
92    /// Step size h
93    pub h: f64,
94    /// Number of steps to record
95    pub n_steps: usize,
96}
97impl VectorEuler {
98    /// Create with given step size and number of steps.
99    pub fn new(h: f64, n_steps: usize) -> Self {
100        Self { h, n_steps }
101    }
102    /// Advance one Euler step: y_{n+1} = y_n + h F(t_n, y_n).
103    pub fn step(&self, y: &[f64], t: f64, f: impl Fn(f64, &[f64]) -> Vec<f64>) -> Vec<f64> {
104        let fy = f(t, y);
105        y.iter()
106            .zip(fy.iter())
107            .map(|(yi, fi)| yi + self.h * fi)
108            .collect()
109    }
110    /// Global error is O(h): returns the step size as proxy.
111    pub fn global_error_o_h(&self) -> f64 {
112        self.h
113    }
114}
115/// Adaptive Dormand-Prince RK45 solver with error control.
116pub struct AdaptiveStepRK45 {
117    /// Absolute tolerance
118    pub atol: f64,
119    /// Relative tolerance
120    pub rtol: f64,
121}
122impl AdaptiveStepRK45 {
123    /// Create with given tolerances.
124    pub fn new(atol: f64, rtol: f64) -> Self {
125        Self { atol, rtol }
126    }
127    /// Attempt one adaptive step; returns (y_new, h_new, accepted).
128    pub fn adaptive_step(
129        &self,
130        f: &dyn Fn(f64, f64) -> f64,
131        t: f64,
132        y: f64,
133        h: f64,
134    ) -> (f64, f64, bool) {
135        let k1 = f(t, y);
136        let k2 = f(t + h / 5.0, y + h / 5.0 * k1);
137        let k3 = f(
138            t + 3.0 * h / 10.0,
139            y + h * (3.0 / 40.0 * k1 + 9.0 / 40.0 * k2),
140        );
141        let k4 = f(
142            t + 4.0 * h / 5.0,
143            y + h * (44.0 / 45.0 * k1 - 56.0 / 15.0 * k2 + 32.0 / 9.0 * k3),
144        );
145        let k5 = f(
146            t + 8.0 * h / 9.0,
147            y + h
148                * (19372.0 / 6561.0 * k1 - 25360.0 / 2187.0 * k2 + 64448.0 / 6561.0 * k3
149                    - 212.0 / 729.0 * k4),
150        );
151        let k6 = f(
152            t + h,
153            y + h
154                * (9017.0 / 3168.0 * k1 - 355.0 / 33.0 * k2
155                    + 46732.0 / 5247.0 * k3
156                    + 49.0 / 176.0 * k4
157                    - 5103.0 / 18656.0 * k5),
158        );
159        let y5 = y + h
160            * (35.0 / 384.0 * k1 + 500.0 / 1113.0 * k3 + 125.0 / 192.0 * k4 - 2187.0 / 6784.0 * k5
161                + 11.0 / 84.0 * k6);
162        let k7 = f(t + h, y5);
163        let y4 = y + h
164            * (5179.0 / 57600.0 * k1 + 7571.0 / 16695.0 * k3 + 393.0 / 640.0 * k4
165                - 92097.0 / 339200.0 * k5
166                + 187.0 / 2100.0 * k6
167                + 1.0 / 40.0 * k7);
168        let err = (y5 - y4).abs();
169        let tol = self.atol + self.rtol * y.abs().max(y5.abs());
170        let accepted = err <= tol;
171        let factor = if err < 1e-15 {
172            5.0
173        } else {
174            0.9 * (tol / err).powf(0.2)
175        };
176        let h_new = (h * factor).min(10.0 * h).max(h / 10.0);
177        (y5, h_new, accepted)
178    }
179    /// Solve from t0 to t_end.
180    pub fn solve_to(
181        &self,
182        f: &dyn Fn(f64, f64) -> f64,
183        t0: f64,
184        y0: f64,
185        t_end: f64,
186    ) -> Vec<(f64, f64)> {
187        let mut result = Vec::new();
188        let mut t = t0;
189        let mut y = y0;
190        let mut h = (t_end - t0) / 100.0;
191        result.push((t, y));
192        while t < t_end - 1e-12 {
193            if t + h > t_end {
194                h = t_end - t;
195            }
196            let (y_new, h_new, accepted) = self.adaptive_step(f, t, y, h);
197            if accepted {
198                t += h;
199                y = y_new;
200                result.push((t, y));
201            }
202            h = h_new;
203        }
204        result
205    }
206}
207/// Lyapunov exponent estimator for a scalar ODE.
208pub struct LyapunovExponent {
209    /// Integration time
210    pub t_end: f64,
211    /// Step size
212    pub h: f64,
213}
214impl LyapunovExponent {
215    /// Create with given integration time and step size.
216    pub fn new(t_end: f64, h: f64) -> Self {
217        Self { t_end, h }
218    }
219    /// Estimate the maximal Lyapunov exponent via finite-time growth of a nearby orbit.
220    ///
221    /// λ ≈ (1/T) log(|δ(T)| / |δ(0)|).
222    pub fn estimate(&self, f: &dyn Fn(f64, f64) -> f64, x0: f64) -> f64 {
223        let delta0 = 1e-8;
224        let rk4 = RungeKutta4::new(self.h);
225        let traj1 = rk4.solve_to(f, 0.0, x0, self.t_end);
226        let traj2 = rk4.solve_to(f, 0.0, x0 + delta0, self.t_end);
227        let y1 = traj1.last().map(|&(_, y)| y).unwrap_or(x0);
228        let y2 = traj2.last().map(|&(_, y)| y).unwrap_or(x0 + delta0);
229        let delta_t = (y2 - y1).abs();
230        if delta_t < 1e-15 || self.t_end < 1e-12 {
231            0.0
232        } else {
233            (delta_t / delta0).ln() / self.t_end
234        }
235    }
236}
237/// RK4 for vector ODEs.
238pub struct VectorRK4 {
239    /// Step size h
240    pub h: f64,
241}
242impl VectorRK4 {
243    /// Create with given step size.
244    pub fn new(h: f64) -> Self {
245        Self { h }
246    }
247    /// Advance one RK4 step.
248    pub fn step(&self, y: &[f64], t: f64, f: impl Fn(f64, &[f64]) -> Vec<f64>) -> Vec<f64> {
249        let h = self.h;
250        let k1 = f(t, y);
251        let y2: Vec<f64> = y
252            .iter()
253            .zip(&k1)
254            .map(|(yi, ki)| yi + h / 2.0 * ki)
255            .collect();
256        let k2 = f(t + h / 2.0, &y2);
257        let y3: Vec<f64> = y
258            .iter()
259            .zip(&k2)
260            .map(|(yi, ki)| yi + h / 2.0 * ki)
261            .collect();
262        let k3 = f(t + h / 2.0, &y3);
263        let y4: Vec<f64> = y.iter().zip(&k3).map(|(yi, ki)| yi + h * ki).collect();
264        let k4 = f(t + h, &y4);
265        y.iter()
266            .enumerate()
267            .map(|(i, yi)| yi + h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
268            .collect()
269    }
270    /// Confirm RK4 is 4th-order accurate.
271    pub fn is_4th_order(&self) -> bool {
272        true
273    }
274}
275/// The Lorenz chaotic system: dx/dt = σ(y-x), dy/dt = x(ρ-z)-y, dz/dt = xy-βz.
276pub struct LorenzSystem {
277    /// σ (Prandtl number)
278    pub sigma: f64,
279    /// ρ (Rayleigh number)
280    pub rho: f64,
281    /// β
282    pub beta: f64,
283}
284impl LorenzSystem {
285    /// Create with the classic Lorenz parameters.
286    pub fn new(sigma: f64, rho: f64, beta: f64) -> Self {
287        Self { sigma, rho, beta }
288    }
289    /// Advance one RK4 step of size h from the given state [x, y, z].
290    pub fn step(&self, state: &[f64; 3], h: f64) -> [f64; 3] {
291        let lorenz = |s: &[f64; 3]| -> [f64; 3] {
292            [
293                self.sigma * (s[1] - s[0]),
294                s[0] * (self.rho - s[2]) - s[1],
295                s[0] * s[1] - self.beta * s[2],
296            ]
297        };
298        let k1 = lorenz(state);
299        let s2 = [
300            state[0] + h / 2.0 * k1[0],
301            state[1] + h / 2.0 * k1[1],
302            state[2] + h / 2.0 * k1[2],
303        ];
304        let k2 = lorenz(&s2);
305        let s3 = [
306            state[0] + h / 2.0 * k2[0],
307            state[1] + h / 2.0 * k2[1],
308            state[2] + h / 2.0 * k2[2],
309        ];
310        let k3 = lorenz(&s3);
311        let s4 = [
312            state[0] + h * k3[0],
313            state[1] + h * k3[1],
314            state[2] + h * k3[2],
315        ];
316        let k4 = lorenz(&s4);
317        [
318            state[0] + h / 6.0 * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]),
319            state[1] + h / 6.0 * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1]),
320            state[2] + h / 6.0 * (k1[2] + 2.0 * k2[2] + 2.0 * k3[2] + k4[2]),
321        ]
322    }
323    /// For the classic parameters (σ=10, ρ=28, β=8/3) the system is chaotic.
324    pub fn is_chaotic(&self) -> bool {
325        self.rho > 24.0 && (self.sigma - 10.0).abs() < 5.0 && self.beta > 0.0
326    }
327    /// Describe the strange (Lorenz) attractor.
328    pub fn strange_attractor(&self) -> String {
329        if self.is_chaotic() {
330            format!(
331                "Lorenz strange attractor: fractal dimension ≈ 2.06,                  Hausdorff dimension ≈ 2.06, σ={}, ρ={}, β={}",
332                self.sigma, self.rho, self.beta
333            )
334        } else {
335            format!(
336                "No strange attractor: parameters σ={}, ρ={}, β={} are not in the chaotic regime.",
337                self.sigma, self.rho, self.beta
338            )
339        }
340    }
341}
342/// A Jacobian matrix Df(x*) for linearisation at an equilibrium.
343pub struct JacobianMatrix {
344    /// 2×2 matrix entries
345    pub entries: [[f64; 2]; 2],
346}
347impl JacobianMatrix {
348    /// Construct from explicit 2×2 entries.
349    pub fn new(entries: [[f64; 2]; 2]) -> Self {
350        Self { entries }
351    }
352    /// Compute the determinant.
353    pub fn det(&self) -> f64 {
354        self.entries[0][0] * self.entries[1][1] - self.entries[0][1] * self.entries[1][0]
355    }
356    /// Compute the trace.
357    pub fn trace(&self) -> f64 {
358        self.entries[0][0] + self.entries[1][1]
359    }
360    /// Classify the equilibrium from this Jacobian.
361    pub fn classify_equilibrium(&self) -> StabilityType {
362        let ep = EquilibriumPoint {
363            x: 0.0,
364            y: 0.0,
365            jacobian: self.entries,
366        };
367        ep.classify()
368    }
369}
370/// A bifurcation point in a parameterized family of ODEs.
371pub struct BifurcationPoint {
372    /// Parameter value at the bifurcation
373    pub param_value: f64,
374    /// Type of bifurcation
375    pub bif_type: String,
376}
377impl BifurcationPoint {
378    /// Create with given parameter value and bifurcation type.
379    pub fn new(param_value: f64, bif_type: impl Into<String>) -> Self {
380        Self {
381            param_value,
382            bif_type: bif_type.into(),
383        }
384    }
385    /// Saddle-node bifurcation description.
386    pub fn saddle_node(&self) -> String {
387        format!(
388            "Saddle-node bifurcation at μ = {}: two equilibria collide and annihilate.",
389            self.param_value
390        )
391    }
392    /// Hopf bifurcation description.
393    pub fn hopf(&self) -> String {
394        format!(
395            "Hopf bifurcation at μ = {}: equilibrium loses stability, a limit cycle is born.",
396            self.param_value
397        )
398    }
399    /// Pitchfork bifurcation description.
400    pub fn pitchfork(&self) -> String {
401        format!(
402            "Pitchfork bifurcation at μ = {}: one equilibrium splits into three (supercritical) or vice versa.",
403            self.param_value
404        )
405    }
406}
407/// A heat equation u_t = κΔu.
408pub struct HeatEquation {
409    /// Thermal diffusivity κ > 0
410    pub kappa: f64,
411}
412impl HeatEquation {
413    /// Create with given diffusivity.
414    pub fn new(kappa: f64) -> Self {
415        Self { kappa }
416    }
417    /// Heat equations are parabolic.
418    pub fn classify_pde(&self) -> PDEType {
419        PDEType::Parabolic
420    }
421    /// Fundamental solution (heat kernel) at point x and time t.
422    ///
423    /// K(x, t) = 1/√(4πκt) exp(−x²/(4κt)).
424    pub fn fundamental_solution(&self, x: f64, t: f64) -> f64 {
425        if t <= 0.0 {
426            return f64::NAN;
427        }
428        let denom = (4.0 * std::f64::consts::PI * self.kappa * t).sqrt();
429        (-x * x / (4.0 * self.kappa * t)).exp() / denom
430    }
431}
432/// A delay differential equation y'(t) = f(t, y(t), y(t-τ)).
433pub struct DelayDE {
434    /// Delay τ > 0
435    pub tau: f64,
436    /// Step size for the method of steps
437    pub h: f64,
438}
439impl DelayDE {
440    /// Create with given delay and step size.
441    pub fn new(tau: f64, h: f64) -> Self {
442        Self { tau, h }
443    }
444    /// Solve by the method of steps from t0 to t_end.
445    ///
446    /// `phi` is the history function on [t0 - tau, t0].
447    pub fn solve_to(
448        &self,
449        f: &dyn Fn(f64, f64, f64) -> f64,
450        phi: &dyn Fn(f64) -> f64,
451        t0: f64,
452        y0: f64,
453        t_end: f64,
454    ) -> Vec<(f64, f64)> {
455        let mut result = Vec::new();
456        let mut t = t0;
457        let mut y = y0;
458        result.push((t, y));
459        while t < t_end - 1e-12 {
460            let h = self.h.min(t_end - t);
461            let t_del = t - self.tau;
462            let y_del = if t_del < t0 {
463                phi(t_del)
464            } else {
465                let idx = result
466                    .partition_point(|&(ti, _)| ti <= t_del)
467                    .saturating_sub(1);
468                result[idx].1
469            };
470            y = y + h * f(t, y, y_del);
471            t += h;
472            result.push((t, y));
473        }
474        result
475    }
476}
477impl DelayDE {
478    /// Characteristic equation for the linear DDE y'(t) = ay(t) + by(t-τ).
479    pub fn characteristic_equation(&self) -> String {
480        format!(
481            "Characteristic equation: λ = a + b·e^(-λτ), τ = {} (transcendental)",
482            self.tau
483        )
484    }
485    /// Stability region: for the simple linear DDE the equilibrium is stable iff
486    /// a + |b| < 0 (when a < 0 and |b| < -a).
487    pub fn stability_region(&self) -> String {
488        format!(
489            "Stability region for y'=ay+by(t-{}): requires a + |b| < 0              (exact boundary determined by characteristic roots on imaginary axis)",
490            self.tau
491        )
492    }
493}
494/// A strange attractor record (metadata only — no full simulation here).
495pub struct StrangeAttractor {
496    /// Name of the attractor (e.g. "Lorenz")
497    pub name: &'static str,
498    /// Estimated fractal dimension
499    pub fractal_dimension: f64,
500    /// Maximal Lyapunov exponent
501    pub max_lyapunov: f64,
502}
503impl StrangeAttractor {
504    /// Create a strange attractor descriptor.
505    pub fn new(name: &'static str, fractal_dimension: f64, max_lyapunov: f64) -> Self {
506        Self {
507            name,
508            fractal_dimension,
509            max_lyapunov,
510        }
511    }
512    /// Returns true if the attractor exhibits sensitive dependence (max Lyapunov > 0).
513    pub fn is_chaotic(&self) -> bool {
514        self.max_lyapunov > 0.0
515    }
516}
517/// Flow map φ_t for an autonomous ODE, computed via RK4.
518pub struct FlowMap {
519    rk4: RungeKutta4,
520}
521impl FlowMap {
522    /// Create a flow map with the given step size.
523    pub fn new(h: f64) -> Self {
524        Self {
525            rk4: RungeKutta4::new(h),
526        }
527    }
528    /// Apply the flow for time t: φ_t(x₀) using RK4.
529    pub fn apply(&self, f: &dyn Fn(f64, f64) -> f64, x0: f64, t: f64) -> f64 {
530        let traj = self.rk4.solve_to(f, 0.0, x0, t);
531        traj.last().map(|&(_, y)| y).unwrap_or(x0)
532    }
533}
534/// Represents an Itô SDE: dX_t = μ(X_t, t) dt + σ(X_t, t) dW_t.
535#[allow(dead_code)]
536pub struct ItoSDE {
537    /// Name/identifier for this SDE.
538    pub name: String,
539    /// Drift coefficient μ (simplified: linear form μ(x, t) = a*x + b).
540    pub drift_a: f64,
541    pub drift_b: f64,
542    /// Diffusion coefficient σ (constant in this simplified form).
543    pub diffusion_sigma: f64,
544}
545#[allow(dead_code)]
546impl ItoSDE {
547    /// Create a new Itô SDE with linear drift and constant diffusion.
548    pub fn new(name: &str, drift_a: f64, drift_b: f64, diffusion_sigma: f64) -> Self {
549        ItoSDE {
550            name: name.to_string(),
551            drift_a,
552            drift_b,
553            diffusion_sigma,
554        }
555    }
556    /// Geometric Brownian Motion: dX = μX dt + σX dW.
557    /// Here drift_a = μ, diffusion proportional to X.
558    pub fn geometric_brownian_motion(mu: f64, sigma: f64) -> Self {
559        ItoSDE::new("GBM", mu, 0.0, sigma)
560    }
561    /// Ornstein-Uhlenbeck process: dX = θ(μ - X) dt + σ dW.
562    pub fn ornstein_uhlenbeck(theta: f64, mu: f64, sigma: f64) -> Self {
563        ItoSDE::new("OU", -theta, theta * mu, sigma)
564    }
565    /// Euler-Maruyama step: X_{n+1} = X_n + μ(X_n, t) dt + σ ΔW
566    /// where ΔW ~ N(0, dt).
567    pub fn euler_maruyama_step(&self, x: f64, _t: f64, dt: f64, dw: f64) -> f64 {
568        let drift = self.drift_a * x + self.drift_b;
569        let diffusion = self.diffusion_sigma;
570        x + drift * dt + diffusion * dw
571    }
572    /// Milstein scheme: higher-order correction using σ σ' (ΔW^2 - dt) / 2.
573    /// For constant σ, σ' = 0, so Milstein = Euler-Maruyama.
574    pub fn milstein_step(&self, x: f64, t: f64, dt: f64, dw: f64) -> f64 {
575        self.euler_maruyama_step(x, t, dt, dw)
576    }
577    /// Variance of the Ornstein-Uhlenbeck stationary distribution: σ^2 / (2θ).
578    pub fn ou_stationary_variance(&self) -> f64 {
579        let theta = -self.drift_a;
580        if theta <= 0.0 {
581            return f64::INFINITY;
582        }
583        self.diffusion_sigma * self.diffusion_sigma / (2.0 * theta)
584    }
585    /// Mean of the OU stationary distribution: μ = drift_b / (-drift_a).
586    pub fn ou_stationary_mean(&self) -> f64 {
587        if self.drift_a >= 0.0 {
588            return f64::NAN;
589        }
590        self.drift_b / (-self.drift_a)
591    }
592    /// Itô's lemma: for f(X_t), df = (f' μ + f'' σ^2 / 2) dt + f' σ dW.
593    /// Returns the generator (drift part) for a smooth test function with f'(x)=1, f''(x)=0.
594    pub fn ito_generator(&self, x: f64) -> f64 {
595        self.drift_a * x + self.drift_b
596    }
597}
598/// Represents a Fredholm integral equation of the second kind:
599/// u(x) = f(x) + λ ∫_a^b K(x, y) u(y) dy.
600#[allow(dead_code)]
601pub struct FredholmEquation {
602    /// The interval [a, b].
603    pub a: f64,
604    pub b: f64,
605    /// The parameter λ.
606    pub lambda: f64,
607    /// Number of quadrature points for numerical solution.
608    pub n_points: usize,
609}
610#[allow(dead_code)]
611impl FredholmEquation {
612    /// Create a new Fredholm equation.
613    pub fn new(a: f64, b: f64, lambda: f64, n_points: usize) -> Self {
614        FredholmEquation {
615            a,
616            b,
617            lambda,
618            n_points,
619        }
620    }
621    /// Quadrature nodes using midpoint rule.
622    pub fn quadrature_nodes(&self) -> Vec<f64> {
623        let h = (self.b - self.a) / self.n_points as f64;
624        (0..self.n_points)
625            .map(|i| self.a + (i as f64 + 0.5) * h)
626            .collect()
627    }
628    /// Quadrature weight (uniform for midpoint rule).
629    pub fn quadrature_weight(&self) -> f64 {
630        (self.b - self.a) / self.n_points as f64
631    }
632    /// Neumann series approximation: u ≈ f + λ K f + λ^2 K^2 f + ...
633    /// For |λ| * ||K||_∞ < 1, this converges. Returns order-2 approximation.
634    pub fn neumann_series_order2(&self, f_at_nodes: &[f64], kernel_norm: f64) -> Vec<f64> {
635        let w = self.quadrature_weight();
636        let n = self.n_points;
637        let sum_f: f64 = f_at_nodes.iter().sum::<f64>();
638        let kf_approx = kernel_norm * sum_f * w;
639        (0..n)
640            .map(|i| f_at_nodes[i] + self.lambda * kf_approx)
641            .collect()
642    }
643    /// Spectral radius condition for Neumann series convergence: |λ| * ||K||_2 < 1.
644    pub fn neumann_convergence_condition(&self, kernel_l2_norm: f64) -> bool {
645        self.lambda.abs() * kernel_l2_norm < 1.0
646    }
647}
648/// A Laplace equation Δu = 0 representation.
649pub struct LaplacianEqn {
650    /// Domain: [x_min, x_max] × [y_min, y_max]
651    pub domain: [f64; 4],
652}
653impl LaplacianEqn {
654    /// Create with the given rectangular domain.
655    pub fn new(x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
656        Self {
657            domain: [x_min, x_max, y_min, y_max],
658        }
659    }
660    /// PDEs of the form Δu = 0 are always elliptic.
661    pub fn classify_pde(&self) -> PDEType {
662        PDEType::Elliptic
663    }
664}
665/// An ordinary differential equation dy/dt = f(t, y) with initial value y(t₀) = y₀.
666pub struct ODE {
667    /// Right-hand side f(t, y)
668    pub f: Box<dyn Fn(f64, f64) -> f64>,
669    /// Initial time t₀
670    pub t0: f64,
671    /// Initial value y₀ = y(t₀)
672    pub y0: f64,
673}
674impl ODE {
675    /// Create a new ODE initial-value problem.
676    pub fn new(f: impl Fn(f64, f64) -> f64 + 'static, t0: f64, y0: f64) -> Self {
677        Self {
678            f: Box::new(f),
679            t0,
680            y0,
681        }
682    }
683}
684/// Single-step Euler method solver.
685pub struct EulerMethod {
686    /// Step size h
687    pub h: f64,
688}
689impl EulerMethod {
690    /// Create with the given step size.
691    pub fn new(h: f64) -> Self {
692        Self { h }
693    }
694    /// Perform one Euler step: y_{n+1} = y_n + h f(t_n, y_n).
695    pub fn step(&self, f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64) -> f64 {
696        y + self.h * f(t, y)
697    }
698    /// Solve from t0 to t_end, returning (t, y) pairs.
699    pub fn solve_to(
700        &self,
701        f: &dyn Fn(f64, f64) -> f64,
702        t0: f64,
703        y0: f64,
704        t_end: f64,
705    ) -> Vec<(f64, f64)> {
706        let mut result = Vec::new();
707        let mut t = t0;
708        let mut y = y0;
709        result.push((t, y));
710        while t < t_end - 1e-12 {
711            let h = self.h.min(t_end - t);
712            y = y + h * f(t, y);
713            t += h;
714            result.push((t, y));
715        }
716        result
717    }
718    /// Global error is O(h): returns h as a proxy for the error constant.
719    pub fn global_error_o_h(&self) -> f64 {
720        self.h
721    }
722}
723/// Stability analysis via eigenvalues (real_part, imag_part).
724pub struct StabilityAnalysis {
725    /// Eigenvalues as (real part, imaginary part) pairs
726    pub eigenvalues: Vec<(f64, f64)>,
727}
728impl StabilityAnalysis {
729    /// Create from a list of (real, imag) eigenvalue pairs.
730    pub fn new(eigenvalues: Vec<(f64, f64)>) -> Self {
731        Self { eigenvalues }
732    }
733    /// Lyapunov stable: all eigenvalues have non-positive real parts.
734    pub fn is_stable(&self) -> bool {
735        self.eigenvalues.iter().all(|(re, _)| *re <= 1e-10)
736    }
737    /// Asymptotically stable: all eigenvalues have strictly negative real parts.
738    pub fn is_asymptotically_stable(&self) -> bool {
739        self.eigenvalues.iter().all(|(re, _)| *re < -1e-10)
740    }
741    /// Lyapunov criterion: the equilibrium is stable iff all eigenvalues have Re ≤ 0
742    /// (with non-strictly: marginal stability). Returns a descriptive string.
743    pub fn lyapunov_criterion(&self) -> String {
744        let max_re = self
745            .eigenvalues
746            .iter()
747            .map(|(r, _)| *r)
748            .fold(f64::NEG_INFINITY, f64::max);
749        if max_re < -1e-10 {
750            format!("Asymptotically stable (max Re(λ) = {:.4e})", max_re)
751        } else if max_re <= 1e-10 {
752            format!("Lyapunov stable — marginal (max Re(λ) = {:.4e})", max_re)
753        } else {
754            format!("Unstable (max Re(λ) = {:.4e} > 0)", max_re)
755        }
756    }
757}
758/// A solution in function space C([t₀−τ, T]).
759pub struct FunctionSpaceSolution {
760    /// The solution values at grid points
761    pub times: Vec<f64>,
762    /// Corresponding y values
763    pub values: Vec<f64>,
764}
765impl FunctionSpaceSolution {
766    /// Create from parallel time/value vectors.
767    pub fn new(times: Vec<f64>, values: Vec<f64>) -> Self {
768        Self { times, values }
769    }
770    /// Evaluate by linear interpolation.
771    pub fn eval(&self, t: f64) -> f64 {
772        let idx = self.times.partition_point(|&ti| ti <= t);
773        if idx == 0 {
774            return self.values[0];
775        }
776        if idx >= self.times.len() {
777            return *self
778                .values
779                .last()
780                .expect("values is non-empty: checked by idx >= len condition");
781        }
782        let t0 = self.times[idx - 1];
783        let t1 = self.times[idx];
784        let y0 = self.values[idx - 1];
785        let y1 = self.values[idx];
786        if (t1 - t0).abs() < 1e-15 {
787            y0
788        } else {
789            y0 + (y1 - y0) * (t - t0) / (t1 - t0)
790        }
791    }
792    /// Infinity-norm of the solution.
793    pub fn sup_norm(&self) -> f64 {
794        self.values.iter().cloned().fold(0.0_f64, f64::max)
795    }
796}
797/// Volterra integral equation y(t) = ∫₀ᵗ K(t,s)y(s)ds + g(t).
798pub struct VolterraIntegralEquation {
799    /// Forcing function g(t)
800    pub g: Box<dyn Fn(f64) -> f64>,
801    /// Number of quadrature points per step
802    pub n_quad: usize,
803}
804impl VolterraIntegralEquation {
805    /// Create with given forcing function and quadrature count.
806    pub fn new(g: impl Fn(f64) -> f64 + 'static, n_quad: usize) -> Self {
807        Self {
808            g: Box::new(g),
809            n_quad,
810        }
811    }
812    /// Solve numerically using the Nyström method (trapezoidal rule in s) on [0, t_end].
813    pub fn solve(&self, kernel: &dyn Fn(f64, f64) -> f64, t_end: f64, n: usize) -> Vec<(f64, f64)> {
814        let h = t_end / n as f64;
815        let mut y = vec![0.0f64; n + 1];
816        y[0] = (self.g)(0.0);
817        for i in 1..=n {
818            let ti = i as f64 * h;
819            let mut integral = 0.0;
820            for j in 0..i {
821                let sj = j as f64 * h;
822                let w = if j == 0 || j == i - 1 { 0.5 } else { 1.0 };
823                integral += w * h * kernel(ti, sj) * y[j];
824            }
825            y[i] = integral + (self.g)(ti);
826        }
827        (0..=n).map(|i| (i as f64 * h, y[i])).collect()
828    }
829}
830/// A wave equation u_tt = c²Δu.
831pub struct WaveEquation {
832    /// Wave speed c
833    pub c: f64,
834}
835impl WaveEquation {
836    /// Create with given wave speed.
837    pub fn new(c: f64) -> Self {
838        Self { c }
839    }
840    /// Wave equations are hyperbolic.
841    pub fn classify_pde(&self) -> PDEType {
842        PDEType::Hyperbolic
843    }
844}
845/// Classification of a second-order linear PDE Au_xx + Bu_xy + Cu_yy + ... = 0.
846#[derive(Debug, Clone, PartialEq)]
847pub enum PDEType {
848    /// B² - 4AC < 0
849    Elliptic,
850    /// B² - 4AC = 0
851    Parabolic,
852    /// B² - 4AC > 0
853    Hyperbolic,
854    /// Discriminant changes sign in the domain
855    Mixed,
856}
857/// A boundary condition type.
858#[derive(Debug, Clone)]
859pub enum BoundaryCondition {
860    /// u = g on boundary
861    Dirichlet(f64),
862    /// du/dn = g on boundary
863    Neumann(f64),
864    /// αu + β du/dn = g on boundary
865    Robin(f64, f64, f64),
866    /// Periodic boundary conditions
867    Periodic,
868}
869/// A Hamiltonian system with symplectic structure.
870pub struct HamiltonianSystem {
871    /// String representation of the Hamiltonian H(q,p)
872    pub hamiltonian: String,
873    /// Number of degrees of freedom (dim of q or p separately)
874    pub dim: usize,
875}
876impl HamiltonianSystem {
877    /// Create with given Hamiltonian string and dimension.
878    pub fn new(hamiltonian: impl Into<String>, dim: usize) -> Self {
879        Self {
880            hamiltonian: hamiltonian.into(),
881            dim,
882        }
883    }
884    /// Describe Hamilton's equations of motion: q̇ = ∂H/∂p, ṗ = -∂H/∂q.
885    pub fn hamilton_equations(&self) -> String {
886        format!(
887            "Hamilton's equations for H = {}:\n  q̇ᵢ = ∂H/∂pᵢ\n  ṗᵢ = -∂H/∂qᵢ  (i = 1..{})",
888            self.hamiltonian, self.dim
889        )
890    }
891    /// Describe the symplectic structure: the canonical 2-form ω = dq ∧ dp.
892    pub fn symplectic_structure(&self) -> String {
893        format!(
894            "Symplectic form ω = Σᵢ dqᵢ ∧ dpᵢ on ℝ^{} (standard)",
895            2 * self.dim
896        )
897    }
898    /// Liouville's theorem: the Hamiltonian flow preserves phase-space volume.
899    pub fn liouville_theorem(&self) -> String {
900        "Liouville: Hamiltonian flow is volume-preserving (div X_H = 0).".to_string()
901    }
902}
903/// Manifold type at an equilibrium.
904#[derive(Debug, Clone, PartialEq)]
905pub enum Manifold {
906    /// Tangent to eigenspace for eigenvalues with Re < 0
907    StableManifold,
908    /// Tangent to eigenspace for eigenvalues with Re > 0
909    UnstableManifold,
910    /// Tangent to eigenspace for eigenvalues with Re = 0
911    CenterManifold,
912}
913/// An exact ODE P(x,y)dx + Q(x,y)dy = 0.
914pub struct ExactODE {
915    /// P(x, y)
916    pub p: Box<dyn Fn(f64, f64) -> f64>,
917    /// Q(x, y)
918    pub q: Box<dyn Fn(f64, f64) -> f64>,
919}
920impl ExactODE {
921    /// Create a new exact-ODE candidate.
922    pub fn new(
923        p: impl Fn(f64, f64) -> f64 + 'static,
924        q: impl Fn(f64, f64) -> f64 + 'static,
925    ) -> Self {
926        Self {
927            p: Box::new(p),
928            q: Box::new(q),
929        }
930    }
931    /// Test exactness ∂P/∂y ≈ ∂Q/∂x at a sample point using central differences.
932    pub fn is_exact(&self) -> bool {
933        let h = 1e-5;
934        let x0 = 1.0_f64;
935        let y0 = 1.0_f64;
936        let dp_dy = ((self.p)(x0, y0 + h) - (self.p)(x0, y0 - h)) / (2.0 * h);
937        let dq_dx = ((self.q)(x0 + h, y0) - (self.q)(x0 - h, y0)) / (2.0 * h);
938        (dp_dy - dq_dx).abs() < 1e-6
939    }
940}
941/// Represents a one-parameter family of ODEs: x' = f(x, λ).
942#[allow(dead_code)]
943pub struct BifurcationDiagram {
944    /// Parameter values λ.
945    pub params: Vec<f64>,
946    /// Fixed points at each λ (simplified: polynomial roots approximation).
947    pub fixed_points: Vec<Vec<f64>>,
948}
949#[allow(dead_code)]
950impl BifurcationDiagram {
951    /// Create a bifurcation diagram for the normal form x' = λx - x^3 (pitchfork).
952    pub fn pitchfork_normal_form(lambda_range: &[f64]) -> Self {
953        let mut fps = vec![];
954        for &lam in lambda_range {
955            if lam < 0.0 {
956                fps.push(vec![0.0]);
957            } else {
958                let sqrt_lam = lam.sqrt();
959                fps.push(vec![-sqrt_lam, 0.0, sqrt_lam]);
960            }
961        }
962        BifurcationDiagram {
963            params: lambda_range.to_vec(),
964            fixed_points: fps,
965        }
966    }
967    /// Saddle-node normal form: x' = λ + x^2.
968    pub fn saddle_node_normal_form(lambda_range: &[f64]) -> Self {
969        let mut fps = vec![];
970        for &lam in lambda_range {
971            if lam < 0.0 {
972                let sqrt_neg = (-lam).sqrt();
973                fps.push(vec![-sqrt_neg, sqrt_neg]);
974            } else if lam == 0.0 {
975                fps.push(vec![0.0]);
976            } else {
977                fps.push(vec![]);
978            }
979        }
980        BifurcationDiagram {
981            params: lambda_range.to_vec(),
982            fixed_points: fps,
983        }
984    }
985    /// Hopf normal form: r' = λr - r^3 in polar coords.
986    /// Limit cycle radius = sqrt(λ) for λ > 0.
987    pub fn hopf_limit_cycle_radius(lambda: f64) -> f64 {
988        if lambda > 0.0 {
989            lambda.sqrt()
990        } else {
991            0.0
992        }
993    }
994    /// Count bifurcation points (parameter values where structure changes).
995    pub fn count_bifurcation_points(&self) -> usize {
996        let mut count = 0;
997        for i in 1..self.fixed_points.len() {
998            if self.fixed_points[i].len() != self.fixed_points[i - 1].len() {
999                count += 1;
1000            }
1001        }
1002        count
1003    }
1004}
1005/// A stochastic differential equation dX = f(X,t)dt + g(X,t)dW.
1006pub struct StochasticDE {
1007    /// String representation of the drift coefficient f(X,t)
1008    pub drift: String,
1009    /// String representation of the diffusion coefficient g(X,t)
1010    pub diffusion: String,
1011}
1012impl StochasticDE {
1013    /// Create with given drift and diffusion descriptions.
1014    pub fn new(drift: impl Into<String>, diffusion: impl Into<String>) -> Self {
1015        Self {
1016            drift: drift.into(),
1017            diffusion: diffusion.into(),
1018        }
1019    }
1020    /// Itô's formula: for smooth F(t,X), dF = (∂F/∂t + f·∂F/∂X + ½g²·∂²F/∂X²)dt + g·∂F/∂X·dW.
1021    pub fn ito_formula(&self) -> String {
1022        format!(
1023            "Itô formula for dX = ({})dt + ({})dW:\n             dF = (∂F/∂t + f·F' + ½g²·F'')dt + g·F'·dW",
1024            self.drift, self.diffusion
1025        )
1026    }
1027    /// Explain the difference between Itô and Stratonovich conventions.
1028    pub fn stratonovich_vs_ito(&self) -> String {
1029        format!(
1030            "Stratonovich SDE: dX = ({})dt + ({})∘dW\n             Conversion: Itô drift = Stratonovich drift - ½g·(∂g/∂X).\n             Itô is the standard in mathematical finance; Stratonovich in physics.",
1031            self.drift, self.diffusion
1032        )
1033    }
1034}
1035/// A multi-dimensional ODE system dy/dt = F(t, y) with vector state.
1036pub struct VectorODE {
1037    /// Spatial dimension (number of equations)
1038    pub dimension: usize,
1039    /// Whether the ODE is autonomous (F does not depend explicitly on t)
1040    pub is_autonomous: bool,
1041    /// Right-hand side F(t, y) → dy/dt
1042    pub rhs: Box<dyn Fn(f64, &[f64]) -> Vec<f64>>,
1043}
1044impl VectorODE {
1045    /// Create a new vector ODE with the given dimension and RHS.
1046    pub fn new(
1047        dimension: usize,
1048        is_autonomous: bool,
1049        rhs: impl Fn(f64, &[f64]) -> Vec<f64> + 'static,
1050    ) -> Self {
1051        Self {
1052            dimension,
1053            is_autonomous,
1054            rhs: Box::new(rhs),
1055        }
1056    }
1057    /// Return a string description of the RHS vector field.
1058    pub fn rhs_vector_field(&self) -> String {
1059        if self.is_autonomous {
1060            format!(
1061                "Autonomous vector field F: ℝ^{} → ℝ^{}",
1062                self.dimension, self.dimension
1063            )
1064        } else {
1065            format!(
1066                "Non-autonomous vector field F: ℝ × ℝ^{} → ℝ^{}",
1067                self.dimension, self.dimension
1068            )
1069        }
1070    }
1071    /// Approximate equilibrium points by sampling the grid [-5,5]^n (only for dim ≤ 2).
1072    pub fn equilibrium_points(&self, tol: f64) -> Vec<Vec<f64>> {
1073        let mut result = Vec::new();
1074        if self.dimension == 1 {
1075            let n = 200usize;
1076            for i in 0..n {
1077                let x = -5.0 + 10.0 * i as f64 / n as f64;
1078                let f = (self.rhs)(0.0, &[x]);
1079                if f[0].abs() < tol {
1080                    result.push(vec![x]);
1081                }
1082            }
1083        } else if self.dimension == 2 {
1084            let n = 40usize;
1085            for i in 0..n {
1086                for j in 0..n {
1087                    let x = -5.0 + 10.0 * i as f64 / n as f64;
1088                    let y = -5.0 + 10.0 * j as f64 / n as f64;
1089                    let f = (self.rhs)(0.0, &[x, y]);
1090                    if f[0].abs() < tol && f[1].abs() < tol {
1091                        result.push(vec![x, y]);
1092                    }
1093                }
1094            }
1095        }
1096        result
1097    }
1098}
1099/// Classification of a 2-D equilibrium point.
1100#[derive(Debug, Clone, PartialEq)]
1101pub enum StabilityType {
1102    /// Real negative eigenvalues: stable node
1103    StableNode,
1104    /// Real positive eigenvalues: unstable node
1105    UnstableNode,
1106    /// Real eigenvalues of opposite sign: saddle
1107    Saddle,
1108    /// Complex eigenvalues with negative real part: stable spiral
1109    StableSpiral,
1110    /// Complex eigenvalues with positive real part: unstable spiral
1111    UnstableSpiral,
1112    /// Pure imaginary eigenvalues: center
1113    Center,
1114    /// Degenerate (zero eigenvalue)
1115    Degenerate,
1116}
1117/// Classical 4th-order Runge-Kutta solver.
1118pub struct RungeKutta4 {
1119    /// Step size h
1120    pub h: f64,
1121}
1122impl RungeKutta4 {
1123    /// Create with the given step size.
1124    pub fn new(h: f64) -> Self {
1125        Self { h }
1126    }
1127    /// Perform one RK4 step.
1128    pub fn step(&self, f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64) -> f64 {
1129        let h = self.h;
1130        let k1 = f(t, y);
1131        let k2 = f(t + h / 2.0, y + h / 2.0 * k1);
1132        let k3 = f(t + h / 2.0, y + h / 2.0 * k2);
1133        let k4 = f(t + h, y + h * k3);
1134        y + h / 6.0 * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
1135    }
1136    /// Solve from t0 to t_end, returning (t, y) pairs.
1137    pub fn solve_to(
1138        &self,
1139        f: &dyn Fn(f64, f64) -> f64,
1140        t0: f64,
1141        y0: f64,
1142        t_end: f64,
1143    ) -> Vec<(f64, f64)> {
1144        let mut result = Vec::new();
1145        let mut t = t0;
1146        let mut y = y0;
1147        result.push((t, y));
1148        while t < t_end - 1e-12 {
1149            let h = self.h.min(t_end - t);
1150            let k1 = f(t, y);
1151            let k2 = f(t + h / 2.0, y + h / 2.0 * k1);
1152            let k3 = f(t + h / 2.0, y + h / 2.0 * k2);
1153            let k4 = f(t + h, y + h * k3);
1154            y += h / 6.0 * (k1 + 2.0 * k2 + 2.0 * k3 + k4);
1155            t += h;
1156            result.push((t, y));
1157        }
1158        result
1159    }
1160    /// Error estimate (Richardson extrapolation with half-step).
1161    pub fn error_estimate(&self, f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64) -> f64 {
1162        let y_full = self.step(f, t, y);
1163        let half = RungeKutta4::new(self.h / 2.0);
1164        let y_half1 = half.step(f, t, y);
1165        let y_half2 = half.step(f, t + self.h / 2.0, y_half1);
1166        (y_half2 - y_full).abs() / 15.0
1167    }
1168    /// Confirm this is a 4th-order method.
1169    pub fn is_4th_order(&self) -> bool {
1170        true
1171    }
1172}
1173/// A Sturm-Liouville problem (p(x)y')' + (q(x) + λw(x))y = 0.
1174pub struct SturmLiouville {
1175    /// p(x) — coefficient of y'
1176    pub p: Box<dyn Fn(f64) -> f64>,
1177    /// q(x) — potential
1178    pub q: Box<dyn Fn(f64) -> f64>,
1179    /// w(x) — weight function (positive)
1180    pub w: Box<dyn Fn(f64) -> f64>,
1181}
1182impl SturmLiouville {
1183    /// Create a new Sturm-Liouville problem.
1184    pub fn new(
1185        p: impl Fn(f64) -> f64 + 'static,
1186        q: impl Fn(f64) -> f64 + 'static,
1187        w: impl Fn(f64) -> f64 + 'static,
1188    ) -> Self {
1189        Self {
1190            p: Box::new(p),
1191            q: Box::new(q),
1192            w: Box::new(w),
1193        }
1194    }
1195    /// Approximate the lowest eigenvalue by power-iteration on the Rayleigh quotient
1196    /// (grid-based, for regular SL problems on [0, 1]).
1197    pub fn approximate_lowest_eigenvalue(&self, n: usize) -> f64 {
1198        let h = 1.0 / (n + 1) as f64;
1199        let mut y: Vec<f64> = (1..=n)
1200            .map(|k| (k as f64 * h * std::f64::consts::PI).sin())
1201            .collect();
1202        let norm: f64 = y.iter().map(|v| v * v).sum::<f64>().sqrt();
1203        for v in &mut y {
1204            *v /= norm;
1205        }
1206        let mut ay: Vec<f64> = vec![0.0; n];
1207        for i in 0..n {
1208            let xi = (i + 1) as f64 * h;
1209            let yi_prev = if i == 0 { 0.0 } else { y[i - 1] };
1210            let yi_next = if i == n - 1 { 0.0 } else { y[i + 1] };
1211            ay[i] =
1212                -(self.p)(xi) * (yi_next - 2.0 * y[i] + yi_prev) / (h * h) + (self.q)(xi) * y[i];
1213        }
1214        let numerator: f64 = y.iter().zip(ay.iter()).map(|(a, b)| a * b).sum();
1215        let denominator: f64 = y
1216            .iter()
1217            .enumerate()
1218            .map(|(i, v)| {
1219                let xi = (i + 1) as f64 * h;
1220                v * v * (self.w)(xi)
1221            })
1222            .sum();
1223        if denominator.abs() < 1e-15 {
1224            0.0
1225        } else {
1226            numerator / denominator
1227        }
1228    }
1229    /// Check whether this is a regular Sturm-Liouville problem (p, q, w continuous and p > 0, w > 0 on [0,1]).
1230    pub fn is_regular(&self) -> bool {
1231        let n = 10usize;
1232        (0..=n).all(|k| {
1233            let x = k as f64 / n as f64;
1234            (self.p)(x) > 1e-12 && (self.w)(x) > 1e-12
1235        })
1236    }
1237}
1238/// Poincaré first-return map for a scalar ODE with a section at y = section_value.
1239pub struct PoincareMap {
1240    /// Section value (y = c defines the Poincaré section)
1241    pub section_value: f64,
1242    /// Step size for integration
1243    pub h: f64,
1244    /// Maximum integration time
1245    pub t_max: f64,
1246}
1247impl PoincareMap {
1248    /// Create with given section and integration parameters.
1249    pub fn new(section_value: f64, h: f64, t_max: f64) -> Self {
1250        Self {
1251            section_value,
1252            h,
1253            t_max,
1254        }
1255    }
1256    /// Compute successive Poincaré crossings (upward through section_value).
1257    pub fn crossings(&self, f: &dyn Fn(f64, f64) -> f64, y0: f64) -> Vec<f64> {
1258        let rk4 = RungeKutta4::new(self.h);
1259        let traj = rk4.solve_to(f, 0.0, y0, self.t_max);
1260        let s = self.section_value;
1261        let mut crossings = Vec::new();
1262        for w in traj.windows(2) {
1263            let (_, ya) = w[0];
1264            let (_, yb) = w[1];
1265            if ya < s && yb >= s {
1266                let frac = (s - ya) / (yb - ya);
1267                crossings.push(ya + frac * (yb - ya));
1268            }
1269        }
1270        crossings
1271    }
1272}
1273/// An equilibrium point with its Jacobian for a 2-D autonomous ODE.
1274pub struct EquilibriumPoint {
1275    /// x-coordinate of the equilibrium
1276    pub x: f64,
1277    /// y-coordinate of the equilibrium
1278    pub y: f64,
1279    /// 2×2 Jacobian matrix [[a, b], [c, d]]
1280    pub jacobian: [[f64; 2]; 2],
1281}
1282impl EquilibriumPoint {
1283    /// Create an equilibrium point; the Jacobian is computed via finite differences.
1284    pub fn new(f: &dyn Fn(f64, f64) -> (f64, f64), x: f64, y: f64) -> Self {
1285        let h = 1e-5;
1286        let (fp0, fp1) = f(x + h, y);
1287        let (fm0, fm1) = f(x - h, y);
1288        let (fq0, fq1) = f(x, y + h);
1289        let (fq0m, fq1m) = f(x, y - h);
1290        let df00 = (fp0 - fm0) / (2.0 * h);
1291        let df01 = (fq0 - fq0m) / (2.0 * h);
1292        let df10 = (fp1 - fm1) / (2.0 * h);
1293        let df11 = (fq1 - fq1m) / (2.0 * h);
1294        Self {
1295            x,
1296            y,
1297            jacobian: [[df00, df01], [df10, df11]],
1298        }
1299    }
1300    /// Classify the equilibrium from eigenvalue analysis of the Jacobian.
1301    pub fn classify(&self) -> StabilityType {
1302        let [[a, b], [c, d]] = self.jacobian;
1303        let trace = a + d;
1304        let det = a * d - b * c;
1305        let discriminant = trace * trace - 4.0 * det;
1306        if det < -1e-10 {
1307            StabilityType::Saddle
1308        } else if discriminant < -1e-10 {
1309            if trace < -1e-10 {
1310                StabilityType::StableSpiral
1311            } else if trace > 1e-10 {
1312                StabilityType::UnstableSpiral
1313            } else {
1314                StabilityType::Center
1315            }
1316        } else if det.abs() < 1e-10 {
1317            StabilityType::Degenerate
1318        } else {
1319            if trace < -1e-10 {
1320                StabilityType::StableNode
1321            } else if trace > 1e-10 {
1322                StabilityType::UnstableNode
1323            } else {
1324                StabilityType::Degenerate
1325            }
1326        }
1327    }
1328}
1329/// Fredholm integral equation of the second kind: y(t) = λ ∫_a^b K(t,s)y(s)ds + g(t).
1330pub struct FredholmIntegralEquation {
1331    /// Eigenvalue parameter λ
1332    pub lambda: f64,
1333    /// Interval [a, b]
1334    pub a: f64,
1335    /// Interval [a, b]
1336    pub b: f64,
1337    /// Forcing function g(t)
1338    pub g: Box<dyn Fn(f64) -> f64>,
1339}
1340impl FredholmIntegralEquation {
1341    /// Create with given parameters.
1342    pub fn new(lambda: f64, a: f64, b: f64, g: impl Fn(f64) -> f64 + 'static) -> Self {
1343        Self {
1344            lambda,
1345            a,
1346            b,
1347            g: Box::new(g),
1348        }
1349    }
1350    /// Solve using the Nyström method (collocation at n equally spaced points).
1351    pub fn solve(&self, kernel: &dyn Fn(f64, f64) -> f64, n: usize) -> Option<Vec<(f64, f64)>> {
1352        let h = (self.b - self.a) / (n - 1) as f64;
1353        let nodes: Vec<f64> = (0..n).map(|i| self.a + i as f64 * h).collect();
1354        let weights: Vec<f64> = (0..n)
1355            .map(|j| if j == 0 || j == n - 1 { h / 2.0 } else { h })
1356            .collect();
1357        let mut mat = vec![vec![0.0f64; n]; n];
1358        for i in 0..n {
1359            for j in 0..n {
1360                mat[i][j] = if i == j { 1.0 } else { 0.0 }
1361                    - self.lambda * kernel(nodes[i], nodes[j]) * weights[j];
1362            }
1363        }
1364        let rhs: Vec<f64> = nodes.iter().map(|&t| (self.g)(t)).collect();
1365        let y_vals = solve_linear(mat, rhs)?;
1366        Some(nodes.into_iter().zip(y_vals).collect())
1367    }
1368}
1369/// Limit set ω(x) approximated by the tail of a long orbit.
1370pub struct LimSet {
1371    /// How long to integrate before sampling
1372    pub transient_time: f64,
1373    /// Sampling interval
1374    pub sample_interval: f64,
1375    /// Number of samples
1376    pub num_samples: usize,
1377}
1378impl LimSet {
1379    /// Create with given parameters.
1380    pub fn new(transient_time: f64, sample_interval: f64, num_samples: usize) -> Self {
1381        Self {
1382            transient_time,
1383            sample_interval,
1384            num_samples,
1385        }
1386    }
1387    /// Approximate ω(x₀) by long-time integration.
1388    pub fn approximate(&self, f: &dyn Fn(f64, f64) -> f64, x0: f64) -> Vec<f64> {
1389        let h = 1e-2;
1390        let rk4 = RungeKutta4::new(h);
1391        let traj = rk4.solve_to(f, 0.0, x0, self.transient_time);
1392        let (_, mut y) = traj.last().copied().unwrap_or((0.0, x0));
1393        let mut t = self.transient_time;
1394        let mut samples = Vec::with_capacity(self.num_samples);
1395        for _ in 0..self.num_samples {
1396            let seg = rk4.solve_to(f, t, y, t + self.sample_interval);
1397            let (t_new, y_new) = seg.last().copied().unwrap_or((t, y));
1398            t = t_new;
1399            y = y_new;
1400            samples.push(y);
1401        }
1402        samples
1403    }
1404}
1405/// Represents a Volterra integral equation of the second kind:
1406/// u(x) = f(x) + λ ∫_a^x K(x, y) u(y) dy.
1407#[allow(dead_code)]
1408pub struct VolterraEquation {
1409    /// Left endpoint a.
1410    pub a: f64,
1411    /// The parameter λ.
1412    pub lambda: f64,
1413}
1414#[allow(dead_code)]
1415impl VolterraEquation {
1416    /// Create a new Volterra equation.
1417    pub fn new(a: f64, lambda: f64) -> Self {
1418        VolterraEquation { a, lambda }
1419    }
1420    /// Successive approximation (Picard iteration) up to n steps.
1421    /// u_{n+1}(x) = f(x) + λ ∫_a^x K(x, y) u_n(y) dy.
1422    /// Simplified: returns the number of iterations taken.
1423    pub fn picard_iterations_needed(&self, tolerance: f64) -> u64 {
1424        if self.lambda.abs() < tolerance {
1425            return 1;
1426        }
1427        let rate = self.lambda.abs();
1428        if rate >= 1.0 {
1429            return u64::MAX;
1430        }
1431        (tolerance.ln() / rate.ln()).ceil().abs() as u64
1432    }
1433}
1434/// Implicit solver for stiff ODEs using backward Euler (BDF-1) with Newton iteration.
1435pub struct StiffSolver {
1436    /// Step size h
1437    pub h: f64,
1438    /// Newton iteration tolerance
1439    pub tol: f64,
1440}
1441impl StiffSolver {
1442    /// Create with given step size and tolerance.
1443    pub fn new(h: f64, tol: f64) -> Self {
1444        Self { h, tol }
1445    }
1446    /// Perform one backward Euler step via fixed-point iteration.
1447    pub fn step(&self, f: &dyn Fn(f64, f64) -> f64, t: f64, y: f64) -> f64 {
1448        let h = self.h;
1449        let t_new = t + h;
1450        let mut y_new = y;
1451        for _ in 0..50 {
1452            let g = y_new - y - h * f(t_new, y_new);
1453            let eps = 1e-7;
1454            let dg = 1.0 - h * (f(t_new, y_new + eps) - f(t_new, y_new - eps)) / (2.0 * eps);
1455            let delta = g / dg;
1456            y_new -= delta;
1457            if delta.abs() < self.tol {
1458                break;
1459            }
1460        }
1461        y_new
1462    }
1463    /// Solve from t0 to t_end using backward Euler.
1464    pub fn solve_to(
1465        &self,
1466        f: &dyn Fn(f64, f64) -> f64,
1467        t0: f64,
1468        y0: f64,
1469        t_end: f64,
1470    ) -> Vec<(f64, f64)> {
1471        let mut result = Vec::new();
1472        let mut t = t0;
1473        let mut y = y0;
1474        result.push((t, y));
1475        while t < t_end - 1e-12 {
1476            let h = self.h.min(t_end - t);
1477            let t_new = t + h;
1478            let mut y_new = y;
1479            let tol = self.tol;
1480            for _ in 0..50 {
1481                let g = y_new - y - h * f(t_new, y_new);
1482                let eps = 1e-7;
1483                let dg = 1.0 - h * (f(t_new, y_new + eps) - f(t_new, y_new - eps)) / (2.0 * eps);
1484                let delta = g / dg;
1485                y_new -= delta;
1486                if delta.abs() < tol {
1487                    break;
1488                }
1489            }
1490            t = t_new;
1491            y = y_new;
1492            result.push((t, y));
1493        }
1494        result
1495    }
1496}