Skip to content

Materials API

materials

Material definitions and permittivity/permeability calculations.

Crystal classes are modelled with frequency-dependent Lorentz-oscillator permittivity (parameters in material_params.json): uniaxial, biaxial, monoclinic (non-zero off-diagonal components), isotropic, and arbitrary user-defined permittivity/permeability tensors. Use :func:list_materials for the current built-in catalogue and :func:create_material to instantiate one.

Classes:

  • BaseMaterial

    Base class for all materials providing common functionality.

  • UniaxialMaterial

    Base class for anisotropic materials with a single optical axis.

  • ParameterizedUniaxialMaterial

    Base class for uniaxial materials with parameters from configuration.

  • Quartz

    Quartz material implementation.

  • Sapphire

    Sapphire material implementation.

  • Calcite

    Calcite material implementation.

  • CalciteLower

    Lower frequency range Calcite implementation.

  • CalciteUpper

    Upper frequency range Calcite implementation.

  • AluminiumNitride

    Wurtzite AlN — c-cut uniaxial (E1 ordinary, A1 extraordinary phonons).

  • SiliconCarbide

    4H-SiC — modeled as a c-cut uniaxial crystal (ordinary ≈ extraordinary).

  • HexagonalBoronNitride

    hBN — the benchmark natural hyperbolic material (c-cut uniaxial, two bands).

  • GalliumNitride

    Wurtzite GaN — c-cut uniaxial polar semiconductor.

  • BiaxialMaterial

    Orthorhombic biaxial material: a diagonal ε tensor with three distinct axes.

  • ParameterizedBiaxialMaterial

    Base class for biaxial materials with parameters from configuration.

  • MolybdenumTrioxide

    α-MoO₃ — biaxial van der Waals crystal supporting in-plane hyperbolic polaritons.

  • MonoclinicMaterial

    Base class for monoclinic materials with more complex permittivity tensors.

  • GalliumOxide

    Gallium Oxide implementation.

  • ArbitraryMaterial

    Material with arbitrary permittivity and permeability tensor components.

  • IsotropicMaterial

    Base class for isotropic materials like air.

  • Air

    Air material implementation.

Functions:

BaseMaterial

BaseMaterial(frequency_length=410)

Base class for all materials providing common functionality.

Initialize base material with frequency array length.

Parameters:

  • frequency_length (int, default: 410 ) –

    Number of frequency points for dispersion calculations

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(self, frequency_length: int = 410) -> None:
    """Initialize base material with frequency array length.

    Args:
        frequency_length: Number of frequency points for dispersion calculations
    """
    self.frequency_length = frequency_length
    self.name = "Base Material"
    self.frequency = None
    self.mu_r = 1.0  # Default magnetic permeability

fetch_magnetic_tensor

fetch_magnetic_tensor()

Fetch magnetic permeability tensor for full frequency range.

Returns:

  • ndarray

    Complex magnetic permeability tensor with shape matching permittivity

Note

Default implementation returns isotropic tensor. Override in subclasses for magnetic materials.

Source code in hyperbolic_optics/materials.py
def fetch_magnetic_tensor(self) -> np.ndarray:
    """Fetch magnetic permeability tensor for full frequency range.

    Returns:
        Complex magnetic permeability tensor with shape matching permittivity

    Note:
        Default implementation returns isotropic tensor. Override in subclasses
        for magnetic materials.
    """
    eps_tensor = self.fetch_permittivity_tensor()
    return self._create_isotropic_mu_tensor_like(eps_tensor)

fetch_magnetic_tensor_for_freq

fetch_magnetic_tensor_for_freq(requested_frequency)

Fetch magnetic permeability tensor for specific frequency.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹

Returns:

  • ndarray

    Complex magnetic permeability tensor at the requested frequency

