Inverse design without normalization shortcuts#
This tutorial follows the physical problem in Tidy3D
Autograd24DigitalSplitter.ipynb: a 220 nm silicon-on-oxide splitter whose
design variables are the permittivities of 200 shallow circular holes.
Validation state
The old 24×24 continuous-topology example was a different device and its 0.716 FOM is withdrawn. The benchmark now reproduces the published hole array, uniform start, y symmetry, five wavelengths, update rule, and FOM. See the inverse-design audit.
1. Define the published problem#
WAVELENGTHS_UM = np.linspace(1.53, 1.57, 5)
DESIGN_LENGTH_UM = 2.6
PIXEL_UM = 0.13
PARAMETER_SHAPE = (20, 10) # y half, mirrored by the solver
HOLE_RADIUS_UM = 0.045
HOLE_DEPTH_UM = 0.14
SILICON_HEIGHT_UM = 0.22
EPS_AIR, EPS_SILICON = 1.0, 12.0
parameters = np.full(PARAMETER_SHAPE, 6.5, dtype=np.float32)
That uniform ε=6.5 array is the notebook’s start. It is not an optimized warm start. Each value controls one 90 nm-diameter, 140 nm-deep cylinder on a 130 nm pitch.
2. Continue every guide through PML#
The input and both outputs run from the design boundary to the non-PML domain
edge. After parameterized material is applied,
fdtdx.extend_material_to_pml() copies the connected guide material through
the absorber. This avoids the artificial termination and back-scattering that
occurred in the earlier examples.
arrays, objects, _ = fdtdx.apply_params(arrays, objects, parameters, key)
arrays = fdtdx.extend_material_to_pml(objects=objects, arrays=arrays)
The shared device builder has a regression test requiring every guided port continuation to overlap its port and reach the domain edge.
3. Respect the symmetry normalization#
The simulation uses electric y symmetry, (0, -1, 0). The source plane
straddles that symmetry plane, while the measured upper output is wholly in
the retained half. FDTDX gives the reduced source plane unit flux; its unfolded
physical source has twice that power. Therefore
The lower port is the symmetry image, so total desired transmission is \(2J\). The benchmark records that total spectrum and rejects any value above 1.05. It also records source-power drift and requires it to stay below 5%.
4. Use the published optimizer, then continue automatically if needed#
The notebook performs two stages of 25 gradient-ascent updates. The first uses
unit-maximum gradient steps; the second adds a 5% bias away from ε=6.5 before
clipping to [1, 12].
for iteration in range(50):
value, gradient = jax.value_and_grad(fom)(parameters)
parameters += gradient / jnp.maximum(jnp.max(gradient), 1e-12)
if iteration >= 25:
parameters = 1.05 * (parameters - 6.5) + 6.5
parameters = jnp.clip(parameters, 1.0, 12.0)
No Tidy3D final pixels are loaded and no alternate FOM is substituted. Because FDTDX may converge more slowly, the identical update can continue from the same cold run up to the catalog-wide 3× iteration cap. Every 25 updates, the exact published ternary/radius fabrication transform receives a fresh forward solve; the best fully binary checkpoint is retained. This uses no reference geometry and does not change the objective.
5. Mesh and memory#
The selected 45 nm xy / 50 nm z mesh is still coarser than the notebook’s roughly 22.4 nm in-silicon target. It is chosen from the 90 nm smallest feature, aligns the substrate top, etch bottom, and slab top to exact cell faces, and retains 540 nm of physical PML as the grid changes:
Model |
Initial physical upper-port FOM |
|---|---|
50 nm, automatic planar-interface alignment |
0.1324 |
45 nm, 8×8 fractional hole coverage |
0.12245 |
pinned Tidy3D notebook |
0.1113 |
The raw reverse tape would use 8.70 GiB before adjoint workspace. The automatic planner selects float16 PML history sampled every fourth step with linear reconstruction, reducing the tape to 1.09 GiB inside a 1.22 GiB budget and holding the full adjoint near 9.2 GB. Fully binary checkpoints at 50/75/100/125/150 updates are 0.42142/0.42690/0.42899/0.43042/0.43253. The selected design reports 91.62% of Tidy3D’s published 0.47210 locally, but the independent Tidy3D forward run scores that same binary geometry at 0.31384 (66.5%). It therefore fails both the requested 90% gate and the repository’s 95% threshold. Automatic local refinement remains the next step toward the notebook’s much finer material mesh.
6. Run and inspect#
uv run fdtdx-bench smoke
uv run fdtdx-bench run --case invdes_power_splitter
The benchmark artifact contains all 200 best hole permittivities. The result
contains the complete FOM history, per-wavelength total transmission, source
drift, gradient norms, and binarization error. A documentation field solve uses
a five-frequency PhasorDetector, unfolds the y symmetry, and plots one
\(|E|^2\) map at every design wavelength—not a terminal-time field snapshot.
Open the executed notebook to inspect every code cell and retained plot. It shows the actual realized hole radii, binary Yee-grid material, five fresh phasor-field maps, spectral power, insertion loss, and all scalar gates.
7. Broader inverse-design status#
WDM and bandpass reproduce their published uniform starts, frequency sampling, β=50 continuation, conic filters, and erosion/dilation penalties, but their independent Tidy3D final-artifact checks fail. The 451×451 S-matrix crossing with exact D4 symmetry and β=30 is within 10% of published performance, though it remains below the 95% external gate. Bend and Autograd25 crossing remain explicit exclusions because their published differentiable shape controls and full tapes are not yet supported within memory. The audit matrix records the exact evidence.
8. Reuse the optimizer for a new device#
Density-based problems do not need to reimplement continuation, bounds, best-candidate tracking, callbacks, or early stopping. The device-specific function supplies only a differentiable physical objective and its audit data:
config = fdtdx.TopologyOptimizerConfig(
iterations=75,
continuation_steps=50,
learning_rate=0.08,
beta_start=1.0,
beta_end=50.0,
lower_bound=0.0,
upper_bound=1.0,
early_stop_patience=15,
)
def objective(raw_density, beta, fraction):
material = fdtdx.filter_and_project(
raw_density,
radius=feature_radius,
spacing=design_spacing,
beta=beta,
)
arrays = apply_design_material(base_arrays, material)
result = run_differentiable_fdtd(arrays)
fom, total_power = normalized_port_objective(result)
penalty = fdtdx.erosion_dilation_penalty(
raw_density,
radius=feature_radius,
spacing=design_spacing,
beta=beta,
)
value = fom - fraction * fabrication_weight * penalty
return value, {"fom": fom, "total_power": total_power}
optimized = fdtdx.optimize_topology(
objective,
initial_parameters=np.full(design_shape, 0.5),
config=config,
accept=lambda aux: float(np.max(aux["total_power"])) <= 1.05,
)
binary = fdtdx.binary_density(
optimized.best_parameters,
radius=feature_radius,
spacing=design_spacing,
)
The final line is a fabrication candidate, not proof. Run it again in a fresh forward simulation, plot its material and fields, and report the held-out FOM, passivity, source normalization, and minimum-feature checks. The executed WDM, bandpass, and S-matrix notebooks show that complete workflow.