Inverse design of a high-Q nanobeam cavity#

This executable tutorial starts from the validated nanobeam cavity, differentiates a fitted ringdown \(Q\) through a GPU-native FDTD solve, and validates the optimized design with a fresh long simulation of literal air cylinders. It follows the same practical structure as a Tidy3D inverse-design notebook: parameters, geometry, plotted material, differentiable simulation, optimization, final fields, and quantitative analysis.

The acceptance test is deliberately harder than “the objective increased.” The final binary geometry must exceed twice the unchanged eight-hole cavity and must also beat an unoptimized, automatically mirror-completed control. The saved run uses one exact-checkpointed adjoint update from a zero-vector cold start—no optimized warm start, imported pixels, or Tidy3D solve is involved.

Imports and accelerator#

import gc
import json
import os
os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")

from pathlib import Path
from time import perf_counter
import warnings
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle
import numpy as np
import pandas as pd
from scipy.signal import hilbert
import fdtdx
from IPython import get_ipython

from benchmarks.cases.device_nanobeam_cavity.run import (
    NanobeamParameters,
    _dominant_resonance,
    build_nanobeam_simulation,
    run_nanobeam_simulation,
)
from benchmarks.cases.invdes_nanobeam_cavity.run import (
    ARTIFACT,
    BASELINE_Q,
    GUARANTEED_MIN_GAP_NM,
    MIRROR_COMPLETED_CONTROL_Q,
    NUM_DESIGN_HOLES,
    SAMPLES_PER_WINDOW,
    TARGET_Q,
    fabrication_metrics,
    optimize_nanobeam,
    physical_hole_geometry,
)
from benchmarks.device_geometry import nanobeam_holes_um

get_ipython().run_line_magic("matplotlib", "inline")
warnings.filterwarnings("ignore", category=FutureWarning, module=r"tidy3d\.components\.mode")
plt.rcParams.update({"figure.figsize": (10, 4.8), "figure.dpi": 120})
tidy_validation_path = ARTIFACT.parents[1] / "goldens" / "tidy3d_validation_invdes_nanobeam_cavity.npz"
print(f"JAX devices: {jax.devices()}")
print(f"Optimization artifact: {ARTIFACT.relative_to(ARTIFACT.parents[2])}")
print(f"Independent Tidy3D validation: {tidy_validation_path}")
JAX devices: [CudaDevice(id=0)]
Optimization artifact: benchmarks/artifacts/invdes_nanobeam_cavity_best.npz
Independent Tidy3D validation: /home/hoodlab/github/fdtdx-hoodlab/benchmarks/goldens/tidy3d_validation_invdes_nanobeam_cavity.npz

1. Physical, numerical, and optimization parameters#

The forward benchmark uses a 25 nm Yee grid, 12 PML cells, x/y parity, a broadband \(E_y\) dipole, and a 3 ps validation ringdown. The differentiable solve uses only 0.8 ps: the source transient is discarded and three coherent 2,000-sample windows estimate one complex pole. Exact checkpointing trades compute for memory so the reverse pass fits on a 12 GB RTX 3080.

parameters = NanobeamParameters(
    grid_spacing_um=0.025,
    run_time_ps=3.0,
    analysis_start_ps=0.2,
    wavelength_window_um=(1.4, 1.6),
    beam_width_um=0.500,
    beam_thickness_um=0.220,
    pml_layers=12,
)
pd.Series({
    **parameters._asdict(),
    "positive-side shape controls": 2 * NUM_DESIGN_HOLES,
    "short-fit windows": 3,
    "samples per fit window": SAMPLES_PER_WINDOW,
    "unchanged baseline Q": BASELINE_Q,
    "required Q (2× baseline)": TARGET_Q,
    "mirror-completed control Q": MIRROR_COMPLETED_CONTROL_Q,
    "guaranteed minimum bridge (nm)": GUARANTEED_MIN_GAP_NM,
}, name="value").to_frame()
value
grid_spacing_um 0.025
interior_um (8.4, 3.7, 3.4)
run_time_ps 3.0
analysis_start_ps 0.2
wavelength_window_um (1.4, 1.6)
pml_layers 12
beam_width_um 0.5
beam_thickness_um 0.22
silicon_index 3.5
oxide_index 1.44
positive-side shape controls 20
short-fit windows 3
samples per fit window 2000
unchanged baseline Q 35125.300715
required Q (2× baseline) 70250.60143
mirror-completed control Q 92493.301338
guaranteed minimum bridge (nm) 65.0

2. Initial geometry and automatic mirror completion#

The original compact example has eight positive-side holes. High-Q optimization is otherwise capped by leakage through that short mirror, so extend_periodic_mirror_to_boundary appends complete nominal periods while the hole edge remains inside the non-PML domain. The silicon beam already crosses the PML. The helper adds two holes per side here; it does not tune the cavity taper.

original_centers_um, original_radii_um = nanobeam_holes_um()
zero_controls = np.zeros(2 * NUM_DESIGN_HOLES, dtype=np.float32)
control_centers_um, control_radii_um = physical_hole_geometry(zero_controls)

def draw_geometry(ax, centers, radii, title):
    domain_half = parameters.interior_um[0] / 2 + parameters.pml_layers * parameters.grid_spacing_um
    interior_half = parameters.interior_um[0] / 2
    ax.axvspan(-domain_half, -interior_half, color="#d9a21b", alpha=0.18, label="PML")
    ax.axvspan(interior_half, domain_half, color="#d9a21b", alpha=0.18)
    ax.add_patch(Rectangle(
        (-domain_half, -parameters.beam_width_um / 2),
        2 * domain_half, parameters.beam_width_um,
        facecolor="#55c2b5", edgecolor="#087d83", label="silicon",
    ))
    for center, radius in zip(centers, radii, strict=True):
        ax.add_patch(Circle((center, 0), radius, facecolor="white", edgecolor="#18364a", lw=0.7))
    ax.axvline(-interior_half, color="#d29b00", ls="--", lw=0.9)
    ax.axvline(interior_half, color="#d29b00", ls="--", lw=0.9)
    ax.set(xlim=(-domain_half, domain_half), ylim=(-0.42, 0.42), aspect="equal", title=title,
           xlabel="x (µm)", ylabel="y (µm)")

fig, axes = plt.subplots(2, 1, figsize=(12, 5.5), sharex=True)
draw_geometry(axes[0], original_centers_um, original_radii_um, "Unchanged compact cavity")
draw_geometry(axes[1], control_centers_um, control_radii_um, "Automatic mirror-completed cold start")
axes[0].legend(loc="upper right", ncol=2)
plt.tight_layout()
plt.show()

print(f"Full holes: {len(original_centers_um)}{len(control_centers_um)}")
print(f"Last hole edge: {control_centers_um[-1] + control_radii_um[-1]:.3f} µm; non-PML edge: {parameters.interior_um[0]/2:.3f} µm")
../../_images/a1fef05de6362683ac31e35dd605297f8e67c181257478d283bb0f393670dfed.png
Full holes: 16 → 20
Last hole edge: 3.955 µm; non-PML edge: 4.200 µm

3. Differentiable fitted-Q objective#

Each short ringdown window is coherently demodulated near the tracked resonance. A complex ratio fit gives \(z=\exp[(-\gamma+i\Delta\omega)T]\), then \(Q=\pi f/\gamma\). Because this estimator is written entirely in JAX, its scalar Q can be reverse-differentiated through the complete time-domain solve.

The physical forward raster is not a grey sigmoid. It is the same binary 8×8 subcell fill fraction and diagonal Farjadpour interface tensor used by literal cylinders. A sigmoid boundary is used only for its straight-through shape derivative. Residual, window-consistency, amplitude, positive-decay, and wavelength guards prevent the optimizer from selecting a meaningless fitted pole.

# This call runs one cold-start adjoint when no compatible artifact exists.
# On an executed release notebook it resumes the saved passing stage and performs
# no additional speculative updates; the fresh validation solves below still run.
started = perf_counter()
optimization = optimize_nanobeam(iterations=1)
setup = optimization.pop("setup", None)
del setup
gc.collect()

