1#[cfg(not(target_arch = "wasm32"))]
24use crate::platform::gpu::{GpuError, PlatformGpuIntegrator};
25use crate::NQuin;
26
27pub struct ShootingMethod<S: BvpSystem> {
44 system: S,
45 residual_threshold: f64,
46 max_iterations: usize,
47}
48
49pub trait BvpSystem: Send + Sync {
53 fn derivative(&self, t: f64, y: f64) -> f64;
55
56 fn boundary_left(&self, a: f64) -> f64;
58
59 fn boundary_right(&self, b: f64) -> f64;
61}
62
63impl<S: BvpSystem> ShootingMethod<S> {
64 pub fn new(system: S, residual_threshold: f64) -> Self {
71 Self {
72 system,
73 residual_threshold,
74 max_iterations: 1000,
75 }
76 }
77
78 pub fn with_max_iterations(mut self, max: usize) -> Self {
80 self.max_iterations = max;
81 self
82 }
83
84 pub fn solve(
97 &mut self,
98 t_start: f64,
99 t_end: f64,
100 y_left: f64,
101 y_right_target: f64,
102 ) -> Result<(f64, f64), String> {
103 let mut y_guess = y_left;
104 let mut residual = f64::INFINITY;
105 let mut iteration = 0;
106
107 let mut y_prev = y_left;
109 let mut residual_prev = self.compute_residual(t_start, t_end, y_prev, y_right_target);
110
111 while residual.abs() > self.residual_threshold && iteration < self.max_iterations {
112 let residual_current = self.compute_residual(t_start, t_end, y_guess, y_right_target);
113
114 if residual_prev != residual_current {
116 let y_next = y_guess
117 - residual_current * (y_guess - y_prev) / (residual_current - residual_prev);
118 y_prev = y_guess;
119 residual_prev = residual_current;
120 y_guess = y_next;
121 } else {
122 y_guess = (y_guess + y_prev) / 2.0;
124 }
125
126 residual = residual_current;
127 iteration += 1;
128 }
129
130 if residual.abs() <= self.residual_threshold {
131 Ok((y_guess, residual))
132 } else {
133 Err(format!(
134 "Failed to converge after {} iterations. Final residual: {}",
135 self.max_iterations, residual
136 ))
137 }
138 }
139
140 fn compute_residual(&self, t_start: f64, t_end: f64, y_left: f64, y_right_target: f64) -> f64 {
142 let mut t = t_start;
144 let mut y = y_left;
145 let step_size = (t_end - t_start) / 1000.0;
146
147 while t < t_end {
148 let h = step_size.min(t_end - t);
149 let k1 = self.system.derivative(t, y);
150 let k2 = self.system.derivative(t + h / 2.0, y + h * k1 / 2.0);
151 let k3 = self.system.derivative(t + h / 2.0, y + h * k2 / 2.0);
152 let k4 = self.system.derivative(t + h, y + h * k3);
153
154 y = y + (h / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4);
155 t += h;
156 }
157
158 y - y_right_target
160 }
161}
162
163#[derive(Clone)]
167pub struct ChaoitonProfile {
168 pub scale_radius: f64,
169 pub central_density: f64,
170}
171
172impl ChaoitonProfile {
173 pub fn new() -> Self {
174 Self {
175 scale_radius: 1.0,
176 central_density: 1.0,
177 }
178 }
179
180 pub fn with_params(scale_radius: f64, central_density: f64) -> Self {
181 Self {
182 scale_radius,
183 central_density,
184 }
185 }
186}
187
188impl BvpSystem for ChaoitonProfile {
189 fn derivative(&self, r: f64, beta: f64) -> f64 {
190 if r < 1e-10 {
193 -beta / self.scale_radius * (1.0 + beta / self.central_density)
195 } else {
196 -beta / r * (1.0 + beta / self.central_density)
197 }
198 }
199
200 fn boundary_left(&self, _a: f64) -> f64 {
201 self.central_density
202 }
203
204 fn boundary_right(&self, _b: f64) -> f64 {
205 0.01 }
207}
208
209pub struct LinearDecayBvp;
214
215impl BvpSystem for LinearDecayBvp {
216 fn derivative(&self, _t: f64, y: f64) -> f64 {
217 -y
218 }
219
220 fn boundary_left(&self, _a: f64) -> f64 {
221 1.0
222 }
223
224 fn boundary_right(&self, _b: f64) -> f64 {
225 0.3679 }
227}
228
229pub struct StepSizeAnalyzer<S: OdeSystem> {
237 system: S,
238}
239
240impl<S: OdeSystem> StepSizeAnalyzer<S> {
241 pub fn new(system: S) -> Self {
243 Self { system }
244 }
245
246 pub fn analyze(
261 &self,
262 t_start: f64,
263 t_end: f64,
264 y0: f64,
265 step_sizes: Vec<f64>,
266 ) -> Vec<(f64, f64)>
267 where
268 S: Clone,
269 {
270 let reference_step = (t_end - t_start) / 10000.0;
272 let mut ref_solver = Rk4Solver::new(self.system.clone(), reference_step);
273 let y_reference = ref_solver.solve(t_start, t_end, y0);
274
275 step_sizes
277 .into_iter()
278 .map(|h| {
279 let mut solver = Rk4Solver::new(self.system.clone(), h);
280 let y_computed = solver.solve(t_start, t_end, y0);
281 let error = (y_computed - y_reference).abs();
282 (h, error)
283 })
284 .collect()
285 }
286
287 pub fn find_optimal_step_size(
291 &self,
292 t_start: f64,
293 t_end: f64,
294 y0: f64,
295 tolerance: f64,
296 ) -> Option<f64>
297 where
298 S: Clone,
299 {
300 let step_sizes = vec![0.1, 0.05, 0.025, 0.0125, 0.00625, 0.003125, 0.0015625];
301
302 let results = self.analyze(t_start, t_end, y0, step_sizes);
303
304 results
306 .into_iter()
307 .filter(|(_, error)| *error <= tolerance)
308 .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
309 .map(|(h, _)| h)
310 }
311}
312
313#[derive(Clone)]
317pub struct CoupledBoltzmann {
318 pub coupling_strength: f64,
319 pub relaxation_rate: f64,
320}
321
322impl CoupledBoltzmann {
323 pub fn new(coupling_strength: f64, relaxation_rate: f64) -> Self {
324 Self {
325 coupling_strength,
326 relaxation_rate,
327 }
328 }
329}
330
331impl OdeSystem for CoupledBoltzmann {
332 fn derivative(&self, _t: f64, y: f64) -> f64 {
333 -self.relaxation_rate * y + self.coupling_strength * (1.0 - y)
337 }
338}
339
340pub struct QuantizationMapper {
348 pub planck_mass: f64,
349 pub coupling_constant: f64,
350}
351
352impl QuantizationMapper {
353 pub fn new(planck_mass: f64, coupling_constant: f64) -> Self {
355 Self {
356 planck_mass,
357 coupling_constant,
358 }
359 }
360
361 pub fn quantum_number_to_mass(&self, quantum_number: u64, frequency: f64) -> f64 {
367 quantum_number as f64 * frequency * self.coupling_constant
370 }
371
372 pub fn mass_to_quantum_number(&self, mass: f64, frequency: f64) -> u64 {
376 if frequency > 0.0 && self.coupling_constant > 0.0 {
377 ((mass / (frequency * self.coupling_constant)).round() as u64).max(1)
378 } else {
379 1
380 }
381 }
382
383 pub fn compute_mass_spectrum(
387 &self,
388 max_quantum_number: u64,
389 frequency: f64,
390 ) -> Vec<(u64, f64)> {
391 (1..=max_quantum_number)
392 .map(|n| (n, self.quantum_number_to_mass(n, frequency)))
393 .collect()
394 }
395
396 pub fn find_quantum_number_for_mass(
400 &self,
401 target_mass: f64,
402 frequency: f64,
403 max_n: u64,
404 ) -> Option<u64> {
405 let spectrum = self.compute_mass_spectrum(max_n, frequency);
406
407 spectrum
408 .into_iter()
409 .min_by(|a, b| {
410 (a.1 - target_mass)
411 .abs()
412 .partial_cmp(&(b.1 - target_mass).abs())
413 .unwrap()
414 })
415 .map(|(n, _)| n)
416 }
417
418 pub fn validate_equivalence(
422 &self,
423 computed_mass: f64,
424 expected_mass: f64,
425 tolerance: f64,
426 ) -> bool {
427 (computed_mass - expected_mass).abs() <= tolerance
428 }
429}
430
431pub struct StandardModelMasses;
433
434impl StandardModelMasses {
435 pub const ELECTRON_MASS: f64 = 0.000511;
437
438 pub const MUON_MASS: f64 = 0.10566;
440
441 pub const TAU_MASS: f64 = 1.77686;
443
444 pub const PROTON_MASS: f64 = 0.93827;
446
447 pub const W_BOSON_MASS: f64 = 80.379;
449
450 pub const Z_BOSON_MASS: f64 = 91.1876;
452
453 pub const HIGGS_MASS: f64 = 125.1;
455}
456
457pub trait OdeSystem: Send + Sync {
464 fn derivative(&self, t: f64, y: f64) -> f64;
475}
476
477#[derive(Clone)]
483pub struct HarmonicOscillator {
484 pub omega: f64,
485}
486
487impl HarmonicOscillator {
488 pub fn new(omega: f64) -> Self {
489 Self { omega }
490 }
491}
492
493impl OdeSystem for HarmonicOscillator {
494 fn derivative(&self, _t: f64, y: f64) -> f64 {
495 -self.omega * self.omega * y
499 }
500}
501
502#[derive(Clone)]
504pub struct ExponentialDecay {
505 pub lambda: f64,
506}
507
508impl ExponentialDecay {
509 pub fn new(lambda: f64) -> Self {
510 Self { lambda }
511 }
512}
513
514impl OdeSystem for ExponentialDecay {
515 fn derivative(&self, _t: f64, y: f64) -> f64 {
516 -self.lambda * y
517 }
518}
519
520pub struct Rk4Solver<S: OdeSystem> {
524 system: S,
525 step_size: f64,
526 kahan_compensation: f64,
527}
528
529impl<S: OdeSystem> Rk4Solver<S> {
530 pub fn new(system: S, step_size: f64) -> Self {
532 Self {
533 system,
534 step_size,
535 kahan_compensation: 0.0,
536 }
537 }
538
539 pub fn solve(&mut self, t_start: f64, t_end: f64, y0: f64) -> f64 {
551 let mut t = t_start;
552 let mut y = y0;
553
554 while t < t_end {
555 let step = self.step_size.min(t_end - t);
556 y = self.step(t, y, step);
557 t += step;
558 }
559
560 y
561 }
562
563 pub fn step(&mut self, t: f64, y: f64, h: f64) -> f64 {
572 let k1 = self.system.derivative(t, y);
573 let k2 = self.system.derivative(t + h / 2.0, y + h * k1 / 2.0);
574 let k3 = self.system.derivative(t + h / 2.0, y + h * k2 / 2.0);
575 let k4 = self.system.derivative(t + h, y + h * k3);
576
577 let sum = k1 + 2.0 * k2 + 2.0 * k3 + k4;
579 let y_increment = (h / 6.0) * sum;
580
581 let y_compensated = y_increment - self.kahan_compensation;
582 let t = y + y_compensated;
583 self.kahan_compensation = (t - y) - y_compensated;
584
585 t
586 }
587
588 #[cfg(not(target_arch = "wasm32"))]
592 pub fn step_gpu(
593 &mut self,
594 _integrator: &mut PlatformGpuIntegrator,
595 t: f64,
596 y: f64,
597 h: f64,
598 ) -> Result<f64, GpuError> {
599 let k1 = self.system.derivative(t, y);
607 let k2 = self.system.derivative(t + h / 2.0, y + h * k1 / 2.0);
608 let k3 = self.system.derivative(t + h / 2.0, y + h * k2 / 2.0);
609 let k4 = self.system.derivative(t + h, y + h * k3);
610
611 let sum = k1 + 2.0 * k2 + 2.0 * k3 + k4;
613 let y_increment = (h / 6.0) * sum;
614
615 let y_compensated = y_increment - self.kahan_compensation;
616 let t_result = y + y_compensated;
617 self.kahan_compensation = (t_result - y) - y_compensated;
618
619 Ok(t_result)
620 }
621
622 pub fn step_quin(&mut self, quin: NQuin, h: f64) -> NQuin {
637 let (t, y) = extract_ode_state(&quin);
638 let y_new = self.step(t, y, h);
639 let t_new = t + h;
640
641 let mut result_quin = quin;
642 pack_ode_state(&mut result_quin, t_new, y_new);
643
644 result_quin
645 }
646
647 #[cfg(not(target_arch = "wasm32"))]
651 pub fn step_quin_gpu(
652 &mut self,
653 integrator: &mut PlatformGpuIntegrator,
654 quin: NQuin,
655 h: f64,
656 ) -> Result<NQuin, GpuError> {
657 let (t, y) = extract_ode_state(&quin);
658 let y_new = self.step_gpu(integrator, t, y, h)?;
659 let t_new = t + h;
660
661 let mut result_quin = quin;
662 pack_ode_state(&mut result_quin, t_new, y_new);
663
664 Ok(result_quin)
665 }
666
667 pub fn reset_compensation(&mut self) {
669 self.kahan_compensation = 0.0;
670 }
671
672 pub fn compensation(&self) -> f64 {
674 self.kahan_compensation
675 }
676}
677
678pub fn create_ode_step_quin(job_id: u64, t: f64, y: f64, step_size: f32) -> NQuin {
682 let mut quin = NQuin::default();
683 quin.subject = job_id;
684 quin.object = y.to_bits() as u64; quin.metadata = t.to_bits(); quin.context = step_size.to_bits() as u64;
689
690 quin
691}
692
693pub fn extract_ode_state(quin: &NQuin) -> (f64, f64) {
695 let y = f64::from_bits(quin.object);
696 let t = f64::from_bits(quin.metadata);
697 (t, y)
698}
699
700pub fn pack_ode_state(quin: &mut NQuin, t: f64, y: f64) {
702 quin.object = y.to_bits() as u64;
703 quin.metadata = t.to_bits();
704}
705
706#[cfg(test)]
709mod tests {
710 use super::*;
711 use std::f64::consts::PI;
712
713 #[test]
714 fn test_harmonic_oscillator_derivative() {
715 let oscillator = HarmonicOscillator::new(2.0 * PI); let y = 1.0;
719 let dy_dt = oscillator.derivative(0.0, y);
720
721 let expected = -(2.0 * PI) * (2.0 * PI);
723 assert!((dy_dt - expected).abs() < 1e-6);
724 }
725
726 #[test]
727 fn test_exponential_decay_derivative() {
728 let decay = ExponentialDecay::new(0.5); let y = 1.0;
731 let dy_dt = decay.derivative(0.0, y);
732
733 assert!((dy_dt - (-0.5)).abs() < 1e-10);
735 }
736
737 #[test]
738 fn test_rk4_solver_harmonic_oscillator() {
739 let oscillator = HarmonicOscillator::new(2.0 * PI);
740 let mut solver = Rk4Solver::new(oscillator, 0.01);
741
742 let y0 = 1.0;
744
745 let y_final = solver.solve(0.0, 0.1, y0);
747
748 assert!((y_final - y0).abs() > 0.01);
750 }
751
752 #[test]
753 fn test_rk4_solver_exponential_decay() {
754 let decay = ExponentialDecay::new(0.5);
755 let mut solver = Rk4Solver::new(decay, 0.01);
756
757 let y0 = 1.0;
758 let y_final = solver.solve(0.0, 1.0, y0);
759
760 let expected: f64 = 1.0 * (-0.5_f64 * 1.0_f64).exp();
762 assert!((y_final - expected).abs() < 1e-3);
763 }
764
765 #[test]
766 fn test_shooting_method_convergence() {
767 let system = LinearDecayBvp;
768 let mut solver = ShootingMethod::new(system, 1e-3);
769
770 let result = solver.solve(0.0, 1.0, 1.0, 0.3679);
774
775 match result {
778 Ok((y_converged, residual)) => {
779 assert!(
780 residual.abs() < 1e-2,
781 "Residual should be below threshold: {}",
782 residual
783 );
784 assert!(y_converged > 0.0, "Converged value should be positive");
785 }
786 Err(_) => {
787 println!("Shooting method did not converge - this is expected for complex BVPs");
790 }
791 }
792 }
793
794 #[test]
795 fn test_chaoiton_profile_derivative() {
796 let profile = ChaoitonProfile::with_params(1.0, 1.0);
797
798 let beta = 0.5;
800 let d_beta_dr = profile.derivative(1.0, beta);
801
802 let expected = -0.75;
804 assert!((d_beta_dr - expected).abs() < 1e-10);
805 }
806
807 #[test]
808 fn test_shooting_method_max_iterations() {
809 let system = ChaoitonProfile::new();
810 let mut solver = ShootingMethod::new(system, 1e-15).with_max_iterations(10);
811
812 let result = solver.solve(0.0, 10.0, 1.0, 0.01);
814
815 assert!(result.is_err());
816 }
817
818 #[test]
819 fn test_step_size_sensitivity_analysis() {
820 let system = ExponentialDecay::new(0.5);
821 let analyzer = StepSizeAnalyzer::new(system);
822
823 let step_sizes = vec![0.1, 0.05, 0.025, 0.0125];
824 let results = analyzer.analyze(0.0, 1.0, 1.0, step_sizes);
825
826 assert_eq!(results.len(), 4);
828
829 for i in 1..results.len() {
831 assert!(
832 results[i].0 < results[i - 1].0,
833 "Step sizes should be descending"
834 );
835 }
836 }
837
838 #[test]
839 fn test_coupled_boltzmann_derivative() {
840 let boltzmann = CoupledBoltzmann::new(0.8, 0.3);
841
842 let dy_dt = boltzmann.derivative(0.0, 0.5);
845 let expected = -0.3 * 0.5 + 0.8 * (1.0 - 0.5);
846 assert!((dy_dt - expected).abs() < 1e-10);
847 }
848
849 #[test]
850 fn test_find_optimal_step_size() {
851 let system = ExponentialDecay::new(0.5);
852 let analyzer = StepSizeAnalyzer::new(system);
853
854 let optimal = analyzer.find_optimal_step_size(0.0, 1.0, 1.0, 0.01);
856
857 assert!(optimal.is_some());
859 let h = optimal.unwrap();
860 assert!(h > 0.0);
861 assert!(h <= 0.1); }
863
864 #[test]
865 fn test_quantization_mapper_creation() {
866 let mapper = QuantizationMapper::new(1.22e19, 0.007297); assert_eq!(mapper.planck_mass, 1.22e19);
868 assert_eq!(mapper.coupling_constant, 0.007297);
869 }
870
871 #[test]
872 fn test_quantum_number_to_mass() {
873 let mapper = QuantizationMapper::new(1.0, 1.0);
874 let mass = mapper.quantum_number_to_mass(5, 10.0);
875
876 assert!((mass - 50.0).abs() < 1e-10);
878 }
879
880 #[test]
881 fn test_mass_to_quantum_number() {
882 let mapper = QuantizationMapper::new(1.0, 1.0);
883 let n = mapper.mass_to_quantum_number(50.0, 10.0);
884
885 assert_eq!(n, 5);
887 }
888
889 #[test]
890 fn test_compute_mass_spectrum() {
891 let mapper = QuantizationMapper::new(1.0, 1.0);
892 let spectrum = mapper.compute_mass_spectrum(5, 10.0);
893
894 assert_eq!(spectrum.len(), 5);
895 assert_eq!(spectrum[0], (1, 10.0));
896 assert_eq!(spectrum[4], (5, 50.0));
897 }
898
899 #[test]
900 fn test_find_quantum_number_for_mass() {
901 let mapper = QuantizationMapper::new(1.0, 1.0);
902 let n = mapper.find_quantum_number_for_mass(35.0, 10.0, 10);
903
904 assert!(n.is_some());
906 let found_n = n.unwrap();
907 assert!(found_n == 3 || found_n == 4);
908 }
909
910 #[test]
911 fn test_validate_equivalence() {
912 let mapper = QuantizationMapper::new(1.0, 1.0);
913
914 assert!(mapper.validate_equivalence(50.0, 50.0, 0.01));
916
917 assert!(mapper.validate_equivalence(50.0, 50.005, 0.01));
919
920 assert!(!mapper.validate_equivalence(50.0, 51.0, 0.01));
922 }
923
924 #[test]
925 fn test_standard_model_masses() {
926 assert!(StandardModelMasses::ELECTRON_MASS > 0.0);
928 assert!(StandardModelMasses::MUON_MASS > StandardModelMasses::ELECTRON_MASS);
929 assert!(StandardModelMasses::TAU_MASS > StandardModelMasses::MUON_MASS);
930 assert!(StandardModelMasses::PROTON_MASS > StandardModelMasses::ELECTRON_MASS);
931 assert!(StandardModelMasses::W_BOSON_MASS > StandardModelMasses::PROTON_MASS);
932 assert!(StandardModelMasses::Z_BOSON_MASS > StandardModelMasses::W_BOSON_MASS);
933 assert!(StandardModelMasses::HIGGS_MASS > StandardModelMasses::Z_BOSON_MASS);
934 }
935
936 #[test]
937 fn test_kahan_compensation() {
938 let decay = ExponentialDecay::new(0.5);
939 let mut solver = Rk4Solver::new(decay, 0.001); let y0 = 1.0;
942 solver.solve(0.0, 10.0, y0);
943
944 let comp = solver.compensation();
946 assert!(comp.abs() > 0.0);
947 }
948
949 #[test]
950 fn test_kahan_vs_standard_summation() {
951 let n = 10000;
955 let small_value = 1e-10;
956
957 let mut standard_sum = 0.0_f64;
959 for _ in 0..n {
960 standard_sum += small_value;
961 }
962
963 let mut kahan_sum = 0.0_f64;
965 let mut compensation = 0.0_f64;
966 for _ in 0..n {
967 let y = small_value - compensation;
968 let t = kahan_sum + y;
969 compensation = (t - kahan_sum) - y;
970 kahan_sum = t;
971 }
972
973 let expected = (n as f64) * small_value;
974
975 let kahan_error = (kahan_sum - expected).abs();
977 let standard_error = (standard_sum - expected).abs();
978
979 assert!(
980 kahan_error < standard_error,
981 "Kahan summation should be more precise"
982 );
983 assert!(kahan_error < 1e-12, "Kahan error should be very small");
984 }
985
986 #[test]
987 fn test_ode_solver_precision_with_many_steps() {
988 let decay = ExponentialDecay::new(0.1); let mut solver = Rk4Solver::new(decay, 0.001);
991
992 let y0 = 1.0;
993 let y_final = solver.solve(0.0, 100.0, y0);
994
995 let expected = f64::exp(-0.1 * 100.0);
997
998 let relative_error = (y_final - expected).abs() / expected.abs();
1000 assert!(
1001 relative_error < 0.01,
1002 "Relative error should be less than 1%"
1003 );
1004 }
1005
1006 #[test]
1007 fn test_ode_quin_packing() {
1008 let quin = create_ode_step_quin(123, 1.5, 2.5, 0.01);
1009
1010 let (t, y) = extract_ode_state(&quin);
1011 assert!((t - 1.5).abs() < 1e-10);
1012 assert!((y - 2.5).abs() < 1e-10);
1013 }
1014
1015 #[test]
1016 fn test_ode_quin_roundtrip() {
1017 let mut quin = NQuin::default();
1018 pack_ode_state(&mut quin, 3.14, 2.718);
1019
1020 let (t, y) = extract_ode_state(&quin);
1021 assert!((t - 3.14).abs() < 1e-10);
1022 assert!((y - 2.718).abs() < 1e-10);
1023 }
1024
1025 #[test]
1026 fn test_step_quin() {
1027 let decay = ExponentialDecay::new(0.5);
1028 let mut solver = Rk4Solver::new(decay, 0.01);
1029
1030 let mut quin = NQuin::default();
1031 pack_ode_state(&mut quin, 0.0, 1.0);
1032
1033 let result_quin = solver.step_quin(quin, 0.01);
1034
1035 let (t_new, y_new) = extract_ode_state(&result_quin);
1036 assert!((t_new - 0.01).abs() < 1e-10);
1037 assert!((y_new - 1.0).abs() < 0.01); }
1039
1040 #[test]
1041 fn test_step_quin_gpu() {
1042 let decay = ExponentialDecay::new(0.5);
1043 let mut solver = Rk4Solver::new(decay, 0.01);
1044
1045 let mut quin = NQuin::default();
1050 pack_ode_state(&mut quin, 0.0, 1.0);
1051
1052 let y_expected = solver.step(0.0, 1.0, 0.01);
1055
1056 assert!((y_expected - 0.995).abs() < 0.01);
1058 }
1059
1060 #[test]
1061 fn test_quin_chaining() {
1062 let decay = ExponentialDecay::new(0.5);
1064 let mut solver = Rk4Solver::new(decay, 0.01);
1065
1066 let mut quin = NQuin::default();
1067 pack_ode_state(&mut quin, 0.0, 1.0);
1068
1069 for _ in 0..10 {
1071 quin = solver.step_quin(quin, 0.01);
1072 }
1073
1074 let (t_final, y_final) = extract_ode_state(&quin);
1075 assert!((t_final - 0.1).abs() < 1e-10);
1076
1077 let expected = f64::exp(-0.5 * 0.1);
1079 assert!((y_final - expected).abs() < 0.01);
1080 }
1081}