mathhook_core/calculus/ode/numerical/
euler.rs1use crate::calculus::ode::first_order::ODEError;
7
8pub fn euler_method<F>(f: F, x0: f64, y0: f64, x_end: f64, step: f64) -> Vec<(f64, f64)>
39where
40 F: Fn(f64, f64) -> f64,
41{
42 if step <= 0.0 {
43 return vec![(x0, y0)];
44 }
45
46 let num_steps = ((x_end - x0) / step).abs().ceil() as usize;
47 let mut solution = Vec::with_capacity(num_steps + 1);
48
49 let mut x = x0;
50 let mut y = y0;
51 solution.push((x, y));
52
53 let h = if x_end > x0 { step } else { -step };
54
55 for _ in 0..num_steps {
56 if (x_end > x0 && x >= x_end) || (x_end < x0 && x <= x_end) {
57 break;
58 }
59
60 let slope = f(x, y);
61 y += h * slope;
62 x += h;
63
64 solution.push((x, y));
65 }
66
67 solution
68}
69
70pub fn solve_euler<F>(
84 f: F,
85 x0: f64,
86 y0: f64,
87 x_end: f64,
88 step: f64,
89) -> Result<Vec<(f64, f64)>, ODEError>
90where
91 F: Fn(f64, f64) -> f64,
92{
93 if step <= 0.0 {
94 return Err(ODEError::InvalidInput {
95 message: "Step size must be positive".to_owned(),
96 });
97 }
98
99 if !x0.is_finite() || !y0.is_finite() || !x_end.is_finite() {
100 return Err(ODEError::InvalidInput {
101 message: "Initial values and endpoints must be finite".to_owned(),
102 });
103 }
104
105 Ok(euler_method(f, x0, y0, x_end, step))
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_euler_constant_derivative() {
114 let solution = euler_method(|_x, _y| 2.0, 0.0, 0.0, 1.0, 0.1);
115
116 assert_eq!(solution.len(), 11);
117 assert_eq!(solution[0], (0.0, 0.0));
118
119 let (x_final, y_final) = solution.last().unwrap();
120 assert!((x_final - 1.0).abs() < 1e-10);
121 assert!((y_final - 2.0).abs() < 0.1);
122 }
123
124 #[test]
125 fn test_euler_linear_ode() {
126 let solution = euler_method(|x, _y| x, 0.0, 0.0, 1.0, 0.1);
127
128 let (x_final, y_final) = solution.last().unwrap();
129 assert!((x_final - 1.0).abs() < 1e-10);
130 assert!((y_final - 0.5).abs() < 0.1);
131 }
132
133 #[test]
134 fn test_euler_exponential_growth() {
135 let solution = euler_method(|_x, y| y, 0.0, 1.0, 1.0, 0.1);
136
137 let (x_final, y_final) = solution.last().unwrap();
138 assert!((x_final - 1.0).abs() < 1e-10);
139
140 let expected = 1.0_f64.exp();
141 let relative_error = (y_final - expected).abs() / expected;
142 assert!(relative_error < 0.1);
143 }
144
145 #[test]
146 fn test_euler_backward_integration() {
147 let solution = euler_method(|x, _y| x, 1.0, 0.5, 0.0, 0.1);
148
149 assert!(solution.len() > 1);
150 let (x_first, y_first) = solution[0];
151 let (x_final, _y_final) = solution.last().unwrap();
152
153 assert_eq!((x_first, y_first), (1.0, 0.5));
154 assert!((x_final - 0.0).abs() < 1e-10);
155 }
156
157 #[test]
158 fn test_euler_zero_step_size() {
159 let solution = euler_method(|x, _y| x, 0.0, 0.0, 1.0, 0.0);
160
161 assert_eq!(solution.len(), 1);
162 assert_eq!(solution[0], (0.0, 0.0));
163 }
164
165 #[test]
166 fn test_solve_euler_invalid_input() {
167 let result = solve_euler(|x, _y| x, 0.0, 0.0, 1.0, -0.1);
168 assert!(result.is_err());
169
170 let result = solve_euler(|x, _y| x, f64::NAN, 0.0, 1.0, 0.1);
171 assert!(result.is_err());
172 }
173
174 #[test]
175 fn test_euler_variable_step() {
176 let solution1 = euler_method(|x, _y| x, 0.0, 0.0, 1.0, 0.1);
177 let solution2 = euler_method(|x, _y| x, 0.0, 0.0, 1.0, 0.05);
178
179 let (_, y1) = solution1.last().unwrap();
180 let (_, y2) = solution2.last().unwrap();
181
182 assert!(solution2.len() > solution1.len());
183 assert!((y2 - 0.5).abs() < (y1 - 0.5).abs());
184 }
185}