use dualis::prelude::*;
struct Lamp {
watts: f64,
reserve: f64,
saved: Option<f64>,
}
impl Domain for Lamp {
fn name(&self) -> &str {
"lamp"
}
fn kind(&self) -> Kind {
Kind::QuasiStatic
}
fn step(&mut self, _t: Time, dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
let j = (self.watts * dt.to_si()).min(self.reserve);
self.reserve -= j;
bus.publish(HEAT, j);
Ok(())
}
fn ledger(&self) -> Ledger {
Ledger::new().with(quantity::ENERGY, self.reserve)
}
fn checkpoint(&mut self) {
self.saved = Some(self.reserve);
}
fn restore(&mut self) {
if let Some(r) = self.saved {
self.reserve = r;
}
}
fn supports_restore(&self) -> bool {
true
}
}
fn grey_aluminium() -> Substance {
let mut s = Substance::aluminium_6061();
if let Some(t) = s.thermal.as_mut() {
t.emissivity = 0.0;
}
s
}
fn plate() -> LumpedMass {
LumpedMass::new(
"plate",
grey_aluminium(),
Volume::from_si(60e-3 * 60e-3 * 3e-3),
Length::mm(1.5),
Temperature::celsius(20.0),
Environment::still_air(
Temperature::celsius(20.0),
Area::from_si(2.0 * 60e-3 * 60e-3),
),
)
}
fn rise_after(schedule: Schedule, outer: Time, steps: usize) -> f64 {
let mut sim = Simulation::new(schedule)
.conservation_tolerance(1e-6)
.with(Lamp {
watts: 2.0,
reserve: f64::INFINITY,
saved: None,
})
.with(plate());
for _ in 0..steps {
sim.advance(outer).expect("the books close");
}
sim.domain_as::<LumpedMass>("plate")
.expect("the plate is there")
.rise()
.to_si()
}
#[test]
fn multirate_now_beats_staggered_because_a_substep_takes_only_its_share() {
let p = plate();
let c = p.heat_capacity().to_si();
let tau = p.time_constant().to_si();
let ha = c / tau;
let analytic = 2.0 / ha * (1.0 - (-600.0 / tau).exp());
let coarse = Time::from_si(300.0);
let stag = (rise_after(Schedule::Staggered, coarse, 2) - analytic).abs();
let multi = (rise_after(Schedule::Multirate, coarse, 2) - analytic).abs();
assert!(
multi * 5.0 < stag,
"subcycling should now be worth something: staggered off by {stag:.4} K, multirate by {multi:.4} K"
);
let at_300 = rise_after(Schedule::Multirate, Time::from_si(300.0), 2);
let at_150 = rise_after(Schedule::Multirate, Time::from_si(150.0), 4);
assert_eq!(
at_300, at_150,
"the same substep should give the same answer however the outer steps are grouped"
);
let fine = (rise_after(Schedule::Multirate, Time::from_si(75.0), 8) - analytic).abs();
assert!(
fine < multi,
"a 37.5 s substep should beat a 50 s one: {fine:.4} K against {multi:.4} K"
);
}
#[test]
fn a_channel_apportioned_over_substeps_ends_exactly_empty() {
for n in [3usize, 7, 64, 1000] {
let mut bus = Exchange::new();
let dt = Time::from_si(1.0);
bus.covering(dt);
bus.publish(HEAT, 1.234_567_890_123e9);
let h = Time::from_si(1.0 / n as f64);
let mut got = 0.0;
for _ in 0..n {
got += bus.take_share(HEAT, h);
}
assert_eq!(
bus.peek(HEAT),
0.0,
"{n} substeps left {} on the channel",
bus.peek(HEAT)
);
assert!(
(got / 1.234_567_890_123e9 - 1.0).abs() < 1e-9,
"{n} substeps collected {got}"
);
assert!(
bus.unclaimed().next().is_none(),
"{n}: something is unclaimed"
);
}
}
#[test]
fn an_unknown_interval_hands_over_everything() {
let mut bus = Exchange::new();
bus.publish(HEAT, 42.0);
assert_eq!(bus.take_share(HEAT, Time::from_si(0.1)), 42.0);
assert_eq!(bus.peek(HEAT), 0.0);
}
#[test]
fn a_lumped_mass_survives_an_iterative_sweep() {
let mut sim = Simulation::new(Schedule::Iterative {
max_iter: 3,
tol: 0.0,
})
.conservation_tolerance(1e-9)
.with(Lamp {
watts: 2.0,
reserve: 1e9,
saved: None,
})
.with(plate());
for _ in 0..4 {
sim.advance(Time::from_si(10.0))
.expect("an iterative sweep must not create energy");
}
}