Skip to content

Commit 098908e

Browse files
committed
Better validation of TFSF sources, and enabling broadband
1 parent 54a74b4 commit 098908e

File tree

5 files changed

+98
-41
lines changed

5 files changed

+98
-41
lines changed

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- A property `interior_angle` in `PolySlab` that stores angles formed inside polygon by two adjacent edges.
1414
- `eps_component` argument in `td.Simulation.plot_eps()` to optionally select a specific permittivity component to plot (eg. `"xx"`).
1515
- Monitor `AuxFieldTimeMonitor` for aux fields like the free carrier density in `TwoPhotonAbsorption`.
16+
- Broadband handling (`num_freqs` argument) to the TFSF source.
1617

1718
### Fixed
1819
- Compatibility with `xarray>=2025.03`.
1920
- Inaccurate gradient when auto-grabbing permittivities for structures using `td.PolySlab` when using dispersive material models.
2021
- Fixed scaling for adjoint sources when differentiating with respect to `FieldData` to account for the mesh size of the monitor and thus the created source. This aligns adjoint gradient magnitudes with numerical finite difference gradients for field data.
2122
- Warn when mode solver pml covers a significant portion of the mode plane.
23+
- TFSF server errors related to the auxiliary plane wave source that would previously happen on the server are now caught upon simulation creation.
24+
25+
### Changed
26+
- `num_freqs` in Gaussian beam type sources limited to 20, which should besufficient for all cases.
2227

2328
## [2.8.1] - 2025-03-20
2429

Diff for: tests/test_components/test_simulation.py

+24
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,30 @@ def test_tfsf_symmetry():
18871887
)
18881888

18891889

1890+
def test_tfsf_aux_source_outside_domain():
1891+
"""Test that a TFSF source cannot be too close to the simulation domain boundaries
1892+
along the injection direction."""
1893+
src_time = td.GaussianPulse(freq0=1e12, fwidth=0.1e12)
1894+
1895+
source = td.TFSF(
1896+
size=[1, 1, 1],
1897+
source_time=src_time,
1898+
pol_angle=0,
1899+
angle_theta=np.pi / 4,
1900+
angle_phi=np.pi / 6,
1901+
direction="+",
1902+
injection_axis=2,
1903+
)
1904+
1905+
with pytest.raises(SetupError):
1906+
_ = td.Simulation(
1907+
size=(2.0, 2.0, 1.01),
1908+
grid_spec=td.GridSpec.auto(wavelength=td.C_0 / 1.0),
1909+
run_time=1e-12,
1910+
sources=[source],
1911+
)
1912+
1913+
18901914
def test_tfsf_boundaries():
18911915
"""Test that a TFSF source is allowed to cross boundaries only in particular cases."""
18921916
src_time = td.GaussianPulse(freq0=td.C_0, fwidth=0.1e12)

Diff for: tests/test_package/test_log.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ def test_logging_warning_capture():
148148
medium=td.Medium(permittivity=6),
149149
)
150150

151-
# 1 warning: too high "num_freqs"
152151
# 1 warning: glancing angle
153152
gaussian_beam = td.GaussianBeam(
154153
center=(4, 0, 0),
@@ -157,7 +156,6 @@ def test_logging_warning_capture():
157156
waist_distance=1,
158157
source_time=source_time,
159158
direction="+",
160-
num_freqs=30,
161159
angle_theta=np.pi / 2.1,
162160
)
163161

@@ -217,7 +215,7 @@ def test_logging_warning_capture():
217215
sim.validate_pre_upload()
218216
warning_list = td.log.captured_warnings()
219217
print(json.dumps(warning_list, indent=4))
220-
assert len(warning_list) == 31
218+
assert len(warning_list) == 30
221219
td.log.set_capture(False)
222220

223221
# check that capture doesn't change validation errors

Diff for: tidy3d/components/simulation.py

+50-1
Original file line numberDiff line numberDiff line change
@@ -2657,7 +2657,7 @@ def bloch_boundaries_diff_mnt(cls, val, values):
26572657
@pydantic.validator("boundary_spec", always=True)
26582658
@skip_if_fields_missing(["medium", "center", "size", "structures", "sources"])
26592659
def tfsf_boundaries(cls, val, values):
2660-
"""Error if the boundary conditions are compatible with TFSF sources, if any."""
2660+
"""Error if the boundary conditions are incompatible with TFSF sources, if any."""
26612661
boundaries = val.to_list
26622662
sources = values.get("sources")
26632663
size = values.get("size")
@@ -3572,6 +3572,7 @@ def _post_init_validators(self) -> None:
35723572
_ = self.scene
35733573
self._validate_no_structures_pml()
35743574
self._validate_tfsf_nonuniform_grid()
3575+
self._validate_tfsf_aux_sources()
35753576
self._validate_nonlinear_specs()
35763577
self._validate_custom_source_time()
35773578
self._validate_mode_object_bends()
@@ -3707,6 +3708,54 @@ def _validate_tfsf_nonuniform_grid(self) -> None:
37073708
custom_loc=["sources", source_ind],
37083709
)
37093710

