Skip to content

Commit d1ba328

Browse files
pec frame
1 parent 6f91441 commit d1ba328

File tree

5 files changed

+143
-25
lines changed

5 files changed

+143
-25
lines changed

tidy3d/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ def set_logging_level(level: str) -> None:
421421
"PMC",
422422
"PML",
423423
"TFSF",
424+
"ABCBoundary",
424425
"Absorber",
425426
"AbsorberParams",
426427
"AbstractFieldProjectionData",
@@ -592,6 +593,7 @@ def set_logging_level(level: str) -> None:
592593
"Medium2D",
593594
"MediumMediumInterface",
594595
"MeshOverrideStructure",
596+
"ModeABCBoundary",
595597
"ModeAmpsDataArray",
596598
"ModeData",
597599
"ModeIndexDataArray",
@@ -715,6 +717,4 @@ def set_logging_level(level: str) -> None:
715717
"set_logging_console",
716718
"set_logging_file",
717719
"wavelengths",
718-
"ABCBoundary",
719-
"ModeABCBoundary",
720720
]

tidy3d/components/boundary.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from abc import ABC
6-
from typing import Union
6+
from typing import Optional, Union
77

88
import numpy as np
99
import pydantic.v1 as pd
@@ -46,6 +46,8 @@ def _warn_num_layers(cls, val):
4646

4747
return _warn_num_layers
4848

49+
DEFAULT_MODE_SPEC_MODE_ABC = ModeSpec()
50+
4951

5052
class BoundaryEdge(ABC, Tidy3dBaseModel):
5153
"""Electromagnetic boundary condition at a domain edge."""
@@ -119,7 +121,7 @@ class ModeABCBoundary(AbstractABCBoundary):
119121
"""One-way wave equation absorbing boundary conditions for absorbing a waveguide mode."""
120122

121123
mode_spec: ModeSpec = pd.Field(
122-
ModeSpec(),
124+
DEFAULT_MODE_SPEC_MODE_ABC,
123125
title="Mode Specification",
124126
description="Parameters that determine the modes computed by the mode solver.",
125127
)
@@ -894,7 +896,7 @@ def abc(
894896
def mode_abc(
895897
cls,
896898
plane: Box,
897-
mode_spec: ModeSpec = ModeSpec(),
899+
mode_spec: ModeSpec = DEFAULT_MODE_SPEC_MODE_ABC,
898900
mode_index: pd.NonNegativeInt = 0,
899901
frequency: Optional[pd.PositiveFloat] = None,
900902
):

tidy3d/components/simulation.py

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
from .data.unstructured.tetrahedral import TetrahedralGridDataset
5050
from .data.unstructured.triangular import TriangularGridDataset
5151
from .data.utils import CustomSpatialDataType
52-
from .geometry.base import Box, Geometry
52+
from .geometry.base import Box, ClipOperation, Geometry, GeometryGroup
5353
from .geometry.mesh import TriangleMesh
5454
from .geometry.utils import flatten_groups, traverse_geometries
5555
from .geometry.utils_2d import get_bounds, get_thickened_geom, snap_coordinate_to_grid, subdivide
@@ -67,6 +67,7 @@
6767
Medium2D,
6868
MediumType,
6969
MediumType3D,
70+
PECMedium,
7071
)
7172
from .monitor import (
7273
AbstractFieldProjectionMonitor,
@@ -175,6 +176,10 @@
175176
# RF frequency warning
176177
RF_FREQ_WARNING = 300e9
177178

179+
# length and thickness of optional PEC frames around mode sources (in cells)
180+
MODE_PEC_FRAME_LENGTH = 2
181+
MODE_PEC_FRAME_THICKNESS = 1e-3
182+
178183

179184
def validate_boundaries_for_zero_dims(warn_on_change: bool = True):
180185
"""Error if absorbing boundaries, bloch boundaries, unmatching pec/pmc, or symmetry is used along a zero dimension."""
@@ -849,7 +854,7 @@ def pml_thicknesses(self) -> list[tuple[float, float]]:
849854
850855
Returns
851856
-------
852-
List[Tuple[float, float]]
857+
list[Tuple[float, float]]
853858
List containing the absorber thickness (micron) in - and + boundaries.
854859
"""
855860
num_layers = self.num_pml_layers
@@ -867,7 +872,7 @@ def internal_override_structures(self) -> list[MeshOverrideStructure]:
867872
868873
Returns
869874
-------
870-
List[MeshOverrideSructure]
875+
list[MeshOverrideSructure]
871876
List of override structures.
872877
"""
873878
wavelength = self.grid_spec.get_wavelength(self.sources)
@@ -884,7 +889,7 @@ def internal_snapping_points(self) -> list[CoordinateOptional]:
884889
885890
Returns
886891
-------
887-
List[CoordinateOptional]
892+
list[CoordinateOptional]
888893
List of snapping points coordinates.
889894
"""
890895
return self.grid_spec.internal_snapping_points(
@@ -1223,7 +1228,7 @@ def _grid_and_snapping_lines(self) -> tuple[Grid, list[CoordinateOptional]]:
12231228
12241229
Returns
12251230
-------
1226-
Tuple[:class:`.Grid`, List[CoordinateOptional]]
1231+
Tuple[:class:`.Grid`, list[CoordinateOptional]]
12271232
:class:`.Grid` storing the spatial locations relevant to the simulation
12281233
the list of snapping points generated during iterative gap meshing.
12291234
"""
@@ -1281,7 +1286,7 @@ def _gap_meshing_snapping_lines(self) -> list[CoordinateOptional]:
12811286
12821287
Returns
12831288
-------
1284-
List[CoordinateOptional]
1289+
list[CoordinateOptional]
12851290
List of snapping lines resolving thin gaps and strips.
12861291
"""
12871292

@@ -1345,7 +1350,7 @@ def num_pml_layers(self) -> list[tuple[float, float]]:
13451350
13461351
Returns
13471352
-------
1348-
List[Tuple[float, float]]
1353+
list[Tuple[float, float]]
13491354
List containing the number of absorber layers in - and + boundaries.
13501355
"""
13511356
num_layers = [[0, 0], [0, 0], [0, 0]]
@@ -3278,13 +3283,13 @@ def _projection_monitors_homogeneous(cls, val, values):
32783283
@classmethod
32793284
def _get_mediums_on_abc(
32803285
cls, boundary_spec, medium, center, size, structures
3281-
) -> Tuple[
3282-
List[MediumType3D],
3283-
List[MediumType3D],
3284-
List[MediumType3D],
3285-
List[MediumType3D],
3286-
List[MediumType3D],
3287-
List[MediumType3D],
3286+
) -> tuple[
3287+
list[MediumType3D],
3288+
list[MediumType3D],
3289+
list[MediumType3D],
3290+
list[MediumType3D],
3291+
list[MediumType3D],
3292+
list[MediumType3D],
32883293
]:
32893294
"""For each ABC boundary that needs an automatic medium detection (permittivity=None)
32903295
determine mediums it crosses.
@@ -3303,7 +3308,7 @@ def _get_mediums_on_abc(
33033308
center=structure_bg.geometry.center, size=structure_bg.geometry.size
33043309
)
33053310

3306-
total_structures = [structure_bg] + list(structures)
3311+
total_structures = [structure_bg, *list(structures)]
33073312

33083313
mediums = []
33093314
for boundary, surface in zip(np.ravel(boundary_spec.to_list), surfaces):
@@ -4537,7 +4542,7 @@ def mediums(self) -> set[MediumType]:
45374542
45384543
Returns
45394544
-------
4540-
List[:class:`.AbstractMedium`]
4545+
set[:class:`.AbstractMedium`]
45414546
Set of distinct mediums in the simulation.
45424547
"""
45434548
log.warning(
@@ -4599,12 +4604,12 @@ def intersecting_media(
45994604
-------
46004605
test_object : :class:`.Box`
46014606
Object for which intersecting media are to be detected.
4602-
structures : List[:class:`.AbstractMedium`]
4607+
structures : tuple[:class:`.AbstractMedium`]
46034608
List of structures whose media will be tested.
46044609
46054610
Returns
46064611
-------
4607-
List[:class:`.AbstractMedium`]
4612+
tuple[:class:`.AbstractMedium`]
46084613
Set of distinct mediums that intersect with the given planar object.
46094614
"""
46104615

@@ -4626,12 +4631,12 @@ def intersecting_structures(
46264631
-------
46274632
test_object : :class:`.Box`
46284633
Object for which intersecting media are to be detected.
4629-
structures : List[:class:`.AbstractMedium`]
4634+
structures : tuple[:class:`.AbstractMedium`]
46304635
List of structures whose media will be tested.
46314636
46324637
Returns
46334638
-------
4634-
List[:class:`.Structure`]
4639+
tuple[:class:`.Structure`]
46354640
Set of distinct structures that intersect with the given surface, or with the surfaces
46364641
of the given volume.
46374642
"""
@@ -5444,3 +5449,101 @@ def from_scene(cls, scene: Scene, **kwargs) -> Simulation:
54445449
)
54455450

54465451
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()
5452+
5453+
def _make_pec_frame(self, mode_source) -> Structure:
5454+
"""Make a pec frame around a mode source."""
5455+
5456+
coords = self.grid.boundaries.to_list
5457+
axis = mode_source.injection_axis
5458+
direction = mode_source.direction
5459+
length = mode_source.pec_frame
5460+
5461+
span_inds = np.array(self.grid.discretize_inds(mode_source))
5462+
if direction == "+":
5463+
span_inds[axis][1] += length - 1
5464+
else:
5465+
span_inds[axis][0] -= length - 1
5466+
5467+
box_bounds = [
5468+
[
5469+
c[beg],
5470+
c[end],
5471+
]
5472+
for c, (beg, end) in zip(coords, span_inds)
5473+
]
5474+
5475+
prev_cell = span_inds[axis][0] - 1
5476+
if prev_cell >= 0:
5477+
box_bounds[axis][0] = (1 - MODE_PEC_FRAME_THICKNESS) * box_bounds[axis][0] + MODE_PEC_FRAME_THICKNESS * coords[axis][prev_cell]
5478+
5479+
next_cell = span_inds[axis][1] + 1
5480+
if next_cell <= len(coords[axis]) - 1 :
5481+
box_bounds[axis][1] = (1 - MODE_PEC_FRAME_THICKNESS) * box_bounds[axis][1] + MODE_PEC_FRAME_THICKNESS * coords[axis][next_cell]
5482+
5483+
box = Box.from_bounds(*np.transpose(box_bounds))
5484+
5485+
surfaces = Box.surfaces(box.size, box.center)
5486+
del surfaces[2 * axis: 2 * axis + 2]
5487+
5488+
structure = Structure(
5489+
geometry=GeometryGroup(
5490+
geometries=surfaces,
5491+
),
5492+
medium=PECMedium(),
5493+
)
5494+
5495+
5496+
# bounds_outer = [
5497+
# [
5498+
# (1 - MODE_PEC_FRAME_THICKNESS) * c[beg]
5499+
# + MODE_PEC_FRAME_THICKNESS * c[max(0, beg - 1)],
5500+
# (1 - MODE_PEC_FRAME_THICKNESS) * c[end]
5501+
# + MODE_PEC_FRAME_THICKNESS * c[min(len(c) - 1, end + 1)],
5502+
# ]
5503+
# for c, (beg, end) in zip(coords, span_inds)
5504+
# ]
5505+
# bounds_inner = [
5506+
# [
5507+
# (1 - MODE_PEC_FRAME_THICKNESS) * c[beg] + MODE_PEC_FRAME_THICKNESS * c[beg + 1],
5508+
# (1 - MODE_PEC_FRAME_THICKNESS) * c[end] + MODE_PEC_FRAME_THICKNESS * c[end - 1],
5509+
# ]
5510+
# for c, (beg, end) in zip(coords, span_inds)
5511+
# ]
5512+
# bounds_inner[axis] = [-inf, inf]
5513+
# structure = Structure(
5514+
# geometry=ClipOperation(
5515+
# geometry_a=Box.from_bounds(*np.transpose(bounds_outer)),
5516+
# geometry_b=Box.from_bounds(*np.transpose(bounds_inner)),
5517+
# operation="difference",
5518+
# ),
5519+
# medium=PECMedium(),
5520+
# )
5521+
return structure
5522+
5523+
@cached_property
5524+
def with_mode_source_pec_frames(self) -> Simulation:
5525+
"""Return an instance with added pec frames around mode sources."""
5526+
5527+
pec_frames = [
5528+
self._make_pec_frame(src)
5529+
for src in self.sources
5530+
if isinstance(src, ModeSource) and src.pec_frame > 0
5531+
]
5532+
5533+
if len(pec_frames) == 0:
5534+
return self
5535+
5536+
return self.updated_copy(
5537+
grid_spec=GridSpec.from_grid(self.grid), structures=list(self.structures) + pec_frames
5538+
)
5539+
5540+
def _validate_with_mode_source_pec_frames(self):
5541+
"""Validate that after adding pec frames simulation setup is still valid."""
5542+
5543+
try:
5544+
_ = self.with_mode_source_pec_frames
5545+
except Exception:
5546+
log.error(
5547+
"Simulation fails after requested mode source PEC frames are added. "
5548+
"Please inspec '.with_mode_source_pec_frames'."
5549+
)

tidy3d/components/source/field.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,12 @@ class ModeSource(DirectionalSource, PlanarSource, BroadbandSource):
403403
"``num_modes`` in the solver will be set to ``mode_index + 1``.",
404404
)
405405

406+
pec_frame: pydantic.NonNegativeInt = pydantic.Field(
407+
0,
408+
title="PEC Frame.",
409+
description="Add a thin pec frame around the source during FDTD run.",
410+
)
411+
406412
@cached_property
407413
def angle_theta(self):
408414
"""Polar angle of propagation."""

tidy3d/plugins/smatrix/ports/wave.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ class WavePort(AbstractTerminalPort, Box):
7676
"Must be greater than or equal to 3. When set to `None`, no grid refinement is performed.",
7777
)
7878

79+
pec_frame: pd.NonNegativeInt = pd.Field(
80+
0,
81+
title="PEC Frame.",
82+
description="Add a thin pec frame around the source during FDTD run.",
83+
)
84+
7985
def _mode_voltage_coefficients(self, mode_data: ModeData) -> FreqModeDataArray:
8086
"""Calculates scaling coefficients to convert mode amplitudes
8187
to the total port voltage.
@@ -131,6 +137,7 @@ def to_source(
131137
mode_index=self.mode_index,
132138
direction=self.direction,
133139
name=self.name,
140+
pec_frame=self.pec_frame,
134141
)
135142

136143
def to_monitors(

0 commit comments

Comments
 (0)