use dualis_units::{Length, LengthVec, Time};
use glam::DVec3;
pub trait ScalarField {
fn at(&self, p: LengthVec, t: Time) -> f64;
fn gradient(&self, p: LengthVec, t: Time, h: Length) -> DVec3 {
let step = h.to_si();
let mut out = DVec3::ZERO;
for axis in 0..3 {
let mut d = DVec3::ZERO;
d[axis] = step;
let plus = self.at(p + LengthVec::from_si(d), t);
let minus = self.at(p - LengthVec::from_si(d), t);
out[axis] = (plus - minus) / (2.0 * step);
}
out
}
fn laplacian(&self, p: LengthVec, t: Time, h: Length) -> f64 {
let step = h.to_si();
let centre = self.at(p, t);
let mut sum = 0.0;
for axis in 0..3 {
let mut d = DVec3::ZERO;
d[axis] = step;
sum += self.at(p + LengthVec::from_si(d), t) + self.at(p - LengthVec::from_si(d), t)
- 2.0 * centre;
}
sum / (step * step)
}
fn rate(&self, p: LengthVec, t: Time, dt: Time) -> f64 {
(self.at(p, t + dt) - self.at(p, t - dt)) / (2.0 * dt.to_si())
}
}
pub trait VectorField {
fn at(&self, p: LengthVec, t: Time) -> DVec3;
fn divergence(&self, p: LengthVec, t: Time, h: Length) -> f64 {
let step = h.to_si();
let mut sum = 0.0;
for axis in 0..3 {
let mut d = DVec3::ZERO;
d[axis] = step;
sum += (self.at(p + LengthVec::from_si(d), t)[axis]
- self.at(p - LengthVec::from_si(d), t)[axis])
/ (2.0 * step);
}
sum
}
fn curl(&self, p: LengthVec, t: Time, h: Length) -> DVec3 {
let step = h.to_si();
let d = |axis: usize| {
let mut e = DVec3::ZERO;
e[axis] = step;
let plus = self.at(p + LengthVec::from_si(e), t);
let minus = self.at(p - LengthVec::from_si(e), t);
(plus - minus) / (2.0 * step)
};
let (dx, dy, dz) = (d(0), d(1), d(2));
DVec3::new(dy.z - dz.y, dz.x - dx.z, dx.y - dy.x)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Uniform(pub f64);
impl ScalarField for Uniform {
fn at(&self, _p: LengthVec, _t: Time) -> f64 {
self.0
}
}
pub struct Analytic<F>(pub F);
impl<F: Fn(LengthVec, Time) -> f64> ScalarField for Analytic<F> {
fn at(&self, p: LengthVec, t: Time) -> f64 {
(self.0)(p, t)
}
}
impl<F: Fn(LengthVec, Time) -> DVec3> VectorField for Analytic<F> {
fn at(&self, p: LengthVec, t: Time) -> DVec3 {
(self.0)(p, t)
}
}
#[cfg(test)]
mod tests {
use super::*;
const H: Length = Length::from_si(1e-4);
#[test]
fn derivatives_match_the_closed_form() {
let f = Analytic(|p: LengthVec, _t: Time| {
let v = p.to_si();
v.x * v.x + 2.0 * v.y * v.y + 3.0 * v.z * v.z
});
let p = LengthVec::m(0.3, -0.2, 0.5);
let t = Time::ZERO;
let grad = f.gradient(p, t, H);
let expected = DVec3::new(0.6, -0.8, 3.0);
assert!((grad - expected).length() < 1e-9, "got {grad}");
let lap = f.laplacian(p, t, H);
assert!((lap - 12.0).abs() < 1e-6, "got {lap}");
}
#[test]
fn divergence_and_curl_match_the_closed_form() {
let omega = 3.0;
let rotation = Analytic(|p: LengthVec, _t: Time| {
let v = p.to_si();
DVec3::new(-3.0 * v.y, 3.0 * v.x, 0.0)
});
let p = LengthVec::m(0.4, 0.1, -0.2);
let t = Time::ZERO;
assert!(
VectorField::divergence(&rotation, p, t, H).abs() < 1e-9,
"a rotation moves fluid around, not outwards"
);
let curl = rotation.curl(p, t, H);
assert!(
(curl - DVec3::new(0.0, 0.0, 2.0 * omega)).length() < 1e-9,
"got {curl}"
);
}
#[test]
fn a_radial_field_diverges_without_rotating() {
let outflow = Analytic(|p: LengthVec, _t: Time| p.to_si());
let p = LengthVec::m(0.2, -0.4, 0.7);
let t = Time::ZERO;
assert!((VectorField::divergence(&outflow, p, t, H) - 3.0).abs() < 1e-9);
assert!(outflow.curl(p, t, H).length() < 1e-9);
}
#[test]
fn a_uniform_field_has_no_derivatives_of_any_kind() {
let u = Uniform(4.2);
let p = LengthVec::m(1.0, 2.0, 3.0);
assert_eq!(u.at(p, Time::s(9.0)), 4.2);
assert_eq!(u.gradient(p, Time::ZERO, H), DVec3::ZERO);
assert_eq!(u.laplacian(p, Time::ZERO, H), 0.0);
assert_eq!(u.rate(p, Time::ZERO, Time::s(0.1)), 0.0);
}
#[test]
fn time_derivatives_match_the_closed_form() {
let f = Analytic(|_p: LengthVec, t: Time| t.to_si() * t.to_si());
let rate = f.rate(LengthVec::ZERO, Time::s(3.0), Time::s(1e-4));
assert!((rate - 6.0).abs() < 1e-9, "got {rate}");
}
}