def grad(self, input_vals: List[goos.Flow], grad_val: goos.ArrayFlow.Grad) -> List[goos.Flow.Grad]: eps = input_vals[0].array sim = FdfdSimProp(eps=eps, source=np.zeros_like(eps), wlen=self._wlen, dxes=self._dxes, pml_layers=self._pml_layers, bloch_vec=self._bloch_vector, grid=self._grid) omega = 2 * np.pi / sim.wlen for out, g in zip(self._outputs, grad_val.flows_grad): out.before_adjoint_sim(sim, g) adjoint_fields = self._solver.solve( omega=2 * np.pi / sim.wlen, dxes=sim.dxes, epsilon=fdfd_tools.vec(sim.eps), mu=None, J=fdfd_tools.vec(sim.source), pml_layers=sim.pml_layers, bloch_vec=sim.bloch_vec, adjoint=True, ) adjoint_fields = np.stack(fdfd_tools.unvec(adjoint_fields, eps[0].shape), axis=0) grad = -1j * omega * np.conj( adjoint_fields) * self._last_results.fields if np.isrealobj(eps): grad = 2 * np.real(grad) return [goos.NumericFlow.Grad(array_grad=grad)]
def grad(self, input_vals: List[goos.Flow], grad_val: goos.ArrayFlow.Grad) -> List[goos.Flow.Grad]: """Runs the simulation. Args: input_vals: List with single element corresponding to the permittivity distribution. Returns: Simulated fields. """ if self._last_eps is None or self._last_eps != input_vals[0]: eps = input_vals[0].array sim = EigSimProp(eps=eps, source=np.zeros_like(eps), wlen=self._wlen, dxes=self._dxes, pml_layers=self._pml_layers, bloch_vec=self._bloch_vector, grid=self._grid) for out in self._outputs: out.before_adjoint_sim(sim) #TODO vcruysse: for now we limit to 1 eigenvalue fields, omega = self._solver.solve(omega=2 * np.pi / sim.wlen, dxes=sim.dxes, J=fdfd_tools.vec(sim.source), epsilon=fdfd_tools.vec(sim.eps), mu=None, bloch_vec=sim.bloch_vec, pml_layers=sim.pml_layers, symmetry=sim.symmetry, n_eig=1) fields = fdfd_tools.unvec(fields[0], eps[0].shape) sim.fields = np.stack(fields, axis=0) sim.omegas = np.real(omega[0]) self._last_eps = input_vals[0] self._last_results = sim fields = self._last_results.fields omega = self._last_results.omegas eps = self._last_results.eps domega_deps = np.real(-omega / 2 * 1 / (fields.flatten().conj() @ fields.flatten()) * fields.conj() * eps**(-1) * fields) # find the grad for omega for out, grad in zip(self._outputs, grad_val): if isinstance(out, EigenValueImpl): df_domega = grad.array_grad[0] return [goos.NumericFlow.Grad(array_grad=df_domega * domega_deps)]
def test_gaussian_source_2d(): grid = gridlock.Grid(simspace.create_edge_coords( goos.Box3d(center=[0, 0, 0], extents=[14000, 40, 3000]), 40), ext_dir=gridlock.Direction.z, initial=1, num_grids=3) grid.render() eps = np.array(grid.grids) dxes = [grid.dxyz, grid.autoshifted_dxyz()] sim = maxwell.FdfdSimProp(eps=eps, source=np.zeros_like(eps), wlen=1550, dxes=dxes, pml_layers=[10, 10, 0, 0, 10, 10], grid=grid, solver=maxwell.DIRECT_SOLVER) src = maxwell.GaussianSourceImpl( maxwell.GaussianSource(w0=5200, center=[0, 0, 0], extents=[14000, 0, 0], normal=[0, 0, -1], power=1, theta=0, psi=0, polarization_angle=np.pi / 2, normalize_by_sim=True)) src.before_sim(sim) fields = maxwell.DIRECT_SOLVER.solve( omega=2 * np.pi / sim.wlen, dxes=sim.dxes, epsilon=fdfd_tools.vec(sim.eps), mu=None, J=fdfd_tools.vec(sim.source), pml_layers=sim.pml_layers, bloch_vec=sim.bloch_vec, ) fields = fdfd_tools.unvec(fields, grid.shape) field_y = fields[1].squeeze() np.testing.assert_allclose(field_y[:, 53], np.zeros_like(field_y[:, 53]), atol=1e-4) # Calculate what the amplitude of the Gaussian field should look like. # Amplitude determined empirically through simulation. coords = (np.arange(len(field_y[:, 20])) - len(field_y[:, 20]) / 2) * 40 target_gaussian = np.exp(-coords**2 / 5200**2) * 0.00278 np.testing.assert_allclose(np.abs(field_y[:, 20]), target_gaussian, atol=1e-4)
def get_fg_and_bg(simspace: SimulationSpace, wlen: float ) -> Tuple[fdfd_tools.VecField, fdfd_tools.VecField]: """Quick utility function to construct the fg and bg permittivities. Args: simspace: SimulationSpace object. wlen: Wavelength to plot simulation space. Returns: A tuple `(eps_fg, eps_bg)` where `eps_fg` is the permittivity if the structure vector is all ones and `eps_bg` is the permittivity if the structure vector is all zeros. """ simspace_inst = simspace(wlen) # Number of elements in structure vector. num_el = simspace_inst.selection_matrix.shape[1] eps_bg = fdfd_tools.vec(simspace_inst.eps_bg.grids) eps_fg = eps_bg + simspace_inst.selection_matrix @ np.ones(num_el) # Reshape into the appropriate size. return fdfd_tools.unvec(eps_fg, simspace.dims), fdfd_tools.unvec( eps_bg, simspace.dims)
def compute_overlap_e_angled( E: field_t, H: field_t, axis: int, omega: complex, dxes: dx_lists_t, slices: List[slice], mu: field_t = None, ) -> field_t: """ Given an eigenmode obtained by solve_waveguide_mode, calculates overlap_e for the mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) [assumes reflection symmetry]. overlap_e makes use of the e2h operator to collapse the above expression into (vec(E) @ vec(overlap_e)), allowing for simple calculation of the mode overlap. :param E: E-field of the mode :param H: H-field of the mode (advanced by half of a Yee cell from E) :axis: propagation axis :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use as the waveguide cross-section. slices[axis] should select only one :param mu: Magnetic permeability (default 1 everywhere) :return: overlap_e for calculating the mode overlap """ if (slices[axis].stop - slices[axis].start) != 1: raise ValueError('The slice in the axis direction is not 1 wide.') # Write out the operator product for the mode orthogonality integral domain = np.zeros_like(E) domain[[slice(0, 3)] + slices] = 1 dn = np.zeros_like(E) dn[axis] = np.ones_like(E[axis]) dn_vec = vec(dn) e2h = operators.e2h(omega, dxes, mu) ds = sparse.diags(vec(domain)) h_cross_ = operators.poynting_h_cross(np.conj(vec(H)), dxes) e_cross_ = operators.poynting_e_cross(np.conj(vec(E)), dxes) overlap_e = dn_vec @ ds @ (-h_cross_ + e_cross_ @ e2h) # Normalize norm_factor = np.abs(overlap_e @ vec(E)) overlap_e /= norm_factor return unvec(overlap_e, E[0].shape)
def eval(self, input_vals: List[goos.Flow]) -> goos.ArrayFlow: """Runs the simulation. Args: input_vals: List with single element corresponding to the permittivity distribution. Returns: Simulated fields. """ if self._last_eps is None or self._last_eps != input_vals[0]: eps = input_vals[0].array sim = EigSimProp(eps=eps, source=np.zeros_like(eps), wlen=self._wlen, dxes=self._dxes, pml_layers=self._pml_layers, bloch_vec=self._bloch_vector, symmetry=self._symmetry, grid=self._grid) for src in self._sources: src.before_sim(sim) for out in self._outputs: out.before_sim(sim) #TODO vcruysse: for now we limit to 1 eigenvalue fields, omega = self._solver.solve(omega=2 * np.pi / sim.wlen, dxes=sim.dxes, J=fdfd_tools.vec(sim.source), epsilon=fdfd_tools.vec(sim.eps), mu=None, bloch_vec=sim.bloch_vec, pml_layers=sim.pml_layers, symmetry=sim.symmetry, n_eig=1) fields = fdfd_tools.unvec(fields[0], eps[0].shape) sim.fields = np.stack(fields, axis=0) sim.omegas = np.real(omega[0]) self._last_eps = input_vals[0] self._last_results = sim return goos.ArrayFlow( [out.eval(self._last_results) for out in self._outputs])
def eval(self, input_vals: List[np.ndarray]) -> np.ndarray: """Returns the 3D vector field. Args: input_vals: Single-element list containing the field. Returns: List of all three field components. """ # Get fields. fields = fdfd_tools.unvec(input_vals[0], self._sim_space.dims) # Make slices. if self._slices: fields = [field[tuple(self._slices)] for field in fields] return fields
def compute_overlap_annulus( E: field_t, omega: complex, dxes: dx_lists_t, axis: int, mu: field_t = None, ) -> field_t: """This is hopefully going to calculate the overlap """ # need to extract the size of dxes to adjust the size of mask and the E field len_dxes = np.concatenate(dxes, axis=0) # want to extract the absolute value, x=0, y=1, z=2 # Domain is all zero domain = np.zeros_like(E[0], dtype=int) # Then set the slices part equal to 1 - may need to use our mask t = np.abs(len_dxes[0].size) # TODO adjust these values, may call from problem mask = annulus(t, t, [0, 0], 0.3 * t, 0.2 * t) mask = mask[..., newaxis] # adds the z to mask domain[mask] = 1 npts = E[0].size dn = np.zeros(npts * 3, dtype=int) dn[0:npts] = 1 dn = np.roll(dn, npts * axis) e2h = operators.e2h(omega, dxes, mu) # Hopefully this works with the mask ds = sparse.diags(vec([domain] * 3)) e_cross_ = operators.poynting_e_cross(vec(E), dxes) # Difference is, we have no H field overlap_e = dn @ ds @ (e_cross_ @ e2h) # Normalize norm_factor = np.abs(overlap_e @ vec(E)) overlap_e /= norm_factor return unvec(overlap_e, E[0].shape)
def test_simspace_reduced(): mat_stack = optplan.GdsMaterialStack( background=optplan.Material(mat_name="Air"), stack=[ optplan.GdsMaterialStackLayer( foreground=optplan.Material(mat_name="SiO2"), background=optplan.Material(mat_name="SiO2"), gds_layer=[101, 0], extents=[-10000, -110], ), optplan.GdsMaterialStackLayer( foreground=optplan.Material(mat_name="Si"), background=optplan.Material(mat_name="Air"), gds_layer=[100, 0], extents=[-110, 110], ), ], ) simspace_spec = optplan.SimulationSpace( mesh=optplan.UniformMesh(dx=40), eps_fg=optplan.GdsEps(gds="WDM_example_fg.gds", mat_stack=mat_stack), eps_bg=optplan.GdsEps(gds="WDM_example_bg.gds", mat_stack=mat_stack), sim_region=optplan.Box3d(center=[0, 0, 0], extents=[5000, 5000, 500]), pml_thickness=[10, 10, 10, 10, 0, 0], selection_matrix_type=optplan.SelectionMatrixType.REDUCED.value, ) space = simspace.SimulationSpace(simspace_spec, TESTDATA) space_inst = space(1550) eps_bg = space_inst.eps_bg.grids eps_fg = fdfd_tools.unvec( fdfd_tools.vec(space_inst.eps_bg.grids) + space_inst.selection_matrix @ np.ones(np.prod(space._design_dims)), space_inst.eps_bg.shape) assert space_inst.selection_matrix.shape == (609375, 2601) np.testing.assert_array_equal(eps_bg[2][:, :, -3], 1) np.testing.assert_allclose(eps_bg[2][10, 10, 2], 2.0852) np.testing.assert_allclose(eps_fg[2][107, 47, 2], 2.0852) np.testing.assert_allclose(eps_fg[2][107, 47, 6], 12.086617) np.testing.assert_allclose(eps_fg[2][112, 57, 6], 1) np.testing.assert_allclose(eps_fg[2][107, 47, 10], 1)
def __init__(self, input_function: problem.OptimizationFunction, overlap_model: np.array, slice_model, slice_in, slice_out, path: np.array, wavelength, shape, num_phase_checks=10): """Constructs the objective. Args: input_function: Input objectives (typically a simulation). overlap: WaveguideModeOverlap of the mode of interest path: path from the source along which to take the phase difference """ super().__init__(input_function) self._input = input_function self.overlap_model = overlap_model self.path = path self.wavelength = wavelength self.num_phase_checks = num_phase_checks self.model_unvec = fdfd_tools.unvec(overlap_model, shape) self.model_region = [ self.model_unvec[i][tuple(slice_model)] for i in range(3) ] self.overlap_in_unvec = np.zeros_like(self.model_unvec) self.overlap_out_unvec = np.zeros_like(self.model_unvec) for i in range(3): self.overlap_in_unvec[i][tuple(slice_in)] = self.model_region[i] self.overlap_out_unvec[i][tuple(slice_out)] = self.model_region[i] self.overlap_in = fdfd_tools.vec(self.overlap_in_unvec) self.overlap_out = fdfd_tools.vec(self.overlap_out_unvec) self.overlap_in_function = OverlapFunction(input_function, self.overlap_in) self.overlap_out_function = OverlapFunction(input_function, self.overlap_out)
def compute_source_angle( E: field_t, H: field_t, wavevector: List[float], omega: complex, eps: List[np.array], dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], mu: field_t = None, ) -> field_t: """ Given an eigenmode obtained by solve_waveguide_mode, returns the current source distribution necessary to position a unidirectional source at the slice location. :param E: E-field of the mode :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use as the waveguide cross-section. slices[axis] should select only one :param mu: Magnetic permeability (default 1 everywhere) :return: J distribution for the unidirectional source """ if polarity == 1: Mslice = copy.deepcopy(slices) Jslice = copy.deepcopy(slices) Jslice[axis] = slice(Jslice[axis].start + 1, Jslice[axis].stop + 1) elif polarity == -1: Mslice = copy.deepcopy(slices) Mslice[axis] = slice(Mslice[axis].start - 1, Mslice[axis].stop - 1) Jslice = copy.deepcopy(slices) if mu is None: mu = np.ones_like(eps) curl_h = lambda x: unvec( operators.curl_h(dxes=dxes, bloch_vec=wavevector) @ vec(x), x[0].shape) curl_e = lambda x: unvec( operators.curl_e(dxes=dxes, bloch_vec=wavevector) @ vec(x), x[0].shape) M_temp = -np.array(curl_e(E)) - 1j * omega * np.array(mu) * np.array(H) J_temp = np.array(curl_h(H)) - 1j * omega * np.array(eps) * np.array(E) M = np.zeros_like(M_temp) J = np.zeros_like(J_temp) J[[slice(0, 3)] + Jslice] = J_temp[[slice(0, 3)] + Jslice] M[[slice(0, 3)] + Mslice] = M_temp[[slice(0, 3)] + Mslice] J[axis] = np.zeros_like(J[axis]) M[axis] = np.zeros_like(M[axis]) Js = np.array(n_cross(H)) Ms = -np.array(n_cross(E)) Jm = np.array(curl_h(M / mu)) J = J - Jm / (-1j * omega) return J
def mode_solver(bloch_vec: np.ndarray, omega_appx: float, num_modes: int, dxes: dx_lists_t, epsilon: np.ndarray, op_type: str, set_init_cond: bool, init_vec: np.ndarray = None, mu: np.ndarray = None, shift_orthogonal=np.zeros((3, 3))): if mu is None: mu = np.ones_like(epsilon) # Setting up the operator if op_type == 'efield': op, eps_norm, eps_un_norm = efield_operator(bloch_vec, dxes, vec(epsilon), vec(mu), shift_orthogonal) elif op_type == 'hfield': op, eps_norm, eps_norm = hfield_operator(bloch_vec, dxes, vec(epsilon), vec(mu), shift_orthogonal) else: raise ValueError('Undefined operator type') if set_init_cond and init_vec is None: # Starting with initial condition with a FDFD simulation J = np.zeros_like(epsilon) J[0][J.shape[1] // 2, J.shape[2] // 2, J.shape[3] // 2] = 1.0 J[1][J.shape[1] // 2, J.shape[2] // 2, J.shape[3] // 2] = 1.0 #J[2][J.shape[1]//2, J.shape[2]//2,J.shape[3]//2] = 1.0 solver = DirectSolver() sim_args = { 'omega': omega_appx, 'dxes': dxes, 'epsilon': vec(epsilon), 'mu': vec(mu), 'J': vec(J), 'bloch_vec': bloch_vec } E = solver.solve(**sim_args) elec_field = unvec(E, J[0].shape) if op_type == 'efield': mode_estimate = eps_un_norm @ E elif op_type == 'hfield': op_e2h = operators.e2h(omega=1.0, dxes=dxes, mu=vec(mu), bloch_vec=bloch_vec) mode_estimate = op_e2h @ E else: raise ValueError('Undefined operator type') # Solving for eigen values eig_value, mode_field = scipy.sparse.linalg.eigs(op, num_modes, sigma=omega_appx**2, v0=mode_estimate) elif init_vec is not None: eig_value, mode_field = scipy.sparse.linalg.eigs(op, 1, sigma=omega_appx**2, v0=init_vec) else: eig_value, mode_field = scipy.sparse.linalg.eigs(op, num_modes, sigma=omega_appx**2) omega = np.sqrt(eig_value) if op_type == 'efield': if len(eig_value) == 1: mode_field = eps_norm @ mode_field else: mode_field = [eps_norm @ mode for mode in mode_field] return omega, mode_field.transpose()
def test_straight_waveguide_power(): """Tests that a straight waveguide with a single source and overlap.""" # TODO(logansu): Refactor. class Simspace: def __init__(self, filepath, params: optplan.SimulationSpace): # Setup the grid. self._dx = params.mesh.dx from spins.invdes.problem_graph.simspace import _create_edge_coords self._edge_coords = _create_edge_coords(params.sim_region, self._dx) self._ext_dir = gridlock.Direction.z # Currently always extrude in z. # TODO(logansu): Factor out grid functionality and drawing. # Create a grid object just so we can calculate dxes. self._grid = gridlock.Grid(self._edge_coords, ext_dir=self._ext_dir, num_grids=3) self._pml_layers = params.pml_thickness self._filepath = filepath self._eps_bg = params.eps_bg @property def dx(self) -> float: return self._dx @property def dxes(self) -> fdfd_tools.GridSpacing: return [self._grid.dxyz, self._grid.autoshifted_dxyz()] @property def pml_layers(self) -> fdfd_tools.PmlLayers: return self._pml_layers @property def dims(self) -> Tuple[int, int, int]: return [ len(self._edge_coords[0]) - 1, len(self._edge_coords[1]) - 1, len(self._edge_coords[2]) - 1 ] @property def edge_coords(self) -> fdfd_tools.GridSpacing: return self._edge_coords def __call__(self, wlen: float): from spins.invdes.problem_graph.simspace import _create_grid from spins.invdes.problem_graph.simspace import SimulationSpaceInstance eps_bg = _create_grid(self._eps_bg, self._edge_coords, wlen, self._ext_dir, self._filepath) return SimulationSpaceInstance(eps_bg=eps_bg, selection_matrix=None) space = Simspace( TESTDATA, optplan.SimulationSpace( pml_thickness=[10, 10, 10, 10, 0, 0], mesh=optplan.UniformMesh(dx=40), sim_region=optplan.Box3d( center=[0, 0, 0], extents=[5000, 5000, 40], ), eps_bg=optplan.GdsEps( gds="straight_waveguide.gds", mat_stack=optplan.GdsMaterialStack( background=optplan.Material(mat_name="air"), stack=[ optplan.GdsMaterialStackLayer( gds_layer=[100, 0], extents=[-80, 80], foreground=optplan.Material(mat_name="Si"), background=optplan.Material(mat_name="air"), ), ], ), ), )) source = creator_em.WaveguideModeSource( optplan.WaveguideModeSource( power=1.0, extents=[40, 1500, 600], normal=[1.0, 0.0, 0.0], center=[-1770, 0, 0], mode_num=0, )) overlap = creator_em.WaveguideModeOverlap( optplan.WaveguideModeOverlap( power=1.0, extents=[40, 1500, 600], normal=[1.0, 0.0, 0.0], center=[1770, 0, 0], mode_num=0, )) wlen = 1550 eps_grid = space(wlen).eps_bg.grids source_grid = source(space, wlen) overlap_grid = overlap(space, wlen) eps = problem.Constant(fdfd_tools.vec(eps_grid)) sim = creator_em.FdfdSimulation( eps=eps, solver=local_matrix_solvers.DirectSolver(), wlen=wlen, source=fdfd_tools.vec(source_grid), simspace=space, ) overlap_fun = creator_em.OverlapFunction(sim, fdfd_tools.vec(overlap_grid)) efield_grid = fdfd_tools.unvec(graph_executor.eval_fun(sim, None), eps_grid[0].shape) # Calculate emitted power. edotj = np.real( fdfd_tools.vec(efield_grid) * np.conj(fdfd_tools.vec(source_grid))) * 40**3 power = -0.5 * np.sum(edotj) # Allow for 4% error in emitted power. assert power > 0.96 and power < 1.04 # Check that overlap observes nearly unity power. np.testing.assert_almost_equal(np.abs( graph_executor.eval_fun(overlap_fun, None))**2, 1, decimal=2)
def build_laguerre_gaussian_source(eps_grid: Grid, omega: float, w0: float, center: np.ndarray, axis: int, slices=List[slice], mu: List[np.ndarray] = None, theta: float = 0, psi: float = 0, polarization_angle: float = 0, m: int = 0, p : int = 0, polarity: int = 1, power: float = 1): """Builds a laguerre gaussian beam source. By default, the laguerre gaussian beam propagates along polarity of the given axis and is linearly polarized along the x-direction if axis is z, y-direction if x and z direction if y. `theta` rotates the propagation direction around the E-field, then 'psi' rotates source plane normal, and the polarization_angle rotates around the propagation direction. Args: eps_grid: gridlock.grid with the permittivity distribution. omega: The frequency of the mode. axis: Direction of propagation. slices: Source slice which define the position of the source in the grid. mu: Permeability distribution. theta: Rotation around the default E-component. psi: Rotation around the source plane normal. polarization_angle: Rotation around the propagation direction. m : parameters of the laguerre gaussian beam p : parameters of the laguerre gaussian beam polarity: 1 if forward propagating. -1 if backward propagating. power: Power is the gaussian beam. Returns: Current source J. """ # Make scalar fields. eps_val = np.real(np.average(eps_grid.grids[0][tuple(slices)])) scalar_field_function = lambda x, y, z, R: laguerre_gaussian_beam_z_axis_x_pol( x, y, z, w0, center, R, omega, m, p, polarity, eps_val) # Get vector fields. fields = scalar2rotated_vector_fields( eps_grid=eps_grid, scalar_field_function=scalar_field_function, mu=mu, omega=omega, axis=axis, slices=slices, theta=theta, psi=psi, polarization_angle=polarization_angle, polarity=polarity, power=power, full_fields=True) # Calculate the source. dxes = [eps_grid.dxyz, eps_grid.autoshifted_dxyz()] # make current field_slices = [slice(None, None)] * 3 J_slices = slices if polarity == -1: ind = slices[axis].start field_slices[axis] = slice(None, ind) J_slices[axis] = slice(ind - 1, ind + 1) else: ind = slices[axis].stop - 1 field_slices[axis] = slice(ind, None) J_slices[axis] = slice(ind - 1, ind + 1) E = np.zeros_like(fields['E']) for i in range(3): E[i][tuple(field_slices)] = fields['E'][i][tuple(field_slices)] full = operators.e_full(omega, dxes, vec(eps_grid.grids), bloch_vec=fields['wavevector']) J_vec = 1 / (-1j * omega) * full @ vec(E) J_temp = unvec(J_vec, E[0].shape) J = np.zeros_like(J_temp) for i in range(3): J[i][tuple(J_slices)] = J_temp[i][tuple(J_slices)] return J, fields['wavevector']
def build_plane_wave_source(eps_grid: Grid, omega: float, axis: int, slices=List[slice], mu: List[np.ndarray] = None, theta: float = 0, psi: float = 0, polarization_angle: float = 0, polarity: int = 1, border: int or List[int] = 0, power: float = 1): """Builds a plane wave source. By default, the plane wave propagates along polarity of the given axis and is linearly polarized along the x-direction if axis is z, y-direction if x and z direction if y. `theta` rotates the propagation direction around the E-field, then 'psi' rotates source plane normal, and the polarization_angle rotates around the propagation direction. Args: eps_grid: gridlock.grid with the permittivity distribution. omega: The frequency of the mode. axis: Direction of propagation. slices: Source slice which define the position of the source in the grid. mu: Permeability distribution. theta: Rotation around the default E-component. psi: Rotation around the source plane normal. polarization_angle: Rotation around the propagation direction. polarity: 1 if forward propagating. -1 if backward propagating. border: border in grid points where the intensity of the planewave decreased to 0. For example: [10, 10, 10] assuming radiation in the z direction will put 10 grid border on both sides in the x and y direction and ignore the z. (If the border is larger then the simulation it will be ignored aswell.) power: power transmitted through the slice. Returns: Current source J. """ # Perpare arguments. shape = eps_grid.shape # Make scalar fields. eps_val = np.real(np.average(eps_grid.grids[0][tuple(slices)])) scalar_field_function = lambda x, y, z, R: plane_wave_z_axis_x_pol( x, y, z, R, omega, polarity, eps_val) # Get vector fields. fields = scalar2rotated_vector_fields( eps_grid=eps_grid, scalar_field_function=scalar_field_function, mu=mu, omega=omega, axis=axis, slices=slices, theta=theta, psi=psi, polarization_angle=polarization_angle, polarity=polarity, power=power, full_fields=True) def make_mask(shape: List[int], slices: List, axis: int, border: List[int]) -> np.ndarray: """Builds a mask with a border. On the border the intensity goes from 1 to 0. Args: shape: Shape of the simulation. slices: List of slices where the plane wave is. axis: Direction of the plane wave. border: number of gridpoint that make up the border. Returns: mask """ size = [s.stop - s.start for s in slices] coords = [] for i in range(3): if (size[i] - 1) > 2 * border[i] and border[i] > 0: d = 1 / np.array(border[i]) x = np.hstack([ np.arange(1, 0, -d), np.zeros(size[i] - 2 * border[i]), np.arange(d, 1 + d, d) ]) else: x = np.zeros(size[i]) coords.append(x) coords[axis] = np.zeros(shape[axis]) ind = np.meshgrid(coords[0], coords[1], coords[2], indexing='ij') co = (ind[0]**2 + ind[1]**2 + ind[2]**2)**0.5 co[co > 1] = 1 mask_slice = 1 + 2 * co**3 - 3 * co**2 mask = np.zeros(shape) slices_mask = copy.deepcopy(slices) slices_mask[axis] = slice(None, None) mask[tuple(slices_mask)] = mask_slice return mask # Prepare border and the mask for be fields. if isinstance(border, int): border = 3 * [border] border[axis] = 0 # You can not have a border in the axis direction size = [s.stop - s.start for s in slices] for i, (b, s) in enumerate(zip(border, size)): if 2 * b < s: border[i] = b elif s == 1: border[i] = 0 else: raise ValueError("Two times the border is larger then the size" + " of the plane wave in axis {}.".format(i)) border = [b if 2 * b < s else 0 for b, s in zip(border, size)] mask = make_mask(shape, slices, axis, border) # Normalize fields dxes = [eps_grid.dxyz, eps_grid.autoshifted_dxyz()] slices_P = copy.deepcopy(slices) slices_P = [ slice(sl.start + b, sl.stop - b) for sl, b in zip(slices_P, border) ] P = waveguide_mode.compute_transmission_chew(E=fields['E'], H=fields['H'], axis=axis, omega=omega, dxes=dxes, slices=slices_P) fields['E'] /= np.sqrt(abs(P)) fields['H'] /= np.sqrt(abs(P)) fields['E'] = [mask * e for e in fields['E']] fields['H'] = [mask * h for h in fields['H']] # Calculate the source. dxes = [eps_grid.dxyz, eps_grid.autoshifted_dxyz()] # make current field_slices = [slice(None, None)] * 3 J_slices = slices if polarity == -1: ind = slices[axis].start field_slices[axis] = slice(None, ind) J_slices[axis] = slice(ind - 1, ind + 1) else: ind = slices[axis].stop - 1 field_slices[axis] = slice(ind, None) J_slices[axis] = slice(ind - 1, ind + 1) E = np.zeros_like(fields['E']) for i in range(3): E[i][tuple(field_slices)] = fields['E'][i][tuple(field_slices)] full = operators.e_full(omega, dxes, vec(eps_grid.grids), bloch_vec=fields['wavevector']) J_vec = 1 / (-1j * omega) * full @ vec(E) J_temp = unvec(J_vec, E[0].shape) J = np.zeros_like(J_temp) for i in range(3): J[i][tuple(J_slices)] = J_temp[i][tuple(J_slices)] return J, fields['wavevector']
def scalar2rotated_vector_fields(eps_grid: Grid, scalar_field_function: Callable, mu: List[np.ndarray], omega: float, axis: int, slices: List[slice], theta: float = 0, psi: float = 0, polarization_angle: float = 0, polarity: int = 1, power: float = 1, full_fields: bool = False): """ Given a function that evaluates the scalar field that propagates in the z-direction, this function will make the vector field taking into account three rotations. Args: eps_grid: gridlock.grid with the permittivity distribution. mu: Permeability distribution. omega: The frequency of the mode. axis: Direction of propagation. slices: Source slice which define the position of the source in the grid. theta: Rotation around the default E-component. psi: Rotation around the source plane normal. polarization_angle: Rotation around the propagation direction. polarity: 1 if forward propagating. -1 if backward propagating. power: power full_fields: True gives you the field in the entire simulation space. False gives only the fields in the slices, the rest of the simulation space will be zero. Returns: Results: dict with the wavevector, the e-field and the h-field. """ if mu is None: mu = [np.ones(eps_grid.shape)] * 3 dxes = [eps_grid.dxyz, eps_grid.autoshifted_dxyz()] epsilon = eps_grid.grids xyz_shift = [eps_grid.shifted_xyz(which_shifts=i) for i in range(3)] # Define rotation to set z as propagation direction. order = np.roll(range(3), 2 - axis) reverse_order = np.roll(range(3), axis - 2) #Make grid points xyz = xyz_shift[order[0]] x_Ex, y_Ex, z_Ex = np.meshgrid(xyz[order[0]], xyz[order[1]], xyz[order[2]], indexing='ij') xyz = xyz_shift[order[1]] x_Ey, y_Ey, z_Ey = np.meshgrid(xyz[order[0]], xyz[order[1]], xyz[order[2]], indexing='ij') xyz = xyz_shift[order[2]] x_Ez, y_Ez, z_Ez = np.meshgrid(xyz[order[0]], xyz[order[1]], xyz[order[2]], indexing='ij') #Make total rotation matrix k = np.array([0, 0, polarity]) e0 = np.array([1, 0, 0]) R_theta = rotation_matrix(e0, theta) R_psi = rotation_matrix(k, psi) k = R_psi @ R_theta @ k R_pol = rotation_matrix(k, polarization_angle) R = R_pol @ R_psi @ R_theta e0_rot = R @ e0 #Make wavevector ref_index = np.sqrt(np.real(np.average(epsilon[0][tuple(slices)]))) bloch_vector = (k * omega * ref_index) #Evaluate fields. ex_mag = scalar_field_function(x_Ex, y_Ex, z_Ex, np.linalg.inv(R)) * e0_rot[0] ey_mag = scalar_field_function(x_Ey, y_Ey, z_Ey, np.linalg.inv(R)) * e0_rot[1] ez_mag = scalar_field_function(x_Ez, y_Ez, z_Ez, np.linalg.inv(R)) * e0_rot[2] E_fields = [ex_mag, ey_mag, ez_mag] #Make H fields. dxes = [[dx[i] for i in order] for dx in dxes] e_vec = vec(E_fields) h_vec = operators.e2h(omega=omega, dxes=dxes, mu=vec([mu[i].transpose(order) for i in order]), bloch_vec=bloch_vector) @ e_vec H_fields = unvec(h_vec, E_fields[0].shape) #Normalize fields. #Roll back E = [None] * 3 H = [None] * 3 wavevector = np.zeros(3) for a, o in enumerate(reverse_order): E[a] = np.zeros_like(epsilon[0], dtype=complex) H[a] = np.zeros_like(epsilon[0], dtype=complex) if full_fields: E[a] = np.sqrt(power) * E_fields[o].transpose(reverse_order) H[a] = np.sqrt(power) * H_fields[o].transpose(reverse_order) else: E[a][tuple(slices)] = np.sqrt(power) * E_fields[o][tuple( [slices[i] for i in order])].transpose(reverse_order) H[a][tuple(slices)] = np.sqrt(power) * H_fields[o][tuple( [slices[i] for i in order])].transpose(reverse_order) wavevector[a] = bloch_vector[o] results = { 'wavevector': wavevector, 'H': H, 'E': E, } return results
def test_simspace_mesh_list(): """Checks parity between `GdsMeshEps` and `GdsEps`.""" # First create using `GdsEps`. mat_stack = optplan.GdsMaterialStack( background=optplan.Material(mat_name="Air"), stack=[ optplan.GdsMaterialStackLayer( foreground=optplan.Material(mat_name="SiO2"), background=optplan.Material(mat_name="SiO2"), gds_layer=[101, 0], extents=[-10000, -110], ), optplan.GdsMaterialStackLayer( foreground=optplan.Material(mat_name="Si"), background=optplan.Material(mat_name="Air"), gds_layer=[100, 0], extents=[-110, 110], ), ], ) simspace_spec = optplan.SimulationSpace( mesh=optplan.UniformMesh(dx=40), eps_fg=optplan.GdsEps(gds="WDM_example_fg.gds", mat_stack=mat_stack), eps_bg=optplan.GdsEps(gds="WDM_example_bg.gds", mat_stack=mat_stack), sim_region=optplan.Box3d(center=[0, 0, 0], extents=[5000, 5000, 500]), pml_thickness=[10, 10, 10, 10, 0, 0], ) space = simspace.SimulationSpace(simspace_spec, TESTDATA) space_inst = space(1550) eps_bg = space_inst.eps_bg.grids eps_fg = fdfd_tools.unvec( fdfd_tools.vec(space_inst.eps_bg.grids) + space_inst.selection_matrix @ np.ones(np.prod(space.design_dims)), space_inst.eps_bg.shape) # Validate that `GdsEps` behaves as expected. assert space_inst.selection_matrix.shape == (609375, 10000) np.testing.assert_array_equal(eps_bg[2][:, :, -3], 1) np.testing.assert_allclose(eps_bg[2][10, 10, 2], 2.0852) np.testing.assert_allclose(eps_fg[2][107, 47, 2], 2.0852) np.testing.assert_allclose(eps_fg[2][107, 47, 6], 12.086617) np.testing.assert_allclose(eps_fg[2][112, 57, 6], 1) np.testing.assert_allclose(eps_fg[2][107, 47, 10], 1) # Now create space using `GdsMeshEps`. mesh_list = [ optplan.SlabMesh( material=optplan.Material(mat_name="SiO2"), extents=[-10000, -110]), optplan.GdsMesh( material=optplan.Material(mat_name="Si"), extents=[-110, 110], gds_layer=[100, 0], ), ] simspace_spec = optplan.SimulationSpace( mesh=optplan.UniformMesh(dx=40), eps_fg=optplan.GdsMeshEps( gds="WDM_example_fg.gds", background=optplan.Material(mat_name="Air"), mesh_list=mesh_list), eps_bg=optplan.GdsMeshEps( gds="WDM_example_bg.gds", background=optplan.Material(mat_name="Air"), mesh_list=mesh_list), sim_region=optplan.Box3d(center=[0, 0, 0], extents=[5000, 5000, 500]), pml_thickness=[10, 10, 10, 10, 0, 0], ) space_mesh = simspace.SimulationSpace(simspace_spec, TESTDATA) space_inst_mesh = space(1550) eps_bg_mesh = space_inst_mesh.eps_bg.grids eps_fg_mesh = fdfd_tools.unvec( fdfd_tools.vec(space_inst_mesh.eps_bg.grids) + space_inst_mesh.selection_matrix @ np.ones( np.prod(space_mesh.design_dims)), space_inst_mesh.eps_bg.shape) # Verify that the two methods yield the same permittivities. np.testing.assert_allclose(eps_bg_mesh, eps_bg) np.testing.assert_allclose(eps_fg_mesh, eps_fg)
def get_electric_fields(self, param: Parametrization): return fdfd_tools.unvec(self.sim.simulate(param.get_structure()), self.sim.get_dims())
def _run_solver(self, z: np.ndarray, J: np.ndarray) -> np.ndarray: """ Runs the solver to compute electric fields. Here I use the notation M* = conj(M), M.' = transpose(M), M' = ctranspose(M), M N = dot(M, N) Maxwell uses a symmetrized wave operator M = (L A R) = (L A R).', where L = inv(R) = diag(sqrt(s)) [s as in the code below] when solving the wave equation; it thus solves the problem M y = d => (L A R) (inv(R) x) = (L b) => A x = b with x = R y From the fact that M is symmetric, we can write M* = (L A R)* = (L A R)' = R' A' L' = R* A' L* We obtain M* by conjugating the contents of sim (except sim.J). We then multiply sim.J by (R* R*) = diag(1./s)*, obtaining (R* A' L*) v = (L* (R* R* b)) = (R* b) and then fix the returned value v by multiplying by (L* L*) = diag(s) leading to the result (L* L* v) = (L* L* (R* x)) = L* x Putting it all together, with adjoint=true, we have (R* A' L*) (inv(L* L* R*) x) = (L* (R* R* b)) => (R* A' L*) (inv(L*) x) = (R* b) => A' x = b Args: z: The structure. Returns: Vectorized form of the electric fields. """ # TODO(logansu): Support PEC/PMC. dxes = [[np.conj(dx) for dx in grid] for grid in self.sim.dxes] spx, spy, spz = np.meshgrid(dxes[1][0], dxes[1][1], dxes[1][2], indexing='ij') sdx, sdy, sdz = np.meshgrid(dxes[0][0], dxes[0][1], dxes[0][2], indexing='ij') mult = np.multiply s = [ mult(mult(sdx, spy), spz), mult(mult(spx, sdy), spz), mult(mult(spx, spy), sdz) ] new_J = np.copy(fdfd_tools.unvec(J, self.sim.dims)) for k in range(3): new_J[k] /= np.conj(s[k]) new_J = fdfd_tools.vec(new_J) mu = None if self.sim.mu is not None: mu = np.conj(fdfd_tools.vec(self.sim.mu)) sim_args = { 'omega': np.conj(self.sim.omega), 'dxes': dxes, 'epsilon': np.conj(self.sim.get_epsilon(z)), 'mu': mu, 'J': new_J, 'pec': fdfd_tools.vec(self.sim.pec), 'pmc': fdfd_tools.vec(self.sim.pmc), 'bloch_vec': self.sim.bloch_vec, } efields = self.sim.solver.solve(**sim_args) # Unvec to undo right pre-conditioner. efields = fdfd_tools.unvec(efields, self.sim.dims) for i in range(3): efields[i] = np.multiply(efields[i], np.conj(s[i])) efields = fdfd_tools.vec(efields) return efields
def test_straight_waveguide_power_poynting(): """Tests that total through straight waveguide is unity.""" space = Simspace( TESTDATA, optplan.SimulationSpace( pml_thickness=[10, 10, 10, 10, 0, 0], mesh=optplan.UniformMesh(dx=40), sim_region=optplan.Box3d( center=[0, 0, 0], extents=[5000, 5000, 40], ), eps_bg=optplan.GdsEps( gds="straight_waveguide.gds", mat_stack=optplan.GdsMaterialStack( background=optplan.Material(mat_name="air"), stack=[ optplan.GdsMaterialStackLayer( gds_layer=[100, 0], extents=[-80, 80], foreground=optplan.Material(mat_name="Si"), background=optplan.Material(mat_name="air"), ), ], ), ), )) source = creator_em.WaveguideModeSource( optplan.WaveguideModeSource( power=1.0, extents=[40, 1500, 600], normal=[1.0, 0.0, 0.0], center=[-1770, 0, 0], mode_num=0, )) wlen = 1550 eps_grid = space(wlen).eps_bg.grids source_grid = source(space, wlen) eps = problem.Constant(fdfd_tools.vec(eps_grid)) sim = creator_em.FdfdSimulation( eps=eps, solver=local_matrix_solvers.DirectSolver(), wlen=wlen, source=fdfd_tools.vec(source_grid), simspace=space, ) power_fun = poynting.PowerTransmissionFunction( field=sim, simspace=space, wlen=wlen, plane_slice=grid_utils.create_region_slices( space.edge_coords, [1770, 0, 0], [40, 1500, 600]), axis=gridlock.axisvec2axis([1, 0, 0]), polarity=gridlock.axisvec2polarity([1, 0, 0])) # Same as `power_fun` but with opposite normal vector. Should give same # answer but with a negative sign. power_fun_back = poynting.PowerTransmissionFunction( field=sim, simspace=space, wlen=wlen, plane_slice=grid_utils.create_region_slices( space.edge_coords, [1770, 0, 0], [40, 1500, 600]), axis=gridlock.axisvec2axis([-1, 0, 0]), polarity=gridlock.axisvec2polarity([-1, 0, 0])) efield_grid = fdfd_tools.unvec( graph_executor.eval_fun(sim, None), eps_grid[0].shape) # Calculate emitted power. edotj = np.real( fdfd_tools.vec(efield_grid) * np.conj( fdfd_tools.vec(source_grid))) * 40**3 power = -0.5 * np.sum(edotj) np.testing.assert_almost_equal( graph_executor.eval_fun(power_fun, None), power, decimal=4) np.testing.assert_almost_equal( graph_executor.eval_fun(power_fun_back, None), -power, decimal=4)
def solve(self, omega: complex, dxes: List[List[np.ndarray]], J: np.ndarray, epsilon: np.ndarray, pml_layers: Optional[fdfd_tools.PmlLayers] = None, mu: np.ndarray = None, pec: np.ndarray = None, pmc: np.ndarray = None, pemc: np.ndarray = None, bloch_vec: np.ndarray = None, symmetry: np.ndarray = None, adjoint: bool = False, E0: np.ndarray = None, solver_info: bool = False, n_eig: int = 1): if symmetry is None: symmetry = np.zeros(3) if pemc is None: pemc = np.zeros(6) server_url = 'http://' + self.server + '/' dxes = fdfd_tools.grid.apply_scpml(dxes, pml_layers, omega) # Set initial condition to all zeros if not specified. if E0 is None: E0 = np.zeros_like(epsilon) # Set mu to 1 if not specified. if mu is None: mu = np.ones_like(epsilon) if bloch_vec is None: bloch_phase = np.ones([3, 3]) else: # TODO(Dries): check this phase calculation for non-uniform grid sim_length = np.array([np.real(np.sum(a)) for a in dxes[0]]) bloch_phase_uniform = np.exp(1j * (sim_length * bloch_vec)) bloch_phase = np.transpose([bloch_phase_uniform]) @ np.ones([1, 3]) # Set up using symmetry J_unvec = fdfd_tools.unvec(J, self.shape) eps_unvec = fdfd_tools.unvec(epsilon, self.shape) mu_unvec = fdfd_tools.unvec(mu, self.shape) E0_unvec = fdfd_tools.unvec(E0, self.shape) slices = [slice(0, sh) for sh in self.shape] if symmetry[0] == 1: slices[0] = slice(self.shape[0] // 2, self.shape[0]) pemc[:2] = 1 elif symmetry[0] == 2: slices[0] = slice(self.shape[0] // 2, self.shape[0]) pemc[:2] = 2 if symmetry[1] == 1: slices[1] = slice(self.shape[1] // 2, self.shape[1]) pemc[2:4] = 1 elif symmetry[1] == 2: slices[1] = slice(self.shape[1] // 2, self.shape[1]) pemc[2:4] = 2 if symmetry[2] == 1: slices[2] = slice(self.shape[2] // 2, self.shape[2]) pemc[4:6] = 1 elif symmetry[2] == 2: slices[2] = slice(self.shape[2] // 2, self.shape[2]) pemc[4:6] = 2 shape = [sl.stop - sl.start for sl in slices] J = fdfd_tools.vec([j[tuple(slices)] for j in J_unvec]) E0 = fdfd_tools.vec([e[tuple(slices)] for e in E0_unvec]) epsilon = fdfd_tools.vec([e[tuple(slices)] for e in eps_unvec]) mu = fdfd_tools.vec([m[tuple(slices)] for m in mu_unvec]) dxes = [[dxes[j][i][slices[i]] for i in range(3)] for j in range(2)] if adjoint: omega = np.conj(omega) mu = np.conj(mu) epsilon = np.conj(epsilon) new_dxes = [] for i in range(2): dx = [] for j in range(3): dx.append(np.conj(dxes[i][j])) new_dxes.append(dx) dxes = new_dxes spx, spy, spz = np.meshgrid(dxes[1][0], dxes[1][1], dxes[1][2], indexing='ij') sdx, sdy, sdz = np.meshgrid(dxes[0][0], dxes[0][1], dxes[0][2], indexing='ij') mult = np.multiply s = [ mult(mult(sdx, spy), spz), mult(mult(spx, sdy), spz), mult(mult(spx, spy), sdz) ] new_J = fdfd_tools.unvec(J, shape) for k in range(3): new_J[k] /= np.conj(s[k]) J = fdfd_tools.vec(new_J) # Generate ID based on date, time, and UUID. sim_id = time.strftime('%Y%m%d-%H%M%S-') + str(uuid.uuid1()) sim_name_prefix = 'maxwell-' + sim_id + '.' # Create a temporary directory. upload_dir = tempfile.mkdtemp() local_prefix = os.path.join(upload_dir, sim_name_prefix) make_array = lambda a: np.array([a]) # set solver solver = 0 # If there is a Bloch phase, we must use BiCGSTAB. if np.any(bloch_phase != np.ones([3, 3])): solver = 1 if self.solver == 'biCGSTAB': solver = 1 elif self.solver == 'lgmres': solver = 2 elif self.solver == 'Jacobi-Davidson': solver = 3 # Write the grid file. gridfile = local_prefix + 'grid' with h5py.File(gridfile, 'w') as f: f.create_dataset('omega_r', data=make_array(np.real(omega))) f.create_dataset('omega_i', data=make_array(np.imag(omega))) f.create_dataset('shape', data=shape) f.create_dataset('n_eig', data=make_array(n_eig)) f.create_dataset('max_iters', data=make_array(self.max_iters)) f.create_dataset('err_thresh', data=make_array(self.err_thresh)) f.create_dataset('bloch_phase', data=bloch_phase) f.create_dataset('pemc', data=pemc) f.create_dataset('solver', data=solver) xyz = ['x', 'y', 'z'] for direc in range(3): f.create_dataset('sd_' + xyz[direc] + 'r', data=np.real(dxes[0][direc]).astype( np.float64)) f.create_dataset('sd_' + xyz[direc] + 'i', data=np.imag(dxes[0][direc]).astype( np.float64)) f.create_dataset('sp_' + xyz[direc] + 'r', data=np.real(dxes[1][direc]).astype( np.float64)) f.create_dataset('sp_' + xyz[direc] + 'i', data=np.imag(dxes[1][direc]).astype( np.float64)) # Write the rest of the files. write_field(local_prefix + 'e', fdfd_tools.unvec(epsilon, shape)) write_field(local_prefix + 'J', fdfd_tools.unvec(J, shape)) write_field(local_prefix + 'm', fdfd_tools.unvec(mu, shape)) write_field(local_prefix + 'A', fdfd_tools.unvec(E0, shape)) # Upload files. upload_files(server_url, upload_dir, os.listdir(upload_dir)) # Upload empty request file. request_filename = os.path.join(upload_dir, sim_name_prefix + 'request') with open(request_filename, 'w') as f: f.write('All files uploaded at {0}.'.format( time.strftime('%Y-%m-%d-%H:%M:%S'))) upload_files(server_url, upload_dir, [sim_name_prefix + 'request']) # Delete temporary upload directory. shutil.rmtree(upload_dir) # Create temporary download directory. download_dir = tempfile.mkdtemp() # Wait for solution. def check_existence(filename): r = requests.get(server_url + sim_name_prefix + filename) return r.status_code == 200 while True: time.sleep(1) # Wait one second before retry. try: if check_existence('request'): # Request not yet processed. Wait longer. time.sleep(5) elif check_existence('finished'): break except requests.exceptions.ConnectionError: logger.exception( 'ConnectionError while waiting for results. Retrying...') # Random retry time to prevent DoS on any machine. # Choose somewhere between 5 and 15 seconds. time.sleep(5 + random.uniform(0, 10)) # Download the files. if self.solver == 'Jacobi-Davidson': filenames = [ sim_name_prefix + 'Q' + str(i) + '_' + comp + quad for comp in 'xyz' for quad in 'ri' for i in range(n_eig) ] filenames += [sim_name_prefix + 'q' + quad for quad in 'ri'] else: filenames = [ sim_name_prefix + 'E_' + comp + quad for comp in 'xyz' for quad in 'ri' ] download_files(server_url, download_dir, filenames) # define apply_symmetry def apply_symmetry(E, symmetry): dummy = np.expand_dims(np.zeros_like(E[1][0, :, :]), 0) if symmetry[0] == 1: E[0] = np.concatenate((np.flip(E[0], 0), E[0]), axis=0) E[1] = np.concatenate( (dummy, -np.flip(E[1][1:, :, :], 0), E[1]), axis=0) E[2] = np.concatenate( (dummy, -np.flip(E[2][1:, :, :], 0), E[2]), axis=0) elif symmetry[0] == 2: E[0] = np.concatenate((-np.flip(E[0], 0), E[0]), axis=0) E[1] = np.concatenate((dummy, np.flip(E[1][1:, :, :], 0), E[1]), axis=0) E[2] = np.concatenate((dummy, np.flip(E[2][1:, :, :], 0), E[2]), axis=0) dummy = np.expand_dims(np.zeros_like(E[1][:, 0, :]), 1) if symmetry[1] == 1: E[0] = np.concatenate( (dummy, -np.flip(E[0][:, 1:, :], 1), E[0]), axis=1) E[1] = np.concatenate((np.flip(E[1], 1), E[1]), axis=1) E[2] = np.concatenate( (dummy, -np.flip(E[2][:, 1:, :], 1), E[2]), axis=1) elif symmetry[1] == 2: E[0] = np.concatenate((dummy, np.flip(E[0][:, 1:, :], 1), E[0]), axis=1) E[1] = np.concatenate((-np.flip(E[1], 1), E[1]), axis=1) E[2] = np.concatenate((dummy, np.flip(E[2][:, 1:, :], 1), E[2]), axis=1) dummy = np.expand_dims(np.zeros_like(E[1][:, :, 0]), 2) if symmetry[2] == 1: E[0] = np.concatenate( (dummy, -np.flip(E[0][:, :, 1:], 2), E[0]), axis=2) E[1] = np.concatenate( (dummy, -np.flip(E[1][:, :, 1:], 2), E[1]), axis=2) E[2] = np.concatenate((np.flip(E[2], 2), E[2]), axis=2) elif symmetry[2] == 2: E[0] = np.concatenate((dummy, np.flip(E[0][:, :, 1:], 2), E[0]), axis=2) E[1] = np.concatenate((dummy, np.flip(E[1][:, :, 1:], 2), E[1]), axis=2) E[2] = np.concatenate((-np.flip(E[2], 2), E[2]), axis=2) return E # Load in electric field. if self.solver == 'Jacobi-Davidson': E = [] for i in range(n_eig): Q = [] for comp in 'xyz': file_prefix = os.path.join( download_dir, sim_name_prefix + 'Q' + str(i) + '_' + comp) field_comp = None with h5py.File(file_prefix + 'r') as f: field_comp = f['data'][:].astype(np.complex128) with h5py.File(file_prefix + 'i') as f: field_comp += 1j * f['data'][:].astype(np.complex128) Q.append(field_comp) E.append(apply_symmetry(Q, symmetry)) else: E = [] for comp in 'xyz': file_prefix = os.path.join(download_dir, sim_name_prefix + 'E_' + comp) field_comp = None with h5py.File(file_prefix + 'r') as f: field_comp = f['data'][:].astype(np.complex128) with h5py.File(file_prefix + 'i') as f: field_comp += 1j * f['data'][:].astype(np.complex128) E.append(field_comp) E = apply_symmetry(E, symmetry) # Undo right preconditioner for adjoint calculations. if adjoint: for i in range(3): E[i] = np.multiply(E[i], np.conj(s[i])) # Remove downloaded files. if solver_info: file_prefix = os.path.join(download_dir, sim_name_prefix) field_comp = None with h5py.File(file_prefix + 'time_info') as f: solve_time = f['data'][0] text_file = open(file_prefix + 'status', "r") lines = text_file.read().split('\n') error = [float(l) for l in lines[:-1]] shutil.rmtree(download_dir) return fdfd_tools.vec(E), solve_time, error elif self.solver == 'Jacobi-Davidson': file_prefix = os.path.join(download_dir, sim_name_prefix) q = None with h5py.File(file_prefix + 'qr') as f: q = f['data'][()].astype(np.complex128) with h5py.File(file_prefix + 'qi') as f: q += 1j * f['data'][()].astype(np.complex128) shutil.rmtree(download_dir) return [fdfd_tools.vec(Q) for Q in E], q**(0.5) else: shutil.rmtree(download_dir) return fdfd_tools.vec(E)
def solve_waveguide_mode_2d( mode_number: int, omega: complex, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, wavenumber_correction: bool = False) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. :param mode_number: Number of the mode, 0-indexed :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :param wavenumber_correction: Whether to correct the wavenumber to account for numerical dispersion (default True) :return: {'E': List[np.ndarray], 'H': List[np.ndarray], 'wavenumber': complex} """ ''' Solve for the largest-magnitude eigenvalue of the real operator by using power iteration. ''' # check if eps has a imaginary part if (np.imag(epsilon) != 0).any(): warnings.warn('Epsilon in 2D mode solver has an imaginary part') dxes_real = [[np.real(dx) for dx in dxi] for dxi in dxes] A_r = waveguide.operator(np.real(omega), dxes_real, np.real(epsilon), np.real(mu)) # Use power iteration for 20 steps to estimate the dominant eigenvector v = np.random.rand(A_r.shape[0]) for _ in range(20): v = A_r @ v v /= np.linalg.norm(v) lm_eigval = v @ A_r @ v ''' Shift by the absolute value of the largest eigenvalue, then find a few of the largest (shifted) eigenvalues. The shift ensures that we find the largest _positive_ eigenvalues, since any negative eigenvalues will be shifted to the range 0 >= neg_eigval + abs(lm_eigval) > abs(lm_eigval) ''' shifted_A_r = A_r + abs(lm_eigval) * sparse.eye(A_r.shape[0]) eigvals, eigvecs = spalg.eigs(shifted_A_r, which='LM', k=mode_number + 3, ncv=50) # Pick the eigenvalue we want from the few we found k = eigvals.argsort()[-(mode_number + 1)] v = eigvecs[:, k] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' A = waveguide.operator(omega, dxes, epsilon, mu) eigval = None for _ in range(40): eigval = v @ A @ v if np.linalg.norm(A @ v - eigval * v) < 1e-13: break w = spalg.spsolve(A - eigval * sparse.eye(A.shape[0]), v) v = w / np.linalg.norm(w) # Calculate the wave-vector (force the real part to be positive) wavenumber = np.sqrt(eigval) wavenumber *= np.sign(np.real(wavenumber)) e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu) ''' Perform correction on wavenumber to account for numerical dispersion. See Numerical Dispersion in Taflove's FDTD book. This correction term reduces the error in emitted power, but additional error is introduced into the E_err and H_err terms. This effect becomes more pronounced as beta increases. ''' if wavenumber_correction: # TODO(logansu): This was the original code but written out more # clearly. Clearly, `dx` is not always unity but this function # currently does not have access to the `dx` in the propagating # direction. dx = 1 wavenumber = np.sin(np.real( wavenumber * dx / 2)) / (dx / 2) + np.imag(wavenumber) shape = [d.size for d in dxes[0]] fields = { 'wavenumber': wavenumber, 'E': unvec(e, shape), 'H': unvec(h, shape), } return fields
def solve_waveguide_mode_3d( mode_number: int, omega_appx: float, wavenumber: float, dxes: dx_lists_t, axis: int, axis_rot: int, angle: float, polarity: int, slices: List[slice], eps: field_t, mu: field_t = None, expand_fields: bool = False, additional_mode_cal: int = 1) -> Dict[str, complex or np.ndarray]: """ Given a 3d region, attempts to solve for the eigenmode with the specified mode number. :param mode_number: Number of the mode, 0-indexed :param omega_appx: approximated angular frequency of the simulation :param wavenumber: wavenumber of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param axis: propagation axis, i.e. x, y or z :param angle: angle with the propagation axis :param polarity: propagation direction (1 or -1) :param slices: slices that describe the region for the mode solve :param eps: permittivity :param mu: magnetic permeability (default 1 everywhere) :param expand_field: whether the calculated fields are duplicated over the entire simulation according to the periodicity and angle (default=false) :additional_mode_cal: how many mode are calculated :return: {'wavenumber': List[np.ndarray], 'H': List[np.ndarray], 'E':List[np.array]}, omega """ # set propagation direction to the z-axis order = np.roll(range(3), 2 - axis) reverse_order = np.roll(range(3), axis - 2) source_slices = [slices[i] for i in order] if polarity == 1: source_slices[2] = slice(source_slices[2].stop - 1, source_slices[2].stop) else: source_slices[2] = slice(source_slices[2].start, source_slices[2].start + 1) eig_slices = [slices[i] for i in order] eps_sim = [eps[i].transpose(order) for i in order] dxes_sim = [[dx[i] for i in order] for dx in dxes] eps_eig = [eps[i][tuple(slices)].transpose(order) for i in order] if mu: mu_eig = [mu[i][tuple(slices)].transpose(order) for i in order] else: mu_eig = np.ones_like(eps_eig) dxes_eig = [[dx[i][slices[i]] for i in order] for dx in dxes] shp_eig = eps_eig[0].shape # calculate eigenmode shift_orthogonal = np.zeros((3, 3)) shift_orthogonal[2, int(not reverse_order[axis_rot])] = -np.round( shp_eig[2] * np.tan(angle)) bloch_vec = polarity * np.array( [np.sin(angle), np.sin(angle), np.cos(angle)]) * wavenumber bloch_vec[reverse_order[axis_rot]] = 0 mode_arg = { 'omega_appx': omega_appx, 'bloch_vec': bloch_vec, 'dxes': dxes_eig, 'epsilon': eps_eig, 'op_type': 'hfield', 'set_init_cond': False, 'num_modes': mode_number + 1 + additional_mode_cal, 'shift_orthogonal': shift_orthogonal } eig_list, mode_list = phc.mode_solver(**mode_arg) index_sort = np.argsort(np.real(eig_list)) h_vec = mode_list[index_sort[mode_number]] e_vec = operators.h2e(omega=eig_list[index_sort[mode_number]], dxes=dxes_eig, eps=vec(eps_eig), bloch_vec=bloch_vec, shift_orthogonal=shift_orthogonal) @ h_vec h_unvec = unvec(h_vec, shp_eig) e_unvec = unvec(e_vec, shp_eig) # expand fields axis_angle = int(not reverse_order[axis_rot]) h_fields = np.zeros_like(eps_sim) e_fields = np.zeros_like(eps_sim) i_start = slices[axis].start i = i_start while i < e_fields[0].shape[2]: sl = [source_slices[0], source_slices[1], slice(i, i + 1)] for a in range(3): e_fields[a][sl] = operators.append_bloch_shift( A=e_unvec[a], axis=2, ind_axis=i - i_start, dx=dxes_eig[0], bloch_vector=bloch_vec, shift_orthogonal=shift_orthogonal[2]) h_fields[a][sl] = operators.append_bloch_shift( A=h_unvec[a], axis=2, ind_axis=i - i_start, dx=dxes_eig[1], bloch_vector=bloch_vec, shift_orthogonal=shift_orthogonal[2]) i += 1 i = slices[axis].start - 1 while i >= 0: sl = [source_slices[0], source_slices[1], slice(i, i + 1)] for a in range(3): e_fields[a][sl] = operators.append_bloch_shift( A=e_unvec[a], axis=2, ind_axis=i - i_start, dx=dxes_eig[0], bloch_vector=bloch_vec, shift_orthogonal=shift_orthogonal[2]) h_fields[a][sl] = operators.append_bloch_shift( A=h_unvec[a], axis=2, ind_axis=i - i_start, dx=dxes_eig[1], bloch_vector=bloch_vec, shift_orthogonal=shift_orthogonal[2]) i -= 1 omega = eig_list[index_sort[mode_number]] # normalize field by the transmission through the slice transmission = compute_transmission_chew( E=e_fields, H=h_fields, axis=2, omega=omega, dxes=dxes_sim, slices=source_slices, ) e = e_fields / np.sqrt(transmission) h = h_fields / np.sqrt(transmission) # select slice if not expand_fields: h_fields = np.zeros_like(eps_sim) h_fields[[slice(0, 3)] + source_slices] = h[[slice(0, 3)] + source_slices] e_fields = np.zeros_like(eps_sim) e_fields[[slice(0, 3)] + source_slices] = e[[slice(0, 3)] + source_slices] else: e_fields = e h_fields = h # reverse the order e_fields = [e_fields[i].transpose(reverse_order) for i in reverse_order] h_fields = [h_fields[i].transpose(reverse_order) for i in reverse_order] return {'wavenumber': bloch_vec[2], 'H': h_fields, 'E': e_fields}, omega
def compute_overlap_e( E: field_t, H: field_t, wavenumber: complex, omega: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], mu: field_t = None, ) -> field_t: """ Given an eigenmode obtained by solve_waveguide_mode, calculates overlap_e for the mode orthogonality relation Integrate(((E x H_mode) + (E_mode x H)) dot dn) [assumes reflection symmetry]. overlap_e makes use of the e2h operator to collapse the above expression into (vec(E) @ vec(overlap_e)), allowing for simple calculation of the mode overlap. :param E: E-field of the mode :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use as the waveguide cross-section. slices[axis] should select only one :param mu: Magnetic permeability (default 1 everywhere) :return: overlap_e for calculating the mode overlap """ cross_plane = [slice(None)] * 3 cross_plane[axis] = slices[axis] # Determine phase factors for parallel slices a_shape = np.roll([-1, 1, 1], axis) a_E = np.real(dxes[0][axis]).cumsum() a_H = np.real(dxes[1][axis]).cumsum() iphi = -polarity * 1j * wavenumber phase_E = np.exp(iphi * (a_E - a_E[slices[axis]])).reshape(a_shape) phase_H = np.exp(iphi * (a_H - a_H[slices[axis]])).reshape(a_shape) # Expand our slice to the entire grid using the calculated phase factors Ee = [None] * 3 He = [None] * 3 for k in range(3): Ee[k] = phase_E * E[k][tuple(cross_plane)] He[k] = phase_H * H[k][tuple(cross_plane)] # Write out the operator product for the mode orthogonality integral domain = np.zeros_like(E[0], dtype=int) domain[tuple(slices)] = 1 npts = E[0].size dn = np.zeros(npts * 3, dtype=int) dn[0:npts] = 1 dn = np.roll(dn, npts * axis) e2h = operators.e2h(omega, dxes, mu) ds = sparse.diags(vec([domain] * 3)) h_cross_ = operators.poynting_h_cross(vec(He), dxes) e_cross_ = operators.poynting_e_cross(vec(Ee), dxes) overlap_e = dn @ ds @ (-h_cross_ + e_cross_ @ e2h) # Normalize norm_factor = np.abs(overlap_e @ vec(Ee)) overlap_e /= norm_factor return unvec(overlap_e, E[0].shape)
def get_electric_fields(self, param: Parametrization): # TODO(logansu): Refactor please return fdfd_tools.unvec(self.sim.simulate(param.get_structure()), self.sim.get_dims())