Skip to content

Materials API Reference

The src.material module provides a glass material database with dispersion models for computing wavelength-dependent refractive indices.


Material

src.material.Material

Material(name=None, device='cpu')

Bases: DeepObj

Optical material defined by its wavelength-dependent refractive index.

Materials are looked up by name in the bundled CDGM, SCHOTT, or MISC AGF catalogs, in a custom JSON catalog, or specified inline as "n/V" (Cauchy approximation from Abbe number V).

Supported dispersion models: "sellmeier", "cauchy", "schott", and "interp" (lookup table).

Attributes:

Name Type Description
name str

Lowercase material name.

dispersion str

Dispersion model used ("sellmeier", "cauchy", "schott", or "interp").

n float

Refractive index at the d-line (587 nm).

V float

Abbe number.

Initialize an optical material.

Parameters:

Name Type Description Default
name str or None

Material name (case-insensitive). Accepted forms:

  • Glass catalog name, e.g. "N-BK7", "H-K9L"
  • "air" (n = 1, non-dispersive). Legacy names "vacuum" and "occluder" are accepted and normalised to "air".
  • Inline Cauchy, e.g. "1.5168/64.17"
  • Custom name registered in materials_data.json

Defaults to None (treated as "air").

None
device str

Compute device. Defaults to "cpu".

'cpu'

Raises:

Type Description
NotImplementedError

If name is not found in any catalog.

Example

mat = Material("N-BK7") n_green = mat.get_ri(0.587) # refractive index at 587 nm

Source code in src/material/materials.py
def __init__(self, name=None, device="cpu"):
    """Initialize an optical material.

    Args:
        name (str or None, optional): Material name (case-insensitive).
            Accepted forms:

            * Glass catalog name, e.g. ``"N-BK7"``, ``"H-K9L"``
            * ``"air"`` (n = 1, non-dispersive). Legacy names
              ``"vacuum"`` and ``"occluder"`` are accepted and
              normalised to ``"air"``.
            * Inline Cauchy, e.g. ``"1.5168/64.17"``
            * Custom name registered in ``materials_data.json``

            Defaults to ``None`` (treated as ``"air"``).
        device (str, optional): Compute device. Defaults to ``"cpu"``.

    Raises:
        NotImplementedError: If *name* is not found in any catalog.

    Example:
        >>> mat = Material("N-BK7")
        >>> n_green = mat.get_ri(0.587)  # refractive index at 587 nm
    """
    raw = "air" if name is None else name.lower()
    # Normalise legacy aliases to "air"
    self.name = "air" if raw in ("vacuum", "occluder") else raw
    self.load_dispersion()
    self.device = device

load_dispersion

load_dispersion()

Load material dispersion equation.

