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

eigenmodes of diffracted planewaves #1316

Merged
merged 15 commits into from
Aug 26, 2020

Conversation

oskooi
Copy link
Collaborator

@oskooi oskooi commented Aug 14, 2020

Follows the outline described by @stevengj in #291:comment.

Documentation, tutorial, and test will likely be added in a separate PR once the API is working.

class DiffractedPlanewave(object):
def __init__(self,
g=None,
axis=None,
Copy link
Collaborator

@stevengj stevengj Aug 14, 2020

Choose a reason for hiding this comment

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

Eventually, we'll have the EigenmodeSource etcetera choose a reasonable default if axis=None when the source is added.

(You'll generally want axis to lie in the plane of the source, I think. In 2d you'll also want it to be in the xy plane, i.e. it should be along the source line.)

@@ -98,6 +98,32 @@ def py_v3_to_vec(dims, iterable, is_cylindrical=False):
else:
raise ValueError("Invalid dimensions in Volume: {}".format(dims))

class DiffractedPlanewave(object):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note: the current plan is to optionally pass this for the band_num parameter to eigenmode sources etcetera.

@oskooi oskooi force-pushed the diffracted_planewaves branch from da9db99 to 332c75d Compare August 17, 2020 00:14
@oskooi
Copy link
Collaborator Author

oskooi commented Aug 17, 2020

The basic framework now seems to be working although there are several issues that need to be resolved.

As an initial demonstration/test, the mode coefficients of the diffracted orders of the 2d binary phase grating (from Tutorial/Mode Decomposition/Transmittance Spectra for Planewave at Normal Incidence) computed using a DiffractedPlanewave object passed as the bands parameter to get_eigenmode_coefficients are equivalent to those from the tutorial as demonstrated below.

import meep as mp
import math
import cmath
import numpy as np

resolution = 50        # pixels/μm      

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

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

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

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

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

# rotation angle of incident planewave; counter clockwise (CCW) about Z axis, 0 degrees along +X axis
theta_in = math.radians(0)

# k (in source medium) with correct length (plane of incidence: XY) 
k = mp.Vector3(fcen*ng).rotate(mp.Vector3(z=1), theta_in)

symmetries = []
eig_parity = mp.ODD_Z
if theta_in == 0:
  k = mp.Vector3(0,0,0)
  symmetries = [mp.Mirror(mp.Y)]
  eig_parity += mp.EVEN_Y

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.3*dsub,0,0)
sources = [mp.Source(mp.GaussianSource(fcen,fwidth=df),
                     component=mp.Ez,
                     center=src_pt,
                     size=mp.Vector3(0,sy,0),
                     amp_func=pw_amp(k,src_pt))]

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    k_point=k,
                    default_material=glass,
                    sources=sources,
                    symmetries=symmetries)

tran_pt = mp.Vector3(0.5*sx-dpml-0.5*dpad,0,0)
tran_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=tran_pt, size=mp.Vector3(0,sy,0)))

sim.run(until_after_sources=50)

input_flux = mp.get_fluxes(tran_flux)

sim.reset_meep()

geometry = [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)),
            mp.Block(material=glass,
                     size=mp.Vector3(gh,gdc*gp,mp.inf),
                     center=mp.Vector3(-0.5*sx+dpml+dsub+0.5*gh,0,0))]

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

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

sim.run(until_after_sources=200)

## when using "bands" as a list:         
##      bands=[1] is 0th diffraction order, bands[2] is 1st diffraction order, ...   
## when using "bands" as a DiffractedPlanewave: 
##      (0,0,0) is 0th diffraction order, (0,1,0) is 1st diffraction order, ...
diff_order = 1

res = sim.get_eigenmode_coefficients(tran_flux, [diff_order+1], eig_parity=eig_parity)
t_ref = res.alpha

res = sim.get_eigenmode_coefficients(tran_flux, mp.DiffractedPlanewave((0,diff_order,0),mp.Vector3(1,1,0),1,0))
t_dp = res.alpha

print("tran:, {}, {:.8f} (eigensolver), {:.8f} (planewave)".format(diff_order,
                                                                   0.5*abs(t_ref[0,0,0])**2/input_flux[0],
                                                                   abs(t_dp[0,0,0])**2/input_flux[0]))

first diffraction order (diff_order=1)

tran:, 1, 0.38779358 (eigensolver), 0.38779517 (planewave)

third diffraction order (diff_order=3)

tran:, 3, 0.04276357 (eigensolver), 0.04276669 (planewave)

fifth diffraction order (diff_order=5)

tran:, 5, 0.01513773 (eigensolver), 0.01514239 (planewave)

Issues that need to be fixed:

  • passing eig_parity to get_eigenmode_coefficients when using DiffractedPlanewave and replacing sim.add_mode_monitor with sim.add_flux (in order to exploit symmetries) produces incorrect results. Is there something missing when calling maxwell_set_planewave when symmetries are present?
  • the dominant wavevector obtained from calling maxwell_dominant_planewave at the end of the get_eigenmode function call is incorrect when using a DiffractedPlanewave object because the band_num input parameter is always 1. How should the call to maxwell_dominant_planewave be modified when using DiffractedPlanewave? Should the band_num parameter be set to the maximum absolute value of one of the three elements of its g member object?
  • the serial build on Python 2.7 is failing for reasons that are not clear
  • additional testing is required using a 3d example

@oskooi
Copy link
Collaborator Author

oskooi commented Aug 19, 2020

The following is a 3d test based on extending the 2d binary-grating tutorial example. The test involves computing the mode coefficients of all transmitted diffraction orders based on individual calls to get_eigenmode_coefficients using DiffractedPlanewave and verifying that the sum of the power in all the orders are equivalent to the Poynting flux value obtained using get_fluxes. (This is similar to python/tests/binary_grating.py.)

# binary grating on a semi-infinite glass substrate                                                                                                                  
# structure is 3d extrusion of tutorial example                                                                                                                      

import math
import cmath
import numpy as np
import meep as mp

resolution = 50        # pixels/μm                                                                                                                                   

dpml = 1.0             # PML thickness                                                                                                                               
dsub = 3.0             # substrate length                                                                                                                            
dpad = 3.0             # padding length between grating and pml                                                                                                      
gp = 2.0               # grating period                                                                                                                              
gh = 0.5               # grating height                                                                                                                              
gdc = 0.5              # grating duty cycle                                                                                                                          

sx = dpml+dsub+gh+dpad+dpml
sy = gp
sz = gp

cell_size = mp.Vector3(sx,sy,sz)
pml_layers = [mp.PML(thickness=dpml,direction=mp.X)]

wvl = 0.5              # wavelength                                                                                                                                  
fcen = 1/wvl           # frequency                                                                                                                                   

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

# angle of incident planewave; CCW about Z axis, 0 degrees along +X axis                                                                                             
theta_in = math.radians(0)

# k with correct length (plane of incidence: XY)                                                                                                                     
k = mp.Vector3(math.cos(theta_in),math.sin(theta_in)).scale(fcen)

## disabled: causes field instability                                                                                                                                
# symmetries = [mp.Mirror(mp.Z, phase=-1)]                                                                                                                           
symmetries = []
if theta_in == 0:
  k = mp.Vector3()
  symmetries.append(mp.Mirror(mp.Y))

def pw_amp(k,x0):
  def _pw_amp(x):
    return cmath.exp(1j*2*math.pi*ng*k.dot(x+x0))
  return _pw_amp

src_pt = mp.Vector3(-0.5*sx+dpml+0.5*dsub)
sources = [mp.Source(mp.GaussianSource(fcen, fwidth=0.2*fcen),
                     component=mp.Ez,
                     center=src_pt,
                     size=mp.Vector3(0,sy,sz),
                     amp_func=pw_amp(k,src_pt))]

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    k_point=k,
                    default_material=glass,
                    sources=sources,
                    symmetries=symmetries)

flux_mon = sim.add_flux(fcen, 0, 1,
                        mp.FluxRegion(center=mp.Vector3(0.5*sx-dpml-0.5*dpad), size=mp.Vector3(0,sy,sz)))

sim.run(until_after_sources=10)

input_flux = mp.get_fluxes(flux_mon)

sim.reset_meep()

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

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

mode_mon = sim.add_flux(fcen, 0, 1,
                        mp.FluxRegion(center=mp.Vector3(0.5*sx-dpml-0.5*dpad), size=mp.Vector3(0,sy,sz)))

sim.run(until_after_sources=100)

m_flux = 0
for ny in range(-4,5):
  for nz in range(-4,5):
    res = sim.get_eigenmode_coefficients(mode_mon,
                                         mp.DiffractedPlanewave((0,ny,nz),mp.Vector3(1.2,1.5,0),1,0),
                                         eig_parity=mp.EVEN_Y+mp.ODD_Z)
    tran = abs(res.alpha[0,0,0])**2/input_flux[0]
    print("tran:, {}, {}, {}".format(ny,nz,tran))
    m_flux += tran

flux = mp.get_fluxes(mode_mon)
t_flux = t_flux[0]/input_flux[0]

err = abs(m_flux_t_flux)/t_flux

print("flux:, {} (mode coeffs.), {} (flux), {} (error)".format(0.5*m_flux,t_flux,err))

The results show that the relative error is converging to zero with increasing resolution.

resolution = 30

flux:, 0.843164820063639 (mode coeffs.), 0.9033501462240456 (flux), 0.06662458229732734 (error)

resolution = 40

flux:, 0.871396520203924 (mode coeffs.), 0.9137573158711103 (flux), 0.04635891273472605 (error)

@RobinD42
Copy link
Contributor

@oskooi, If I'm reading the diffs and the current code correctly, it looks like you didn't add the new diffractedplanewave *dp parameter to the version of the fields::get_ methods that are used when the build is done without MPB. Look inside the #else // #ifdef HAVE_MPB branch.

@stevengj
Copy link
Collaborator

Diffracted planewaves are not mirror symmetric. As explained in the documentation, you have to use add_mode_monitor when computing mode coefficients that don't match the Symmetry of the simulation — you can only use add_flux to compute coupling to modes of the same symmetry.

@oskooi
Copy link
Collaborator Author

oskooi commented Aug 19, 2020

Output for the third diffraction order:

Dominant planewave for band 4: (1.977372,-0.300000,0.000000)
eigenmode group velocity: 0.98869
diffracted planewave: k = 2, 0, 0 for omega = 2
diffracted planewave: group velocity v=1.41421
Dominant planewave for band 1: (2.000000,0.300000,0.000000)
tran:, 3, 0.04292399 (eigensolver), 0.04292726 (planewave). 0.00007630 (error)

Note that the kx can be computed analytically (which agrees with the output for get_eigenmode_coefficients in the first of the two runs)

>>> (2**2-(3/10)**2)**0.5
1.977371993328519

src/mpb.cpp Outdated
scalar_complex s = {real(dp->get_s()),imag(dp->get_s())};
scalar_complex p = {real(dp->get_p()),imag(dp->get_p())};

master_printf("diffracted planewave: k = %g, %g, %g for omega = %g\n", k[0],k[1],k[2], frequency);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It needs to first compute the correct k vector from the frequency and get_g().

src/mpb.cpp Outdated
update_maxwell_data_k(mdata, k, G[0], G[1], G[2]);
maxwell_set_planewave(mdata, H, band_num, dp->get_g(), s, p, dp->get_axis());

eigvals[band_num - 1] = frequency;
Copy link
Collaborator

Choose a reason for hiding this comment

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

or if match_frequency is false then it needs to compute the frequency from k and get_g().

@oskooi
Copy link
Collaborator Author

oskooi commented Aug 20, 2020

The following code computes the correct "k" (which is actually equivalent to the dominant planewave) from the frequency and get_g():

    if (match_frequency) {
      double k2sum = 0;
      LOOP_OVER_DIRECTIONS(v.dim, dd) {
        int m = dp->get_g()[dd - X];
        if ((float(eig_vol.in_direction(dd)) == float(v.in_direction(dd))) &&
                (boundaries[High][dd] == Periodic && boundaries[Low][dd] == Periodic)) {
          k[dd - X] += m/v.in_direction(dd);
          k2sum += k[dd - X]*k[dd - X];
        }
      }
      vec cen = eig_vol.center();
      double nn = sqrt(real(get_eps(cen, frequency)) * real(get_mu(cen, frequency)));
      LOOP_OVER_DIRECTIONS(v.dim, dd) {
        if ((float(eig_vol.in_direction(dd)) != float(v.in_direction(dd))) &&
                (boundaries[High][dd] == Periodic && boundaries[Low][dd] == Periodic)) {
          k[dd - X] = sqrt(frequency*frequency*nn*nn - k2sum);
        }
      }
    }

While it produces the correct result for "k" as shown in the output below on the line diffracted planewave: k = ..., the dominant planewave (following the call to maxwell_dominant_planewave after this code block) and the eigenmode coefficients are incorrect.

Dominant planewave for band 4: (1.977372,-0.300000,0.000000)
eigenmode group velocity: 0.98869
diffracted planewave: k = 1.97737, 0.3, 0 for omega = 2
diffracted planewave: group velocity v=1.39821
Dominant planewave for band 1: (1.977372,0.330000,0.000000)
tran:, 3, 0.04292408 (eigensolver), 0.00794525 (planewave), 0.81490000 (error)

The dominant planewave is reported as (1.977372,0.330000,0.000000); the correct answer is (1.97737, 0.3, 0).

Is the code computing an incorrect value for "k"?

@oskooi
Copy link
Collaborator Author

oskooi commented Aug 22, 2020

Working now: the dominant planewave computed directly in mpb.cpp is equivalent (up to a reciprocal lattice vector) to the dominant planewave returned by MPB via the call to eigensolver (see results below.) If the user specifies a diffraction order which is evanescent, get_eigenmode returns NULL following the proposal in #291:comment.

There is one major limitation in the implementation: because the diffracted planewaves are not symmetric (as noted above), symmetries cannot be used in conjunction with DiffractedPlanewave objects (even when using add_mode_monitor in place of add_flux). Including symmetries produces incorrect results. Is there a workaround?

first diffraction order (diff_order = 1)

Dominant planewave for band 2: (1.997498,-0.100000,0.000000)
eigenmode group velocity: 0.99875
diffracted planewave: k = 1.9975, 0, 0 for omega = 2
diffracted planewave: group velocity v=1.41244
Dominant planewave for band 1: (1.997498,0.100000,0.000000)
tran:, 1, 0.38794770 (eigensolver), 0.38794892 (planewave), 0.00000313 (error)

third diffraction order (diff_order = 3)

Dominant planewave for band 4: (1.977372,-0.300000,0.000000)
eigenmode group velocity: 0.98869
diffracted planewave: k = 1.97737, 0, 0 for omega = 2
diffracted planewave: group velocity v=1.39821
Dominant planewave for band 1: (1.977372,0.300000,0.000000)
tran:, 3, 0.04292397 (eigensolver), 0.04292397 (planewave), 0.00000010 (error)

third diffraction order (diff_order = 5)

Dominant planewave for band 6: (1.936492,-0.500000,0.000000)
eigenmode group velocity: 0.96825
diffracted planewave: k = 1.93649, 0, 0 for omega = 2
diffracted planewave: group velocity v=1.36931
Dominant planewave for band 1: (1.936492,0.500000,0.000000)
tran:, 5, 0.01530716 (eigensolver), 0.01530709 (planewave), 0.00000466 (error)

@oskooi
Copy link
Collaborator Author

oskooi commented Aug 22, 2020

Any ideas how to fix the following SWIG-related error from Travis (serial, Python2.7, no MPB)?

meep-meep_wrap.o: In function `_wrap_new_meep_diffractedplanewave(scm_unused_struct*, scm_unused_struct*, scm_unused_struct*, scm_unused_struct*)':
meep_wrap.cxx:(.text+0x28988): undefined reference to `meep::diffractedplanewave::diffractedplanewave(int*, double*, std::complex<double>, std::complex<double>)'
collect2: error: ld returned 1 exit status
Makefile:518: recipe for target 'meep' failed
make[3]: *** [meep] Error 1
make[3]: Leaving directory '/home/travis/build/NanoComp/meep/build/scheme'
Makefile:432: recipe for target 'all' failed
make[2]: *** [all] Error 2
make[2]: Leaving directory '/home/travis/build/NanoComp/meep/build/scheme'
Makefile:506: recipe for target 'all-recursive' failed
make[1]: *** [all-recursive] Error 1
make[1]: Leaving directory '/home/travis/build/NanoComp/meep/build'
Makefile:415: recipe for target 'all' failed
make: *** [all] Error 2
The command "if [[ "${BUILD_WITHOUT_MPB}" = "1" ]] && [[ "${MAKE_DISTCHECK_EXIT_CODE}" = "0" ]]; then
  ../configure --enable-maintainer-mode --prefix=$HOME/local --with-libctl=$HOME/local/share/libctl ${MPICONF} ac_cv_header_mpb_h=no &&
  make clean &&
  make;
fi
" exited with 2.
0.00s$ kill %1
The command "kill %1" exited with 0.

@oskooi
Copy link
Collaborator Author

oskooi commented Aug 23, 2020

This is ready to be merged. I added a new test (python/tests/diffracted_planewave.py) which verifies that the mode coefficients for the transmitted diffraction orders of a binary grating with an incident planewave (normal and oblique) computed using the DiffractedPlanewave object match those from the Newton solver. The same functionality will be added to the eigenmode source in a separate PR.

note: in 3d, the results depend somewhat on the choice of axis; i.e. choosing two different values for the axis in the same xy plane produces different results. Possible bug in maxwell_set_planewave in the way it handles axis in 3d?

@oskooi oskooi changed the title WIP: eigenmodes of diffracted planewaves in 3d eigenmodes of diffracted planewaves Aug 23, 2020
@stevengj
Copy link
Collaborator

stevengj commented Aug 26, 2020

Notes:

For the 3d case, things seem fine — the basic conceptual issue here is that in 3d, different diffraction orders have different planes of incidence (not just the xy plane!), and so the definition of s and p will depend on the choice of axis. Normally you would want the axis to lie within the mode-monitor plane, however, I think — it would be good to make this the default.

The issue with symmetries seems likely to be unrelated to this PR, so probably it should be debugged separately.

@stevengj stevengj merged commit 6bed831 into NanoComp:master Aug 26, 2020
@oskooi oskooi deleted the diffracted_planewaves branch August 26, 2020 04:17
@oskooi
Copy link
Collaborator Author

oskooi commented Aug 26, 2020

I think I know why DiffractedPlanewave is not working with symmetries. It has to do with fields::add_mode_monitor which ignores symmetries when creating the mode monitor:

meep/src/dft.cpp

Lines 658 to 660 in 859d219

dft_flux fields::add_mode_monitor(direction d, const volume &where, const double *freq, size_t Nfreq, bool centered_grid) {
return add_dft_flux(d, where, freq, Nfreq, /*use_symmetry=*/false, centered_grid);
}

Omitting symmetries in the mode monitor when symmetries are specified for the cell volume in the case of a normally incident planewave with k_point=mp.Vector3(0,0,0) therefore means that the length of the mode monitor in the periodic directions (which extends the entire length of the cell when computing diffracted planewaves) is not equivalent to the cell length in those directions (for which a mirror symmetry has been defined; the difference being a factor of ~2) which therefore causes these two checks in fields::get_eigenmode for the k and k components of the diffractedplanewave object to fail:

meep/src/mpb.cpp

Lines 603 to 604 in 859d219

if ((float(eig_vol.in_direction(dd)) == float(v.in_direction(dd))) &&
(boundaries[High][dd] == Periodic && boundaries[Low][dd] == Periodic)) {

meep/src/mpb.cpp

Lines 614 to 615 in 859d219

if ((float(eig_vol.in_direction(dd)) != float(v.in_direction(dd))) &&
(boundaries[High][dd] == Periodic && boundaries[Low][dd] == Periodic)) {

We obviously do not want to modify add_mode_monitor to support symmetries since it is not intended for that purpose.

Unfortunately, there does not seem to be a simple workaround.

bencbartlett pushed a commit to bencbartlett/meep that referenced this pull request Sep 9, 2021
* eigenmodes of diffracted planewaves in 3d

* overload get_eigenmode_coefficients_and_kpoints with diffractedplanewave object in place of bands array

* SWIG typemap for axis member of class diffractedplanewave

* only set kperp when computing diffracted orders and fix get_eigenmode header for non-MPB builds

* add missing header for get_eigenmode_coefficients for non-MPB build

* typo

* SWIG typemap for diffraction order input parameter g

* rename parameter s to spol to resolve name conflict with Scheme intersect_ray_with_segment argument

* move constructor for diffractedplanewave from mpb.cpp to sources.cpp

* bug fix in k2sum and add Python test

* remove unicode character from Python test

* bands parameter of get_eigenmode_coefficients can only be either a list or DiffractedPlanewave object

* use exception handling for isinstance since Python2 does not recognize range as separate type

* eigenvalue is square if eigenfrequency (bug fix for group velocity)

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

Successfully merging this pull request may close these issues.

3 participants