indicatrix 0.9.0

A physically-based spectral gemstone renderer that turns GemCAD-style cutting instructions into rendered output.
Documentation
//! Built-in material data: Pyrope, Almandine, Spessartine garnets.

use super::{CrystalSystem, GemMaterial, OpticalCharacter};
use crate::optics::{
    absorption::{AbsorptionBand, AbsorptionTensor},
    dispersion::DispersionModel,
};
use glam::Vec3;

/// Spessartine's absorption (Mn2+ lines and bands plus a UV charge-transfer edge), fitted to a
/// measured spectrum; see the comment at the entry below. A separate function so the entry stays
/// within clippy's function-length limit.
fn spessartine_absorption() -> AbsorptionTensor {
    AbsorptionTensor::isotropic(vec![
        // UV / O->Mn charge-transfer edge (stand-in, centre on the bound)
        AbsorptionBand::energy(320.0, 7_023.0, 27.4),
        // Mn2+ 6A1 -> 4E,4A1 (4G) group
        AbsorptionBand::energy(411.0, 556.0, 14.2),
        AbsorptionBand::energy(421.0, 419.0, 6.62),
        AbsorptionBand::energy(429.0, 659.0, 8.24),
        // Mn2+ 4T2 (G)
        AbsorptionBand::energy(462.0, 1_551.0, 2.23),
        AbsorptionBand::energy(485.0, 811.0, 1.59),
        // Mn2+ 4T1 (G) + Fe2+ 505/520
        AbsorptionBand::energy(523.0, 1_971.0, 1.57),
        // weak 550-620 nm tail (sets T(600) = 0.86; without it L*80 C*78)
        AbsorptionBand::energy(590.0, 3_000.0, 0.132),
    ])
}

