Inverse design quickstart

Inverse design quickstart#

This notebook will get users up and running with a very simple inverse design optimization with tidy3d. Inverse design uses the “adjoint method” to compute gradients of a figure of merit with respect to design parameters using only 2 simulations no matter how many design parameters are present. This gradient is then used to do high dimensional, gradient-based optimization of the system.

The setup we’ll demonstrate here involves a point dipole source and a point field monitor on either side of a dielectric box. Using the adjoint plugin in tidy3d, we use gradient-based optimization to maximize the intensity enhancement at the measurement spot with respect to the box size in all 3 dimensions.

Schematic of the design problem.

For more detailed notebooks, see these

[1]:
# To install other packages needed, uncomment lines below.
# !pip install optax
[2]:
import tidy3d as td
from tidy3d.web import run
import matplotlib.pylab as plt

import autograd as ag
import autograd.numpy as anp
import optax

Setup#

First, we set up some basic parameters and “static” components of our simulation.

[3]:
# wavelength and frequency
wavelength = 1.55
freq0 = td.C_0 / wavelength

# permittivity of box
eps_box = 2

# size of sim in x,y,z
L = 10 * wavelength

# spc between sources, monitors, and PML / box
buffer = 1.0 * wavelength
[4]:
# create a source to the left of sim
source = td.PointDipole(
    center=(-L / 2 + buffer, 0, 0),
    source_time=td.GaussianPulse(freq0=freq0, fwidth=freq0 / 10.0),
    polarization="Ez",
)
[5]:
# create a monitor to right of sim for measuring intensity
monitor = td.FieldMonitor(
    center=(+L / 2 - buffer, 0, 0),
    size=(0.0, 0.0, 0.0),
    freqs=[freq0],
    name="point",
)
[6]:
# create "base" simulation (the box will be added inside of the objective function later)
sim = td.Simulation(
    size=(L, L, L),
    grid_spec=td.GridSpec.auto(min_steps_per_wvl=25),
    structures=[],
    sources=[source],
    monitors=[monitor],
    run_time=120 / freq0,
)

Define objective function#

Now we construct our objective function out of some helper functions. Our objective function measures the intensity enhancement at the measurement point as a function of a design parameter that controls the box size.

[7]:
# function to get box size (um) as a function of the design parameter (-inf, inf)

size_min = 0
size_max = L - 4 * buffer


def get_size(param: float):
    """Size of box as function of parameter, smoothly maps (-inf, inf) to (size_min, size_max)."""
    param_01 = 0.5 * (anp.tanh(param) + 1)
    return (size_max * param_01) + (size_min * (1 - param_01))
[8]:
# function to construct the simulation as a function of the design parameter


def make_sim(param: float) -> float:
    """Make simulation with a Box added, as given by the design parameter."""

    # for normalization, ignore any structures and return base sim
    if param is None:
        return sim.copy()

    # make a Box with the side length set by the parameter
    size_box = get_size(param)

    box = td.Structure(
        geometry=td.Box(center=(0, 0, 0), size=(size_box, size_box, size_box)),
        medium=td.Medium(permittivity=eps_box),
    )

    # add the box to the simulation
    return sim.updated_copy(structures=[box])
[9]:
# function to compute and measure intensity as function of the design paramater


def measure_intensity(sim_data: td.SimulationData) -> float:
    """get intensity from SimulationData."""
    return anp.sum(sim_data.get_intensity(monitor.name).values)


def intensity(param: float) -> float:
    """Intensity measured at monitor as function of parameter."""

    # make the sim using the parameter value
    sim_with_square = make_sim(param)

    # run sim through tidy3d web API
    data = run(sim_with_square, task_name="adjoint_quickstart", verbose=False)

    # evaluate the intensity at the measurement position
    return measure_intensity(data)
[10]:
# get the intensity with no box, for normalization (care about enhancement, not abs value)
intensity_norm = intensity(param=None)
print(f"With no box, intensity = {intensity_norm:.4f}.")
print("This value will be used for normalization of the objective function.")
With no box, intensity = 95.8381.
This value will be used for normalization of the objective function.
[11]:
def objective_fn(param: float) -> float:
    """Intensity at measurement point, normalized by intensity with no box."""
    return intensity(param) / intensity_norm

Optimization Loop#

Next, we use autograd to construct a function that returns the gradient of our objective function and use this to run our gradient-based optimization in a for loop.

