From 688f30f8d818393b502c5e7abc9ced870905d5d1 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Wed, 7 May 2025 16:41:36 -0700 Subject: [PATCH 1/2] ABC boundary conditions --- tests/test_components/test_boundaries.py | 271 +++++++++++++++++++ tidy3d/__init__.py | 4 + tidy3d/components/boundary.py | 325 ++++++++++++++++++++++- tidy3d/components/simulation.py | 118 +++++++- 4 files changed, 710 insertions(+), 8 deletions(-) diff --git a/tests/test_components/test_boundaries.py b/tests/test_components/test_boundaries.py index 9ca0f7d915..7919204ec3 100644 --- a/tests/test_components/test_boundaries.py +++ b/tests/test_components/test_boundaries.py @@ -185,3 +185,274 @@ def test_boundaryspec_classmethods(): assert all( isinstance(boundary, PML) for boundary_dim in boundaries for boundary in boundary_dim ) + + +def test_abc_boundary(): + # check basic instance + _ = td.ABCBoundary() + + # check enforced perm (and conductivity) + _ = td.ABCBoundary(permittivity=2) + _ = td.ABCBoundary(permittivity=2, conductivity=0.1) + + with pytest.raises(pydantic.ValidationError): + _ = td.ABCBoundary(permittivity=0) + + with pytest.raises(pydantic.ValidationError): + _ = td.ABCBoundary(permittivity=2, conductivity=-0.1) + + with pytest.raises(pydantic.ValidationError): + _ = td.ABCBoundary(permittivity=None, conductivity=-0.1) + + # test mode abc + wvl_um = 1 + freq0 = td.C_0 / wvl_um + mode_abc = td.ModeABCBoundary( + plane=td.Box(size=(1, 1, 0)), + mode_spec=td.ModeSpec(num_modes=2), + mode_index=1, + frequency=freq0, + ) + + with pytest.raises(pydantic.ValidationError): + _ = td.ModeABCBoundary( + plane=td.Box(size=(1, 1, 0)), + mode_spec=td.ModeSpec(num_modes=2), + mode_index=1, + frequency=-1, + ) + + with pytest.raises(pydantic.ValidationError): + _ = td.ModeABCBoundary( + plane=td.Box(size=(1, 1, 0)), + mode_spec=td.ModeSpec(num_modes=2), + mode_index=-1, + frequency=freq0, + ) + + with pytest.raises(pydantic.ValidationError): + _ = td.ModeABCBoundary( + plane=td.Box(size=(1, 1, 1)), + mode_spec=td.ModeSpec(num_modes=2), + mode_index=0, + frequency=freq0, + ) + + # from mode source + mode_source = td.ModeSource( + size=(1, 1, 0), + source_time=td.GaussianPulse(freq0=freq0, fwidth=0.2 * freq0), + mode_spec=td.ModeSpec(num_modes=2), + mode_index=1, + direction="+", + ) + mode_abc_from_source = td.ModeABCBoundary.from_source(mode_source) + assert mode_abc == mode_abc_from_source + + # from mode monitor + mode_monitor = td.ModeMonitor( + size=(1, 1, 0), mode_spec=td.ModeSpec(num_modes=2), freqs=[freq0], name="mnt" + ) + mode_abc_from_monitor = td.ModeABCBoundary.from_monitor( + mode_monitor, mode_index=1, frequency=freq0 + ) + assert mode_abc == mode_abc_from_monitor + + # in Boundary + _ = td.Boundary( + minus=td.ABCBoundary(permittivity=3), plus=td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0))) + ) + _ = td.Boundary.abc(permittivity=3, conductivity=1e-5) + abc_boundary = td.Boundary.mode_abc( + plane=td.Box(size=(1, 1, 0)), + mode_spec=td.ModeSpec(num_modes=2), + mode_index=1, + frequency=freq0, + ) + abc_boundary_from_source = td.Boundary.mode_abc_from_source(mode_source) + abc_boundary_from_monitor = td.Boundary.mode_abc_from_monitor( + mode_monitor, mode_index=1, frequency=freq0 + ) + assert abc_boundary == abc_boundary_from_source + assert abc_boundary == abc_boundary_from_monitor + + with pytest.raises(pydantic.ValidationError): + _ = td.Boundary(minus=td.Periodic(), plus=td.ABCBoundary()) + + with pytest.raises(pydantic.ValidationError): + _ = td.Boundary(minus=td.Periodic(), plus=td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)))) + + # in Simulation + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary()), + ) + + # validate ABC medium is not anisotorpic + with pytest.raises(pydantic.ValidationError): + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + medium=td.AnisotropicMedium(xx=td.Medium(), yy=td.Medium(), zz=td.Medium()), + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary()), + ) + + # validate homogeneous medium when permittivity=None, that is, automatic detection + box_crossing_boundary = td.Structure( + geometry=td.Box(size=(0.3, 0.2, td.inf)), + medium=td.Medium(permittivity=2), + ) + # ok if ABC boundary is not crossed + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + structures=[box_crossing_boundary], + run_time=1e-20, + boundary_spec=td.BoundarySpec( + x=td.Boundary.abc(), + y=td.Boundary.abc(), + z=td.Boundary.pml(), + ), + ) + # or if we override manually + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + structures=[box_crossing_boundary], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary(permittivity=2)), + ) + # not ok if ABC boudary is crossed + with pytest.raises(pydantic.ValidationError): + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + structures=[box_crossing_boundary], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary()), + ) + # edge case when a structure exactly coincides with simulation domain + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + structures=[box_crossing_boundary.updated_copy(geometry=td.Box(size=(1, 1, 1)))], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary()), + ) + + # warning for possibly non-uniform custom medium + with AssertLogLevel( + "WARNING", contains_str="Nonuniform custom medium detected on an 'ABCBoundary'" + ): + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + medium=td.CustomMedium( + permittivity=td.SpatialDataArray([[[2, 3]]], coords=dict(x=[0], y=[0], z=[0, 1])) + ), + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary()), + ) + + # disallow ABC boundaries in zero dimensions + with pytest.raises(pydantic.ValidationError): + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 0], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + structures=[box_crossing_boundary], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ABCBoundary()), + ) + + # need to define frequence for ModeABCBoundary + # manually + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides( + td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)), frequency=freq0) + ), + ) + # or at least one source + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[mode_source], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides(td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)))), + ) + # multiple sources with different central freqs is still ok, but show warning + with AssertLogLevel( + "WARNING", contains_str="The central frequency of the first source will be used" + ): + _ = td.Simulation( + center=[0, 0, 0], + size=[1, 1, 1], + grid_spec=td.GridSpec.auto( + min_steps_per_wvl=10, + wavelength=wvl_um, + ), + sources=[ + mode_source, + mode_source.updated_copy( + source_time=td.GaussianPulse(freq0=2 * freq0, fwidth=0.2 * freq0) + ), + ], + run_time=1e-20, + boundary_spec=td.BoundarySpec.all_sides( + td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0))) + ), + ) diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index e3948a8d52..ea5786992a 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -96,6 +96,7 @@ # boundary from .components.boundary import ( PML, + ABCBoundary, Absorber, AbsorberParams, BlochBoundary, @@ -106,6 +107,7 @@ DefaultAbsorberParameters, DefaultPMLParameters, DefaultStablePMLParameters, + ModeABCBoundary, PECBoundary, Periodic, PMCBoundary, @@ -534,6 +536,8 @@ def set_logging_level(level: str) -> None: "Periodic", "PECBoundary", "PMCBoundary", + "ABCBoundary", + "ModeABCBoundary", "PML", "StablePML", "Absorber", diff --git a/tidy3d/components/boundary.py b/tidy3d/components/boundary.py index 2e0acc62fa..5e3ebe2bb0 100644 --- a/tidy3d/components/boundary.py +++ b/tidy3d/components/boundary.py @@ -3,16 +3,19 @@ from __future__ import annotations from abc import ABC -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union import numpy as np import pydantic.v1 as pd from ..constants import EPSILON_0, MU_0, PML_SIGMA -from ..exceptions import DataError, SetupError +from ..exceptions import DataError, SetupError, ValidationError from ..log import log -from .base import Tidy3dBaseModel, cached_property +from .base import Tidy3dBaseModel, cached_property, skip_if_fields_missing +from .geometry.base import Box from .medium import Medium +from .mode_spec import ModeSpec +from .monitor import ModeMonitor, ModeSolverMonitor from .source.field import TFSF, GaussianBeam, ModeSource, PlaneWave from .types import TYPE_TAG_STR, Axis, Complex @@ -48,6 +51,174 @@ class PMCBoundary(BoundaryEdge): """Perfect magnetic conductor boundary condition class.""" +# ABC keyword +class AbstractABCBoundary(BoundaryEdge, ABC): + """One-way wave equation absorbing boundary conditions abstract base class.""" + + small_conductivity_approx: bool = pd.Field( + True, + title="Small Conductivity Approximation", + description="If ``False`` then the effective permettivity ``eps`` in the one-wave equation" + " is modified such that the equation exactly satisfy wave propagation at the central " + "frequency.", + ) + + +class ABCBoundary(AbstractABCBoundary): + """One-way wave equation absorbing boundary conditions.""" + + permittivity: Optional[pd.PositiveFloat] = pd.Field( + None, + title="Effective Permittivity", + description="Enforced effective permittivity.", + ) + + conductivity: Optional[pd.NonNegativeFloat] = pd.Field( + None, + title="Effective Conductivity", + description="Enforced effective conductivity.", + ) + + @pd.validator("conductivity", always=True) + @skip_if_fields_missing(["permittivity"]) + def _conductivity_only_with_float_permittivity(cls, val, values): + """Validate that conductivity can be provided only with float permittivity.""" + perm = values["permittivity"] + if val is not None and not isinstance(perm, float): + raise ValidationError( + "Field 'conductivity' in 'ABCBoundary' can only be provided " + "simultaneously with 'permittivity'." + ) + return val + + +class ModeABCBoundary(AbstractABCBoundary): + """One-way wave equation absorbing boundary conditions for absorbing a waveguide mode.""" + + small_conductivity_approx: bool = pd.Field( + False, + title="Small Conductivity Approximation", + description="If ``False`` then the effective permettivity ``eps`` in the one-wave equation" + " is modified such that the equation exactly satisfy wave propagation at the central" + " frequency.", + ) + + mode_spec: ModeSpec = pd.Field( + ModeSpec(), + title="Mode Specification", + description="Parameters to feed to mode solver which determine modes.", + ) + + mode_index: pd.NonNegativeInt = pd.Field( + 0, + title="Mode Index", + description="Index into the collection of modes returned by mode solver. " + " Specifies which mode to absorbed using these boundary conditions. " + "If larger than ``mode_spec.num_modes``, " + "``num_modes`` in the solver will be set to ``mode_index + 1``.", + ) + + frequency: Optional[pd.PositiveFloat] = pd.Field( + None, + title="Frequency", + description="Frequency at which the absorbed mode is evaluated. If ``None``, then the central frequency of the source is used.", + ) + + plane: Box = pd.Field( + ..., + title="Plane", + description="Cross-sectional plane in which the absorbed mode will be computed.", + ) + + @pd.validator("plane", always=True) + def is_plane(cls, val): + """Raise validation error if not planar.""" + if val.size.count(0.0) != 1: + raise ValidationError( + f"'ModeABCBoundary' target plane must be planar, given size={val.size}" + ) + return val + + @classmethod + def from_source( + cls, source: ModeSource, small_conductivity_approx: bool = False + ) -> ModeABCBoundary: + """Instantiate from a ``ModeSource``. + + Parameters + ---------- + source : :class:`ModeSource` + Mode source. + small_conductivity_approx : bool = False, + If ``False`` then the effective permettivity ``eps`` in the one-wave equation + is modified such that the equation exactly satisfy wave propagation at the central + frequency. + + Returns + ------- + :class:`ModeABCBoundary` + Boundary conditions for absorbing the desired mode. + + Example + ------- + >>> from tidy3d import GaussianPulse, ModeSource, inf + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + >>> source = ModeSource(size=(1, 1, 0), source_time=pulse, direction='+') + >>> abc_boundary = ModeABCBoundary.from_source(source=source) + """ + + return cls( + plane=source.bounding_box, + mode_spec=source.mode_spec, + mode_index=source.mode_index, + frequency=source.source_time.freq0, + small_conductivity_approx=small_conductivity_approx, + ) + + @classmethod + def from_monitor( + cls, + monitor: Union[ModeMonitor, ModeSolverMonitor], + mode_index: pd.NonNengativeInt = 0, + frequency: Optional[pd.PositiveFloat] = None, + small_conductivity_approx: bool = False, + ) -> ModeABCBoundary: + """Instantiate from a ``ModeMonitor`` or ``ModeSolverMonitor``. + + Parameters + ---------- + monitor : Union[:class:`ModeMonitor`, :class:`ModeSolverMonitor`] + Mode monitor. + mode_index : pd.NonNengativeInt = 0 + Mode index. + frequency : Optional[pd.PositiveFloat] = None + Frequency for estimating propagation index of absorbed mode. + small_conductivity_approx : bool = False, + If ``False`` then the effective permettivity ``eps`` in the one-wave equation + is modified such that the equation exactly satisfy wave propagation at the central + frequency. + + Returns + ------- + :class:`ModeABCBoundary` + Boundary conditions for absorbing the desired mode. + + Example + ------- + >>> from tidy3d import ModeMonitor + >>> mnt = ModeMonitor(size=(1, 1, 0), freqs=[1e10], name="mnt") + >>> abc_boundary = ModeABCBoundary.from_monitor(monitor=mnt, mode_index=0) + """ + + return cls( + plane=monitor.bounding_box, + mode_spec=monitor.mode_spec, + mode_index=mode_index, + frequency=frequency, + small_conductivity_approx=small_conductivity_approx, + ) + + # """ Bloch boundary """ # sources from which Bloch boundary conditions can be defined @@ -493,7 +664,15 @@ class Absorber(AbsorberSpec): # types of boundaries that can be used in Simulation BoundaryEdgeType = Union[ - Periodic, PECBoundary, PMCBoundary, PML, StablePML, Absorber, BlochBoundary + Periodic, + PECBoundary, + PMCBoundary, + PML, + StablePML, + Absorber, + BlochBoundary, + ABCBoundary, + ModeABCBoundary, ] @@ -557,9 +736,9 @@ def periodic_with_pml(cls, values): plus = values.get("plus") minus = values.get("minus") num_pbc = isinstance(plus, Periodic) + isinstance(minus, Periodic) - num_pml = isinstance(plus, (PML, StablePML, Absorber)) + isinstance( - minus, (PML, StablePML, Absorber) - ) + num_pml = isinstance( + plus, (PML, StablePML, Absorber, ABCBoundary, ModeABCBoundary) + ) + isinstance(minus, (PML, StablePML, Absorber, ABCBoundary, ModeABCBoundary)) if num_pbc == 1 and num_pml == 1: raise SetupError("Cannot have both PML and PBC along the same dimension.") return values @@ -675,6 +854,138 @@ def pmc(cls): minus = PMCBoundary() return cls(plus=plus, minus=minus) + @classmethod + def abc( + cls, + permittivity: Optional[pd.PositiveFloat] = None, + conductivity: Optional[pd.NonNegativeFloat] = None, + small_conductivity_approx: bool = True, + ): + """ABC boundary specification on both sides along a dimension. + + Example + ------- + >>> abc = Boundary.abc() + """ + plus = ABCBoundary( + permittivity=permittivity, + conductivity=conductivity, + small_conductivity_approx=small_conductivity_approx, + ) + minus = ABCBoundary( + permittivity=permittivity, + conductivity=conductivity, + small_conductivity_approx=small_conductivity_approx, + ) + return cls(plus=plus, minus=minus) + + @classmethod + def mode_abc( + cls, + plane: Box, + mode_spec: ModeSpec = ModeSpec(), + mode_index: pd.NonNegativeInt = 0, + frequency: Optional[pd.PositiveFloat] = None, + small_conductivity_approx: bool = False, + ): + """One-way wave equation mode ABC boundary specification on both sides along a dimension. + + Parameters + ---------- + plane: Box + Cross-sectional plane in which the absorbed mode will be computed. + mode_spec: ModeSpec = ModeSpec() + Parameters to feed to mode solver which determine modes. + mode_index : pd.NonNengativeInt = 0 + Mode index. + frequency : Optional[pd.PositiveFloat] = None + Frequency for estimating propagation index of absorbed mode. + small_conductivity_approx : bool = False, + If ``False`` then the effective permettivity ``eps`` in the one-wave equation + is modified such that the equation exactly satisfy wave propagation at the central + frequency. + + Example + ------- + >>> from tidy3d import Box + >>> abc = Boundary.mode_abc(plane=Box(size=(1, 1, 0))) + """ + + plus = ModeABCBoundary( + plane=plane, + mode_spec=mode_spec, + mode_index=mode_index, + frequency=frequency, + small_conductivity_approx=small_conductivity_approx, + ) + minus = ModeABCBoundary( + plane=plane, + mode_spec=mode_spec, + mode_index=mode_index, + frequency=frequency, + small_conductivity_approx=small_conductivity_approx, + ) + + return cls(plus=plus, minus=minus) + + @classmethod + def mode_abc_from_source(cls, source: ModeSource, small_conductivity_approx: bool = False): + """One-way wave equation mode ABC boundary specification on both sides along a dimension constructed from a mode source. + + Parameters + ---------- + source : :class:`ModeSource` + Mode source. + small_conductivity_approx : bool = False, + If ``False`` then the effective permettivity ``eps`` in the one-wave equation + is modified such that the equation exactly satisfy wave propagation at the central + frequency. + + Example + ------- + >>> from tidy3d import GaussianPulse, ModeSource, inf + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + >>> source = ModeSource(size=(1, 1, 0), source_time=pulse, direction='+') + >>> abc = Boundary.mode_abc_from_source(source=source) + """ + plus = ModeABCBoundary.from_source( + source=source, small_conductivity_approx=small_conductivity_approx + ) + minus = ModeABCBoundary.from_source( + source=source, small_conductivity_approx=small_conductivity_approx + ) + return cls(plus=plus, minus=minus) + + @classmethod + def mode_abc_from_monitor( + cls, + monitor: Union[ModeMonitor, ModeSolverMonitor], + mode_index: pd.NonNengativeInt = 0, + frequency: Optional[pd.PositiveFloat] = None, + small_conductivity_approx: bool = False, + ): + """One-way wave equation mode ABC boundary specification on both sides along a dimension constructed from a mode monitor. + + Example + ------- + >>> from tidy3d import ModeMonitor + >>> mnt = ModeMonitor(size=(1, 1, 0), freqs=[1e10], name="mnt") + >>> abc = Boundary.mode_abc_from_monitor(monitor=mnt) + """ + plus = ModeABCBoundary.from_monitor( + monitor=monitor, + mode_index=mode_index, + frequency=frequency, + small_conductivity_approx=small_conductivity_approx, + ) + minus = ModeABCBoundary.from_monitor( + monitor=monitor, + mode_index=mode_index, + frequency=frequency, + small_conductivity_approx=small_conductivity_approx, + ) + return cls(plus=plus, minus=minus) + @classmethod def pml(cls, num_layers: pd.NonNegativeInt = 12, parameters: PMLParams = DefaultPMLParameters): """PML boundary specification on both sides along a dimension. diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index ce307acf2c..dd1bab938d 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -27,11 +27,13 @@ from .base_sim.simulation import AbstractSimulation from .boundary import ( PML, + ABCBoundary, Absorber, AbsorberSpec, BlochBoundary, Boundary, BoundarySpec, + ModeABCBoundary, PECBoundary, Periodic, PMCBoundary, @@ -188,7 +190,10 @@ def boundaries_for_zero_dims(cls, val, values): for dim, (boundary, symmetry_dim, size_dim) in enumerate(zip(boundaries, symmetry, size)): if size_dim == 0: axis = axis_names[dim] - num_absorbing_bdries = sum(isinstance(bnd, AbsorberSpec) for bnd in boundary) + num_absorbing_bdries = sum( + isinstance(bnd, (AbsorberSpec, ABCBoundary, ModeABCBoundary)) + for bnd in boundary + ) num_bloch_bdries = sum(isinstance(bnd, BlochBoundary) for bnd in boundary) if num_absorbing_bdries > 0: @@ -2916,6 +2921,29 @@ def check_fixed_angle_components(cls, values): return values + @pydantic.validator("boundary_spec", always=True) + @skip_if_fields_missing(["sources"]) + def _validate_frequency_mode_abc(cls, val, values): + """Warn if ModeABCBoundary expects a frequency from a source, but there are multiple sources with different central frequencies.""" + boundaries = val.to_list + need_wavelength = any( + isinstance(edge, ModeABCBoundary) and edge.frequency is None + for edge in np.ravel(boundaries) + ) + + if need_wavelength: + sources = values.get("sources") + + freq0s = [source.source_time.freq0 for source in sources] + if not all(math.isclose(freq0, freq0s[0]) for freq0 in freq0s): + log.warning( + "At least one 'ModeABCBoundary' does not specify frequency at which the absorbed mode must be evaluated. " + "The central frequency of the first source will be used.", + capture=False, + ) + + return val + @pydantic.validator("sources", always=True) def _validate_num_sources(cls, val): """Error if too many sources present.""" @@ -3206,6 +3234,94 @@ def _projection_monitors_homogeneous(cls, val, values): return val + @classmethod + def _get_mediums_on_abc( + cls, boundary_spec, medium, center, size, structures + ) -> Tuple[ + List[MediumType3D], + List[MediumType3D], + List[MediumType3D], + List[MediumType3D], + List[MediumType3D], + List[MediumType3D], + ]: + """For each ABC boundary that needs an automatic medium detection (permittivity=None) + determine mediums it crosses. + """ + + # list of structures including background as a Box() + structure_bg = Structure( + geometry=Box( + size=size, + center=center, + ), + medium=medium, + ) + + surfaces = Box.surfaces( + center=structure_bg.geometry.center, size=structure_bg.geometry.size + ) + + total_structures = [structure_bg] + list(structures) + + mediums = [] + for boundary, surface in zip(np.ravel(boundary_spec.to_list), surfaces): + if isinstance(boundary, ABCBoundary) and boundary.permittivity is None: + mediums.append(Scene.intersecting_media(surface, total_structures)) + else: + mediums.append(None) + + return mediums + + @pydantic.validator("boundary_spec", always=True) + @skip_if_fields_missing(["medium", "center", "size", "structures"]) + def _abc_boundaries_homogeneous(cls, val, values): + """Error if abc boundaries intersect multiple mediums or anisotropic mediums.""" + + if val is None: + return val + + # expand zero dimensions to make the treatment uniform + size = [fp_eps if s == 0 else s for s in values.get("size")] + + mediums_all_sides = cls._get_mediums_on_abc( + boundary_spec=val, + medium=values.get("medium"), + size=size, + center=values.get("center"), + structures=values.get("structures") or [], + ) + + with log as consolidated_logger: + for mediums in mediums_all_sides: + if mediums is not None: + # make sure there is no more than one medium in the returned list + if len(mediums) > 1: + raise SetupError( + f"{len(mediums)} different mediums detected on an 'ABCBoundary'. Boundary must be homogeneous." + ) + # 0 medium, something is wrong + if len(mediums) < 1: + raise SetupError( + "No medium detected on plane containing 'ABCBoundary', " + "indicating an unexpected error. Please create a github issue so " + "that the problem can be investigated." + ) + # 1 medium, check if the medium is spatially uniform + if not list(mediums)[0].is_spatially_uniform: + consolidated_logger.warning( + "Nonuniform custom medium detected on an 'ABCBoundary'. " + "Boundary must be homogeneous. Make sure custom medium is uniform on the boundary.", + ) + + if isinstance(list(mediums)[0], (AnisotropicMedium, FullyAnisotropicMedium)): + raise SetupError( + "An anisotropic medium is detected on an 'ABCBoundary. " + "Boundary medium must be homogeneous and isotropic." + ) + + return val + @pydantic.validator("monitors", always=True) def _projection_direction(cls, val, values): """Warn if field projection observation points are behind surface projection monitors.""" From cc75b13b03d00fdecc059e4336ce8a942031a0f8 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Fri, 23 May 2025 18:17:31 -0700 Subject: [PATCH 2/2] PR comments --- tidy3d/components/boundary.py | 78 +++++++-------------------------- tidy3d/components/simulation.py | 2 + 2 files changed, 19 insertions(+), 61 deletions(-) diff --git a/tidy3d/components/boundary.py b/tidy3d/components/boundary.py index 5e3ebe2bb0..baa0b7f211 100644 --- a/tidy3d/components/boundary.py +++ b/tidy3d/components/boundary.py @@ -55,28 +55,26 @@ class PMCBoundary(BoundaryEdge): class AbstractABCBoundary(BoundaryEdge, ABC): """One-way wave equation absorbing boundary conditions abstract base class.""" - small_conductivity_approx: bool = pd.Field( - True, - title="Small Conductivity Approximation", - description="If ``False`` then the effective permettivity ``eps`` in the one-wave equation" - " is modified such that the equation exactly satisfy wave propagation at the central " - "frequency.", - ) - class ABCBoundary(AbstractABCBoundary): - """One-way wave equation absorbing boundary conditions.""" + """One-way wave equation absorbing boundary conditions. + See, for example, John B. Schneider, Understanding the Finite-Difference Time-Domain Method, Chapter 6. + """ permittivity: Optional[pd.PositiveFloat] = pd.Field( None, title="Effective Permittivity", - description="Enforced effective permittivity.", + description="Effective permittivity for determining propagation constant. " + "If ``None``, this value will be automatically inferred from the medium at " + "the domain boundary and the central frequency of the source.", ) conductivity: Optional[pd.NonNegativeFloat] = pd.Field( None, title="Effective Conductivity", - description="Enforced effective conductivity.", + description="Effective conductivity for determining propagation constant." + "If ``None``, this value will be automatically inferred from the medium at " + "the domain boundary and the central frequency of the source.", ) @pd.validator("conductivity", always=True) @@ -84,7 +82,7 @@ class ABCBoundary(AbstractABCBoundary): def _conductivity_only_with_float_permittivity(cls, val, values): """Validate that conductivity can be provided only with float permittivity.""" perm = values["permittivity"] - if val is not None and not isinstance(perm, float): + if val is not None and perm is None: raise ValidationError( "Field 'conductivity' in 'ABCBoundary' can only be provided " "simultaneously with 'permittivity'." @@ -95,25 +93,17 @@ def _conductivity_only_with_float_permittivity(cls, val, values): class ModeABCBoundary(AbstractABCBoundary): """One-way wave equation absorbing boundary conditions for absorbing a waveguide mode.""" - small_conductivity_approx: bool = pd.Field( - False, - title="Small Conductivity Approximation", - description="If ``False`` then the effective permettivity ``eps`` in the one-wave equation" - " is modified such that the equation exactly satisfy wave propagation at the central" - " frequency.", - ) - mode_spec: ModeSpec = pd.Field( ModeSpec(), title="Mode Specification", - description="Parameters to feed to mode solver which determine modes.", + description="Parameters that determine the modes computed by the mode solver.", ) mode_index: pd.NonNegativeInt = pd.Field( 0, title="Mode Index", description="Index into the collection of modes returned by mode solver. " - " Specifies which mode to absorbed using these boundary conditions. " + "The absorbing boundary conditions are configured to absorb the specified mode. " "If larger than ``mode_spec.num_modes``, " "``num_modes`` in the solver will be set to ``mode_index + 1``.", ) @@ -140,19 +130,13 @@ def is_plane(cls, val): return val @classmethod - def from_source( - cls, source: ModeSource, small_conductivity_approx: bool = False - ) -> ModeABCBoundary: + def from_source(cls, source: ModeSource) -> ModeABCBoundary: """Instantiate from a ``ModeSource``. Parameters ---------- source : :class:`ModeSource` Mode source. - small_conductivity_approx : bool = False, - If ``False`` then the effective permettivity ``eps`` in the one-wave equation - is modified such that the equation exactly satisfy wave propagation at the central - frequency. Returns ------- @@ -172,7 +156,6 @@ def from_source( mode_spec=source.mode_spec, mode_index=source.mode_index, frequency=source.source_time.freq0, - small_conductivity_approx=small_conductivity_approx, ) @classmethod @@ -181,7 +164,6 @@ def from_monitor( monitor: Union[ModeMonitor, ModeSolverMonitor], mode_index: pd.NonNengativeInt = 0, frequency: Optional[pd.PositiveFloat] = None, - small_conductivity_approx: bool = False, ) -> ModeABCBoundary: """Instantiate from a ``ModeMonitor`` or ``ModeSolverMonitor``. @@ -193,10 +175,6 @@ def from_monitor( Mode index. frequency : Optional[pd.PositiveFloat] = None Frequency for estimating propagation index of absorbed mode. - small_conductivity_approx : bool = False, - If ``False`` then the effective permettivity ``eps`` in the one-wave equation - is modified such that the equation exactly satisfy wave propagation at the central - frequency. Returns ------- @@ -215,7 +193,6 @@ def from_monitor( mode_spec=monitor.mode_spec, mode_index=mode_index, frequency=frequency, - small_conductivity_approx=small_conductivity_approx, ) @@ -859,7 +836,6 @@ def abc( cls, permittivity: Optional[pd.PositiveFloat] = None, conductivity: Optional[pd.NonNegativeFloat] = None, - small_conductivity_approx: bool = True, ): """ABC boundary specification on both sides along a dimension. @@ -870,12 +846,10 @@ def abc( plus = ABCBoundary( permittivity=permittivity, conductivity=conductivity, - small_conductivity_approx=small_conductivity_approx, ) minus = ABCBoundary( permittivity=permittivity, conductivity=conductivity, - small_conductivity_approx=small_conductivity_approx, ) return cls(plus=plus, minus=minus) @@ -886,7 +860,6 @@ def mode_abc( mode_spec: ModeSpec = ModeSpec(), mode_index: pd.NonNegativeInt = 0, frequency: Optional[pd.PositiveFloat] = None, - small_conductivity_approx: bool = False, ): """One-way wave equation mode ABC boundary specification on both sides along a dimension. @@ -895,15 +868,11 @@ def mode_abc( plane: Box Cross-sectional plane in which the absorbed mode will be computed. mode_spec: ModeSpec = ModeSpec() - Parameters to feed to mode solver which determine modes. + Parameters that determine the modes computed by the mode solver. mode_index : pd.NonNengativeInt = 0 Mode index. frequency : Optional[pd.PositiveFloat] = None Frequency for estimating propagation index of absorbed mode. - small_conductivity_approx : bool = False, - If ``False`` then the effective permettivity ``eps`` in the one-wave equation - is modified such that the equation exactly satisfy wave propagation at the central - frequency. Example ------- @@ -916,30 +885,24 @@ def mode_abc( mode_spec=mode_spec, mode_index=mode_index, frequency=frequency, - small_conductivity_approx=small_conductivity_approx, ) minus = ModeABCBoundary( plane=plane, mode_spec=mode_spec, mode_index=mode_index, frequency=frequency, - small_conductivity_approx=small_conductivity_approx, ) return cls(plus=plus, minus=minus) @classmethod - def mode_abc_from_source(cls, source: ModeSource, small_conductivity_approx: bool = False): + def mode_abc_from_source(cls, source: ModeSource): """One-way wave equation mode ABC boundary specification on both sides along a dimension constructed from a mode source. Parameters ---------- source : :class:`ModeSource` Mode source. - small_conductivity_approx : bool = False, - If ``False`` then the effective permettivity ``eps`` in the one-wave equation - is modified such that the equation exactly satisfy wave propagation at the central - frequency. Example ------- @@ -948,12 +911,8 @@ def mode_abc_from_source(cls, source: ModeSource, small_conductivity_approx: boo >>> source = ModeSource(size=(1, 1, 0), source_time=pulse, direction='+') >>> abc = Boundary.mode_abc_from_source(source=source) """ - plus = ModeABCBoundary.from_source( - source=source, small_conductivity_approx=small_conductivity_approx - ) - minus = ModeABCBoundary.from_source( - source=source, small_conductivity_approx=small_conductivity_approx - ) + plus = ModeABCBoundary.from_source(source=source) + minus = ModeABCBoundary.from_source(source=source) return cls(plus=plus, minus=minus) @classmethod @@ -962,7 +921,6 @@ def mode_abc_from_monitor( monitor: Union[ModeMonitor, ModeSolverMonitor], mode_index: pd.NonNengativeInt = 0, frequency: Optional[pd.PositiveFloat] = None, - small_conductivity_approx: bool = False, ): """One-way wave equation mode ABC boundary specification on both sides along a dimension constructed from a mode monitor. @@ -976,13 +934,11 @@ def mode_abc_from_monitor( monitor=monitor, mode_index=mode_index, frequency=frequency, - small_conductivity_approx=small_conductivity_approx, ) minus = ModeABCBoundary.from_monitor( monitor=monitor, mode_index=mode_index, frequency=frequency, - small_conductivity_approx=small_conductivity_approx, ) return cls(plus=plus, minus=minus) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index dd1bab938d..7a775b5fef 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -3299,6 +3299,8 @@ def _abc_boundaries_homogeneous(cls, val, values): if len(mediums) > 1: raise SetupError( f"{len(mediums)} different mediums detected on an 'ABCBoundary'. Boundary must be homogeneous." + "Alternatively, effective permeability and conductivity can be directly provided as " + "parameters for an 'ABCBoundary', in which case this medium check is skipped." ) # 0 medium, something is wrong if len(mediums) < 1: