Skip to content

Commit

Permalink
Add subsection for EMESimulation
Browse files Browse the repository at this point in the history
  • Loading branch information
caseyflex committed Aug 5, 2024
1 parent 514a6cf commit 6e25dfb
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 250 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- More convenient mesh importing from another simulation through `grid_spec = GridSpec.from_grid(sim.grid)`.
- `autograd` gradient calculations can be performed on the server by passing `local_gradient = False` into `web.run()` or `web.run_async()`.
- Automatic differentiation with `autograd` supports multiple frequencies through single, broadband adjoint simulation when the objective function can be formulated as depending on a single dataset in the output `SimulationData` with frequency dependence only.
- Convenience method `EMESimulation.subsection` to create a new EME simulation based on a subregion of an existing one.

### Changed
- Error if field projection monitors found in 2D simulations, except `FieldProjectionAngleMonitor` with `far_field_approx = True`. Support for other monitors and for exact field projection will be coming in a subsequent Tidy3D version.
Expand Down
13 changes: 13 additions & 0 deletions tests/test_components/test_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,19 @@ def verify_custom_medium_methods(mat, reduced_fields):
sim.subsection(subsection, remove_outside_custom_mediums=False)
sim.subsection(subsection, remove_outside_custom_mediums=True)

# eme
eme_sim = td.EMESimulation(
axis=2,
freqs=[td.C_0],
size=(1, 1, 1),
grid_spec=td.GridSpec.auto(wavelength=1.0),
structures=(struct,),
eme_grid_spec=td.EMEUniformGrid(num_cells=1, mode_spec=td.EMEModeSpec()),
)
_ = eme_sim.grid
eme_sim.subsection(subsection, remove_outside_custom_mediums=False)
eme_sim.subsection(subsection, remove_outside_custom_mediums=True)


def test_anisotropic_custom_medium():
"""Anisotropic CustomMedium."""
Expand Down
27 changes: 27 additions & 0 deletions tests/test_components/test_eme.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,3 +1193,30 @@ def test_eme_sim_data():
assert "mode_index" not in field_in_basis.Ex.coords
field_in_basis = sim_data.field_in_basis(field=sim_data["field"], modes=modes_in0, port_index=1)
assert "mode_index" not in field_in_basis.Ex.coords


def test_eme_sim_subsection():
eme_sim = td.EMESimulation(
axis=2,
size=(2, 2, 2),
freqs=[td.C_0],
grid_spec=td.GridSpec.auto(),
eme_grid_spec=td.EMEUniformGrid(num_cells=2, mode_spec=td.EMEModeSpec()),
)
# check 3d subsection
region = td.Box(size=(2, 2, 1))
subsection = eme_sim.subsection(region=region)
assert subsection.size[2] == 1

# check 3d subsection with identical eme grid
region = td.Box(size=(2, 2, 1))
subsection = eme_sim.subsection(region=region, eme_grid_spec="identical")
assert subsection.size[2] == 2
region = td.Box(size=(2, 2, 0.5), center=(0, 0, 0.5))
subsection = eme_sim.subsection(region=region, eme_grid_spec="identical")
assert subsection.size[2] == 1

# 2d subsection errors
region = td.Box(size=(2, 2, 0))
with pytest.raises(SetupError):
subsection = eme_sim.subsection(region=region)
2 changes: 2 additions & 0 deletions tests/test_plugins/test_mode_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,3 +1032,5 @@ def test_modes_eme_sim(mock_remote_api, local):
with pytest.raises(SetupError):
_ = msweb.run(solver)
_ = msweb.run(solver.to_fdtd_mode_solver())

_ = solver.reduced_simulation_copy
26 changes: 15 additions & 11 deletions tidy3d/components/eme/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,12 @@ def _validate_boundaries(cls, val, values):
raise ValidationError(
"There must be exactly one fewer item in 'boundaries' than " "in 'mode_specs'."
)
rmin = boundaries[0]
for rmax in boundaries[1:]:
if rmax < rmin:
raise ValidationError("The 'boundaries' must be increasing.")
rmin = rmax
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

def make_grid(self, center: Coordinate, size: Size, axis: Axis) -> EMEGrid:
Expand All @@ -237,12 +238,15 @@ def make_grid(self, center: Coordinate, size: Size, axis: Axis) -> EMEGrid:
"""
sim_rmin = center[axis] - size[axis] / 2
sim_rmax = center[axis] + size[axis] / 2
if self.boundaries[0] < sim_rmin - 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.")
if len(self.boundaries) > 0:
if self.boundaries[0] < sim_rmin - 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(
Expand Down
109 changes: 96 additions & 13 deletions tidy3d/components/eme/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import Dict, List, Literal, Optional, Tuple
from typing import Dict, List, Literal, Optional, Tuple, Union

import matplotlib as mpl
import numpy as np
Expand All @@ -12,18 +12,19 @@
from ...log import log
from ..base import cached_property
from ..boundary import BoundarySpec, PECBoundary
from ..geometry.base import Box
from ..grid.grid import Grid
from ..grid.grid_spec import GridSpec
from ..medium import FullyAnisotropicMedium
from ..monitor import AbstractModeMonitor, ModeSolverMonitor, Monitor
from ..monitor import AbstractModeMonitor, ModeSolverMonitor, Monitor, MonitorType
from ..scene import Scene
from ..simulation import AbstractYeeGridSimulation, Simulation
from ..source import GaussianPulse, ModeSource
from ..source import GaussianPulse, PointDipole
from ..structure import Structure
from ..types import Ax, Axis, FreqArray, annotate_type
from ..types import Ax, Axis, FreqArray, Symmetry, annotate_type
from ..validators import MIN_FREQUENCY, validate_freqs_min, validate_freqs_not_empty
from ..viz import add_ax_if_none, equal_aspect
from .grid import EMECompositeGrid, EMEGrid, EMEGridSpec, EMEGridSpecType
from .grid import EMECompositeGrid, EMEExplicitGrid, EMEGrid, EMEGridSpec, EMEGridSpecType
from .monitor import EMEFieldMonitor, EMEModeSolverMonitor, EMEMonitor, EMEMonitorType
from .sweep import EMEFreqSweep, EMELengthSweep, EMEModeSweep, EMESweepSpecType

Expand Down Expand Up @@ -231,6 +232,12 @@ class EMESimulation(AbstractYeeGridSimulation):
_freqs_not_empty = validate_freqs_not_empty()
_freqs_lower_bound = validate_freqs_min()

@pd.validator("size", always=True)
def _validate_size(cls, val):
"""An EME simulation must be fully 3D."""
if any(val == 0):
raise SetupError("'EMESimulation' cannot have any component of 'size' equal to zero.")

@pd.validator("grid_spec", always=True)
def _validate_auto_grid_wavelength(cls, val, values):
"""Handle the case where grid_spec is auto and wavelength is not provided."""
Expand Down Expand Up @@ -1005,12 +1012,10 @@ def grid(self) -> Grid:
)
plane = self.eme_grid.mode_planes[0]
sources.append(
ModeSource(
PointDipole(
center=plane.center,
size=plane.size,
source_time=GaussianPulse(freq0=freqs[0], fwidth=0.1 * freqs[0]),
direction="+",
mode_spec=self.eme_grid.mode_specs[0],
polarization="Ez",
)
)

Expand Down Expand Up @@ -1050,12 +1055,10 @@ def _to_fdtd_sim(self) -> Simulation:
plane = self.eme_grid.mode_planes[0]
freq0 = self.freqs[0]
source_time = GaussianPulse(freq0=freq0, fwidth=0.1 * freq0)
source = ModeSource(
source = PointDipole(
center=plane.center,
size=plane.size,
source_time=source_time,
direction="+",
mode_spec=self.eme_grid.mode_specs[0]._to_mode_spec(),
polarization="Ez",
)
# copy over all FDTD monitors too
monitors = [monitor for monitor in self.monitors if not isinstance(monitor, EMEMonitor)]
Expand All @@ -1073,3 +1076,83 @@ def _to_fdtd_sim(self) -> Simulation:
sources=[source],
monitors=monitors,
)

def subsection(
self,
region: Box,
grid_spec: Union[GridSpec, Literal["identical"]] = None,
eme_grid_spec: Union[EMEGridSpec, Literal["identical"]] = None,
symmetry: Tuple[Symmetry, Symmetry, Symmetry] = None,
monitors: Tuple[MonitorType, ...] = None,
remove_outside_structures: bool = True,
remove_outside_custom_mediums: bool = False,
**kwargs,
) -> EMESimulation:
"""Generate a simulation instance containing only the ``region``.
Same as in :class:`.AbstractYeeGridSimulation`, except also restricting EME grid.
Parameters
----------
region : :class:.`Box`
New simulation domain.
grid_spec : :class:.`GridSpec` = None
New grid specification. If ``None``, then it is inherited from the original
simulation. If ``identical``, then the original grid is transferred directly as a
:class:.`CustomGrid`. Note that in the latter case the region of the new simulation is
snapped to the original grid lines.
eme_grid_spec: :class:`.EMEGridSpec` = None
New EME grid specification. If ``None``, then it is inherited from the original
simulation. If ``identical``, then the original grid is transferred directly as a
:class:`.EMEExplicitGrid`. Noe that in the latter case the region of the new simulation
is expanded to contain full EME cells.
symmetry : Tuple[Literal[0, -1, 1], Literal[0, -1, 1], Literal[0, -1, 1]] = None
New simulation symmetry. If ``None``, then it is inherited from the original
simulation. Note that in this case the size and placement of new simulation domain
must be commensurate with the original symmetry.
monitors : Tuple[MonitorType, ...] = None
New list of monitors. If ``None``, then the monitors intersecting the new simulation
domain are inherited from the original simulation.
remove_outside_structures : bool = True
Remove structures outside of the new simulation domain.
remove_outside_custom_mediums : bool = True
Remove custom medium data outside of the new simulation domain.
**kwargs
Other arguments passed to new simulation instance.
"""

new_region = region
if eme_grid_spec is None:
eme_grid_spec = self.eme_grid_spec
elif isinstance(eme_grid_spec, str) and eme_grid_spec == "identical":
axis = self.axis
mode_specs = self.eme_grid.mode_specs
boundaries = self.eme_grid.boundaries
indices = self.eme_grid.cell_indices_in_box(box=region)

new_boundaries = boundaries[indices[0] : indices[-1] + 2]
new_mode_specs = mode_specs[indices[0] : indices[-1] + 1]

rmin = list(region.bounds[0])
rmax = list(region.bounds[1])
rmin[axis] = min(rmin[axis], new_boundaries[0])
rmax[axis] = max(rmax[axis], new_boundaries[-1])
new_region = Box.from_bounds(rmin=rmin, rmax=rmax)

# remove outer boundaries for explicit grid
new_boundaries = new_boundaries[1:-1]

eme_grid_spec = EMEExplicitGrid(mode_specs=new_mode_specs, boundaries=new_boundaries)

new_sim = super().subsection(
region=new_region,
grid_spec=grid_spec,
symmetry=symmetry,
monitors=monitors,
remove_outside_structures=remove_outside_structures,
remove_outside_custom_mediums=remove_outside_custom_mediums,
**kwargs,
)

new_sim = new_sim.updated_copy(eme_grid_spec=eme_grid_spec)

return new_sim
2 changes: 1 addition & 1 deletion tidy3d/components/grid/grid_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ class CustomGridBoundaries(GridSpec1d):
Example
-------
>>> grid_1d = CustomGridCoords(boundaries=[-0.2, 0.0, 0.2, 0.4, 0.5, 0.6, 0.7])
>>> grid_1d = CustomGridBoundaries(coords=[-0.2, 0.0, 0.2, 0.4, 0.5, 0.6, 0.7])
"""

coords: Coords1D = pd.Field(
Expand Down
Loading

0 comments on commit 6e25dfb

Please sign in to comment.