impl GemMaterial {
    /// Three of the five garnet-group species (the other two, Grossular and
    /// Andradite, are [`Self::built_in_materials_garnets_grossular_and_andradite`] --
    /// split purely to keep each part under clippy's function-length lint). All five
    /// are cubic (isotropic, `birefringence_delta: 0.0`).
    ///
    /// No primary Sellmeier/Cauchy fit is available for any of the five
    /// (garnet-group gemstones are not present in the refractiveindex.info database);
    /// every dispersion fit below is a 2-parameter Cauchy solved from `n_d` and its
    /// "dispersion" figure treated as the standard gemological Fraunhofer B-G
    /// interval, converted to F-C via the same 0.579 ratio as every other gemological
    /// entry in this file -- see
    /// [`Self::built_in_materials_aquamarine_through_citrine`]'s doc comment for the
    /// full convention note. LOWER CONFIDENCE tier, same as Zircon/Topaz/
    /// Tourmaline/Tanzanite/Emerald above; flagged for human cross-check.
    pub(super) fn built_in_materials_garnets_pyrope_through_spessartine() -> Vec<Self> {
        vec![
            // Pyrope (Mg3Al2(SiO4)3): n_d=1.714, B-G 0.022 -> Delta n(F-C) = 0.01274,
            // A=1.714-0.006668/0.5893^2=1.694804, B=0.006668. Abbe V_d ~ 56.05.
            Self {
                name: "Pyrope Garnet".to_string(),
                crystal_system: CrystalSystem::Cubic,
                optical_character: OpticalCharacter::Isotropic,
                dispersion: DispersionModel::Cauchy {
                    a: 1.694_804,
                    b: 0.006_668,
                    c: 0.0,
                },
                birefringence_delta: 0.0,
                // Chromophore: the classic Cr3+ (505nm)/Fe2+ (570nm) pair
                // responsible for pyrope's deep red color. Widths (35nm/45nm) and
                // peaks (1.2/1.4) TUNED for a clear deep red; band CENTRES cited.
                absorption: AbsorptionTensor::isotropic(vec![
                    AbsorptionBand::new(505.0, 35.0, 1.2),
                    AbsorptionBand::new(570.0, 45.0, 1.4),
                ]),
                c_axis: Vec3::Y,
                biaxial_delta_beta_alpha: None,
                scattering_sigma_s: 0.0,
                scattering_g: 0.0,
                edge_rounding_radius: 0.0,
                absorption_path_scale: 1.0,
                absorption_unit: super::AbsorptionUnit::ModelUnit,
                zoning: None,
                uniaxial_extraordinary_dispersion: None,
            },
            // Almandine (Fe3Al2(SiO4)3): n_d=1.790, B-G 0.024 -> Delta n(F-C) =
            // 0.01390, A=1.790-0.007274/0.5893^2=1.769057, B=0.007274. Abbe V_d~56.86.
            Self {
                name: "Almandine Garnet".to_string(),
                crystal_system: CrystalSystem::Cubic,
                optical_character: OpticalCharacter::Isotropic,
                dispersion: DispersionModel::Cauchy {
                    a: 1.769_057,
                    b: 0.007_274,
                    c: 0.0,
                },
                birefringence_delta: 0.0,
                // Chromophore: the diagnostic Fe2+ 505/520/573nm triplet (almandine's
                // characteristic three-line absorption pattern, the standard
                // gemological identification feature for this species). Widths
                // (20nm each) and peaks (1.3/1.5/1.3) TUNED for a deep red-brown/
                // violet-red; band CENTRES cited.
                absorption: AbsorptionTensor::isotropic(vec![
                    AbsorptionBand::new(505.0, 20.0, 1.3),
                    AbsorptionBand::new(520.0, 20.0, 1.5),
                    AbsorptionBand::new(573.0, 20.0, 1.3),
                ]),
                c_axis: Vec3::Y,
                biaxial_delta_beta_alpha: None,
                scattering_sigma_s: 0.0,
                scattering_g: 0.0,
                edge_rounding_radius: 0.0,
                absorption_path_scale: 1.0,
                absorption_unit: super::AbsorptionUnit::ModelUnit,
                zoning: None,
                uniaxial_extraordinary_dispersion: None,
            },
            // Spessartine (Mn3Al2(SiO4)3): n_d=1.800, B-G 0.027 -> Delta n(F-C) =
            // 0.01563, A=1.800-0.008182/0.5893^2=1.776445, B=0.008182. Abbe V_d~51.19.
            Self {
                name: "Spessartine Garnet".to_string(),
                crystal_system: CrystalSystem::Cubic,
                optical_character: OpticalCharacter::Isotropic,
                dispersion: DispersionModel::Cauchy {
                    a: 1.776_445,
                    b: 0.008_182,
                    c: 0.0,
                },
                birefringence_delta: 0.0,
                // Chromophore: Mn2+ spin-forbidden lines at 411/421/429 nm (the
                // near-UV cut-off), the 462 and 485 nm Mn2+ bands and the weak 523 nm
                // Mn2+/Fe2+ band, plus a UV charge-transfer edge (stand-in band at
                // 320 nm). FITTED 2026-10-19 by a bounded
                // least-squares energy-Gaussian fit (log residual, 380-780 nm) to the
                // Caltech Mineral Spectroscopy Server spectrum of spessartine GRR 43
                // (Rutherford Mine, Amelia, Virginia; 2.165 mm; baseline 0.030
                // removed),
                // `data/reference_spectra/caltech_spessartine_grr43_2p165mm.csv`,
                // scaled to the 8.8 mm face-up path of a 7 mm stone at the sample's
                // own (near end-member, Fe-poor "Mandarin") composition. Swatch
                // (7 mm, D65): L*77 C*72 h=66; p = 1.5: h 63, p = 2: h 61. The
                // Fe-richer GRR 72 from the same mine gives L*51 h 62.
                absorption: spessartine_absorption(),
                c_axis: Vec3::Y,
                biaxial_delta_beta_alpha: None,
                scattering_sigma_s: 0.0,
                scattering_g: 0.0,
                edge_rounding_radius: 0.0,
                absorption_path_scale: 1.0,
                absorption_unit: super::AbsorptionUnit::ModelUnit,
                zoning: None,
                uniaxial_extraordinary_dispersion: None,
            },
        ]
    }

