mathhook_core/calculus/pde/standard/
heat.rs1use 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
26pub struct HeatEquationSolver {
28 max_terms: usize,
29}
30
31impl HeatEquationSolver {
32 pub fn new() -> Self {
34 Self { max_terms: 10 }
35 }
36
37 pub fn with_max_terms(max_terms: usize) -> Self {
39 Self { max_terms }
40 }
41
42 #[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}