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

unit test and documentation for MaterialGrid #1508

Merged
merged 4 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions doc/docs/Python_User_Interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -4156,6 +4156,85 @@ media](FAQ.md#why-does-my-simulation-diverge-if-0).

</div>

---
<a id="MaterialGrid"></a>

### MaterialGrid

```python
class MaterialGrid(object):
```

<div class="class_docstring" markdown="1">

This class is used to specify materials on a discrete Cartesian grid. A class object is passed as the `material`
argument of a [`GeometricObject`](#GeometricObject) or the `default_material` argument of the `Simulation`
constructor (similar to a material function).

</div>



<a id="MaterialGrid.__init__"></a>

<div class="class_members" markdown="1">

```python
def __init__(self,
grid_size,
medium1,
medium2,
design_parameters=None,
grid_type='U_DEFAULT',
do_averaging=False,
beta=0,
eta=0.5):
```

<div class="method_docstring" markdown="1">

Creates a `MaterialGrid` object.

The input are two materials `medium1` and `medium2` which are linearly interpolated at each grid point using
a NumPy array `design_parameters` of size `grid_size` (a 3-tuple or `Vector3` of integers) with floating-point values in
the range [0,1] used to define a linear weight (i.e., 0 is `medium1` and 1 is `medium2`). Currently, only two material
types are supported: (1) frequency-independent isotropic $\varepsilon$ or $\mu$ and (2) `LorentzianSusceptibility`.
`medium1` and `medium2` must both be the same type.

[Subpixel smoothing](Subpixel_Smoothing.md) can be enabled by specifying `do_averaging=True`. To improve the accuracy
stevengj marked this conversation as resolved.
Show resolved Hide resolved
of the subpixel smoothing, a [level set](https://en.wikipedia.org/wiki/Level_set) of the grid can be generated by applying
a projection operator to the grid values after Meep has bilinearly interpolated the user-specified grid onto the
[Yee grid](Yee_Lattice.md). The projection is based on a hyperbolic tangent function and is specified using the parameters `beta`
stevengj marked this conversation as resolved.
Show resolved Hide resolved
($\beta$: "smoothness" of the turn on) and `eta` ($\eta$: contraction/dilation). The level set provides a general approach for
stevengj marked this conversation as resolved.
Show resolved Hide resolved
defining a *discontinuous* representation of the otherwise continuously varying (via the bilinear interpolation) grid values.
The subpixel smoothing is based on an adaptive quadrature scheme with properties `subpixel_maxeval` and `subpixel_tol` which
can be specified using the [`Simulation`](#Simulation) constructor.

Grids which are symmetric (e.g., mirror, rotation) must be explicitly defined. This requires overlapping a given
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/This requires/One way to implement this is by/

`MaterialGrid` object with a symmetrized copy of itself. In this case, the overlapping grid points are combined using
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the case of spatially overlapping MaterialGrid objects (with no intervening objects), any overlapping points are computed using

the method `grid_type` which is one of `"U_MIN"` (minimum of two grid values), `"U_PROD"` (product), `"U_SUM"` (sum),
Copy link
Collaborator

Choose a reason for hiding this comment

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

minimum of the overlapping grid values

Copy link
Collaborator

Choose a reason for hiding this comment

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

U_SUM is the mean, not the sum

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe it should be renamed to U_MEAN

`"U_DEFAULT"` (default material at grid point).
stevengj marked this conversation as resolved.
Show resolved Hide resolved

</div>

</div>

<a id="MaterialGrid.update_parameters"></a>

<div class="class_members" markdown="1">

```python
def update_parameters(self, x):
```

<div class="method_docstring" markdown="1">

Reset the design grid `design_parameters` to `x`.

</div>

</div>

---
<a id="Susceptibility"></a>

Expand Down
2 changes: 2 additions & 0 deletions doc/docs/Python_User_Interface.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,8 @@ The following classes are available directly via the `meep` package.

@@ Medium[methods-with-docstrings] @@

@@ MaterialGrid[methods-with-docstrings] @@

@@ Susceptibility[methods-with-docstrings] @@

@@ LorentzianSusceptibility[methods-with-docstrings] @@
Expand Down
31 changes: 31 additions & 0 deletions python/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,35 @@ def _get_epsmu(self, diag, offdiag, susceptibilities, conductivity_diag, conduct
return np.squeeze(epsmu)

class MaterialGrid(object):
"""
This class is used to specify materials on a discrete Cartesian grid. A class object is passed as the `material`
argument of a [`GeometricObject`](#GeometricObject) or the `default_material` argument of the `Simulation`
constructor (similar to a material function).
"""
def __init__(self,grid_size,medium1,medium2,design_parameters=None,grid_type="U_DEFAULT",do_averaging=False,beta=0,eta=0.5):
"""
Creates a `MaterialGrid` object.

The input are two materials `medium1` and `medium2` which are linearly interpolated at each grid point using
a NumPy array `design_parameters` of size `grid_size` (a 3-tuple or `Vector3` of integers) with floating-point values in
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe call it weights and a "weight function"

the range [0,1] used to define a linear weight (i.e., 0 is `medium1` and 1 is `medium2`). Currently, only two material
types are supported: (1) frequency-independent isotropic $\\varepsilon$ or $\\mu$ and (2) `LorentzianSusceptibility`.
`medium1` and `medium2` must both be the same type.

[Subpixel smoothing](Subpixel_Smoothing.md) can be enabled by specifying `do_averaging=True`. To improve the accuracy
of the subpixel smoothing, a [level set](https://en.wikipedia.org/wiki/Level_set) of the grid can be generated by applying
a projection operator to the grid values after Meep has bilinearly interpolated the user-specified grid onto the
[Yee grid](Yee_Lattice.md). The projection is based on a hyperbolic tangent function and is specified using the parameters `beta`
($\\beta$: "smoothness" of the turn on) and `eta` ($\\eta$: contraction/dilation). The level set provides a general approach for
defining a *discontinuous* representation of the otherwise continuously varying (via the bilinear interpolation) grid values.
The subpixel smoothing is based on an adaptive quadrature scheme with properties `subpixel_maxeval` and `subpixel_tol` which
can be specified using the [`Simulation`](#Simulation) constructor.

Grids which are symmetric (e.g., mirror, rotation) must be explicitly defined. This requires overlapping a given
`MaterialGrid` object with a symmetrized copy of itself. In this case, the overlapping grid points are combined using
the method `grid_type` which is one of `"U_MIN"` (minimum of two grid values), `"U_PROD"` (product), `"U_SUM"` (sum),
`"U_DEFAULT"` (default material at grid point).
"""
self.grid_size = mp.Vector3(*grid_size)
self.medium1 = medium1
self.medium2 = medium2
Expand Down Expand Up @@ -558,6 +586,9 @@ def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):

self.swigobj = None
def update_parameters(self,x):
"""
Reset the design grid `design_parameters` to `x`.
"""
if x.size != self.num_params:
raise ValueError("design_parameters of shape {} do not match user specified grid dimension: {}".format(self.design_parameters.size,self.grid_size))
self.design_parameters[:]=x.flatten().astype(np.float64)
Expand Down
130 changes: 74 additions & 56 deletions python/tests/material_grid.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,81 @@
import meep as mp
import numpy as np
from meep.materials import Si, SiO2
from scipy.ndimage import gaussian_filter
import unittest

class TestMaterialGrid(unittest.TestCase):
def gen_sim(self):
resolution = 10 # pixels/um
cell_size = mp.Vector3(14,14)
pml_layers = [mp.PML(thickness=2)]
w = 3.0 # width of waveguide
m1 = SiO2
m2 = Si
n = 10
gs = mp.Vector3(n,n)
np.random.seed(1)
dp = np.random.rand(n*n)
mg = mp.MaterialGrid(gs,m1,m2,dp,"U_SUM")
geometry = []
rot_angle = np.radians(0)
geometry += [mp.Block(center=mp.Vector3(),
size=mp.Vector3(w,w,mp.inf),
e1=mp.Vector3(x=1).rotate(mp.Vector3(z=1), rot_angle),
e2=mp.Vector3(y=1).rotate(mp.Vector3(z=1), rot_angle),
material=mg)]
geometry += [mp.Block(center=mp.Vector3(),
size=mp.Vector3(w,w,mp.inf),
e1=mp.Vector3(x=-1).rotate(mp.Vector3(z=1), np.pi/2),
e2=mp.Vector3(y=1).rotate(mp.Vector3(z=1), np.pi/2),
material=mg)]
fsrc = 1/1.55 # frequency of eigenmode or constant-amplitude source
bnum = 1 # band number of eigenmode
kpoint = mp.Vector3(x=1).rotate(mp.Vector3(z=1), rot_angle)
compute_flux = True # compute flux (True) or plot the field profile (False)
eig_src = True # eigenmode (True) or constant-amplitude (False) source
if eig_src:
sources = [mp.EigenModeSource(src=mp.GaussianSource(fsrc,fwidth=0.2*fsrc) if compute_flux else mp.ContinuousSource(fsrc),
center=mp.Vector3(),
size=mp.Vector3(y=3*w),
direction=mp.NO_DIRECTION,
eig_kpoint=kpoint,
eig_band=bnum,
eig_parity=mp.EVEN_Y+mp.ODD_Z if rot_angle == 0 else mp.ODD_Z,
eig_match_freq=True)]
else:
sources = [mp.Source(src=mp.GaussianSource(fsrc,fwidth=0.2*fsrc) if compute_flux else mp.ContinuousSource(fsrc),
center=mp.Vector3(),
size=mp.Vector3(y=3*w),
component=mp.Ez)]

sim = mp.Simulation(cell_size=cell_size,
resolution=resolution,
boundary_layers=pml_layers,
def compute_resonant_mode(res):
cell_size = mp.Vector3(1,1,0)

rad = 0.301943

fcen = 0.3
df = 0.2*fcen
sources = [mp.Source(mp.GaussianSource(fcen,fwidth=df),
component=mp.Hz,
center=mp.Vector3(-0.1057,0.2094,0))]

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

design_shape = mp.Vector3(1,1,0)
design_region_resolution = 50
Nx = int(design_region_resolution*design_shape.x)
Ny = int(design_region_resolution*design_shape.y)
x = np.linspace(-0.5*design_shape.x,0.5*design_shape.x,Nx)
y = np.linspace(-0.5*design_shape.y,0.5*design_shape.y,Ny)
xv, yv = np.meshgrid(x,y)
design_params = np.sqrt(np.square(xv) + np.square(yv)) < rad
filtered_design_params = gaussian_filter(design_params,
sigma=3.0,
output=np.double)

matgrid = mp.MaterialGrid(mp.Vector3(Nx,Ny),
mp.air,
mp.Medium(index=3.5),
design_parameters=filtered_design_params,
do_averaging=True,
beta=1000,
eta=0.5)

geometry = [mp.Block(center=mp.Vector3(),
size=mp.Vector3(design_shape.x,design_shape.y,0),
material=matgrid)]

sim = mp.Simulation(resolution=res,
cell_size=cell_size,
geometry=geometry,
sources=sources,
extra_materials = [m1,m2],
geometry=geometry
)
return sim

def test_eval(self):
sim = self.gen_sim()
k_point=k_point)

h = mp.Harminv(mp.Hz, mp.Vector3(0.3718,-0.2076), fcen, df)
sim.run(mp.after_sources(h),
until_after_sources=200)

try:
for m in h.modes:
print("harminv:, {}, {}, {}".format(res,m.freq,m.Q))
freq = h.modes[0].freq
except:
print("No resonant modes found.")

sim.reset_meep()
return freq

class TestMaterialGrid(unittest.TestCase):

def test_material_grid(self):
## reference frequency computed using MaterialGrid at resolution = 300
freq_ref = 0.3068839373003908

res = [25, 50]
freq_matgrid = []
for r in res:
freq_matgrid.append(compute_resonant_mode(r))
## verify that the resonant mode is approximately equivalent to
## the reference value
self.assertAlmostEqual(freq_ref, freq_matgrid[-1], 2)

## verify that the relative error is decreasing with increasing resolution
self.assertLess(abs(freq_matgrid[1]-freq_ref),abs(freq_matgrid[0]-freq_ref))
Copy link
Collaborator

Choose a reason for hiding this comment

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

We really should check that it's less by some factor, say by a factor of 3 (ideally 4, but…)


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