152 lines
4.8 KiB
Python
152 lines
4.8 KiB
Python
"""
|
|
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()
|