From f7aa21a42ac0433cf4db6f33e4f43c99067e909c Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Sun, 19 Apr 2026 14:43:49 -0700 Subject: [PATCH] [examples] add waveguide_real --- examples/waveguide_real.py | 152 +++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 examples/waveguide_real.py diff --git a/examples/waveguide_real.py b/examples/waveguide_real.py new file mode 100644 index 0000000..0eb0fc7 --- /dev/null +++ b/examples/waveguide_real.py @@ -0,0 +1,152 @@ +""" +Real-valued straight-waveguide FDTD/FDFD comparison. + +This example shows the user-facing "compare real FDTD against reconstructed real +FDFD" workflow: + +1. build a straight waveguide on a uniform Yee grid, +2. drive it with a real-valued continuous-wave mode source, +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. +""" + +import numpy + +from meanas import fdfd, fdtd +from meanas.fdfd import functional, scpml, waveguide_3d +from meanas.fdmath import vec, unvec + + +DT = 0.25 +PERIOD_STEPS = 64 +OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT) +CPML_THICKNESS = 3 +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 + + +def build_uniform_dxes(shape: tuple[int, int, int, int]) -> list[list[numpy.ndarray]]: + return [[numpy.ones(shape[axis + 1]) for axis in range(3)] for _ in range(2)] + + +def build_epsilon(shape: tuple[int, int, int, int]) -> numpy.ndarray: + epsilon = numpy.ones(shape, dtype=float) + y0 = (shape[2] - 3) // 2 + z0 = (shape[3] - 3) // 2 + epsilon[:, :, y0:y0 + 3, z0:z0 + 3] = 12.0 + return epsilon + + +def build_stretched_dxes(base_dxes: list[list[numpy.ndarray]]) -> list[list[numpy.ndarray]]: + stretched_dxes = [[dx.copy() for dx in group] for group in base_dxes] + for axis in (0, 1, 2): + for polarity in (-1, 1): + stretched_dxes = scpml.stretch_with_scpml( + stretched_dxes, + axis=axis, + polarity=polarity, + omega=OMEGA, + epsilon_effective=1.0, + thickness=CPML_THICKNESS, + ) + return stretched_dxes + + +def build_cpml_params() -> list[list[dict[str, numpy.ndarray | float]]]: + return [ + [fdtd.cpml_params(axis=axis, polarity=polarity, dt=DT, thickness=CPML_THICKNESS, epsilon_eff=1.0) + for polarity in (-1, 1)] + for axis in range(3) + ] + + +def main() -> None: + epsilon = build_epsilon(SHAPE) + base_dxes = build_uniform_dxes(SHAPE) + stretched_dxes = build_stretched_dxes(base_dxes) + + source_mode = waveguide_3d.solve_mode( + 0, + omega=OMEGA, + dxes=base_dxes, + axis=0, + polarity=1, + slices=SOURCE_SLICES, + epsilon=epsilon, + ) + j_mode = waveguide_3d.compute_source( + E=source_mode['E'], + wavenumber=source_mode['wavenumber'], + omega=OMEGA, + dxes=base_dxes, + axis=0, + polarity=1, + slices=SOURCE_SLICES, + epsilon=epsilon, + ) + + e_fdfd = unvec( + fdfd.solvers.generic( + J=vec(j_mode), + omega=OMEGA, + dxes=stretched_dxes, + epsilon=vec(epsilon), + matrix_solver_opts={'atol': 1e-10, 'rtol': 1e-7}, + ), + SHAPE[1:], + ) + h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd) + + update_e, update_h = fdtd.updates_with_cpml( + cpml_params=build_cpml_params(), + dt=DT, + dxes=base_dxes, + epsilon=epsilon, + ) + + e_field = numpy.zeros_like(epsilon) + h_field = numpy.zeros_like(epsilon) + total_steps = (WARMUP_PERIODS + 1) * PERIOD_STEPS + e_errors: list[float] = [] + h_errors: list[float] = [] + + for step in range(total_steps): + update_e(e_field, h_field, epsilon) + + # Real-valued FDTD uses the real part of the analytic mode source. + t_half = (step + 0.5) * DT + j_real = (j_mode.real * numpy.cos(OMEGA * t_half) - j_mode.imag * numpy.sin(OMEGA * t_half)).real + e_field -= DT * j_real / epsilon + + update_h(e_field, h_field) + + if step >= total_steps - PERIOD_STEPS // 4: + reconstructed_e = fdtd.reconstruct_real_e( + e_fdfd[:, MONITOR_SLICES[0], :, :][numpy.newaxis, ...], + OMEGA, + DT, + step + 1, + )[0] + reconstructed_h = fdtd.reconstruct_real_h( + h_fdfd[:, MONITOR_SLICES[0], :, :][numpy.newaxis, ...], + 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)) + + 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}') + + +if __name__ == '__main__': + main()