use numra_core::Scalar;
#[derive(Clone, Debug)]
pub struct Domain1D<S: Scalar> {
pub left: Bound<S>,
pub right: Bound<S>,
pub n_points: usize,
}
impl<S: Scalar> Domain1D<S> {
pub fn fixed(x_min: S, x_max: S, n_points: usize) -> Self {
Self {
left: Bound::Fixed(x_min),
right: Bound::Fixed(x_max),
n_points,
}
}
pub fn with_moving_right(x_min: S, s_initial: S, n_points: usize) -> Self {
Self {
left: Bound::Fixed(x_min),
right: Bound::Moving(MovingBound::new(s_initial)),
n_points,
}
}
pub fn bounds(&self) -> (S, S) {
(self.left.position(), self.right.position())
}
pub fn length(&self) -> S {
let (lo, hi) = self.bounds();
hi - lo
}
pub fn grid_points(&self) -> Vec<S> {
let (lo, hi) = self.bounds();
let n = self.n_points;
(0..n)
.map(|i| lo + (hi - lo) * S::from_usize(i) / S::from_usize(n - 1))
.collect()
}
pub fn dx(&self) -> S {
self.length() / S::from_usize(self.n_points - 1)
}
}
#[derive(Clone, Debug)]
pub enum Bound<S: Scalar> {
Fixed(S),
Moving(MovingBound<S>),
}
impl<S: Scalar> Bound<S> {
pub fn position(&self) -> S {
match self {
Bound::Fixed(x) => *x,
Bound::Moving(mb) => mb.position,
}
}
pub fn is_fixed(&self) -> bool {
matches!(self, Bound::Fixed(_))
}
pub fn is_moving(&self) -> bool {
matches!(self, Bound::Moving(_))
}
pub fn set_position(&mut self, new_pos: S) {
if let Bound::Moving(mb) = self {
mb.position = new_pos;
}
}
}
#[derive(Clone, Debug)]
pub struct MovingBound<S: Scalar> {
pub position: S,
pub stefan: Option<StefanCondition<S>>,
}
impl<S: Scalar> MovingBound<S> {
pub fn new(position: S) -> Self {
Self {
position,
stefan: None,
}
}
pub fn with_stefan(position: S, stefan: StefanCondition<S>) -> Self {
Self {
position,
stefan: Some(stefan),
}
}
}
#[derive(Clone, Debug)]
pub struct StefanCondition<S: Scalar> {
pub coefficient: S,
}
impl<S: Scalar> StefanCondition<S> {
pub fn new(coefficient: S) -> Self {
Self { coefficient }
}
pub fn velocity(&self, flux: S) -> S {
-self.coefficient * flux
}
}
#[derive(Clone)]
pub struct CoordinateTransform<S: Scalar> {
pub s: S,
}
impl<S: Scalar> CoordinateTransform<S> {
pub fn new(s: S) -> Self {
Self { s }
}
pub fn update(&mut self, s: S) {
self.s = s;
}
pub fn physical_to_computational(&self, x: S) -> S {
x / self.s
}
pub fn computational_to_physical(&self, xi: S) -> S {
xi * self.s
}
pub fn jacobian(&self) -> S {
self.s
}
pub fn transform_d1(&self, du_dxi: S) -> S {
du_dxi / self.s
}
pub fn transform_d2(&self, d2u_dxi2: S) -> S {
d2u_dxi2 / (self.s * self.s)
}
pub fn convection_coefficient(&self, dsdt: S, xi: S) -> S {
dsdt * xi / self.s
}
}
#[derive(Clone)]
pub struct FieldState1D<S: Scalar> {
pub values: Vec<S>,
pub domain: Domain1D<S>,
}
impl<S: Scalar> FieldState1D<S> {
pub fn new(values: Vec<S>, domain: Domain1D<S>) -> Self {
assert_eq!(values.len(), domain.n_points);
Self { values, domain }
}
pub fn get(&self, i: usize) -> S {
self.values[i]
}
pub fn set(&mut self, i: usize, value: S) {
self.values[i] = value;
}
pub fn flux_left(&self) -> S {
let dx = self.domain.dx();
let v = &self.values;
let n = v.len();
if n < 3 {
return (v[1] - v[0]) / dx;
}
(S::from_f64(-3.0) * v[0] + S::from_f64(4.0) * v[1] - v[2]) / (S::from_f64(2.0) * dx)
}
pub fn flux_right(&self) -> S {
let dx = self.domain.dx();
let v = &self.values;
let n = v.len();
if n < 3 {
return (v[n - 1] - v[n - 2]) / dx;
}
(S::from_f64(3.0) * v[n - 1] - S::from_f64(4.0) * v[n - 2] + v[n - 3])
/ (S::from_f64(2.0) * dx)
}
pub fn evaluate(&self, x: S) -> S {
let points = self.domain.grid_points();
let (lo, hi) = self.domain.bounds();
if x <= lo {
return self.values[0];
}
if x >= hi {
return *self.values.last().unwrap();
}
let dx = self.domain.dx();
let i = ((x - lo) / dx).to_f64().floor() as usize;
let i = i.min(self.values.len() - 2);
let x0 = points[i];
let x1 = points[i + 1];
let t = (x - x0) / (x1 - x0);
self.values[i] * (S::ONE - t) + self.values[i + 1] * t
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixed_domain() {
let domain = Domain1D::fixed(0.0_f64, 1.0, 11);
assert!((domain.length() - 1.0).abs() < 1e-10);
assert!((domain.dx() - 0.1).abs() < 1e-10);
let points = domain.grid_points();
assert_eq!(points.len(), 11);
assert!((points[0]).abs() < 1e-10);
assert!((points[10] - 1.0).abs() < 1e-10);
}
#[test]
fn test_moving_domain() {
let mut domain = Domain1D::with_moving_right(0.0_f64, 0.5, 11);
assert!((domain.length() - 0.5).abs() < 1e-10);
domain.right.set_position(0.8);
assert!((domain.length() - 0.8).abs() < 1e-10);
}
#[test]
fn test_stefan_condition() {
let stefan = StefanCondition::new(0.1_f64);
let velocity = stefan.velocity(10.0);
assert!((velocity - (-1.0)).abs() < 1e-10);
let velocity = stefan.velocity(-5.0);
assert!((velocity - 0.5).abs() < 1e-10);
}
#[test]
fn test_coordinate_transform() {
let transform = CoordinateTransform::new(2.0_f64);
let xi = transform.physical_to_computational(1.0);
assert!((xi - 0.5).abs() < 1e-10);
let x = transform.computational_to_physical(0.5);
assert!((x - 1.0).abs() < 1e-10);
assert!((transform.jacobian() - 2.0).abs() < 1e-10);
let du_dx = transform.transform_d1(4.0);
assert!((du_dx - 2.0).abs() < 1e-10);
let d2u_dx2 = transform.transform_d2(8.0);
assert!((d2u_dx2 - 2.0).abs() < 1e-10);
}
#[test]
fn test_field_state_flux() {
let domain = Domain1D::fixed(0.0_f64, 1.0, 11);
let values: Vec<f64> = (0..11).map(|i| i as f64 * 0.1).collect();
let field = FieldState1D::new(values, domain);
let flux_left = field.flux_left();
let flux_right = field.flux_right();
assert!((flux_left - 1.0).abs() < 0.01);
assert!((flux_right - 1.0).abs() < 0.01);
}
#[test]
fn test_field_evaluate() {
let domain = Domain1D::fixed(0.0_f64, 1.0, 11);
let values: Vec<f64> = (0..11).map(|i| i as f64 * 0.1).collect();
let field = FieldState1D::new(values, domain);
assert!((field.evaluate(0.0) - 0.0).abs() < 1e-10);
assert!((field.evaluate(0.5) - 0.5).abs() < 1e-10);
assert!((field.evaluate(1.0) - 1.0).abs() < 1e-10);
assert!((field.evaluate(0.25) - 0.25).abs() < 0.01);
assert!((field.evaluate(0.75) - 0.75).abs() < 0.01);
}
}