Skip to content

Inverse design

inverse

Inverse design: density-based topology optimisation on any differentiable backend.

Quick start::

import lightsmith as ls
from lightsmith import inverse

spec = inverse.OptimizationProblem(device=device_problem, design_regions=[...],
                                   objective=[inverse.SParameterTerm(out_port="o2", in_port="o1")])
res = inverse.optimize(spec, device="cpu", checkpoint="run.npz")
res.final_device        # DeviceProblem with the exported polygons
inverse.write_gds("design.gds", inverse.marching_squares(...))  # via lightsmith.layout

The differentiable-backend protocol

Inverse design works with any registered backend that sets differentiable = True and provides these classmethods (the FDFD backend, lightsmith-fdfd, is the reference implementation)::

design_grid(problem) -> grid
    The grid on which permittivities are given; must expose node coordinates
    ``grid.x``, ``grid.y`` (nm, 1D arrays), the cell size ``grid.dl`` (nm) and
    ``grid.shape``. (3D backends add ``grid.z``.)

base_permittivity(problem) -> np.ndarray, shape (n_wl, *grid.shape), complex
    The rasterised permittivity of the problem's own structures.

differentiable_solve(problem, eps_grid_override, device, sources=None,
                     with_fields=True, **context) -> DifferentiableSolution
    eps_grid_override: torch tensor (n_wl, *grid.shape) or (*grid.shape), which
    may require grad. Returns torch tensors connected to the autograd graph:
    ``.s`` (n_wl, n_out, n_src) complex S[out, src], ``.outputs`` / ``.inputs``
    (port-mode labels, 'name' or 'name@mode'), ``.fields`` (n_wl, n_src, *grid.shape)
    or None, ``.grid``. Gradients must be exact (adjoint), and ports must not
    depend on the override inside design regions.

prepare(problem) -> dict            (optional)
    Cached context (e.g. a factorisation-ready engine) passed back to
    ``differentiable_solve`` as ``**context`` on every iteration.

Conventions: lengths in nm, vacuum wavelengths, exp(-i omega t) (Im eps > 0 = loss), power-normalised S-parameters.

ObjectiveTerm module-attribute

ObjectiveTerm = Annotated[Union[SParameterTerm, FieldIntensityTerm], Field(discriminator='kind')]

DesignRegion

Bases: _Model

A box whose permittivity is free: density 0 -> material_low, 1 -> material_high.

name class-attribute instance-attribute

name: str = 'design'

center_nm class-attribute instance-attribute

center_nm: tuple[float, float, float] = (0.0, 0.0, 0.0)

size_nm class-attribute instance-attribute

size_nm: tuple[float, float, float] = Field(description='Box size; z is ignored in 2D.')

material_low class-attribute instance-attribute

material_low: Material = Field(description='Material at density 0 (usually the cladding).')

material_high class-attribute instance-attribute

material_high: Material = Field(description='Material at density 1 (usually the core).')

init class-attribute instance-attribute

init: float | Literal['random'] = Field(0.5, description="Initial density, or 'random' (seeded).")

seed class-attribute instance-attribute

seed: int = 0

filter_radius_nm class-attribute instance-attribute

filter_radius_nm: float = Field(0.0, ge=0, description='Conic filter radius; 0 = no filter.')

eta class-attribute instance-attribute

eta: float = Field(0.5, gt=0, lt=1, description='Projection threshold.')

min_feature_nm class-attribute instance-attribute

min_feature_nm: float | None = Field(None, gt=0, description='Minimum solid and void feature size, imposed with lengthscale penalties (needs filter_radius_nm >= min_feature_nm / 2).')

symmetry class-attribute instance-attribute

symmetry: list[Literal['x', 'y']] = Field(default_factory=list, description="Mirror symmetry: 'x' = symmetric under x -> -x about the region centre, 'y' likewise.")

FieldIntensityTerm

Bases: _Term

Mean |field|^2 (Hz for TE, Ez for TM) in a box, with one port mode as the source.

kind class-attribute instance-attribute

