use materia::*;
use materia::crystal::Crystal;
use materia::material::PhaseState;
fn check_density(name: &str, substance: Substance, real: f32, tolerance: f32) {
let mat = compile(&substance);
let err = (mat.structural.density - real).abs() / real;
eprintln!(
"{name} density: {:.0} kg/m3 (real: {real:.0}, err: {:.0}%)",
mat.structural.density,
err * 100.0
);
assert!(
err < tolerance,
"{name} density error {:.0}% exceeds {:.0}% tolerance",
err * 100.0,
tolerance * 100.0
);
}
#[test]
fn iron_density() {
check_density("iron", Substance::Crystalline(Crystal::iron()), 7874.0, 0.50);
}
#[test]
fn copper_density() {
check_density("copper", Substance::Crystalline(Crystal::copper()), 8960.0, 0.50);
}
#[test]
fn gold_density() {
check_density("gold", Substance::Crystalline(Crystal::gold()), 19300.0, 0.50);
}
#[test]
fn aluminum_density() {
check_density("aluminum", Substance::Crystalline(Crystal::aluminum()), 2700.0, 0.55);
}
fn check_melting(name: &str, substance: Substance, real: f32, tolerance: f32) {
let mat = compile(&substance);
let mp = mat.thermal.melting_point.unwrap_or(f32::NAN);
let err = (mp - real).abs() / real.abs().max(1.0);
eprintln!(
"{name} melting point: {mp:.0} C (real: {real:.0}, err: {:.0}%)",
err * 100.0
);
assert!(
err < tolerance,
"{name} melting point error {:.0}% exceeds {:.0}% tolerance",
err * 100.0,
tolerance * 100.0
);
}
fn check_melting_abs(name: &str, substance: Substance, real: f32, abs_tolerance: f32) {
let mat = compile(&substance);
let mp = mat.thermal.melting_point.unwrap_or(f32::NAN);
let err = (mp - real).abs();
eprintln!(
"{name} melting point: {mp:.1} C (real: {real:.1}, abs err: {err:.1} C)",
);
assert!(
err < abs_tolerance,
"{name} melting point abs error {err:.1} C exceeds {abs_tolerance:.1} C tolerance"
);
}
#[test]
fn iron_melting() {
check_melting("iron", Substance::Crystalline(Crystal::iron()), 1538.0, 0.10);
}
#[test]
fn copper_melting() {
check_melting("copper", Substance::Crystalline(Crystal::copper()), 1085.0, 0.10);
}
#[test]
fn gold_melting() {
check_melting("gold", Substance::Crystalline(Crystal::gold()), 1064.0, 0.10);
}
#[test]
fn water_melting() {
check_melting_abs("water", Substance::Molecular(Molecule::water()), 0.0, 50.0);
}
#[test]
fn oxygen_melting() {
check_melting_abs("oxygen", Substance::Molecular(Molecule::oxygen_gas()), -218.79, 10.0);
}
#[test]
fn hydrogen_melting() {
check_melting_abs("hydrogen", Substance::Molecular(Molecule::hydrogen_gas()), -259.16, 10.0);
}
#[test]
fn oxygen_boiling() {
let mat = compile(&Substance::Molecular(Molecule::oxygen_gas()));
let bp = mat.thermal.vaporisation_point.unwrap_or(f32::NAN);
let real = -182.96;
let err = (bp - real).abs();
eprintln!("oxygen boiling: {bp:.1} C (real: {real:.1}, err: {err:.1} C)");
assert!(err < 10.0, "oxygen bp error {err:.1} C too large");
}
#[test]
fn hydrogen_boiling() {
let mat = compile(&Substance::Molecular(Molecule::hydrogen_gas()));
let bp = mat.thermal.vaporisation_point.unwrap_or(f32::NAN);
let real = -252.87;
let err = (bp - real).abs();
eprintln!("hydrogen boiling: {bp:.1} C (real: {real:.1}, err: {err:.1} C)");
assert!(err < 10.0, "hydrogen bp error {err:.1} C too large");
}
fn check_specific_heat(name: &str, substance: Substance, real: f32, tolerance: f32) {
let mat = compile(&substance);
let cp = mat.thermal.specific_heat;
let err = (cp - real).abs() / real;
eprintln!(
"{name} Cp: {cp:.0} J/(kg*K) (real: {real:.0}, err: {:.0}%)",
err * 100.0
);
assert!(
err < tolerance,
"{name} specific heat error {:.0}% exceeds {:.0}% tolerance",
err * 100.0,
tolerance * 100.0
);
}
#[test]
fn iron_specific_heat() {
check_specific_heat("iron", Substance::Crystalline(Crystal::iron()), 449.0, 0.30);
}
#[test]
fn copper_specific_heat() {
check_specific_heat("copper", Substance::Crystalline(Crystal::copper()), 385.0, 0.30);
}
#[test]
fn aluminum_specific_heat() {
check_specific_heat("aluminum", Substance::Crystalline(Crystal::aluminum()), 897.0, 0.30);
}
#[test]
fn gold_specific_heat() {
check_specific_heat("gold", Substance::Crystalline(Crystal::gold()), 129.0, 0.30);
}
fn check_conductivity(name: &str, substance: Substance, real: f32, tolerance: f32) {
let mat = compile(&substance);
let k = mat.thermal.thermal_conductivity;
let err = (k - real).abs() / real;
eprintln!(
"{name} conductivity: {k:.1} W/(m*K) (real: {real:.1}, err: {:.0}%)",
err * 100.0
);
assert!(
err < tolerance,
"{name} conductivity error {:.0}% exceeds {:.0}% tolerance",
err * 100.0,
tolerance * 100.0
);
}
#[test]
fn iron_conductivity() {
check_conductivity("iron", Substance::Crystalline(Crystal::iron()), 80.2, 0.50);
}
#[test]
fn copper_conductivity_order_of_magnitude() {
let mat = compile(&Substance::Crystalline(Crystal::copper()));
let k = mat.thermal.thermal_conductivity;
eprintln!("copper conductivity: {k:.1} W/(m*K) (real: 401.0)");
assert!(k > 30.0, "copper conductivity {k:.1} too low for a metal");
}
#[test]
fn oxygen_is_gas_at_room_temp() {
let mat = compile(&Substance::Molecular(Molecule::oxygen_gas()));
assert_eq!(mat.phase.state, PhaseState::Gas, "O2 should be gas at 25C");
}
#[test]
fn water_is_liquid_at_room_temp() {
let mat = compile(&Substance::Molecular(Molecule::water()));
assert_eq!(mat.phase.state, PhaseState::Liquid, "Water should be liquid at 25C");
}
#[test]
fn hydrogen_is_gas_at_room_temp() {
let mat = compile(&Substance::Molecular(Molecule::hydrogen_gas()));
assert_eq!(mat.phase.state, PhaseState::Gas, "H2 should be gas at 25C");
}
#[test]
fn combustion_methane_energy_order() {
let r = Reaction::combustion_methane();
assert!(r.is_exothermic(), "Methane combustion should be exothermic");
let e = r.energy();
eprintln!("Methane combustion: {e:.0} kJ/mol (real: ~890)");
assert!(e > 100.0, "Methane combustion energy {e:.0} too low");
}
#[test]
fn reaction_to_rule_compiles() {
let r = Reaction::combustion_methane();
let rule = r.to_interaction_rule(&[0, 1], &[2, 3]);
assert_eq!(rule.id, "combustion_methane");
assert_eq!(rule.probability, 1.0);
assert!(!rule.conditions.is_empty());
assert!(!rule.effects.is_empty());
let threshold = rule.conditions[0].threshold;
eprintln!("Methane activation temperature threshold: {threshold:.0} C");
assert!(threshold > 0.0, "Threshold should be positive");
}
#[test]
fn reaction_to_rule_exothermic_adds_heat() {
let r = Reaction::combustion_hydrogen();
let rule = r.to_interaction_rule(&[0, 1], &[2]);
let has_heat = rule.effects.iter().any(|e| matches!(
e,
materia::interaction::InteractionEffect::AddHeat { delta_celsius_per_tick, .. }
if *delta_celsius_per_tick > 0.0
));
assert!(has_heat, "Exothermic reaction should add positive heat");
}
#[test]
fn print_material_table() {
let materials: Vec<(&str, materia::material::MaterialDef)> = vec![
("stone", presets::stone()),
("granite", presets::granite()),
("sand", presets::sand()),
("clay", presets::clay()),
("dirt", presets::dirt()),
("gravel", presets::gravel()),
("grass", presets::grass()),
("wood", presets::wood()),
("leaf", presets::leaf()),
("water", presets::water()),
("ice", presets::ice()),
("snow", presets::snow()),
("iron", presets::iron()),
("steel", presets::steel()),
("copper", presets::copper()),
("gold", presets::gold()),
("air", presets::air()),
("steam", presets::steam()),
("smoke", presets::smoke()),
("lava", presets::lava()),
];
eprintln!();
eprintln!("{:<12} {:>8} {:>8} {:>8} {:>8} {:>10} {:>6} {:>5}",
"Material", "Density", "Melt C", "Cp", "k", "sigma_y", "tau", "IOR");
eprintln!("{}", "-".repeat(80));
for (name, mat) in &materials {
eprintln!("{:<12} {:>8.0} {:>8.0} {:>8.0} {:>8.1} {:>10.0} {:>6.1} {:>5.2}",
name,
mat.structural.density,
mat.thermal.melting_point.unwrap_or(0.0),
mat.thermal.specific_heat,
mat.thermal.thermal_conductivity,
mat.structural.yield_strength / 1e6,
mat.phase.relaxation_time,
mat.optical.refractive_index);
}
eprintln!();
}
#[test]
fn iron_yield_strength_order_of_magnitude() {
let mat = compile(&Substance::Crystalline(Crystal::iron()));
let ys = mat.structural.yield_strength;
eprintln!("Iron yield strength: {:.0} MPa (real: ~250 MPa for pure iron)", ys / 1e6);
assert!(ys > 50e6 && ys < 5000e6, "Iron yield {:.0} MPa out of range", ys / 1e6);
}
#[test]
fn steel_stronger_than_iron() {
let iron = compile(&Substance::Crystalline(Crystal::iron()));
let steel = compile(&Substance::Crystalline(Crystal::steel(0.008)));
assert!(steel.structural.yield_strength > iron.structural.yield_strength);
}
#[test]
fn metals_are_metallic() {
for crystal in [Crystal::iron(), Crystal::copper(), Crystal::gold()] {
let mat = compile(&Substance::Crystalline(crystal.clone()));
assert!(mat.optical.metallic > 0.8, "{} metallic={}", crystal.name, mat.optical.metallic);
}
}
#[test]
fn water_is_transparent() {
let mat = compile(&Substance::Molecular(Molecule::water()));
assert!(mat.optical.opacity < 0.5, "Water opacity={}", mat.optical.opacity);
assert!((mat.optical.refractive_index - 1.33).abs() < 0.1);
}
#[test]
fn gold_is_yellow() {
let mat = compile(&Substance::Crystalline(Crystal::gold()));
let [r, g, b, _] = mat.optical.base_color;
assert!(r > g && g > b, "Gold color [{r:.2}, {g:.2}, {b:.2}] not yellow");
}
#[test]
fn methane_is_gas_at_room_temp() {
let mat = compile(&Substance::Molecular(Molecule::methane()));
assert_eq!(mat.phase.state, PhaseState::Gas, "Methane should be gas (bp=-161C)");
}
#[test]
fn ethanol_is_condensed_at_room_temp() {
let mat = compile(&Substance::Molecular(Molecule::ethanol()));
assert_ne!(mat.phase.state, PhaseState::Gas, "Ethanol should not be gas (bp=78C)");
}
#[test]
fn water_less_viscous_than_honey_like_materials() {
let water = compile(&Substance::Molecular(Molecule::water()));
assert!(water.phase.relaxation_time < 1.5, "Water tau={} too high", water.phase.relaxation_time);
}
#[test]
fn boiling_above_melting_for_metals() {
for crystal in [Crystal::iron(), Crystal::copper(), Crystal::gold(), Crystal::aluminum()] {
let mat = compile(&Substance::Crystalline(crystal.clone()));
let mp = mat.thermal.melting_point.unwrap_or(0.0);
let bp = mat.thermal.vaporisation_point.unwrap_or(0.0);
assert!(bp > mp, "{}: bp={:.0} should be > mp={:.0}", crystal.name, bp, mp);
}
}
#[test]
fn density_ordering() {
let g = presets::gold().structural.density;
let fe = presets::iron().structural.density;
let st = presets::stone().structural.density;
let w = presets::water().structural.density;
let a = presets::air().structural.density;
assert!(g > fe, "gold({g:.0}) should be denser than iron({fe:.0})");
assert!(fe > st, "iron({fe:.0}) should be denser than stone({st:.0})");
assert!(st > w, "stone({st:.0}) should be denser than water({w:.0})");
assert!(w > a, "water({w:.0}) should be denser than air({a:.0})");
}
#[test]
fn melting_point_ordering() {
let w = compile(&Substance::Crystalline(Crystal {
name: "W".into(),
lattice: materia::LatticeType::BCC,
base: materia::Element::W,
solutes: vec![],
}));
let fe = presets::iron();
let cu = presets::copper();
let au = presets::gold();
let h2o = presets::water();
let w_mp = w.thermal.melting_point.unwrap();
let fe_mp = fe.thermal.melting_point.unwrap();
let cu_mp = cu.thermal.melting_point.unwrap();
let au_mp = au.thermal.melting_point.unwrap();
let h2o_mp = h2o.thermal.melting_point.unwrap();
assert!(w_mp > fe_mp, "W({w_mp:.0}) > Fe({fe_mp:.0})");
assert!(fe_mp > cu_mp, "Fe({fe_mp:.0}) > Cu({cu_mp:.0})");
assert!(cu_mp > au_mp, "Cu({cu_mp:.0}) > Au({au_mp:.0})");
assert!(au_mp > h2o_mp, "Au({au_mp:.0}) > water({h2o_mp:.0})");
}
#[test]
fn boiling_above_melting_for_common_elements() {
use materia::element::Element;
let elements = [
Element::Fe, Element::Cu, Element::Au, Element::Al,
Element::Si, Element::H, Element::O, Element::N,
Element::C, Element::Na, Element::Mg,
];
for e in &elements {
let mp = e.melting_point_pure();
let bp = e.boiling_point_pure();
eprintln!("{:?}: mp={mp:.0} C, bp={bp:.0} C", e);
assert!(
bp > mp,
"{:?} boiling point {bp:.0} should exceed melting point {mp:.0}",
e
);
}
}