From ca3b2b0e6858dbfd277b321eec28139b100629a9 Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Thu, 9 Oct 2025 16:18:00 -0700 Subject: [PATCH 1/2] feat(microwave): tidy3d_microwave and BroadbandPulse feature --- CHANGELOG.md | 2 + docs/api/sources.rst | 1 + pyproject.toml | 2 + tests/test_components/test_packaging.py | 21 ++++++++ tidy3d/__init__.py | 2 + tidy3d/components/source/time.py | 66 ++++++++++++++++++++++++- tidy3d/packaging.py | 46 +++++++++++++++++ 7 files changed, 139 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b2c93244..504c95cd90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added current integral specification classes: `AxisAlignedCurrentIntegralSpec`, `CompositeCurrentIntegralSpec`, and `Custom2DCurrentIntegralSpec`. - `sort_spec` in `ModeSpec` allows for fine-grained filtering and sorting of modes. This also deprecates `filter_pol`. The equivalent usage for example to `filter_pol="te"` is `sort_spec=ModeSortSpec(filter_key="TE_polarization", filter_reference=0.5)`. `ModeSpec.track_freq` has also been deprecated and moved to `ModeSortSpec.track_freq`. - Added `custom_source_time` parameter to `ComponentModeler` classes (`ModalComponentModeler` and `TerminalComponentModeler`), allowing specification of custom source time dependence. +- Added `tidy3d-microwave` package for enabling advanced microwave device simulation features. +- Introduced `BroadbandPulse` for exciting simulations across a wide frequency spectrum. ### Changed - Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`. diff --git a/docs/api/sources.rst b/docs/api/sources.rst index 4119e93dc6..b5cc48cdba 100644 --- a/docs/api/sources.rst +++ b/docs/api/sources.rst @@ -34,6 +34,7 @@ Source Time Dependence tidy3d.GaussianPulse tidy3d.ContinuousWave + tidy3d.BroadbandPulse tidy3d.SourceTime tidy3d.CustomSourceTime diff --git a/pyproject.toml b/pyproject.toml index 4ab08bad25..c743f24fa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,6 +118,7 @@ devsim = { version = "*", optional = true } cma = { version = "*", optional = true } openpyxl = { version = "*", optional = true } # tidy3d-extras = { version = "2.10.0rc3", optional = true } +# tidy3d-microwave = { version = "2.10.0rc3", optional = true } [tool.poetry.extras] dev = [ @@ -214,6 +215,7 @@ heatcharge = ["trimesh", "vtk", "devsim"] pytorch = ["torch"] design = ["bayesian-optimization", "pygad", "pyswarms"] # extras = ["tidy3d-extras"] +# microwave = ["tidy3d-microwave"] [tool.poetry.scripts] tidy3d = "tidy3d.web.cli:tidy3d_cli" diff --git a/tests/test_components/test_packaging.py b/tests/test_components/test_packaging.py index d51ad23c2b..ac8b12d7fc 100644 --- a/tests/test_components/test_packaging.py +++ b/tests/test_components/test_packaging.py @@ -6,7 +6,9 @@ Tidy3dImportError, check_import, supports_local_subpixel, + supports_microwave, tidy3d_extras, + tidy3d_microwave, verify_packages_import, ) @@ -90,5 +92,24 @@ def get_eps(): get_eps() +def test_tidy3d_microwave(): + import importlib + + has_tidy3d_microwave = importlib.util.find_spec("tidy3d_microwave") is not None + print(f"has_tidy3d_microwave = {has_tidy3d_microwave}") + + if has_tidy3d_microwave: + + @supports_microwave + def get_pulse(): + assert tidy3d_microwave["mod"] is not None + pulse = tidy3d_microwave["mod"].BroadbandPulse(fmin=0.1, fmax=10) + _ = pulse.frequency_range(2) + + get_pulse() + else: + assert tidy3d_microwave["mod"] is None + + if __name__ == "__main__": pytest.main() diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index d82c1735cb..f568c03146 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -405,6 +405,7 @@ # sources from .components.source.time import ( + BroadbandPulse, ContinuousWave, CustomSourceTime, GaussianPulse, @@ -510,6 +511,7 @@ def set_logging_level(level: str) -> None: "Box", "BroadbandModeABCFitterParam", "BroadbandModeABCSpec", + "BroadbandPulse", "CaugheyThomasMobility", "CellDataArray", "ChargeConductorMedium", diff --git a/tidy3d/components/source/time.py b/tidy3d/components/source/time.py index f2532ea3d7..9f4ad3b915 100644 --- a/tidy3d/components/source/time.py +++ b/tidy3d/components/source/time.py @@ -21,6 +21,7 @@ from tidy3d.constants import HERTZ from tidy3d.exceptions import ValidationError from tidy3d.log import log +from tidy3d.packaging import supports_microwave, tidy3d_microwave # how many units of ``twidth`` from the ``offset`` until a gaussian pulse is considered "off" END_TIME_FACTOR_GAUSSIAN = 10 @@ -605,4 +606,67 @@ def end_time(self) -> Optional[float]: return np.max(t_non_zero) -SourceTimeType = Union[GaussianPulse, ContinuousWave, CustomSourceTime] +class BroadbandPulse(SourceTime): + """A source time injecting significant energy in the entire custom frequency range.""" + + freq_range: FreqBound = pydantic.Field( + ..., + title="Frequency Range", + description="Frequency range where the pulse should have significant energy.", + units=HERTZ, + ) + minimum_amplitude: float = pydantic.Field( + 0.3, + title="Minimum Amplitude", + description="Minimum amplitude of the pulse relative to the peak amplitude in the frequency range.", + gt=0.05, + lt=0.5, + ) + offset: float = pydantic.Field( + 5.0, + title="Offset", + description="Time delay of the maximum value of the " + "pulse in units of 1 / (``2pi * fwidth``).", + ge=2.5, + ) + + @cached_property + @supports_microwave + def _source(self): + """Implementation of broadband pulse.""" + return tidy3d_microwave["mod"].BroadbandPulse( + fmin=self.freq_range[0], + fmax=self.freq_range[1], + minRelAmp=self.minimum_amplitude, + amp=self.amplitude, + phase=self.phase, + offset=self.offset, + ) + + def end_time(self) -> float: + """Time after which the source is effectively turned off / close to zero amplitude.""" + return self._source.end_time(END_TIME_FACTOR_GAUSSIAN) + + def amp_time(self, time: float) -> complex: + """Complex-valued source amplitude as a function of time.""" + return self._source.amp_time(time) + + def amp_freq(self, freq: float) -> complex: + """Complex-valued source amplitude as a function of frequency.""" + return self._source.amp_freq(freq) + + def frequency_range_sigma(self, sigma: float = DEFAULT_SIGMA) -> FreqBound: + """Frequency range where the source amplitude is within ``exp(-sigma**2/2)`` of the peak amplitude.""" + return self._source.frequency_range(sigma) + + def frequency_range(self, num_fwidth: float = DEFAULT_SIGMA) -> FreqBound: + """Frequency range where the source amplitude is within ``exp(-sigma**2/2)`` of the peak amplitude.""" + return self.frequency_range_sigma(num_fwidth) + + @cached_property + def _freq0(self) -> float: + """Central frequency from frequency range.""" + return np.mean(self.freq_range) + + +SourceTimeType = Union[GaussianPulse, ContinuousWave, CustomSourceTime, BroadbandPulse] diff --git a/tidy3d/packaging.py b/tidy3d/packaging.py index c85e560d42..c0c8ade859 100644 --- a/tidy3d/packaging.py +++ b/tidy3d/packaging.py @@ -28,6 +28,8 @@ tidy3d_extras = {"mod": None, "use_local_subpixel": None} +tidy3d_microwave = {"mod": None} + def check_import(module_name: str) -> bool: """ @@ -258,3 +260,47 @@ def _fn(*args, **kwargs): config.use_local_subpixel = use_local_subpixel return _fn + + +def supports_microwave(fn): + """When decorating a method, checks that 'tidy3d-microwave' is available.""" + + @functools.wraps(fn) + def _fn(*args, **kwargs): + # first try to import the module + if tidy3d_microwave["mod"] is None: + try: + import tidy3d_microwave as tidy3d_microwave_mod + + except ImportError as exc: + tidy3d_microwave["mod"] = None + raise Tidy3dImportError( + "The package 'tidy3d-microwave' is required for this " + "operation. " + "Please install the 'tidy3d-microwave' package using, for " + "example, 'pip install tidy3d[microwave]'." + ) from exc + + else: + version = tidy3d_microwave_mod.__version__ + + if version is None: + tidy3d_microwave["mod"] = None + raise Tidy3dImportError( + "The package 'tidy3d-microwave' did not initialize correctly, " + "likely due to an invalid API key." + ) + + if version != __version__: + log.warning( + "The package 'tidy3d-microwave' is required for this " + "operation. The version of 'tidy3d-microwave' should match " + "the version of 'tidy3d'. You can install the correct " + "version using 'pip install tidy3d[microwave]'." + ) + + tidy3d_microwave["mod"] = tidy3d_microwave_mod + + return fn(*args, **kwargs) + + return _fn From 41466740cb2f5951f6273fd3bc8b9a032665a88e Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Wed, 22 Oct 2025 09:49:28 -0700 Subject: [PATCH 2/2] num_freqs --- tidy3d/plugins/smatrix/ports/wave.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tidy3d/plugins/smatrix/ports/wave.py b/tidy3d/plugins/smatrix/ports/wave.py index 890c906446..975cf637da 100644 --- a/tidy3d/plugins/smatrix/ports/wave.py +++ b/tidy3d/plugins/smatrix/ports/wave.py @@ -108,6 +108,11 @@ class WavePort(AbstractTerminalPort, Box): description="Extrudes structures that intersect the wave port plane by a few grid cells when ``True``, improving mode injection accuracy.", ) + num_freqs: int = pd.Field( + 1, + title="Number of frequencies", + ) + def _mode_voltage_coefficients(self, mode_data: ModeData) -> FreqModeDataArray: """Calculates scaling coefficients to convert mode amplitudes to the total port voltage. @@ -166,6 +171,7 @@ def to_source( direction=self.direction, name=self.name, frame=self.frame, + num_freqs=self.num_freqs, ) def to_monitors(