-
Notifications
You must be signed in to change notification settings - Fork 758
Description
Related to #2011.
This is an initial attempt to use the new subpixel smoothing feature of the adjoint solver to compute the gradient of a structure parameterized by its geometry (a level set) as an alternative to the conventional density-based (MaterialGrid) representation. The example involves computing the gradient of the diffraction efficiency of the m=+1 transmitted order of a 1D binary grating with respect to its height and fill factor. The results are to be validated by the usual method of the brute-force gradient computed using a finite difference. Once everything is working correctly, this demonstration will be converted into a tutorial for the user manual and a unit test.
The main feature of this calculation is a differentiable wrapper function (grating_matgrid in the script below) that takes the grating height and fill factor as arguments and returns the weights of a MaterialGrid for the binary grating. These weights are then passed to the adjoint solver via a DesignRegion object in the usual way and the gradient which is computed is then backpropagated through the wrapper function.
An image of the geometry created using this approach confirms that the structure is set up correctly. However, it seems there is a bug in the current setup because the final gradient is zero. Also, there is an important warning from autograd:
/.local/lib/python3/site-packages/autograd/tracer.py:14: UserWarning: Output seems independent of input.
warnings.warn("Output seems independent of input.")
This message suggests there is a likely bug in the backpropagation step.
output of running the script below
-----------
Initializing structure...
Halving computational cell along direction y
Splitting into 2 chunks by voxels
time for choose_chunkdivision = 0.000121082 s
Working in 2D dimensions.
Computational cell is 8.5 x 0.78 x 0 with resolution 100
time for set_epsilon = 0.024535 s
-----------
field decay(t = 50.005): 0.04304565716016541 / 0.04304565716016541 = 1.0
field decay(t = 100.01): 0.11354452052348654 / 0.11354452052348654 = 1.0
on time step 29645 (time=148.225), 0.000134933 s/step
field decay(t = 150.01500000000001): 1.3975259335832059e-08 / 0.11354452052348654 = 1.2308175922008755e-07
field decay(t = 200.02): 5.861816753051918e-16 / 0.11354452052348654 = 5.162571232875486e-15
run 0 finished at t = 200.02 (40004 timesteps)
Starting forward run...
-----------
Initializing structure...
Warning: grid volume is not an integer number of pixels; cell size will be rounded to nearest pixel.
Halving computational cell along direction y
Splitting into 2 chunks by voxels
time for choose_chunkdivision = 0.000153305 s
Working in 2D dimensions.
Computational cell is 8.5 x 0.78 x 0 with resolution 100
block, center = (-2.25,0,0)
size (4,1e+20,1e+20)
axes (1,0,0), (0,1,0), (0,0,1)
dielectric constant epsilon diagonal = (2.25,2.25,2.25)
block, center = (1.5,0,0)
size (3.5,0.777862,0)
axes (1,0,0), (0,1,0), (0,0,1)
time for set_epsilon = 0.10944 s
-----------
run 0 finished at t = 160.28 (32056 timesteps)
Warning: grid volume is not an integer number of pixels; cell size will be rounded to nearest pixel.
MPB solved for frequency_1(1.53209,0,0) = 1.53209 after 11 iters
MPB solved for frequency_1(2,0,0) = 2 after 1 iters
Dominant planewave for band 1: (2.000000,-0.000000,0.000000)
Starting adjoint run...
Warning: grid volume is not an integer number of pixels; cell size will be rounded to nearest pixel.
MPB solved for frequency_1(-1.53209,0,0) = 1.53209 after 11 iters
MPB solved for frequency_1(-2,0,0) = 2 after 1 iters
run 1 finished at t = 106.055 (21211 timesteps)
Calculating gradient...
objective:, [0.1019033]
Warning: grid volume is not an integer number of pixels; cell size will be rounded to nearest pixel.
block, center = (-2.25,0,0)
size (4,1e+20,1e+20)
axes (1,0,0), (0,1,0), (0,0,1)
dielectric constant epsilon diagonal = (2.25,2.25,2.25)
block, center = (1.5,0,0)
size (3.5,0.777862,0)
axes (1,0,0), (0,1,0), (0,0,1)
/.local/lib/python3/site-packages/autograd/tracer.py:14: UserWarning: Output seems independent of input.
warnings.warn("Output seems independent of input.")
gradient:, 0.0
Elapsed run time = 15.9436 s
import math
import cmath
from enum import Enum
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
from autograd import numpy as npa
from autograd import tensor_jacobian_product
import meep as mp
import meep.adjoint as mpa
dpml = 1.0 # PML thickness
dsub = 3.0 # substrate thickness
dpad = 3.0 # padding between PML and grating
ng = 1.5
glass = mp.Medium(index=ng)
# angle of transmtted order
# 0 is +x with CCW rotation around z axis
theta_tran = math.radians(40.0)
boundary_layers = [mp.PML(thickness=dpml,direction=mp.X)]
Polarization = Enum('Polarization','S P')
def tran_order(res: float, wvl: float, theta_pw: float, pol: Polarization,
m: int, gp: float, gh: float, gff: float):
"""Computes the gradient of the diffraction efficiency of the m'th
transmitted order of a 1D binary grating with respect to the grating
height and fill factor using the adjoint solver.
Args:
res: resolution (pixels/μm)
wvl: wavelength of the diffraction order.
theta_pw: angle (degrees) of incident planewave.
0° is +x with CCW rotation around z axis.
plane of incidence is xy.
pol: polarization of the incident planewave.
m: diffraction order in y direction.
gp: grating period.
gh: grating height.
gff: grating fill factor.
"""
sx = dpml+dsub+gh+dpad+dpml
sy = gp
cell_size = mp.Vector3(sx,sy,0)
design_region_size = mp.Vector3(gh+dpad,sy,0)
design_region_resolution = int(2*res)
Nx = int(design_region_size.x*design_region_resolution)
Ny = int(design_region_size.y*design_region_resolution)
matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny),
mp.air,
glass,
weights=np.ones((Nx,Ny)))
matgrid_region = mpa.DesignRegion(
matgrid,
volume=mp.Volume(
center=mp.Vector3(0.5*sx-dpml-0.5*(gh+dpad),0),
size=mp.Vector3(design_region_size.x,
design_region_size.y)
)
)
matgrid_geometry = [mp.Block(center=matgrid_region.center,
size=matgrid_region.size,
material=matgrid)]
if pol.name == 'S':
src_cmpt = mp.Ez
mode_parity = mp.ODD_Z
elif pol.name == 'P':
src_cmpt = mp.Hz
mode_parity = mp.EVEN_Z
else:
raise ValueError("pol must be S or P, only.")
fcen = 1/wvl
# wavevector of transmitted order in air
kdiff = mp.Vector3((fcen**2-(m/sy)**2)**0.5,m/sy,0)
if theta_pw == 0:
k = mp.Vector3()
symmetries = [mp.Mirror(direction=mp.Y,
phase=1. if pol.name == 'S' else -1.)]
else:
k = mp.Vector3(ng*fcen,0,0).rotate(mp.Vector3(0,0,1),math.radians(theta_pw))
symmetries = []
def pw_amp(k,x0):
def _pw_amp(x):
return cmath.exp(1j*2*math.pi*k.dot(x+x0))
return _pw_amp
src_pt = mp.Vector3(-0.5*sx+dpml,0,0)
sources = [mp.Source(mp.GaussianSource(fcen,fwidth=0.05*fcen),
component=src_cmpt,
center=src_pt,
size=mp.Vector3(0,sy,0),
amp_func=pw_amp(k,src_pt))]
sim = mp.Simulation(resolution=res,
cell_size=cell_size,
default_material=glass,
symmetries=symmetries,
boundary_layers=boundary_layers,
k_point=k,
sources=sources)
tran_pt = mp.Vector3(0.5*sx-dpml,0)
tran_mon = sim.add_flux(fcen,
0,
1,
mp.FluxRegion(center=tran_pt,
size=mp.Vector3(0,sy,0)))
sim.run(
until_after_sources=mp.stop_when_fields_decayed(
50,
src_cmpt,
tran_pt,
1e-9
)
)
input_flux = mp.get_fluxes(tran_mon)[0]
sim.reset_meep()
substrate = [mp.Block(material=glass,
size=mp.Vector3(dpml+dsub,mp.inf,mp.inf),
center=mp.Vector3(-0.5*sx+0.5*(dpml+dsub),0,0))]
geometry = substrate + matgrid_geometry
sim = mp.Simulation(resolution=res,
cell_size=cell_size,
symmetries=symmetries,
boundary_layers=boundary_layers,
k_point=k,
sources=sources,
geometry=geometry)
obj_list = [mpa.EigenmodeCoefficient(
sim,
mp.Volume(center=tran_pt,
size=mp.Vector3(0,sy,0)),
1,
eig_parity=mode_parity,
kpoint_func=lambda *not_used: kdiff
)]
def J(mode_mon):
return npa.power(npa.abs(mode_mon),2) / input_flux
opt = mpa.OptimizationProblem(simulation=sim,
objective_functions=J,
objective_arguments=obj_list,
design_regions=[matgrid_region],
frequencies=[fcen])
def grating_matgrid(h, ff):
"""Generates the weights for a `MaterialGrid`
based on the grating height and fill factor.
"""
xcoord = npa.linspace(-0.5*design_region_size.x,
+0.5*design_region_size.x,
Nx)
ycoord = npa.linspace(-0.5*design_region_size.y,
+0.5*design_region_size.y,
Ny)
xv, yv = npa.meshgrid(xcoord,ycoord)
p = npa.where(npa.abs(yv) <= 0.5*ff*gp,
npa.where(xv <= xcoord[0] + h,
1.,
0.),
0.)
p = p.flatten(order='F')
return p
design_params = grating_matgrid(gh, gff)
f, dJ_du = opt([design_params])
print(f"objective:, {f}")
opt.plot2D()
if mp.am_master():
plt.savefig('binary_grating_matgrid.png',dpi=150,bbox_inches='tight')
backprop_dJ_du = tensor_jacobian_product(grating_matgrid, 0)(gh, gff, dJ_du)
sim.reset_meep()
return backprop_dJ_du
if __name__ == '__main__':
res = 100
wvl = 0.5 # wavelength
inc_ang = 0.
m = +1
pol = Polarization.S
gp = wvl/math.sin(theta_tran) # grating period
gh = 0.5 # grating height
gff = 0.5 # grating fill factor
diff_eff_grad = tran_order(res, wvl, inc_ang, pol, m, gp, gh, gff)
print(f"gradient:, {diff_eff_grad}")