Skip to content

Commit 68fc687

Browse files
committed
add mesh refinement option for WavePorts and validator to check if the grid is refined enough around WavePorts
1 parent 7c093b1 commit 68fc687

File tree

5 files changed

+152
-11
lines changed

5 files changed

+152
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Added `TriangleMesh.from_height_expression` class method to create a mesh from an analytical height function defined on a 2D grid and `TriangleMesh.from_height_grid` class method to create a mesh from height values sampled on a 2D grid.
2121
- Added heat sources with custom spatial dependence. It is now possible to add a `SpatialDataArray` as the `rate` in a `HeatSource`.
2222
- Added Transient Heat simulations. It is now possible to run transient Heat simulations. This can be done by specifying `analysis_spec` of `HeatChargeSimulation` object as `UnsteadyHeatAnalysis`.
23+
- A `num_grid_cells` field to `WavePort`, which ensures that there are 5 grid cells across the port. Grid refinement can be disabled by passing `None` to `num_grid_cells`.
2324

2425
### Fixed
2526
- Fixed bug in broadband adjoint source creation when forward simulation had a pulse amplitude greater than 1 or a nonzero pulse phase.

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,9 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
304304
normal_axis=2,
305305
clockwise=direction != "+",
306306
)
307-
307+
port_cells = None
308+
if port_refinement:
309+
port_cells = 5
308310
port = WavePort(
309311
center=center,
310312
size=[2 * Router, 2 * Router, 0],
@@ -314,6 +316,7 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
314316
mode_index=0,
315317
voltage_integral=voltage_integral,
316318
current_integral=current_integral,
319+
num_grid_cells=port_cells,
317320
)
318321
return port
319322

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import tidy3d as td
1010
from tidy3d.components.data.data_array import FreqDataArray
1111
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError
12-
from tidy3d.plugins.microwave import CustomCurrentIntegral2D, VoltageIntegralAxisAligned
12+
from tidy3d.plugins.microwave import (
13+
CurrentIntegralAxisAligned,
14+
CustomCurrentIntegral2D,
15+
VoltageIntegralAxisAligned,
16+
)
1317
from tidy3d.plugins.smatrix import (
1418
AbstractComponentModeler,
1519
CoaxialLumpedPort,
@@ -604,6 +608,69 @@ def test_wave_port_path_integral_validation():
604608
assert wave_port.bounds[0][2] > wave_port.voltage_integral.bounds[0][2]
605609

606610

611+
def test_wave_port_grid_validation(tmp_path):
612+
"""Ensure that 'num_grid_cells' is validated and works to ensure that the grid is refined around wave ports."""
613+
size_port = [2, 2, 0]
614+
center_port = [0, 0, -10]
615+
616+
voltage_path = VoltageIntegralAxisAligned(
617+
center=(0.5, 0, -10),
618+
size=(1.0, 0, 0),
619+
extrapolate_to_endpoints=True,
620+
snap_path_to_grid=True,
621+
sign="+",
622+
)
623+
624+
current_path = CurrentIntegralAxisAligned(
625+
center=(0.5, 0, -10),
626+
size=(0.25, 0.5, 0),
627+
snap_contour_to_grid=True,
628+
sign="+",
629+
)
630+
631+
mode_spec = td.ModeSpec(num_modes=1, target_neff=1.8)
632+
633+
_ = WavePort(
634+
center=center_port,
635+
size=size_port,
636+
name="wave_port_1",
637+
mode_spec=mode_spec,
638+
direction="+",
639+
voltage_integral=voltage_path,
640+
current_integral=current_path,
641+
num_grid_cells=None,
642+
)
643+
644+
with pytest.raises(pd.ValidationError):
645+
_ = WavePort(
646+
center=center_port,
647+
size=size_port,
648+
name="wave_port_1",
649+
mode_spec=mode_spec,
650+
direction="+",
651+
voltage_integral=voltage_path,
652+
current_integral=current_path,
653+
num_grid_cells=2,
654+
)
655+
656+
modeler = make_coaxial_component_modeler(
657+
grid_spec=td.GridSpec.auto(wavelength=10e3),
658+
port_refinement=True,
659+
path_dir=str(tmp_path),
660+
port_types=(WavePort, WavePort),
661+
)
662+
_ = modeler.sim_dict
663+
664+
modeler = make_coaxial_component_modeler(
665+
grid_spec=td.GridSpec.auto(wavelength=10e3),
666+
port_refinement=False,
667+
path_dir=str(tmp_path),
668+
port_types=(WavePort, WavePort),
669+
)
670+
with pytest.raises(SetupError):
671+
_ = modeler.sim_dict
672+
673+
607674
def test_wave_port_to_mode_solver(tmp_path):
608675
"""Checks that wave port can be converted to a mode solver."""
609676
modeler = make_coaxial_component_modeler(

tidy3d/plugins/smatrix/component_modelers/terminal.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from tidy3d.components.types import Ax
2020
from tidy3d.components.viz import add_ax_if_none, equal_aspect
2121
from tidy3d.constants import C_0, OHM
22-
from tidy3d.exceptions import Tidy3dError, Tidy3dKeyError, ValidationError
22+
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError, ValidationError
2323
from tidy3d.log import log
2424
from tidy3d.plugins.smatrix.data.terminal import PortDataArray, TerminalPortDataArray
2525
from tidy3d.plugins.smatrix.ports.base_lumped import AbstractLumpedPort
@@ -142,9 +142,17 @@ def sim_dict(self) -> dict[str, Simulation]:
142142
port.to_load(snap_center=snap_centers[port.name]) for port in self._lumped_ports
143143
]
144144

145+
# Add mesh overrides for any wave ports present
146+
mesh_overrides = list(sim_wo_source.grid_spec.override_structures)
147+
for wave_port in self._wave_ports:
148+
if wave_port.num_grid_cells is not None:
149+
mesh_overrides.extend(wave_port.to_mesh_overrides())
150+
new_grid_spec = sim_wo_source.grid_spec.updated_copy(override_structures=mesh_overrides)
151+
145152
update_dict = {
146153
"monitors": new_mnts,
147154
"lumped_elements": new_lumped_elements,
155+
"grid_spec": new_grid_spec,
148156
}
149157

150158
# This is the new default simulation will all shared components added
@@ -158,10 +166,6 @@ def sim_dict(self) -> dict[str, Simulation]:
158166
task_name = self._task_name(port=port)
159167
sim_dict[task_name] = sim_wo_source.updated_copy(sources=[port_source])
160168

161-
# Check final simulations for grid size at lumped ports
162-
for _, sim in sim_dict.items():
163-
TerminalComponentModeler._check_grid_size_at_ports(sim, self._lumped_ports)
164-
165169
# Now, create simulations with wave port sources and mode solver monitors for computing port modes
166170
for wave_port in self._wave_ports:
167171
# Source is placed just before the field monitor of the port
@@ -174,6 +178,12 @@ def sim_dict(self) -> dict[str, Simulation]:
174178

175179
task_name = self._task_name(port=wave_port)
176180
sim_dict[task_name] = sim_wo_source.copy(update=update_dict)
181+
182+
# Check final simulations for grid size at ports
183+
for _, sim in sim_dict.items():
184+
TerminalComponentModeler._check_grid_size_at_ports(sim, self._lumped_ports)
185+
TerminalComponentModeler._check_grid_size_at_wave_ports(sim, self._wave_ports)
186+
177187
return sim_dict
178188

179189
@cached_property
@@ -242,12 +252,34 @@ def _validate_radiation_monitors(cls, val, values):
242252
return val
243253

244254
@staticmethod
245-
def _check_grid_size_at_ports(simulation: Simulation, ports: list[Union[AbstractLumpedPort]]):
255+
def _check_grid_size_at_ports(
256+
simulation: Simulation, ports: list[Union[LumpedPort, CoaxialLumpedPort]]
257+
):
246258
"""Raises :class:`.SetupError` if the grid is too coarse at port locations"""
247259
yee_grid = simulation.grid.yee
248260
for port in ports:
249261
port._check_grid_size(yee_grid)
250262

263+
@staticmethod
264+
def _check_grid_size_at_wave_ports(simulation: Simulation, ports: list[WavePort]):
265+
"""Raises :class:`.SetupError` if the grid is too coarse at port locations"""
266+
for port in ports:
267+
disc_grid = simulation.discretize(port)
268+
check_axes = port.transverse_axes
269+
msg_header = f"'WavePort' '{port.name}' "
270+
for axis in check_axes:
271+
sim_size = simulation.size[axis]
272+
dim_cells = disc_grid.num_cells[axis]
273+
if sim_size > 0 and dim_cells <= 2:
274+
small_dim = "xyz"[axis]
275+
raise SetupError(
276+
msg_header + f"is too small along the "
277+
f"'{small_dim}' axis. Less than '3' grid cells were detected. "
278+
"Please ensure that the port's 'num_grid_cells' is not 'None'. "
279+
"You also may need to use an 'AutoGrid' or `QuasiUniformGrid` "
280+
"for the simulation passed to the 'TerminalComponentModeler'."
281+
)
282+
251283
def compute_power_wave_amplitudes_at_each_port(
252284
self, port_reference_impedances: PortDataArray, sim_data: SimulationData
253285
) -> tuple[PortDataArray, PortDataArray]:

tidy3d/plugins/smatrix/ports/wave.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
from tidy3d.components.simulation import Simulation
1919
from tidy3d.components.source.field import ModeSource, ModeSpec
2020
from tidy3d.components.source.time import GaussianPulse
21-
from tidy3d.components.types import Direction, FreqArray
21+
from tidy3d.components.structure import MeshOverrideStructure
22+
from tidy3d.components.types import Axis, Direction, FreqArray
2223
from tidy3d.constants import fp_eps
2324
from tidy3d.exceptions import ValidationError
2425
from tidy3d.plugins.microwave import CurrentIntegralTypes, ImpedanceCalculator, VoltageIntegralTypes
2526
from tidy3d.plugins.mode import ModeSolver
2627

2728
from .base_terminal import AbstractTerminalPort
2829

30+
DEFAULT_WAVE_PORT_NUM_CELLS = 5
31+
MIN_WAVE_PORT_NUM_CELLS = 3
32+
2933

3034
class WavePort(AbstractTerminalPort, Box):
3135
"""Class representing a single wave port"""
@@ -63,6 +67,15 @@ class WavePort(AbstractTerminalPort, Box):
6367
description="Definition of current integral used to compute current and the characteristic impedance.",
6468
)
6569

70+
num_grid_cells: Optional[int] = pd.Field(
71+
DEFAULT_WAVE_PORT_NUM_CELLS,
72+
ge=MIN_WAVE_PORT_NUM_CELLS,
73+
title="Number of Grid Cells",
74+
description="Number of mesh grid cells in the transverse plane of the `WavePort`. "
75+
"Used in generating the suggested list of :class:`.MeshOverrideStructure` objects. "
76+
"Must be greater than or equal to 3. When set to `None`, no grid refinement is performed.",
77+
)
78+
6679
def _mode_voltage_coefficients(self, mode_data: ModeData) -> FreqModeDataArray:
6780
"""Calculates scaling coefficients to convert mode amplitudes
6881
to the total port voltage.
@@ -88,10 +101,16 @@ def _mode_current_coefficients(self, mode_data: ModeData) -> FreqModeDataArray:
88101
return current_coeffs.squeeze()
89102

90103
@cached_property
91-
def injection_axis(self):
104+
def injection_axis(self) -> Axis:
92105
"""Injection axis of the port."""
93106
return self.size.index(0.0)
94107

108+
@cached_property
109+
def transverse_axes(self) -> tuple[Axis, Axis]:
110+
"""Transverse axes of the port."""
111+
_, trans_axes = Box.pop_axis([0, 1, 2], self.injection_axis)
112+
return trans_axes
113+
95114
@cached_property
96115
def _mode_monitor_name(self) -> str:
97116
"""Return the name of the :class:`.ModeMonitor` associated with this port."""
@@ -186,6 +205,25 @@ def compute_port_impedance(
186205
impedance_array = impedance_calc.compute_impedance(mode_data)
187206
return impedance_array
188207

208+
def to_mesh_overrides(self) -> list[MeshOverrideStructure]:
209+
"""Creates a list of :class:`.MeshOverrideStructure` for mesh refinement in the transverse
210+
plane of the port. The mode source requires at least 3 grid cells in the transverse
211+
dimensions, so these mesh overrides will be added to the simulation to ensure that this
212+
requirement is satisfied.
213+
"""
214+
dl = [None] * 3
215+
for trans_axis in self.transverse_axes:
216+
dl[trans_axis] = self.size[trans_axis] / self.num_grid_cells
217+
218+
return [
219+
MeshOverrideStructure(
220+
geometry=Box(center=self.center, size=self.size),
221+
dl=dl,
222+
shadow=False,
223+
priority=-1,
224+
)
225+
]
226+
189227
@pd.validator("voltage_integral", "current_integral")
190228
def _validate_path_integrals_within_port(cls, val, values):
191229
"""Raise ``ValidationError`` when the supplied path integrals are not within the port bounds."""
@@ -213,7 +251,7 @@ def _check_voltage_or_current(cls, val, values):
213251
return val
214252

215253
@pd.validator("current_integral", always=True)
216-
def validate_current_integral_sign(cls, val, values):
254+
def _validate_current_integral_sign(cls, val, values):
217255
"""
218256
Validate that the sign of ``current_integral`` matches the port direction.
219257
"""

0 commit comments

Comments
 (0)