Source code in src/material/materials.py
def load_dispersion(self):
    """Load material dispersion equation."""
    # Air (n=1, non-dispersive)
    if self.name == "air":
        self.dispersion = "sellmeier"
        self.k1, self.l1, self.k2, self.l2, self.k3, self.l3 = 0, 0, 0, 0, 0, 0
        self.n, self.V = 1.0, 1e38

    # Material found in AGF file
    elif self.name.lower() in MATERIAL_data:
        self.set_material_param_agf(MATERIAL_data, self.name.lower())

    # Material is given by a (n, V) string, e.g. "1.5168/64.17"
    elif "/" in self.name:
        self.dispersion = "cauchy"
        self.n = float(self.name.split("/")[0])
        self.V = float(self.name.split("/")[1])
        self.A, self.B = self.nV_to_AB(self.n, self.V)

    # Material found in custom JSON file
    elif self.name in CUSTOM_data["INTERP_TABLE"]:
        self.dispersion = "interp"
        mat_data = CUSTOM_data["INTERP_TABLE"][self.name]
        self.ref_wvlns = mat_data["wvlns"]
        self.ref_n = mat_data["n"]
        self._ref_wvlns_t = torch.tensor(self.ref_wvlns)
        self._ref_n_t = torch.tensor(self.ref_n)
        # Compute Abbe number V from interpolated nd, nF, nC
        import numpy as np
        nd = float(np.interp(0.5893, self.ref_wvlns, self.ref_n))
        nF = float(np.interp(0.4861, self.ref_wvlns, self.ref_n))
        nC = float(np.interp(0.6563, self.ref_wvlns, self.ref_n))
        self.n = nd
        self.V = (nd - 1) / (nF - nC) if nF != nC else 1e38

    elif self.name in CUSTOM_data["SELLMEIER_TABLE"]:
        self.dispersion = "sellmeier"
        self.k1, self.l1, self.k2, self.l2, self.k3, self.l3 = CUSTOM_data[
            "SELLMEIER_TABLE"
        ][self.name]
        try:
            self.n = CUSTOM_data["MATERIAL_TABLE"][self.name][0]
            self.V = CUSTOM_data["MATERIAL_TABLE"][self.name][1]
        except KeyError:
            print(f"Warning: {self.name} found in SELLMEIER_TABLE but not in MATERIAL_TABLE.")

    elif self.name in CUSTOM_data["SCHOTT_TABLE"]:
        self.dispersion = "schott"
        self.a0, self.a1, self.a2, self.a3, self.a4, self.a5 = CUSTOM_data[
            "SCHOTT_TABLE"
        ][self.name]
        try:
            self.n = CUSTOM_data["MATERIAL_TABLE"][self.name][0]
            self.V = CUSTOM_data["MATERIAL_TABLE"][self.name][1]
        except KeyError:
            print(f"Warning: {self.name} found in SCHOTT_TABLE but not in MATERIAL_TABLE.")

    elif self.name in CUSTOM_data["MATERIAL_TABLE"]:
        self.dispersion = "cauchy"
        self.n, self.V = CUSTOM_data["MATERIAL_TABLE"][self.name]
        self.A, self.B = self.nV_to_AB(self.n, self.V)

    else:
        raise NotImplementedError(f"Material {self.name} not implemented.")

set_material_param_agf

set_material_param_agf(material_data, material_name)

Set the material parameters and dispersion equation from AGF file.

Source code in src/material/materials.py
def set_material_param_agf(self, material_data, material_name):
    """Set the material parameters and dispersion equation from AGF file."""
    if material_name in material_data:
        material = material_data[material_name]

        if material["calculate_mode"] == 1:
            self.dispersion = "schott"
            self.a0 = material["a_coeff"]
            self.a1 = material["b_coeff"]
            self.a2 = material["c_coeff"]
            self.a3 = material["d_coeff"]
            self.a4 = material["e_coeff"]
            self.a5 = material["f_coeff"]
        elif material["calculate_mode"] == 2:
            self.dispersion = "sellmeier"
            self.k1 = material["a_coeff"]
            self.l1 = material["b_coeff"]
            self.k2 = material["c_coeff"]
            self.l2 = material["d_coeff"]
            self.k3 = material["e_coeff"]
            self.l3 = material["f_coeff"]
        else:
            raise NotImplementedError(
                f"Error: {material_name} calculate_mode {material['calculate_mode']}"
            )

        self.n = material["nd"]
        self.V = material["vd"]
    else:
        print(f"error: not {material_name}")

set_sellmeier_param

set_sellmeier_param(params=None)

Manually set sellmeier parameters k1, l1, k2, l2, k3, l3.

This function is used when we want to manually set the sellmeier parameters for a custom material.

Source code in src/material/materials.py
def set_sellmeier_param(self, params=None):
    """Manually set sellmeier parameters k1, l1, k2, l2, k3, l3.

    This function is used when we want to manually set the sellmeier parameters for a custom material.
    """
    if params is None:
        self.k1, self.l1, self.k2, self.l2, self.k3, self.l3 = (
            0.0,
            0.0,
            0.0,
            0.0,
            0.0,
            0.0,
        )
    else:
        self.k1, self.l1, self.k2, self.l2, self.k3, self.l3 = params

refractive_index

refractive_index(wvln)

Compute the refractive index at given wvln.

Source code in src/material/materials.py
def refractive_index(self, wvln):
    """Compute the refractive index at given wvln."""
    if isinstance(wvln, float):
        wvln = torch.tensor(wvln, device=self.device)
        return self.ior(wvln).item()

    return self.ior(wvln)

ior

ior(wvln)

Compute the refractive index at given wvln.

