#![cfg(feature = "embedded-data")]
use xraydb::{CrossSectionKind, XrayDb};
#[test]
fn xray_lines_filters_by_initial_level() {
let db = XrayDb::new();
let all = db.xray_lines("Au", None, None).expect("all lines");
let k_only = db.xray_lines("Au", Some("K"), None).expect("K lines");
let l3_only = db.xray_lines("Au", Some("L3"), None).expect("L3 lines");
assert!(!k_only.is_empty() && !l3_only.is_empty());
assert!(k_only.len() < all.len(), "filtering must actually narrow");
assert!(k_only.values().all(|l| l.initial_level == "K"));
assert!(l3_only.values().all(|l| l.initial_level == "L3"));
assert!(
db.xray_lines("Au", Some("Q9"), None)
.expect("ok")
.is_empty()
);
}
#[test]
fn xray_lines_excitation_filter_drops_unreachable_levels() {
let db = XrayDb::new();
let below_k = db
.xray_lines("Au", None, Some(20_000.0))
.expect("below K edge");
assert!(
below_k.values().all(|l| l.initial_level != "K"),
"K lines must be dropped below the K edge"
);
assert!(!below_k.is_empty(), "L and M lines should remain");
let above_k = db
.xray_lines("Au", None, Some(100_000.0))
.expect("above K edge");
assert!(
above_k.values().any(|l| l.initial_level == "K"),
"K lines must return above the K edge"
);
assert!(above_k.len() > below_k.len());
}
#[test]
fn xray_lines_by_level_groups_in_spectroscopic_order() {
let db = XrayDb::new();
let groups = db.xray_lines_by_level("Au", None).expect("grouped");
assert!(groups.len() > 1, "Au should have several initial levels");
assert_eq!(groups[0].0, "K", "K must sort first");
for (level, lines) in &groups {
assert!(!lines.is_empty(), "group {level} is empty");
assert!(lines.iter().all(|(_, l)| &l.initial_level == level));
for pair in lines.windows(2) {
assert!(pair[0].1.intensity >= pair[1].1.intensity, "group {level}");
}
}
let filtered = db
.xray_lines_by_level("Au", Some(20_000.0))
.expect("filtered");
assert!(filtered.iter().all(|(level, _)| level != "K"));
}
#[test]
fn guess_edge_honours_a_restricted_edge_list() {
let db = XrayDb::new();
let guess = db
.guess_edge(7112.0, Some(&["L3"]), None)
.expect("some L3 edge exists");
assert_eq!(guess.edge, "L3");
assert!(db.guess_edge(7112.0, Some(&["Z9"]), None).is_none());
let below = db.guess_edge(7100.0, Some(&["K"]), None).expect("k edge");
let above = db.guess_edge(7130.0, Some(&["K"]), None).expect("k edge");
assert!(below.difference > 0.0, "edge above query -> positive");
assert!(above.difference < 0.0, "edge below query -> negative");
}
#[test]
fn xray_edges_and_lines_reject_unknown_elements() {
let db = XrayDb::new();
assert!(db.xray_edges("Xx").is_err());
assert!(db.xray_lines("Xx", None, None).is_err());
assert!(db.xray_lines_by_level("Xx", None).is_err());
assert!(db.xray_lines_by_intensity("Xx", None, None).is_err());
assert!(db.xray_edge("Fe", "Q9").is_err());
}
#[test]
fn coster_kronig_returns_direct_and_total_probabilities() {
let db = XrayDb::new();
let direct = db.ck_probability("Au", "L1", "L3", false).expect("direct");
let total = db.ck_probability("Au", "L1", "L3", true).expect("total");
assert!(direct > 0.0 && direct <= 1.0, "direct = {direct}");
assert!(total > 0.0 && total <= 1.0, "total = {total}");
assert!(
total >= direct,
"total ({total}) includes routes via intermediate states, so it cannot be \
smaller than direct ({direct})"
);
}
#[test]
fn coster_kronig_errors_are_specific() {
let db = XrayDb::new();
assert!(db.ck_probability("Xx", "L1", "L2", false).is_err());
let err = db
.ck_probability("Fe", "L1", "Q9", false)
.expect_err("no such transition");
assert!(err.to_string().contains("L1->Q9"), "{err}");
}
#[test]
fn chantler_energies_can_be_filtered() {
let db = XrayDb::new();
let all = db.chantler_energies("Fe", None, None).expect("all");
let window = db
.chantler_energies("Fe", Some(5_000.0), Some(10_000.0))
.expect("window");
assert!(!window.is_empty() && window.len() < all.len());
assert!(window.iter().all(|&e| (5_000.0..=10_000.0).contains(&e)));
assert!(window.windows(2).all(|w| w[1] >= w[0]), "must stay sorted");
assert!(
db.chantler_energies("Fe", Some(1e8), Some(1e9))
.expect("ok")
.is_empty()
);
assert!(db.chantler_energies("Xx", None, None).is_err());
}
#[test]
fn chantler_kinds_differ_and_are_consistent() {
use xraydb::ChantlerKind;
let db = XrayDb::new();
let e = 10_000.0;
let total = db
.mu_chantler_at("Fe", e, ChantlerKind::Total)
.expect("total");
let photo = db
.mu_chantler_at("Fe", e, ChantlerKind::Photo)
.expect("photo");
let incoh = db
.mu_chantler_at("Fe", e, ChantlerKind::Incoherent)
.expect("incoh");
assert!(total > 0.0 && photo > 0.0 && incoh > 0.0);
assert!(photo < total, "a component cannot exceed the total");
assert!(incoh < total);
assert!(photo > incoh);
assert!(db.mu_chantler("Xx", &[e], ChantlerKind::Total).is_err());
}
#[test]
fn chantler_energy_range_bounds_are_respected() {
let db = XrayDb::new();
let (emin, emax) = db.chantler_energy_range("Fe").expect("range");
assert!(emin > 0.0 && emax > emin);
let below = db.f2_chantler_at("Fe", emin / 10.0).expect("below");
let at_min = db.f2_chantler_at("Fe", emin).expect("at min");
assert_eq!(below, at_min);
let above = db.f2_chantler_at("Fe", emax * 10.0).expect("above");
let at_max = db.f2_chantler_at("Fe", emax).expect("at max");
assert_eq!(above, at_max);
assert!(db.chantler_energy_range("Xx").is_err());
}
#[test]
fn ionchamber_validates_its_arguments() {
let db = XrayDb::new();
let ok = |gases: &[(&str, f64)], len: f64| {
db.ionchamber_fluxes(gases, 1.0, len, 10_000.0, 1e-6, true, true)
};
assert!(ok(&[], 30.0).is_err(), "empty gas mixture");
assert!(
ok(&[("nitrogen", 0.0)], 30.0).is_err(),
"fractions sum to 0"
);
assert!(ok(&[("nitrogen", 1.0)], 0.0).is_err(), "zero length");
assert!(ok(&[("nitrogen", 1.0)], -5.0).is_err(), "negative length");
assert!(ok(&[("nitrogen", 1.0)], f64::NAN).is_err(), "NaN length");
assert!(ok(&[("unobtainium", 1.0)], 30.0).is_err(), "unknown gas");
}
#[test]
fn ionchamber_handles_mixtures_and_gas_aliases() {
let db = XrayDb::new();
let pure = db
.ionchamber_fluxes(&[("nitrogen", 1.0)], 1.0, 30.0, 10_000.0, 1e-6, true, true)
.expect("pure N2");
let aliased = db
.ionchamber_fluxes(&[("N2", 1.0)], 1.0, 30.0, 10_000.0, 1e-6, true, true)
.expect("N2 alias");
assert!((pure.incident - aliased.incident).abs() < 1e-6 * pure.incident.abs());
let scaled = db
.ionchamber_fluxes(&[("nitrogen", 5.0)], 1.0, 30.0, 10_000.0, 1e-6, true, true)
.expect("scaled");
assert!((pure.incident - scaled.incident).abs() < 1e-6 * pure.incident.abs());
let argon = db
.ionchamber_fluxes(&[("argon", 1.0)], 1.0, 30.0, 10_000.0, 1e-6, true, true)
.expect("Ar");
let mixed = db
.ionchamber_fluxes(
&[("nitrogen", 0.5), ("argon", 0.5)],
1.0,
30.0,
10_000.0,
1e-6,
true,
true,
)
.expect("mixture");
let (lo, hi) = if argon.incident < pure.incident {
(argon.incident, pure.incident)
} else {
(pure.incident, argon.incident)
};
assert!(
mixed.incident > lo && mixed.incident < hi,
"mixture {} should sit between {lo} and {hi}",
mixed.incident
);
}
#[test]
fn ionchamber_toggles_change_the_answer() {
let db = XrayDb::new();
let run = |compton: bool, both: bool| {
db.ionchamber_fluxes(
&[("nitrogen", 1.0)],
1.0,
30.0,
10_000.0,
1e-6,
compton,
both,
)
.expect("fluxes")
};
let base = run(true, true);
assert!(run(false, true).incident != base.incident, "compton toggle");
let single = run(true, false);
assert!(
(single.incident / base.incident - 2.0).abs() < 1e-9,
"one carrier should double the flux: {} vs {}",
single.incident,
base.incident
);
}
#[test]
fn material_mu_named_at_matches_the_batch_form() {
let db = XrayDb::new();
let e = 12_000.0;
let scalar = db
.material_mu_named_at("kapton", e, CrossSectionKind::Total, None)
.expect("scalar");
let batch = db
.material_mu_named("kapton", &[e], CrossSectionKind::Total, None)
.expect("batch");
assert!((scalar - batch[0]).abs() <= 1e-12 * batch[0].abs());
let doubled = db
.material_mu_named_at("kapton", e, CrossSectionKind::Total, Some(2.84))
.expect("override");
assert!((doubled / scalar - 2.0).abs() < 1e-9);
}
#[test]
fn f0_ion_listing_and_errors() {
let db = XrayDb::new();
let all = db.f0_ions(None).expect("all ions");
let iron = db.f0_ions(Some("Fe")).expect("Fe ions");
assert!(iron.len() < all.len() && !iron.is_empty());
assert!(iron.iter().all(|i| i.starts_with("Fe")));
assert!(db.f0_ions(Some("Xx")).is_err());
assert!(db.f0("NotAnIon", &[0.0]).is_err());
assert!(db.f0_at("NotAnIon", 0.0).is_err());
}
#[test]
fn f0_decays_with_momentum_transfer() {
let db = XrayDb::new();
let q: Vec<f64> = (0..=12).map(|i| f64::from(i) * 0.5).collect();
let f0 = db.f0("Fe", &q).expect("f0");
assert!((f0[0] - 26.0).abs() < 0.1, "f0(0) = {}", f0[0]);
for pair in f0.windows(2) {
assert!(pair[1] <= pair[0], "f0 must decrease with q");
}
let fe3 = db.f0_at("Fe3+", 0.0).expect("Fe3+");
assert!(fe3 < f0[0], "Fe3+ has fewer electrons: {fe3} vs {}", f0[0]);
}