def test_Ez_reverse(self): print('\ttesting reverse-mode Ez in FDFD_MF') f = fdfd_mf_ez(self.omega, self.dL, self.eps_r, self.omega_mod, self.delta, self.phi, self.Nsb, self.pml) def J_fdfd(params): eps_r = params[:self.N].reshape((self.Nx, self.Ny)) delta = params[self.N:(self.Nfreq+1)*self.N].reshape((self.Nfreq, self.Nx, self.Ny)) phi = params[(self.Nfreq+1)*self.N:].reshape((self.Nfreq, self.Nx, self.Ny)) # set the permittivity, modulation depth, and modulation phase f.eps_r = eps_r f.delta = delta f.phi = phi # set the source amplitude to the permittivity at that point Hx, Hy, Ez = f.solve((eps_r + npa.sum(delta*npa.exp(1j*phi),axis=0))* self.source_ez) return npa.sum(npa.square(npa.abs(Ez))) \ + npa.sum(npa.square(npa.abs(Hx))) \ + npa.sum(npa.square(npa.abs(Hy))) grad_autograd_rev = jacobian(J_fdfd, mode='reverse')(self.params) grad_numerical = jacobian(J_fdfd, mode='numerical')(self.params) if VERBOSE: print('\ttesting epsilon, delta and phi.') print('\tobjective function value: ', J_fdfd(self.params)) print('\tgrad (auto): \n\t\t', grad_autograd_rev) print('\tgrad (num): \n\t\t', grad_numerical) self.check_gradient_error(grad_numerical, grad_autograd_rev)
def test_Hz_reverse(self): print('\ttesting reverse-mode Hz in FDFD') f = fdfd_hz(self.omega, self.dL, self.eps_r, self.pml) def J_fdfd(eps_arr): eps_r = eps_arr.reshape((self.Nx, self.Ny)) # set the permittivity f.eps_r = eps_r # set the source amplitude to the permittivity at that point Ex, Ey, Hz = f.solve(eps_r * self.source_hz) return npa.sum(npa.square(npa.abs(Hz))) \ + npa.sum(npa.square(npa.abs(Ex))) \ + npa.sum(npa.square(npa.abs(Ey))) grad_autograd_rev = jacobian(J_fdfd, mode='reverse')(self.eps_arr) grad_numerical = jacobian(J_fdfd, mode='numerical')(self.eps_arr) if VERBOSE: print('\tobjective function value: ', J_fdfd(self.eps_arr)) print('\tgrad (auto): \n\t\t', grad_autograd_rev) print('\tgrad (num): \n\t\t\n', grad_numerical) self.check_gradient_error(grad_numerical, grad_autograd_rev)
def test_Ez_forward(self): print('\ttesting forward-mode Ez in FDFD_MF') f = fdfd_mf_ez(self.omega, self.dL, self.eps_r, self.omega_mod, self.delta, self.phi, self.Nsb, self.pml) def J_fdfd(c): # set the permittivity, modulation depth, and modulation phase f.eps_r = c * self.eps_r f.delta = c * self.delta f.phi = c * self.phi # set the source amplitude to the permittivity at that point Hx, Hy, Ez = f.solve(c * self.eps_r * self.source_ez) return npa.square(npa.abs(Ez)) \ + npa.square(npa.abs(Hx)) \ + npa.square(npa.abs(Hy)) grad_autograd_for = jacobian(J_fdfd, mode='forward')(1.0) grad_numerical = jacobian(J_fdfd, mode='numerical')(1.0) if VERBOSE: print('\tobjective function value: ', J_fdfd(1.0)) print('\tgrad (auto): \n\t\t', grad_autograd_for) print('\tgrad (num): \n\t\t', grad_numerical) self.check_gradient_error(grad_numerical, grad_autograd_for)
def test_Hz_forward(self): print('\ttesting forward-mode Hz in FDFD') f = fdfd_hz(self.omega, self.dL, self.eps_r, self.pml) def J_fdfd(c): # set the permittivity f.eps_r = c * self.eps_r # set the source amplitude to the permittivity at that point Ex, Ey, Hz = f.solve(c * self.eps_r * self.source_hz) return npa.square(npa.abs(Hz)) \ + npa.square(npa.abs(Ex)) \ + npa.square(npa.abs(Ey)) grad_autograd_for = jacobian(J_fdfd, mode='forward')(1.0) grad_numerical = jacobian(J_fdfd, mode='numerical')(1.0) if VERBOSE: print('\tobjective function value: ', J_fdfd(1.0)) print('\tgrad (auto): \n\t\t', grad_autograd_for) print('\tgrad (num): \n\t\t', grad_numerical) self.check_gradient_error(grad_numerical, grad_autograd_for)
def test_mult_x(self): def fn_mult_x(x): # sparse matrix multiplication (Ax = b) as a function of dense vector 'x' b = sp_mult(self.entries_const, self.indices_const, x) return self.out_fn(b) x = make_rand_complex(self.N) grad_rev = ceviche.jacobian(fn_mult_x, mode='reverse')(x)[0] grad_for = ceviche.jacobian(fn_mult_x, mode='forward')(x)[0] grad_true = grad_num(fn_mult_x, x) np.testing.assert_almost_equal(grad_rev, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_mult_x', 'reverse')) np.testing.assert_almost_equal(grad_for, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_mult_x', 'forward'))
def test_solve_b(self): def fn_solve_b(b): # sparse matrix solve (x = A^{-1}b) as a function of source 'b' x = sp_solve(self.entries_const, self.indices_const, b) return self.out_fn(x) b = make_rand_complex(self.N) grad_rev = ceviche.jacobian(fn_solve_b, mode='reverse')(b)[0] grad_for = ceviche.jacobian(fn_solve_b, mode='forward')(b)[0] grad_true = grad_num(fn_solve_b, b) np.testing.assert_almost_equal(grad_rev, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_solve_b', 'reverse')) np.testing.assert_almost_equal(grad_for, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_solve_b', 'forward'))
def test_solve_entries(self): def fn_solve_entries(entries): # sparse matrix solve (x = A^{-1}b) as a function of matrix entries 'A(entries)' x = sp_solve(entries, self.indices_const, self.b_const) return self.out_fn(x) entries = make_rand_complex(self.M) grad_rev = ceviche.jacobian(fn_solve_entries, mode='reverse')(entries)[0] grad_for = ceviche.jacobian(fn_solve_entries, mode='forward')(entries)[0] grad_true = grad_num(fn_solve_entries, entries) np.testing.assert_almost_equal(grad_rev, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_solve_entries', 'reverse')) np.testing.assert_almost_equal(grad_for, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_solve_entries', 'forward'))
def test_spmut_entries(self): def fn_spsp_entries_a(entries): # sparse matrix - sparse matrix dot procut as function of entries into first matrix (A) entries_c, indices_c = spsp_mult(entries, self.indices_const, self.entries_const2, self.indices_const2, N=self.N) # and then as a function of entries in the second matrix (X) entries_d, indices_d = spsp_mult(self.entries_const, self.indices_const, entries_c, indices_c, N=self.N) # do a sparse linear solve using the resulting matrix and return out_fn of result x = sp_solve(entries_d, indices_d, self.b_const) return self.out_fn(x) entries = make_rand_complex(self.M) grad_rev = ceviche.jacobian(fn_spsp_entries_a, mode='reverse')(entries)[0] grad_for = ceviche.jacobian(fn_spsp_entries_a, mode='forward')(entries)[0] grad_true = grad_num(fn_spsp_entries_a, entries) np.testing.assert_almost_equal(grad_rev, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_solve_entries', 'reverse')) np.testing.assert_almost_equal(grad_for, grad_true, decimal=DECIMAL, err_msg=self.err_msg( 'fn_solve_entries', 'forward'))
# defines the intensity on the other side of the box as a function of the relative permittivity grid def intensity(eps_arr): eps_r = eps_arr.reshape((Nx, Ny)) # set the permittivity of the FDFD and solve the fields F.eps_r = eps_r Ex, Ey, Hz = F.solve(source) # compute the gradient and normalize if you want I = npa.sum(npa.square(npa.abs(Hz * probe))) return -I / I_H0 # define the gradient for autograd grad_I = jacobian(intensity, mode='reverse') # initialize the design region with some eps eps_r[box_region == 1] = eps_max from scipy.optimize import minimize bounds = [(1, eps_max) if box_region.flatten()[i] == 1 else (1, 1) for i in range(eps_r.size)] minimize(intensity, eps_r, args=(), method='L-BFGS-B', jac=grad_I, bounds=bounds, tol=None,
# defines the acceleration gradient as a function of the relative permittivity grid def accel_gradient(eps_arr): # set the permittivity of the FDFD and solve the fields F.eps_r = eps_arr.reshape((Nx, Ny)) Ex, Ey, Hz = F.solve(source) # compute the gradient and normalize if you want G = npa.sum(Ey * eta / Ny) return -np.abs(G) # / Emax(Ex, Ey, eps_r) # define the gradient for autograd grad_g = jacobian(accel_gradient) # optimization NIter = 200 bounds_eps = [(1, eps_max) if design_region.flatten()[i] == 1 else (1, 1) for i in range(eps_r.size)] minimize(accel_gradient, eps_r.flatten(), args=(), method='L-BFGS-B', jac=grad_g, bounds=bounds_eps, tol=None, callback=None, options={ 'disp': True,
F.eps_r = eps_arr Ex, Ey, Hz = F.solve(source) G = npa.abs(npa.sum(Ey*probe)) # calculate objective value G # normalize G by maximum field E_mag = npa.sqrt(npa.square(npa.abs(Ex)) + npa.square(npa.abs(Ey))) material_density = (eps_r - 1) / (epsr_max - 1) max_field = npa.max(E_mag * material_density) return G/max_field # Optimization run N_opts = 50 objective_jac = jacobian(objective, mode='reverse') (rho_optimum, loss) = adam_optimize(objective, rho_init.flatten(), objective_jac, Nsteps=N_opts, direction='max', step_size=1e-2) ## Visualization of result eps_r = convert_rho_epsr(design_region * rho_optimum.reshape((Nx, Ny))) G = objective(rho_optimum) F.eps_r = eps_r Ex, Ey, Hz = F.solve(source) # calculate the fields for our source term f, ax = plt.subplots(1, 1, tight_layout=True, figsize=(Nx/40+1,Ny/40+0.5)) max_val = np.max(np.abs(Ey)) plt.contour(eps_r.T, levels=[(epsr_max+1)/2]) im = plt.imshow(np.real(Ey.T*np.exp(1j*3)), cmap='RdBu', aspect='auto', vmin=-max_val, vmax=max_val) divider = make_axes_locatable(ax) cax = divider.append_axes('right', size='5%', pad=0.05)
measure_pos = np.zeros((Nx, Ny, Nz)) measure_pos[-npml-10, Ny//2, Nz//2] = 1 def objective(eps_space): F.eps_r *= eps_space measured = [] for t_index in range(steps): fields = F.forward(Jz=source(t_index)) measured.append(npa.sum(fields['Ez'] * measure_pos)) measured_f = my_fft(npa.array(measured)) spectral_power = npa.square(npa.abs(measured_f)) return spectral_power eps_space = 1.0 spectral_power = objective(eps_space) jac_power = jacobian(objective, mode='forward')(eps_space) jac_power_num = jacobian(objective, mode='numerical')(eps_space) n_disp = 140 fig, ax1 = plt.subplots() ax2 = ax1.twinx() delta_f = 1 / steps / dt freq_x = np.arange(n_disp) * delta_f ax1.plot(freq_x, spectral_power[:n_disp], 'k-') ax2.plot(freq_x, jac_power[:n_disp,0], 'g-', label='FMD') ax2.plot(freq_x, jac_power_num[:n_disp,0], 'bo', label='numerical') ax1.set_ylabel('spectral power', color='k') ax2.set_ylabel('dP/depsilon', color='g') ax2.spines['right'].set_color('g') ax2.legend()
np.sum(probes[j] * Hz_i))) / powers[i] # compute the power at the probe if i == j: power_ii += power_ij # add to sum else: power_cross += power_ij # add to sum obj_total += power_ii / (power_ii + power_cross) # power_total += powers_ii / powers_ij return -np.log( obj_total / Nw ) # return negative (for maximizing) and normalize by number of wavelengths (starting objective = 1) # define the gradient for autograd grad_J = jacobian(objective) """ optimization loop """ NIter = 100 # max number of iterations # set the material bounds to (1, eps_max) in design region, (1,1) outside bounds = [(1, eps_max) if design_region.flatten()[i] == 1 else (1, 1) for i in range(eps_r.size)] # call a constrained optimization function from Scipy (progress prints to terminal) minimize(objective, eps_r, args=(), method='L-BFGS-B', jac=grad_J, bounds=bounds,
return npa.abs(npa.sum(output_vector)) def fn_spsp_entries(entries): # sparse matrix multiplication (Ax = b) as a function of matrix entries 'A(entries)' entries_c, indices_c = spsp_mult(entries_const, indices_const, entries, indices_const, N=N) x = sp_solve(entries_c, indices_c, b_const) return out_fn(x) entries = make_rand_complex(M) # doesnt pass yet grad_rev = ceviche.jacobian(fn_spsp_entries, mode='reverse')(entries)[0] grad_true = grad_num(fn_spsp_entries, entries) npa.testing.assert_almost_equal(grad_rev, grad_true, decimal=DECIMAL) # Testing Gradients of 'Sparse-Sparse Multiply entries Forward-mode' grad_for = ceviche.jacobian(fn_spsp_entries, mode='forward')(entries)[0] grad_true = grad_num(fn_spsp_entries, entries) npa.testing.assert_almost_equal(grad_for, grad_true, decimal=DECIMAL) ## TESTS SPARSE MATRX CREATION A = make_rand_sparse(N, M) B = make_rand_sparse(N, M) C_true = A.dot(B).todense()
# total powers of the input and measured P_in = np.sum(spect_in[:steps // 4]) P_out = np.sum(spect[:steps // 4]) # max power, for normalization P_in_max = np.max(spect_in[:steps // 4]) coupling_efficiency = P_out / P_in print('calculated a coupling efficiency of {} %'.format(100 * coupling_efficiency)) """ DIFFERENTIATION W.R.T. FILL FACTOR """ # compute jacobians print('-> FMD-ing') jac_FMD = jacobian(spectral_power, mode='forward')(ff) do_num_jac = False # whether to compute numerical jacobian as well if do_num_jac: jac_num = jacobian(spectral_power, mode='numerical')(ff) delta_f = 1 / steps / dt # frequency spacing in the FFT output freq_x = np.arange(n_disp) * delta_f # frequency range # plot derivatives along with spectral power if plot_all: right_color = '#c23b22' fig, ax1 = plt.subplots() ax1.plot(freq_x / 1e12, spect_in[:n_disp] / P_in_max, label='input') ax1.plot(freq_x / 1e12, spect[:n_disp] / P_in_max, label='output') ax1.set_ylabel('normalized power (P)', color='k') ax1.set_xlabel('frequency (THz)')