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

Anisotropic material gradient support #1801

Merged
merged 14 commits into from
Nov 3, 2021
7 changes: 4 additions & 3 deletions python/adjoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@

from .objective import *

from . import utils
from .utils import DesignRegion

from .basis import BilinearInterpolationBasis

from .optimization_problem import (OptimizationProblem, Grid, DesignRegion)
from .optimization_problem import (OptimizationProblem)

from .filter_source import FilteredSource

from .filters import *

from .connectivity import *

from . import utils

try:
from .wrapper import MeepJaxWrapper
except ModuleNotFoundError as _:
Expand Down
3 changes: 2 additions & 1 deletion python/adjoint/objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import numpy as np
import meep as mp
from .filter_source import FilteredSource
from .optimization_problem import Grid
from meep.simulation import py_v3_to_vec
from collections import namedtuple

Grid = namedtuple('Grid', ['x', 'y', 'z', 'w'])

class ObjectiveQuantity(abc.ABC):
"""A differentiable objective quantity.
Expand Down
128 changes: 24 additions & 104 deletions python/adjoint/optimization_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,7 @@
from autograd import grad, jacobian
from collections import namedtuple

Grid = namedtuple('Grid', ['x', 'y', 'z', 'w'])
YeeDims = namedtuple('YeeDims', ['Ex', 'Ey', 'Ez'])


class DesignRegion(object):
def __init__(
self,
design_parameters,
volume=None,
size=None,
center=mp.Vector3(),
MaterialGrid=None,
):
self.volume = volume if volume else mp.Volume(center=center, size=size)
self.size = self.volume.size
self.center = self.volume.center
self.design_parameters = design_parameters
self.num_design_params = design_parameters.num_params
self.MaterialGrid = MaterialGrid

def update_design_parameters(self, design_parameters):
self.design_parameters.update_weights(design_parameters)

def get_gradient(self, sim, fields_a, fields_f, frequencies):
for c in range(3):
fields_a[c] = fields_a[c].flatten(order='C')
fields_f[c] = fields_f[c].flatten(order='C')
fields_a = np.concatenate(fields_a)
fields_f = np.concatenate(fields_f)
num_freqs = np.array(frequencies).size

grad = np.zeros((num_freqs, self.num_design_params)) # preallocate

geom_list = sim.geometry
f = sim.fields
vol = sim._fit_volume_to_simulation(self.volume)
# compute the gradient
sim_is_cylindrical = (sim.dimensions == mp.CYLINDRICAL) or sim.is_cylindrical
mp._get_gradient(grad,fields_a,fields_f,vol,np.array(frequencies),geom_list,f, sim_is_cylindrical)

return np.squeeze(grad).T

from . import utils, DesignRegion

class OptimizationProblem(object):
"""Top-level class in the MEEP adjoint module.
Expand Down Expand Up @@ -76,9 +35,6 @@ def __init__(
):

self.sim = simulation
self.components = [mp.Ex,mp.Ey,mp.Ez]
if self.sim.is_cylindrical or self.sim.dimensions == mp.CYLINDRICAL:
self.components = [mp.Er,mp.Ep,mp.Ez]

if isinstance(objective_functions, list):
self.objective_functions = objective_functions
Expand Down Expand Up @@ -210,28 +166,9 @@ def prepare_forward_run(self):
self.forward_monitors.append(m.register_monitors(self.frequencies))

# register design region
self.design_region_monitors = [
self.sim.add_dft_fields(
self.components,
self.frequencies,
where=dr.volume,
yee_grid=True,
decimation_factor=self.decimation_factor,
) for dr in self.design_regions
]

# store design region voxel parameters
self.design_grids = []
if self.sim.is_cylindrical or self.sim.dimensions == mp.CYLINDRICAL:
YeeDims = namedtuple('YeeDims', ['Er','Ep','Ez'])
else:
YeeDims = namedtuple('YeeDims', ['Ex','Ey','Ez'])
for drm in self.design_region_monitors:
s = [
self.sim.get_array_slice_dimensions(c, vol=drm.where)[0]
for c in self.components
]
self.design_grids += [YeeDims(*s)]
self.design_region_monitors = utils.install_design_region_monitors(
self.sim, self.design_regions, self.frequencies, self.decimation_factor
)

def forward_run(self):
# set up monitors
Expand All @@ -254,16 +191,8 @@ def forward_run(self):
if len(self.f0) == 1:
self.f0 = self.f0[0]

