Skip to content

Commit c70cba7

Browse files
committed
Revert to num_freqs=1 for broadband angled gaussian beam
1 parent 994bdc2 commit c70cba7

File tree

5 files changed

+171
-37
lines changed

5 files changed

+171
-37
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Arrow lengths are now scaled consistently in the X and Y directions, and their lengths no longer exceed the height of the plot window.
1919
- Bug in `PlaneWave` defined with a negative `angle_theta` which would lead to wrong injection.
2020

21+
### Changed
22+
- `GaussianBeam` and `AstigmaticGaussianBeam` default `num_freqs` reset to 1 (it was set to 3 in v2.8.0) and a warning is issued for a broadband, angled beam for which `num_freqs` may not be sufficiently large.
23+
- Set the maximum `num_freqs` to 20 for all broadband sources (we have been warning about the introduction of this hard limit for a while).
24+
2125
## [2.9.0rc1] - 2025-06-10
2226

2327
### Added

tests/test_components/test_source.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,80 @@ def test_fixed_angle_source():
459459
)
460460

461461
assert not plane_wave._is_fixed_angle
462+
463+
464+
def test_broadband_angled_gaussian_warning():
465+
g = td.GaussianPulse(freq0=1e14, fwidth=0.8e14)
466+
# Case 1: num_freqs = 3, angle_theta = np.pi / 3, should warn
467+
with AssertLogLevel("WARNING", contains_str="number of frequencies"):
468+
s = td.GaussianBeam(
469+
size=(0, 1, 1),
470+
source_time=g,
471+
pol_angle=np.pi / 2,
472+
direction="+",
473+
angle_theta=np.pi / 3,
474+
num_freqs=3,
475+
)
476+
_ = td.Simulation(
477+
size=(2, 2, 2),
478+
run_time=1e-12,
479+
grid_spec=td.GridSpec.uniform(dl=0.1),
480+
sources=[s],
481+
normalize_index=None,
482+
)
483+
484+
# Case 2: Increasing to num_freqs = 10 should NOT warn
485+
with AssertLogLevel(None):
486+
s = td.GaussianBeam(
487+
size=(0, 1, 1),
488+
source_time=g,
489+
pol_angle=np.pi / 2,
490+
direction="+",
491+
angle_theta=np.pi / 3,
492+
num_freqs=10,
493+
)
494+
_ = td.Simulation(
495+
size=(2, 2, 2),
496+
run_time=1e-12,
497+
grid_spec=td.GridSpec.uniform(dl=0.1),
498+
sources=[s],
499+
normalize_index=None,
500+
)
501+
502+
# Case 3: Case 2 but changed to astigmatic gaussian beam with one larger waist size should warn
503+
with AssertLogLevel("WARNING", contains_str="number of frequencies"):
504+
s = td.AstigmaticGaussianBeam(
505+
size=(0, 1, 1),
506+
source_time=g,
507+
pol_angle=np.pi / 2,
508+
direction="+",
509+
angle_theta=np.pi / 3,
510+
num_freqs=10,
511+
waist_sizes=(1, 5),
512+
)
513+
_ = td.Simulation(
514+
size=(2, 2, 2),
515+
run_time=1e-12,
516+
grid_spec=td.GridSpec.uniform(dl=0.1),
517+
sources=[s],
518+
normalize_index=None,
519+
)
520+
521+
# Case 4: Case 3 but with num_freqs = 1 should NOT warn (broadband treatment is off)
522+
with AssertLogLevel(None):
523+
s = td.AstigmaticGaussianBeam(
524+
size=(0, 1, 1),
525+
source_time=g,
526+
pol_angle=np.pi / 2,
527+
direction="+",
528+
angle_theta=np.pi / 3,
529+
num_freqs=1,
530+
waist_sizes=(1, 5),
531+
)
532+
_ = td.Simulation(
533+
size=(2, 2, 2),
534+
run_time=1e-12,
535+
grid_spec=td.GridSpec.uniform(dl=0.1),
536+
sources=[s],
537+
normalize_index=None,
538+
)

tests/test_package/test_log.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,13 @@ def test_logging_warning_capture():
8585
name="mode",
8686
)
8787

88-
# 2 warnings: too high num_freqs; too many points
88+
# 1 warning: too many points
8989
mode_source = td.ModeSource(
9090
size=(domain_size, 0, domain_size),
9191
source_time=source_time,
9292
mode_spec=td.ModeSpec(num_modes=2, precision="single"),
9393
mode_index=1,
94-
num_freqs=50,
94+
num_freqs=10,
9595
direction="-",
9696
)
9797

@@ -218,7 +218,7 @@ def test_logging_warning_capture():
218218
sim.validate_pre_upload()
219219
warning_list = td.log.captured_warnings()
220220
print(json.dumps(warning_list, indent=4))
221-
assert len(warning_list) == 30
221+
assert len(warning_list) == 29
222222
td.log.set_capture(False)
223223

224224
# check that capture doesn't change validation errors

tidy3d/components/simulation.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3618,6 +3618,53 @@ def _source_homogeneous_isotropic(cls, val, values):
36183618
"A fixed angle plane wave can only be injected into a homogeneous isotropic"
36193619
"dispersionless medium."
36203620
)
3621+
# check if broadband angled gaussian beam frequency variation is too fast
3622+
if (
3623+
isinstance(source, (GaussianBeam, AstigmaticGaussianBeam))
3624+
and np.abs(source.angle_theta) > 0
3625+
and source.num_freqs > 1
3626+
):
3627+
3628+
def radius(waist_radius, waist_distance, k0):
3629+
"""Gaussian beam radius at a given waist distance and k0."""
3630+
z_r = waist_radius**2 * k0 / 2
3631+
return waist_radius * np.sqrt(1 + (waist_distance / z_r) ** 2)
3632+
3633+
# A slanted GaussianBeam will accumulate a phase that's frequency-dependent
3634+
# like phi = K f, with the derivative dphi / df = K = 2 * pi * n * r * sin(theta) / c_0.
3635+
# Here, we compute the maximum value of this coefficient computed at the waist radius
3636+
# and over all frequencies. Then we compare this to the frequency spacing to
3637+
# determine whether the frequency dependence is too fast, and issue a warning.
3638+
optical_path_length = []
3639+
freqs = source.frequency_grid
3640+
for freq in freqs:
3641+
n_freq, _ = src_medium.nk_model(frequency=freq)
3642+
k0 = 2 * np.pi * n_freq * freq / C_0
3643+
if isinstance(source, GaussianBeam):
3644+
rad = radius(source.waist_radius, source.waist_distance, k0)
3645+
else:
3646+
rad = max(
3647+
radius(source.waist_sizes[0], source.waist_distances[0], k0),
3648+
radius(source.waist_sizes[1], source.waist_distances[1], k0),
3649+
)
3650+
optical_path_length.append(n_freq * rad * np.sin(source.angle_theta))
3651+
# Maximum value of the path length over all freqs
3652+
max_path_length = np.max(optical_path_length)
3653+
# Maximum value of the phase difference
3654+
max_phase_diff = max_path_length * 2 * np.pi * (freqs[-1] - freqs[0]) / C_0
3655+
# Compare this in magnitude to the frequency spacing assuming uniform
3656+
# spacing. This is heuristic since in reality we use a Chebyshev grid,
3657+
# but it should be a good rule of thumb. Because the Chebyshev interpolation
3658+
# is much better than simple interpolation, we don't require << 1, just < 1
3659+
if not max_phase_diff / source.num_freqs < 1:
3660+
log.warning(
3661+
f"Broadband, angled {source.type} source has a phase dependence "
3662+
"with frequency that might be under-resolved by the provided "
3663+
"number of frequencies. Consider reducing the source bandwidth, "
3664+
"or increasing the 'num_freqs' of the source, and verify the "
3665+
"source injection in an empty simulation.",
3666+
)
3667+
36213668
return val
36223669

36233670
@pydantic.validator("normalize_index", always=True)

tidy3d/components/source/field.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727

2828
# width of Chebyshev grid used for broadband sources (in units of pulse width)
2929
CHEB_GRID_WIDTH = 1.5
30-
# Number of frequencies in a broadband source above which to issue a warning
31-
WARN_NUM_FREQS = 20
3230
# For broadband plane waves with constan in-plane k, the Chebyshev grid is truncated at
3331
# ``CRITICAL_FREQUENCY_FACTOR * f_crit``, where ``f_crit`` is the critical frequency
3432
# (oblique propagation).
@@ -90,14 +88,14 @@ def _dir_vector(self) -> tuple[float, float, float]:
9088
class BroadbandSource(Source, ABC):
9189
"""A source with frequency dependent field distributions."""
9290

