mathhook_core/calculus/pde/
separation_of_variables.rs

1//! Separation of variables method for PDEs
2//!
3//! This module implements complete separation of variables including:
4//! - Eigenvalue problem solving with boundary conditions
5//! - Fourier coefficient computation from initial conditions
6//! - Complete series solution assembly
7//!
8//! # Implementation
9//!
10//! For standard PDEs like heat and wave equations:
11//! 1. Assume product solution: u(x,t) = X(x)T(t)
12//! 2. Apply boundary conditions → solve eigenvalue problem for X(x)
13//! 3. Solve temporal ODE for T(t)
14//! 4. Apply initial conditions → compute Fourier coefficients
15//! 5. Assemble infinite series solution
16
17use crate::calculus::pde::common::eigenvalue_problem::solve_sturm_liouville;
18use crate::calculus::pde::common::fourier_coefficients::compute_fourier_coefficients;
19use crate::calculus::pde::types::{BoundaryCondition, InitialCondition, Pde};
20use crate::core::{Expression, Symbol};
21use crate::expr;
22
23/// Result of applying separation of variables
24#[derive(Debug, Clone, PartialEq)]
25pub struct SeparatedSolution {
26    /// The separated functions (e.g., X(x), T(t) for u(x,t) = X(x)T(t))
27    pub functions: Vec<Expression>,
28    /// The separation constants (λ₀, λ₁, ...)
29    pub constants: Vec<Expression>,
30    /// The general product solution (before applying ICs)
31    pub solution: Expression,
32    /// Computed eigenvalues from boundary conditions
33    pub eigenvalues: Vec<Expression>,
34    /// Computed eigenfunctions from boundary conditions
35    pub eigenfunctions: Vec<Expression>,
36    /// Fourier coefficients from initial conditions
37    pub coefficients: Vec<Expression>,
38}
39
40/// Applies separation of variables to a PDE with boundary and initial conditions
41///
42/// This is the complete implementation that:
43/// 1. Parses boundary conditions
44/// 2. Solves eigenvalue problem
45/// 3. Computes Fourier coefficients
46/// 4. Assembles complete solution
47///
48/// # Arguments
49///
50/// * `pde` - The PDE to solve
51/// * `boundary_conditions` - Spatial boundary conditions (must have exactly 2)
52/// * `initial_conditions` - Temporal initial conditions
53///
54/// # Returns
55///
56/// Complete separated solution with eigenvalues, eigenfunctions, and coefficients
57///
58/// # Examples
59///
60/// ```rust
61/// use mathhook_core::calculus::pde::separation_of_variables::separate_variables;
62/// use mathhook_core::calculus::pde::types::{Pde, BoundaryCondition, InitialCondition};
63/// use mathhook_core::{symbol, expr};
64///
65/// let u = symbol!(u);
66/// let x = symbol!(x);
67/// let t = symbol!(t);
68/// let equation = expr!(u);
69/// let pde = Pde::new(equation, u, vec![x.clone(), t]);
70///
71/// let bc_left = BoundaryCondition::dirichlet_at(x.clone(), expr!(0), expr!(0));
72/// let bc_right = BoundaryCondition::dirichlet_at(x, expr!(pi), expr!(0));
73/// let bcs = vec![bc_left, bc_right];
74///
75/// let sin_x = expr!(sin(x));
76/// let ic = InitialCondition::value(sin_x);
77/// let ics = vec![ic];
78///
79/// let result = separate_variables(&pde, &bcs, &ics);
80/// assert!(result.is_ok());
81/// ```
82pub fn separate_variables(
83    pde: &Pde,
84    boundary_conditions: &[BoundaryCondition],
85    initial_conditions: &[InitialCondition],
86) -> Result<SeparatedSolution, String> {
87    let num_vars = pde.independent_vars.len();
88
89    if num_vars < 2 {
90        return Err("Separation of variables requires at least 2 independent variables".to_owned());
91    }
92
93    let functions = create_separated_functions(&pde.independent_vars);
94    let constants = create_separation_constants(num_vars - 1);
95    let solution = construct_product_solution(&functions);
96
97    if boundary_conditions.is_empty() {
98        return Ok(SeparatedSolution {
99            functions,
100            constants,
101            solution,
102            eigenvalues: Vec::new(),
103            eigenfunctions: Vec::new(),
104            coefficients: Vec::new(),
105        });
106    }
107
108    if boundary_conditions.len() != 2 {
109        return Err(format!(
110            "Expected exactly 2 boundary conditions, got {}",
111            boundary_conditions.len()
112        ));
113    }
114
115    let eigenvalue_solution =
116        solve_sturm_liouville(&boundary_conditions[0], &boundary_conditions[1], 10)?;
117
118    let coefficients = if initial_conditions.is_empty() {
119        Vec::new()
120    } else {
121        compute_fourier_coefficients(
122            &initial_conditions[0],
123            &eigenvalue_solution.eigenfunctions,
124            &eigenvalue_solution.domain,
125            &eigenvalue_solution.variable,
126        )?
127    };
128
129    Ok(SeparatedSolution {
130        functions,
131        constants,
132        solution,
133        eigenvalues: eigenvalue_solution.eigenvalues,
134        eigenfunctions: eigenvalue_solution.eigenfunctions,
135        coefficients,
136    })
137}
138
139/// Construct complete series solution from eigenvalues and coefficients
140///
141/// Builds: u(x,t) = Σₙ cₙ Xₙ(x) Tₙ(t)
142///
143/// # Arguments
144///
145/// * `coefficients` - Fourier coefficients cₙ
146/// * `spatial_eigenfunctions` - Spatial eigenfunctions Xₙ(x)
147/// * `temporal_solutions` - Temporal solutions Tₙ(t)
148/// * `num_terms` - Number of terms to include in series
149///
150/// # Returns
151///
152/// Series solution expression
153pub fn construct_series_solution(
154    coefficients: &[Expression],
155    spatial_eigenfunctions: &[Expression],
156    temporal_solutions: &[Expression],
157    num_terms: usize,
158) -> Expression {
159    let mut terms = Vec::new();
160
161    let max_terms = num_terms.min(coefficients.len());
162
163    for n in 0..max_terms {
164        let c_n = &coefficients[n];
165        let x_n = &spatial_eigenfunctions[n];
166        let t_n = &temporal_solutions[n];
167
168        let term = Expression::mul(vec![c_n.clone(), x_n.clone(), t_n.clone()]);
169        terms.push(term);
170    }
171
172    if terms.is_empty() {
173        return expr!(0);
174    }
175
176    Expression::add(terms)
177}
178
179/// Create separated functions F(x), G(y), etc. for each variable
180fn create_separated_functions(vars: &[Symbol]) -> Vec<Expression> {
181    vars.iter()
182        .map(|var| Expression::function("F", vec![Expression::symbol(var.clone())]))
183        .collect()
184}
185
186/// Create separation constants λ₀, λ₁, etc.
187fn create_separation_constants(count: usize) -> Vec<Expression> {
188    (0..count)
189        .map(|i| {
190            let lambda = Symbol::new(format!("lambda_{}", i));
191            Expression::symbol(lambda)
192        })
193        .collect()
194}
195
196/// Construct product solution u = F(x)G(y)H(t)...
197fn construct_product_solution(functions: &[Expression]) -> Expression {
198    if functions.is_empty() {
199        return expr!(1);
200    }
201
202    Expression::mul(functions.to_vec())
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{expr, symbol};
209    use std::slice::from_ref;
210
211    #[test]
212    fn test_separate_variables_basic_no_bc() {
213        let u = symbol!(u);
214        let x = symbol!(x);
215        let t = symbol!(t);
216        let equation = expr!(u);
217        let pde = Pde::new(equation, u, vec![x, t]);
218
219        let result = separate_variables(&pde, &[], &[]);
220        assert!(result.is_ok());
221
222        let solution = result.unwrap();
223        assert_eq!(solution.functions.len(), 2);
224        assert_eq!(solution.constants.len(), 1);
225        assert!(solution.eigenvalues.is_empty());
226        assert!(solution.eigenfunctions.is_empty());
227    }
228
229    #[test]
230    fn test_separate_variables_with_dirichlet_bc() {
231        let u = symbol!(u);
232        let x = symbol!(x);
233        let t = symbol!(t);
234        let equation = expr!(u);
235        let pde = Pde::new(equation, u, vec![x.clone(), t]);
236
237        let bc_left = BoundaryCondition::dirichlet_at(x.clone(), expr!(0), expr!(0));
238        let bc_right = BoundaryCondition::dirichlet_at(x, expr!(pi), expr!(0));
239        let bcs = vec![bc_left, bc_right];
240
241        let result = separate_variables(&pde, &bcs, &[]);
242        assert!(result.is_ok());
243
244        let solution = result.unwrap();
245        assert_eq!(solution.eigenvalues.len(), 10);
246        assert_eq!(solution.eigenfunctions.len(), 10);
247    }
248
249    #[test]
250    fn test_separate_variables_with_neumann_bc() {
251        let u = symbol!(u);
252        let x = symbol!(x);
253        let t = symbol!(t);
254        let equation = expr!(u);
255        let pde = Pde::new(equation, u, vec![x.clone(), t]);
256
257        let bc_left = BoundaryCondition::neumann_at(x.clone(), expr!(0), expr!(0));
258        let bc_right = BoundaryCondition::neumann_at(x, expr!(pi), expr!(0));
259        let bcs = vec![bc_left, bc_right];
260
261        let result = separate_variables(&pde, &bcs, &[]);
262        assert!(result.is_ok());
263
264        let solution = result.unwrap();
265        assert_eq!(solution.eigenvalues.len(), 10);
266        assert_eq!(solution.eigenfunctions.len(), 10);
267    }
268
269    #[test]
270    fn test_separate_variables_with_ic() {
271        let u = symbol!(u);
272        let x = symbol!(x);
273        let t = symbol!(t);
274        let equation = expr!(u);
275        let pde = Pde::new(equation, u, vec![x.clone(), t]);
276
277        let bc_left = BoundaryCondition::dirichlet_at(x.clone(), expr!(0), expr!(0));
278        let bc_right = BoundaryCondition::dirichlet_at(x.clone(), expr!(pi), expr!(0));
279        let bcs = vec![bc_left, bc_right];
280
281        let sin_x = Expression::function("sin", vec![Expression::symbol(x)]);
282        let ic = InitialCondition::value(sin_x);
283        let ics = vec![ic];
284
285        let result = separate_variables(&pde, &bcs, &ics);
286        assert!(result.is_ok());
287
288        let solution = result.unwrap();
289        assert_eq!(solution.coefficients.len(), 10);
290    }
291
292    #[test]
293    fn test_separate_variables_insufficient_bcs() {
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.clone(), t]);
299
300        let bc = BoundaryCondition::dirichlet_at(x, expr!(0), expr!(0));
301        let bcs = vec![bc];
302
303        let result = separate_variables(&pde, &bcs, &[]);
304        assert!(result.is_err());
305    }
306
307    #[test]
308    fn test_separate_variables_three_vars() {
309        let u = symbol!(u);
310        let x = symbol!(x);
311        let y = symbol!(y);
312        let t = symbol!(t);
313        let equation = expr!(u);
314        let pde = Pde::new(equation, u, vec![x, y, t]);
315
316        let result = separate_variables(&pde, &[], &[]);
317        assert!(result.is_ok());
318
319        let solution = result.unwrap();
320        assert_eq!(solution.functions.len(), 3);
321        assert_eq!(solution.constants.len(), 2);
322    }
323
324    #[test]
325    fn test_separate_variables_insufficient_vars() {
326        let u = symbol!(u);
327        let x = symbol!(x);
328        let equation = expr!(u);
329        let pde = Pde::new(equation, u, vec![x]);
330
331        let result = separate_variables(&pde, &[], &[]);
332        assert!(result.is_err());
333    }
334
335    #[test]
336    fn test_create_separated_functions() {
337        let x = symbol!(x);
338        let t = symbol!(t);
339        let vars = vec![x, t];
340
341        let functions = create_separated_functions(&vars);
342        assert_eq!(functions.len(), 2);
343    }
344
345    #[test]
346    fn test_create_separation_constants() {
347        let constants = create_separation_constants(2);
348        assert_eq!(constants.len(), 2);
349    }
350
351    #[test]
352    fn test_construct_product_solution_empty() {
353        let solution = construct_product_solution(&[]);
354        assert_eq!(solution, expr!(1));
355    }
356
357    #[test]
358    fn test_construct_product_solution_single() {
359        let x = symbol!(x);
360        let f = Expression::function("F", vec![Expression::symbol(x)]);
361        let solution = construct_product_solution(from_ref(&f));
362        assert_eq!(solution, f);
363    }
364
365    #[test]
366    fn test_construct_series_solution_single_term() {
367        let x = symbol!(x);
368        let _t = symbol!(t);
369
370        let coefficients = vec![expr!(1)];
371        let spatial = vec![Expression::function("sin", vec![Expression::symbol(x)])];
372        let temporal = vec![Expression::function("exp", vec![expr!(-t)])];
373
374        let solution = construct_series_solution(&coefficients, &spatial, &temporal, 1);
375
376        assert!(matches!(solution, Expression::Mul(_)));
377    }
378
379    #[test]
380    fn test_construct_series_solution_multiple_terms() {
381        let x = symbol!(x);
382        let t = symbol!(t);
383
384        let coefficients = vec![expr!(1), expr!(2)];
385        let spatial = vec![
386            Expression::function("sin", vec![Expression::symbol(x.clone())]),
387            Expression::function(
388                "sin",
389                vec![Expression::mul(vec![
390                    Expression::integer(2),
391                    Expression::symbol(x),
392                ])],
393            ),
394        ];
395        let temporal = vec![
396            Expression::function(
397                "exp",
398                vec![Expression::mul(vec![
399                    Expression::integer(-1),
400                    Expression::symbol(t.clone()),
401                ])],
402            ),
403            Expression::function(
404                "exp",
405                vec![Expression::mul(vec![
406                    Expression::integer(-4),
407                    Expression::symbol(t),
408                ])],
409            ),
410        ];
411
412        let solution = construct_series_solution(&coefficients, &spatial, &temporal, 2);
413
414        assert!(matches!(solution, Expression::Add(_)));
415    }
416
417    #[test]
418    fn test_construct_series_solution_empty() {
419        let solution = construct_series_solution(&[], &[], &[], 0);
420        assert_eq!(solution, expr!(0));
421    }
422
423    #[test]
424    fn test_separated_solution_clone() {
425        let u = symbol!(u);
426        let x = symbol!(x);
427        let t = symbol!(t);
428        let equation = expr!(u);
429        let pde = Pde::new(equation, u, vec![x, t]);
430
431        let result = separate_variables(&pde, &[], &[]);
432        assert!(result.is_ok());
433
434        let solution = result.unwrap();
435        let _cloned = solution.clone();
436    }
437}