kind: Literal['field_intensity'] = 'field_intensity'

source_port instance-attribute

source_port: str

source_mode class-attribute instance-attribute

source_mode: int = Field(0, ge=0)

center_nm instance-attribute

center_nm: tuple[float, float, float]

size_nm class-attribute instance-attribute

size_nm: tuple[float, float, float] = Field(description='Box size; zero sizes select a line or point.')

OptimizationProblem

Bases: _Model

Topology optimisation of a device: maximise sum(weight * term) (minimise terms with goal='minimize').

kind class-attribute instance-attribute

kind: Literal['optimization'] = 'optimization'

device instance-attribute

device: DeviceProblem

design_regions class-attribute instance-attribute

design_regions: list[DesignRegion] = Field(min_length=1)

objective class-attribute instance-attribute

objective: list[ObjectiveTerm] = Field(min_length=1)

optimizer class-attribute instance-attribute

backend class-attribute instance-attribute

backend: str = Field('lightsmith-fdfd', description='Any backend with differentiable=True and differentiable_solve (see lightsmith.inverse).')

options class-attribute instance-attribute

options: dict[str, dict[str, Any]] = Field(default_factory=dict)

to_json

to_json(**kw) -> str

from_json classmethod

from_json(s: str) -> 'OptimizationProblem'

OptimizerSettings

Bases: _Model

method class-attribute instance-attribute

method: Literal['mma', 'adam', 'lbfgsb'] = Field('mma', description='mma = globally convergent MMA (monotone; Svanberg 2002); adam; lbfgsb = SciPy L-BFGS-B.')

max_iter class-attribute instance-attribute

max_iter: int = Field(100, ge=1, description='Total objective/gradient evaluations budget (outer iterations).')

learning_rate class-attribute instance-attribute

learning_rate: float = Field(0.02, gt=0, description='Adam step size (densities are in [0, 1]).')

move_limit class-attribute instance-attribute

move_limit: float = Field(0.2, gt=0, le=1, description='MMA max change per iteration.')

beta_start class-attribute instance-attribute

beta_start: float = Field(4.0, ge=0, description='Initial projection sharpness.')

beta_max class-attribute instance-attribute

beta_max: float = Field(64.0, ge=0)

beta_growth class-attribute instance-attribute

beta_growth: float = Field(2.0, ge=1, description='beta *= beta_growth every beta_every iterations (continuation).')

beta_every class-attribute instance-attribute

beta_every: int = Field(20, ge=1)

lengthscale_weight class-attribute instance-attribute

lengthscale_weight: float = Field(100.0, ge=0, description='Penalty weight of the min-feature constraints.')

lengthscale_from_beta class-attribute instance-attribute

lengthscale_from_beta: float = Field(16.0, ge=0, description='Apply min-feature penalties once beta >= this.')

ftol class-attribute instance-attribute

ftol: float = Field(0.0, ge=0, description='Stop when the relative improvement over 10 iterations is below this.')

SParameterTerm

Bases: _Term

|S[out <- in]|^2 (power) of port modes, e.g. transmission o1 -> o2 or reflection o1 -> o1.

kind class-attribute instance-attribute

kind: Literal['sparameter'] = 'sparameter'

out_port instance-attribute

out_port: str

in_port instance-attribute

in_port: str

out_mode class-attribute instance-attribute

out_mode: int = Field(0, ge=0)

in_mode class-attribute instance-attribute

in_mode: int = Field(0, ge=0)

quantity class-attribute instance-attribute

quantity: Literal['power', 'real', 'imag'] = 'power'

DifferentiableBackend

Bases: Protocol

Structural type of a backend usable by :func:optimize (see the module docstring).

name class-attribute

name: str

differentiable class-attribute

differentiable: bool

design_grid classmethod

design_grid(problem) -> Any

base_permittivity classmethod

base_permittivity(problem) -> Any

differentiable_solve classmethod

differentiable_solve(problem, eps_grid_override=None, device='cpu', sources=None, with_fields: bool = True, **context) -> Any