Skip to content

Commit 46f78d5

Browse files
authored
Merge pull request #21 from sbuddhiraju/fdfd_mf
Multi-frequency FDFD
2 parents 44aa696 + 89695de commit 46f78d5

File tree

6 files changed

+919
-1
lines changed

6 files changed

+919
-1
lines changed

Diff for: ceviche/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
__version__ = '0.1.1'
55

66
from .fdtd import fdtd
7-
from .fdfd import fdfd_ez, fdfd_hz
7+
from .fdfd import fdfd_ez, fdfd_hz, fdfd_mf_ez
88
from .jacobians import jacobian
99

1010
from . import viz

Diff for: ceviche/fdfd.py

+115
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,121 @@ def _solve_fn(self, eps_vec, entries_a, indices_a, Mz_vec):
243243

244244
return Ex_vec, Ey_vec, Hz_vec
245245

246+
247+
class fdfd_mf_ez(fdfd):
248+
""" FDFD class for multifrequency linear Ez polarization. New variables:
249+
omega_mod: angular frequency of modulation (rad/s)
250+
delta: array of shape (Nfreq, Nx, Ny) containing pointwise modulation depth for each modulation harmonic (1,...,Nfreq)
251+
phi: array of same shape as delta containing pointwise modulation phase for each modulation harmonic
252+
Nsb: number of numerical sidebands to consider when solving for fields.
253+
This is not the same as the number of modulation frequencies Nfreq. For physically meaningful results, Nsb >= Nfreq.
254+
"""
255+
256+
def __init__(self, omega, dL, eps_r, omega_mod, delta, phi, Nsb, npml, bloch_phases=None):
257+
super().__init__(omega, dL, eps_r, npml, bloch_phases=bloch_phases)
258+
self.omega_mod = omega_mod
259+
self.delta = delta
260+
self.phi = phi
261+
self.Nsb = Nsb
262+
263+
def solve(self, source_z):
264+
""" Outward facing function (what gets called by user) that takes a source grid and returns the field components """
265+
# flatten the permittivity and source grid
266+
source_vec = self._grid_to_vec(source_z)
267+
eps_vec = self._grid_to_vec(self.eps_r)
268+
Nfreq = npa.shape(self.delta)[0]
269+
delta_matrix = self.delta.reshape([Nfreq, npa.prod(self.shape)])
270+
phi_matrix = self.phi.reshape([Nfreq, npa.prod(self.shape)])
271+
# create the A matrix for this polarization
272+
entries_a, indices_a = self._make_A(eps_vec, delta_matrix, phi_matrix)
273+
274+
# solve field componets usng A and the source
275+
Fx_vec, Fy_vec, Fz_vec = self._solve_fn(eps_vec, entries_a, indices_a, source_vec)
276+
277+
# put all field components into a tuple, convert to grid shape and return them all
278+
Fx = self._vec_to_grid(Fx_vec)
279+
Fy = self._vec_to_grid(Fy_vec)
280+
Fz = self._vec_to_grid(Fz_vec)
281+
282+
return Fx, Fy, Fz
283+
284+
def _make_A(self, eps_vec, delta_matrix, phi_matrix):
285+
""" Builds the multi-frequency electromagnetic operator A in Ax = b """
286+
M = 2*self.Nsb + 1
287+
N = self.Nx * self.Ny
288+
W = self.omega + npa.arange(-self.Nsb,self.Nsb+1)*self.omega_mod
289+
290+
C = sp.kron(sp.eye(M), - 1 / MU_0 * self.Dxf.dot(self.Dxb) - 1 / MU_0 * self.Dyf.dot(self.Dyb))
291+
entries_c, indices_c = get_entries_indices(C)
292+
293+
# diagonal entries representing static refractive index
294+
# this part is just a block diagonal version of the single frequency fdfd_ez
295+
entries_diag = - EPSILON_0 * npa.kron(W**2, eps_vec)
296+
indices_diag = npa.vstack((npa.arange(M*N), npa.arange(M*N)))
297+
298+
entries_a = npa.hstack((entries_diag, entries_c))
299+
indices_a = npa.hstack((indices_diag, indices_c))
300+
301+
# off-diagonal entries representing dynamic modulation
302+
# this part couples different frequencies due to modulation
303+
# for a derivation of these entries, see Y. Shi, W. Shin, and S. Fan. Optica 3(11), 2016.
304+
Nfreq = npa.shape(delta_matrix)[0]
305+
for k in npa.arange(Nfreq):
306+
# super-diagonal entries (note the +1j phase)
307+
mod_p = - 0.5 * EPSILON_0 * delta_matrix[k,:] * npa.exp(1j*phi_matrix[k,:])
308+
entries_p = npa.kron(W[:-k-1]**2, mod_p)
309+
indices_p = npa.vstack((npa.arange((M-k-1)*N), npa.arange((k+1)*N, M*N)))
310+
entries_a = npa.hstack((entries_p, entries_a))
311+
indices_a = npa.hstack((indices_p,indices_a))
312+
# sub-diagonal entries (note the -1j phase)
313+
mod_m = - 0.5 * EPSILON_0 * delta_matrix[k,:] * npa.exp(-1j*phi_matrix[k,:])
314+
entries_m = npa.kron(W[k+1:]**2, mod_m)
315+
indices_m = npa.vstack((npa.arange((k+1)*N, M*N), npa.arange((M-k-1)*N)))
316+
entries_a = npa.hstack((entries_m, entries_a))
317+
indices_a = npa.hstack((indices_m,indices_a))
318+
319+
return entries_a, indices_a
320+
321+
def _solve_fn(self, eps_vec, entries_a, indices_a, Jz_vec):
322+
""" Multi-frequency version of _solve_fn() defined in fdfd_ez """
323+
M = 2*self.Nsb + 1
324+
N = self.Nx * self.Ny
325+
W = self.omega + npa.arange(-self.Nsb,self.Nsb+1)*self.omega_mod
326+
P = sp.kron(sp.spdiags(W,[0],M,M), sp.eye(N))
327+
entries_p, indices_p = get_entries_indices(P)
328+
b_vec = 1j * sp_mult(entries_p,indices_p,Jz_vec)
329+
Ez_vec = sp_solve(entries_a, indices_a, b_vec)
330+
Hx_vec, Hy_vec = self._Ez_to_Hx_Hy(Ez_vec)
331+
return Hx_vec, Hy_vec, Ez_vec
332+
333+
def _Ez_to_Hx(self, Ez_vec):
334+
""" Multi-frequency version of _Ez_to_Hx() defined in fdfd """
335+
M = 2*self.Nsb + 1
336+
Winv = 1/(self.omega + npa.arange(-self.Nsb,self.Nsb+1)*self.omega_mod)
337+
Dyb_mf = sp.kron(sp.spdiags(Winv,[0],M,M), self.Dyb)
338+
entries_Dyb_mf, indices_Dyb_mf = get_entries_indices(Dyb_mf)
339+
return -1 / 1j / MU_0 * sp_mult(entries_Dyb_mf, indices_Dyb_mf, Ez_vec)
340+
341+
def _Ez_to_Hy(self, Ez_vec):
342+
""" Multi-frequency version of _Ez_to_Hy() defined in fdfd """
343+
M = 2*self.Nsb + 1
344+
Winv = 1/(self.omega + npa.arange(-self.Nsb,self.Nsb+1)*self.omega_mod)
345+
Dxb_mf = sp.kron(sp.spdiags(Winv,[0],M,M), self.Dxb)
346+
entries_Dxb_mf, indices_Dxb_mf = get_entries_indices(Dxb_mf)
347+
return 1 / 1j / MU_0 * sp_mult(entries_Dxb_mf, indices_Dxb_mf, Ez_vec)
348+
349+
def _Ez_to_Hx_Hy(self, Ez_vec):
350+
""" Multi-frequency version of _Ez_to_Hx_Hy() defined in fdfd """
351+
Hx_vec = self._Ez_to_Hx(Ez_vec)
352+
Hy_vec = self._Ez_to_Hy(Ez_vec)
353+
return Hx_vec, Hy_vec
354+
355+
def _vec_to_grid(self, vec):
356+
""" Multi-frequency version of _vec_to_grid() defined in fdfd """
357+
# grid shape has Nx*Ny cells per frequency sideband
358+
grid_shape = (2*self.Nsb + 1, self.Nx, self.Ny)
359+
return npa.reshape(vec, grid_shape)
360+
246361
class fdfd_3d(fdfd):
247362
""" 3D FDFD class (work in progress) """
248363

