use glam::DVec3;
use pantometry::prelude::*;
fn steel_ball() -> Substance {
Substance::aluminium_6061()
}
fn ball_volume() -> Volume {
Volume::from_si(4.0 / 3.0 * std::f64::consts::PI * 0.01f64.powi(3))
}
fn ball_area() -> Area {
Area::from_si(4.0 * std::f64::consts::PI * 0.01f64.powi(2))
}
fn ball_mass() -> Mass {
steel_ball().mass_of(ball_volume())
}
fn bouncing() -> ContactSystem {
ContactSystem::new(
"ball",
&[Body::new(
ball_mass(),
LengthVec::m(0.0, 0.0, 0.5),
VelocityVec::ZERO,
)],
AccelerationVec::from_si(-DVec3::Z * G0.to_si()),
Ground::floor(),
Stiffness::from_si(2e5),
Damping::from_si(20.0),
)
}
fn warming_ball() -> LumpedMass {
LumpedMass::new(
"ball-temperature",
steel_ball(),
ball_volume(),
Length::mm(10.0),
Temperature::celsius(20.0),
Environment::still_air(Temperature::celsius(20.0), ball_area()),
)
}
#[test]
fn a_bouncing_ball_warms_itself() {
let contact = bouncing();
let start_energy = contact.mechanical_energy();
let restitution = contact.restitution();
assert!(
restitution > 0.1 && restitution < 0.8,
"the test needs a bounce that visibly loses energy, got e = {restitution:.3}"
);
let mut sim = Simulation::new(Schedule::Multirate)
.conservation_tolerance(2e-2)
.with(contact)
.with(warming_ball());
for _ in 0..200 {
sim.advance(Time::ms(10.0))
.expect("mechanical energy plus heat must be conserved across the bus");
}
assert!((sim.time().to_si() - 2.0).abs() < 1e-9);
let crossed = sim.bus().total_consumed(quantity::ENERGY);
assert!(
crossed > 0.0,
"the dashpot should have dissipated something"
);
assert!(sim.bus().unclaimed().next().is_none());
let end_energy = sim
.domain_as::<ContactSystem>("ball")
.expect("the contact system is still in the simulation")
.mechanical_energy();
let lost = start_energy.to_si() - end_energy.to_si();
assert!(
lost > 0.0,
"the ball must have lost mechanical energy, got {lost:.4} J"
);
assert!(
(crossed / lost - 1.0).abs() < 2.5e-2,
"the joules that crossed the bus are the joules that went missing: \
{crossed:.4} J crossed against {lost:.4} J lost"
);
}
#[test]
fn the_rise_from_a_fall_is_tiny_and_independent_of_mass() {
let mass = ball_mass();
assert!(
(mass.to_si() * 1e3 - 11.31).abs() < 0.05,
"a 20 mm aluminium ball is 11.3 g, got {} g",
mass.to_si() * 1e3
);
let drop = Length::m(0.5);
let potential: Energy = Energy::from_si(mass.to_si() * G0.to_si() * drop.to_si());
assert!(
(potential.to_si() * 1e3 - 55.4).abs() < 0.5,
"half a metre is 55 mJ, got {} mJ",
potential.to_si() * 1e3
);
let capacity: HeatCapacity = steel_ball()
.heat_capacity(ball_volume())
.expect("aluminium has a specific heat");
assert!(
(capacity.to_si() - 10.13).abs() < 0.05,
"the ball holds 10.1 J/K, got {:?}",
capacity.to_si()
);
let rise: Temperature = potential / capacity;
assert!(
(rise.to_si() * 1e3 - 5.47).abs() < 0.05,
"the whole drop is 5.5 mK, got {} mK",
rise.to_si() * 1e3
);
assert!(
rise.to_si() < 0.01,
"a bouncing ball does not get warm, and the model should say so"
);
let specific_heat = steel_ball()
.thermal
.expect("aluminium is thermal")
.specific_heat;
let closed_form = G0.to_si() * drop.to_si() / specific_heat.to_si();
assert!(
(rise.to_si() / closed_form - 1.0).abs() < 1e-12,
"the rise should be g h / c_p exactly: {} vs {closed_form}",
rise.to_si()
);
}
#[test]
fn a_lossless_contact_publishes_nothing_to_lose() {
let lossless = ContactSystem::new(
"ball",
&[Body::new(
ball_mass(),
LengthVec::m(0.0, 0.0, 0.5),
VelocityVec::ZERO,
)],
AccelerationVec::from_si(-DVec3::Z * G0.to_si()),
Ground::floor(),
Stiffness::from_si(2e5),
Damping::from_si(0.0),
);
assert!((lossless.restitution() - 1.0).abs() < 1e-15);
let mut sim = Simulation::new(Schedule::Multirate)
.conservation_tolerance(2e-2)
.with(lossless);
for _ in 0..100 {
sim.advance(Time::ms(10.0))
.expect("nothing is dissipated, so nothing is unclaimed");
}
assert_eq!(sim.bus().total_consumed(quantity::ENERGY), 0.0);
}
#[test]
fn the_kernel_polices_momentum_as_readily_as_energy() {
let mut sim = Simulation::new(Schedule::Multirate)
.conservation_tolerance(1e-11)
.with(NBody::new(
"binary",
&[
Body::new(
Mass::kg(5e12),
LengthVec::m(-500.0, 0.0, 0.0),
VelocityVec::m_per_s(0.0, 0.4, 0.0),
),
Body::new(
Mass::kg(5e12),
LengthVec::m(500.0, 0.0, 0.0),
VelocityVec::m_per_s(0.0, -0.4, 0.0),
),
],
));
for _ in 0..50 {
sim.advance(Time::s(10.0))
.expect("pairwise forces cancel, so momentum cannot move");
}
let ledger = sim.ledger();
for axis in [
pantometry::mechanics::conserved::MOMENTUM_X,
pantometry::mechanics::conserved::MOMENTUM_Y,
pantometry::mechanics::conserved::MOMENTUM_Z,
] {
let p = ledger.get(axis).expect("the domain reports every axis");
assert!(p.abs() < 1e-6, "{axis} drifted to {p:e}");
}
}