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),
("Color-Change Garnet (Pyrope-Spessartine)", 1.760, 0.001),
("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
);
}