Source code in hyperbolic_optics/materials.py
def fetch_magnetic_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Fetch magnetic permeability tensor for specific frequency.

    Args:
        requested_frequency: Frequency in cm⁻¹

    Returns:
        Complex magnetic permeability tensor at the requested frequency
    """
    eps_tensor = self.fetch_permittivity_tensor_for_freq(requested_frequency)
    return self._create_isotropic_mu_tensor_like(eps_tensor)

UniaxialMaterial

UniaxialMaterial(frequency_length=410)

Bases: BaseMaterial

Base class for anisotropic materials with a single optical axis.

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(self, frequency_length: int = 410) -> None:
    """Initialize base material with frequency array length.

    Args:
        frequency_length: Number of frequency points for dispersion calculations
    """
    self.frequency_length = frequency_length
    self.name = "Base Material"
    self.frequency = None
    self.mu_r = 1.0  # Default magnetic permeability

permittivity_calc_for_freq

permittivity_calc_for_freq(frequency, high_freq, omega_tn, gamma_tn, omega_ln, gamma_ln)

Calculate permittivity at a single frequency using Lorentz oscillator model.

Parameters:

  • frequency (float) –

    Frequency in cm⁻¹

  • high_freq (float) –

    High-frequency dielectric constant (ε∞)

  • omega_tn (ndarray) –

    Transverse optical phonon frequencies

  • gamma_tn (ndarray) –

    Transverse phonon damping constants

  • omega_ln (ndarray) –

    Longitudinal optical phonon frequencies

  • gamma_ln (ndarray) –

    Longitudinal phonon damping constants

Returns:

  • complex

    Complex permittivity at the specified frequency

Note

Uses the factorized form: ε(ω) = ε∞ ∏ᵢ (ωₗᵢ² - ω² - iωγₗᵢ)/(ωₜᵢ² - ω² - iωγₜᵢ)

Source code in hyperbolic_optics/materials.py
def permittivity_calc_for_freq(
    self,
    frequency: float,
    high_freq: float,
    omega_tn: np.ndarray,
    gamma_tn: np.ndarray,
    omega_ln: np.ndarray,
    gamma_ln: np.ndarray,
) -> complex:
    """Calculate permittivity at a single frequency using Lorentz oscillator model.

    Args:
        frequency: Frequency in cm⁻¹
        high_freq: High-frequency dielectric constant (ε∞)
        omega_tn: Transverse optical phonon frequencies
        gamma_tn: Transverse phonon damping constants
        omega_ln: Longitudinal optical phonon frequencies
        gamma_ln: Longitudinal phonon damping constants

    Returns:
        Complex permittivity at the specified frequency

    Note:
        Uses the factorized form: ε(ω) = ε∞ ∏ᵢ (ωₗᵢ² - ω² - iωγₗᵢ)/(ωₜᵢ² - ω² - iωγₜᵢ)
    """
    frequency = np.array([frequency], dtype=np.float64)

    # Convert parameters to numpy arrays
    omega_ln = np.asarray(omega_ln, dtype=np.complex128)
    gamma_ln = np.asarray(gamma_ln, dtype=np.complex128)
    omega_tn = np.asarray(omega_tn, dtype=np.complex128)
    gamma_tn = np.asarray(gamma_tn, dtype=np.complex128)

    # Expand dimensions for broadcasting
    omega_ln_expanded = omega_ln[:, np.newaxis]
    gamma_ln_expanded = gamma_ln[:, np.newaxis]
    omega_tn_expanded = omega_tn[:, np.newaxis]
    gamma_tn_expanded = gamma_tn[:, np.newaxis]

    top_line = omega_ln_expanded**2.0 - frequency**2.0 - 1j * frequency * gamma_ln_expanded
    bottom_line = omega_tn_expanded**2.0 - frequency**2.0 - 1j * frequency * gamma_tn_expanded
    result = top_line / bottom_line

    return (high_freq * np.prod(result, axis=0))[0]

permittivity_calc

permittivity_calc(high_freq, omega_tn, gamma_tn, omega_ln, gamma_ln)

Calculate permittivity over full frequency range.

Parameters:

  • high_freq (float) –

    High-frequency dielectric constant

  • omega_tn (ndarray) –

    Transverse optical phonon frequencies

  • gamma_tn (ndarray) –

    Transverse phonon damping constants

  • omega_ln (ndarray) –

    Longitudinal optical phonon frequencies

  • gamma_ln (ndarray) –

    Longitudinal phonon damping constants

Returns:

  • ndarray

    Complex permittivity array over all frequencies

Source code in hyperbolic_optics/materials.py
def permittivity_calc(
    self,
    high_freq: float,
    omega_tn: np.ndarray,
    gamma_tn: np.ndarray,
    omega_ln: np.ndarray,
    gamma_ln: np.ndarray,
) -> np.ndarray:
    """Calculate permittivity over full frequency range.

    Args:
        high_freq: High-frequency dielectric constant
        omega_tn: Transverse optical phonon frequencies
        gamma_tn: Transverse phonon damping constants
        omega_ln: Longitudinal optical phonon frequencies
        gamma_ln: Longitudinal phonon damping constants

    Returns:
        Complex permittivity array over all frequencies
    """
    frequency = np.expand_dims(self.frequency, 0)

    # Convert parameters to numpy arrays
    omega_ln = np.asarray(omega_ln, dtype=np.complex128)
    gamma_ln = np.asarray(gamma_ln, dtype=np.complex128)
    omega_tn = np.asarray(omega_tn, dtype=np.complex128)
    gamma_tn = np.asarray(gamma_tn, dtype=np.complex128)

    omega_ln_expanded = omega_ln[:, np.newaxis]
    gamma_ln_expanded = gamma_ln[:, np.newaxis]
    omega_tn_expanded = omega_tn[:, np.newaxis]
    gamma_tn_expanded = gamma_tn[:, np.newaxis]

    top_line = omega_ln_expanded**2.0 - frequency**2.0 - 1j * frequency * gamma_ln_expanded
    bottom_line = omega_tn_expanded**2.0 - frequency**2.0 - 1j * frequency * gamma_tn_expanded
    result = top_line / bottom_line

    return high_freq * np.prod(result, axis=0)

fetch_permittivity_tensor

fetch_permittivity_tensor()

Fetch full permittivity tensor for all frequencies.

Returns:

  • ndarray

    Permittivity tensor with shape [N, 3, 3] where N is number of frequencies

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor(self) -> np.ndarray:
    """Fetch full permittivity tensor for all frequencies.

    Returns:
        Permittivity tensor with shape [N, 3, 3] where N is number of frequencies
    """
    eps_ext, eps_ord = self.permittivity_fetch()
    return self._create_permittivity_tensor(eps_ext, eps_ord)

fetch_permittivity_tensor_for_freq

fetch_permittivity_tensor_for_freq(requested_frequency)

Fetch permittivity tensor at specific frequency.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹

Returns:

  • ndarray

    Permittivity tensor with shape [3, 3]

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Fetch permittivity tensor at specific frequency.

    Args:
        requested_frequency: Frequency in cm⁻¹

    Returns:
        Permittivity tensor with shape [3, 3]
    """
    params = self.permittivity_parameters()
    eps_ext = self.permittivity_calc_for_freq(requested_frequency, **params["extraordinary"])
    eps_ord = self.permittivity_calc_for_freq(requested_frequency, **params["ordinary"])
    return self._create_permittivity_tensor(eps_ext, eps_ord)

permittivity_fetch

permittivity_fetch()

Fetch extraordinary and ordinary permittivity values.

Returns:

  • tuple[ndarray, ndarray]

    Tuple of (eps_extraordinary, eps_ordinary) arrays

Source code in hyperbolic_optics/materials.py
def permittivity_fetch(self) -> tuple[np.ndarray, np.ndarray]:
    """Fetch extraordinary and ordinary permittivity values.

    Returns:
        Tuple of (eps_extraordinary, eps_ordinary) arrays
    """
    params = self.permittivity_parameters()
    eps_ext = self.permittivity_calc(**params["extraordinary"])
    eps_ord = self.permittivity_calc(**params["ordinary"])
    return eps_ext, eps_ord

ParameterizedUniaxialMaterial

ParameterizedUniaxialMaterial(material_type, freq_min=None, freq_max=None, mu_r=1.0)

Bases: UniaxialMaterial

Base class for uniaxial materials with parameters from configuration.

Initialize uniaxial material from parameter configuration.

Parameters:

  • material_type (str) –

    Material identifier in configuration ('quartz', 'sapphire', etc.)

  • freq_min (float | None, default: None ) –

    Override minimum frequency in cm⁻¹

  • freq_max (float | None, default: None ) –

    Override maximum frequency in cm⁻¹

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0 for non-magnetic)

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(
    self,
    material_type: str,
    freq_min: float | None = None,
    freq_max: float | None = None,
    mu_r: float = 1.0,
) -> None:
    """Initialize uniaxial material from parameter configuration.

    Args:
        material_type: Material identifier in configuration ('quartz', 'sapphire', etc.)
        freq_min: Override minimum frequency in cm⁻¹
        freq_max: Override maximum frequency in cm⁻¹
        mu_r: Relative magnetic permeability (default: 1.0 for non-magnetic)
    """
    super().__init__()
    params = load_material_parameters()["uniaxial_materials"][material_type]
    self.name = params.get("name", "Unnamed Material")
    self.material_type = material_type
    self.mu_r = mu_r

    if "frequency_range" in params:
        self._initialize_frequency_range(params, freq_min, freq_max)
    else:
        self.frequency = None

permittivity_parameters

permittivity_parameters()

Get permittivity parameters from JSON configuration.

Returns:

  • dict[str, dict[str, ndarray]]

    Dictionary containing ordinary and extraordinary axis parameters

Source code in hyperbolic_optics/materials.py
def permittivity_parameters(self) -> dict[str, dict[str, np.ndarray]]:
    """Get permittivity parameters from JSON configuration.

    Returns:
        Dictionary containing ordinary and extraordinary axis parameters
    """
    params = load_material_parameters()["uniaxial_materials"][self.material_type]["parameters"]
    return {
        axis: {key: np.array(value, dtype=np.complex128) for key, value in axis_params.items()}
        for axis, axis_params in params.items()
    }

Quartz

Quartz(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

Quartz material implementation.

Initialize Quartz (α-SiO₂) material.

Parameters:

  • freq_min (float | None, default: None ) –

    Override minimum frequency (default: 410 cm⁻¹)

  • freq_max (float | None, default: None ) –

    Override maximum frequency (default: 600 cm⁻¹)

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0)

Note

Quartz is a uniaxial positive crystal supporting hyperbolic phonon polaritons in the far-infrared.

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize Quartz (α-SiO₂) material.

    Args:
        freq_min: Override minimum frequency (default: 410 cm⁻¹)
        freq_max: Override maximum frequency (default: 600 cm⁻¹)
        mu_r: Relative magnetic permeability (default: 1.0)

    Note:
        Quartz is a uniaxial positive crystal supporting hyperbolic phonon
        polaritons in the far-infrared.
    """
    super().__init__("quartz", freq_min, freq_max, mu_r)

Sapphire

Sapphire(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

Sapphire material implementation.

Initialize Sapphire (α-Al₂O₃) material.

Parameters:

  • freq_min (float | None, default: None ) –

    Override minimum frequency (default: 210 cm⁻¹)

  • freq_max (float | None, default: None ) –

    Override maximum frequency (default: 1000 cm⁻¹)

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0)

Note

Sapphire is a uniaxial crystal with hyperbolic dispersion.

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize Sapphire (α-Al₂O₃) material.

    Args:
        freq_min: Override minimum frequency (default: 210 cm⁻¹)
        freq_max: Override maximum frequency (default: 1000 cm⁻¹)
        mu_r: Relative magnetic permeability (default: 1.0)

    Note:
        Sapphire is a uniaxial crystal with hyperbolic
        dispersion.
    """
    super().__init__("sapphire", freq_min, freq_max, mu_r)

Calcite

Calcite(freq_min=None, freq_max=None, variant=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

Calcite material implementation.

Initialize Calcite (CaCO₃) material with specified reststrahlen band.

Parameters:

  • freq_min (float | None, default: None ) –

    Override minimum frequency

  • freq_max (float | None, default: None ) –

    Override maximum frequency

  • variant (str | None, default: None ) –

    'lower' for 860-920 cm⁻¹ or 'upper' for 1300-1600 cm⁻¹

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0)

Raises:

  • ValueError

    If variant is not 'lower' or 'upper'

Note

Calcite must be instantiated through CalciteLower or CalciteUpper subclasses rather than directly.

Source code in hyperbolic_optics/materials.py
def __init__(
    self,
    freq_min: float | None = None,
    freq_max: float | None = None,
    variant: str | None = None,
    mu_r: float = 1.0,
) -> None:
    """Initialize Calcite (CaCO₃) material with specified reststrahlen band.

    Args:
        freq_min: Override minimum frequency
        freq_max: Override maximum frequency
        variant: 'lower' for 860-920 cm⁻¹ or 'upper' for 1300-1600 cm⁻¹
        mu_r: Relative magnetic permeability (default: 1.0)

    Raises:
        ValueError: If variant is not 'lower' or 'upper'

    Note:
        Calcite must be instantiated through CalciteLower or CalciteUpper
        subclasses rather than directly.
    """
    if variant is None:
        raise ValueError(
            "Calcite material must be instantiated with a variant ('lower' or 'upper')"
        )

    calcite_config = load_material_parameters()["uniaxial_materials"]["calcite"]
    super().__init__("calcite", freq_min, freq_max, mu_r)

    if variant not in calcite_config["variants"]:
        raise ValueError("Calcite variant must be either 'lower' or 'upper'")

    variant_params = calcite_config["variants"][variant]
    self.name = variant_params.get("name", self.name)
    self._initialize_frequency_range(variant_params, freq_min, freq_max)

