use aequitas::systems::si::{quantities::Length, units::Meter};
use eunomia::{FloatElement, NumericElement, UnitScalar};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundaryCondition {
Dirichlet,
Neumann,
Periodic,
}
impl From<BoundaryCondition> for u32 {
#[inline]
fn from(value: BoundaryCondition) -> Self {
match value {
BoundaryCondition::Dirichlet => 0,
BoundaryCondition::Neumann => 1,
BoundaryCondition::Periodic => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LaplacianPolarity {
Laplacian,
NegativeLaplacian,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum LaplacianError {
#[error("Laplacian grid axes must contain at least two points: nx={nx}, ny={ny}")]
GridTooSmall {
nx: usize,
ny: usize,
},
#[error("Laplacian grid size overflows usize: nx={nx}, ny={ny}")]
GridSizeOverflow {
nx: usize,
ny: usize,
},
#[error("Laplacian spacing must be finite and positive: dx={dx}, dy={dy}")]
InvalidSpacing {
dx: String,
dy: String,
},
#[error("Laplacian input length mismatch: expected {expected}, actual {actual}")]
InputLength {
expected: usize,
actual: usize,
},
#[error("Laplacian output length mismatch: expected {expected}, actual {actual}")]
OutputLength {
expected: usize,
actual: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Laplacian2D<T> {
nx: usize,
ny: usize,
len: usize,
inverse_spacing_squared: [T; 2],
boundary: BoundaryCondition,
polarity: LaplacianPolarity,
}
impl<T> Laplacian2D<T>
where
T: FloatElement + UnitScalar,
{
pub fn new(
nx: usize,
ny: usize,
dx: Length<T>,
dy: Length<T>,
boundary: BoundaryCondition,
) -> Result<Self, LaplacianError> {
if nx < 2 || ny < 2 {
return Err(LaplacianError::GridTooSmall { nx, ny });
}
let len = nx
.checked_mul(ny)
.ok_or(LaplacianError::GridSizeOverflow { nx, ny })?;
let dx_m = dx.in_unit::<Meter>();
let dy_m = dy.in_unit::<Meter>();
if !dx_m.is_finite()
|| dx_m <= <T as NumericElement>::ZERO
|| !dy_m.is_finite()
|| dy_m <= <T as NumericElement>::ZERO
{
return Err(LaplacianError::InvalidSpacing {
dx: format!("{dx_m:?} m"),
dy: format!("{dy_m:?} m"),
});
}
Ok(Self {
nx,
ny,
len,
inverse_spacing_squared: [dx_m.recip().powi(2), dy_m.recip().powi(2)],
boundary,
polarity: LaplacianPolarity::Laplacian,
})
}
#[must_use]
pub const fn with_polarity(mut self, polarity: LaplacianPolarity) -> Self {
self.polarity = polarity;
self
}
#[must_use]
pub const fn nx(&self) -> usize {
self.nx
}
#[must_use]
pub const fn ny(&self) -> usize {
self.ny
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub const fn boundary(&self) -> BoundaryCondition {
self.boundary
}
#[must_use]
pub const fn polarity(&self) -> LaplacianPolarity {
self.polarity
}
#[must_use]
pub fn signed_inverse_spacing_squared(&self) -> [T; 2] {
match self.polarity {
LaplacianPolarity::Laplacian => self.inverse_spacing_squared,
LaplacianPolarity::NegativeLaplacian => [
<T as NumericElement>::ZERO - self.inverse_spacing_squared[0],
<T as NumericElement>::ZERO - self.inverse_spacing_squared[1],
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_positive_spacing() {
let error = Laplacian2D::new(
4,
4,
Length::from_unit::<Meter>(0.0f32),
Length::from_unit::<Meter>(1.0f32),
BoundaryCondition::Dirichlet,
)
.expect_err("zero spacing must be rejected");
assert!(matches!(error, LaplacianError::InvalidSpacing { .. }));
}
}