use super::*;
use crate::optics::absorption::AbsorptionBand;
fn cat() -> &'static ChromophoreCatalogue {
ChromophoreCatalogue::global()
}
fn peak_near(bands: &[AbsorptionBand], nm: f32) -> Option<f32> {
bands
.iter()
.find(|b| (b.center_nm - nm).abs() < 1.0)
.map(|b| b.peak)
}
fn recipe(host: &str, entries: &[(&str, f64)], treatments: &[&str]) -> ColorRecipe {
let mut r = ColorRecipe::new(host, cat().data_version);
for (id, amount) in entries {
assert!(r.set_amount(id, *amount), "{id} amount {amount} rejected");
}
r.treatments = treatments.iter().map(|t| (*t).to_string()).collect();
r
}
fn has_relation(warnings: &[ResolveWarning]) -> bool {
warnings
.iter()
.any(|w| matches!(w, ResolveWarning::Relation { .. }))
}
fn has_clamp(warnings: &[ResolveWarning]) -> bool {
warnings
.iter()
.any(|w| matches!(w, ResolveWarning::AmountClamped { .. }))
}
fn clamped(warnings: &[ResolveWarning], id: &str) -> Option<(f64, f64)> {
warnings.iter().find_map(|w| match w {
ResolveWarning::AmountClamped {
id: got,
requested,
used,
} if got == id => Some((*requested, *used)),
_ => None,
})
}
#[test]
fn glass_copper_is_blue_until_the_reducing_strike() {
let host = cat().host("glass_silicate").expect("glass host");
let mut r = recipe("glass_silicate", &[("Cu", 1.0)], &[]);
let (t, w) = resolve(&r, cat()).expect("resolves");
assert!(
peak_near(&t.o_ray, 790.0).is_some(),
"Cu2+ band: {:?}",
t.o_ray
);
assert!(
peak_near(&t.o_ray, 570.0).is_none(),
"colloid: {:?}",
t.o_ray
);
assert!(!has_relation(&w), "{w:?}");
r.treatments.push("reducing_strike".into());
let (t, w) = resolve(&r, cat()).expect("resolves");
assert!(
peak_near(&t.o_ray, 570.0).is_some(),
"colloid: {:?}",
t.o_ray
);
assert!(peak_near(&t.o_ray, 790.0).is_none(), "Cu2+: {:?}", t.o_ray);
assert!(!has_relation(&w), "{w:?}");
assert!(
!w.iter()
.any(|x| matches!(x, ResolveWarning::TreatmentSkipped { .. })),
"{w:?}"
);
assert!((host.element_conc_max("Cu") - 8.0).abs() < 1e-9);
let split = host.treated_split(&r.treatments, &|id| id == "Cu");
let colloid_bound =
10_000.0 * host.n_site_for_unit("ppm_site") / host.n_site_for_unit("wt_pct_oxide:CuO");
let got = host.element_conc_max_split("Cu", &split);
assert!(got < 8.0, "{got}");
assert!(
(got - colloid_bound).abs() < 1e-9,
"{got} vs {colloid_bound}"
);
r.set_amount("Cu", 5.0);
let (_, w) = resolve(&r, cat()).expect("resolves");
let (requested, used) = clamped(&w, "Cu").expect("clamped under the strike");
assert!((requested - 5.0).abs() < 1e-12, "{requested}");
assert!(
(used - colloid_bound).abs() < 1e-9,
"{used} vs {colloid_bound}"
);
r.treatments.clear();
let (_, w) = resolve(&r, cat()).expect("resolves");
assert!(clamped(&w, "Cu").is_none(), "{w:?}");
}
#[test]
fn quartz_iron_is_colorless_until_treated_and_its_states_exclude_each_other() {
let run = |treatments: &[&str]| {
resolve(&recipe("quartz", &[("Fe", 50.0)], treatments), cat()).expect("resolves")
};
let (t, w) = run(&[]);
assert!(
t.o_ray.is_empty(),
"iron quartz is colorless: {:?}",
t.o_ray
);
assert!(!has_relation(&w), "{w:?}");
let (t, _) = run(&["gamma_irradiation"]);
assert!(peak_near(&t.o_ray, 539.0).is_some(), "{:?}", t.o_ray);
let (t, w) = run(&["gamma_irradiation", "thermal_anneal_450"]);
assert!(peak_near(&t.o_ray, 539.0).is_none(), "{:?}", t.o_ray);
assert!(
peak_near(&t.o_ray, 295.0).is_some() || peak_near(&t.o_ray, 411.0).is_some(),
"citrine: {:?}",
t.o_ray
);
assert!(!has_relation(&w), "{w:?}");
let (t, w) = run(&["thermal_anneal_550"]);
assert!(peak_near(&t.o_ray, 720.0).is_some(), "{:?}", t.o_ray);
assert!(peak_near(&t.o_ray, 539.0).is_none(), "{:?}", t.o_ray);
assert!(!has_relation(&w), "{w:?}");
let (t, w) = run(&["gamma_irradiation", "thermal_anneal_550"]);
assert!(peak_near(&t.o_ray, 539.0).is_none(), "{:?}", t.o_ray);
assert!(!has_relation(&w), "{w:?}");
let _ = run(&["thermal_anneal_450", "thermal_anneal_550"]);
}
#[test]
fn two_element_chromophores_bound_the_sum() {
let andalusite = cat().host("andalusite").expect("andalusite");
assert_eq!(
andalusite.element_unit("Cr").as_deref(),
Some("wt_pct_oxide:V2O3")
);
assert_eq!(andalusite.element_unit("V"), andalusite.element_unit("Cr"));
let (t, w) = resolve(
&recipe("andalusite", &[("V", 0.4), ("Cr", 0.4)], &[]),
cat(),
)
.expect("resolves");
for id in ["V", "Cr"] {
let (requested, used) = clamped(&w, id).unwrap_or_else(|| panic!("{id} clamped: {w:?}"));
assert!(
(requested - 0.4).abs() < 1e-12,
"{id} requested {requested}"
);
assert!((used - 0.25).abs() < 1e-9, "{id} used {used}");
}
let (at_cap, w_cap) = resolve(
&recipe("andalusite", &[("V", 0.25), ("Cr", 0.25)], &[]),
cat(),
)
.expect("resolves");
assert!(!has_clamp(&w_cap), "{w_cap:?}");
assert_eq!(t.o_ray.len(), at_cap.o_ray.len());
assert!(!t.o_ray.is_empty(), "V3+ / Cr3+ colors andalusite");
for (a, b) in t.o_ray.iter().zip(&at_cap.o_ray) {
assert!((a.center_nm - b.center_nm).abs() < 1e-3);
assert!(
(a.peak - b.peak).abs() <= 1e-5 * b.peak.abs(),
"{a:?} vs {b:?}"
);
}
let (_, w) = resolve(&recipe("andalusite", &[("V", 0.5)], &[]), cat()).expect("resolves");
assert!(!has_clamp(&w), "{w:?}");
let mut r = recipe("andalusite", &[("V", 0.4), ("Cr", 0.4)], &[]);
assert_eq!(r.clamp_amounts(cat()), 2);
assert!((r.amount("V") - 0.25).abs() < 1e-9 && (r.amount("Cr") - 0.25).abs() < 1e-9);
assert_eq!(r.clamp_amounts(cat()), 0, "already within the bound");
let room = andalusite
.summed_room("Cr", &|id| if id == "V" { 0.4 } else { 0.0 })
.expect("Cr drives V3+ / Cr3+");
assert!((room - 0.1).abs() < 1e-9, "{room}");
let none_left = andalusite
.summed_room("Cr", &|id| if id == "V" { 0.9 } else { 0.0 })
.expect("Cr drives V3+ / Cr3+");
assert!(none_left.abs() < 1e-12, "floored at 0: {none_left}");
let corundum = cat().host("corundum").expect("corundum");
assert!(corundum.summed_room("Cr", &|_| 0.0).is_none());
let tourmaline = cat().host("tourmaline").expect("tourmaline");
assert_eq!(tourmaline.element_unit("V"), tourmaline.element_unit("Cr"));
let (_, w) = resolve(
&recipe("tourmaline", &[("Cr", 2.0), ("V", 2.0)], &[]),
cat(),
)
.expect("resolves");
for id in ["Cr", "V"] {
let (_, used) = clamped(&w, id).unwrap_or_else(|| panic!("{id} clamped: {w:?}"));
assert!((used - 1.5).abs() < 1e-9, "{id} used {used}");
}
let tanzanite = cat().host("tanzanite").expect("tanzanite");
assert!(tanzanite.summed_room("V", &|_| 0.0).is_none());
let (_, w) = resolve(&recipe("tanzanite", &[("V", 5000.0)], &[]), cat()).expect("resolves");
assert!(clamped(&w, "V").is_none(), "{w:?}");
}
#[test]
fn untreated_element_bounds_are_unchanged() {
let pinned = [
("corundum", "Cr", 2.0),
("glass_silicate", "Cu", 8.0),
("quartz", "Fe", 500.0),
("quartz", "Al", 100.0),
("beryl", "Mn", 0.5),
("diamond", "N", 3500.0),
("andalusite", "V", 0.5),
("tourmaline", "Cr", 3.0),
];
for (host, id, expected) in pinned {
let got = cat().host(host).expect("host").element_conc_max(id);
assert!(
(got - expected).abs() < 1e-9,
"{host} {id}: {got} vs {expected}"
);
}
for host in &cat().hosts {
for id in cat().selectable_elements(&host.id) {
let plain = host.element_conc_max(&id);
let split = host.element_conc_max_split(&id, &host.valence_split);
assert_eq!(plain.to_bits(), split.to_bits(), "{} {id}", host.id);
}
}
}
#[test]
fn treated_split_matches_the_resolver_and_skips_unmet_requirements() {
let corundum = cat().host("corundum").expect("corundum");
let anneal = ["reducing_anneal".to_string()];
let split = corundum.treated_split(&anneal, &|_| true);
assert!((split["Fe"]["Fe2+"] - 0.97).abs() < 1e-12, "{split:?}");
assert!((split["Fe"]["Fe3+"] - 0.03).abs() < 1e-12, "{split:?}");
let split = corundum.treated_split(&anneal, &|id| id == "Fe");
assert_eq!(split, corundum.valence_split);
let unknown = ["no_such_treatment".to_string()];
assert_eq!(
corundum.treated_split(&unknown, &|_| true),
corundum.valence_split
);
assert_eq!(
corundum.treated_split(&[], &|_| true),
corundum.valence_split
);
let r = recipe("corundum", &[("Fe", 200.0)], &["reducing_anneal"]);
let (_, w) = resolve(&r, cat()).expect("resolves");
assert!(
w.iter()
.any(|x| matches!(x, ResolveWarning::TreatmentSkipped { .. })),
"{w:?}"
);
}
#[test]
fn solver_recipes_respect_summed_caps() {
let res = solve_physics(cat(), "andalusite", [25.0, -15.0, 20.0], None, 5.0, &[]);
let total = res.recipe.amount("V") + res.recipe.amount("Cr");
assert!(total <= 0.5 + 1e-9, "V + Cr = {total} wt% V2O3");
let (_, w) = resolve(&res.recipe, cat()).expect("resolves");
assert!(
!has_clamp(&w),
"the returned recipe is already within the bounds: {w:?}"
);
}