artifact = np.load(ARTIFACT, allow_pickle=True)
best_controls = np.asarray(artifact["best_parameters"])
history = json.loads(str(artifact["history_json"]))
print(f"Accepted adjoint updates: {len(history)}")
print(f"Cold-start control norm: {np.linalg.norm(history[0]['parameters_before']):.1f}")
print(f"Notebook replay/setup time: {perf_counter() - started:.1f} s")
pd.DataFrame(history)[[
    "step", "quality_factor_before", "quality_factor", "fit_residual",
    "window_consistency", "gradient_norm", "accepted_trust_radius", "backtracks",
]]
Accepted adjoint updates: 1
Cold-start control norm: 0.0
Notebook replay/setup time: 18.0 s
step quality_factor_before quality_factor fit_residual window_consistency gradient_norm accepted_trust_radius backtracks
0 0 83600.84375 156639.46875 0.00021 0.154287 12.755676 0.075 1

4. Optimization history and the actual adjoint gradient#

first = history[0]
gradient = np.asarray(first["gradient"])
indices = np.arange(NUM_DESIGN_HOLES)

fig, axes = plt.subplots(1, 2, figsize=(12, 4.2))
stages = ["unchanged\n8-hole", "mirror-completed\ncontrol", "optimized\nliteral"]
long_q = [BASELINE_Q, MIRROR_COMPLETED_CONTROL_Q, float(artifact["validated_q"])]
colors = ["#6f8290", "#087d83", "#8221a8"]
axes[0].bar(stages, long_q, color=colors)
axes[0].axhline(TARGET_Q, color="#d29b00", ls="--", label="2× target")
axes[0].set(ylabel="independent 3 ps Q", title="Physical validation ladder")
axes[0].legend()

axes[1].bar(indices - 0.18, gradient[:NUM_DESIGN_HOLES], width=0.36, label="center", color="#087d83")
axes[1].bar(indices + 0.18, gradient[NUM_DESIGN_HOLES:], width=0.36, label="radius", color="#8221a8")
axes[1].axhline(0, color="black", lw=0.7)
axes[1].set(xlabel="positive-side hole index", ylabel="d score / d control", title="Exact-checkpointed FDTD gradient")
axes[1].legend()
plt.tight_layout()
plt.show()

pd.Series({
    "short fitted Q before": first["quality_factor_before"],
    "short fitted Q after": first["quality_factor"],
    "accepted": first["accepted"],
    "line-search backtracks": first["backtracks"],
    "adjoint wall time (s)": first["elapsed_seconds"],
}, name="value").to_frame()
../../_images/eb92187b4f412a6387bf1f1a316a61c6ad0518f9097bf6629138d0a6360bc092.png
value
short fitted Q before 83600.84375
short fitted Q after 156639.46875
accepted True
line-search backtracks 1
adjoint wall time (s) 249.182671

5. Final binary geometry and voxelized material#

The unconstrained optimizer variables are mapped through bounded tanh transforms: centers can move by at most 25 nm and radii by at most 15 nm. Those hard bounds make the 65 nm minimum bridge a geometric guarantee, not a penalty that might be violated. The plotted circles below are the exact arrays passed to the final forward builder.

final_centers_um, final_radii_um = physical_hole_geometry(best_controls)
fabrication = fabrication_metrics(best_controls)

fig, ax = plt.subplots(figsize=(12, 2.9))
draw_geometry(ax, final_centers_um, final_radii_um, "Final optimized literal-cylinder geometry")
ax.legend(loc="upper right", ncol=2)
plt.tight_layout()
plt.show()

