Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for DiffractedPlanewave to EigenmodeCoefficient objective function of adjoint solver #2054

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

oskooi
Copy link
Collaborator

@oskooi oskooi commented May 2, 2022

Fixes #2047.

This is a draft and still needs a unit test.

It seems all that is required to enable this feature is to simply reverse (when necessary) the direction of the wavevector of the DiffractedPlanewave object (its g property) in the place_adjoint_source and __call__ functions of the EigenmodeCoefficient class. No other changes to the adjoint solver are necessary.

Copy link
Collaborator

@smartalecH smartalecH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@oskooi
Copy link
Collaborator Author

oskooi commented May 5, 2022

Even though we cannot add a unit test in this PR until #2055 is first resolved, here is a draft of the unit test which computes the directional derivative based on an objective function which uses the mode coefficient of a transmitted order of a 2D cylindrical grating with 3D unit cell. The gradient from the adjoint solver is validated using the standard approach of comparing to the brute-force result obtained via a finite difference. Unfortunately, there is a noticeable discrepancy (a factor of ~4) which is likely due to discretization errors that tend to be larger in 3D compared to 2D.

cell_xy

cell_yz

cell_xz

obj:, 0.15079297055706548 (forward simulation), [0.15574255] (adjoint solver)
dir-deriv:, 4.698860180868403e-06 (finite difference), [1.34754361e-06] (adjoint solver)

One possibility is to just use a 2D test rather than this 3D test at a larger resolution.

import meep as mp
import meep.adjoint as mpa
import math
import numpy as np
from autograd import numpy as npa
from autograd import tensor_jacobian_product


resolution = 25  # pixels/μm                                                                                      

nSi = 3.45
Si = mp.Medium(index=nSi)
nSiO2 = 1.45
SiO2 = mp.Medium(index=nSiO2)

wvl = 0.5  # wavelength                                                                                           
fcen = 1/wvl

dpml = 1.0  # PML thickness                                                                                       
dsub = 3.0  # substrate thickness                                                                                 
dair = 3.0  # air padding                                                                                         
hcyl = 0.5  # cylinder height                                                                                     
rcyl = 0.2  # cylinder radius                                                                                     

sx = 1.1
sy = 0.8
sz = dpml+dsub+hcyl+dair+dpml

cell_size = mp.Vector3(sx,sy,sz)

design_region_size = mp.Vector3(sx,sy)
design_region_resolution = int(2*resolution)
Nx = int(design_region_size.x*design_region_resolution)
Ny = int(design_region_size.y*design_region_resolution)

# ensure reproducible results                                                                                     
rng = np.random.RandomState(59387385)

xcoord = np.linspace(-0.5*design_region_size.x,0.5*design_region_size.x,Nx)
ycoord = np.linspace(-0.5*design_region_size.y,0.5*design_region_size.y,Ny)
xv, yv = np.meshgrid(xcoord,ycoord)

# cylindrical grating                                                                                             
p = np.sqrt(np.square(xv) + np.square(yv)) < rcyl
p = p.flatten(order='F')

# random epsilon perturbation for design region                                                                   
deps = 1e-5
dp = deps*rng.rand(Nx*Ny)

boundary_layers = [mp.PML(thickness=dpml,direction=mp.Z)]

# periodic boundary conditions                                                                                    
k_point = mp.Vector3()

# plane of incidence: XZ                                                                                          
# polarization: (1) S/TE: Ey or (2) P/TM: Ex                                                                      
src_cmpt = mp.Ex
sources = [mp.Source(src=mp.GaussianSource(fcen,fwidth=0.2*fcen),
                     size=mp.Vector3(sx,sy,0),
                     center=mp.Vector3(0,0,-0.5*sz+dpml),
                     component=src_cmpt)]

tran_pt = mp.Vector3(0,0,0.5*sz-dpml)
stop_cond = mp.stop_when_dft_decayed()

substrate = [mp.Block(size=mp.Vector3(mp.inf,mp.inf,dpml+dsub),
                      center=mp.Vector3(0,0,-0.5*sz+0.5*(dpml+dsub)),
                      material=SiO2)]


def forward_simulation(design_params):
    matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny,0),
                              mp.air,
                              Si,
                              weights=design_params)

    matgrid_geometry = [mp.Block(center=mp.Vector3(0,0,-0.5*sz+dpml+dsub+0.5*hcyl),
                                 size=mp.Vector3(design_region_size.x,design_region_size.y,hcyl),
                                 material=matgrid)]

    geometry = substrate + matgrid_geometry

    sim = mp.Simulation(resolution=resolution,
                        cell_size=cell_size,
                        sources=sources,
                        geometry=geometry,
                        boundary_layers=boundary_layers,
                        k_point=k_point)

    tran_flux = sim.add_mode_monitor(fcen,
                                     0,
                                     1,
                                     mp.ModeRegion(center=tran_pt,
                                                   size=mp.Vector3(sx,sy,0)))

    sim.run(until_after_sources=stop_cond)

    res = sim.get_eigenmode_coefficients(tran_flux,
                                         mp.DiffractedPlanewave([0,0,0],
                                                                mp.Vector3(1,0,0),
                                                                1,
                                                                0))

    t_coeffs = res.alpha
    tran = abs(t_coeffs[0,0,0])**2

    sim.reset_meep()

    return tran

