Skip to content

Commit 086d5ec

Browse files
feat: implement structure extrusion when waveport is defined on a boundary
1 parent ded37cd commit 086d5ec

File tree

8 files changed

+527
-27
lines changed

8 files changed

+527
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Added
1212
- Added S-parameter de-embedding to `TerminalComponentModelerData`, enabling recalculation with shifted reference planes.
13+
- Added optional automatic extrusion of structures intersecting with a `WavePort` via the new `extrude_structures` field, ensuring mode sources, absorbers, and PEC frames are fully contained.
1314
- Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging.
1415
- Added support for `symlog` and `log` scale plotting in `Scene.plot_eps()` and `Scene.plot_structures_property()` methods. The `symlog` scale provides linear behavior near zero and logarithmic behavior elsewhere, while 'log' is a base 10 logarithmic scale.
1516
- Added `LowFrequencySmoothingSpec` and `ModelerLowFrequencySmoothingSpec` for automatic smoothing of mode monitor data at low frequencies where DFT sampling is insufficient.

schemas/TerminalComponentModeler.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17880,6 +17880,10 @@
1788017880
],
1788117881
"type": "string"
1788217882
},
17883+
"extrude_structures": {
17884+
"default": false,
17885+
"type": "boolean"
17886+
},
1788317887
"frame": {
1788417888
"allOf": [
1788517889
{

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66

77
import tidy3d as td
8+
import tidy3d.plugins.microwave as mw
89
from tidy3d.plugins.smatrix import (
910
CoaxialLumpedPort,
1011
LumpedPort,
@@ -331,3 +332,145 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
331332
)
332333

333334
return modeler
335+
336+
337+
def make_differential_stripline_modeler():
338+
# Frequency range (Hz)
339+
f_min, f_max = (1e9, 70e9)
340+
341+
# Frequency sample points
342+
freqs = np.linspace(f_min, f_max, 101)
343+
344+
# Geometry
345+
mil = 25.4 # conversion to mils to microns (default unit)
346+
w = 3.2 * mil # Signal strip width
347+
t = 0.7 * mil # Conductor thickness
348+
h = 10.7 * mil # Substrate thickness
349+
se = 7 * mil # gap between edge-coupled pair
350+
L = 4000 * mil # Line length
351+
len_inf = 1e6 # Effective infinity
352+
353+
left_end = -L / 2
354+
right_end = len_inf
355+
356+
len_z = right_end - left_end
357+
cent_z = (left_end + right_end) / 2
358+
waveport_z = L
359+
360+
# Material properties
361+
eps = 4.4 # Relative permittivity, substrate
362+
363+
# define media
364+
med_sub = td.Medium(permittivity=eps)
365+
med_metal = td.PEC
366+
367+
left_strip_geometry = td.Box(center=(-(se + w) / 2, 0, 0), size=(w, t, L))
368+
right_strip_geometry = td.Box(center=((se + w) / 2, 0, 0), size=(w, t, L))
369+
370+
# Substrate
371+
str_sub = td.Structure(geometry=td.Box(center=(0, 0, 0), size=(len_inf, h, L)), medium=med_sub)
372+
373+
# disjoint signal strips
374+
str_signal_strips = td.Structure(
375+
geometry=td.GeometryGroup(geometries=[left_strip_geometry, right_strip_geometry]),
376+
medium=med_metal,
377+
)
378+
379+
# Top ground plane
380+
str_gnd_top = td.Structure(
381+
geometry=td.Box(center=(0, h / 2 + t / 2, 0), size=(len_inf, t, L)), medium=med_metal
382+
)
383+
384+
# Bottom ground plane
385+
str_gnd_bot = td.Structure(
386+
geometry=td.Box(center=(0, -h / 2 - t / 2, 0), size=(len_inf, t, L)), medium=med_metal
387+
)
388+
389+
# Create a LayerRefinementSpec from signal trace structures
390+
lr_spec = td.LayerRefinementSpec.from_structures(
391+
structures=[str_signal_strips],
392+
axis=1, # Layer normal is in y-direction
393+
min_steps_along_axis=10, # Min 10 grid cells along normal direction
394+
refinement_inside_sim_only=False, # Metal structures extend outside sim domain. Set 'False' to snap to corners outside sim.
395+
bounds_snapping="bounds", # snap grid to metal boundaries
396+
corner_refinement=td.GridRefinement(
397+
dl=t / 10, num_cells=2
398+
), # snap to corners and apply added refinement
399+
)
400+
401+
# Layer refinement for top and bottom ground planes
402+
lr_spec2 = lr_spec.updated_copy(center=(0, h / 2 + t / 2, cent_z), size=(len_inf, t, len_z))
403+
lr_spec3 = lr_spec.updated_copy(center=(0, -h / 2 - t / 2, cent_z), size=(len_inf, t, len_z))
404+
405+
# Define overall grid specification
406+
grid_spec = td.GridSpec.auto(
407+
wavelength=td.C_0 / f_max,
408+
min_steps_per_wvl=30,
409+
layer_refinement_specs=[lr_spec, lr_spec2, lr_spec3],
410+
)
411+
412+
# boundary specs
413+
boundary_spec = td.BoundarySpec(
414+
x=td.Boundary.pml(),
415+
y=td.Boundary.pec(),
416+
z=td.Boundary.pml(),
417+
)
418+
419+
# Define port specification
420+
wave_port_mode_spec = td.ModeSpec(num_modes=1, target_neff=np.sqrt(eps))
421+
422+
# Define current and voltage integrals
423+
current_integral = mw.AxisAlignedCurrentIntegral(
424+
center=((se + w) / 2, 0, -waveport_z / 2), size=(2 * w, 3 * t, 0), sign="+"
425+
)
426+
voltage_integral = mw.AxisAlignedVoltageIntegral(
427+
center=(0, 0, -waveport_z / 2),
428+
size=(se, 0, 0),
429+
extrapolate_to_endpoints=True,
430+
snap_path_to_grid=True,
431+
sign="+",
432+
)
433+
434+
# Define wave ports
435+
WP1 = WavePort(
436+
center=(0, 0, -waveport_z / 2),
437+
size=(len_inf, len_inf, 0),
438+
mode_spec=wave_port_mode_spec,
439+
direction="+",
440+
name="WP1",
441+
mode_index=0,
442+
current_integral=current_integral,
443+
voltage_integral=voltage_integral,
444+
)
445+
WP2 = WP1.updated_copy(
446+
name="WP2",
447+
center=(0, 0, waveport_z / 2),
448+
direction="-",
449+
current_integral=current_integral.updated_copy(
450+
center=((se + w) / 2, 0, waveport_z / 2), sign="-"
451+
),
452+
voltage_integral=voltage_integral.updated_copy(center=(0, 0, waveport_z / 2)),
453+
)
454+
455+
# define fimulation
456+
sim = td.Simulation(
457+
size=(50 * mil, h + 2 * t, 1.05 * L),
458+
center=(0, 0, 0),
459+
grid_spec=grid_spec,
460+
boundary_spec=boundary_spec,
461+
structures=[str_sub, str_signal_strips, str_gnd_top, str_gnd_bot],
462+
monitors=[],
463+
run_time=2e-9, # simulation run time in seconds
464+
shutoff=1e-7, # lower shutoff threshold for more accurate low frequency
465+
plot_length_units="mm",
466+
symmetry=(-1, 0, 0), # odd symmetry in x-direction
467+
)
468+
469+
# set up component modeler
470+
tcm = TerminalComponentModeler(
471+
simulation=sim, # simulation, previously defined
472+
ports=[WP1, WP2], # wave ports, previously defined
473+
freqs=freqs, # S-parameter frequency points
474+
)
475+
476+
return tcm

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
from tidy3d.plugins.smatrix.utils import s_to_z, validate_square_matrix
3030

3131
from ...utils import run_emulated
32-
from .terminal_component_modeler_def import make_coaxial_component_modeler, make_component_modeler
32+
from .terminal_component_modeler_def import (
33+
make_coaxial_component_modeler,
34+
make_component_modeler,
35+
make_differential_stripline_modeler,
36+
)
3337

3438
mm = 1e3
3539

@@ -1595,3 +1599,182 @@ def test_S_parameter_deembedding(monkeypatch, tmp_path):
15951599
S_dmb_shortcut = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)
15961600
assert not np.allclose(S_dmb.data.values, s_matrix_LP.data.values)
15971601
assert np.allclose(S_dmb_shortcut.data.values, S_dmb.data.values)
1602+
1603+
1604+
def test_wave_port_extrusion_coaxial():
1605+
"""Test extrusion of structures wave port absorber."""
1606+
1607+
# define a terminal component modeler
1608+
tcm = make_coaxial_component_modeler(
1609+
length=100000,
1610+
port_types=(WavePort, WavePort),
1611+
)
1612+
1613+
# update ports and set flag to extrude structures
1614+
ports = tcm.ports
1615+
port_1 = ports[0]
1616+
port_2 = ports[1]
1617+
port_1 = port_1.updated_copy(center=(0, 0, -50000), extrude_structures=True)
1618+
1619+
# test that structure extrusion requires an internal absorber (should raise ValidationError)
1620+
with pytest.raises(pd.ValidationError):
1621+
_ = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True, absorber=False)
1622+
1623+
# define a valid waveport
1624+
port_2 = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True)
1625+
1626+
# update component modeler
1627+
tcm = tcm.updated_copy(ports=[port_1, port_2])
1628+
1629+
# generate simulations from component modeler
1630+
sim = tcm.base_sim
1631+
1632+
# get injection axis that would be used to extrude structure
1633+
inj_axis = sim.internal_absorbers[0].size.index(0.0)
1634+
1635+
# get grid boundaries
1636+
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
1637+
1638+
# get size of structures along injection axis directions
1639+
str_bnds = [
1640+
np.min(sim.structures[-4].geometry.geometries[0].slab_bounds),
1641+
np.max(sim.structures[-2].geometry.geometries[0].slab_bounds),
1642+
]
1643+
1644+
pec_bnds = []
1645+
1646+
# infer placement of PEC plates beyond internal absorber
1647+
for absorber in sim.internal_absorbers:
1648+
absorber_cntr = absorber.center[inj_axis]
1649+
right_ind = np.searchsorted(bnd_coords, absorber_cntr, side="right")
1650+
left_ind = np.searchsorted(bnd_coords, absorber_cntr, side="left") - 1
1651+
pec_bnds.append(bnd_coords[right_ind + 1])
1652+
pec_bnds.append(bnd_coords[left_ind - 1])
1653+
1654+
# get range of coordinates along injection axis for PEC plates
1655+
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
1656+
1657+
# ensure that structures were extruded up to PEC plates
1658+
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))
1659+
1660+
# generate a new TCM simulation to test edge case when wave port plane does not intersect any structures
1661+
tcm = make_coaxial_component_modeler(
1662+
length=100000, port_types=(WavePort, WavePort), use_current=False
1663+
)
1664+
1665+
# update ports and set flag to extrude structures
1666+
ports = tcm.ports
1667+
port_1 = ports[0]
1668+
port_2 = ports[1]
1669+
port_1 = port_1.updated_copy(center=(0, 0, -50000), extrude_structures=True)
1670+
port_2 = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True)
1671+
1672+
# move wave port plane so that is does not intersect any structures
1673+
port_1_center_new = (638.4, 0.0, -51000)
1674+
1675+
# update voltage integral
1676+
voltage_int = port_1.voltage_integral.updated_copy(center=port_1_center_new)
1677+
# update WavePort
1678+
port_1 = port_1.updated_copy(center=port_1_center_new, voltage_integral=voltage_int)
1679+
1680+
# update component modeler
1681+
tcm = tcm.updated_copy(ports=[port_1, port_2])
1682+
1683+
# make sure that
1684+
with pytest.raises(SetupError):
1685+
sim = tcm.base_sim
1686+
1687+
1688+
def test_wave_port_extrusion_differential_stripline():
1689+
"""Test extrusion of structures wave port absorber for differential stripline."""
1690+
1691+
tcm = make_differential_stripline_modeler()
1692+
1693+
# update ports and set flag to extrude structures
1694+
ports = tcm.ports
1695+
port_1 = ports[0]
1696+
port_2 = ports[1]
1697+
port_1 = port_1.updated_copy(extrude_structures=True)
1698+
1699+
# test that structure extrusion requires an internal absorber (should raise ValidationError)
1700+
with pytest.raises(pd.ValidationError):
1701+
_ = port_2.updated_copy(extrude_structures=True, absorber=False)
1702+
1703+
# define a valid waveport
1704+
port_2 = port_2.updated_copy(extrude_structures=True)
1705+
1706+
# update component modeler
1707+
tcm = tcm.updated_copy(ports=[port_1, port_2])
1708+
1709+
# generate simulations from component modeler
1710+
sim = tcm.base_sim
1711+
1712+
# get injection axis that would be used to extrude structure
1713+
inj_axis = sim.internal_absorbers[0].size.index(0.0)
1714+
1715+
# get grid boundaries
1716+
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
1717+
1718+
# get size of structures along injection axis directions
1719+
str_bnds = [
1720+
np.min(sim.structures[-6].geometry.geometries[0].slab_bounds),
1721+
np.max(sim.structures[-1].geometry.geometries[0].slab_bounds),
1722+
]
1723+
1724+
pec_bnds = []
1725+
1726+
# infer placement of PEC plates beyond internal absorber
1727+
for absorber in sim._shifted_internal_absorbers:
1728+
# get the PEC box with its face surfaces
1729+
(box, inj_axis, direction) = sim._pec_frame_box(absorber)
1730+
surfaces = box.surfaces(box.size, box.center)
1731+
1732+
# get extrusion coordinates and a cutting plane for inference of intersecting structures.
1733+
sign = 1 if direction == "+" else -1
1734+
cutting_plane = surfaces[2 * inj_axis + (1 if direction == "+" else 0)]
1735+
1736+
# get extrusion extent along injection axis
1737+
pec_bnds.append(cutting_plane.center[inj_axis])
1738+
1739+
# get range of coordinates along injection axis for PEC plates
1740+
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
1741+
1742+
# ensure that structures were extruded up to PEC plates
1743+
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))
1744+
1745+
# test scenario when wave port extrusion is requested, but port plane does not intersect any structures
1746+
mil = 25.4
1747+
port_1_center_new = (0, 0, -2010 * mil)
1748+
1749+
# re-assemble a new component modeler
1750+
tcm = make_differential_stripline_modeler()
1751+
1752+
# update ports and set flag to extrude structures
1753+
ports = tcm.ports
1754+
port_1 = ports[0]
1755+
port_2 = ports[1]
1756+
port_1 = port_1.updated_copy(extrude_structures=True)
1757+
port_2 = port_2.updated_copy(extrude_structures=True)
1758+
1759+
# update current and voltage integrals
1760+
current_int = port_1.current_integral.updated_copy(center=port_1_center_new)
1761+
voltage_int = port_1.voltage_integral.updated_copy(center=port_1_center_new)
1762+
1763+
# update WavePort
1764+
port_1 = port_1.updated_copy(
1765+
center=port_1_center_new, current_integral=current_int, voltage_integral=voltage_int
1766+
)
1767+
1768+
# update component modeler
1769+
tcm = tcm.updated_copy(ports=[port_1, port_2])
1770+
1771+
# make sure that the error is triggered
1772+
with pytest.raises(SetupError):
1773+
sim = tcm.base_sim
1774+
1775+
# update component modeler
1776+
tcm = tcm.updated_copy(ports=[port_2, port_1])
1777+
1778+
# make sure that the error is triggered even when ports are reshuffled
1779+
with pytest.raises(SetupError):
1780+
sim = tcm.base_sim

tidy3d/components/geometry/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,10 +735,11 @@ def _shift_value_signed(
735735
f"{name} position '{obj_position}' is outside of simulation bounds '({grid_boundaries[0]}, {grid_boundaries[-1]})' along dimension '{'xyz'[normal_axis]}'."
736736
)
737737
obj_index = obj_pos_gt_grid_bounds[-1]
738-
739738
# shift the obj to the left
740739
signed_shift = shift if direction == "+" else -shift
741740
if signed_shift < 0:
741+
if np.isclose(obj_position, grid_boundaries[obj_index + 1]):
742+
obj_index += 1
742743
shifted_index = obj_index + signed_shift
743744
if shifted_index < 0 or grid_centers[shifted_index] <= bounds[0][normal_axis]:
744745
raise SetupError(

0 commit comments

Comments
 (0)