3711+
def _aux_tfsf_source(self, source: TFSF) -> PlaneWave:
3712+
"""Create the auxiliary plane wave source for a give TFSF source."""
3713+
# center and size of the plane wave source
3714+
source_size = [inf] * 3
3715+
source_size[source.injection_axis] = 0
3716+
source_center = list(source.injection_plane_center)
3717+
3718+
# since we need to access values of the aux self at dual grid locations below the actual
3719+
# injection plane, we need to place the aux sim's source at least one full cell below the
3720+
# location of the injection plane; for good measure, we'll offset the source by two cells
3721+
src_grid = self.discretize(source, extend=False)
3722+
src_grid_sizes = src_grid.sizes.to_list
3723+
if source.direction == "+":
3724+
offset = -sum(src_grid_sizes[source.injection_axis][0:2])
3725+
else:
3726+
offset = sum(src_grid_sizes[source.injection_axis][-1:-3:-1])
3727+
source_center[source.injection_axis] += offset
3728+
3729+
# Make sure that the new source center is within the simulation bounds
3730+
sim_axis_bounds = [self.bounds[i][source.injection_axis] for i in range(2)]
3731+
if (
3732+
source_center[source.injection_axis] < sim_axis_bounds[0]
3733+
or source_center[source.injection_axis] > sim_axis_bounds[1]
3734+
):
3735+
raise SetupError(
3736+
"The TFSF source is too close to the simulation domain boundary along the "
3737+
"injection axis. Slightly increase the simulation domain size along that "
3738+
"dimension, or decrease the source size."
3739+
)
3740+
3741+
# Note: broadband injection for TFSF not currently supported
3742+
return PlaneWave(
3743+
size=source_size,
3744+
center=source_center,
3745+
source_time=source.source_time,
3746+
angle_theta=source.angle_theta,
3747+
angle_phi=source.angle_phi,
3748+
pol_angle=source.pol_angle,
3749+
direction=source.direction,
3750+
num_freqs=source.num_freqs,
3751+
)
3752+
3753+
def _validate_tfsf_aux_sources(self):
3754+
"""Validate that PlaneWave sources auxiliary to TFSF sources can be successfully created."""
3755+
for source in self.sources:
3756+
if isinstance(source, TFSF):
3757+
_ = self._aux_tfsf_source(source)
3758+
37103759
def _validate_nonlinear_specs(self) -> None:
37113760
"""Run :class:`.NonlinearSpec` validators that depend on knowing the central
37123761
frequencies of the sources. Also print some warnings only once per unique medium."""

Diff for: tidy3d/components/source/field.py

+18-37
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,16 @@ def _dir_vector(self) -> Tuple[float, float, float]:
9595
class BroadbandSource(Source, ABC):
9696
"""A source with frequency dependent field distributions."""
9797

98+
# Default as for analytic beam sources; overwrriten for ModeSource below
9899
num_freqs: int = pydantic.Field(
99-
1,
100+
3,
100101
title="Number of Frequency Points",
101-
description="Number of points used to approximate the frequency dependence of injected "
102-
"field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less "
103-
"than 20, is typically sufficient to obtain converged results.",
102+
description="Number of points used to approximate the frequency dependence of the injected "
103+
"field. Default is 3, which should cover even very broadband sources. For simulations "
104+
"which are not very broadband and the source is very large (e.g. metalens simulations), "
105+
"decreasing the value to 1 may lead to a speed up in the preprocessing.",
104106
ge=1,
105-
le=99,
107+
le=20,
106108
)
107109

108110
@cached_property
@@ -425,6 +427,16 @@ class ModeSource(DirectionalSource, PlanarSource, BroadbandSource):
425427
"``num_modes`` in the solver will be set to ``mode_index + 1``.",
426428
)
427429

430+
num_freqs: int = pydantic.Field(
431+
1,
432+
title="Number of Frequency Points",
433+
description="Number of points used to approximate the frequency dependence of injected "
434+
"field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less "
435+
"than 20, is typically sufficient to obtain converged results.",
436+
ge=1,
437+
le=99,
438+
)
439+
428440
@cached_property
429441
def angle_theta(self):
430442
"""Polar angle of propagation."""
@@ -500,17 +512,6 @@ class PlaneWave(AngledFieldSource, PlanarSource, BroadbandSource):
500512
discriminator=TYPE_TAG_STR,
501513
)
502514

503-
num_freqs: int = pydantic.Field(
504-
3,
505-
title="Number of Frequency Points",
506-
description="Number of points used to approximate the frequency dependence of the injected "
507-
"field. Default is 3, which should cover even very broadband sources. For simulations "
508-
"which are not very broadband and the source is very large (e.g. metalens simulations), "
509-
"decreasing the value to 1 may lead to a speed up in the preprocessing.",
510-
ge=1,
511-
le=10,
512-
)
513-
514515
@cached_property
515516
def _is_fixed_angle(self) -> bool:
516517
"""Whether the plane wave is at a fixed non-zero angle."""
@@ -590,16 +591,6 @@ class GaussianBeam(AngledFieldSource, PlanarSource, BroadbandSource):
590591
units=MICROMETER,
591592
)
592593

593-
num_freqs: int = pydantic.Field(
594-
3,
595-
title="Number of Frequency Points",
596-
description="Number of points used to approximate the frequency dependence of injected "
597-
"field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less "
598-
"than 20, is typically sufficient to obtain converged results.",
599-
ge=1,
600-
le=99,
601-
)
602-
603594

604595
class AstigmaticGaussianBeam(AngledFieldSource, PlanarSource, BroadbandSource):
605596
"""The simple astigmatic Gaussian distribution allows
@@ -648,18 +639,8 @@ class AstigmaticGaussianBeam(AngledFieldSource, PlanarSource, BroadbandSource):
648639
units=MICROMETER,
649640
)
650641

651-
num_freqs: int = pydantic.Field(
652-
3,
653-
title="Number of Frequency Points",
654-
description="Number of points used to approximate the frequency dependence of injected "
655-
"field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less "
656-
"than 20, is typically sufficient to obtain converged results.",
657-
ge=1,
658-
le=99,
659-
)
660-
661642

662-
class TFSF(AngledFieldSource, VolumeSource):
643+
class TFSF(AngledFieldSource, VolumeSource, BroadbandSource):
663644
"""Total-field scattered-field (TFSF) source that can inject a plane wave in a finite region.
664645
665646
Notes

0 commit comments

Comments
 (0)