def compute_fom_and_gradient_hz(self, omega_idx, device_permittivity): omega = self.omega_values[omega_idx] fwd_Ex, fwd_Ey, fwd_Hz = self.compute_forward_fields_hz( omega, device_permittivity) fom = (1. / self.hz_transmission_normalization[omega_idx] ) * self.compute_fom_from_fields_hz( fwd_Ex, fwd_Ey, fwd_Hz, self.src_scattered_Ex_by_omega[omega_idx, :], self.src_scattered_Ey_by_omega[omega_idx, :], self.src_scattered_Hz_by_omega[omega_idx, :]) scattered_Ex = ( fwd_Ex[self.transmission_x_bounds[0]:self.transmission_x_bounds[1], self.transmission_y] - self.src_scattered_Ex_by_omega[omega_idx, :]) adj_source = np.zeros( (self.simulation_width_voxels, self.simulation_height_voxels), dtype=np.complex) adj_source[self.transmission_x_bounds[0]:self.transmission_x_bounds[1], self.transmission_y] = -np.conj(scattered_Ex) simulation = ceviche.fdfd_hz(omega, self.mesh_size_m, self.rel_eps_simulation, [self.pml_voxels, self.pml_voxels]) adj_Ex, adj_Ey, adj_Hz = simulation.solve(adj_source) gradient = (1. / self.hz_transmission_normalization[omega_idx] ) * 2 * np.real(omega * eps_nought * (fwd_Ex * adj_Ex + fwd_Ey * adj_Ey) / (1j)) return fom, gradient
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_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_Hz(self): print('\ttesting Hz') F = fdfd_hz(self.omega, self.dL, self.eps_r, self.npml) Ex, Ey, Hz = F.solve(self.source) Hz_max = np.max(np.abs(Hz)) plt.imshow(np.real(Hz), cmap='RdBu', vmin=-Hz_max / 5, vmax=Hz_max / 5) plt.show()
def test_Hz(self): print('\ttesting Hz') F = fdfd_hz(self.omega, self.dL, self.eps_r, self.npml) Ex, Ey, Hz = F.solve(self.source) plot_component = Hz field_max = np.max(np.abs(plot_component)) plt.imshow(np.real(plot_component), cmap='RdBu', vmin=-field_max / 5, vmax=field_max / 5) plt.show()
def compute_forward_fields_hz(self, omega, device_permittivity, normalization_permittivity=False): self.rel_eps_simulation[self.device_width_start:self.device_width_end, self.device_height_start:self. device_height_end] = device_permittivity choose_permittivity = self.rel_eps_simulation if normalization_permittivity: choose_permittivity = np.ones(self.rel_eps_simulation.shape) simulation = ceviche.fdfd_hz(omega, self.mesh_size_m, choose_permittivity, [self.pml_voxels, self.pml_voxels]) fwd_Ex, fwd_Ey, fwd_Hz = simulation.solve(self.fwd_source_hz) return fwd_Ex, fwd_Ey, fwd_Hz
def data_generation(full_pattern: np.array) -> np.array: # Grating pattern eps_r[space_x + pml_x:space_x + pml_x + device_pxls_x, space_y + pml_y:space_y + pml_y + device_pxls_y] = full_pattern #ceviche.viz.abs(eps_r[:, int(2431.25/dL):int(2800/dL)], cbar=True) #ceviche.viz.abs(np.hstack((np.ones([256,5])*2.1025,full_pattern[0,5,5])), cbar=True) #ceviche.viz.abs(np.hstack((np.ones([int(1600/dL),int(31.25/dL)])*2.1025,full_pattern[0,5,5])), cbar=True) # Set up the FDFD simulation for TM F = fdfd_hz(omega, dL[0] * 1e-9, eps_r, npml) # Source source_amp = 64e9 / dL[0] / dL[1] random_source = np.zeros(grid_shape, dtype=complex) k0 = 2 * np.pi / wavelength n_angles = 4 # number of different angles in each line source #bottom source bottom_source_loc_y = pml_y + int(space_y / 2) source_amp_x = np.zeros(int(1 / 2 * Nx), dtype=complex) for j in range(n_angles): angle_deg = random.randint(-90, 90) angle_rad = angle_deg * np.pi / 180 # Compute the wave vector kx = k0 * np.cos(angle_rad) ky = k0 * np.sin(angle_rad) # Get an array of the x positions across the simulation domain Lx = Nx * dL[0] * 1e-9 x_vec = np.linspace(-Lx / 4, Lx / 4, int(1 / 2 * Nx)) # Make a new source where source[x] ~ exp(i * kx * x) to simulate an angle phase = 2 * np.pi * random.random() source_amp_x += np.exp(1j * kx * x_vec + phase) source_amp_x *= 1 / n_angles # print("shapes: ", random_source[int(1/4*Nx):int(3/4*Nx), bottom_source_loc_y].shape, source_amp_x.shape) random_source[int(1 / 4 * Nx):int(3 / 4 * Nx), bottom_source_loc_y] = source_amp * source_amp_x #top source top_source_loc_y = Ny - 1 - pml_y - int(space_y / 2) source_amp_x = np.zeros(int(1 / 2 * Nx), dtype=complex) for j in range(n_angles): angle_deg = random.randint(-90, 90) angle_rad = angle_deg * np.pi / 180 # Compute the wave vector kx = k0 * np.cos(angle_rad) ky = k0 * np.sin(angle_rad) # Get an array of the x positions across the simulation domain Lx = Nx * dL[0] * 1e-9 x_vec = np.linspace(-Lx / 4, Lx / 4, int(1 / 2 * Nx)) # Make a new source where source[x] ~ exp(i * kx * x) to simulate an angle phase = 2 * np.pi * random.random() source_amp_x += np.exp(1j * kx * x_vec + phase) source_amp_x *= 1 / n_angles random_source[int(1 / 4 * Nx):int(3 / 4 * Nx), top_source_loc_y] = source_amp * source_amp_x #left source left_source_loc_x = pml_x + int(space_x / 2) source_amp_y = np.zeros(int(1 / 2 * Ny), dtype=complex) for j in range(n_angles): angle_deg = random.randint(-90, 90) angle_rad = angle_deg * np.pi / 180 # Compute the wave vector kx = k0 * np.cos(angle_rad) ky = k0 * np.sin(angle_rad) # Get an array of the x positions across the simulation domain Ly = Ny * dL[1] * 1e-9 y_vec = np.linspace(-Ly / 4, Ly / 4, int(1 / 2 * Ny)) # Make a new source where source[x] ~ exp(i * kx * x) to simulate an angle phase = 2 * np.pi * random.random() source_amp_y += np.exp(1j * ky * y_vec + phase) source_amp_y *= 1 / n_angles random_source[left_source_loc_x, int(1 / 4 * Ny):int(3 / 4 * Ny)] = source_amp * source_amp_y #right source right_source_loc_x = Nx - 1 - pml_x - int(space_x / 2) source_amp_y = np.zeros(int(1 / 2 * Ny), dtype=complex) for j in range(n_angles): angle_deg = random.randint(-90, 90) angle_rad = angle_deg * np.pi / 180 # Compute the wave vector kx = k0 * np.cos(angle_rad) ky = k0 * np.sin(angle_rad) # Get an array of the x positions across the simulation domain Ly = Ny * dL[1] * 1e-9 y_vec = np.linspace(-Ly / 4, Ly / 4, int(1 / 2 * Ny)) # Make a new source where source[x] ~ exp(i * kx * x) to simulate an angle phase = 2 * np.pi * random.random() source_amp_y += np.exp(1j * ky * y_vec + phase) source_amp_y *= 1 / n_angles random_source[right_source_loc_x, int(1 / 4 * Ny):int(3 / 4 * Ny)] = source_amp * source_amp_y # Solve the FDFD simulation for the fields, offset the phase such that Ex has 0 phase at the center of the bottom row of the window Ex_forward, Ey_forward, Hz_forward = F.solve(random_source) #ceviche.viz.real(Ex_forward[:, int(2400/dL):int(2800/dL)], outline = eps_r[:, int(2400/dL):int(2800/dL)], cbar = True) #ceviche.viz.real(Hz_out_forward, outline = np.hstack((np.ones([int(1600/dL),int(31.25/dL)])*2.1025,full_pattern[0,5,5])), cbar = True) return eps_r, Hz_forward, Ex_forward, Ey_forward, random_source
# make design region box_region = np.zeros((Nx, Ny)) box_region[npml + 2 * spc:npml + 2 * spc + int(L / dL), npml + 2 * spc:npml + 2 * spc + int(L / dL)] = 1 # make the accelration probe probe = np.zeros((Nx, Ny), dtype=np.complex128) probe[-npml - spc, Ny // 2] = 1 # plot the probe through channel if PLOT: plt.imshow(np.abs(imarr(probe + box_region + source))) plt.show() # vacuum test, get normalization F = fdfd_hz(omega, dL, eps_r, [npml, npml]) Ex, Ey, Hz = F.solve(source) E_mag = np.sqrt(np.square(np.abs(Ex)) + np.square(np.abs(Ey))) H_mag = np.abs(Hz) I_E0 = np.abs(np.square(np.sum(E_mag * probe))) I_H0 = np.abs(np.square(np.sum(H_mag * probe))) print('I_H0 = {}'.format(I_H0)) # plot the vacuum fields if PLOT: plt.imshow(np.real(imarr(Hz)), cmap='RdBu') plt.title('real(Hz)') plt.xlabel('y') plt.ylabel('x') plt.colorbar()
eps_r[design_region == 1] = eps_max # make the accelration probe eta = np.zeros((Nx, Ny), dtype=np.complex128) channel_ys = np.arange(Ny) eta[Nx // 2, :] = np.exp(1j * 2 * np.pi * channel_ys / Ny) # plot the probe through channel if PLOT: plt.plot(np.real(imarr(eta[Nx // 2, :])), label='RE\{eta\}') plt.xlabel('position along channel (y)') plt.ylabel('eta (y)') plt.show() # vacuum test, get normalization F = fdfd_hz(omega, dL, eps_r, npml) Ex, Ey, Hz = F.solve(source) E_mag = np.sqrt(np.square(np.abs(Ex)) + np.square(np.abs(Ey))) E0 = np.max(E_mag[spc:-spc, :]) print('E0 = {} V/m'.format(E0)) # plot the vacuum fields if PLOT: plt.imshow(np.real(Ey) / E0, cmap='RdBu') plt.title('E_y / E0 (<-)') plt.xlabel('y') plt.ylabel('x') plt.colorbar() plt.show()
return npa.divide(npa.tanh(gamma * eta) + npa.tanh(gamma * (rho - eta)), npa.tanh(gamma * eta) + npa.tanh(gamma * (1 - eta))) def convert_rho_epsr(rho): """ Helper function to convert the material density rho to permittivity eps_r """ return epsr_min + (epsr_max-epsr_min)*operator_proj(make_rho(rho, design_region, blur_radius, Ny), 0.5, gamma) rho_init = 0.5 * design_region eps_r = convert_rho_epsr(rho_init) ### Source term source = np.zeros((Nx, Ny)) source[npml[0]+10, :] = 1 # use just this line for single side drive source[-npml[0]-10-1, :] = 1 # add this for dual side drive # Setup simulation F = fdfd_hz(omega, dL, eps_r, npml) # create an object that stores our parameters and current structure. Ex, Ey, Hz = F.solve(source) # calculate the fields for our source term # Objective function probe = np.zeros((Nx, Ny), dtype=np.complex128) probe[spc+pillar_width+gap//2:spc+pillar_width+gap//2+2,:] = np.exp(-1j * 2*np.pi * np.arange(Ny)/Ny) def objective(rho): """ Objective function measuring the acceleration gradient """ eps_arr = convert_rho_epsr(design_region*rho.reshape((Nx, Ny))) 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
def data_generation(full_pattern: np.array) -> np.array: # Grating pattern eps_r[:, int(2431.25/dL):int(2800/dL)] = full_pattern; #ceviche.viz.abs(eps_r[:, int(2431.25/dL):int(2800/dL)], cbar=True); #ceviche.viz.abs(np.hstack((np.ones([256,5])*2.1025,full_pattern[0,5,5])), cbar=True); #ceviche.viz.abs(np.hstack((np.ones([int(1600/dL),int(31.25/dL)])*2.1025,full_pattern[0,5,5])), cbar=True); # Set up the FDFD simulation for TM F = fdfd_hz(omega, dL*1e-9, eps_r, npml); # Source source_amp = 64e9/dL/dL; source_loc_y = int(2320/dL); # Define the source as just a constant source along x at `y = source_loc_y`, modeling plane wave source = np.zeros(grid_shape, dtype=complex); source[:, source_loc_y] = source_amp; # Add a source directly behind to cancel back traveling wave (for TFSF effect) source[:, source_loc_y-1] = source[:, source_loc_y] * np.exp(-1j * k_sub * dL*1e-9 - 1j * np.pi); # Solve the FDFD simulation for the fields, offset the phase such that Ex has 0 phase at the center of the bottom row of the window Ex_forward, Ey_forward, Hz_forward = F.solve(source); Hz_out_forward = Hz_forward[:, int(2400/dL):int(2800/dL)]*np.exp(-1j*0.784368210509431); Ex_out_forward = Ex_forward[:, int(2400/dL):int(2800/dL)]*np.exp(-1j*0.784368210509431); Ey_out_forward = Ey_forward[:, int(2400/dL):int(2800/dL)]*np.exp(-1j*0.784368210509431); #ceviche.viz.real(Ex_forward[:, int(2400/dL):int(2800/dL)], outline = eps_r[:, int(2400/dL):int(2800/dL)], cbar = True); #ceviche.viz.real(Hz_out_forward, outline = np.hstack((np.ones([int(1600/dL),int(31.25/dL)])*2.1025,full_pattern[0,5,5])), cbar = True); # Adjoint simulation k0 = 2 * np.pi / wavelength; # Wavenumber in air # Oblique incident angle, described below # Forward: # y # ^ / # | / # | / # |/ angle theta #----------------------->x # | # | # | # | # Backward: # | # | # | # | # x<-------------------------- # angle / | # / | # / | # / | # y angle_deg = 90 - np.arcsin(wavelength/period)*180/np.pi; angle_rad = angle_deg * np.pi / 180; source_loc_y_adj = int(3200/dL); # Compute the wave vector kx = k0 * np.cos(angle_rad); ky = k0 * np.sin(angle_rad); #k_vector = [kx, ky]; # Get an array of the x positions across the simulation domain Lx = Nx * dL*1e-9; x_vec = np.linspace(-Lx / 2, Lx / 2, Nx); # Make a new source where source[x] ~ exp(i * kx * x) to simulate an angle source_amp_x = np.exp(1j * kx * x_vec); source_angle = np.zeros(grid_shape, dtype=complex); source_angle[:, source_loc_y_adj] = source_amp * source_amp_x; # Add another source panel directly behind to cancel the back-traveling wave source_angle[:, source_loc_y_adj + 1] = source_angle[:, source_loc_y_adj] * np.exp(-1j * ky * dL*1e-9 - 1j * np.pi); # Add a bloch phase of kx * Lx across boundary to compensate for plane wave F_Bloch = fdfd_hz(omega, dL*1e-9, eps_r, npml, bloch_phases=[kx * Lx, 0]); # Solve the adjoint fields, offset the phase such that Ey has 0 phase at the center of the top row of the window Ex_adjoint, Ey_adjoint, Hz_adjoint = F_Bloch.solve(source_angle); Hz_out_adjoint = Hz_adjoint[:, int(2400/dL):int(2800/dL)]*np.exp(-1j*(-0.27908708565765883)); Ex_out_adjoint = Ex_adjoint[:, int(2400/dL):int(2800/dL)]*np.exp(-1j*(-0.27908708565765883)); Ey_out_adjoint = Ey_adjoint[:, int(2400/dL):int(2800/dL)]*np.exp(-1j*(-0.27908708565765883)); #ceviche.viz.real(Ex_adjoint[:, 300:350], outline = eps_r[:, 300:350], cbar = True); return -Hz_out_forward, Ex_out_forward, Ey_out_forward, -Hz_out_adjoint, Ex_out_adjoint, Ey_out_adjoint;
# make a list of probe locations, evenly spaced in x and at probe_index_y probes = [] space_x = Nx / float(Nw) # even spacing between probes for i in range(Nw): index_i = int(space_x * (1 / 2 + i)) # the x index of the ith probe probe_i = np.zeros((Nx, Ny)) # ith probe probe_i[index_i, probe_index_y] = 1 # define at this point probes.append(probe_i) # add to list # make a list of FDFDs at each wavelength and their power normalizations fdfds = [] powers = [] for i, lam in enumerate(wavelengths): omega_i = 2 * np.pi * C_0 / lam # the angular frequency fdfd_i = fdfd_hz(omega_i, dL, eps_r, npml=[0, npml]) # make an FDFD simulation fdfds.append(fdfd_i) # add it to the list Ex, Ey, Hz = fdfd_i.solve(source) # solve the fields powers.append(np.sum( np.square(np.abs(Hz) * probes[i]))) # compute the power at its probe, add to list # plot the domain (design region + probes + source) if PLOT: plt.imshow((sum(probes) + source + design_region).T, cmap='Greys') plt.title('domain') plt.xlabel('x') plt.ylabel('y') plt.colorbar() plt.show()