# Store forward fields for each set of design variables in array (x,y,z,field_components,frequencies)
self.d_E = [[
np.zeros((self.nf, c[0], c[1], c[2]), dtype=np.complex128)
for c in dg
] for dg in self.design_grids]
for nb, dgm in enumerate(self.design_region_monitors):
for ic, c in enumerate(self.components):
for f in range(self.nf):
self.d_E[nb][ic][f, :, :, :] = atleast_3d(
self.sim.get_dft_array(dgm, c, f))
# Store forward fields for each set of design variables in array
self.d_E = utils.gather_design_region_fields(self.sim,self.design_region_monitors,self.frequencies)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the D field now, correct? Perhaps this var should be renamed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.


# store objective function evaluation in memory
self.f_bank.append(self.f0)
Expand All @@ -288,11 +217,14 @@ def adjoint_run(self):
# set up adjoint sources and monitors
self.prepare_adjoint_run()

if self.sim.is_cylindrical or self.sim.dimensions == mp.CYLINDRICAL:
# flip the m number
if utils._check_if_cylindrical(self.sim):
self.sim.m = -self.sim.m

# flip the k point
if self.sim.k_point:
self.sim.k_point *= -1

for ar in range(len(self.objective_functions)):
# Reset the fields
self.sim.reset_meep()
Expand All @@ -301,15 +233,9 @@ def adjoint_run(self):
self.sim.change_sources(self.adjoint_sources[ar])

# register design flux
self.design_region_monitors = [
self.sim.add_dft_fields(
self.components,
self.frequencies,
where=dr.volume,
yee_grid=True,
decimation_factor=self.decimation_factor,
) for dr in self.design_regions
]
self.design_region_monitors = utils.install_design_region_monitors(
self.sim, self.design_regions, self.frequencies, self.decimation_factor
)

# Adjoint run
self.sim.run(until_after_sources=mp.stop_when_dft_decayed(
Expand All @@ -318,25 +244,17 @@ def adjoint_run(self):
self.maximum_run_time
))

# Store adjoint fields for each design set of design variables in array (x,y,z,field_components,frequencies)
self.a_E.append([[
np.zeros((self.nf, c[0], c[1], c[2]), dtype=np.complex128)
for c in dg
] for dg in self.design_grids])
for nb, dgm in enumerate(self.design_region_monitors):
for ic, c in enumerate(self.components):
for f in range(self.nf):
if (self.sim.is_cylindrical or self.sim.dimensions == mp.CYLINDRICAL):
# Addtional factor of 2 for cyldrical coordinate
self.a_E[ar][nb][ic][f, :, :, :] = 2 * atleast_3d(self.sim.get_dft_array(dgm, c, f))
else:
self.a_E[ar][nb][ic][f, :, :, :] = atleast_3d(self.sim.get_dft_array(dgm, c, f))

if self.sim.is_cylindrical or self.sim.dimensions == mp.CYLINDRICAL:
# Store adjoint fields for each design set of design variables
self.a_E.append(utils.gather_design_region_fields(self.sim,self.design_region_monitors,self.frequencies))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is also the D field? Rename the var?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.


# reset the m number
if utils._check_if_cylindrical(self.sim):
self.sim.m = -self.sim.m

# update optimizer's state
# reset the k point
if self.sim.k_point: self.sim.k_point *= -1

# update optimizer's state
self.current_state = "ADJ"