positive = slice(NUM_DESIGN_HOLES, None)
geometry_table = pd.DataFrame({
    "control x (µm)": control_centers_um[positive],
    "optimized x (µm)": final_centers_um[positive],
    "Δx (nm)": 1e3 * (final_centers_um[positive] - control_centers_um[positive]),
    "control r (µm)": control_radii_um[positive],
    "optimized r (µm)": final_radii_um[positive],
    "Δr (nm)": 1e3 * (final_radii_um[positive] - control_radii_um[positive]),
})
display(geometry_table.round(5))
pd.Series(fabrication, name="value").to_frame()
../../_images/9d8ae7d35d450537bce726382319b24daf3ab17adcd2003f4f68f215640953c2.png
control x (µm) optimized x (µm) Δx (nm) control r (µm) optimized r (µm) Δr (nm)
0 0.16500 0.16355 -1.45445 0.09240 0.09274 0.34064
1 0.50281 0.50250 -0.31162 0.09677 0.09668 -0.09714
2 0.86562 0.86638 0.75556 0.10640 0.10606 -0.34333
3 1.26281 1.26280 -0.01012 0.11602 0.11605 0.02641
4 1.68500 1.68483 -0.16894 0.12040 0.12048 0.07917
5 2.11500 2.11496 -0.03729 0.12040 0.12041 0.01162
6 2.54500 2.54502 0.02160 0.12040 0.12039 -0.01062
7 2.97500 2.97502 0.01592 0.12040 0.12039 -0.00539
8 3.40500 3.40500 0.00364 0.12040 0.12040 -0.00024
9 3.83500 3.83500 -0.00182 0.12040 0.12040 0.00121
value
minimum_silicon_gap_nm 141.609818
minimum_hole_diameter_nm 185.481277
fabrication_violation_nm 0.000000
object_list, constraints, config = build_nanobeam_simulation(
    parameters,
    hole_centers_um=final_centers_um,
    hole_radii_um=final_radii_um,
)
key = jax.random.PRNGKey(0)
objects, arrays, design_params, config, _ = fdtdx.place_objects(
    object_list=object_list, config=config, constraints=constraints, key=key,
)
arrays, objects, _ = fdtdx.apply_params(arrays, objects, design_params, key)

fig, axes = plt.subplots(1, 2, figsize=(12, 4.2))
fdtdx.plot_material_from_side(config, arrays, viewing_side="z", position=0.0,
                              material_axis=0, plot_legend=True, ax=axes[0])
fdtdx.plot_material_from_side(config, arrays, viewing_side="y", position=0.0,
                              material_axis=0, plot_legend=True, ax=axes[1])
axes[0].set_title("Final Yee material · device plane")
axes[1].set_title("Final Yee material · vertical section")
plt.tight_layout()
plt.show()
del arrays, objects, object_list
gc.collect()
../../_images/b3aa6a2e6efd241fc428daf1ef78baceeddbc4028628ae7617fdc946f864729d.png
13963

6. Fresh baseline and final FDTD simulations#

These are not optimizer-surrogate plots. Both scenes are rebuilt from ordinary Cylinder objects and propagated for 3 ps. The baseline uses the unchanged default geometry; the final run uses the optimized center/radius arrays above. Terminal fields and complete point-monitor ringdowns are retained for the plots that follow.

started = perf_counter()
baseline_sim = run_nanobeam_simulation(parameters, record_field=True, show_progress=False)
baseline_seconds = perf_counter() - started
started = perf_counter()
final_sim = run_nanobeam_simulation(
    parameters, record_field=True, show_progress=False,
    hole_centers_um=final_centers_um, hole_radii_um=final_radii_um,
)
final_seconds = perf_counter() - started
print(f"Baseline GPU solve: {baseline_seconds:.1f} s")
print(f"Final GPU solve:    {final_seconds:.1f} s")
print(f"Reduced grid: {final_sim.objects.volume.grid_shape}; time steps: {final_sim.config.time_steps_total:,}")
Baseline GPU solve: 33.7 s
Final GPU solve:    33.2 s
Reduced grid: (180, 86, 160); time steps: 62,940

7. Material and electric-field profiles#

def unfold_quarter(values):
    values = np.asarray(values)
    full_x = np.concatenate((values[::-1, :], values), axis=0)
    return np.concatenate((full_x[:, ::-1], full_x), axis=1)

domain_x = parameters.interior_um[0] / 2 + parameters.pml_layers * parameters.grid_spacing_um
domain_y = parameters.interior_um[1] / 2 + parameters.pml_layers * parameters.grid_spacing_um
extent = [-domain_x, domain_x, -domain_y, domain_y]

