diff --git a/tests/test_components/test_absorbers.py b/tests/test_components/test_absorbers.py index 7c17b7663d..f9385a63c5 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=102) + + # 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/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/__init__.py b/tidy3d/__init__.py index 92a7f42b09..865253b1d4 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from tidy3d.components.boundary import BroadbandModeABCFitterParam, BroadbandModeABCSpec from tidy3d.components.material.multi_physics import MultiPhysicsMedium from tidy3d.components.material.tcad.charge import ( ChargeConductorMedium, @@ -446,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 16158e47ff..3bc745166c 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, 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 = 101 class BoundaryEdge(ABC, Tidy3dBaseModel): @@ -126,6 +131,108 @@ def _conductivity_only_with_float_permittivity(cls, val, values): return val +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 + ------- + >>> fitter_param = BroadbandModeABCFitterParam(max_num_poles=5, tolerance_rms=1e-4, frequency_sampling_points=10) + """ + + 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, + ) + + tolerance_rms: pd.NonNegativeFloat = pd.Field( + DEFAULT_BROADBAND_MODE_ABC_FITTER_TOLERANCE, + title="Fitting Tolerance", + description="Tolerance in fitting the mode propagation index.", + ) + + 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, + ) + + +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=(100e12, 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_wavelength_range( + cls, + wavelength_range: FreqBound, + fit_param: BroadbandModeABCFitterParam = DEFAULT_BROADBAND_MODE_ABC_FITTER_PARAMS, + ) -> BroadbandModeABCSpec: + """Instantiate from a wavelength range. + + Parameters + ---------- + 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. + """ + # 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.""" @@ -144,10 +251,10 @@ class ModeABCBoundary(AbstractABCBoundary): "``num_modes`` in the solver will be set to ``mode_index + 1``.", ) - frequency: Optional[pd.PositiveFloat] = 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( @@ -166,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 ------- @@ -187,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, - frequency=source.source_time.freq0, + freq_spec=freq_spec, ) @classmethod @@ -199,7 +315,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``. @@ -209,8 +325,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 ------- @@ -228,7 +344,7 @@ def from_monitor( plane=monitor.bounding_box, mode_spec=monitor.mode_spec, mode_index=mode_index, - frequency=frequency, + freq_spec=freq_spec, ) @@ -993,7 +1109,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. @@ -1005,8 +1121,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 ------- @@ -1018,25 +1134,31 @@ 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) @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 ------- @@ -1045,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 @@ -1054,7 +1176,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. @@ -1067,12 +1189,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..48bf55f9b7 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,10 @@ 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=(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 641c68fd79..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: @@ -193,21 +194,25 @@ 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) 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=frequency, - ), + boundary_spec=boundary_spec, direction="-" if self.direction == "+" else "+", # absorb in the opposite direction of source