mathhook_core/calculus/pde/standard/
heat.rs

1//! Heat equation solver
2//!
3//! Solves the heat equation: du/dt = alpha * nabla^2(u)
4//!
5//! Uses separation of variables and Fourier series for standard boundary conditions.
6//!
7//! # Limitations
8//!
9//! Returns solutions with symbolic Fourier coefficients (A_1, A_2, A_3, ...).
10//! Numerical evaluation of these coefficients requires symbolic integration,
11//! which is not yet implemented in MathHook.
12//!
13//! **What you get**: Correct solution structure `u(x,t) = sum A_n sin(sqrt(lambda_n) x) exp(-lambda_n alpha t)`
14//! where lambda_n are correctly computed eigenvalues.
15//!
16//! **What's missing**: Actual values of A_n computed from initial conditions via
17//! Fourier series expansion (requires symbolic integration).
18
19use crate::calculus::pde::common::{
20    compute_dirichlet_1d_eigenvalues, create_symbolic_coefficients,
21};
22use crate::calculus::pde::registry::{PDEError, PDEResult, PDESolver};
23use crate::calculus::pde::types::{BoundaryCondition, InitialCondition, PDESolution, Pde, PdeType};
24use crate::core::{Expression, Symbol};
25
26/// Heat equation solver implementing PDESolver trait
27pub struct HeatEquationSolver {
28    max_terms: usize,
29}
30
31impl HeatEquationSolver {
32    /// Creates a new heat equation solver
33    pub fn new() -> Self {
34        Self { max_terms: 10 }
35    }
36
37    /// Creates solver with custom maximum number of terms
38    pub fn with_max_terms(max_terms: usize) -> Self {
39        Self { max_terms }
40    }
41
42    /// Solves the 1D heat equation with full Fourier series computation
43    ///
44    /// # Arguments
45    ///
46    /// * `pde` - The heat equation PDE
47    /// * `alpha` - Thermal diffusivity coefficient
48    /// * `boundary_conditions` - Boundary conditions
49    /// * `_initial_condition` - Initial temperature distribution (currently unused)
50    ///
51    /// # Returns
52    ///
53    /// A `PDESolution` containing the heat equation solution with eigenvalues and
54    /// symbolic Fourier coefficients.
55    ///
56    /// # Errors
57    ///
58    /// Returns `PDEError::InvalidForm` if the PDE does not have exactly 2 independent
59    /// variables (x and t for 1D heat equation).
60    ///
61    /// # Mathematical Background
62    ///
63    /// Solution form: u(x,t) = sum A_n sin(sqrt(lambda_n) x) exp(-lambda_n alpha t)
64    /// where lambda_n are eigenvalues determined by boundary conditions and A_n are
65    /// Fourier coefficients determined by initial condition.
66    #[allow(unused_variables)]
67    pub fn solve_heat_equation_1d(
68        &self,
69        pde: &Pde,
70        alpha: &Expression,
71        boundary_conditions: &[BoundaryCondition],
72        _initial_condition: &InitialCondition,
73    ) -> PDEResult {
74        if pde.independent_vars.len() != 2 {
75            return Err(PDEError::InvalidForm {
76                reason: "1D heat equation requires exactly 2 independent variables (x, t)"
77                    .to_owned(),
78            });
79        }
80
81        let eigenvalues = compute_dirichlet_1d_eigenvalues(
82            boundary_conditions,
83            &pde.independent_vars[0],
84            self.max_terms,
85        )?;
86
87        let coefficients = create_symbolic_coefficients("A", eigenvalues.len())?;
88
89        let solution =
90            self.construct_heat_solution(&pde.independent_vars, alpha, &eigenvalues, &coefficients);
91
92        Ok(PDESolution::heat(
93            solution,
94            alpha.clone(),
95            eigenvalues,
96            coefficients,
97        ))
98    }
99
100    fn construct_heat_solution(
101        &self,
102        vars: &[Symbol],
103        alpha: &Expression,
104        eigenvalues: &[Expression],
105        coefficients: &[Expression],
106    ) -> Expression {
107        let x = &vars[0];
108        let t = &vars[1];
109
110        if eigenvalues.is_empty() || coefficients.is_empty() {
111            return Expression::integer(0);
112        }
113
114        let mut terms = Vec::new();
115
116        for (lambda, a_n) in eigenvalues.iter().zip(coefficients.iter()) {
117            let spatial = Expression::function(
118                "sin",
119                vec![Expression::mul(vec![
120                    Expression::pow(lambda.clone(), Expression::rational(1, 2)),
121                    Expression::symbol(x.clone()),
122                ])],
123            );
124
125            let temporal = Expression::function(
126                "exp",
127                vec![Expression::mul(vec![
128                    Expression::integer(-1),
129                    lambda.clone(),
130                    alpha.clone(),
131                    Expression::symbol(t.clone()),
132                ])],
133            );
134
135            let term = Expression::mul(vec![a_n.clone(), spatial, temporal]);
136            terms.push(term);
137        }
138
139        Expression::add(terms)
140    }
141}
142
143impl PDESolver for HeatEquationSolver {
144    fn solve(&self, pde: &Pde) -> PDEResult {
145        use crate::expr;
146
147        let alpha = expr!(1);
148        let ic = InitialCondition::value(expr!(1));
149
150        self.solve_heat_equation_1d(pde, &alpha, &[], &ic)
151    }
152
153    fn can_solve(&self, pde_type: PdeType) -> bool {
154        matches!(pde_type, PdeType::Parabolic)
155    }
156
157    fn priority(&self) -> u8 {
158        100
159    }
160
161    fn name(&self) -> &'static str {
162        "Heat Equation Solver"
163    }
164
165    fn description(&self) -> &'static str {
166        "Solves heat equation du/dt = alpha * nabla^2(u) using separation of variables and Fourier series"
167    }
168}
169
170impl Default for HeatEquationSolver {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::calculus::pde::types::{BoundaryLocation, SolutionMetadata};
180    use crate::{expr, symbol};
181
182    #[test]
183    fn test_heat_solver_creation() {
184        let solver = HeatEquationSolver::new();
185        assert_eq!(solver.name(), "Heat Equation Solver");
186        assert_eq!(solver.priority(), 100);
187    }
188
189    #[test]
190    fn test_heat_solver_can_solve() {
191        let solver = HeatEquationSolver::new();
192        assert!(solver.can_solve(PdeType::Parabolic));
193        assert!(!solver.can_solve(PdeType::Hyperbolic));
194        assert!(!solver.can_solve(PdeType::Elliptic));
195    }
196
197    #[test]
198    fn test_solve_heat_equation_1d_basic() {
199        let u = symbol!(u);
200        let x = symbol!(x);
201        let t = symbol!(t);
202        let equation = expr!(u);
203        let pde = Pde::new(equation, u, vec![x.clone(), t]);
204        let alpha = expr!(1);
205
206        let bc1 = BoundaryCondition::dirichlet(
207            expr!(0),
208            BoundaryLocation::Simple {
209                variable: x.clone(),
210                value: expr!(0),
211            },
212        );
213        let bc2 = BoundaryCondition::dirichlet(
214            expr!(0),
215            BoundaryLocation::Simple {
216                variable: x,
217                value: expr!(1),
218            },
219        );
220
221        let ic = InitialCondition::value(expr!(1));
222
223        let solver = HeatEquationSolver::new();
224        let result = solver.solve_heat_equation_1d(&pde, &alpha, &[bc1, bc2], &ic);
225        assert!(result.is_ok());
226
227        let solution = result.unwrap();
228        match &solution.metadata {
229            SolutionMetadata::Heat {
230                alpha: sol_alpha,
231                eigenvalues,
232                coefficients,
233            } => {
234                assert_eq!(sol_alpha, &alpha);
235                assert!(!eigenvalues.is_empty());
236                assert!(!coefficients.is_empty());
237            }
238            _ => panic!("Expected Heat metadata"),
239        }
240    }
241
242    #[test]
243    fn test_solve_heat_equation_wrong_dimensions() {
244        let u = symbol!(u);
245        let x = symbol!(x);
246        let equation = expr!(u);
247        let pde = Pde::new(equation, u, vec![x]);
248        let alpha = expr!(1);
249        let ic = InitialCondition::value(expr!(1));
250
251        let solver = HeatEquationSolver::new();
252        let result = solver.solve_heat_equation_1d(&pde, &alpha, &[], &ic);
253        assert!(result.is_err());
254    }
255
256    #[test]
257    fn test_heat_solution_structure() {
258        let u = symbol!(u);
259        let x = symbol!(x);
260        let t = symbol!(t);
261        let equation = expr!(u);
262        let pde = Pde::new(equation, u, vec![x.clone(), t]);
263        let alpha = expr!(1);
264
265        let bc = BoundaryCondition::dirichlet(
266            expr!(0),
267            BoundaryLocation::Simple {
268                variable: x,
269                value: expr!(0),
270            },
271        );
272        let ic = InitialCondition::value(expr!(1));
273
274        let solver = HeatEquationSolver::new();
275        let result = solver.solve_heat_equation_1d(&pde, &alpha, &[bc], &ic);
276        assert!(result.is_ok());
277
278        let solution = result.unwrap();
279        match &solution.metadata {
280            SolutionMetadata::Heat {
281                eigenvalues,
282                coefficients,
283                ..
284            } => {
285                assert_eq!(eigenvalues.len(), coefficients.len());
286            }
287            _ => panic!("Expected Heat metadata"),
288        }
289    }
290
291    #[test]
292    fn test_pde_solver_trait() {
293        let solver = HeatEquationSolver::new();
294        let u = symbol!(u);
295        let x = symbol!(x);
296        let t = symbol!(t);
297        let equation = expr!(u);
298        let pde = Pde::new(equation, u, vec![x, t]);
299
300        let result = solver.solve(&pde);
301        assert!(result.is_ok());
302
303        let solution = result.unwrap();
304        match solution.metadata {
305            SolutionMetadata::Heat {
306                alpha,
307                eigenvalues,
308                coefficients,
309            } => {
310                assert_eq!(alpha, expr!(1));
311                assert!(!eigenvalues.is_empty());
312                assert!(!coefficients.is_empty());
313            }
314            _ => panic!("Expected Heat metadata"),
315        }
316    }
317
318    #[test]
319    fn test_heat_solver_with_max_terms() {
320        let solver = HeatEquationSolver::with_max_terms(5);
321        assert_eq!(solver.max_terms, 5);
322    }
323
324    #[test]
325    fn test_heat_solver_default() {
326        let solver = HeatEquationSolver::default();
327        assert_eq!(solver.max_terms, 10);
328    }
329}