fig, axes = plt.subplots(2, 2, figsize=(13, 7.2), sharex=True, sharey=True)
for column, (label, sim) in enumerate((("unchanged baseline", baseline_sim), ("optimized final", final_sim))):
    eps = 1 / np.asarray(sim.arrays.inv_permittivities[1])
    eps_xy = unfold_quarter(eps[:, :, eps.shape[2] // 2])
    ey_xy = unfold_quarter(np.abs(sim.field_snapshot))
    material = axes[0, column].imshow(eps_xy.T, origin="lower", extent=extent, cmap="viridis", aspect="equal")
    field = axes[1, column].imshow(
        (ey_xy / (ey_xy.max() + 1e-30)).T, origin="lower", extent=extent,
        cmap="magma", vmin=0, vmax=1, aspect="equal",
    )
    axes[1, column].contour(
        np.linspace(extent[0], extent[1], eps_xy.shape[0]),
        np.linspace(extent[2], extent[3], eps_xy.shape[1]),
        eps_xy.T, levels=[2.0], colors="cyan", linewidths=0.5,
    )
    axes[0, column].set_title(f"{label} · εᵧᵧ")
    axes[1, column].set_title(f"{label} · normalized |Ey| at 3 ps")
for ax in axes.flat:
    ax.set(xlim=(-4.5, 4.5), ylim=(-0.7, 0.7), ylabel="y (µm)")
axes[1, 0].set_xlabel("x (µm)")
axes[1, 1].set_xlabel("x (µm)")
fig.colorbar(material, ax=axes[0, :], shrink=0.8, label="relative permittivity")
fig.colorbar(field, ax=axes[1, :], shrink=0.8, label="normalized |Ey|")
plt.show()
../../_images/55b7d647f6114f137152593b7b6c9558e6d5e94da26eddaea33ce08f5c890b68.png

8. Ringdown, fitted pole, and spectrum#

sample_dt = 2 * final_sim.config.time_step_duration
time_ps = parameters.analysis_start_ps + np.arange(final_sim.ringdown.size) * sample_dt * 1e12
baseline_pole = _dominant_resonance(baseline_sim.ringdown, sample_dt)
final_pole = _dominant_resonance(final_sim.ringdown, sample_dt)

fig, axes = plt.subplots(1, 3, figsize=(15, 4.2))
for signal, label, color in (
    (baseline_sim.ringdown, "baseline", "#087d83"),
    (final_sim.ringdown, "optimized", "#8221a8"),
):
    normalized = signal / np.max(np.abs(signal))
    axes[0].plot(time_ps[::4], normalized[::4], lw=0.65, label=label, color=color)
    envelope = np.abs(hilbert(signal))
    axes[1].semilogy(time_ps[::10], (envelope / envelope.max())[::10], lw=1.0, label=label, color=color)

    frequencies = np.fft.rfftfreq(signal.size, sample_dt)
    spectrum = np.abs(np.fft.rfft(signal * np.hanning(signal.size)))
    mask = (frequencies > fdtdx.constants.c / 1.6e-6) & (frequencies < fdtdx.constants.c / 1.4e-6)
    wavelength = fdtdx.constants.c / frequencies[mask] * 1e6
    order = np.argsort(wavelength)
    axes[2].plot(wavelength[order], (spectrum[mask] / spectrum[mask].max())[order], label=label, color=color)

axes[0].set(xlabel="time (ps)", ylabel="normalized Ey", title="Recorded ringdown")
axes[1].set(xlabel="time (ps)", ylabel="normalized analytic envelope", title="Small measured decay fraction")
axes[2].axvline(final_pole["wavelength_um"], color="#d29b00", ls="--", label="fitted final pole")
axes[2].set(xlabel="wavelength (µm)", ylabel="normalized FFT", title="Ringdown spectrum", xlim=(1.4, 1.6))
for ax in axes:
    ax.legend()
plt.tight_layout()
plt.show()

pd.DataFrame({
    "unchanged baseline": [baseline_pole["wavelength_um"], baseline_pole["Q"], baseline_pole["fit_error"]],
    "optimized literal": [final_pole["wavelength_um"], final_pole["Q"], final_pole["fit_error"]],
}, index=["wavelength (µm)", "independent Q", "ResonanceFinder error"])
../../_images/b1039493345788d3e931ed048baf07817863d79372c2241904d3820f1b25230f.png
unchanged baseline optimized literal
wavelength (µm) 1.568706e+00 1.568299
independent Q 3.512530e+04 187133.234081
ResonanceFinder error 3.919232e-07 0.000005

9. Differentiable short fit versus independent long fit#

The optimization fit sees only the first 6,000 post-source samples (three windows), whereas the held-out ResonanceFinder sees all 29,371 samples. Agreement does not have to be exact—the short fit is intentionally cheap—but both must track the same wavelength and the long independent result alone decides pass/fail.

short_fit = fdtdx.fit_resonance(
    jnp.asarray(final_sim.ringdown[:3 * SAMPLES_PER_WINDOW]),
    time_step=sample_dt,
    center_frequency=fdtdx.constants.c / 1.57e-6,
    samples_per_window=SAMPLES_PER_WINDOW,
    num_windows=3,
)
fit_table = pd.DataFrame({
    "differentiable 0.6 ps fit": [
        float(fdtdx.constants.c / short_fit.frequency * 1e6),
        float(short_fit.quality_factor), float(short_fit.residual),
        float(short_fit.window_consistency),
    ],
    "independent 2.8 ps fit": [
        final_pole["wavelength_um"], final_pole["Q"],
        final_pole["fit_error"], np.nan,
    ],
}, index=["wavelength (µm)", "Q", "fit residual/error", "window consistency"])
fit_table
differentiable 0.6 ps fit independent 2.8 ps fit
wavelength (µm) 1.568298 1.568299
Q 157462.078125 187133.234081
fit residual/error 0.000204 0.000005
window consistency 0.151263 NaN

10. Local FDTDX metrics#

metrics = pd.Series({
    "unchanged baseline Q": baseline_pole["Q"],
    "required Q": 2 * baseline_pole["Q"],
    "mirror-completed control Q": MIRROR_COMPLETED_CONTROL_Q,
    "optimized literal Q": final_pole["Q"],
    "optimized / unchanged": final_pole["Q"] / baseline_pole["Q"],
    "optimized / completed control": final_pole["Q"] / MIRROR_COMPLETED_CONTROL_Q,
    "minimum silicon bridge (nm)": fabrication["minimum_silicon_gap_nm"],
    "minimum hole diameter (nm)": fabrication["minimum_hole_diameter_nm"],
    "fabrication violation (nm)": fabrication["fabrication_violation_nm"],
    "binary material fraction": 1.0,
}, name="measured value")
display(metrics.to_frame())
assert metrics["optimized / unchanged"] >= 2.0
assert metrics["optimized / completed control"] >= 1.0
assert metrics["fabrication violation (nm)"] == 0.0
print("PASS: literal inverse-designed Q exceeds both physical controls.")
print("uv run fdtdx-bench run --case invdes_nanobeam_cavity")
measured value
unchanged baseline Q 35125.300715
required Q 70250.601430
mirror-completed control Q 92493.301338
optimized literal Q 187133.234081
optimized / unchanged 5.327591
optimized / completed control 2.023209
minimum silicon bridge (nm) 141.609818
minimum hole diameter (nm) 185.481277
fabrication violation (nm) 0.000000
binary material fraction 1.000000
PASS: literal inverse-designed Q exceeds both physical controls.
uv run fdtdx-bench run --case invdes_nanobeam_cavity

11. Independent Tidy3D verification of the same cylinders#

The fixed-count control and optimized arrays above were inserted into the trusted Tidy3D nanobeam builder without changing its 25 nm uniform grid, silicon/oxide materials, source, three time probes, 12-layer PML, (1, -1, 0) symmetry, or 3 ps runtime. Both Tidy3D scenes contain exactly 20 holes, so this ratio cannot benefit from adding mirror periods. The cloud solves are frozen here; executing this notebook reads their signals and does not submit a paid task.

tidy = np.load(tidy_validation_path, allow_pickle=True)
tidy_meta = json.loads(str(tidy["meta_json"]))

fig, axes = plt.subplots(1, 2, figsize=(12.5, 4.2))
for stem, label, color in (
    ("mirror_completed_control", "20-hole control", "#087d83"),
    ("optimized", "20-hole optimized", "#8221a8"),
):
    time_ps_tidy = tidy[f"{stem}_time_s"] * 1e12
    signal_tidy = np.asarray(tidy[f"{stem}_signal"])
    normalized = signal_tidy / np.max(np.abs(signal_tidy))
    envelope = np.abs(hilbert(np.real(signal_tidy)))
    axes[0].plot(time_ps_tidy[::8], np.real(normalized[::8]), lw=0.65,
                 label=label, color=color)
    axes[1].semilogy(time_ps_tidy[::12], (envelope / envelope.max())[::12],
                    lw=1.0, label=label, color=color)
axes[0].set(xlabel="time (ps)", ylabel="normalized Re(Ey)",
            title="Matched Tidy3D ringdowns")
axes[1].set(xlabel="time (ps)", ylabel="normalized envelope",
            title="Optimized cavity decays more slowly")
for ax in axes:
    ax.grid(alpha=0.2)
    ax.legend()
plt.tight_layout()
plt.show()

cross_solver = pd.DataFrame({
    "FDTDX": [
        MIRROR_COMPLETED_CONTROL_Q,
        final_pole["Q"],
        final_pole["Q"] / MIRROR_COMPLETED_CONTROL_Q,
    ],
    "Tidy3D": [
        float(tidy["mirror_completed_control_Q"][0]),
        float(tidy["optimized_Q"][0]),
        float(tidy["tidy3d_q_ratio"][0]),
    ],
}, index=["20-hole control Q", "20-hole optimized Q", "optimized / control"])
display(cross_solver)
print("Tidy3D control fit error:", float(tidy["mirror_completed_control_fit_error"][0]))
print("Tidy3D optimized fit error:", float(tidy["optimized_fit_error"][0]))
print("Tidy3D task IDs:", {name: values["task_id"] for name, values in tidy_meta["tasks"].items()})
print("Billed FlexCredits:", sum(values["actual_flexcredits"] for values in tidy_meta["tasks"].values()))
assert float(tidy["tidy3d_q_ratio"][0]) > 1.0
../../_images/c9f122464fea3ed94ff66e448c04970fa14650ee000767290824347f069b6588.png
FDTDX Tidy3D
20-hole control Q 92493.301338 84246.788385
20-hole optimized Q 187133.234081 146677.821390
optimized / control 2.023209 1.741049
Tidy3D control fit error: 0.023585881021420136
Tidy3D optimized fit error: 0.001235640038273991
Tidy3D task IDs: {'mirror_completed_control': 'fdve-1b9cf912-868e-4779-a79d-8fe99e72b2a4', 'optimized': 'fdve-f63006bd-ae2e-490e-8506-ce3d0310e9b0'}
Billed FlexCredits: 0.09794087790122365

Interpretation#

The unchanged eight-hole cavity reaches about \(3.5 imes10^4\). Automatic mirror completion removes an artificial leakage bottleneck and reaches about \(9.2 imes10^4\); that setup improvement is reported separately and is not attributed to the adjoint. One fitted-Q adjoint update then more than doubles the completed control in the independent literal solve, reaching about \(1.87 imes10^5\) or 5.3× the unchanged cavity. In the independent matched Tidy3D pair, the same shape change raises Q from 84,247 to 146,678, a 1.741× fixed-hole-count improvement. Tidy3D therefore confirms that the shape optimization works, while also showing that the present 25 nm FDTDX discretization overpredicts its magnitude (2.023× locally). The small center/radius shifts are physically resolved by both solvers’ subpixel interface treatments; this was a bounded 20-parameter shape optimization, not a pixel design.

For fabrication or publication, repeat across grid spacings, etch bias, sidewall angle, material dispersion, and multiple fitting windows. This notebook proves the requested GPU-native automatic-differentiation workflow and its held-out physical result; it does not claim that a single nominal design is robust to every process variation.