[docs] document scattering conventions and frozen-basis workflows

Update examples for sampled-interface tapers, explicit physical CPML spacing, copied SCPML widths, and the consolidated complex mode solver. Document breaking API changes, overlap measures, and PML convergence.
This commit is contained in:
Forgejo Actions 2026-10-18 23:07:53 -07:00
commit 7e407dd1ac
10 changed files with 152 additions and 29 deletions

View file

@ -1,5 +1,5 @@
"""
Local-mode CMT example for a continuously varying rib-waveguide taper.
Sampled-interface EME example for a continuously varying rib-waveguide taper.
This example keeps geometry construction outside `meanas.fdfd.eme`: it samples a
width taper at several cross-sections, solves and normalizes each local mode with
@ -112,14 +112,15 @@ def print_summary(
print('sampled taper effective indices:', ', '.join(f'{value:.5f}' for value in neffs))
print(f'abrupt endpoint reflection |S_00|^2 = {abs(abrupt_s[0, 0]) ** 2:.6f}')
print(f'abrupt endpoint transmission |S_{n_modes},0|^2 = {abs(abrupt_s[n_modes, 0]) ** 2:.6f}')
print(f'taper CMT reflection |S_00|^2 = {abs(taper_s[0, 0]) ** 2:.6f}')
print(f'taper CMT transmission |S_{n_modes},0|^2 = {abs(taper_s[n_modes, 0]) ** 2:.6f}')
print(f'taper CMT total output power = {numpy.sum(abs(taper_s[:, 0]) ** 2):.6f}')
print(f'taper reflection |S_00|^2 = {abs(taper_s[0, 0]) ** 2:.6f}')
print(f'taper transmission |S_{n_modes},0|^2 = {abs(taper_s[n_modes, 0]) ** 2:.6f}')
print(f'taper total output power = {numpy.sum(abs(taper_s[:, 0]) ** 2):.6f}')
def main() -> None:
sections, neffs, _omega, dxes_2d = solve_taper_sections()
taper_s = eme.get_taper_s(sections, dxes=dxes_2d)
prepared = eme.prepare_taper(sections, dxes=dxes_2d)
taper_s = eme.evaluate_taper_s(prepared)
abrupt_s = eme.get_s(
sections[0].modes,
sections[0].wavenumbers,

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import numpy
from meanas.fdmath import vec
from meanas.fdfd import eme, scpml, waveguide_2d_complex
from meanas.fdfd import eme, scpml, waveguide_2d
WAVELENGTH = 1.55
@ -34,6 +34,7 @@ def build_grids() -> tuple[list[list[numpy.ndarray]], list[list[numpy.ndarray]]]
omega=OMEGA,
epsilon_effective=EPS_CLADDING,
thickness=PML_CELLS,
max_imag_ratio=1.0,
)
return physical, stretched
@ -61,29 +62,48 @@ def main() -> None:
"""Solve, track, and summarize the sampled complex modal bases."""
physical_dxes, solve_dxes = build_grids()
z_coords = numpy.array([0.0, 5.0, 10.0])
bases = [
waveguide_2d_complex.solve_modes_near(
epsilon_samples = [build_epsilon(edge_fill) for edge_fill in (0.30, 0.35, 0.40)]
solved = [
waveguide_2d.solve_modes_near(
OMEGA,
solve_dxes,
build_epsilon(edge_fill),
epsilon,
target_wavenumbers=[1.45 * OMEGA],
modes_per_target=3,
candidate_margin=3,
overlap_dxes=physical_dxes,
)
for edge_fill in (0.30, 0.35, 0.40)
for epsilon in epsilon_samples
]
bases = [basis for basis, _diagnostics in solved]
tracked = waveguide_2d.track_mode_path(bases, overlap_dxes=physical_dxes)
tracked_diagnostics = [
waveguide_2d.compute_mode_diagnostics(
basis,
OMEGA,
solve_dxes,
epsilon,
target_wavenumbers=[1.45 * OMEGA],
overlap_dxes=physical_dxes,
)
for basis, epsilon in zip(tracked, epsilon_samples, strict=True)
]
tracked = waveguide_2d_complex.track_mode_path(bases)
sections = eme.mode_sections_from_bases(z_coords, tracked)
prepared = eme.prepare_taper(sections, physical_dxes)
# Fields and interface overlaps are reused: these are frozen-basis length
# sweeps. Wavelength-dependent beta overrides would also freeze the fields.
length_sweep = [eme.evaluate_taper_s(prepared, z_coords=z_coords * scale)
for scale in (0.8, 1.0, 1.2)]
for z_coord, basis in zip(z_coords, tracked, strict=True):
for z_coord, basis, diagnostics in zip(z_coords, tracked, tracked_diagnostics, strict=True):
print(
f'z={z_coord:5.1f}: '
f'beta={basis.wavenumbers[0]:.6g}, '
f'PML participation={basis.diagnostics.pml_participation[0]:.3f}, '
f'right residual={basis.diagnostics.right_residuals[0]:.2e}',
f'PML participation={diagnostics.pml_participation[0]:.3f}, '
f'right residual={diagnostics.right_residuals[0]:.2e}',
)
print(f'constructed {len(sections)} tracked EME sections with {len(sections[0].modes)} modes each')
print('frozen-basis length sweep |T_00|:', [float(abs(ss[len(tracked[0]), 0])) for ss in length_sweep])
if __name__ == '__main__':

View file

@ -145,7 +145,7 @@ def main() -> None:
# PMLs in every direction
pml_params = [
[cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_air ** 2)
[cpml_params(axis=dd, polarity=pp, dt=dt, dx=dx, thickness=pml_thickness, epsilon_eff=n_air ** 2)
for pp in (-1, +1)]
for dd in range(3)]
update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon, dtype=complex)
@ -203,7 +203,7 @@ def main() -> None:
dxes_fdfd = copy.deepcopy(dxes)
for pp in (-1, +1):
for dd in range(3):
stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness)
dxes_fdfd = stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_air ** 2, thickness=pml_thickness)
# Compare the extracted phasor to the FDFD operator on the stretched grid,
# not the unstretched Yee spacings used by the raw time-domain update.
A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon)

View file

@ -273,7 +273,7 @@ def main2():
# PMLs in every direction
pml_params = [
[cpml_params(axis=dd, polarity=pp, dt=dt, thickness=pml_thickness, epsilon_eff=n_cladding ** 2)
[cpml_params(axis=dd, polarity=pp, dt=dt, dx=dx, thickness=pml_thickness, epsilon_eff=n_cladding ** 2)
for pp in (-1, +1)]
for dd in range(3)]
update_E, update_H = updates_with_cpml(cpml_params=pml_params, dt=dt, dxes=dxes, epsilon=epsilon, dtype=complex)
@ -330,7 +330,7 @@ def main2():
dxes_fdfd = copy.deepcopy(dxes)
for pp in (-1, +1):
for dd in range(3):
stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_cladding ** 2, thickness=pml_thickness)
dxes_fdfd = stretch_with_scpml(dxes_fdfd, axis=dd, polarity=pp, omega=omega, epsilon_effective=n_cladding ** 2, thickness=pml_thickness)
# Residuals must be checked on the stretched FDFD grid, because the FDTD run
# already includes those same absorbing layers through CPML.
A = e_full(omega=omega, dxes=dxes_fdfd, epsilon=epsilon)