Source code in src/material/materials.py
def ior(self, wvln):
    """Compute the refractive index at given wvln."""
    assert wvln.min() > 0.1 and wvln.max() < 10, "Wavelength should be in [um]."

    if self.dispersion == "sellmeier":
        # Sellmeier equation: https://en.wikipedia.org/wiki/Sellmeier_equation
        n2 = (
            1
            + self.k1 * wvln**2 / (wvln**2 - self.l1)
            + self.k2 * wvln**2 / (wvln**2 - self.l2)
            + self.k3 * wvln**2 / (wvln**2 - self.l3)
        )
        n = torch.sqrt(n2)

    elif self.dispersion == "schott":
        # Schott equation: https://johnloomis.org/eop501/notes/matlab/sect1/schott.html
        ws = wvln**2
        n2 = (
            self.a0
            + self.a1 * ws
            + (self.a2 + (self.a3 + (self.a4 + self.a5 / ws) / ws) / ws) / ws
        )
        n = torch.sqrt(n2)

    elif self.dispersion == "cauchy":
        # Cauchy equation: https://en.wikipedia.org/wiki/Cauchy%27s_equation
        n = self.A + self.B / (wvln * 1e3) ** 2

    elif self.dispersion == "interp":
        # Use cached tensors, move to correct device if needed
        if self._ref_wvlns_t.device != wvln.device:
            self._ref_wvlns_t = self._ref_wvlns_t.to(wvln.device)
            self._ref_n_t = self._ref_n_t.to(wvln.device)
        ref_wvlns = self._ref_wvlns_t
        ref_n = self._ref_n_t

        # Find the lower and upper bracketing wavelengths
        i = torch.searchsorted(ref_wvlns, wvln, side="right")
        num_ref_wvlns = len(ref_wvlns)
        idx_low = torch.clamp(i - 1, 0, num_ref_wvlns - 1)
        idx_high = torch.clamp(i, 0, num_ref_wvlns - 1)

        wvln_ref_low = ref_wvlns[idx_low]
        wvln_ref_high = ref_wvlns[idx_high]
        n_ref_low = ref_n[idx_low]
        n_ref_high = ref_n[idx_high]

        # Interpolate n
        weight_high = (wvln - wvln_ref_low) / (wvln_ref_high - wvln_ref_low)
        weight_low = 1.0 - weight_high
        n = n_ref_low * weight_low + n_ref_high * weight_high

    elif self.dispersion == "optimizable":
        # Cauchy's equation, calculate (A, B) on the fly
        B = (self.n - 1) / self.V / (1 / 0.486**2 - 1 / 0.656**2)
        A = self.n - B * 1 / 0.587**2
        n = A + B / wvln**2

    else:
        raise NotImplementedError(f"Error: {self.dispersion} not implemented.")

    return n

nV_to_AB staticmethod

nV_to_AB(n, V)

Convert (n ,V) paramters to (A, B) parameters to find the material.

Source code in src/material/materials.py
@staticmethod
def nV_to_AB(n, V):
    """Convert (n ,V) paramters to (A, B) parameters to find the material."""

    def ivs(a):
        return 1.0 / a**2

    lambdas = [656.3, 587.6, 486.1]
    B = (n - 1) / V / (ivs(lambdas[2]) - ivs(lambdas[0]))
    A = n - B * ivs(lambdas[1])
    return A, B

match_material

match_material(mat_table=None)

Find the closest material in the CDGM common glasses database.

Source code in src/material/materials.py
def match_material(self, mat_table=None):
    """Find the closest material in the CDGM common glasses database."""
    if not self.name == "air":
        # Material match table
        if mat_table is None:
            print("No material table provided. Using CDGM common glasses as default.")
            mat_table = CUSTOM_data["CDGM_GLASS"]
        elif mat_table == "CDGM":
            # CDGM common glasses
            mat_table = CUSTOM_data["CDGM_GLASS"]
        elif mat_table == "PLASTIC":
            mat_table = CUSTOM_data["PLASTIC_TABLE"]
        else:
            raise NotImplementedError(f"Material table {mat_table} not implemented.")

        # Find the closest material
        n_range = 0.4 # refractive index range usually [1.5, 1.9]
        V_range = 40.0 # Abbe number range usually [30, 70]
        n_self = float(self.n) if torch.is_tensor(self.n) else self.n
        V_self = float(self.V) if torch.is_tensor(self.V) else self.V
        self.name = min(
            mat_table,
            key=lambda name: abs(mat_table[name][0] - n_self) / n_range + abs(mat_table[name][1] - V_self) / V_range,
        )

        # Load the new material parameters
        self.load_dispersion()

