Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subsection for EMESimulation #1863

Merged
merged 1 commit into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ 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.
- Mode solver now always operates on a reduced simulation copy.

### Fixed
- Error when loading a previously run `Batch` or `ComponentModeler` containing custom data.
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
45 changes: 44 additions & 1 deletion tests/test_components/test_eme.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,22 @@ def test_eme_simulation(log_capture):
_ = sim2.plot(y=0, ax=AX)
_ = sim2.plot(z=0, ax=AX)

# must be 3D
with pytest.raises(pd.ValidationError):
_ = td.EMESimulation(
size=(0, 2, 2),
freqs=[td.C_0],
axis=2,
eme_grid_spec=td.EMEUniformGrid(num_cells=2, mode_spec=td.EMEModeSpec()),
)
with pytest.raises(pd.ValidationError):
_ = td.EMESimulation(
size=(2, 2, 0),
freqs=[td.C_0],
axis=2,
eme_grid_spec=td.EMEUniformGrid(num_cells=2, mode_spec=td.EMEModeSpec()),
)

# need at least one freq
with pytest.raises(pd.ValidationError):
_ = sim.updated_copy(freqs=[])
Expand Down Expand Up @@ -324,7 +340,7 @@ def test_eme_simulation(log_capture):
)

# test port offsets
with pytest.raises(SetupError):
with pytest.raises(ValidationError):
_ = sim.updated_copy(port_offsets=[sim.size[sim.axis] * 2 / 3, sim.size[sim.axis] * 2 / 3])

# test duplicate freqs
Expand Down Expand Up @@ -1193,3 +1209,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(pd.ValidationError):
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
117 changes: 102 additions & 15 deletions tidy3d/components/eme/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@

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
import pydantic.v1 as pd

from ...exceptions import SetupError
from ...exceptions import SetupError, ValidationError
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,16 @@ class EMESimulation(AbstractYeeGridSimulation):
_freqs_not_empty = validate_freqs_not_empty()
_freqs_lower_bound = validate_freqs_min()

@pd.validator("size", always=True)
def _validate_fully_3d(cls, val):
"""An EME simulation must be fully 3D."""
if val.count(0.0) != 0:
raise ValidationError(
"'EMESimulation' cannot have any component of 'size' equal to "
f"zero, given 'size={val}'."
)
return val

@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 @@ -606,7 +617,7 @@ def _validate_port_offsets(self):
size = self.size
axis = self.axis
if size[axis] < total_offset:
raise SetupError(
raise ValidationError(
"The sum of the two 'port_offset' fields "
"cannot exceed the simulation 'size' in the 'axis' direction."
)
Expand Down Expand Up @@ -1005,12 +1016,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 +1059,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 +1080,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
Loading