93-
# Default as for analytic beam sources; overwrriten for ModeSource below
9491
num_freqs: int = pydantic.Field(
95-
3,
92+
1,
9693
title="Number of Frequency Points",
9794
description="Number of points used to approximate the frequency dependence of the injected "
98-
"field. Default is 3, which should cover even very broadband sources. For simulations "
99-
"which are not very broadband and the source is very large (e.g. metalens simulations), "
100-
"decreasing the value to 1 may lead to a speed up in the preprocessing.",
95+
"field. A Chebyshev interpolation is used, thus, only a small number of points is "
96+
"typically sufficient to obtain converged results. Note that larger values of 'num_freqs' "
97+
"could spread out the source time signal and introduce numerical noise, or prevent timely "
98+
"field decay.",
10199
ge=1,
102100
le=20,
103101
)
@@ -116,23 +114,6 @@ def _chebyshev_freq_grid(self, freq_min, freq_max):
116114
cheb_points = np.cos(np.pi * np.flip(uni_points))
117115
return freq_avg + freq_diff * cheb_points
118116

119-
@pydantic.validator("num_freqs", always=True, allow_reuse=True)
120-
def _warn_if_large_number_of_freqs(cls, val):
121-
"""Warn if a large number of frequency points is requested."""
122-
123-
if val is None:
124-
return val
125-
126-
if val >= WARN_NUM_FREQS:
127-
log.warning(
128-
f"A large number ({val}) of frequency points is used in a broadband source. "
129-
"This can lead to solver slow-down and increased cost, and even introduce "
130-
"numerical noise. This may become a hard limit in future Tidy3D versions.",
131-
custom_loc=["num_freqs"],
132-
)
133-
134-
return val
135-
136117

137118
""" Source current profiles determined by user-supplied data on a plane."""
138119

@@ -422,16 +403,6 @@ class ModeSource(DirectionalSource, PlanarSource, BroadbandSource):
422403
"``num_modes`` in the solver will be set to ``mode_index + 1``.",
423404
)
424405

425-
num_freqs: int = pydantic.Field(
426-
1,
427-
title="Number of Frequency Points",
428-
description="Number of points used to approximate the frequency dependence of injected "
429-
"field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less "
430-
"than 20, is typically sufficient to obtain converged results.",
431-
ge=1,
432-
le=99,
433-
)
434-
435406
@cached_property
436407
def angle_theta(self):
437408
"""Polar angle of propagation."""
@@ -515,6 +486,17 @@ class PlaneWave(AngledFieldSource, PlanarSource, BroadbandSource):
515486
discriminator=TYPE_TAG_STR,
516487
)
517488

489+
num_freqs: int = pydantic.Field(
490+
3,
491+
title="Number of Frequency Points",
492+
description="Number of points used to approximate the frequency dependence of the injected "
493+
"field. Default is 3, which should cover even very broadband plane waves. For simulations "
494+
"which are not very broadband and the source is very large (e.g. metalens simulations), "
495+
"decreasing the value to 1 may lead to a speed up in the preprocessing.",
496+
ge=1,
497+
le=20,
498+
)
499+
518500
@cached_property
519501
def _is_fixed_angle(self) -> bool:
520502
"""Whether the plane wave is at a fixed non-zero angle."""
@@ -594,6 +576,18 @@ class GaussianBeam(AngledFieldSource, PlanarSource, BroadbandSource):
594576
units=MICROMETER,
595577
)
596578

579+
num_freqs: int = pydantic.Field(
580+
1,
581+
title="Number of Frequency Points",
582+
description="Number of points used to approximate the frequency dependence of the injected "
583+
"field. For broadband, angled Gaussian beams it is advisable to check the beam propagation "
584+
"in an empty simulation to ensure there are no injection artifacts when 'num_freqs' > 1. "
585+
"Note that larger values of 'num_freqs' could spread out the source time signal and "
586+
"introduce numerical noise, or prevent timely field decay.",
587+
ge=1,
588+
le=20,
589+
)
590+
597591

598592
class AstigmaticGaussianBeam(AngledFieldSource, PlanarSource, BroadbandSource):
599593
"""The simple astigmatic Gaussian distribution allows
@@ -642,6 +636,18 @@ class AstigmaticGaussianBeam(AngledFieldSource, PlanarSource, BroadbandSource):
642636
units=MICROMETER,
643637
)
644638

639+
num_freqs: int = pydantic.Field(
640+
1,
641+
title="Number of Frequency Points",
642+
description="Number of points used to approximate the frequency dependence of the injected "
643+
"field. For broadband, angled Gaussian beams it is advisable to check the beam propagation "
644+
"in an empty simulation to ensure there are no injection artifacts when 'num_freqs' > 1. "
645+
"Note that larger values of 'num_freqs' could spread out the source time signal and "
646+
"introduce numerical noise, or prevent timely field decay.",
647+
ge=1,
648+
le=20,
649+
)
650+
645651

646652
class TFSF(AngledFieldSource, VolumeSource, BroadbandSource):
647653
"""Total-field scattered-field (TFSF) source that can inject a plane wave in a finite region.

0 commit comments

Comments
 (0)