Skip to main content

numra_pde/
boundary.rs

1//! Boundary conditions for PDE problems.
2//!
3//! Author: Moussa Leblouba
4//! Date: 3 February 2026
5//! Modified: 2 May 2026
6
7use numra_core::Scalar;
8
9/// Trait for boundary conditions.
10pub trait BoundaryCondition<S: Scalar>: Clone {
11    /// Apply the boundary condition at the given time.
12    ///
13    /// Returns the boundary value (for Dirichlet) or modifies the ghost point (for Neumann).
14    fn apply(&self, t: S, interior_value: S, dx: S) -> S;
15
16    /// Get the boundary value directly (for Dirichlet-type BCs).
17    fn value(&self, _t: S) -> Option<S> {
18        None
19    }
20
21    /// Get the flux value (for Neumann-type BCs).
22    fn flux(&self, _t: S) -> Option<S> {
23        None
24    }
25
26    /// Whether this BC specifies the solution value (Dirichlet-type).
27    fn is_dirichlet(&self) -> bool;
28}
29
30/// Dirichlet boundary condition: u = g(t) at the boundary.
31#[derive(Clone, Debug)]
32pub struct DirichletBC<S: Scalar> {
33    /// Boundary value (constant or function of time)
34    value: DirichletValue<S>,
35}
36
37#[derive(Clone, Debug)]
38enum DirichletValue<S: Scalar> {
39    Constant(S),
40    // For time-varying BC, we'd need a Box<dyn Fn(S) -> S + Send + Sync>
41    // but for simplicity, keeping constant for now
42}
43
44impl<S: Scalar> DirichletBC<S> {
45    /// Create a constant Dirichlet BC.
46    pub fn new(value: S) -> Self {
47        Self {
48            value: DirichletValue::Constant(value),
49        }
50    }
51}
52
53impl<S: Scalar> BoundaryCondition<S> for DirichletBC<S> {
54    fn apply(&self, t: S, _interior_value: S, _dx: S) -> S {
55        self.value(t).unwrap()
56    }
57
58    fn value(&self, _t: S) -> Option<S> {
59        match &self.value {
60            DirichletValue::Constant(v) => Some(*v),
61        }
62    }
63
64    fn is_dirichlet(&self) -> bool {
65        true
66    }
67}
68
69/// Neumann boundary condition: ∂u/∂n = g(t) at the boundary.
70#[derive(Clone, Debug)]
71pub struct NeumannBC<S: Scalar> {
72    /// Normal derivative value (positive = outward flux)
73    flux: S,
74}
75
76impl<S: Scalar> NeumannBC<S> {
77    /// Create a constant Neumann BC.
78    pub fn new(flux: S) -> Self {
79        Self { flux }
80    }
81
82    /// Create a zero-flux (insulated) BC.
83    pub fn zero_flux() -> Self {
84        Self { flux: S::ZERO }
85    }
86}
87
88impl<S: Scalar> BoundaryCondition<S> for NeumannBC<S> {
89    fn apply(&self, _t: S, interior_value: S, dx: S) -> S {
90        // Ghost point value for central difference:
91        // (u[1] - u[-1]) / (2*dx) = flux
92        // => u[-1] = u[1] - 2*dx*flux
93        interior_value - S::from_f64(2.0) * dx * self.flux
94    }
95
96    fn flux(&self, _t: S) -> Option<S> {
97        Some(self.flux)
98    }
99
100    fn is_dirichlet(&self) -> bool {
101        false
102    }
103}
104
105/// Robin boundary condition: α*u + β*∂u/∂n = g at the boundary.
106#[derive(Clone, Debug)]
107pub struct RobinBC<S: Scalar> {
108    /// Coefficient for u
109    pub alpha: S,
110    /// Coefficient for ∂u/∂n
111    pub beta: S,
112    /// Right-hand side value
113    pub gamma: S,
114}
115
116impl<S: Scalar> RobinBC<S> {
117    /// Create a Robin BC: α*u + β*∂u/∂n = γ.
118    pub fn new(alpha: S, beta: S, gamma: S) -> Self {
119        Self { alpha, beta, gamma }
120    }
121
122    /// Create a convective BC: h*(u - u_ambient) = -k*∂u/∂n.
123    /// This is equivalent to: (h/k)*u + ∂u/∂n = (h/k)*u_ambient.
124    pub fn convective(h_over_k: S, u_ambient: S) -> Self {
125        Self {
126            alpha: h_over_k,
127            beta: S::ONE,
128            gamma: h_over_k * u_ambient,
129        }
130    }
131}
132
133impl<S: Scalar> BoundaryCondition<S> for RobinBC<S> {
134    fn apply(&self, _t: S, interior_value: S, dx: S) -> S {
135        // At left boundary: α*u[0] + β*(u[1] - u[-1])/(2*dx) = γ
136        // Solve for ghost point u[-1]:
137        // u[-1] = u[1] - (2*dx/β)*(γ - α*u[0])
138        // We don't have u[0] here, so this is an approximation
139        // using interior_value ≈ u[1]:
140        //
141        // For accuracy, the MOL solver should handle Robin BCs specially.
142        // This is a simple approximation.
143        let flux_approx = (self.gamma - self.alpha * interior_value) / self.beta;
144        interior_value - S::from_f64(2.0) * dx * flux_approx
145    }
146
147    fn is_dirichlet(&self) -> bool {
148        // Robin is neither pure Dirichlet nor pure Neumann
149        self.beta.abs() < S::from_f64(1e-10)
150    }
151}
152
153/// Periodic boundary condition.
154#[derive(Clone, Debug)]
155pub struct PeriodicBC;
156
157impl<S: Scalar> BoundaryCondition<S> for PeriodicBC {
158    fn apply(&self, _t: S, interior_value: S, _dx: S) -> S {
159        // For periodic BC, the ghost point equals the value from the other end.
160        // The MOL solver handles this by wrapping indices.
161        interior_value
162    }
163
164    fn is_dirichlet(&self) -> bool {
165        false
166    }
167}
168
169/// Generic boundary condition that boxes the specific type.
170#[derive(Clone)]
171pub struct BoxedBC<S: Scalar> {
172    inner: BcType<S>,
173}
174
175#[derive(Clone)]
176enum BcType<S: Scalar> {
177    Dirichlet(DirichletBC<S>),
178    Neumann(NeumannBC<S>),
179    Robin(RobinBC<S>),
180    Periodic,
181}
182
183impl<S: Scalar> BoxedBC<S> {
184    pub fn dirichlet(value: S) -> Self {
185        Self {
186            inner: BcType::Dirichlet(DirichletBC::new(value)),
187        }
188    }
189
190    pub fn neumann(flux: S) -> Self {
191        Self {
192            inner: BcType::Neumann(NeumannBC::new(flux)),
193        }
194    }
195
196    pub fn robin(alpha: S, beta: S, gamma: S) -> Self {
197        Self {
198            inner: BcType::Robin(RobinBC::new(alpha, beta, gamma)),
199        }
200    }
201
202    pub fn periodic() -> Self {
203        Self {
204            inner: BcType::Periodic,
205        }
206    }
207}
208
209impl<S: Scalar> BoundaryCondition<S> for BoxedBC<S> {
210    fn apply(&self, t: S, interior_value: S, dx: S) -> S {
211        match &self.inner {
212            BcType::Dirichlet(bc) => bc.apply(t, interior_value, dx),
213            BcType::Neumann(bc) => bc.apply(t, interior_value, dx),
214            BcType::Robin(bc) => bc.apply(t, interior_value, dx),
215            BcType::Periodic => interior_value,
216        }
217    }
218
219    fn value(&self, t: S) -> Option<S> {
220        match &self.inner {
221            BcType::Dirichlet(bc) => bc.value(t),
222            _ => None,
223        }
224    }
225
226    fn flux(&self, t: S) -> Option<S> {
227        match &self.inner {
228            BcType::Neumann(bc) => bc.flux(t),
229            _ => None,
230        }
231    }
232
233    fn is_dirichlet(&self) -> bool {
234        matches!(&self.inner, BcType::Dirichlet(_))
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_dirichlet_bc() {
244        let bc = DirichletBC::new(100.0_f64);
245        assert!(bc.is_dirichlet());
246        assert!((bc.value(0.0).unwrap() - 100.0).abs() < 1e-10);
247        assert!((bc.apply(0.0, 50.0, 0.1) - 100.0).abs() < 1e-10);
248    }
249
250    #[test]
251    fn test_neumann_bc() {
252        let bc = NeumannBC::new(10.0_f64);
253        assert!(!bc.is_dirichlet());
254        assert!((bc.flux(0.0).unwrap() - 10.0).abs() < 1e-10);
255
256        // Ghost point: u[-1] = u[1] - 2*dx*flux
257        // With interior_value = 50, dx = 0.1, flux = 10:
258        // ghost = 50 - 2*0.1*10 = 50 - 2 = 48
259        let ghost = bc.apply(0.0, 50.0, 0.1);
260        assert!((ghost - 48.0).abs() < 1e-10);
261    }
262
263    #[test]
264    fn test_zero_flux_bc() {
265        let bc = NeumannBC::<f64>::zero_flux();
266        assert!((bc.flux(0.0).unwrap()).abs() < 1e-10);
267
268        // Zero flux: ghost = interior
269        let ghost = bc.apply(0.0, 50.0, 0.1);
270        assert!((ghost - 50.0).abs() < 1e-10);
271    }
272
273    #[test]
274    fn test_boxed_bc() {
275        let bc = BoxedBC::dirichlet(100.0_f64);
276        assert!(bc.is_dirichlet());
277        assert!((bc.value(0.0).unwrap() - 100.0).abs() < 1e-10);
278
279        let bc = BoxedBC::neumann(10.0_f64);
280        assert!(!bc.is_dirichlet());
281        assert!((bc.flux(0.0).unwrap() - 10.0).abs() < 1e-10);
282    }
283}