1use super::symbolic_algebra::{add, c, cos, div, exp, mul, sin, var, Expr};
27use super::symbolic_integration::{integrate, IntegrationError};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum OdeError {
31 NotSupported,
33 NotIntegrable,
35}
36
37impl From<IntegrationError> for OdeError {
38 fn from(_: IntegrationError) -> Self {
39 OdeError::NotIntegrable
40 }
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub enum OdeSolution {
46 Explicit(Expr),
48 Implicit { f_y: Expr, g_x: Expr },
50}
51
52pub fn solve_separable(
56 g_x: &Expr,
57 h_y: &Expr,
58 xvar: &str,
59 yvar: &str,
60) -> Result<OdeSolution, OdeError> {
61 let f_y = integrate(&div(c(1.0), h_y.clone()), yvar)?; let g_int = integrate(g_x, xvar)?; Ok(OdeSolution::Implicit {
64 f_y,
65 g_x: add(g_int, var("C")),
66 })
67}
68
69pub fn solve_linear_first_order(a: f64, b: f64, xvar: &str) -> OdeSolution {
73 if a == 0.0 {
74 OdeSolution::Explicit(add(mul(c(b), var(xvar)), var("C")))
75 } else {
76 let homogeneous = mul(var("C"), exp(mul(c(-a), var(xvar))));
77 OdeSolution::Explicit(add(c(b / a), homogeneous))
78 }
79}
80
81pub fn solve_linear_second_order(
85 a: f64,
86 b: f64,
87 cc: f64,
88 xvar: &str,
89) -> Result<OdeSolution, OdeError> {
90 if a == 0.0 {
91 return Err(OdeError::NotSupported);
92 }
93 let disc = b * b - 4.0 * a * cc;
94 let x = var(xvar);
95 let sol = if disc > 1e-12 {
96 let s = disc.sqrt();
98 let r1 = (-b + s) / (2.0 * a);
99 let r2 = (-b - s) / (2.0 * a);
100 add(
101 mul(var("C1"), exp(mul(c(r1), x.clone()))),
102 mul(var("C2"), exp(mul(c(r2), x))),
103 )
104 } else if disc.abs() <= 1e-12 {
105 let r = -b / (2.0 * a);
107 mul(add(var("C1"), mul(var("C2"), x.clone())), exp(mul(c(r), x)))
108 } else {
109 let alpha = -b / (2.0 * a);
111 let beta = (-disc).sqrt() / (2.0 * a);
112 mul(
113 exp(mul(c(alpha), x.clone())),
114 add(
115 mul(var("C1"), cos(mul(c(beta), x.clone()))),
116 mul(var("C2"), sin(mul(c(beta), x))),
117 ),
118 )
119 };
120 Ok(OdeSolution::Explicit(sol))
121}
122
123#[derive(Debug, Clone, PartialEq)]
127pub enum PdeSolution {
128 GeneralFunctionOf { invariant: Expr },
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum PdeClass {
136 Elliptic,
138 Parabolic,
140 Hyperbolic,
142}
143
144pub fn solve_first_order_linear_pde(
147 a: f64,
148 b: f64,
149 xvar: &str,
150 yvar: &str,
151) -> Result<PdeSolution, OdeError> {
152 if a == 0.0 && b == 0.0 {
153 return Err(OdeError::NotSupported);
154 }
155 let invariant = add(mul(c(b), var(xvar)), mul(c(-a), var(yvar)));
157 Ok(PdeSolution::GeneralFunctionOf { invariant })
158}
159
160pub fn classify_second_order_pde(a_xx: f64, b_xy: f64, c_yy: f64) -> PdeClass {
163 let disc = b_xy * b_xy - 4.0 * a_xx * c_yy;
164 if disc < -1e-12 {
165 PdeClass::Elliptic
166 } else if disc <= 1e-12 {
167 PdeClass::Parabolic
168 } else {
169 PdeClass::Hyperbolic
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::super::symbolic_algebra::{differentiate, pow, simplify, var};
176 use super::*;
177 use std::collections::HashMap;
178
179 fn env(pairs: &[(&str, f64)]) -> HashMap<String, f64> {
180 pairs.iter().map(|&(k, v)| (k.to_string(), v)).collect()
181 }
182
183 #[test]
184 fn separable_growth() {
185 let sol = solve_separable(&c(1.0), &var("y"), "x", "y").unwrap();
187 match sol {
188 OdeSolution::Implicit { f_y, g_x } => {
189 assert!(
191 (f_y.eval(&env(&[("y", std::f64::consts::E)])).unwrap() - 1.0).abs() < 1e-9
192 );
193 assert!((g_x.eval(&env(&[("x", 2.0), ("C", 3.0)])).unwrap() - 5.0).abs() < 1e-9);
195 }
196 _ => panic!("expected implicit solution"),
197 }
198 }
199
200 #[test]
201 fn separable_fails_closed() {
202 let h = super::super::symbolic_algebra::sin(pow(var("y"), 2));
204 assert_eq!(
205 solve_separable(&c(1.0), &h, "x", "y").unwrap_err(),
206 OdeError::NotIntegrable
207 );
208 }
209
210 #[test]
211 fn linear_first_order_satisfies_the_ode() {
212 let OdeSolution::Explicit(y) = solve_linear_first_order(2.0, 6.0, "x") else {
214 panic!()
215 };
216 let yp = simplify(&differentiate(&y, "x"));
217 for &(x, cval) in &[(0.0, 1.0), (0.7, -2.0), (1.5, 4.0)] {
218 let e = env(&[("x", x), ("C", cval)]);
219 let residual = yp.eval(&e).unwrap() + 2.0 * y.eval(&e).unwrap();
220 assert!(
221 (residual - 6.0).abs() < 1e-7,
222 "residual {residual} at x={x}"
223 );
224 }
225 }
226
227 fn verify_second_order(y: &Expr, a: f64, b: f64, cc: f64) {
229 let yp = simplify(&differentiate(y, "x"));
230 let ypp = simplify(&differentiate(&yp, "x"));
231 for &(x, c1, c2) in &[(0.0, 1.0, 0.5), (0.8, -1.0, 2.0), (1.7, 3.0, -1.5)] {
232 let e = env(&[("x", x), ("C1", c1), ("C2", c2)]);
233 let r = a * ypp.eval(&e).unwrap() + b * yp.eval(&e).unwrap() + cc * y.eval(&e).unwrap();
234 assert!(r.abs() < 1e-6, "residual {r} at x={x}");
235 }
236 }
237
238 #[test]
239 fn second_order_distinct_real_roots() {
240 let OdeSolution::Explicit(y) = solve_linear_second_order(1.0, -3.0, 2.0, "x").unwrap()
242 else {
243 panic!()
244 };
245 verify_second_order(&y, 1.0, -3.0, 2.0);
246 }
247
248 #[test]
249 fn second_order_repeated_root() {
250 let OdeSolution::Explicit(y) = solve_linear_second_order(1.0, -2.0, 1.0, "x").unwrap()
252 else {
253 panic!()
254 };
255 verify_second_order(&y, 1.0, -2.0, 1.0);
256 }
257
258 #[test]
259 fn second_order_complex_roots() {
260 let OdeSolution::Explicit(y) = solve_linear_second_order(1.0, 0.0, 1.0, "x").unwrap()
262 else {
263 panic!()
264 };
265 verify_second_order(&y, 1.0, 0.0, 1.0);
266 }
267
268 #[test]
269 fn second_order_rejects_non_second_order() {
270 assert_eq!(
271 solve_linear_second_order(0.0, 1.0, 1.0, "x").unwrap_err(),
272 OdeError::NotSupported
273 );
274 }
275
276 #[test]
277 fn transport_pde_invariant_satisfies_equation() {
278 let PdeSolution::GeneralFunctionOf { invariant } =
280 solve_first_order_linear_pde(2.0, 3.0, "x", "y").unwrap();
281 let u = pow(invariant, 2);
282 let ux = simplify(&differentiate(&u, "x"));
283 let uy = simplify(&differentiate(&u, "y"));
284 for &(x, y) in &[(0.0, 0.0), (1.0, 2.0), (-1.0, 0.5)] {
285 let e = env(&[("x", x), ("y", y)]);
286 let r = 2.0 * ux.eval(&e).unwrap() + 3.0 * uy.eval(&e).unwrap();
287 assert!(r.abs() < 1e-7, "transport residual {r}");
288 }
289 }
290
291 #[test]
292 fn second_order_pde_classification() {
293 assert_eq!(classify_second_order_pde(1.0, 0.0, 1.0), PdeClass::Elliptic);
295 assert_eq!(
297 classify_second_order_pde(1.0, 0.0, -1.0),
298 PdeClass::Hyperbolic
299 );
300 assert_eq!(
302 classify_second_order_pde(1.0, 0.0, 0.0),
303 PdeClass::Parabolic
304 );
305 }
306}