1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[doc(inline)]
7pub use approx::{approx_eq, clamp_epsilon, relative_eq};
8#[doc(inline)]
9pub use difference::{backward_difference, central_difference, forward_difference};
10#[doc(inline)]
11pub use integration::{midpoint_rule, rectangle_rule, simpsons_rule, trapezoidal_rule};
12#[cfg(feature = "interval")]
13#[doc(inline)]
14pub use root::bisection_interval;
15#[doc(inline)]
16pub use root::{RootError, RootOptions, bisection, newton_raphson};
17
18pub mod approx {
20 #[must_use]
25 pub const fn clamp_epsilon(epsilon: f64) -> f64 {
26 if epsilon.is_nan() {
27 0.0
28 } else if epsilon.is_infinite() {
29 f64::MAX
30 } else {
31 epsilon.abs()
32 }
33 }
34
35 #[must_use]
40 pub fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
41 if a.is_nan() || b.is_nan() {
42 return false;
43 }
44
45 if a == b {
46 return true;
47 }
48
49 if !a.is_finite() || !b.is_finite() {
50 return false;
51 }
52
53 (a - b).abs() <= clamp_epsilon(epsilon)
54 }
55
56 #[must_use]
62 pub fn relative_eq(a: f64, b: f64, epsilon: f64) -> bool {
63 if a.is_nan() || b.is_nan() {
64 return false;
65 }
66
67 if a == b {
68 return true;
69 }
70
71 if !a.is_finite() || !b.is_finite() {
72 return false;
73 }
74
75 let epsilon = clamp_epsilon(epsilon);
76 let scale = a.abs().max(b.abs()).max(1.0);
77
78 (a - b).abs() <= epsilon * scale
79 }
80}
81
82pub mod difference {
84 #[must_use]
89 pub fn forward_difference<F>(f: F, x: f64, h: f64) -> f64
90 where
91 F: Fn(f64) -> f64,
92 {
93 (f(x + h) - f(x)) / h
94 }
95
96 #[must_use]
101 pub fn backward_difference<F>(f: F, x: f64, h: f64) -> f64
102 where
103 F: Fn(f64) -> f64,
104 {
105 (f(x) - f(x - h)) / h
106 }
107
108 #[must_use]
114 pub fn central_difference<F>(f: F, x: f64, h: f64) -> f64
115 where
116 F: Fn(f64) -> f64,
117 {
118 (f(x + h) - f(x - h)) / (2.0 * h)
119 }
120}
121
122pub mod integration {
124 #[allow(clippy::cast_precision_loss)]
125 const fn usize_to_f64(value: usize) -> f64 {
126 value as f64
127 }
128
129 fn normalized_bounds(a: f64, b: f64) -> Option<(f64, f64, f64)> {
130 if !a.is_finite() || !b.is_finite() {
131 return None;
132 }
133
134 if a <= b {
135 Some((a, b, 1.0))
136 } else {
137 Some((b, a, -1.0))
138 }
139 }
140
141 #[must_use]
147 pub fn rectangle_rule<F>(
148 integrand: F,
149 lower_bound: f64,
150 upper_bound: f64,
151 subinterval_count: usize,
152 ) -> Option<f64>
153 where
154 F: Fn(f64) -> f64,
155 {
156 if subinterval_count == 0 {
157 return None;
158 }
159
160 let (start, end, sign) = normalized_bounds(lower_bound, upper_bound)?;
161 let step = (end - start) / usize_to_f64(subinterval_count);
162 let mut sum = 0.0;
163
164 for sample_index in 0..subinterval_count {
165 let sample_point = usize_to_f64(sample_index).mul_add(step, start);
166 let value = integrand(sample_point);
167 if !value.is_finite() {
168 return None;
169 }
170
171 sum += value;
172 }
173
174 Some(sign * sum * step)
175 }
176
177 #[must_use]
183 pub fn midpoint_rule<F>(
184 integrand: F,
185 lower_bound: f64,
186 upper_bound: f64,
187 subinterval_count: usize,
188 ) -> Option<f64>
189 where
190 F: Fn(f64) -> f64,
191 {
192 if subinterval_count == 0 {
193 return None;
194 }
195
196 let (start, end, sign) = normalized_bounds(lower_bound, upper_bound)?;
197 let step = (end - start) / usize_to_f64(subinterval_count);
198 let mut sum = 0.0;
199
200 for sample_index in 0..subinterval_count {
201 let sample_point = (usize_to_f64(sample_index) + 0.5).mul_add(step, start);
202 let value = integrand(sample_point);
203 if !value.is_finite() {
204 return None;
205 }
206
207 sum += value;
208 }
209
210 Some(sign * sum * step)
211 }
212
213 #[must_use]
219 pub fn trapezoidal_rule<F>(
220 integrand: F,
221 lower_bound: f64,
222 upper_bound: f64,
223 subinterval_count: usize,
224 ) -> Option<f64>
225 where
226 F: Fn(f64) -> f64,
227 {
228 if subinterval_count == 0 {
229 return None;
230 }
231
232 let (start, end, sign) = normalized_bounds(lower_bound, upper_bound)?;
233 let step = (end - start) / usize_to_f64(subinterval_count);
234 let start_value = integrand(start);
235 let end_value = integrand(end);
236
237 if !start_value.is_finite() || !end_value.is_finite() {
238 return None;
239 }
240
241 let mut sum = 0.5 * (start_value + end_value);
242
243 for sample_index in 1..subinterval_count {
244 let sample_point = usize_to_f64(sample_index).mul_add(step, start);
245 let value = integrand(sample_point);
246 if !value.is_finite() {
247 return None;
248 }
249
250 sum += value;
251 }
252
253 Some(sign * sum * step)
254 }
255
256 #[must_use]
262 pub fn simpsons_rule<F>(
263 integrand: F,
264 lower_bound: f64,
265 upper_bound: f64,
266 subinterval_count: usize,
267 ) -> Option<f64>
268 where
269 F: Fn(f64) -> f64,
270 {
271 if subinterval_count == 0 || !subinterval_count.is_multiple_of(2) {
272 return None;
273 }
274
275 let (start, end, sign) = normalized_bounds(lower_bound, upper_bound)?;
276 let step = (end - start) / usize_to_f64(subinterval_count);
277 let start_value = integrand(start);
278 let end_value = integrand(end);
279
280 if !start_value.is_finite() || !end_value.is_finite() {
281 return None;
282 }
283
284 let mut sum = start_value + end_value;
285
286 for sample_index in 1..subinterval_count {
287 let sample_point = usize_to_f64(sample_index).mul_add(step, start);
288 let value = integrand(sample_point);
289 if !value.is_finite() {
290 return None;
291 }
292
293 if sample_index.is_multiple_of(2) {
294 sum += 2.0 * value;
295 } else {
296 sum += 4.0 * value;
297 }
298 }
299
300 Some(sign * sum * step / 3.0)
301 }
302}
303
304pub mod root {
306 use crate::approx::approx_eq;
307
308 #[cfg(feature = "interval")]
309 use use_interval::{Bound, Interval};
310
311 #[derive(Debug, Clone, Copy, PartialEq)]
313 pub struct RootOptions {
314 pub tolerance: f64,
316 pub max_iterations: usize,
318 }
319
320 impl Default for RootOptions {
321 fn default() -> Self {
322 Self {
323 tolerance: 1e-10,
324 max_iterations: 100,
325 }
326 }
327 }
328
329 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
331 pub enum RootError {
332 InvalidInterval,
334 InvalidTolerance,
336 MaxIterationsReached,
338 NonFiniteValue,
340 ZeroDerivative,
342 }
343
344 fn validate_options(options: RootOptions) -> Result<(), RootError> {
345 if !options.tolerance.is_finite() || options.tolerance <= 0.0 {
346 Err(RootError::InvalidTolerance)
347 } else {
348 Ok(())
349 }
350 }
351
352 fn same_sign(left: f64, right: f64) -> bool {
353 (left < 0.0 && right < 0.0) || (left > 0.0 && right > 0.0)
354 }
355
356 fn bisection_with_policy<F>(
357 f: &F,
358 lower: f64,
359 upper: f64,
360 options: RootOptions,
361 allow_lower_endpoint: bool,
362 allow_upper_endpoint: bool,
363 ) -> Result<f64, RootError>
364 where
365 F: Fn(f64) -> f64,
366 {
367 validate_options(options)?;
368
369 if !lower.is_finite() || !upper.is_finite() || lower > upper {
370 return Err(RootError::InvalidInterval);
371 }
372
373 let lower_value = f(lower);
374 let upper_value = f(upper);
375
376 if !lower_value.is_finite() || !upper_value.is_finite() {
377 return Err(RootError::NonFiniteValue);
378 }
379
380 if approx_eq(lower_value, 0.0, options.tolerance) {
381 return if allow_lower_endpoint {
382 Ok(lower)
383 } else {
384 Err(RootError::InvalidInterval)
385 };
386 }
387
388 if approx_eq(upper_value, 0.0, options.tolerance) {
389 return if allow_upper_endpoint {
390 Ok(upper)
391 } else {
392 Err(RootError::InvalidInterval)
393 };
394 }
395
396 if same_sign(lower_value, upper_value) {
397 return Err(RootError::InvalidInterval);
398 }
399
400 let mut left = lower;
401 let mut right = upper;
402 let mut left_value = lower_value;
403
404 for _ in 0..options.max_iterations {
405 let midpoint = (right - left).mul_add(0.5, left);
406 let midpoint_value = f(midpoint);
407
408 if !midpoint.is_finite() || !midpoint_value.is_finite() {
409 return Err(RootError::NonFiniteValue);
410 }
411
412 if approx_eq(midpoint_value, 0.0, options.tolerance)
413 || (right - left).abs() * 0.5 <= options.tolerance
414 {
415 return Ok(midpoint);
416 }
417
418 if same_sign(left_value, midpoint_value) {
419 left = midpoint;
420 left_value = midpoint_value;
421 } else {
422 right = midpoint;
423 }
424 }
425
426 Err(RootError::MaxIterationsReached)
427 }
428
429 pub fn bisection<F>(
445 f: F,
446 lower: f64,
447 upper: f64,
448 options: RootOptions,
449 ) -> Result<f64, RootError>
450 where
451 F: Fn(f64) -> f64,
452 {
453 bisection_with_policy(&f, lower, upper, options, true, true)
454 }
455
456 pub fn newton_raphson<F, D>(
472 f: F,
473 derivative: D,
474 initial: f64,
475 options: RootOptions,
476 ) -> Result<f64, RootError>
477 where
478 F: Fn(f64) -> f64,
479 D: Fn(f64) -> f64,
480 {
481 validate_options(options)?;
482
483 if !initial.is_finite() {
484 return Err(RootError::NonFiniteValue);
485 }
486
487 let mut current = initial;
488
489 for _ in 0..options.max_iterations {
490 let value = f(current);
491 if !value.is_finite() {
492 return Err(RootError::NonFiniteValue);
493 }
494
495 if approx_eq(value, 0.0, options.tolerance) {
496 return Ok(current);
497 }
498
499 let slope = derivative(current);
500 if !slope.is_finite() {
501 return Err(RootError::NonFiniteValue);
502 }
503
504 if approx_eq(slope, 0.0, options.tolerance) {
505 return Err(RootError::ZeroDerivative);
506 }
507
508 let next = current - value / slope;
509 if !next.is_finite() {
510 return Err(RootError::NonFiniteValue);
511 }
512
513 if (next - current).abs() <= options.tolerance {
514 return Ok(next);
515 }
516
517 current = next;
518 }
519
520 Err(RootError::MaxIterationsReached)
521 }
522
523 #[cfg(feature = "interval")]
538 pub fn bisection_interval<F>(
539 f: F,
540 interval: Interval<f64>,
541 options: RootOptions,
542 ) -> Result<f64, RootError>
543 where
544 F: Fn(f64) -> f64,
545 {
546 if interval.is_empty() {
547 return Err(RootError::InvalidInterval);
548 }
549
550 let (lower, allow_lower_endpoint) = match interval.lower {
551 Bound::Open(value) => (value, false),
552 Bound::Closed(value) => (value, true),
553 Bound::Unbounded => return Err(RootError::InvalidInterval),
554 };
555
556 let (upper, allow_upper_endpoint) = match interval.upper {
557 Bound::Open(value) => (value, false),
558 Bound::Closed(value) => (value, true),
559 Bound::Unbounded => return Err(RootError::InvalidInterval),
560 };
561
562 bisection_with_policy(
563 &f,
564 lower,
565 upper,
566 options,
567 allow_lower_endpoint,
568 allow_upper_endpoint,
569 )
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::{
576 RootError, RootOptions, approx_eq, backward_difference, bisection, central_difference,
577 clamp_epsilon, forward_difference, midpoint_rule, newton_raphson, rectangle_rule,
578 relative_eq, simpsons_rule, trapezoidal_rule,
579 };
580
581 #[cfg(feature = "interval")]
582 use super::bisection_interval;
583
584 #[cfg(feature = "interval")]
585 use use_interval::Interval;
586
587 #[test]
588 fn absolute_approximation_equality_uses_absolute_difference() {
589 assert!(approx_eq(1.0, 1.0 + 5.0e-7, 1.0e-6));
590 assert!(!approx_eq(1.0, 1.0 + 2.0e-6, 1.0e-6));
591 }
592
593 #[test]
594 fn relative_approximation_equality_uses_relative_difference() {
595 assert!(relative_eq(10_000.0, 10_000.01, 1.0e-6));
596 assert!(!relative_eq(10_000.0, 10_000.5, 1.0e-6));
597 }
598
599 #[test]
600 fn zero_and_near_zero_comparisons_use_safe_fallback() {
601 assert!(relative_eq(1.0e-12, 0.0, 1.0e-9));
602 assert!(!relative_eq(1.0e-3, 0.0, 1.0e-6));
603 }
604
605 #[test]
606 fn negative_epsilon_behavior_is_clamped_to_positive() {
607 assert!((clamp_epsilon(-1.0e-6) - 1.0e-6).abs() <= f64::EPSILON);
608 assert!(approx_eq(2.0, 2.0 + 5.0e-7, -1.0e-6));
609 }
610
611 #[test]
612 fn non_finite_comparison_behavior_is_explicit() {
613 assert!(!approx_eq(f64::NAN, f64::NAN, 1.0e-6));
614 assert!(!relative_eq(f64::NAN, 1.0, 1.0e-6));
615 assert!(approx_eq(f64::INFINITY, f64::INFINITY, 1.0e-6));
616 assert!(!approx_eq(f64::INFINITY, f64::NEG_INFINITY, 1.0e-6));
617 assert!(!relative_eq(f64::INFINITY, 1.0, 1.0e-6));
618 }
619
620 #[test]
621 fn forward_difference_approximates_first_derivative() {
622 let derivative = forward_difference(|x| x * x, 3.0, 1.0e-6);
623
624 assert!((derivative - 6.0).abs() < 1.0e-5);
625 }
626
627 #[test]
628 fn backward_difference_approximates_first_derivative() {
629 let derivative = backward_difference(|x| x * x, 3.0, 1.0e-6);
630
631 assert!((derivative - 6.0).abs() < 1.0e-5);
632 }
633
634 #[test]
635 fn central_difference_approximates_first_derivative() {
636 let derivative = central_difference(|x| x * x, 3.0, 1.0e-6);
637
638 assert!((derivative - 6.0).abs() < 1.0e-5);
639 }
640
641 #[test]
642 fn rectangle_rule_approximates_integrals() {
643 let area = rectangle_rule(|x| x * x, 0.0, 1.0, 10_000).unwrap();
644
645 assert!((area - 1.0 / 3.0).abs() < 1.0e-4);
646 }
647
648 #[test]
649 fn midpoint_rule_approximates_integrals() {
650 let area = midpoint_rule(|x| x * x, 0.0, 1.0, 1_000).unwrap();
651
652 assert!((area - 1.0 / 3.0).abs() < 1.0e-6);
653 }
654
655 #[test]
656 fn trapezoidal_rule_approximates_integrals() {
657 let area = trapezoidal_rule(|x| x * x, 0.0, 1.0, 1_000).unwrap();
658
659 assert!((area - 1.0 / 3.0).abs() < 1.0e-6);
660 }
661
662 #[test]
663 fn simpsons_rule_approximates_integrals() {
664 let area = simpsons_rule(|x| x * x, 0.0, 1.0, 10).unwrap();
665
666 assert!((area - 1.0 / 3.0).abs() < 1.0e-12);
667 }
668
669 #[test]
670 fn invalid_integration_subdivision_count_returns_none() {
671 assert_eq!(rectangle_rule(|x| x, 0.0, 1.0, 0), None);
672 assert_eq!(midpoint_rule(|x| x, 0.0, 1.0, 0), None);
673 assert_eq!(trapezoidal_rule(|x| x, 0.0, 1.0, 0), None);
674 }
675
676 #[test]
677 fn simpsons_rule_rejects_odd_subdivision_counts() {
678 assert_eq!(simpsons_rule(|x| x, 0.0, 1.0, 3), None);
679 }
680
681 #[test]
682 fn reversed_integration_bounds_return_negative_integral() {
683 let area = trapezoidal_rule(|x| x * x, 1.0, 0.0, 1_000).unwrap();
684
685 assert!((area + 1.0 / 3.0).abs() < 1.0e-6);
686 }
687
688 #[test]
689 fn bisection_succeeds_for_bracketed_root() {
690 let root = bisection(|x| x.mul_add(x, -2.0), 1.0, 2.0, RootOptions::default()).unwrap();
691
692 assert!((root - 2.0_f64.sqrt()).abs() < 1.0e-8);
693 }
694
695 #[test]
696 fn bisection_returns_endpoint_root_when_present() {
697 let root = bisection(|x| x - 1.0, 1.0, 3.0, RootOptions::default()).unwrap();
698
699 assert!((root - 1.0).abs() <= f64::EPSILON);
700 }
701
702 #[test]
703 fn bisection_rejects_invalid_intervals() {
704 assert_eq!(
705 bisection(|x| x.mul_add(x, 1.0), -1.0, 1.0, RootOptions::default()),
706 Err(RootError::InvalidInterval)
707 );
708 }
709
710 #[test]
711 fn bisection_rejects_invalid_tolerance() {
712 assert_eq!(
713 bisection(
714 |x| x.mul_add(x, -2.0),
715 1.0,
716 2.0,
717 RootOptions {
718 tolerance: 0.0,
719 max_iterations: 100,
720 },
721 ),
722 Err(RootError::InvalidTolerance)
723 );
724 }
725
726 #[test]
727 fn bisection_reports_max_iterations_reached() {
728 assert_eq!(
729 bisection(
730 |x| x.mul_add(x, -2.0),
731 1.0,
732 2.0,
733 RootOptions {
734 tolerance: 1.0e-20,
735 max_iterations: 1,
736 },
737 ),
738 Err(RootError::MaxIterationsReached)
739 );
740 }
741
742 #[test]
743 fn newton_raphson_succeeds_for_simple_root() {
744 let root = newton_raphson(
745 |x| x.mul_add(x, -2.0),
746 |x| 2.0 * x,
747 1.0,
748 RootOptions::default(),
749 )
750 .unwrap();
751
752 assert!((root - 2.0_f64.sqrt()).abs() < 1.0e-8);
753 }
754
755 #[test]
756 fn newton_raphson_reports_zero_derivative() {
757 assert_eq!(
758 newton_raphson(
759 |x| (x * x).mul_add(x, 1.0),
760 |x| 3.0 * x * x,
761 0.0,
762 RootOptions::default(),
763 ),
764 Err(RootError::ZeroDerivative)
765 );
766 }
767
768 #[test]
769 fn newton_raphson_rejects_invalid_tolerance() {
770 assert_eq!(
771 newton_raphson(
772 |x| x.mul_add(x, -2.0),
773 |x| 2.0 * x,
774 1.0,
775 RootOptions {
776 tolerance: 0.0,
777 max_iterations: 100,
778 },
779 ),
780 Err(RootError::InvalidTolerance)
781 );
782 }
783
784 #[test]
785 fn newton_raphson_reports_max_iterations_reached() {
786 assert_eq!(
787 newton_raphson(
788 |x| x.mul_add(x, -2.0),
789 |x| 2.0 * x,
790 1.0,
791 RootOptions {
792 tolerance: 1.0e-20,
793 max_iterations: 1,
794 },
795 ),
796 Err(RootError::MaxIterationsReached)
797 );
798 }
799
800 #[test]
801 fn non_finite_root_finding_behavior_is_reported() {
802 assert_eq!(
803 bisection(|_| f64::NAN, 0.0, 1.0, RootOptions::default()),
804 Err(RootError::NonFiniteValue)
805 );
806 assert_eq!(
807 newton_raphson(|_| f64::NAN, |_| 1.0, 0.0, RootOptions::default(),),
808 Err(RootError::NonFiniteValue)
809 );
810 }
811
812 #[cfg(feature = "interval")]
813 #[test]
814 fn bisection_interval_supports_bounded_intervals() {
815 let root = bisection_interval(
816 |x| x.mul_add(x, -2.0),
817 Interval::closed(1.0, 2.0),
818 RootOptions::default(),
819 )
820 .unwrap();
821
822 assert!((root - 2.0_f64.sqrt()).abs() < 1.0e-8);
823 }
824
825 #[cfg(feature = "interval")]
826 #[test]
827 fn bisection_interval_does_not_accept_open_endpoint_roots() {
828 assert_eq!(
829 bisection_interval(|x| x, Interval::open(0.0, 2.0), RootOptions::default()),
830 Err(RootError::InvalidInterval)
831 );
832 }
833}