def calculate_gradient(self):
Expand Down Expand Up @@ -486,7 +404,7 @@ def calculate_fd_gradient(

return fd_gradient, fd_gradient_idx

def update_design(self, rho_vector):
def update_design(self, rho_vector, beta=None):
"""Update the design permittivity function.

rho_vector ....... a list of numpy arrays that maps to each design region
Expand All @@ -497,6 +415,8 @@ def update_design(self, rho_vector):
"Each vector of design variables must contain only one dimension."
)
b.update_design_parameters(rho_vector[bi])
if beta:
b.update_beta(beta)

self.sim.reset_meep()
self.current_state = "INIT"
Expand Down
84 changes: 79 additions & 5 deletions python/adjoint/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,87 @@
import meep as mp
import numpy as onp

from . import ObjectiveQuantity, DesignRegion
from . import ObjectiveQuantity

# Meep field components used to compute adjoint sensitivities
_ADJOINT_FIELD_COMPONENTS = [mp.Ex, mp.Ey, mp.Ez]
_ADJOINT_FIELD_COMPONENTS = [mp.Dx, mp.Dy, mp.Dz]
_ADJOINT_FIELD_COMPONENTS_CYL = [mp.Dr, mp.Dp, mp.Dz]

# The frequency axis in the array returned by `mp._get_gradient()`
_GRADIENT_FREQ_AXIS = 1

# The returned axis order from get_dft_array in cylindrical coordinates
_FREQ_AXIS = 0
_RHO_AXIS = 2
_PHI_AXIS = 3
_Z_AXIS = 1

class DesignRegion(object):
def __init__(
self,
design_parameters,
volume=None,
size=None,
center=mp.Vector3()
):
self.volume = volume if volume else mp.Volume(center=center, size=size)
self.size = self.volume.size
self.center = self.volume.center
self.design_parameters = design_parameters
self.num_design_params = design_parameters.num_params

def update_design_parameters(self, design_parameters):
self.design_parameters.update_weights(design_parameters)

def update_beta(self,beta):
self.design_parameters.beta=beta

def get_gradient(self, sim, fields_a, fields_f, frequencies):
num_freqs = onp.array(frequencies).size
shapes = []
com = _compute_components(sim)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just move the call to _compute_components() inside enumerate(...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

scalegrad = 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a comment as to why scalegrad is needed? If it's hardcoded to 1 here, can it just be removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

for ci, c in enumerate(com):
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest for component_idx, component for better readability

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

'''We need to correct for the rare cases that get_dft_array
returns a singleton element for the forward and adjoint fields.
This only occurs when we are in 2D and only working with a particular
polarization (as the other fields are never stored). Our get_gradient
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps we can be more explicit here and state that in 2D, the Ez polarization consists of a single scalar Ez field.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

algorithm, however, requires we pass an array of zeros with the
proper shape as the design_region.
'''
s = sim.get_array_slice_dimensions(c, vol=self.volume)[0]
if (onp.squeeze(fields_a[ci][0,...]).size == 1):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need the squeeze? Can't you just check the size?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

fields_a[ci] = onp.zeros(onp.insert(s,0,num_freqs))
fields_f[ci] = onp.zeros(onp.insert(s,0,num_freqs))
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be more readable to do onp.zeros((num_freqs,) + s). Also, would help to use a more descriptive name than s... maybe spatial_shape or something similar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

maybe spatial_shape or something similar?

Good idea

onp.zeros((num_freqs,) + s)

unfortunately s is a numpy array, and can't be joined like python lists. This seems pretty standard when prepending to numpy arrays, from what I've seen (there are hundreds of possibilities of course...)

if _check_if_cylindrical(sim):
'''For some reason, get_dft_array returns the field
components in a different order than the convention used
throughout meep. So, we reorder them here so we can use
the same field macros later in our get_gradient function.
'''
fields_a[ci] = onp.transpose(fields_a[ci],(_FREQ_AXIS,_RHO_AXIS,_PHI_AXIS,_Z_AXIS))
fields_f[ci] = onp.transpose(fields_f[ci],(_FREQ_AXIS,_RHO_AXIS,_PHI_AXIS,_Z_AXIS))
shapes.append(fields_a[ci].shape)
fields_a[ci] = fields_a[ci].flatten(order='C')
fields_f[ci] = fields_f[ci].flatten(order='C')
shapes = onp.asarray(shapes).flatten(order='C')
fields_a = onp.concatenate(fields_a)
fields_f = onp.concatenate(fields_f)

grad = onp.zeros((num_freqs, self.num_design_params)) # preallocate
geom_list = sim.geometry
f = sim.fields
vol = sim._fit_volume_to_simulation(self.volume)

# compute the gradient
mp._get_gradient(grad,scalegrad,fields_a,fields_f,sim.gv,vol.swigobj,onp.array(frequencies),sim.geps,shapes)
return onp.squeeze(grad).T

def _check_if_cylindrical(sim):
return sim.is_cylindrical or (sim.dimensions == mp.CYLINDRICAL)

def _compute_components(sim):
return _ADJOINT_FIELD_COMPONENTS_CYL if _check_if_cylindrical(sim) else _ADJOINT_FIELD_COMPONENTS

def _make_at_least_nd(x: onp.ndarray, dims: int = 3) -> onp.ndarray:
"""Makes an array have at least the specified number of dimensions."""
Expand Down Expand Up @@ -61,15 +134,16 @@ def install_design_region_monitors(
simulation: mp.Simulation,
design_regions: List[DesignRegion],
frequencies: List[float],
decimation_factor: int = 0
) -> List[mp.DftFields]:
"""Installs DFT field monitors at the design regions of the simulation."""
design_region_monitors = [
simulation.add_dft_fields(
_ADJOINT_FIELD_COMPONENTS,
_compute_components(simulation),
frequencies,
where=design_region.volume,
yee_grid=True,
decimation_factor=0
decimation_factor=decimation_factor
) for design_region in design_regions
]
return design_region_monitors
Expand Down Expand Up @@ -120,7 +194,7 @@ def gather_design_region_fields(
fwd_fields = []
for monitor in design_region_monitors:
fields_by_component = []
for component in _ADJOINT_FIELD_COMPONENTS:
for component in _compute_components(simulation):
fields_by_freq = []
for freq_idx, _ in enumerate(frequencies):
fields = simulation.get_dft_array(monitor, component, freq_idx)
Expand Down
Loading