use pantometry_core::conserved::quantity;
use pantometry_core::units::Time;
use pantometry_core::{
audit, audit_with, Domain, Exchange, Kind, Ledger, Schedule, Simulation, Tolerances, Violation,
};
struct Leaky {
energy: f64,
momentum: f64,
energy_leak: f64,
momentum_leak: f64,
}
impl Domain for Leaky {
fn name(&self) -> &str {
"leaky"
}
fn kind(&self) -> Kind {
Kind::Evolving
}
fn step(&mut self, _t: Time, _dt: Time, _bus: &mut Exchange) -> Result<(), Violation> {
self.energy *= 1.0 - self.energy_leak;
self.momentum *= 1.0 - self.momentum_leak;
Ok(())
}
fn ledger(&self) -> Ledger {
Ledger::new()
.with(quantity::ENERGY, self.energy)
.with(quantity::MOMENTUM, self.momentum)
}
}
fn leaky(energy_leak: f64, momentum_leak: f64) -> Leaky {
Leaky {
energy: 1000.0,
momentum: 1000.0,
energy_leak,
momentum_leak,
}
}
#[test]
fn a_single_tolerance_cannot_separate_two_schemes() {
let strict = Simulation::new(Schedule::Staggered)
.conservation_tolerance(1e-9)
.with(leaky(0.0, 1e-7));
let mut strict = strict;
let refused = strict
.advance(Time::from_si(1.0))
.expect_err("1e-9 refuses a momentum drift the scheme cannot avoid");
assert_eq!(refused.quantity, "momentum");
let mut loose = Simulation::new(Schedule::Staggered)
.conservation_tolerance(1e-6)
.with(leaky(1e-8, 1e-7));
loose
.advance(Time::from_si(1.0))
.expect("and 1e-6 lets a real energy leak straight through");
let after = loose.ledger().get(quantity::ENERGY).unwrap_or(0.0);
assert!(
(1000.0 - after) / 1000.0 > 5e-9,
"the energy really did leak: {after}"
);
}
#[test]
fn a_tolerance_per_quantity_catches_the_leak_and_allows_the_drift() {
let mut sim = Simulation::new(Schedule::Staggered)
.conservation_tolerance(1e-9)
.conservation_tolerance_for(quantity::MOMENTUM, 1e-6)
.with(leaky(0.0, 1e-7));
sim.advance(Time::from_si(1.0))
.expect("momentum at 1e-6 allows what the tree cannot avoid");
let mut sim = Simulation::new(Schedule::Staggered)
.conservation_tolerance(1e-9)
.conservation_tolerance_for(quantity::MOMENTUM, 1e-6)
.with(leaky(1e-8, 1e-7));
let caught = sim
.advance(Time::from_si(1.0))
.expect_err("and energy at 1e-9 still sees the leak");
assert_eq!(caught.quantity, "energy");
assert!(
(caught.tolerance - 1e-9).abs() < 1e-30,
"the violation should name the energy tolerance, said {}",
caught.tolerance
);
}
#[test]
fn the_default_and_the_overrides_are_independent() {
let a = Simulation::new(Schedule::Staggered)
.conservation_tolerance(1e-12)
.conservation_tolerance_for(quantity::MOMENTUM, 1e-6);
let b = Simulation::new(Schedule::Staggered)
.conservation_tolerance_for(quantity::MOMENTUM, 1e-6)
.conservation_tolerance(1e-12);
for sim in [&a, &b] {
assert_eq!(sim.tolerances().for_quantity(quantity::ENERGY), 1e-12);
assert_eq!(sim.tolerances().for_quantity(quantity::MOMENTUM), 1e-6);
assert_eq!(sim.tolerances().default_tolerance(), 1e-12);
}
assert_eq!(a.tolerances(), b.tolerances());
let named: Vec<(&str, f64)> = a.tolerances().overrides().collect();
assert_eq!(named, vec![(quantity::MOMENTUM, 1e-6)]);
}
#[test]
fn the_uniform_case_is_unchanged() {
let before = Ledger::new()
.with(quantity::ENERGY, 100.0)
.with(quantity::MOMENTUM, 50.0);
let after = Ledger::new()
.with(quantity::ENERGY, 100.0 * (1.0 - 1e-7))
.with(quantity::MOMENTUM, 50.0);
for tol in [1e-9, 1e-6, 1e-3] {
let old = audit("site", &before, &after, tol);
let new = audit_with("site", &before, &after, &Tolerances::uniform(tol));
assert_eq!(old.is_err(), new.is_err(), "at {tol}");
if let (Err(a), Err(b)) = (old, new) {
assert_eq!(a.quantity, b.quantity);
assert_eq!(a.tolerance, b.tolerance);
}
}
let t = Tolerances::uniform(1e-9).with(quantity::MOMENTUM, 1.0);
assert_eq!(t.for_quantity(quantity::CHARGE), 1e-9);
assert_eq!(t.for_quantity("a channel invented by a domain"), 1e-9);
}
#[test]
fn the_overrides_are_ordered_not_insertion_ordered() {
let one = Tolerances::uniform(1e-9)
.with(quantity::PHOTONS, 3.0)
.with(quantity::CHARGE, 1.0)
.with(quantity::MOMENTUM, 2.0);
let other = Tolerances::uniform(1e-9)
.with(quantity::MOMENTUM, 2.0)
.with(quantity::PHOTONS, 3.0)
.with(quantity::CHARGE, 1.0);
let names: Vec<&str> = one.overrides().map(|(q, _)| q).collect();
assert_eq!(names, vec!["charge", "momentum", "photons"]);
assert_eq!(
one.overrides().collect::<Vec<_>>(),
other.overrides().collect::<Vec<_>>()
);
assert_eq!(one, other);
}