[waveguide_real / phasor] more work towards real-FDTD to FDFD equivalence

This commit is contained in:
Forgejo Actions 2026-04-19 15:47:00 -07:00
commit e50637dc1c
7 changed files with 213 additions and 25 deletions

View file

@ -10,7 +10,8 @@ FDFD" workflow:
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.
as the main output. The comparison target is the real field itself, with both
full-plane and mode-weighted monitor errors reported.
"""
import numpy
@ -28,6 +29,8 @@ SHAPE = (3, 37, 13, 13)
SOURCE_SLICES = (slice(5, 6), slice(None), slice(None))
MONITOR_SLICES = (slice(30, 31), slice(None), slice(None))
WARMUP_PERIODS = 16
SOURCE_PHASE = 0.4
CORE_SLICES = (slice(None), slice(None), slice(4, 9), slice(4, 9))
def build_uniform_dxes(shape: tuple[int, int, int, int]) -> list[list[numpy.ndarray]]:
@ -65,6 +68,17 @@ def build_cpml_params() -> list[list[dict[str, numpy.ndarray | float]]]:
]
def weighted_rel_err(observed: numpy.ndarray, reference: numpy.ndarray, weight: numpy.ndarray) -> float:
return numpy.linalg.norm((observed - reference) * weight) / numpy.linalg.norm(reference * weight)
def project_onto_mode(observed: numpy.ndarray, mode: numpy.ndarray) -> tuple[complex, numpy.ndarray, numpy.ndarray]:
coefficient = numpy.vdot(mode, observed) / numpy.vdot(mode, mode)
guided = coefficient * mode
residual = observed - guided
return coefficient, guided, residual
def main() -> None:
epsilon = build_epsilon(SHAPE)
base_dxes = build_uniform_dxes(SHAPE)
@ -89,6 +103,22 @@ def main() -> None:
slices=SOURCE_SLICES,
epsilon=epsilon,
)
# A small global phase aligns the real-valued source with the late-cycle
# monitor comparison. The underlying phasor problem is unchanged.
j_mode *= numpy.exp(1j * SOURCE_PHASE)
monitor_mode = waveguide_3d.solve_mode(
0,
omega=OMEGA,
dxes=base_dxes,
axis=0,
polarity=1,
slices=MONITOR_SLICES,
epsilon=epsilon,
)
e_weight = numpy.abs(monitor_mode['E'][:, MONITOR_SLICES[0], :, :])
h_weight = numpy.abs(monitor_mode['H'][:, MONITOR_SLICES[0], :, :])
e_mode = monitor_mode['E'][:, MONITOR_SLICES[0], :, :]
h_mode = monitor_mode['H'][:, MONITOR_SLICES[0], :, :]
e_fdfd = unvec(
fdfd.solvers.generic(
@ -114,6 +144,14 @@ def main() -> None:
total_steps = (WARMUP_PERIODS + 1) * PERIOD_STEPS
e_errors: list[float] = []
h_errors: list[float] = []
e_core_errors: list[float] = []
h_core_errors: list[float] = []
e_weighted_errors: list[float] = []
h_weighted_errors: list[float] = []
e_guided_errors: list[float] = []
h_guided_errors: list[float] = []
e_residual_errors: list[float] = []
h_residual_errors: list[float] = []
for step in range(total_steps):
update_e(e_field, h_field, epsilon)
@ -127,25 +165,51 @@ def main() -> None:
if step >= total_steps - PERIOD_STEPS // 4:
reconstructed_e = fdtd.reconstruct_real_e(
e_fdfd[:, MONITOR_SLICES[0], :, :][numpy.newaxis, ...],
e_fdfd[:, MONITOR_SLICES[0], :, :],
OMEGA,
DT,
step + 1,
)[0]
)
reconstructed_h = fdtd.reconstruct_real_h(
h_fdfd[:, MONITOR_SLICES[0], :, :][numpy.newaxis, ...],
h_fdfd[:, MONITOR_SLICES[0], :, :],
OMEGA,
DT,
step + 1,
)[0]
)
e_monitor = e_field[:, MONITOR_SLICES[0], :, :]
h_monitor = h_field[:, MONITOR_SLICES[0], :, :]
e_errors.append(numpy.linalg.norm(e_monitor - reconstructed_e) / numpy.linalg.norm(reconstructed_e))
h_errors.append(numpy.linalg.norm(h_monitor - reconstructed_h) / numpy.linalg.norm(reconstructed_h))
e_core_errors.append(
numpy.linalg.norm(e_monitor[CORE_SLICES] - reconstructed_e[CORE_SLICES])
/ numpy.linalg.norm(reconstructed_e[CORE_SLICES]),
)
h_core_errors.append(
numpy.linalg.norm(h_monitor[CORE_SLICES] - reconstructed_h[CORE_SLICES])
/ numpy.linalg.norm(reconstructed_h[CORE_SLICES]),
)
e_weighted_errors.append(weighted_rel_err(e_monitor, reconstructed_e, e_weight))
h_weighted_errors.append(weighted_rel_err(h_monitor, reconstructed_h, h_weight))
e_guided_coeff, _, e_residual = project_onto_mode(e_monitor, e_mode)
e_guided_coeff_ref, _, e_residual_ref = project_onto_mode(reconstructed_e, e_mode)
h_guided_coeff, _, h_residual = project_onto_mode(h_monitor, h_mode)
h_guided_coeff_ref, _, h_residual_ref = project_onto_mode(reconstructed_h, h_mode)
e_guided_errors.append(abs(e_guided_coeff - e_guided_coeff_ref) / abs(e_guided_coeff_ref))
h_guided_errors.append(abs(h_guided_coeff - h_guided_coeff_ref) / abs(h_guided_coeff_ref))
e_residual_errors.append(numpy.linalg.norm(e_residual - e_residual_ref) / numpy.linalg.norm(e_residual_ref))
h_residual_errors.append(numpy.linalg.norm(h_residual - h_residual_ref) / numpy.linalg.norm(h_residual_ref))
print(f'late-cycle monitor E errors: min={min(e_errors):.4f} max={max(e_errors):.4f}')
print(f'late-cycle monitor H errors: min={min(h_errors):.4f} max={max(h_errors):.4f}')
print(f'late-cycle core-window E errors: min={min(e_core_errors):.4f} max={max(e_core_errors):.4f}')
print(f'late-cycle core-window H errors: min={min(h_core_errors):.4f} max={max(h_core_errors):.4f}')
print(f'late-cycle mode-weighted E errors: min={min(e_weighted_errors):.4f} max={max(e_weighted_errors):.4f}')
print(f'late-cycle mode-weighted H errors: min={min(h_weighted_errors):.4f} max={max(h_weighted_errors):.4f}')
print(f'late-cycle guided-mode E coefficient errors: min={min(e_guided_errors):.4f} max={max(e_guided_errors):.4f}')
print(f'late-cycle guided-mode H coefficient errors: min={min(h_guided_errors):.4f} max={max(h_guided_errors):.4f}')
print(f'late-cycle orthogonal-residual E errors: min={min(e_residual_errors):.4f} max={max(e_residual_errors):.4f}')
print(f'late-cycle orthogonal-residual H errors: min={min(h_residual_errors):.4f} max={max(h_residual_errors):.4f}')
if __name__ == '__main__':