# 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.

```{admonition} Validation state
:class: important

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](../project/inverse_design_audit.md).
```

## 1. Define the published problem

```python
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.

```python
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

$$
J=\operatorname{mean}_\lambda\left[
\frac{1}{2}\frac{|a_\mathrm{upper}(\lambda)|^2}
{|a_\mathrm{source}(\lambda)|^2}\right].
$$

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]`.

```python
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

```bash
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](../examples/notebooks/digital_splitter.ipynb) 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](../project/inverse_design_audit.md)
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:

```python
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.
