Architecture
DiffTMM is a differentiable thin-film library built on PyTorch. The whole calculation — Snell's law, Fresnel coefficients, layer propagation, the transfer matrix product — is expressed in differentiable tensor ops, so gradients flow from the output Fresnel coefficients back to the layer thicknesses (and any other tensor input). That is what makes gradient-based inverse design possible.
The code is organized into three parts: a functional core of pure TMM functions, a thin layer of solver classes that hold the stack geometry and the optimizable thicknesses, and a materials subsystem that supplies wavelength-dependent refractive indices.
Package layout
difftmm/
├── __init__.py Public API (re-exports the solvers + materials)
│
├── film_solver_isotropic.py IsotropicFilmSolver (2×2)
│ + create_jones_matrix_isotropic() ← functional core
├── film_solver_anisotropic.py FilmSolver / AnisotropicFilmSolver (4×4)
│ + create_jones_matrix_AOIAz() ← functional core
├── film_solver_incoherent.py IncoherentIsotropicFilmSolver (2×2 + incoherent layers)
│ + create_intensity_RT_isotropic() ← functional core
│
└── material/
├── materials.py Material + list_materials() + AGF/JSON catalog loaders
└── catalogs/ CDGM / SCHOTT / MISC / PLASTIC2022 (.AGF) + thin_film_materials.json
tmm_numpy/ Reference NumPy TMM (sbyrnes321/tmm) — accuracy validation
benchmarks/ Accuracy + speed/memory scripts
tests/ Pytest suite
*.ipynb Example notebooks
Two layers: solvers and the functional core
Each solver is a thin, stateful wrapper around a pure function that does the
actual transfer-matrix math. The solver holds the stack description and the
optimizable thicknesses and offers a convenient simulate(theta, wvln) call; the
function is stateless and takes every quantity explicitly.
| Solver (stateful) | Functional core (pure) | Returns |
|---|---|---|
IsotropicFilmSolver |
create_jones_matrix_isotropic() |
complex ts, tp, rs, rp |
FilmSolver |
create_jones_matrix_AOIAz() |
complex ts, tp, rs, rp |
IncoherentIsotropicFilmSolver |
create_intensity_RT_isotropic() |
real Rs, Rp, Ts, Tp |
Use the solver for ordinary forward simulation and for optimizing the
thicknesses it owns. Reach for the function when the thicknesses live outside
the solver — e.g. as a torch.nn.Parameter or the output of a network — which is
the natural fit for inverse design. Both paths share
the same math and are equally differentiable.
Solver class hierarchy
IsotropicFilmSolver 2×2 TMM, isotropic, complex amplitudes
└── IncoherentIsotropicFilmSolver adds per-layer coherence (c_list); returns real power
FilmSolver ( = AnisotropicFilmSolver) 4×4 TMM, anisotropic/general, complex amplitudes
IncoherentIsotropicFilmSolver subclasses IsotropicFilmSolver: it reuses
the parent's constructor, thickness handling, and checkpoint I/O, adds a c_list
that marks each interior layer coherent ('c') or incoherent ('i'), and
overrides simulate() to return real power coefficients (phase is discarded by
design). FilmSolver is an independent 4×4 implementation; AnisotropicFilmSolver
is simply an alias for it.
Shared solver interface
All solvers construct from the same stack description and expose the same methods:
Solver(
mat_in, mat_out, mat_ls, # incident medium, exit medium, interior layers
thickness_ls=None, # layer thicknesses in um (random if None)
thickness_min=0.0, # thickness bounds in um
thickness_max=0.2,
batch_size=1, # number of film stacks evaluated in parallel
sigmoid_param=False, # thickness parameterization (see below)
device=...,
)
| Method | Purpose |
|---|---|
simulate(theta, wvln) / __call__ |
Evaluate the stack at angles (rad) and wavelengths (µm) |
get_film_thickness() |
Current physical thicknesses (batch, n_layers) in µm |
to(device) |
Move the solver (and its materials) to a device |
save_ckpt(path) / load_ckpt(path) |
Persist / restore thicknesses + material specs |
Differentiable thicknesses
The optimizable state of a solver is not the thickness directly but an internal
film_params tensor. get_film_thickness() maps it to a physical thickness in
micrometers, clamped to [thickness_min, thickness_max]:
- Linear (default) —
film_paramsis the normalized thickness, clamped to the valid range. - Sigmoid (
sigmoid_param=True) — thickness issigmoid(film_params)rescaled into the range, so the parameter is unconstrained and the thickness can never leave its physical bounds during optimization.
Because get_film_thickness() is part of the autograd graph, attaching an
optimizer to film_params and stepping on a loss computed from simulate() is a
complete inverse-design loop. (The default thickness_max is 0.2 µm for thin
coatings; the incoherent solver raises it to 1000 µm so substrate-sized layers
fit without extra configuration.)
Materials
A solver never sees raw numbers internally — every entry of mat_in, mat_out,
and mat_ls is wrapped in a Material, which resolves a
wavelength-dependent complex refractive index. Material accepts:
| Input | Dispersion model |
|---|---|
float / complex (e.g. 1.5, 2.4 + 0.001j) |
constant |
Sellmeier glass name (e.g. "N-BK7", "air") |
sellmeier (from bundled AGF catalogs) |
Thin-film name (e.g. "SiO2", "TiO2", "Ag") |
interp (complex n+k lookup table) |
Material.ior(wvln) returns a torch.complex64 index — always complex, even for
lossless materials, because beyond the critical angle the layer's cos θ becomes
imaginary (evanescent waves). For the 4×4 FilmSolver, a
birefringent layer is given as a (mat_x, mat_y, mat_z) tuple and each axis gets
its own Material.
Physics and conventions
- 2×2 vs 4×4 — the isotropic solver uses the standard 2×2 transfer matrix and avoids eigen-decomposition entirely (fast). The anisotropic solver uses the general 4×4 formulation needed for birefringent media and full polarization (Jones) coupling.
- Bidirectional propagation — angles in
[0, π/2]propagate forward (incident → exit medium); angles in[π/2, π]propagate in reverse. The reverse case is internally converted to an equivalent forward problem with swapped media and reversed layer order. - Coherent vs incoherent — coherent solvers track complex amplitudes and
return
ts, tp, rs, rp. The incoherent solver groups layers by coherence, propagates intensities across incoherent boundaries, and returns real powerRs, Rp, Ts, Tp(the two semi-infinite media are always incoherent). - Output convention — square the magnitude of a coherent amplitude for power
(
T = |t|²,R = |r|²); the incoherent solver already returns power in[0, 1].
Validation
DiffTMM bundles the reference NumPy TMM library
(sbyrnes321/tmm) under tmm_numpy/ and
checks its solvers against it — surface plasmon resonance, the anisotropic
isotropic-limit, energy conservation, cross-polarization, reciprocity, and the
incoherent thick-substrate case. The scripts and results live on the
Benchmarks page.
Next steps
- API Reference — full class and function documentation
- Examples — forward simulation, inverse design, real materials, and benchmarks
- Setup — install DiffTMM and run your first simulation