def adjoint_solver(design_params):
    matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny,0),
                              mp.air,
                              Si,
                              weights=np.ones((Nx,Ny)))

    matgrid_region = mpa.DesignRegion(matgrid,
                                      volume=mp.Volume(center=mp.Vector3(0,0,-0.5*sz+dpml+dsub+0.5*hcyl),
                                                       size=mp.Vector3(design_region_size.x,
                                                                       design_region_size.y,
                                                                       hcyl)))

    matgrid_geometry = [mp.Block(center=matgrid_region.center,
                                 size=matgrid_region.size,
                                 material=matgrid)]

    geometry = substrate + matgrid_geometry

    sim = mp.Simulation(resolution=resolution,
                        cell_size=cell_size,
                        sources=sources,
                        geometry=geometry,
                        boundary_layers=boundary_layers,
                        k_point=k_point)

    obj_list = [mpa.EigenmodeCoefficient(sim,
                                         mp.Volume(center=tran_pt,
                                                   size=mp.Vector3(sx,sy,0)),
                                         mp.DiffractedPlanewave([0,0,0],
                                                                mp.Vector3(1,0,0),
                                                                0,
                                                                1))]

    def J(mode_mon):
        return npa.power(npa.abs(mode_mon),2)

    opt = mpa.OptimizationProblem(simulation=sim,
                                  objective_functions=J,
                                  objective_arguments=obj_list,
                                  design_regions=[matgrid_region],
                                  frequencies=[fcen])

    f, dJ_du = opt([design_params])

    sim.reset_meep()

    return f, dJ_du


if __name__ == '__main__':
    flux_unperturbed = forward_simulation(p)
    flux_perturbed = forward_simulation(p+dp)
    fd_grad = flux_perturbed-flux_unperturbed

    adjsol_obj, adjsol_grad = adjoint_solver(p)
    if adjsol_grad.ndim < 2:
        adjsol_grad = np.expand_dims(adjsol_grad,axis=1)
    adj_scale = (dp[None,:]@adjsol_grad).flatten()

    print("obj:, {} (forward simulation), {} (adjoint solver)".format(flux_unperturbed,adjsol_obj))
    print("dir-deriv:, {} (finite difference), {} (adjoint solver)".format(fd_grad,adj_scale))

@stevengj
Copy link
Collaborator

stevengj commented May 5, 2022

discretization errors that tend to be larger in 3D compared to 2D.

I can't think of any intrinsic reason why discretization errors would be larger in 3d than in 2d … except for the fact that it is hard to avoid corner singularities in the fields in 3d, which degrade the discretization accuracy, vs 2d where you don't have these singularities for the Ez polarization.

(A way to check this would be to try a 2d simulation with the Hz polarization, in which case you have the same types of singularities.)

If corner singularities are a problem, one option would be a smooth test structure in 3d.

@smartalecH
Copy link
Collaborator

smartalecH commented May 5, 2022

Note that all the dimensions in the MaterialGrid should be strictly positive (no zero). We use a cartesian product to compute the total number of design variables here:

self.num_params=int(self.grid_size.x*self.grid_size.y*self.grid_size.z)

Actually, looks like we do some checking before hand (thanks @mochen4)

meep/python/geom.py

Lines 598 to 603 in 2f46698

if isclose(self.grid_size.x,0):
self.grid_size.x = 1
if isclose(self.grid_size.y,0):
self.grid_size.y = 1
if isclose(self.grid_size.z,0):
self.grid_size.z = 1

@oskooi
Copy link
Collaborator Author

oskooi commented May 9, 2022

I have been working on a unit test in 2D for this feature which can be available now and does not depend on #2055 being fixed first. The test involves computing the transmittance of a binary grating using an oblique incident planewave which is defined using two different but equivalent methods: as (1) an eigenmode number or (2) DiffractedPlanewave object of the eig_band property of an EigenModeSource. In principle, the transmittance should be identical using these two approaches and this forms the basis of the test.

However, while trying to set up this particular test I discovered that it does not seem to be possible to specify an oblique planewave this way using the DiffractedPlanewave object of the EigenModeSource. This is because this would require using a k_point of mp.Vector3(0,0,0) (i.e., periodic boundaries). In contrast, launching an oblique planewave using the eigenmode number of the EigenModeSource (as demonstrated in this tutorial) involves specifying a non-zero k_point (i.e., Bloch-periodic boundaries) which is the same as the eig_kpoint property of the EigenModeSource. Because of this discrepancy, the two approaches give rise to two different simulations and thus will always produce different results which is what is observed in the test below.

tran:, 370.28116 (eigenmode number), 92.56200 (diffracted planewave)
import unittest
import meep as mp
import math
import cmath
import numpy as np

class TestDiffractedPlanewave(unittest.TestCase):

  @classmethod
  def setUp(cls):
    cls.resolution = 50   # pixels/μm                                                                             

    cls.dpml = 1.0        # PML thickness                                                                         
    cls.dsub = 3.0        # substrate thickness                                                                   
    cls.dpad = 3.0        # length of padding between grating and PML                                             
    cls.gp = 2.6          # grating period                                                                        
    cls.gh = 0.4          # grating height                                                                        
    cls.gdc = 0.5         # grating duty cycle                                                                    

    cls.sx = cls.dpml+cls.dsub+cls.gh+cls.dpad+cls.dpml
    cls.sy = cls.gp

    cls.cell_size = mp.Vector3(cls.sx,cls.sy,0)
    cls.absorber_layers = [mp.PML(thickness=cls.dpml,direction=mp.X)]

    cls.wvl = 0.5             # center wavelength                                                                 
    cls.fcen = 1/cls.wvl      # center frequency                                                                  
    cls.df = 0.05*cls.fcen    # frequency width                                                                   

    cls.ng = 1.5
    cls.glass = mp.Medium(index=cls.ng)

    cls.eig_parity = mp.ODD_Z

    cls.src_pt = mp.Vector3(-0.5*cls.sx+cls.dpml,0,0)
    cls.tran_pt = mp.Vector3(0.5*cls.sx-cls.dpml,0,0)

    cls.geometry = [mp.Block(material=cls.glass,
                             size=mp.Vector3(cls.dpml+cls.dsub,mp.inf,mp.inf),
                             center=mp.Vector3(-0.5*cls.sx+0.5*(cls.dpml+cls.dsub),0,0)),
                    mp.Block(material=cls.glass,
                             size=mp.Vector3(cls.gh,cls.gdc*cls.gp,mp.inf),
                             center=mp.Vector3(-0.5*cls.sx+cls.dpml+cls.dsub+0.5*cls.gh,0,0))]


  def run_eigenmode_source(self,m,dp):
    ky = m/self.gp
    theta = math.asin(ky/(self.fcen*self.ng))

    # k (in source medium) with correct length (plane of incidence: XY)                                           
    k = mp.Vector3(self.fcen*self.ng).rotate(mp.Vector3(z=1), theta)
    print("mode:, {} (m), {:.4f} (theta), ({:.4f},{:.4f},{:.4f}) (k)".format(m,math.degrees(theta),k.x,k.y,k.z))

    symmetries = []
    if theta == 0:
      k = mp.Vector3()
      self.eig_parity += mp.EVEN_Y
      symmetries = [mp.Mirror(direction=mp.Y)]

    if dp:
      sources = [mp.EigenModeSource(mp.GaussianSource(self.fcen,fwidth=self.df),
                                    center=self.src_pt,
                                    size=mp.Vector3(0,self.sy,0),
                                    eig_band=mp.DiffractedPlanewave((0,m,0),
                                                                    mp.Vector3(0,1,0),
                                                                    1,
                                                                    0))]
    else:
      sources = [mp.EigenModeSource(mp.GaussianSource(self.fcen,fwidth=self.df),
                                    center=self.src_pt,
                                    size=mp.Vector3(0,self.sy,0),
                                    direction=mp.AUTOMATIC if theta==0 else mp.NO_DIRECTION,
                                    eig_kpoint=k,
                                    eig_band=1,
                                    eig_parity=self.eig_parity)]

    sim = mp.Simulation(resolution=self.resolution,
                        cell_size=self.cell_size,
                        boundary_layers=self.absorber_layers,
                        k_point=mp.Vector3() if dp else k,
                        geometry=self.geometry,
                        sources=sources,
                        symmetries=symmetries)

    tran_flux = sim.add_flux(self.fcen,
                             0,
                             1,
                             mp.FluxRegion(center=self.tran_pt,
                                           size=mp.Vector3(0,self.sy,0)))

    sim.run(until_after_sources=mp.stop_when_dft_decayed())

    tran = mp.get_fluxes(tran_flux)[0]

    sim.reset_meep()

    return tran


  def test_diffracted_planewave(self):
    """Unit test for launching an oblique planewave using an `EigenModeSource`.                                   
                                                                                                                  
    Verifies that the transmittance of a binary grating is identical using two                                    
    different methods to launch a planewave at oblique incidence.                                                 
    """
    m = 1
    tran_md = self.run_eigenmode_source(m,False)
    tran_dp = self.run_eigenmode_source(m,True)

    print("tran:, {:.5f} (eigenmode number), {:.5f} (diffracted planewave)".format(tran_md,tran_dp))

    self.assertAlmostEqual(tran_md,tran_dp)


if __name__ == '__main__':
  unittest.main()

@oskooi
Copy link
Collaborator Author

oskooi commented May 10, 2022

It seems all that is required to enable this feature is to simply reverse (when necessary) the direction of the wavevector of the DiffractedPlanewave object (its g property) in the place_adjoint_source and __call__ functions of the EigenmodeCoefficient class. No other changes to the adjoint solver are necessary.

Actually, this statement is incorrect. Simply flipping the sign of the diffraction order g of the DiffractedPlanewave will not generate a planewave propagating in the opposite direction. Rather, as demonstrated in the example below for a planewave in homogeneous media, only the component of the wavevector which is in the periodic direction of the cell changes sign (e.g., a planewave propagating in the NE direction changes to SE rather than SW).

To properly reverse the direction of the planewave (in order to enable the adjoint source), we also need to flip the sign of the component of the wavevector in the non-periodic direction of the cell. This requires some changes to fields::add_eigenmode_source of src/mpb.cpp.

diffpw_m7_ang27 82_plus

diffpw_m7_ang27 82_minus

import meep as mp
import math
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt

resolution = 20 # pixels/μm

cell_size = mp.Vector3(14,10,0)

pml_layers = [mp.PML(thickness=2,direction=mp.X)]

fsrc = 1.0 # frequency of planewave (wavelength = 1/fsrc)

n = 1.5 # refractive index of homogeneous material
default_material = mp.Medium(index=n)

m = -7
ky = m/cell_size.y
theta = math.asin(ky/(fsrc*n))
k_point = mp.Vector3(fsrc*n).rotate(mp.Vector3(z=1), theta)

print("stats:, {}, {:.5f}, k=({:.2f},{:.2f})".format(m,math.degrees(theta),k_point.x,k_point.y))

sources = [mp.EigenModeSource(src=mp.ContinuousSource(fsrc),
                              center=mp.Vector3(),
                              size=mp.Vector3(y=10),
                              eig_band=mp.DiffractedPlanewave((0,m,0),
                                                              mp.Vector3(0,1,0),
                                                              1,
                                                              0),
                              eig_match_freq=True)]

sim = mp.Simulation(cell_size=cell_size,
                    resolution=resolution,
                    boundary_layers=pml_layers,
                    sources=sources,
                    k_point=mp.Vector3(),
                    default_material=default_material,
                    symmetries=[mp.Mirror(mp.Y)] if theta==0 else [])

sim.run(until=100)

nonpml_vol = mp.Volume(center=mp.Vector3(), size=mp.Vector3(10,10,0))

sim.plot2D(fields=mp.Ez,
           plot_boundaries_flag=False,
           output_plane=nonpml_vol)

if mp.am_master():
    plt.title('m = {}, angle = {:.2f}°'.format(m,math.degrees(theta)))
    plt.savefig('diffpw_m{}_ang{:.2f}_{}.png'.format(abs(m),
                                                     math.degrees(abs(theta)),
                                                     'minus' if m < 0 else 'plus'),
		bbox_inches='tight')

@oskooi
Copy link
Collaborator Author

oskooi commented May 11, 2022

I think I may have a workaround to needing to use a DiffractedPlanewave object in order to compute the mode coefficient of a diffracted planewave which uses the existing mode number feature for the bands/eig_band property of get_eigenmode_coefficients/EigenModeSource without any modification. All that is necessary is to specify the eig_parity and kpoint_func for the diffracted planewave when defining the get_eigenmode_coefficient or the EigenModeCoefficient object of the adjoint solver.

This is demonstrated in the following example which involves computing the flux into the m=+1 order of a 2D grating with a random design region (black rectangular region in the schematic below). The test involves the usual check of verifying that the gradient of the objective function computed by the adjoint solver matches the brute-force result via finite difference using the directional derivative.

cell_xz

The key is computing the wavevector of the diffracted order manually and then passing this value as the kpoint_func parameter along with the mode's eig_parity. In this example, the plane of incidence is xz and so the S and P polarizations correspond to mp.ODD_Y and mp.EVEN_Y, respectively.

m = 1  # diffraction order in x direction        

# wavevector of diffraction order in xz plane         
kdiff = mp.Vector3(m/sx,0,(fcen**2-(m/sx)**2)**0.5)

As a test, the forward simulation computes the flux in the diffraction order using a DiffractedPlanewave object and the objective function of the adjoint solver is defined using a mode number (1, in this case).

1. forward simulation (DiffractedPlanewave)

    res = sim.get_eigenmode_coefficients(tran_flux,
                                         mp.DiffractedPlanewave([m,0,0],
                                                                mp.Vector3(1,0,0),
                                                                0,
                                                                1))

2. adjoint solver (mode number)

    obj_list = [mpa.EigenmodeCoefficient(sim,
                                         mp.Volume(center=tran_pt,
                                                   size=mp.Vector3(sx,sy,0)),
                                         1,
                                         eig_parity=mp.EVEN_Y,
                                         kpoint_func=lambda *not_used: kdiff)]

The results demonstrate that these two different approaches produce equivalent results for the objective function (of the unperturbed structure) and the gradient/directional derivative.

obj:, 0.06565651493561703 (forward sim.), [0.06570647] (adjoint solver))
dir-deriv:, -1.9957059570474556e-06 (finite difference), [-2.00424896e-06] (adjoint solver)
import meep as mp
import meep.adjoint as mpa
import math
import numpy as np
from autograd import numpy as npa
from autograd import tensor_jacobian_product


resolution = 50  # pixels/μm                                                                                                                                 

nSi = 3.45
Si = mp.Medium(index=nSi)
nSiO2 = 1.45
SiO2 = mp.Medium(index=nSiO2)

theta_d = math.radians(50.0)  # deflection angle                                                                                                             

wvl = 1.05  # wavelength                                                                                                                                     
fcen = 1/wvl

sx = wvl/math.sin(theta_d)  # period in x                                                                                                                    
sy = 0.5*wvl  # period in y                                                                                                                                  

m = 1  # diffraction order in x direction                                                                                                                    

# wavevector of diffraction order in xz plane                                                                                                                
kdiff = mp.Vector3(m/sx,0,(fcen**2-(m/sx)**2)**0.5)

dpml = 1.0  # PML thickness                                                                                                                                  
dsub = 4.0  # substrate thickness                                                                                                                            
dair = 4.0  # air padding                                                                                                                                    
hblk = 0.5  # block height                                                                                                                                   
xblk = 0.9  # block width in x                                                                                                                               
yblk = 0.3  # block width in y                                                                                                                               

sz = dpml+dsub+hblk+dair+dpml

cell_size = mp.Vector3(sx,sy,sz)

design_region_size = mp.Vector3(xblk,yblk,0)
design_region_resolution = int(2*resolution)
Nx = int(design_region_size.x*design_region_resolution)
Ny = int(design_region_size.y*design_region_resolution)

def mapping(w,filter_radius):
    filtered_field = mpa.conic_filter(w.reshape((Nx,Ny)),
                                      filter_radius,
                                      design_region_size.x,
                                      design_region_size.y,
                                      design_region_resolution)
    return filtered_field.flatten()

# ensure reproducible results                                                                                                                                
rng = np.random.RandomState(59387385)

# random design region                                                                                                                                       
p = 0.5*rng.rand(Nx*Ny)

fr = 0.1095834  # filter radius                                                                                                                              
mapped_p = mapping(p,fr)

# random epsilon perturbation for design region                                                                                                              
deps = 1e-5
dp = deps*rng.rand(Nx*Ny)

boundary_layers = [mp.PML(thickness=dpml,direction=mp.Z)]

# periodic boundary conditions                                                                                                                               
k_point = mp.Vector3()

# plane of incidence: XZ                                                                                                                                     
# polarization: (1) S/TE: Ey or (2) P/TM: Ex                                                                                                                 
src_cmpt = mp.Ex
sources = [mp.Source(src=mp.GaussianSource(fcen,fwidth=0.2*fcen),
                     size=mp.Vector3(sx,sy,0),
                     center=mp.Vector3(0,0,-0.5*sz+dpml),
                     component=src_cmpt)]

tran_pt = mp.Vector3(0,0,0.5*sz-dpml)

stop_cond = mp.stop_when_dft_decayed()

substrate = [mp.Block(size=mp.Vector3(mp.inf,mp.inf,dpml+dsub),
                      center=mp.Vector3(0,0,-0.5*sz+0.5*(dpml+dsub)),
                      material=SiO2)]


def forward_simulation(design_params):
    matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny),
                              mp.air,
                              Si,
                              weights=design_params.reshape(Nx,Ny))

    matgrid_geometry = [mp.Block(center=mp.Vector3(0,0,-0.5*sz+dpml+dsub+0.5*hblk),
                                 size=mp.Vector3(design_region_size.x,
                                                 design_region_size.y,
                                                 hblk),
                                 material=matgrid)]

    geometry = substrate + matgrid_geometry

    sim = mp.Simulation(resolution=resolution,
                        cell_size=cell_size,
                        sources=sources,
                        geometry=geometry,
                        boundary_layers=boundary_layers,
                        k_point=k_point)

    tran_flux = sim.add_mode_monitor(fcen,
                                     0,
                                     1,
                                     mp.ModeRegion(center=tran_pt,
                                                   size=mp.Vector3(sx,sy,0)))

    sim.run(until_after_sources=stop_cond)

    res = sim.get_eigenmode_coefficients(tran_flux,
                                         mp.DiffractedPlanewave([m,0,0],
                                                                mp.Vector3(1,0,0),
                                                                0,
                                                                1))

    t_coeffs = res.alpha
    tran = abs(t_coeffs[0,0,0])**2

    sim.reset_meep()

    return tran

def adjoint_solver(design_params):
    matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny,0),
                              mp.air,
                              Si,
                              weights=np.ones((Nx,Ny)))

    matgrid_region = mpa.DesignRegion(matgrid,
                                      volume=mp.Volume(center=mp.Vector3(0,0,-0.5*sz+dpml+dsub+0.5*hblk),
                                                       size=mp.Vector3(design_region_size.x,
                                                                       design_region_size.y,
                                                                       hblk)))

    matgrid_geometry = [mp.Block(center=matgrid_region.center,
                                 size=matgrid_region.size,
                                 material=matgrid)]

    geometry = substrate + matgrid_geometry

    sim = mp.Simulation(resolution=resolution,
                        cell_size=cell_size,
                        sources=sources,
                        geometry=geometry,
                        boundary_layers=boundary_layers,
                        k_point=k_point)

    obj_list = [mpa.EigenmodeCoefficient(sim,
                                         mp.Volume(center=tran_pt,
                                                   size=mp.Vector3(sx,sy,0)),
                                         1,
                                         eig_parity=mp.EVEN_Y,
                                         kpoint_func=lambda *not_used: kdiff)]

    def J(mode_mon):
        return npa.power(npa.abs(mode_mon),2)

    opt = mpa.OptimizationProblem(simulation=sim,
                                  objective_functions=J,
                                  objective_arguments=obj_list,
                                  design_regions=[matgrid_region],
                                  frequencies=[fcen])

    f, dJ_du = opt([design_params])

    sim.reset_meep()

    return f, dJ_du


flux_unperturbed = forward_simulation(mapped_p)
flux_perturbed = forward_simulation(mapping(p+dp,fr))
fd_grad = flux_perturbed-flux_unperturbed
print("flux:, {}, {}".format(flux_unperturbed,flux_perturbed))

adjsol_obj, adjsol_grad = adjoint_solver(mapped_p)
bp_adjsol_grad = tensor_jacobian_product(mapping,0)(p,fr,adjsol_grad)
if bp_adjsol_grad.ndim < 2:
    bp_adjsol_grad = np.expand_dims(bp_adjsol_grad,axis=1)
adj_scale = (dp[None,:]@bp_adjsol_grad).flatten()

print("obj:, {} (forward sim.), {} (adjoint solver))".format(flux_unperturbed,adjsol_obj))
print("dir-deriv:, {} (finite difference), {} (adjoint solver)".format(fd_grad,adj_scale))

@stevengj
Copy link
Collaborator

Note that I think the corresponding DiffractedPlanewave adjoint source should (a) flip the simulation k_point (which we do anyway in adjoint simulations), (b) flip the g index, and (c) flip the axis.

@oskooi
Copy link
Collaborator Author

oskooi commented May 14, 2022

Note that I think the corresponding DiffractedPlanewave adjoint source should (a) flip the simulation k_point (which we do anyway in adjoint simulations), (b) flip the g index, and (c) flip the axis.

Looks like it is not currently possible to generate a backward-propagating planewave using this approach. To demonstrate, consider this simple example which involves launching an oblique planewave in a homogeneous medium (adapted from this tutorial):

import meep as mp
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt


resolution = 20  # pixels/μm                                                                                                                                  

cell_size = mp.Vector3(14,10,0)

pml_layers = [mp.PML(thickness=2,direction=mp.X)]

# rotation angle (in degrees) of planewave                                                                                                                    
# counter clockwise (CCW) around Z axis, 0° is +X                                                                                                             
rot_angle = np.radians(22.3)

fsrc = 1.0  # frequency (wavelength = 1/fsrc)                                                                                                                 

n = 1.5  # refractive index of homogeneous material                                                                                                           
default_material = mp.Medium(index=n)

k_point = -1 * mp.Vector3(fsrc*n).rotate(mp.Vector3(z=1), rot_angle)

sources = [mp.EigenModeSource(src=mp.ContinuousSource(fsrc),
                              center=mp.Vector3(),
                              size=mp.Vector3(y=10),
                              eig_band=mp.DiffractedPlanewave((0,0,0),
                                                              mp.Vector3(0,-1,0),
                                                              1,
                                                              0))]

sim = mp.Simulation(cell_size=cell_size,
                    resolution=resolution,
                    boundary_layers=pml_layers,
                    sources=sources,
                    k_point=k_point,
                    default_material=default_material,
                    symmetries=[mp.Mirror(mp.Y)] if rot_angle==0 else [])

sim.run(until=20)

nonpml_vol = mp.Volume(center=mp.Vector3(),
                       size=mp.Vector3(10,10,0))

sim.plot2D(fields=mp.Ez,
           plot_boundaries_flag=False,
           output_plane=nonpml_vol)

if mp.am_master():
    plt.savefig('pw.png',bbox_inches='tight')

Note that, as suggested in #2069 (comment), we are using the zeroth diffraction ((0,0,0)) order combined with a negative axis (mp.Vector3(0,-1,0)) as part of the definition of the DiffractedPlanewave object of the EigenModeSource. The k_point of the Simulation object is negative. Based on this configuration, the planewave should be propagating in the SW direction to the left of the line source at an angle of 22.3° counter-clockwise from the -x axis. However, the field snapshot shows the planewave propagating to the right of the line source at 22.3° clockwise from the +x axis. This is incorrect.

incorrect
pw_positive_knorm

The reason this is not working is because the k passed to MPB's maxwell_set_planewave does not contain components which are all non positive. The problem: the component of k normal to the line source is positive. To obtain the correct result, we can hack the source code to manually negate the component of the wavevector normal to the line source:

meep/src/mpb.cpp

Lines 651 to 653 in 5275f43

else if (k2 > 0)
k[dd - X] = sqrt(k2);
}

change to:

        else if (k2 > 0)
          k[dd - X] = -1 * sqrt(k2);
      }

This simple modification produces the correct result.

correct
pw_negative_knorm

To fix this bug, the question though is do we want to modify (1) mpb.cpp in Meep or (2) MPB's maxwell_set_planewave? Seems the latter would be more appropriate.

@smartalecH
Copy link
Collaborator

@oskooi did we decide on a solution here? Seems pretty straightforward to implement

@smartalecH
Copy link
Collaborator

Alternatively it might be just as easy to create a PlanewaveSource from the analytic formula of a planewave. No need to depend on MPB for something this simple.

@smartalecH
Copy link
Collaborator

In fact, for the adjoint solver, I think it's cleaner to separate diffraction orders from eigenmode coefficients. ie have an entirely separate ObjectiveQuantity subclass. Then we can directly drive a planewave for the adjoint source.

@oskooi
Copy link
Collaborator Author

oskooi commented Jan 22, 2023

did we decide on a solution here? Seems pretty straightforward to implement

Unfortunately, work on this feature is currently blocked by a bug related to oblique modes and get_eigenmode_coefficients described in #2226 (comment).

@oskooi oskooi marked this pull request as ready for review September 15, 2023 05:51
@codecov-commenter
Copy link

Codecov Report

Merging #2054 (4fc6d08) into master (b065eae) will decrease coverage by 0.10%.
The diff coverage is 60.00%.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

@@            Coverage Diff             @@
##           master    #2054      +/-   ##
==========================================
- Coverage   73.81%   73.72%   -0.10%     
==========================================
  Files          18       18              
  Lines        5294     5305      +11     
==========================================
+ Hits         3908     3911       +3     
- Misses       1386     1394       +8     
Files Changed Coverage Δ
python/adjoint/objective.py 89.14% <60.00%> (-3.25%) ⬇️

@oskooi
Copy link
Collaborator Author

oskooi commented Jan 2, 2024

Hi @smartalecH. I had a chance to revisit this issue recently. I have a workaround which does not use DiffractedPlanewave (#2226 is still unresolved) but does require the one-line change to mpb.cpp in #2285 for computing the adjoint gradients of the diffraction efficiencies of 1D gratings. I validated the adjoint result using the brute-force finite difference.

The key to making this work is to specify the meep.adjoint.EigenmodeCoefficient objective function for a given diffracted order using the parameters kpoint_func and eig_parity:

    order_y = 1
    kdiff = mp.Vector3(
        (frequency_center**2 - (order_y / GRATING_PERIOD_UM)**2)**0.5,
        order_y / GRATING_PERIOD_UM,
        0
    )

    eig_parity = mp.ODD_Z if S_pol else mp.EVEN_Z

    obj_list = [
        mpa.EigenmodeCoefficient(
            sim,
            mp.Volume(
                center=tran_pt,
                size=mp.Vector3(0, size_y_um, 0),
            ),
            mode=1,
            kpoint_func=lambda *not_used: kdiff,
            eig_parity=eig_parity,
        ),
    ]

The full script is below and demonstrates using a binary grating with a random design region that the directional derivative computed using the adjoint gradient and finite difference agree for S and P polarizations:

1. S polarization

grad:, 8.750385165678942e-06 (finite difference), 8.750406206638e-06 (adjoint)

2. P polarization

grad:, 1.7184546485249363e-05 (finite difference), 1.7189598795606938e-05 (adjoint)

Important: The problem is that this approach does not work for a 2D grating (will post additional results shortly). The broader issue, I think, seems to be related to inaccurate adjoint gradients in 3D calculations in general. See the discussions @mawc2019 and I had putting together the scripts for https://github.com/NanoComp/photonics-opt-testbed/tree/main/Metagrating3D. I am working on 3D tests for #2661 (to be posted shortly as well) which will provide some examples.

binary_grating_1d_plot2D

"""Validates the adjoint gradient of the diffraction efficiency of a 1D grating.
                                                                                                                  
The 2D test involves computing the gradient of the diffraction efficiency of
the first transmitted order in air of a 1D binary grating on a substrate given
a normally incident planewave from the substrate. The adjoint gradient is
validated using the brute-force finite difference via the directional
derivative.
"""

from typing import Tuple

from autograd import numpy as npa
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
import meep as mp
import meep.adjoint as mpa
import numpy as np


RESOLUTION = 100  # pixels/μm

GRATING_PERIOD_UM = 2.5
GRATING_HEIGHT_UM = 0.5
GRATING_DUTY_CYCLE = 0.5

DESIGN_REGION_SIZE = mp.Vector3(
    GRATING_HEIGHT_UM, GRATING_DUTY_CYCLE * GRATING_PERIOD_UM, 0
)
DESIGN_REGION_RESOLUTION = int(2 * RESOLUTION)
NX = int(DESIGN_REGION_SIZE.x * DESIGN_REGION_RESOLUTION) + 1
NY = int(DESIGN_REGION_SIZE.y * DESIGN_REGION_RESOLUTION) + 1


def binary_grating_1d(S_pol: bool = True) -> mpa.OptimizationProblem:
    """Sets up the adjoint problem for a 1D binary grating."""

    pml_um = 1.0
    substrate_um = 3.0
    padding_um = 3.0

    wavelength_um = 0.5
    frequency_center = 1 / wavelength_um

    pml_layers = [mp.PML(direction=mp.X, thickness=pml_um)]

    n_glass = 1.5
    glass = mp.Medium(index=n_glass)

    size_x_um = pml_um + substrate_um + GRATING_HEIGHT_UM + padding_um + pml_um
    size_y_um = GRATING_PERIOD_UM
    cell_size = mp.Vector3(size_x_um, size_y_um, 0)

    k_point = mp.Vector3()

    eig_parity = mp.ODD_Z if S_pol else mp.EVEN_Z

    src_pt = mp.Vector3(-0.5 * size_x_um + pml_um, 0, 0)
    sources = [
        mp.Source(
            mp.GaussianSource(frequency_center, fwidth=0.1 * frequency_center),
            component=mp.Ez if S_pol else mp.Hz,
            center=src_pt,
            size=mp.Vector3(0, size_y_um, 0),
        )
    ]

    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 * size_x_um + pml_um + substrate_um +
                 0.5 * GRATING_HEIGHT_UM),
                0,
                0
            ),
            size=mp.Vector3(
                DESIGN_REGION_SIZE.x,
                DESIGN_REGION_SIZE.y,
                0
            ),
        ),
    )

    geometry = [
        mp.Block(
            material=glass,
            size=mp.Vector3(pml_um + substrate_um, mp.inf, mp.inf),
            center=mp.Vector3(
                -0.5 * size_x_um + 0.5 * (pml_um + substrate_um), 0, 0
            ),
        ),
        mp.Block(
            material=matgrid,
            size=matgrid_region.size,
            center=matgrid_region.center,
        ),
    ]

    sim = mp.Simulation(
        resolution=RESOLUTION,
        cell_size=cell_size,
        boundary_layers=pml_layers,
        k_point=k_point,
        sources=sources,
        geometry=geometry,
    )

    tran_pt = mp.Vector3(0.5 * size_x_um - pml_um, 0, 0)

    order_y = 1
    kdiff = mp.Vector3(
        (frequency_center**2 - (order_y / GRATING_PERIOD_UM)**2)**0.5,
        order_y / GRATING_PERIOD_UM,
        0
    )

    obj_list = [
        mpa.EigenmodeCoefficient(
            sim,
            mp.Volume(
                center=tran_pt,
                size=mp.Vector3(0, size_y_um, 0),
            ),
            mode=1,
            kpoint_func=lambda *not_used: kdiff,
            eig_parity=eig_parity,
        ),
    ]

    def J(tran_mon):
        return npa.abs(tran_mon)**2

    opt = mpa.OptimizationProblem(
        simulation=sim,
        objective_functions=J,
        objective_arguments=obj_list,
        design_regions=[matgrid_region],
        frequencies=[frequency_center],
    )

    return opt

if __name__ == "__main__":
    opt = binary_grating_1d(False)

    # ensure reproducible results
    rng = np.random.RandomState(9861548)

    # random design region
    p = 0.9 * rng.rand(NX * NY)

    # constant design region
    # p = 0.9 * np.ones((NX * NY))

    # random perturbation for design region                                                                       
    deps = 1e-5
    dp = deps * rng.rand(NX * NY)

    f0, dJ_du = opt([p], need_gradient=True)

    fig, ax = plt.subplots()
    opt.plot2D(init_opt=False, ax=ax)
    fig.savefig(
        'binary_grating_1d_plot2D.png', dpi=150, bbox_inches='tight'
    )

    f1, _ = opt([p + dp], need_gradient=False)

    if dJ_du.ndim < 2:
        dJ_du = np.expand_dims(dJ_du, axis=1)

    adj_directional_deriv = (dp[None, :] @ dJ_du).flatten()
    fnd_directional_deriv = f1[0] - f0[0]

    print(f"grad:, {fnd_directional_deriv} (finite difference), {adj_directional_deriv[0]} (adjoint)")

@smartalecH
Copy link
Collaborator

The broader issue, I think, seems to be related to inaccurate adjoint gradients in 3D

Have you tried checking the gradient for a design region that has 1D degrees of freedom, but in a 3D simulation cell? Just to further narrow down the potential bug.

Also, did you turn off smoothing in the material grid?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for DiffractedPlanewave to EigenmodeCoefficient objective function of adjoint solver
4 participants