get_optimizer_params

get_optimizer_params(lrs=[0.0001, 0.01])

Optimize the material parameters (n, V).

Optimizing refractive index is more important than optimizing Abbe number.

Parameters:

Name Type Description Default
lrs list

learning rates for n and V. Defaults to [1e-4, 1e-4].

[0.0001, 0.01]
Source code in src/material/materials.py
def get_optimizer_params(self, lrs=[1e-4, 1e-2]):
    """Optimize the material parameters (n, V). 

    Optimizing refractive index is more important than optimizing Abbe number.

    Args:
        lrs (list): learning rates for n and V. Defaults to [1e-4, 1e-4].
    """
    if isinstance(self.n, float):
        self.n = torch.tensor(self.n, device=self.device)
        self.V = torch.tensor(self.V, device=self.device)

    self.n.requires_grad = True
    self.V.requires_grad = True
    self.dispersion = "optimizable"

    params = [
        {"params": [self.n], "lr": lrs[0]},
        {"params": [self.V], "lr": lrs[1]},
    ]
    return params
Material(name=None, device="cpu")
Parameter Type Description
name str Material name (see supported formats below)

Supported Name Formats

Format Example Description
Glass catalog "N-BK7", "H-K9L" Standard glass from CDGM, SCHOTT, MISC, or PLASTIC catalogs
Air "air" Non-dispersive, \(n = 1\)
Inline Cauchy "1.5168/64.17" Specify \(n/V\) directly
Custom "V122", "OM614" From materials_data.json

Key Attributes

Attribute Type Description
name str Material name (lowercase)
n float Refractive index at d-line (587 nm)
V float Abbe number
dispersion str Dispersion model type

Key Methods

ior(wvln) / get_ri(wvln)

Compute refractive index at a given wavelength (in micrometers).

mat = Material("N-BK7")
n_d = mat.ior(0.5876)   # d-line: n = 1.5168
n_F = mat.ior(0.4861)   # F-line: n = 1.5224
n_C = mat.ior(0.6563)   # C-line: n = 1.5143

get_optimizer_params()

Make refractive index \(n\) and Abbe number \(V\) differentiable for material optimization.

mat_params = mat.get_optimizer_params()
# Returns parameter group for torch optimizer

Dispersion Models

Model Description Source
sellmeier 6-coefficient Sellmeier equation SCHOTT, CDGM catalogs
schott Schott dispersion formula SCHOTT catalog
cauchy 2-parameter Cauchy approximation from \((n, V)\) Inline specification
interp Interpolation table materials_data.json
optimizable Differentiable \((n, V)\) for gradient-based material optimization Runtime

Sellmeier Equation

\[n^2(\lambda) - 1 = \frac{B_1 \lambda^2}{\lambda^2 - C_1} + \frac{B_2 \lambda^2}{\lambda^2 - C_2} + \frac{B_3 \lambda^2}{\lambda^2 - C_3}\]

Glass Catalogs

AutoLens ships with four AGF glass catalogs merged into MATERIAL_data (~700+ glasses):

Catalog File Description
CDGM CDGM.AGF China CDGM glasses
SCHOTT SCHOTT.AGF Schott AG glasses
MISC MISC.AGF Miscellaneous glasses
PLASTIC PLASTIC2022.AGF Optical plastics (COC, PMMA, PC, PS, etc.)

Custom materials (e.g., CODE V .ZTG catalogs) are stored in materials_data.json with Sellmeier, Schott, or interpolation table entries.

Common Glasses

The COMMON_GLASSES list in src/geolens_pkg/utils.py contains a curated selection used for random material initialization in create_lens():

COMMON_GLASSES = [
    "N-BK7", "N-SF6", "N-LAK9", "N-SK16",
    "H-K9L", "H-ZF7LA", "H-LAK50A",
    "coc", "pmma", "pc", "ps", "okp4",
    # ...
]

Material Optimization

During curriculum learning, materials can be optimized as continuous \((n, V)\) parameters, then snapped to the nearest real catalog glass via match_materials():

# Enable material optimization
lens.optimize(lrs=[1e-3, 1e-4], optim_mat=True, ...)

# Snap to nearest real glass
lens.match_materials()