Skip to content

Commit 156ae24

Browse files
--amend
1 parent 1bcd02e commit 156ae24

File tree

8 files changed

+372
-259
lines changed

8 files changed

+372
-259
lines changed

CHANGELOG.md

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

1010
### Added
11-
- Add automatic structure extrusion for waveports defined on boundaries, controlled by the `extrude_structures` field in `WavePort`.
12-
- 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`.
11+
- 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.
1312
- Added rectangular and radial taper support to `RectangularAntennaArrayCalculator` for phased array amplitude weighting; refactored array factor calculation for improved clarity and performance.
1413
- 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.
1514
- 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
@@ -16129,6 +16129,10 @@
1612916129
],
1613016130
"type": "string"
1613116131
},
16132+
"extrude_structures": {
16133+
"default": false,
16134+
"type": "boolean"
16135+
},
1613216136
"frame": {
1613316137
"allOf": [
1613416138
{

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: 87 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
@@ -1370,40 +1371,102 @@ def test_wave_port_extrusion_coaxial():
13701371
port_1 = ports[0]
13711372
port_2 = ports[1]
13721373
port_1 = port_1.updated_copy(center=(0, 0, -50000), extrude_structures=True)
1374+
1375+
# test that structure extrusion requires an internal absorber (should raise ValidationError)
1376+
with pytest.raises(pd.ValidationError):
1377+
_ = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True, absorber=False)
1378+
1379+
# define a valid waveport
13731380
port_2 = port_2.updated_copy(center=(0, 0, 50000), extrude_structures=True)
13741381

13751382
# update component modeler
13761383
tcm = tcm.updated_copy(ports=[port_1, port_2])
13771384

13781385
# generate simulations from component modeler
1379-
sims = list(tcm.sim_dict.values())
1386+
sim = tcm.base_sim
13801387

1381-
# loop over simulations
1382-
for sim in sims:
1383-
# get injection axis that would be used to extrude structure
1384-
inj_axis = sim.sources[0].injection_axis
1388+
# get injection axis that would be used to extrude structure
1389+
inj_axis = sim.internal_absorbers[0].size.index(0.0)
13851390

1386-
# get grid boundaries
1387-
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
1391+
# get grid boundaries
1392+
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
13881393

1389-
# get size of structures along injection axis directions
1390-
str_bnds = [
1391-
np.min(sim.structures[0].geometry.geometries[0].slab_bounds),
1392-
np.max(sim.structures[2].geometry.geometries[0].slab_bounds),
1393-
]
1394+
# get size of structures along injection axis directions
1395+
str_bnds = [
1396+
np.min(sim.structures[0].geometry.geometries[1].geometries[0].slab_bounds),
1397+
np.max(sim.structures[0].geometry.geometries[0].slab_bounds),
1398+
]
1399+
1400+
pec_bnds = []
1401+
1402+
# infer placement of PEC plates beyond internal absorber
1403+
for absorber in sim.internal_absorbers:
1404+
absorber_cntr = absorber.center[inj_axis]
1405+
right_ind = np.searchsorted(bnd_coords, absorber_cntr, side="right")
1406+
left_ind = np.searchsorted(bnd_coords, absorber_cntr, side="left") - 1
1407+
pec_bnds.append(bnd_coords[right_ind + 1])
1408+
pec_bnds.append(bnd_coords[left_ind - 1])
1409+
1410+
# get range of coordinates along injection axis for PEC plates
1411+
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
1412+
1413+
# ensure that structures were extruded up to PEC plates
1414+
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))
1415+
1416+
1417+
def test_wave_port_extrusion_differential_stripline():
1418+
"""Test extrusion of structures wave port absorber for differential stripline."""
1419+
1420+
tcm = make_differential_stripline_modeler()
1421+
1422+
# update ports and set flag to extrude structures
1423+
ports = tcm.ports
1424+
port_1 = ports[0]
1425+
port_2 = ports[1]
1426+
port_1 = port_1.updated_copy(extrude_structures=True)
1427+
1428+
# test that structure extrusion requires an internal absorber (should raise ValidationError)
1429+
with pytest.raises(pd.ValidationError):
1430+
_ = port_2.updated_copy(extrude_structures=True, absorber=False)
1431+
1432+
# define a valid waveport
1433+
port_2 = port_2.updated_copy(extrude_structures=True)
1434+
1435+
# update component modeler
1436+
tcm = tcm.updated_copy(ports=[port_1, port_2])
1437+
1438+
# generate simulations from component modeler
1439+
sim = tcm.base_sim
1440+
1441+
# get injection axis that would be used to extrude structure
1442+
inj_axis = sim.internal_absorbers[0].size.index(0.0)
1443+
1444+
# get grid boundaries
1445+
bnd_coords = sim.grid.boundaries.to_list[inj_axis]
1446+
1447+
# get size of structures along injection axis directions
1448+
str_bnds = [
1449+
np.min(sim.structures[0].geometry.geometries[1].geometries[0].slab_bounds),
1450+
np.max(sim.structures[0].geometry.geometries[0].slab_bounds),
1451+
]
1452+
1453+
pec_bnds = []
1454+
1455+
# infer placement of PEC plates beyond internal absorber
1456+
for absorber in sim._shifted_internal_absorbers:
1457+
# get the PEC box with its face surfaces
1458+
(box, inj_axis, direction) = sim._pec_frame_box(absorber)
1459+
surfaces = box.surfaces(box.size, box.center)
13941460

1395-
pec_bnds = []
1461+
# get extrusion coordinates and a cutting plane for inference of intersecting structures.
1462+
sign = 1 if direction == "+" else -1
1463+
cutting_plane = surfaces[2 * inj_axis + (1 if direction == "+" else 0)]
13961464

1397-
# infer placement of PEC plates beyond internal absorber
1398-
for absorber in sim.internal_absorbers:
1399-
absorber_cntr = absorber.center[inj_axis]
1400-
right_ind = np.searchsorted(bnd_coords, absorber_cntr, side="right")
1401-
left_ind = np.searchsorted(bnd_coords, absorber_cntr, side="left") - 1
1402-
pec_bnds.append(bnd_coords[right_ind + 1])
1403-
pec_bnds.append(bnd_coords[left_ind - 1])
1465+
# get extrusion extent along injection axis
1466+
pec_bnds.append(cutting_plane.center[inj_axis])
14041467

1405-
# get range of coordinates along injection axis for PEC plates
1406-
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
1468+
# get range of coordinates along injection axis for PEC plates
1469+
pec_bnds = [np.min(pec_bnds), np.max(pec_bnds)]
14071470

1408-
# ensure that structures were extruded up to PEC plates
1409-
assert all(np.isclose(str_bnd, pec_bnd) for str_bnd, pec_bnd in zip(str_bnds, pec_bnds))
1471+
# ensure that structures were extruded up to PEC plates
1472+
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
@@ -522,10 +522,11 @@ def _shift_value_signed(
522522
f"{name} position '{obj_position}' is outside of simulation bounds '({grid_boundaries[0]}, {grid_boundaries[-1]})' along dimension '{'xyz'[normal_axis]}'."
523523
)
524524
obj_index = obj_pos_gt_grid_bounds[-1]
525-
526525
# shift the obj to the left
527526
signed_shift = shift if direction == "+" else -shift
528527
if signed_shift < 0:
528+
if np.isclose(obj_position, grid_boundaries[obj_index + 1]):
529+
obj_index += 1
529530
shifted_index = obj_index + signed_shift
530531
if shifted_index < 0 or grid_centers[shifted_index] <= bounds[0][normal_axis]:
531532
raise SetupError(

tidy3d/components/simulation.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5638,33 +5638,9 @@ def _make_pec_frame(self, obj: Union[ModeSource, InternalAbsorber]) -> Structure
56385638
the frame is added around the injection plane. For internal absorbers, a backing pec
56395639
plate is also added on the non-absorbing side.
56405640
"""
5641-
span_inds = np.array(self.grid.discretize_inds(obj))
5642-
5643-
coords = self.grid.boundaries.to_list
5644-
direction = obj.direction
5645-
if isinstance(obj, ModeSource):
5646-
axis = obj.injection_axis
5647-
length = obj.frame.length
5648-
else:
5649-
axis = obj.size.index(0.0)
5650-
length = 1
5651-
5652-
if direction == "+":
5653-
span_inds[axis][1] += length - 1
5654-
span_inds[axis][0] -= 1
5655-
else:
5656-
span_inds[axis][1] += 1
5657-
span_inds[axis][0] -= length - 1
5658-
5659-
box_bounds = [
5660-
[
5661-
c[beg],
5662-
c[end],
5663-
]
5664-
for c, (beg, end) in zip(coords, span_inds)
5665-
]
56665641

5667-
box = Box.from_bounds(*np.transpose(box_bounds))
5642+
# get pec frame bounding box, object's axis and direction
5643+
(box, axis, direction) = self._pec_frame_box(obj)
56685644

56695645
surfaces = Box.surfaces(box.size, box.center)
56705646
if isinstance(obj, ModeSource):
@@ -5723,3 +5699,30 @@ def _validate_finalized(self):
57235699
"Simulation fails after requested mode source PEC frames are added. "
57245700
"Please inspect '._finalized'."
57255701
)
5702+
5703+
def _pec_frame_box(self, obj: Union[ModeSource, InternalAbsorber]) -> tuple[Box, int, str]:
5704+
"""Return pec bounding box, frame axis and object's direction"""
5705+
5706+
span_inds = np.array(self.grid.discretize_inds(obj))
5707+
5708+
coords = self.grid.boundaries.to_list
5709+
direction = obj.direction
5710+
if isinstance(obj, ModeSource):
5711+
axis = obj.injection_axis
5712+
length = obj.frame.length
5713+
if direction == "+":
5714+
span_inds[axis][1] += length - 1
5715+
else:
5716+
span_inds[axis][0] -= length - 1
5717+
else:
5718+
axis = obj.size.index(0.0)
5719+
5720+
box_bounds = [
5721+
[
5722+
c[beg],
5723+
c[end],
5724+
]
5725+
for c, (beg, end) in zip(coords, span_inds)
5726+
]
5727+
5728+
return (Box.from_bounds(*np.transpose(box_bounds)), axis, direction)

0 commit comments

Comments
 (0)