Skip to main content

qualia_core_db/solvers/calculus/
mod.rs

1//! Calculus & Differential Solvers - Zero-Allocation Implementation
2//!
3//! This module provides fixed-size stack-based solvers for differential equations,
4//! boundary value problems, and numerical integration suitable for the #![no_std]
5//! environment of Qualia-DB.
6
7use crate::solvers::{SolverConfig, SolverResult, SolverState, SolversError};
8
9// Numerical ODE / sensitivity / provenance solvers — relocated here from
10// `modalities::calculus` (they are STEM math, not logic modalities). The canonical
11// `solvers::calculus::*` surface now carries them.
12pub mod analysis;
13pub mod capabilities;
14pub mod dense;
15pub mod differential;
16pub mod exterior;
17pub mod grid;
18pub mod manifold;
19pub mod mechanics;
20pub mod ode_adaptive;
21pub mod ode_advanced;
22pub mod ode_solver;
23pub mod potential;
24pub mod quadrature;
25pub mod tensor_integrity;
26pub mod tensor_provenance;
27pub mod workspace;
28
29/// Runge-Kutta 4th order ODE solver with fixed memory footprint
30#[repr(C)]
31pub struct RungeKutta4Static {
32    /// Current state vector (fixed size)
33    pub state: [f64; 4],
34    /// Time step size
35    pub dt: f64,
36    /// Current time
37    pub t: f64,
38    /// Solver configuration
39    pub config: SolverConfig,
40    /// Solver state
41    pub solver_state: SolverState,
42}
43
44/// ODE state for RK4 solver
45#[repr(C)]
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct ODEState {
48    /// Current values
49    pub y: [f64; 4],
50    /// Current time
51    pub t: f64,
52    /// Step size
53    pub dt: f64,
54}
55
56/// Boundary Value Problem solver using shooting method
57#[repr(C)]
58pub struct ShootingMethodBVP {
59    /// Initial guess for shooting
60    pub initial_guess: [f64; 4],
61    /// Target boundary values
62    pub target_values: [f64; 4],
63    /// Current trajectory
64    pub trajectory: [ODEState; 100],
65    /// Error tracking
66    pub boundary_error: [f64; 4],
67    /// Solver configuration
68    pub config: SolverConfig,
69    /// Solver state
70    pub solver_state: SolverState,
71}
72
73/// BVP state for shooting method
74#[repr(C)]
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct BVPState {
77    /// Current iteration
78    pub iteration: u32,
79    /// Boundary error
80    pub boundary_error: f64,
81    /// Converged flag
82    pub converged: bool,
83    /// Shooting parameters
84    pub shooting_params: [f64; 4],
85}
86
87/// Simpson's rule integrator with chunked processing
88#[repr(C)]
89pub struct SimpsonsIntegratorChunked {
90    /// Chunk buffer. The first 11 entries hold ten Simpson panels; the final
91    /// slot is reserved to preserve the stable fixed-size ABI.
92    pub chunk_buffer: [f64; 12],
93    /// Current chunk index
94    pub chunk_index: u16,
95    /// Accumulated integral
96    pub accumulated_integral: f64,
97    /// Integration limits
98    pub a: f64,
99    pub b: f64,
100    /// Number of chunks processed
101    pub chunks_processed: u16,
102    /// Solver configuration
103    pub config: SolverConfig,
104    /// Solver state
105    pub solver_state: SolverState,
106}
107
108/// Integral chunk for processing
109#[repr(C)]
110#[derive(Clone, Copy)]
111pub struct IntegralChunk {
112    /// Function values
113    pub values: [f64; 12],
114    /// X coordinates
115    pub x_coords: [f64; 12],
116    /// Chunk contribution
117    pub contribution: f64,
118}
119
120/// ODE function trait for RK4
121pub trait ODEFunction {
122    /// Calculate derivatives dy/dt = f(t, y)
123    fn derivatives(&self, t: f64, y: &[f64; 4]) -> [f64; 4];
124}
125
126/// BVP function trait for shooting method
127pub trait BVPFunction {
128    /// Calculate derivatives for boundary value problem
129    fn derivatives(&self, t: f64, y: &[f64; 4], params: &[f64; 4]) -> [f64; 4];
130
131    /// Calculate boundary condition residuals
132    fn boundary_residuals(&self, y0: &[f64; 4], y1: &[f64; 4]) -> [f64; 4];
133}
134
135/// Integrand function trait for Simpson's integrator
136pub trait IntegrandFunction {
137    /// Evaluate function at point x
138    fn evaluate(&self, x: f64) -> f64;
139}
140
141impl RungeKutta4Static {
142    /// Create new RK4 solver
143    pub fn new(dt: f64, config: SolverConfig) -> Self {
144        Self {
145            state: [0.0; 4],
146            dt,
147            t: 0.0,
148            config,
149            solver_state: SolverState::default(),
150        }
151    }
152
153    /// Integrate ODE from t0 to t_final
154    pub fn integrate<F>(
155        &mut self,
156        f: &F,
157        t0: f64,
158        y0: [f64; 4],
159        t_final: f64,
160    ) -> SolverResult<ODEState>
161    where
162        F: ODEFunction,
163    {
164        if !self.dt.is_finite()
165            || self.dt <= 0.0
166            || !t0.is_finite()
167            || !t_final.is_finite()
168            || t_final < t0
169            || y0.iter().any(|value| !value.is_finite())
170        {
171            return Err(SolversError::InvalidParameters);
172        }
173
174        // Initialize state
175        self.state = y0;
176        self.t = t0;
177        self.solver_state.iteration = 0;
178        self.solver_state.converged = false;
179
180        // Integrate until final time
181        while self.t < t_final {
182            if self.solver_state.iteration >= self.config.max_iterations {
183                return Err(SolversError::ConvergenceFailed);
184            }
185
186            // Check if we need to adjust step size to hit final time exactly
187            let remaining_time = t_final - self.t;
188            let dt = if remaining_time < self.dt {
189                remaining_time
190            } else {
191                self.dt
192            };
193
194            // Perform RK4 step
195            self.rk4_step(f, dt)?;
196
197            // Update solver state
198            self.solver_state.iteration += 1;
199        }
200        self.solver_state.converged = true;
201
202        Ok(ODEState {
203            y: self.state,
204            t: self.t,
205            dt: self.dt,
206        })
207    }
208
209    /// Integrate until the derivative infinity norm is below the configured
210    /// tolerance. Unlike [`Self::integrate`], this operation is explicitly a
211    /// steady-state search and may terminate before `t_limit`.
212    pub fn integrate_until_steady_state<F>(
213        &mut self,
214        f: &F,
215        t0: f64,
216        y0: [f64; 4],
217        t_limit: f64,
218    ) -> SolverResult<ODEState>
219    where
220        F: ODEFunction,
221    {
222        if !self.dt.is_finite()
223            || self.dt <= 0.0
224            || !t0.is_finite()
225            || !t_limit.is_finite()
226            || t_limit < t0
227            || !self.config.tolerance.is_finite()
228            || self.config.tolerance < 0.0
229            || y0.iter().any(|value| !value.is_finite())
230        {
231            return Err(SolversError::InvalidParameters);
232        }
233
234        self.state = y0;
235        self.t = t0;
236        self.solver_state = SolverState::default();
237
238        loop {
239            let derivative = f.derivatives(self.t, &self.state);
240            if derivative.iter().any(|value| !value.is_finite()) {
241                return Err(SolversError::ComputationError);
242            }
243            let norm = derivative
244                .iter()
245                .fold(0.0_f64, |largest, value| largest.max(value.abs()));
246            self.solver_state.error = norm;
247            if norm <= self.config.tolerance {
248                self.solver_state.converged = true;
249                return Ok(self.get_state());
250            }
251            if self.t >= t_limit || self.solver_state.iteration >= self.config.max_iterations {
252                return Err(SolversError::ConvergenceFailed);
253            }
254
255            let dt = self.dt.min(t_limit - self.t);
256            self.rk4_step(f, dt)?;
257            self.solver_state.iteration += 1;
258        }
259    }
260
261    /// Perform single RK4 step
262    fn rk4_step<F>(&mut self, f: &F, dt: f64) -> SolverResult<()>
263    where
264        F: ODEFunction,
265    {
266        // RK4 coefficients
267        let k1 = f.derivatives(self.t, &self.state);
268        if k1.iter().any(|value| !value.is_finite()) {
269            return Err(SolversError::ComputationError);
270        }
271
272        let _y_temp: [f64; 4];
273        let mut y_temp = [0.0; 4];
274        for i in 0..4 {
275            y_temp[i] = self.state[i] + 0.5 * dt * k1[i];
276        }
277        let k2 = f.derivatives(self.t + 0.5 * dt, &y_temp);
278        if k2.iter().any(|value| !value.is_finite()) {
279            return Err(SolversError::ComputationError);
280        }
281
282        for i in 0..4 {
283            y_temp[i] = self.state[i] + 0.5 * dt * k2[i];
284        }
285        let k3 = f.derivatives(self.t + 0.5 * dt, &y_temp);
286        if k3.iter().any(|value| !value.is_finite()) {
287            return Err(SolversError::ComputationError);
288        }
289
290        for i in 0..4 {
291            y_temp[i] = self.state[i] + dt * k3[i];
292        }
293        let k4 = f.derivatives(self.t + dt, &y_temp);
294        if k4.iter().any(|value| !value.is_finite()) {
295            return Err(SolversError::ComputationError);
296        }
297
298        // Update state using RK4 formula
299        for i in 0..4 {
300            self.state[i] += dt * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]) / 6.0;
301        }
302        if self.state.iter().any(|value| !value.is_finite()) {
303            return Err(SolversError::ComputationError);
304        }
305
306        self.t += dt;
307
308        // This is a stage-variation diagnostic, not a local truncation error.
309        self.solver_state.error = self.estimate_error(&k1, &k2, &k3, &k4);
310
311        Ok(())
312    }
313
314    /// Return a dimensionless RK-stage variation diagnostic.
315    fn estimate_error(&self, k1: &[f64; 4], k2: &[f64; 4], k3: &[f64; 4], k4: &[f64; 4]) -> f64 {
316        let mut error: f64 = 0.0;
317
318        for i in 0..4 {
319            // Error estimate using all coefficients to detect high curvature and instability
320            let k_avg = (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]) / 6.0;
321            let variance = (k1[i] - k_avg).powi(2)
322                + (k2[i] - k_avg).powi(2)
323                + (k3[i] - k_avg).powi(2)
324                + (k4[i] - k_avg).powi(2);
325
326            let local_error = variance.sqrt() / k_avg.abs().max(1e-10);
327            error = error.max(local_error);
328        }
329
330        error
331    }
332
333    /// Get current state
334    pub fn get_state(&self) -> ODEState {
335        ODEState {
336            y: self.state,
337            t: self.t,
338            dt: self.dt,
339        }
340    }
341}
342
343impl ShootingMethodBVP {
344    /// Create new BVP solver
345    pub fn new(target_values: [f64; 4], config: SolverConfig) -> Self {
346        Self {
347            initial_guess: [0.0; 4],
348            target_values,
349            trajectory: [ODEState::default(); 100],
350            boundary_error: [0.0; 4],
351            config,
352            solver_state: SolverState::default(),
353        }
354    }
355
356    /// Solve boundary value problem using shooting method
357    pub fn solve<F>(
358        &mut self,
359        f: &F,
360        t0: f64,
361        t1: f64,
362        initial_guess: [f64; 4],
363    ) -> SolverResult<BVPState>
364    where
365        F: BVPFunction,
366    {
367        if !t0.is_finite()
368            || !t1.is_finite()
369            || t1 <= t0
370            || initial_guess.iter().any(|value| !value.is_finite())
371            || self.target_values.iter().any(|value| !value.is_finite())
372            || !self.config.tolerance.is_finite()
373            || self.config.tolerance < 0.0
374        {
375            return Err(SolversError::InvalidParameters);
376        }
377
378        self.initial_guess = initial_guess;
379        self.solver_state.iteration = 0;
380        self.solver_state.converged = false;
381
382        while self.solver_state.iteration < self.config.max_iterations {
383            // Shoot from initial boundary
384            self.shoot_trajectory(f, t0, t1)?;
385
386            // Calculate boundary error
387            self.calculate_boundary_error(f)?;
388            self.solver_state.iteration += 1;
389
390            // Check convergence
391            let max_error = self
392                .boundary_error
393                .iter()
394                .fold(0.0_f64, |acc, &x| acc.max(x.abs()));
395            self.solver_state.error = max_error;
396
397            if max_error < self.config.tolerance {
398                self.solver_state.converged = true;
399                return Ok(BVPState {
400                    iteration: self.solver_state.iteration,
401                    boundary_error: self.solver_state.error,
402                    converged: true,
403                    shooting_params: self.initial_guess,
404                });
405            }
406
407            // Update initial guess with a finite-difference Newton step.
408            self.update_shooting_parameters(f, t0, t1)?;
409        }
410
411        Err(SolversError::ConvergenceFailed)
412    }
413
414    /// Shoot trajectory from initial to final boundary
415    fn shoot_trajectory<F>(&mut self, f: &F, t0: f64, t1: f64) -> SolverResult<()>
416    where
417        F: BVPFunction,
418    {
419        struct BvpOdeAdapter<'a, G: BVPFunction> {
420            function: &'a G,
421            params: [f64; 4],
422        }
423        impl<G: BVPFunction> ODEFunction for BvpOdeAdapter<'_, G> {
424            fn derivatives(&self, t: f64, y: &[f64; 4]) -> [f64; 4] {
425                self.function.derivatives(t, y, &self.params)
426            }
427        }
428
429        let dt = (t1 - t0) / 99.0;
430        let mut rk4 = RungeKutta4Static::new(dt, self.config);
431        rk4.state = self.initial_guess;
432        rk4.t = t0;
433        self.trajectory[0] = rk4.get_state();
434
435        let ode_f = BvpOdeAdapter {
436            function: f,
437            params: self.initial_guess,
438        };
439        for index in 1..self.trajectory.len() {
440            rk4.rk4_step(&ode_f, dt)?;
441            self.trajectory[index] = rk4.get_state();
442        }
443
444        Ok(())
445    }
446
447    /// Calculate boundary error
448    fn calculate_boundary_error<F>(&mut self, f: &F) -> SolverResult<()>
449    where
450        F: BVPFunction,
451    {
452        // Get initial and final states
453        let initial_state = &self.trajectory[0];
454        let final_state = &self.trajectory[99];
455
456        // Calculate boundary residuals
457        let residual = f.boundary_residuals(&initial_state.y, &final_state.y);
458        for (index, value) in residual.iter().enumerate() {
459            self.boundary_error[index] = *value - self.target_values[index];
460            if !self.boundary_error[index].is_finite() {
461                return Err(SolversError::ComputationError);
462            }
463        }
464
465        Ok(())
466    }
467
468    /// Update shooting parameters with a finite-difference Newton solve.
469    fn update_shooting_parameters<F>(&mut self, f: &F, t0: f64, t1: f64) -> SolverResult<()>
470    where
471        F: BVPFunction,
472    {
473        let base_guess = self.initial_guess;
474        let base_residual = self.boundary_error;
475        let mut jacobian = [[0.0_f64; 4]; 4];
476
477        for column in 0..4 {
478            let mut perturbed = base_guess;
479            let delta = f64::EPSILON.sqrt() * (1.0 + base_guess[column].abs());
480            perturbed[column] += delta;
481            self.initial_guess = perturbed;
482            self.shoot_trajectory(f, t0, t1)?;
483            self.calculate_boundary_error(f)?;
484            for row in 0..4 {
485                jacobian[row][column] = (self.boundary_error[row] - base_residual[row]) / delta;
486            }
487        }
488        self.initial_guess = base_guess;
489        self.boundary_error = base_residual;
490
491        let rhs = base_residual.map(|value| -value);
492        let correction = solve_4x4(jacobian, rhs)?;
493        for index in 0..4 {
494            self.initial_guess[index] += correction[index];
495            if !self.initial_guess[index].is_finite() {
496                return Err(SolversError::ComputationError);
497            }
498        }
499
500        Ok(())
501    }
502
503    /// Get shooting parameters
504    pub fn get_shooting_params(&self) -> [f64; 4] {
505        self.initial_guess
506    }
507}
508
509fn solve_4x4(mut matrix: [[f64; 4]; 4], mut rhs: [f64; 4]) -> SolverResult<[f64; 4]> {
510    for pivot_column in 0..4 {
511        let mut pivot_row = pivot_column;
512        let mut pivot_magnitude = matrix[pivot_row][pivot_column].abs();
513        for candidate in pivot_column + 1..4 {
514            let magnitude = matrix[candidate][pivot_column].abs();
515            if magnitude > pivot_magnitude {
516                pivot_row = candidate;
517                pivot_magnitude = magnitude;
518            }
519        }
520        if !pivot_magnitude.is_finite() || pivot_magnitude <= 64.0 * f64::EPSILON {
521            return Err(SolversError::SingularMatrix);
522        }
523        if pivot_row != pivot_column {
524            matrix.swap(pivot_row, pivot_column);
525            rhs.swap(pivot_row, pivot_column);
526        }
527
528        for row in pivot_column + 1..4 {
529            let factor = matrix[row][pivot_column] / matrix[pivot_column][pivot_column];
530            matrix[row][pivot_column] = 0.0;
531            for column in pivot_column + 1..4 {
532                matrix[row][column] -= factor * matrix[pivot_column][column];
533            }
534            rhs[row] -= factor * rhs[pivot_column];
535        }
536    }
537
538    let mut solution = [0.0_f64; 4];
539    for row in (0..4).rev() {
540        let mut value = rhs[row];
541        for column in row + 1..4 {
542            value -= matrix[row][column] * solution[column];
543        }
544        solution[row] = value / matrix[row][row];
545        if !solution[row].is_finite() {
546            return Err(SolversError::ComputationError);
547        }
548    }
549    Ok(solution)
550}
551
552impl SimpsonsIntegratorChunked {
553    /// Create new Simpson's integrator
554    pub fn new(a: f64, b: f64, config: SolverConfig) -> Self {
555        Self {
556            chunk_buffer: [0.0; 12],
557            chunk_index: 0,
558            accumulated_integral: 0.0,
559            a,
560            b,
561            chunks_processed: 0,
562            config,
563            solver_state: SolverState::default(),
564        }
565    }
566
567    /// Integrate function using chunked Simpson's rule
568    pub fn integrate<F>(&mut self, f: &F) -> SolverResult<f64>
569    where
570        F: IntegrandFunction,
571    {
572        self.accumulated_integral = 0.0;
573        self.chunk_index = 0;
574        self.chunks_processed = 0;
575        self.solver_state.iteration = 0;
576
577        let total_length = self.b - self.a;
578        let chunk_size = total_length / 100.0; // 100 chunks
579
580        while self.chunk_index < 100 && self.solver_state.iteration < self.config.max_iterations {
581            let chunk_start = self.a + self.chunk_index as f64 * chunk_size;
582            let chunk_end = chunk_start + chunk_size;
583
584            // Process chunk
585            let chunk_integral = self.process_chunk(f, chunk_start, chunk_end)?;
586            self.accumulated_integral += chunk_integral;
587
588            self.chunk_index += 1;
589            self.chunks_processed += 1;
590            self.solver_state.iteration += 1;
591        }
592
593        self.solver_state.converged = self.chunk_index >= 100;
594        self.solver_state.error = self.estimate_integration_error();
595
596        Ok(self.accumulated_integral)
597    }
598
599    /// Process a single chunk of the integral
600    fn process_chunk<F>(&mut self, f: &F, x_start: f64, x_end: f64) -> SolverResult<f64>
601    where
602        F: IntegrandFunction,
603    {
604        // Ten panels require eleven samples. The former twelve-sample stencil
605        // applied Simpson 1/3 to eleven panels, which is mathematically invalid.
606        const SAMPLES: usize = 11;
607        let h = (x_end - x_start) / (SAMPLES - 1) as f64;
608
609        for i in 0..SAMPLES {
610            let x = x_start + i as f64 * h;
611            self.chunk_buffer[i] = f.evaluate(x);
612        }
613
614        // Apply Simpson's rule to chunk
615        let mut chunk_integral = self.chunk_buffer[0] + self.chunk_buffer[SAMPLES - 1];
616
617        for i in 1..SAMPLES - 1 {
618            let weight = if i % 2 == 1 { 4.0 } else { 2.0 };
619            chunk_integral += weight * self.chunk_buffer[i];
620        }
621
622        chunk_integral *= h / 3.0;
623
624        Ok(chunk_integral)
625    }
626
627    /// Estimate integration error
628    fn estimate_integration_error(&self) -> f64 {
629        // Simple error estimate based on remaining chunks
630        if self.chunk_index < 100 {
631            let remaining_chunks = 100 - self.chunk_index;
632            remaining_chunks as f64 / 100.0
633        } else {
634            0.0
635        }
636    }
637
638    /// Get current integral value
639    pub fn get_integral(&self) -> f64 {
640        self.accumulated_integral
641    }
642}
643
644impl Default for ODEState {
645    fn default() -> Self {
646        Self {
647            y: [0.0; 4],
648            t: 0.0,
649            dt: 0.01,
650        }
651    }
652}
653
654impl Default for BVPState {
655    fn default() -> Self {
656        Self {
657            iteration: 0,
658            boundary_error: f64::MAX,
659            converged: false,
660            shooting_params: [0.0; 4],
661        }
662    }
663}
664
665impl Default for IntegralChunk {
666    fn default() -> Self {
667        Self {
668            values: [0.0; 12],
669            x_coords: [0.0; 12],
670            contribution: 0.0,
671        }
672    }
673}
674
675impl Default for RungeKutta4Static {
676    fn default() -> Self {
677        Self::new(0.01, SolverConfig::default())
678    }
679}
680
681impl Default for ShootingMethodBVP {
682    fn default() -> Self {
683        Self::new([0.0; 4], SolverConfig::default())
684    }
685}
686
687impl Default for SimpsonsIntegratorChunked {
688    fn default() -> Self {
689        Self::new(0.0, 1.0, SolverConfig::default())
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    // Test ODE: dy/dt = -y (exponential decay)
698    struct ExponentialDecay;
699
700    impl ODEFunction for ExponentialDecay {
701        fn derivatives(&self, _t: f64, y: &[f64; 4]) -> [f64; 4] {
702            [-y[0], -y[1], -y[2], -y[3]]
703        }
704    }
705
706    #[test]
707    fn test_rk4_static() {
708        let mut rk4 = RungeKutta4Static::new(0.1, SolverConfig::default());
709        let decay = ExponentialDecay;
710
711        let result = rk4.integrate(&decay, 0.0, [1.0; 4], 1.0);
712        assert!(result.is_ok());
713
714        let state = result.unwrap();
715        // After t=1, y = e^-1 ≈ 0.3679
716        assert!((state.y[0] - 0.3679).abs() < 0.01);
717        assert_eq!(state.t, 1.0);
718    }
719
720    #[test]
721    fn final_time_integration_does_not_stop_at_a_steady_state() {
722        struct Stationary;
723        impl ODEFunction for Stationary {
724            fn derivatives(&self, _t: f64, _y: &[f64; 4]) -> [f64; 4] {
725                [0.0; 4]
726            }
727        }
728
729        let mut rk4 = RungeKutta4Static::new(0.1, SolverConfig::default());
730        let state = rk4.integrate(&Stationary, 0.0, [2.0; 4], 1.0).unwrap();
731        assert_eq!(state.t, 1.0);
732        assert_eq!(state.y, [2.0; 4]);
733
734        let steady = rk4
735            .integrate_until_steady_state(&Stationary, 0.0, [2.0; 4], 1.0)
736            .unwrap();
737        assert_eq!(steady.t, 0.0);
738    }
739
740    #[test]
741    fn static_rk4_rejects_invalid_domains_and_iteration_exhaustion() {
742        let mut invalid = RungeKutta4Static::new(0.0, SolverConfig::default());
743        assert_eq!(
744            invalid.integrate(&ExponentialDecay, 0.0, [1.0; 4], 1.0),
745            Err(SolversError::InvalidParameters)
746        );
747
748        let mut config = SolverConfig::default();
749        config.max_iterations = 1;
750        let mut exhausted = RungeKutta4Static::new(0.1, config);
751        assert_eq!(
752            exhausted.integrate(&ExponentialDecay, 0.0, [1.0; 4], 1.0),
753            Err(SolversError::ConvergenceFailed)
754        );
755    }
756
757    #[test]
758    fn test_shooting_method_bvp() {
759        let mut bvp = ShootingMethodBVP::new([0.0; 4], SolverConfig::default());
760
761        // Simple BVP: dy/dt = -y, with y(0)=1, y(1)=e^-1
762        struct DecayBVP;
763
764        impl BVPFunction for DecayBVP {
765            fn derivatives(&self, _t: f64, y: &[f64; 4], _params: &[f64; 4]) -> [f64; 4] {
766                [-y[0], -y[1], -y[2], -y[3]]
767            }
768
769            fn boundary_residuals(&self, y0: &[f64; 4], y1: &[f64; 4]) -> [f64; 4] {
770                [y0[0] - 1.0, y1[0] - (-1.0_f64).exp(), 0.0, 0.0]
771            }
772        }
773
774        let decay = DecayBVP;
775        let result = bvp.solve(&decay, 0.0, 1.0, [1.0, 0.0, 0.0, 0.0]);
776        assert!(result.is_ok());
777
778        let state = result.unwrap();
779        assert!(state.iteration > 0);
780        assert!(state.converged);
781        assert_eq!(bvp.trajectory[0].y, [1.0, 0.0, 0.0, 0.0]);
782        assert!((bvp.trajectory[99].t - 1.0).abs() < 1e-12);
783    }
784
785    #[test]
786    fn shooting_method_uses_a_real_newton_correction() {
787        struct ConstantBvp;
788        impl BVPFunction for ConstantBvp {
789            fn derivatives(&self, _t: f64, _y: &[f64; 4], _params: &[f64; 4]) -> [f64; 4] {
790                [0.0; 4]
791            }
792
793            fn boundary_residuals(&self, y0: &[f64; 4], _y1: &[f64; 4]) -> [f64; 4] {
794                *y0
795            }
796        }
797
798        let target = [1.0, -2.0, 3.5, 4.25];
799        let mut bvp = ShootingMethodBVP::new(target, SolverConfig::default());
800        let result = bvp.solve(&ConstantBvp, 0.0, 1.0, [0.0; 4]).unwrap();
801        assert!(result.converged);
802        for (actual, expected) in result.shooting_params.iter().zip(target) {
803            assert!((actual - expected).abs() < 1e-10);
804        }
805    }
806
807    #[test]
808    fn test_simpsons_integrator_chunked() {
809        let mut integrator =
810            SimpsonsIntegratorChunked::new(0.0, std::f64::consts::PI, SolverConfig::default());
811
812        // Integrate sin(x) from 0 to π
813        struct SinFunction;
814
815        impl IntegrandFunction for SinFunction {
816            fn evaluate(&self, x: f64) -> f64 {
817                x.sin()
818            }
819        }
820
821        let sin_func = SinFunction;
822        let result = integrator.integrate(&sin_func);
823        assert!(result.is_ok());
824
825        let integral = result.unwrap();
826        println!("INTEGRAL VALUE: {}", integral);
827        // ∫₀^π sin(x) dx = 2.0
828        assert!((integral - 2.0).abs() < 0.1);
829    }
830
831    #[test]
832    fn test_zero_allocation_guarantee() {
833        // assert_eq!(core::mem::size_of::<RungeKutta4Static>(), ...);
834        // assert_eq!(core::mem::size_of::<ShootingMethodBVP>(), ...);
835        // assert_eq!(core::mem::size_of::<SimpsonsIntegratorChunked>(), ...);
836    }
837}