CalciteLower

CalciteLower(freq_min=None, freq_max=None, mu_r=1.0)

Bases: Calcite

Lower frequency range Calcite implementation.

Initialize Calcite lower reststrahlen band (860-920 cm⁻¹).

Parameters:

  • freq_min (float | None, default: None ) –

    Override minimum frequency

  • freq_max (float | None, default: None ) –

    Override maximum frequency

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0)

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize Calcite lower reststrahlen band (860-920 cm⁻¹).

    Args:
        freq_min: Override minimum frequency
        freq_max: Override maximum frequency
        mu_r: Relative magnetic permeability (default: 1.0)
    """
    super().__init__(freq_min, freq_max, variant="lower", mu_r=mu_r)

CalciteUpper

CalciteUpper(freq_min=None, freq_max=None, mu_r=1.0)

Bases: Calcite

Upper frequency range Calcite implementation.

Initialize Calcite upper reststrahlen band (1300-1600 cm⁻¹).

Parameters:

  • freq_min (float | None, default: None ) –

    Override minimum frequency

  • freq_max (float | None, default: None ) –

    Override maximum frequency

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0)

Note

The upper band exhibits type-II hyperbolic dispersion (ε_∥ < 0, ε_⊥ > 0).

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize Calcite upper reststrahlen band (1300-1600 cm⁻¹).

    Args:
        freq_min: Override minimum frequency
        freq_max: Override maximum frequency
        mu_r: Relative magnetic permeability (default: 1.0)

    Note:
        The upper band exhibits type-II hyperbolic dispersion (ε_∥ < 0, ε_⊥ > 0).
    """
    super().__init__(freq_min, freq_max, variant="upper", mu_r=mu_r)

AluminiumNitride

