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

WIP: near2far for cylindrical coordinates. #1090

Merged
merged 14 commits into from
Jan 22, 2020
Merged

WIP: near2far for cylindrical coordinates. #1090

merged 14 commits into from
Jan 22, 2020

Conversation

stevengj
Copy link
Collaborator

@stevengj stevengj commented Jan 3, 2020

This is a rough draft to fix #1086 using the simplest possible method: integrating green3d numerically over φ to get the corresponding cylindrical function.

(Integrates by repeatedly doubling the number of φ points to a tolerance of 1e-3 or 2^16 points, whichever comes first, with a simple Euler/trapezoidal rule — which should be exponentially convergent for smooth cylindrical integrals like this.)

Untested except to check that it compiles, intended as a first draft.

@oskooi
Copy link
Collaborator

oskooi commented Jan 4, 2020

To test whether this PR is working correctly or not, the following example computes the far fields at the focal length (known analytically) of a binary-phase zone plate as described in http://zoneplate.lbl.gov/theory. (The specifications of the zone plate are similar to the binary phase grating in Tutorial/Mode Decomposition/Diffraction Spectrum of a Binary Grating.)

However, the far fields do not show any focusing behavior. This is unexpected and could either be due to a bug in: (1) the simulation script or (2) greencyl.

For reference, the width of the focal spot is 1.3 um for this configuration computed using eq. 14 of http://zoneplate.lbl.gov/theory.

import meep as mp
import numpy as np
import math

import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt

resolution = 25             # pixels/um

dpml = 1.0                  # PML thickness
dsub = 2.0                  # substrate thickness
dpad = 2.0                  # padding betweeen zone plate and PML
zh = 0.5                    # zone-plate height
zN = 15                     # number of zones (odd numbers: \pi phase shift, even numbers: none)
focal_length = 200          # focal length of zone plate

spot_length = 100           # far-field line length
ff_npts = 50                # number of far-field points to compute along spot_length

pml_layers = [mp.PML(thickness=dpml)]

wvl_cen = 0.5                                                                                                                                frq_cen = 1/wvl_cen
dfrq = 0.2*frq_cen

## ref: eq. 7 of http://zoneplate.lbl.gov/theory
r = [math.sqrt(n*wvl_cen*(focal_length+n*wvl_cen/4)) for n in range(1,zN+1)]

sr = r[-1]+dpad+dpml
sz = dpml+dsub+zh+dpad+dpml
cell_size = mp.Vector3(sr,0,sz)

sources = [mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
                     component=mp.Er,
                     center=mp.Vector3(0.5*sr,0,-0.5*sz+dpml),
                     size=mp.Vector3(sr)),
           mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
                     component=mp.Ep,
                     center=mp.Vector3(0.5*sr,0,-0.5*sz+dpml),
                     size=mp.Vector3(sr),
                     amplitude=-1j)]

glass = mp.Medium(index=1.5)

geometry = [mp.Block(material=glass,
                     size=mp.Vector3(sr,0,dpml+dsub),
                     center=mp.Vector3(0.5*sr,0,-0.5*sz+0.5*(dpml+dsub)))]

for n in range(zN-1,-1,-1):
    geometry.append(mp.Block(material=glass if n % 2 == 0 else mp.vacuum,
                             size=mp.Vector3(r[n],0,zh),
                             center=mp.Vector3(0.5*r[n],0,-0.5*sz+dpml+dsub+0.5*zh)))

sim = mp.Simulation(cell_size=cell_size,
                    boundary_layers=pml_layers,
                    resolution=resolution,
                    sources=sources,
                    geometry=geometry,
                    dimensions=mp.CYLINDRICAL,
                    m=-1)

n2f_obj = sim.add_near2far(frq_cen, 0, 1, mp.Near2FarRegion(center=mp.Vector3(0.5*sr,0,0.5*sz-dpml),size=mp.Vector3(sr)))

sim.run(mp.at_beginning(mp.output_epsilon),until_after_sources=50)

ff = sim.get_farfields(n2f_obj, ff_npts/spot_length, center=mp.Vector3(z=-0.5*sz+dpml+dsub+zh+focal_length),size=mp.Vector3(z=spot_length))

energy_density = np.absolute(ff['Ex'])**2+np.absolute(ff['Ey'])**2+np.absolute(ff['Ez'])**2

z = np.linspace(focal_length-0.5*spot_length,focal_length+0.5*spot_length,ff_npts)

plt.figure(dpi=200)
plt.semilogy(z,energy_density,'bo-')
plt.xlabel('z coordinate (um)')
plt.ylabel(r'energy density of far-field electric fields, |E|^2')
plt.tight_layout()

plt.savefig('zone_plate_farfields.png')

structure

zone_15

energy density of far-fields at focal length

