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.
110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""Build tracked complex/PML modal bases for a slowly varying taper."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy
|
|
|
|
from meanas.fdmath import vec
|
|
from meanas.fdfd import eme, scpml, waveguide_2d
|
|
|
|
|
|
WAVELENGTH = 1.55
|
|
OMEGA = 2 * numpy.pi / WAVELENGTH
|
|
SHAPE = (17, 17)
|
|
SPACING = 0.25
|
|
PML_CELLS = 3
|
|
EPS_CLADDING = 1.45 ** 2
|
|
EPS_CORE = 2.0 ** 2
|
|
|
|
|
|
def build_grids() -> tuple[list[list[numpy.ndarray]], list[list[numpy.ndarray]]]:
|
|
"""Build matching physical and complex-stretched transverse Yee grids.
|
|
|
|
Returns:
|
|
The real overlap grid followed by the PML-stretched solve grid.
|
|
"""
|
|
physical = [[numpy.full(size, SPACING) for size in SHAPE] for _ in range(2)]
|
|
stretched = [[values.copy() for values in family] for family in physical]
|
|
for axis in (0, 1):
|
|
for polarity in (-1, +1):
|
|
stretched = scpml.stretch_with_scpml(
|
|
stretched,
|
|
axis=axis,
|
|
polarity=polarity,
|
|
omega=OMEGA,
|
|
epsilon_effective=EPS_CLADDING,
|
|
thickness=PML_CELLS,
|
|
max_imag_ratio=1.0,
|
|
)
|
|
return physical, stretched
|
|
|
|
|
|
def build_epsilon(edge_fill: float) -> numpy.ndarray:
|
|
"""Construct and vectorize one taper cross-section permittivity profile.
|
|
|
|
Args:
|
|
edge_fill: Fractional core fill used for the two cells beside the
|
|
central three-cell-wide core.
|
|
|
|
Returns:
|
|
Vectorized three-component relative permittivity field.
|
|
"""
|
|
epsilon = numpy.full((3, *SHAPE), EPS_CLADDING)
|
|
center_x, center_y = (size // 2 for size in SHAPE)
|
|
epsilon[:, center_x - 1:center_x + 2, center_y] = EPS_CORE
|
|
mixed_epsilon = EPS_CLADDING + edge_fill * (EPS_CORE - EPS_CLADDING)
|
|
epsilon[:, center_x - 1:center_x + 2, center_y - 1] = mixed_epsilon
|
|
epsilon[:, center_x - 1:center_x + 2, center_y + 1] = mixed_epsilon
|
|
return vec(epsilon)
|
|
|
|
|
|
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])
|
|
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,
|
|
epsilon,
|
|
target_wavenumbers=[1.45 * OMEGA],
|
|
modes_per_target=3,
|
|
candidate_margin=3,
|
|
overlap_dxes=physical_dxes,
|
|
)
|
|
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)
|
|
]
|
|
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, 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={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__':
|
|
main()
|