From c70e7f2adfc45866778a1526f5c544b74ce5c31e Mon Sep 17 00:00:00 2001 From: George-Guryev-flxcmp Date: Tue, 18 Nov 2025 00:30:43 -0500 Subject: [PATCH] feat(rf): FXC 1604 add api for automatic extrusion of boundary structures into pml --- CHANGELOG.md | 1 + schemas/EMESimulation.json | 20 ++++++++++++++++++ schemas/ModeSimulation.json | 26 +++++++++++++++++++++++ schemas/Simulation.json | 26 +++++++++++++++++++++++ schemas/TerminalComponentModeler.json | 26 +++++++++++++++++++++++ tests/test_components/test_boundaries.py | 27 ++++++++++++++++++++++++ tidy3d/components/boundary.py | 15 +++++++++++++ tidy3d/components/simulation.py | 27 +++++++++++++++++++++--- 8 files changed, 165 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f094397115..73173ed2f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.10.0rc3] - 2025-11-26 ### Added +- Added optional automatic extrusion of structures at the simulation boundaries into/through PML/Absorber layers via `enable_extrusion` field in class `AbsorberSpec`. - Added S-parameter de-embedding to `TerminalComponentModelerData`, enabling recalculation with shifted reference planes. - 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. - Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging. diff --git a/schemas/EMESimulation.json b/schemas/EMESimulation.json index 42e7a1ac19..5ac97418d8 100644 --- a/schemas/EMESimulation.json +++ b/schemas/EMESimulation.json @@ -36,6 +36,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, @@ -1008,6 +1012,7 @@ "minus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1072,6 +1077,7 @@ "plus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1167,6 +1173,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1186,6 +1193,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1216,6 +1224,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1235,6 +1244,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1265,6 +1275,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1284,6 +1295,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -9352,6 +9364,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": true, + "type": "boolean" + }, "name": { "type": "string" }, @@ -11286,6 +11302,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/schemas/ModeSimulation.json b/schemas/ModeSimulation.json index dda93ce073..ab026c8cd5 100644 --- a/schemas/ModeSimulation.json +++ b/schemas/ModeSimulation.json @@ -36,6 +36,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, @@ -1008,6 +1012,7 @@ "minus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1072,6 +1077,7 @@ "plus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1167,6 +1173,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1186,6 +1193,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1216,6 +1224,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1235,6 +1244,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1265,6 +1275,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1284,6 +1295,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -9073,6 +9085,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": true, + "type": "boolean" + }, "name": { "type": "string" }, @@ -11007,6 +11023,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, @@ -12159,6 +12179,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -12178,6 +12199,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -12201,6 +12223,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -12220,6 +12243,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -12243,6 +12267,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -12262,6 +12287,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { diff --git a/schemas/Simulation.json b/schemas/Simulation.json index 9b86e9efdb..a92d765b15 100644 --- a/schemas/Simulation.json +++ b/schemas/Simulation.json @@ -36,6 +36,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, @@ -1389,6 +1393,7 @@ "minus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1453,6 +1458,7 @@ "plus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1548,6 +1554,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1567,6 +1574,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1597,6 +1605,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1616,6 +1625,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1646,6 +1656,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1665,6 +1676,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -13055,6 +13067,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": true, + "type": "boolean" + }, "name": { "type": "string" }, @@ -15357,6 +15373,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, @@ -16854,6 +16874,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -16873,6 +16894,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -16896,6 +16918,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -16915,6 +16938,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -16938,6 +16962,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -16957,6 +16982,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { diff --git a/schemas/TerminalComponentModeler.json b/schemas/TerminalComponentModeler.json index 64e8bfabf9..a70ac994ac 100644 --- a/schemas/TerminalComponentModeler.json +++ b/schemas/TerminalComponentModeler.json @@ -36,6 +36,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, @@ -1389,6 +1393,7 @@ "minus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1453,6 +1458,7 @@ "plus": { "default": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1548,6 +1554,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1567,6 +1574,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1597,6 +1605,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1616,6 +1625,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1646,6 +1656,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -1665,6 +1676,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -13435,6 +13447,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": true, + "type": "boolean" + }, "name": { "type": "string" }, @@ -15465,6 +15481,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -15484,6 +15501,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -15507,6 +15525,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -15526,6 +15545,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -15549,6 +15569,7 @@ "attrs": {}, "minus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -15568,6 +15589,7 @@ }, "plus": { "attrs": {}, + "extrude_structures": true, "name": null, "num_layers": 12, "parameters": { @@ -16479,6 +16501,10 @@ "default": {}, "type": "object" }, + "extrude_structures": { + "default": false, + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/tests/test_components/test_boundaries.py b/tests/test_components/test_boundaries.py index 3c190919ca..0e99f867b9 100644 --- a/tests/test_components/test_boundaries.py +++ b/tests/test_components/test_boundaries.py @@ -193,6 +193,33 @@ def test_boundaryspec_classmethods(): ) +def test_extrude_structures_to_pml(): + """Test ``enable_extrusion`` field in PML/Absorber API.""" + + # check default state + boundary_pml = PML() + boundary_abs = Absorber() + boundary_bloch = BlochBoundary(bloch_vec=1) + boundary_pec = PECBoundary() + + assert boundary_pml.extrude_structures is True + assert boundary_abs.extrude_structures is False + + # make sure attribute error is raised if other BC attempt to access/use the feature + with pytest.raises(AttributeError): + boundary_bloch.extrude_structures + with pytest.raises(AttributeError): + boundary_pec.extrude_structures + + # change state of boundary condition + boundary_pml = PML(extrude_structures=False) + boundary_abs = Absorber(extrude_structures=True) + + # make sure field values were correctly updated + assert boundary_pml.extrude_structures is False + assert boundary_abs.extrude_structures is True + + @pytest.mark.parametrize("absorber_type", [PML, StablePML, Absorber]) def test_num_layers_validator(absorber_type): """Test the Field validators that enforce ``num_layers>0``.""" diff --git a/tidy3d/components/boundary.py b/tidy3d/components/boundary.py index 2a586278a0..c7d748b1bb 100644 --- a/tidy3d/components/boundary.py +++ b/tidy3d/components/boundary.py @@ -672,6 +672,15 @@ class AbsorberSpec(BoundaryEdge): description="Parameters to fine tune the absorber profile and properties.", ) + extrude_structures: bool = pd.Field( + False, + title="Enable structure extrusion to PML", + description="Automatically extrude structures into the absorbing region (e.g., PML or adiabatic absorber)." + "Any structure located within `CLIP_OFFSET` of a simulation boundary will be extended through the full thickness of the PML/absorber." + "The extruded region is assigned the material properties of the structure at the `CLIP_OFFSET` location." + "Extrusion is performed along the direction normal to the PML/absorber surface.", + ) + class PML(AbsorberSpec): """Specifies a standard PML along a single dimension. @@ -794,6 +803,12 @@ class PML(AbsorberSpec): min_num_layers=MIN_NUM_PML_LAYERS, descr="perfectly-matched layer" ) + extrude_structures: bool = pd.Field( + True, + title=AbsorberSpec.__fields__["extrude_structures"].field_info.title, + description=AbsorberSpec.__fields__["extrude_structures"].field_info.description, + ) + class StablePML(AbsorberSpec): """Specifies a 'stable' PML along a single dimension. diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 86c57d71a1..1ad9980990 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -3451,6 +3451,17 @@ def warn(structure, istruct, side) -> None: custom_loc=["structures", istruct], ) + def warn_extruded(structure, istruct, side) -> None: + """Warning message for a structure close to PML that will be automatically extruded.""" + obj_descr = named_obj_descr(structure, "structures", istruct) + consolidated_logger.warning( + f"Structure: {obj_descr} was detected as being less " + f"than half of a central wavelength from a PML on side {side}. " + "The structure will be automatically extruded to the end of the PML region " + "to ensure translational invariance.", + custom_loc=["structures", istruct], + ) + for istruct, structure in enumerate(structures): struct_bound_min, struct_bound_max = structure.geometry.bounds @@ -3467,7 +3478,10 @@ def warn(structure, istruct, side) -> None: and struct_val > sim_val and abs(sim_val - struct_val) < lambda0 / 2 ): - warn(structure, istruct, axis + "-min") + if boundary[0].extrude_structures: + warn_extruded(structure, istruct, axis + "-min") + else: + warn(structure, istruct, axis + "-min") zipped = zip(["x", "y", "z"], sim_bound_max, struct_bound_max, boundaries) for axis, sim_val, struct_val, boundary in zipped: @@ -3479,7 +3493,10 @@ def warn(structure, istruct, side) -> None: and struct_val < sim_val and abs(sim_val - struct_val) < lambda0 / 2 ): - warn(structure, istruct, axis + "-max") + if boundary[1].extrude_structures: + warn_extruded(structure, istruct, axis + "-max") + else: + warn(structure, istruct, axis + "-max") return val @@ -4376,7 +4393,11 @@ def _validate_no_structures_pml(self) -> None: sim_pos_pml = sim_pos + pm_val * pml in_pml_plus = (pm_val > 0) and (sim_pos < geo_pos <= sim_pos_pml) in_pml_mnus = (pm_val < 0) and (sim_pos > geo_pos >= sim_pos_pml) - if not isinstance(bound_edge, Absorber) and (in_pml_plus or in_pml_mnus): + if ( + not isinstance(bound_edge, Absorber) + and (in_pml_plus or in_pml_mnus) + and not bound_edge.extrude_structures + ): warn = True if warn: obj_descr = named_obj_descr(structure, "structures", i)