use crate::optics::{
absorption::AbsorptionBand,
birefringence::AbsorptionTensor3,
materials::{GemMaterial, OpticalCharacter},
};
use glam::Vec3;
#[test]
fn only_biaxial_materials_expose_a_biaxial_indicatrix() {
let biaxial_names = [
"Alexandrite",
"Topaz",
"Tanzanite",
"Chrysoberyl (Yellow)",
"Peridot",
"Andalusite",
];
for material in GemMaterial::all_materials() {
let indicatrix = material.biaxial_indicatrix(589.3);
if biaxial_names.contains(&material.name.as_str()) {
assert!(
indicatrix.is_some(),
"{} should expose a biaxial indicatrix",
material.name
);
} else {
assert!(
indicatrix.is_none(),
"{} should NOT expose a biaxial indicatrix (uniaxial/isotropic)",
material.name
);
}
}
}
#[test]
fn biaxial_indicatrix_is_internally_consistent_with_existing_fields() {
for name in ["Alexandrite", "Topaz", "Tanzanite"] {
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);
let indicatrix = material
.biaxial_indicatrix(589.3)
.unwrap_or_else(|| panic!("{name} must expose a biaxial indicatrix"));
assert!(
indicatrix.n_alpha <= indicatrix.n_beta && indicatrix.n_beta <= indicatrix.n_gamma,
"{name}: principal indices must be ordered n_alpha<=n_beta<=n_gamma, got ({}, {}, {})",
indicatrix.n_alpha,
indicatrix.n_beta,
indicatrix.n_gamma
);
assert!(
(indicatrix.n_beta - n_d).abs() < 1e-5,
"{name}: n_beta ({}) must equal the base dispersion curve's n_d ({n_d})",
indicatrix.n_beta
);
assert!(
(indicatrix.n_gamma - indicatrix.n_alpha - material.birefringence_delta).abs() < 1e-5,
"{name}: n_gamma - n_alpha ({}) must equal birefringence_delta ({})",
indicatrix.n_gamma - indicatrix.n_alpha,
material.birefringence_delta
);
}
}
#[test]
fn biaxial_sign_matches_optical_character() {
for material in GemMaterial::all_materials() {
let Some(delta_beta_alpha) = material.biaxial_delta_beta_alpha else {
continue;
};
let n_beta = material.dispersion.evaluate(589.3);
let n_alpha = n_beta - delta_beta_alpha;
let n_gamma = n_alpha + material.birefringence_delta;
let beta_minus_alpha = n_beta - n_alpha;
let gamma_minus_beta = n_gamma - n_beta;
match material.optical_character {
OpticalCharacter::BiaxialPositive => assert!(
beta_minus_alpha < gamma_minus_beta,
"{}: declared BiaxialPositive requires (n_beta - n_alpha) < \
(n_gamma - n_beta), got {beta_minus_alpha} >= {gamma_minus_beta} \
(n_alpha={n_alpha}, n_beta={n_beta}, n_gamma={n_gamma})",
material.name
),
OpticalCharacter::BiaxialNegative => assert!(
beta_minus_alpha > gamma_minus_beta,
"{}: declared BiaxialNegative requires (n_beta - n_alpha) > \
(n_gamma - n_beta), got {beta_minus_alpha} <= {gamma_minus_beta} \
(n_alpha={n_alpha}, n_beta={n_beta}, n_gamma={n_gamma})",
material.name
),
other => panic!(
"{}: carries biaxial_delta_beta_alpha but optical_character is {other:?}, \
not BiaxialPositive/BiaxialNegative",
material.name
),
}
}
}
#[test]
fn alexandrite_trichroic_band_sets_follow_the_cited_pleochroic_pattern() {
let alex =
GemMaterial::by_name("Alexandrite").expect("Alexandrite must be a built-in material");
let bands_alpha = &alex.absorption.o_ray;
let bands_gamma = &alex.absorption.e_ray;
let bands_beta = alex
.absorption
.beta_ray
.as_deref()
.expect("Alexandrite must carry a third (beta) trichroic band set");
assert!(
alex.absorption.is_pleochroic,
"Alexandrite's trichroic tensor must be flagged pleochroic"
);
let sum_at =
|bands: &[AbsorptionBand], nm: f32| -> f32 { bands.iter().map(|b| b.evaluate(nm)).sum() };
let (t2_alpha, t2_beta, t2_gamma) = (
sum_at(bands_alpha, 595.0),
sum_at(bands_beta, 595.0),
sum_at(bands_gamma, 595.0),
);
assert!(
t2_gamma > t2_alpha && t2_alpha > t2_beta,
"4T2 amplitude order must be gamma(green) > alpha(red) > beta(yellow), got \
gamma={t2_gamma:.3}, alpha={t2_alpha:.3}, beta={t2_beta:.3}"
);
let (t1_alpha, t1_beta, t1_gamma) = (
sum_at(bands_alpha, 422.0),
sum_at(bands_beta, 422.0),
sum_at(bands_gamma, 422.0),
);
assert!(
t1_beta > t1_alpha && t1_beta > t1_gamma,
"4T1 amplitude must peak on beta(yellow, crystal a), got alpha={t1_alpha:.3}, \
beta={t1_beta:.3}, gamma={t1_gamma:.3}"
);
assert_eq!(
alex.c_axis,
Vec3::Y,
"test premise: Alexandrite c_axis is +Y"
);
let tensor = AbsorptionTensor3::biaxial(t2_alpha, t2_beta, t2_gamma, alex.c_axis);
for (axis, expected, label) in [
(Vec3::X, t2_alpha, "alpha on +X"),
(Vec3::Z, t2_beta, "beta on Z"),
(Vec3::Y, t2_gamma, "gamma on +Y (c_axis)"),
] {
let measured = tensor.quadratic_form(axis);
assert!(
(measured - expected).abs() < 1e-5,
"{label}: quadratic_form along {axis:?} must return that principal set's \
band sum ({expected:.4}), got {measured:.4}"
);
}
}
#[test]
fn m4_new_species_match_their_target_birefringence() {
let expected: &[(&str, f32, f32)] = &[
("Aquamarine", -0.006, 1e-4),
("Morganite", -0.006, 1e-4),
("Chrysoberyl (Yellow)", 0.009, 1e-4),
("Amethyst", 0.0091, 1e-4),
("Citrine", 0.0091, 1e-4),
("Peridot", 0.036, 1e-4),
("Benitoite", 0.047, 1e-4),
("Andalusite", 0.010, 1e-4),
];
for &(name, target, tol) in expected {
let material =
GemMaterial::by_name(name).unwrap_or_else(|| panic!("{name} must resolve via by_name"));
assert!(
(material.birefringence_delta - target).abs() <= tol,
"{name}: birefringence_delta={} does not match target {target}",
material.birefringence_delta
);
}
for name in [
"Pyrope Garnet",
"Almandine Garnet",
"Spessartine Garnet",
"Color-Change Garnet (Pyrope-Spessartine)",
"Grossular Garnet (Tsavorite)",
"Andradite Garnet (Demantoid)",
"YAG",
"GGG",
"Opal",
"Glass (N-BK7)",
"Glass (F2)",
] {
let material =
GemMaterial::by_name(name).unwrap_or_else(|| panic!("{name} must resolve via by_name"));
assert_eq!(
material.birefringence_delta, 0.0,
"{name}: isotropic material must have birefringence_delta == 0.0"
);
}
}