AluminiumNitride(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

Wurtzite AlN — c-cut uniaxial (E1 ordinary, A1 extraordinary phonons).

Initialize AlN (aluminium nitride), a c-cut uniaxial polar crystal.

Used (with SiC and MoO₃) in the layer-resolved absorption example. See material_params.json for the phonon parameters and their provenance.

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize AlN (aluminium nitride), a c-cut uniaxial polar crystal.

    Used (with SiC and MoO₃) in the layer-resolved absorption example. See
    ``material_params.json`` for the phonon parameters and their provenance.
    """
    super().__init__("aluminium_nitride", freq_min, freq_max, mu_r)

SiliconCarbide

SiliconCarbide(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

4H-SiC — modeled as a c-cut uniaxial crystal (ordinary ≈ extraordinary).

Initialize SiC (silicon carbide), a polar crystal with a single TO-LO pair.

SiC is nearly isotropic in its reststrahlen band; the parameters use equal ordinary/extraordinary oscillators (see material_params.json).

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize SiC (silicon carbide), a polar crystal with a single TO-LO pair.

    SiC is nearly isotropic in its reststrahlen band; the parameters use equal
    ordinary/extraordinary oscillators (see ``material_params.json``).
    """
    super().__init__("silicon_carbide", freq_min, freq_max, mu_r)

HexagonalBoronNitride

HexagonalBoronNitride(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

hBN — the benchmark natural hyperbolic material (c-cut uniaxial, two bands).

Initialize natural hexagonal boron nitride.

Two reststrahlen bands give type-I (out-of-plane, ~760-825 cm⁻¹) and type-II (in-plane, ~1360-1614 cm⁻¹) hyperbolic dispersion. See material_params.json for the phonon parameters and their provenance.

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize natural hexagonal boron nitride.

    Two reststrahlen bands give type-I (out-of-plane, ~760-825 cm⁻¹) and
    type-II (in-plane, ~1360-1614 cm⁻¹) hyperbolic dispersion. See
    ``material_params.json`` for the phonon parameters and their provenance.
    """
    super().__init__("hexagonal_boron_nitride", freq_min, freq_max, mu_r)

GalliumNitride

GalliumNitride(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedUniaxialMaterial

Wurtzite GaN — c-cut uniaxial polar semiconductor.

Initialize GaN (gallium nitride), a wurtzite polar semiconductor.

See material_params.json for the E1/A1 phonon parameters.

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize GaN (gallium nitride), a wurtzite polar semiconductor.

    See ``material_params.json`` for the E1/A1 phonon parameters.
    """
    super().__init__("gallium_nitride", freq_min, freq_max, mu_r)

BiaxialMaterial

BiaxialMaterial(frequency_length=410)

Bases: UniaxialMaterial

Orthorhombic biaxial material: a diagonal ε tensor with three distinct axes.

Generalizes :class:UniaxialMaterial from two principal values (ordinary, extraordinary) to three (x, y, z). It reuses the factorized TO-LO permittivity_calc per axis — the same Lowndes/Gervais form the α-MoO₃ literature uses — and only changes how the diagonal tensor is assembled. There is no off-diagonal coupling (unlike monoclinic :class:MonoclinicMaterial).

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(self, frequency_length: int = 410) -> None:
    """Initialize base material with frequency array length.

    Args:
        frequency_length: Number of frequency points for dispersion calculations
    """
    self.frequency_length = frequency_length
    self.name = "Base Material"
    self.frequency = None
    self.mu_r = 1.0  # Default magnetic permeability

permittivity_fetch

permittivity_fetch()

Return (eps_x, eps_y, eps_z) over the full frequency range.

Source code in hyperbolic_optics/materials.py
def permittivity_fetch(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:  # type: ignore[override]
    """Return ``(eps_x, eps_y, eps_z)`` over the full frequency range."""
    params = self.permittivity_parameters()
    eps_x = self.permittivity_calc(**params["x"])
    eps_y = self.permittivity_calc(**params["y"])
    eps_z = self.permittivity_calc(**params["z"])
    return eps_x, eps_y, eps_z

fetch_permittivity_tensor

fetch_permittivity_tensor()

Fetch the full diagonal permittivity tensor for all frequencies.

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor(self) -> np.ndarray:
    """Fetch the full diagonal permittivity tensor for all frequencies."""
    eps_x, eps_y, eps_z = self.permittivity_fetch()
    return self._create_permittivity_tensor(eps_x, eps_y, eps_z)

fetch_permittivity_tensor_for_freq

fetch_permittivity_tensor_for_freq(requested_frequency)

Fetch the diagonal permittivity tensor at a single frequency.

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Fetch the diagonal permittivity tensor at a single frequency."""
    params = self.permittivity_parameters()
    eps_x = self.permittivity_calc_for_freq(requested_frequency, **params["x"])
    eps_y = self.permittivity_calc_for_freq(requested_frequency, **params["y"])
    eps_z = self.permittivity_calc_for_freq(requested_frequency, **params["z"])
    return self._create_permittivity_tensor(eps_x, eps_y, eps_z)

ParameterizedBiaxialMaterial

ParameterizedBiaxialMaterial(material_type, freq_min=None, freq_max=None, mu_r=1.0)

Bases: BiaxialMaterial

Base class for biaxial materials with parameters from configuration.

Initialize a biaxial material from the biaxial_materials config block.

Parameters:

  • material_type (str) –

    Key in material_params.json biaxial_materials.

  • freq_min (float | None, default: None ) –

    Override minimum frequency in cm⁻¹.

  • freq_max (float | None, default: None ) –

    Override maximum frequency in cm⁻¹.

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0, non-magnetic).

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(
    self,
    material_type: str,
    freq_min: float | None = None,
    freq_max: float | None = None,
    mu_r: float = 1.0,
) -> None:
    """Initialize a biaxial material from the ``biaxial_materials`` config block.

    Args:
        material_type: Key in ``material_params.json`` ``biaxial_materials``.
        freq_min: Override minimum frequency in cm⁻¹.
        freq_max: Override maximum frequency in cm⁻¹.
        mu_r: Relative magnetic permeability (default: 1.0, non-magnetic).
    """
    BaseMaterial.__init__(self)
    params = load_material_parameters()["biaxial_materials"][material_type]
    self.name = params.get("name", "Unnamed Biaxial Material")
    self.material_type = material_type
    self.mu_r = mu_r

    if "frequency_range" in params:
        self._initialize_frequency_range(params, freq_min, freq_max)
    else:
        self.frequency = None

permittivity_parameters

permittivity_parameters()

Get per-axis (x, y, z) oscillator parameters from configuration.

Source code in hyperbolic_optics/materials.py
def permittivity_parameters(self) -> dict[str, dict[str, np.ndarray]]:
    """Get per-axis (x, y, z) oscillator parameters from configuration."""
    params = load_material_parameters()["biaxial_materials"][self.material_type]["parameters"]
    return {
        axis: {key: np.array(value, dtype=np.complex128) for key, value in axis_params.items()}
        for axis, axis_params in params.items()
    }

MolybdenumTrioxide

MolybdenumTrioxide(freq_min=None, freq_max=None, mu_r=1.0)

Bases: ParameterizedBiaxialMaterial

α-MoO₃ — biaxial van der Waals crystal supporting in-plane hyperbolic polaritons.

Initialize α-MoO₃ (orthorhombic, biaxial).

Three distinct reststrahlen bands give strong in-plane anisotropy, the origin of the azimuth-dependent hyperbolic phonon polariton in the layer-resolved absorption example. Parameters and provenance are in material_params.json.

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize α-MoO₃ (orthorhombic, biaxial).

    Three distinct reststrahlen bands give strong in-plane anisotropy, the
    origin of the azimuth-dependent hyperbolic phonon polariton in the
    layer-resolved absorption example. Parameters and provenance are in
    ``material_params.json``.
    """
    super().__init__("molybdenum_trioxide", freq_min, freq_max, mu_r)

MonoclinicMaterial

MonoclinicMaterial(frequency_length=410)

Bases: BaseMaterial

Base class for monoclinic materials with more complex permittivity tensors.

Source code in hyperbolic_optics/materials.py
def __init__(self, frequency_length: int = 410) -> None:
    """Initialize base material with frequency array length.

    Args:
        frequency_length: Number of frequency points for dispersion calculations
    """
    self.frequency_length = frequency_length
    self.name = "Base Material"
    self.frequency = None
    self.mu_r = 1.0  # Default magnetic permeability

GalliumOxide

GalliumOxide(freq_min=None, freq_max=None, mu_r=1.0)

Bases: MonoclinicMaterial

Gallium Oxide implementation.

Initialize β-Ga₂O₃ (monoclinic) material.

Parameters:

  • freq_min (float | None, default: None ) –

    Override minimum frequency (default: 350 cm⁻¹)

  • freq_max (float | None, default: None ) –

    Override maximum frequency (default: 800 cm⁻¹)

  • mu_r (float, default: 1.0 ) –

    Relative magnetic permeability (default: 1.0)

Note

Gallium oxide is a monoclinic crystal with non-zero ε_xy coupling, supporting hyperbolic polaritons with in-plane anisotropy.

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(
    self, freq_min: float | None = None, freq_max: float | None = None, mu_r: float = 1.0
) -> None:
    """Initialize β-Ga₂O₃ (monoclinic) material.

    Args:
        freq_min: Override minimum frequency (default: 350 cm⁻¹)
        freq_max: Override maximum frequency (default: 800 cm⁻¹)
        mu_r: Relative magnetic permeability (default: 1.0)

    Note:
        Gallium oxide is a monoclinic crystal with non-zero ε_xy coupling,
        supporting hyperbolic polaritons with in-plane anisotropy.
    """
    super().__init__()
    params = load_material_parameters()["monoclinic_materials"]["gallium_oxide"]
    self.name = params["name"]
    self.mu_r = mu_r
    self._initialize_frequency_range(params, freq_min, freq_max)

permittivity_parameters

permittivity_parameters()

Get Gallium Oxide symmetry mode parameters.

Returns:

  • dict[str, dict[str, Any]]

    Dictionary with 'Au' and 'Bu' mode parameters including oscillator

  • dict[str, dict[str, Any]]

    strengths, frequencies, dampings, and orientation angles

Source code in hyperbolic_optics/materials.py
def permittivity_parameters(self) -> dict[str, dict[str, Any]]:
    """Get Gallium Oxide symmetry mode parameters.

    Returns:
        Dictionary with 'Au' and 'Bu' mode parameters including oscillator
        strengths, frequencies, dampings, and orientation angles
    """
    params = load_material_parameters()["monoclinic_materials"]["gallium_oxide"]["parameters"]
    # Convert all numeric values to numpy arrays
    result = {}
    for mode, mode_params in params.items():
        result[mode] = {}
        for key, value in mode_params.items():
            if isinstance(value, dict):
                result[mode][key] = value  # Keep high_freq dict as is
            elif isinstance(value, list):
                result[mode][key] = np.array(value, dtype=np.complex128)
            else:
                result[mode][key] = np.complex128(value)
    return result

permittivity_calc

permittivity_calc()

Calculate all permittivity tensor components over frequency range.

Returns:

  • tuple[ndarray, ndarray, ndarray, ndarray]

    Tuple of (eps_xx, eps_yy, eps_zz, eps_xy) arrays

Source code in hyperbolic_optics/materials.py
def permittivity_calc(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Calculate all permittivity tensor components over frequency range.

    Returns:
        Tuple of (eps_xx, eps_yy, eps_zz, eps_xy) arrays
    """
    parameters = self.permittivity_parameters()
    frequency = self.frequency[:, np.newaxis]

    eps_xx_bu, eps_xy_bu, eps_yy_bu = self._calculate_bu_components(parameters, frequency)
    eps_zz_au = self._calculate_au_component(parameters, frequency)

    eps_xx = parameters["Bu"]["high_freq"]["xx"] + eps_xx_bu
    eps_xy = parameters["Bu"]["high_freq"]["xy"] + eps_xy_bu
    eps_yy = parameters["Bu"]["high_freq"]["yy"] + eps_yy_bu
    eps_zz = parameters["Au"]["high_freq"] + eps_zz_au

    return eps_xx, eps_yy, eps_zz, eps_xy

fetch_permittivity_tensor

fetch_permittivity_tensor()

Fetch full permittivity tensor for all frequencies.

Returns:

  • ndarray

    Permittivity tensor with shape [N, 3, 3]

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor(self) -> np.ndarray:
    """Fetch full permittivity tensor for all frequencies.

    Returns:
        Permittivity tensor with shape [N, 3, 3]
    """
    eps_xx, eps_yy, eps_zz, eps_xy = self.permittivity_calc()
    return self._create_permittivity_tensor(eps_xx, eps_yy, eps_zz, eps_xy)

fetch_permittivity_tensor_for_freq

fetch_permittivity_tensor_for_freq(requested_frequency)

Fetch permittivity tensor at specific frequency.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹

Returns:

  • ndarray

    Permittivity tensor with shape [3, 3]

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Fetch permittivity tensor at specific frequency.

    Args:
        requested_frequency: Frequency in cm⁻¹

    Returns:
        Permittivity tensor with shape [3, 3]
    """
    parameters = self.permittivity_parameters()
    frequency = np.array([[requested_frequency]], dtype=np.float64)

    eps_xx_bu, eps_xy_bu, eps_yy_bu = self._calculate_bu_components(parameters, frequency)
    eps_zz_au = self._calculate_au_component(parameters, frequency)

    eps_xx = parameters["Bu"]["high_freq"]["xx"] + eps_xx_bu[0]
    eps_xy = parameters["Bu"]["high_freq"]["xy"] + eps_xy_bu[0]
    eps_yy = parameters["Bu"]["high_freq"]["yy"] + eps_yy_bu[0]
    eps_zz = parameters["Au"]["high_freq"] + eps_zz_au[0]

    return self._create_permittivity_tensor(eps_xx, eps_yy, eps_zz, eps_xy)

ArbitraryMaterial

ArbitraryMaterial(material_data=None)

Bases: BaseMaterial

Material with arbitrary permittivity and permeability tensor components.

Initialize material with arbitrary permittivity and permeability tensors.

Parameters:

  • material_data (dict[str, Any] | None, default: None ) –

    Dictionary with tensor components (eps_xx, eps_yy, etc.) If None, uses default identity-like values

Example

mat_data = { ... "eps_xx": {"real": 2.5, "imag": 0.1}, ... "eps_yy": {"real": 3.0, "imag": 0.0}, ... "eps_zz": {"real": -4.0, "imag": 0.5}, ... "mu_r": 1.0 ... } material = ArbitraryMaterial(mat_data)

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(self, material_data: dict[str, Any] | None = None) -> None:
    """Initialize material with arbitrary permittivity and permeability tensors.

    Args:
        material_data: Dictionary with tensor components (eps_xx, eps_yy, etc.)
                    If None, uses default identity-like values

    Example:
        >>> mat_data = {
        ...     "eps_xx": {"real": 2.5, "imag": 0.1},
        ...     "eps_yy": {"real": 3.0, "imag": 0.0},
        ...     "eps_zz": {"real": -4.0, "imag": 0.5},
        ...     "mu_r": 1.0
        ... }
        >>> material = ArbitraryMaterial(mat_data)
    """
    super().__init__()
    self.name = "Arbitrary Material"

    if material_data is None:
        material_data = load_material_parameters()["arbitrary_materials"]["default"]

    self._init_tensor_components(material_data)

fetch_permittivity_tensor

fetch_permittivity_tensor()

Construct full permittivity tensor from components.

Returns:

  • ndarray

    3×3 complex permittivity tensor

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor(self) -> np.ndarray:
    """Construct full permittivity tensor from components.

    Returns:
        3×3 complex permittivity tensor
    """
    tensor_elements = [
        [self.eps_xx, self.eps_xy, self.eps_xz],
        [self.eps_xy, self.eps_yy, self.eps_yz],
        [self.eps_xz, self.eps_yz, self.eps_zz],
    ]
    return np.array(tensor_elements, dtype=np.complex128)

fetch_permittivity_tensor_for_freq

fetch_permittivity_tensor_for_freq(requested_frequency)

Return frequency-independent permittivity tensor.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹ (ignored)

Returns:

  • ndarray

    3×3 complex permittivity tensor

Note

Arbitrary materials are frequency-independent by definition.

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Return frequency-independent permittivity tensor.

    Args:
        requested_frequency: Frequency in cm⁻¹ (ignored)

    Returns:
        3×3 complex permittivity tensor

    Note:
        Arbitrary materials are frequency-independent by definition.
    """
    return self.fetch_permittivity_tensor()

fetch_magnetic_tensor

fetch_magnetic_tensor()

Construct full magnetic permeability tensor from components.

Returns:

  • ndarray

    3×3 complex permeability tensor

Source code in hyperbolic_optics/materials.py
def fetch_magnetic_tensor(self) -> np.ndarray:
    """Construct full magnetic permeability tensor from components.

    Returns:
        3×3 complex permeability tensor
    """
    tensor_elements = [
        [self.mu_xx, self.mu_xy, self.mu_xz],
        [self.mu_xy, self.mu_yy, self.mu_yz],
        [self.mu_xz, self.mu_yz, self.mu_zz],
    ]
    return np.array(tensor_elements, dtype=np.complex128)

fetch_magnetic_tensor_for_freq

fetch_magnetic_tensor_for_freq(requested_frequency)

Return frequency-independent magnetic tensor.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹ (ignored)

Returns:

  • ndarray

    3×3 complex permeability tensor

Note

Arbitrary materials are frequency-independent by definition.

Source code in hyperbolic_optics/materials.py
def fetch_magnetic_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Return frequency-independent magnetic tensor.

    Args:
        requested_frequency: Frequency in cm⁻¹ (ignored)

    Returns:
        3×3 complex permeability tensor

    Note:
        Arbitrary materials are frequency-independent by definition.
    """
    return self.fetch_magnetic_tensor()

IsotropicMaterial

IsotropicMaterial(permittivity=None, permeability=None)

Bases: BaseMaterial

Base class for isotropic materials like air.

Initialize isotropic material with scalar permittivity and permeability.

Parameters:

  • permittivity (float | complex | dict[str, float] | None, default: None ) –

    Relative permittivity (scalar or dict with 'real'/'imag')

  • permeability (float | complex | dict[str, float] | None, default: None ) –

    Relative permeability (scalar or dict with 'real'/'imag')

Note

For isotropic materials, all diagonal tensor components are equal and off-diagonal components are zero.

Methods:

Source code in hyperbolic_optics/materials.py
def __init__(
    self,
    permittivity: float | complex | dict[str, float] | None = None,
    permeability: float | complex | dict[str, float] | None = None,
) -> None:
    """Initialize isotropic material with scalar permittivity and permeability.

    Args:
        permittivity: Relative permittivity (scalar or dict with 'real'/'imag')
        permeability: Relative permeability (scalar or dict with 'real'/'imag')

    Note:
        For isotropic materials, all diagonal tensor components are equal
        and off-diagonal components are zero.
    """
    super().__init__()
    self.permittivity = self._process_permittivity(permittivity)
    self.permeability = (
        self._process_permittivity(permeability)
        if permeability is not None
        else complex(1.0, 0.0)
    )

construct_tensor_singular

construct_tensor_singular()

Create diagonal tensor with scalar permittivity value.

Returns:

  • ndarray

    3×3 diagonal tensor with permittivity on diagonal

Source code in hyperbolic_optics/materials.py
def construct_tensor_singular(self) -> np.ndarray:
    """Create diagonal tensor with scalar permittivity value.

    Returns:
        3×3 diagonal tensor with permittivity on diagonal
    """
    return np.array(
        [
            [self.permittivity, 0.0, 0.0],
            [0.0, self.permittivity, 0.0],
            [0.0, 0.0, self.permittivity],
        ],
        dtype=np.complex128,
    )

fetch_permittivity_tensor

fetch_permittivity_tensor()

Get permittivity tensor for isotropic material.

Returns:

  • ndarray

    3×3 diagonal permittivity tensor

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor(self) -> np.ndarray:
    """Get permittivity tensor for isotropic material.

    Returns:
        3×3 diagonal permittivity tensor
    """
    return self.construct_tensor_singular()

fetch_permittivity_tensor_for_freq

fetch_permittivity_tensor_for_freq(requested_frequency)

Get frequency-independent permittivity tensor.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹ (ignored)

Returns:

  • ndarray

    3×3 diagonal permittivity tensor

Source code in hyperbolic_optics/materials.py
def fetch_permittivity_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Get frequency-independent permittivity tensor.

    Args:
        requested_frequency: Frequency in cm⁻¹ (ignored)

    Returns:
        3×3 diagonal permittivity tensor
    """
    return self.construct_tensor_singular()

fetch_magnetic_tensor

fetch_magnetic_tensor()

Get magnetic permeability tensor for isotropic material.

Returns:

  • ndarray

    3×3 diagonal permeability tensor

Source code in hyperbolic_optics/materials.py
def fetch_magnetic_tensor(self) -> np.ndarray:
    """Get magnetic permeability tensor for isotropic material.

    Returns:
        3×3 diagonal permeability tensor
    """
    return np.array(
        [
            [self.permeability, 0.0, 0.0],
            [0.0, self.permeability, 0.0],
            [0.0, 0.0, self.permeability],
        ],
        dtype=np.complex128,
    )

fetch_magnetic_tensor_for_freq

fetch_magnetic_tensor_for_freq(requested_frequency)

Get frequency-independent magnetic tensor.

Parameters:

  • requested_frequency (float) –

    Frequency in cm⁻¹ (ignored)

Returns:

  • ndarray

    3×3 diagonal permeability tensor

Source code in hyperbolic_optics/materials.py
def fetch_magnetic_tensor_for_freq(self, requested_frequency: float) -> np.ndarray:
    """Get frequency-independent magnetic tensor.

    Args:
        requested_frequency: Frequency in cm⁻¹ (ignored)

    Returns:
        3×3 diagonal permeability tensor
    """
    return self.fetch_magnetic_tensor()

Air

Air(permittivity=None, permeability=None)

Bases: IsotropicMaterial

Air material implementation.

Initialize Air material (vacuum approximation).

Parameters:

  • permittivity (float | complex | dict[str, float] | None, default: None ) –

    Relative permittivity (default: 1.0 from config)

  • permeability (float | complex | dict[str, float] | None, default: None ) –

    Relative permeability (default: 1.0)

Note

Air is treated as an isotropic, non-dispersive material with ε ≈ 1.0 and μ ≈ 1.0 across all frequencies.

Source code in hyperbolic_optics/materials.py
def __init__(
    self,
    permittivity: float | complex | dict[str, float] | None = None,
    permeability: float | complex | dict[str, float] | None = None,
) -> None:
    """Initialize Air material (vacuum approximation).

    Args:
        permittivity: Relative permittivity (default: 1.0 from config)
        permeability: Relative permeability (default: 1.0)

    Note:
        Air is treated as an isotropic, non-dispersive material with
        ε ≈ 1.0 and μ ≈ 1.0 across all frequencies.
    """
    if permittivity is None:
        params = load_material_parameters()["isotropic_materials"]["air"]
        permittivity = params["permittivity"]

    if permeability is None:
        permeability = 1.0

    super().__init__(permittivity=permittivity, permeability=permeability)
    self.name = "Air"

load_material_parameters

load_material_parameters()

Load material parameters from JSON configuration file.

Returns:

  • dict[str, Any]

    Dictionary containing all material parameters organized by material type

  • dict[str, Any]

    (uniaxial, monoclinic, arbitrary, isotropic)

Note

The configuration file is located at hyperbolic_optics/material_params.json

Source code in hyperbolic_optics/materials.py
def load_material_parameters() -> dict[str, Any]:
    """Load material parameters from JSON configuration file.

    Returns:
        Dictionary containing all material parameters organized by material type
        (uniaxial, monoclinic, arbitrary, isotropic)

    Note:
        The configuration file is located at hyperbolic_optics/material_params.json
    """
    config_path = Path(__file__).parent / "material_params.json"
    with open(config_path, "r") as f:
        return json.load(f)

create_material

create_material(material)

Instantiate a material from a name string or an arbitrary-tensor dict.

Parameters:

  • material (str | dict[str, Any]) –

    A registered material name (see :func:list_materials), or a dict of permittivity/permeability components for an :class:ArbitraryMaterial.

Raises:

  • NotImplementedError

    If the name is not recognised.

Source code in hyperbolic_optics/materials.py
def create_material(material: str | dict[str, Any]) -> BaseMaterial:
    """Instantiate a material from a name string or an arbitrary-tensor dict.

    Args:
        material: A registered material name (see :func:`list_materials`), or a
            dict of permittivity/permeability components for an
            :class:`ArbitraryMaterial`.

    Raises:
        NotImplementedError: If the name is not recognised.
    """
    if isinstance(material, dict):
        return ArbitraryMaterial(material)
    try:
        return _MATERIAL_REGISTRY[material]()
    except KeyError:
        raise NotImplementedError(f"Material {material} not implemented") from None

list_materials

list_materials()

Summarize the registered (named) materials.

Returns:

  • dict[str, dict[str, Any]]

    A dict keyed by the registry name a payload would use (e.g. "hBN"),

  • dict[str, dict[str, Any]]

    each value giving the implementing class name, the optical type

  • dict[str, dict[str, Any]]

    ("uniaxial", "biaxial", "monoclinic" or "isotropic"), and

  • dict[str, dict[str, Any]]

    the default frequency_range (min, max) in cm⁻¹ (None if the

  • dict[str, dict[str, Any]]

    material has no intrinsic range).

Example

for name, info in list_materials().items(): ... print(name, info["type"], info["frequency_range"])

Source code in hyperbolic_optics/materials.py
def list_materials() -> dict[str, dict[str, Any]]:
    """Summarize the registered (named) materials.

    Returns:
        A dict keyed by the registry name a payload would use (e.g. ``"hBN"``),
        each value giving the implementing ``class`` name, the optical ``type``
        (``"uniaxial"``, ``"biaxial"``, ``"monoclinic"`` or ``"isotropic"``), and
        the default ``frequency_range`` ``(min, max)`` in cm⁻¹ (``None`` if the
        material has no intrinsic range).

    Example:
        >>> for name, info in list_materials().items():
        ...     print(name, info["type"], info["frequency_range"])
    """
    summary: dict[str, dict[str, Any]] = {}
    for name, cls in _MATERIAL_REGISTRY.items():
        material = cls()
        if isinstance(material, BiaxialMaterial):  # subclass of UniaxialMaterial
            material_type = "biaxial"
        elif isinstance(material, MonoclinicMaterial):
            material_type = "monoclinic"
        elif isinstance(material, UniaxialMaterial):
            material_type = "uniaxial"
        else:
            material_type = "isotropic"
        frequency = material.frequency
        frequency_range = (
            (float(np.min(frequency)), float(np.max(frequency))) if frequency is not None else None
        )
        summary[name] = {
            "class": cls.__name__,
            "type": material_type,
            "frequency_range": frequency_range,
        }
    return summary