use crate::optics::materials::{GemMaterial, OpticalCharacter};
#[test]
fn builtin_material_abbe_numbers_match_published_values() {
let expected: &[(&str, f32, f32)] = &[
("Diamond", 55.27, 0.5),
("Sapphire", 72.27, 0.5),
("Ruby", 72.27, 0.5),
("Quartz", 69.65, 0.5),
("Spinel", 60.63, 0.5),
("Cubic Zirconia", 33.52, 0.5),
("Alexandrite", 73.90, 0.5),
("Synthetic Moissanite", 25.94, 0.5),
("Zircon", 40.96, 3.0),
("Topaz", 76.83, 4.0),
("Tourmaline", 64.58, 3.5),
("Tanzanite", 40.21, 3.0),
("Emerald", 70.93, 3.5),
];
for &(name, published_abbe, tol) in expected {
let material = GemMaterial::by_name(name)
.unwrap_or_else(|| panic!("{name} must be a built-in material"));
let n_f = material.dispersion.evaluate(486.1);
let n_d = material.dispersion.evaluate(589.3);
let n_c = material.dispersion.evaluate(656.3);
let dn = n_f - n_c;
assert!(
dn > 0.0,
"{name}: Delta n(F-C) must be positive (normal dispersion), got {dn}"
);
let computed_abbe = (n_d - 1.0) / dn;
assert!(
(computed_abbe - published_abbe).abs() <= tol,
"{name}: computed Abbe V_d={computed_abbe:.2} (n_d={n_d:.5}, n_F={n_f:.5}, n_C={n_c:.5}) \
does not match published V_d={published_abbe:.2} within tolerance {tol} -- check the \
dispersion coefficients against the source cited in all_materials()"
);
}
}
#[test]
fn builtin_material_n_d_matches_sourced_values() {
let expected_n_d: &[(&str, f32, f32)] = &[
("Zircon", 1.925, 1e-4),
("Synthetic Moissanite", 2.647_434, 1e-4),
("Topaz", 1.627_178, 1e-4),
("Tourmaline", 1.639_405, 1e-4),
("Tanzanite", 1.700_858, 1e-4),
("Alexandrite", 1.742_73, 1e-4),
];
for &(name, expected, tol) in expected_n_d {
let material = GemMaterial::by_name(name)
.unwrap_or_else(|| panic!("{name} must be a built-in material"));
let n_d = material.dispersion.evaluate(589.3);
assert!(
(n_d - expected).abs() < tol,
"{name}: n_d={n_d:.6} does not match the sourced value {expected:.6} -- check the \
dispersion coefficients against the source cited in all_materials(); if this is a \
deliberate re-sourcing, update this test's expected value and flag the n_d change \
in the task report if it moves by more than ~0.002 (critical-angle significance)"
);
}
}
#[test]
fn every_builtin_dispersion_curve_stays_physical_at_the_sampled_band_edges() {
for material in GemMaterial::all_materials() {
for lambda_nm in [380.0f32, 780.0] {
let n = material.dispersion.evaluate(lambda_nm);
assert!(
n.is_finite(),
"{}: dispersion.evaluate({lambda_nm}) must be finite, got {n}",
material.name
);
assert!(
n >= 1.0,
"{}: dispersion.evaluate({lambda_nm}) = {n} must be >= 1.0 \
(physically-impossible index)",
material.name
);
if material.optical_character == OpticalCharacter::UniaxialPositive
|| material.optical_character == OpticalCharacter::UniaxialNegative
{
let n_e = material.extraordinary_index_at(lambda_nm, n);
assert!(
n_e.is_finite() && n_e >= 1.0,
"{}: extraordinary_index_at({lambda_nm}, {n}) = {n_e} must be \
finite and >= 1.0",
material.name
);
}
}
}
}
#[test]
fn m4_new_species_match_their_target_n_d_within_tolerance() {
let expected: &[(&str, f32, f32)] = &[
("Aquamarine", 1.577, 0.002),
("Morganite", 1.577, 0.002),
("Chrysoberyl (Yellow)", 1.746, 0.002),
("Amethyst", 1.544, 0.001),
("Citrine", 1.544, 0.001),
("Pyrope Garnet", 1.714, 0.002),
("Almandine Garnet", 1.790, 0.002),
("Spessartine Garnet", 1.800, 0.002),
("Grossular Garnet (Tsavorite)", 1.734, 0.002),
("Andradite Garnet (Demantoid)", 1.887, 0.002),
("Peridot", 1.654, 0.002),
("YAG", 1.833, 0.002),
("Benitoite", 1.757, 0.002),
("Andalusite", 1.634, 0.002),
("Opal", 1.45, 0.002),
("Glass (N-BK7)", 1.5168, 0.002),
("Glass (F2)", 1.620, 0.002),
];
for &(name, target_n_d, tol) in expected {
let material =
GemMaterial::by_name(name).unwrap_or_else(|| panic!("{name} must resolve via by_name"));
assert_eq!(material.name, name, "{name} must resolve to itself exactly");
let n_d = material.dispersion.evaluate(589.3);
assert!(
(n_d - target_n_d).abs() <= tol,
"{name}: n_d={n_d:.5} does not match target {target_n_d} within {tol}"
);
}
let ggg = GemMaterial::by_name("GGG").expect("GGG must resolve via by_name");
let ggg_n_d = ggg.dispersion.evaluate(589.3);
assert!(
(ggg_n_d - 1.970).abs() <= 0.002,
"GGG: n_d={ggg_n_d:.5} does not match target 1.970 within 0.002"
);
}
#[test]
fn ggg_dispersion_uses_the_converted_f_minus_c_value() {
let ggg = GemMaterial::by_name("GGG").expect("GGG must resolve via by_name");
let delta_f_c = ggg.dispersion.evaluate(486.1) - ggg.dispersion.evaluate(656.3);
assert!(
(delta_f_c - 0.026_055).abs() <= 1e-3,
"GGG: n(F)-n(C)={delta_f_c:.5} should be 0.045 * 0.579 = 0.026055 within 1e-3"
);
}
#[test]
fn quartz_extraordinary_index_has_wavelength_dependent_birefringence_while_legacy_entries_stay_constant()
{
let quartz = GemMaterial::by_name("Quartz").expect("Quartz must resolve");
assert!(
quartz.uniaxial_extraordinary_dispersion.is_some(),
"test premise: Quartz must carry a genuine per-axis e-ray dispersion curve"
);
let delta_at = |lambda_nm: f32| {
let n_o = quartz.dispersion.evaluate(lambda_nm);
let n_e = quartz.extraordinary_index_at(lambda_nm, n_o);
n_e - n_o
};
let delta_380 = delta_at(380.0);
let delta_780 = delta_at(780.0);
assert!(
(delta_380 - delta_780).abs() > 1e-4,
"Quartz's n_e - n_o must genuinely vary across the visible band: \
delta(380nm)={delta_380:.6}, delta(780nm)={delta_780:.6}"
);
let sapphire = GemMaterial::by_name("Sapphire").expect("Sapphire must resolve");
assert!(
sapphire.uniaxial_extraordinary_dispersion.is_none(),
"test premise: Sapphire must NOT carry a per-axis e-ray dispersion curve"
);
for lambda_nm in [380.0f32, 589.3, 780.0] {
let n_o = sapphire.dispersion.evaluate(lambda_nm);
let n_e = sapphire.extraordinary_index_at(lambda_nm, n_o);
assert!(
(n_e - n_o - sapphire.birefringence_delta).abs() < 1e-6,
"Sapphire (legacy entry) at {lambda_nm}nm: n_e - n_o must equal the \
constant birefringence_delta exactly"
);
}
}
#[test]
fn rutile_matches_devore_indices_and_dispersion() {
let rutile = GemMaterial::by_name("Rutile").expect("Rutile must be a built-in material");
let e_ray = rutile
.uniaxial_extraordinary_dispersion
.expect("Rutile must carry its own e-ray dispersion curve");
let n_o = rutile.dispersion.evaluate(589.3);
let n_e = e_ray.evaluate(589.3);
assert!((n_o - 2.613).abs() <= 0.002, "n_o(D) = {n_o}");
assert!((n_e - 2.909).abs() <= 0.002, "n_e(D) = {n_e}");
let fc_o = rutile.dispersion.evaluate(486.1) - rutile.dispersion.evaluate(656.3);
let fc_e = e_ray.evaluate(486.1) - e_ray.evaluate(656.3);
assert!((fc_o - 0.164).abs() <= 0.003, "o-ray F-C = {fc_o}");
assert!((fc_e - 0.207).abs() <= 0.003, "e-ray F-C = {fc_e}");
assert!(
(rutile.birefringence_delta - (n_e - n_o)).abs() <= 1e-3,
"birefringence_delta {} must equal n_e(D) - n_o(D) = {}",
rutile.birefringence_delta,
n_e - n_o
);
}