    /// The color-change garnet (Tanzanian pyrope-spessartine), split out of
    /// [`Self::built_in_materials_garnets_pyrope_through_spessartine`] purely to keep each
    /// part under clippy's function-length lint. Same 2-parameter Cauchy convention as the
    /// three species above.
    pub(super) fn built_in_materials_garnet_color_change() -> Vec<Self> {
        vec![
            // Color-Change Garnet: pyrope-spessartine solid solution (Mg,Mn)3Al2(SiO4)3 with
            // traces of Cr3+ and/or V3+, Tanzania (Umba valley, Tanga; also Lindi/Tunduru).
            // Cubic, isotropic. SG ~3.85 (comment only, not stored on the material).
            //
            // n_d=1.760 (typical range 1.755-1.765), roughly the midpoint of pyrope 1.714 and
            // spessartine 1.800 (cross-check: `chromophore::catalogue::garnet_optics(
            // &[("spessartine", 0.5)])` fills the remainder with pyrope and returns the
            // linear mix 0.5*1.714+0.5*1.800 = 1.757, SG 3.88; the stored 1.760 is the
            // typical gemmological value for the Tanzanian material and stays).
            // Dispersion B-G 0.025 (between pyrope 0.022 and spessartine 0.027) -> Delta n(F-C)
            // = 0.025*0.579 = 0.014475; B = 0.014475/(1/0.4861^2-1/0.6563^2) =
            // 0.014475/1.91053 = 0.007576; A = 1.760-0.007576/0.5893^2 = 1.760-0.021816 =
            // 1.738184. Abbe V_d ~ 52.5.
            Self {
                name: "Color-Change Garnet (Pyrope-Spessartine)".to_string(),
                crystal_system: CrystalSystem::Cubic,
                optical_character: OpticalCharacter::Isotropic,
                dispersion: DispersionModel::Cauchy {
                    a: 1.738_184,
                    b: 0.007_576,
                    c: 0.0,
                },
                birefringence_delta: 0.0,
                // Chromophores: Mn2+ 410/421/430nm (narrow spin-forbidden group, sigma
                // 7.5-8nm = ~18-19nm FWHM, strong) blocks violet; Fe2+ 505nm weak (sigma
                // 10.5nm = ~25nm FWHM); and the color-change band, ONE BROAD Cr3+/V3+ band
                // centred ~573nm (sigma 40nm = ~94nm FWHM, strong). It leaves two
                // transmission windows -- blue-green ~450-500nm and red >620nm -- and that
                // balance is what swings the hue between daylight (D65: brownish/greyish
                // mauve-green, the blue window dominates) and incandescent (3200K/A: the
                // lamp is weak in the blue, so the red window wins: pink-red to
                // orange-red). Band CENTRES cited; widths and peaks (1.5/1.2/1.5, 0.25,
                // 1.3) TUNED, on the same band-weight scale as the garnets above.
                absorption: AbsorptionTensor::isotropic(vec![
                    AbsorptionBand::new(410.0, 8.0, 1.5),
                    AbsorptionBand::new(421.0, 7.5, 1.2),
                    AbsorptionBand::new(430.0, 8.0, 1.5),
                    AbsorptionBand::new(505.0, 10.5, 0.25),
                    AbsorptionBand::new(573.0, 40.0, 1.3),
                ]),
                c_axis: Vec3::Y,
                biaxial_delta_beta_alpha: None,
                scattering_sigma_s: 0.0,
                scattering_g: 0.0,
                edge_rounding_radius: 0.0,
                absorption_path_scale: 1.0,
                absorption_unit: super::AbsorptionUnit::ModelUnit,
                zoning: None,
                uniaxial_extraordinary_dispersion: None,
            },
        ]
    }
}