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
|
|
dispersion |
str
|
Dispersion model used ("sellmeier", "cauchy",
"schott", or "interp").
|
n |
float
|
Refractive index at the d-line (587 nm).
|
V |
float
|
|
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 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
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
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
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) |
| 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()