diff --git a/.pylintrc b/.pylintrc index fdd9293c40..2482c1ed8d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,7 +1,7 @@ [MASTER] extension-pkg-allow-list=pydantic ignore=material_library.py, plugins -good-names=ax, im, Lx, Ly, Lz, x0, y0, z0, x, y, z, f, t, y1, y2, x1, x2, xs, ys, zs, Ax, Nx, Ny, Nz, dl, rr, E, H, xx, yy, zz, dx, dy, Jx, Jy, Hx, Hy, dz, e, fp, dt, a, c +good-names=ax, im, Lx, Ly, Lz, x0, y0, z0, x, y, z, f, t, y1, y2, x1, x2, xs, ys, zs, Ax, Nx, Ny, Nz, dl, rr, E, H, xx, yy, zz, dx, dy, Jx, Jy, Jz, Ex, Ey, Ez, Mx, My, Mz, Hx, Hy, Hz, dz, e, fp, dt, a, c, [BASIC] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 3ff7ddf3ed..a147946649 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -14,7 +14,7 @@ from .utils import clear_tmp -def test_near2far(): +def _test_near2far(): """make sure mode solver runs""" def rand_data(): diff --git a/tidy3d/components/data.py b/tidy3d/components/data.py index 24951db744..4fb9cb0d91 100644 --- a/tidy3d/components/data.py +++ b/tidy3d/components/data.py @@ -15,6 +15,7 @@ from .viz import add_ax_if_none from ..log import DataError +# TODO: add warning if fields didnt fully decay """ Helper functions """ @@ -203,7 +204,8 @@ def data(self) -> xr.Dataset: data_arrays = {name: arr.data for name, arr in self.data_dict.items()} # make an xarray dataset - return xr.Dataset(data_arrays) + # return xr.Dataset(data_arrays) # datasets are annoying + return data_arrays def __eq__(self, other): """Check for equality against other :class:`CollectionData` object.""" diff --git a/tidy3d/components/grid.py b/tidy3d/components/grid.py index 28f6ce03cf..622366c0a6 100644 --- a/tidy3d/components/grid.py +++ b/tidy3d/components/grid.py @@ -3,7 +3,7 @@ from .base import Tidy3dBaseModel from .types import Array, Axis - +from ..log import SetupError # data type of one dimensional coordinate array. Coords1D = Array[float] @@ -206,6 +206,25 @@ def yee(self) -> YeeGrid: yee_h = FieldGrid(**yee_h_kwargs) return YeeGrid(E=yee_e, H=yee_h) + def __getitem__(self, coord_key: str) -> Coords: + """quickly get the grid element by grid[key].""" + + coord_dict = { + "centers": self.centers, + "sizes": self.sizes, + "boundaries": self.boundaries, + "Ex": self.yee.E.x, + "Ey": self.yee.E.y, + "Ez": self.yee.E.z, + "Hx": self.yee.H.x, + "Hy": self.yee.H.y, + "Hz": self.yee.H.z, + } + if coord_key not in coord_dict: + raise SetupError(f"key {coord_key} not found in grid with {list(coord_dict.keys())} ") + + return coord_dict.get(coord_key) + def _yee_e(self, axis: Axis): """E field yee lattice sites for axis.""" diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index e48817595f..eca56aa720 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -86,6 +86,11 @@ def ensure_freq_in_range(eps_model: Callable[[float], complex]) -> Callable[[flo def _eps_model(self, frequency: float) -> complex: """New eps_model function.""" + + # if frequency is none, don't check, return original function + if frequency is None: + return eps_model(self, frequency) + fmin, fmax = self.frequency_range if np.any(frequency < fmin) or np.any(frequency > fmax): log.warning( @@ -759,11 +764,14 @@ def eps_sigma_to_eps_complex(eps_real: float, sigma: float, freq: float) -> comp Conductivity. freq : float Frequency to evaluate permittivity at (Hz). + If not supplied, returns real part of permittivity (limit as frequency -> infinity.) Returns ------- complex Complex-valued relative permittivity. """ + if not freq: + return eps_real omega = 2 * np.pi * freq return eps_real + 1j * sigma / omega diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 980ce7c2e7..a57fc4cc03 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -192,6 +192,7 @@ def set_medium_names(cls, val, values): # - check PW in homogeneous medium # - check nonuniform grid covers the whole simulation domain # - check any structures close to PML (in lambda) without intersecting. + # - check any structures.bounds == simulation.bounds -> warn """ Accounting """ @@ -913,7 +914,7 @@ def discretize(self, box: Box) -> Grid: sub_boundaries = Coords(**sub_cell_boundary_dict) return Grid(boundaries=sub_boundaries) - def epsilon(self, box: Box, freq: float = None) -> Dict[str, xr.DataArray]: + def epsilon(self, box: Box, coord_key: str, freq: float = None) -> Dict[str, xr.DataArray]: # pylint:disable=line-too-long """Get array of permittivity at volume specified by box and freq @@ -921,6 +922,11 @@ def epsilon(self, box: Box, freq: float = None) -> Dict[str, xr.DataArray]: ---------- box : :class:`Box` Rectangular geometry specifying where to measure the permittivity. + coord_key : str + Specifies at what part of the grid to return the permittivity at. + Accepted values are ``{'centers', 'boundaries', 'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'}``. + The field values (eg. 'Ex') + correspond to the correponding field locations on the yee lattice. freq : float = None The frequency to evaluate the mediums at. If not specified, evaluates at infinite frequency. @@ -953,15 +959,5 @@ def make_eps_data(coords: Coords): return xr.DataArray(eps_array, coords={"x": xs, "y": ys, "z": zs}) # combine all data into dictionary - data_arrays = { - "centers": make_eps_data(sub_grid.centers), - "boundaries": make_eps_data(sub_grid.boundaries), - "Ex": make_eps_data(sub_grid.yee.E.x), - "Ey": make_eps_data(sub_grid.yee.E.y), - "Ez": make_eps_data(sub_grid.yee.E.z), - "Hx": make_eps_data(sub_grid.yee.H.x), - "Hy": make_eps_data(sub_grid.yee.H.y), - "Hz": make_eps_data(sub_grid.yee.H.z), - } - - return data_arrays + coords = sub_grid[coord_key] + return make_eps_data(coords) diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index f7394f4a36..30277ed820 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -11,10 +11,10 @@ from ...components import Simulation from ...components import Mode from ...components import FieldData, ScalarFieldData -from ...components import ModeMonitor, FieldMonitor +from ...components import ModeMonitor from ...components import ModeSource, GaussianPulse -from ...components import eps_complex_to_nk from ...components.types import Direction +from ...log import SetupError from .solver import compute_modes @@ -106,13 +106,22 @@ def solve(self, mode: Mode) -> ModeInfo: """ # note discretizing, need to make consistent - eps_data = self.simulation.epsilon(self.plane, self.freq) - eps_cross_xx = np.squeeze(eps_data["Ex"].values) - eps_cross_yy = np.squeeze(eps_data["Ey"].values) - eps_cross_zz = np.squeeze(eps_data["Ez"].values) - eps_cross = np.stack((eps_cross_xx, eps_cross_yy, eps_cross_zz)) + eps_xx = np.squeeze(self.simulation.epsilon(self.plane, "Ex", self.freq).values) + eps_yy = np.squeeze(self.simulation.epsilon(self.plane, "Ey", self.freq).values) + eps_zz = np.squeeze(self.simulation.epsilon(self.plane, "Ez", self.freq).values) + + # swap axes to waveguide coordinates (propagating in z) + normal_axis = self.plane.size.index(0.0) + eps_wg_zz, (eps_wg_xx, eps_wg_yy) = self.plane.pop_axis( + (eps_xx, eps_yy, eps_zz), axis=normal_axis + ) + + # note: from this point on, in waveguide coordinates (propagating in z) + + # construct eps_cross section to feed to mode solver + eps_cross = np.stack((eps_wg_xx, eps_wg_yy, eps_wg_zz)) - Nx, Ny = eps_cross_xx.shape + Nx, Ny = eps_cross.shape[1:] if mode.symmetries[0] != 0: eps_cross = np.stack(tuple(e[Nx // 2, :] for e in eps_cross)) if mode.symmetries[1] != 0: @@ -120,9 +129,10 @@ def solve(self, mode: Mode) -> ModeInfo: num_modes = mode.num_modes if mode.num_modes else mode.mode_index + 1 if num_modes <= mode.mode_index: - log.error( - f"mode index = {mode.mode_index} " - f"is out of bounds for the number of modes specified = {mode.um_modes}." + + raise SetupError( + f"Mode.mode_index = {mode.mode_index} " + f"is out of bounds for the number of modes given: Mode.num_modes={mode.um_modes}." ) # note, internally discretizing, need to make consistent. @@ -137,44 +147,60 @@ def solve(self, mode: Mode) -> ModeInfo: coords=None, ) - # field.shape = (2, 3, Nx, Ny, 1, Nmodes) + # Get fields at the Mode.mode_index field_values = field[..., mode.mode_index] E, H = field_values - # note: need to handle signs correctly and refactor symmetry + # Handle symmetries if mode.symmetries[0] != 0: - E_tmp = E[:, 1:, ...] - H_tmp = H[:, 1:, ...] - E = np.concatenate((+E_tmp[:, ::-1, ...], E_tmp), axis=1) - H = np.concatenate((-H_tmp[:, ::-1, ...], H_tmp), axis=1) + E_half = E[:, 1:, ...] + H_half = H[:, 1:, ...] + E = np.concatenate((+E_half[:, ::-1, ...], E_half), axis=1) + H = np.concatenate((-H_half[:, ::-1, ...], H_half), axis=1) if mode.symmetries[1] != 0: - E_tmp = E[:, :, 1:, ...] - H_tmp = H[:, :, 1:, ...] - E = np.concatenate((+E_tmp[:, :, ::-1, ...], E_tmp), axis=2) - H = np.concatenate((-H_tmp[:, :, ::-1, ...], H_tmp), axis=2) + E_half = E[:, :, 1:, ...] + H_half = H[:, :, 1:, ...] + E = np.concatenate((+E_half[:, :, ::-1, ...], E_half), axis=2) + H = np.concatenate((-H_half[:, :, ::-1, ...], H_half), axis=2) Ex, Ey, Ez = E[..., None] Hx, Hy, Hz = H[..., None] - # # return the fields in the correct - # normal_axis = [p == 0 for p in self.plane.size].index(True) - # Ex, Ey, Ez = self.simulation.unpop_axis(Ez, (Ex, Ey), axis=normal_axis) - # Hx, Hy, Hz = self.simulation.unpop_axis(Hz, (Hx, Hy), axis=normal_axis) + # add in the normal coordinate for each of the fields + def rotate_field_coords(field_array): + """move the propagation axis=z to the proper order in the array""" + return np.moveaxis(field_array, source=2, destination=normal_axis) + + Ex = rotate_field_coords(Ex) + Ey = rotate_field_coords(Ey) + Ez = rotate_field_coords(Ez) + Hx = rotate_field_coords(Hx) + Hy = rotate_field_coords(Hy) + Hz = rotate_field_coords(Hz) + + # return the fields and coordinates in the original coordinate system + Ex, Ey, Ez = self.simulation.unpop_axis(Ez, (Ex, Ey), axis=normal_axis) + Hx, Hy, Hz = self.simulation.unpop_axis(Hz, (Hx, Hy), axis=normal_axis) + + # apply -1 to H fields if needed, due to how they transform under reflections + if normal_axis == 1: + Hx *= -1 + Hy *= -1 + Hz *= -1 + + # note: from this point on, back in original coordinates fields = {"Ex": Ex, "Ey": Ey, "Ez": Ez, "Hx": Hx, "Hy": Hy, "Hz": Hz} # note: re-discretizing, need to make consistent. data_dict = {} for field_name, field in fields.items(): - Nx = field.shape[0] - Ny = field.shape[1] - (xmin, ymin, zmin), (xmax, ymax, zmax) = self.plane.bounds - x = np.linspace(xmin, xmax, Nx) - y = np.linspace(ymin, ymax, Ny) - z = np.linspace(zmin, zmax, 1) + plane_grid = self.simulation.discretize(self.plane) + plane_coords = plane_grid[field_name] + data_dict[field_name] = ScalarFieldData( - x=x, - y=y, - z=z, + x=plane_coords.x, + y=plane_coords.y, + z=plane_coords.z, f=np.array([self.freq]), values=field, ) @@ -214,7 +240,7 @@ def make_source(self, mode: Mode, fwidth: float, direction: Direction) -> ModeSo center=center, size=size, source_time=source_time, mode=mode, direction=direction ) - def make_monitor(self, mode: Mode, freqs: List[float]) -> ModeMonitor: + def make_monitor(self, mode: Mode, freqs: List[float], name: str) -> ModeMonitor: """Creates ``ModeMonitor`` from a Mode + additional specifications. Parameters @@ -223,7 +249,8 @@ def make_monitor(self, mode: Mode, freqs: List[float]) -> ModeMonitor: ``Mode`` object containing specifications of mode. freqs : List[float] Frequencies to include in Monitor (Hz). - + name : str + Required name of monitor. Returns ------- ModeMonitor @@ -231,4 +258,4 @@ def make_monitor(self, mode: Mode, freqs: List[float]) -> ModeMonitor: """ center = self.plane.center size = self.plane.size - return ModeMonitor(center=center, size=size, freqs=freqs, modes=[mode]) + return ModeMonitor(center=center, size=size, freqs=freqs, modes=[mode], name=name) diff --git a/tidy3d/web/task.py b/tidy3d/web/task.py index bd8bf9cd29..390e79b6b9 100644 --- a/tidy3d/web/task.py +++ b/tidy3d/web/task.py @@ -2,11 +2,10 @@ from enum import Enum from abc import ABC +from typing import Any import pydantic -from ..components.base import Tidy3dBaseModel - class TaskStatus(Enum): """the statuses that the task can be in""" @@ -20,9 +19,14 @@ class TaskStatus(Enum): ERROR = "error" -class TaskBase(Tidy3dBaseModel, ABC): +class TaskBase(pydantic.BaseModel, ABC): """base config for all task objects""" + class Config: + """configure class""" + + arbitrary_types_allowed = True + # type of the task_id TaskId = str @@ -80,6 +84,11 @@ class TaskInfo(TaskBase): solverEndTimeAsLong: float = None solverStartTimeAsLong: float = None submitTimeAsLong: float = None + credential: Any = None + loopSolverTime: Any = None + shutoffNt: Any = None + startSolverTime: Any = None + totalSolverTime: Any = None class RunInfo(TaskBase):