def __call__(self, simspace: SimulationSpace, wlen: float, **kwargs) -> fdfd_tools.VecField: """Creates the source vector. Args: simspace: Simulation space object to use. wlen: Wavelength to operate source. Returns: The vector field corresponding to the source. """ space_inst = simspace(wlen) return fdfd_solvers.waveguide_mode.build_waveguide_source( omega=2 * np.pi / wlen, dxes=simspace.dxes, eps=space_inst.eps_bg.grids, mu=None, mode_num=self._params.mode_num, waveguide_slice=grid_utils.create_region_slices( simspace.edge_coords, self._params.center, self._params.extents), axis=gridlock.axisvec2axis(self._params.normal), polarity=gridlock.axisvec2polarity(self._params.normal), power=self._params.power)
def __init__(self, function: problem.OptimizationFunction, sim_space: simspace.SimulationSpace, slice_point: Optional[List[float]] = None, slice_normal: Optional[List[int]] = None) -> None: """Initializes the monitor. If `slice_point` and `slice_normal` are set, then a 2D slice is taken over the 3D vector field. Args: function: Field function to monitor. sim slice_point: Point in the field slice to monitor. slice_normal: Normal of the slice that is evaluated. """ super().__init__(function) self._function = function self._slices = None self._sim_space = sim_space if slice_point and slice_normal: slice_axis = gridlock.axisvec2axis(slice_normal) grid = gridlock.Grid(sim_space.edge_coords, num_grids=3) slice_ind = grid.pos2ind(slice_point, which_shifts=None).astype(int) slice_ind = slice_ind[slice_axis] self._slices = 3 * [slice(0, None)] self._slices[slice_axis] = slice(slice_ind, slice_ind + 1)
def __call__( self, simspace: SimulationSpace, wlen: float, solver, **kwargs ) -> Union[fdfd_tools.VecField, Tuple[fdfd_tools.VecField, fdfd_tools.Vec3d]]: """Creates the plane wave source. Args: simspace: Simulation space to use for the source. wlen: Wavelength of source. Returns: If `overwrite_bloch_vector` is `True`, a tuple containing the source field and the Bloch vector corresponding to the plane wave source. Otherwise, only the source field is returned. """ space_inst = simspace(wlen) # Calculate the border in gridpoints and igore the border if it's larger then the simulation. dx = simspace.dx border = [int(b // dx) for b in self._params.border] # The plane wave is assumed to be in the z direction so the border is 0 for z. border.append(0) source, kvector = fdfd_tools.free_space_sources.build_plane_wave_source( omega=2 * np.pi / wlen, eps_grid=space_inst.eps_bg, mu=None, axis=gridlock.axisvec2axis(self._params.normal), polarity=gridlock.axisvec2polarity(self._params.normal), slices=grid_utils.create_region_slices(simspace.edge_coords, self._params.center, self._params.extents), theta=self._params.theta, psi=self._params.psi, polarization_angle=self._params.polarization_angle, border=border, power=self._params.power) if self._params.normalize_by_sim: source = fdfd_tools.free_space_sources.normalize_source_by_sim( omega=2 * np.pi / wlen, source=source, eps=space_inst.eps_bg.grids, dxes=simspace.dxes, pml_layers=simspace.pml_layers, solver=solver, power=self._params.power) if self._params.overwrite_bloch_vector: # TODO(logansu): Figure out what's wrong with mixing PMLs and # Bloch vector. It seems to create problems. # For now, we manually set Bloch to zero is PML is nonzero. if simspace.pml_layers[0] != 0 or simspace.pml_layers[1] != 0: kvector[0] = 0 if simspace.pml_layers[2] != 0 or simspace.pml_layers[3] != 0: kvector[1] = 0 if simspace.pml_layers[4] != 0 or simspace.pml_layers[5] != 0: kvector[2] = 0 return source, kvector return source
def before_sim(self, sim: FdfdSimProp) -> None: beam_center = self._params.beam_center if beam_center is None: beam_center = self._params.center eps_grid = copy.deepcopy(sim.grid) eps_grid.grids = sim.eps source, _ = fdfd_tools.free_space_sources.build_gaussian_source( omega=2 * np.pi / sim.wlen, eps_grid=eps_grid, mu=None, axis=gridlock.axisvec2axis(self._params.normal), polarity=gridlock.axisvec2polarity(self._params.normal), slices=simspace.create_region_slices(sim.grid.exyz, self._params.center, self._params.extents), theta=self._params.theta, psi=self._params.psi, polarization_angle=self._params.polarization_angle, w0=self._params.w0, center=beam_center, power=self._params.power) if self._params.normalize_by_sim: source = fdfd_tools.free_space_sources.normalize_source_by_sim( omega=2 * np.pi / sim.wlen, source=source, eps=sim.eps, dxes=sim.dxes, pml_layers=sim.pml_layers, solver=sim.solver, power=self._params.power) sim.source += source
def __call__(self, simspace: SimulationSpace, wlen: float, **kwargs) -> fdfd_tools.VecField: # wlen should hopefully be defined when we define the problem space_inst = simspace(wlen) # Need to update the path here too return fdfd_solvers.annulus_mode.build_overlap_annulus( omega=2 * np.pi / wlen, dxes=simspace.dxes, eps=space_inst.eps_bg.grids, mu=None, mode_num=self._params.mode_num, axis=gridlock.axisvec2axis(self._params.normal), power=self._params.power)
def create_power_transmission_function( params: PowerTransmission, context: workspace.Workspace) -> PowerTransmissionFunction: simspace = context.get_object(params.field.simulation_space) return PowerTransmissionFunction( field=context.get_object(params.field), simspace=simspace, wlen=params.field.wavelength, plane_slice=grid_utils.create_region_slices(simspace.edge_coords, params.center, params.extents), axis=gridlock.axisvec2axis(params.normal), polarity=gridlock.axisvec2polarity(params.normal))
def __call__(self, simspace: SimulationSpace, wlen: float, **kwargs) -> fdfd_tools.VecField: space_inst = simspace(wlen) return fdfd_solvers.waveguide_mode.build_overlap( omega=2 * np.pi / wlen, dxes=simspace.dxes, eps=space_inst.eps_bg.grids, mu=None, mode_num=self._params.mode_num, waveguide_slice=grid_utils.create_region_slices( simspace.edge_coords, self._params.center, self._params.extents), axis=gridlock.axisvec2axis(self._params.normal), polarity=gridlock.axisvec2polarity(self._params.normal), power=self._params.power)
def before_sim(self, sim: FdfdSimProp) -> None: if self._wg_mode is None: self._wg_mode = fdfd_solvers.waveguide_mode.build_waveguide_source( omega=2 * np.pi / sim.wlen, dxes=sim.dxes, eps=sim.eps, mu=None, mode_num=self._src.mode_num, waveguide_slice=simspace.create_region_slices( sim.grid.exyz, self._src.center, self._src.extents), axis=gridlock.axisvec2axis(self._src.normal), polarity=gridlock.axisvec2polarity(self._src.normal), power=self._src.power) sim.source += self._wg_mode
def before_sim(self, sim: FdfdSimProp) -> None: # Calculate the eigenmode if we have not already. if self._wg_overlap is None: self._wg_overlap = fdfd_solvers.waveguide_mode.build_overlap( omega=2 * np.pi / sim.wlen, dxes=sim.dxes, eps=sim.eps, mu=None, mode_num=self._overlap.mode_num, waveguide_slice=simspace.create_region_slices( sim.grid.exyz, self._overlap.center, self._overlap.extents), axis=gridlock.axisvec2axis(self._overlap.normal), polarity=gridlock.axisvec2polarity(self._overlap.normal), power=self._overlap.power) self._wg_overlap = np.stack(self._wg_overlap, axis=0)
def test_axisvec2axis_and_axisvec2polarity(self): # x-dir vec = np.array([1, 0, 0]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 0) self.assertTrue(pol == 1) vec = np.array([2, 1e-8, 1e-8]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 0) self.assertTrue(pol == 1) vec = np.array([-1, 0, 0]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 0) self.assertTrue(pol == -1) # y-dir vec = np.array([0, 1, 0]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 1) self.assertTrue(pol == 1) vec = np.array([0, -1, 0]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 1) self.assertTrue(pol == -1) # z-dir vec = np.array([0, 0, 1]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 2) self.assertTrue(pol == 1) vec = np.array([0, 0, -1]) ax = axisvec2axis(vec) pol = axisvec2polarity(vec) self.assertTrue(ax == 2) self.assertTrue(pol == -1)
def before_sim(self, sim: mp.Simulation) -> None: # Set extents to zero in the direction of the normal as MEEP # can automatically deduce the normal this way. extents = np.array(self._overlap.extents) * 1.0 extents[gridlock.axisvec2axis(self._overlap.normal)] = 0 # Add a flux region to monitor the power. self._mon = sim.add_mode_monitor( 1 / self._overlap.wavelength, 0, 1, mp.FluxRegion(center=self._overlap.center, size=extents)) # Calculate the eigenmode if we have not already. if not self._wg_mode: self._wg_mode = _get_waveguide_mode(sim, self._overlap) norms = _get_overlap_integral(self._wg_mode.mode_fields, self._wg_mode.mode_fields, self._wg_mode.xyzw) self._mode_norm = np.sqrt(0.5 * np.abs(norms[0] + norms[1]))
def test_plane_power_grad(): space = Simspace( TESTDATA, optplan.SimulationSpace( pml_thickness=[0, 0, 0, 0, 0, 0], mesh=optplan.UniformMesh(dx=40), sim_region=optplan.Box3d( center=[0, 0, 0], extents=[80, 80, 80], ), 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"), ), ], ), ), )) wlen = 1550 power_fun = poynting.PowerTransmissionFunction( field=problem.Variable(1), simspace=space, wlen=wlen, plane_slice=grid_utils.create_region_slices(space.edge_coords, [0, 0, 0], [40, 80, 80]), axis=gridlock.axisvec2axis([1, 0, 0]), polarity=gridlock.axisvec2polarity([1, 0, 0])) field = np.arange(np.prod(space.dims) * 3).astype(np.complex128) * 1j grad_actual = power_fun.grad([field], 1) fun = lambda vec: power_fun.eval([vec]) grad_brute = eval_grad_brute_wirt(field, fun) np.testing.assert_array_almost_equal(grad_actual[0], grad_brute, decimal=4)
def __call__( self, simspace: SimulationSpace, wlen: float, **kwargs ) -> Union[fdfd_tools.VecField, Tuple[fdfd_tools.VecField, fdfd_tools.Vec3d]]: """Creates the plane wave source. Args: simspace: Simulation space to use for the source. wlen: Wavelength of source. Returns: If `overwrite_bloch_vector` is `True`, a tuple containing the source field and the Bloch vector corresponding to the plane wave source. Otherwise, only the source field is returned. """ space_inst = simspace(wlen) # Calculate the border in gridpoints and igore the border if it's larger then the simulation. dx = simspace.dx border = [int(b // dx) for b in self._params.border] # The plane wave is assumed to be in the z direction so the border is 0 for z. border.append(0) source, kvector = fdfd_tools.free_space_sources.build_plane_wave_source( omega=2 * np.pi / wlen, eps_grid=space_inst.eps_bg, mu=None, axis=gridlock.axisvec2axis(self._params.normal), polarity=gridlock.axisvec2polarity(self._params.normal), slices=grid_utils.create_region_slices(simspace.edge_coords, self._params.center, self._params.extents), theta=self._params.theta, psi=self._params.psi, polarization_angle=self._params.polarization_angle, border=border, power=self._params.power) if self._params.overwrite_bloch_vector: return source, kvector return source
def __call__(self, simspace: SimulationSpace, wlen: float, solver: Callable, **kwargs) -> fdfd_tools.VecField: """Creates the source vector. Args: simspace: Simulation space. wlen: Wavelength of source. solver: If `normalize_by_source` is `True`, `solver` will be used to run an EM simulation to normalize the source power. Returns: The source. """ space_inst = simspace(wlen) source, _ = fdfd_tools.free_space_sources.build_gaussian_source( omega=2 * np.pi / wlen, eps_grid=space_inst.eps_bg, mu=None, axis=gridlock.axisvec2axis(self._params.normal), polarity=gridlock.axisvec2polarity(self._params.normal), slices=grid_utils.create_region_slices(simspace.edge_coords, self._params.center, self._params.extents), theta=self._params.theta, psi=self._params.psi, polarization_angle=self._params.polarization_angle, w0=self._params.w0, center=self._params.beam_center, power=self._params.power) if self._params.normalize_by_sim: source = fdfd_tools.free_space_sources.normalize_source_by_sim( omega=2 * np.pi / wlen, source=source, eps=space_inst.eps_bg.grids, dxes=simspace.dxes, pml_layers=simspace.pml_layers, solver=solver, power=self._params.power) return source
def test_stored_energy_grad(): space = Simspace( TESTDATA, optplan.SimulationSpace( pml_thickness=[0, 0, 0, 0, 0, 0], mesh=optplan.UniformMesh(dx=40), sim_region=optplan.Box3d( center=[0, 0, 0], extents=[80, 80, 80], ), 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"), ), ], ), ), )) wlen = 1550 energy_fun = stored_energy.StoredEnergyFunction( input_function=problem.Variable(1), simspace=space, center=[0,0,0], extents=[0,0,0], epsilon=space._eps_bg) plane_slice=grid_utils.create_region_slices(space.edge_coords, [0, 0, 0], [40, 80, 80]), axis=gridlock.axisvec2axis([1, 0, 0]), polarity=gridlock.axisvec2polarity([1, 0, 0]))
def test_axisvec2axis_no_primary_coordinate_raises_value_error(vec): with pytest.raises(ValueError, match="no valid primary coordinate axis"): axisvec2axis(vec)
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 _get_waveguide_mode(sim: mp.Simulation, src: WaveguideModeSource) -> MeepWaveguideMode: """Computes the waveguide mode in Meep (via MPB). Args: sim: Meep simulation object. src: Waveguide mode source properties. Returns: A tuple `(mode_fields, mode_comps, xyzw)`. `mode_fields` is the sampled transverse fields of the mode. `mode_fields` is an array with four elements, one for each transverse element as determined by `mode_comps`. `xyzw` is the Meep array metadata for the region: It is a 4-element tuple `(x, y, z, w)` where the `x`, `y`, and `z` are arrays indicating the cell coordinates where the fields are sampled and `w` is the volume weight factor used when calculating integrals involving the fields (see Meep documentation for details). """ wlen = src.wavelength fcen = 1 / wlen normal_axis = gridlock.axisvec2axis(src.normal) # Extract the eigenmode from Meep. # `xyzw` is a tuple `(x_coords, y_coords, z_coords, weights)` where # `x_coords`, `y_coords`, and `z_coords` is a list of the coordinates of # the Yee cells within the source region. # TODO(logansu): Remove this hack when Meep fixes its bugs with # `get_array_metadata`. For now, we ensure that the slices are 3D, otherwise # the values returned for `w` can be wrong and undeterministic. dx = 1 / sim.resolution # Count number of dimensions are effectively "zero". This is used to # determine the dimensionality of the source (1D or 2D). overlap_dims = 3 - sum(val <= dx for val in src.extents) extents = [val if val >= dx else dx for val in src.extents] xyzw = sim.get_array_metadata(center=src.center, size=extents) # TODO(logansu): Understand how to guess a k-vector. Is it necessary? k_guess = [0, 0, 0] k_guess[normal_axis] = fcen k_guess = mp.Vector3(*k_guess) mode_dirs = [mp.X, mp.Y, mp.Z] mode_data = sim.get_eigenmode( fcen, mode_dirs[normal_axis], mp.Volume(center=src.center, size=src.extents), src.mode_num + 1, k_guess) # Determine which field components are relevant for TFSF source (i.e. the # field components tangential to the propagation direction. # For simplicity, we order them in circular permutation order (x->y->z) with # E fields followed by H fields. if normal_axis == 0: field_comps = [mp.Ey, mp.Ez, mp.Hy, mp.Hz] elif normal_axis == 1: field_comps = [mp.Ez, mp.Ex, mp.Hz, mp.Hx] else: field_comps = [mp.Ex, mp.Ey, mp.Hx, mp.Hy] # Extract the actual field values for the relevant field components. # Note that passing in `mode_data.amplitude` into `amp_func` parameter of # `mp.Source` seems to give a bad source. mode_fields = [] for c in field_comps: field_slice = [] for x, y, z in itertools.product(xyzw[0], xyzw[1], xyzw[2]): field_slice.append(mode_data.amplitude(mp.Vector3(x, y, z), c)) field_slice = np.reshape(field_slice, (len(xyzw[0]), len(xyzw[1]), len(xyzw[2]))) mode_fields.append(field_slice) # Sometimes Meep returns an extra layer of fields when one of the dimensions # of the overlap region is zero. In that case, only use the first slice. field_slicer = [slice(None), slice(None), slice(None)] field_slicer[normal_axis] = slice(0, 1) field_slicer = tuple(field_slicer) for i in range(4): mode_fields[i] = mode_fields[i][field_slicer] xyzw[3] = np.reshape(xyzw[3], (len(xyzw[0]), len(xyzw[1]), len(xyzw[2]))) xyzw[3] = xyzw[3][field_slicer] # TODO(logansu): See above TODO about hacking `get_array_metadata`. # For now, just guess the correct value. The error introduced by this # occurs at the edges of the source/overlap where the fields are small # anyway. xyzw[3][:] = dx**overlap_dims # Fix the phase of the mode by normalizing the phase by the phase where # the electric field as largest magnitude. arr = np.hstack([mode_fields[0].flatten(), mode_fields[1].flatten()]) phase_norm = np.angle(arr[np.argmax(np.abs(arr))]) phase_norm = np.exp(1j * phase_norm) mode_fields = [f / phase_norm for f in mode_fields] return MeepWaveguideMode(wlen, field_comps, mode_fields, xyzw)