Skip to content

Commit 9f9b5ba

Browse files
--amend
1 parent 71d2d07 commit 9f9b5ba

File tree

8 files changed

+450
-234
lines changed

8 files changed

+450
-234
lines changed

CHANGELOG.md

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

1111
### Added
12+
- 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.
1213
- Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging.
1314
- 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.
1415
- Added `LowFrequencySmoothingSpec` and `ModelerLowFrequencySmoothingSpec` for automatic smoothing of mode monitor data at low frequencies where DFT sampling is insufficient.
@@ -51,8 +52,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5152
## [v2.10.0rc1] - 2025-09-11
5253

5354
### Added
54-
- Add automatic structure extrusion for waveports defined on boundaries, controlled by the `extrude_structures` field in `WavePort`.
55-
- The extrusion method, implemented in `TerminalComponentModeler`, ensures that mode sources, absorbers, and PEC frames are fully contained within the extruded structures; extrusion occurs only when `extrude_structures` is set to `True`.
5655
- Added rectangular and radial taper support to `RectangularAntennaArrayCalculator` for phased array amplitude weighting; refactored array factor calculation for improved clarity and performance.
5756
- Selective simulation capabilities to `TerminalComponentModeler` via `run_only` and `element_mappings` fields, allowing users to run fewer simulations and extract only needed scattering matrix elements.
5857
- Added KLayout plugin, with DRC functionality for running design rule checks in `plugins.klayout.drc`. Supports running DRC on GDS files as well as `Geometry`, `Structure`, and `Simulation` objects.

