[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
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
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
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.

View file

@ -26,6 +26,15 @@ Relevant starting examples:
mode-weighted, and guided-mode / residual comparisons
- `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:
- 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
4. compare late real monitor slices against `fdtd.reconstruct_real_e/h(...)`.
Unlike the complex-source examples, this script does not use phasor extraction
as the main output. The comparison target is the real field itself, with both
full-plane and mode-weighted monitor errors reported.
Unlike the phasor-based examples, this script does not use extracted phasors as
the main output. It is a stricter diagnostic: the comparison target is the raw
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
@ -104,7 +107,7 @@ def main() -> None:
epsilon=epsilon,
)
# 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)
monitor_mode = waveguide_3d.solve_mode(
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
`-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
injected source, fields, and CPML auxiliary state complex-valued. The
`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-
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
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
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()
ranked_snapshots = sorted(
result.snapshots,