From 2d485ce9f8017494b206dffd55b7392f44712389 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Sat, 2 Aug 2025 10:08:36 -0700 Subject: [PATCH 1/3] wip --- tidy3d/__init__.py | 2 ++ tidy3d/components/boundary.py | 59 +++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 92a7f42b09..f61cda290e 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from tidy3d.components.boundary import BroadbandModeABCSpec from tidy3d.components.material.multi_physics import MultiPhysicsMedium from tidy3d.components.material.tcad.charge import ( ChargeConductorMedium, @@ -428,6 +429,7 @@ def set_logging_level(level: str) -> None: "ABCBoundary", "Absorber", "AbsorberParams", + "BroadbandModeABCSpec", "AbstractFieldProjectionData", "AbstractMedium", "AdmittanceNetwork", diff --git a/tidy3d/components/boundary.py b/tidy3d/components/boundary.py index 16158e47ff..d1bc82b096 100644 --- a/tidy3d/components/boundary.py +++ b/tidy3d/components/boundary.py @@ -15,7 +15,7 @@ PlotParams, plot_params_absorber, ) -from tidy3d.constants import CONDUCTIVITY, EPSILON_0, MU_0, PML_SIGMA +from tidy3d.constants import C_0, CONDUCTIVITY, EPSILON_0, MU_0, PML_SIGMA from tidy3d.exceptions import DataError, SetupError, ValidationError from tidy3d.log import log @@ -124,7 +124,62 @@ def _conductivity_only_with_float_permittivity(cls, val, values): "simultaneously with 'permittivity'." ) return val + +class BroadbandModeABCSpec(Tidy3dBaseModel): + """Specifies the broadband mode absorption boundary conditions. The mode propagation index is approximated by a sum of pole-residue pairs. + + Example + ------- + >>> broadband_mode_abc_spec = BroadbandModeABCSpec(fmin=100e12, fmax=120e12, num_freqs=3) + """ + + fmin: pd.PositiveFloat = pd.Field( + ..., + title="Lower Frequency Bound", + description="Lower frequency bound for the broadband mode absorption.", + ) + + fmax: pd.PositiveFloat = pd.Field( + ..., + title="Upper Frequency Bound", + description="Upper frequency bound for the broadband mode absorption.", + ) + + num_freqs: pd.PositiveInt = pd.Field( + 3, + title="Number of Frequencies", + description="Number of frequencies to use to approximate the mode propagation index. " + "The frequencies are evenly spaced between ``fmin`` and ``fmax``.", + ) + + num_poles: pd.PositiveInt = pd.Field( + 1, + title="Number of Poles", + description="Number of poles to use to approximate the mode propagation index.", + ) + + @classmethod + def from_wvl_range(cls, wvl_min: float, wvl_max: float, num_freqs: int = 3, num_poles: int = 1) -> BroadbandModeABCSpec: + """Instantiate from a wavelength range. + + Parameters + ---------- + wvl_min : float + Minimum wavelength. + wvl_max : float + Maximum wavelength. + num_freqs : int = 3 + Number of frequencies to use to approximate the mode propagation index. + num_poles : int = 1 + Number of poles to use to approximate the mode propagation index. + + Returns + ------- + :class:`BroadbandModeABCSpec` + Broadband mode absorption boundary conditions. + """ + return cls(fmin=C_0 / wvl_max, fmax=C_0 / wvl_min, num_freqs=num_freqs, num_poles=num_poles) class ModeABCBoundary(AbstractABCBoundary): """One-way wave equation absorbing boundary conditions for absorbing a waveguide mode.""" @@ -144,7 +199,7 @@ class ModeABCBoundary(AbstractABCBoundary): "``num_modes`` in the solver will be set to ``mode_index + 1``.", ) - frequency: Optional[pd.PositiveFloat] = pd.Field( + frequency: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = 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.", From 37a3e604b29dc4c181db1637471c7d021c723076 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Mon, 4 Aug 2025 16:17:21 -0700 Subject: [PATCH 2/3] renaming and tests --- tests/test_components/test_absorbers.py | 65 ++++++- tidy3d/__init__.py | 5 +- tidy3d/components/boundary.py | 158 ++++++++++++------ tidy3d/components/simulation.py | 2 +- .../smatrix/component_modelers/terminal.py | 4 +- tidy3d/plugins/smatrix/ports/wave.py | 4 +- 6 files changed, 170 insertions(+), 68 deletions(-) diff --git a/tests/test_components/test_absorbers.py b/tests/test_components/test_absorbers.py index 7c17b7663d..eeee38c5f7 100644 --- a/tests/test_components/test_absorbers.py +++ b/tests/test_components/test_absorbers.py @@ -133,7 +133,7 @@ def test_port_absorbers_simulations(): td.InternalAbsorber( size=(0.4, 0.5, 0), direction="-", - boundary_spec=td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)), frequency=freq0), + boundary_spec=td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)), freq_spec=freq0), ) ], ) @@ -207,7 +207,7 @@ def test_abc_boundaries_alone(): plane=td.Box(size=(1, 1, 0)), mode_spec=td.ModeSpec(num_modes=2), mode_index=1, - frequency=freq0, + freq_spec=freq0, ) with pytest.raises(pydantic.ValidationError): @@ -215,7 +215,7 @@ def test_abc_boundaries_alone(): plane=td.Box(size=(1, 1, 0)), mode_spec=td.ModeSpec(num_modes=2), mode_index=1, - frequency=-1, + freq_spec=-1, ) with pytest.raises(pydantic.ValidationError): @@ -223,7 +223,7 @@ def test_abc_boundaries_alone(): plane=td.Box(size=(1, 1, 0)), mode_spec=td.ModeSpec(num_modes=2), mode_index=-1, - frequency=freq0, + freq_spec=freq0, ) with pytest.raises(pydantic.ValidationError): @@ -231,7 +231,7 @@ def test_abc_boundaries_alone(): plane=td.Box(size=(1, 1, 1)), mode_spec=td.ModeSpec(num_modes=2), mode_index=0, - frequency=freq0, + freq_spec=freq0, ) # from mode source @@ -250,7 +250,7 @@ def test_abc_boundaries_alone(): 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 + mode_monitor, mode_index=1, freq_spec=freq0 ) assert mode_abc == mode_abc_from_monitor @@ -263,11 +263,11 @@ def test_abc_boundaries_alone(): plane=td.Box(size=(1, 1, 0)), mode_spec=td.ModeSpec(num_modes=2), mode_index=1, - frequency=freq0, + freq_spec=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 + mode_monitor, mode_index=1, freq_spec=freq0 ) assert abc_boundary == abc_boundary_from_source assert abc_boundary == abc_boundary_from_monitor @@ -430,7 +430,7 @@ def test_abc_boundaries_simulations(): sources=[], run_time=1e-20, boundary_spec=td.BoundarySpec.all_sides( - td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)), frequency=freq0) + td.ModeABCBoundary(plane=td.Box(size=(1, 1, 0)), freq_spec=freq0) ), ) # or at least one source @@ -533,3 +533,50 @@ def test_abc_boundaries_simulations(): td.ABCBoundary(permittivity=2, conductivity=1e-5) ), ) + + +def test_abc_boundaries_broadband(): + # test broadband fitter params + fitter_params = td.BroadbandModeABCFitterParam( + max_num_poles=10, tolerance_rms=1e-4, frequency_sampling_points=10 + ) + + # test max num poles > 0 + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCFitterParam(max_num_poles=0) + # test max num poles <= 10 + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCFitterParam(max_num_poles=11) + + # test tolerance rms >= 0 + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCFitterParam(tolerance_rms=-1) + + # test frequency sampling points > 0 + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCFitterParam(frequency_sampling_points=0) + # test frequency sampling points <= 21 + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCFitterParam(frequency_sampling_points=22) + + # test basic instance + fmin = 100e12 + fmax = 200e12 + abc_boundary = td.BroadbandModeABCSpec(frequency_range=(fmin, fmax)) + + # test max frequency > min frequency + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCSpec(frequency_range=(fmax, fmin)) + + # test min frequency > 0 + with pytest.raises(pydantic.ValidationError): + _ = td.BroadbandModeABCSpec(frequency_range=(0, fmax)) + + # test from_wavelength_range + wvl_min = td.C_0 / fmax + wvl_max = td.C_0 / fmin + abc_boundary = td.BroadbandModeABCSpec.from_wavelength_range( + wavelength_range=(wvl_min, wvl_max), fit_param=fitter_params + ) + assert abc_boundary.frequency_range == (fmin, fmax) + assert abc_boundary.fit_param == fitter_params diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index f61cda290e..865253b1d4 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from tidy3d.components.boundary import BroadbandModeABCSpec +from tidy3d.components.boundary import BroadbandModeABCFitterParam, BroadbandModeABCSpec from tidy3d.components.material.multi_physics import MultiPhysicsMedium from tidy3d.components.material.tcad.charge import ( ChargeConductorMedium, @@ -429,7 +429,6 @@ def set_logging_level(level: str) -> None: "ABCBoundary", "Absorber", "AbsorberParams", - "BroadbandModeABCSpec", "AbstractFieldProjectionData", "AbstractMedium", "AdmittanceNetwork", @@ -448,6 +447,8 @@ def set_logging_level(level: str) -> None: "BoundaryEdgeType", "BoundarySpec", "Box", + "BroadbandModeABCFitterParam", + "BroadbandModeABCSpec", "CaugheyThomasMobility", "CellDataArray", "ChargeConductorMedium", diff --git a/tidy3d/components/boundary.py b/tidy3d/components/boundary.py index d1bc82b096..e6c290cf9e 100644 --- a/tidy3d/components/boundary.py +++ b/tidy3d/components/boundary.py @@ -15,7 +15,7 @@ PlotParams, plot_params_absorber, ) -from tidy3d.constants import C_0, CONDUCTIVITY, EPSILON_0, MU_0, PML_SIGMA +from tidy3d.constants import C_0, CONDUCTIVITY, EPSILON_0, HERTZ, MU_0, PML_SIGMA from tidy3d.exceptions import DataError, SetupError, ValidationError from tidy3d.log import log @@ -25,7 +25,7 @@ from .mode_spec import ModeSpec from .monitor import ModeMonitor, ModeSolverMonitor from .source.field import TFSF, GaussianBeam, ModeSource, PlaneWave -from .types import TYPE_TAG_STR, Ax, Axis, Complex, Direction +from .types import TYPE_TAG_STR, Ax, Axis, Complex, Direction, FreqBound MIN_NUM_PML_LAYERS = 6 MIN_NUM_STABLE_PML_LAYERS = 6 @@ -55,6 +55,11 @@ def _warn_num_layers(cls, val): DEFAULT_MODE_SPEC_MODE_ABC = ModeSpec() +DEFAULT_BROADBAND_MODE_ABC_FITTER_TOLERANCE = 1e-6 +DEFAULT_BROADBAND_MODE_ABC_NUM_FREQS = 15 +DEFAULT_BROADBAND_MODE_ABC_NUM_POLES = 5 +MAX_BROADBAND_MODE_ABC_NUM_POLES = 10 +MAX_BROADBAND_MODE_ABC_NUM_FREQS = 21 class BoundaryEdge(ABC, Tidy3dBaseModel): @@ -124,62 +129,109 @@ def _conductivity_only_with_float_permittivity(cls, val, values): "simultaneously with 'permittivity'." ) return val - -class BroadbandModeABCSpec(Tidy3dBaseModel): - """Specifies the broadband mode absorption boundary conditions. The mode propagation index is approximated by a sum of pole-residue pairs. - + +class BroadbandModeABCFitterParam(Tidy3dBaseModel): + """Parameters for fitting the mode propagation index over the frequency range using pole-residue pair model. + + Notes + ----- + The number of poles and frequency sampling points are constrained to be within the range [1, 10] and [1, 21] respectively. + Example ------- - >>> broadband_mode_abc_spec = BroadbandModeABCSpec(fmin=100e12, fmax=120e12, num_freqs=3) + >>> fitter_param = BroadbandModeABCFitterParam(max_num_poles=5, tolerance_rms=1e-4, frequency_sampling_points=10) """ - fmin: pd.PositiveFloat = pd.Field( - ..., - title="Lower Frequency Bound", - description="Lower frequency bound for the broadband mode absorption.", + max_num_poles: int = pd.Field( + DEFAULT_BROADBAND_MODE_ABC_NUM_POLES, + title="Maximal Number Of Poles", + description="Maximal number of poles in complex-conjugate pole residue model for " + "fitting the mode propagation index.", + gt=0, + le=MAX_BROADBAND_MODE_ABC_NUM_POLES, ) - fmax: pd.PositiveFloat = pd.Field( - ..., - title="Upper Frequency Bound", - description="Upper frequency bound for the broadband mode absorption.", + tolerance_rms: pd.NonNegativeFloat = pd.Field( + DEFAULT_BROADBAND_MODE_ABC_FITTER_TOLERANCE, + title="Fitting Tolerance", + description="Tolerance in fitting the mode propagation index.", ) - num_freqs: pd.PositiveInt = pd.Field( - 3, - title="Number of Frequencies", - description="Number of frequencies to use to approximate the mode propagation index. " - "The frequencies are evenly spaced between ``fmin`` and ``fmax``.", + frequency_sampling_points: int = pd.Field( + DEFAULT_BROADBAND_MODE_ABC_NUM_FREQS, + title="Number Of Frequencies", + description="Number of sampling frequencies used in fitting the mode propagation index.", + gt=0, + le=MAX_BROADBAND_MODE_ABC_NUM_FREQS, ) - num_poles: pd.PositiveInt = pd.Field( - 1, - title="Number of Poles", - description="Number of poles to use to approximate the mode propagation index.", + +DEFAULT_BROADBAND_MODE_ABC_FITTER_PARAMS = BroadbandModeABCFitterParam() + + +class BroadbandModeABCSpec(Tidy3dBaseModel): + """Specifies the broadband mode absorption boundary conditions. The mode propagation index is approximated by a sum of pole-residue pairs. + + Example + ------- + >>> broadband_mode_abc_spec = BroadbandModeABCSpec(frequency_range=(fmin=100e12, fmax=120e12), fit_param=BroadbandModeABCFitterParam()) + """ + + frequency_range: FreqBound = pd.Field( + ..., + title="Frequency Range", + description="Frequency range for the broadband mode absorption boundary conditions.", + units=(HERTZ, HERTZ), ) + fit_param: BroadbandModeABCFitterParam = pd.Field( + DEFAULT_BROADBAND_MODE_ABC_FITTER_PARAMS, + title="Fitting Parameters For Broadband Mode Absorption Boundary Conditions", + description="Parameters for fitting the mode propagation index over the frequency range using pole-residue pair model.", + ) + + @pd.validator("frequency_range", always=True) + def validate_frequency_range(cls, val, values): + """Validate that max frequency is greater than min frequency.""" + if val[1] <= val[0]: + raise ValidationError("max frequency must be greater than min frequency.") + if val[0] <= 0: + raise ValidationError("min frequency must be greater than 0.") + return val + @classmethod - def from_wvl_range(cls, wvl_min: float, wvl_max: float, num_freqs: int = 3, num_poles: int = 1) -> BroadbandModeABCSpec: + def from_wavelength_range( + cls, + wavelength_range: FreqBound, + fit_param: BroadbandModeABCFitterParam = DEFAULT_BROADBAND_MODE_ABC_FITTER_PARAMS, + ) -> BroadbandModeABCSpec: """Instantiate from a wavelength range. - + Parameters ---------- - wvl_min : float - Minimum wavelength. - wvl_max : float - Maximum wavelength. - num_freqs : int = 3 - Number of frequencies to use to approximate the mode propagation index. - num_poles : int = 1 - Number of poles to use to approximate the mode propagation index. - - Returns + wavelength_range : FreqBound + Wavelength range for the broadband mode absorption boundary conditions. + fit_param : BroadbandModeABCFitterParam = DEFAULT_BROADBAND_MODE_ABC_FITTER_PARAMS + Parameters for fitting the mode propagation index over the frequency range using pole-residue pair model. + + Returns ------- :class:`BroadbandModeABCSpec` Broadband mode absorption boundary conditions. """ - return cls(fmin=C_0 / wvl_max, fmax=C_0 / wvl_min, num_freqs=num_freqs, num_poles=num_poles) + # check that min wavelength > 0 + if wavelength_range[0] <= 0: + raise SetupError("min wavelength must be greater than 0.") + # check that max wavelength > min wavelength + if wavelength_range[1] <= wavelength_range[0]: + raise SetupError("max wavelength must be greater than min wavelength.") + + return cls( + frequency_range=(C_0 / wavelength_range[1], C_0 / wavelength_range[0]), + fit_param=fit_param, + ) + class ModeABCBoundary(AbstractABCBoundary): """One-way wave equation absorbing boundary conditions for absorbing a waveguide mode.""" @@ -199,10 +251,10 @@ class ModeABCBoundary(AbstractABCBoundary): "``num_modes`` in the solver will be set to ``mode_index + 1``.", ) - frequency: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = pd.Field( + freq_spec: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = 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.", + title="Absorption Frequency Specification", + description="Specifies the frequency at which field is absorbed. If ``None``, then the central frequency of the source is used. If ``BroadbandModeABCSpec``, then the field is absorbed over the specified frequency range.", ) plane: Box = pd.Field( @@ -246,7 +298,7 @@ def from_source(cls, source: ModeSource) -> ModeABCBoundary: plane=source.bounding_box, mode_spec=source.mode_spec, mode_index=source.mode_index, - frequency=source.source_time.freq0, + freq_spec=source.source_time.freq0, ) @classmethod @@ -254,7 +306,7 @@ def from_monitor( cls, monitor: Union[ModeMonitor, ModeSolverMonitor], mode_index: pd.NonNegativeInt = 0, - frequency: Optional[pd.PositiveFloat] = None, + freq_spec: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None, ) -> ModeABCBoundary: """Instantiate from a ``ModeMonitor`` or ``ModeSolverMonitor``. @@ -264,8 +316,8 @@ def from_monitor( Mode monitor. mode_index : pd.NonNegativeInt = 0 Mode index. - frequency : Optional[pd.PositiveFloat] = None - Frequency for estimating propagation index of absorbed mode. + freq_spec : Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None + Specifies the frequency at which field is absorbed. If ``None``, then the central frequency of the source is used. If ``BroadbandModeABCSpec``, then the field is absorbed over the specified frequency range. Returns ------- @@ -283,7 +335,7 @@ def from_monitor( plane=monitor.bounding_box, mode_spec=monitor.mode_spec, mode_index=mode_index, - frequency=frequency, + freq_spec=freq_spec, ) @@ -1048,7 +1100,7 @@ def mode_abc( plane: Box, mode_spec: ModeSpec = DEFAULT_MODE_SPEC_MODE_ABC, mode_index: pd.NonNegativeInt = 0, - frequency: Optional[pd.PositiveFloat] = None, + freq_spec: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None, ): """One-way wave equation mode ABC boundary specification on both sides along a dimension. @@ -1060,8 +1112,8 @@ def mode_abc( Parameters that determine the modes computed by the mode solver. mode_index : pd.NonNegativeInt = 0 Mode index. - frequency : Optional[pd.PositiveFloat] = None - Frequency for estimating propagation index of absorbed mode. + freq_spec : Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None + Specifies the frequency at which field is absorbed. If ``None``, then the central frequency of the source is used. If ``BroadbandModeABCSpec``, then the field is absorbed over the specified frequency range. Example ------- @@ -1073,13 +1125,13 @@ def mode_abc( plane=plane, mode_spec=mode_spec, mode_index=mode_index, - frequency=frequency, + freq_spec=freq_spec, ) minus = ModeABCBoundary( plane=plane, mode_spec=mode_spec, mode_index=mode_index, - frequency=frequency, + freq_spec=freq_spec, ) return cls(plus=plus, minus=minus) @@ -1109,7 +1161,7 @@ def mode_abc_from_monitor( cls, monitor: Union[ModeMonitor, ModeSolverMonitor], mode_index: pd.NonNegativeInt = 0, - frequency: Optional[pd.PositiveFloat] = None, + freq_spec: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None, ): """One-way wave equation mode ABC boundary specification on both sides along a dimension constructed from a mode monitor. @@ -1122,12 +1174,12 @@ def mode_abc_from_monitor( plus = ModeABCBoundary.from_monitor( monitor=monitor, mode_index=mode_index, - frequency=frequency, + freq_spec=freq_spec, ) minus = ModeABCBoundary.from_monitor( monitor=monitor, mode_index=mode_index, - frequency=frequency, + freq_spec=freq_spec, ) return cls(plus=plus, minus=minus) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 61e1a63cc4..60d8411733 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -3104,7 +3104,7 @@ def _validate_frequency_mode_abc(cls, values): """Warn if ModeABCBoundary expects a frequency from a source, but there are multiple sources with different central frequencies.""" def boundary_needs_freq(boundary): - return (isinstance(boundary, ModeABCBoundary) and boundary.frequency is None) or ( + return (isinstance(boundary, ModeABCBoundary) and boundary.freq_spec is None) or ( isinstance(boundary, ABCBoundary) and ( (boundary.conductivity is not None and boundary.conductivity != 0) diff --git a/tidy3d/plugins/smatrix/component_modelers/terminal.py b/tidy3d/plugins/smatrix/component_modelers/terminal.py index 2551c339d3..b56f0372dd 100644 --- a/tidy3d/plugins/smatrix/component_modelers/terminal.py +++ b/tidy3d/plugins/smatrix/component_modelers/terminal.py @@ -8,6 +8,7 @@ import pydantic.v1 as pd from tidy3d.components.base import cached_property +from tidy3d.components.boundary import BroadbandModeABCSpec from tidy3d.components.data.data_array import DataArray, FreqDataArray from tidy3d.components.data.monitor_data import MonitorData from tidy3d.components.data.sim_data import SimulationData @@ -253,7 +254,8 @@ def base_sim(self) -> Simulation: wave_port.injection_axis ] + self._shift_value_signed(wave_port) port_absorber = wave_port.to_absorber( - snap_center=mode_src_pos, frequency=0.5 * (min(self.freqs) + max(self.freqs)) + snap_center=mode_src_pos, + freq_spec=BroadbandModeABCSpec(frequency_range=(self.freqs, self.freqs)), ) new_absorbers.append(port_absorber) diff --git a/tidy3d/plugins/smatrix/ports/wave.py b/tidy3d/plugins/smatrix/ports/wave.py index 641c68fd79..67849f0ef1 100644 --- a/tidy3d/plugins/smatrix/ports/wave.py +++ b/tidy3d/plugins/smatrix/ports/wave.py @@ -193,7 +193,7 @@ def to_mode_solver(self, simulation: Simulation, freqs: FreqArray) -> ModeSolver return mode_solver def to_absorber( - self, snap_center: Optional[float] = None, frequency: Optional[pd.NonNegativeFloat] = None + self, snap_center: Optional[float] = None, freq_spec: Optional[pd.NonNegativeFloat] = None ) -> InternalAbsorber: """Create an internal absorber from the wave port.""" center = list(self.center) @@ -206,7 +206,7 @@ def to_absorber( mode_spec=self.mode_spec, mode_index=self.mode_index, plane=self.bounding_box, - frequency=frequency, + frequency=freq_spec, ), direction="-" if self.direction == "+" From 9fd5ed4b1e5525a397f54c8844fabcd2936dbce4 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Tue, 5 Aug 2025 10:29:10 -0700 Subject: [PATCH 3/3] clean up --- tests/test_components/test_absorbers.py | 2 +- .../test_terminal_component_modeler.py | 43 +++++++++++++++++++ tidy3d/components/boundary.py | 29 ++++++++++--- .../smatrix/component_modelers/terminal.py | 4 +- tidy3d/plugins/smatrix/ports/wave.py | 23 ++++++---- 5 files changed, 83 insertions(+), 18 deletions(-) diff --git a/tests/test_components/test_absorbers.py b/tests/test_components/test_absorbers.py index eeee38c5f7..f9385a63c5 100644 --- a/tests/test_components/test_absorbers.py +++ b/tests/test_components/test_absorbers.py @@ -557,7 +557,7 @@ def test_abc_boundaries_broadband(): _ = td.BroadbandModeABCFitterParam(frequency_sampling_points=0) # test frequency sampling points <= 21 with pytest.raises(pydantic.ValidationError): - _ = td.BroadbandModeABCFitterParam(frequency_sampling_points=22) + _ = td.BroadbandModeABCFitterParam(frequency_sampling_points=102) # test basic instance fmin = 100e12 diff --git a/tests/test_plugins/smatrix/test_terminal_component_modeler.py b/tests/test_plugins/smatrix/test_terminal_component_modeler.py index edd78baf1b..e73f153e13 100644 --- a/tests/test_plugins/smatrix/test_terminal_component_modeler.py +++ b/tests/test_plugins/smatrix/test_terminal_component_modeler.py @@ -8,6 +8,7 @@ import xarray as xr import tidy3d as td +from tidy3d.components.boundary import BroadbandModeABCSpec from tidy3d.components.data.data_array import FreqDataArray from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError from tidy3d.plugins.microwave import ( @@ -1296,3 +1297,45 @@ def check_S_matrix(S_computed, S_expected, tol=1e-12): # Check power wave S matrix S_computed = modeler._internal_construct_smatrix(batch_data, s_param_def="power").values check_S_matrix(S_computed, S_power) + + +def test_wave_port_to_absorber(tmp_path): + """Test that wave port absorber can be specified as a boolean, ABCBoundary, or ModeABCBoundary.""" + + # test automatic absorber + modeler = make_coaxial_component_modeler( + path_dir=str(tmp_path), port_types=(WavePort, WavePort) + ) + sim = list(modeler.sim_dict.values())[0] + + absorber = sim.internal_absorbers[0] + + assert absorber.boundary_spec.mode_spec == modeler.ports[0].mode_spec + assert absorber.boundary_spec.mode_index == modeler.ports[0].mode_index + assert absorber.boundary_spec.plane == modeler.ports[0].geometry + assert absorber.boundary_spec.freq_spec == BroadbandModeABCSpec( + frequency_range=(np.min(modeler.freqs), np.max(modeler.freqs)) + ) + + # test to_absorber() + absorber = modeler.ports[0].to_absorber(freq_spec=1e9) + assert absorber.boundary_spec.freq_spec == 1e9 + + absorber = modeler.ports[0].to_absorber( + freq_spec=BroadbandModeABCSpec(frequency_range=(1e9, 2e9)) + ) + assert absorber.boundary_spec.freq_spec == BroadbandModeABCSpec(frequency_range=(1e9, 2e9)) + + # test no automatic absorber + modeler = modeler.updated_copy(ports=[modeler.ports[0].updated_copy(absorber=False)]) + sim = list(modeler.sim_dict.values())[0] + assert len(sim.internal_absorbers) == 0 + + # test custom boundary spec + custom_boundary_spec = td.ModeABCBoundary(plane=td.Box(size=(0.1, 0.1, 0)), freq_spec=1e9) + modeler = modeler.updated_copy( + ports=[modeler.ports[0].updated_copy(absorber=custom_boundary_spec)] + ) + sim = list(modeler.sim_dict.values())[0] + absorber = sim.internal_absorbers[0] + assert absorber.boundary_spec == custom_boundary_spec diff --git a/tidy3d/components/boundary.py b/tidy3d/components/boundary.py index e6c290cf9e..3bc745166c 100644 --- a/tidy3d/components/boundary.py +++ b/tidy3d/components/boundary.py @@ -59,7 +59,7 @@ def _warn_num_layers(cls, val): DEFAULT_BROADBAND_MODE_ABC_NUM_FREQS = 15 DEFAULT_BROADBAND_MODE_ABC_NUM_POLES = 5 MAX_BROADBAND_MODE_ABC_NUM_POLES = 10 -MAX_BROADBAND_MODE_ABC_NUM_FREQS = 21 +MAX_BROADBAND_MODE_ABC_NUM_FREQS = 101 class BoundaryEdge(ABC, Tidy3dBaseModel): @@ -175,7 +175,7 @@ class BroadbandModeABCSpec(Tidy3dBaseModel): Example ------- - >>> broadband_mode_abc_spec = BroadbandModeABCSpec(frequency_range=(fmin=100e12, fmax=120e12), fit_param=BroadbandModeABCFitterParam()) + >>> broadband_mode_abc_spec = BroadbandModeABCSpec(frequency_range=(100e12, 120e12), fit_param=BroadbandModeABCFitterParam()) """ frequency_range: FreqBound = pd.Field( @@ -273,13 +273,19 @@ def is_plane(cls, val): return val @classmethod - def from_source(cls, source: ModeSource) -> ModeABCBoundary: + def from_source( + cls, + source: ModeSource, + freq_spec: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None, + ) -> ModeABCBoundary: """Instantiate from a ``ModeSource``. Parameters ---------- source : :class:`ModeSource` Mode source. + freq_spec : Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None + Specifies the frequency at which field is absorbed. If ``None``, then the central frequency of the source is used. If ``BroadbandModeABCSpec``, then the field is absorbed over the specified frequency range. Returns ------- @@ -294,11 +300,14 @@ def from_source(cls, source: ModeSource) -> ModeABCBoundary: >>> abc_boundary = ModeABCBoundary.from_source(source=source) """ + if freq_spec is None: + freq_spec = source.source_time.freq0 + return cls( plane=source.bounding_box, mode_spec=source.mode_spec, mode_index=source.mode_index, - freq_spec=source.source_time.freq0, + freq_spec=freq_spec, ) @classmethod @@ -1137,13 +1146,19 @@ def mode_abc( return cls(plus=plus, minus=minus) @classmethod - def mode_abc_from_source(cls, source: ModeSource): + def mode_abc_from_source( + cls, + source: ModeSource, + freq_spec: Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None, + ): """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. + freq_spec : Optional[Union[pd.PositiveFloat, BroadbandModeABCSpec]] = None + Specifies the frequency at which field is absorbed. If ``None``, then the central frequency of the source is used. If ``BroadbandModeABCSpec``, then the field is absorbed over the specified frequency range. Example ------- @@ -1152,8 +1167,8 @@ def mode_abc_from_source(cls, source: ModeSource): >>> source = ModeSource(size=(1, 1, 0), source_time=pulse, direction='+') >>> abc = Boundary.mode_abc_from_source(source=source) """ - plus = ModeABCBoundary.from_source(source=source) - minus = ModeABCBoundary.from_source(source=source) + plus = ModeABCBoundary.from_source(source=source, freq_spec=freq_spec) + minus = ModeABCBoundary.from_source(source=source, freq_spec=freq_spec) return cls(plus=plus, minus=minus) @classmethod diff --git a/tidy3d/plugins/smatrix/component_modelers/terminal.py b/tidy3d/plugins/smatrix/component_modelers/terminal.py index b56f0372dd..48bf55f9b7 100644 --- a/tidy3d/plugins/smatrix/component_modelers/terminal.py +++ b/tidy3d/plugins/smatrix/component_modelers/terminal.py @@ -255,7 +255,9 @@ def base_sim(self) -> Simulation: ] + self._shift_value_signed(wave_port) port_absorber = wave_port.to_absorber( snap_center=mode_src_pos, - freq_spec=BroadbandModeABCSpec(frequency_range=(self.freqs, self.freqs)), + freq_spec=BroadbandModeABCSpec( + frequency_range=(np.min(self.freqs), np.max(self.freqs)) + ), ) new_absorbers.append(port_absorber) diff --git a/tidy3d/plugins/smatrix/ports/wave.py b/tidy3d/plugins/smatrix/ports/wave.py index 67849f0ef1..a98aeb1905 100644 --- a/tidy3d/plugins/smatrix/ports/wave.py +++ b/tidy3d/plugins/smatrix/ports/wave.py @@ -8,7 +8,7 @@ import pydantic.v1 as pd from tidy3d.components.base import cached_property, skip_if_fields_missing -from tidy3d.components.boundary import InternalAbsorber, ModeABCBoundary +from tidy3d.components.boundary import ABCBoundary, InternalAbsorber, ModeABCBoundary from tidy3d.components.data.data_array import FreqDataArray, FreqModeDataArray from tidy3d.components.data.monitor_data import ModeData from tidy3d.components.data.sim_data import SimulationData @@ -91,10 +91,11 @@ class WavePort(AbstractTerminalPort, Box): description="Add a thin frame around the source during FDTD run for an improved injection.", ) - absorber: bool = pd.Field( + absorber: Union[bool, ABCBoundary, ModeABCBoundary] = pd.Field( True, title="Absorber.", - description="Place a mode absorber in the port.", + description="Place a mode absorber in the port. If ``True``, an automatically generated mode absorber is placed in the port. " + "If ``ABCBoundary`` or ``ModeABCBoundary``, a mode absorber is placed in the port with the specified boundary conditions.", ) def _mode_voltage_coefficients(self, mode_data: ModeData) -> FreqModeDataArray: @@ -199,15 +200,19 @@ def to_absorber( center = list(self.center) if snap_center: center[self.injection_axis] = snap_center + if isinstance(self.absorber, (ABCBoundary, ModeABCBoundary)): + boundary_spec = self.absorber + else: + boundary_spec = ModeABCBoundary( + mode_spec=self.mode_spec, + mode_index=self.mode_index, + plane=self.geometry, + freq_spec=freq_spec, + ) return InternalAbsorber( center=center, size=self.size, - boundary_spec=ModeABCBoundary( - mode_spec=self.mode_spec, - mode_index=self.mode_index, - plane=self.bounding_box, - frequency=freq_spec, - ), + boundary_spec=boundary_spec, direction="-" if self.direction == "+" else "+", # absorb in the opposite direction of source