schemas/TerminalComponentModeler.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16808,6 +16808,10 @@
1680816808
],
1680916809
"type": "string"
1681016810
},
16811+
"extrude_structures": {
16812+
"default": false,
16813+
"type": "boolean"
16814+
},
1681116815
"frame": {
1681216816
"allOf": [
1681316817
{

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,147 @@ def make_port(center, direction, type, name) -> Union[CoaxialLumpedPort, WavePor
332332
)
333333

334334
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(
372+
geometry=td.Box(center=(0, 0, 0), size=(len_inf, h, len_inf)), medium=med_sub
373+
)
374+
375+
# disjoint signal strips
376+
str_signal_strips = td.Structure(
377+
geometry=td.GeometryGroup(geometries=[left_strip_geometry, right_strip_geometry]),
378+
medium=med_metal,
379+
)
380+
381+
# Top ground plane
382+
str_gnd_top = td.Structure(
383+
geometry=td.Box(center=(0, h / 2 + t / 2, 0), size=(len_inf, t, L)), medium=med_metal
384+
)
385+
386+
# Bottom ground plane
387+
str_gnd_bot = td.Structure(
388+
geometry=td.Box(center=(0, -h / 2 - t / 2, 0), size=(len_inf, t, L)), medium=med_metal
389+
)
390+
391+
# Create a LayerRefinementSpec from signal trace structures
392+
lr_spec = td.LayerRefinementSpec.from_structures(
393+
structures=[str_signal_strips],
394+
axis=1, # Layer normal is in y-direction
395+
min_steps_along_axis=10, # Min 10 grid cells along normal direction
396+
refinement_inside_sim_only=False, # Metal structures extend outside sim domain. Set 'False' to snap to corners outside sim.
397+
bounds_snapping="bounds", # snap grid to metal boundaries
398+
corner_refinement=td.GridRefinement(
399+
dl=t / 10, num_cells=2
400+
), # snap to corners and apply added refinement
401+
)
402+
403+
# Layer refinement for top and bottom ground planes
404+
lr_spec2 = lr_spec.updated_copy(center=(0, h / 2 + t / 2, cent_z), size=(len_inf, t, len_z))
405+
lr_spec3 = lr_spec.updated_copy(center=(0, -h / 2 - t / 2, cent_z), size=(len_inf, t, len_z))
406+
407+
# Define overall grid specification
408+
grid_spec = td.GridSpec.auto(
409+
wavelength=td.C_0 / f_max,
410+
min_steps_per_wvl=30,
411+
layer_refinement_specs=[lr_spec, lr_spec2, lr_spec3],
412+
)
413+
414+
# boundary specs
415+
boundary_spec = td.BoundarySpec(
416+
x=td.Boundary.pml(),
417+
y=td.Boundary.pec(),
418+
z=td.Boundary.pml(),
419+
)
420+
421+
# Define port specification
422+
wave_port_mode_spec = td.ModeSpec(num_modes=1, target_neff=np.sqrt(eps))
423+
424+
# Define current and voltage integrals
425+
current_integral = microwave.CurrentIntegralAxisAligned(
426+
center=((se + w) / 2, 0, -waveport_z / 2), size=(2 * w, 3 * t, 0), sign="+"
427+
)
428+
voltage_integral = microwave.VoltageIntegralAxisAligned(
429+
center=(0, 0, -waveport_z / 2),
430+
size=(se, 0, 0),
431+
extrapolate_to_endpoints=True,
432+
snap_path_to_grid=True,
433+
sign="+",
434+
)
435+
436+
# Define wave ports
437+
WP1 = WavePort(
438+
center=(0, 0, -waveport_z / 2),
439+
size=(len_inf, len_inf, 0),
440+
mode_spec=wave_port_mode_spec,
441+
direction="+",
442+
name="WP1",
443+
mode_index=0,
444+
current_integral=current_integral,
445+
voltage_integral=voltage_integral,
446+
)
447+
WP2 = WP1.updated_copy(
448+
name="WP2",
449+
center=(0, 0, waveport_z / 2),
450+
direction="-",
451+
current_integral=current_integral.updated_copy(
452+
center=((se + w) / 2, 0, waveport_z / 2), sign="-"
453+
),
454+
voltage_integral=voltage_integral.updated_copy(center=(0, 0, waveport_z / 2)),
455+
)
456+
457+
# define fimulation
458+
sim = td.Simulation(
459+
size=(50 * mil, h + 2 * t, 1.05 * L),
460+
center=(0, 0, 0),
461+
grid_spec=grid_spec,
462+
boundary_spec=boundary_spec,
463+
structures=[str_sub, str_signal_strips, str_gnd_top, str_gnd_bot],
464+
monitors=[],
465+
run_time=2e-9, # simulation run time in seconds
466+
shutoff=1e-7, # lower shutoff threshold for more accurate low frequency
467+
plot_length_units="mm",
468+
symmetry=(-1, 0, 0), # odd symmetry in x-direction
469+
)
470+
471+
# set up component modeler
472+
tcm = TerminalComponentModeler(
473+
simulation=sim, # simulation, previously defined
474+
ports=[WP1, WP2], # wave ports, previously defined
475+
freqs=freqs, # S-parameter frequency points
476+
)
477+
478+
return tcm

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from .terminal_component_modeler_def import (
3737
make_coaxial_component_modeler,
3838
make_component_modeler,
39+
make_differential_stripline_modeler,
3940
)
4041

4142
mm = 1e3
@@ -1428,6 +1429,7 @@ def test_wave_port_to_absorber(tmp_path):
14281429
absorber = sim.internal_absorbers[0]
14291430
assert absorber.boundary_spec == custom_boundary_spec
14301431

1432+
14311433
def test_low_freq_smoothing_spec_initialization_default_values():
14321434
"""Test that LowFrequencySmoothingSpec initializes with correct default values."""
14331435
from tidy3d.plugins.smatrix.component_modelers.terminal import ModelerLowFrequencySmoothingSpec
@@ -1539,6 +1541,7 @@ def test_low_freq_smoothing_spec_sim_dict():
15391541
for sim in modeler.sim_dict.values():
15401542
assert sim.low_freq_smoothing is None
15411543

1544+
15421545
def test_wave_port_extrusion_coaxial():
15431546
"""Test extrusion of structures wave port absorber."""
15441547

@@ -1553,40 +1556,102 @@ def test_wave_port_extrusion_coaxial():
15531556
port_1 = ports[0]
15541557
port_2 = ports[1]
15551558
port_1 = port_1.updated_copy(center=(0, 0, -50000), extrude_structures=True)
1559+
1560+
# test that structure extrusion requires an internal absorber (should raise ValidationError)
1561+
with pytest.raises(pd.ValidationError):
1562+
_ = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True, absorber=False)
1563+
1564+
# define a valid waveport
15561565
port_2 = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True)
15571566

15581567
# update component modeler
15591568
tcm = tcm.updated_copy(ports=[port_1, port_2])
15601569

15611570
# generate simulations from component modeler
1562-
sims = list(tcm.sim_dict.values())
1571+
sim = tcm.base_sim
15631572

1564-
# loop over simulations
1565-
for sim in sims:
1566-
# get injection axis that would be used to extrude structure
1567-
inj_axis = sim.sources[0].injection_axis
1573+
# get injection axis that would be used to extrude structure
1574+
inj_axis = sim.internal_absorbers[0].size.index(0.0)
15681575

1569-
# get grid boundaries
1570-
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
1576+
# get grid boundaries
1577+
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
15711578

1572-
# get size of structures along injection axis directions
1573-
str_bnds = [
1574-
np.min(sim.structures[0].geometry.geometries[0].slab_bounds),
1575-
np.max(sim.structures[2].geometry.geometries[0].slab_bounds),
1576-
]
1579+
# get size of structures along injection axis directions
1580+
str_bnds = [
1581+
np.min(sim.structures[-4].geometry.geometries[0].slab_bounds),
1582+
np.max(sim.structures[-2].geometry.geometries[0].slab_bounds),
1583+
]
1584+
1585+
pec_bnds = []
1586+
1587+
# infer placement of PEC plates beyond internal absorber
1588+
for absorber in sim.internal_absorbers:
1589+
absorber_cntr = absorber.center[inj_axis]
1590+
right_ind = np.searchsorted(bnd_coords, absorber_cntr, side="right")
1591+
left_ind = np.searchsorted(bnd_coords, absorber_cntr, side="left") - 1
1592+
pec_bnds.append(bnd_coords[right_ind + 1])
1593+
pec_bnds.append(bnd_coords[left_ind - 1])
1594+
1595+
# get range of coordinates along injection axis for PEC plates
1596+
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
1597+
1598+
# ensure that structures were extruded up to PEC plates
1599+
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))
1600+
1601+
1602+
def test_wave_port_extrusion_differential_stripline():
1603+
"""Test extrusion of structures wave port absorber for differential stripline."""
1604+
1605+
tcm = make_differential_stripline_modeler()
1606+
1607+
# update ports and set flag to extrude structures
1608+
ports = tcm.ports
1609+
port_1 = ports[0]
1610+
port_2 = ports[1]
1611+
port_1 = port_1.updated_copy(extrude_structures=True)
1612+
1613+
# test that structure extrusion requires an internal absorber (should raise ValidationError)
1614+
with pytest.raises(pd.ValidationError):
1615+
_ = port_2.updated_copy(extrude_structures=True, absorber=False)
1616+
1617+
# define a valid waveport
1618+
port_2 = port_2.updated_copy(extrude_structures=True)
1619+
1620+
# update component modeler
1621+
tcm = tcm.updated_copy(ports=[port_1, port_2])
1622+
1623+
# generate simulations from component modeler
1624+
sim = tcm.base_sim
1625+
1626+
# get injection axis that would be used to extrude structure
1627+
inj_axis = sim.internal_absorbers[0].size.index(0.0)
1628+
1629+
# get grid boundaries
1630+
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
1631+
1632+
# get size of structures along injection axis directions
1633+
str_bnds = [
1634+
np.min(sim.structures[-6].geometry.geometries[0].slab_bounds),
1635+
np.max(sim.structures[-1].geometry.geometries[0].slab_bounds),
1636+
]
1637+
1638+
pec_bnds = []
1639+
1640+
# infer placement of PEC plates beyond internal absorber
1641+
for absorber in sim._shifted_internal_absorbers:
1642+
# get the PEC box with its face surfaces
1643+
(box, inj_axis, direction) = sim._pec_frame_box(absorber)
1644+
surfaces = box.surfaces(box.size, box.center)
15771645

1578-
pec_bnds = []
1646+
# get extrusion coordinates and a cutting plane for inference of intersecting structures.
1647+
sign = 1 if direction == "+" else -1
1648+
cutting_plane = surfaces[2 * inj_axis + (1 if direction == "+" else 0)]
15791649

1580-
# infer placement of PEC plates beyond internal absorber
1581-
for absorber in sim.internal_absorbers:
1582-
absorber_cntr = absorber.center[inj_axis]
1583-
right_ind = np.searchsorted(bnd_coords, absorber_cntr, side="right")
1584-
left_ind = np.searchsorted(bnd_coords, absorber_cntr, side="left") - 1
1585-
pec_bnds.append(bnd_coords[right_ind + 1])
1586-
pec_bnds.append(bnd_coords[left_ind - 1])
1650+
# get extrusion extent along injection axis
1651+
pec_bnds.append(cutting_plane.center[inj_axis])
15871652

1588-
# get range of coordinates along injection axis for PEC plates
1589-
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
1653+
# get range of coordinates along injection axis for PEC plates
1654+
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
15901655

1591-
# ensure that structures were extruded up to PEC plates
1592-
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))
1656+
# ensure that structures were extruded up to PEC plates
1657+
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))

tidy3d/components/geometry/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,10 +541,11 @@ def _shift_value_signed(
541541
f"{name} position '{obj_position}' is outside of simulation bounds '({grid_boundaries[0]}, {grid_boundaries[-1]})' along dimension '{'xyz'[normal_axis]}'."
542542
)
543543
obj_index = obj_pos_gt_grid_bounds[-1]
544-
545544
# shift the obj to the left
546545
signed_shift = shift if direction == "+" else -shift
547546
if signed_shift < 0:
547+
if np.isclose(obj_position, grid_boundaries[obj_index + 1]):
548+
obj_index += 1
548549
shifted_index = obj_index + signed_shift
549550
if shifted_index < 0 or grid_centers[shifted_index] <= bounds[0][normal_axis]:
550551
raise SetupError(

0 commit comments

Comments
 (0)