[12]:
# use autograd to get function that returns objective function and its gradient
val_and_grad_fn = ag.value_and_grad(objective_fn)
[13]:
# hyperparameters
num_steps = 9
learning_rate = 0.05

# initialize adam optimizer with starting parameter
param = -0.5
optimizer = optax.adam(learning_rate=learning_rate)
opt_state = optimizer.init(param)

# store history
objective_history = [1.0]  # the normalized objective function with no box
param_history = [-100, param]  # -100 is approximately "no box" (size=0)

for i in range(num_steps):
    print(f"step = {i + 1}")
    print(f"\tparam = {param:.4f}")
    print(f"\tsize = {get_size(param):.4f} um")

    # compute gradient and current objective funciton value
    value, gradient = val_and_grad_fn(param)

    # outputs
    print(f"\tintensity = {value:.4e}")
    print(f"\tgrad_norm = {anp.linalg.norm(gradient):.4e}")

    # compute and apply updates to the optimizer based on gradient (-1 sign to maximize obj_fn)
    updates, opt_state = optimizer.update(-gradient, opt_state, param)
    param = optax.apply_updates(param, updates)
    param = float(param)

    # save history
    objective_history.append(value)
    param_history.append(param)
step = 1
        param = -0.5000
        size = 2.5012 um
        intensity = 7.9076e+00
        grad_norm = 1.8164e+00
step = 2
        param = -0.4500
        size = 2.6882 um
        intensity = 9.8537e+00
        grad_norm = 2.1918e+00
step = 3
        param = -0.4000
        size = 2.8833 um
        intensity = 1.1362e+01
        grad_norm = 1.8857e+00
step = 4
        param = -0.3501
        size = 3.0855 um
        intensity = 1.2988e+01
        grad_norm = 2.3163e+00
step = 5
        param = -0.3000
        size = 3.2955 um
        intensity = 1.6538e+01
        grad_norm = 4.7152e+00
step = 6
        param = -0.2516
        size = 3.5043 um
        intensity = 1.8165e+01
        grad_norm = 2.1677e+00
step = 7
        param = -0.2036
        size = 3.7162 um
        intensity = 2.0758e+01
        grad_norm = 2.7685e+00
step = 8
        param = -0.1552
        size = 3.9342 um
        intensity = 2.3270e+01
        grad_norm = 1.3610e+00
step = 9
        param = -0.1086
        size = 4.1470 um
        intensity = 2.2610e+01
        grad_norm = 3.5083e+00

Analysis#

Finally we plot our results: optimization progress, field pattern, and box size vs intensity enhancement.

[14]:
# objective function vs iteration number
plt.plot(objective_history)
plt.xlabel("iteration number")
plt.ylabel("intensity enhancement (unitless)")
plt.title("intensity enhancement during optimization")
plt.show()
../_images/notebooks_Autograd0Quickstart_18_0.png
[15]:
# construct simulation with final parameters
sim_final = make_sim(param=param_history[-1])

# add a field monitor for plotting
fld_mnt = td.FieldMonitor(
    center=(+L / 2 - buffer, 0, 0),
    size=(td.inf, td.inf, 0),
    freqs=[freq0],
    name="fields",
)
sim_final = sim_final.updated_copy(monitors=[monitor, fld_mnt])

# run simulation
data_final = run(sim_final, task_name="quickstart_final", verbose=False)
[17]:
# record final intensity
intensity_final = measure_intensity(data_final)
intensity_final_normalized = intensity_final / intensity_norm

objective_history.append(intensity_final_normalized)
[18]:
# plot intensity distribution
ax = data_final.plot_field(
    field_monitor_name="fields", field_name="E", val="abs^2", vmax=intensity_final
)

ax.plot(source.center[0], 0, marker="o", mfc="limegreen", mec="black", ms=10)
ax.plot(monitor.center[0], 0, marker="o", mfc="orange", mec="black", ms=10)
plt.show()
../_images/notebooks_Autograd0Quickstart_21_0.png
[19]:
# scatter the intensity enhancement vs the box size
sizes = [get_size(p) for p in param_history]
objective_history = objective_history
_ = plt.scatter(sizes, objective_history)
ax = plt.gca()
ax.set_xlabel("box size (um)")
ax.set_ylabel("intensity enhancement (unitless)")
plt.title("intensity enhancement vs. box size")
plt.show()
../_images/notebooks_Autograd0Quickstart_22_0.png
[ ]: