NanoComp / meep

free finite-difference time-domain (FDTD) software for electromagnetic simulations
GNU General Public License v2.0
1.21k stars 618 forks source link

extend mode decomposition to circularly-polarized modes #484

Closed oskooi closed 5 years ago

oskooi commented 6 years ago

The mode decomposition feature currently only works for linearly-polarized modes. It would be useful to extend this to circularly-polarized modes. An example application which would be nice to set up as a tutorial in the documentation is described in Optics Letters, Vol. 33, No. 20, pp. 2287-9, 2008 (pdf). This involves a linearly-polarized planewave incident on a 2d slab of cholesteric liquid crystal. The transmitted fields are diffracted planewaves at various angles with left or right circular polarization. The mode coefficients of these circularly-polarized modes could be used to calculate the diffraction efficiency of each order.

stevengj commented 6 years ago

Given the complex mode coefficients of the linearly-polarized modes, there is a simple formula to compute the coefficients of the circularly polarized modes.

stevengj commented 5 years ago

Simple approach with existing code:

  1. Do two simulations: (a) one for TM (z-polarized) and (b) one for TE (xy-polarized) input. Let's say the light is incident from the x direction, and we have mirror symmetry in y. The TM input is ODD_Z+EVEN_Y, and the TE input is EVEN_Z+ODD_Y.

  2. In each simulation, compute the complex mode amplitudes for both TM and TE outputs (i.e. all of the mode parities). If you have mirror symmetry in y, then the y parity is preserved, but the z parity is not because z symmetry is broken by the anisotropy of the medium in this example. So, for the TM input you will calculate both ODD_Z+EVEN_Y and EVEN_Z+EVEN_Y coefficients. For the TE input you will compute both ODD_Z+ODD_Y and EVEN_Z+ODD_Y coefficients.

  3. Circularly polarized light is TM ± iTE. Since this is a linear problem, the same relationship applies to the outputs: the mode amplitudes you would get from circularly polarized light should be (TM amplitudes) ± i(TE amplitudes). There is a subtlety, however, because the TM amplitudes are EVEN_Y and the TE amplitudes are ODD_Y: you should think of each TM amplitude as (+y)+(-y) and each TE amplitude as (+y)-(-y) if you decompose them into beams diffracting in the ±y directions. So, if you have (TM) + i(TE) circular input, then to get the corresponding +y output amplitude you should compute (TM amplitudes) + i(TE amplitudes), whereas to get the -y diffracted output you compute (TM amplitudes) - i(TE amplitudes). Vice versa for (TM) - i(TE) circular input.

(This approach actually gives you a factor of two speed advantage compared to simulations with circularly polarized inputs, because it allows you to exploit the y mirror symmetry.)

oskooi commented 5 years ago

The structure in this example breaks the mirror symmetry in y because the nematic director of the liquid crystal is rotating around x. (Fig. 1a of the reference shows the nematic director at just a single cross section where the twist angle happens to be 0° and therefore appears to be mirror symmetric in x. However, for any non-zero twist angle this symmetry is broken.) Exploiting the symmetry to obtain a factor of two speed advantage is therefore negated.

The following script computes the first 5 TM (ODD_Z+EVEN_Y) and TE (EVEN_Z+ODD_Y) mode coefficients for a TM (ODD_Z+EVEN_Y) input. As usual, a normalization run with an empty cell is used to obtain the input flux of the source. At the end of the simulation, the complex mode coefficients as well as the transmittance is displayed for each mode. For reference, the reflectance and transmittance computed using the Poynting flux are also shown.

# polarization grating from C. Oh and M.J. Escuti, Optics Letters, Vol. 33, No. 20, pp. 2287-9, 2008                                                                      
# note: reference uses z as the propagation direction and y as the out-of-plane direction; this script uses x and z, respectively                                         

import meep as mp
import math

resolution = 50        # pixels/um                                                                                                                                        

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

dpml = 1.0             # PML thickness                                                                                                                                    
dsub = 1.0             # substrate thickness                                                                                                                              
dpad = 1.0             # padding thickness                                                                                                                                

d = 1.7                # chiral layer thickness                                                                                                                           
ph = math.radians(70)  # twist angle of each chiral layer                                                                                                                 
gp = 6.5               # grating period                                                                                                                                   

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

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

# twist angle of nematic director; from equation 1b                                                                                                                       
def phi(p):
    xx  = p.x-(-0.5*sx+dpml+dsub)
    if (xx >= 0) and (xx <= d):
        return math.pi*p.y/gp + ph*xx/d
    else:
        return math.pi*p.y/gp - ph*xx/d + 2*ph

n_0 = 1.55
delta_n = 0.159
epsilon_diag = mp.Matrix(mp.Vector3(n_0**2,0,0),mp.Vector3(0,n_0**2,0),mp.Vector3(0,0,(n_0+delta_n)**2))

# return the anisotropic permittivity tensor for a uniaxial, twisted nematic liquid crystal                                                                               
def lc_mat(p):
    # rotation matrix for rotation around x axis                                                                                                                          
    Rx = mp.Matrix(mp.Vector3(1,0,0),mp.Vector3(0,math.cos(phi(p)),math.sin(phi(p))),mp.Vector3(0,-math.sin(phi(p)),math.cos(phi(p))))
    lc_epsilon = Rx * epsilon_diag * Rx.transpose()
    lc_epsilon_diag = mp.Vector3(lc_epsilon[0].x,lc_epsilon[1].y,lc_epsilon[2].z)
    lc_epsilon_offdiag = mp.Vector3(lc_epsilon[1].x,lc_epsilon[2].x,lc_epsilon[2].y)
    return mp.Medium(epsilon_diag=lc_epsilon_diag,epsilon_offdiag=lc_epsilon_offdiag)

geometry = [mp.Block(center=mp.Vector3(-0.5*sx+0.5*(dpml+dsub)),size=mp.Vector3(dpml+dsub,mp.inf,mp.inf),material=mp.Medium(index=n_0)),
            mp.Block(center=mp.Vector3(-0.5*sx+dpml+dsub+d),size=mp.Vector3(2*d,mp.inf,mp.inf),material=lc_mat)]

# linearly polarized planewave: TM (mp.Ez) or TE (mp.Hz)                                                                                                                  
src_cmpt = mp.Ez
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), amplitude=1 if src_cmpt==mp.Ez else 0),
           mp.Source(mp.GaussianSource(fcen,fwidth=df), component=mp.Hz, center=src_pt, size=mp.Vector3(0,sy,0), amplitude=1 if src_cmpt==mp.Hz else 0)]

k_point = mp.Vector3(0,0,0)

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    k_point=k_point,
                    sources=sources,
                    default_material=mp.Medium(index=n_0))

refl_pt = mp.Vector3(-0.5*sx+dpml+0.5*dsub,0,0)
refl_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=refl_pt, size=mp.Vector3(0,sy,0)))

sim.run(until_after_sources=100)

input_flux = mp.get_fluxes(refl_flux)
input_flux_data = sim.get_flux_data(refl_flux)

sim.reset_meep()

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

refl_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=refl_pt, size=mp.Vector3(0,sy,0)))
sim.load_minus_flux_data(refl_flux,input_flux_data)

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=300)

nmode = 5  # number of mode coefficients to compute                                                                                                                       
Tsum = 0

res1 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.ODD_Z+mp.EVEN_Y)
coeffs1 = res1.alpha
res2 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.EVEN_Z+mp.ODD_Y)
coeffs2 = res2.alpha
for nm in range(nmode):
    kdom = res1.kdom[nm]
    angle = math.degrees(math.acos(kdom.x/fcen))
    tran1 = abs(coeffs1[nm,0,0])**2/input_flux[0]
    tran2 = abs(coeffs2[nm,0,0])**2/input_flux[0]
    print("mode1:, {}, {:.2f}, {:.5f}{:+.5f}j, {:.8f}".format(nm,angle,coeffs1[nm,0,0].real,coeffs1[nm,0,0].imag,tran1))
    print("mode2:, {}, {:.2f}, {:.5f}{:+.5f}j, {:.8f}".format(nm,angle,coeffs2[nm,0,0].real,coeffs2[nm,0,0].imag,tran2))
    Tsum += tran1+tran2

print("tran:, {:.6f}".format(Tsum))

r_flux = mp.get_fluxes(refl_flux)
t_flux = mp.get_fluxes(tran_flux)
Rflux = -r_flux[0]/input_flux[0]
Tflux =  t_flux[0]/input_flux[0]
print("flux:, {:.6f}, {:.6f}, {:.6f}".format(Rflux,Tflux,Rflux+Tflux))

(Note: two sources with different polarizations are defined even though only one is used in the 2d simulation. This is necessary to ensure that all field components are used in the normalization run and not just those pertaining to the particular polarization of the source. Specifying extra_materials using an anisotropic permittivity was not sufficient in this regard.)

The output from this script is shown below. The values for the mode coefficients as well as the transmittance are non-zero only for the first order which is consistent with the reference. Also, the sum of the transmittance values from the mode coefficients (0.474278) is exactly half the transmittance obtained using the Poynting flux (0.947084).

mode1:, 0, 0.00, -0.07646-0.14027j, 0.00082210
mode2:, 0, 0.00, -0.04977-0.09338j, 0.00036070
mode1:, 1, 4.77, -2.34913+0.23330j, 0.17950932
mode2:, 1, 4.77, 3.00414-0.29894j, 0.29358311
mode1:, 2, 9.56, -0.00423+0.00761j, 0.00000244
mode2:, 2, 9.56, 0.00427+0.00115j, 0.00000063
mode1:, 3, 14.43, 0.00014-0.00003j, 0.00000000
mode2:, 3, 14.43, 0.00018-0.00010j, 0.00000000
mode1:, 4, 19.41, 0.00000+0.00000j, 0.00000000
mode2:, 4, 19.41, 0.00000-0.00000j, 0.00000000
tran:, 0.474278
flux:, 0.052870, 0.947084, 0.999954

The results for a TE (EVEN_Z+ODD_Y) input are shown below and are also similar.

mode1:, 0, 0.00, -0.07608-0.14283j, 0.00036289
mode2:, 0, 0.00, 0.11645+0.21409j, 0.00082306
mode1:, 1, 4.77, -4.57737+0.44810j, 0.29313212
mode2:, 1, 4.77, -3.58358+0.35179j, 0.17967491
mode1:, 2, 9.56, -0.00646-0.00146j, 0.00000061
mode2:, 2, 9.56, -0.00649+0.01106j, 0.00000228
mode1:, 3, 14.43, -0.00028+0.00014j, 0.00000000
mode2:, 3, 14.43, 0.00021-0.00005j, 0.00000000
mode1:, 4, 19.41, -0.00000+0.00000j, 0.00000000
mode2:, 4, 19.41, 0.00000+0.00000j, 0.00000000
tran:, 0.473996
flux:, 0.052871, 0.947084, 0.999954

Following your prescription for computing the mode coefficients of +y output for a (TM)+i(TE) circular input, the transmittance based on these mode coefficients (which involve halving the input flux in the denominator) correctly indicates that only the m=+1 diffraction order is non-zero. The following are the transmittance values for the first five orders:

0.00235
0.94126
0.00001
0.00000
0.00000

Similarly, computing the mode coefficients of -y output for a (TM)-i(TE) circular input indicates that only the transmittance for the m=-1 diffraction order is non-zero:

0.00238
0.94348
0.00000
0.00000
0.00000

Pending further testing, the results of the reference seem to therefore have been successfully reproduced.

oskooi commented 5 years ago

The following script which is adapted from the recent tutorial example computes the transmittance spectrum of a uniaxial homogeneous polarization grating for a circular-polarized planewave input (the tutorial involved a linear-polarized source). The calculation is based on the strategy outlined previously in this issue of doing two separate simulations using a linear-polarized planewave input with orthogonal polarizations (ODD_Z+EVEN_Y and EVEN_Z+ODD_Y) and then summing/diffing the mode coefficients appropriately.

import meep as mp
import math
import numpy as np
import argparse

resolution = 50        # pixels/μm                                                                                                                         

dpml = 1.0             # PML thickness                                                                                                                     
dsub = 1.0             # substrate thickness                                                                                                               
dpad = 1.0             # padding thickness                                                                                                                 

k_point = mp.Vector3(0,0,0)

n_0 = 1.55
delta_n = 0.159
epsilon_diag = mp.Matrix(mp.Vector3(n_0**2,0,0),mp.Vector3(0,n_0**2,0),mp.Vector3(0,0,(n_0+delta_n)**2))

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

nmode = 5              # number of mode coefficients to compute                                                                                            

def pol_grating(d,gp,amp_ez,amp_hz):
    sx = dpml+dsub+d+d+dpad+dpml
    sy = gp

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

    def phi(p):
        return math.pi*p.y/gp

    # return the anisotropic permittivity tensor for a uniaxial, twisted nematic liquid crystal                                                            
    def lc_mat(p):
    # rotation matrix for rotation around x axis                                                                                                       
    Rx = mp.Matrix(mp.Vector3(1,0,0),mp.Vector3(0,math.cos(phi(p)),math.sin(phi(p))),mp.Vector3(0,-math.sin(phi(p)),math.cos(phi(p))))
    lc_epsilon = Rx * epsilon_diag * Rx.transpose()
    lc_epsilon_diag = mp.Vector3(lc_epsilon[0].x,lc_epsilon[1].y,lc_epsilon[2].z)
    lc_epsilon_offdiag = mp.Vector3(lc_epsilon[1].x,lc_epsilon[2].x,lc_epsilon[2].y)
    return mp.Medium(epsilon_diag=lc_epsilon_diag,epsilon_offdiag=lc_epsilon_offdiag)

    geometry = [mp.Block(center=mp.Vector3(-0.5*sx+0.5*(dpml+dsub)),size=mp.Vector3(dpml+dsub,mp.inf,mp.inf),material=mp.Medium(index=n_0)),
        mp.Block(center=mp.Vector3(-0.5*sx+dpml+dsub+d),size=mp.Vector3(2*d,mp.inf,mp.inf),material=lc_mat)]

    # linear-polarized planewave pulse source                                                                                                              
    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), amplitude=amp_ez),
           mp.Source(mp.GaussianSource(fcen,fwidth=df), component=mp.Hz, center=src_pt, size=mp.Vector3(0,sy,0), amplitude=amp_hz)]

    symmetries = []
    if amp_ez == 1 and amp_hz == 0:
    symmetries = [mp.Mirror(mp.Y,phase=+1)]
    elif amp_ez == 0 and amp_hz == 1:
    symmetries = [mp.Mirror(mp.Y,phase=-1)]

    sim = mp.Simulation(resolution=resolution,
            cell_size=cell_size,
            boundary_layers=pml_layers,
            k_point=k_point,
            sources=sources,
                        symmetries=symmetries,
                        extra_materials=[lc_mat(mp.Vector3())],
                        default_material=mp.Medium(index=n_0))

    refl_pt = mp.Vector3(-0.5*sx+dpml+0.5*dsub,0,0)
    refl_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=refl_pt, size=mp.Vector3(0,sy,0)))

    sim.run(until_after_sources=100)

    input_flux = mp.get_fluxes(refl_flux)
    input_flux_data = sim.get_flux_data(refl_flux)

    sim.reset_meep()

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

    refl_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=refl_pt, size=mp.Vector3(0,sy,0)))
    sim.load_minus_flux_data(refl_flux,input_flux_data)

    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=300)

    if amp_ez == 1 and amp_hz == 0:
        res1 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.ODD_Z+mp.EVEN_Y)
        res2 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.EVEN_Z+mp.EVEN_Y)
    elif amp_ez == 0 and amp_hz == 1:
        res1 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.ODD_Z+mp.ODD_Y)
        res2 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.EVEN_Z+mp.ODD_Y)

    angles1 = [math.degrees(math.acos(kdom.x/fcen)) for kdom in res1.kdom]
    angles2 = [math.degrees(math.acos(kdom.x/fcen)) for kdom in res2.kdom]

    for nm in range(nmode):
            print("mode1:, {}, {:.2f}, {:.5f}{:+.5f}j".format(nm,angles1[nm],res1.alpha[nm,0,0].real,res1.alpha[nm,0,0].imag))
            print("mode2:, {}, {:.2f}, {:.5f}{:+.5f}j".format(nm,angles2[nm],res2.alpha[nm,0,0].real,res2.alpha[nm,0,0].imag))

    return input_flux[0], angles1, angles2, res1.alpha[:,0,0], res2.alpha[:,0,0];

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-dd', type=float, default=1.7, help='chiral layer thickness (default: 1.7 μm)')
    parser.add_argument('-gp', type=float, default=6.5, help='grating period (default: 6.5 μm)')
    args = parser.parse_args()

    input_flux1, angles1a, angles1b, coeffs1a, coeffs1b = pol_grating(args.dd,args.gp,1,0)
    input_flux2, angles2a, angles2b, coeffs2a, coeffs2b = pol_grating(args.dd,args.gp,0,1)

    tran1 = np.zeros(nmode)
    tran2 = np.zeros(nmode)
    tran1[0] = abs(coeffs1a[0]+1j*coeffs2b[0])**2/(input_flux1+input_flux2)
    tran2[0] = abs(coeffs1a[0]-1j*coeffs2b[0])**2/(input_flux1+input_flux2)
    tran1[1:] = abs(coeffs1a[1:]+coeffs1b[:-1]+1j*(coeffs2a[:-1]+coeffs2b[1:]))**2/(input_flux1+input_flux2)
    tran2[1:] = abs(coeffs1a[1:]+coeffs1b[:-1]-1j*(coeffs2a[:-1]+coeffs2b[1:]))**2/(input_flux1+input_flux2)

    print("tran:, 0, 0, {:.5f}".format(0.5*(tran1[0]+tran2[0])))
    for m in range(1,nmode):
        print("tran:, +{}, +{:.2f}, {:.5f}".format(m,angles1a[m],tran1[m]))
        print("tran:, -{}, -{:.2f}, {:.5f}".format(m,angles1a[m],tran2[m]))

Note: when adding the mode coefficients for non-zero orders, the arrays need to be offset from each other in order for the mode coefficients to correspond to the same angles.

The diffraction efficiency can be computed analytically: η0=cos2(πΔnd/λ), η±1=0.5*(1∓S)sin2(πΔnd/λ) where S is the ellipticity of the polarization (e.g., CCW: S=+1, CW: S=-1). We can verify the correctness of the script using two runs: (1) Δnd/λ=0.5, and (2) Δnd/λ=1.0.

Δnd/λ=0.5

tran:, 0, 0, 0.00066
tran:, +1, +4.77, 1.34654
tran:, -1, -4.77, 1.34253
tran:, +2, +9.56, 0.00003
tran:, -2, -9.56, 0.00002
tran:, +3, +14.43, 0.00002
tran:, -3, -14.43, 0.00002
tran:, +4, +19.41, 0.00002
tran:, -4, -19.41, 0.00002

The results are incorrect: the transmittance for the m=+1 and -1 orders should be 0.94 and 0 (or vice versa) rather than 1.34654 and 1.34654. (the reflectance is 0.06 which is why the transmittance is not 1.0).

Δnd/λ=1.0

tran:, 0, 0, 0.92771
tran:, +1, +4.77, 0.00424
tran:, -1, -4.77, 0.00375
tran:, +2, +9.56, 0.00001
tran:, -2, -9.56, 0.00003
tran:, +3, +14.43, 0.00002
tran:, -3, -14.43, 0.00002
tran:, +4, +19.41, 0.00002
tran:, -4, -19.41, 0.00002

The results are (mostly) correct: the m=0 order is approximately 0.93 and the m=±1 orders are 0 (although the results are still off slightly from their expected values).

stevengj commented 5 years ago

Two fixes:

oskooi commented 5 years ago

Even with the two suggested changes, the results are still incorrect for Δnd/λ=0.5 where only one of the +1 or -1 orders should be non-zero. Rather, as previously, the transmittance in both orders is non-zero and equal. For Δnd/λ=1, the results are correct (same as before).

Δnd/λ=0.5

tran:, 0, 0, 0.00064
tran:, +1, +4.77, 0.47147
tran:, -1, -4.77, 0.47147
tran:, +2, +9.56, 0.00001
tran:, -2, -9.56, 0.00001
tran:, +3, +14.43, 0.00001
tran:, -3, -14.43, 0.00001
tran:, +4, +19.41, 0.00001
tran:, -4, -19.41, 0.00001

Δnd/λ=1.0

tran:, 0, 0, 0.92773
tran:, +1, +4.77, 0.00147
tran:, -1, -4.77, 0.00145
tran:, +2, +9.56, 0.00001
tran:, -2, -9.56, 0.00001
tran:, +3, +14.43, 0.00001
tran:, -3, -14.43, 0.00001
tran:, +4, +19.41, 0.00001
tran:, -4, -19.41, 0.00001
import meep as mp
import math
import numpy as np
import argparse

resolution = 50        # pixels/μm                                                                                                                               

dpml = 1.0             # PML thickness                                                                                                                           
dsub = 1.0             # substrate thickness                                                                                                                     
dpad = 1.0             # padding thickness                                                                                                                       

k_point = mp.Vector3(0,0,0)

n_0 = 1.55
delta_n = 0.159
epsilon_diag = mp.Matrix(mp.Vector3(n_0**2,0,0),mp.Vector3(0,n_0**2,0),mp.Vector3(0,0,(n_0+delta_n)**2))

wvl = 0.54             # center wavelength                                                                                                                       
fcen = 1/wvl           # center frequency                                                                                                                        

nmode = 5              # number of mode coefficients to compute                                                                                                  

def pol_grating(d,gp,amp_ez):
    sx = dpml+dsub+d+d+dpad+dpml
    sy = gp

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

    def phi(p):
        return math.pi*p.y/gp

    # return the anisotropic permittivity tensor for a uniaxial, twisted nematic liquid crystal                                                                  
    def lc_mat(p):
        # rotation matrix for rotation around x axis                                                                                                             
        Rx = mp.Matrix(mp.Vector3(1,0,0),mp.Vector3(0,math.cos(phi(p)),math.sin(phi(p))),mp.Vector3(0,-math.sin(phi(p)),math.cos(phi(p))))
        lc_epsilon = Rx * epsilon_diag * Rx.transpose()
        lc_epsilon_diag = mp.Vector3(lc_epsilon[0].x,lc_epsilon[1].y,lc_epsilon[2].z)
        lc_epsilon_offdiag = mp.Vector3(lc_epsilon[1].x,lc_epsilon[2].x,lc_epsilon[2].y)
        return mp.Medium(epsilon_diag=lc_epsilon_diag,epsilon_offdiag=lc_epsilon_offdiag)

    geometry = [mp.Block(center=mp.Vector3(-0.5*sx+0.5*(dpml+dsub)),size=mp.Vector3(dpml+dsub,mp.inf,mp.inf),material=mp.Medium(index=n_0)),
        mp.Block(center=mp.Vector3(-0.5*sx+dpml+dsub+d),size=mp.Vector3(2*d,mp.inf,mp.inf),material=lc_mat)]

    # linear-polarized planewave pulse source                                                                                                                    
    src_pt = mp.Vector3(-0.5*sx+dpml+0.3*dsub,0,0)
    sources = [mp.Source(mp.GaussianSource(fcen,fwidth=0.05*fcen), component=mp.Ez if amp_ez else mp.Ey, center=src_pt, size=mp.Vector3(0,sy,0))]

    symmetries = []
    if amp_ez:
        symmetries = [mp.Mirror(mp.Y,phase=+1)]
    else:
        symmetries = [mp.Mirror(mp.Y,phase=-1)]

    sim = mp.Simulation(resolution=resolution,
                        cell_size=cell_size,
                        boundary_layers=pml_layers,
                        k_point=k_point,
                        sources=sources,
                        symmetries=symmetries,
                        default_material=mp.Medium(index=n_0))

    sim.init_sim()
    if amp_ez:
        sim.fields.require_component(mp.Hz)
    else:
        sim.fields.require_component(mp.Ez)

    refl_pt = mp.Vector3(-0.5*sx+dpml+0.5*dsub,0,0)
    refl_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=refl_pt, size=mp.Vector3(0,sy,0)))

    sim.run(until_after_sources=100)

    input_flux = mp.get_fluxes(refl_flux)
    input_flux_data = sim.get_flux_data(refl_flux)

    sim.reset_meep()

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

    refl_flux = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=refl_pt, size=mp.Vector3(0,sy,0)))
    sim.load_minus_flux_data(refl_flux,input_flux_data)

    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=300)

    if amp_ez:
        res1 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.ODD_Z+mp.EVEN_Y)
        res2 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.EVEN_Z+mp.EVEN_Y)
    else:
        res1 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.ODD_Z+mp.ODD_Y)
        res2 = sim.get_eigenmode_coefficients(tran_flux, range(1,nmode+1), eig_parity=mp.EVEN_Z+mp.ODD_Y)

    angles1 = [math.degrees(math.acos(kdom.x/fcen)) for kdom in res1.kdom]
    angles2 = [math.degrees(math.acos(kdom.x/fcen)) for kdom in res2.kdom]

    for nm in range(nmode):
            print("mode1:, {}, {:.2f}, {:.5f}{:+.5f}j".format(nm,angles1[nm],res1.alpha[nm,0,0].real,res1.alpha[nm,0,0].imag))
            print("mode2:, {}, {:.2f}, {:.5f}{:+.5f}j".format(nm,angles2[nm],res2.alpha[nm,0,0].real,res2.alpha[nm,0,0].imag))

    return input_flux[0], angles1, angles2, res1.alpha[:,0,0], res2.alpha[:,0,0]

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-dd', type=float, default=0.85, help='chiral layer thickness (default: 0.85 μm)')
    parser.add_argument('-gp', type=float, default=6.5, help='grating period (default: 6.5 μm)')
    args = parser.parse_args()

    input_flux1, angles1a, angles1b, coeffs1a, coeffs1b = pol_grating(args.dd,args.gp,1)
    input_flux2, angles2a, angles2b, coeffs2a, coeffs2b = pol_grating(args.dd,args.gp,0)

    tran1 = np.zeros(nmode)
    tran2 = np.zeros(nmode)
    tran1[0] = (abs(coeffs1a[0])**2 + abs(coeffs2b[0])**2)/(input_flux1+input_flux2)
    tran2[0] = (abs(coeffs1a[0])**2 + abs(coeffs2b[0])**2)/(input_flux1+input_flux2)
    tran1[1:] = (abs(coeffs1a[1:]+1j*coeffs2a[:-1])**2 + abs(coeffs1b[:-1]+1j*coeffs2b[1:])**2)/(input_flux1+input_flux2)
    tran2[1:] = (abs(coeffs1a[1:]-1j*coeffs2a[:-1])**2 + abs(coeffs1b[:-1]-1j*coeffs2b[1:])**2)/(input_flux1+input_flux2)
    tran1 = 0.5*tran1
    tran2 = 0.5*tran2
    print("tran:, 0, 0, {:.5f}".format(tran1[0]+tran2[0]))
    for m in range(1,nmode):
        print("tran:, +{}, +{:.2f}, {:.5f}".format(m,angles1a[m],tran1[m]))
        print("tran:, -{}, -{:.2f}, {:.5f}".format(m,angles1a[m],tran2[m]))
oskooi commented 5 years ago

Δnd/λ=0.5 Ez source

mode1:, 0, 0.00, -0.12669-0.03749j
mode2:, 0, 4.77, -1.97321+3.20535j
mode1:, 1, 4.77, 1.98577-3.20884j
mode2:, 1, 9.56, 0.00229+0.00413j
mode1:, 2, 9.56, 0.00897-0.02405j
mode2:, 2, 14.43, 0.00001-0.00012j
mode1:, 3, 14.43, 0.01153-0.01964j
mode2:, 3, 19.41, -0.00000-0.00000j
mode1:, 4, 19.41, 0.01155-0.01976j
mode2:, 4, 24.54, -0.00000+0.00000j

Hz source

mode1:, 0, 4.77, 1.98027-3.19884j
mode2:, 0, 0.00, 0.14403+0.00967j
mode1:, 1, 9.56, -0.00252-0.00428j
mode2:, 1, 4.77, 1.97990-3.21514j
mode1:, 2, 14.43, -0.00002+0.00012j
mode2:, 2, 9.56, 0.01046-0.02367j
mode1:, 3, 19.41, 0.00000+0.00000j
mode2:, 3, 14.43, 0.01267-0.01947j
mode1:, 4, 24.54, 0.00000-0.00000j
mode2:, 4, 19.41, 0.01268-0.01959j
oskooi commented 5 years ago

The polarization grating has been correctly set up in the script. This is verified by results shown in the figure below for snapshots of Ez for 4 different cases involving a circularly-polarized input planewave: Δnd/λ=0.5 and 1.0, and input chirality of Ez+iEy and Ez-iEy. As expected, the chirality of the input planewave determines the first diffraction order (m=±1) for Δnd/λ=0.5. The angle of the first diffracted order (±4.77°) also agrees with the expected analytic result. Results are similar for Ey snapshots.

circular_polarization_grating_circular_input

Is it possible that there is a bug in mode decomposition for the case of a 2d cell where all field components are used?

oskooi commented 5 years ago

It seems the correct results are produced by modifying the transmittance expression as follows:

original

tran1[1:] = (abs(coeffs1a[1:]+1j*coeffs2a[:-1])**2 + abs(coeffs1b[:-1]+1j*coeffs2b[1:])**2)/(input_flux1+input_flux2)
tran2[1:] = (abs(coeffs1a[1:]-1j*coeffs2a[:-1])**2 + abs(coeffs1b[:-1]-1j*coeffs2b[1:])**2)/(input_flux1+input_flux2)

modified

tran1[1:] = (abs(coeffs1a[1:]+coeffs2a[:-1])**2 + abs(coeffs1b[:-1]-coeffs2b[1:])**2)/(input_flux1+input_flux2)
tran2[1:] = (abs(coeffs1a[1:]-coeffs2a[:-1])**2 + abs(coeffs1b[:-1]+coeffs2b[1:])**2)/(input_flux1+input_flux2)

The main change is that the 1j prefactor multiplying the mode coefficients with ODD_Z parity for an Ey linearly-polarized input has been removed.

Δnd/λ=0.5

tran:, 0, 0, 0.00064
tran:, +1, +4.77, 0.94294
tran:, -1, -4.77, 0.00000
tran:, +2, +9.56, 0.00001
tran:, -2, -9.56, 0.00001
tran:, +3, +14.43, 0.00001
tran:, -3, -14.43, 0.00001
tran:, +4, +19.41, 0.00001
tran:, -4, -19.41, 0.00001

Note that only the m=+1 order has non-zero transmittance.

Δnd/λ=1.0

tran:, 0, 0, 0.92773
tran:, +1, +4.77, 0.00001
tran:, -1, -4.77, 0.00291
tran:, +2, +9.56, 0.00001
tran:, -2, -9.56, 0.00001
tran:, +3, +14.43, 0.00001
tran:, -3, -14.43, 0.00001
tran:, +4, +19.41, 0.00001
tran:, -4, -19.41, 0.00001

Note that only the m=0 order has non-zero transmittance.

As another verification, changing the amplitude of the Ey source from +1 to -1 which in effect flips the chirality of the input source results in the m=-1 having non-zero transmittance.

tran:, 0, 0, 0.00064
tran:, +1, +4.77, 0.00000
tran:, -1, -4.77, 0.94294
tran:, +2, +9.56, 0.00001
tran:, -2, -9.56, 0.00001
tran:, +3, +14.43, 0.00001
tran:, -3, -14.43, 0.00001
tran:, +4, +19.41, 0.00001
tran:, -4, -19.41, 0.00001
stevengj commented 5 years ago

Maybe the issue is that MPB is not choosing the phase for the Ez and Hz polarized modes in the same way? (Once we implement the set_planewave feature you will be able to choose whatever phase you want.) That is, the phase is chosen deterministically, but it is not necessarily chosen in the same way for different polarizations.

If you call get-eigenmode-field (or whatever it is called) for Ez at the center of one mode and for Hz at the center of the other mode, you will see the relative phases. In particular, look at the ratio Hz/Ez — if that isn't one, then the phases are being chosen differently. (I'm guessing from above that they differ by a factor of ±i.) Then you could correct the mode amplitude α of the Hz mode by multiplying α by Ez/Hz, I think.

oskooi commented 5 years ago

The ratio Hz/Ez is not one as shown in the figure below (bottom right inset). The imaginary part of Hz/Ez is 0 but the real part is a non-zero constant (-1.5). These results were generated using the accompanying script which computes two separate eigenmodes (Ez:ODD_Z+EVEN_Y and Hz:EVEN_Z+ODD_Y) for an arbitrary band (5) of a dielectric medium with n=1.5 at a k_point of (0,0,0). The dominant planewave for both eigenmodes is (1.445683,-0.400000,0.000000). The other three insets show the real and imaginary parts as well as the intensity profile of these two eigenmodes. Results are similar for other bands.

ez_hz_eigenmode

import meep as mp
import matplotlib.pyplot as plt
import numpy as np

resolution = 50        # pixels/μm                                                                                                                                            

dpml = 1               # PML thickness                                                                                                                                        
dmat = 10              # length of material                                                                                                                                   

sx = dpml+dmat+dpml
sy = 10

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

fcen = 1               # center frequency (wavelength = 1 μm)                                                                                                                 
band_num = 5

glass = mp.Medium(index=1.5)

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

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

sim.init_sim()

mon_pt = mp.Vector3(0.5*sx-dpml-0.2*dmat,0,0)
data_oddz = sim.get_eigenmode(fcen, mp.X, mp.Volume(center=mon_pt,size=mp.Vector3(0,sy,0)), band_num, k, parity=eig_parity, resolution=resolution, verbose=True)

sim.reset_meep()

eig_parity = mp.EVEN_Z+mp.ODD_Y
symmetries = [mp.Mirror(mp.Y, phase=-1)]

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

sim.init_sim()

data_evenz = sim.get_eigenmode(fcen, mp.X, mp.Volume(center=mon_pt,size=mp.Vector3(0,sy,0)), band_num, k, parity=eig_parity, resolution=resolution, verbose=True)

y = np.linspace(-0.5*sy,0.5*sy,sy*resolution)
Ez_data = np.asarray([data_oddz.amplitude(mp.Vector3(0,yc,0),mp.Ez) for yc in y])
Hz_data = np.asarray([data_evenz.amplitude(mp.Vector3(0,yc,0),mp.Hz) for yc in y])
phase = Hz_data/Ez_data

plt.figure(dpi=300)
plt.subplot(2,2,1)
plt.plot(y,np.real(Ez_data),'bo-',label=r'real (E$_z$)')
plt.plot(y,np.real(Hz_data),'ro-',label=r'real (H$_z$)')
plt.xlabel('y coordinate')
plt.ylabel(r'E$_z$ or H$_z$')
plt.legend(loc='lower right')
plt.title('real part')

plt.subplot(2,2,2)
plt.plot(y,np.imag(Ez_data),'bo-',label=r'imag (E$_z$)')
plt.plot(y,np.imag(Hz_data),'ro-',label=r'imag (H$_z$)')
plt.xlabel('y coordinate')
plt.ylabel(r'E$_z$ or H$_z$')
plt.legend(loc='lower right')
plt.title('imaginary part')

plt.subplot(2,2,3)
plt.plot(y,abs(Ez_data)**2,'bo-',label=r'E$_z$')
plt.plot(y,abs(Hz_data)**2,'ro-',label=r'H$_z$')
plt.xlabel('y coordinate')
plt.ylabel(r'|E$_z$|$^2$ or |H$_z$|$^2$')
plt.legend(loc='lower right')
plt.title('intensity')

plt.subplot(2,2,4)
plt.plot(y,np.real(phase),'bo-',label='real')
plt.plot(y,np.imag(phase),'ro-',label='imag')
plt.xlabel('y coordinate')
plt.ylabel('H$_z$/E$_z$')
plt.legend(loc='center')
plt.title('relative phase')

plt.tight_layout()
plt.show()
oskooi commented 5 years ago

The mode coefficients for the first band (i.e., zeroth order) of 2d vacuum for a Ez and Ey source have a phase difference of exactly π (i.e., the coefficients differ by a scalar factor of -1). This is demonstrated using the following script.

import meep as mp

dpml = 1
dair = 10
sx = dpml+dair+dpml
sy = 5
cell_size = mp.Vector3(sx,sy,0)

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

resolution = 25
fcen = 1.0

src_pt = mp.Vector3(-0.5*sx+dpml+0.1*dair)
mon_pt = mp.Vector3(+0.5*sx-dpml-0.1*dair)

k = mp.Vector3(0,0,0)

def compute_zero_difforder(src_ez):
    sources = [mp.Source(mp.GaussianSource(fcen, 0.2*fcen),
                         center=src_pt,
                         size=mp.Vector3(0,sy,0),
                         component=mp.Ez if src_ez else mp.Ey)]

    symmetries = [mp.Mirror(direction=mp.Y, phase=+1 if src_ez else -1)]

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

    mode_mon = sim.add_flux(fcen, 0, 1, mp.FluxRegion(center=mon_pt, size=mp.Vector3(0,sy,0)))

    sim.run(until_after_sources=100)

    res = sim.get_eigenmode_coefficients(mode_mon, [1], eig_parity=mp.ODD_Z+mp.EVEN_Y if src_ez else mp.EVEN_Z+mp.ODD_Y)
    coeffs = res.alpha

    print("mode:, {}, {:.5f}{:+.5f}j, {:.5f}".format("ez" if src_ez else "ey",coeffs[0,0,0].real,coeffs[0,0,0].imag,abs(coeffs[0,0,0])**2))

if __name__ == '__main__':
    compute_zero_difforder(True)
    compute_zero_difforder(False)

The output displays the mode coefficient for each source as well as its squared magnitude (i.e., power):

mode:, ez, 0.10776+0.00400j, 0.01163
mode:, ey, -0.10776-0.00400j, 0.01163

Does this e phase get introduced somewhere in get_eigenmode?

stevengj commented 5 years ago

Okay, good to know — everything is clear and correct, and the UI issue will be fixed by the set-planewave functionality.