Skip to content

Computing the gradient of a level-set function #2235

@oskooi

Description

@oskooi

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.

binary_grating_matgrid

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}")

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions