Source code for materials.gradient_material

"""Gradient-index (GRIN) material.

This module defines :class:`GradientMaterial`, a material whose refractive index
varies with position inside the medium.

Jie Cai (zhazhajust), 2025
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import optiland.backend as be
from optiland.materials.base import BaseMaterial
from optiland.propagation.grin import GRINPropagation

if TYPE_CHECKING:
    from optiland._types import BEArray, ScalarOrArray

_COEFFICIENTS = ("n0", "nr2", "nr4", "nr6", "nz1", "nz2", "nz3")


[docs] class GradientMaterial(BaseMaterial): """Material with a radial and axial gradient index. The index follows the Gradient 3 law .. math:: n(r, z) = n_0 + n_{r2} r^2 + n_{r4} r^4 + n_{r6} r^6 + n_{z1} z + n_{z2} z^2 + n_{z3} z^3, with :math:`r^2 = x^2 + y^2`. Coordinates are in mm, in the local frame of the surface where the medium starts. The index does not depend on wavelength, and the medium does not absorb. Rays are traced through the medium by :class:`GRINPropagation`. Args: n0: Index at the medium origin. nr2: Coefficient of :math:`r^2`, in mm⁻². nr4: Coefficient of :math:`r^4`, in mm⁻⁴. nr6: Coefficient of :math:`r^6`, in mm⁻⁶. nz1: Coefficient of :math:`z`, in mm⁻¹. nz2: Coefficient of :math:`z^2`, in mm⁻². nz3: Coefficient of :math:`z^3`, in mm⁻³. propagation_model: The integrator settings. Defaults to ``GRINPropagation()``. """ def __init__( self, n0: ScalarOrArray = 1.5, nr2: ScalarOrArray = 0.0, nr4: ScalarOrArray = 0.0, nr6: ScalarOrArray = 0.0, nz1: ScalarOrArray = 0.0, nz2: ScalarOrArray = 0.0, nz3: ScalarOrArray = 0.0, propagation_model: GRINPropagation | None = None, ): super().__init__(propagation_model or GRINPropagation()) self.n0 = n0 self.nr2 = nr2 self.nr4 = nr4 self.nr6 = nr6 self.nz1 = nz1 self.nz2 = nz2 self.nz3 = nz3
[docs] def index_and_gradient( self, x: BEArray, y: BEArray, z: BEArray, wavelength: ScalarOrArray, ) -> tuple[BEArray, BEArray, BEArray, BEArray]: """Returns the index and its gradient at points in the medium. Args: x: x-coordinates in the medium frame, in mm. y: y-coordinates in the medium frame, in mm. z: z-coordinates in the medium frame, in mm. wavelength: Wavelength in µm. Not used by this law. Returns: The index ``n`` and the partial derivatives ``dn/dx``, ``dn/dy`` and ``dn/dz``. """ r2 = x**2 + y**2 n = ( self.n0 + r2 * (self.nr2 + r2 * (self.nr4 + r2 * self.nr6)) + z * (self.nz1 + z * (self.nz2 + z * self.nz3)) ) radial = 2 * self.nr2 + r2 * (4 * self.nr4 + 6 * self.nr6 * r2) dn_dz = self.nz1 + z * (2 * self.nz2 + 3 * self.nz3 * z) return n, x * radial, y * radial, dn_dz + be.zeros_like(x)
def _calculate_n(self, wavelength: ScalarOrArray, **kwargs) -> BEArray: """Returns the index at ``x``, ``y``, ``z``, or ``n0`` without a position. Args: wavelength: Wavelength in µm. Not used by this law. **kwargs: Optional ``x``, ``y`` and ``z`` in the medium frame. Returns: The refractive index. """ if not {"x", "y", "z"} & kwargs.keys(): return self._broadcast_wavelength(self.n0, wavelength) x, y, z = (be.asarray(kwargs.get(key, 0.0)) for key in ("x", "y", "z")) return self.index_and_gradient(x, y, z, wavelength)[0] def _calculate_k(self, wavelength: ScalarOrArray, **kwargs) -> BEArray: """Returns zero: the medium does not absorb. Args: wavelength: Wavelength in µm. **kwargs: Ignored. Returns: Zero, shaped like ``wavelength`` when it is an array. """ return self._broadcast_wavelength(0.0, wavelength) @staticmethod def _broadcast_wavelength( value: ScalarOrArray, wavelength: ScalarOrArray ) -> BEArray: """Returns ``value`` as a backend array, shaped like an array wavelength.""" value = be.array(value) if be.is_array_like(wavelength): return value + be.zeros_like(be.array(wavelength)) return value
[docs] def to_dict(self) -> dict: """Returns a dictionary representation of the material. Returns: The dictionary representation. """ data = super().to_dict() data.update( {key: float(be.to_numpy(getattr(self, key))) for key in _COEFFICIENTS} ) return data
[docs] @classmethod def from_dict(cls, data: dict) -> GradientMaterial: """Creates a material from a dictionary representation. Args: data: The dictionary representation. Returns: The material. """ return cls(**{key: data[key] for key in _COEFFICIENTS if key in data})