[docs] clarify FDFD-to-FDTD field reconstruction

This commit is contained in:
Forgejo Actions 2026-04-19 16:15:06 -07:00
commit 40efe7a450
5 changed files with 51 additions and 5 deletions

View file

@ -194,6 +194,10 @@ For a broadband or continuous-wave FDTD run:
part of the simulation, and compare the extracted phasor to the FDFD field or part of the simulation, and compare the extracted phasor to the FDFD field or
residual. residual.
This is the primary FDTD/FDFD equivalence workflow. The phasor extraction step
filters the time-domain run down to the guided `+\omega` content that FDFD
solves for directly, so it is the cleanest apples-to-apples comparison.
### Real-field reconstruction workflow ### Real-field reconstruction workflow
For a continuous-wave real-valued FDTD run: For a continuous-wave real-valued FDTD run:
@ -211,4 +215,14 @@ For a continuous-wave real-valued FDTD run:
pieces to see whether the remaining mismatch is actually in the mode or in pieces to see whether the remaining mismatch is actually in the mode or in
weak nonguided tails. weak nonguided tails.
This is a stricter diagnostic, not the primary equivalence benchmark. A raw
monitor slice contains both the guided field and the remaining orthogonal
content on that plane,
$$ E_{\text{monitor}} = E_{\text{guided}} + E_{\text{residual}} , $$
so its full-plane instantaneous error is naturally noisier than the extracted
phasor comparison even when the underlying guided `+\omega` content matches
well.
`examples/waveguide_real.py` is the reference implementation of this workflow. `examples/waveguide_real.py` is the reference implementation of this workflow.

View file

@ -26,6 +26,15 @@ Relevant starting examples:
mode-weighted, and guided-mode / residual comparisons mode-weighted, and guided-mode / residual comparisons
- `examples/fdfd.py` for direct frequency-domain waveguide excitation - `examples/fdfd.py` for direct frequency-domain waveguide excitation
For solver equivalence, prefer the phasor-based examples first. They compare
the extracted `+\omega` content of the FDTD run directly against the FDFD
solution and are the main accuracy benchmarks in the test suite.
`examples/waveguide_real.py` answers a different, stricter question: how well a
late raw real snapshot matches `Re(E_\omega e^{i\omega t})` on a monitor plane.
That diagnostic is useful, but it also includes orthogonal residual structure
that the phasor comparison intentionally filters out.
The API pages are better read as a toolbox map and derivation reference: The API pages are better read as a toolbox map and derivation reference:
- Use the [FDTD API](api/fdtd.md) for time-domain stepping, CPML, phasor - Use the [FDTD API](api/fdtd.md) for time-domain stepping, CPML, phasor

View file

@ -9,9 +9,12 @@ FDFD" workflow:
3. solve the matching FDFD problem from the analytic source phasor, and 3. solve the matching FDFD problem from the analytic source phasor, and
4. compare late real monitor slices against `fdtd.reconstruct_real_e/h(...)`. 4. compare late real monitor slices against `fdtd.reconstruct_real_e/h(...)`.
Unlike the complex-source examples, this script does not use phasor extraction Unlike the phasor-based examples, this script does not use extracted phasors as
as the main output. The comparison target is the real field itself, with both the main output. It is a stricter diagnostic: the comparison target is the raw
full-plane and mode-weighted monitor errors reported. real field itself, with full-plane, mode-weighted, guided-mode, and orthogonal-
residual errors reported. Strong phasor agreement can coexist with visibly
larger raw-snapshot error because the latter includes weak nonguided tails on
the monitor plane.
""" """
import numpy import numpy
@ -104,7 +107,7 @@ def main() -> None:
epsilon=epsilon, epsilon=epsilon,
) )
# A small global phase aligns the real-valued source with the late-cycle # A small global phase aligns the real-valued source with the late-cycle
# monitor comparison. The underlying phasor problem is unchanged. # raw-snapshot diagnostic. The underlying phasor problem is unchanged.
j_mode *= numpy.exp(1j * SOURCE_PHASE) j_mode *= numpy.exp(1j * SOURCE_PHASE)
monitor_mode = waveguide_3d.solve_mode( monitor_mode = waveguide_3d.solve_mode(
0, 0,

View file

@ -165,6 +165,11 @@ with caller-provided sample times and weights. In this codebase the matching
electric-current convention is typically `E -= dt * J / epsilon` in FDTD and electric-current convention is typically `E -= dt * J / epsilon` in FDTD and
`-i \omega J` on the right-hand side of the FDFD wave equation. `-i \omega J` on the right-hand side of the FDFD wave equation.
For FDTD/FDFD equivalence, this phasor path is the primary comparison workflow.
It isolates the guided `+\omega` response that the frequency-domain solver
targets directly, regardless of whether the underlying FDTD run used real- or
complex-valued fields.
For exact pulsed FDTD/FDFD equivalence it is often simplest to keep the For exact pulsed FDTD/FDFD equivalence it is often simplest to keep the
injected source, fields, and CPML auxiliary state complex-valued. The injected source, fields, and CPML auxiliary state complex-valued. The
`real_injection_scale(...)` helper is instead for the more ordinary one-run `real_injection_scale(...)` helper is instead for the more ordinary one-run
@ -172,6 +177,17 @@ real-valued source path, where the intended positive-frequency waveform is
injected through `numpy.real(scale * waveform)` and any remaining negative- injected through `numpy.real(scale * waveform)` and any remaining negative-
frequency leakage is controlled by the pulse bandwidth and run length. frequency leakage is controlled by the pulse bandwidth and run length.
`reconstruct_real(...)` is for a different question: given a phasor, what late
real-valued field snapshot should it produce? That raw-snapshot comparison is
stricter and noisier because a monitor plane generally contains both the guided
field and the remaining orthogonal content,
$$ E_{\text{monitor}} = E_{\text{guided}} + E_{\text{residual}} . $$
Phasor/modal comparisons mostly validate the guided `+\omega` term. Raw
real-field comparisons expose both terms at once, so they should be treated as
secondary diagnostics rather than the main solver-equivalence benchmark.
The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse The Ricker wavelet (normalized second derivative of a Gaussian) is commonly used for the pulse
shape. It can be written shape. It can be written

View file

@ -711,7 +711,11 @@ def test_straight_waveguide_fdtd_fdfd_overlap_and_flux_agree() -> None:
assert result.overlap_phase_deg < 0.5 assert result.overlap_phase_deg < 0.5
def test_straight_waveguide_real_monitor_fields_match_reconstructed_real_fields() -> None: def test_straight_waveguide_real_snapshot_diagnostic_tracks_guided_content_and_bounded_residual() -> None:
# The phasor-based waveguide tests above are the primary FDTD/FDFD
# equivalence benchmark. This raw real-field check is intentionally stricter:
# it validates that late monitor snapshots keep the guided content close to
# the reconstructed FDFD field while the orthogonal residual stays bounded.
result = _run_real_field_straight_waveguide_case() result = _run_real_field_straight_waveguide_case()
ranked_snapshots = sorted( ranked_snapshots = sorted(
result.snapshots, result.snapshots,