679 lines
22 KiB
Python
679 lines
22 KiB
Python
import dataclasses
|
|
from functools import lru_cache
|
|
|
|
import numpy
|
|
|
|
from .. import fdfd, fdtd
|
|
from ..fdtd.misc import gaussian_packet
|
|
from ..fdmath import vec, unvec
|
|
from ..fdfd import functional, scpml, waveguide_3d
|
|
|
|
|
|
DT = 0.25
|
|
PERIOD_STEPS = 64
|
|
OMEGA = 2 * numpy.pi / (PERIOD_STEPS * DT)
|
|
WAVELENGTH = 2 * numpy.pi / OMEGA
|
|
PULSE_DWL = 4.0
|
|
CPML_THICKNESS = 3
|
|
WARMUP_PERIODS = 9
|
|
ACCUMULATION_PERIODS = 9
|
|
SHAPE = (3, 25, 13, 13)
|
|
SOURCE_SLICES = (slice(4, 5), slice(None), slice(None))
|
|
MONITOR_SLICES = (slice(18, 19), slice(None), slice(None))
|
|
CHOSEN_VARIANT = 'base'
|
|
SCATTERING_SHAPE = (3, 35, 15, 15)
|
|
SCATTERING_SOURCE_SLICES = (slice(4, 5), slice(None), slice(None))
|
|
SCATTERING_REFLECT_SLICES = (slice(10, 11), slice(None), slice(None))
|
|
SCATTERING_TRANSMIT_SLICES = (slice(29, 30), slice(None), slice(None))
|
|
SCATTERING_STEP_X = 18
|
|
SCATTERING_WARMUP_PERIODS = 10
|
|
SCATTERING_ACCUMULATION_PERIODS = 10
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class WaveguideCalibrationResult:
|
|
variant: str
|
|
e_ph: numpy.ndarray
|
|
h_ph: numpy.ndarray
|
|
j_ph: numpy.ndarray
|
|
e_fdfd: numpy.ndarray
|
|
h_fdfd: numpy.ndarray
|
|
overlap_td: complex
|
|
overlap_fd: complex
|
|
flux_td: float
|
|
flux_fd: float
|
|
snapshots: tuple['MonitorSliceSnapshot', ...]
|
|
|
|
@property
|
|
def overlap_rel_err(self) -> float:
|
|
return float(abs(self.overlap_td - self.overlap_fd) / abs(self.overlap_fd))
|
|
|
|
@property
|
|
def overlap_mag_rel_err(self) -> float:
|
|
return float(abs(abs(self.overlap_td) - abs(self.overlap_fd)) / abs(self.overlap_fd))
|
|
|
|
@property
|
|
def overlap_phase_deg(self) -> float:
|
|
return float(abs(numpy.degrees(numpy.angle(self.overlap_td / self.overlap_fd))))
|
|
|
|
@property
|
|
def flux_rel_err(self) -> float:
|
|
return float(abs(self.flux_td - self.flux_fd) / abs(self.flux_fd))
|
|
|
|
@property
|
|
def combined_error(self) -> float:
|
|
return self.overlap_mag_rel_err + self.flux_rel_err
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class WaveguideScatteringResult:
|
|
e_ph: numpy.ndarray
|
|
h_ph: numpy.ndarray
|
|
j_ph: numpy.ndarray
|
|
e_fdfd: numpy.ndarray
|
|
h_fdfd: numpy.ndarray
|
|
reflected_td: complex
|
|
reflected_fd: complex
|
|
transmitted_td: complex
|
|
transmitted_fd: complex
|
|
reflected_flux_td: float
|
|
reflected_flux_fd: float
|
|
transmitted_flux_td: float
|
|
transmitted_flux_fd: float
|
|
|
|
@property
|
|
def reflected_overlap_mag_rel_err(self) -> float:
|
|
return float(abs(abs(self.reflected_td) - abs(self.reflected_fd)) / abs(self.reflected_fd))
|
|
|
|
@property
|
|
def transmitted_overlap_mag_rel_err(self) -> float:
|
|
return float(abs(abs(self.transmitted_td) - abs(self.transmitted_fd)) / abs(self.transmitted_fd))
|
|
|
|
@property
|
|
def reflected_flux_rel_err(self) -> float:
|
|
return float(abs(self.reflected_flux_td - self.reflected_flux_fd) / abs(self.reflected_flux_fd))
|
|
|
|
@property
|
|
def transmitted_flux_rel_err(self) -> float:
|
|
return float(abs(self.transmitted_flux_td - self.transmitted_flux_fd) / abs(self.transmitted_flux_fd))
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class MonitorSliceSnapshot:
|
|
step: int
|
|
e_monitor: numpy.ndarray
|
|
h_monitor: numpy.ndarray
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class PulsedWaveguideCalibrationResult:
|
|
e_ph: numpy.ndarray
|
|
h_ph: numpy.ndarray
|
|
j_ph: numpy.ndarray
|
|
j_target: numpy.ndarray
|
|
e_fdfd: numpy.ndarray
|
|
h_fdfd: numpy.ndarray
|
|
overlap_td: complex
|
|
overlap_fd: complex
|
|
flux_td: float
|
|
flux_fd: float
|
|
|
|
@property
|
|
def j_rel_err(self) -> float:
|
|
return float(numpy.linalg.norm(vec(self.j_ph - self.j_target)) / numpy.linalg.norm(vec(self.j_target)))
|
|
|
|
@property
|
|
def overlap_rel_err(self) -> float:
|
|
return float(abs(self.overlap_td - self.overlap_fd) / abs(self.overlap_fd))
|
|
|
|
@property
|
|
def overlap_mag_rel_err(self) -> float:
|
|
return float(abs(abs(self.overlap_td) - abs(self.overlap_fd)) / abs(self.overlap_fd))
|
|
|
|
@property
|
|
def overlap_phase_deg(self) -> float:
|
|
return float(abs(numpy.degrees(numpy.angle(self.overlap_td / self.overlap_fd))))
|
|
|
|
@property
|
|
def flux_rel_err(self) -> float:
|
|
return float(abs(self.flux_td - self.flux_fd) / abs(self.flux_fd))
|
|
|
|
|
|
|
|
|
|
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_base_dxes() -> list[list[numpy.ndarray]]:
|
|
return _build_uniform_dxes(SHAPE)
|
|
|
|
|
|
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_epsilon() -> 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_scattering_epsilon() -> numpy.ndarray:
|
|
epsilon = numpy.ones(SCATTERING_SHAPE, dtype=float)
|
|
y0 = SCATTERING_SHAPE[2] // 2
|
|
z0 = SCATTERING_SHAPE[3] // 2
|
|
epsilon[:, :SCATTERING_STEP_X, y0 - 1:y0 + 2, z0 - 1:z0 + 2] = 12.0
|
|
epsilon[:, SCATTERING_STEP_X:, y0 - 2:y0 + 3, z0 - 2:z0 + 3] = 12.0
|
|
return epsilon
|
|
|
|
|
|
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 _build_complex_pulse_waveform(total_steps: int) -> tuple[numpy.ndarray, complex]:
|
|
source_phasor, _delay = gaussian_packet(wl=WAVELENGTH, dwl=PULSE_DWL, dt=DT, turn_on=1e-5)
|
|
aa, cc, ss = source_phasor(numpy.arange(total_steps) + 0.5)
|
|
waveform = aa * (cc + 1j * ss)
|
|
scale = fdtd.temporal_phasor_scale(waveform, OMEGA, DT, offset_steps=0.5)[0]
|
|
return waveform, scale
|
|
|
|
|
|
def _continuous_wave_accumulation_response() -> complex:
|
|
warmup_steps = WARMUP_PERIODS * PERIOD_STEPS
|
|
accumulation_steps = ACCUMULATION_PERIODS * PERIOD_STEPS
|
|
accumulation_indices = numpy.arange(warmup_steps, warmup_steps + accumulation_steps)
|
|
accumulation_times = (accumulation_indices + 0.5) * DT
|
|
return DT * numpy.sum(
|
|
numpy.exp(-1j * OMEGA * accumulation_times) * numpy.cos(OMEGA * accumulation_times),
|
|
)
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def _run_straight_waveguide_case(variant: str) -> WaveguideCalibrationResult:
|
|
assert variant in ('stretched', 'base')
|
|
|
|
epsilon = _build_epsilon()
|
|
base_dxes = _build_base_dxes()
|
|
stretched_dxes = _build_stretched_dxes(base_dxes)
|
|
mode_dxes = stretched_dxes if variant == 'stretched' else base_dxes
|
|
|
|
source_mode = waveguide_3d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=mode_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=mode_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=SOURCE_SLICES,
|
|
epsilon=epsilon,
|
|
)
|
|
monitor_mode = waveguide_3d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=mode_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=MONITOR_SLICES,
|
|
epsilon=epsilon,
|
|
)
|
|
overlap_e = waveguide_3d.compute_overlap_e(
|
|
E=monitor_mode['E'],
|
|
wavenumber=monitor_mode['wavenumber'],
|
|
dxes=mode_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=MONITOR_SLICES,
|
|
omega=OMEGA,
|
|
)
|
|
|
|
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)
|
|
e_accumulator = numpy.zeros((1, *SHAPE), dtype=complex)
|
|
h_accumulator = numpy.zeros((1, *SHAPE), dtype=complex)
|
|
j_accumulator = numpy.zeros((1, *SHAPE), dtype=complex)
|
|
|
|
warmup_steps = WARMUP_PERIODS * PERIOD_STEPS
|
|
accumulation_steps = ACCUMULATION_PERIODS * PERIOD_STEPS
|
|
snapshot_steps = range(warmup_steps + accumulation_steps - PERIOD_STEPS, warmup_steps + accumulation_steps)
|
|
snapshots: list[MonitorSliceSnapshot] = []
|
|
for step in range(warmup_steps + accumulation_steps):
|
|
update_e(e_field, h_field, epsilon)
|
|
|
|
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
|
|
|
|
if step >= warmup_steps:
|
|
fdtd.accumulate_phasor_j(j_accumulator, OMEGA, DT, j_real, step)
|
|
fdtd.accumulate_phasor_e(e_accumulator, OMEGA, DT, e_field, step + 1)
|
|
|
|
update_h(e_field, h_field)
|
|
|
|
if step in snapshot_steps:
|
|
snapshots.append(
|
|
MonitorSliceSnapshot(
|
|
step=step,
|
|
e_monitor=e_field[:, MONITOR_SLICES[0], :, :].copy(),
|
|
h_monitor=h_field[:, MONITOR_SLICES[0], :, :].copy(),
|
|
),
|
|
)
|
|
|
|
if step >= warmup_steps:
|
|
fdtd.accumulate_phasor_h(h_accumulator, OMEGA, DT, h_field, step + 1)
|
|
|
|
e_ph = e_accumulator[0]
|
|
h_ph = h_accumulator[0]
|
|
j_ph = j_accumulator[0]
|
|
|
|
e_fdfd = unvec(
|
|
fdfd.solvers.generic(
|
|
J=vec(j_ph),
|
|
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)
|
|
|
|
overlap_td = vec(e_ph) @ vec(overlap_e).conj()
|
|
overlap_fd = vec(e_fdfd) @ vec(overlap_e).conj()
|
|
|
|
poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj())
|
|
poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj())
|
|
flux_td = float(0.5 * poynting_td[0, MONITOR_SLICES[0], :, :].real.sum())
|
|
flux_fd = float(0.5 * poynting_fd[0, MONITOR_SLICES[0], :, :].real.sum())
|
|
|
|
return WaveguideCalibrationResult(
|
|
variant=variant,
|
|
e_ph=e_ph,
|
|
h_ph=h_ph,
|
|
j_ph=j_ph,
|
|
e_fdfd=e_fdfd,
|
|
h_fdfd=h_fdfd,
|
|
overlap_td=overlap_td,
|
|
overlap_fd=overlap_fd,
|
|
flux_td=flux_td,
|
|
flux_fd=flux_fd,
|
|
snapshots=tuple(snapshots),
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _run_width_step_scattering_case() -> WaveguideScatteringResult:
|
|
epsilon = _build_scattering_epsilon()
|
|
base_dxes = _build_uniform_dxes(SCATTERING_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=SCATTERING_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=SCATTERING_SOURCE_SLICES,
|
|
epsilon=epsilon,
|
|
)
|
|
reflected_mode = waveguide_3d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=base_dxes,
|
|
axis=0,
|
|
polarity=-1,
|
|
slices=SCATTERING_REFLECT_SLICES,
|
|
epsilon=epsilon,
|
|
)
|
|
reflected_overlap = waveguide_3d.compute_overlap_e(
|
|
E=reflected_mode['E'],
|
|
wavenumber=reflected_mode['wavenumber'],
|
|
dxes=base_dxes,
|
|
axis=0,
|
|
polarity=-1,
|
|
slices=SCATTERING_REFLECT_SLICES,
|
|
omega=OMEGA,
|
|
)
|
|
transmitted_mode = waveguide_3d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=base_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=SCATTERING_TRANSMIT_SLICES,
|
|
epsilon=epsilon,
|
|
)
|
|
transmitted_overlap = waveguide_3d.compute_overlap_e(
|
|
E=transmitted_mode['E'],
|
|
wavenumber=transmitted_mode['wavenumber'],
|
|
dxes=base_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=SCATTERING_TRANSMIT_SLICES,
|
|
omega=OMEGA,
|
|
)
|
|
|
|
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)
|
|
e_accumulator = numpy.zeros((1, *SCATTERING_SHAPE), dtype=complex)
|
|
h_accumulator = numpy.zeros((1, *SCATTERING_SHAPE), dtype=complex)
|
|
j_accumulator = numpy.zeros((1, *SCATTERING_SHAPE), dtype=complex)
|
|
|
|
warmup_steps = SCATTERING_WARMUP_PERIODS * PERIOD_STEPS
|
|
accumulation_steps = SCATTERING_ACCUMULATION_PERIODS * PERIOD_STEPS
|
|
for step in range(warmup_steps + accumulation_steps):
|
|
update_e(e_field, h_field, epsilon)
|
|
|
|
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
|
|
|
|
if step >= warmup_steps:
|
|
fdtd.accumulate_phasor_j(j_accumulator, OMEGA, DT, j_real, step)
|
|
fdtd.accumulate_phasor_e(e_accumulator, OMEGA, DT, e_field, step + 1)
|
|
|
|
update_h(e_field, h_field)
|
|
|
|
if step >= warmup_steps:
|
|
fdtd.accumulate_phasor_h(h_accumulator, OMEGA, DT, h_field, step + 1)
|
|
|
|
e_ph = e_accumulator[0]
|
|
h_ph = h_accumulator[0]
|
|
j_ph = j_accumulator[0]
|
|
|
|
e_fdfd = unvec(
|
|
fdfd.solvers.generic(
|
|
J=vec(j_ph),
|
|
omega=OMEGA,
|
|
dxes=stretched_dxes,
|
|
epsilon=vec(epsilon),
|
|
matrix_solver_opts={'atol': 1e-10, 'rtol': 1e-7},
|
|
),
|
|
SCATTERING_SHAPE[1:],
|
|
)
|
|
h_fdfd = functional.e2h(OMEGA, stretched_dxes)(e_fdfd)
|
|
|
|
reflected_td = vec(e_ph) @ vec(reflected_overlap).conj()
|
|
reflected_fd = vec(e_fdfd) @ vec(reflected_overlap).conj()
|
|
transmitted_td = vec(e_ph) @ vec(transmitted_overlap).conj()
|
|
transmitted_fd = vec(e_fdfd) @ vec(transmitted_overlap).conj()
|
|
|
|
poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj())
|
|
poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj())
|
|
reflected_flux_td = float(0.5 * poynting_td[0, SCATTERING_REFLECT_SLICES[0], :, :].real.sum())
|
|
reflected_flux_fd = float(0.5 * poynting_fd[0, SCATTERING_REFLECT_SLICES[0], :, :].real.sum())
|
|
transmitted_flux_td = float(0.5 * poynting_td[0, SCATTERING_TRANSMIT_SLICES[0], :, :].real.sum())
|
|
transmitted_flux_fd = float(0.5 * poynting_fd[0, SCATTERING_TRANSMIT_SLICES[0], :, :].real.sum())
|
|
|
|
return WaveguideScatteringResult(
|
|
e_ph=e_ph,
|
|
h_ph=h_ph,
|
|
j_ph=j_ph,
|
|
e_fdfd=e_fdfd,
|
|
h_fdfd=h_fdfd,
|
|
reflected_td=reflected_td,
|
|
reflected_fd=reflected_fd,
|
|
transmitted_td=transmitted_td,
|
|
transmitted_fd=transmitted_fd,
|
|
reflected_flux_td=reflected_flux_td,
|
|
reflected_flux_fd=reflected_flux_fd,
|
|
transmitted_flux_td=transmitted_flux_td,
|
|
transmitted_flux_fd=transmitted_flux_fd,
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _run_pulsed_straight_waveguide_case() -> PulsedWaveguideCalibrationResult:
|
|
epsilon = _build_epsilon()
|
|
base_dxes = _build_base_dxes()
|
|
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,
|
|
)
|
|
monitor_mode = waveguide_3d.solve_mode(
|
|
0,
|
|
omega=OMEGA,
|
|
dxes=base_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=MONITOR_SLICES,
|
|
epsilon=epsilon,
|
|
)
|
|
overlap_e = waveguide_3d.compute_overlap_e(
|
|
E=monitor_mode['E'],
|
|
wavenumber=monitor_mode['wavenumber'],
|
|
dxes=base_dxes,
|
|
axis=0,
|
|
polarity=1,
|
|
slices=MONITOR_SLICES,
|
|
omega=OMEGA,
|
|
)
|
|
|
|
update_e, update_h = fdtd.updates_with_cpml(cpml_params=_build_cpml_params(), dt=DT, dxes=base_dxes, epsilon=epsilon, dtype=complex)
|
|
|
|
e_field = numpy.zeros_like(epsilon, dtype=complex)
|
|
h_field = numpy.zeros_like(epsilon, dtype=complex)
|
|
e_accumulator = numpy.zeros((1, *SHAPE), dtype=complex)
|
|
h_accumulator = numpy.zeros((1, *SHAPE), dtype=complex)
|
|
j_accumulator = numpy.zeros((1, *SHAPE), dtype=complex)
|
|
|
|
total_steps = (WARMUP_PERIODS + ACCUMULATION_PERIODS) * PERIOD_STEPS
|
|
waveform, pulse_scale = _build_complex_pulse_waveform(total_steps)
|
|
|
|
for step in range(total_steps):
|
|
update_e(e_field, h_field, epsilon)
|
|
|
|
j_step = pulse_scale * waveform[step] * j_mode
|
|
e_field -= DT * j_step / epsilon
|
|
|
|
fdtd.accumulate_phasor_j(j_accumulator, OMEGA, DT, j_step, step)
|
|
fdtd.accumulate_phasor_e(e_accumulator, OMEGA, DT, e_field, step + 1)
|
|
|
|
update_h(e_field, h_field)
|
|
|
|
fdtd.accumulate_phasor_h(h_accumulator, OMEGA, DT, h_field, step + 1)
|
|
|
|
e_ph = e_accumulator[0]
|
|
h_ph = h_accumulator[0]
|
|
j_ph = j_accumulator[0]
|
|
|
|
e_fdfd = unvec(
|
|
fdfd.solvers.generic(
|
|
J=vec(j_ph),
|
|
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)
|
|
|
|
overlap_td = vec(e_ph) @ vec(overlap_e).conj()
|
|
overlap_fd = vec(e_fdfd) @ vec(overlap_e).conj()
|
|
|
|
poynting_td = functional.poynting_e_cross_h(stretched_dxes)(e_ph, h_ph.conj())
|
|
poynting_fd = functional.poynting_e_cross_h(stretched_dxes)(e_fdfd, h_fdfd.conj())
|
|
flux_td = float(0.5 * poynting_td[0, MONITOR_SLICES[0], :, :].real.sum())
|
|
flux_fd = float(0.5 * poynting_fd[0, MONITOR_SLICES[0], :, :].real.sum())
|
|
|
|
return PulsedWaveguideCalibrationResult(
|
|
e_ph=e_ph,
|
|
h_ph=h_ph,
|
|
j_ph=j_ph,
|
|
j_target=j_mode,
|
|
e_fdfd=e_fdfd,
|
|
h_fdfd=h_fdfd,
|
|
overlap_td=overlap_td,
|
|
overlap_fd=overlap_fd,
|
|
flux_td=flux_td,
|
|
flux_fd=flux_fd,
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_straight_waveguide_base_variant_outperforms_stretched_variant() -> None:
|
|
base_result = _run_straight_waveguide_case('base')
|
|
stretched_result = _run_straight_waveguide_case('stretched')
|
|
|
|
assert base_result.variant == CHOSEN_VARIANT
|
|
assert base_result.combined_error < stretched_result.combined_error
|
|
|
|
|
|
def test_straight_waveguide_fdtd_fdfd_overlap_and_flux_agree() -> None:
|
|
result = _run_straight_waveguide_case(CHOSEN_VARIANT)
|
|
|
|
assert numpy.isfinite(result.e_ph).all()
|
|
assert numpy.isfinite(result.h_ph).all()
|
|
assert numpy.isfinite(result.j_ph).all()
|
|
assert numpy.isfinite(result.e_fdfd).all()
|
|
assert numpy.isfinite(result.h_fdfd).all()
|
|
assert abs(result.overlap_td) > 0
|
|
assert abs(result.overlap_fd) > 0
|
|
assert abs(result.flux_td) > 0
|
|
assert abs(result.flux_fd) > 0
|
|
|
|
assert result.overlap_mag_rel_err < 0.01
|
|
assert result.flux_rel_err < 0.01
|
|
assert result.overlap_rel_err < 0.01
|
|
assert result.overlap_phase_deg < 0.5
|
|
|
|
|
|
def test_straight_waveguide_real_monitor_fields_match_reconstructed_real_fields() -> None:
|
|
result = _run_straight_waveguide_case(CHOSEN_VARIANT)
|
|
response = _continuous_wave_accumulation_response()
|
|
e_fdfd = result.e_fdfd / response
|
|
h_fdfd = result.h_fdfd / response
|
|
final_step = (WARMUP_PERIODS + ACCUMULATION_PERIODS) * PERIOD_STEPS - 1
|
|
stable_snapshots = [
|
|
snapshot
|
|
for snapshot in result.snapshots
|
|
if snapshot.step >= final_step - PERIOD_STEPS // 4
|
|
]
|
|
|
|
ranked_snapshots = sorted(
|
|
stable_snapshots,
|
|
key=lambda snapshot: numpy.linalg.norm(
|
|
numpy.real(
|
|
e_fdfd[:, MONITOR_SLICES[0], :, :]
|
|
* numpy.exp(1j * OMEGA * ((snapshot.step + 1.0) * DT)),
|
|
),
|
|
),
|
|
reverse=True,
|
|
)
|
|
|
|
for snapshot in ranked_snapshots[:4]:
|
|
e_time = (snapshot.step + 1.0) * DT
|
|
h_time = (snapshot.step + 1.5) * DT
|
|
reconstructed_e = numpy.real(e_fdfd[:, MONITOR_SLICES[0], :, :] * numpy.exp(1j * OMEGA * e_time))
|
|
reconstructed_h = numpy.real(h_fdfd[:, MONITOR_SLICES[0], :, :] * numpy.exp(1j * OMEGA * h_time))
|
|
|
|
e_rel_err = numpy.linalg.norm(snapshot.e_monitor - reconstructed_e) / numpy.linalg.norm(reconstructed_e)
|
|
h_rel_err = numpy.linalg.norm(snapshot.h_monitor - reconstructed_h) / numpy.linalg.norm(reconstructed_h)
|
|
|
|
assert e_rel_err < 0.15
|
|
assert h_rel_err < 0.13
|
|
|
|
|
|
def test_width_step_waveguide_fdtd_fdfd_modal_powers_and_flux_agree() -> None:
|
|
result = _run_width_step_scattering_case()
|
|
|
|
assert numpy.isfinite(result.e_ph).all()
|
|
assert numpy.isfinite(result.h_ph).all()
|
|
assert numpy.isfinite(result.j_ph).all()
|
|
assert numpy.isfinite(result.e_fdfd).all()
|
|
assert numpy.isfinite(result.h_fdfd).all()
|
|
assert abs(result.reflected_td) > 0
|
|
assert abs(result.reflected_fd) > 0
|
|
assert abs(result.transmitted_td) > 0
|
|
assert abs(result.transmitted_fd) > 0
|
|
assert abs(result.reflected_flux_td) > 0
|
|
assert abs(result.reflected_flux_fd) > 0
|
|
assert abs(result.transmitted_flux_td) > 0
|
|
assert abs(result.transmitted_flux_fd) > 0
|
|
|
|
assert result.transmitted_overlap_mag_rel_err < 0.03
|
|
assert result.reflected_overlap_mag_rel_err < 0.03
|
|
assert result.transmitted_flux_rel_err < 0.01
|
|
assert result.reflected_flux_rel_err < 0.01
|
|
|
|
|
|
def test_pulsed_straight_waveguide_fdtd_fdfd_overlap_flux_and_source_agree() -> None:
|
|
result = _run_pulsed_straight_waveguide_case()
|
|
|
|
assert numpy.isfinite(result.e_ph).all()
|
|
assert numpy.isfinite(result.h_ph).all()
|
|
assert numpy.isfinite(result.j_ph).all()
|
|
assert numpy.isfinite(result.e_fdfd).all()
|
|
assert numpy.isfinite(result.h_fdfd).all()
|
|
assert abs(result.overlap_td) > 0
|
|
assert abs(result.overlap_fd) > 0
|
|
assert abs(result.flux_td) > 0
|
|
assert abs(result.flux_fd) > 0
|
|
|
|
assert result.j_rel_err < 1e-9
|
|
assert result.overlap_mag_rel_err < 0.01
|
|
assert result.flux_rel_err < 0.03
|
|
assert result.overlap_rel_err < 0.01
|
|
assert result.overlap_phase_deg < 0.5
|