"""Defines cells for the EME simulation."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Literal, Optional, Union
import numpy as np
import pydantic.v1 as pd
from tidy3d.components.base import Tidy3dBaseModel, skip_if_fields_missing
from tidy3d.components.geometry.base import Box
from tidy3d.components.grid.grid import Coords1D
from tidy3d.components.mode_spec import ModeSpec
from tidy3d.components.structure import Structure
from tidy3d.components.types import ArrayFloat1D, Axis, Coordinate, Size, TrackFreq
from tidy3d.constants import RADIAN, fp_eps, inf
from tidy3d.exceptions import SetupError, ValidationError
# grid limits
MAX_NUM_MODES = 100
MAX_NUM_EME_CELLS = 100
MAX_NUM_REPS = 100000
[docs]
class EMEModeSpec(ModeSpec):
"""Mode spec for EME cells. Overrides some of the defaults and allowed values."""
track_freq: Union[TrackFreq, None] = pd.Field(
None,
title="Mode Tracking Frequency",
description="Parameter that turns on/off mode tracking based on their similarity. "
"Can take values ``'lowest'``, ``'central'``, or ``'highest'``, which correspond to "
"mode tracking based on the lowest, central, or highest frequency. "
"If ``None`` no mode tracking is performed, which is the default for best performance.",
)
angle_theta: Literal[0.0] = pd.Field(
0.0,
title="Polar Angle",
description="Polar angle of the propagation axis from the injection axis. Not currently "
"supported in EME cells. Use an additional 'ModeSolverMonitor' and "
"'sim_data.smatrix_in_basis' to achieve off-normal injection in EME.",
units=RADIAN,
)
angle_phi: Literal[0.0] = pd.Field(
0.0,
title="Azimuth Angle",
description="Azimuth angle of the propagation axis in the plane orthogonal to the "
"injection axis. Not currently supported in EME cells. Use an additional "
"'ModeSolverMonitor' and 'sim_data.smatrix_in_basis' to achieve off-normal "
"injection in EME.",
units=RADIAN,
)
precision: Literal["auto", "single", "double"] = pd.Field(
"auto",
title="single, double, or automatic precision in mode solver",
description="The solver will be faster and using less memory under "
"single precision, but more accurate under double precision. "
"Choose ``'auto'`` to apply double precision if the simulation contains a good "
"conductor, single precision otherwise.",
)
# this method is not supported because not all ModeSpec features are supported
# @classmethod
# def _from_mode_spec(cls, mode_spec: ModeSpec) -> EMEModeSpec:
# """Convert to ordinary :class:`.ModeSpec`."""
# return cls(
# num_modes=mode_spec.num_modes,
# target_neff=mode_spec.target_neff,
# num_pml=mode_spec.num_pml,
# filter_pol=mode_spec.filter_pol,
# angle_theta=mode_spec.angle_theta,
# angle_phi=mode_spec.angle_phi,
# precision=mode_spec.precision,
# bend_radius=mode_spec.bend_radius,
# bend_axis=mode_spec.bend_axis,
# track_freq=mode_spec.track_freq,
# group_index_step=mode_spec.group_index_step,
# )
def _to_mode_spec(self) -> ModeSpec:
"""Convert to ordinary :class:`.ModeSpec`."""
ms_dict = self.dict()
ms_dict.pop("type")
return ModeSpec.parse_obj(ms_dict)
class EMEGridSpec(Tidy3dBaseModel, ABC):
"""Specification for an EME grid.
An EME grid is a 1D grid aligned with the propagation axis,
dividing the simulation into cells. Modes and mode coefficients
are defined at the central plane of each cell. Typically,
cell boundaries are aligned with interfaces between structures
in the simulation.
"""
num_reps: pd.PositiveInt = pd.Field(
1,
title="Number of Repetitions",
description="Number of periodic repetitions of this EME grid. Useful for "
"efficiently simulating long periodic structures like Bragg gratings. "
"Instead of explicitly repeating the cells, setting 'num_reps' allows "
"the EME solver to reuse the modes and cell interface scattering matrices.",
)
name: Optional[str] = pd.Field(
None, title="Name", description="Name of this 'EMEGridSpec'. Used in 'EMEPeriodicitySweep'."
)
@pd.validator("num_reps", always=True)
def _validate_num_reps(cls, val):
"""Check num_reps is not too large."""
if val > MAX_NUM_REPS:
raise SetupError(
f"'EMEGridSpec' has 'num_reps={val:.2e}'; "
f"the largest value allowed is '{MAX_NUM_REPS}'."
)
return val
@abstractmethod
def make_grid(self, center: Coordinate, size: Size, axis: Axis) -> EMEGrid:
"""Generate EME grid from the EME grid spec.
Parameters
----------
center: :class:`.Coordinate`
Center of the EME simulation.
size: :class:`.Size`
Size of the EME simulation.
axis: :class:`.Axis`
Propagation axis for the EME simulation.
Returns
-------
:class:`.EMEGrid`
An EME grid dividing the EME simulation into cells, as defined
by the EME grid spec.
"""
@property
def real_cell_indices(self) -> int:
"""The cell indices inside this EME grid, starting at 0,
not including periodic repetition of cells."""
return np.arange(self.num_real_cells)
@property
@abstractmethod
def num_real_cells(self) -> int:
"""Number of real cells in this EME grid spec."""
@property
def virtual_cell_indices(self) -> int:
"""The cell indices inside this EME grid, starting at 0
and including periodic repetition of cells with ``num_reps``."""
return list(self.real_cell_indices) * self.num_reps
@property
def num_virtual_cells(self) -> int:
"""Number of virtual cells in this EME grid spec."""
return len(self.virtual_cell_indices)
def _updated_copy_num_reps(self, num_reps: dict[str, pd.PositiveInt]) -> EMEGridSpec:
"""Update ``num_reps`` of named subgrids."""
if self.name is not None:
new_num_reps = num_reps.get(self.name)
if new_num_reps is not None:
return self.updated_copy(num_reps=new_num_reps)
return self
@property
def _cell_index_pairs(self) -> list[pd.NonNegativeInt]:
"""Pairs of adjacent cell indices."""
cell_indices = self.virtual_cell_indices
pairs = []
for i in range(len(cell_indices) - 1):
if (cell_indices[i], cell_indices[i + 1]) not in pairs:
pairs.append((cell_indices[i], cell_indices[i + 1]))
return pairs
[docs]
class EMEExplicitGrid(EMEGridSpec):
"""EME grid with explicitly defined internal boundaries.
Example
-------
>>> from tidy3d import EMEExplicitGrid, EMEModeSpec
>>> mode_spec1 = EMEModeSpec(num_modes=10)
>>> mode_spec2 = EMEModeSpec(num_modes=20)
>>> eme_grid = EMEExplicitGrid(
... mode_specs=[mode_spec1, mode_spec2],
... boundaries=[1],
... )
"""
mode_specs: list[EMEModeSpec] = pd.Field(
...,
title="Mode Specifications",
description="Mode specifications for each cell in the explicit EME grid.",
)
boundaries: ArrayFloat1D = pd.Field(
...,
title="Boundaries",
description="List of coordinates of internal cell boundaries along the propagation axis. "
"Must contain one fewer item than 'mode_specs', and must be strictly increasing. "
"Each cell spans the region between an adjacent pair of boundaries. "
"The first (last) cell spans the region between the first (last) boundary "
"and the simulation boundary.",
)
@pd.validator("boundaries", always=True)
@skip_if_fields_missing(["mode_specs"])
def _validate_boundaries(cls, val, values):
"""Check that boundaries is increasing and contains one fewer element than mode_specs."""
mode_specs = values["mode_specs"]
boundaries = val
if len(mode_specs) - 1 != len(boundaries):
raise ValidationError(
"There must be exactly one fewer item in 'boundaries' than in 'mode_specs'."
)
if len(boundaries) > 0:
rmin = boundaries[0]
for rmax in boundaries[1:]:
if rmax < rmin:
raise ValidationError("The 'boundaries' must be increasing.")
rmin = rmax
return val
[docs]
def make_grid(self, center: Coordinate, size: Size, axis: Axis) -> EMEGrid:
"""Generate EME grid from the EME grid spec.
Parameters
----------
center: :class:`.Coordinate`
Center of the EME simulation.
size: :class:`.Size`
Size of the EME simulation.
axis: :class:`.Axis`
Propagation axis for the EME simulation.
Returns
-------
:class:`.EMEGrid`
An EME grid dividing the EME simulation into cells, as defined
by the EME grid spec.
"""
sim_rmin = center[axis] - size[axis] / 2
sim_rmax = center[axis] + size[axis] / 2
if len(self.boundaries) > 0:
if sim_rmin - self.boundaries[0] > fp_eps:
raise ValidationError(
"The first item in 'boundaries' is outside the simulation domain."
)
if self.boundaries[-1] - sim_rmax > fp_eps:
raise ValidationError(
"The last item in 'boundaries' is outside the simulation domain."
)
boundaries = [sim_rmin, *list(self.boundaries), sim_rmax]
return EMEGrid(
boundaries=boundaries,
center=center,
size=size,
axis=axis,
mode_specs=self.mode_specs,
)
[docs]
@classmethod
def from_structures(
cls, structures: list[Structure], axis: Axis, mode_spec: EMEModeSpec, **kwargs
) -> EMEExplicitGrid:
"""Create an explicit EME grid with boundaries aligned with
structure bounding boxes. Every cell in the resulting grid
has the same mode specification.
Parameters
----------
structures : List[:class:`.Structure`]
A list of structures to define the :class:`.EMEExplicitGrid`.
The EME grid boundaries will be placed at the lower and upper bounds
of the bounding boxes of all the structures in the list.
axis : :class:`.Axis`
Propagation axis for the EME simulation.
mode_spec : :class:`.EMEModeSpec`
Mode specification for the EME grid. The same mode specification will
be used in every cell in the resulting :class:`.EMEExplicitGrid`.
**kwargs
Other arguments passed to the new :class:`.EMEExplicitGrid` instance.
Returns
-------
:class:`.EMEExplicitGrid`
Explicit EME grid with boundaries aligned with the structure bounding boxes.
Example
-------
>>> from tidy3d import EMEModeSpec, Structure, Box, Medium
>>> mode_spec = EMEModeSpec(num_modes=1)
>>> box = Structure(
... geometry=Box(center=(0, 0, 0), size=(1, 2, 3)),
... medium=Medium(permittivity=5),
... )
>>> box2 = Structure(
... geometry=Box(center=(0, 0, 4), size=(1, 2, 3)),
... medium=Medium(permittivity=5),
... )
>>> eme_grid_spec = EMEExplicitGrid.from_structures(
... structures=[box, box2],
... axis=2,
... mode_spec=mode_spec
... )
"""
rmins = [structure.geometry.bounds[0][axis] for structure in structures]
rmaxs = [structure.geometry.bounds[1][axis] for structure in structures]
boundaries = np.sort(np.unique(rmins + rmaxs))
if len(boundaries) > 1:
# first and last bounds are not needed
boundaries = boundaries[1:-1]
mode_specs = [mode_spec] * (len(boundaries) + 1)
return EMEExplicitGrid(boundaries=boundaries, mode_specs=mode_specs, **kwargs)
@property
def num_real_cells(self) -> int:
"""Number of real cells in this EME grid spec."""
return len(self.mode_specs)
EMESubgridType = Union[EMEUniformGrid, EMEExplicitGrid, "EMECompositeGrid"]
[docs]
class EMECompositeGrid(EMEGridSpec):
"""EME grid made out of multiple subgrids.
Example
-------
>>> from tidy3d import EMEUniformGrid, EMEModeSpec
>>> mode_spec1 = EMEModeSpec(num_modes=10)
>>> mode_spec2 = EMEModeSpec(num_modes=20)
>>> subgrid1 = EMEUniformGrid(num_cells=5, mode_spec=mode_spec1)
>>> subgrid2 = EMEUniformGrid(num_cells=10, mode_spec=mode_spec2)
>>> eme_grid = EMECompositeGrid(
... subgrids=[subgrid1, subgrid2],
... subgrid_boundaries=[1]
... )
"""
subgrids: list[EMESubgridType] = pd.Field(
..., title="Subgrids", description="Subgrids in the composite grid."
)
subgrid_boundaries: ArrayFloat1D = pd.Field(
...,
title="Subgrid Boundaries",
description="List of coordinates of internal subgrid boundaries along the propagation axis. "
"Must contain one fewer item than 'subgrids', and must be strictly increasing. "
"Each subgrid spans the region between an adjacent pair of subgrid boundaries. "
"The first (last) subgrid spans the region between the first (last) subgrid boundary "
"and the simulation boundary.",
)
@pd.validator("subgrid_boundaries", always=True)
def _validate_subgrid_boundaries(cls, val, values):
"""Check that subgrid boundaries is increasing and contains one fewer element than subgrids."""
subgrids = values["subgrids"]
subgrid_boundaries = val
if len(subgrids) - 1 != len(subgrid_boundaries):
raise ValidationError(
"There must be exactly one fewer item in 'subgrid_boundaries' than in 'subgrids'."
)
rmin = subgrid_boundaries[0]
for rmax in subgrid_boundaries[1:]:
if rmax < rmin:
raise ValidationError("The 'subgrid_boundaries' must be increasing.")
rmin = rmax
return val
[docs]
def subgrid_bounds(
self, center: Coordinate, size: Size, axis: Axis
) -> list[tuple[float, float]]:
"""Subgrid bounds: a list of pairs (rmin, rmax) of the
bounds of the subgrids along the propagation axis.
Parameters
----------
center: :class:`.Coordinate`
Center of the EME simulation.
size: :class:`.Size`
Size of the EME simulation.
axis: :class:`.Axis`
Propagation axis for the EME simulation.
Returns
-------
List[Tuple[float, float]]
A list of pairs (rmin, rmax) of the bounds of the subgrids
along the propagation axis.
"""
bounds = []
sim_rmin = center[axis] - size[axis] / 2
sim_rmax = center[axis] + size[axis] / 2
if sim_rmin - self.subgrid_boundaries[0] > fp_eps:
raise ValidationError(
"The first item in 'subgrid_boundaries' is outside the simulation domain."
)
if self.subgrid_boundaries[-1] - sim_rmax > fp_eps:
raise ValidationError(
"The last item in 'subgrid_boundaries' is outside the simulation domain."
)
rmin = sim_rmin
for rmax in self.subgrid_boundaries:
bounds.append((rmin, rmax))
rmin = rmax
rmax = sim_rmax
bounds.append((rmin, rmax))
return bounds
[docs]
def make_grid(self, center: Coordinate, size: Size, axis: Axis) -> EMEGrid:
"""Generate EME grid from the EME grid spec.
Parameters
----------
center: :class:`.Coordinate`
Center of the EME simulation.
size: :class:`.Size`
Size of the EME simulation.
axis: :class:`.Axis`
Propagation axis for the EME simulation.
Returns
-------
:class:`.EMEGrid`
An EME grid dividing the EME simulation into cells, as defined
by the EME grid spec.
"""
boundaries = []
mode_specs = []
subgrid_center = list(center)
subgrid_size = list(size)
subgrid_bounds = self.subgrid_bounds(center, size, axis)
for subgrid_spec, bounds in zip(self.subgrids, subgrid_bounds):
subgrid_center[axis] = (bounds[0] + bounds[1]) / 2
subgrid_size[axis] = bounds[1] - bounds[0]
subgrid = subgrid_spec.make_grid(center=subgrid_center, size=subgrid_size, axis=axis)
boundaries += list(subgrid.boundaries[:-1])
mode_specs += list(subgrid.mode_specs)
boundaries.append(subgrid_bounds[-1][1])
return EMEGrid(
boundaries=boundaries,
center=center,
size=size,
axis=axis,
mode_specs=mode_specs,
)
@property
def num_real_cells(self) -> int:
"""Number of real cells in this EME grid spec."""
return np.sum([subgrid.num_real_cells for subgrid in self.subgrids])
@property
def virtual_cell_indices(self) -> int:
"""The cell indices inside this EME grid, starting at 0
and including periodic repetition of cells with ``num_reps``."""
inds = []
for subgrid in self.subgrids:
start_ind = 0 if len(inds) == 0 else inds[-1] + 1
inds += [ind + start_ind for ind in subgrid.virtual_cell_indices]
return list(inds) * self.num_reps
def _updated_copy_num_reps(self, num_reps: dict[str, pd.PositiveInt]) -> EMEGridSpec:
"""Update ``num_reps`` of named subgrids."""
new_self = super()._updated_copy_num_reps(num_reps=num_reps)
new_subgrids = [
subgrid._updated_copy_num_reps(num_reps=num_reps) for subgrid in self.subgrids
]
return new_self.updated_copy(subgrids=new_subgrids)
[docs]
@classmethod
def from_structure_groups(
cls,
structure_groups: list[list[Structure]],
axis: Axis,
mode_specs: list[EMEModeSpec],
names: Optional[list[str]] = None,
num_reps: Optional[list[pd.PositiveInt]] = None,
) -> EMECompositeGrid:
"""Create a composite EME grid with boundaries aligned with
structure bounding boxes.
Parameters
----------
structure_groups : List[List[:class:`.Structure`]]
A list of structure groups to define the :class:`.EMECompositeGrid`.
Each structure group will be used to generate an :class:`.EMEExplicitGrid`
with boundaries aligned with the bounding boxes of the structures
in that group. These will then be assembled as subgrids of an
:class:`.EMECompositeGrid`. Empty structure groups give rise to grids
containing a single cell. The boundary between adjacent subgrids
is determined from the structure groups; thus they must be consistent,
meaning that either the upper bound of one structure group must equal
the lower bound of the next, or one of the structure groups must be empty.
Two adjacent structure groups cannot be empty.
axis : :class:`.Axis`
Propagation axis for the EME simulation.
mode_specs : List[:class:`.EMEModeSpec`]
Mode specifications for each subgrid. Must be the same length as
``structure_groups``.
names : List[str] = None
Names for each subgrid. Must be the same length as ``structure_groups``.
If ``None``, the subgrids do not recieve names.
num_reps : List[pd.PositiveInt] = None
Number of repetitions for each subgrid. Must be the same length as
``structure_groups``. If ``None``, the subgrids are not repeated.
Returns
-------
:class:`.EMECompositeGrid`
Composite EME grid with subgrids defined by the structure groups.
Example
-------
>>> from tidy3d import EMEModeSpec, Structure, Box, Medium
>>> mode_spec = EMEModeSpec(num_modes=1)
>>> box = Structure(
... geometry=Box(center=(0, 0, 0), size=(1, 2, 3)),
... medium=Medium(permittivity=5),
... )
>>> box2 = Structure(
... geometry=Box(center=(0, 0, 3), size=(1, 2, 3)),
... medium=Medium(permittivity=5),
... )
>>> eme_grid_spec = EMECompositeGrid.from_structure_groups(
... structure_groups=[[box], [box2]],
... axis=2,
... mode_specs=[mode_spec]*2,
... names=["subgrid1", None],
... num_reps=[2, 1]
... )
"""
if len(structure_groups) == 0:
raise ValidationError("The list 'structure_groups' cannot be empty.")
if len(mode_specs) != len(structure_groups):
raise ValidationError(
"The lists 'mode_specs' and 'structure_groups' must have the same length."
)
subgrids = []
for structures, mode_spec in zip(structure_groups, mode_specs):
subgrids.append(
EMEExplicitGrid.from_structures(
structures=structures, axis=axis, mode_spec=mode_spec
)
)
if names is not None:
if len(names) != len(structure_groups):
raise ValidationError(
"The lists 'names' and 'structure_groups' must have the same length."
)
for i in range(len(subgrids)):
subgrids[i] = subgrids[i].updated_copy(name=names[i])
if num_reps is not None:
if len(num_reps) != len(structure_groups):
raise ValidationError(
"The lists 'num_reps' and 'structure_groups' must have the same length."
)
for i in range(len(subgrids)):
subgrids[i] = subgrids[i].updated_copy(num_reps=num_reps[i])
# now try to determine subgrid_boundaries
# they need to be consistently determined by adjacent structure groups
subgrid_boundaries = [None] * (len(subgrids) - 1)
subgrid_rmins = [None] * len(subgrids)
subgrid_rmaxs = [None] * len(subgrids)
for i, structures in enumerate(structure_groups):
rmins = [structure.geometry.bounds[0][axis] for structure in structures]
rmaxs = [structure.geometry.bounds[1][axis] for structure in structures]
boundaries = np.sort(np.unique(rmins + rmaxs))
if len(boundaries) > 1:
subgrid_rmins[i] = boundaries[0]
subgrid_rmaxs[i] = boundaries[-1]
for i in range(len(subgrid_boundaries)):
rmax = subgrid_rmaxs[i]
rmin = subgrid_rmins[i + 1]
if rmax is not None:
if rmin is not None and rmax != rmin:
raise ValidationError(
f"The upper bound of 'structure_groups[{i}]', "
f"'{rmax}', does not equal the lower bound of "
f"'structure_groups[{i + 1}]', '{rmin}'."
)
subgrid_boundaries[i] = rmax
elif rmin is not None:
subgrid_boundaries[i] = rmin
else:
raise ValidationError(
"Not enough structures provided at "
f"'structure_groups[{i}]' and "
f"'structure_groups[{i + 1}]' to determine "
"'subgrid_boundaries'."
)
return EMECompositeGrid(subgrids=subgrids, subgrid_boundaries=subgrid_boundaries)
[docs]
class EMEGrid(Box):
"""EME grid.
An EME grid is a 1D grid aligned with the propagation axis,
dividing the simulation into cells. Modes and mode coefficients
are defined at the central plane of each cell. Typically,
cell boundaries are aligned with interfaces between structures
in the simulation.
"""
axis: Axis = pd.Field(
..., title="Propagation axis", description="Propagation axis for the EME simulation."
)
mode_specs: list[EMEModeSpec] = pd.Field(
..., title="Mode Specifications", description="Mode specifications for the EME cells."
)
boundaries: Coords1D = pd.Field(
..., title="Cell boundaries", description="Boundary coordinates of the EME cells."
)
@pd.validator("mode_specs", always=True)
def _validate_size(cls, val):
"""Check grid size and num modes."""
num_eme_cells = len(val)
if num_eme_cells > MAX_NUM_EME_CELLS:
raise SetupError(
f"Simulation has {num_eme_cells:.2e} EME cells, "
f"a maximum of {MAX_NUM_EME_CELLS:.2e} are allowed."
)
num_modes = np.max([mode_spec.num_modes for mode_spec in val])
if num_modes > MAX_NUM_MODES:
raise SetupError(
f"Simulation has {num_modes:.2e} EME modes, "
f"a maximum of {MAX_NUM_MODES:.2e} are allowed."
)
return val
@pd.validator("boundaries", always=True, pre=False)
@skip_if_fields_missing(["mode_specs", "axis", "center", "size"])
def _validate_boundaries(cls, val, values):
"""Check that boundaries is increasing, in simulation domain, and contains
one more element than 'mode_specs'."""
mode_specs = values["mode_specs"]
boundaries = val
axis = values["axis"]
center = values["center"][axis]
size = values["size"][axis]
sim_rmin = center - size / 2
sim_rmax = center + size / 2
if len(mode_specs) + 1 != len(boundaries):
raise ValidationError(
"There must be exactly one more item in 'boundaries' than in 'mode_specs', "
"so that there is one mode spec per EME cell."
)
rmin = boundaries[0]
if sim_rmin - rmin > fp_eps:
raise ValidationError(
"The first item in 'boundaries' is outside the simulation domain."
)
for rmax in boundaries[1:]:
if rmax < rmin:
raise ValidationError("The 'subgrid_boundaries' must be increasing.")
rmin = rmax
if rmax - sim_rmax > fp_eps:
raise ValidationError("The last item in 'boundaries' is outside the simulation domain.")
return val
@property
def centers(self) -> Coords1D:
"""Centers of the EME cells along the propagation axis."""
rmin = self.boundaries[0]
centers = []
for rmax in self.boundaries[1:]:
center = (rmax + rmin) / 2
centers.append(center)
rmin = rmax
return centers
@property
def lengths(self) -> list[pd.NonNegativeFloat]:
"""Lengths of the EME cells along the propagation axis."""
rmin = self.boundaries[0]
lengths = []
for rmax in self.boundaries[1:]:
length = rmax - rmin
lengths.append(length)
rmin = rmax
return lengths
@property
def num_cells(self) -> pd.NonNegativeInteger:
"""The number of cells in the EME grid."""
return len(self.centers)
@property
def mode_planes(self) -> list[Box]:
"""Planes for mode solving, aligned with cell centers."""
size = [inf, inf, inf]
center = list(self.center)
axis = self.axis
size[axis] = 0
mode_planes = []
for cell_center in self.centers:
center[axis] = cell_center
mode_planes.append(Box(center=center, size=size))
return mode_planes
@property
def boundary_planes(self) -> list[Box]:
"""Planes aligned with cell boundaries."""
size = list(self.size)
center = list(self.center)
axis = self.axis
size[axis] = 0
boundary_planes = []
for cell_boundary in self.boundaries:
center[axis] = cell_boundary
boundary_planes.append(Box(center=center, size=size))
return boundary_planes
@property
def cells(self) -> list[Box]:
"""EME cells in the grid. Each cell is a :class:`.Box`."""
size = list(self.size)
center = list(self.center)
axis = self.axis
cells = []
for cell_center, length in zip(self.centers, self.lengths):
size[axis] = length
center[axis] = cell_center
cells.append(Box(center=center, size=size))
return cells
[docs]
def cell_indices_in_box(self, box: Box) -> list[pd.NonNegativeInteger]:
"""Indices of cells that overlap with 'box'. Used to determine
which data is recorded by a monitor.
Parameters
----------
box: :class:`.Box`
The box to check for intersecting cells.
Returns
-------
List[pd.NonNegativeInteger]
The indices of the cells that intersect the provided box.
"""
indices = []
for i, cell in enumerate(self.cells):
if cell.intersects(box):
indices.append(i)
return indices
EMEGridSpecType = Union[EMEUniformGrid, EMECompositeGrid, EMEExplicitGrid]