[examples] add waveguide_real

This commit is contained in:
Forgejo Actions 2026-04-19 14:43:49 -07:00
commit f7aa21a42a

152
examples/waveguide_real.py Normal file
View file

@ -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()