Skip to content

Commit bdb9420

Browse files
committed
fix bug in port snapping and correct bounds for current integral
added grid size check to TerminalComponentModeler and improved test coverage of smatrix plugin
1 parent 5d1927c commit bdb9420

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

tests/test_plugins/terminal_component_modeler_def.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
freq_stop = 10e9
2626

2727

28-
def make_simulation(planar_pec: bool, length: float = None):
28+
def make_simulation(planar_pec: bool, length: float = None, auto_grid: bool = True):
2929
if length:
3030
strip_length = length
3131
else:
@@ -45,7 +45,10 @@ def make_simulation(planar_pec: bool, length: float = None):
4545
run_time = 60 / fwidth
4646

4747
# Spatial grid specification
48-
grid_spec = td.GridSpec.auto(min_steps_per_wvl=10, wavelength=td.C_0 / freq_stop)
48+
if auto_grid:
49+
grid_spec = td.GridSpec.auto(min_steps_per_wvl=10, wavelength=td.C_0 / freq_stop)
50+
else:
51+
grid_spec = td.GridSpec.uniform(wavelength0 / 11)
4952

5053
# Make structures
5154
strip = td.Structure(
@@ -98,14 +101,19 @@ def make_simulation(planar_pec: bool, length: float = None):
98101

99102

100103
def make_component_modeler(
101-
planar_pec: bool, reference_impedance: complex = 50, length: float = None, **kwargs
104+
planar_pec: bool,
105+
reference_impedance: complex = 50,
106+
length: float = None,
107+
port_refinement: bool = True,
108+
auto_grid: bool = True,
109+
**kwargs
102110
):
103111
if length:
104112
strip_length = length
105113
else:
106114
strip_length = default_strip_length
107115

108-
sim = make_simulation(planar_pec, length=length)
116+
sim = make_simulation(planar_pec, length=length, auto_grid=auto_grid)
109117

110118
if planar_pec:
111119
height = 0
@@ -118,7 +126,9 @@ def make_component_modeler(
118126
center_src2 = [strip_length / 2, 0, height + gap / 2]
119127
size_src2 = [0, strip_width, gap]
120128

121-
port_cells = np.ceil(gap / (metal_thickness / 1))
129+
port_cells = None
130+
if port_refinement:
131+
port_cells = np.ceil(gap / (metal_thickness / 1))
122132

123133
port_1 = LumpedPort(
124134
center=center_src1,

tests/test_plugins/test_terminal_component_modeler.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import tidy3d as td
77
from tidy3d.plugins.smatrix import (
88
AbstractComponentModeler,
9+
LumpedPort,
910
LumpedPortDataArray,
1011
TerminalComponentModeler,
1112
)
12-
from tidy3d.exceptions import Tidy3dKeyError
13+
from tidy3d.exceptions import Tidy3dKeyError, SetupError
1314
from ..utils import run_emulated
1415
from .terminal_component_modeler_def import make_component_modeler
1516

@@ -55,8 +56,11 @@ def test_plot_sim_eps(tmp_path):
5556
plt.close()
5657

5758

58-
def test_make_component_modeler(tmp_path):
59-
_ = make_component_modeler(planar_pec=False, path_dir=str(tmp_path))
59+
@pytest.mark.parametrize("port_refinement", [False, True])
60+
def test_make_component_modeler(tmp_path, port_refinement):
61+
_ = make_component_modeler(
62+
planar_pec=False, path_dir=str(tmp_path), port_refinement=port_refinement
63+
)
6064

6165

6266
def test_run(monkeypatch, tmp_path):
@@ -138,3 +142,24 @@ def test_ab_to_s_component_modeler():
138142
b_matrix = LumpedPortDataArray(data=b_values, coords=coords)
139143
S_matrix = AbstractComponentModeler.ab_to_s(a_matrix, b_matrix)
140144
assert np.isclose(S_matrix, b_matrix).all()
145+
146+
147+
def test_port_snapping(monkeypatch, tmp_path):
148+
modeler = make_component_modeler(
149+
planar_pec=True, path_dir=str(tmp_path), port_refinement=False, auto_grid=False
150+
)
151+
# Without port refinement the grid is much too coarse for these port sizes
152+
with pytest.raises(SetupError):
153+
_ = run_component_modeler(monkeypatch, modeler)
154+
155+
156+
def test_coarse_grid_at_port(monkeypatch, tmp_path):
157+
modeler = make_component_modeler(planar_pec=True, path_dir=str(tmp_path), port_refinement=False)
158+
# Without port refinement the grid is much too coarse for these port sizes
159+
with pytest.raises(SetupError):
160+
_ = run_component_modeler(monkeypatch, modeler)
161+
162+
163+
def test_validate_port_voltage_axis():
164+
with pytest.raises(pydantic.ValidationError):
165+
LumpedPort(center=(0, 0, 0), size=(0, 1, 2), voltage_axis=0, impedance=50)

tidy3d/plugins/smatrix/component_modelers/terminal.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import xarray as xr
1010

1111
from ....constants import C_0, fp_eps
12+
from ....exceptions import SetupError
1213
from ....components.simulation import Simulation
1314
from ....components.geometry.utils_2d import snap_coordinate_to_grid
1415
from ....components.data.sim_data import SimulationData
@@ -148,7 +149,7 @@ def sim_dict(self) -> Dict[str, Simulation]:
148149
list(self.simulation.monitors) + port_voltage_monitors + port_current_monitors
149150
)
150151
for port, center in zip(self.ports, snap_centers):
151-
port_source = self.to_source(self._source_time, port=port, snap_center=center)
152+
port_source = port.to_source(self._source_time, snap_center=center)
152153
update_dict = dict(
153154
sources=[port_source],
154155
monitors=new_mnts,
@@ -160,6 +161,9 @@ def sim_dict(self) -> Dict[str, Simulation]:
160161
task_name = self._task_name(port=port)
161162
sim_dict[task_name] = sim_copy
162163

164+
# Check final simulations for grid size at ports
165+
for _, sim in sim_dict.items():
166+
TerminalComponentModeler._check_grid_size_at_ports(sim, self.ports)
163167
return sim_dict
164168

165169
@cached_property
@@ -189,6 +193,15 @@ def _construct_smatrix(self, batch_data: BatchData) -> LumpedPortDataArray:
189193
a_matrix = LumpedPortDataArray(values, coords=coords)
190194
b_matrix = a_matrix.copy(deep=True)
191195

196+
def select_within_bounds(coords: np.array, min, max):
197+
"""Helper to slice coordinates within min and max bounds, including a tolerance."""
198+
"""xarray does not have this functionality yet. """
199+
np_idx = np.logical_and(coords > min, coords < max)
200+
np_idx = np.logical_or(np_idx, np.isclose(coords, min, rtol=fp_eps, atol=fp_eps))
201+
np_idx = np.logical_or(np_idx, np.isclose(coords, max, rtol=fp_eps, atol=fp_eps))
202+
coords = coords[np_idx]
203+
return coords
204+
192205
def port_voltage(port: LumpedPort, sim_data: SimulationData) -> xr.DataArray:
193206
"""Helper to compute voltage across the port."""
194207
e_component = "xyz"[port.voltage_axis]
@@ -263,13 +276,21 @@ def port_current(port: LumpedPort, sim_data: SimulationData) -> xr.DataArray:
263276
h1_field = h_field.isel({orth_component: orth_index - 1})
264277
h2_field = h_field.isel({orth_component: orth_index})
265278
h_field = h1_field - h2_field
279+
280+
# Find exact bounds of port taking into consideration the Yee grid
281+
np_coords = h_coords[port.current_axis].values
282+
port_min = port.bounds[0][port.current_axis]
283+
port_max = port.bounds[1][port.current_axis]
284+
np_coords = select_within_bounds(np_coords, port_min, port_max)
285+
coord_low = np_coords[0]
286+
coord_high = np_coords[-1]
266287
# Extract cap field which is coincident with sheet
267288
h_cap = h_cap_field.isel({orth_component: orth_index})
268289

269290
# Need to make sure to use the nearest coordinate that is
270291
# at least greater than the port bounds
271-
hcap_minus = h_cap.sel({h_component: slice(-np.inf, port.bounds[0][port.current_axis])})
272-
hcap_plus = h_cap.sel({h_component: slice(port.bounds[1][port.current_axis], np.inf)})
292+
hcap_minus = h_cap.sel({h_component: slice(-np.inf, coord_low)})
293+
hcap_plus = h_cap.sel({h_component: slice(coord_high, np.inf)})
273294
hcap_minus = hcap_minus.isel({h_component: -1})
274295
hcap_plus = hcap_plus.isel({h_component: 0})
275296
# Length of integration along the h_cap contour is a single cell width
@@ -344,3 +365,21 @@ def _validate_3d_simulation(cls, val):
344365
f"'{cls.__name__}' must be setup with a 3D simulation with all sizes greater than 0."
345366
)
346367
return val
368+
369+
@staticmethod
370+
def _check_grid_size_at_ports(simulation: Simulation, ports: list[LumpedPort]):
371+
"""Raises :class:`SetupError` if the grid is too coarse at port locations"""
372+
yee_grid = simulation.grid.yee
373+
for port in ports:
374+
e_component = "xyz"[port.voltage_axis]
375+
e_yee_grid = yee_grid.grid_dict[f"E{e_component}"]
376+
coords = e_yee_grid.to_dict[e_component]
377+
min_bound = port.bounds[0][port.voltage_axis]
378+
max_bound = port.bounds[1][port.voltage_axis]
379+
coords_within_port = np.any(np.logical_and(coords > min_bound, coords < max_bound))
380+
if not coords_within_port:
381+
raise SetupError(
382+
f"Grid is too coarse along '{e_component}' direction for the lumped port "
383+
f"at location {port.center}. Either set the port's 'num_grid_cells' to "
384+
f"a nonzero integer or modify the 'GridSpec'. "
385+
)

0 commit comments

Comments
 (0)