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

Add two unit tests for adjoint gradients related to source and monitor bandwidths #2314

Merged
merged 3 commits into from
Nov 23, 2022
Merged
Changes from all commits
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
173 changes: 146 additions & 27 deletions python/tests/test_adjoint_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import unittest
from enum import Enum

from typing import List, Union
import numpy as np
from autograd import numpy as npa
from autograd import tensor_jacobian_product
Expand Down Expand Up @@ -60,8 +60,21 @@ def setUpClass(cls):
)
]

# source center frequency and bandwidth
cls.fcen = 1 / 1.55
cls.df = 0.2 * cls.fcen
cls.df = 0.05 * cls.fcen

# monitor frequencies
# two cases: (1) single and (2) multi frequency
cls.mon_frqs = [
[cls.fcen],
[
cls.fcen - 0.09 * cls.df,
cls.fcen,
cls.fcen + 0.06 * cls.df,
],
]

cls.mode_source = [
mp.EigenModeSource(
src=mp.GaussianSource(cls.fcen, fwidth=cls.df),
Expand Down Expand Up @@ -99,9 +112,9 @@ def adjoint_solver(
self,
design_params,
mon_type: MonitorObject,
frequencies=None,
mat2=None,
need_gradient=True,
frequencies: Union[float, List] = None,
mat2: mp.Medium = None,
need_gradient: bool = True,
):
matgrid = mp.MaterialGrid(
mp.Vector3(self.Nx, self.Ny),
Expand Down Expand Up @@ -516,10 +529,11 @@ def mapping(self, x, filter_radius, eta, beta):
return projected_field.flatten()

def test_DFT_fields(self):
"""Verifies that the gradient for an objective function based on the
DFT fields agrees with the finite-difference approximation."""
print("*** TESTING DFT OBJECTIVE ***")

# test the single frequency and multi frequency cases
for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver(
self.p, MonitorObject.DFT, frequencies
Expand All @@ -539,14 +553,16 @@ def test_DFT_fields(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.07 if mp.is_single_precision() else 0.006
tol = 0.03 if mp.is_single_precision() else 0.002
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_eigenmode(self):
"""Verifies that the gradient for an objective function based on
eigenmode decomposition agrees with the finite-difference
approximation."""
print("*** TESTING EIGENMODE OBJECTIVE ***")

# test the single frequency and multi frequency case
for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver(
self.p, MonitorObject.EIGENMODE, frequencies
Expand All @@ -569,14 +585,16 @@ def test_eigenmode(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.04 if mp.is_single_precision() else 0.01
tol = 0.008 if mp.is_single_precision() else 0.002
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_ldos(self):
"""Verifies that the gradient for an objective function based on the
local density of states (LDoS) agrees with the finite-difference
approximation."""
print("*** TESTING LDOS OBJECTIVE ***")

# test the single frequency and multi frequency cases
for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver(
self.p, MonitorObject.LDOS, frequencies
Expand All @@ -596,18 +614,21 @@ def test_ldos(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.07 if mp.is_single_precision() else 0.006
tol = 0.002 if mp.is_single_precision() else 0.001
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_gradient_backpropagation(self):
"""Verifies that the adjoint gradient can be back propagated through
a differentiable mapping function applied to the design region
and agrees with the finite-difference approximation."""
print("*** TESTING GRADIENT BACKPROPAGATION ***")

# filter/thresholding parameters
filter_radius = 0.21985
eta = 0.49093
beta = 4.0698

for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
mapped_p = self.mapping(self.p, filter_radius, eta, beta)

# compute objective value and its gradient for unperturbed design
Expand Down Expand Up @@ -646,13 +667,16 @@ def test_gradient_backpropagation(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.025 if mp.is_single_precision() else 0.01
tol = 0.005 if mp.is_single_precision() else 0.002
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_complex_fields(self):
"""Verifies that the adjoint gradient for an objective function based
on the DFT fields obtained from complex time-dependent fields agrees
with the finite-difference approximation."""
print("*** TESTING COMPLEX FIELDS ***")

for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver_complex_fields(
self.p, frequencies
Expand All @@ -672,13 +696,15 @@ def test_complex_fields(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.06 if mp.is_single_precision() else 0.01
tol = 0.02 if mp.is_single_precision() else 0.001
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_damping(self):
"""Verifies that the adjoint gradient for a design region with a non-zero
conductivity agrees with the finite-difference approximation."""
print("*** TESTING CONDUCTIVITY ***")

for frequencies in [[1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in [self.mon_frqs[1]]:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver_damping(
self.p, frequencies
Expand All @@ -698,10 +724,13 @@ def test_damping(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.06 if mp.is_single_precision() else 0.04
tol = 0.04 if mp.is_single_precision() else 0.01
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_offdiagonal(self):
"""Verifies that the adjoint gradient for a design region involving an
anisotropic material with non-zero off-diagonal entries of the
permittivity tensor agrees with the finite-difference approxmiation."""
print("*** TESTING ANISOTROPIC ε ***")
filt = lambda x: mpa.conic_filter(
x.reshape((self.Nx, self.Ny)),
Expand All @@ -711,8 +740,7 @@ def test_offdiagonal(self):
self.design_region_resolution,
).flatten()

# test the single frequency and multi frequency case
for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver(
filt(self.p), MonitorObject.EIGENMODE, frequencies, self.sapphire
Expand Down Expand Up @@ -750,14 +778,15 @@ def test_offdiagonal(self):
f"directional derivative:, {adj_dd} (adjoint solver), {fnd_dd} (finite difference)"
)

tol = 0.1 if mp.is_single_precision() else 0.04
tol = 0.05 if mp.is_single_precision() else 0.005
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_two_objfunc(self):
print("*** TESTING TWO OBJECTIVE FUNCTIONS***")
"""Verifies that the adjoint gradients from two objective functions
each agree with the finite-difference approximation."""
print("*** TESTING TWO OBJECTIVE FUNCTIONS ***")

# test the single frequency and multi frequency case
for frequencies in [[self.fcen], [1 / 1.58, self.fcen, 1 / 1.53]]:
for frequencies in self.mon_frqs:
# compute objective value and its gradient for unperturbed design
unperturbed_val, unperturbed_grad = self.adjoint_solver_two_objfunc(
self.p, frequencies
Expand All @@ -771,7 +800,7 @@ def test_two_objfunc(self):
)

nfrq = len(frequencies)
tol = 0.05 if mp.is_single_precision() else 0.01
tol = 0.15 if mp.is_single_precision() else 0.001
for m in [0, 1]:
frq_slice = slice(0, nfrq, 1) if m == 0 else slice(nfrq, 2 * nfrq, 1)
adj_dd = (self.dp[None, :] @ unperturbed_grad[:, frq_slice]).flatten()
Expand All @@ -783,6 +812,96 @@ def test_two_objfunc(self):
)
self.assertClose(adj_dd, fnd_dd, epsilon=tol)

def test_multifreq_monitor(self):
"""Verifies that the individual adjoint gradients from a multifrequency
eigenmode-coefficient monitor are equivalent to a single-frequency
monitor."""
print("*** TESTING MULTIFREQUENCY MONITOR ***")

nfrq = 5
frqs = np.linspace(
self.fcen - 0.2 * self.df,
self.fcen + 0.2 * self.df,
nfrq,
)
multifrq_val, multifrq_grad = self.adjoint_solver_two_objfunc(
self.p,
frqs,
)

tol = 0.005 if mp.is_single_precision() else 0.004
for n in range(nfrq):
frq = frqs[n]
singlefreq_val, singlefreq_grad = self.adjoint_solver_two_objfunc(
self.p,
[frq],
)
for m in [0, 1]:
s = n + m * nfrq
self.assertAlmostEqual(singlefreq_val[m], multifrq_val[s], places=4)
self.assertClose(
singlefreq_grad[:, m], multifrq_grad[:, s], epsilon=tol
)
print(f"PASSED: frequency={frq:.5f}, m={m}.")

def test_mode_source_bandwidth(self):
"""Verifies that the accuracy of the adjoint gradient of an
eigenmode-coefficient monitor at a single frequency is independent
of the bandwidth of the pulsed mode source."""
print("*** TESTING MODE SOURCE BANDWIDTH ***")

# compute objective value for unperturbed design
unperturbed_objf = self.adjoint_solver_two_objfunc(
self.p,
[self.fcen],
need_gradient=False,
)

# compute objective value for perturbed design
perturbed_objf = self.adjoint_solver_two_objfunc(
self.p + self.dp,
[self.fcen],
need_gradient=False,
)

# finite-difference approximation
# for the directional derivative
fnd_dd = perturbed_objf - unperturbed_objf

nfw = 5
fw = np.linspace(
0.05,
0.25,
nfw,
)

# minimum error for directional derivative of adjoint
# gradient relative to finite-difference approximation
tol = 0.04

for n in range(nfw):
fwidth = fw[n] * self.fcen
self.mode_source = [
mp.EigenModeSource(
src=mp.GaussianSource(self.fcen, fwidth=fwidth),
center=mp.Vector3(-0.5 * self.sxy + self.dpml, 0),
size=mp.Vector3(0, self.sxy - 2 * self.dpml),
eig_parity=self.eig_parity,
)
]

objf, grad = self.adjoint_solver_two_objfunc(
self.p,
[self.fcen],
)

for m in [0, 1]:
self.assertAlmostEqual(unperturbed_objf[m], objf[m], places=6)
adj_dd = (self.dp[None, :] @ grad[:, m]).flatten()
rel_err = abs((fnd_dd[m] - adj_dd[0]) / fnd_dd[m])
self.assertLessEqual(rel_err, tol)
print(f"PASSED:, fwidth={fwidth:.5f}, m={m}, err={rel_err}")


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