Skip to content

Commit 026198a

Browse files
feat(source_time): broadband pulse
1 parent c73a140 commit 026198a

File tree

5 files changed

+160
-41
lines changed

5 files changed

+160
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- Added `DirectivityMonitorSpec` for automated creation and configuration of directivity radiation monitors in `TerminalComponentModeler`.
3131
- Added multimode support to `WavePort` in the smatrix plugin, allowing multiple modes to be analyzed per port.
3232
- Added support for `.lydrc` files for design rule checking in the `klayout` plugin.
33+
- Introduced `BroadbandPulse` for exciting simulations across a wide frequency spectrum.
3334

3435
### Breaking Changes
3536
- Edge singularity correction at PEC and lossy metal edges defaults to `True`.

docs/api/sources.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Source Time Dependence
3434

3535
tidy3d.GaussianPulse
3636
tidy3d.ContinuousWave
37+
tidy3d.BroadbandPulse
3738
tidy3d.SourceTime
3839
tidy3d.CustomSourceTime
3940

tidy3d/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@
411411

412412
# sources
413413
from .components.source.time import (
414+
BroadbandPulse,
414415
ContinuousWave,
415416
CustomSourceTime,
416417
GaussianPulse,
@@ -516,6 +517,7 @@ def set_logging_level(level: str) -> None:
516517
"Box",
517518
"BroadbandModeABCFitterParam",
518519
"BroadbandModeABCSpec",
520+
"BroadbandPulse",
519521
"CaugheyThomasMobility",
520522
"CellDataArray",
521523
"ChargeConductorMedium",

tidy3d/components/source/time.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from tidy3d.constants import HERTZ
2222
from tidy3d.exceptions import ValidationError
2323
from tidy3d.log import log
24+
from tidy3d.packaging import check_tidy3d_extras_licensed_feature, tidy3d_extras
2425

2526
# how many units of ``twidth`` from the ``offset`` until a gaussian pulse is considered "off"
2627
END_TIME_FACTOR_GAUSSIAN = 10
@@ -605,4 +606,83 @@ def end_time(self) -> Optional[float]:
605606
return np.max(t_non_zero)
606607

607608

608-
SourceTimeType = Union[GaussianPulse, ContinuousWave, CustomSourceTime]
609+
class BroadbandPulse(SourceTime):
610+
"""A source time injecting significant energy in the entire custom frequency range."""
611+
612+
freq_range: FreqBound = pydantic.Field(
613+
...,
614+
title="Frequency Range",
615+
description="Frequency range where the pulse should have significant energy.",
616+
units=HERTZ,
617+
)
618+
minimum_amplitude: float = pydantic.Field(
619+
0.3,
620+
title="Minimum Amplitude",
621+
description="Minimum amplitude of the pulse relative to the peak amplitude in the frequency range.",
622+
gt=0.05,
623+
lt=0.5,
624+
)
625+
offset: float = pydantic.Field(
626+
5.0,
627+
title="Offset",
628+
description="Time delay of the maximum value of the "
629+
"pulse in units of 1 / (``2pi * fwidth``).",
630+
ge=2.5,
631+
)
632+
633+
@pydantic.validator("freq_range", always=True)
634+
def _validate_freq_range(cls, val):
635+
"""Validate that freq_range is positive and properly ordered."""
636+
if val[0] <= 0 or val[1] <= 0:
637+
raise ValidationError("Both elements of 'freq_range' must be positive.")
638+
if val[1] <= val[0]:
639+
raise ValidationError(
640+
f"'freq_range[1]' ({val[1]}) must be greater than 'freq_range[0]' ({val[0]})."
641+
)
642+
return val
643+
644+
@pydantic.root_validator()
645+
def _check_broadband_pulse_available(cls, values):
646+
"""Check if BroadbandPulse is available."""
647+
check_tidy3d_extras_licensed_feature("BroadbandPulse")
648+
return values
649+
650+
@cached_property
651+
def _source(self):
652+
"""Implementation of broadband pulse."""
653+
return tidy3d_extras["mod"].extension.BroadbandPulse(
654+
fmin=self.freq_range[0],
655+
fmax=self.freq_range[1],
656+
minRelAmp=self.minimum_amplitude,
657+
amp=self.amplitude,
658+
phase=self.phase,
659+
offset=self.offset,
660+
)
661+
662+
def end_time(self) -> float:
663+
"""Time after which the source is effectively turned off / close to zero amplitude."""
664+
return self._source.end_time(END_TIME_FACTOR_GAUSSIAN)
665+
666+
def amp_time(self, time: float) -> complex:
667+
"""Complex-valued source amplitude as a function of time."""
668+
return self._source.amp_time(time)
669+
670+
def amp_freq(self, freq: float) -> complex:
671+
"""Complex-valued source amplitude as a function of frequency."""
672+
return self._source.amp_freq(freq)
673+
674+
def frequency_range_sigma(self, sigma: float = DEFAULT_SIGMA) -> FreqBound:
675+
"""Frequency range where the source amplitude is within ``exp(-sigma**2/2)`` of the peak amplitude."""
676+
return self._source.frequency_range(sigma)
677+
678+
def frequency_range(self, num_fwidth: float = DEFAULT_SIGMA) -> FreqBound:
679+
"""Frequency range where the source amplitude is within ``exp(-sigma**2/2)`` of the peak amplitude."""
680+
return self.frequency_range_sigma(num_fwidth)
681+
682+
@cached_property
683+
def _freq0(self) -> float:
684+
"""Central frequency from frequency range."""
685+
return np.mean(self.freq_range)
686+
687+
688+
SourceTimeType = Union[GaussianPulse, ContinuousWave, CustomSourceTime, BroadbandPulse]

tidy3d/packaging.py

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,74 @@ def get_numpy_major_version(module=np):
183183
return major_version
184184

185185

186+
def _check_tidy3d_extras_available():
187+
"""Helper function to check if 'tidy3d-extras' is available and version matched.
188+
189+
Raises
190+
------
191+
Tidy3dImportError
192+
If tidy3d-extras is not available or not properly initialized.
193+
"""
194+
if tidy3d_extras["mod"] is None:
195+
try:
196+
import tidy3d_extras as tidy3d_extras_mod
197+
198+
except ImportError as exc:
199+
tidy3d_extras["mod"] = None
200+
raise Tidy3dImportError(
201+
"The package 'tidy3d-extras' is required for this "
202+
"operation. Please install the 'tidy3d-extras' package using, for "
203+
"example, 'pip install tidy3d[extras]'."
204+
) from exc
205+
206+
else:
207+
version = tidy3d_extras_mod.__version__
208+
209+
if version is None:
210+
tidy3d_extras["mod"] = None
211+
raise Tidy3dImportError(
212+
"The package 'tidy3d-extras' did not initialize correctly, "
213+
"likely due to an invalid API key."
214+
)
215+
216+
if version != __version__:
217+
log.warning(
218+
"The package 'tidy3d-extras' is required for this "
219+
"operation. The version of 'tidy3d-extras' should match "
220+
"the version of 'tidy3d'. You can install the correct "
221+
"version using 'pip install tidy3d[extras]'."
222+
)
223+
224+
tidy3d_extras["mod"] = tidy3d_extras_mod
225+
226+
227+
def check_tidy3d_extras_licensed_feature(feature_name: str):
228+
"""Helper function to check if a specific feature is licensed in 'tidy3d-extras'.
229+
230+
Parameters
231+
----------
232+
feature_name : str
233+
The name of the feature to check for.
234+
235+
Raises
236+
------
237+
Tidy3dImportError
238+
If the feature is not available with your license.
239+
"""
240+
241+
try:
242+
_check_tidy3d_extras_available()
243+
except Tidy3dImportError as exc:
244+
raise Tidy3dImportError(f"Failed to load 'tidy3d-extras'. {exc!s}") from exc
245+
else:
246+
features = tidy3d_extras["mod"].extension._features()
247+
if feature_name not in features:
248+
raise Tidy3dImportError(
249+
f"The feature '{feature_name}' is not available with your license. "
250+
"Please contact Tidy3D support, or upgrade your license."
251+
)
252+
253+
186254
def supports_local_subpixel(fn):
187255
"""When decorating a method, checks that 'tidy3d-extras' is available,
188256
conditioned on 'config.use_local_subpixel'."""
@@ -193,47 +261,14 @@ def _fn(*args: Any, **kwargs: Any):
193261

194262
if preference is False:
195263
tidy3d_extras["use_local_subpixel"] = False
196-
tidy3d_extras["mod"] = None
197264
else:
198-
# first try to import the module
199-
if tidy3d_extras["mod"] is None:
200-
try:
201-
import tidy3d_extras as tidy3d_extras_mod
202-
203-
except ImportError as exc:
204-
tidy3d_extras["mod"] = None
205-
tidy3d_extras["use_local_subpixel"] = False
206-
if preference is True:
207-
raise Tidy3dImportError(
208-
"The package 'tidy3d-extras' is required for this "
209-
"operation when 'config.use_local_subpixel' is 'True'. "
210-
"Please install the 'tidy3d-extras' package using, for "
211-
"example, 'pip install tidy3d[extras]'."
212-
) from exc
213-
214-
else:
215-
version = tidy3d_extras_mod.__version__
216-
217-
if version is None:
218-
tidy3d_extras["mod"] = None
219-
tidy3d_extras["use_local_subpixel"] = False
220-
raise Tidy3dImportError(
221-
"The package 'tidy3d-extras' did not initialize correctly, "
222-
"likely due to an invalid API key."
223-
)
224-
225-
if version != __version__:
226-
log.warning(
227-
"The package 'tidy3d-extras' is required for this "
228-
"operation. The version of 'tidy3d-extras' should match "
229-
"the version of 'tidy3d'. You can install the correct "
230-
"version using 'pip install tidy3d[extras]'."
231-
)
232-
233-
features = tidy3d_extras_mod.extension._features()
234-
235-
tidy3d_extras["mod"] = tidy3d_extras_mod
236-
tidy3d_extras["use_local_subpixel"] = "local_subpixel" in features
265+
try:
266+
_check_tidy3d_extras_available()
267+
except Tidy3dImportError as exc:
268+
tidy3d_extras["use_local_subpixel"] = False
269+
raise Tidy3dImportError(
270+
f"Failed to load 'tidy3d-extras' for local subpixel support. {exc!s}"
271+
) from exc
237272
else:
238273
features = tidy3d_extras["mod"].extension._features()
239274
tidy3d_extras["use_local_subpixel"] = "local_subpixel" in features

0 commit comments

Comments
 (0)