Diff for: examples/fdfd_mf_intro.ipynb

+333
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: examples/multifrequency_fdfd.ipynb

+333
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: tests/test_all_gradients.sh

+1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ echo "testing all gradient checkers in $TEST_DIR"
77

88
# runs all of the gradient specific tests
99
python $TEST_DIR/test_gradients_fdfd.py
10+
python $TEST_DIR/test_gradients_fdfd_mf.py
1011
python $TEST_DIR/test_gradients_fdtd.py
1112
python $TEST_DIR/test_primitives.py

Diff for: tests/test_gradients_fdfd_mf.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import unittest
2+
3+
import numpy as np
4+
import autograd.numpy as npa
5+
import scipy.sparse as sp
6+
import scipy.sparse.linalg as spl
7+
import copy
8+
9+
from autograd.extend import primitive, defvjp
10+
from autograd import grad
11+
12+
import sys
13+
sys.path.append('../ceviche')
14+
15+
from ceviche.utils import grad_num
16+
from ceviche import jacobian, fdfd_mf_ez
17+
18+
"""
19+
This file tests the autograd gradients of an FDFD and makes sure that they
20+
equal the numerical derivatives
21+
"""
22+
23+
# test parameters
24+
ALLOWED_RATIO = 1e-4 # maximum allowed ratio of || grad_num - grad_auto || vs. || grad_num ||
25+
VERBOSE = False # print out full gradients?
26+
DEPS = 1e-6 # numerical gradient step size
27+
28+
print("Testing the Multi-frequency Linear FDFD Ez gradients")
29+
30+
class TestFDFD(unittest.TestCase):
31+
32+
""" Tests the flexible objective function specifier """
33+
34+
def setUp(self):
35+
36+
# basic simulation parameters
37+
self.Nx = 30
38+
self.Ny = 30
39+
self.N = self.Nx * self.Ny
40+
self.Nfreq = 1
41+
self.Nsb = 1
42+
self.omega = 2*np.pi*200e12
43+
self.omega_mod = 2*np.pi*2e12
44+
self.dL = 1e-6
45+
self.pml = [10, 10]
46+
47+
# sources (chosen to have objectives around 1)
48+
self.source_amp_ez = 1
49+
self.source_amp_hz = 1
50+
51+
self.source_ez = np.zeros((2*self.Nsb+1, self.Nx, self.Ny))
52+
self.source_ez[self.Nsb, self.Nx//2, self.Ny//2] = self.source_amp_ez
53+
54+
# starting relative permittivity (random for debugging)
55+
self.eps_r = np.random.random((self.Nx, self.Ny)) + 1
56+
self.delta = np.random.random((self.Nfreq, self.Nx, self.Ny))
57+
self.phi = 2*np.pi*np.random.random((self.Nfreq, self.Nx, self.Ny))
58+
self.eps_arr = self.eps_r.flatten()
59+
self.params = npa.concatenate( (npa.concatenate((self.eps_arr, self.delta.flatten() )), self.phi.flatten() ) )
60+
def check_gradient_error(self, grad_num, grad_auto):
61+
""" Checks the test case:
62+
compares the norm of the gradient to the norm of the difference
63+
Throws error if this is greater than ALLOWED RATIO
64+
"""
65+
norm_grad = np.linalg.norm(grad_num)
66+
print('\t\tnorm of gradient: ', norm_grad)
67+
norm_diff = np.linalg.norm(grad_num - grad_auto)
68+
print('\t\tnorm of difference: ', norm_diff)
69+
norm_ratio = norm_diff / norm_grad
70+
print('\t\tratio of norms: ', norm_ratio)
71+
self.assertLessEqual(norm_ratio, ALLOWED_RATIO)
72+
print('')
73+
74+
def test_Ez_reverse(self):
75+
76+
print('\ttesting reverse-mode Ez in FDFD_MF')
77+
f = fdfd_mf_ez(self.omega, self.dL, self.eps_r, self.omega_mod, self.delta, self.phi, self.Nsb, self.pml)
78+
79+
def J_fdfd(params):
80+
eps_r = params[:self.N].reshape((self.Nx, self.Ny))
81+
delta = params[self.N:(self.Nfreq+1)*self.N].reshape((self.Nfreq, self.Nx, self.Ny))
82+
phi = params[(self.Nfreq+1)*self.N:].reshape((self.Nfreq, self.Nx, self.Ny))
83+
# set the permittivity, modulation depth, and modulation phase
84+
f.eps_r = eps_r
85+
f.delta = delta
86+
f.phi = phi
87+
# set the source amplitude to the permittivity at that point
88+
Hx, Hy, Ez = f.solve((eps_r + npa.sum(delta*npa.exp(1j*phi),axis=0))* self.source_ez)
89+
90+
return npa.sum(npa.square(npa.abs(Ez))) \
91+
+ npa.sum(npa.square(npa.abs(Hx))) \
92+
+ npa.sum(npa.square(npa.abs(Hy)))
93+
94+
grad_autograd_rev = jacobian(J_fdfd, mode='reverse')(self.params)
95+
grad_numerical = jacobian(J_fdfd, mode='numerical')(self.params)
96+
97+
if VERBOSE:
98+
print('\ttesting epsilon, delta and phi.')
99+
print('\tobjective function value: ', J_fdfd(self.params))
100+
print('\tgrad (auto): \n\t\t', grad_autograd_rev)
101+
print('\tgrad (num): \n\t\t', grad_numerical)
102+
103+
self.check_gradient_error(grad_numerical, grad_autograd_rev)
104+
105+
def test_Ez_forward(self):
106+
107+
print('\ttesting forward-mode Ez in FDFD_MF')
108+
109+
f = fdfd_mf_ez(self.omega, self.dL, self.eps_r, self.omega_mod, self.delta, self.phi, self.Nsb, self.pml)
110+
111+
def J_fdfd(c):
112+
113+
# set the permittivity, modulation depth, and modulation phase
114+
f.eps_r = c * self.eps_r
115+
f.delta = c * self.delta
116+
f.phi = c * self.phi
117+
# set the source amplitude to the permittivity at that point
118+
Hx, Hy, Ez = f.solve(c * self.eps_r * self.source_ez)
119+
120+
return npa.square(npa.abs(Ez)) \
121+
+ npa.square(npa.abs(Hx)) \
122+
+ npa.square(npa.abs(Hy))
123+
124+
grad_autograd_for = jacobian(J_fdfd, mode='forward')(1.0)
125+
grad_numerical = jacobian(J_fdfd, mode='numerical')(1.0)
126+
127+
if VERBOSE:
128+
print('\tobjective function value: ', J_fdfd(1.0))
129+
print('\tgrad (auto): \n\t\t', grad_autograd_for)
130+
print('\tgrad (num): \n\t\t', grad_numerical)
131+
132+
self.check_gradient_error(grad_numerical, grad_autograd_for)
133+
134+
135+
if __name__ == '__main__':
136+
unittest.main()

0 commit comments

Comments
 (0)