Skip to content

Commit b780155

Browse files
pec frame
1 parent 325603c commit b780155

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",
@@ -593,6 +594,7 @@ def set_logging_level(level: str) -> None:
593594
"Medium2D",
594595
"MediumMediumInterface",
595596
"MeshOverrideStructure",
597+
"ModeABCBoundary",
596598
"ModeAmpsDataArray",
597599
"ModeData",
598600
"ModeIndexDataArray",
@@ -716,6 +718,4 @@ def set_logging_level(level: str) -> None:
716718
"set_logging_console",
717719
"set_logging_file",
718720
"wavelengths",
719-
"ABCBoundary",
720-
"ModeABCBoundary",
721721
]

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]]
@@ -3285,13 +3290,13 @@ def _projection_monitors_homogeneous(cls, val, values):
32853290
@classmethod
32863291
def _get_mediums_on_abc(
32873292
cls, boundary_spec, medium, center, size, structures
3288-
) -> Tuple[
3289-
List[MediumType3D],
3290-
List[MediumType3D],
3291-
List[MediumType3D],
3292-
List[MediumType3D],
3293-
List[MediumType3D],
3294-
List[MediumType3D],
3293+
) -> tuple[
3294+
list[MediumType3D],
3295+
list[MediumType3D],
3296+
list[MediumType3D],
3297+
list[MediumType3D],
3298+
list[MediumType3D],
3299+
list[MediumType3D],
32953300
]:
32963301
"""For each ABC boundary that needs an automatic medium detection (permittivity=None)
32973302
determine mediums it crosses.
@@ -3310,7 +3315,7 @@ def _get_mediums_on_abc(
33103315
center=structure_bg.geometry.center, size=structure_bg.geometry.size
33113316
)
33123317

3313-
total_structures = [structure_bg] + list(structures)
3318+
total_structures = [structure_bg, *list(structures)]
33143319

33153320
mediums = []
33163321
for boundary, surface in zip(np.ravel(boundary_spec.to_list), surfaces):
@@ -4544,7 +4549,7 @@ def mediums(self) -> set[MediumType]:
45444549
45454550
Returns
45464551
-------
4547-
List[:class:`.AbstractMedium`]
4552+
set[:class:`.AbstractMedium`]
45484553
Set of distinct mediums in the simulation.
45494554
"""
45504555
log.warning(
@@ -4606,12 +4611,12 @@ def intersecting_media(
46064611
-------
46074612
test_object : :class:`.Box`
46084613
Object for which intersecting media are to be detected.
4609-
structures : List[:class:`.AbstractMedium`]
4614+
structures : tuple[:class:`.AbstractMedium`]
46104615
List of structures whose media will be tested.
46114616
46124617
Returns
46134618
-------
4614-
List[:class:`.AbstractMedium`]
4619+
tuple[:class:`.AbstractMedium`]
46154620
Set of distinct mediums that intersect with the given planar object.
46164621
"""
46174622

@@ -4633,12 +4638,12 @@ def intersecting_structures(
46334638
-------
46344639
test_object : :class:`.Box`
46354640
Object for which intersecting media are to be detected.
4636-
structures : List[:class:`.AbstractMedium`]
4641+
structures : tuple[:class:`.AbstractMedium`]
46374642
List of structures whose media will be tested.
46384643
46394644
Returns
46404645
-------
4641-
List[:class:`.Structure`]
4646+
tuple[:class:`.Structure`]
46424647
Set of distinct structures that intersect with the given surface, or with the surfaces
46434648
of the given volume.
46444649
"""
@@ -5451,3 +5456,101 @@ def from_scene(cls, scene: Scene, **kwargs) -> Simulation:
54515456
)
54525457

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

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
@@ -82,6 +82,12 @@ class WavePort(AbstractTerminalPort, Box):
8282
description="Use conjugated or non-conjugated dot product for mode decomposition.",
8383
)
8484

85+
pec_frame: pd.NonNegativeInt = pd.Field(
86+
0,
87+
title="PEC Frame.",
88+
description="Add a thin pec frame around the source during FDTD run.",
89+
)
90+
8591
def _mode_voltage_coefficients(self, mode_data: ModeData) -> FreqModeDataArray:
8692
"""Calculates scaling coefficients to convert mode amplitudes
8793
to the total port voltage.
@@ -139,6 +145,7 @@ def to_source(
139145
mode_index=self.mode_index,
140146
direction=self.direction,
141147
name=self.name,
148+
pec_frame=self.pec_frame,
142149
)
143150

144151
def to_monitors(

0 commit comments

Comments
 (0)