Source code for thin_film.tolerancing.perturbation

"""Thin film perturbation classes.

Applies thickness or refractive-index perturbations to individual layers of a
ThinFilmStack.  Reuses the sampler hierarchy from ``optiland.tolerancing``.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal

from optiland.materials import IdealMaterial

if TYPE_CHECKING:
    from optiland.thin_film import ThinFilmStack
    from optiland.tolerancing.perturbation import BaseSampler


[docs] class ThinFilmPerturbation: """Perturbation applied to a single layer of a thin-film stack. Args: stack: The thin film stack containing the layer to perturb. layer_index: Index of the layer to perturb. perturbation_type: ``"thickness"`` or ``"index"``. sampler: A sampler instance (``RangeSampler``, ``DistributionSampler``, etc.) that generates perturbation values. is_relative: If True (default), perturbation is multiplicative (nominal * (1 + delta)). If False, the sampled value replaces the nominal directly. """ def __init__( self, stack: ThinFilmStack, layer_index: int, perturbation_type: Literal["thickness", "index"], sampler: BaseSampler, is_relative: bool = True, ): self.stack = stack self.layer_index = layer_index self.perturbation_type = perturbation_type self.sampler = sampler self.is_relative = is_relative self.value: float | None = None layer = self.stack.layers[self.layer_index] if perturbation_type == "thickness": self._nominal = layer.thickness_um elif perturbation_type == "index": if not isinstance(layer.material, IdealMaterial): raise TypeError( "Index perturbation is only supported for IdealMaterial. " f"Got {type(layer.material).__name__}." ) self._nominal = float(layer.material.n(0.55)) else: raise ValueError( f"perturbation_type must be 'thickness' or 'index', " f"got '{perturbation_type}'." ) def __str__(self) -> str: return f"Layer {self.layer_index} {self.perturbation_type}"
[docs] def apply(self) -> None: """Sample a perturbation value and apply it to the layer.""" delta = self.sampler.sample() self.value = float(delta) layer = self.stack.layers[self.layer_index] if self.perturbation_type == "thickness": if self.is_relative: layer.thickness_um = self._nominal * (1.0 + delta) else: layer.thickness_um = delta elif self.perturbation_type == "index": new_n = self._nominal * (1.0 + delta) if self.is_relative else delta layer.material = IdealMaterial(n=new_n)
[docs] def reset(self) -> None: """Restore the layer to its nominal state.""" layer = self.stack.layers[self.layer_index] if self.perturbation_type == "thickness": layer.thickness_um = self._nominal elif self.perturbation_type == "index": layer.material = IdealMaterial(n=self._nominal) self.value = None