mathhook_core/calculus/pde/standard/
heat.rs

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