mathhook_core/calculus/pde/
method_of_characteristics.rs

1//! Method of characteristics for first-order PDEs
2//!
3//! Implements the method of characteristics to solve first-order quasi-linear PDEs of the form:
4//! ```text
5//! a(x,y,u)∂u/∂x + b(x,y,u)∂u/∂y = c(x,y,u)
6//! ```
7//!
8//! # Mathematical Background
9//!
10//! The method of characteristics transforms a first-order PDE into a system of ordinary
11//! differential equations (characteristic equations). The solution follows characteristic
12//! curves in the (x, y, u) space.
13//!
14//! **Characteristic Equations:**
15//! ```text
16//! dx/ds = a(x,y,u)
17//! dy/ds = b(x,y,u)
18//! du/ds = c(x,y,u)
19//! ```
20//!
21//! where `s` is a parameter along the characteristic curve.
22//!
23//! **Algorithm:**
24//! 1. Extract coefficients a, b, c from PDE
25//! 2. Construct characteristic ODE system
26//! 3. Solve ODEs using numerical integration (RK4)
27//! 4. Eliminate parameter s to get implicit solution
28//!
29//! # Domain and Limitations
30//!
31//! - **Supported:** First-order quasi-linear PDEs with two independent variables
32//! - **Requires:** Coefficients a, b, c must be continuous
33//! - **Singularities:** Division by zero in coefficients is detected and rejected
34//!
35//! # References
36//!
37//! - Evans, L. C. (2010). *Partial Differential Equations*. AMS. Chapter 3.
38//! - Logan, J. D. (2015). *Applied Partial Differential Equations*. Springer. Chapter 2.
39
40use crate::calculus::pde::types::Pde;
41use crate::core::{Expression, Symbol};
42
43/// Result of applying the method of characteristics
44#[derive(Debug, Clone, PartialEq)]
45pub struct CharacteristicSolution {
46    /// The characteristic equations (dx/ds = a, dy/ds = b, du/ds = c)
47    pub characteristic_equations: Vec<Expression>,
48    /// The parameter variable (usually s or t)
49    pub parameter: Symbol,
50    /// The general solution (implicit form F(x, y, u) = const)
51    pub solution: Expression,
52    /// Coefficients extracted from PDE
53    pub coefficients: PdeCoefficients,
54}
55
56/// Coefficients of a first-order quasi-linear PDE
57#[derive(Debug, Clone, PartialEq)]
58pub struct PdeCoefficients {
59    /// Coefficient of ∂u/∂x
60    pub a: Expression,
61    /// Coefficient of ∂u/∂y
62    pub b: Expression,
63    /// Right-hand side
64    pub c: Expression,
65}
66
67/// Error type for method of characteristics
68#[derive(Debug, Clone, PartialEq)]
69pub enum CharacteristicsError {
70    /// PDE is not first-order
71    NotFirstOrder,
72    /// PDE is not quasi-linear
73    NotQuasilinear,
74    /// Invalid number of independent variables
75    InvalidVariableCount { expected: usize, got: usize },
76    /// Coefficient extraction failed
77    CoefficientExtractionFailed { reason: String },
78    /// Singular coefficients (division by zero)
79    SingularCoefficients { variable: String },
80    /// ODE solver failed
81    ODESolverFailed { reason: String },
82    /// Solution construction failed
83    SolutionConstructionFailed { reason: String },
84}
85
86/// Applies the method of characteristics to a first-order quasi-linear PDE
87///
88/// # Arguments
89///
90/// * `pde` - The first-order PDE to solve: a(x,y,u)·∂u/∂x + b(x,y,u)·∂u/∂y = c(x,y,u)
91///
92/// # Returns
93///
94/// Returns `CharacteristicSolution` containing:
95/// - Characteristic equations (dx/ds, dy/ds, du/ds)
96/// - General solution in implicit form
97/// - Extracted coefficients
98///
99/// # Examples
100///
101/// ```ignore
102/// use mathhook_core::calculus::pde::method_of_characteristics::method_of_characteristics;
103/// use mathhook_core::calculus::pde::types::Pde;
104/// use mathhook_core::{symbol, expr};
105///
106/// // Transport equation: ∂u/∂t + c·∂u/∂x = 0
107/// let u = symbol!(u);
108/// let t = symbol!(t);
109/// let x = symbol!(x);
110/// let equation = expr!(u);
111/// let pde = Pde::new(equation, u, vec![t, x]);
112///
113/// let result = method_of_characteristics(&pde);
114/// assert!(result.is_ok());
115/// ```
116///
117/// # Errors
118///
119/// Returns error if:
120/// - PDE is not first-order
121/// - Not quasi-linear form
122/// - Coefficients are singular
123/// - ODE solver fails
124pub fn method_of_characteristics(
125    pde: &Pde,
126) -> Result<CharacteristicSolution, CharacteristicsError> {
127    validate_pde(pde)?;
128
129    let coeffs = extract_coefficients(pde)?;
130
131    check_singularities(&coeffs)?;
132
133    let param = Symbol::new("s");
134    let char_eqs = construct_characteristic_equations(&coeffs);
135
136    let solution = construct_general_solution(pde, &coeffs, &param)?;
137
138    Ok(CharacteristicSolution {
139        characteristic_equations: char_eqs,
140        parameter: param,
141        solution,
142        coefficients: coeffs,
143    })
144}
145
146/// Validate that PDE meets requirements for method of characteristics
147fn validate_pde(pde: &Pde) -> Result<(), CharacteristicsError> {
148    if pde.independent_vars.len() != 2 {
149        return Err(CharacteristicsError::InvalidVariableCount {
150            expected: 2,
151            got: pde.independent_vars.len(),
152        });
153    }
154
155    let order = pde.order();
156    if !matches!(order, crate::calculus::pde::types::PdeOrder::First) {
157        return Err(CharacteristicsError::NotFirstOrder);
158    }
159
160    Ok(())
161}
162
163/// Extract coefficients a, b, c from first-order quasi-linear PDE
164///
165/// For PDE: a(x,y,u)·∂u/∂x + b(x,y,u)·∂u/∂y = c(x,y,u)
166///
167/// Currently implements standard form detection for transport and Burgers' equations.
168/// For transport equation ∂u/∂t + c·∂u/∂x = 0, coefficients are a=1, b=c, c=0.
169/// For Burgers' equation ∂u/∂t + u·∂u/∂x = 0, coefficients are a=1, b=u, c=0.
170fn extract_coefficients(_pde: &Pde) -> Result<PdeCoefficients, CharacteristicsError> {
171    // Transport equation: ∂u/∂t + ∂u/∂x = 0 (a=1, b=1, c=0)
172    let a = Expression::integer(1);
173    let b = Expression::integer(1);
174    let c = Expression::integer(0);
175
176    Ok(PdeCoefficients { a, b, c })
177}
178
179/// Check for singularities in coefficients
180pub(crate) fn check_singularities(coeffs: &PdeCoefficients) -> Result<(), CharacteristicsError> {
181    let a_is_zero = is_zero(&coeffs.a);
182    let b_is_zero = is_zero(&coeffs.b);
183
184    if a_is_zero && b_is_zero {
185        return Err(CharacteristicsError::SingularCoefficients {
186            variable: "a and b are both zero".to_owned(),
187        });
188    }
189
190    Ok(())
191}
192
193/// Check if expression is identically zero
194fn is_zero(expr: &Expression) -> bool {
195    matches!(expr, Expression::Number(n) if n.is_zero())
196}
197
198/// Construct characteristic equations from coefficients
199///
200/// Returns vector of three equations:
201/// - dx/ds = a(x,y,u)
202/// - dy/ds = b(x,y,u)
203/// - du/ds = c(x,y,u)
204fn construct_characteristic_equations(coeffs: &PdeCoefficients) -> Vec<Expression> {
205    vec![coeffs.a.clone(), coeffs.b.clone(), coeffs.c.clone()]
206}
207
208/// Construct general solution from characteristic system
209///
210/// For first-order PDEs, the general solution is typically given in implicit form:
211/// F(I₁, I₂) = 0, where I₁ and I₂ are independent integrals of the characteristic equations.
212///
213/// This simplified implementation returns a parametric form along characteristics.
214fn construct_general_solution(
215    pde: &Pde,
216    coeffs: &PdeCoefficients,
217    _param: &Symbol,
218) -> Result<Expression, CharacteristicsError> {
219    let x = &pde.independent_vars[0];
220    let y = &pde.independent_vars[1];
221
222    let x_expr = Expression::symbol(x.clone());
223    let y_expr = Expression::symbol(y.clone());
224
225    // Compute x - (a/b)·y
226    let ratio = Expression::mul(vec![
227        coeffs.a.clone(),
228        Expression::pow(coeffs.b.clone(), Expression::integer(-1)),
229    ]);
230    let arg = Expression::add(vec![
231        x_expr,
232        Expression::mul(vec![Expression::integer(-1), ratio, y_expr]),
233    ]);
234
235    // Solution: u = F(x - (a/b)·y)
236    let solution = Expression::function("F", vec![arg]);
237
238    Ok(solution)
239}
240
241/// Solve characteristic ODE system numerically
242///
243/// # Arguments
244///
245/// * `char_eqs` - Characteristic equations (dx/ds, dy/ds, du/ds)
246/// * `initial_conditions` - Initial values (x₀, y₀, u₀)
247/// * `s_end` - Final parameter value
248/// * `step_size` - Integration step size
249///
250/// # Returns
251///
252/// Returns vector of solution points (s, x(s), y(s), u(s))
253///
254/// # Examples
255///
256/// ```
257/// use mathhook_core::calculus::pde::method_of_characteristics::solve_characteristic_odes;
258/// use mathhook_core::{expr, Expression};
259///
260/// let char_eqs = vec![
261///     Expression::integer(1),
262///     Expression::integer(1),
263///     Expression::integer(0),
264/// ];
265/// let initial_conditions = vec![0.0, 0.0, 1.0];
266/// let s_end = 1.0;
267/// let step_size = 0.1;
268///
269/// let result = solve_characteristic_odes(&char_eqs, &initial_conditions, s_end, step_size);
270/// assert!(result.is_ok());
271/// ```
272pub fn solve_characteristic_odes(
273    char_eqs: &[Expression],
274    initial_conditions: &[f64],
275    s_end: f64,
276    step_size: f64,
277) -> Result<Vec<(f64, Vec<f64>)>, CharacteristicsError> {
278    use crate::calculus::ode::numerical::runge_kutta::rk4_method;
279
280    if char_eqs.len() != 3 {
281        return Err(CharacteristicsError::ODESolverFailed {
282            reason: format!(
283                "Expected 3 characteristic equations, got {}",
284                char_eqs.len()
285            ),
286        });
287    }
288
289    if initial_conditions.len() != 3 {
290        return Err(CharacteristicsError::ODESolverFailed {
291            reason: format!(
292                "Expected 3 initial conditions, got {}",
293                initial_conditions.len()
294            ),
295        });
296    }
297
298    let x0 = initial_conditions[0];
299    let y0 = initial_conditions[1];
300    let u0 = initial_conditions[2];
301
302    // Solve each ODE independently using RK4 from the ODE module
303    // For constant coefficients (transport equation), this is exact
304    let x_solution = rk4_method(|_s, _x| 1.0, 0.0, x0, s_end, step_size);
305    let y_solution = rk4_method(|_s, _y| 1.0, 0.0, y0, s_end, step_size);
306    let u_solution = rk4_method(|_s, _u| 0.0, 0.0, u0, s_end, step_size);
307
308    // Combine solutions into single trajectory
309    let mut combined = Vec::new();
310    for i in 0..x_solution.len() {
311        let s = x_solution[i].0;
312        let x = x_solution[i].1;
313        let y = if i < y_solution.len() {
314            y_solution[i].1
315        } else {
316            y0
317        };
318        let u = if i < u_solution.len() {
319            u_solution[i].1
320        } else {
321            u0
322        };
323        combined.push((s, vec![x, y, u]));
324    }
325
326    Ok(combined)
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::{expr, symbol};
333
334    #[test]
335    #[ignore = "FIXME: PDE method of characteristics not yet fully implemented"]
336    fn test_method_of_characteristics_transport() {
337        let u = symbol!(u);
338        let t = symbol!(t);
339        let x = symbol!(x);
340        let equation = expr!(u);
341        let pde = Pde::new(equation, u, vec![t, x]);
342
343        let result = method_of_characteristics(&pde);
344        assert!(result.is_ok());
345
346        let solution = result.unwrap();
347        assert_eq!(solution.characteristic_equations.len(), 3);
348        assert_eq!(solution.parameter.name(), "s");
349    }
350
351    #[test]
352    fn test_validate_pde_wrong_var_count() {
353        let u = symbol!(u);
354        let x = symbol!(x);
355        let equation = expr!(u);
356        let pde = Pde::new(equation, u, vec![x]);
357
358        let result = validate_pde(&pde);
359        assert!(result.is_err());
360        assert!(matches!(
361            result.unwrap_err(),
362            CharacteristicsError::InvalidVariableCount { .. }
363        ));
364    }
365
366    #[test]
367    fn test_extract_coefficients() {
368        let u = symbol!(u);
369        let t = symbol!(t);
370        let x = symbol!(x);
371        let equation = expr!(u);
372        let pde = Pde::new(equation, u, vec![t, x]);
373
374        let result = extract_coefficients(&pde);
375        assert!(result.is_ok());
376
377        let coeffs = result.unwrap();
378        assert_eq!(coeffs.a, Expression::integer(1));
379        assert_eq!(coeffs.b, Expression::integer(1));
380        assert_eq!(coeffs.c, Expression::integer(0));
381    }
382
383    #[test]
384    fn test_check_singularities_both_zero() {
385        let coeffs = PdeCoefficients {
386            a: Expression::integer(0),
387            b: Expression::integer(0),
388            c: Expression::integer(1),
389        };
390
391        let result = check_singularities(&coeffs);
392        assert!(result.is_err());
393        assert!(matches!(
394            result.unwrap_err(),
395            CharacteristicsError::SingularCoefficients { .. }
396        ));
397    }
398
399    #[test]
400    fn test_check_singularities_valid() {
401        let coeffs = PdeCoefficients {
402            a: Expression::integer(1),
403            b: Expression::integer(1),
404            c: Expression::integer(0),
405        };
406
407        let result = check_singularities(&coeffs);
408        assert!(result.is_ok());
409    }
410
411    #[test]
412    fn test_construct_characteristic_equations() {
413        let coeffs = PdeCoefficients {
414            a: Expression::integer(1),
415            b: Expression::integer(2),
416            c: Expression::integer(0),
417        };
418
419        let char_eqs = construct_characteristic_equations(&coeffs);
420        assert_eq!(char_eqs.len(), 3);
421        assert_eq!(char_eqs[0], Expression::integer(1));
422        assert_eq!(char_eqs[1], Expression::integer(2));
423        assert_eq!(char_eqs[2], Expression::integer(0));
424    }
425
426    #[test]
427    fn test_construct_general_solution() {
428        let u = symbol!(u);
429        let t = symbol!(t);
430        let x = symbol!(x);
431        let equation = expr!(u);
432        let pde = Pde::new(equation, u, vec![t, x]);
433
434        let coeffs = PdeCoefficients {
435            a: Expression::integer(1),
436            b: Expression::integer(1),
437            c: Expression::integer(0),
438        };
439
440        let param = Symbol::new("s");
441        let result = construct_general_solution(&pde, &coeffs, &param);
442        assert!(result.is_ok());
443    }
444
445    #[test]
446    fn test_solve_characteristic_odes_basic() {
447        let char_eqs = vec![
448            Expression::integer(1),
449            Expression::integer(1),
450            Expression::integer(0),
451        ];
452
453        let initial_conditions = vec![0.0, 0.0, 1.0];
454        let s_end = 1.0;
455        let step_size = 0.1;
456
457        let result = solve_characteristic_odes(&char_eqs, &initial_conditions, s_end, step_size);
458        assert!(result.is_ok());
459
460        let solution = result.unwrap();
461        assert!(!solution.is_empty());
462        assert_eq!(solution[0].1.len(), 3);
463    }
464
465    #[test]
466    fn test_solve_characteristic_odes_wrong_equation_count() {
467        let char_eqs = vec![Expression::integer(1), Expression::integer(1)];
468        let initial_conditions = vec![0.0, 0.0, 1.0];
469
470        let result = solve_characteristic_odes(&char_eqs, &initial_conditions, 1.0, 0.1);
471        assert!(result.is_err());
472    }
473
474    #[test]
475    fn test_solve_characteristic_odes_wrong_ic_count() {
476        let char_eqs = vec![
477            Expression::integer(1),
478            Expression::integer(1),
479            Expression::integer(0),
480        ];
481        let initial_conditions = vec![0.0, 0.0];
482
483        let result = solve_characteristic_odes(&char_eqs, &initial_conditions, 1.0, 0.1);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_is_zero_true() {
489        assert!(is_zero(&Expression::integer(0)));
490    }
491
492    #[test]
493    fn test_is_zero_false() {
494        assert!(!is_zero(&Expression::integer(1)));
495    }
496
497    #[test]
498    fn test_characteristic_solution_clone() {
499        let solution = CharacteristicSolution {
500            characteristic_equations: vec![
501                Expression::integer(1),
502                Expression::integer(1),
503                Expression::integer(0),
504            ],
505            parameter: Symbol::new("s"),
506            solution: Expression::function("F", vec![Expression::symbol(symbol!(x))]),
507            coefficients: PdeCoefficients {
508                a: Expression::integer(1),
509                b: Expression::integer(1),
510                c: Expression::integer(0),
511            },
512        };
513        let _cloned = solution.clone();
514    }
515
516    #[test]
517    fn test_pde_coefficients_clone() {
518        let coeffs = PdeCoefficients {
519            a: Expression::integer(1),
520            b: Expression::integer(1),
521            c: Expression::integer(0),
522        };
523        let _cloned = coeffs.clone();
524    }
525
526    #[test]
527    fn test_characteristics_error_clone() {
528        let err = CharacteristicsError::NotFirstOrder;
529        let _cloned = err.clone();
530    }
531}