# Define simulation parameters (see above)
= 2 * np.pi * 200e12
omega = 25e-9
dl
= 200
Nx = 120
Ny = 20
Npml
# Define permittivity for a straight waveguide
= np.ones((Nx, Ny))
epsr 50:67] = 12.0
epsr[:,
# Source position
= np.arange(20, 100)
src_y = 30 * np.ones(src_y.shape, dtype=int)
src_x
# Source for mode 1
= insert_mode(omega, dl, src_x, src_y, epsr, m=1)
source1
# Source for mode 2
= insert_mode(omega, dl, src_x, src_y, epsr, m=2)
source2
# Run the simulation exciting mode 1
= ceviche.fdfd_ez(omega, dl, epsr, [Npml, Npml])
simulation = simulation.solve(source1)
Hx, Hy, Ez
# Visualize the electric field
= ceviche.viz.real(Ez, outline=epsr, cmap="RdBu_r")
ax "k")
ax.plot(src_x, src_y,
# Run the simulation exciting mode 2
= ceviche.fdfd_ez(omega, dl, epsr, [Npml, Npml])
simulation = simulation.solve(source2)
Hx, Hy, Ez
# Visualize the electric field
= ceviche.viz.real(Ez, outline=epsr, cmap="RdBu_r")
ax "k")
ax.plot(src_x, src_y,
plt.show()
Naive Inverse Design
This notebook was adapted from Ceviche’s inverse design introduction to use a JAX-based optimization loop in stead of the default Ceviche optimization loop.
Introduction: multi-mode waveguides
The ceviche
package has a built-in method insert_mode()
that allows different modes to be inserted as sources.
Below, we demonstrate how this functionality can be used to excite the first and second order modes of a straight waveguide:
Simulation and optimization parameters
Our toy optimization problem will be to design a device that converts an input in the first-order mode into an output as the second-order mode. First, we define the parameters of our device and optimization:
# Angular frequency of the source in Hz
= 2 * np.pi * 200e12
omega # Spatial resolution in meters
= 40e-9
dl # Number of pixels in x-direction
= 100
Nx # Number of pixels in y-direction
= 100
Ny # Number of pixels in the PMLs in each direction
= 20
Npml # Initial value of the structure's relative permittivity
= 12.0
epsr_init # Space between the PMLs and the design region (in pixels)
= 10
space # Width of the waveguide (in pixels)
= 12
wg_width # Length in pixels of the source/probe slices on each side of the center point
= 8
space_slice # Number of epochs in the optimization
= 100
Nsteps # Step size for the Adam optimizer
= 1e-2 step_size
Utility functions
We now define some utility functions for initialization and optimization:
init_domain
init_domain (Nx=100, Ny=100, Npml=20, space=10, wg_width=12, space_slice=8)
Initializes the domain and design region
space : The space between the PML and the structure wg_width : The feed and probe waveguide width space_slice : The added space for the probe and source slices
mask_combine_epsr
mask_combine_epsr (epsr, bg_epsr, design_region)
Utility function for combining the design region epsr and the background epsr
viz_sim
viz_sim (epsr, source, slices=[])
Solve and visualize a simulation with permittivity ‘epsr’
mode_overlap
mode_overlap (E1, E2)
Defines an overlap integral between the simulated field and desired field
Visualizing the starting device
We can visualize what our starting device looks like and how it behaves. Our device is initialized by the init_domain()
function which was defined several cells above.
# Initialize the parametrization rho and the design region
= init_domain(
epsr, bg_epsr, design_region, input_slice, output_slice =space, wg_width=wg_width, space_slice=space_slice
Nx, Ny, Npml, space
)
= mask_combine_epsr(epsr, bg_epsr, design_region)
epsr_total
# Setup source
= insert_mode(omega, dl, input_slice.x, input_slice.y, epsr_total, m=1)
source
# Setup probe
= insert_mode(omega, dl, output_slice.x, output_slice.y, epsr_total, m=2) probe
# Simulate initial device
= viz_sim(epsr_total, source, slices=[input_slice, output_slice])
simulation, ax
# get normalization factor (field overlap before optimizing)
= simulation.solve(source)
_, _, Ez = mode_overlap(Ez, probe) E0
Define objective function
We will now define our objective function. This is a scalar-valued function which our optimizer uses to improve the device’s performance.
Our objective function will consist of maximizing an overlap integral of the field in the output waveguide of the simulated device and the field of the waveguide’s second order mode (minimizing the negative overlap). The function takes in a single argument, epsr
and returns the value of the overlap integral. The details of setting the permittivity and solving for the fields happens inside the objective function.
loss_fn
loss_fn (epsr)
Objective function called by optimizer
- Takes the epsr distribution as input
- Runs the simulation
- Returns the overlap integral between the output wg field and the desired mode field
Run optimization
This is where our discussion deviates from the original discussion by the ceviche maintainers. In our case, we would like the optimization to fit in a JAX-based optimization scheme:
# Simulate initial device
= viz_sim(epsr_total, source, slices=[input_slice, output_slice]) simulation, ax
= jacobian(loss_fn, mode='reverse') grad_fn
= adam(step_size)
init_fn, update_fn, params_fn = init_fn(epsr.reshape(1, -1)) state
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
this is the optimization step:
step_fn
step_fn (step, state)
we can now loop over the optimization:
= trange(30)
range_ for step in range_:
= step_fn(step, state)
loss, state =float(loss)) range_.set_postfix(loss
loss
-6146280.518657835
= params_fn(state)
epsr_optimum = epsr_optimum.reshape((Nx, Ny)) epsr_optimum
# Simulate and show the optimal device
= mask_combine_epsr(epsr_optimum, bg_epsr, design_region)
epsr_optimum_total = viz_sim(epsr_optimum_total, source, slices=[input_slice, output_slice]) simulation, ax
At the end of the optimization we can see our final device. From the field pattern, we can easily observe that the device is doing what we intend: the even mode enters from the left and exits as the odd mode on the right.
However, an additional observation is that our device’s permittivity changes continuously. This is not ideal if we wanted to fabricated our device. We’re also not constraining the minimum and maximum values of \(\epsilon_r\). Thus, we need to consider alternative ways of parameterizing our device.
="plasma", vmin=1, vmax=4)
plt.imshow(np.sqrt(epsr_optimum_total.T), cmap*plt.ylim()[::-1])
plt.ylim(=[1,2,3,4], label="n")
plt.colorbar(ticks"x")
plt.xlabel("y")
plt.xlabel(True) plt.grid(