zone_plate_farfields_15

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 4, 2020

Are you using the latest version of this PR?

@oskooi
Copy link
Collaborator

oskooi commented Jan 4, 2020

Yes: using latest version of this PR with commit message "use meep::pi".

As a further diagnostic, the following is the output of the energy density of the electric field separated by component:

print(np.absolute(ff['Ex'])**2)
print(np.absolute(ff['Ey'])**2)
print(np.absolute(ff['Ez'])**2)

The output contains non-zero values for Ex and Ey which are the same at every point along a line in the focus direction. This is wrong.

[ 453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876]
[ 453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876
  453.56840876  453.56840876  453.56840876  453.56840876  453.56840876]
[  9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29   9.31253355e-29   9.31253355e-29
   9.31253355e-29   9.31253355e-29]

@oskooi
Copy link
Collaborator

oskooi commented Jan 4, 2020

I think I've tracked down what the problem is: greencyl is computing the far fields at just a single point rather than different points along a line. This can be seen by adding the following print statement right before the break on near2far.cpp:296:

 master_printf("(%0.4f,%0.4f,%0.4f): converged with N=%d\n",x_3d.x(),x_3d.y(),x_3d.z(),N);`

which produces the following for the above test:

(0.0000,0.0000,150.2500): converged with N=16
(0.0000,0.0000,150.2500): converged with N=16
(0.0000,0.0000,150.2500): converged with N=16
(0.0000,0.0000,150.2500): converged with N=16
(0.0000,0.0000,150.2500): converged with N=16
(0.0000,0.0000,150.2500): converged with N=16
(0.0000,0.0000,150.2500): converged with N=16
....
....

This explains why the values of the energy density shown in the previous comment are all identical.

Since there does not seem to be anything wrong in the call to get_farfields in the simulation script, it's likely that the bug is either in farfield_lowlevel of greencyl.

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 6, 2020

There was a bug in get_farfields_array; try it with the latest PR.

@oskooi
Copy link
Collaborator

oskooi commented Jan 6, 2020

Working now: the incident planewave is being focused at 200 um as expected.

zone_plate_farfields_60

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 6, 2020

Great! Would be good to do a more quantitative test by comparing the near2far predictions with an explicit calculation using a larger cell, the same way that we tested the Cartesian near2far feature.

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 6, 2020

It would also be nice to know the value of N (the number of quadrature points) when the if (sumdiff <= sumabs * tol) break; test stops the loop.

@oskooi
Copy link
Collaborator

oskooi commented Jan 7, 2020

convergence stats for resolution = 30, ff_npts = 10, and zN = 35 for various tolerance values showing breakdown of the iteration number by N value (number of quadrature points).

tol = 1e-3
N=8: 30 (0.05%)
N=16: 57060 (99.95%)
total: 57090

tol = 1e-5
N=8: 30 (0.05%)
N=16: 57060 (99.95%)
total: 57090

tol = 1e-12
N=8: 30 (0.05%)
N=16: 57060 (99.95%)
total: 57090

tol = 1e-16
N=8: 30 (0.20%)
N=16: 8448 (56.02%)
N=32: 4921 (32.63%)
N=64: 1324 (8.78%)
N=128: 300 (1.99%)
N=256: 49 (0.32%)
N=512: 7 (0.05%)
total: 15079

It's only when the tolerance is reduced to 1e-16 that non-zero values for N larger than 16 are present. Note that the total iteration number for a tolerance of 1e-16 is less than the others (because some iterations reached the hard limit of 65536 quadrature points without converging to this low tolerance value).

Nevertheless, in all cases, the far-field profile is identical to the following.

zone_plate_farfields_zn35_ffnpts10_tol16

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 7, 2020

It's only when the tolerance is reduced to 1e-16 that non-zero values for N larger than 16 are present.

Probably we should change the starting N (N0) to a smaller value, then, say N0 = 4.

What do the "(99.95%)" numbers in your output indicate? What is 57060?

@oskooi
Copy link
Collaborator

oskooi commented Jan 7, 2020

The percentage values refer to the fraction of all converged calls of greencyl which converged with that particular N. For example, 99.95% of all the iterations for tol = 1e-3 converged with N of 16.

As an alternative to doubling N on each iteration, we may want to consider just adding a constant factor. Will post results for this shortly.

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 7, 2020

As an alternative to doubling N on each iteration, we may want to consider just adding a constant factor. Will post results for this shortly.

Note that the re-using of points from the previous N will no longer work in that case, so you'll need to change the i loop.

It's probably not worth it. Let's just change N0 to 4 and call it a day.

@oskooi
Copy link
Collaborator

oskooi commented Jan 9, 2020

As additional validation that this PR is working correctly, the far fields obtained using an actual enlarged cell are proportional (by roughly a constant factor) to those obtained using near2far (shown above). For reference, the runtime of the "brute-force" calculation is about 70X larger than the near2far calculation.

zone_plate_extended_cell_farfields_zn15

import meep as mp
import numpy as np
import math

import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt

resolution = 25             # pixels/um                                                                                                                                                

dpml = 1.0                  # PML thickness                                                                                                                                            
dsub = 2.0                  # substrate thickness                                                                                                                                      
dpad = 2.0                  # padding betweeen zone plate and PML                                                                                                                      
zh = 0.5                    # zone-plate height                                                                                                                                        
zN = 15                     # number of zones (odd numbers: \pi phase shift, even numbers: none)                                                                                       
focal_length = 200          # focal length of zone plate                                                                                                                               
spot_length = 100           # far-field line length                                                                                                                                    

pml_layers = [mp.PML(thickness=dpml)]

wvl_cen = 0.5
frq_cen = 1/wvl_cen
dfrq = 0.2*frq_cen

## ref: eq. 7 of http://zoneplate.lbl.gov/theory                                                                                                                                       
r = [math.sqrt(n*wvl_cen*(focal_length+n*wvl_cen/4)) for n in range(1,zN+1)]

sr = r[-1]+dpad+dpml
sz = dpml+dsub+zh+focal_length+0.5*spot_length+dpad+dpml
cell_size = mp.Vector3(sr,0,sz)

sources = [mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
                     component=mp.Er,
                     center=mp.Vector3(0.5*sr,0,-0.5*sz+dpml),
                     size=mp.Vector3(sr)),
           mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
                     component=mp.Ep,
                     center=mp.Vector3(0.5*sr,0,-0.5*sz+dpml),
                     size=mp.Vector3(sr),
                     amplitude=-1j)]

glass = mp.Medium(index=1.5)

geometry = [mp.Block(material=glass,
                     size=mp.Vector3(sr,0,dpml+dsub),
                     center=mp.Vector3(0.5*sr,0,-0.5*sz+0.5*(dpml+dsub)))]

for n in range(zN-1,-1,-1):
    geometry.append(mp.Block(material=glass if n % 2 == 0 else mp.vacuum,
                             size=mp.Vector3(r[n],0,zh),
                             center=mp.Vector3(0.5*r[n],0,-0.5*sz+dpml+dsub+0.5*zh)))

sim = mp.Simulation(cell_size=cell_size,
                    boundary_layers=pml_layers,
                    resolution=resolution,
                    sources=sources,
                    geometry=geometry,
                    dimensions=mp.CYLINDRICAL,
                    m=-1)

dft_obj = sim.add_dft_fields([mp.Er,mp.Ep,mp.Ez], frq_cen, frq_cen, 1, center=mp.Vector3(z=-0.5*sz+dpml+dsub+zh+focal_length), size=mp.Vector3(z=spot_length))

sim.run(until_after_sources=400)

energy_density = np.absolute(sim.get_dft_array(dft_obj,mp.Er,0))**2+np.absolute(sim.get_dft_array(dft_obj,mp.Ep,0))**2+np.absolute(sim.get_dft_array(dft_obj,mp.Ez,0))**2

z = np.linspace(focal_length-0.5*spot_length,focal_length+0.5*spot_length,len(energy_density))

if mp.am_master():
    plt.figure(dpi=200)
    plt.semilogy(z,energy_density,'bo-')
    plt.xlabel('z coordinate (um)')
    plt.ylabel(r'energy density of far-field electric fields, |E|^2')
    plt.title("focusing peropties of a binary-phase zone plate (extended cell)")
    plt.tight_layout()
    plt.savefig("zone_plate_extended_cell_farfields_zn{}.png".format(zN))

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 9, 2020

If they are only proportional, not nearly identical, then something is wrong. What is the proportionality factor?

@oskooi
Copy link
Collaborator

oskooi commented Jan 9, 2020

The far fields from the near2far calculation are ~68X larger than the brute-force calculation. This discrepancy could be related to the fact that the energy density is computed differently in the two calculations: (1) |Ex|2+|Ey|2+|Ez|2 vs. (2) |Er|2+|Eφ|2+|Ez|2.

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 9, 2020

This discrepancy could be related to the fact that the energy density is computed differently in the two calculations

No, that shouldn't cause any difference bigger than discretization errors.

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 9, 2020

I noticed that a factor of 2π was being included twice, so in the energy you would have gotten an extra factor of (2π)². Can you try it again with the updated PR?

@oskooi
Copy link
Collaborator

oskooi commented Jan 9, 2020

There is a bug in commit rm redundant 2pi factor which produces just zeroes for the far fields.

@stevengj
Copy link
Collaborator Author

For testing purposes, I would use a much smaller cell, e.g. predicting the fields 10µm away.

Not only will this be a lot faster, but it should also be a lot easier to get good agreement — when you propagate for a really long distance in a brute-force calculations, numerical dispersion effects become larger and larger, so you will probably need higher resolution to get agreement for a large cell.

@oskooi
Copy link
Collaborator

oskooi commented Jan 10, 2020

Even with a smaller cell (focal length of 10 µm rather than 200 µm), the near2far fields are ~1.8X larger than the brute force calculation (though the overlap of the peaks is improved due to the reduced effects of numerical dispersion in the smaller cell). Results are converged with resolution which suggests that the discrepancy is likely due to a missing constant factor somewhere in greencyl or dft_near2far::get_farfields_array.

zone_plate_compare_farfields_zn15_res50

@stevengj
Copy link
Collaborator Author

Can you try a point with r > 0?

@oskooi
Copy link
Collaborator

oskooi commented Jan 11, 2020

The discrepancy between the two methods becomes larger for r > 0 as shown below for r=0.2 and r=4.6.

zone_plate_r0 2_compare_farfields_zn15_res50

zone_plate_r4 6_compare_farfields_zn15_res50

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 11, 2020

Okay, so it's not a constant factor at all, it's some more nontrivial bug. 😿

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 14, 2020

Not sure what the problem here is.

One thing to try would be an m=0 test case (e.g. in case I am using an inconsistent sign for the exp(imφ) term). I would just use vacuum to keep it as simple as possible.

You could also try to increase N0 just to enforce more accuracy. e.g. set N0 = 256 and then loop for (int N = N0; N <= N0; N *= 2) {

(The basic problem here is that there are a lot of sign errors and things that would still give qualitatively reasonable results.)

@stevengj
Copy link
Collaborator Author

Note: as the point where you are trying to compute the fields gets closer to the near2far surface, the computation time will get larger because the required N (the number of φ integration points) increases. This is a well known effect because the φ integrand a Green's function that diverges as the evaluation point approaches the source point.

There are many ways to mitigate this, and in fact there is a whole literature on this sort of integral because it arises for integral-equation methods (like Homer's SCUFF code), but they will all be much more complicated than what we are doing now. I doubt that it is worth it because — once we get this debugged — the practical application of the near2far code is to calculate the field at far-away points, not at nearby points.

@oskooi
Copy link
Collaborator

oskooi commented Jan 15, 2020

The test involving m=0 and an Eφ source produces an unexpected result which may provide some clues for debugging: in the near2far calculation, the far fields Er (or equivalently Ex) and Ez are non zero (which is incorrect) whereas in the brute-force calculation they are zero (expected). The reason the far fields Er and Ez are non zero seems to be related to a nan value at r=0 for the DFT near field Eφ:

>>> sim.get_dft_array(n2f_obj,mp.Er,0).shape
(127,)
>>> sim.get_dft_array(n2f_obj,mp.Ep,0).shape
(126,)
>>> sim.get_dft_array(n2f_obj,mp.Ez,0).shape
()
>>> sim.get_dft_array(n2f_obj,mp.Er,0)[:5]
array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j])
>>> sim.get_dft_array(n2f_obj,mp.Ep,0)[:5]
array([       nan       +nanj, 0.10197228-0.13763517j,
       0.20317604-0.27240901j, 0.30281877-0.40152707j,
       0.40006188-0.5223275j ])

The same is true for the magnetic fields: Hr is nan at r=0:

>>> sim.get_dft_array(n2f_obj,mp.Hr,0).shape
(126,)
>>> sim.get_dft_array(n2f_obj,mp.Hp,0).shape
(127,)
>>> sim.get_dft_array(n2f_obj,mp.Hz,0).shape
()
>>> sim.get_dft_array(n2f_obj,mp.Hr,0)[:5]
array([        nan       +nanj, -0.09697845+0.12614261j,
       -0.19315487+0.24966255j, -0.28770817+0.36799967j,
       -0.3797808 +0.47871785j])
>>> sim.get_dft_array(n2f_obj,mp.Hp,0)[:5]
array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j])

The brute-force DFT fields are as expected:

>>> sim.get_dft_array(dft_obj,mp.Er,0).shape
(127,)
>>> sim.get_dft_array(dft_obj,mp.Ep,0).shape
(127,)
>>> sim.get_dft_array(dft_obj,mp.Ez,0).shape
(127,)
>>> np.sum(np.absolute(sim.get_dft_array(dft_obj,mp.Er,0))**2)
0.0
>>> np.sum(np.absolute(sim.get_dft_array(dft_obj,mp.Ep,0))**2)
7.29380755877031
>>> np.sum(np.absolute(sim.get_dft_array(dft_obj,mp.Ez,0))**2)
0.0

summary: in the near2far calculation, the far fields are not being comptued correctly for the case of m=0 and an Eφ source because of a nan value for the DFT near fields Eφ and Hr at r=0.

addendum: Eφ and Hr at r=0 are also nan for the m=-1 test case involving the zone plate.

empty_cyl_compare_farfields_res25_fd20

import meep as mp
import numpy as np
import math

import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt

## run 1: near2far calculation                                                                                                                                                              

resolution = 25             # pixels/um                                                                                                                                                     

dpml = 1.0                  # PML thickness                                                                                                                                                 
pml_layers = [mp.PML(thickness=dpml)]

wvl_cen = 0.5
frq_cen = 1/wvl_cen

r = 5
z = 8
sr = r+dpml
sz = dpml+z+dpml
cell_size = mp.Vector3(sr,0,sz)

sources = [mp.Source(mp.GaussianSource(frq_cen,fwidth=0.2*frq_cen),
                     component=mp.Ep,
                     center=mp.Vector3(0.5*(sr-dpml),0,-0.5*sz+dpml),
                     size=mp.Vector3())]

sim = mp.Simulation(cell_size=cell_size,
                    boundary_layers=pml_layers,
                    resolution=resolution,
                    sources=sources,
                    dimensions=mp.CYLINDRICAL,
                    m=0)

n2f_obj = sim.add_near2far(frq_cen, 0, 1, mp.Near2FarRegion(center=mp.Vector3(0.5*(sr-dpml),0,0.5*sz-dpml),size=mp.Vector3(sr-dpml)))

sim.run(until_after_sources=100)

ff_distance = 20
ff = sim.get_farfields(n2f_obj, resolution, center=mp.Vector3(sr-dpml-0.5*(sr-dpml),0,-0.5*sz+dpml+z+ff_distance), size=mp.Vector3(sr-dpml))

E2_n2f = np.absolute(ff['Ex'])**2+np.absolute(ff['Ey'])**2+np.absolute(ff['Ez'])**2

sim.reset_meep()

## run 2: brute-force calculation                                                                                                                                                           

sz = dpml+z+ff_distance+dpml
cell_size = mp.Vector3(sr,0,sz)

sources = [mp.Source(mp.GaussianSource(frq_cen,fwidth=0.2*frq_cen),
                     component=mp.Ep,
                     center=mp.Vector3(0.5*(sr-dpml),0,-0.5*sz+dpml),
                     size=mp.Vector3())]

sim = mp.Simulation(cell_size=cell_size,
                    boundary_layers=pml_layers,
                    resolution=resolution,
                    sources=sources,
                    dimensions=mp.CYLINDRICAL,
                    m=0)

dft_obj = sim.add_dft_fields([mp.Er,mp.Ep,mp.Ez], frq_cen, frq_cen, 1, center=mp.Vector3(0.5*(sr-dpml),0,-0.5*sz+dpml+z+ff_distance), size=mp.Vector3(sr-dpml))

sim.run(until_after_sources=200)

E2_dft = np.absolute(sim.get_dft_array(dft_obj,mp.Er,0))**2+np.absolute(sim.get_dft_array(dft_obj,mp.Ep,0))**2+np.absolute(sim.get_dft_array(dft_obj,mp.Ez,0))**2

if mp.am_master():
    plt.figure(dpi=200)
    plt.semilogy(np.linspace(0,sr-dpml,len(E2_n2f)),E2_n2f,'bo-',label="near2far")
    plt.semilogy(np.linspace(0,sr-dpml,len(E2_dft)),E2_dft,'ro-',label="extended cell")
    plt.grid(True,axis="y",which="both",ls="-")
    plt.legend(loc='upper right')
    plt.xlabel('r coordinate (μm)')
    plt.ylabel(r'energy density of far-field electric fields, |E|$^2$')
    plt.title("empty cell in cylindrical coordinates with m=0")
    plt.tight_layout()
    plt.savefig("empty_cyl_compare_farfields_res{}_fd{}.png".format(resolution,ff_distance))

@stevengj
Copy link
Collaborator Author

The NaN is arising just from get_dft_array and should be unrelated to the far-field discrepancy.

The issue is that the DFT weight includes the dV factor, and get_dft_array divides by dV to return the field. But at r=0, the dV factor (which includes 2πr in cylindrical coordinates) is zero, so it is doing 0/0 which produces a NaN.

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 17, 2020

The next thing we might try is to test the greencyl function at a low level to make sure it is correct. To do that:

  1. Compute a brute-force Green's function: in a vacuum cylindrical simulation, put (say) an Ep source at some point and calculate the steady-state complex fields from a CW source.

  2. Compare the resulting fields (except right at the source point where there is a divergence) with the output of greencyl for the same source. You'll probably have to write a little C++ routine to evaluate greencyl on a grid of points x — we don't want the whole near2far transformation here, just the greencyl from a single source current (a single x0). e.g. try m=0 and look at Ep.

Ideally they should be the same (up to an overall normalization).

@oskooi
Copy link
Collaborator

oskooi commented Jan 19, 2020

The following are the results for the Green's function for ε=2 everywhere computed in C++ using (1) the brute-force approach via the CW solver and (2) directly from greencyl. The schematic below shows the simulation setup as well as the real and imaginary parts of Ep for m=0 computed using the brute-force approach. The far fields are computed at 10 equally-spaced points along a line in the radial direction that is 20 wavelengths away from the source. The ratio of the magnitude of Ep computed using the two methods is not a constant factor; it varies sinusoidally along the radial direction (as shown in the final plot below).

One thing to note: Ep at r=0 computed using the brute-force approach is zero (correct) but non-zero with greencyl. This is shown as the third and fourth columns of data on the first row of the output lines prefixed by green:,. This seems to be clearly an error.

Ep_comparison_real_imag

#include <stdio.h>
#include <stdlib.h>

#include <meep.hpp>
using namespace meep;
using std::complex;
using std::polar;

double two(const vec &) { return 2.0; }

int check_greencyl(double sr, double sz, double res, component c0, double m, double tol) {
  const double dpml = 2.0;
  grid_volume gv = volcyl(sr+dpml, dpml+sz+dpml, res);
  gv.center_origin();
  structure s(gv, two, pml(dpml));
  fields f(&s, m);
  double w = 0.5;
  continuous_src_time src(w);
  vec x0 = veccyl(0.5*sr,-0.5*sz);
  f.add_point_source(c0, src, x0);
  f.solve_cw(1e-6);
  f.output_hdf5(c0, gv.surroundings());

  const int N = 10;
  double dr = sr / N;
  complex<double> F[N], F0[N], EH[6];
  complex<double> phase = polar(1.0, (4 * w * f.dt) * pi);
  for (int i = 0; i < N; ++i) {
    double rr = dr * i;
    vec x = veccyl(rr, 0.5*sz-0.1);
    F[i] = f.get_field(Ep, x) * phase;
    greencyl(EH, x, w, 2.0, 1.0, x0, c0, 1.0, m, tol);
    F0[i] = EH[1]; // Ey = Ep for \phi = 0 (rz = xz) plane                                                                                                                     
    master_printf("green:, %0.2f, %0.10f, %0.10f, %0.10f\n",rr,abs(F[i]),abs(F0[i]),abs(F[i])/abs(F0[i]));
  }
  return 0;
}

int main(int argc, char **argv) {
  initialize mpi(argc, argv);
  check_greencyl(5.0, 10.0, 40.0, Ep, 0, 1e-6);
  return 0;
}

output

...
...
...
residual[751] = 5.62449e-05
on time step 3112 (time=38.9), 0.026935 s/step
residual[789] = 8.70952e-05
on time step 3260 (time=40.75), 0.0270559 s/step
residual[826] = 2.15682e-05
on time step 3409 (time=42.6125), 0.0269163 s/step
residual[864] = 1.03799e-06
Finished solve_cw after 3461 steps and 865 CG iters.
green:, 0.00, 0.0000000000, 0.0205975247, 0.0000000000
green:, 0.50, 0.1016346654, 0.0206550932, 4.9205619451
green:, 1.00, 0.1788617140, 0.0205583338, 8.7002047676
green:, 1.50, 0.2173186486, 0.0203082885, 10.7009829415
green:, 2.00, 0.2132532677, 0.0199129993, 10.7092490166
green:, 2.50, 0.1720173216, 0.0193869437, 8.8728437286
green:, 3.00, 0.1067063428, 0.0187497817, 5.6910712059
green:, 3.50, 0.0337553537, 0.0180246415, 1.8727337110
green:, 4.00, 0.0325202320, 0.0172362019, 1.8867400258
green:, 4.50, 0.0806650610, 0.0164088465, 4.9159495048

ratio_Ep_vs_r

@oskooi
Copy link
Collaborator

oskooi commented Jan 20, 2020

Four additional notes for debugging:

  1. in the brute-force calculation, f.get_field(E,x) = 0 for E=Er,Ey,Ez. E=Ep is the only field component which produces non-zero values. These results are correct.

  2. computing the far fields at the 3d Cartesian point vec x = vec(rr, 0, 0.5*sz-0.1) instead of the Cylindrical coordinate veccyl(rr, 0.5*sz-0.1) produces identical values of Ep for get_field (brute-force calculation) but not for greencyl. This probably means that for a Cylindrical grid, the points passed to get_field must also be in Cylindrical coordinates. Might be useful for generality to also support passing Cartesian points to get_field (since this is the case for greencyl).

  3. for the greencyl calculation, the far fields Ex, Ey, and Ez (i.e. EH[0], EH[1], and EH[2]) are all non zero. This is incorrect. Since m=0, only Ey (equivalent to Ep) should be nonzero whereas Ex (equivalent to Er) and Ez should be zero.

  4. increasing the distance of the far-field points from the source (i.e. approaching the Fraunhofer limit) produces no change in the results. Shown below is the imaginary part of Ep from the brute-force calculation for sz = 50.0 (25 times the wavelength). The wavefront is mostly planar at the monitor points in the far field (near the right edge of the cell). The ratio of the magnitude of Ep computed using the brute-force calculation and greencyl is still sinusoidal.

epi_sz50

@stevengj
Copy link
Collaborator Author

I can confirm that the fields it's returning for an Ep source with m=0 have nonzero r and z components. This means that it is violating a symmetry somewhere, which is good because it should be easier to debug than a mere quantitative error. Test program:

#include "meep_internals.hpp"
#include <complex>
#include <stdio.h>

using namespace meep;
using namespace std;

int main(void) {
    std::complex<double> EH[6];
    vec x0 = veccyl(1,2), x = veccyl(2,5);
    double freq = 0.5;
    greencyl(EH, x, freq, 1.0, 1.0, x0, Ep, 1.0, 0.0, 1e-6);
    printf("Er=%g%+gi, Ep=%g%+gi, Ez=%g%+gi\n",
        real(EH[0]), imag(EH[0]),
        real(EH[1]), imag(EH[1]),
        real(EH[2]), imag(EH[2]));
    return 0;
}

@stevengj
Copy link
Collaborator Author

Just pushed a fix to the symmetry — was a simple bug where I was evaluating at the wrong angles.

@oskooi
Copy link
Collaborator

oskooi commented Jan 21, 2020

Seems to be working now: the ratio of the magnitude of Ep from the brute-force calculation and greencyl (last column in the output below) are now just different by a constant factor for all points along the monitor line (except at r=0 where Ep is 0):

final residual = 7.90639e-07
Finished solve_cw after 4245 steps and 1061 CG iters.
green:, 0.00, 0.0000000000, 0.0000000000, 0.0000000000
green:, 0.50, 0.0278338781, 0.0017083401, 16.2929373299
green:, 1.00, 0.0528503243, 0.0033133093, 15.9509180009
green:, 1.50, 0.0742473821, 0.0047200491, 15.7302139907
green:, 2.00, 0.0918065821, 0.0058497796, 15.6940239770
green:, 2.50, 0.1043282940, 0.0066456107, 15.6988273055
green:, 3.00, 0.1108035678, 0.0070760402, 15.6589794746
green:, 3.50, 0.1116704519, 0.0071359013, 15.6491026651
green:, 4.00, 0.1079766682, 0.0068448470, 15.7748841573
green:, 4.50, 0.0996746781, 0.0062437524, 15.9639062278

@stevengj
Copy link
Collaborator Author

Great! I'm expecting a constant factor in this test because the normalizations are different. The greencyl function has an implicit 2πr factor, and the CW solver might imply some other scaling factor from the Fourier transform?

@oskooi
Copy link
Collaborator

oskooi commented Jan 21, 2020

Here is the output of the real and imaginary parts of the ratio obtained using:

master_printf("green:, %0.2f, %g%+gi, %g%+gi, %g%+gi\n",rr,real(F[i]),imag(F[i]),real(F0[i]),imag(F0[i]),real(F[i]/F0[i]),imag(F[i]/F0[i]));
Finished solve_cw after 4305 steps and 1076 CG iters.
green:, 0.00, 0+0i, 8.67362e-19-2.1684e-19i, 0+0i
green:, 0.50, 0.011867+0.0941662i, 0.000737903+0.00636039i, 14.822-0.146174i
green:, 1.00, -0.00497851+0.175209i, -0.00039671+0.0113527i, 15.4297-0.100646i
green:, 1.50, -0.0606265+0.211026i, -0.00388816+0.0133006i, 15.8444-0.0736255i
green:, 2.00, -0.127189+0.174304i, -0.00800932+0.0109673i, 15.8886-0.00621644i
green:, 2.50, -0.150979+0.0823817i, -0.00957674+0.00524395i, 15.7524+0.0232955i
green:, 3.00, -0.104547-0.00146232i, -0.00674863-0.000130717i, 15.49-0.0833483i
green:, 3.50, -0.0278262-0.0168489i, -0.00183844-0.00108603i, 15.2336+0.165733i
green:, 4.00, -0.00159477+0.0306794i, -7.77893e-05+0.00211178i, 14.5358+0.219738i
green:, 4.50, -0.0561902+0.0582896i, -0.00351375+0.00384988i, 15.5273+0.423679i

@stevengj
Copy link
Collaborator Author

stevengj commented Jan 21, 2020

In particular, the greencyl function is actually divided by a factor of 2πr where r is the source location, in order to compensate for a corresponding factor in the near2far Fourier transforms. In your case, this comes to a factor of 2pi*sr / 2 = pi*5 = 15.707963267948966, very close to what you are seeing. So, can you set F0[i] = EH[1] * 2*pi * x0.r()?

(Your imaginary parts look small, around 2% of the real parts, so I think that is just a discretization error in the phase, which is expected.)

@oskooi
Copy link
Collaborator

oskooi commented Jan 21, 2020

Changing to F0[i] = EH[1] * 2.0*pi*sr/2.0; results in:

final residual = 9.8757e-07
Finished solve_cw after 4305 steps and 1076 CG iters.
green:, 0.00, 0+0i, 1.36245e-17-3.40612e-18i, 0+0i
green:, 0.50, 0.011867+0.0941662i, 0.0115909+0.0999088i, 0.943601-0.00930574i
green:, 1.00, -0.00497851+0.175209i, -0.0062315+0.178328i, 0.982286-0.00640734i
green:, 1.50, -0.0606265+0.211026i, -0.0610751+0.208925i, 1.00869-0.00468715i
green:, 2.00, -0.127189+0.174304i, -0.12581+0.172274i, 1.0115-0.000395751i
green:, 2.50, -0.150979+0.0823817i, -0.150431+0.0823718i, 1.00283+0.00148304i
green:, 3.00, -0.104547-0.00146232i, -0.106007-0.0020533i, 0.986126-0.00530612i
green:, 3.50, -0.0278262-0.0168489i, -0.0288782-0.0170594i, 0.969804+0.0105509i
green:, 4.00, -0.00159477+0.0306794i, -0.00122191+0.0331717i, 0.92538+0.013989i
green:, 4.50, -0.0561902+0.0582896i, -0.0551939+0.0604737i, 0.9885+0.0269723i

@oskooi
Copy link
Collaborator

oskooi commented Jan 21, 2020

As an additional check, the m=1 case is also working now (even though the CW convergence failed):

on time step 7022 (time=87.775), 0.0181224 s/step
residual[1766] = 0.00392548
Finished solve_cw after 7243 steps and 1811 CG iters.
 -- CONVERGENCE FAILURE (-1) in solve_cw!
green:, 0.00, -2.0007-0.673643i, -0.189615+0.0317195i, 9.68605+5.17301i
green:, 0.50, -0.17025+0.0214035i, -0.170037+0.0200355i, 1.00217-0.00778974i
green:, 1.00, -0.115337-0.00493316i, -0.113824-0.00280147i, 1.01375+0.0183897i
green:, 1.50, -0.0373103-0.0094057i, -0.034639-0.007555i, 1.08474+0.0349461i
green:, 2.00, 0.0326882+0.0309186i, 0.0337597+0.0297006i, 1+0.0360767i
green:, 2.50, 0.0487616+0.101036i, 0.0469339+0.0984965i, 1.02822-0.00510991i
green:, 3.00, -0.0104857+0.146286i, -0.012685+0.145544i, 1.00375-0.0154379i
green:, 3.50, -0.099875+0.114473i, -0.100089+0.115442i, 0.99429+0.00309618i
green:, 4.00, -0.133455+0.0185495i, -0.131851+0.0188449i, 1.01161+0.00389992i
green:, 4.50, -0.0787489-0.0573579i, -0.0762103-0.0578588i, 1.01797-0.020212i

@oskooi
Copy link
Collaborator

oskooi commented Jan 22, 2020

The far fields at the focal length of the binary-phase zone plate example are also now showing good agreement for a monitor line along both r and z as shown below.

zone_plate_r_compare_farfields_zn15_res40_fl20

zone_plate_z_compare_farfields_zn15_res80_fl20

@stevengj
Copy link
Collaborator Author

I can merge this now as an undocumented new feature if you want, and then you can make a separate PR with a test and documentation?

@oskooi
Copy link
Collaborator

oskooi commented Jan 22, 2020

Yes, we can merge this now.

@stevengj stevengj merged commit c48c32c into master Jan 22, 2020
@stevengj stevengj deleted the near2far_cyl branch January 22, 2020 03:54
bencbartlett pushed a commit to bencbartlett/meep that referenced this pull request Sep 9, 2021
* initial stab at a greencyl function

* whoops

* formatting

* use meep::pi

* fix get_farfields_array in cylindrical coords

* slight code cleanup

* rm redundant 2pi factor

* smaller N0

* add some comments to greencyl

* typo in comment

* whoops, 2/N is integer division in C

* add green functions to meep internals header in case we want to export them some day, and to make testing easier

* whoops, fix evaluation angles

* clarify normalization of greencyl
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.

near to far field transformation for cylindrical coordinates
2 participants