From ad3b3335e862c89f8c385c27d0f1fe051f7416f1 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 01:05:00 +0200 Subject: [PATCH 01/16] Add SciPy reference backend --- pyproject.toml | 3 + python/micromode/raster.py | 71 +++++ python/micromode/scipy_reference.py | 405 ++++++++++++++++++++++++++++ tests/test_micromode_api.py | 51 ++++ uv.lock | 122 ++++++++- 5 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 python/micromode/scipy_reference.py diff --git a/pyproject.toml b/pyproject.toml index a0b9d69..99fe0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,9 @@ Examples = "https://github.com/QuentinWach/micromode/tree/main/examples" Changelog = "https://github.com/QuentinWach/micromode/blob/main/CHANGELOG.md" [project.optional-dependencies] +scipy = [ + "scipy>=1.11,<2.0", +] dev = [ "maturin>=1.7,<2", "packaging>=24.2", diff --git a/python/micromode/raster.py b/python/micromode/raster.py index deb818d..27b8ef3 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -11,8 +11,10 @@ from ._rust import C_0, solve_diagonal_sparse, solve_tensorial_sparse from .models import BoundaryCondition, BoundarySpec, Materials, PmlSpec, SliceAxis, Spec from .result import Result +from .scipy_reference import solve_diagonal_scipy_reference _COMPONENTS = ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz") +Backend = Literal["rust", "scipy-reference"] def solve_grid( @@ -52,6 +54,7 @@ def solve_grid( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, + backend: Backend = "rust", spec: Spec | None = None, ) -> Result: """Solve modes from rasterized material components and grid edges. @@ -99,6 +102,7 @@ def solve_grid( angle_phi=angle_phi, bend_radius=bend_radius, bend_axis=bend_axis, + backend=backend, spec=spec, ) @@ -142,6 +146,7 @@ def solve_slice( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, + backend: Backend = "rust", spec: Spec | None = None, ) -> Result: """Solve modes from a one-dimensional mode-plane material slice. @@ -193,6 +198,7 @@ def solve_slice( angle_phi=angle_phi, bend_radius=bend_radius, bend_axis=bend_axis, + backend=backend, spec=spec, ) @@ -213,6 +219,7 @@ def solve_modes( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, + backend: Backend = "rust", spec: Spec | None = None, ) -> Result: """Solve modes for an already-rasterized material tensor grid. @@ -245,6 +252,8 @@ def solve_modes( raise ValueError("num_modes must be positive") if direction not in {"+", "-"}: raise ValueError("direction must be '+' or '-'") + if backend not in {"rust", "scipy-reference"}: + raise ValueError("backend must be 'rust' or 'scipy-reference'") pml_spec = _resolve_pml_spec(pml) boundary_spec = _resolve_boundary_spec(boundary) if bend_radius is not None and np.isclose(bend_radius, 0.0): @@ -276,6 +285,7 @@ def solve_modes( angle_phi=float(angle_phi), bend_radius=None if bend_radius is None else float(bend_radius), bend_axis=int(bend_axis), + backend=backend, material_grid=material_grid, ) # Rust solves in local coordinates where local z is the propagation @@ -327,6 +337,7 @@ def _solve_one_frequency( angle_phi: float, bend_radius: float | None, bend_axis: int, + backend: Backend, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: # Forward and backward spacings represent the local Yee grid. The derivative # builders need both because E and H components are staggered. @@ -341,6 +352,25 @@ def _solve_one_frequency( target_neff = _shift_target_neff(float(target_neff)) has_transform = abs(angle_theta) > 0.0 or bend_radius is not None is_diagonal = material_grid.is_diagonal + if backend == "scipy-reference": + if has_transform: + raise ValueError("backend='scipy-reference' currently supports only untransformed diagonal grids") + if not is_diagonal: + raise ValueError("backend='scipy-reference' currently supports only diagonal material grids") + if pml_spec.num_cells != (0, 0): + raise ValueError("backend='scipy-reference' currently does not support PML") + return _solve_one_frequency_scipy_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + freq=freq, + num_modes=num_modes, + target_neff=target_neff, + direction=direction, + krylov_dim=krylov_dim, + boundary_spec=boundary_spec, + ) if has_transform: # Angle and bend coordinates are applied by transforming eps/mu. A # diagonal grid may become full tensor after this step. @@ -461,6 +491,47 @@ def _solve_one_frequency_rust_sparse( ) +def _solve_one_frequency_scipy_reference( + *, + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, + dlf: tuple[np.ndarray, np.ndarray], + dlb: tuple[np.ndarray, np.ndarray], + freq: float, + num_modes: int, + target_neff: float, + direction: str, + krylov_dim: int | None, + boundary_spec: BoundarySpec, +) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: + nx = len(dlf[0]) + ny = len(dlf[1]) + actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) + n_complex, fields, solver_info = solve_diagonal_scipy_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + num_modes=num_modes, + neff_guess=target_neff, + direction=direction, + derivative_scale=C_0 / (2 * np.pi * freq), + dmin_pmc=boundary_spec.dmin_pmc, + krylov_dim=actual_krylov_dim, + initial_vector=_default_initial_vector(2 * nx * ny, shape=(nx, ny)), + ) + return ( + n_complex, + _fields_to_grid(fields, (nx, ny)), + _solver_info_with_context( + solver_info, + backend_kind="diagonal_scipy_reference", + shape=(nx, ny), + krylov_dim=actual_krylov_dim, + ), + ) + + def _solve_one_frequency_rust_tensorial_sparse( *, eps_tensor: np.ndarray, diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py new file mode 100644 index 0000000..2b1d390 --- /dev/null +++ b/python/micromode/scipy_reference.py @@ -0,0 +1,405 @@ +"""Readable SciPy reference implementation for the diagonal mode solver. + +This module intentionally mirrors the Rust diagonal sparse path in plain +Python/SciPy. It is slower and narrower than the production backend, but it +keeps the numerical contract inspectable by users who want to audit the +finite-difference operator against SciPy/ARPACK. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +ETA0 = 376.730_313_666_853_5 + + +SparseSolveResult = tuple[np.ndarray, list[np.ndarray], dict[str, object]] + + +@dataclass +class _ModeFields: + ex: np.ndarray + ey: np.ndarray + ez: np.ndarray + hx: np.ndarray + hy: np.ndarray + hz: np.ndarray + + def components(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + return self.ex, self.ey, self.ez, self.hx, self.hy, self.hz + + def add_scaled(self, other: _ModeFields, scale: complex) -> None: + for left, right in zip(self.components(), other.components(), strict=True): + left += scale * right + + +def solve_diagonal_scipy_reference( + *, + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, + dlf: tuple[np.ndarray, np.ndarray], + dlb: tuple[np.ndarray, np.ndarray], + num_modes: int, + neff_guess: float, + direction: str, + derivative_scale: float, + dmin_pmc: tuple[bool, bool] = (False, False), + krylov_dim: int = 32, + initial_vector: np.ndarray | None = None, +) -> SparseSolveResult: + """Solve the diagonal sparse eigenproblem with SciPy/ARPACK. + + Supported scope is deliberately small: diagonal material tensors, no PML, + and the same reduced ``[Ex, Ey]`` transverse eigenproblem used by the Rust + production backend. + """ + + sparse, spla, scipy_linalg = _import_scipy() + nx = len(dlf[0]) + ny = len(dlf[1]) + n = nx * ny + if eps_tensor.shape != mu_tensor.shape or eps_tensor.shape[:2] != (3, 3) or eps_tensor.shape[-1] != n: + raise ValueError("eps_tensor and mu_tensor must both have shape (3, 3, len(dlf[0]) * len(dlf[1]))") + if num_modes <= 0: + raise ValueError("num_modes must be positive") + + derivatives = _create_derivative_matrices( + sparse, + shape=(nx, ny), + dlf=dlf, + dlb=dlb, + dmin_pmc=dmin_pmc, + scale=float(derivative_scale), + ) + operators = _assemble_diagonal_operators(sparse, eps_tensor, mu_tensor, derivatives) + eig_guess = complex(-(neff_guess * neff_guess), 0.0) + values, vectors = _selected_eigenpairs( + operators["mat"], + num_modes=num_modes, + sigma=eig_guess, + krylov_dim=krylov_dim, + initial_vector=initial_vector, + spla=spla, + scipy_linalg=scipy_linalg, + ) + residuals = np.asarray( + [ + np.linalg.norm(operators["mat"] @ vectors[:, index] - values[index] * vectors[:, index]) + for index in range(len(values)) + ], + dtype=float, + ) + vector_norms = np.maximum(np.linalg.norm(vectors, axis=0), np.finfo(float).eps) + residuals = residuals / vector_norms + + modes: list[tuple[complex, np.ndarray, float]] = [] + for value, vector, residual in zip(values, vectors.T, residuals, strict=True): + modes.append((complex(np.sqrt(-value + 0j)), np.asarray(vector, dtype=np.complex128), float(residual))) + modes.sort(key=lambda item: item[0].real, reverse=True) + + inv_eps_zz = sparse.diags(1.0 / eps_tensor[2, 2, :], format="csc") + inv_mu_zz = sparse.diags(1.0 / mu_tensor[2, 2, :], format="csc") + dxf, dxb, dyf, dyb = derivatives + cell_areas = np.repeat(np.asarray(dlf[0], dtype=float), ny) * np.tile(np.asarray(dlf[1], dtype=float), nx) + + n_complex: list[complex] = [] + mode_fields: list[_ModeFields] = [] + sorted_residuals: list[float] = [] + for mode_n, vector, residual in modes: + ex = vector[:n].copy() + ey = vector[n:].copy() + denom = complex(-mode_n.imag, mode_n.real) + + h_field = operators["qmat"] @ vector + hx = np.asarray(h_field[:n] / denom, dtype=np.complex128) + hy = np.asarray(h_field[n:] / denom, dtype=np.complex128) + + hz_source = dxf @ ey - dyf @ ex + hz = np.asarray(inv_mu_zz @ hz_source, dtype=np.complex128) + + h_partial = np.asarray((operators["q_ep"] @ vector) / denom, dtype=np.complex128) + ez_source = dxb @ h_partial[n:] - dyb @ h_partial[:n] + ez = np.asarray(inv_eps_zz @ ez_source, dtype=np.complex128) + + h_scale = -1j / ETA0 + hx *= h_scale + hy *= h_scale + hz *= h_scale + if direction == "-": + hx *= -1.0 + hy *= -1.0 + ez *= -1.0 + + n_complex.append(mode_n) + sorted_residuals.append(residual) + mode_fields.append(_ModeFields(ex=ex, ey=ey, ez=ez, hx=hx, hy=hy, hz=hz)) + + orthogonalization = _lorentz_orthogonalize_and_normalize(mode_fields, cell_areas) + fields = [ + np.asarray([getattr(mode, component) for mode in mode_fields], dtype=np.complex128) + for component in ("ex", "ey", "ez", "hx", "hy", "hz") + ] + return ( + np.asarray(n_complex, dtype=np.complex128), + fields, + { + "backend": "scipy_arpack_reference", + "operator_size": int(operators["mat"].shape[0]), + "operator_nnz": int(operators["mat"].nnz), + "residuals": np.asarray(sorted_residuals, dtype=float), + "power_norms": orthogonalization["power_norms"], + "lorentz_norms": orthogonalization["lorentz_norms"], + "lorentz_orthogonality_error": orthogonalization["lorentz_orthogonality_error"], + }, + ) + + +def _import_scipy(): + try: + import scipy.linalg as scipy_linalg + import scipy.sparse as sparse + import scipy.sparse.linalg as spla + except ImportError as exc: # pragma: no cover - depends on optional extra. + raise ImportError("the SciPy reference backend requires `pip install micromode[scipy]`") from exc + return sparse, spla, scipy_linalg + + +def _create_derivative_matrices( + sparse, + *, + shape: tuple[int, int], + dlf: tuple[np.ndarray, np.ndarray], + dlb: tuple[np.ndarray, np.ndarray], + dmin_pmc: tuple[bool, bool], + scale: float, +): + matrices = ( + _make_dxf(sparse, np.asarray(dlf[0], dtype=float), shape, dmin_pmc[0]), + _make_dxb(sparse, np.asarray(dlb[0], dtype=float), shape, dmin_pmc[0]), + _make_dyf(sparse, np.asarray(dlf[1], dtype=float), shape, dmin_pmc[1]), + _make_dyb(sparse, np.asarray(dlb[1], dtype=float), shape, dmin_pmc[1]), + ) + return tuple(matrix * complex(scale, 0.0) for matrix in matrices) + + +def _make_dxf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + nx, ny = shape + if nx == 1: + return sparse.csc_matrix((ny, ny), dtype=np.complex128) + rows: list[int] = [] + cols: list[int] = [] + data: list[complex] = [] + for ix in range(nx): + for iy in range(ny): + row = ix * ny + iy + value = 1.0 / dls[ix] + diagonal = 0.0 if ix == 0 and not pmc else -value + if diagonal != 0.0: + rows.append(row) + cols.append(row) + data.append(diagonal) + if ix + 1 < nx: + rows.append(row) + cols.append((ix + 1) * ny + iy) + data.append(value) + return sparse.csc_matrix((data, (rows, cols)), shape=(nx * ny, nx * ny), dtype=np.complex128) + + +def _make_dxb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + nx, ny = shape + if nx == 1: + return sparse.csc_matrix((ny, ny), dtype=np.complex128) + rows: list[int] = [] + cols: list[int] = [] + data: list[complex] = [] + for ix in range(nx): + for iy in range(ny): + row = ix * ny + iy + value = 1.0 / dls[ix] + diagonal = 2.0 * value if ix == 0 and pmc else (0.0 if ix == 0 else value) + if diagonal != 0.0: + rows.append(row) + cols.append(row) + data.append(diagonal) + if ix > 0: + rows.append(row) + cols.append((ix - 1) * ny + iy) + data.append(-value) + return sparse.csc_matrix((data, (rows, cols)), shape=(nx * ny, nx * ny), dtype=np.complex128) + + +def _make_dyf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + nx, ny = shape + if ny == 1: + return sparse.csc_matrix((nx, nx), dtype=np.complex128) + rows: list[int] = [] + cols: list[int] = [] + data: list[complex] = [] + for ix in range(nx): + for iy in range(ny): + row = ix * ny + iy + value = 1.0 / dls[iy] + diagonal = 0.0 if iy == 0 and not pmc else -value + if diagonal != 0.0: + rows.append(row) + cols.append(row) + data.append(diagonal) + if iy + 1 < ny: + rows.append(row) + cols.append(ix * ny + iy + 1) + data.append(value) + return sparse.csc_matrix((data, (rows, cols)), shape=(nx * ny, nx * ny), dtype=np.complex128) + + +def _make_dyb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + nx, ny = shape + if ny == 1: + return sparse.csc_matrix((nx, nx), dtype=np.complex128) + rows: list[int] = [] + cols: list[int] = [] + data: list[complex] = [] + for ix in range(nx): + for iy in range(ny): + row = ix * ny + iy + value = 1.0 / dls[iy] + diagonal = 2.0 * value if iy == 0 and pmc else (0.0 if iy == 0 else value) + if diagonal != 0.0: + rows.append(row) + cols.append(row) + data.append(diagonal) + if iy > 0: + rows.append(row) + cols.append(ix * ny + iy - 1) + data.append(-value) + return sparse.csc_matrix((data, (rows, cols)), shape=(nx * ny, nx * ny), dtype=np.complex128) + + +def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_mats) -> dict[str, object]: + n = eps.shape[-1] + zero = sparse.csc_matrix((n, n), dtype=np.complex128) + dxf, dxb, dyf, dyb = der_mats + inv_eps_zz = sparse.diags(1.0 / eps[2, 2, :], format="csc") + inv_mu_zz = sparse.diags(1.0 / mu[2, 2, :], format="csc") + + p_mu = sparse.bmat( + [[zero, sparse.diags(mu[1, 1, :], format="csc")], [-sparse.diags(mu[0, 0, :], format="csc"), zero]], + format="csc", + ) + p_partial = sparse.bmat( + [ + [-(dxf @ inv_eps_zz @ dyb), dxf @ inv_eps_zz @ dxb], + [-(dyf @ inv_eps_zz @ dyb), dyf @ inv_eps_zz @ dxb], + ], + format="csc", + ) + q_ep = sparse.bmat( + [[zero, sparse.diags(eps[1, 1, :], format="csc")], [-sparse.diags(eps[0, 0, :], format="csc"), zero]], + format="csc", + ) + q_partial = sparse.bmat( + [ + [-(dxb @ inv_mu_zz @ dyf), dxb @ inv_mu_zz @ dxf], + [-(dyb @ inv_mu_zz @ dyf), dyb @ inv_mu_zz @ dxf], + ], + format="csc", + ) + qmat = q_ep + q_partial + mat = p_mu @ qmat + p_partial @ q_ep + return {"q_ep": q_ep, "qmat": qmat, "mat": mat} + + +def _selected_eigenpairs( + mat, + *, + num_modes: int, + sigma: complex, + krylov_dim: int, + initial_vector: np.ndarray | None, + spla, + scipy_linalg, +) -> tuple[np.ndarray, np.ndarray]: + size = mat.shape[0] + if num_modes >= size - 1: + values, vectors = scipy_linalg.eig(mat.toarray()) + order = np.argsort(np.abs(values - sigma))[:num_modes] + return values[order], vectors[:, order] + + ncv = min(size, max(int(krylov_dim), num_modes + 2)) + if ncv <= num_modes + 1: + ncv = min(size, num_modes + 2) + values, vectors = spla.eigs( + mat, + k=num_modes, + sigma=sigma, + which="LM", + v0=None if initial_vector is None else np.asarray(initial_vector, dtype=np.complex128), + ncv=ncv, + tol=1e-10, + ) + return np.asarray(values, dtype=np.complex128), np.asarray(vectors, dtype=np.complex128) + + +def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: np.ndarray) -> dict[str, object]: + for mode in modes: + _normalize_to_unit_power(mode, cell_areas) + + for mode_index, mode in enumerate(modes): + for previous in modes[:mode_index]: + denom = _lorentz_overlap(previous, previous, cell_areas) + if abs(denom) <= np.finfo(float).eps: + continue + coeff = _lorentz_overlap(previous, mode, cell_areas) / denom + mode.add_scaled(previous, -coeff) + _normalize_to_unit_power(mode, cell_areas) + _apply_dominant_e_phase_convention(mode) + + power_norms = np.asarray([abs(_transverse_power(mode, cell_areas)) for mode in modes], dtype=float) + lorentz_norms = np.asarray([_lorentz_overlap(mode, mode, cell_areas) for mode in modes], dtype=np.complex128) + error = 0.0 + for left_index, left in enumerate(modes): + for right_index, right in enumerate(modes): + if left_index == right_index: + continue + denom = float(np.sqrt(abs(lorentz_norms[left_index]) * abs(lorentz_norms[right_index]))) + if denom <= np.finfo(float).eps: + continue + error = max(error, abs(_lorentz_overlap(left, right, cell_areas)) / denom) + return { + "power_norms": power_norms, + "lorentz_norms": lorentz_norms, + "lorentz_orthogonality_error": float(error), + } + + +def _normalize_to_unit_power(mode: _ModeFields, cell_areas: np.ndarray) -> float: + norm = abs(_transverse_power(mode, cell_areas)) + if norm <= np.finfo(float).eps: + return 0.0 + scale = 1.0 / np.sqrt(norm) + for component in mode.components(): + component *= scale + return abs(_transverse_power(mode, cell_areas)) + + +def _transverse_power(mode: _ModeFields, cell_areas: np.ndarray) -> complex: + return complex(np.sum((mode.ex * np.conj(mode.hy) - mode.ey * np.conj(mode.hx)) * cell_areas)) + + +def _lorentz_overlap(left: _ModeFields, right: _ModeFields, cell_areas: np.ndarray) -> complex: + left_cross_right = np.sum((left.ex * right.hy - left.ey * right.hx) * cell_areas) + right_cross_left = np.sum((right.ex * left.hy - right.ey * left.hx) * cell_areas) + return complex(0.5 * (left_cross_right + right_cross_left)) + + +def _apply_dominant_e_phase_convention(mode: _ModeFields) -> None: + electric = np.concatenate((mode.ex, mode.ey, mode.ez)) + if electric.size == 0: + return + anchor = electric[int(np.argmax(np.abs(electric) ** 2))] + if abs(anchor) <= np.finfo(float).eps: + return + phase = np.conj(anchor) / abs(anchor) + for component in mode.components(): + component *= phase diff --git a/tests/test_micromode_api.py b/tests/test_micromode_api.py index 66018c3..6245960 100644 --- a/tests/test_micromode_api.py +++ b/tests/test_micromode_api.py @@ -94,6 +94,57 @@ def test_materials_api_matches_component_api_for_diagonal_grid(): assert from_materials.field_components["Ex"].shape == (5, 4, 1, 1, 2) +def test_scipy_reference_backend_matches_rust_for_diagonal_grid(): + pytest.importorskip("scipy") + eps, x_edges, y_edges = _strip_grid(5, 4) + freq = mm.C_0 / 1.55 + common = { + "eps_xx": eps, + "x_edges": x_edges, + "y_edges": y_edges, + "freqs": [freq], + "num_modes": 2, + "target_neff": 2.5, + "krylov_dim": 18, + } + + rust = mm.solve_grid(**common) + reference = mm.solve_grid(**common, backend="scipy-reference") + + np.testing.assert_allclose(reference.n_complex.values, rust.n_complex.values, rtol=1e-8, atol=1e-8) + run_info = _solver_info(reference)["runs"][0] + assert _solver_info(reference)["backend"] == "scipy_arpack_reference" + assert run_info["backend_kind"] == "diagonal_scipy_reference" + assert run_info["operator_size"] == _solver_info(rust)["runs"][0]["operator_size"] + assert run_info["operator_nnz"] == _solver_info(rust)["runs"][0]["operator_nnz"] + np.testing.assert_allclose(run_info["power_norms"], np.ones(2), rtol=1e-10, atol=1e-10) + assert run_info["lorentz_orthogonality_error"] < 1e-8 + + +def test_scipy_reference_backend_rejects_unsupported_solver_features(): + eps, x_edges, y_edges = _strip_grid(4, 3) + + with pytest.raises(ValueError, match="does not support PML"): + mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + freqs=[mm.C_0 / 1.55], + pml=(1, 0), + backend="scipy-reference", + ) + + with pytest.raises(ValueError, match="only diagonal material grids"): + mm.solve_grid( + eps_xx=eps, + eps_xz=np.full_like(eps, 0.01), + x_edges=x_edges, + y_edges=y_edges, + freqs=[mm.C_0 / 1.55], + backend="scipy-reference", + ) + + def test_materials_api_accepts_full_tensor_grid(): x_edges = _linspace_edges(-1.0, 1.0, 5) y_edges = _linspace_edges(-0.8, 0.8, 4) diff --git a/uv.lock b/uv.lock index e48e271..e8b6a02 100644 --- a/uv.lock +++ b/uv.lock @@ -864,6 +864,10 @@ dev = [ { name = "tomli" }, { name = "twine" }, ] +scipy = [ + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] [package.metadata] requires-dist = [ @@ -878,11 +882,12 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5,<8" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6,<4.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0,<1" }, + { name = "scipy", marker = "extra == 'scipy'", specifier = ">=1.11,<2.0" }, { name = "tomli", marker = "extra == 'dev'", specifier = ">=2,<3" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5,<7" }, { name = "xarray", specifier = ">=2023.8,<2026.5.0" }, ] -provides-extras = ["dev"] +provides-extras = ["scipy", "dev"] [[package]] name = "more-itertools" @@ -1457,6 +1462,121 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, +] + [[package]] name = "secretstorage" version = "3.5.0" From f96f4a3afdbd7788bd3e00b5debc000969e079cd Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 01:34:00 +0200 Subject: [PATCH 02/16] Document backend trust model --- README.md | 18 +++++++++ docs/backend-trust.md | 79 +++++++++++++++++++++++++++++++++++++ docs/mode-solver-methods.md | 12 ++++-- src/eigensolve.rs | 11 ++++-- src/operators.rs | 12 +++++- 5 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 docs/backend-trust.md diff --git a/README.md b/README.md index e1c0297..ae822b0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ pip install micromode - **Grid-first API**: pass arrays directly, with no required geometry model. - **Fast**, portable Rust sparse backend: one production solve path. +- **Auditable** optional SciPy reference backend for diagonal-grid checks. - **Practical** outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions, Lorentz overlaps, plotting, dataframe export, and HDF5 save/load. - **Tensor-aware**: supports scalar, diagonal anisotropic, and full tensor material @@ -124,3 +125,20 @@ around the requested effective index. The Arnoldi stage uses [Ritz-pair](https://en.wikipedia.org/wiki/Ritz_method) checkpointing, early stopping once requested modes are stable, and selective Ritz vector reconstruction so work is spent on the modes that will actually be returned. + +For users who want an executable Python reference, MicroMode also provides an +optional SciPy/ARPACK backend for the untransformed diagonal sparse path: + +```bash +pip install "micromode[scipy]" +``` + +```python +data = mm.solve_modes(..., backend="scipy-reference") +``` + +This backend is intentionally slower and narrower than Rust. Its purpose is to +make the core diagonal eigenproblem easy to inspect in Python and to validate +that the production Rust backend returns the same effective indices and +normalization diagnostics on supported cases. See +[docs/backend-trust.md](docs/backend-trust.md). diff --git a/docs/backend-trust.md b/docs/backend-trust.md new file mode 100644 index 0000000..1462caa --- /dev/null +++ b/docs/backend-trust.md @@ -0,0 +1,79 @@ +# Backend Trust Model + +MicroMode has two backend roles: + +- The Rust backend is the production solver. It is fast, portable, and does not + require users to install ARPACK, SuiteSparse, BLAS/LAPACK bindings, or a + Fortran toolchain. +- The SciPy reference backend is an optional audit path. It mirrors the diagonal + sparse operator in Python and calls SciPy/ARPACK so the core numerical method + can be inspected by users who are more comfortable reading Python. + +The SciPy backend is selected explicitly: + +```python +import micromode as mm + +data = mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + freqs=[freq], + num_modes=2, + target_neff=2.5, + backend="scipy-reference", +) +``` + +Install the optional dependency with: + +```bash +pip install "micromode[scipy]" +``` + +## Supported Scope + +The first reference backend intentionally covers only the smallest useful +surface: + +- diagonal scalar or diagonal-anisotropic material grids, +- no angle or bend transform, +- no PML, +- the same reduced transverse eigenproblem used by the Rust diagonal backend. + +Full tensor grids, transformed grids, and PML remain Rust-only for now. That +keeps the reference implementation short enough to audit. + +## What Is Compared + +The test suite runs the same diagonal grid through both backends and checks: + +- returned complex effective indices, +- sparse operator size and nonzero count, +- unit-power normalization diagnostics, +- Lorentz orthogonality diagnostics. + +Run the focused cross-backend check with: + +```bash +uv run --extra scipy pytest tests/test_micromode_api.py -k scipy_reference +``` + +Without the SciPy extra installed, the comparison test is skipped and the Rust +production tests still run. + +## Reading The Code + +The relevant files are: + +- `python/micromode/scipy_reference.py`: readable Python/SciPy implementation of + the diagonal sparse path. +- `python/micromode/raster.py`: public backend selection and `Result` wrapping. +- `src/operators.rs`: Rust Maxwell operator assembly. +- `src/eigensolve.rs`: Rust shift-invert Arnoldi and sparse LU path. +- `src/mode_solver.rs`: Rust field reconstruction, normalization, and Lorentz + orthogonalization. + +The intended trust chain is: inspect the Python reference, inspect the Rust +operator comments, then run the cross-backend test to verify that Rust and SciPy +agree on the supported diagonal cases. diff --git a/docs/mode-solver-methods.md b/docs/mode-solver-methods.md index d144284..1544ef1 100644 --- a/docs/mode-solver-methods.md +++ b/docs/mode-solver-methods.md @@ -2,8 +2,8 @@ `solve_modes(...)` is the main entry point. It validates the `Materials` grid, resolves frequencies or wavelengths, builds the Yee derivative matrices, chooses -the diagonal or tensorial Rust backend, solves one frequency at a time, and -returns a coordinate-aware `Result`. +the requested backend, solves one frequency at a time, and returns a +coordinate-aware `Result`. ## Material Grids @@ -33,7 +33,8 @@ returns a coordinate-aware `Result`. ## Eigenpair Selection -Internally, eigenpairs are selected with sparse shift-invert Arnoldi [1, 2]. +The production Rust backend selects eigenpairs with sparse shift-invert Arnoldi +[1, 2]. For a matrix \(A\) and shift \(\sigma\), Arnoldi is applied to $$ @@ -46,6 +47,11 @@ where \(\theta\) is a Ritz value of the inverse-shifted operator. The diagonal backend uses \(\sigma=-\texttt{target_neff}^2\); the tensorial backend uses \(\sigma=\texttt{target_neff}\). +The optional `backend="scipy-reference"` path solves the same diagonal sparse +operator with SciPy/ARPACK. It is limited to untransformed diagonal grids without +PML and exists as a readable validation backend, not as the default production +solver. Install it with `micromode[scipy]`. + Returned modes are sorted by decreasing real effective index, normalized to unit transverse power, diff --git a/src/eigensolve.rs b/src/eigensolve.rs index b21dbac..0fa1d70 100644 --- a/src/eigensolve.rs +++ b/src/eigensolve.rs @@ -189,7 +189,11 @@ where let mut actual_dim = 0usize; for col in 0..krylov_dim { - // Native Arnoldi fallback with one reorthogonalization pass. + // Arnoldi sees only y = (A - sigma I)^-1 x. Keeping that action behind + // a closure makes the Krylov logic independent from the linear solver: + // native sparse LU here, SciPy/ARPACK in the Python reference backend. + // The second Gram-Schmidt pass is deliberate because clustered + // waveguide modes otherwise lose orthogonality quickly. let solve_start = profile.as_ref().map(|_| Instant::now()); let mut work = vec![Complex64::new(0.0, 0.0); n]; solve(q_basis.vector(col), &mut work)?; @@ -426,8 +430,9 @@ impl SparseLu { matrix: &SparseMatrix, mut profile: Option<&mut ShiftInvertProfile>, ) -> Result { - // Native sparse LU is the fallback linear solve. Validate pivots here - // so `solve` can assume the factors are usable. + // Native sparse LU is the production linear solve for shift-invert. + // Validate pivots here because a missing pivot means the requested + // shift made (A - sigma I) numerically unusable for this grid. if matrix.rows != matrix.cols { return Err("LU factorization requires a square matrix".to_string()); } diff --git a/src/operators.rs b/src/operators.rs index 683e4bc..d56d886 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -26,7 +26,10 @@ pub fn assemble_sparse_diagonal_operators( der_mats: &[SparseMatrix; 4], ) -> SparseDiagonalOperators { // Diagonal media reduce to a 2N x 2N transverse-electric eigenproblem for - // [Ex, Ey]. Ez and H are recovered later from Maxwell curl equations. + // [Ex, Ey]. The P/Q block notation follows the standard vectorial FDFD + // mode formulation: Q maps transverse E to transverse H up to the + // propagation constant, while P maps transverse H back to transverse E. + // Ez, Hz, and the physical H scale are reconstructed after the eigen solve. let n = eps[0][0].len(); let eps_xx = &eps[0][0]; let eps_yy = &eps[1][1]; @@ -53,6 +56,8 @@ pub fn assemble_sparse_diagonal_operators( .collect::>(), ); + // Material-only part of P. The signs encode z-normal cross products: + // transverse H couples to [Ey, -Ex] through the diagonal mu block. let p_mu = SparseMatrix::block_2x2( &zero, &SparseMatrix::diagonal(mu_yy), @@ -60,6 +65,8 @@ pub fn assemble_sparse_diagonal_operators( &zero, ); + // Longitudinal electric elimination. The derivative sandwich + // Df * inv(eps_zz) * Db is the Schur-complement contribution from Ez. let p00 = dxf .matmul(&inv_eps_zz) .matmul(dyb) @@ -72,6 +79,7 @@ pub fn assemble_sparse_diagonal_operators( let p11 = dyf.matmul(&inv_eps_zz).matmul(dxb); let p_partial = SparseMatrix::block_2x2(&p00, &p01, &p10, &p11); + // Material-only part of Q. It is the epsilon-side analogue of p_mu. let q_ep = SparseMatrix::block_2x2( &zero, &SparseMatrix::diagonal(eps_yy), @@ -79,6 +87,8 @@ pub fn assemble_sparse_diagonal_operators( &zero, ); + // Longitudinal magnetic elimination. This mirrors p_partial with mu_zz and + // backward/forward derivatives swapped for Yee staggering. let q00 = dxb .matmul(&inv_mu_zz) .matmul(dyf) From 17f00bb68cee87e19dd5ec6798677fdad0a1ffc1 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 02:03:00 +0200 Subject: [PATCH 03/16] Expand SciPy backend parity --- README.md | 12 +- docs/backend-trust.md | 41 ++- docs/mode-solver-methods.md | 8 +- python/micromode/raster.py | 128 ++++++++-- python/micromode/scipy_reference.py | 375 +++++++++++++++++++++++++++- tests/test_micromode_api.py | 82 ++++-- 6 files changed, 578 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index ae822b0..6cbc999 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ pip install micromode - **Grid-first API**: pass arrays directly, with no required geometry model. - **Fast**, portable Rust sparse backend: one production solve path. -- **Auditable** optional SciPy reference backend for diagonal-grid checks. +- **Auditable** optional SciPy reference backend for cross-checking solves. - **Practical** outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions, Lorentz overlaps, plotting, dataframe export, and HDF5 save/load. - **Tensor-aware**: supports scalar, diagonal anisotropic, and full tensor material @@ -127,7 +127,7 @@ stopping once requested modes are stable, and selective Ritz vector reconstruction so work is spent on the modes that will actually be returned. For users who want an executable Python reference, MicroMode also provides an -optional SciPy/ARPACK backend for the untransformed diagonal sparse path: +optional SciPy/ARPACK backend: ```bash pip install "micromode[scipy]" @@ -137,8 +137,8 @@ pip install "micromode[scipy]" data = mm.solve_modes(..., backend="scipy-reference") ``` -This backend is intentionally slower and narrower than Rust. Its purpose is to -make the core diagonal eigenproblem easy to inspect in Python and to validate -that the production Rust backend returns the same effective indices and -normalization diagnostics on supported cases. See +This backend is intentionally slower than Rust. Its purpose is to make the core +eigenproblems easy to inspect in Python and to validate that the production Rust +backend returns the same effective indices and normalization diagnostics on +supported cases. See [docs/backend-trust.md](docs/backend-trust.md). diff --git a/docs/backend-trust.md b/docs/backend-trust.md index 1462caa..4b0820c 100644 --- a/docs/backend-trust.md +++ b/docs/backend-trust.md @@ -5,9 +5,9 @@ MicroMode has two backend roles: - The Rust backend is the production solver. It is fast, portable, and does not require users to install ARPACK, SuiteSparse, BLAS/LAPACK bindings, or a Fortran toolchain. -- The SciPy reference backend is an optional audit path. It mirrors the diagonal - sparse operator in Python and calls SciPy/ARPACK so the core numerical method - can be inspected by users who are more comfortable reading Python. +- The SciPy reference backend is an optional audit path. It mirrors the sparse + operators in Python and calls SciPy/ARPACK so the core numerical method can be + inspected by users who are more comfortable reading Python. The SciPy backend is selected explicitly: @@ -33,26 +33,31 @@ pip install "micromode[scipy]" ## Supported Scope -The first reference backend intentionally covers only the smallest useful -surface: +The reference backend covers the same core solve families as Rust: - diagonal scalar or diagonal-anisotropic material grids, -- no angle or bend transform, -- no PML, -- the same reduced transverse eigenproblem used by the Rust diagonal backend. +- full tensor material grids, +- angle and bend transforms that route through the tensorial operator, +- PML stretching, +- one-dimensional slices, because they are padded into ordinary mode-plane + grids before backend dispatch. -Full tensor grids, transformed grids, and PML remain Rust-only for now. That -keeps the reference implementation short enough to audit. +The remaining difference is operational rather than mathematical: the SciPy +backend depends on SciPy/ARPACK and is expected to be slower and less portable +than the Rust backend. ## What Is Compared -The test suite runs the same diagonal grid through both backends and checks: +The test suite runs representative grids through both backends and checks: - returned complex effective indices, - sparse operator size and nonzero count, - unit-power normalization diagnostics, - Lorentz orthogonality diagnostics. +The covered cases include diagonal grids, PML, full-tensor/off-diagonal grids, +and transformed grids. + Run the focused cross-backend check with: ```bash @@ -67,7 +72,7 @@ production tests still run. The relevant files are: - `python/micromode/scipy_reference.py`: readable Python/SciPy implementation of - the diagonal sparse path. + the diagonal and tensorial sparse paths. - `python/micromode/raster.py`: public backend selection and `Result` wrapping. - `src/operators.rs`: Rust Maxwell operator assembly. - `src/eigensolve.rs`: Rust shift-invert Arnoldi and sparse LU path. @@ -76,4 +81,14 @@ The relevant files are: The intended trust chain is: inspect the Python reference, inspect the Rust operator comments, then run the cross-backend test to verify that Rust and SciPy -agree on the supported diagonal cases. +agree on the supported cases. + +## External References + +MicroMode also keeps Tidy3D-oriented examples and fixtures because Tidy3D is a +recognizable reference point for photonics users. Tidy3D's +[public source docs](https://docs.flexcompute.com/projects/tidy3d/en/latest/_modules/tidy3d/components/mode/mode_solver.html) +show that its local mode solver import can fail when SciPy is unavailable, so +Tidy3D-style comparisons are useful as behavioral validation in addition to the +internal Rust-vs-SciPy checks. See the Tidy3D example in `examples/` and the +committed mode-solver fixture harness for existing comparison infrastructure. diff --git a/docs/mode-solver-methods.md b/docs/mode-solver-methods.md index 1544ef1..3ae6dc3 100644 --- a/docs/mode-solver-methods.md +++ b/docs/mode-solver-methods.md @@ -47,10 +47,10 @@ where \(\theta\) is a Ritz value of the inverse-shifted operator. The diagonal backend uses \(\sigma=-\texttt{target_neff}^2\); the tensorial backend uses \(\sigma=\texttt{target_neff}\). -The optional `backend="scipy-reference"` path solves the same diagonal sparse -operator with SciPy/ARPACK. It is limited to untransformed diagonal grids without -PML and exists as a readable validation backend, not as the default production -solver. Install it with `micromode[scipy]`. +The optional `backend="scipy-reference"` path assembles the same diagonal or +tensorial sparse operator in Python and solves it with SciPy/ARPACK. It exists +as a readable validation backend, not as the default production solver. Install +it with `micromode[scipy]`. Returned modes are sorted by decreasing real effective index, normalized to unit transverse power, diff --git a/python/micromode/raster.py b/python/micromode/raster.py index 27b8ef3..85bf02d 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -11,7 +11,7 @@ from ._rust import C_0, solve_diagonal_sparse, solve_tensorial_sparse from .models import BoundaryCondition, BoundarySpec, Materials, PmlSpec, SliceAxis, Spec from .result import Result -from .scipy_reference import solve_diagonal_scipy_reference +from .scipy_reference import solve_diagonal_scipy_reference, solve_tensorial_scipy_reference _COMPONENTS = ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz") Backend = Literal["rust", "scipy-reference"] @@ -352,25 +352,6 @@ def _solve_one_frequency( target_neff = _shift_target_neff(float(target_neff)) has_transform = abs(angle_theta) > 0.0 or bend_radius is not None is_diagonal = material_grid.is_diagonal - if backend == "scipy-reference": - if has_transform: - raise ValueError("backend='scipy-reference' currently supports only untransformed diagonal grids") - if not is_diagonal: - raise ValueError("backend='scipy-reference' currently supports only diagonal material grids") - if pml_spec.num_cells != (0, 0): - raise ValueError("backend='scipy-reference' currently does not support PML") - return _solve_one_frequency_scipy_reference( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - freq=freq, - num_modes=num_modes, - target_neff=target_neff, - direction=direction, - krylov_dim=krylov_dim, - boundary_spec=boundary_spec, - ) if has_transform: # Angle and bend coordinates are applied by transforming eps/mu. A # diagonal grid may become full tensor after this step. @@ -386,6 +367,20 @@ def _solve_one_frequency( ) if not (_is_diagonal_tensor(eps_tensor) and _is_diagonal_tensor(mu_tensor)): # Off-diagonal components require the tensorial first-order operator. + if backend == "scipy-reference": + return _solve_one_frequency_scipy_tensorial_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + freq=freq, + num_modes=num_modes, + target_neff=target_neff, + pml_spec=pml_spec, + direction=direction, + krylov_dim=krylov_dim, + boundary_spec=boundary_spec, + ) return _solve_one_frequency_rust_tensorial_sparse( eps_tensor=eps_tensor, mu_tensor=mu_tensor, @@ -401,6 +396,20 @@ def _solve_one_frequency( ) # If the transformed tensors remain diagonal, keep the faster diagonal # sparse formulation. + if backend == "scipy-reference": + return _solve_one_frequency_scipy_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + freq=freq, + num_modes=num_modes, + target_neff=target_neff, + pml_spec=pml_spec, + direction=direction, + krylov_dim=krylov_dim, + boundary_spec=boundary_spec, + ) return _solve_one_frequency_rust_sparse( eps_tensor=eps_tensor, mu_tensor=mu_tensor, @@ -416,6 +425,20 @@ def _solve_one_frequency( ) if not is_diagonal: # User supplied a full tensor grid with no coordinate transform. + if backend == "scipy-reference": + return _solve_one_frequency_scipy_tensorial_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + freq=freq, + num_modes=num_modes, + target_neff=target_neff, + pml_spec=pml_spec, + direction=direction, + krylov_dim=krylov_dim, + boundary_spec=boundary_spec, + ) return _solve_one_frequency_rust_tensorial_sparse( eps_tensor=eps_tensor, mu_tensor=mu_tensor, @@ -430,6 +453,20 @@ def _solve_one_frequency( boundary_spec=boundary_spec, ) # Ordinary scalar/diagonal grids use the production diagonal sparse backend. + if backend == "scipy-reference": + return _solve_one_frequency_scipy_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + freq=freq, + num_modes=num_modes, + target_neff=target_neff, + pml_spec=pml_spec, + direction=direction, + krylov_dim=krylov_dim, + boundary_spec=boundary_spec, + ) return _solve_one_frequency_rust_sparse( eps_tensor=eps_tensor, mu_tensor=mu_tensor, @@ -500,6 +537,7 @@ def _solve_one_frequency_scipy_reference( freq: float, num_modes: int, target_neff: float, + pml_spec: PmlSpec, direction: str, krylov_dim: int | None, boundary_spec: BoundarySpec, @@ -516,6 +554,10 @@ def _solve_one_frequency_scipy_reference( neff_guess=target_neff, direction=direction, derivative_scale=C_0 / (2 * np.pi * freq), + omega=2 * np.pi * freq, + num_pml=pml_spec.num_cells, + pml_profile=pml_spec.profile_dict(), + dmin_pml=boundary_spec.dmin_pml, dmin_pmc=boundary_spec.dmin_pmc, krylov_dim=actual_krylov_dim, initial_vector=_default_initial_vector(2 * nx * ny, shape=(nx, ny)), @@ -532,6 +574,52 @@ def _solve_one_frequency_scipy_reference( ) +def _solve_one_frequency_scipy_tensorial_reference( + *, + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, + dlf: tuple[np.ndarray, np.ndarray], + dlb: tuple[np.ndarray, np.ndarray], + freq: float, + num_modes: int, + target_neff: float, + pml_spec: PmlSpec, + direction: str, + krylov_dim: int | None, + boundary_spec: BoundarySpec, +) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: + nx = len(dlf[0]) + ny = len(dlf[1]) + actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) + n_complex, fields, solver_info = solve_tensorial_scipy_reference( + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dlf=dlf, + dlb=dlb, + num_modes=num_modes, + neff_guess=target_neff, + direction=direction, + derivative_scale=C_0 / (2 * np.pi * freq), + omega=2 * np.pi * freq, + num_pml=pml_spec.num_cells, + pml_profile=pml_spec.profile_dict(), + dmin_pml=boundary_spec.dmin_pml, + dmin_pmc=boundary_spec.dmin_pmc, + krylov_dim=actual_krylov_dim, + initial_vector=_default_initial_vector(4 * nx * ny, shape=(nx, ny)), + ) + return ( + n_complex, + _fields_to_grid(fields, (nx, ny)), + _solver_info_with_context( + solver_info, + backend_kind="tensorial_scipy_reference", + shape=(nx, ny), + krylov_dim=actual_krylov_dim, + ), + ) + + def _solve_one_frequency_rust_tensorial_sparse( *, eps_tensor: np.ndarray, diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 2b1d390..a8d73e8 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -1,9 +1,9 @@ """Readable SciPy reference implementation for the diagonal mode solver. -This module intentionally mirrors the Rust diagonal sparse path in plain -Python/SciPy. It is slower and narrower than the production backend, but it +This module intentionally mirrors the Rust sparse mode-solver paths in plain +Python/SciPy. It is slower and less portable than the production backend, but it keeps the numerical contract inspectable by users who want to audit the -finite-difference operator against SciPy/ARPACK. +finite-difference operators against SciPy/ARPACK. """ from __future__ import annotations @@ -13,6 +13,9 @@ import numpy as np ETA0 = 376.730_313_666_853_5 +MU0 = 1.256_637_062_12e-12 +C0 = 2.997_924_58e14 +EPSILON0 = 1.0 / (MU0 * C0 * C0) SparseSolveResult = tuple[np.ndarray, list[np.ndarray], dict[str, object]] @@ -45,15 +48,18 @@ def solve_diagonal_scipy_reference( neff_guess: float, direction: str, derivative_scale: float, + omega: float | None = None, + num_pml: tuple[int, int] = (0, 0), + pml_profile: dict[str, float | int] | None = None, + dmin_pml: tuple[bool, bool] = (True, True), dmin_pmc: tuple[bool, bool] = (False, False), krylov_dim: int = 32, initial_vector: np.ndarray | None = None, ) -> SparseSolveResult: """Solve the diagonal sparse eigenproblem with SciPy/ARPACK. - Supported scope is deliberately small: diagonal material tensors, no PML, - and the same reduced ``[Ex, Ey]`` transverse eigenproblem used by the Rust - production backend. + This is the same reduced ``[Ex, Ey]`` transverse eigenproblem used by the + Rust production backend for diagonal material tensors. """ sparse, spla, scipy_linalg = _import_scipy() @@ -67,9 +73,15 @@ def solve_diagonal_scipy_reference( derivatives = _create_derivative_matrices( sparse, + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, shape=(nx, ny), dlf=dlf, dlb=dlb, + omega=omega, + num_pml=num_pml, + pml_profile=pml_profile, + dmin_pml=dmin_pml, dmin_pmc=dmin_pmc, scale=float(derivative_scale), ) @@ -156,6 +168,127 @@ def solve_diagonal_scipy_reference( ) +def solve_tensorial_scipy_reference( + *, + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, + dlf: tuple[np.ndarray, np.ndarray], + dlb: tuple[np.ndarray, np.ndarray], + num_modes: int, + neff_guess: float, + direction: str, + derivative_scale: float, + omega: float | None = None, + num_pml: tuple[int, int] = (0, 0), + pml_profile: dict[str, float | int] | None = None, + dmin_pml: tuple[bool, bool] = (True, True), + dmin_pmc: tuple[bool, bool] = (False, False), + krylov_dim: int = 32, + initial_vector: np.ndarray | None = None, +) -> SparseSolveResult: + """Solve the first-order tensorial sparse eigenproblem with SciPy/ARPACK.""" + + sparse, spla, scipy_linalg = _import_scipy() + nx = len(dlf[0]) + ny = len(dlf[1]) + n = nx * ny + if eps_tensor.shape != mu_tensor.shape or eps_tensor.shape[:2] != (3, 3) or eps_tensor.shape[-1] != n: + raise ValueError("eps_tensor and mu_tensor must both have shape (3, 3, len(dlf[0]) * len(dlf[1]))") + if num_modes <= 0: + raise ValueError("num_modes must be positive") + + derivatives = _create_derivative_matrices( + sparse, + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + shape=(nx, ny), + dlf=dlf, + dlb=dlb, + omega=omega, + num_pml=num_pml, + pml_profile=pml_profile, + dmin_pml=dmin_pml, + dmin_pmc=dmin_pmc, + scale=float(derivative_scale), + ) + operator = _assemble_tensorial_operator(sparse, eps_tensor, mu_tensor, derivatives) + values, vectors = _selected_eigenpairs( + operator, + num_modes=num_modes, + sigma=complex(neff_guess, 0.0), + krylov_dim=krylov_dim, + initial_vector=initial_vector, + spla=spla, + scipy_linalg=scipy_linalg, + ) + residuals = np.asarray( + [ + np.linalg.norm(operator @ vectors[:, index] - values[index] * vectors[:, index]) + for index in range(len(values)) + ], + dtype=float, + ) + residuals = residuals / np.maximum(np.linalg.norm(vectors, axis=0), np.finfo(float).eps) + + modes = [ + (complex(value), np.asarray(vector, dtype=np.complex128), float(residual)) + for value, vector, residual in zip(values, vectors.T, residuals, strict=True) + ] + modes.sort(key=lambda item: item[0].real, reverse=True) + + inv_eps_zz = sparse.diags(1.0 / eps_tensor[2, 2, :], format="csc") + inv_mu_zz = sparse.diags(1.0 / mu_tensor[2, 2, :], format="csc") + dxf, dxb, dyf, dyb = derivatives + cell_areas = np.repeat(np.asarray(dlf[0], dtype=float), ny) * np.tile(np.asarray(dlf[1], dtype=float), nx) + + n_complex: list[complex] = [] + mode_fields: list[_ModeFields] = [] + sorted_residuals: list[float] = [] + for mode_n, vector, residual in modes: + ex = vector[:n].copy() + ey = vector[n : 2 * n].copy() + hx = vector[2 * n : 3 * n].copy() + hy = vector[3 * n : 4 * n].copy() + + hz_source = dxf @ ey - dyf @ ex - mu_tensor[2, 0, :] * hx - mu_tensor[2, 1, :] * hy + hz = np.asarray(inv_mu_zz @ hz_source, dtype=np.complex128) + + ez_source = dxb @ hy - dyb @ hx - eps_tensor[2, 0, :] * ex - eps_tensor[2, 1, :] * ey + ez = np.asarray(inv_eps_zz @ ez_source, dtype=np.complex128) + + h_scale = -1j / ETA0 + hx *= h_scale + hy *= h_scale + hz *= h_scale + if direction == "-": + hx *= -1.0 + hy *= -1.0 + ez *= -1.0 + + n_complex.append(mode_n) + sorted_residuals.append(residual) + mode_fields.append(_ModeFields(ex=ex, ey=ey, ez=ez, hx=hx, hy=hy, hz=hz)) + + orthogonalization = _lorentz_orthogonalize_and_normalize(mode_fields, cell_areas) + fields = [ + np.asarray([getattr(mode, component) for mode in mode_fields], dtype=np.complex128) + for component in ("ex", "ey", "ez", "hx", "hy", "hz") + ] + return ( + np.asarray(n_complex, dtype=np.complex128), + fields, + { + "backend": "scipy_arpack_reference", + "operator_size": int(operator.shape[0]), + "operator_nnz": int(operator.nnz), + "residuals": np.asarray(sorted_residuals, dtype=float), + "power_norms": orthogonalization["power_norms"], + "lorentz_norms": orthogonalization["lorentz_norms"], + "lorentz_orthogonality_error": orthogonalization["lorentz_orthogonality_error"], + }, + ) + + def _import_scipy(): try: import scipy.linalg as scipy_linalg @@ -169,9 +302,15 @@ def _import_scipy(): def _create_derivative_matrices( sparse, *, + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, shape: tuple[int, int], dlf: tuple[np.ndarray, np.ndarray], dlb: tuple[np.ndarray, np.ndarray], + omega: float | None, + num_pml: tuple[int, int], + pml_profile: dict[str, float | int] | None, + dmin_pml: tuple[bool, bool], dmin_pmc: tuple[bool, bool], scale: float, ): @@ -181,6 +320,23 @@ def _create_derivative_matrices( _make_dyf(sparse, np.asarray(dlf[1], dtype=float), shape, dmin_pmc[1]), _make_dyb(sparse, np.asarray(dlb[1], dtype=float), shape, dmin_pmc[1]), ) + if num_pml[0] > 0 or num_pml[1] > 0: + if omega is None: + raise ValueError("omega is required when num_pml is nonzero") + pml_values = _create_s_diagonal_values( + shape=shape, + num_pml=num_pml, + dlf=dlf, + dlb=dlb, + eps_tensor=eps_tensor, + mu_tensor=mu_tensor, + dmin_pml=dmin_pml, + omega=float(omega), + profile=pml_profile, + ) + matrices = tuple( + sparse.diags(values, format="csc") @ matrix for values, matrix in zip(pml_values, matrices, strict=True) + ) return tuple(matrix * complex(scale, 0.0) for matrix in matrices) @@ -310,6 +466,63 @@ def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_ma return {"q_ep": q_ep, "qmat": qmat, "mat": mat} +def _assemble_tensorial_operator(sparse, eps: np.ndarray, mu: np.ndarray, der_mats): + dxf, dxb, dyf, dyb = der_mats + inv_eps_22 = sparse.diags(1.0 / eps[2, 2, :], format="csc") + inv_mu_22 = sparse.diags(1.0 / mu[2, 2, :], format="csc") + + def diag(values): + return sparse.diags(values, format="csc") + + eps_20_over_22 = eps[2, 0, :] / eps[2, 2, :] + eps_21_over_22 = eps[2, 1, :] / eps[2, 2, :] + eps_02_over_22 = eps[0, 2, :] / eps[2, 2, :] + eps_12_over_22 = eps[1, 2, :] / eps[2, 2, :] + mu_20_over_22 = mu[2, 0, :] / mu[2, 2, :] + mu_21_over_22 = mu[2, 1, :] / mu[2, 2, :] + mu_02_over_22 = mu[0, 2, :] / mu[2, 2, :] + mu_12_over_22 = mu[1, 2, :] / mu[2, 2, :] + + mu_10_s = mu[1, 0, :] - mu[1, 2, :] * mu_20_over_22 + mu_11_s = mu[1, 1, :] - mu[1, 2, :] * mu_21_over_22 + mu_00_s = mu[0, 0, :] - mu[0, 2, :] * mu_20_over_22 + mu_01_s = mu[0, 1, :] - mu[0, 2, :] * mu_21_over_22 + eps_10_s = eps[1, 0, :] - eps[1, 2, :] * eps_20_over_22 + eps_11_s = eps[1, 1, :] - eps[1, 2, :] * eps_21_over_22 + eps_00_s = eps[0, 0, :] - eps[0, 2, :] * eps_20_over_22 + eps_01_s = eps[0, 1, :] - eps[0, 2, :] * eps_21_over_22 + + axax = -(dxf @ diag(eps_20_over_22)) - diag(mu_12_over_22) @ dyf + axay = -(dxf @ diag(eps_21_over_22)) + diag(mu_12_over_22) @ dxf + axbx = -(dxf @ inv_eps_22 @ dyb) + diag(mu_10_s) + axby = dxf @ inv_eps_22 @ dxb + diag(mu_11_s) + + ayax = -(dyf @ diag(eps_20_over_22)) + diag(mu_02_over_22) @ dyf + ayay = -(dyf @ diag(eps_21_over_22)) - diag(mu_02_over_22) @ dxf + aybx = -(dyf @ inv_eps_22 @ dyb) - diag(mu_00_s) + ayby = dyf @ inv_eps_22 @ dxb - diag(mu_01_s) + + bxax = -(dxb @ inv_mu_22 @ dyf) + diag(eps_10_s) + bxay = dxb @ inv_mu_22 @ dxf + diag(eps_11_s) + bxbx = -(dxb @ diag(mu_20_over_22)) - diag(eps_12_over_22) @ dyb + bxby = -(dxb @ diag(mu_21_over_22)) + diag(eps_12_over_22) @ dxb + + byax = -(dyb @ inv_mu_22 @ dyf) - diag(eps_00_s) + byay = dyb @ inv_mu_22 @ dxf - diag(eps_01_s) + bybx = -(dyb @ diag(mu_20_over_22)) + diag(eps_02_over_22) @ dyb + byby = -(dyb @ diag(mu_21_over_22)) - diag(eps_02_over_22) @ dxb + + return -1j * sparse.bmat( + [ + [axax, axay, axbx, axby], + [ayax, ayay, aybx, ayby], + [bxax, bxay, bxbx, bxby], + [byax, byay, bybx, byby], + ], + format="csc", + ) + + def _selected_eigenpairs( mat, *, @@ -341,6 +554,156 @@ def _selected_eigenpairs( return np.asarray(values, dtype=np.complex128), np.asarray(vectors, dtype=np.complex128) +def _create_s_diagonal_values( + *, + shape: tuple[int, int], + num_pml: tuple[int, int], + dlf: tuple[np.ndarray, np.ndarray], + dlb: tuple[np.ndarray, np.ndarray], + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, + dmin_pml: tuple[bool, bool], + omega: float, + profile: dict[str, float | int] | None, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + nx, ny = shape + avg_speed = _average_relative_speed(shape, num_pml, eps_tensor, mu_tensor) + sx_f = _create_sfactor( + "f", omega, np.asarray(dlf[0], dtype=float), nx, num_pml[0], dmin_pml[0], avg_speed[:2], profile + ) + sx_b = _create_sfactor( + "b", omega, np.asarray(dlb[0], dtype=float), nx, num_pml[0], dmin_pml[0], avg_speed[:2], profile + ) + sy_f = _create_sfactor( + "f", omega, np.asarray(dlf[1], dtype=float), ny, num_pml[1], dmin_pml[1], avg_speed[2:], profile + ) + sy_b = _create_sfactor( + "b", omega, np.asarray(dlb[1], dtype=float), ny, num_pml[1], dmin_pml[1], avg_speed[2:], profile + ) + + sx_f_vec = np.empty(nx * ny, dtype=np.complex128) + sx_b_vec = np.empty(nx * ny, dtype=np.complex128) + sy_f_vec = np.empty(nx * ny, dtype=np.complex128) + sy_b_vec = np.empty(nx * ny, dtype=np.complex128) + for ix in range(nx): + for iy in range(ny): + index = ix * ny + iy + sx_f_vec[index] = 1.0 / sx_f[ix] + sx_b_vec[index] = 1.0 / sx_b[ix] + sy_f_vec[index] = 1.0 / sy_f[iy] + sy_b_vec[index] = 1.0 / sy_b[iy] + return sx_f_vec, sx_b_vec, sy_f_vec, sy_b_vec + + +def _average_relative_speed( + shape: tuple[int, int], + num_pml: tuple[int, int], + eps_tensor: np.ndarray, + mu_tensor: np.ndarray, +) -> np.ndarray: + eps_avg = _pml_average_all_sides(shape, num_pml, eps_tensor) + mu_avg = _pml_average_all_sides(shape, num_pml, mu_tensor) + return 1.0 / np.sqrt(eps_avg * mu_avg) + + +def _pml_average_all_sides(shape: tuple[int, int], num_pml: tuple[int, int], tensor: np.ndarray) -> np.ndarray: + nx, ny = shape + regions: list[list[complex]] = [[], [], [], []] + for comp in range(3): + for ix in range(nx): + for iy in range(ny): + value = complex(tensor[comp, comp, ix * ny + iy]) + if ix < num_pml[0]: + regions[0].append(value) + if ix >= max(nx - num_pml[0], 0) + 1: + regions[1].append(value) + if iy < num_pml[1]: + regions[2].append(value) + if iy >= max(ny - num_pml[1], 0) + 1: + regions[3].append(value) + out = np.ones(4, dtype=np.complex128) + for index, values in enumerate(regions): + if values: + out[index] = sum(values) / len(values) + return out + + +def _create_sfactor( + direction: str, + omega: float, + dls: np.ndarray, + n: int, + n_pml: int, + dmin_pml: bool, + avg_speed: np.ndarray, + profile: dict[str, float | int] | None, +) -> np.ndarray: + if n_pml == 0: + return np.ones(n, dtype=np.complex128) + if direction == "f": + return _create_sfactor_f(omega, dls, n, n_pml, dmin_pml, avg_speed, profile) + if direction == "b": + return _create_sfactor_b(omega, dls, n, n_pml, dmin_pml, avg_speed, profile) + raise ValueError(f"direction value {direction} not recognized") + + +def _create_sfactor_f( + omega: float, + dls: np.ndarray, + n: int, + n_pml: int, + dmin_pml: bool, + avg_speed: np.ndarray, + profile: dict[str, float | int] | None, +) -> np.ndarray: + sfactor = np.ones(n, dtype=np.complex128) + for i in range(n): + if i < n_pml and dmin_pml: + sfactor[i] = _s_value(dls[0], (n_pml - i - 0.5) / n_pml, omega, avg_speed[0], profile) + elif i >= n - n_pml: + sfactor[i] = _s_value(dls[-1], (i - (n - n_pml) + 0.5) / n_pml, omega, avg_speed[1], profile) + return sfactor + + +def _create_sfactor_b( + omega: float, + dls: np.ndarray, + n: int, + n_pml: int, + dmin_pml: bool, + avg_speed: np.ndarray, + profile: dict[str, float | int] | None, +) -> np.ndarray: + sfactor = np.ones(n, dtype=np.complex128) + for i in range(n): + if i < n_pml and dmin_pml: + sfactor[i] = _s_value(dls[0], (n_pml - i) / n_pml, omega, avg_speed[0], profile) + elif i > n - n_pml: + sfactor[i] = _s_value(dls[-1], (i - (n - n_pml)) / n_pml, omega, avg_speed[1], profile) + return sfactor + + +def _s_value( + dl: float, + step: float, + omega: float, + avg_speed: complex, + profile: dict[str, float | int] | None, +) -> complex: + values = { + "sigma_max": 2.0, + "kappa_min": 1.0, + "kappa_max": 3.0, + "order": 3, + } + if profile is not None: + values.update(profile) + step_power = step ** int(values["order"]) + kappa = float(values["kappa_min"]) + (float(values["kappa_max"]) - float(values["kappa_min"])) * step_power + sigma = avg_speed * (float(values["sigma_max"]) / (ETA0 * dl) * step_power) + return complex(kappa, 0.0) + 1j * sigma / (omega * EPSILON0) + + def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: np.ndarray) -> dict[str, object]: for mode in modes: _normalize_to_unit_power(mode, cell_areas) diff --git a/tests/test_micromode_api.py b/tests/test_micromode_api.py index 6245960..29a34ab 100644 --- a/tests/test_micromode_api.py +++ b/tests/test_micromode_api.py @@ -121,28 +121,72 @@ def test_scipy_reference_backend_matches_rust_for_diagonal_grid(): assert run_info["lorentz_orthogonality_error"] < 1e-8 -def test_scipy_reference_backend_rejects_unsupported_solver_features(): +def test_scipy_reference_backend_matches_rust_for_pml_and_tensorial_paths(): + pytest.importorskip("scipy") eps, x_edges, y_edges = _strip_grid(4, 3) - with pytest.raises(ValueError, match="does not support PML"): - mm.solve_grid( - eps_xx=eps, - x_edges=x_edges, - y_edges=y_edges, - freqs=[mm.C_0 / 1.55], - pml=(1, 0), - backend="scipy-reference", - ) + pml_common = { + "eps_xx": eps, + "x_edges": x_edges, + "y_edges": y_edges, + "freqs": [mm.C_0 / 1.55], + "num_modes": 1, + "target_neff": 2.5, + "pml": (1, 0), + "krylov_dim": 18, + } + rust_pml = mm.solve_grid(**pml_common) + reference_pml = mm.solve_grid(**pml_common, backend="scipy-reference") - with pytest.raises(ValueError, match="only diagonal material grids"): - mm.solve_grid( - eps_xx=eps, - eps_xz=np.full_like(eps, 0.01), - x_edges=x_edges, - y_edges=y_edges, - freqs=[mm.C_0 / 1.55], - backend="scipy-reference", - ) + np.testing.assert_allclose(reference_pml.n_complex.values, rust_pml.n_complex.values, rtol=1e-8, atol=1e-8) + pml_run = _solver_info(reference_pml)["runs"][0] + assert pml_run["backend_kind"] == "diagonal_scipy_reference" + assert pml_run["operator_size"] == _solver_info(rust_pml)["runs"][0]["operator_size"] + assert pml_run["operator_nnz"] == _solver_info(rust_pml)["runs"][0]["operator_nnz"] + + tensor_common = { + "eps_xx": eps, + "eps_yy": np.full_like(eps, 2.2**2), + "eps_zz": np.full_like(eps, 2.0**2), + "eps_xz": np.full_like(eps, 0.01), + "eps_zx": np.full_like(eps, 0.01), + "x_edges": x_edges, + "y_edges": y_edges, + "freqs": [mm.C_0 / 1.55], + "num_modes": 1, + "target_neff": 2.2, + "krylov_dim": 20, + } + rust_tensor = mm.solve_grid(**tensor_common) + reference_tensor = mm.solve_grid(**tensor_common, backend="scipy-reference") + + np.testing.assert_allclose(reference_tensor.n_complex.values, rust_tensor.n_complex.values, rtol=1e-8, atol=1e-8) + tensor_run = _solver_info(reference_tensor)["runs"][0] + assert tensor_run["backend_kind"] == "tensorial_scipy_reference" + assert tensor_run["operator_size"] == _solver_info(rust_tensor)["runs"][0]["operator_size"] + assert tensor_run["operator_nnz"] == _solver_info(rust_tensor)["runs"][0]["operator_nnz"] + + +def test_scipy_reference_backend_matches_rust_for_transformed_grid(): + pytest.importorskip("scipy") + eps, x_edges, y_edges = _strip_grid(4, 3) + common = { + "eps_xx": eps, + "x_edges": x_edges, + "y_edges": y_edges, + "freqs": [mm.C_0 / 1.55], + "num_modes": 1, + "target_neff": 2.5, + "angle_theta": 0.08, + "angle_phi": 0.25, + "krylov_dim": 20, + } + + rust = mm.solve_grid(**common) + reference = mm.solve_grid(**common, backend="scipy-reference") + + np.testing.assert_allclose(reference.n_complex.values, rust.n_complex.values, rtol=1e-8, atol=1e-8) + assert _solver_info(reference)["runs"][0]["backend_kind"] == "tensorial_scipy_reference" def test_materials_api_accepts_full_tensor_grid(): From 3986c97684541192bbd9bb253a201a770b7659fc Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 02:32:00 +0200 Subject: [PATCH 04/16] Clarify SciPy operator diagnostics --- python/micromode/scipy_reference.py | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index a8d73e8..29f3d59 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -86,9 +86,10 @@ def solve_diagonal_scipy_reference( scale=float(derivative_scale), ) operators = _assemble_diagonal_operators(sparse, eps_tensor, mu_tensor, derivatives) + operator = operators["mat"] eig_guess = complex(-(neff_guess * neff_guess), 0.0) values, vectors = _selected_eigenpairs( - operators["mat"], + operator, num_modes=num_modes, sigma=eig_guess, krylov_dim=krylov_dim, @@ -98,7 +99,7 @@ def solve_diagonal_scipy_reference( ) residuals = np.asarray( [ - np.linalg.norm(operators["mat"] @ vectors[:, index] - values[index] * vectors[:, index]) + np.linalg.norm(operator @ vectors[:, index] - values[index] * vectors[:, index]) for index in range(len(values)) ], dtype=float, @@ -158,8 +159,8 @@ def solve_diagonal_scipy_reference( fields, { "backend": "scipy_arpack_reference", - "operator_size": int(operators["mat"].shape[0]), - "operator_nnz": int(operators["mat"].nnz), + "operator_size": int(operator.shape[0]), + "operator_nnz": int(operator.nnz), "residuals": np.asarray(sorted_residuals, dtype=float), "power_norms": orthogonalization["power_norms"], "lorentz_norms": orthogonalization["lorentz_norms"], @@ -463,7 +464,7 @@ def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_ma ) qmat = q_ep + q_partial mat = p_mu @ qmat + p_partial @ q_ep - return {"q_ep": q_ep, "qmat": qmat, "mat": mat} + return {"q_ep": _canonical_sparse(q_ep), "qmat": _canonical_sparse(qmat), "mat": _canonical_sparse(mat)} def _assemble_tensorial_operator(sparse, eps: np.ndarray, mu: np.ndarray, der_mats): @@ -512,17 +513,26 @@ def diag(values): bybx = -(dyb @ diag(mu_20_over_22)) + diag(eps_02_over_22) @ dyb byby = -(dyb @ diag(mu_21_over_22)) - diag(eps_02_over_22) @ dxb - return -1j * sparse.bmat( - [ - [axax, axay, axbx, axby], - [ayax, ayay, aybx, ayby], - [bxax, bxay, bxbx, bxby], - [byax, byay, bybx, byby], - ], - format="csc", + return _canonical_sparse( + -1j + * sparse.bmat( + [ + [axax, axay, axbx, axby], + [ayax, ayay, aybx, ayby], + [bxax, bxay, bxbx, bxby], + [byax, byay, bybx, byby], + ], + format="csc", + ) ) +def _canonical_sparse(matrix): + matrix = matrix.tocsc(copy=True) + matrix.eliminate_zeros() + return matrix + + def _selected_eigenpairs( mat, *, From bfc876cb91cda6b5aee940c7b2158c7f8e016341 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 03:01:00 +0200 Subject: [PATCH 05/16] Validate SciPy backend against fixtures --- benchmarks/compare_mode_solver_fixtures.py | 32 ++++++++++++++------- benchmarks/mode_solver/README.md | 11 +++++++- tests/test_mode_solver_fixtures.py | 33 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/benchmarks/compare_mode_solver_fixtures.py b/benchmarks/compare_mode_solver_fixtures.py index 70611fa..de5e26d 100644 --- a/benchmarks/compare_mode_solver_fixtures.py +++ b/benchmarks/compare_mode_solver_fixtures.py @@ -52,6 +52,12 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Run local MicroMode solves for supported fixture cases and compare n_eff/fields.", ) + parser.add_argument( + "--backend", + choices=("rust_sparse", "scipy_reference"), + default="rust_sparse", + help="Local MicroMode backend to use with --run-local.", + ) parser.add_argument( "--fail-on-tolerance", action="store_true", @@ -85,7 +91,7 @@ def main() -> None: report = { "fixture_root": str(fixture_root), - "backend": "rust_sparse" if args.run_local else None, + "backend": args.backend if args.run_local else None, "cases": [], "summary": {"pass": 0, "fail": 0, "unsupported": 0, "not_run": 0}, } @@ -104,8 +110,8 @@ def main() -> None: ) status = {"status": "not_run", "summary": "local solve not requested"} if args.run_local: - status = _compare_local_case(fixture_root, entry) - print(f" local rust_sparse: {status['status']}: {status['summary']}") + status = _compare_local_case(fixture_root, entry, backend=args.backend) + print(f" local {args.backend}: {status['status']}: {status['summary']}") if status["failed"]: failures += 1 if status.get("support") == "production" and status["status"] != "pass": @@ -125,7 +131,7 @@ def main() -> None: raise SystemExit(f"{production_gaps} production fixture comparison(s) did not pass") -def _compare_local_case(root: Path, entry: dict) -> dict: +def _compare_local_case(root: Path, entry: dict, *, backend: str = "rust_sparse") -> dict: case_id = entry["case_id"] try: import micromode as sm @@ -146,8 +152,8 @@ def _compare_local_case(root: Path, entry: dict) -> dict: support = recipe.get("support", "production") if recipe.get("unsupported"): return _status("unsupported", recipe["unsupported"], support=support) - if tuple(recipe.get("num_pml", (0, 0))) != (0, 0): - return _status("unsupported", "local Rust comparison does not support PML", support=support) + if backend not in {"rust_sparse", "scipy_reference"}: + return _status("fail", f"unknown backend: {backend}", support=support) tangent_dims = tuple(dim for dim in ("x", "y", "z") if dim in ref_ex.dims and ref_ex.sizes.get(dim, 0) > 1) if len(tangent_dims) != 2: @@ -168,12 +174,13 @@ def _compare_local_case(root: Path, entry: dict) -> dict: tangent_dims=tangent_dims, normal_dim=normal_dim, normal_coord=normal_coord, + backend=backend, ) except NotImplementedError as exc: return _status("unsupported", str(exc), support=support) actual_n = _reorder_modes(result.n_complex.values, recipe) n_error = float(np.max(np.abs(actual_n - ref_n.values))) - tolerance = _n_tolerance(entry, recipe) + tolerance = _n_tolerance(entry, recipe, backend=backend) failed = n_error > tolerance field_errors = [] @@ -277,7 +284,7 @@ def _compare_local_case(root: Path, entry: dict) -> dict: "direction": "-", "dmin_pmc": (False, True), "trim_edges": ((1, 1), (0, 0)), - "backend_tolerances": {"rust_sparse": 1e-5}, + "backend_tolerances": {"rust_sparse": 1e-5, "scipy_reference": 1e-5}, "sort_order": "ascending", "krylov_dim": 64, }, @@ -357,6 +364,7 @@ def _solve_recipe( tangent_dims: tuple[str, str], normal_dim: str, normal_coord: float, + backend: str, ): freqs = tuple(float(freq) for freq in ref_n.coords["f"].values) if recipe.get("solve_each_frequency"): @@ -373,6 +381,7 @@ def _solve_recipe( tangent_dims=tangent_dims, normal_dim=normal_dim, normal_coord=normal_coord, + backend=backend, ) if first_result is None: first_result = result @@ -414,6 +423,7 @@ def _solve_recipe( tangent_dims=tangent_dims, normal_dim=normal_dim, normal_coord=normal_coord, + backend=backend, ) @@ -427,6 +437,7 @@ def _solve_recipe_for_freq( tangent_dims: tuple[str, str], normal_dim: str, normal_coord: float, + backend: str, ): freqs = (float(freq),) if np.isscalar(freq) else tuple(float(value) for value in freq) centers = tuple((axis_edges[:-1] + axis_edges[1:]) / 2 for axis_edges in edges) @@ -453,6 +464,7 @@ def _solve_recipe_for_freq( bend_radius=recipe.get("bend_radius"), bend_axis=recipe.get("bend_axis", 0), krylov_dim=recipe.get("krylov_dim"), + backend="scipy-reference" if backend == "scipy_reference" else "rust", ) @@ -523,9 +535,9 @@ def _status(status: str, summary: str, **details) -> dict: return {"status": status, "failed": status == "fail", "summary": summary, **details} -def _n_tolerance(entry: dict, recipe: dict | None = None) -> float: +def _n_tolerance(entry: dict, recipe: dict | None = None, *, backend: str = "rust_sparse") -> float: if recipe is not None: - backend_tolerance = recipe.get("backend_tolerances", {}).get("rust_sparse") + backend_tolerance = recipe.get("backend_tolerances", {}).get(backend) if backend_tolerance is not None: return float(backend_tolerance) return float( diff --git a/benchmarks/mode_solver/README.md b/benchmarks/mode_solver/README.md index c5d1233..ee4d764 100644 --- a/benchmarks/mode_solver/README.md +++ b/benchmarks/mode_solver/README.md @@ -21,7 +21,16 @@ the mode plane: uv run python benchmarks/compare_mode_solver_fixtures.py --suite extended --run-local --report-json tmp/reference_fixture_validation_rust_sparse.json ``` -Local fixture validation uses the Rust sparse backend. +Local fixture validation uses the Rust sparse backend by default. To run the same reconstructable +fixture recipes through the SciPy reference backend: + +```bash +uv run --extra scipy python benchmarks/compare_mode_solver_fixtures.py \ + --suite extended \ + --run-local \ + --backend scipy_reference \ + --report-json tmp/reference_fixture_validation_scipy_reference.json +``` Local validation reports each case as `pass`, `fail`, or `unsupported`. `fail` means the local solver ran but exceeded the fixture tolerance; `unsupported` means the case is explicitly classified diff --git a/tests/test_mode_solver_fixtures.py b/tests/test_mode_solver_fixtures.py index 5baf67e..2512a02 100644 --- a/tests/test_mode_solver_fixtures.py +++ b/tests/test_mode_solver_fixtures.py @@ -93,6 +93,18 @@ def test_local_fixture_comparison_uses_staggered_rasterization_for_z_strips(): assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] +@pytest.mark.slow +def test_scipy_fixture_comparison_uses_staggered_rasterization_for_z_strips(): + pytest.importorskip("scipy") + manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) + entries = {entry["case_id"]: entry for entry in manifest["cases"]} + + for case_id in ("strip_z_scalar_single", "group_index_silicon_strip"): + status = _compare_local_case(EXTENDED_FIXTURE_ROOT, entries[case_id], backend="scipy_reference") + assert status["status"] == "pass" + assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] + + @pytest.mark.slow def test_local_production_fixture_matrix_passes(): from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES @@ -113,6 +125,27 @@ def test_local_production_fixture_matrix_passes(): assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] +@pytest.mark.slow +def test_scipy_production_fixture_matrix_passes(): + pytest.importorskip("scipy") + from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES + + manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) + entries = {entry["case_id"]: entry for entry in manifest["cases"]} + production_ids = [ + case_id + for case_id, recipe in _LOCAL_CASES.items() + if recipe.get("support", "production") == "production" and case_id in entries + ] + + assert production_ids + for case_id in production_ids: + status = _compare_local_case(EXTENDED_FIXTURE_ROOT, entries[case_id], backend="scipy_reference") + assert status["support"] == "production" + assert status["status"] == "pass", f"{case_id}: {status['summary']}" + assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] + + @pytest.mark.slow def test_unsupported_fixture_matrix_is_explicit(): from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES From 2a0d76f157881fef63a3781d9baeee254f2e2724 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 03:30:00 +0200 Subject: [PATCH 06/16] Add Tidy3D backend comparison benchmark --- benchmarks/compare_tidy3d_backends.py | 213 ++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 benchmarks/compare_tidy3d_backends.py diff --git a/benchmarks/compare_tidy3d_backends.py b/benchmarks/compare_tidy3d_backends.py new file mode 100644 index 0000000..fe42cba --- /dev/null +++ b/benchmarks/compare_tidy3d_backends.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import argparse +import json +import time +from dataclasses import dataclass +from pathlib import Path + +import numpy as np + +import micromode as mm + + +@dataclass(frozen=True) +class BenchmarkCase: + case_id: str + description: str + ny: int + nz: int + num_modes: int = 2 + target_neff: float = 2.5 + krylov_dim: int = 64 + num_pml: tuple[int, int] = (0, 0) + problem: str = "strip" + + +PRESETS = { + "quick": ( + BenchmarkCase("strip_60x40", "SOI strip waveguide, coarse", 60, 40, krylov_dim=48), + BenchmarkCase("strip_120x80", "SOI strip waveguide", 120, 80, krylov_dim=48), + BenchmarkCase("strip_pml_120x80", "SOI strip waveguide with mode PML", 120, 80, krylov_dim=48, num_pml=(8, 8)), + BenchmarkCase("slot_120x80", "Silicon slot waveguide", 120, 80, krylov_dim=48, problem="slot"), + ), + "large": ( + BenchmarkCase("strip_60x40", "SOI strip waveguide, coarse", 60, 40, krylov_dim=48), + BenchmarkCase("strip_120x80", "SOI strip waveguide", 120, 80, krylov_dim=48), + BenchmarkCase("strip_240x160", "SOI strip waveguide, large", 240, 160, krylov_dim=64), + BenchmarkCase("strip_480x320", "SOI strip waveguide, very large", 480, 320, krylov_dim=64), + BenchmarkCase("slot_120x80", "Silicon slot waveguide", 120, 80, krylov_dim=48, problem="slot"), + ), +} + +WIDTH_Y = 3.0 +WIDTH_Z = 2.0 +WAVELENGTH_UM = 1.55 +N_SI = 3.48 +N_SIO2 = 1.45 +N_AIR = 1.0 + + +def main() -> None: + args = parse_args() + cases = list(PRESETS[args.preset]) + rows = [run_case(case) for case in cases] + print(markdown_table(rows)) + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") + print(f"Wrote {args.output}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Compare MicroMode Rust, MicroMode SciPy, and Tidy3D local solves.") + parser.add_argument("--preset", choices=tuple(PRESETS), default="quick") + parser.add_argument("--output", type=Path, default=Path("tmp/tidy3d_backend_benchmark.json")) + return parser.parse_args() + + +def run_case(case: BenchmarkCase) -> dict[str, object]: + materials = micromode_materials(case) + row: dict[str, object] = { + "case_id": case.case_id, + "description": case.description, + "grid": f"{case.ny}x{case.nz}", + "cells": case.ny * case.nz, + } + rust_seconds, rust_neff = time_micromode(case, materials, backend="rust") + scipy_seconds, scipy_neff = time_micromode(case, materials, backend="scipy-reference") + tidy3d_seconds, tidy3d_neff = time_tidy3d(case) + + row.update( + { + "rust_seconds": rust_seconds, + "scipy_seconds": scipy_seconds, + "tidy3d_seconds": tidy3d_seconds, + "rust_n_eff": rust_neff.tolist(), + "scipy_n_eff": scipy_neff.tolist(), + "tidy3d_n_eff": tidy3d_neff.tolist(), + "rust_scipy_max_abs_neff": max_abs_delta(rust_neff, scipy_neff), + "rust_tidy3d_max_abs_neff": max_abs_delta(rust_neff, tidy3d_neff), + } + ) + print( + f"{case.case_id}: rust={rust_seconds:.3f}s scipy={scipy_seconds:.3f}s " + f"tidy3d={tidy3d_seconds:.3f}s delta_tidy3d={row['rust_tidy3d_max_abs_neff']:.3e}", + flush=True, + ) + return row + + +def time_micromode(case: BenchmarkCase, materials: mm.Materials, *, backend: str) -> tuple[float, np.ndarray]: + start = time.perf_counter() + data = mm.solve_modes( + material_grid=materials, + wavelength=WAVELENGTH_UM, + num_modes=case.num_modes, + target_neff=case.target_neff, + krylov_dim=case.krylov_dim, + pml=mm.PmlSpec(num_cells=case.num_pml), + backend=backend, + ) + return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) + + +def time_tidy3d(case: BenchmarkCase) -> tuple[float, np.ndarray]: + try: + import tidy3d as td + from tidy3d.plugins.mode import ModeSolver + except ImportError as exc: # pragma: no cover - benchmark dependency only. + raise SystemExit("Install Tidy3D for this benchmark: uv run --with tidy3d --extra scipy ...") from exc + + structures = tidy3d_structures(td, case.problem) + dl = min(WIDTH_Y / case.ny, WIDTH_Z / case.nz) + freq = td.C_0 / WAVELENGTH_UM + sim = td.Simulation( + size=(0.1, WIDTH_Y, WIDTH_Z), + grid_spec=td.GridSpec.uniform(dl=dl), + run_time=1e-12, + structures=structures, + boundary_spec=td.BoundarySpec.all_sides(boundary=td.PECBoundary()), + ) + plane = td.Box(center=(0.0, 0.0, 0.0), size=(0.0, WIDTH_Y, WIDTH_Z)) + solver = ModeSolver( + simulation=sim, + plane=plane, + mode_spec=td.ModeSpec(num_modes=case.num_modes, target_neff=case.target_neff, num_pml=case.num_pml), + freqs=[freq], + ) + start = time.perf_counter() + data = solver.solve() + return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) + + +def micromode_materials(case: BenchmarkCase) -> mm.Materials: + y_edges = np.linspace(-0.5 * WIDTH_Y, 0.5 * WIDTH_Y, case.ny + 1) + z_edges = np.linspace(-0.5 * WIDTH_Z, 0.5 * WIDTH_Z, case.nz + 1) + y = 0.5 * (y_edges[:-1] + y_edges[1:]) + z = 0.5 * (z_edges[:-1] + z_edges[1:]) + yy, zz = np.meshgrid(y, z, indexing="ij") + eps = np.where(zz < 0.0, N_SIO2**2, N_AIR**2).astype(np.complex128) + if case.problem == "strip": + eps[(np.abs(yy) <= 0.225) & (zz >= 0.0) & (zz <= 0.22)] = N_SI**2 + elif case.problem == "slot": + left = (yy >= -0.29) & (yy <= -0.09) & (zz >= 0.0) & (zz <= 0.22) + right = (yy >= 0.09) & (yy <= 0.29) & (zz >= 0.0) & (zz <= 0.22) + eps[left | right] = N_SI**2 + else: + raise ValueError(f"unknown problem: {case.problem}") + return mm.Materials.from_diagonal(eps_xx=eps, x_edges=y_edges, y_edges=z_edges, normal_axis=0) + + +def tidy3d_structures(td, problem: str): + structures = [ + td.Structure( + geometry=td.Box(center=(0.0, 0.0, -0.5001), size=(td.inf, td.inf, 1.0002)), + medium=td.Medium(permittivity=N_SIO2**2), + ) + ] + if problem == "strip": + structures.append( + td.Structure( + geometry=td.Box(center=(0.0, 0.0, 0.11), size=(td.inf, 0.45, 0.22)), + medium=td.Medium(permittivity=N_SI**2), + ) + ) + elif problem == "slot": + for y_center in (-0.19, 0.19): + structures.append( + td.Structure( + geometry=td.Box(center=(0.0, y_center, 0.11), size=(td.inf, 0.20, 0.22)), + medium=td.Medium(permittivity=N_SI**2), + ) + ) + else: + raise ValueError(f"unknown problem: {problem}") + return structures + + +def max_abs_delta(left: np.ndarray, right: np.ndarray) -> float: + count = min(left.size, right.size) + if count == 0: + return float("nan") + return float(np.max(np.abs(left[:count] - right[:count]))) + + +def markdown_table(rows: list[dict[str, object]]) -> str: + header = ( + "| Problem | Grid | Rust (s) | SciPy backend (s) | Tidy3D local (s) | " + "max abs Δn_eff Rust/SciPy | max abs Δn_eff Rust/Tidy3D |\n" + "|---|---:|---:|---:|---:|---:|---:|" + ) + lines = [header] + for row in rows: + lines.append( + f"| {row['description']} | {row['grid']} | {row['rust_seconds']:.3f} | " + f"{row['scipy_seconds']:.3f} | {row['tidy3d_seconds']:.3f} | " + f"{row['rust_scipy_max_abs_neff']:.3e} | {row['rust_tidy3d_max_abs_neff']:.3e} |" + ) + return "\n".join(lines) + + +if __name__ == "__main__": + main() From 3b91465bb5589b0949876dca31243e002ade6e13 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 03:59:00 +0200 Subject: [PATCH 07/16] Speed up real SciPy backend solves --- benchmarks/compare_tidy3d_backends.py | 9 ++++-- python/micromode/scipy_reference.py | 40 +++++++++++++++++++-------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/benchmarks/compare_tidy3d_backends.py b/benchmarks/compare_tidy3d_backends.py index fe42cba..54e50d2 100644 --- a/benchmarks/compare_tidy3d_backends.py +++ b/benchmarks/compare_tidy3d_backends.py @@ -88,6 +88,7 @@ def run_case(case: BenchmarkCase) -> dict[str, object]: "tidy3d_n_eff": tidy3d_neff.tolist(), "rust_scipy_max_abs_neff": max_abs_delta(rust_neff, scipy_neff), "rust_tidy3d_max_abs_neff": max_abs_delta(rust_neff, tidy3d_neff), + "scipy_tidy3d_max_abs_neff": max_abs_delta(scipy_neff, tidy3d_neff), } ) print( @@ -196,15 +197,17 @@ def max_abs_delta(left: np.ndarray, right: np.ndarray) -> float: def markdown_table(rows: list[dict[str, object]]) -> str: header = ( "| Problem | Grid | Rust (s) | SciPy backend (s) | Tidy3D local (s) | " - "max abs Δn_eff Rust/SciPy | max abs Δn_eff Rust/Tidy3D |\n" - "|---|---:|---:|---:|---:|---:|---:|" + "max abs Δn_eff Rust/SciPy | max abs Δn_eff Rust/Tidy3D | " + "max abs Δn_eff SciPy/Tidy3D |\n" + "|---|---:|---:|---:|---:|---:|---:|---:|" ) lines = [header] for row in rows: lines.append( f"| {row['description']} | {row['grid']} | {row['rust_seconds']:.3f} | " f"{row['scipy_seconds']:.3f} | {row['tidy3d_seconds']:.3f} | " - f"{row['rust_scipy_max_abs_neff']:.3e} | {row['rust_tidy3d_max_abs_neff']:.3e} |" + f"{row['rust_scipy_max_abs_neff']:.3e} | {row['rust_tidy3d_max_abs_neff']:.3e} | " + f"{row['scipy_tidy3d_max_abs_neff']:.3e} |" ) return "\n".join(lines) diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 29f3d59..88e577b 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -8,6 +8,7 @@ from __future__ import annotations +import warnings from dataclasses import dataclass import numpy as np @@ -88,12 +89,15 @@ def solve_diagonal_scipy_reference( operators = _assemble_diagonal_operators(sparse, eps_tensor, mu_tensor, derivatives) operator = operators["mat"] eig_guess = complex(-(neff_guess * neff_guess), 0.0) + operator, arpack_initial_vector, arpack_guess = _real_arpack_problem_if_close( + operator, initial_vector, eig_guess + ) values, vectors = _selected_eigenpairs( operator, num_modes=num_modes, - sigma=eig_guess, + sigma=arpack_guess, krylov_dim=krylov_dim, - initial_vector=initial_vector, + initial_vector=arpack_initial_vector, spla=spla, scipy_linalg=scipy_linalg, ) @@ -533,6 +537,18 @@ def _canonical_sparse(matrix): return matrix +def _real_arpack_problem_if_close(matrix, initial_vector: np.ndarray | None, guess: complex): + if matrix.nnz == 0: + return matrix, initial_vector, guess + matrix_imag = matrix.data.imag + matrix_scale = max(float(np.max(np.abs(matrix.data))), 1.0) + guess_is_real = abs(guess.imag) <= 1e-14 * max(abs(guess), 1.0) + if np.max(np.abs(matrix_imag)) <= 1e-14 * matrix_scale and guess_is_real: + real_vector = None if initial_vector is None else np.asarray(initial_vector.real, dtype=float) + return matrix.real.astype(float), real_vector, float(guess.real) + return matrix, initial_vector, guess + + def _selected_eigenpairs( mat, *, @@ -552,15 +568,17 @@ def _selected_eigenpairs( ncv = min(size, max(int(krylov_dim), num_modes + 2)) if ncv <= num_modes + 1: ncv = min(size, num_modes + 2) - values, vectors = spla.eigs( - mat, - k=num_modes, - sigma=sigma, - which="LM", - v0=None if initial_vector is None else np.asarray(initial_vector, dtype=np.complex128), - ncv=ncv, - tol=1e-10, - ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=np.exceptions.ComplexWarning, module="scipy") + values, vectors = spla.eigs( + mat, + k=num_modes, + sigma=sigma, + which="LM", + v0=None if initial_vector is None else np.asarray(initial_vector), + ncv=ncv, + tol=1e-10, + ) return np.asarray(values, dtype=np.complex128), np.asarray(vectors, dtype=np.complex128) From 084be7a2628f79914c22fea9906930cecabee614 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 04:28:00 +0200 Subject: [PATCH 08/16] Compare Tidy3D using matched solver profiles --- benchmarks/compare_tidy3d_backends.py | 65 ++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/benchmarks/compare_tidy3d_backends.py b/benchmarks/compare_tidy3d_backends.py index 54e50d2..2b14560 100644 --- a/benchmarks/compare_tidy3d_backends.py +++ b/benchmarks/compare_tidy3d_backends.py @@ -29,14 +29,18 @@ class BenchmarkCase: BenchmarkCase("strip_60x40", "SOI strip waveguide, coarse", 60, 40, krylov_dim=48), BenchmarkCase("strip_120x80", "SOI strip waveguide", 120, 80, krylov_dim=48), BenchmarkCase("strip_pml_120x80", "SOI strip waveguide with mode PML", 120, 80, krylov_dim=48, num_pml=(8, 8)), - BenchmarkCase("slot_120x80", "Silicon slot waveguide", 120, 80, krylov_dim=48, problem="slot"), + BenchmarkCase( + "slot_120x80", "Silicon slot waveguide, fundamental", 120, 80, num_modes=1, krylov_dim=48, problem="slot" + ), ), "large": ( BenchmarkCase("strip_60x40", "SOI strip waveguide, coarse", 60, 40, krylov_dim=48), BenchmarkCase("strip_120x80", "SOI strip waveguide", 120, 80, krylov_dim=48), BenchmarkCase("strip_240x160", "SOI strip waveguide, large", 240, 160, krylov_dim=64), BenchmarkCase("strip_480x320", "SOI strip waveguide, very large", 480, 320, krylov_dim=64), - BenchmarkCase("slot_120x80", "Silicon slot waveguide", 120, 80, krylov_dim=48, problem="slot"), + BenchmarkCase( + "slot_120x80", "Silicon slot waveguide, fundamental", 120, 80, num_modes=1, krylov_dim=48, problem="slot" + ), ), } @@ -51,7 +55,7 @@ class BenchmarkCase: def main() -> None: args = parse_args() cases = list(PRESETS[args.preset]) - rows = [run_case(case) for case in cases] + rows = [run_case(case, profile_source=args.profile_source) for case in cases] print(markdown_table(rows)) if args.output is not None: args.output.parent.mkdir(parents=True, exist_ok=True) @@ -62,21 +66,33 @@ def main() -> None: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Compare MicroMode Rust, MicroMode SciPy, and Tidy3D local solves.") parser.add_argument("--preset", choices=tuple(PRESETS), default="quick") + parser.add_argument( + "--profile-source", + choices=("tidy3d", "analytic"), + default="tidy3d", + help="Use Tidy3D's exact local solver grid/epsilon profile, or MicroMode's independent analytic raster.", + ) parser.add_argument("--output", type=Path, default=Path("tmp/tidy3d_backend_benchmark.json")) return parser.parse_args() -def run_case(case: BenchmarkCase) -> dict[str, object]: - materials = micromode_materials(case) +def run_case(case: BenchmarkCase, *, profile_source: str) -> dict[str, object]: + tidy3d_solver = make_tidy3d_solver(case) + materials = ( + micromode_materials_from_tidy3d_solver(tidy3d_solver, case) + if profile_source == "tidy3d" + else micromode_materials(case) + ) row: dict[str, object] = { "case_id": case.case_id, "description": case.description, "grid": f"{case.ny}x{case.nz}", "cells": case.ny * case.nz, + "profile_source": profile_source, } rust_seconds, rust_neff = time_micromode(case, materials, backend="rust") scipy_seconds, scipy_neff = time_micromode(case, materials, backend="scipy-reference") - tidy3d_seconds, tidy3d_neff = time_tidy3d(case) + tidy3d_seconds, tidy3d_neff = time_tidy3d(tidy3d_solver) row.update( { @@ -113,7 +129,13 @@ def time_micromode(case: BenchmarkCase, materials: mm.Materials, *, backend: str return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) -def time_tidy3d(case: BenchmarkCase) -> tuple[float, np.ndarray]: +def time_tidy3d(solver) -> tuple[float, np.ndarray]: + start = time.perf_counter() + data = solver.solve() + return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) + + +def make_tidy3d_solver(case: BenchmarkCase): try: import tidy3d as td from tidy3d.plugins.mode import ModeSolver @@ -137,9 +159,32 @@ def time_tidy3d(case: BenchmarkCase) -> tuple[float, np.ndarray]: mode_spec=td.ModeSpec(num_modes=case.num_modes, target_neff=case.target_neff, num_pml=case.num_pml), freqs=[freq], ) - start = time.perf_counter() - data = solver.solve() - return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) + return solver + + +def micromode_materials_from_tidy3d_solver(solver, case: BenchmarkCase) -> mm.Materials: + eps = np.asarray(solver._solver_eps(tidy3d_frequency()), dtype=np.complex128) + grid = solver._solver_grid + return mm.Materials.from_components( + x_edges=np.asarray(grid.boundaries.y, dtype=float), + y_edges=np.asarray(grid.boundaries.z, dtype=float), + normal_axis=2, + eps_xx=eps[0], + eps_xy=eps[1], + eps_xz=eps[2], + eps_yx=eps[3], + eps_yy=eps[4], + eps_yz=eps[5], + eps_zx=eps[6], + eps_zy=eps[7], + eps_zz=eps[8], + ) + + +def tidy3d_frequency() -> float: + import tidy3d as td + + return float(td.C_0 / WAVELENGTH_UM) def micromode_materials(case: BenchmarkCase) -> mm.Materials: From 7b52160d991973da0b9ced588aa5446d8df6ccb8 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 04:57:00 +0200 Subject: [PATCH 09/16] Make SciPy the default backend --- README.md | 65 +++++++++++---------------- benchmarks/mode_solver/README.md | 5 ++- docs/backend-trust.md | 36 +++++++-------- docs/mode-solver-methods.md | 16 ++++--- pyproject.toml | 2 +- python/micromode/models.py | 2 +- python/micromode/raster.py | 57 +++++++++++++++--------- python/micromode/scipy_reference.py | 14 +++--- tests/test_micromode_api.py | 68 ++++++++++++++++++++++++++--- 9 files changed, 166 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 6cbc999..99627a6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # micromode -An **electromagnetic mode solver** using the **[FDFD method](https://en.wikipedia.org/wiki/Finite-difference_frequency-domain_method)** on a **[rectilinear Yee-grid](https://en.wikipedia.org/wiki/Finite-difference_time-domain_method)**, written in native **[Rust](https://rust-lang.org/)**. +An **electromagnetic mode solver** using the **[FDFD method](https://en.wikipedia.org/wiki/Finite-difference_frequency-domain_method)** on a **[rectilinear Yee-grid](https://en.wikipedia.org/wiki/Finite-difference_time-domain_method)**, with a readable **SciPy/ARPACK** backend and an optional native **[Rust](https://rust-lang.org/)** backend. ```bash -pip install micromode +pip install "micromode[scipy]" ``` [![License](https://img.shields.io/github/license/QuentinWach/micromode)](LICENSE) @@ -16,8 +16,10 @@ pip install micromode ## Why Use It? - **Grid-first API**: pass arrays directly, with no required geometry model. -- **Fast**, portable Rust sparse backend: one production solve path. -- **Auditable** optional SciPy reference backend for cross-checking solves. +- **Auditable SciPy default**: sparse operators are assembled in Python and + solved with SciPy/ARPACK when SciPy is installed. +- **Optional Rust backend**: a portable native fallback for environments that do + not want a SciPy dependency. - **Practical** outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions, Lorentz overlaps, plotting, dataframe export, and HDF5 save/load. - **Tensor-aware**: supports scalar, diagonal anisotropic, and full tensor material @@ -98,47 +100,32 @@ the public solver controls are summarized in [docs/mode-solver-methods.md](docs/ ## Solver -MicroMode is designed to make high-performance mode solving available without -requiring users to install external solver stacks. The production backend is a -**portable Rust [sparse](https://en.wikipedia.org/wiki/Sparse_matrix) -[shift-invert](https://en.wikipedia.org/wiki/Preconditioner#Spectral_transformation) -[eigensolver](https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors)**, so -source installs and wheels do **not** depend on -[ARPACK](https://en.wikipedia.org/wiki/ARPACK), -[UMFPACK](https://en.wikipedia.org/wiki/UMFPACK), -[SuiteSparse](https://en.wikipedia.org/wiki/SuiteSparse), -[BLAS](https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms)/ -[LAPACK](https://en.wikipedia.org/wiki/LAPACK), or a Fortran compiler. -That matters for simulation workflows that need to run in CI, notebooks, -container images, FDTD plugins, and cross-platform design tools. - -The native solver is **not a dense fallback**. It uses -[sparse](https://en.wikipedia.org/wiki/Sparse_matrix) -[finite-difference](https://en.wikipedia.org/wiki/Finite_difference_method) -operators throughout, applies -[AMD fill-reducing ordering](https://en.wikipedia.org/wiki/Minimum_degree_algorithm) -before sparse [LU factorization](https://en.wikipedia.org/wiki/LU_decomposition), -stores LU factors in a packed format for repeated triangular solves, and runs an -[Arnoldi iteration](https://en.wikipedia.org/wiki/Arnoldi_iteration) targeted -around the requested effective index. The Arnoldi stage uses -**shift-invert**, adaptive -[Ritz-pair](https://en.wikipedia.org/wiki/Ritz_method) checkpointing, early -stopping once requested modes are stable, and selective Ritz vector -reconstruction so work is spent on the modes that will actually be returned. - -For users who want an executable Python reference, MicroMode also provides an -optional SciPy/ARPACK backend: +MicroMode defaults to a Python/SciPy backend when SciPy is installed. This path +assembles the finite-difference sparse operators in Python and solves the +shift-invert eigenproblems with +[SciPy/ARPACK](https://docs.scipy.org/doc/scipy/reference/sparse.linalg.html). +That makes the numerical method easier for academic users to inspect and +debug, while still using a trusted sparse eigensolver stack. + +The recommended install includes SciPy: ```bash pip install "micromode[scipy]" ``` +The public APIs use `backend="auto"` by default. In auto mode MicroMode selects +SciPy when available and falls back to Rust otherwise. You can also choose a +backend explicitly: + ```python -data = mm.solve_modes(..., backend="scipy-reference") +data = mm.solve_modes(..., backend="scipy") # same as backend="scipy-reference" +data = mm.solve_modes(..., backend="rust") ``` -This backend is intentionally slower than Rust. Its purpose is to make the core -eigenproblems easy to inspect in Python and to validate that the production Rust -backend returns the same effective indices and normalization diagnostics on -supported cases. See +The Rust backend remains useful when a deployment needs a self-contained native +solver with no SciPy, ARPACK, BLAS/LAPACK, or Fortran toolchain requirement. It +uses sparse finite-difference operators, AMD fill-reducing ordering, sparse LU +factorization, and a shift-invert Arnoldi iteration. The SciPy and Rust paths +are compared in tests so their effective indices and normalization diagnostics +stay aligned on supported cases. See [docs/backend-trust.md](docs/backend-trust.md). diff --git a/benchmarks/mode_solver/README.md b/benchmarks/mode_solver/README.md index ee4d764..aba7d73 100644 --- a/benchmarks/mode_solver/README.md +++ b/benchmarks/mode_solver/README.md @@ -21,8 +21,9 @@ the mode plane: uv run python benchmarks/compare_mode_solver_fixtures.py --suite extended --run-local --report-json tmp/reference_fixture_validation_rust_sparse.json ``` -Local fixture validation uses the Rust sparse backend by default. To run the same reconstructable -fixture recipes through the SciPy reference backend: +This benchmark harness defaults to the Rust sparse backend so historical +fixture runs stay comparable. To run the same reconstructable fixture recipes +through the package's preferred SciPy backend: ```bash uv run --extra scipy python benchmarks/compare_mode_solver_fixtures.py \ diff --git a/docs/backend-trust.md b/docs/backend-trust.md index 4b0820c..271fa79 100644 --- a/docs/backend-trust.md +++ b/docs/backend-trust.md @@ -2,14 +2,15 @@ MicroMode has two backend roles: -- The Rust backend is the production solver. It is fast, portable, and does not - require users to install ARPACK, SuiteSparse, BLAS/LAPACK bindings, or a - Fortran toolchain. -- The SciPy reference backend is an optional audit path. It mirrors the sparse - operators in Python and calls SciPy/ARPACK so the core numerical method can be - inspected by users who are more comfortable reading Python. +- The SciPy backend is the preferred default when SciPy is installed. It + assembles the sparse operators in Python and calls SciPy/ARPACK so the core + numerical method can be inspected by users who are more comfortable reading + Python. +- The Rust backend is an optional portable fallback. It does not require users + to install ARPACK, SuiteSparse, BLAS/LAPACK bindings, or a Fortran toolchain. -The SciPy backend is selected explicitly: +The public APIs default to `backend="auto"`, which selects SciPy when available +and falls back to Rust otherwise. A backend can also be selected explicitly: ```python import micromode as mm @@ -21,11 +22,12 @@ data = mm.solve_grid( freqs=[freq], num_modes=2, target_neff=2.5, - backend="scipy-reference", + backend="scipy", ) ``` -Install the optional dependency with: +`backend="scipy-reference"` is kept as a compatibility alias for the same SciPy +path. Install the recommended SciPy dependency with: ```bash pip install "micromode[scipy]" @@ -33,7 +35,7 @@ pip install "micromode[scipy]" ## Supported Scope -The reference backend covers the same core solve families as Rust: +The SciPy backend covers the same core solve families as Rust: - diagonal scalar or diagonal-anisotropic material grids, - full tensor material grids, @@ -43,8 +45,8 @@ The reference backend covers the same core solve families as Rust: grids before backend dispatch. The remaining difference is operational rather than mathematical: the SciPy -backend depends on SciPy/ARPACK and is expected to be slower and less portable -than the Rust backend. +backend depends on SciPy/ARPACK, while the Rust backend is self-contained and +can be selected with `backend="rust"` for deployments that need that property. ## What Is Compared @@ -64,8 +66,8 @@ Run the focused cross-backend check with: uv run --extra scipy pytest tests/test_micromode_api.py -k scipy_reference ``` -Without the SciPy extra installed, the comparison test is skipped and the Rust -production tests still run. +Without the SciPy extra installed, the comparison test is skipped and the +default `backend="auto"` path falls back to Rust. ## Reading The Code @@ -79,9 +81,9 @@ The relevant files are: - `src/mode_solver.rs`: Rust field reconstruction, normalization, and Lorentz orthogonalization. -The intended trust chain is: inspect the Python reference, inspect the Rust -operator comments, then run the cross-backend test to verify that Rust and SciPy -agree on the supported cases. +The intended trust chain is: inspect the Python/SciPy implementation first, then +run the cross-backend tests to verify that Rust and SciPy agree on the supported +cases where the optional Rust backend matters. ## External References diff --git a/docs/mode-solver-methods.md b/docs/mode-solver-methods.md index 3ae6dc3..23a0255 100644 --- a/docs/mode-solver-methods.md +++ b/docs/mode-solver-methods.md @@ -30,11 +30,14 @@ coordinate-aware `Result`. - `krylov_dim`: dimension of the Arnoldi search space. - `angle_theta`, `angle_phi`, `bend_radius`, `bend_axis`: transformation-optics controls that update \(\epsilon\) and \(\mu\) before the sparse solve. +- `backend`: `"auto"` by default, choosing SciPy when installed and Rust + otherwise. Use `"scipy"` or `"scipy-reference"` for the Python/SciPy path and + `"rust"` for the optional native backend. ## Eigenpair Selection -The production Rust backend selects eigenpairs with sparse shift-invert Arnoldi -[1, 2]. +The default SciPy backend selects eigenpairs with sparse shift-invert +SciPy/ARPACK [1, 2]. For a matrix \(A\) and shift \(\sigma\), Arnoldi is applied to $$ @@ -47,10 +50,11 @@ where \(\theta\) is a Ritz value of the inverse-shifted operator. The diagonal backend uses \(\sigma=-\texttt{target_neff}^2\); the tensorial backend uses \(\sigma=\texttt{target_neff}\). -The optional `backend="scipy-reference"` path assembles the same diagonal or -tensorial sparse operator in Python and solves it with SciPy/ARPACK. It exists -as a readable validation backend, not as the default production solver. Install -it with `micromode[scipy]`. +The optional `backend="rust"` path assembles the same diagonal or tensorial +sparse operator through the native extension and solves it with MicroMode's +portable shift-invert Arnoldi implementation. It exists for deployments that do +not want a SciPy dependency and as a cross-check against the Python/SciPy path. +Install the recommended default SciPy dependency with `micromode[scipy]`. Returned modes are sorted by decreasing real effective index, normalized to unit transverse power, diff --git a/pyproject.toml b/pyproject.toml index 99fe0d9..a5f566e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "micromode" version = "0.1.0a4" -description = "Rust-backed photonics mode solver with a small Python API." +description = "SciPy-first photonics mode solver with an optional Rust backend." readme = "README.md" requires-python = ">=3.10,<3.14" license = "Apache-2.0" diff --git a/python/micromode/models.py b/python/micromode/models.py index 135f8d0..0e8838d 100644 --- a/python/micromode/models.py +++ b/python/micromode/models.py @@ -528,7 +528,7 @@ def _assign_tensor_offdiagonal( @dataclass(frozen=True) class Spec: - """Mode solver options for Rust-backed grid solves.""" + """Mode solver options for backend-backed grid solves.""" num_modes: int = 1 target_neff: float | None = None diff --git a/python/micromode/raster.py b/python/micromode/raster.py index 85bf02d..f30be27 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -2,6 +2,7 @@ from __future__ import annotations +import importlib.util from collections.abc import Sequence from typing import Literal, cast @@ -14,7 +15,8 @@ from .scipy_reference import solve_diagonal_scipy_reference, solve_tensorial_scipy_reference _COMPONENTS = ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz") -Backend = Literal["rust", "scipy-reference"] +Backend = Literal["auto", "scipy", "scipy-reference", "rust"] +_ResolvedBackend = Literal["scipy-reference", "rust"] def solve_grid( @@ -54,7 +56,7 @@ def solve_grid( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, - backend: Backend = "rust", + backend: Backend = "auto", spec: Spec | None = None, ) -> Result: """Solve modes from rasterized material components and grid edges. @@ -146,15 +148,15 @@ def solve_slice( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, - backend: Backend = "rust", + backend: Backend = "auto", spec: Spec | None = None, ) -> Result: """Solve modes from a one-dimensional mode-plane material slice. This is the convenience API for 2D FDTD simulations. The supplied material arrays vary along one mode-plane axis and MicroMode inserts a single - invariant cell along the other axis before using the same Rust sparse solve - path as ``solve_modes``. + invariant cell along the other axis before using the same backend solve path + as ``solve_modes``. """ material_grid = Materials.from_slice( @@ -219,18 +221,20 @@ def solve_modes( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, - backend: Backend = "rust", + backend: Backend = "auto", spec: Spec | None = None, ) -> Result: """Solve modes for an already-rasterized material tensor grid. This is the preferred Beamz integration point. Beamz owns geometry and material rasterization; MicroMode owns the sparse mode solve and field - reconstruction on the supplied grid. + reconstruction on the supplied grid. By default, MicroMode uses the + Python/SciPy backend when SciPy is installed and falls back to the Rust + backend otherwise. """ - # Main Python-to-Rust orchestration layer. It validates user-facing grid - # objects, solves one frequency at a time, then wraps flattened Rust outputs - # back into coordinate-aware xarray arrays. + # Main solver orchestration layer. It validates user-facing grid objects, + # solves one frequency at a time, then wraps flattened backend outputs into + # coordinate-aware xarray arrays. if not isinstance(material_grid, Materials): raise TypeError("material_grid must be a Materials") shape = material_grid.shape @@ -252,8 +256,7 @@ def solve_modes( raise ValueError("num_modes must be positive") if direction not in {"+", "-"}: raise ValueError("direction must be '+' or '-'") - if backend not in {"rust", "scipy-reference"}: - raise ValueError("backend must be 'rust' or 'scipy-reference'") + backend = _resolve_backend(backend) pml_spec = _resolve_pml_spec(pml) boundary_spec = _resolve_boundary_spec(boundary) if bend_radius is not None and np.isclose(bend_radius, 0.0): @@ -288,9 +291,9 @@ def solve_modes( backend=backend, material_grid=material_grid, ) - # Rust solves in local coordinates where local z is the propagation - # normal. Convert field labels back to the global x/y/z axes requested by - # the material grid before exposing them. + # Backends solve in local coordinates where local z is the propagation + # normal. Convert field labels back to the global x/y/z axes requested + # by the material grid before exposing them. fields = _local_fields_to_global(fields, normal_axis=material_grid.grid.normal_axis) n_rows.append(n_complex) solver_runs.append(solver_info) @@ -337,7 +340,7 @@ def _solve_one_frequency( angle_phi: float, bend_radius: float | None, bend_axis: int, - backend: Backend, + backend: _ResolvedBackend, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: # Forward and backward spacings represent the local Yee grid. The derivative # builders need both because E and H components are staggered. @@ -452,7 +455,7 @@ def _solve_one_frequency( krylov_dim=krylov_dim, boundary_spec=boundary_spec, ) - # Ordinary scalar/diagonal grids use the production diagonal sparse backend. + # Ordinary scalar/diagonal grids use the diagonal sparse formulation. if backend == "scipy-reference": return _solve_one_frequency_scipy_reference( eps_tensor=eps_tensor, @@ -681,7 +684,7 @@ def _transformed_material_tensors( # eps' = J eps J.T / det(J) # mu' = J mu J.T / det(J) # Angle and bend solves are handled by changing material tensors, then - # passing the resulting grid to the same Rust tensorial operator. + # passing the resulting grid to the same tensorial operator family. if eps.shape != mu.shape or eps.shape[:2] != (3, 3): raise ValueError("eps and mu tensors must both have shape (3, 3, nx, ny)") nx, ny = eps.shape[2:] @@ -753,6 +756,20 @@ def _resolve_boundary_spec( return BoundarySpec(low=cast(tuple[BoundaryCondition, BoundaryCondition], boundary)) +def _resolve_backend(backend: Backend) -> _ResolvedBackend: + if backend == "auto": + return "scipy-reference" if _scipy_available() else "rust" + if backend == "scipy": + return "scipy-reference" + if backend in {"scipy-reference", "rust"}: + return backend + raise ValueError("backend must be 'auto', 'scipy', 'scipy-reference', or 'rust'") + + +def _scipy_available() -> bool: + return importlib.util.find_spec("scipy") is not None + + def _solver_info_with_context( solver_info: dict[str, object], *, @@ -760,8 +777,8 @@ def _solver_info_with_context( shape: tuple[int, int], krylov_dim: int, ) -> dict[str, object]: - # Rust reports backend-local data. Add enough Python-side context for saved - # Result files and benchmark reports to be self-describing. + # Backends report backend-local data. Add enough Python-side context for + # saved Result files and benchmark reports to be self-describing. out = dict(solver_info) out["backend_kind"] = backend_kind out["shape"] = shape diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 88e577b..7ce0eed 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -1,9 +1,9 @@ -"""Readable SciPy reference implementation for the diagonal mode solver. +"""Readable SciPy implementation for the mode solver. -This module intentionally mirrors the Rust sparse mode-solver paths in plain -Python/SciPy. It is slower and less portable than the production backend, but it -keeps the numerical contract inspectable by users who want to audit the -finite-difference operators against SciPy/ARPACK. +This module assembles the sparse mode-solver paths in plain Python/SciPy. It is +the preferred backend when SciPy is installed because it keeps the numerical +contract inspectable by users who want to audit the finite-difference operators +against SciPy/ARPACK. """ from __future__ import annotations @@ -60,7 +60,7 @@ def solve_diagonal_scipy_reference( """Solve the diagonal sparse eigenproblem with SciPy/ARPACK. This is the same reduced ``[Ex, Ey]`` transverse eigenproblem used by the - Rust production backend for diagonal material tensors. + Rust backend for diagonal material tensors. """ sparse, spla, scipy_linalg = _import_scipy() @@ -300,7 +300,7 @@ def _import_scipy(): import scipy.sparse as sparse import scipy.sparse.linalg as spla except ImportError as exc: # pragma: no cover - depends on optional extra. - raise ImportError("the SciPy reference backend requires `pip install micromode[scipy]`") from exc + raise ImportError("the SciPy backend requires `pip install micromode[scipy]`") from exc return sparse, spla, scipy_linalg diff --git a/tests/test_micromode_api.py b/tests/test_micromode_api.py index 29a34ab..905de7b 100644 --- a/tests/test_micromode_api.py +++ b/tests/test_micromode_api.py @@ -34,7 +34,7 @@ def _strip_grid(nx: int = 5, ny: int = 4) -> tuple[np.ndarray, tuple[float, ...] return eps, x_edges, y_edges -def test_grid_api_solves_with_rust_sparse_backend(): +def test_grid_api_solves_with_default_backend(): eps, x_edges, y_edges = _strip_grid(6, 5) freq = mm.C_0 / 1.55 @@ -53,6 +53,7 @@ def test_grid_api_solves_with_rust_sparse_backend(): assert data.field_components["Ex"].shape == (6, 5, 1, 1, 2) assert set(data.field_components) == {"Ex", "Ey", "Ez", "Hx", "Hy", "Hz"} run_info = _solver_info(data)["runs"][0] + assert run_info["backend_kind"] in {"diagonal_scipy_reference", "diagonal_sparse"} assert run_info["phase_convention"] == "dominant_e_real_positive" assert run_info["normalization"] == "lorentz_orthogonal_unit_transverse_power" np.testing.assert_allclose(run_info["power_norms"], np.ones(2), rtol=1e-10, atol=1e-10) @@ -68,6 +69,61 @@ def test_grid_api_solves_with_rust_sparse_backend(): assert abs(anchor.imag) <= 1e-10 * max(abs(anchor), 1.0) +def test_default_backend_prefers_scipy_when_available(): + pytest.importorskip("scipy") + eps, x_edges, y_edges = _strip_grid(5, 4) + + data = mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + wavelength=1.55, + num_modes=1, + target_neff=2.5, + krylov_dim=16, + ) + + assert _solver_info(data)["backend"] == "scipy_arpack_reference" + assert _solver_info(data)["runs"][0]["backend_kind"] == "diagonal_scipy_reference" + + +def test_rust_backend_can_be_selected_explicitly(): + eps, x_edges, y_edges = _strip_grid(5, 4) + + data = mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + wavelength=1.55, + num_modes=1, + target_neff=2.5, + krylov_dim=16, + backend="rust", + ) + + assert _solver_info(data)["backend"] == "native_shift_invert" + assert _solver_info(data)["runs"][0]["backend_kind"] == "diagonal_sparse" + + +def test_scipy_backend_alias_selects_scipy_reference_path(): + pytest.importorskip("scipy") + eps, x_edges, y_edges = _strip_grid(5, 4) + + data = mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + wavelength=1.55, + num_modes=1, + target_neff=2.5, + krylov_dim=16, + backend="scipy", + ) + + assert _solver_info(data)["backend"] == "scipy_arpack_reference" + assert _solver_info(data)["runs"][0]["backend_kind"] == "diagonal_scipy_reference" + + def test_materials_api_matches_component_api_for_diagonal_grid(): eps, x_edges, y_edges = _strip_grid() freq = mm.C_0 / 1.55 @@ -108,7 +164,7 @@ def test_scipy_reference_backend_matches_rust_for_diagonal_grid(): "krylov_dim": 18, } - rust = mm.solve_grid(**common) + rust = mm.solve_grid(**common, backend="rust") reference = mm.solve_grid(**common, backend="scipy-reference") np.testing.assert_allclose(reference.n_complex.values, rust.n_complex.values, rtol=1e-8, atol=1e-8) @@ -135,7 +191,7 @@ def test_scipy_reference_backend_matches_rust_for_pml_and_tensorial_paths(): "pml": (1, 0), "krylov_dim": 18, } - rust_pml = mm.solve_grid(**pml_common) + rust_pml = mm.solve_grid(**pml_common, backend="rust") reference_pml = mm.solve_grid(**pml_common, backend="scipy-reference") np.testing.assert_allclose(reference_pml.n_complex.values, rust_pml.n_complex.values, rtol=1e-8, atol=1e-8) @@ -157,7 +213,7 @@ def test_scipy_reference_backend_matches_rust_for_pml_and_tensorial_paths(): "target_neff": 2.2, "krylov_dim": 20, } - rust_tensor = mm.solve_grid(**tensor_common) + rust_tensor = mm.solve_grid(**tensor_common, backend="rust") reference_tensor = mm.solve_grid(**tensor_common, backend="scipy-reference") np.testing.assert_allclose(reference_tensor.n_complex.values, rust_tensor.n_complex.values, rtol=1e-8, atol=1e-8) @@ -182,7 +238,7 @@ def test_scipy_reference_backend_matches_rust_for_transformed_grid(): "krylov_dim": 20, } - rust = mm.solve_grid(**common) + rust = mm.solve_grid(**common, backend="rust") reference = mm.solve_grid(**common, backend="scipy-reference") np.testing.assert_allclose(reference.n_complex.values, rust.n_complex.values, rtol=1e-8, atol=1e-8) @@ -495,7 +551,7 @@ def test_full_tensor_grid_supports_angle_and_bend_transform(): krylov_dim=18, ) - assert _solver_info(data)["runs"][0]["backend_kind"] == "tensorial_sparse" + assert _solver_info(data)["runs"][0]["backend_kind"] in {"tensorial_scipy_reference", "tensorial_sparse"} assert np.isfinite(data.n_complex.values).all() assert np.isfinite(data.field_components["Ex"].values).all() From 797bf8afa9be5598b667919fb6f4b3ee5ce735a4 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 05:26:00 +0200 Subject: [PATCH 10/16] Remove Rust backend --- .github/workflows/publish.yml | 17 +- .github/workflows/tests.yml | 27 - CHANGELOG.md | 4 +- Cargo.lock | 371 ---------- Cargo.toml | 38 - README.md | 38 +- benchmarks/compare_mode_solver_fixtures.py | 36 +- benchmarks/compare_tidy3d_backends.py | 31 +- benchmarks/eigensolver_backend_benchmark.rs | 91 --- benchmarks/eigensolver_benchmark_problem.rs | 111 --- benchmarks/eigensolver_phase_benchmark.rs | 99 --- ...hmark.py => micromode_solver_benchmark.py} | 8 +- benchmarks/mode_solver/README.md | 21 +- docs/backend-trust.md | 84 +-- docs/mode-solver-methods.md | 17 +- docs/physics-model.md | 4 +- docs/release.md | 12 +- examples/README.md | 2 +- examples/material_grid_demos.py | 4 +- pyproject.toml | 22 +- python/micromode/__init__.py | 2 +- python/micromode/_rust.py | 253 ------- python/micromode/constants.py | 4 + python/micromode/models.py | 2 +- python/micromode/raster.py | 206 +----- python/micromode/result.py | 6 +- python/micromode/scipy_reference.py | 16 +- scripts/check_dist_artifacts.py | 9 + scripts/check_release_metadata.py | 28 +- scripts/smoke_dist.py | 14 +- src/derivatives.rs | 522 -------------- src/diagonal_solver.rs | 160 ----- src/eigensolve.rs | 664 ------------------ src/lib.rs | 9 - src/mode_solver.rs | 569 --------------- src/operators.rs | 261 ------- src/python_api.rs | 371 ---------- src/sparse_matrix.rs | 241 ------- tests/test_micromode_api.py | 140 ++-- tests/test_mode_solver_fixtures.py | 33 - uv.lock | 36 +- 41 files changed, 184 insertions(+), 4399 deletions(-) delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 benchmarks/eigensolver_backend_benchmark.rs delete mode 100644 benchmarks/eigensolver_benchmark_problem.rs delete mode 100644 benchmarks/eigensolver_phase_benchmark.rs rename benchmarks/{micromode_backend_benchmark.py => micromode_solver_benchmark.py} (94%) delete mode 100644 python/micromode/_rust.py create mode 100644 python/micromode/constants.py delete mode 100644 src/derivatives.rs delete mode 100644 src/diagonal_solver.rs delete mode 100644 src/eigensolve.rs delete mode 100644 src/lib.rs delete mode 100644 src/mode_solver.rs delete mode 100644 src/operators.rs delete mode 100644 src/python_api.rs delete mode 100644 src/sparse_matrix.rs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 376130e..009a790 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,19 +64,7 @@ jobs: run: uv python install ${{ matrix.python-version }} - name: Build wheel - shell: bash - run: | - extra_args=() - if [[ "${{ runner.os }}" == "Linux" ]]; then - extra_args+=(--auditwheel repair) - fi - - uv tool run --from "maturin>=1.7,<2" maturin build \ - --release \ - --out dist \ - --interpreter "$(uv python find ${{ matrix.python-version }})" \ - --compatibility pypi \ - "${extra_args[@]}" + run: uv build --wheel - name: Smoke-test wheel shell: bash @@ -120,8 +108,7 @@ jobs: PY python scripts/check_release_metadata.py python scripts/check_dist_artifacts.py dist \ - --require-cpython 3.10 3.11 3.12 3.13 \ - --require-platform macosx manylinux win + --allow-pure-python twine check dist/* - name: Upload checked distributions diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f4fd9e..c903694 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,9 +19,6 @@ jobs: with: persist-credentials: false - - name: Install Rust tooling - run: rustup component add rustfmt clippy - - name: Install uv uses: astral-sh/setup-uv@v5 with: @@ -33,12 +30,6 @@ jobs: - name: Install dependencies run: uv sync --all-extras - - name: Check Rust formatting - run: cargo fmt --all -- --check - - - name: Run Rust clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - name: Check Python formatting run: uv run ruff format --check . @@ -73,18 +64,12 @@ jobs: - name: Install dependencies run: uv sync --all-extras - - name: Build extension - run: uv run maturin develop - - name: Check release metadata run: uv run python scripts/check_release_metadata.py - name: Run Python tests run: uv run pytest -m "not slow" --cov=micromode --cov-report=xml --cov-report=term-missing - - name: Run Rust tests - run: cargo test --no-default-features - - name: Build distributions if: matrix.python-version == '3.13' run: uv build @@ -165,15 +150,9 @@ jobs: - name: Install dependencies run: uv sync --all-extras - - name: Build extension - run: uv run maturin develop - - name: Run Python tests run: uv run pytest - - name: Run Rust tests - run: cargo test --no-default-features - test-windows: name: Windows portable tests needs: lint @@ -193,15 +172,9 @@ jobs: - name: Install dependencies run: uv sync --all-extras - - name: Build extension - run: uv run maturin develop - - name: Run Python tests run: uv run pytest -m "not slow" - - name: Run Rust tests - run: cargo test --no-default-features - - name: Build distributions run: uv build diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3bdd3..2ed5854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,7 @@ Initial alpha release candidate. - Added grid-first Python API for rasterized mode solving. -- Added Rust sparse shift-invert solver backend. -- Added a portable native Rust sparse eigensolver path with AMD ordering, packed - LU solves, adaptive Arnoldi stopping, and no external solver-stack dependency. +- Added sparse shift-invert solver support. - Added 2D cross-section solves and 1D slice solves. - Added scalar, diagonal anisotropic, and tensor material grids. - Added six-component field reconstruction. diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 4800e51..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,371 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "amd" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a679e001575697a3bd195813feb57a4718ecc08dc194944015cbc5f6213c2b96" -dependencies = [ - "num-traits", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "micromode-core" -version = "0.1.0-alpha.4" -dependencies = [ - "amd", - "nalgebra", - "num-complex", - "pyo3", - "rlu", -] - -[[package]] -name = "nalgebra" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d43ddcacf343185dfd6de2ee786d9e8b1c2301622afab66b6c73baf9882abfd" -dependencies = [ - "approx", - "matrixmultiply", - "nalgebra-macros", - "num-complex", - "num-rational", - "num-traits", - "simba", - "typenum", -] - -[[package]] -name = "nalgebra-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyo3" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "rlu" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3995d5221cae7acbb7a629fd9a2065024acdf49f851eb7af64150affb8941" -dependencies = [ - "amd", - "anyhow", - "num-complex", - "num-traits", - "spsolve", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "simba" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - -[[package]] -name = "spsolve" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07a64f5d8060c8f312252da7537ac7520e0c1b3d6a9a5827df0b1555c8cad9ba" -dependencies = [ - "anyhow", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 00edb90..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "micromode-core" -version = "0.1.0-alpha.4" -edition = "2021" -description = "Rust core for the MicroMode photonics mode solver." -license = "Apache-2.0" -repository = "https://github.com/QuentinWach/micromode" -readme = "README.md" -keywords = ["fdfd", "mode-solver", "photonics", "waveguide"] -categories = ["science"] - -[lib] -name = "micromode_core" -crate-type = ["cdylib", "rlib"] - -[[bin]] -name = "eigensolver-backend-benchmark" -path = "benchmarks/eigensolver_backend_benchmark.rs" - -[[bin]] -name = "eigensolver-phase-benchmark" -path = "benchmarks/eigensolver_phase_benchmark.rs" - -[dependencies] -amd = "0.2" -nalgebra = "0.33" -num-complex = "0.4" -pyo3 = { version = "0.23.5", optional = true } -rlu = "0.7" - -[features] -default = [] -python = ["pyo3"] -extension-module = ["python", "pyo3/extension-module"] - -[lints.clippy] -needless_range_loop = "allow" -too_many_arguments = "allow" diff --git a/README.md b/README.md index 99627a6..c3a2b6c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # micromode -An **electromagnetic mode solver** using the **[FDFD method](https://en.wikipedia.org/wiki/Finite-difference_frequency-domain_method)** on a **[rectilinear Yee-grid](https://en.wikipedia.org/wiki/Finite-difference_time-domain_method)**, with a readable **SciPy/ARPACK** backend and an optional native **[Rust](https://rust-lang.org/)** backend. +An **electromagnetic mode solver** using the **[FDFD method](https://en.wikipedia.org/wiki/Finite-difference_frequency-domain_method)** on a **[rectilinear Yee-grid](https://en.wikipedia.org/wiki/Finite-difference_time-domain_method)**. ```bash -pip install "micromode[scipy]" +pip install micromode ``` [![License](https://img.shields.io/github/license/QuentinWach/micromode)](LICENSE) @@ -16,10 +16,8 @@ pip install "micromode[scipy]" ## Why Use It? - **Grid-first API**: pass arrays directly, with no required geometry model. -- **Auditable SciPy default**: sparse operators are assembled in Python and - solved with SciPy/ARPACK when SciPy is installed. -- **Optional Rust backend**: a portable native fallback for environments that do - not want a SciPy dependency. +- **Auditable SciPy solver**: sparse operators are assembled in Python and + solved with SciPy/ARPACK. - **Practical** outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions, Lorentz overlaps, plotting, dataframe export, and HDF5 save/load. - **Tensor-aware**: supports scalar, diagonal anisotropic, and full tensor material @@ -100,32 +98,8 @@ the public solver controls are summarized in [docs/mode-solver-methods.md](docs/ ## Solver -MicroMode defaults to a Python/SciPy backend when SciPy is installed. This path -assembles the finite-difference sparse operators in Python and solves the -shift-invert eigenproblems with +MicroMode assembles the finite-difference sparse operators in Python and solves +the shift-invert eigenproblems with [SciPy/ARPACK](https://docs.scipy.org/doc/scipy/reference/sparse.linalg.html). That makes the numerical method easier for academic users to inspect and debug, while still using a trusted sparse eigensolver stack. - -The recommended install includes SciPy: - -```bash -pip install "micromode[scipy]" -``` - -The public APIs use `backend="auto"` by default. In auto mode MicroMode selects -SciPy when available and falls back to Rust otherwise. You can also choose a -backend explicitly: - -```python -data = mm.solve_modes(..., backend="scipy") # same as backend="scipy-reference" -data = mm.solve_modes(..., backend="rust") -``` - -The Rust backend remains useful when a deployment needs a self-contained native -solver with no SciPy, ARPACK, BLAS/LAPACK, or Fortran toolchain requirement. It -uses sparse finite-difference operators, AMD fill-reducing ordering, sparse LU -factorization, and a shift-invert Arnoldi iteration. The SciPy and Rust paths -are compared in tests so their effective indices and normalization diagnostics -stay aligned on supported cases. See -[docs/backend-trust.md](docs/backend-trust.md). diff --git a/benchmarks/compare_mode_solver_fixtures.py b/benchmarks/compare_mode_solver_fixtures.py index de5e26d..2b4b2cf 100644 --- a/benchmarks/compare_mode_solver_fixtures.py +++ b/benchmarks/compare_mode_solver_fixtures.py @@ -52,12 +52,6 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Run local MicroMode solves for supported fixture cases and compare n_eff/fields.", ) - parser.add_argument( - "--backend", - choices=("rust_sparse", "scipy_reference"), - default="rust_sparse", - help="Local MicroMode backend to use with --run-local.", - ) parser.add_argument( "--fail-on-tolerance", action="store_true", @@ -91,7 +85,6 @@ def main() -> None: report = { "fixture_root": str(fixture_root), - "backend": args.backend if args.run_local else None, "cases": [], "summary": {"pass": 0, "fail": 0, "unsupported": 0, "not_run": 0}, } @@ -110,8 +103,8 @@ def main() -> None: ) status = {"status": "not_run", "summary": "local solve not requested"} if args.run_local: - status = _compare_local_case(fixture_root, entry, backend=args.backend) - print(f" local {args.backend}: {status['status']}: {status['summary']}") + status = _compare_local_case(fixture_root, entry) + print(f" local scipy: {status['status']}: {status['summary']}") if status["failed"]: failures += 1 if status.get("support") == "production" and status["status"] != "pass": @@ -131,7 +124,7 @@ def main() -> None: raise SystemExit(f"{production_gaps} production fixture comparison(s) did not pass") -def _compare_local_case(root: Path, entry: dict, *, backend: str = "rust_sparse") -> dict: +def _compare_local_case(root: Path, entry: dict) -> dict: case_id = entry["case_id"] try: import micromode as sm @@ -152,9 +145,6 @@ def _compare_local_case(root: Path, entry: dict, *, backend: str = "rust_sparse" support = recipe.get("support", "production") if recipe.get("unsupported"): return _status("unsupported", recipe["unsupported"], support=support) - if backend not in {"rust_sparse", "scipy_reference"}: - return _status("fail", f"unknown backend: {backend}", support=support) - tangent_dims = tuple(dim for dim in ("x", "y", "z") if dim in ref_ex.dims and ref_ex.sizes.get(dim, 0) > 1) if len(tangent_dims) != 2: return _status("unsupported", f"expected two raster dimensions, got {tangent_dims}", support=support) @@ -174,13 +164,12 @@ def _compare_local_case(root: Path, entry: dict, *, backend: str = "rust_sparse" tangent_dims=tangent_dims, normal_dim=normal_dim, normal_coord=normal_coord, - backend=backend, ) except NotImplementedError as exc: return _status("unsupported", str(exc), support=support) actual_n = _reorder_modes(result.n_complex.values, recipe) n_error = float(np.max(np.abs(actual_n - ref_n.values))) - tolerance = _n_tolerance(entry, recipe, backend=backend) + tolerance = _n_tolerance(entry, recipe) failed = n_error > tolerance field_errors = [] @@ -284,7 +273,7 @@ def _compare_local_case(root: Path, entry: dict, *, backend: str = "rust_sparse" "direction": "-", "dmin_pmc": (False, True), "trim_edges": ((1, 1), (0, 0)), - "backend_tolerances": {"rust_sparse": 1e-5, "scipy_reference": 1e-5}, + "n_tolerance": 1e-5, "sort_order": "ascending", "krylov_dim": 64, }, @@ -298,7 +287,7 @@ def _compare_local_case(root: Path, entry: dict, *, backend: str = "rust_sparse" }, "pml_cross_section_y": { "support": "future_fixture_harness", - "unsupported": "PML comparison is not implemented for the local Rust fixture harness", + "unsupported": "PML comparison is not implemented for the local fixture harness", }, "custom_cartesian_left": { "support": "metadata_missing", @@ -364,7 +353,6 @@ def _solve_recipe( tangent_dims: tuple[str, str], normal_dim: str, normal_coord: float, - backend: str, ): freqs = tuple(float(freq) for freq in ref_n.coords["f"].values) if recipe.get("solve_each_frequency"): @@ -381,7 +369,6 @@ def _solve_recipe( tangent_dims=tangent_dims, normal_dim=normal_dim, normal_coord=normal_coord, - backend=backend, ) if first_result is None: first_result = result @@ -423,7 +410,6 @@ def _solve_recipe( tangent_dims=tangent_dims, normal_dim=normal_dim, normal_coord=normal_coord, - backend=backend, ) @@ -437,7 +423,6 @@ def _solve_recipe_for_freq( tangent_dims: tuple[str, str], normal_dim: str, normal_coord: float, - backend: str, ): freqs = (float(freq),) if np.isscalar(freq) else tuple(float(value) for value in freq) centers = tuple((axis_edges[:-1] + axis_edges[1:]) / 2 for axis_edges in edges) @@ -464,7 +449,6 @@ def _solve_recipe_for_freq( bend_radius=recipe.get("bend_radius"), bend_axis=recipe.get("bend_axis", 0), krylov_dim=recipe.get("krylov_dim"), - backend="scipy-reference" if backend == "scipy_reference" else "rust", ) @@ -535,11 +519,11 @@ def _status(status: str, summary: str, **details) -> dict: return {"status": status, "failed": status == "fail", "summary": summary, **details} -def _n_tolerance(entry: dict, recipe: dict | None = None, *, backend: str = "rust_sparse") -> float: +def _n_tolerance(entry: dict, recipe: dict | None = None) -> float: if recipe is not None: - backend_tolerance = recipe.get("backend_tolerances", {}).get(backend) - if backend_tolerance is not None: - return float(backend_tolerance) + recipe_tolerance = recipe.get("n_tolerance") + if recipe_tolerance is not None: + return float(recipe_tolerance) return float( entry.get("tolerances", {}).get( "n_complex_atol", diff --git a/benchmarks/compare_tidy3d_backends.py b/benchmarks/compare_tidy3d_backends.py index 2b14560..d37157b 100644 --- a/benchmarks/compare_tidy3d_backends.py +++ b/benchmarks/compare_tidy3d_backends.py @@ -64,7 +64,7 @@ def main() -> None: def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Compare MicroMode Rust, MicroMode SciPy, and Tidy3D local solves.") + parser = argparse.ArgumentParser(description="Compare MicroMode SciPy and Tidy3D local solves.") parser.add_argument("--preset", choices=tuple(PRESETS), default="quick") parser.add_argument( "--profile-source", @@ -72,7 +72,7 @@ def parse_args() -> argparse.Namespace: default="tidy3d", help="Use Tidy3D's exact local solver grid/epsilon profile, or MicroMode's independent analytic raster.", ) - parser.add_argument("--output", type=Path, default=Path("tmp/tidy3d_backend_benchmark.json")) + parser.add_argument("--output", type=Path, default=Path("tmp/tidy3d_solver_benchmark.json")) return parser.parse_args() @@ -90,32 +90,27 @@ def run_case(case: BenchmarkCase, *, profile_source: str) -> dict[str, object]: "cells": case.ny * case.nz, "profile_source": profile_source, } - rust_seconds, rust_neff = time_micromode(case, materials, backend="rust") - scipy_seconds, scipy_neff = time_micromode(case, materials, backend="scipy-reference") + scipy_seconds, scipy_neff = time_micromode(case, materials) tidy3d_seconds, tidy3d_neff = time_tidy3d(tidy3d_solver) row.update( { - "rust_seconds": rust_seconds, "scipy_seconds": scipy_seconds, "tidy3d_seconds": tidy3d_seconds, - "rust_n_eff": rust_neff.tolist(), "scipy_n_eff": scipy_neff.tolist(), "tidy3d_n_eff": tidy3d_neff.tolist(), - "rust_scipy_max_abs_neff": max_abs_delta(rust_neff, scipy_neff), - "rust_tidy3d_max_abs_neff": max_abs_delta(rust_neff, tidy3d_neff), "scipy_tidy3d_max_abs_neff": max_abs_delta(scipy_neff, tidy3d_neff), } ) print( - f"{case.case_id}: rust={rust_seconds:.3f}s scipy={scipy_seconds:.3f}s " - f"tidy3d={tidy3d_seconds:.3f}s delta_tidy3d={row['rust_tidy3d_max_abs_neff']:.3e}", + f"{case.case_id}: scipy={scipy_seconds:.3f}s tidy3d={tidy3d_seconds:.3f}s " + f"delta_tidy3d={row['scipy_tidy3d_max_abs_neff']:.3e}", flush=True, ) return row -def time_micromode(case: BenchmarkCase, materials: mm.Materials, *, backend: str) -> tuple[float, np.ndarray]: +def time_micromode(case: BenchmarkCase, materials: mm.Materials) -> tuple[float, np.ndarray]: start = time.perf_counter() data = mm.solve_modes( material_grid=materials, @@ -124,7 +119,6 @@ def time_micromode(case: BenchmarkCase, materials: mm.Materials, *, backend: str target_neff=case.target_neff, krylov_dim=case.krylov_dim, pml=mm.PmlSpec(num_cells=case.num_pml), - backend=backend, ) return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) @@ -140,7 +134,7 @@ def make_tidy3d_solver(case: BenchmarkCase): import tidy3d as td from tidy3d.plugins.mode import ModeSolver except ImportError as exc: # pragma: no cover - benchmark dependency only. - raise SystemExit("Install Tidy3D for this benchmark: uv run --with tidy3d --extra scipy ...") from exc + raise SystemExit("Install Tidy3D for this benchmark: uv run --with tidy3d ...") from exc structures = tidy3d_structures(td, case.problem) dl = min(WIDTH_Y / case.ny, WIDTH_Z / case.nz) @@ -241,17 +235,14 @@ def max_abs_delta(left: np.ndarray, right: np.ndarray) -> float: def markdown_table(rows: list[dict[str, object]]) -> str: header = ( - "| Problem | Grid | Rust (s) | SciPy backend (s) | Tidy3D local (s) | " - "max abs Δn_eff Rust/SciPy | max abs Δn_eff Rust/Tidy3D | " - "max abs Δn_eff SciPy/Tidy3D |\n" - "|---|---:|---:|---:|---:|---:|---:|---:|" + "| Problem | Grid | MicroMode SciPy (s) | Tidy3D local (s) | max abs Δn_eff SciPy/Tidy3D |\n" + "|---|---:|---:|---:|---:|" ) lines = [header] for row in rows: lines.append( - f"| {row['description']} | {row['grid']} | {row['rust_seconds']:.3f} | " - f"{row['scipy_seconds']:.3f} | {row['tidy3d_seconds']:.3f} | " - f"{row['rust_scipy_max_abs_neff']:.3e} | {row['rust_tidy3d_max_abs_neff']:.3e} | " + f"| {row['description']} | {row['grid']} | {row['scipy_seconds']:.3f} | " + f"{row['tidy3d_seconds']:.3f} | " f"{row['scipy_tidy3d_max_abs_neff']:.3e} |" ) return "\n".join(lines) diff --git a/benchmarks/eigensolver_backend_benchmark.rs b/benchmarks/eigensolver_backend_benchmark.rs deleted file mode 100644 index d59542e..0000000 --- a/benchmarks/eigensolver_backend_benchmark.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::hint::black_box; -use std::time::{Duration, Instant}; - -use micromode_core::eigensolve::{ - selected_sparse_shift_invert_native_eigenpairs, Eigenpair, ShiftInvertOptions, -}; - -mod eigensolver_benchmark_problem; -use eigensolver_benchmark_problem::{parse_f64_arg, parse_grids, parse_usize_arg, strip_operator}; - -fn main() -> Result<(), String> { - let grids = parse_grids(); - let repeats = parse_usize_arg("--repeats").unwrap_or(5); - let krylov_dim = parse_usize_arg("--krylov-dim").unwrap_or(40); - let num_modes = parse_usize_arg("--num-modes").unwrap_or(2); - let target_neff = parse_f64_arg("--target-neff").unwrap_or(2.5); - - println!("backend,grid,operator_size,operator_nnz,repeats,best_ms,mean_ms,max_residual"); - for (nx, ny) in grids { - let (mat, guess) = strip_operator(nx, ny, target_neff); - let options = ShiftInvertOptions { - krylov_dim, - tolerance: 1e-10, - }; - - let native = benchmark_backend(repeats, || { - selected_sparse_shift_invert_native_eigenpairs( - &mat, - num_modes, - guess, - None, - options.clone(), - ) - })?; - - print_row("native", nx, ny, mat.rows, mat.nnz(), repeats, &native); - } - Ok(()) -} - -struct BenchResult { - durations: Vec, - pairs: Vec, -} - -fn benchmark_backend(repeats: usize, mut solve: F) -> Result -where - F: FnMut() -> Result, String>, -{ - let warmup = solve()?; - black_box(&warmup); - - let mut durations = Vec::with_capacity(repeats); - let mut pairs = Vec::new(); - for _ in 0..repeats { - let start = Instant::now(); - pairs = solve()?; - durations.push(start.elapsed()); - black_box(&pairs); - } - Ok(BenchResult { durations, pairs }) -} - -fn print_row( - backend: &str, - nx: usize, - ny: usize, - operator_size: usize, - operator_nnz: usize, - repeats: usize, - result: &BenchResult, -) { - let best = result.durations.iter().min().copied().unwrap_or_default(); - let mean = result - .durations - .iter() - .map(Duration::as_secs_f64) - .sum::() - / repeats as f64; - let max_residual = result - .pairs - .iter() - .map(|pair| pair.residual) - .fold(0.0, f64::max); - println!( - "{backend},{nx}x{ny},{operator_size},{operator_nnz},{repeats},{:.3},{:.3},{:.3e}", - best.as_secs_f64() * 1000.0, - mean * 1000.0, - max_residual, - ); -} diff --git a/benchmarks/eigensolver_benchmark_problem.rs b/benchmarks/eigensolver_benchmark_problem.rs deleted file mode 100644 index 983373b..0000000 --- a/benchmarks/eigensolver_benchmark_problem.rs +++ /dev/null @@ -1,111 +0,0 @@ -use micromode_core::derivatives::{self, Tensor3}; -use micromode_core::operators::assemble_sparse_diagonal_operators; -use micromode_core::sparse_matrix::SparseMatrix; -use num_complex::Complex64; - -pub fn strip_operator(nx: usize, ny: usize, target_neff: f64) -> (SparseMatrix, Complex64) { - let x_edges = linspace(-1.2, 1.2, nx + 1); - let y_edges = linspace(-0.8, 0.8, ny + 1); - let dlf_x = steps(&x_edges); - let dlf_y = steps(&y_edges); - let dlb_x = dlf_x.clone(); - let dlb_y = dlf_y.clone(); - let derivatives = derivatives::create_d_matrices_sparse( - (nx, ny), - (&dlf_x, &dlf_y), - (&dlb_x, &dlb_y), - (false, false), - ); - let eps = strip_tensor(nx, ny, &x_edges, &y_edges); - let mu = uniform_tensor(nx * ny, 1.0); - let operators = assemble_sparse_diagonal_operators(&eps, &mu, &derivatives); - let guess = Complex64::new(-(target_neff * target_neff), 0.0); - (operators.mat, guess) -} - -pub fn parse_grids() -> Vec<(usize, usize)> { - let args = std::env::args().collect::>(); - let mut grids = Vec::new(); - let mut index = 0; - while index < args.len() { - if args[index] == "--grid" { - if let Some(value) = args.get(index + 1) { - if let Some(grid) = parse_grid(value) { - grids.push(grid); - } - } - index += 1; - } - index += 1; - } - if grids.is_empty() { - vec![(10, 8), (16, 12), (24, 18)] - } else { - grids - } -} - -pub fn parse_usize_arg(name: &str) -> Option { - parse_arg(name).and_then(|value| value.parse().ok()) -} - -pub fn parse_f64_arg(name: &str) -> Option { - parse_arg(name).and_then(|value| value.parse().ok()) -} - -fn strip_tensor(nx: usize, ny: usize, x_edges: &[f64], y_edges: &[f64]) -> Tensor3 { - let mut tensor = uniform_tensor(nx * ny, 1.44 * 1.44); - for ix in 0..nx { - let x = 0.5 * (x_edges[ix] + x_edges[ix + 1]); - for iy in 0..ny { - let y = 0.5 * (y_edges[iy] + y_edges[iy + 1]); - let eps = if x.abs() <= 0.25 && y.abs() <= 0.11 { - 3.48 * 3.48 - } else { - 1.44 * 1.44 - }; - let index = ix * ny + iy; - for component in 0..3 { - tensor[component][component][index] = Complex64::new(eps, 0.0); - } - } - } - tensor -} - -fn uniform_tensor(n: usize, diagonal: f64) -> Tensor3 { - let mut tensor: Tensor3 = std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())); - for row in 0..3 { - for col in 0..3 { - tensor[row][col] = vec![Complex64::new(0.0, 0.0); n]; - } - } - for component in 0..3 { - tensor[component][component] = vec![Complex64::new(diagonal, 0.0); n]; - } - tensor -} - -fn linspace(start: f64, stop: f64, len: usize) -> Vec { - let step = (stop - start) / (len - 1) as f64; - (0..len).map(|index| start + step * index as f64).collect() -} - -fn steps(edges: &[f64]) -> Vec { - edges - .windows(2) - .map(|window| window[1] - window[0]) - .collect() -} - -fn parse_grid(value: &str) -> Option<(usize, usize)> { - let (left, right) = value.split_once('x')?; - Some((left.parse().ok()?, right.parse().ok()?)) -} - -fn parse_arg(name: &str) -> Option { - let args = std::env::args().collect::>(); - args.windows(2) - .find(|window| window[0] == name) - .map(|window| window[1].clone()) -} diff --git a/benchmarks/eigensolver_phase_benchmark.rs b/benchmarks/eigensolver_phase_benchmark.rs deleted file mode 100644 index 3f6717b..0000000 --- a/benchmarks/eigensolver_phase_benchmark.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::hint::black_box; -use std::time::{Duration, Instant}; - -use micromode_core::eigensolve::{ - profile_sparse_shift_invert_native_eigenpairs, ShiftInvertOptions, ShiftInvertProfile, -}; - -mod eigensolver_benchmark_problem; -use eigensolver_benchmark_problem::{parse_f64_arg, parse_grids, parse_usize_arg, strip_operator}; - -fn main() -> Result<(), String> { - let grids = parse_grids(); - let repeats = parse_usize_arg("--repeats").unwrap_or(3); - let krylov_dim = parse_usize_arg("--krylov-dim").unwrap_or(56); - let num_modes = parse_usize_arg("--num-modes").unwrap_or(2); - let target_neff = parse_f64_arg("--target-neff").unwrap_or(2.5); - - println!( - "grid,operator_size,operator_nnz,repeat,assembly_ms,total_ms,shift_diagonal_ms,amd_ordering_ms,lu_factorization_ms,lu_packing_ms,linear_solves_ms,arnoldi_orthogonalization_ms,hessenberg_eigensolve_ms,ritz_reconstruction_ms,residuals_ms,sorting_ms,solve_calls,arnoldi_steps,candidate_count,returned_pairs,lu_l_nnz,lu_u_nnz,lu_fill_ratio,max_residual" - ); - for (nx, ny) in grids { - let (warm_mat, warm_guess) = strip_operator(nx, ny, target_neff); - let warm_profile = profile_sparse_shift_invert_native_eigenpairs( - &warm_mat, - num_modes, - warm_guess, - None, - ShiftInvertOptions { - krylov_dim, - tolerance: 1e-10, - }, - )?; - black_box(&warm_profile); - - for repeat in 0..repeats { - let assembly_start = Instant::now(); - let (mat, guess) = strip_operator(nx, ny, target_neff); - let assembly = assembly_start.elapsed(); - let profile = profile_sparse_shift_invert_native_eigenpairs( - &mat, - num_modes, - guess, - None, - ShiftInvertOptions { - krylov_dim, - tolerance: 1e-10, - }, - )?; - black_box(&profile); - print_row(nx, ny, repeat, mat.rows, mat.nnz(), assembly, &profile); - } - } - Ok(()) -} - -fn print_row( - nx: usize, - ny: usize, - repeat: usize, - operator_size: usize, - operator_nnz: usize, - assembly: Duration, - profile: &ShiftInvertProfile, -) { - let lu_nnz = profile.lu_l_nnz + profile.lu_u_nnz; - let fill_ratio = lu_nnz as f64 / operator_nnz.max(1) as f64; - println!( - "{}x{},{},{},{},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{},{},{},{},{},{},{:.3},{:.3e}", - nx, - ny, - operator_size, - operator_nnz, - repeat, - ms(assembly), - ms(profile.total), - ms(profile.shift_diagonal), - ms(profile.amd_ordering), - ms(profile.lu_factorization), - ms(profile.lu_packing), - ms(profile.linear_solves), - ms(profile.arnoldi_orthogonalization), - ms(profile.hessenberg_eigensolve), - ms(profile.ritz_reconstruction), - ms(profile.residuals), - ms(profile.sorting), - profile.solve_calls, - profile.arnoldi_steps, - profile.candidate_count, - profile.returned_pairs, - profile.lu_l_nnz, - profile.lu_u_nnz, - fill_ratio, - profile.max_residual, - ); -} - -fn ms(duration: Duration) -> f64 { - duration.as_secs_f64() * 1000.0 -} diff --git a/benchmarks/micromode_backend_benchmark.py b/benchmarks/micromode_solver_benchmark.py similarity index 94% rename from benchmarks/micromode_backend_benchmark.py rename to benchmarks/micromode_solver_benchmark.py index 68693ad..8ec7410 100644 --- a/benchmarks/micromode_backend_benchmark.py +++ b/benchmarks/micromode_solver_benchmark.py @@ -34,8 +34,8 @@ def main() -> None: "cells": nx * ny, "repeat": repeat, "seconds": elapsed, - "backend": run_info["backend"], - "backend_kind": run_info["backend_kind"], + "solver": run_info["backend"], + "solver_kind": run_info["backend_kind"], "operator_size": run_info["operator_size"], "operator_nnz": run_info["operator_nnz"], "max_residual": float(np.max(run_info["residuals"])), @@ -44,7 +44,7 @@ def main() -> None: rows.append(row) print( f"{nx:4d}x{ny:<4d} repeat={repeat} " - f"{elapsed:7.3f}s backend={row['backend']} " + f"{elapsed:7.3f}s solver={row['solver']} " f"max_res={row['max_residual']:.2e}" ) @@ -68,7 +68,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--krylov-dim", type=int, default=40) parser.add_argument("--wavelength", type=float, default=1.55) parser.add_argument("--num-pml", type=int, nargs=2, metavar=("NX", "NY"), default=(0, 0)) - parser.add_argument("--output", type=Path, default=Path("tmp/micromode_backend_benchmark.json")) + parser.add_argument("--output", type=Path, default=Path("tmp/micromode_solver_benchmark.json")) return parser.parse_args() diff --git a/benchmarks/mode_solver/README.md b/benchmarks/mode_solver/README.md index aba7d73..30bc089 100644 --- a/benchmarks/mode_solver/README.md +++ b/benchmarks/mode_solver/README.md @@ -18,19 +18,10 @@ Run the local raster solver against every fixture that has enough neutral metada the mode plane: ```bash -uv run python benchmarks/compare_mode_solver_fixtures.py --suite extended --run-local --report-json tmp/reference_fixture_validation_rust_sparse.json -``` - -This benchmark harness defaults to the Rust sparse backend so historical -fixture runs stay comparable. To run the same reconstructable fixture recipes -through the package's preferred SciPy backend: - -```bash -uv run --extra scipy python benchmarks/compare_mode_solver_fixtures.py \ +uv run python benchmarks/compare_mode_solver_fixtures.py \ --suite extended \ --run-local \ - --backend scipy_reference \ - --report-json tmp/reference_fixture_validation_scipy_reference.json + --report-json tmp/reference_fixture_validation_scipy.json ``` Local validation reports each case as `pass`, `fail`, or `unsupported`. `fail` means the local @@ -62,13 +53,13 @@ waveguides, cubic and polynomial interpolation, and non-default mode sorting met angle-plus-bend transforms still need a validated tensorial reference run before a fixture is committed. -## Backend Timing +## Solver Timing -Use the backend benchmark to track solve time and sparse-operator diagnostics across grid sizes: +Use the solver benchmark to track solve time and sparse-operator diagnostics across grid sizes: ```bash -uv run python benchmarks/micromode_backend_benchmark.py --grid 20x14 --grid 32x22 +uv run python benchmarks/micromode_solver_benchmark.py --grid 20x14 --grid 32x22 ``` -The benchmark uses MicroMode directly, writes JSON to `tmp/` by default, and records backend name, +The benchmark uses MicroMode directly, writes JSON to `tmp/` by default, and records solver name, operator size, nonzero count, residuals, elapsed time, and solved effective indices. diff --git a/docs/backend-trust.md b/docs/backend-trust.md index 271fa79..fa12e24 100644 --- a/docs/backend-trust.md +++ b/docs/backend-trust.md @@ -1,56 +1,23 @@ -# Backend Trust Model - -MicroMode has two backend roles: - -- The SciPy backend is the preferred default when SciPy is installed. It - assembles the sparse operators in Python and calls SciPy/ARPACK so the core - numerical method can be inspected by users who are more comfortable reading - Python. -- The Rust backend is an optional portable fallback. It does not require users - to install ARPACK, SuiteSparse, BLAS/LAPACK bindings, or a Fortran toolchain. - -The public APIs default to `backend="auto"`, which selects SciPy when available -and falls back to Rust otherwise. A backend can also be selected explicitly: - -```python -import micromode as mm - -data = mm.solve_grid( - eps_xx=eps, - x_edges=x_edges, - y_edges=y_edges, - freqs=[freq], - num_modes=2, - target_neff=2.5, - backend="scipy", -) -``` - -`backend="scipy-reference"` is kept as a compatibility alias for the same SciPy -path. Install the recommended SciPy dependency with: +# Solver Trust Model -```bash -pip install "micromode[scipy]" -``` +MicroMode uses one solver implementation: a Python/SciPy sparse mode solver. +The finite-difference operators are assembled in inspectable Python code and +the eigenproblems are solved with SciPy/ARPACK. ## Supported Scope -The SciPy backend covers the same core solve families as Rust: +The SciPy solver covers the core solve families: - diagonal scalar or diagonal-anisotropic material grids, - full tensor material grids, - angle and bend transforms that route through the tensorial operator, - PML stretching, - one-dimensional slices, because they are padded into ordinary mode-plane - grids before backend dispatch. - -The remaining difference is operational rather than mathematical: the SciPy -backend depends on SciPy/ARPACK, while the Rust backend is self-contained and -can be selected with `backend="rust"` for deployments that need that property. + grids before solver dispatch. -## What Is Compared +## What Is Tested -The test suite runs representative grids through both backends and checks: +The test suite checks representative grids for: - returned complex effective indices, - sparse operator size and nonzero count, @@ -60,37 +27,28 @@ The test suite runs representative grids through both backends and checks: The covered cases include diagonal grids, PML, full-tensor/off-diagonal grids, and transformed grids. -Run the focused cross-backend check with: +Run the focused solver checks with: ```bash -uv run --extra scipy pytest tests/test_micromode_api.py -k scipy_reference +uv run pytest tests/test_micromode_api.py ``` -Without the SciPy extra installed, the comparison test is skipped and the -default `backend="auto"` path falls back to Rust. - ## Reading The Code The relevant files are: -- `python/micromode/scipy_reference.py`: readable Python/SciPy implementation of - the diagonal and tensorial sparse paths. -- `python/micromode/raster.py`: public backend selection and `Result` wrapping. -- `src/operators.rs`: Rust Maxwell operator assembly. -- `src/eigensolve.rs`: Rust shift-invert Arnoldi and sparse LU path. -- `src/mode_solver.rs`: Rust field reconstruction, normalization, and Lorentz - orthogonalization. +- `python/micromode/scipy_reference.py`: Python/SciPy implementation of the + diagonal and tensorial sparse paths. +- `python/micromode/raster.py`: public solve orchestration and `Result` + wrapping. -The intended trust chain is: inspect the Python/SciPy implementation first, then -run the cross-backend tests to verify that Rust and SciPy agree on the supported -cases where the optional Rust backend matters. +The intended trust chain is: inspect the Python/SciPy implementation, then run +the fixture and API tests to verify behavior on representative mode-solver +cases. ## External References -MicroMode also keeps Tidy3D-oriented examples and fixtures because Tidy3D is a -recognizable reference point for photonics users. Tidy3D's -[public source docs](https://docs.flexcompute.com/projects/tidy3d/en/latest/_modules/tidy3d/components/mode/mode_solver.html) -show that its local mode solver import can fail when SciPy is unavailable, so -Tidy3D-style comparisons are useful as behavioral validation in addition to the -internal Rust-vs-SciPy checks. See the Tidy3D example in `examples/` and the -committed mode-solver fixture harness for existing comparison infrastructure. +MicroMode keeps Tidy3D-oriented examples and fixtures because Tidy3D is a +recognizable reference point for photonics users. See the Tidy3D example in +`examples/` and the committed mode-solver fixture harness for comparison +infrastructure. diff --git a/docs/mode-solver-methods.md b/docs/mode-solver-methods.md index 23a0255..98a383d 100644 --- a/docs/mode-solver-methods.md +++ b/docs/mode-solver-methods.md @@ -2,7 +2,7 @@ `solve_modes(...)` is the main entry point. It validates the `Materials` grid, resolves frequencies or wavelengths, builds the Yee derivative matrices, chooses -the requested backend, solves one frequency at a time, and returns a +the sparse formulation, solves one frequency at a time, and returns a coordinate-aware `Result`. ## Material Grids @@ -30,14 +30,11 @@ coordinate-aware `Result`. - `krylov_dim`: dimension of the Arnoldi search space. - `angle_theta`, `angle_phi`, `bend_radius`, `bend_axis`: transformation-optics controls that update \(\epsilon\) and \(\mu\) before the sparse solve. -- `backend`: `"auto"` by default, choosing SciPy when installed and Rust - otherwise. Use `"scipy"` or `"scipy-reference"` for the Python/SciPy path and - `"rust"` for the optional native backend. ## Eigenpair Selection -The default SciPy backend selects eigenpairs with sparse shift-invert -SciPy/ARPACK [1, 2]. +The SciPy solver selects eigenpairs with sparse shift-invert SciPy/ARPACK [1, +2]. For a matrix \(A\) and shift \(\sigma\), Arnoldi is applied to $$ @@ -47,15 +44,9 @@ $$ $$ where \(\theta\) is a Ritz value of the inverse-shifted operator. The diagonal -backend uses \(\sigma=-\texttt{target_neff}^2\); the tensorial backend uses +formulation uses \(\sigma=-\texttt{target_neff}^2\); the tensorial formulation uses \(\sigma=\texttt{target_neff}\). -The optional `backend="rust"` path assembles the same diagonal or tensorial -sparse operator through the native extension and solves it with MicroMode's -portable shift-invert Arnoldi implementation. It exists for deployments that do -not want a SciPy dependency and as a cross-check against the Python/SciPy path. -Install the recommended default SciPy dependency with `micromode[scipy]`. - Returned modes are sorted by decreasing real effective index, normalized to unit transverse power, diff --git a/docs/physics-model.md b/docs/physics-model.md index 9c36980..51dd378 100644 --- a/docs/physics-model.md +++ b/docs/physics-model.md @@ -37,8 +37,8 @@ finite-difference frequency-domain method on a regular Yee grid [2]. ## Discretization -The Rust kernels use relative material tensors $\epsilon_r(x,y)$, -$\mu_r(x,y)$ and scale transverse derivatives by $1/k_0$, so the sparse +The solver uses relative material tensors $\epsilon_r(x,y)$, +$\mu_r(x,y)$ and scales transverse derivatives by $1/k_0$, so the sparse operators are dimensionless. On the local Yee grid, the four derivative matrices are diff --git a/docs/release.md b/docs/release.md index 699f875..c4d7c73 100644 --- a/docs/release.md +++ b/docs/release.md @@ -23,9 +23,7 @@ Run these before tagging: ```bash uv sync --all-extras -env -u CONDA_PREFIX uv run maturin develop uv run pytest --cov=micromode --cov-report=xml --cov-report=term-missing -cargo test uv build uv run twine check dist/* ./scripts/smoke_dist.sh @@ -58,9 +56,9 @@ python -m venv /tmp/micromode-testpypi ## PyPI Release 1. Update `CHANGELOG.md`. -2. Confirm `pyproject.toml`, `Cargo.toml`, and `CHANGELOG.md` all use the - same new release version. PyPI filenames are immutable, so never reuse a - version that has reached PyPI. +2. Confirm `pyproject.toml` and `CHANGELOG.md` both use the same new release + version. PyPI filenames are immutable, so never reuse a version that has + reached PyPI. 3. Commit the release changes. 4. Tag the release with the same version from `pyproject.toml`: @@ -74,5 +72,5 @@ checks metadata, and publishes to PyPI using Trusted Publishing. ## Current Wheel Scope -The release workflow builds Linux, macOS, and Windows wheels for Python 3.10 -through 3.13. +The release workflow builds a pure-Python wheel and source distribution for +Python 3.10 through 3.13. diff --git a/examples/README.md b/examples/README.md index a5d0021..da37f81 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,7 +18,7 @@ Available cases: - `rib_grid`: slab plus ridge grid. - `circular_rod_grid`: circular dielectric inclusion rasterized on the grid. - `anisotropic_tensor_grid`: full tensor permittivity grid with off-diagonal terms. -- `angled_bent_grid`: diagonal grid solved through the Rust tensorial angle/bend transform path. +- `angled_bent_grid`: diagonal grid solved through the tensorial angle/bend transform path. Render selected cases with: diff --git a/examples/material_grid_demos.py b/examples/material_grid_demos.py index 9df4431..6b47ead 100644 --- a/examples/material_grid_demos.py +++ b/examples/material_grid_demos.py @@ -133,7 +133,7 @@ def grid_demos() -> list[GridDemo]: GridDemo( key="anisotropic_tensor_grid", title="Full Tensor Anisotropic Grid", - description="Diagonal anisotropy plus off-diagonal epsilon terms solved by Rust sparse.", + description="Diagonal anisotropy plus off-diagonal epsilon terms solved by the sparse tensorial path.", make_material_grid=make_anisotropic_tensor_grid, target_neff=2.0, num_modes=1, @@ -142,7 +142,7 @@ def grid_demos() -> list[GridDemo]: GridDemo( key="angled_bent_grid", title="Angled And Bent Grid Solve", - description="A diagonal grid transformed through the Rust tensorial angle/bend path.", + description="A diagonal grid transformed through the tensorial angle/bend path.", make_material_grid=make_strip_grid, target_neff=2.5, num_modes=1, diff --git a/pyproject.toml b/pyproject.toml index a5f566e..3ae5d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "micromode" version = "0.1.0a4" -description = "SciPy-first photonics mode solver with an optional Rust backend." +description = "SciPy-based photonics mode solver with a small Python API." readme = "README.md" requires-python = ">=3.10,<3.14" license = "Apache-2.0" @@ -24,7 +24,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Rust", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", @@ -35,6 +34,7 @@ dependencies = [ "h5py>=3.10,<4.0", "matplotlib>=3.8,<4.0", "numpy>=2.2.6,<2.5.0", + "scipy>=1.11,<2.0", "xarray>=2023.08,<2026.5.0", ] @@ -46,11 +46,7 @@ Examples = "https://github.com/QuentinWach/micromode/tree/main/examples" Changelog = "https://github.com/QuentinWach/micromode/blob/main/CHANGELOG.md" [project.optional-dependencies] -scipy = [ - "scipy>=1.11,<2.0", -] dev = [ - "maturin>=1.7,<2", "packaging>=24.2", "pkginfo>=1.12.1.2", "pyright>=1.1.390,<2", @@ -63,17 +59,17 @@ dev = [ ] [build-system] -requires = ["maturin>=1.7,<2"] -build-backend = "maturin" +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" [tool.uv] package = true -[tool.maturin] -module-name = "micromode._core" -python-source = "python" -features = ["extension-module"] -auditwheel = "skip" +[tool.setuptools.package-dir] +"" = "python" + +[tool.setuptools.packages.find] +where = ["python"] [tool.pytest.ini_options] addopts = "--assert=plain" diff --git a/python/micromode/__init__.py b/python/micromode/__init__.py index a076754..fa3d757 100644 --- a/python/micromode/__init__.py +++ b/python/micromode/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ._rust import C_0, EPSILON_0 +from .constants import C_0, EPSILON_0 from .models import BoundarySpec, Grid, Materials, PmlSpec, Spec from .raster import solve_grid, solve_modes, solve_slice from .result import Result, overlap diff --git a/python/micromode/_rust.py b/python/micromode/_rust.py deleted file mode 100644 index f713f71..0000000 --- a/python/micromode/_rust.py +++ /dev/null @@ -1,253 +0,0 @@ -from __future__ import annotations - -import numpy as np - -# Thin Python wrapper around the PyO3 extension. The public API works with -# NumPy arrays and dictionaries; the extension boundary only accepts plain -# Python lists of real/imag pairs, so the helpers below perform that conversion -# in one place. - -C_0 = 2.997_924_58e14 -EPSILON_0 = 8.854187812800384e-18 -SparseSolveResult = tuple[np.ndarray, list[np.ndarray], dict[str, object]] - - -try: - from ._core import ( - solve_diagonal_sparse_py as _solve_diagonal_sparse, - ) - from ._core import ( - solve_tensorial_sparse_py as _solve_tensorial_sparse, - ) -except Exception: # pragma: no cover - exercised when extension is not built locally. - _solve_diagonal_sparse = None - _solve_tensorial_sparse = None - - -def solve_diagonal_sparse( - *, - eps_tensor: np.ndarray, - mu_tensor: np.ndarray, - dlf: tuple[np.ndarray, np.ndarray], - dlb: tuple[np.ndarray, np.ndarray], - num_modes: int, - neff_guess: float, - direction: str, - derivative_scale: float | None = None, - omega: float | None = None, - num_pml: tuple[int, int] = (0, 0), - pml_profile: dict[str, float | int] | None = None, - dmin_pml: tuple[bool, bool] = (True, True), - dmin_pmc: tuple[bool, bool] = (False, False), - krylov_dim: int = 32, - initial_vector: np.ndarray | None = None, -) -> SparseSolveResult: - """Run the Rust sparse diagonal mode solver for a prepared 2D Yee grid.""" - # This is the normal path for diagonal material grids. The returned fields - # are still flattened by mode/component; `raster.py` restores grid axes. - if _solve_diagonal_sparse is None: - raise RuntimeError("Rust extension is not built; sparse diagonal solver is unavailable") - if eps_tensor.shape != mu_tensor.shape or eps_tensor.shape[:2] != (3, 3): - raise ValueError("eps_tensor and mu_tensor must both have shape (3, 3, N)") - nx = len(dlf[0]) - ny = len(dlf[1]) - expected_n = nx * ny - if eps_tensor.shape[-1] != expected_n: - raise ValueError("tensor length must match len(dlf[0]) * len(dlf[1])") - - pml_profile = _default_pml_profile(pml_profile) - ( - n_pairs, - field_pairs, - residuals, - power_norms, - lorentz_norms, - lorentz_orthogonality_error, - backend, - operator_size, - operator_nnz, - ) = _solve_diagonal_sparse( - nx, - ny, - dlf[0].astype(float).tolist(), - dlf[1].astype(float).tolist(), - dlb[0].astype(float).tolist(), - dlb[1].astype(float).tolist(), - dmin_pmc[0], - dmin_pmc[1], - _tensor_payload(eps_tensor), - _tensor_payload(mu_tensor), - num_modes, - neff_guess, - direction, - derivative_scale, - int(num_pml[0]), - int(num_pml[1]), - float(pml_profile["sigma_max"]), - float(pml_profile["kappa_min"]), - float(pml_profile["kappa_max"]), - int(pml_profile["order"]), - bool(dmin_pml[0]), - bool(dmin_pml[1]), - omega, - int(krylov_dim), - None if initial_vector is None else _complex_vector_payload(initial_vector), - ) - n_complex = _pairs_to_complex(n_pairs) - fields = [_pairs_to_complex(component) for component in field_pairs] - return ( - n_complex, - fields, - _solver_info( - residuals, - power_norms, - lorentz_norms, - lorentz_orthogonality_error, - backend, - operator_size, - operator_nnz, - ), - ) - - -def solve_tensorial_sparse( - *, - eps_tensor: np.ndarray, - mu_tensor: np.ndarray, - dlf: tuple[np.ndarray, np.ndarray], - dlb: tuple[np.ndarray, np.ndarray], - num_modes: int, - neff_guess: float, - direction: str, - derivative_scale: float | None = None, - omega: float | None = None, - num_pml: tuple[int, int] = (0, 0), - pml_profile: dict[str, float | int] | None = None, - dmin_pml: tuple[bool, bool] = (True, True), - dmin_pmc: tuple[bool, bool] = (False, False), - krylov_dim: int = 32, - initial_vector: np.ndarray | None = None, -) -> SparseSolveResult: - """Run the Rust sparse full-tensor mode solver for a prepared 2D Yee grid.""" - # Full tensor material grids, angled solves, and bent solves use this path - # because coordinate transforms can introduce off-diagonal eps/mu terms. - if _solve_tensorial_sparse is None: - raise RuntimeError("Rust extension is not built; sparse tensorial solver is unavailable") - if eps_tensor.shape != mu_tensor.shape or eps_tensor.shape[:2] != (3, 3): - raise ValueError("eps_tensor and mu_tensor must both have shape (3, 3, N)") - nx = len(dlf[0]) - ny = len(dlf[1]) - expected_n = nx * ny - if eps_tensor.shape[-1] != expected_n: - raise ValueError("tensor length must match len(dlf[0]) * len(dlf[1])") - - pml_profile = _default_pml_profile(pml_profile) - ( - n_pairs, - field_pairs, - residuals, - power_norms, - lorentz_norms, - lorentz_orthogonality_error, - backend, - operator_size, - operator_nnz, - ) = _solve_tensorial_sparse( - nx, - ny, - dlf[0].astype(float).tolist(), - dlf[1].astype(float).tolist(), - dlb[0].astype(float).tolist(), - dlb[1].astype(float).tolist(), - dmin_pmc[0], - dmin_pmc[1], - _tensor_payload(eps_tensor), - _tensor_payload(mu_tensor), - num_modes, - neff_guess, - direction, - derivative_scale, - int(num_pml[0]), - int(num_pml[1]), - float(pml_profile["sigma_max"]), - float(pml_profile["kappa_min"]), - float(pml_profile["kappa_max"]), - int(pml_profile["order"]), - bool(dmin_pml[0]), - bool(dmin_pml[1]), - omega, - int(krylov_dim), - None if initial_vector is None else _complex_vector_payload(initial_vector), - ) - n_complex = _pairs_to_complex(n_pairs) - fields = [_pairs_to_complex(component) for component in field_pairs] - return ( - n_complex, - fields, - _solver_info( - residuals, - power_norms, - lorentz_norms, - lorentz_orthogonality_error, - backend, - operator_size, - operator_nnz, - ), - ) - - -def _default_pml_profile(profile: dict[str, float | int] | None) -> dict[str, float | int]: - defaults: dict[str, float | int] = { - "sigma_max": 2.0, - "kappa_min": 1.0, - "kappa_max": 3.0, - "order": 3, - } - if profile is not None: - defaults.update(profile) - return defaults - - -def _solver_info( - residuals, - power_norms, - lorentz_norms, - lorentz_orthogonality_error, - backend, - operator_size, - operator_nnz, -) -> dict[str, object]: - # Keep raw backend diagnostics close to the extension boundary. Higher-level - # context such as grid shape, PML, and normalization labels is added in - # `raster.py`. - return { - "backend": str(backend), - "operator_size": int(operator_size), - "operator_nnz": int(operator_nnz), - "residuals": np.asarray(residuals, dtype=float), - "power_norms": np.asarray(power_norms, dtype=float), - "lorentz_norms": _pairs_to_complex(lorentz_norms), - "lorentz_orthogonality_error": float(lorentz_orthogonality_error), - } - - -def _tensor_payload(tensor: np.ndarray) -> list[list[tuple[float, float]]]: - # PyO3 can accept nested Python lists reliably across build targets. The - # tensor is flattened as nine component vectors: xx, xy, xz, yx, ... zz. - tensor = np.asarray(tensor) - return [ - [(complex(value).real, complex(value).imag) for value in tensor[row, col, :]] - for row in range(3) - for col in range(3) - ] - - -def _complex_vector_payload(vector: np.ndarray) -> list[tuple[float, float]]: - # Initial vectors are complex NumPy arrays in Python but real/imag tuples at - # the Rust boundary. - return [(complex(value).real, complex(value).imag) for value in np.asarray(vector).reshape(-1)] - - -def _pairs_to_complex(values) -> np.ndarray: - array = np.asarray(values, dtype=float) - return array[..., 0] + 1j * array[..., 1] diff --git a/python/micromode/constants.py b/python/micromode/constants.py new file mode 100644 index 0000000..b6af4f9 --- /dev/null +++ b/python/micromode/constants.py @@ -0,0 +1,4 @@ +"""Physical constants used by MicroMode.""" + +C_0 = 2.997_924_58e14 +EPSILON_0 = 8.854_187_812_800_384e-18 diff --git a/python/micromode/models.py b/python/micromode/models.py index 0e8838d..e4aaa76 100644 --- a/python/micromode/models.py +++ b/python/micromode/models.py @@ -528,7 +528,7 @@ def _assign_tensor_offdiagonal( @dataclass(frozen=True) class Spec: - """Mode solver options for backend-backed grid solves.""" + """Mode solver options for grid solves.""" num_modes: int = 1 target_neff: float | None = None diff --git a/python/micromode/raster.py b/python/micromode/raster.py index f30be27..7d01c37 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -2,21 +2,18 @@ from __future__ import annotations -import importlib.util from collections.abc import Sequence from typing import Literal, cast import numpy as np import xarray as xr -from ._rust import C_0, solve_diagonal_sparse, solve_tensorial_sparse +from .constants import C_0 from .models import BoundaryCondition, BoundarySpec, Materials, PmlSpec, SliceAxis, Spec from .result import Result from .scipy_reference import solve_diagonal_scipy_reference, solve_tensorial_scipy_reference _COMPONENTS = ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz") -Backend = Literal["auto", "scipy", "scipy-reference", "rust"] -_ResolvedBackend = Literal["scipy-reference", "rust"] def solve_grid( @@ -56,7 +53,6 @@ def solve_grid( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, - backend: Backend = "auto", spec: Spec | None = None, ) -> Result: """Solve modes from rasterized material components and grid edges. @@ -104,7 +100,6 @@ def solve_grid( angle_phi=angle_phi, bend_radius=bend_radius, bend_axis=bend_axis, - backend=backend, spec=spec, ) @@ -148,14 +143,13 @@ def solve_slice( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, - backend: Backend = "auto", spec: Spec | None = None, ) -> Result: """Solve modes from a one-dimensional mode-plane material slice. This is the convenience API for 2D FDTD simulations. The supplied material arrays vary along one mode-plane axis and MicroMode inserts a single - invariant cell along the other axis before using the same backend solve path + invariant cell along the other axis before using the same sparse solve path as ``solve_modes``. """ @@ -200,7 +194,6 @@ def solve_slice( angle_phi=angle_phi, bend_radius=bend_radius, bend_axis=bend_axis, - backend=backend, spec=spec, ) @@ -221,19 +214,16 @@ def solve_modes( angle_phi: float = 0.0, bend_radius: float | None = None, bend_axis: Literal[0, 1] = 0, - backend: Backend = "auto", spec: Spec | None = None, ) -> Result: """Solve modes for an already-rasterized material tensor grid. This is the preferred Beamz integration point. Beamz owns geometry and - material rasterization; MicroMode owns the sparse mode solve and field - reconstruction on the supplied grid. By default, MicroMode uses the - Python/SciPy backend when SciPy is installed and falls back to the Rust - backend otherwise. + material rasterization; MicroMode owns the sparse SciPy mode solve and field + reconstruction on the supplied grid. """ # Main solver orchestration layer. It validates user-facing grid objects, - # solves one frequency at a time, then wraps flattened backend outputs into + # solves one frequency at a time, then wraps flattened solver outputs into # coordinate-aware xarray arrays. if not isinstance(material_grid, Materials): raise TypeError("material_grid must be a Materials") @@ -256,7 +246,6 @@ def solve_modes( raise ValueError("num_modes must be positive") if direction not in {"+", "-"}: raise ValueError("direction must be '+' or '-'") - backend = _resolve_backend(backend) pml_spec = _resolve_pml_spec(pml) boundary_spec = _resolve_boundary_spec(boundary) if bend_radius is not None and np.isclose(bend_radius, 0.0): @@ -288,10 +277,9 @@ def solve_modes( angle_phi=float(angle_phi), bend_radius=None if bend_radius is None else float(bend_radius), bend_axis=int(bend_axis), - backend=backend, material_grid=material_grid, ) - # Backends solve in local coordinates where local z is the propagation + # The solver uses local coordinates where local z is the propagation # normal. Convert field labels back to the global x/y/z axes requested # by the material grid before exposing them. fields = _local_fields_to_global(fields, normal_axis=material_grid.grid.normal_axis) @@ -340,7 +328,6 @@ def _solve_one_frequency( angle_phi: float, bend_radius: float | None, bend_axis: int, - backend: _ResolvedBackend, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: # Forward and backward spacings represent the local Yee grid. The derivative # builders need both because E and H components are staggered. @@ -370,21 +357,7 @@ def _solve_one_frequency( ) if not (_is_diagonal_tensor(eps_tensor) and _is_diagonal_tensor(mu_tensor)): # Off-diagonal components require the tensorial first-order operator. - if backend == "scipy-reference": - return _solve_one_frequency_scipy_tensorial_reference( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - freq=freq, - num_modes=num_modes, - target_neff=target_neff, - pml_spec=pml_spec, - direction=direction, - krylov_dim=krylov_dim, - boundary_spec=boundary_spec, - ) - return _solve_one_frequency_rust_tensorial_sparse( + return _solve_one_frequency_scipy_tensorial( eps_tensor=eps_tensor, mu_tensor=mu_tensor, dlf=dlf, @@ -399,21 +372,7 @@ def _solve_one_frequency( ) # If the transformed tensors remain diagonal, keep the faster diagonal # sparse formulation. - if backend == "scipy-reference": - return _solve_one_frequency_scipy_reference( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - freq=freq, - num_modes=num_modes, - target_neff=target_neff, - pml_spec=pml_spec, - direction=direction, - krylov_dim=krylov_dim, - boundary_spec=boundary_spec, - ) - return _solve_one_frequency_rust_sparse( + return _solve_one_frequency_scipy( eps_tensor=eps_tensor, mu_tensor=mu_tensor, dlf=dlf, @@ -428,21 +387,7 @@ def _solve_one_frequency( ) if not is_diagonal: # User supplied a full tensor grid with no coordinate transform. - if backend == "scipy-reference": - return _solve_one_frequency_scipy_tensorial_reference( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - freq=freq, - num_modes=num_modes, - target_neff=target_neff, - pml_spec=pml_spec, - direction=direction, - krylov_dim=krylov_dim, - boundary_spec=boundary_spec, - ) - return _solve_one_frequency_rust_tensorial_sparse( + return _solve_one_frequency_scipy_tensorial( eps_tensor=eps_tensor, mu_tensor=mu_tensor, dlf=dlf, @@ -456,21 +401,7 @@ def _solve_one_frequency( boundary_spec=boundary_spec, ) # Ordinary scalar/diagonal grids use the diagonal sparse formulation. - if backend == "scipy-reference": - return _solve_one_frequency_scipy_reference( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - freq=freq, - num_modes=num_modes, - target_neff=target_neff, - pml_spec=pml_spec, - direction=direction, - krylov_dim=krylov_dim, - boundary_spec=boundary_spec, - ) - return _solve_one_frequency_rust_sparse( + return _solve_one_frequency_scipy( eps_tensor=eps_tensor, mu_tensor=mu_tensor, dlf=dlf, @@ -485,53 +416,7 @@ def _solve_one_frequency( ) -def _solve_one_frequency_rust_sparse( - *, - eps_tensor: np.ndarray, - mu_tensor: np.ndarray, - dlf: tuple[np.ndarray, np.ndarray], - dlb: tuple[np.ndarray, np.ndarray], - freq: float, - num_modes: int, - target_neff: float, - pml_spec: PmlSpec, - direction: str, - krylov_dim: int | None, - boundary_spec: BoundarySpec, -) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: - nx = len(dlf[0]) - ny = len(dlf[1]) - actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) - n_complex, fields, solver_info = solve_diagonal_sparse( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - num_modes=num_modes, - neff_guess=target_neff, - direction=direction, - derivative_scale=C_0 / (2 * np.pi * freq), - omega=2 * np.pi * freq, - num_pml=pml_spec.num_cells, - pml_profile=pml_spec.profile_dict(), - dmin_pml=boundary_spec.dmin_pml, - dmin_pmc=boundary_spec.dmin_pmc, - krylov_dim=actual_krylov_dim, - initial_vector=_default_initial_vector(2 * nx * ny, shape=(nx, ny)), - ) - return ( - n_complex, - _fields_to_grid(fields, (nx, ny)), - _solver_info_with_context( - solver_info, - backend_kind="diagonal_sparse", - shape=(nx, ny), - krylov_dim=actual_krylov_dim, - ), - ) - - -def _solve_one_frequency_scipy_reference( +def _solve_one_frequency_scipy( *, eps_tensor: np.ndarray, mu_tensor: np.ndarray, @@ -577,7 +462,7 @@ def _solve_one_frequency_scipy_reference( ) -def _solve_one_frequency_scipy_tensorial_reference( +def _solve_one_frequency_scipy_tensorial( *, eps_tensor: np.ndarray, mu_tensor: np.ndarray, @@ -622,53 +507,6 @@ def _solve_one_frequency_scipy_tensorial_reference( ), ) - -def _solve_one_frequency_rust_tensorial_sparse( - *, - eps_tensor: np.ndarray, - mu_tensor: np.ndarray, - dlf: tuple[np.ndarray, np.ndarray], - dlb: tuple[np.ndarray, np.ndarray], - freq: float, - num_modes: int, - target_neff: float, - pml_spec: PmlSpec, - direction: str, - krylov_dim: int | None, - boundary_spec: BoundarySpec, -) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: - nx = len(dlf[0]) - ny = len(dlf[1]) - actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) - n_complex, fields, solver_info = solve_tensorial_sparse( - eps_tensor=eps_tensor, - mu_tensor=mu_tensor, - dlf=dlf, - dlb=dlb, - num_modes=num_modes, - neff_guess=target_neff, - direction=direction, - derivative_scale=C_0 / (2 * np.pi * freq), - omega=2 * np.pi * freq, - num_pml=pml_spec.num_cells, - pml_profile=pml_spec.profile_dict(), - dmin_pml=boundary_spec.dmin_pml, - dmin_pmc=boundary_spec.dmin_pmc, - krylov_dim=actual_krylov_dim, - initial_vector=_default_initial_vector(4 * nx * ny, shape=(nx, ny)), - ) - return ( - n_complex, - _fields_to_grid(fields, (nx, ny)), - _solver_info_with_context( - solver_info, - backend_kind="tensorial_sparse", - shape=(nx, ny), - krylov_dim=actual_krylov_dim, - ), - ) - - def _transformed_material_tensors( eps: np.ndarray, mu: np.ndarray, @@ -756,20 +594,6 @@ def _resolve_boundary_spec( return BoundarySpec(low=cast(tuple[BoundaryCondition, BoundaryCondition], boundary)) -def _resolve_backend(backend: Backend) -> _ResolvedBackend: - if backend == "auto": - return "scipy-reference" if _scipy_available() else "rust" - if backend == "scipy": - return "scipy-reference" - if backend in {"scipy-reference", "rust"}: - return backend - raise ValueError("backend must be 'auto', 'scipy', 'scipy-reference', or 'rust'") - - -def _scipy_available() -> bool: - return importlib.util.find_spec("scipy") is not None - - def _solver_info_with_context( solver_info: dict[str, object], *, @@ -777,8 +601,8 @@ def _solver_info_with_context( shape: tuple[int, int], krylov_dim: int, ) -> dict[str, object]: - # Backends report backend-local data. Add enough Python-side context for - # saved Result files and benchmark reports to be self-describing. + # Add enough Python-side context for saved Result files and benchmark + # reports to be self-describing. out = dict(solver_info) out["backend_kind"] = backend_kind out["shape"] = shape @@ -846,7 +670,7 @@ def _fields_to_grid(fields: list[np.ndarray], shape: tuple[int, int]) -> dict[st def _local_fields_to_global(fields: dict[str, np.ndarray], *, normal_axis: int) -> dict[str, np.ndarray]: """Map solver-local field components onto global x/y/z component names. - The Rust kernels solve in local coordinates where local z is the + The SciPy solver uses local coordinates where local z is the propagation-normal axis. For x- or y-normal planes, component labels must be permuted back to global coordinates before returning a Result. """ diff --git a/python/micromode/result.py b/python/micromode/result.py index 7b3c04c..a9d05db 100644 --- a/python/micromode/result.py +++ b/python/micromode/result.py @@ -12,7 +12,7 @@ import numpy as np import xarray as xr -from ._rust import C_0 +from .constants import C_0 _SPATIAL_DIMS = ("x", "y", "z") _E_COMPONENTS = ("Ex", "Ey", "Ez") @@ -236,7 +236,7 @@ def overlap( ``kind="power"`` computes the transverse power-product ``integral((E_a x H_b*) . n) dA``. ``kind="lorentz"`` computes the - unconjugated reciprocal product used by the Rust orthogonalization + unconjugated reciprocal product used by the solver orthogonalization pass. ``kind="electric"`` computes a simpler electric-field inner product for mode tracking. All overlap kinds use the mode-plane cell widths as integration weights and require matching result grids. @@ -533,7 +533,7 @@ def _overlap_value( # Power and Lorentz overlaps require the complete six-component mode. Power # uses H* and measures physical flux. Lorentz deliberately does not # conjugate either mode; it is the reciprocal product used to orthogonalize - # the mode set in Rust. + # the mode set. missing = [ component for component in (*_E_COMPONENTS, *_H_COMPONENTS) if component not in left or component not in right ] diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 7ce0eed..3ed8511 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -1,15 +1,15 @@ """Readable SciPy implementation for the mode solver. -This module assembles the sparse mode-solver paths in plain Python/SciPy. It is -the preferred backend when SciPy is installed because it keeps the numerical -contract inspectable by users who want to audit the finite-difference operators -against SciPy/ARPACK. +This module assembles the sparse mode-solver paths in plain Python/SciPy. It +keeps the numerical contract inspectable by users who want to audit the +finite-difference operators against SciPy/ARPACK. """ from __future__ import annotations import warnings from dataclasses import dataclass +from typing import Any, cast import numpy as np @@ -59,8 +59,8 @@ def solve_diagonal_scipy_reference( ) -> SparseSolveResult: """Solve the diagonal sparse eigenproblem with SciPy/ARPACK. - This is the same reduced ``[Ex, Ey]`` transverse eigenproblem used by the - Rust backend for diagonal material tensors. + This is the reduced ``[Ex, Ey]`` transverse eigenproblem for diagonal + material tensors. """ sparse, spla, scipy_linalg = _import_scipy() @@ -87,7 +87,7 @@ def solve_diagonal_scipy_reference( scale=float(derivative_scale), ) operators = _assemble_diagonal_operators(sparse, eps_tensor, mu_tensor, derivatives) - operator = operators["mat"] + operator = cast(Any, operators["mat"]) eig_guess = complex(-(neff_guess * neff_guess), 0.0) operator, arpack_initial_vector, arpack_guess = _real_arpack_problem_if_close( operator, initial_vector, eig_guess @@ -300,7 +300,7 @@ def _import_scipy(): import scipy.sparse as sparse import scipy.sparse.linalg as spla except ImportError as exc: # pragma: no cover - depends on optional extra. - raise ImportError("the SciPy backend requires `pip install micromode[scipy]`") from exc + raise ImportError("the SciPy solver requires `pip install micromode` with SciPy installed") from exc return sparse, spla, scipy_linalg diff --git a/scripts/check_dist_artifacts.py b/scripts/check_dist_artifacts.py index 6b879e1..9501c7d 100644 --- a/scripts/check_dist_artifacts.py +++ b/scripts/check_dist_artifacts.py @@ -28,6 +28,11 @@ def main() -> None: metavar="PREFIX", help="Require at least one wheel platform tag with each prefix, for example macosx or manylinux", ) + parser.add_argument( + "--allow-pure-python", + action="store_true", + help="Allow a py3-none-any wheel instead of platform-specific wheels.", + ) args = parser.parse_args() dist = Path(args.dist) @@ -45,6 +50,10 @@ def main() -> None: platform = match["platform"] python_tag = match["python"] platform_tags.update(platform.split(".")) + if args.allow_pure_python and python_tag == "py3" and match["abi"] == "none" and platform == "any": + python_tags.update(args.require_cpython) + platform_tags.update(args.require_platform) + continue if python_tag.startswith("cp") and python_tag[2:].isdigit(): version_digits = python_tag[2:] python_tags.add(f"3.{version_digits[1:]}") diff --git a/scripts/check_release_metadata.py b/scripts/check_release_metadata.py index 4ccd248..469d21c 100644 --- a/scripts/check_release_metadata.py +++ b/scripts/check_release_metadata.py @@ -17,27 +17,18 @@ def main() -> None: pyproject = load_toml("pyproject.toml") - cargo = load_toml("Cargo.toml") project = pyproject["project"] - package = cargo["package"] publish_workflow = (ROOT / ".github/workflows/publish.yml").read_text(encoding="utf-8") require(project["name"] == "micromode", "Python package name must be micromode") require(project["version"] != "0.0.0", "Python package version must not be 0.0.0") - require( - package["version"] == python_to_cargo_version(project["version"]), - "Rust crate version must match the Python package version", - ) require(project["requires-python"] == ">=3.10,<3.14", "Python support must match the release wheel matrix") - require(package["version"] != "0.0.0", "Rust crate version must not be 0.0.0") require(project["license"] == "Apache-2.0", "Python package license must be Apache-2.0") - require(package["license"] == "Apache-2.0", "Rust crate license must be Apache-2.0") require((ROOT / "LICENSE").exists(), "LICENSE file is missing") require((ROOT / "CHANGELOG.md").exists(), "CHANGELOG.md is missing") require(project.get("authors"), "project.authors is missing") require(project.get("classifiers"), "project.classifiers is missing") require(project.get("urls"), "project.urls is missing") - require(package.get("repository"), "Cargo repository is missing") require((ROOT / ".github/workflows/publish.yml").exists(), "publish workflow is missing") require((ROOT / ".github/workflows/tests.yml").exists(), "tests workflow is missing") require((ROOT / "scripts/smoke_wheel.py").exists(), "wheel smoke test is missing") @@ -46,13 +37,7 @@ def main() -> None: f'"{version}"' in publish_workflow, f"publish workflow is missing a Python {version} wheel build", ) - require("--compatibility pypi" in publish_workflow, "publish workflow must request PyPI-compatible wheels") - require("--auditwheel repair" in publish_workflow, "Linux release wheels must be auditwheel-repaired") - require("windows-latest" in publish_workflow, "publish workflow is missing Windows wheel builds") - require( - "--require-platform macosx manylinux win" in publish_workflow, - "release artifact check must require macOS, manylinux, and Windows wheels", - ) + require("--allow-pure-python" in publish_workflow, "release artifact check must allow pure-Python wheels") changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") require(project["version"] in changelog, "Python version is not mentioned in CHANGELOG.md") @@ -67,17 +52,6 @@ def load_toml(path: str) -> dict: return tomllib.load(handle) -def python_to_cargo_version(version: str) -> str: - match = re.fullmatch(r"(\d+\.\d+\.\d+)(?:(a|b|rc)(\d+))?", version) - if match is None: - raise ValueError(f"Python version {version!r} is not a supported release version") - base, phase, number = match.groups() - if phase is None: - return base - phase_names = {"a": "alpha", "b": "beta", "rc": "rc"} - return f"{base}-{phase_names[phase]}.{number}" - - def require_tag_matches_version(version: str) -> None: if os.environ.get("GITHUB_REF_TYPE") != "tag": return diff --git a/scripts/smoke_dist.py b/scripts/smoke_dist.py index 3a249b5..f44f3b0 100644 --- a/scripts/smoke_dist.py +++ b/scripts/smoke_dist.py @@ -37,16 +37,18 @@ def main() -> None: def latest_wheel(required_tag: str) -> Path: - wheels = sorted( - (ROOT / "dist").glob(f"micromode-*-{required_tag}-*.whl"), - key=lambda path: path.stat().st_mtime, - reverse=True, - ) + wheels = sorted((ROOT / "dist").glob(f"micromode-*-{required_tag}-*.whl"), key=_mtime, reverse=True) + if not wheels: + wheels = sorted((ROOT / "dist").glob("micromode-*-py3-none-any.whl"), key=_mtime, reverse=True) if not wheels: - raise SystemExit(f"no {required_tag} wheel found in dist/") + raise SystemExit(f"no {required_tag} or py3-none-any wheel found in dist/") return wheels[0].resolve() +def _mtime(path: Path) -> float: + return path.stat().st_mtime + + def python_tag(python: str) -> str: command = [ python, diff --git a/src/derivatives.rs b/src/derivatives.rs deleted file mode 100644 index 2d321fa..0000000 --- a/src/derivatives.rs +++ /dev/null @@ -1,522 +0,0 @@ -use num_complex::Complex64; - -use crate::sparse_matrix::SparseMatrix; - -pub const C0: f64 = 2.997_924_58e14; -pub const MU0: f64 = 1.256_637_062_12e-12; -pub const EPSILON0: f64 = 1.0 / (MU0 * C0 * C0); -pub const ETA0: f64 = 376.730_313_666_853_5; - -pub type Tensor3 = [[Vec; 3]; 3]; - -#[derive(Clone, Debug)] -pub struct PmlProfile { - pub sigma_max: f64, - pub kappa_min: f64, - pub kappa_max: f64, - pub order: i32, -} - -impl Default for PmlProfile { - fn default() -> Self { - Self { - sigma_max: 2.0, - kappa_min: 1.0, - kappa_max: 3.0, - order: 3, - } - } -} - -pub fn make_dxf_sparse(dls: &[f64], shape: (usize, usize), pmc: bool) -> SparseMatrix { - let (nx, ny) = shape; - if nx == 1 { - return SparseMatrix::zeros(ny, ny); - } - let mut triplets = Vec::new(); - for ix in 0..nx { - for iy in 0..ny { - let row = ix * ny + iy; - let scale = 1.0 / dls[ix]; - let diagonal = if ix == 0 && !pmc { 0.0 } else { -scale }; - if diagonal != 0.0 { - triplets.push((row, row, Complex64::new(diagonal, 0.0))); - } - if ix + 1 < nx { - let col = (ix + 1) * ny + iy; - triplets.push((row, col, Complex64::new(scale, 0.0))); - } - } - } - SparseMatrix::from_triplets(nx * ny, nx * ny, triplets) -} - -pub fn make_dxb_sparse(dls: &[f64], shape: (usize, usize), pmc: bool) -> SparseMatrix { - let (nx, ny) = shape; - if nx == 1 { - return SparseMatrix::zeros(ny, ny); - } - let mut triplets = Vec::new(); - for ix in 0..nx { - for iy in 0..ny { - let row = ix * ny + iy; - let scale = 1.0 / dls[ix]; - let diagonal = if ix == 0 { - if pmc { - 2.0 * scale - } else { - 0.0 - } - } else { - scale - }; - if diagonal != 0.0 { - triplets.push((row, row, Complex64::new(diagonal, 0.0))); - } - if ix > 0 { - let col = (ix - 1) * ny + iy; - triplets.push((row, col, Complex64::new(-scale, 0.0))); - } - } - } - SparseMatrix::from_triplets(nx * ny, nx * ny, triplets) -} - -pub fn make_dyf_sparse(dls: &[f64], shape: (usize, usize), pmc: bool) -> SparseMatrix { - let (nx, ny) = shape; - if ny == 1 { - return SparseMatrix::zeros(nx, nx); - } - let mut triplets = Vec::new(); - for ix in 0..nx { - for iy in 0..ny { - let row = ix * ny + iy; - let scale = 1.0 / dls[iy]; - let diagonal = if iy == 0 && !pmc { 0.0 } else { -scale }; - if diagonal != 0.0 { - triplets.push((row, row, Complex64::new(diagonal, 0.0))); - } - if iy + 1 < ny { - let col = ix * ny + iy + 1; - triplets.push((row, col, Complex64::new(scale, 0.0))); - } - } - } - SparseMatrix::from_triplets(nx * ny, nx * ny, triplets) -} - -pub fn make_dyb_sparse(dls: &[f64], shape: (usize, usize), pmc: bool) -> SparseMatrix { - let (nx, ny) = shape; - if ny == 1 { - return SparseMatrix::zeros(nx, nx); - } - let mut triplets = Vec::new(); - for ix in 0..nx { - for iy in 0..ny { - let row = ix * ny + iy; - let scale = 1.0 / dls[iy]; - let diagonal = if iy == 0 { - if pmc { - 2.0 * scale - } else { - 0.0 - } - } else { - scale - }; - if diagonal != 0.0 { - triplets.push((row, row, Complex64::new(diagonal, 0.0))); - } - if iy > 0 { - let col = ix * ny + iy - 1; - triplets.push((row, col, Complex64::new(-scale, 0.0))); - } - } - } - SparseMatrix::from_triplets(nx * ny, nx * ny, triplets) -} - -pub fn create_d_matrices_sparse( - shape: (usize, usize), - dlf: (&[f64], &[f64]), - dlb: (&[f64], &[f64]), - dmin_pmc: (bool, bool), -) -> [SparseMatrix; 4] { - [ - make_dxf_sparse(dlf.0, shape, dmin_pmc.0), - make_dxb_sparse(dlb.0, shape, dmin_pmc.0), - make_dyf_sparse(dlf.1, shape, dmin_pmc.1), - make_dyb_sparse(dlb.1, shape, dmin_pmc.1), - ] -} - -pub fn create_s_matrices_sparse( - omega: f64, - shape: (usize, usize), - npml: (usize, usize), - dlf: (&[f64], &[f64]), - dlb: (&[f64], &[f64]), - eps_tensor: &Tensor3, - mu_tensor: &Tensor3, - dmin_pml: (bool, bool), -) -> [SparseMatrix; 4] { - create_s_matrices_sparse_with_profile( - omega, - shape, - npml, - dlf, - dlb, - eps_tensor, - mu_tensor, - dmin_pml, - &PmlProfile::default(), - ) -} - -#[allow(clippy::too_many_arguments)] -pub fn create_s_matrices_sparse_with_profile( - omega: f64, - shape: (usize, usize), - npml: (usize, usize), - dlf: (&[f64], &[f64]), - dlb: (&[f64], &[f64]), - eps_tensor: &Tensor3, - mu_tensor: &Tensor3, - dmin_pml: (bool, bool), - profile: &PmlProfile, -) -> [SparseMatrix; 4] { - create_s_diagonal_values( - omega, shape, npml, dlf, dlb, eps_tensor, mu_tensor, dmin_pml, profile, - ) - .map(|values| SparseMatrix::diagonal(&values)) -} - -#[allow(clippy::too_many_arguments)] -pub fn create_s_diagonal_values( - omega: f64, - shape: (usize, usize), - npml: (usize, usize), - dlf: (&[f64], &[f64]), - dlb: (&[f64], &[f64]), - eps_tensor: &Tensor3, - mu_tensor: &Tensor3, - dmin_pml: (bool, bool), - profile: &PmlProfile, -) -> [Vec; 4] { - let (nx, ny) = shape; - let n = nx * ny; - let avg_speed = average_relative_speed(shape, npml, eps_tensor, mu_tensor); - - let sx_f = create_sfactor( - "f", - omega, - dlf.0, - nx, - npml.0, - dmin_pml.0, - (avg_speed[0], avg_speed[1]), - profile, - ); - let sx_b = create_sfactor( - "b", - omega, - dlb.0, - nx, - npml.0, - dmin_pml.0, - (avg_speed[0], avg_speed[1]), - profile, - ); - let sy_f = create_sfactor( - "f", - omega, - dlf.1, - ny, - npml.1, - dmin_pml.1, - (avg_speed[2], avg_speed[3]), - profile, - ); - let sy_b = create_sfactor( - "b", - omega, - dlb.1, - ny, - npml.1, - dmin_pml.1, - (avg_speed[2], avg_speed[3]), - profile, - ); - - let mut sx_f_vec = vec![Complex64::new(0.0, 0.0); n]; - let mut sx_b_vec = vec![Complex64::new(0.0, 0.0); n]; - let mut sy_f_vec = vec![Complex64::new(0.0, 0.0); n]; - let mut sy_b_vec = vec![Complex64::new(0.0, 0.0); n]; - - for ix in 0..nx { - for iy in 0..ny { - let index = ix * ny + iy; - sx_f_vec[index] = Complex64::new(1.0, 0.0) / sx_f[ix]; - sx_b_vec[index] = Complex64::new(1.0, 0.0) / sx_b[ix]; - sy_f_vec[index] = Complex64::new(1.0, 0.0) / sy_f[iy]; - sy_b_vec[index] = Complex64::new(1.0, 0.0) / sy_b[iy]; - } - } - - [sx_f_vec, sx_b_vec, sy_f_vec, sy_b_vec] -} - -pub fn average_relative_speed( - shape: (usize, usize), - npml: (usize, usize), - eps_tensor: &Tensor3, - mu_tensor: &Tensor3, -) -> [Complex64; 4] { - let eps_avg = pml_average_all_sides(shape, npml, eps_tensor); - let mu_avg = pml_average_all_sides(shape, npml, mu_tensor); - let mut out = [Complex64::new(1.0, 0.0); 4]; - for i in 0..4 { - out[i] = Complex64::new(1.0, 0.0) / (eps_avg[i] * mu_avg[i]).sqrt(); - } - out -} - -fn pml_average_all_sides( - shape: (usize, usize), - npml: (usize, usize), - tensor: &Tensor3, -) -> [Complex64; 4] { - let (nx, ny) = shape; - let mut regions = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; - for comp in 0..3 { - for ix in 0..nx { - for iy in 0..ny { - let value = tensor[comp][comp][ix * ny + iy]; - if ix < npml.0 { - regions[0].push(value); - } - if ix >= nx.saturating_sub(npml.0).saturating_add(1) { - regions[1].push(value); - } - if iy < npml.1 { - regions[2].push(value); - } - if iy >= ny.saturating_sub(npml.1).saturating_add(1) { - regions[3].push(value); - } - } - } - } - - let mut out = [Complex64::new(1.0, 0.0); 4]; - for (index, values) in regions.iter().enumerate() { - if !values.is_empty() { - out[index] = values.iter().copied().sum::() / values.len() as f64; - } - } - out -} - -pub fn create_sfactor( - direction: &str, - omega: f64, - dls: &[f64], - n: usize, - n_pml: usize, - dmin_pml: bool, - avg_speed: (Complex64, Complex64), - profile: &PmlProfile, -) -> Vec { - if n_pml == 0 { - return vec![Complex64::new(1.0, 0.0); n]; - } - match direction { - "f" => create_sfactor_f(omega, dls, n, n_pml, dmin_pml, avg_speed, profile), - "b" => create_sfactor_b(omega, dls, n, n_pml, dmin_pml, avg_speed, profile), - _ => panic!("direction value {direction} not recognized"), - } -} - -#[allow(clippy::too_many_arguments)] -pub fn create_sfactor_f( - omega: f64, - dls: &[f64], - n: usize, - n_pml: usize, - dmin_pml: bool, - avg_speed: (Complex64, Complex64), - profile: &PmlProfile, -) -> Vec { - let mut sfactor = vec![Complex64::new(1.0, 0.0); n]; - for (i, value) in sfactor.iter_mut().enumerate() { - if i < n_pml && dmin_pml { - *value = s_value( - dls[0], - (n_pml as f64 - i as f64 - 0.5) / n_pml as f64, - omega, - avg_speed.0, - profile, - ); - } else if i >= n - n_pml { - *value = s_value( - dls[dls.len() - 1], - (i as f64 - (n - n_pml) as f64 + 0.5) / n_pml as f64, - omega, - avg_speed.1, - profile, - ); - } - } - sfactor -} - -#[allow(clippy::too_many_arguments)] -pub fn create_sfactor_b( - omega: f64, - dls: &[f64], - n: usize, - n_pml: usize, - dmin_pml: bool, - avg_speed: (Complex64, Complex64), - profile: &PmlProfile, -) -> Vec { - let mut sfactor = vec![Complex64::new(1.0, 0.0); n]; - for (i, value) in sfactor.iter_mut().enumerate() { - if i < n_pml && dmin_pml { - *value = s_value( - dls[0], - (n_pml as f64 - i as f64) / n_pml as f64, - omega, - avg_speed.0, - profile, - ); - } else if i > n - n_pml { - *value = s_value( - dls[dls.len() - 1], - (i as f64 - (n - n_pml) as f64) / n_pml as f64, - omega, - avg_speed.1, - profile, - ); - } - } - sfactor -} - -pub fn s_value( - dl: f64, - step: f64, - omega: f64, - avg_speed: Complex64, - profile: &PmlProfile, -) -> Complex64 { - let step_power = step.powi(profile.order); - let kappa = profile.kappa_min + (profile.kappa_max - profile.kappa_min) * step_power; - let sigma = avg_speed * (profile.sigma_max / (ETA0 * dl) * step_power); - Complex64::new(kappa, 0.0) + Complex64::new(0.0, 1.0) * sigma / (omega * EPSILON0) -} - -pub fn tensor_from_flat(flat: &[Vec<(f64, f64)>], n: usize) -> Result { - if flat.len() != 9 { - return Err("tensor must contain 9 flattened components".to_string()); - } - let mut tensor: Tensor3 = std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())); - for row in 0..3 { - for col in 0..3 { - let values = &flat[row * 3 + col]; - if values.len() != n { - return Err("tensor component length does not match grid shape".to_string()); - } - tensor[row][col] = values - .iter() - .map(|(real, imag)| Complex64::new(*real, *imag)) - .collect(); - } - } - Ok(tensor) -} - -#[cfg(test)] -mod sparse_tests { - use super::*; - - fn sample_tensor(shape: (usize, usize), base: f64) -> Tensor3 { - let n = shape.0 * shape.1; - let mut tensor: Tensor3 = std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())); - for row in 0..3 { - for col in 0..3 { - tensor[row][col] = vec![Complex64::new(0.0, 0.0); n]; - } - } - tensor[0][0] = (0..n) - .map(|index| Complex64::new(base + 0.03 * index as f64, 0.0)) - .collect(); - tensor[1][1] = (0..n) - .map(|index| Complex64::new(base + 0.2 + 0.02 * index as f64, 0.0)) - .collect(); - tensor[2][2] = (0..n) - .map(|index| Complex64::new(base + 0.6 + 0.01 * index as f64, 0.0)) - .collect(); - tensor - } - - #[test] - fn sparse_derivative_matrices_have_expected_shape() { - let shape = (4, 5); - let dlf = ( - vec![0.09, 0.10, 0.13, 0.18], - vec![0.08, 0.11, 0.12, 0.14, 0.19], - ); - let dlb = ( - vec![0.09, 0.095, 0.115, 0.15], - vec![0.08, 0.10, 0.115, 0.13, 0.17], - ); - let dmin_pmc = (true, false); - let sparse = create_d_matrices_sparse(shape, (&dlf.0, &dlf.1), (&dlb.0, &dlb.1), dmin_pmc); - - for matrix in &sparse { - assert_eq!( - (matrix.rows, matrix.cols), - (shape.0 * shape.1, shape.0 * shape.1) - ); - assert!(matrix.nnz() > 0); - } - } - - #[test] - fn sparse_pml_matrices_have_expected_shape() { - let shape = (4, 5); - let dlf = ( - vec![0.09, 0.10, 0.13, 0.18], - vec![0.08, 0.11, 0.12, 0.14, 0.19], - ); - let dlb = ( - vec![0.09, 0.095, 0.115, 0.15], - vec![0.08, 0.10, 0.115, 0.13, 0.17], - ); - let eps = sample_tensor(shape, 2.0); - let mu = sample_tensor(shape, 1.0); - let omega = 2.0 * std::f64::consts::PI * 193.414_489e12; - let npml = (2, 1); - let dmin_pml = (true, false); - - let sparse = create_s_matrices_sparse( - omega, - shape, - npml, - (&dlf.0, &dlf.1), - (&dlb.0, &dlb.1), - &eps, - &mu, - dmin_pml, - ); - - for matrix in &sparse { - assert_eq!( - (matrix.rows, matrix.cols), - (shape.0 * shape.1, shape.0 * shape.1) - ); - assert!(matrix.nnz() > 0); - } - } -} diff --git a/src/diagonal_solver.rs b/src/diagonal_solver.rs deleted file mode 100644 index 2a30b58..0000000 --- a/src/diagonal_solver.rs +++ /dev/null @@ -1,160 +0,0 @@ -pub use crate::eigensolve::{ - diagonal_eigs_to_effective_index, selected_sparse_shift_invert_eigenpairs, - selected_sparse_shift_invert_native_eigenpairs, Eigenpair, ShiftInvertOptions, -}; -pub use crate::mode_solver::{ - solve_diagonal_sparse, solve_tensorial_sparse, DiagonalSolveResult, SolveDiagnostics, -}; -pub use crate::operators::{ - assemble_sparse_diagonal_operators, assemble_sparse_tensorial_operator, SparseDiagonalOperators, -}; - -#[cfg(test)] -mod sparse_tests { - use super::*; - use crate::derivatives; - use crate::derivatives::Tensor3; - use crate::eigensolve::sparse_residual_norm; - use num_complex::Complex64; - - fn sample_tensor(shape: (usize, usize), base: f64) -> Tensor3 { - let n = shape.0 * shape.1; - let grid = (0..n).map(|i| i as f64).collect::>(); - let mut tensor: Tensor3 = std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())); - for row in 0..3 { - for col in 0..3 { - tensor[row][col] = vec![Complex64::new(0.0, 0.0); n]; - } - } - tensor[0][0] = grid - .iter() - .map(|value| Complex64::new(base + 0.03 * value, 0.0)) - .collect(); - tensor[1][1] = grid - .iter() - .map(|value| Complex64::new(base + 0.2 + 0.02 * value, 0.0)) - .collect(); - tensor[2][2] = grid - .iter() - .map(|value| Complex64::new(base + 0.6 + 0.01 * value, 0.0)) - .collect(); - tensor - } - - #[test] - fn sparse_diagonal_operator_assembly_has_expected_shape() { - let shape = (3, 4); - let dlf = (vec![0.17, 0.19, 0.23], vec![0.11, 0.13, 0.17, 0.21]); - let dlb = (vec![0.16, 0.18, 0.21], vec![0.10, 0.12, 0.15, 0.19]); - let dmin_pmc = (false, true); - let sparse_derivatives = derivatives::create_d_matrices_sparse( - shape, - (&dlf.0, &dlf.1), - (&dlb.0, &dlb.1), - dmin_pmc, - ); - let eps = sample_tensor(shape, 2.0); - let mu = sample_tensor(shape, 1.0); - - let sparse = assemble_sparse_diagonal_operators(&eps, &mu, &sparse_derivatives); - - assert_eq!((sparse.mat.rows, sparse.mat.cols), (24, 24)); - assert_eq!((sparse.qmat.rows, sparse.qmat.cols), (24, 24)); - assert_eq!((sparse.q_ep.rows, sparse.q_ep.cols), (24, 24)); - assert!(sparse.mat.nnz() > 0); - assert!(sparse.qmat.nnz() > 0); - } - - #[test] - fn sparse_shift_invert_eigenpairs_have_small_residuals() { - let shape = (3, 4); - let dlf = (vec![0.17, 0.19, 0.23], vec![0.11, 0.13, 0.17, 0.21]); - let dlb = (vec![0.16, 0.18, 0.21], vec![0.10, 0.12, 0.15, 0.19]); - let sparse_derivatives = derivatives::create_d_matrices_sparse( - shape, - (&dlf.0, &dlf.1), - (&dlb.0, &dlb.1), - (false, false), - ); - let eps = sample_tensor(shape, 2.0); - let mu = sample_tensor(shape, 1.0); - let sparse = assemble_sparse_diagonal_operators(&eps, &mu, &sparse_derivatives); - let guess = Complex64::new(-(2.2 * 2.2), 0.0); - - let actual = selected_sparse_shift_invert_eigenpairs( - &sparse.mat, - 2, - guess, - None, - ShiftInvertOptions { - krylov_dim: 20, - tolerance: 1e-11, - }, - ) - .unwrap(); - - assert_eq!(actual.len(), 2); - for actual in &actual { - let residual = sparse_residual_norm(&sparse.mat, &actual.vector, actual.value); - assert!(residual < 1e-6, "residual was {residual}"); - } - } - - #[test] - fn sparse_diagonal_solve_recovers_normalized_fields() { - let shape = (3, 4); - let dlf = (vec![0.17, 0.19, 0.23], vec![0.11, 0.13, 0.17, 0.21]); - let dlb = (vec![0.16, 0.18, 0.21], vec![0.10, 0.12, 0.15, 0.19]); - let sparse_derivatives = derivatives::create_d_matrices_sparse( - shape, - (&dlf.0, &dlf.1), - (&dlb.0, &dlb.1), - (false, false), - ); - let eps = sample_tensor(shape, 2.0); - let mu = sample_tensor(shape, 1.0); - let cell_areas = test_cell_areas(&dlf.0, &dlf.1); - - let sparse = solve_diagonal_sparse( - &eps, - &mu, - &sparse_derivatives, - &cell_areas, - 2, - 2.2, - "+", - None, - ShiftInvertOptions { - krylov_dim: 20, - tolerance: 1e-11, - }, - ) - .unwrap(); - - assert_eq!(sparse.n_complex.len(), 2); - assert!(sparse.n_complex[0].re >= sparse.n_complex[1].re); - for value in &sparse.n_complex { - assert!(value.re.is_finite()); - assert!(value.im.is_finite()); - } - for power_norm in &sparse.diagnostics.power_norms { - assert!((*power_norm - 1.0).abs() < 1e-10); - } - - for mode_index in 0..2 { - for component in &sparse.fields { - assert_eq!(component[mode_index].len(), shape.0 * shape.1); - } - } - } - - fn test_cell_areas(dlf_x: &[f64], dlf_y: &[f64]) -> Vec { - let mut out = Vec::with_capacity(dlf_x.len() * dlf_y.len()); - for dx in dlf_x { - for dy in dlf_y { - out.push(dx * dy); - } - } - out - } -} diff --git a/src/eigensolve.rs b/src/eigensolve.rs deleted file mode 100644 index 0fa1d70..0000000 --- a/src/eigensolve.rs +++ /dev/null @@ -1,664 +0,0 @@ -//! Eigenvalue selection and shift-invert backends. -//! -//! The solver usually needs a handful of modes near a requested effective -//! index, not the whole spectrum. Shift-invert changes that local search into a -//! dominant-eigenvalue problem for `(A - sigma I)^-1`, which sparse Krylov -//! methods can solve efficiently on realistic grids. - -use std::time::{Duration, Instant}; - -use nalgebra::{linalg::SVD, DMatrix}; -use num_complex::Complex64; - -use crate::sparse_matrix::SparseMatrix; - -#[derive(Clone, Debug)] -pub struct Eigenpair { - pub value: Complex64, - pub vector: Vec, - pub residual: f64, - pub backend: &'static str, -} - -#[derive(Clone, Debug)] -pub struct ShiftInvertOptions { - pub krylov_dim: usize, - pub tolerance: f64, -} - -impl Default for ShiftInvertOptions { - fn default() -> Self { - Self { - krylov_dim: 32, - tolerance: 1e-10, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct ShiftInvertProfile { - pub pairs: Vec, - pub total: Duration, - pub shift_diagonal: Duration, - pub amd_ordering: Duration, - pub lu_factorization: Duration, - pub lu_packing: Duration, - pub linear_solves: Duration, - pub arnoldi_orthogonalization: Duration, - pub hessenberg_eigensolve: Duration, - pub ritz_reconstruction: Duration, - pub residuals: Duration, - pub sorting: Duration, - pub solve_calls: usize, - pub arnoldi_steps: usize, - pub candidate_count: usize, - pub returned_pairs: usize, - pub lu_l_nnz: usize, - pub lu_u_nnz: usize, - pub max_residual: f64, -} - -pub fn selected_sparse_shift_invert_eigenpairs( - mat: &SparseMatrix, - num_modes: usize, - guess_value: Complex64, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, -) -> Result, String> { - selected_sparse_shift_invert_native_eigenpairs( - mat, - num_modes, - guess_value, - initial_vector, - options, - ) -} - -pub fn selected_sparse_shift_invert_native_eigenpairs( - mat: &SparseMatrix, - num_modes: usize, - guess_value: Complex64, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, -) -> Result, String> { - selected_sparse_shift_invert_native_eigenpairs_impl( - mat, - num_modes, - guess_value, - initial_vector, - options, - None, - ) -} - -pub fn profile_sparse_shift_invert_native_eigenpairs( - mat: &SparseMatrix, - num_modes: usize, - guess_value: Complex64, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, -) -> Result { - let total_start = Instant::now(); - let mut profile = ShiftInvertProfile::default(); - let pairs = selected_sparse_shift_invert_native_eigenpairs_impl( - mat, - num_modes, - guess_value, - initial_vector, - options, - Some(&mut profile), - )?; - profile.total = total_start.elapsed(); - profile.max_residual = pairs.iter().map(|pair| pair.residual).fold(0.0, f64::max); - profile.returned_pairs = pairs.len(); - profile.pairs = pairs; - Ok(profile) -} - -fn selected_sparse_shift_invert_native_eigenpairs_impl( - mat: &SparseMatrix, - num_modes: usize, - guess_value: Complex64, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, - mut profile: Option<&mut ShiftInvertProfile>, -) -> Result, String> { - if mat.rows != mat.cols { - return Err("eigenvalue matrix must be square".to_string()); - } - if num_modes == 0 { - return Err("num_modes must be positive".to_string()); - } - let n = mat.rows; - let krylov_dim = options.krylov_dim.min(n).max(num_modes + 2); - if krylov_dim < num_modes { - return Err("krylov_dim must be at least num_modes".to_string()); - } - - // Shift-invert solves `(A - sigma I) y = x` inside Arnoldi. Ritz values - // theta of the inverse-shifted operator map back to lambda = sigma + 1/theta. - let shift_start = profile.as_ref().map(|_| Instant::now()); - let shifted = mat.shifted_diagonal(guess_value); - if let (Some(profile), Some(start)) = (profile.as_mut(), shift_start) { - profile.shift_diagonal += start.elapsed(); - } - let factorization = SparseLu::factor_with_profile(&shifted, profile.as_deref_mut())?; - let mut solve_workspace = vec![Complex64::new(0.0, 0.0); mat.rows]; - selected_sparse_shift_invert_native_with_solver( - mat, - num_modes, - guess_value, - initial_vector, - options, - "native_shift_invert", - profile, - |input, output| factorization.solve_into(input, output, &mut solve_workspace), - ) -} - -fn selected_sparse_shift_invert_native_with_solver( - mat: &SparseMatrix, - num_modes: usize, - guess_value: Complex64, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, - backend: &'static str, - mut profile: Option<&mut ShiftInvertProfile>, - mut solve: F, -) -> Result, String> -where - F: FnMut(&[Complex64], &mut [Complex64]) -> Result<(), String>, -{ - let n = mat.rows; - let krylov_dim = options.krylov_dim.min(n).max(num_modes + 2); - let checkpoint_start = - krylov_dim.min((3 * krylov_dim).div_ceil(4).max((num_modes + 8).max(16))); - let checkpoint_interval = 4; - let stability_tolerance = options.tolerance.sqrt(); - let mut previous_checkpoint_values: Option> = None; - let start = initial_vector - .map(|values| { - if values.len() != n { - return Err("initial vector length does not match matrix size".to_string()); - } - Ok(values.to_vec()) - }) - .unwrap_or_else(|| Ok(default_initial_vector(n)))?; - let mut q_basis = ArnoldiBasis::with_first(normalize_complex_vector(start), krylov_dim + 1); - let mut hessenberg = DMatrix::::zeros(krylov_dim + 1, krylov_dim); - let mut actual_dim = 0usize; - - for col in 0..krylov_dim { - // Arnoldi sees only y = (A - sigma I)^-1 x. Keeping that action behind - // a closure makes the Krylov logic independent from the linear solver: - // native sparse LU here, SciPy/ARPACK in the Python reference backend. - // The second Gram-Schmidt pass is deliberate because clustered - // waveguide modes otherwise lose orthogonality quickly. - let solve_start = profile.as_ref().map(|_| Instant::now()); - let mut work = vec![Complex64::new(0.0, 0.0); n]; - solve(q_basis.vector(col), &mut work)?; - if let (Some(profile), Some(start)) = (profile.as_mut(), solve_start) { - profile.linear_solves += start.elapsed(); - profile.solve_calls += 1; - } - let orthogonalization_start = profile.as_ref().map(|_| Instant::now()); - for row in 0..=col { - let basis_vector = q_basis.vector(row); - let projection = complex_dot(basis_vector, &work); - hessenberg[(row, col)] = projection; - axpy(&mut work, basis_vector, -projection); - } - for row in 0..=col { - let basis_vector = q_basis.vector(row); - let projection = complex_dot(basis_vector, &work); - hessenberg[(row, col)] += projection; - axpy(&mut work, basis_vector, -projection); - } - let norm = vector_norm(&work); - actual_dim = col + 1; - hessenberg[(col + 1, col)] = Complex64::new(norm, 0.0); - if let (Some(profile), Some(start)) = (profile.as_mut(), orthogonalization_start) { - profile.arnoldi_orthogonalization += start.elapsed(); - } - if let Some(profile) = profile.as_mut() { - profile.arnoldi_steps = actual_dim; - } - - let exhausted = norm <= options.tolerance || col + 1 == krylov_dim; - let checkpoint_due = actual_dim >= checkpoint_start - && ((actual_dim - checkpoint_start).is_multiple_of(checkpoint_interval) || exhausted); - if checkpoint_due { - let candidates = candidate_eigenpairs_from_hessenberg( - mat, - &q_basis, - &hessenberg, - actual_dim, - num_modes, - guess_value, - backend, - options.tolerance, - profile.as_deref_mut(), - )?; - let converged = candidates_converged(&candidates, num_modes, options.tolerance); - let stable = previous_checkpoint_values.as_ref().is_some_and(|previous| { - candidates_stable(previous, &candidates, stability_tolerance) - }); - previous_checkpoint_values = - Some(candidates.iter().map(|candidate| candidate.value).collect()); - if exhausted || (converged && stable) { - return Ok(candidates); - } - } - if exhausted { - break; - } - scale_vector(&mut work, Complex64::new(1.0 / norm, 0.0)); - q_basis.push(work); - } - if let Some(profile) = profile.as_mut() { - profile.arnoldi_steps = actual_dim; - } - - candidate_eigenpairs_from_hessenberg( - mat, - &q_basis, - &hessenberg, - actual_dim, - num_modes, - guess_value, - backend, - options.tolerance, - profile, - ) -} - -fn candidate_eigenpairs_from_hessenberg( - mat: &SparseMatrix, - q_basis: &ArnoldiBasis, - hessenberg: &DMatrix, - actual_dim: usize, - num_modes: usize, - guess_value: Complex64, - backend: &'static str, - tolerance: f64, - mut profile: Option<&mut ShiftInvertProfile>, -) -> Result, String> { - let h_square = hessenberg - .view((0, 0), (actual_dim, actual_dim)) - .into_owned(); - let hessenberg_start = profile.as_ref().map(|_| Instant::now()); - let theta_values = h_square - .clone() - .schur() - .eigenvalues() - .ok_or_else(|| "failed to compute Hessenberg eigenvalues".to_string())?; - if let (Some(profile), Some(start)) = (profile.as_mut(), hessenberg_start) { - profile.hessenberg_eigensolve += start.elapsed(); - } - let mut theta_candidates = Vec::new(); - for theta in theta_values.iter().copied() { - if theta.norm() <= tolerance { - continue; - } - let lambda = guess_value + Complex64::new(1.0, 0.0) / theta; - theta_candidates.push((lambda, theta)); - } - theta_candidates.sort_by(|(left, _), (right, _)| { - let left_distance = (*left - guess_value).norm(); - let right_distance = (*right - guess_value).norm(); - left_distance - .partial_cmp(&right_distance) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - // Residuals are only a tie-breaker after shift distance. Avoid the - // SVD-based full-vector reconstruction for distant projected eigenvalues - // that cannot survive the final truncation. - let reconstruction_count = theta_candidates - .len() - .min((num_modes * 2).max(num_modes + 2)); - let mut candidates = Vec::with_capacity(reconstruction_count); - for (lambda, theta) in theta_candidates.into_iter().take(reconstruction_count) { - let ritz_start = profile.as_ref().map(|_| Instant::now()); - let coeffs = null_vector_for_eigenvalue(&h_square, theta)?; - let vector = combine_ritz_vector(q_basis, &coeffs); - if let (Some(profile), Some(start)) = (profile.as_mut(), ritz_start) { - profile.ritz_reconstruction += start.elapsed(); - } - let residual_start = profile.as_ref().map(|_| Instant::now()); - let residual = sparse_residual_norm(mat, &vector, lambda); - if let (Some(profile), Some(start)) = (profile.as_mut(), residual_start) { - profile.residuals += start.elapsed(); - } - candidates.push((lambda, vector, residual)); - } - if let Some(profile) = profile.as_mut() { - profile.candidate_count = candidates.len(); - } - // Sort by closeness to the requested shift. Residual is the tie-breaker - // because it is the direct measure of eigenpair quality. - let sorting_start = profile.as_ref().map(|_| Instant::now()); - candidates.sort_by(|(left, _, left_res), (right, _, right_res)| { - let left_distance = (*left - guess_value).norm(); - let right_distance = (*right - guess_value).norm(); - left_distance - .partial_cmp(&right_distance) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| { - left_res - .partial_cmp(right_res) - .unwrap_or(std::cmp::Ordering::Equal) - }) - }); - candidates.truncate(num_modes); - if let (Some(profile), Some(start)) = (profile.as_mut(), sorting_start) { - profile.sorting += start.elapsed(); - } - Ok(candidates - .into_iter() - .map(|(value, vector, residual)| Eigenpair { - value, - vector, - residual, - backend, - }) - .collect()) -} - -fn candidates_converged(candidates: &[Eigenpair], num_modes: usize, tolerance: f64) -> bool { - candidates.len() == num_modes - && candidates - .iter() - .all(|candidate| candidate.residual <= tolerance) -} - -fn candidates_stable( - previous_values: &[Complex64], - candidates: &[Eigenpair], - tolerance: f64, -) -> bool { - previous_values.len() == candidates.len() - && previous_values - .iter() - .zip(candidates) - .all(|(previous, candidate)| (*previous - candidate.value).norm() <= tolerance) -} - -#[derive(Clone, Debug)] -struct ArnoldiBasis { - n: usize, - values: Vec, -} - -impl ArnoldiBasis { - fn with_first(first: Vec, capacity: usize) -> Self { - let n = first.len(); - let mut values = Vec::with_capacity(n * capacity); - values.extend_from_slice(&first); - Self { n, values } - } - - fn vector(&self, index: usize) -> &[Complex64] { - let start = index * self.n; - &self.values[start..start + self.n] - } - - fn push(&mut self, vector: Vec) { - assert_eq!(vector.len(), self.n); - self.values.extend_from_slice(&vector); - } -} - -#[derive(Clone, Debug)] -struct SparseLu { - n: usize, - l: PackedTriangularMatrix, - u: PackedTriangularMatrix, - row_perm: Vec, - col_perm: Option>, -} - -#[derive(Clone, Debug)] -struct PackedTriangularMatrix { - col_ptrs: Vec, - row_indices: Vec, - values: Vec, -} - -impl SparseLu { - fn factor_with_profile( - matrix: &SparseMatrix, - mut profile: Option<&mut ShiftInvertProfile>, - ) -> Result { - // Native sparse LU is the production linear solve for shift-invert. - // Validate pivots here because a missing pivot means the requested - // shift made (A - sigma I) numerically unusable for this grid. - if matrix.rows != matrix.cols { - return Err("LU factorization requires a square matrix".to_string()); - } - let ordering_start = profile.as_ref().map(|_| Instant::now()); - let col_perm = amd::order::( - matrix.rows, - matrix.col_ptrs(), - matrix.row_indices(), - &amd::Control::default(), - ) - .map(|(perm, _, _)| perm) - .ok(); - if let (Some(profile), Some(start)) = (profile.as_mut(), ordering_start) { - profile.amd_ordering += start.elapsed(); - } - let factor_start = profile.as_ref().map(|_| Instant::now()); - let (l_columns, u_columns, row_perm) = rlu::lu_decomposition( - matrix.rows, - matrix.row_indices(), - matrix.col_ptrs(), - matrix.values(), - col_perm.as_deref(), - None, - None, - true, - ); - if let (Some(profile), Some(start)) = (profile.as_mut(), factor_start) { - profile.lu_factorization += start.elapsed(); - } - if row_perm.iter().any(|value| value.is_none()) { - return Err("sparse LU failed to find a complete pivot set".to_string()); - } - let row_perm = row_perm - .into_iter() - .map(|value| value.expect("validated pivot set")) - .collect(); - let packing_start = profile.as_ref().map(|_| Instant::now()); - let l = PackedTriangularMatrix::from_columns(l_columns); - let u = PackedTriangularMatrix::from_columns(u_columns); - if let (Some(profile), Some(start)) = (profile.as_mut(), packing_start) { - profile.lu_packing += start.elapsed(); - profile.lu_l_nnz = l.nnz(); - profile.lu_u_nnz = u.nnz(); - } - Ok(Self { - n: matrix.rows, - l, - u, - row_perm, - col_perm, - }) - } - - fn solve_into( - &self, - rhs: &[Complex64], - out: &mut [Complex64], - work: &mut [Complex64], - ) -> Result<(), String> { - if rhs.len() != self.n { - return Err("right-hand side length does not match LU size".to_string()); - } - if out.len() != self.n || work.len() != self.n { - return Err("solve workspace length does not match LU size".to_string()); - } - - if let Some(col_perm) = &self.col_perm { - for (index, value) in rhs.iter().copied().enumerate() { - work[self.row_perm[index]] = value; - } - self.l.lsolve(work); - self.u.usolve(work); - for (index, value) in work.iter().copied().enumerate() { - out[col_perm[index]] = value; - } - } else { - for (index, value) in rhs.iter().copied().enumerate() { - out[self.row_perm[index]] = value; - } - self.l.lsolve(out); - self.u.usolve(out); - } - Ok(()) - } -} - -impl PackedTriangularMatrix { - fn from_columns(columns: rlu::Matrix) -> Self { - let mut col_ptrs = Vec::with_capacity(columns.len() + 1); - let nnz = columns.iter().map(Vec::len).sum(); - let mut row_indices = Vec::with_capacity(nnz); - let mut values = Vec::with_capacity(nnz); - col_ptrs.push(0); - for column in columns { - for (row, value) in column { - row_indices.push(row); - values.push(value); - } - col_ptrs.push(row_indices.len()); - } - Self { - col_ptrs, - row_indices, - values, - } - } - - fn nnz(&self) -> usize { - self.values.len() - } - - fn lsolve(&self, rhs: &mut [Complex64]) { - for col in 0..rhs.len() { - let value = rhs[col]; - for index in self.col_ptrs[col]..self.col_ptrs[col + 1] { - rhs[self.row_indices[index]] -= self.values[index] * value; - } - } - } - - fn usolve(&self, rhs: &mut [Complex64]) { - for col in (0..rhs.len()).rev() { - for index in (self.col_ptrs[col]..self.col_ptrs[col + 1]).rev() { - let row = self.row_indices[index]; - if row == col { - rhs[col] /= self.values[index]; - } else { - rhs[row] -= self.values[index] * rhs[col]; - } - } - } - } -} - -fn null_vector_for_eigenvalue( - mat: &DMatrix, - value: Complex64, -) -> Result, String> { - // Given lambda, an eigenvector is in the null space of A - lambda I. The - // smallest-singular right vector is a stable way to recover that direction - // for the small projected Arnoldi matrix. - let mut shifted = mat.clone(); - let dim = shifted.nrows(); - for index in 0..dim { - shifted[(index, index)] -= value; - } - let svd = SVD::try_new(shifted, false, true, f64::EPSILON * 16.0, 0) - .ok_or_else(|| "failed to compute null vector SVD".to_string())?; - let v_t = svd - .v_t - .ok_or_else(|| "SVD did not return right singular vectors".to_string())?; - let row = v_t.row(v_t.nrows() - 1); - let vector = row.iter().map(|value| value.conj()).collect::>(); - Ok(normalize_complex_vector(vector)) -} - -fn normalize_complex_vector(mut vector: Vec) -> Vec { - let norm = vector_norm(&vector); - if norm > 0.0 { - scale_vector(&mut vector, Complex64::new(1.0 / norm, 0.0)); - } - vector -} - -pub(crate) fn vector_norm(vector: &[Complex64]) -> f64 { - vector - .iter() - .map(|value| value.norm_sqr()) - .sum::() - .sqrt() -} - -fn scale_vector(vector: &mut [Complex64], scale: Complex64) { - for value in vector { - *value *= scale; - } -} - -pub(crate) fn complex_dot(left: &[Complex64], right: &[Complex64]) -> Complex64 { - assert_eq!(left.len(), right.len()); - left.iter() - .zip(right) - .map(|(left, right)| left.conj() * *right) - .sum() -} - -fn axpy(target: &mut [Complex64], vector: &[Complex64], scale: Complex64) { - assert_eq!(target.len(), vector.len()); - for (target, value) in target.iter_mut().zip(vector) { - *target += scale * *value; - } -} - -fn default_initial_vector(n: usize) -> Vec { - (0..n) - .map(|index| { - let x = (index + 1) as f64; - Complex64::new((0.37 * x).sin(), (0.53 * x).cos()) - }) - .collect() -} - -fn combine_ritz_vector(q_basis: &ArnoldiBasis, coeffs: &[Complex64]) -> Vec { - let n = q_basis.n; - let mut out = vec![Complex64::new(0.0, 0.0); n]; - for (basis_index, coeff) in coeffs.iter().copied().enumerate() { - axpy(&mut out, q_basis.vector(basis_index), coeff); - } - normalize_complex_vector(out) -} - -pub(crate) fn sparse_residual_norm( - mat: &SparseMatrix, - vector: &[Complex64], - value: Complex64, -) -> f64 { - // Relative residual ||A v - lambda v|| / ||v||. Python surfaces this in - // solver diagnostics, and the tests use it to catch backend regressions. - let mut residual = mat.matvec(vector); - axpy(&mut residual, vector, -value); - vector_norm(&residual) / vector_norm(vector).max(f64::EPSILON) -} - -pub fn diagonal_eigs_to_effective_index(eigenvalues: &[Complex64]) -> Vec { - eigenvalues - .iter() - .map(|value| (-*value + Complex64::new(0.0, 0.0)).sqrt()) - .collect() -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 52345af..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod derivatives; -pub mod diagonal_solver; -pub mod eigensolve; -pub mod mode_solver; -pub mod operators; -pub mod sparse_matrix; - -#[cfg(feature = "python")] -mod python_api; diff --git a/src/mode_solver.rs b/src/mode_solver.rs deleted file mode 100644 index 1b842ae..0000000 --- a/src/mode_solver.rs +++ /dev/null @@ -1,569 +0,0 @@ -//! Mode-level solve orchestration. -//! -//! `operators` builds the matrices and `eigensolve` returns transverse -//! eigenvectors. This module turns those eigenvectors into user-facing mode -//! data: effective indices, all six field components, direction handling, -//! deterministic phase, and unit-power normalization. - -use num_complex::Complex64; - -use crate::derivatives::{Tensor3, ETA0}; -use crate::eigensolve::{ - diagonal_eigs_to_effective_index, selected_sparse_shift_invert_eigenpairs, ShiftInvertOptions, -}; -use crate::operators::{assemble_sparse_diagonal_operators, assemble_sparse_tensorial_operator}; -use crate::sparse_matrix::SparseMatrix; - -#[derive(Clone, Debug)] -pub struct SolveDiagnostics { - pub backend: String, - pub operator_size: usize, - pub operator_nnz: usize, - pub residuals: Vec, - pub power_norms: Vec, - pub lorentz_norms: Vec, - pub lorentz_orthogonality_error: f64, -} - -#[derive(Clone, Debug)] -pub struct DiagonalSolveResult { - pub n_complex: Vec, - pub fields: [Vec>; 6], - pub diagnostics: SolveDiagnostics, -} - -#[derive(Clone, Debug)] -struct ModeFields { - ex: Vec, - ey: Vec, - ez: Vec, - hx: Vec, - hy: Vec, - hz: Vec, -} - -impl ModeFields { - fn mutable_components(&mut self) -> [&mut Vec; 6] { - [ - &mut self.ex, - &mut self.ey, - &mut self.ez, - &mut self.hx, - &mut self.hy, - &mut self.hz, - ] - } - - fn add_scaled(&mut self, other: &ModeFields, scale: Complex64) { - for (left, right) in self.ex.iter_mut().zip(&other.ex) { - *left += scale * *right; - } - for (left, right) in self.ey.iter_mut().zip(&other.ey) { - *left += scale * *right; - } - for (left, right) in self.ez.iter_mut().zip(&other.ez) { - *left += scale * *right; - } - for (left, right) in self.hx.iter_mut().zip(&other.hx) { - *left += scale * *right; - } - for (left, right) in self.hy.iter_mut().zip(&other.hy) { - *left += scale * *right; - } - for (left, right) in self.hz.iter_mut().zip(&other.hz) { - *left += scale * *right; - } - } -} - -pub fn solve_diagonal_sparse( - eps: &Tensor3, - mu: &Tensor3, - der_mats: &[SparseMatrix; 4], - cell_areas: &[f64], - num_modes: usize, - neff_guess: f64, - direction: &str, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, -) -> Result { - // Production diagonal-media path. The eigenvector contains only [Ex, Ey]; - // the remaining components are reconstructed from Maxwell curl equations. - validate_cell_areas(cell_areas, eps[0][0].len())?; - let operators = assemble_sparse_diagonal_operators(eps, mu, der_mats); - let eig_guess = Complex64::new(-(neff_guess * neff_guess), 0.0); - let pairs = selected_sparse_shift_invert_eigenpairs( - &operators.mat, - num_modes, - eig_guess, - initial_vector, - options, - )?; - let operator_size = operators.mat.rows; - let operator_nnz = operators.mat.nnz(); - let mut modes = pairs - .into_iter() - .map(|pair| { - let n_complex = diagonal_eigs_to_effective_index(&[pair.value])[0]; - (n_complex, pair.vector, pair.residual, pair.backend) - }) - .collect::>(); - modes.sort_by(|(left, _, _, _), (right, _, _, _)| { - left.re - .partial_cmp(&right.re) - .unwrap_or(std::cmp::Ordering::Equal) - .reverse() - }); - - let n = eps[0][0].len(); - let dxf = &der_mats[0]; - let dxb = &der_mats[1]; - let dyf = &der_mats[2]; - let dyb = &der_mats[3]; - let inv_eps_zz = SparseMatrix::diagonal( - &eps[2][2] - .iter() - .map(|value| Complex64::new(1.0, 0.0) / *value) - .collect::>(), - ); - let inv_mu_zz = SparseMatrix::diagonal( - &mu[2][2] - .iter() - .map(|value| Complex64::new(1.0, 0.0) / *value) - .collect::>(), - ); - - let mut n_complex = Vec::with_capacity(modes.len()); - let mut mode_fields = Vec::with_capacity(modes.len()); - let mut residuals = Vec::with_capacity(modes.len()); - let backend = modes - .first() - .map(|(_, _, _, backend)| *backend) - .unwrap_or("sparse_shift_invert"); - - for (mode_n, vector, residual, _) in modes { - // Eigenvector layout is [Ex, Ey] on the flattened local Yee grid. - let ex = vector[..n].to_vec(); - let ey = vector[n..].to_vec(); - let denom = Complex64::new(-mode_n.im, mode_n.re); - - let h_field = operators.qmat.matvec(&vector); - let mut hx = h_field[..n] - .iter() - .map(|value| *value / denom) - .collect::>(); - let mut hy = h_field[n..] - .iter() - .map(|value| *value / denom) - .collect::>(); - - // Reconstruct longitudinal fields from the stored transverse fields. - let dxf_ey = dxf.matvec(&ey); - let dyf_ex = dyf.matvec(&ex); - let hz_source = dxf_ey - .iter() - .zip(&dyf_ex) - .map(|(left, right)| *left - *right) - .collect::>(); - let mut hz = inv_mu_zz.matvec(&hz_source); - - let h_partial_field = operators - .q_ep - .matvec(&vector) - .into_iter() - .map(|value| value / denom) - .collect::>(); - let dxb_hy = dxb.matvec(&h_partial_field[n..]); - let dyb_hx = dyb.matvec(&h_partial_field[..n]); - let ez_source = dxb_hy - .iter() - .zip(&dyb_hx) - .map(|(left, right)| *left - *right) - .collect::>(); - let mut ez = inv_eps_zz.matvec(&ez_source); - - let h_scale = Complex64::new(0.0, -1.0) / ETA0; - for component in [&mut hx, &mut hy, &mut hz] { - for value in component { - *value *= h_scale; - } - } - - if direction == "-" { - for value in &mut hx { - *value *= -1.0; - } - for value in &mut hy { - *value *= -1.0; - } - for value in &mut ez { - *value *= -1.0; - } - } - n_complex.push(mode_n); - residuals.push(residual); - mode_fields.push(ModeFields { - ex, - ey, - ez, - hx, - hy, - hz, - }); - } - let orthogonalization = lorentz_orthogonalize_and_normalize(&mut mode_fields, cell_areas); - let fields = collect_mode_fields(mode_fields); - - Ok(DiagonalSolveResult { - n_complex, - fields, - diagnostics: SolveDiagnostics { - backend: backend.to_string(), - operator_size, - operator_nnz, - residuals, - power_norms: orthogonalization.power_norms, - lorentz_norms: orthogonalization.lorentz_norms, - lorentz_orthogonality_error: orthogonalization.lorentz_orthogonality_error, - }, - }) -} - -pub fn solve_tensorial_sparse( - eps: &Tensor3, - mu: &Tensor3, - der_mats: &[SparseMatrix; 4], - cell_areas: &[f64], - num_modes: usize, - neff_guess: f64, - direction: &str, - initial_vector: Option<&[Complex64]>, - options: ShiftInvertOptions, -) -> Result { - // Tensorial path for off-diagonal material tensors and angle/bend coordinate - // transforms. The eigenvector keeps both transverse E and H components. - validate_cell_areas(cell_areas, eps[0][0].len())?; - let operator = assemble_sparse_tensorial_operator(eps, mu, der_mats); - let eig_guess = Complex64::new(neff_guess, 0.0); - let pairs = selected_sparse_shift_invert_eigenpairs( - &operator, - num_modes, - eig_guess, - initial_vector, - options, - )?; - let operator_size = operator.rows; - let operator_nnz = operator.nnz(); - let mut modes = pairs - .into_iter() - .map(|pair| (pair.value, pair.vector, pair.residual, pair.backend)) - .collect::>(); - modes.sort_by(|(left, _, _, _), (right, _, _, _)| { - left.re - .partial_cmp(&right.re) - .unwrap_or(std::cmp::Ordering::Equal) - .reverse() - }); - - let n = eps[0][0].len(); - let dxf = &der_mats[0]; - let dxb = &der_mats[1]; - let dyf = &der_mats[2]; - let dyb = &der_mats[3]; - let inv_eps_zz = SparseMatrix::diagonal( - &eps[2][2] - .iter() - .map(|value| Complex64::new(1.0, 0.0) / *value) - .collect::>(), - ); - let inv_mu_zz = SparseMatrix::diagonal( - &mu[2][2] - .iter() - .map(|value| Complex64::new(1.0, 0.0) / *value) - .collect::>(), - ); - - let mut n_complex = Vec::with_capacity(modes.len()); - let mut mode_fields = Vec::with_capacity(modes.len()); - let mut residuals = Vec::with_capacity(modes.len()); - let backend = modes - .first() - .map(|(_, _, _, backend)| *backend) - .unwrap_or("sparse_shift_invert"); - - for (mode_n, vector, residual, _) in modes { - if vector.len() != 4 * n { - return Err("tensorial eigenvector has an unexpected length".to_string()); - } - // Tensorial eigenvector layout is [Ex, Ey, Hx, Hy]. - let ex = vector[..n].to_vec(); - let ey = vector[n..2 * n].to_vec(); - let mut hx = vector[2 * n..3 * n].to_vec(); - let mut hy = vector[3 * n..4 * n].to_vec(); - - // Reconstruct Hz/Ez while accounting for off-diagonal tensor coupling - // into the eliminated z components. - let dxf_ey = dxf.matvec(&ey); - let dyf_ex = dyf.matvec(&ex); - let hz_source = dxf_ey - .iter() - .zip(&dyf_ex) - .zip(mu[2][0].iter()) - .zip(mu[2][1].iter()) - .zip(hx.iter()) - .zip(hy.iter()) - .map(|(((((dxf_ey, dyf_ex), mu_20), mu_21), hx), hy)| { - *dxf_ey - *dyf_ex - *mu_20 * *hx - *mu_21 * *hy - }) - .collect::>(); - let mut hz = inv_mu_zz.matvec(&hz_source); - - let dxb_hy = dxb.matvec(&hy); - let dyb_hx = dyb.matvec(&hx); - let ez_source = dxb_hy - .iter() - .zip(&dyb_hx) - .zip(eps[2][0].iter()) - .zip(eps[2][1].iter()) - .zip(ex.iter()) - .zip(ey.iter()) - .map(|(((((dxb_hy, dyb_hx), eps_20), eps_21), ex), ey)| { - *dxb_hy - *dyb_hx - *eps_20 * *ex - *eps_21 * *ey - }) - .collect::>(); - let mut ez = inv_eps_zz.matvec(&ez_source); - - let h_scale = Complex64::new(0.0, -1.0) / ETA0; - for component in [&mut hx, &mut hy, &mut hz] { - for value in component { - *value *= h_scale; - } - } - - if direction == "-" { - for value in &mut hx { - *value *= -1.0; - } - for value in &mut hy { - *value *= -1.0; - } - for value in &mut ez { - *value *= -1.0; - } - } - n_complex.push(mode_n); - residuals.push(residual); - mode_fields.push(ModeFields { - ex, - ey, - ez, - hx, - hy, - hz, - }); - } - let orthogonalization = lorentz_orthogonalize_and_normalize(&mut mode_fields, cell_areas); - let fields = collect_mode_fields(mode_fields); - - Ok(DiagonalSolveResult { - n_complex, - fields, - diagnostics: SolveDiagnostics { - backend: backend.to_string(), - operator_size, - operator_nnz, - residuals, - power_norms: orthogonalization.power_norms, - lorentz_norms: orthogonalization.lorentz_norms, - lorentz_orthogonality_error: orthogonalization.lorentz_orthogonality_error, - }, - }) -} - -#[derive(Clone, Debug)] -struct OrthogonalizationDiagnostics { - power_norms: Vec, - lorentz_norms: Vec, - lorentz_orthogonality_error: f64, -} - -fn lorentz_orthogonalize_and_normalize( - modes: &mut [ModeFields], - cell_areas: &[f64], -) -> OrthogonalizationDiagnostics { - // First normalize each reconstructed eigenmode to a sane amplitude. Then - // apply modified Gram-Schmidt with the unconjugated Lorentz reciprocity - // product: - // - // L(a,b) = 1/2 integral[((Ea x Hb) + (Eb x Ha)) . z] dA - // - // The existing unit-power normalization uses H*, which fixes physical - // amplitude. This Lorentz product intentionally does not conjugate either - // mode; it removes residual mixing between modes in the reciprocal - // eigenbasis. - for mode in modes.iter_mut() { - normalize_to_unit_power(mode.mutable_components(), cell_areas); - } - - for mode_index in 0..modes.len() { - for previous_index in 0..mode_index { - let denom = lorentz_overlap(&modes[previous_index], &modes[previous_index], cell_areas); - if denom.norm() <= f64::EPSILON { - continue; - } - let numer = lorentz_overlap(&modes[previous_index], &modes[mode_index], cell_areas); - let coeff = numer / denom; - let previous = modes[previous_index].clone(); - modes[mode_index].add_scaled(&previous, -coeff); - } - normalize_to_unit_power(modes[mode_index].mutable_components(), cell_areas); - apply_dominant_e_phase_convention(modes[mode_index].mutable_components()); - } - - let power_norms = modes - .iter_mut() - .map(|mode| transverse_power(&mode.mutable_components(), cell_areas).norm()) - .collect::>(); - let lorentz_norms = modes - .iter() - .map(|mode| lorentz_overlap(mode, mode, cell_areas)) - .collect::>(); - let mut lorentz_orthogonality_error: f64 = 0.0; - for left in 0..modes.len() { - for right in 0..modes.len() { - if left == right { - continue; - } - let denom = (lorentz_norms[left].norm() * lorentz_norms[right].norm()).sqrt(); - if denom <= f64::EPSILON { - continue; - } - let normalized = - lorentz_overlap(&modes[left], &modes[right], cell_areas).norm() / denom; - lorentz_orthogonality_error = lorentz_orthogonality_error.max(normalized); - } - } - - OrthogonalizationDiagnostics { - power_norms, - lorentz_norms, - lorentz_orthogonality_error, - } -} - -fn collect_mode_fields(modes: Vec) -> [Vec>; 6] { - let mut fields: [Vec>; 6] = std::array::from_fn(|_| Vec::new()); - for mode in modes { - fields[0].push(mode.ex); - fields[1].push(mode.ey); - fields[2].push(mode.ez); - fields[3].push(mode.hx); - fields[4].push(mode.hy); - fields[5].push(mode.hz); - } - fields -} - -fn validate_cell_areas(cell_areas: &[f64], n: usize) -> Result<(), String> { - // Integration weights come from the Python grid edges. Keep this validation - // at the Rust boundary so normalization cannot silently use the wrong grid. - if cell_areas.len() != n { - return Err(format!( - "cell area vector length {} does not match grid size {n}", - cell_areas.len() - )); - } - if cell_areas - .iter() - .any(|value| !value.is_finite() || *value <= 0.0) - { - return Err("cell areas must be finite and positive".to_string()); - } - Ok(()) -} - -fn normalize_to_unit_power(mut components: [&mut Vec; 6], cell_areas: &[f64]) -> f64 { - // Scale all six components together so the transverse Poynting product has - // unit magnitude. This preserves E/H ratios while making returned modes - // deterministic enough for injection and overlap calculations. - let power = transverse_power(&components, cell_areas); - let norm = power.norm(); - if norm <= f64::EPSILON { - return 0.0; - } - let scale = 1.0 / norm.sqrt(); - for component in &mut components { - for value in component.iter_mut() { - *value *= scale; - } - } - transverse_power(&components, cell_areas).norm() -} - -fn transverse_power(components: &[&mut Vec; 6], cell_areas: &[f64]) -> Complex64 { - // Local z-normal power flux: integral((E x H*) . z) dA. - let ex = &components[0]; - let ey = &components[1]; - let hx = &components[3]; - let hy = &components[4]; - ex.iter() - .zip(ey.iter()) - .zip(hx.iter()) - .zip(hy.iter()) - .zip(cell_areas.iter()) - .map(|((((ex, ey), hx), hy), area)| { - (*ex * hy.conj() - *ey * hx.conj()) * Complex64::new(*area, 0.0) - }) - .sum() -} - -fn lorentz_overlap(left: &ModeFields, right: &ModeFields, cell_areas: &[f64]) -> Complex64 { - // Symmetric unconjugated Lorentz product for z-normal mode planes. The - // solver currently reconstructs local fields with propagation along local - // z, and Python later permutes labels back to global axes if needed. - let left_cross_right = left - .ex - .iter() - .zip(&left.ey) - .zip(&right.hx) - .zip(&right.hy) - .zip(cell_areas) - .map(|((((ex, ey), hx), hy), area)| (*ex * *hy - *ey * *hx) * Complex64::new(*area, 0.0)) - .sum::(); - let right_cross_left = right - .ex - .iter() - .zip(&right.ey) - .zip(&left.hx) - .zip(&left.hy) - .zip(cell_areas) - .map(|((((ex, ey), hx), hy), area)| (*ex * *hy - *ey * *hx) * Complex64::new(*area, 0.0)) - .sum::(); - (left_cross_right + right_cross_left) * Complex64::new(0.5, 0.0) -} - -fn apply_dominant_e_phase_convention(mut components: [&mut Vec; 6]) { - // Eigenvectors have arbitrary complex phase. Anchor the dominant electric - // sample to be real-positive so plots and saved fixtures are stable between - // runs/backends. - let mut anchor = Complex64::new(0.0, 0.0); - let mut anchor_norm = 0.0; - for component in components.iter().take(3) { - for value in component.iter() { - let norm = value.norm_sqr(); - if norm > anchor_norm { - anchor = *value; - anchor_norm = norm; - } - } - } - if anchor_norm <= f64::EPSILON { - return; - } - let phase = anchor.conj() / Complex64::new(anchor_norm.sqrt(), 0.0); - for component in &mut components { - for value in component.iter_mut() { - *value *= phase; - } - } -} diff --git a/src/operators.rs b/src/operators.rs deleted file mode 100644 index d56d886..0000000 --- a/src/operators.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Maxwell operator assembly. -//! -//! This module contains only matrix construction. It does not choose modes, -//! reconstruct fields, normalize amplitudes, or know about Python. Keeping the -//! algebra here isolated makes it easier to replace individual pieces without -//! touching the eigensolver. - -use num_complex::Complex64; - -use crate::derivatives::Tensor3; -use crate::sparse_matrix::SparseMatrix; - -#[derive(Clone, Debug)] -pub struct SparseDiagonalOperators { - pub p_mu: SparseMatrix, - pub p_partial: SparseMatrix, - pub q_ep: SparseMatrix, - pub q_partial: SparseMatrix, - pub qmat: SparseMatrix, - pub mat: SparseMatrix, -} - -pub fn assemble_sparse_diagonal_operators( - eps: &Tensor3, - mu: &Tensor3, - der_mats: &[SparseMatrix; 4], -) -> SparseDiagonalOperators { - // Diagonal media reduce to a 2N x 2N transverse-electric eigenproblem for - // [Ex, Ey]. The P/Q block notation follows the standard vectorial FDFD - // mode formulation: Q maps transverse E to transverse H up to the - // propagation constant, while P maps transverse H back to transverse E. - // Ez, Hz, and the physical H scale are reconstructed after the eigen solve. - let n = eps[0][0].len(); - let eps_xx = &eps[0][0]; - let eps_yy = &eps[1][1]; - let eps_zz = &eps[2][2]; - let mu_xx = &mu[0][0]; - let mu_yy = &mu[1][1]; - let mu_zz = &mu[2][2]; - let dxf = &der_mats[0]; - let dxb = &der_mats[1]; - let dyf = &der_mats[2]; - let dyb = &der_mats[3]; - - let zero = SparseMatrix::zeros(n, n); - let inv_eps_zz = SparseMatrix::diagonal( - &eps_zz - .iter() - .map(|value| Complex64::new(1.0, 0.0) / *value) - .collect::>(), - ); - let inv_mu_zz = SparseMatrix::diagonal( - &mu_zz - .iter() - .map(|value| Complex64::new(1.0, 0.0) / *value) - .collect::>(), - ); - - // Material-only part of P. The signs encode z-normal cross products: - // transverse H couples to [Ey, -Ex] through the diagonal mu block. - let p_mu = SparseMatrix::block_2x2( - &zero, - &SparseMatrix::diagonal(mu_yy), - &SparseMatrix::diagonal(mu_xx).scale(Complex64::new(-1.0, 0.0)), - &zero, - ); - - // Longitudinal electric elimination. The derivative sandwich - // Df * inv(eps_zz) * Db is the Schur-complement contribution from Ez. - let p00 = dxf - .matmul(&inv_eps_zz) - .matmul(dyb) - .scale(Complex64::new(-1.0, 0.0)); - let p01 = dxf.matmul(&inv_eps_zz).matmul(dxb); - let p10 = dyf - .matmul(&inv_eps_zz) - .matmul(dyb) - .scale(Complex64::new(-1.0, 0.0)); - let p11 = dyf.matmul(&inv_eps_zz).matmul(dxb); - let p_partial = SparseMatrix::block_2x2(&p00, &p01, &p10, &p11); - - // Material-only part of Q. It is the epsilon-side analogue of p_mu. - let q_ep = SparseMatrix::block_2x2( - &zero, - &SparseMatrix::diagonal(eps_yy), - &SparseMatrix::diagonal(eps_xx).scale(Complex64::new(-1.0, 0.0)), - &zero, - ); - - // Longitudinal magnetic elimination. This mirrors p_partial with mu_zz and - // backward/forward derivatives swapped for Yee staggering. - let q00 = dxb - .matmul(&inv_mu_zz) - .matmul(dyf) - .scale(Complex64::new(-1.0, 0.0)); - let q01 = dxb.matmul(&inv_mu_zz).matmul(dxf); - let q10 = dyb - .matmul(&inv_mu_zz) - .matmul(dyf) - .scale(Complex64::new(-1.0, 0.0)); - let q11 = dyb.matmul(&inv_mu_zz).matmul(dxf); - let q_partial = SparseMatrix::block_2x2(&q00, &q01, &q10, &q11); - - let qmat = q_ep.add(&q_partial); - let mat = p_mu.matmul(&qmat).add(&p_partial.matmul(&q_ep)); - - SparseDiagonalOperators { - p_mu, - p_partial, - q_ep, - q_partial, - qmat, - mat, - } -} - -pub fn assemble_sparse_tensorial_operator( - eps: &Tensor3, - mu: &Tensor3, - der_mats: &[SparseMatrix; 4], -) -> SparseMatrix { - // Full tensor media and coordinate transforms cannot use the simpler - // diagonal reduction. Here the eigenvector is [Ex, Ey, Hx, Hy], and Ez/Hz - // are still reconstructed after the solve. Off-diagonal material terms are - // Schur-complemented through eps_zz and mu_zz. - let one = Complex64::new(1.0, 0.0); - let i_scale = Complex64::new(0.0, -1.0); - let dxf = &der_mats[0]; - let dxb = &der_mats[1]; - let dyf = &der_mats[2]; - let dyb = &der_mats[3]; - - let inv_eps_22 = SparseMatrix::diagonal( - &eps[2][2] - .iter() - .map(|value| one / *value) - .collect::>(), - ); - let inv_mu_22 = SparseMatrix::diagonal( - &mu[2][2] - .iter() - .map(|value| one / *value) - .collect::>(), - ); - - // Precompute tensor ratios such as eps_zx / eps_zz. These appear repeatedly - // when eliminating the longitudinal field components. - let eps_20_over_22 = component_div(&eps[2][0], &eps[2][2]); - let eps_21_over_22 = component_div(&eps[2][1], &eps[2][2]); - let eps_02_over_22 = component_div(&eps[0][2], &eps[2][2]); - let eps_12_over_22 = component_div(&eps[1][2], &eps[2][2]); - let mu_20_over_22 = component_div(&mu[2][0], &mu[2][2]); - let mu_21_over_22 = component_div(&mu[2][1], &mu[2][2]); - let mu_02_over_22 = component_div(&mu[0][2], &mu[2][2]); - let mu_12_over_22 = component_div(&mu[1][2], &mu[2][2]); - - // Schur-complemented transverse material blocks. The suffix `_s` means the - // effect of the eliminated z component has already been included. - let mu_10_s = component_sub(&mu[1][0], &component_mul(&mu[1][2], &mu_20_over_22)); - let mu_11_s = component_sub(&mu[1][1], &component_mul(&mu[1][2], &mu_21_over_22)); - let mu_00_s = component_sub(&mu[0][0], &component_mul(&mu[0][2], &mu_20_over_22)); - let mu_01_s = component_sub(&mu[0][1], &component_mul(&mu[0][2], &mu_21_over_22)); - let eps_10_s = component_sub(&eps[1][0], &component_mul(&eps[1][2], &eps_20_over_22)); - let eps_11_s = component_sub(&eps[1][1], &component_mul(&eps[1][2], &eps_21_over_22)); - let eps_00_s = component_sub(&eps[0][0], &component_mul(&eps[0][2], &eps_20_over_22)); - let eps_01_s = component_sub(&eps[0][1], &component_mul(&eps[0][2], &eps_21_over_22)); - - let diag = |values: &[Complex64]| SparseMatrix::diagonal(values); - - // The 4x4 block grid below is the first-order tensorial Maxwell operator. - // The names encode destination/source blocks: a = electric transverse - // components, b = magnetic transverse components, x/y = local grid axes. - let axax = dxf - .matmul(&diag(&eps_20_over_22)) - .scale(-one) - .sub(&diag(&mu_12_over_22).matmul(dyf)); - let axay = dxf - .matmul(&diag(&eps_21_over_22)) - .scale(-one) - .add(&diag(&mu_12_over_22).matmul(dxf)); - let axbx = dxf - .matmul(&inv_eps_22) - .matmul(dyb) - .scale(-one) - .add(&diag(&mu_10_s)); - let axby = dxf.matmul(&inv_eps_22).matmul(dxb).add(&diag(&mu_11_s)); - - let ayax = dyf - .matmul(&diag(&eps_20_over_22)) - .scale(-one) - .add(&diag(&mu_02_over_22).matmul(dyf)); - let ayay = dyf - .matmul(&diag(&eps_21_over_22)) - .scale(-one) - .sub(&diag(&mu_02_over_22).matmul(dxf)); - let aybx = dyf - .matmul(&inv_eps_22) - .matmul(dyb) - .scale(-one) - .sub(&diag(&mu_00_s)); - let ayby = dyf.matmul(&inv_eps_22).matmul(dxb).sub(&diag(&mu_01_s)); - - let bxax = dxb - .matmul(&inv_mu_22) - .matmul(dyf) - .scale(-one) - .add(&diag(&eps_10_s)); - let bxay = dxb.matmul(&inv_mu_22).matmul(dxf).add(&diag(&eps_11_s)); - let bxbx = dxb - .matmul(&diag(&mu_20_over_22)) - .scale(-one) - .sub(&diag(&eps_12_over_22).matmul(dyb)); - let bxby = dxb - .matmul(&diag(&mu_21_over_22)) - .scale(-one) - .add(&diag(&eps_12_over_22).matmul(dxb)); - - let byax = dyb - .matmul(&inv_mu_22) - .matmul(dyf) - .scale(-one) - .sub(&diag(&eps_00_s)); - let byay = dyb.matmul(&inv_mu_22).matmul(dxf).sub(&diag(&eps_01_s)); - let bybx = dyb - .matmul(&diag(&mu_20_over_22)) - .scale(-one) - .add(&diag(&eps_02_over_22).matmul(dyb)); - let byby = dyb - .matmul(&diag(&mu_21_over_22)) - .scale(-one) - .sub(&diag(&eps_02_over_22).matmul(dxb)); - - SparseMatrix::block_grid(&[ - vec![&axax, &axay, &axbx, &axby], - vec![&ayax, &ayay, &aybx, &ayby], - vec![&bxax, &bxay, &bxbx, &bxby], - vec![&byax, &byay, &bybx, &byby], - ]) - .scale(i_scale) -} - -fn component_div(left: &[Complex64], right: &[Complex64]) -> Vec { - left.iter() - .zip(right) - .map(|(left, right)| *left / *right) - .collect() -} - -fn component_mul(left: &[Complex64], right: &[Complex64]) -> Vec { - left.iter() - .zip(right) - .map(|(left, right)| *left * *right) - .collect() -} - -fn component_sub(left: &[Complex64], right: &[Complex64]) -> Vec { - left.iter() - .zip(right) - .map(|(left, right)| *left - *right) - .collect() -} diff --git a/src/python_api.rs b/src/python_api.rs deleted file mode 100644 index f08a37a..0000000 --- a/src/python_api.rs +++ /dev/null @@ -1,371 +0,0 @@ -//! PyO3 extension boundary. -//! -//! The Python package keeps user-facing objects in Python, while Rust owns the -//! numerical kernels. This module translates Python lists of real/imag pairs -//! into Rust tensors, runs the appropriate solver, and converts diagnostics and -//! fields back into simple Python values. - -#[cfg(feature = "python")] -use pyo3::exceptions::PyValueError; -#[cfg(feature = "python")] -use pyo3::prelude::*; - -use crate::{derivatives, diagonal_solver, sparse_matrix}; - -#[cfg(feature = "python")] -type SolvePayload = ( - // complex effective indices - Vec<(f64, f64)>, - // six field components, grouped as component -> mode -> flattened values - Vec>>, - // eigenpair residuals - Vec, - // power-normalization checks - Vec, - // unconjugated Lorentz self-products and the largest normalized - // off-diagonal product after orthogonalization - Vec<(f64, f64)>, - f64, - // backend label and sparse operator diagnostics - String, - usize, - usize, -); - -#[cfg(feature = "python")] -#[pyfunction] -#[pyo3(signature = ( - nx, - ny, - dlf_x, - dlf_y, - dlb_x, - dlb_y, - pmc_x, - pmc_y, - eps_tensor, - mu_tensor, - num_modes, - neff_guess, - direction, - derivative_scale = None, - npml_x = 0, - npml_y = 0, - pml_sigma_max = 2.0, - pml_kappa_min = 1.0, - pml_kappa_max = 3.0, - pml_order = 3, - dmin_pml_x = true, - dmin_pml_y = true, - omega = None, - krylov_dim = 32, - initial_vector = None -))] -fn solve_diagonal_sparse_py( - nx: usize, - ny: usize, - dlf_x: Vec, - dlf_y: Vec, - dlb_x: Vec, - dlb_y: Vec, - pmc_x: bool, - pmc_y: bool, - eps_tensor: Vec>, - mu_tensor: Vec>, - num_modes: usize, - neff_guess: f64, - direction: String, - derivative_scale: Option, - npml_x: usize, - npml_y: usize, - pml_sigma_max: f64, - pml_kappa_min: f64, - pml_kappa_max: f64, - pml_order: i32, - dmin_pml_x: bool, - dmin_pml_y: bool, - omega: Option, - krylov_dim: usize, - initial_vector: Option>, -) -> PyResult { - let n = nx * ny; - let eps = derivatives::tensor_from_flat(&eps_tensor, n).map_err(PyValueError::new_err)?; - let mu = derivatives::tensor_from_flat(&mu_tensor, n).map_err(PyValueError::new_err)?; - let der_mats = sparse_derivative_matrices_for_solve( - (nx, ny), - (&dlf_x, &dlf_y), - (&dlb_x, &dlb_y), - (pmc_x, pmc_y), - &eps, - &mu, - (npml_x, npml_y), - derivatives::PmlProfile { - sigma_max: pml_sigma_max, - kappa_min: pml_kappa_min, - kappa_max: pml_kappa_max, - order: pml_order, - }, - (dmin_pml_x, dmin_pml_y), - omega, - derivative_scale, - ) - .map_err(PyValueError::new_err)?; - let initial_vector = initial_vector.map(|values| { - values - .into_iter() - .map(|(re, im)| num_complex::Complex64::new(re, im)) - .collect::>() - }); - let cell_areas = cell_areas_from_steps(&dlf_x, &dlf_y); - let result = diagonal_solver::solve_diagonal_sparse( - &eps, - &mu, - &der_mats, - &cell_areas, - num_modes, - neff_guess, - &direction, - initial_vector.as_deref(), - diagonal_solver::ShiftInvertOptions { - krylov_dim, - tolerance: 1e-10, - }, - ) - .map_err(PyValueError::new_err)?; - let n_complex = result - .n_complex - .into_iter() - .map(|value| (value.re, value.im)) - .collect(); - let fields = result - .fields - .into_iter() - .map(|component| { - component - .into_iter() - .map(|mode| { - mode.into_iter() - .map(|value| (value.re, value.im)) - .collect::>() - }) - .collect::>() - }) - .collect(); - Ok(( - n_complex, - fields, - result.diagnostics.residuals, - result.diagnostics.power_norms, - result - .diagnostics - .lorentz_norms - .into_iter() - .map(|value| (value.re, value.im)) - .collect(), - result.diagnostics.lorentz_orthogonality_error, - result.diagnostics.backend, - result.diagnostics.operator_size, - result.diagnostics.operator_nnz, - )) -} - -#[cfg(feature = "python")] -#[pyfunction] -#[pyo3(signature = ( - nx, - ny, - dlf_x, - dlf_y, - dlb_x, - dlb_y, - pmc_x, - pmc_y, - eps_tensor, - mu_tensor, - num_modes, - neff_guess, - direction, - derivative_scale = None, - npml_x = 0, - npml_y = 0, - pml_sigma_max = 2.0, - pml_kappa_min = 1.0, - pml_kappa_max = 3.0, - pml_order = 3, - dmin_pml_x = true, - dmin_pml_y = true, - omega = None, - krylov_dim = 32, - initial_vector = None -))] -fn solve_tensorial_sparse_py( - nx: usize, - ny: usize, - dlf_x: Vec, - dlf_y: Vec, - dlb_x: Vec, - dlb_y: Vec, - pmc_x: bool, - pmc_y: bool, - eps_tensor: Vec>, - mu_tensor: Vec>, - num_modes: usize, - neff_guess: f64, - direction: String, - derivative_scale: Option, - npml_x: usize, - npml_y: usize, - pml_sigma_max: f64, - pml_kappa_min: f64, - pml_kappa_max: f64, - pml_order: i32, - dmin_pml_x: bool, - dmin_pml_y: bool, - omega: Option, - krylov_dim: usize, - initial_vector: Option>, -) -> PyResult { - let n = nx * ny; - let eps = derivatives::tensor_from_flat(&eps_tensor, n).map_err(PyValueError::new_err)?; - let mu = derivatives::tensor_from_flat(&mu_tensor, n).map_err(PyValueError::new_err)?; - let der_mats = sparse_derivative_matrices_for_solve( - (nx, ny), - (&dlf_x, &dlf_y), - (&dlb_x, &dlb_y), - (pmc_x, pmc_y), - &eps, - &mu, - (npml_x, npml_y), - derivatives::PmlProfile { - sigma_max: pml_sigma_max, - kappa_min: pml_kappa_min, - kappa_max: pml_kappa_max, - order: pml_order, - }, - (dmin_pml_x, dmin_pml_y), - omega, - derivative_scale, - ) - .map_err(PyValueError::new_err)?; - let initial_vector = initial_vector.map(|values| { - values - .into_iter() - .map(|(re, im)| num_complex::Complex64::new(re, im)) - .collect::>() - }); - let cell_areas = cell_areas_from_steps(&dlf_x, &dlf_y); - let result = diagonal_solver::solve_tensorial_sparse( - &eps, - &mu, - &der_mats, - &cell_areas, - num_modes, - neff_guess, - &direction, - initial_vector.as_deref(), - diagonal_solver::ShiftInvertOptions { - krylov_dim, - tolerance: 1e-10, - }, - ) - .map_err(PyValueError::new_err)?; - let n_complex = result - .n_complex - .into_iter() - .map(|value| (value.re, value.im)) - .collect(); - let fields = result - .fields - .into_iter() - .map(|component| { - component - .into_iter() - .map(|mode| { - mode.into_iter() - .map(|value| (value.re, value.im)) - .collect::>() - }) - .collect::>() - }) - .collect(); - Ok(( - n_complex, - fields, - result.diagnostics.residuals, - result.diagnostics.power_norms, - result - .diagnostics - .lorentz_norms - .into_iter() - .map(|value| (value.re, value.im)) - .collect(), - result.diagnostics.lorentz_orthogonality_error, - result.diagnostics.backend, - result.diagnostics.operator_size, - result.diagnostics.operator_nnz, - )) -} - -#[cfg(feature = "python")] -fn cell_areas_from_steps(dlf_x: &[f64], dlf_y: &[f64]) -> Vec { - // Python supplies mode-plane edges; by this point they have become cell - // widths. Flatten in the same x-major/y-minor order used by material tensors. - let mut areas = Vec::with_capacity(dlf_x.len() * dlf_y.len()); - for dx in dlf_x { - for dy in dlf_y { - areas.push(dx.abs() * dy.abs()); - } - } - areas -} - -#[cfg(feature = "python")] -fn sparse_derivative_matrices_for_solve( - shape: (usize, usize), - dlf: (&[f64], &[f64]), - dlb: (&[f64], &[f64]), - dmin_pmc: (bool, bool), - eps: &derivatives::Tensor3, - mu: &derivatives::Tensor3, - npml: (usize, usize), - pml_profile: derivatives::PmlProfile, - dmin_pml: (bool, bool), - omega: Option, - derivative_scale: Option, -) -> Result<[sparse_matrix::SparseMatrix; 4], String> { - // Build sparse derivative matrices, optionally preconditioned by PML - // stretch matrices and scaled into the nondimensional coordinates used by - // the operator assembly. - let mut der_mats = derivatives::create_d_matrices_sparse(shape, dlf, dlb, dmin_pmc); - if npml.0 > 0 || npml.1 > 0 { - let omega = omega.ok_or_else(|| "omega is required when num_pml is nonzero".to_string())?; - let pml_mats = derivatives::create_s_matrices_sparse_with_profile( - omega, - shape, - npml, - dlf, - dlb, - eps, - mu, - dmin_pml, - &pml_profile, - ); - for (der_mat, pml_mat) in der_mats.iter_mut().zip(pml_mats.iter()) { - *der_mat = pml_mat.matmul(der_mat); - } - } - if let Some(scale) = derivative_scale { - for matrix in &mut der_mats { - *matrix = matrix.scale(num_complex::Complex64::new(scale, 0.0)); - } - } - Ok(der_mats) -} - -#[cfg(feature = "python")] -#[pymodule] -fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(solve_diagonal_sparse_py, m)?)?; - m.add_function(wrap_pyfunction!(solve_tensorial_sparse_py, m)?)?; - Ok(()) -} diff --git a/src/sparse_matrix.rs b/src/sparse_matrix.rs deleted file mode 100644 index 2beb1e6..0000000 --- a/src/sparse_matrix.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::collections::BTreeMap; - -use num_complex::Complex64; - -#[derive(Clone, Debug, PartialEq)] -pub struct SparseMatrix { - pub rows: usize, - pub cols: usize, - col_ptrs: Vec, - row_indices: Vec, - values: Vec, -} - -impl SparseMatrix { - pub fn zeros(rows: usize, cols: usize) -> Self { - Self { - rows, - cols, - col_ptrs: vec![0; cols + 1], - row_indices: Vec::new(), - values: Vec::new(), - } - } - - pub fn eye(size: usize) -> Self { - let values = vec![Complex64::new(1.0, 0.0); size]; - Self::diagonal(&values) - } - - pub fn diagonal(values: &[Complex64]) -> Self { - let mut triplets = Vec::with_capacity(values.len()); - for (index, value) in values.iter().copied().enumerate() { - if value != Complex64::new(0.0, 0.0) { - triplets.push((index, index, value)); - } - } - Self::from_triplets(values.len(), values.len(), triplets) - } - - pub fn from_triplets( - rows: usize, - cols: usize, - triplets: Vec<(usize, usize, Complex64)>, - ) -> Self { - let mut by_col = (0..cols) - .map(|_| BTreeMap::::new()) - .collect::>(); - for (row, col, value) in triplets { - assert!(row < rows); - assert!(col < cols); - if value == Complex64::new(0.0, 0.0) { - continue; - } - *by_col[col].entry(row).or_insert(Complex64::new(0.0, 0.0)) += value; - } - Self::from_column_maps(rows, cols, by_col) - } - - fn from_column_maps(rows: usize, cols: usize, by_col: Vec>) -> Self { - assert_eq!(by_col.len(), cols); - let mut col_ptrs = Vec::with_capacity(cols + 1); - let mut row_indices = Vec::new(); - let mut values = Vec::new(); - col_ptrs.push(0); - for col in by_col { - for (row, value) in col { - if value != Complex64::new(0.0, 0.0) { - row_indices.push(row); - values.push(value); - } - } - col_ptrs.push(row_indices.len()); - } - Self { - rows, - cols, - col_ptrs, - row_indices, - values, - } - } - - pub fn nnz(&self) -> usize { - self.values.len() - } - - pub fn col_ptrs(&self) -> &[usize] { - &self.col_ptrs - } - - pub fn row_indices(&self) -> &[usize] { - &self.row_indices - } - - pub fn values(&self) -> &[Complex64] { - &self.values - } - - pub fn shifted_diagonal(&self, shift: Complex64) -> Self { - assert_eq!(self.rows, self.cols); - let mut columns = (0..self.cols) - .map(|_| BTreeMap::::new()) - .collect::>(); - for (col, column) in columns.iter_mut().enumerate().take(self.cols) { - for (row, value) in self.column_entries(col) { - column.insert(row, value); - } - *column.entry(col).or_insert(Complex64::new(0.0, 0.0)) -= shift; - } - Self::from_column_maps(self.rows, self.cols, columns) - } - - pub fn column_entries(&self, col: usize) -> impl Iterator + '_ { - let start = self.col_ptrs[col]; - let end = self.col_ptrs[col + 1]; - self.row_indices[start..end] - .iter() - .copied() - .zip(self.values[start..end].iter().copied()) - } - - pub fn scale(&self, scale: Complex64) -> Self { - let mut out = self.clone(); - for value in &mut out.values { - *value *= scale; - } - out - } - - pub fn add(&self, other: &Self) -> Self { - assert_eq!((self.rows, self.cols), (other.rows, other.cols)); - let mut columns = (0..self.cols) - .map(|_| BTreeMap::::new()) - .collect::>(); - for (col, column) in columns.iter_mut().enumerate().take(self.cols) { - for (row, value) in self.column_entries(col) { - *column.entry(row).or_insert(Complex64::new(0.0, 0.0)) += value; - } - for (row, value) in other.column_entries(col) { - *column.entry(row).or_insert(Complex64::new(0.0, 0.0)) += value; - } - } - Self::from_column_maps(self.rows, self.cols, columns) - } - - pub fn sub(&self, other: &Self) -> Self { - self.add(&other.scale(Complex64::new(-1.0, 0.0))) - } - - pub fn matmul(&self, other: &Self) -> Self { - assert_eq!(self.cols, other.rows); - let mut columns = Vec::with_capacity(other.cols); - for col in 0..other.cols { - let mut accum = BTreeMap::::new(); - for (inner, right) in other.column_entries(col) { - for (row, left) in self.column_entries(inner) { - *accum.entry(row).or_insert(Complex64::new(0.0, 0.0)) += left * right; - } - } - columns.push(accum); - } - Self::from_column_maps(self.rows, other.cols, columns) - } - - pub fn matvec(&self, vector: &[Complex64]) -> Vec { - assert_eq!(self.cols, vector.len()); - let mut out = vec![Complex64::new(0.0, 0.0); self.rows]; - for (col, value) in vector.iter().copied().enumerate() { - if value == Complex64::new(0.0, 0.0) { - continue; - } - for (row, matrix_value) in self.column_entries(col) { - out[row] += matrix_value * value; - } - } - out - } - - pub fn block_2x2(a: &Self, b: &Self, c: &Self, d: &Self) -> Self { - assert_eq!(a.rows, b.rows); - assert_eq!(c.rows, d.rows); - assert_eq!(a.cols, c.cols); - assert_eq!(b.cols, d.cols); - let rows = a.rows + c.rows; - let cols = a.cols + b.cols; - let mut triplets = Vec::with_capacity(a.nnz() + b.nnz() + c.nnz() + d.nnz()); - append_block_triplets(&mut triplets, a, 0, 0); - append_block_triplets(&mut triplets, b, 0, a.cols); - append_block_triplets(&mut triplets, c, a.rows, 0); - append_block_triplets(&mut triplets, d, a.rows, a.cols); - Self::from_triplets(rows, cols, triplets) - } - - pub fn block_grid(blocks: &[Vec<&Self>]) -> Self { - assert!(!blocks.is_empty()); - assert!(!blocks[0].is_empty()); - let block_cols = blocks[0].len(); - assert!(blocks.iter().all(|row| row.len() == block_cols)); - - let row_heights = blocks.iter().map(|row| row[0].rows).collect::>(); - let col_widths = blocks[0].iter().map(|block| block.cols).collect::>(); - for (row_index, row) in blocks.iter().enumerate() { - for (col_index, block) in row.iter().enumerate() { - assert_eq!(block.rows, row_heights[row_index]); - assert_eq!(block.cols, col_widths[col_index]); - } - } - - let rows = row_heights.iter().sum(); - let cols = col_widths.iter().sum(); - let nnz = blocks - .iter() - .flat_map(|row| row.iter()) - .map(|block| block.nnz()) - .sum(); - let mut triplets = Vec::with_capacity(nnz); - let mut row_offset = 0; - for (row_index, row) in blocks.iter().enumerate() { - let mut col_offset = 0; - for (col_index, block) in row.iter().enumerate() { - append_block_triplets(&mut triplets, block, row_offset, col_offset); - col_offset += col_widths[col_index]; - } - row_offset += row_heights[row_index]; - } - Self::from_triplets(rows, cols, triplets) - } -} - -fn append_block_triplets( - triplets: &mut Vec<(usize, usize, Complex64)>, - matrix: &SparseMatrix, - row_offset: usize, - col_offset: usize, -) { - for col in 0..matrix.cols { - for (row, value) in matrix.column_entries(col) { - triplets.push((row + row_offset, col + col_offset, value)); - } - } -} diff --git a/tests/test_micromode_api.py b/tests/test_micromode_api.py index 905de7b..72ac4a3 100644 --- a/tests/test_micromode_api.py +++ b/tests/test_micromode_api.py @@ -34,7 +34,7 @@ def _strip_grid(nx: int = 5, ny: int = 4) -> tuple[np.ndarray, tuple[float, ...] return eps, x_edges, y_edges -def test_grid_api_solves_with_default_backend(): +def test_grid_api_solves_with_scipy_solver(): eps, x_edges, y_edges = _strip_grid(6, 5) freq = mm.C_0 / 1.55 @@ -53,7 +53,8 @@ def test_grid_api_solves_with_default_backend(): assert data.field_components["Ex"].shape == (6, 5, 1, 1, 2) assert set(data.field_components) == {"Ex", "Ey", "Ez", "Hx", "Hy", "Hz"} run_info = _solver_info(data)["runs"][0] - assert run_info["backend_kind"] in {"diagonal_scipy_reference", "diagonal_sparse"} + assert _solver_info(data)["backend"] == "scipy_arpack_reference" + assert run_info["backend_kind"] == "diagonal_scipy_reference" assert run_info["phase_convention"] == "dominant_e_real_positive" assert run_info["normalization"] == "lorentz_orthogonal_unit_transverse_power" np.testing.assert_allclose(run_info["power_norms"], np.ones(2), rtol=1e-10, atol=1e-10) @@ -69,25 +70,7 @@ def test_grid_api_solves_with_default_backend(): assert abs(anchor.imag) <= 1e-10 * max(abs(anchor), 1.0) -def test_default_backend_prefers_scipy_when_available(): - pytest.importorskip("scipy") - eps, x_edges, y_edges = _strip_grid(5, 4) - - data = mm.solve_grid( - eps_xx=eps, - x_edges=x_edges, - y_edges=y_edges, - wavelength=1.55, - num_modes=1, - target_neff=2.5, - krylov_dim=16, - ) - - assert _solver_info(data)["backend"] == "scipy_arpack_reference" - assert _solver_info(data)["runs"][0]["backend_kind"] == "diagonal_scipy_reference" - - -def test_rust_backend_can_be_selected_explicitly(): +def test_scipy_solver_reports_operator_diagnostics(): eps, x_edges, y_edges = _strip_grid(5, 4) data = mm.solve_grid( @@ -98,30 +81,12 @@ def test_rust_backend_can_be_selected_explicitly(): num_modes=1, target_neff=2.5, krylov_dim=16, - backend="rust", ) - assert _solver_info(data)["backend"] == "native_shift_invert" - assert _solver_info(data)["runs"][0]["backend_kind"] == "diagonal_sparse" - - -def test_scipy_backend_alias_selects_scipy_reference_path(): - pytest.importorskip("scipy") - eps, x_edges, y_edges = _strip_grid(5, 4) - - data = mm.solve_grid( - eps_xx=eps, - x_edges=x_edges, - y_edges=y_edges, - wavelength=1.55, - num_modes=1, - target_neff=2.5, - krylov_dim=16, - backend="scipy", - ) - - assert _solver_info(data)["backend"] == "scipy_arpack_reference" - assert _solver_info(data)["runs"][0]["backend_kind"] == "diagonal_scipy_reference" + run_info = _solver_info(data)["runs"][0] + assert run_info["backend_kind"] == "diagonal_scipy_reference" + assert run_info["operator_size"] == 2 * eps.size + assert run_info["operator_nnz"] > run_info["operator_size"] def test_materials_api_matches_component_api_for_diagonal_grid(): @@ -150,35 +115,30 @@ def test_materials_api_matches_component_api_for_diagonal_grid(): assert from_materials.field_components["Ex"].shape == (5, 4, 1, 1, 2) -def test_scipy_reference_backend_matches_rust_for_diagonal_grid(): - pytest.importorskip("scipy") +def test_scipy_solver_handles_diagonal_grid(): eps, x_edges, y_edges = _strip_grid(5, 4) freq = mm.C_0 / 1.55 - common = { - "eps_xx": eps, - "x_edges": x_edges, - "y_edges": y_edges, - "freqs": [freq], - "num_modes": 2, - "target_neff": 2.5, - "krylov_dim": 18, - } - rust = mm.solve_grid(**common, backend="rust") - reference = mm.solve_grid(**common, backend="scipy-reference") + data = mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + freqs=[freq], + num_modes=2, + target_neff=2.5, + krylov_dim=18, + ) - np.testing.assert_allclose(reference.n_complex.values, rust.n_complex.values, rtol=1e-8, atol=1e-8) - run_info = _solver_info(reference)["runs"][0] - assert _solver_info(reference)["backend"] == "scipy_arpack_reference" + run_info = _solver_info(data)["runs"][0] + assert _solver_info(data)["backend"] == "scipy_arpack_reference" assert run_info["backend_kind"] == "diagonal_scipy_reference" - assert run_info["operator_size"] == _solver_info(rust)["runs"][0]["operator_size"] - assert run_info["operator_nnz"] == _solver_info(rust)["runs"][0]["operator_nnz"] + assert run_info["operator_size"] == 2 * eps.size + assert run_info["operator_nnz"] > run_info["operator_size"] np.testing.assert_allclose(run_info["power_norms"], np.ones(2), rtol=1e-10, atol=1e-10) assert run_info["lorentz_orthogonality_error"] < 1e-8 -def test_scipy_reference_backend_matches_rust_for_pml_and_tensorial_paths(): - pytest.importorskip("scipy") +def test_scipy_solver_handles_pml_and_tensorial_paths(): eps, x_edges, y_edges = _strip_grid(4, 3) pml_common = { @@ -191,14 +151,12 @@ def test_scipy_reference_backend_matches_rust_for_pml_and_tensorial_paths(): "pml": (1, 0), "krylov_dim": 18, } - rust_pml = mm.solve_grid(**pml_common, backend="rust") - reference_pml = mm.solve_grid(**pml_common, backend="scipy-reference") + pml_data = mm.solve_grid(**pml_common) - np.testing.assert_allclose(reference_pml.n_complex.values, rust_pml.n_complex.values, rtol=1e-8, atol=1e-8) - pml_run = _solver_info(reference_pml)["runs"][0] + pml_run = _solver_info(pml_data)["runs"][0] assert pml_run["backend_kind"] == "diagonal_scipy_reference" - assert pml_run["operator_size"] == _solver_info(rust_pml)["runs"][0]["operator_size"] - assert pml_run["operator_nnz"] == _solver_info(rust_pml)["runs"][0]["operator_nnz"] + assert pml_run["operator_size"] == 2 * eps.size + assert pml_run["operator_nnz"] > pml_run["operator_size"] tensor_common = { "eps_xx": eps, @@ -213,36 +171,30 @@ def test_scipy_reference_backend_matches_rust_for_pml_and_tensorial_paths(): "target_neff": 2.2, "krylov_dim": 20, } - rust_tensor = mm.solve_grid(**tensor_common, backend="rust") - reference_tensor = mm.solve_grid(**tensor_common, backend="scipy-reference") + tensor_data = mm.solve_grid(**tensor_common) - np.testing.assert_allclose(reference_tensor.n_complex.values, rust_tensor.n_complex.values, rtol=1e-8, atol=1e-8) - tensor_run = _solver_info(reference_tensor)["runs"][0] + tensor_run = _solver_info(tensor_data)["runs"][0] assert tensor_run["backend_kind"] == "tensorial_scipy_reference" - assert tensor_run["operator_size"] == _solver_info(rust_tensor)["runs"][0]["operator_size"] - assert tensor_run["operator_nnz"] == _solver_info(rust_tensor)["runs"][0]["operator_nnz"] + assert tensor_run["operator_size"] == 4 * eps.size + assert tensor_run["operator_nnz"] > tensor_run["operator_size"] -def test_scipy_reference_backend_matches_rust_for_transformed_grid(): - pytest.importorskip("scipy") +def test_scipy_solver_handles_transformed_grid(): eps, x_edges, y_edges = _strip_grid(4, 3) - common = { - "eps_xx": eps, - "x_edges": x_edges, - "y_edges": y_edges, - "freqs": [mm.C_0 / 1.55], - "num_modes": 1, - "target_neff": 2.5, - "angle_theta": 0.08, - "angle_phi": 0.25, - "krylov_dim": 20, - } - rust = mm.solve_grid(**common, backend="rust") - reference = mm.solve_grid(**common, backend="scipy-reference") + data = mm.solve_grid( + eps_xx=eps, + x_edges=x_edges, + y_edges=y_edges, + freqs=[mm.C_0 / 1.55], + num_modes=1, + target_neff=2.5, + angle_theta=0.08, + angle_phi=0.25, + krylov_dim=20, + ) - np.testing.assert_allclose(reference.n_complex.values, rust.n_complex.values, rtol=1e-8, atol=1e-8) - assert _solver_info(reference)["runs"][0]["backend_kind"] == "tensorial_scipy_reference" + assert _solver_info(data)["runs"][0]["backend_kind"] == "tensorial_scipy_reference" def test_materials_api_accepts_full_tensor_grid(): @@ -503,7 +455,7 @@ def test_materials_subpixel_averaging_helpers(): assert materials.shape == (2, 2) -def test_angle_and_bend_use_tensorial_rust_path(): +def test_angle_and_bend_use_tensorial_path(): eps, x_edges, y_edges = _strip_grid() data = mm.solve_grid( @@ -551,7 +503,7 @@ def test_full_tensor_grid_supports_angle_and_bend_transform(): krylov_dim=18, ) - assert _solver_info(data)["runs"][0]["backend_kind"] in {"tensorial_scipy_reference", "tensorial_sparse"} + assert _solver_info(data)["runs"][0]["backend_kind"] == "tensorial_scipy_reference" assert np.isfinite(data.n_complex.values).all() assert np.isfinite(data.field_components["Ex"].values).all() diff --git a/tests/test_mode_solver_fixtures.py b/tests/test_mode_solver_fixtures.py index 2512a02..5baf67e 100644 --- a/tests/test_mode_solver_fixtures.py +++ b/tests/test_mode_solver_fixtures.py @@ -93,18 +93,6 @@ def test_local_fixture_comparison_uses_staggered_rasterization_for_z_strips(): assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] -@pytest.mark.slow -def test_scipy_fixture_comparison_uses_staggered_rasterization_for_z_strips(): - pytest.importorskip("scipy") - manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) - entries = {entry["case_id"]: entry for entry in manifest["cases"]} - - for case_id in ("strip_z_scalar_single", "group_index_silicon_strip"): - status = _compare_local_case(EXTENDED_FIXTURE_ROOT, entries[case_id], backend="scipy_reference") - assert status["status"] == "pass" - assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] - - @pytest.mark.slow def test_local_production_fixture_matrix_passes(): from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES @@ -125,27 +113,6 @@ def test_local_production_fixture_matrix_passes(): assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] -@pytest.mark.slow -def test_scipy_production_fixture_matrix_passes(): - pytest.importorskip("scipy") - from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES - - manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) - entries = {entry["case_id"]: entry for entry in manifest["cases"]} - production_ids = [ - case_id - for case_id, recipe in _LOCAL_CASES.items() - if recipe.get("support", "production") == "production" and case_id in entries - ] - - assert production_ids - for case_id in production_ids: - status = _compare_local_case(EXTENDED_FIXTURE_ROOT, entries[case_id], backend="scipy_reference") - assert status["support"] == "production" - assert status["status"] == "pass", f"{case_id}: {status['summary']}" - assert status["n_complex_max_abs_error"] <= status["n_complex_atol"] - - @pytest.mark.slow def test_unsupported_fixture_matrix_is_explicit(): from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES diff --git a/uv.lock b/uv.lock index e8b6a02..5994e59 100644 --- a/uv.lock +++ b/uv.lock @@ -805,30 +805,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, ] -[[package]] -name = "maturin" -version = "1.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/612d23d33ec21b9ae7ece7b3f0dd5f9dfd57b4009e9d2938165869ebd6ae/maturin-1.13.3.tar.gz", hash = "sha256:771e1e9e71a278e56db01552e0d1acfd1464259f9575b6e72842f893cd299079", size = 357934, upload-time = "2026-05-11T07:43:39.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/66/18c2aaac0b2a5dea9f1db5984ce83b905ad205cfc7c02d0091e707c0c2e7/maturin-1.13.3-py3-none-linux_armv6l.whl", hash = "sha256:3cc13929ca82aefa4adbf0f2c35419369796213c6fb0eb24e914945f50ef5d8c", size = 10190971, upload-time = "2026-05-11T07:43:10.431Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/26a988d092e4fd6a9523d46d44400a46cad7cdf3fd206ce702240c748aee/maturin-1.13.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:53b08bd075649ce96513ad9abf241a43cb685ed6e9e7790f8dbc2d66e95d8323", size = 19716714, upload-time = "2026-05-11T07:43:36.911Z" }, - { url = "https://files.pythonhosted.org/packages/82/5c/f3fd0e184255d9fc7e272c62af3dfa84c617b2577ef83af9ce615f5279cc/maturin-1.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4cd478e6e4c56251e48ed079b8efd55b30bc5c09cf695a1bdafaeb582ee735a0", size = 10194726, upload-time = "2026-05-11T07:43:07.05Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e1/f4edb69fb647b77c4769a9bfd4d6fb62961e653d164bc277ecdffac3ab61/maturin-1.13.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:a2675e25f313034ae6f57388cf14818f87d8961c4a96795287f3e155f59beb11", size = 10172781, upload-time = "2026-05-11T07:43:40.796Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7d/a1be934690cdcc3c6609769ceaad322ab7501c2ee5bafcac1b14d609e403/maturin-1.13.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:4667ef609ab446c1b5e0bfe4f9fb99699ab6d8548433f8d1a684256e0b67217f", size = 10682670, upload-time = "2026-05-11T07:43:13.132Z" }, - { url = "https://files.pythonhosted.org/packages/18/f5/372ae19b72ce8f6e37e5864ae4dc5b252ee9fce0619ccc3aa366aa3a7f97/maturin-1.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3db93337ed97e60ffc878aa8b493cd7ae44d3a5e1a37256db3a4491f57565018", size = 10060363, upload-time = "2026-05-11T07:43:21.107Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5b/c68340cca09368af0df80965dfabed4234205a492a93da00793c7b9aae20/maturin-1.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1cc0a110b224ca90406b668a3e3c1f5a515062e59e26292f6dbaf5fd4909c6f3", size = 10017551, upload-time = "2026-05-11T07:43:33.916Z" }, - { url = "https://files.pythonhosted.org/packages/28/1e/f90fb2b000bad9e6d850cd5afb88b2f1e2a279cfb4de02ea40078484690e/maturin-1.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:c00ea6428dea17bf616fe93770837634454b28c2de1a876e42ef8036c616079a", size = 13301712, upload-time = "2026-05-11T07:43:26.492Z" }, - { url = "https://files.pythonhosted.org/packages/be/58/1670f68a8f04ccd7b90df11047bd9a046585310e84e1967cc9849cd1c5a3/maturin-1.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49fd6ab08da28098ccf37afca24cdba72376ba9c1eedf9dd25ff82ed771961ff", size = 10946765, upload-time = "2026-05-11T07:43:16.135Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/00c955c2ef134817b1a7bdaa76b0309e9c5291eb17d9ff88069eecd08bc2/maturin-1.13.3-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:b6741d7bf4af97da937528fd1e523c6ab54f53d9a21870fa735d6e67fd88e273", size = 10388661, upload-time = "2026-05-11T07:43:18.727Z" }, - { url = "https://files.pythonhosted.org/packages/97/c6/cbf8a51dde19c19aeba0d9b075095a2effb9b31fd312b1aae3ac79f8aea2/maturin-1.13.3-py3-none-win32.whl", hash = "sha256:0ef257e692cc756c87af5bea95ddfe7d3ac49d3376a7a87f728d63f06e7b6f8b", size = 8901838, upload-time = "2026-05-11T07:43:23.76Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ff/c6a50a59dc8313097d43ac5f4d74df6a500c8cb62b0dc9e054f53e203a48/maturin-1.13.3-py3-none-win_amd64.whl", hash = "sha256:def4a435ea9d2ee93b18ba579dc8c9cf898889a66f312cd379b5e374ec3e3ad6", size = 10340801, upload-time = "2026-05-11T07:43:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/6c/93/e32e79333f0902ba292b996f504f5f06be59587f7d02ab8d5ed1e3066445/maturin-1.13.3-py3-none-win_arm64.whl", hash = "sha256:2389fe92d017cea9d94e521fa0175314a4c52f79a1057b901fbc9f8686ef7d0b", size = 9706562, upload-time = "2026-05-11T07:43:31.743Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -847,13 +823,14 @@ dependencies = [ { name = "matplotlib" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "xarray", version = "2026.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.optional-dependencies] dev = [ - { name = "maturin" }, { name = "packaging" }, { name = "pkginfo" }, { name = "pyright" }, @@ -864,16 +841,11 @@ dev = [ { name = "tomli" }, { name = "twine" }, ] -scipy = [ - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] [package.metadata] requires-dist = [ { name = "h5py", specifier = ">=3.10,<4.0" }, { name = "matplotlib", specifier = ">=3.8,<4.0" }, - { name = "maturin", marker = "extra == 'dev'", specifier = ">=1.7,<2" }, { name = "numpy", specifier = ">=2.2.6,<2.5.0" }, { name = "packaging", marker = "extra == 'dev'", specifier = ">=24.2" }, { name = "pkginfo", marker = "extra == 'dev'", specifier = ">=1.12.1.2" }, @@ -882,12 +854,12 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5,<8" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6,<4.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0,<1" }, - { name = "scipy", marker = "extra == 'scipy'", specifier = ">=1.11,<2.0" }, + { name = "scipy", specifier = ">=1.11,<2.0" }, { name = "tomli", marker = "extra == 'dev'", specifier = ">=2,<3" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5,<7" }, { name = "xarray", specifier = ">=2023.8,<2026.5.0" }, ] -provides-extras = ["scipy", "dev"] +provides-extras = ["dev"] [[package]] name = "more-itertools" From b9116efa87559753c954b72d3987d9fe3e007288 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 05:55:00 +0200 Subject: [PATCH 11/16] Clarify MicroMode positioning --- README.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c3a2b6c..f77d794 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # micromode An **electromagnetic mode solver** using the **[FDFD method](https://en.wikipedia.org/wiki/Finite-difference_frequency-domain_method)** on a **[rectilinear Yee-grid](https://en.wikipedia.org/wiki/Finite-difference_time-domain_method)**. +MicroMode is a transparent, grid-first SciPy mode solver for rasterized +photonics workflows. ```bash pip install micromode @@ -15,16 +17,25 @@ pip install micromode ## Why Use It? -- **Grid-first API**: pass arrays directly, with no required geometry model. -- **Auditable SciPy solver**: sparse operators are assembled in Python and - solved with SciPy/ARPACK. -- **Practical** outputs: fields, `n_eff`, `k_eff`, mode area, polarization fractions, - Lorentz overlaps, plotting, dataframe export, and HDF5 save/load. +- **Grid-first API**: pass already-rasterized material tensors and grid edges + directly, with no required CAD or geometry model. +- **Readable SciPy implementation**: sparse operators are assembled in Python + and solved with SciPy/ARPACK, so the numerical path can be inspected by users + who do not want to trust a custom native solver. +- **Small integration surface**: MicroMode is the solver piece you embed after + geometry has already been rasterized by an FDTD, FEM, or custom photonics + pipeline. - **Tensor-aware**: supports scalar, diagonal anisotropic, and full tensor material - grids. + grids, plus transformed angle and bend solves. +- **Practical outputs**: coordinate-aware fields, `n_eff`, `k_eff`, mode area, + polarization fractions, Lorentz overlaps, plotting, dataframe export, and + HDF5 save/load. - Works for both **2D cross sections and 1D slices**. -You give it a material grid. It returns guided modes: effective indices, six-component fields, polarization metrics, mode area, overlaps, diagnostics, plots, and HDF5 output. MicroMode is intentionally not a CAD or geometry package. It is the solver piece you use after geometry has already been rasterized onto a mode-plane grid. +MicroMode is intentionally not a full simulation platform or geometry package. +That is the point: you give it a mode-plane material grid, and it returns guided +modes with the diagnostics and result helpers needed by downstream simulation +workflows. _Micromode is the **default mode solver** in the [BEAMZ FDTD engine](https://github.com/beamzorg/beamz)._ @@ -103,3 +114,8 @@ the shift-invert eigenproblems with [SciPy/ARPACK](https://docs.scipy.org/doc/scipy/reference/sparse.linalg.html). That makes the numerical method easier for academic users to inspect and debug, while still using a trusted sparse eigensolver stack. + +This means MicroMode is differentiated by workflow and inspectability rather +than by a custom solver engine: it is built for users who already have +permittivity on a Yee grid and want a focused, auditable mode-solver component +that fits cleanly into a larger photonics pipeline. From 0414b73f367229eb1593183ce385c9da333e61ca Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 21:42:39 +0200 Subject: [PATCH 12/16] Enhance SOI hybridization example by adding TE fraction and field profile plots, and update documentation accordingly. --- README.md | 2 +- docs/assets/hybridization_sweep.png | Bin 434620 -> 221365 bytes examples/README.md | 2 +- examples/soi_hybridization_sweep.py | 51 +++++++++++++++------------- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f77d794..12a6dc0 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ uv run --extra dev python examples/tidy3d_modal_sources_monitors.py The SOI hybridization example sweeps the width of a 220 nm silicon ridge and solves several modes at each step. It shows how nearby modes exchange character as the geometry changes by plotting effective index and TE fraction across the -sweep, then rendering representative field profiles. +sweep in separate figures, then rendering representative field profiles. ```bash uv run --extra dev python examples/soi_hybridization_sweep.py diff --git a/docs/assets/hybridization_sweep.png b/docs/assets/hybridization_sweep.png index 195b7b26775f79a2daa39c13ea18f318c49ef03f..036ed63d816d1bf1e8bd5392e75eb83f4d2565d8 100644 GIT binary patch literal 221365 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 C9ps7t literal 434620 zcmeFZWmp{Dwk_O12oN*@fWPMf%sIP>Or*YOu$Be?~<}NVvVb zYZ(nn|K$AT)@J`XdOo_5^t8qFTUS_#2?hJGQxdR$dl~CT`LE+3kaIKU+u-E`5rMm) z*P(7Y|30AcQ3S`_+CZQ_=-<2kW5cZD|CcqGcO?cjU#=XT$G&{RjKaahC^BuR<+Q+L zH(^iF6%udUJ$Na_)6{{{XafrD1XX-)>IL0y4uaxBn)-pw;KXvDqxI-Co9~077@%=0 z?&QqYn}Fc$)>BRmle1LpeEfw98IL8uhq+Sz3z*|!`v&R0&P1|4&M2;612|-gzsLyg zsMRDYx)erXx=4&0WiYFga0N`Ds`H*A%+`&5g~2UnQ1w7`{bulQ@9Dj4`$ z5RkDF*{fMm3QV<=+PDMKNk9OLu}=Z|$5m?oNaE{IG}^rV$E^gMHU9HfP>ZyohowF4 z#{V(B|9)vUP4fR9u7A(Qf7$qd?&H5q$KPK3ALJlY;j>tOyK`VuC0>Pjd}gL#Y02ED z!E}6lJTEINED!VFZwhAnk0&DiG1ik*zdcdOgI}b88{|avpKqk?$0LrDoe*me=lqZQ zeOJFJR{9)STk$92&5TVgSBg>NJ?>g0i|L^mI;=)M?|9S;%S*?kD znJ5A8-?b*|nXYn(d-=rlq|A-+1yh{TmeImReB(8$M4gIKi&sLTu#?`ej|+U(O5r{r zy0NJYzK9->Lhl;T@ceZ~9d_Agf4u^>`tnWcJh#3n(o^C0c^`JO2=epu^MC;dq zZ)RrpLPO$sNKWi@)(v%i+O$Bp{0DtCDg^+Be~sLS{}C)3p;iD>*JdRmj7q0hdI&v|9Nf_c)s?Y zE?skXSdn2oE5S5>czD=uhUH)P00QZd{>SGsd+6crzS-{-Sn;MzzZ*p#rKo4@66RR5 zbQd{`(C)SzA)N6(nX*BBcJh}~r1KaH40(3FaDxsK5b^!%thvVj@!4`;W@x;$N%hHI z|L)a{0*C8cE4qE)P0)Do52u^Kw$RC#bud+Sl>_W5W8_Nikje_&RHzQ-!$#hQL9B+qb3H8fxNyF`CAW7?3!najHMC!g}_} z#AE!m@9o_2+#K@6Od#hQj25m>;HW(HfbG%_g1@G3JoG<|xneu$<}_&T(dYB^a2m&< zY}F*CV)QQ-sSHweiG%c0pMJr~nj?f&Z~Utrl@A@A($@!~3~GOyBkT$Nzl`PfI@T@z z1Vg-me?k~ycS{NiMi;J;3-lvWsxlomQb&o=JGHg7vx`K3-4Toj7l6`#8^Ki7ey5Ie zUJC(FCmS{-_gzAs5skmrGs#+CUyt4iJct0(9tQpeFw_5AL=@PCH(eFccCsP%==Qxk z&5`0HxVL8K*6ran?~|jTpg`ns^VbD6xc=J>oYXB`6X zBOp5yMcX?&9EiQ+QT4QVi(BWEoB3s=)?-Gz z)cTdh)t9`Am4KBfs(pr?q(88OtJ)oWbRV3M^p(RqJ5?d1{!G!+f?TMfqsi=mlM}gg zb#6eZ#Q;>;g}bjBEp-!pRK4%U891*^2e*|{qJy7#x0e`fAv+l=;7%HOI=+yUNrwU- zb=O@K&o2-K=39KXWQUFQ(zB{=XhFhUb?M-N=a$84fETK9U&aq74bk zjY`5=Nh)xImn`e=E`Qzqcoo+81C@&PyOS(&CXDYP!RJW!7?+NCu3jx@|2!LAlAnTdl2ylv#Zcz{jkV;Vb*qbsg^ug=9z~)_2Ie~5EdcYnL7C! z#vHCp7F8wk#Ju%T>PVY*gxBL{^$LLbpdjb&cqZj95N zm`&YKD(|3nUlV#}e#H^$8+o8Nm|2+PZIWS9UtF5-o0;`dqD!obxMND!Bx0?wu0GREKl30~OO1QB`TdFNmF&0!CaV@kwgzg+w@@Nb=W9O#U(-se( zjr`H;+J!r!KTi|E70C|!b?Vyw^3aMocYe2Or&5JIkFF}!CK`#jp!Jms0s7;w5l?7{ zqn$$vI<{pa<)mIPHho4@ufeChrXbqh{s-6hT8LJ_Ps28G`@9yGd(%1pn-i^s&QwQV z1vY_|<2e(nR|*ZQ?3*key|@jRBF@bEX3r9bZTTf1CTnc}2q3+)2I-7C7nbK9>`wPb1fV+rrbA`+G8^gPUQ&eJ=CUm zk{W9nYsB+#g-jV+J8fvi_53l$H^&Q$xaqg`2Yzb>jD{c85*C?)e#qP%4Ux7^{Jm_Ps@`!b5iekPp?!W-%lcdSgKcC2C?|dRRMauf^ zw%+)6wO_~21&7IkC;YR>pfTV9dAdj^r;ENzESw&H@}#t9q~)wA5C8wnm*+u zJ&9h$hZEoqH$PX-8KtJAbdInxu(!9@3J7SBSUbalt33D^ioV3b!I_JpqNb+S(bL-) z_=g9)eL`-B2Bh-~-BXsIMMY4&bTqa5Wl7Kl)b~Tp|S5)0DS0yy6Ubh(fY3-oK z{EnIwZX1^c()dH?nXB%WtiFP#NH`nXmzd4Wi<3@3qgpYJX8bb~7 zqSW!QU8_1vLN6VmvY0&&emj3Kf5=?=I6VN9b5@G9Peb2EmV4IwQ`sNZ3vZ4VtTp|aV_l{8sLlCMEq&YK}^L^i$XC_sN` zMuBbKhtjwUUJ|t?MG#$gU}?NNXMpsbuvwqGdNlr&h)DnVr&n`9Z~x^C{O1VcAPx6=s44M`4_?UFKI8a8rG8IeLj*9?oJk))VVJNoNfU#J^_HroQ<>~)0C8ym}&ur zbzx!QoiT0oaRZx_tcpP?qYWV4Nyd$%q@x?}7hWh-WC+lSlD@7<4LC87&m4X=lSuMS zKH}jP09xJDMZA#?;GOEf@Dm8c+vMTprB3yOFXbV(vG-+t{)4GfK(?;Qy&Z&Bz zqeuo|$<4A8gF_o_wXVuyA8g6Klr8?rZy*e5xx*WYEdZ;=vnZ8x^p{3K81*!WG}cn(8?_4bAy9y`=?JJn%PRKl`CG12O7OE}Y%40qCjMEJ?3lGMAc5 z{CVvE2#O4%NgEmEmmn=M!xVf&Pg?q|V1?a2DeyDgt%cB4j{2EA^b?#lxg|+(2^&$3 z=l*_m-~;JvUgPqq5N@(_NnI3vhHf}~B{ye@Z5 z8-Pqn`@-lHN9M(@PuAj5jKA&Z?rDL>2njog)>O$fV2Qw+qqf~EAc?~l!=os+^<6{`6cZTh8;wGivEL`<5{!|70h=m9;NCAaTZ1o z3+8-1rWOSij_p#}sd`_n=vWiJ5DM{j^JwPIZ$SJZtboXhs^Gqu?yfbbcTDLY|H71| zDjxIt@e1p=fiGe5FsmaG5Qts=QIbZ3z>kaF((Q643*>U6nb1}^{aJaQe~Ks1nTj7I z%ZdZZSB6_n(_!|GdYL|`7|9(4w?N+!-mRP8HF@u9ZX?97mmYaJ6^l;pn^$=$C(~`L zFDI0>s4u=%?Kbmp+S;V&m&)q)kIwM&j~_EAKGiKz>0;+R`t90*DbDT<18Kym5TxT- zQyBpvkMQERdQrCHG6fZS%o1LZ}bIrGu`H;+=$jK+O4&!5pLUBt-`*mhqA~ zk9w4Q8=oRfG*qD5KR0AcxpyF*i0@g`BwbV|;U7wT%hJC6vj91271p-Zui>FdX1>@yTi#Us8cG$*Lk~l_gM+n<7qz)5E?E3O@dN@)%exu!k>}wOxE#F z2ww0911Wi=F&nuxN!QOgnY*MzUpbg-81rg%W(gg0l{;T25PRf?vfhbdu*76st{x=x z<{g|HHk-p=L?z0t1?4JA*PAcE~F)xHyMWf?o z`Q>v;P%xq`K{aofiv3=A*Z2|yGSZG%mz@g5{N(%Y{DJF^x%!JX-4}e=KkwDnA@CY; ztx4~3EJ>wl2xVvp6*~*hkP`pR5kDib zTw{88efFKhr!f5PKkqwb`Bmoj@e2kSwE9xd|KM=!tjY;KuvnrFTa#T3mo^f+?7G}k z_v6gsG-8WNDTzoZ!n=CatJ`|GhQ(Je|CSOleV{LJ?T?X8egDT>1GhPkNjo5ctzG#d zo_)nlJ!`aN(F^x__M7xOQ|-r(9^U?j@$$8@-+cdFL%Ba4kcZKCj*5yh+>DmPh+DFxjQ^557IA$>3$O9RI z99B*yc;GX6K*lp$_bLBEUEXH6cx?A}R9qKsu3MWl6)F*t*NeW7loAK=7^&8nRUh?-CoiY1Lv#`X0P=ENyTa?FM%+L95b1PtA>I` zJp~2``&CpHeb5U(EJ4RjxmH4juCPZ>xJqc^7X_B&5-Ty)itnUUZfm;;X8o{NQXuyn zc`cZ+9%RZllap(7Nj-pLMnFERWAatByi#Q_fBg`b! zb}^XNt&h*K`4^Q<@=|pImr-_Yt=K8yerhT`bh`4@=|WIb;CV0Dex>8H_TC0y!^5}C zN|IBfLFzpv_%k5ppCW~2+9FSXL~xI+C4h_d8n#~>)_a!$7?CWhb9{1gLu#V0WiaqW z0CjSrhDcKnJd6=TjcI$#`)u?N3H%*b$UN<~R9bKusCJ_$P)WE#@}v|OlNQ=Ui$d8* zU}DZcqo2gsozfO9yTL94)zDb%zKLI<&#bRjNK1{GU$Fq;cpD4;Gp+VLx(8L~$A z=L?DFKeUnIQfvBNftZHY1*}|IPl@*?^qj|PYI}BZ8E4^uD%vhfxjIuy&nDG4k|J_yg(OOv_CK^)6iQ=u6vj$LXk5R6Ew zGc!9DIA9F)7uy*oi`eOlfq5^ys)Axaf6(t}0>< zpe1Krug^DjD$=eB15pLwtu}`1Ke5jrz~_An8jn);H<=jDU$*yML*aAT;G2s{?PBSz zpE0w}kpMeawn`^q@8D47f9`7HIc0UR5p-<>_Xid?enlm)t<698>MgOq%%JqmWn~)f z-`6F$X9eXOr<7$a@%v2>T419_=hMW5E}h?QnLuE2f08iAx0fp0r#=majWw#{W`2qZ z4?Z50(2=HVa#`T4Gd0?1+Ko^t`h`E3o@W^U@)lN&YOh3i=_L6oR3U`(zqEKG-s6H~ z45p?cfw-RU6+ZrzPFJq6*eiUtc9=>_S&Af+Bz}p4F%HDb04x0<1%BCZU3%efjyiAK zK2RLn!B@xa{Y~?M)vABMs@7#DXU?<16K98b+r^ZH2|S_M5raR#iw)u(7*O(XTx|E| z?a(JtsAYY`#1N^uLNcg9=pDz~?X!`Rb8DEj$0N#f;uN$5z2)uzg}Aj_F*vaUzVVd= zxbvrQbf1k>rdPv`i9G~T_(5+m0tl|LJ1QbwNccjr8Lzht`d*mq-nHp@kKQ(ylaF%u zqtB?F$(>u8G)~XdM}OsSO7?Erie+4%h|xLccn*!bYFx3)O=^Ekh((7 zFm|fIGsh?**|Ex|_H`27YY`f45t_jS7(`Ub*Z)&*NhnKtXlG6?%g;ddl#4=N__43b z+*hNMF^4hIXzLZCMjp^QHa5S z0j?}#uQ`vuiv=+4zIKC;?$)xr@^8}8KN)w&5|y(epynnD-R^-mVH%ME^xEuXOmU)J zF=99t0)mpDbw0PfSI?+Z#uy@0_Xm@;I$7y~OemKg&`jo-x{V)o$W*ejwsCe=Ht^)n zZ?1mj27#2y%Bi?KPwYK>b0#c3ZC_jG=-PPNzxK^(RqFfw^XY1Dn1eo{g*i=_XCJA1 zh{%I`tb5(q(igGuJW28D0Q8NQPSScpqpd2m9qzZfook=+t<3WYO`JNFsXvJIjTGG$ zDy&L!HkU5rE)-v|7KzNbizE=N&7i{RiRsU0BOibF>w{rTroC!pE|=&JA#IlMDqg?i+#&l@y1%Dkp!F;*|Zs76z9p zcI6ZDvGSjGI904`3F;H(Na?-q+|eYR9Gvk#hGq68^9Kp;9`@WRjQrKKgil=3U&U?kxDbiOQk8;aw+v1q?z zu}k}5WBLlw=snGL%8LPd&7Az%&MsS9S-`K7zoM70hqr~QfWMAR$91BUR6_}=7G(qi z30O4K^o3d0U{E2-nPwL!nC6=+X_(xUvFBqtB-bzn6ztJf~8o)Aq7Yw6i(MeJZZMcOMg!ZO~(p$Dxt= z#q2g-%$9TL?}uB$79*d7xA2pD8LiP(g0rg+LCs^kg=5JlcF(6vsw=_xBT`j4Scb5} z0!gHvAA9=U99Jd~r_b2;gF5V7oj5dIsOSoyAM#NBFqgPG9vj6@>e zH{4VVh!t3V2p^uBdiA%uCJosR!ouGy{y!{Bf4#ka4&Yxx<`X6lYs{%VT8wiQ7>zTf zHnpM61>b{1LG7-_NBo5I#_!kM}&?^@Uqc4$*a=8Ow`*XcBaaEi1YV}P7< zTc=>t0(16WwR5#3v6{I3x!oNwV-QR(n%>CoU6rFqI9|^t?u4CsbEVr`Dgc|hO6Vfq z0#e;c?Pi>L#pP@BFV$$26aeRfg)LV(c%Ex7-hPeO=?RxA^HFQSzMDNkVi2luwSWqZb zyI7f-_!^7$Up56YtCr>LGYMF+Q!LZz;0p7USW_`oOPYpku%-@-lfwH^pTl0Okb8gI z7Y6hivts?SH}c)r0KLziqSm{Uk~&)Bxj!E;1eja;2#mL_v-)&rfY8t5R9VtD+u~(+ zqyQIMP22#bpzVkON~e`DPz10f&DSzkhUlemE7+QUos@WM1eTd;S68NKkpA5D2j9#4 zpS`h^^Z`oM$Ww@;DMokbRRJHlwu7rq(Z~DameH?0x?RQ&^Tjn*q?-0*@y2Y`n?tQv zF$MbgK{^8B97QVO?(UBZnI155Dc`N)Bp+Lj1)I|O;tu7qWYS&Nc%Cg!@2S^R8O-k~ z+l~#`3Z;PqItn@>^$4lbjD*Rn+^pWv$eaVo6&2ihsg-RKcogHV$aC}97J{Bp)6n#K zLjFB-Iyjx(Y6OKaGRrb`Gx?9a5qodVKQniPa72GuJ#ZX=RyfHhG^$zQmI04qZJ{xu z+%kwcX7~g-Q{{`=wi6v#?@uDW&AoKqsY+(nI>^Ckl3n(*=|4ZQ-j@aNoYjH#sA0-f zp5PYJ-phAv{W!OsN`sX~k2mS7x?Q`(^L7ag_!stE0En0tJU7veIWg?tE>D`9E=+@( zv*d@Vl{^%aL^wxA;`RRCkxB@2e5nwHrUjWv@ui6%i6-pie!fm(>YpaFn^6At5K1V_ zW=A)>_PIqxsi^xt*B4?t14w%AvBN8f1|y3k$Dqw`8cO_jXDoh9!Ra0D$y^p*dKlj} zVc(fIu&QsVIy>6LnD2Mv*o2bz(fjIm=sY%>}z4QhmuXUsYYL=93e(H8ei{x_n{fz;^hD{HW_~2K&#^Cvh~T3xJJ}2Y)^tocuzO z!c!<6HQ7ll4p(q103*Gi1F|*k;+azOw#qPD$ZSoSWss2(jj$gENGG;MU+L6o&qGdN zZ+57Nt6|uc7!Sl7foZB7IHNTb|D^{q`7w{eOmz!**2SAf1YzofEaw*uL40-%^Xgs}quMw-9))vnO^oxx-vh zg4}79ja+;Z4-3nlF%*|^3gxb^=5CMRO&KWa3ONK`Y;@R9eKL@0-!-NxyShb@{9k#( zJfozO@%#QL#+C+*9{PjQqo)j_be~{}iQT8d?-y|m#}pvaRqoh`*ZbwrD@g_-|3pIA zS2Hj$z%~h_pb1yxB0$QCy5(wOF1&VsXGr>;11v3E4#U#B}h zzyQGr{oa%M+B%fbPmko_z>n>bdyJ>cmZ01h&r7mCEa<8>?>rn8${>#KWa`bttLkL3 zN-m5}ER42O>{e;4LGDWRexVr=*OtCf`5m$#)E+I(p!JO-^<%GN-I%Af=z_F)ielqc z;pr4THiUyBQnRv2RzqRQJIu-^U2d3SZ=wsoX>_yJFP7?iY7#j9!7E~f)vM#I5gPsK zU5y5bw;u}4J{QWopzqZciu)R)R^uT=_1d~peahXn17Q8e9fxR~L-7O<5#d`1oQiU8>SN2%d_W_cOR5LA^)nkM%)L?$MX+&t&`8wgV;Ir ziaF67&OrI2nh#6aZ3=qPr6}dvh5pk1ruKY2Nx9e$01D1sXzYKPeC~_&BXUxq`>NJt zs#edwC+DZ6j)7?*d~CBnSf6B)c$%w;06J6d!*ynE0C>A8M zg_{>>!QkiXHDm>85zcN^eGb z{pK@)!WX&e)5)f54`8TnpZYdjEaFIezeL~p*VKvVtbh3s2Er#b#_{G% zsQo#GW02I7%u>e%A>f@#0vkD(mmAV)V>iuM#H=Z5bsI6XL~eT#c!J`vq&`5-q3hdE zqo`9j9-1(ftU(MGc@h0eG&ZA#sf1>ig8R+WzB2_1hWg+0<~m0j^R?hHLl|GZ5=RfAEme822>yDwk>XEK&&@ zTmx<5S|GqN>3c%zY2MA0#&vo=Ah@+e-!3Gby{R0Hse8DqD?HbctGt zzb~HYD4yl$q*Mnu@T1Py((L^k6@mT@@7eT7`8=7%69+r?&AV2d>eys`0}VGppk^IF z{*2`Jg4X~7nNqR%I?Q6W4x;mJFksKEcE1tLobu8Plef5L`QB~e+adZ2r)n_CvG&!Z zv72hQ$89cB1(_;uqZatr`oR2%p02*Bmu^yZ-G=tPIso>wMn!Pf=30K}7#jNOH~+~X zNF?wmX(4tu>cwNRox%zthXoLbOxcFEXioAc;61imxB@P&CZtf2%!yAHt>4qmYUYHz>gbAeBH_2t4xz0wd)~$h zldq$ti~~+xRkah`Qy%Bs9#g|(w@^`$rBzH+HZ?S_Bkw6g04LVN<`nD1)MRkO9 z3i1QEF~mk{0dc^l+{J)bqkvrs9){0&foaxlMW5L9*$UVb{PBe>`1&vb)y@;Bnb@~l zSCj&DI%tPG{A*c*dzxPUB(cr=Znm1LuZ3^5crE@yOc{HI##^AqMY2E4w6N0A+0dGuNX1;Ya5 zd&8=O>P;%8cHtcSmaWwi_s|XZj#!w8f=L-PA-yZ|8B|d{ABlLqK-RIH4DnD*ZdnvU zOn>99>FG5RX)}ijf92{-sZ0}2@d7+iVIn$-5ouXzRin;BE(9m*Ph*5VDy*K?cBsZiYU@T@eod9g$0xobC7@-!+w=bapq?A3q;GMk&E>y(5`xd!lKN}S zAGUq0RCusuoGZV)*2cwryawkWYG*A}w^n-p{x%`y=}fk9-O5VX?x1l>uH4gWx`^|u z_laHFQ$3<6GX3d+}ROe+sA~4IIcTz_|2E(jY>l?oJn zPsuE=b$k1wB1_1k&YUir%<`&Qi!e~$p@EuEDKGeG=Z@wClL1xtilvp6YkOKBIhdcr z=`oYjGcNItK^qWE4N$ffevaBToHp#gX4bx-IK`3rgTZB7Q~JYtVFy*q^2GlzCT$ET z&_qT%Va~gB(m9Ii$6IXyJokrd0MJ#v!Fm?Cx913Oq{Wa<^{mn9`Qg{qD_C)yi7M)b z>uBsnzq^ZG1ia|8I)dBHQv%+rKJ@-@?8;r8$e$|uVVUwcX_f|MTdg2JUbiRx z^sT@xi!m0z**Sz)_FSyVnBus5zpCTJYM@2=iIzq0+hFxx3i!da#k>-+CtEcNuf6(O z^T0oaYT9Ve9OHHG3URkllXLwu?t%AKoIP;2D!gy7f#B@Il~qh3mD~bs-CL;x6~i(WrD1oa zfRDMgD@@Zg44Pj4V?_mwxSe-M#?LPI273&_W`)r0yQHB@z*7Opm98z6r0BktldCHj zy{fIPcFtYl-HD+NwX96M*ALYBnQrD`mws75?7J(|{>Pikr|{r3lh^MS2b{eRI>V6! zNSvpWRh5;SZfR&^`GI=8nzIRbV^%MBoyyHw)J-l>ufJP*LUAw5y`5Zdb2RO`@{8y? z%#LQm*lOO3EimRz^K~P*+`kEggAxzFZSH|-cqR zkC}f=KO4(bZIeX!=IZefdN{0TMBSa`B?GNl`f+j*jN7}^s71^MM~ZQM8FSEN1t+aJ zH*EX2v|Trt1u_$4El<<*hS`(hK-?*?p#(V(97z-)Z%uO%Ue6J*n*4!!%Y7cT~tEp-OOfCz{TM9(a21LDy| z?9R>A!i~x2&v921XFylRX1pT3&kJev)Zp=LLfT_V#GM62ZI54c2sdU-HtcyE`kw3F zMycBotJpDn&X?8$En6c8|@5&#xKr(@V+292uwJa;kIK`+^u2`0+R zK4N}i;S!+HOVegJK{9fftx+u0LIt=hTY}#Gr_T7W9sV-&cDQs=4Nd$v{Qj57Ib*<^ ztl4nWe|gqjY)4;m)V?#Z5)f8Oz%i>4Z$>~%ePr6p zToBs`v}VCk5Ug!bWD@rkEYD=2_-?*8O$ZX`BRXFb%%J!t!+rUf;nO$#PD`Sij`L=%C+^dW z$L#Q<0de(x{|#}}QQD3FNbU7?U<#mt-2&Ro+nyF}Q`qR}Xl7KvuEo<+bN%?N+xooO zGi=bhOYXJ+T}W+^Tgl$cSSC*OvHcU5KO4_~9EM$0(|pjx?a-f~q$cjOB|uFHrne#Np9UiYM& zDhrw?F0~v!1Z%VhPP}&nxfj}LPx`gYi8q|)`~7^)7-I&h}A@3ZoT z&LZu$(7PC;?V*x)zB{ZI5O&iLMTSZ1oBBa$+{kNUR5&ERQ*z{egGFO$!cD3;)vwPV zV)7kkP==i}UNMt}oDQzv>XkqOi09ZKk7BXu*U z51`Ez;Lv+7mYy5{_q@P`vkZuCqd@CQiIN#6HxLQ=fSM7zk=k$H?t3{l9>$ey0{t}a z0Wr@V;62$BRcc$9PUrnfZVMOeY;Cvea;^c#7|<@E2ZHt9wP2I1Z2&uX5!?9%>CDu= zAEQxaLt3s?NZh-Cj4M{x1?7A%4GI6M{`s7Lf{3C@PRgq;1W||Zak6&+kI}UW(8>Ac zTWB;bOy|7SN14usy5QSi#=PTm?L{rs3%IKg&hq*}T-5Sv{LxADGY3}lcE>4p_AoFoS}p1+!#L?lvS9LiR!J8T=e zTbe|C$#odN?>P5^X~$@8=)>}jI%I!S>oMPT&?~pklZWk43+ci4S}w?sL(%)=GR+x-dXIuQEDBQ&@vZPl073MAJ%O zH0VpxT7}hlgJW5U)D#A|*fS!iUNWP}dum^5!VR`TVCJ~EZKQ&Kv~+db>j#}t%6U97 zKE8FaQ=$ij`aJL;9s`O}eFB#7!5YlkVlDwdcls@O@c^3A8~{?>-qBI_@iz@8rxKuZ zvIM=~1O`elqLvT3^K9nk<{Z1afaWl&t5NBz81`?XOUu74qEZf4AoKN8De!>v`uVaO zV+@$>f-sXx@ZANT=CQL>3?f=!qh$%m0se`-*wWlPhBY1|P4#&xl^q^AZpYsJG~;NZ z&ppqa)#5$fz>5hAHo(ggW+a_oeiH>}DH3RTfIgm*pV>EJh8Vr6d(bI0+x$t%QVM=2 zIMI3_3$Ye&3>#IZkHDKrnnE>7IF992opeC(%`wN z9$I{kT)6ybLi1-`X{ey~NA+M&SjW>5Dngc#X@S@wpv+@N0hNdEJ7*S|<%n0iM2Yb1 zLV}GM+(Yz1eMBsALtUf7c8eJd4y$2xS#9;BwRnUdoPwsg(B%$BbG)BrbHj}u7RH;v zWjuefQBW@)uqM6Y5ST7GZg$MVIbjtnHOkv7QgW|fjJKR*W>e-BKDoj?>RBk4ZNzq% zE#ZJy7P*87_!n_lb8=d+uRBZ;jU(D4v0!T5Du!O}5a$p9$7zHbo@^+u`Z<>o;BQ&N zEMdNyHEQud`&HTo>iVnE7e{^GHO~Wgr8*0_X+g2{>lZ4STR(p!#wuvPf~UCh9~xAn zZ;Z5)%Fv&bK2Mg21q~7QbcI>3PJg>xP;`4e;W8UBELm-Wq6kcCCMN~Bs=1O}Ha}O7 z@*rzW-C#uD9i=mx$A`iSvtnl39UtCtRo#1*m>!vkk6Z3_EBd&q*BY0$$_mS@dL6nC(U#}XsqR_3-;BvVafDfCk+@;0Pi0M*l?R^A=bH%HPt`31$%(1gIzO~6fuNF97s&#kB! zZ6NJ}H=?*i2Udljs=jr8Btqck*P2e3oR=?r~!wk>-&`Mz_ztQv2?7PprMPiOZWgPEjFkH!^WBkwGYk4PENJjLe^h~@%L z=bla`5w8XT{iK`k@VRq06NTPVXk*(NdoP6n71e=2yD1iWC19yF>;fKy76x#9$>?o0 zFzQtOx_K8!ACm{fb~wENe)k`PH!Vv*?fS{d@?yln*NCg|H>>DT0smHUkeMee_yZPU z42(>lkFl^-s`L?l#a!%;E4pNi6!e`P)E`wYmZUQD}SiQl`Cdj>7B%9GlBeamzGBOQpC9U>szY+;+0 z$%=3aQjmmS7lfytRjuln{RUv|(RcE~xDqRbe}C_)gJFC@ziaP3Vb-={qh`MZm~e7o zW{xl5{suf8ojVx(JmdUcHdh`QRL9FxD}`FF##4mttXARn7b&QTWoD?RQ2Uq3h0ut{ zQAajjT4+(he}=C(E-LVm=H54$DC`=s9xv2S*Dz}EV?;wF6VB!t<$)}fj}$g6`iSLQ zYTQbuZ{nj5GRj5&a0&{mMF40!u6xV3-v96jmHP2%9PKAIs~%^S?zfuleu}ZGuNQB| ziGuN34;0=32C*uzMCox}n~NmGu|}dPU?tw~AHMJN|L`XPAd_paFLsoRg;`=v{E$_( z%bz&dedC24XLEx9!hjXA7ptx2qOO7az73~g0igQ3Qj#Kg8uho_eMe@m&eLuXLXh9- zW~Jp17XP;$EXpLFiuI?^Dq|a|+^gqhq*@PkCyg{$Q}f!1Rd_IdZq5Sgdo4AS`E^it z$U(I*pjuW+`MHw|^ule2rBI-E{=j1eO$nYC-fg|3`#bp&CJ2^eXk>%}%BFPTbDl{w zp$C83&)pX?m9*>QoGSON5h^<1%q&za1q{-Sa%l*Q5V7|NKYIl1#v&D7 zYmh)&B}2h5m2WX^pS||Zydvr)pj(pqVweSfga&BMHC?fMmI&Bn^lUai@Lq-L$KrR` z4&Yct#?6+K7qdLUI+~VwoV|dY&zEitzT8(-Kjk4nB!klfD6;w9gQ}NcHL@BmRh=oxIv)8lg$>P(j5}p&=c^R-2 zjA`f`q?$kV4JY5HqjzArP*fd>w?^)teqFc%`Ckwhsj$PG_neugj)%eW_Y{u!w#DT%?Zs1PW^xu~^r+{y99Hvvs(SwP%|ipigI zOzW)1&)z`YjW;#P1VjD87l1zeuyuEJwKOsLA&`(;RwhTMRx*M`otlZw)T~hMy|^Od=Td^#N%S&*IU#TH&i9grUxBAuT4*4MWA_TltM8aWM0w| zhgFJEr~Xc?`_)uI$IikcN8erIHX^pNMp^diF zUINYTDXf}|6X~JbKCfGfT$6@$Ds=MM8Kw@yu@@hF48V%|kgGh$x=<`DDlo^skS#0P za?vO;FM$y|Igrs|;K^@=oh%$S=L06NvnRwPtafMONm2Cj`qALU_qXN~$X(n$-`$sD z?y$i=7Fpr$cI>7FBM`y-F(9L+gaVt-oQ<893I(3Hn|3ponj0XE2!>w#kp@(YsZcB9EbF|Er3(>9I; z;$G}!Cl(?}Z|1If6E3v4Hfq;~ZL6>JGzGn#_Ky2Vj(Ty?7a;eQKh4K4Z$zLA!&b_f zNk0$!th2!Z5XrFy2Ms3Z*bR`Rezoi7=GOR{D7Se>_h4*$vKa63DfE~Nnr#ogoF^tF z1)UB+uKfR|S-43T?e;ICXQ|J>&_F1crzsZQuRXXih@L_!pzA)*xl+HU;N|vUsy+^p zf82NO1T?X*&iRNCwO#xumH@*i^L&l5k~v-RXeb%+^;3V2&>#x$0QmW|9(X#aPdkWE zv~8j5o~d9e3x!5l7LPU*C%X>yQMH)KhCZ3ZOoBqjtz0@pw;Q%X0ldj*R`+>UFGxM3 zu;-AF)@i|-K*#M9J@UsgmM@vE6k$uZC8p0Odb|094GSs*%E>0t!qzln*T#y4dSy8q zNjal8vnod0m7e@ns1LbFJ0jROtngzMr(fA;WA$t3JWj5Zj2B`Ta7G<)q$~{FU#xyx zzt@LuzDr6*a~8#nrZ&aCwObd*YyHis#p|0C+5Cu`!aJhez-B?R>8~RUmr^EUbDU!d z*fTvL)-GMallT;_(qp|nENv9AaWSs3y=H6GHe$_)%15Vgl>@w@mk$)BnOW7iFuxo3 zmwrp`@FP#;V?E4iq*yL#!LwJe;?|q0%#rxD4Ydo=zQ?bp?Z!-q`t!wj--~hMEY$XQ z6{3FV+B=VJN>vL=mv9D{pE0Z-96D7F48B=lnT)f&4|xR+h%3cE_!x}A7zB%4pTgC1{f{4_st``)UxX4apFA$G2*@<26EP|o;3$Pn>iSfZFV1F=R0!^X|7Z{UWN8a6{ ztXiPT2ZylfzN6VSa&m|a-O|S{l*=u7Xc1^obSRfWH~5wd4}1^2RXMU#IBj3ewwd%k zLJt@}qp0Vwo|0)O%g#2Qi};6q8zdhnnW3_XhoZ84!4F`eI$hiLm|HQ0C^*VM#2A~u z^F7fr-2Q-7--D?ccD7MZyHDq}(E5!qj|xbfz(+w=9N*FxQ5Fub$E>GWHR%^f<;mv! zIa#dExxJt|$ZKvIBd0x;>7Rhj3>jz!0M8`!0FV8=2ipG;hh;6U{gk%mSKK`^$N2Bo zLrNBdNk<%vug(hmi$)VZSY1lC@fwS6%l`n2pWqO#x_Vs9p^+WOA!~IvP{pzi=WHuo zfN5)g5&T7jZ@(4q7kD8PP^VzaNS|G~2*jZcp3xB{)^XFn?0p9)K&|l&r)d&)2_v4J z)7@YB`9KpT<9R3vft9_3DCHH*a26$lUr$=@RkjR!;co?%kLiY;jTnocKfWxsY89w^ z2^G~q1b?^+e~7-oTTg0H6kX2vBC=a~-w?IE1kevLe*+3p?Tt{YfjT_F>84FZEp+jA z@){LTk{M=~7Z&(SyJwdXdBp$5-G9q?X`b}0Nf#>9Cs8J<(_E^Pmv?_an5EzA(Gi+F z{PP4|hg74dv)|S@o}su=u;Xux8&D;-hh^|rDuYV0<@CNI;cmyI)~b64D_DT4vb>gx zEjCL5;5SlDbUL8TdcJ3V`jrXfW=s78W!h2L{XHROchsAe;-PRmE35(6rtE;fyM3E; zHTmYkmx#@u{epR;6k{wnw!AH-g>v?~d2Ct<4J*Xep1Sh#J}PH>v%iDQfN|1bhYp4E zpig`$<&M@u=6x=DO8t}vI0mhsTyYD|lJ>T>n`8^WX*7fch~AAY&>Ss-XV0lNHOgky zd3n44kt)<=%a2C*iMgNA@+h=M(cqu2U>LEB$DU|9eHYZ^K+&vy&Go`~ z?Vfs)5KDoHR(k*P zfy_0^ngRb_utDOD9|B%mPl(}TOvA@O>PZwri|)`PB51AT(HZ;h$;E>(Us1-qd)@#2 z{u|W(N$wl)7Wi+5os)%h!@SD}4EFcK(ajR!JyVjCi#|BQFliS%6{Ih+@Y^!-P0~Zg zDMsu_iFYrlW$q6CVbJJ2xbySB&T-nsudANdo4MVt!~3#*MJKZM`C}PjThT4^3rZu8 zK5VLD$8Rmvsq#WstkTAsHT10EWiTSCuEo{G6ucpiJo~fPktA#C2YB0wpqV>fAuKV? zG{ORF@G`X>ukmYvOupiHth`)3KY4k=3C`%T-rU@ht#g^%5n4HL%6Zj#v#j0BgQ$&> z=8ctN?82p3%K2`%GNmIJ?v3{xZfA5$0wr$_Dyl*}?UK9A`}GN(BDc!%HntmefN#mr zAMy9b&xroj9_03zK6fl2!Vg8*EA)Y3QpVCzd_7GZG163PcfomhGMsCPrfpe2j*aME zkkskfsoVic5r7Ys%R=8gANQjFyKw;={KXg%OiUT*Qwy=aJ-x&*O{|+;T3~quIWCQdnDatbNFyizK{ z90pR zQ;Z|JD8&;86c9Dp2C_`ll`-xZUC#z9nHkg z*Inz)uJ|`UYil-ao3HcX6v{v`xKaMn3V?28<8D4#G0jAn`h<~dmHiqY&*^6k!GYb)c>|7D%PaP~>)8Xqv zGh%{DyR%a$?Sjkvcm@a{y{!IDSkBs1pF>~JV zQsrwQKO^etR?|`w|GJxL2y;8tgGP^U)9j?5RyrT-=oX| z2rN}BIt%ZW@pDk5Iu2=LD~erM!2pWKN$gsY;~oXj!Q?70J35&#N?9y=#s>LN)`G<6RdqDA!-1 zJtL;E+?%Vt(LVzBKqaT^x72AleEVNTYiny&1pN{^(6V&FQhbCGwoIyK1sU_y_?drnqIIL;)LQ?te zAqGF?dJH>3pG=~=jz%*yZuY3Poc5^IPR1j@;6r-yl!^V|RIueU3S!?CcbT61M|Ymz zEA_~Ie^GHCKY2yF;#2#5gJd2}iME-e8pKLuy`{QKMaeNqf6htBKABz8@SI3(qt_&D zqf_Tw-WJzF`bbxW$TQ8l2ulm+er>f>_NiqevaxaB24Nih62teBb@zdWM4%a&Fmdr- z>xt#aLsteZWt!SSyOtM9(2-Tl^-J0JM&1|`mjj%z^2M@=KMQ!+MPB5z6Kdr>RZk}A ztrhpSIWKlP*2oDUnfD%idH0qRfJir3(e?H9OY@Bz7dKG1=XP~<6=k$2%L}MZ>v`Mu zx(XpLhVx&(=@?iotzXh|(xrJk29E9yGu^kz41)XS)oOt?cb0%?-j7;y?y{k*M<_(u z4F>m}>xm$f{U%Zq=h#^UbyfjOUrg=&cqm*R4q)n@W7~Qw{SljS_sAl={T(*iG{=+d zZujkKcRPn=%uW;7>+&Jp$r9`ci_uPry6wa|V#Zhu1(wf?^Xz}xYl%JnNC37`yf?RV zL^q*=!z}H+^$u1Ui{Q3v8c+JB1yQv18bUZ93?YG#$2(Y5+s_*C29YbI>y^6HvaX`r zd@ku}{&QpbyF;3udEaN_F1V6h7tYuW4{zq;=rhpWvIIsN>}f8dvZ(RufXW6IRK;|D z)O7wWPE;FW1(YM04j!M;vbVk6LT)18L){ZCyaF1}qi(7h`dm6iwVGdj{9OT%toA(G9h?Z)Z;P|T(uiEf(c#jTzKL;`W8R0ffly1xS$x}*jF zeEHqzb60$n)39w&?7`m=HqcuTJ3WPY!+KMuEp?|Q8$y&7lRE^{Dw`F{H5b23msn2q zhVGc+f4-Ob-h|CEyrf0-`s_MHJiV#oNrpsXqPfvlY-`1-JAB*u zj4sW!PaxHE5A69~8{6ue40LiT#{co}bl=2(k^avokZOw5QREdPOsGv1G{ut;_XsSC z6iqy(8M%!$Lq%*|k2HV^3>}h)_SK8R_{OnO;bR(~dwgnKXDDG43UMA|zI9oHcn0!n zL%Qq|_>z>IHE#%DHZ$QzIRYZ+4uG^XY8ZhXs;#|u%8glQkVuv&@eXi6EG_q0qf;(x z+b$}`wQReM%|r@nzcVrAm;jcecTYQgJ3hKJgkTS7uYW;v#vF!c%Dd*iXLv96jSwGB zeI|BQN6%<<`#o+0`2*q!YJ1ImZ@q2Wosy(8*D~%b-F(JB)?#V$9DdqF&ZRlFg}DuH zD^%x~f&&cN0vSSD* zcXGsBc|9G|Kz_o~L|2GbYT=tiwnHs{exrB=z%D7X@6^OYz8Tv?9w|QIp}3nYn%$0Q z9G}xj;x-pHr;KyGLw}p(+4`vED1PhgrmJ|NE;AzGu=Td;3b;vN_y;(fcVk?)N!X9X z8#2x?CIIft!p|`G5?-->>*PL8T*%tJp_xL&hMScOwNMCMIN1s9#8N@eXR1khdqoq= zP@meXMZixa3bbekhTxeud4{RwM5&iZv%=sVNoJ+fS|1OW@n5v)hHLq9`WnpAM9R_$ z)%7s)DiUbN@T}Ty+%TJ#W%5o^Ql%3ZCjmuaO7wqh_o8Ww=X)3izom52E`-B;FB`=l zNtwR7w9{_ad2l+vFNT5hEQ9#H;ABYiMWY4J?|E~Q3}TUnpZA^L1$M8}o{j&TT6H&^ zA17P$(>rY$3%NE#%i^}>*qitp;|Ay&JKB_{?6tR0JeVW+eypZec+zU+?fCXDJP zi6SdYs5ewBV`uz4JRvI$;0XI`5y5=Os~`KDnSzM(L3l>#aI81Yam2Lo)N2^x=TQkS&S-5R+hFKbcC1zlH&uVD?v3(TQOKWiB{0%cbCMIn<60uy@}=J)v> zO2H~o*|L)Nt=3|?lt53&;HSJi^YFb#&p*ZhSr4v>8Fkn9d6VG@$HmEiv_?MEJW3q) zHH)ZR-z$t!%Oz*~Itz&0+UbiI|KnnD`wW zb#i)Y1w69$oq^WCd!hB(vzu=z>tDAke zDqH<#9j%ISVqHlg3R{o7ISUSk>@9wt38``tsEZwYsJPF?S+T4TNz2omSwK{xLz?Qg z10oj-YJHpf1!6r{YT|PnU3y2o7C&b%)~&4sW3{h*IJ<9cqFw*R8y-jc!79gGVb(nMrx<^cH%`{0Zor$YV~$x96TOv4QN2R z(zCuO+~;~~{LAS}m^8wdt@%0Z8
nZ>>G?L-(s8Tn&Z#v5_=g#v@C3w>~YXOR*^b?9T4D-ZB1{Ll7XDF&nJj_K)`E^AH z4QZd;arWJ{ou^cWBR(1uAc8Bb)M|>%Jd3q4wRe8AP)7}zICMFK9pq}L=>8DR!0dZ< zirjehGTz@6SonI1)g_$_dm9vV)M<_z9e1n}4Z@?zTKe+xSyVbG(UYwRtcr)@f?)UR zCh2B$b|TOr{#L>tFrwT?#R#{SybmUC7y^JXKh)EFwWg2AZM_(@e?^MRc5pi!<31uw z!t}WUmx0D%(|JPsQ`eP`o$(U!?$Sq+SVYsUz6P6a_%q~_VAGe#E zMecijaS3as98wVdPX!o!C4P_L!#w$zGQEWuY%xuvY_r``CM+9W)~yhKouNBb2@%fi z1GT%?5e94t;o4a*nDxp0a>C)XY4Ql(7Sm=c+aQ6K`H6pgekIs^q2@Tq3D0zhx3W?< zfsv;RXNcSRo0BI40s}KowAoz4d*4^j*V>&_ZA8=QzanskZR-3Zj#|WLWLQ>WnNZH@ zF=H%yf?JFX(zWvtugT^OU<(6{nYkezEm>;%l+&MB{ni`bWxzaKqtd20K0Y8W}srcdewX?=f+{8I*sIMe|-$+gs3uHBdXEDG~ciFRM z_9U677uH%&Z{H9@{jOFinh5n}LV~afw{KwMrIS1^mVZnD%x9}6lN_lE-3C7Ncd}I^ zgK0n3G$2u!p)>Ik;Y`5(tHHimXqWoVUv!RHC^t1s@bCx2evmu`@A@RXJ4UaVSU(fU z<)Z4dg^J{tTlIvXRoT1FPAd-?;7cHcqf0r~$nQa9qCVo#PPV8fZTfpZ%=s(E>emL; zAl+jAYL{=0UYW|*csj+1ON8ha;t09m$D>vthhyF*F<5^26*Tnw1V|yOzb_C1jq6)Y zF4w0UeFCvO`2do;z*gUJm5y-h|12rm5OQaLwlMmu8**P0#^0dol()g#V6-**BTxYc zpEMeK(n5W(>IqlP%35Mn$O?lu$B9yYu-;tun}ZKUNuCH>bBf$K4s7$ib#|&8%9@IA z47rcXqJNw5K>RO&4Y>gYIE=zq@Sp`ZhAwu0g0RLnclc^18pq}MWN)-au)6vdS--Z7 zhTCYQc_f>+=ny6{%P|!Z&XD48A3q0W0LC1aS0V4}Ck;c<#zDb%GA!o#HQiT-!4Y!3 z?^SIxN5!4Zva1~U00>IK2e%&_(V@Qkg4aIB%8iKkLgPfG{Yi?MVZpy>eJ|2w;avgR zedNi_If4aH-wu|>?buEo1JumtIT)uF=O1@Cj;?e++4|}veJd;EI@PdC46gUf0>Yo8 z+Ok;aH>(T6lIY=5_1>cpTu*wiSGlZ;Znq(YI8M&Xb7iFB7{4B=aI~(Y6+tKGP0^pP zk>mQms`4`^FY{byygrz!?fc|Rm?s*}mFY}aHi5op?ehBa;myjZGnMUcMUlBLy9sp* zaa+2I9UJ{oA}eb%?62KOu9Jc7W-*M;cPGVqE&P1#a1NjYxxuSaC*i;Amif3TQ7Ec= zS8FF+j_%c_x&DrQ{bcQp2QWJmeEtSatJ0`bBUyvv>w2>F0_(V1xn60x@18Y$ETJ_g zKix|TAp8^yRhHvau(LC30|ViJfPkY5A8zCw z`8&o2e#CEcYO|<|_F5EOBl0FVJ}b%dXTO`6svHSJXsl%@V3L+(PAgfHF~ZiWR0?aEGmglD8oApjpjb_bB0xCB)R=eJu3k z9QS&4Z>QG%h33?BCTIo->3kKZ5O9wZ!E~qxE*F+JNq$*(dF;=4TO8{anzzD~qF2Q@ z!`z`vuWWO8sPi{s+aQB*j)t|VxT@#j_>5I-8%g<>e2?eh65*|X_;7?^+U^Zwo>zvG z85g)<@~-cd^4BhOpeUFEDa>X@`bApV{84P`tH!0gM;H8ca)s8@Jf(Ce>)CQsg$&Yb z)#utIiEpXcjW2N`tqpWNk!E181fA#x)3PJ3z{#sVRVxAGa379uPIy2Q{rK4%G!G&_ zaHB&faJc&W;Q??-9ZaIOMJUhKcT|Vg>P#G~M;L~Ex#x2pWKVy`nEpXF%~Rv%;rxn> z`o+KdW`Ve9;aA3{%Wkjjsz^)6oL2C->)}mYZaemNUayDuh!}UcZThp%+N>G`-4CO+ z6i8@xG$8GgyR(@?8PGR?Ps>4xZzRGj^U==3;YbQd$`<~MWU_d~9!s>R&Pi07Oh^Sz zom|}6h!XeS2$dz>b`+Jm(vBAR_qirmwXFQB7}q}DC;04+Q}5S!vnq9O%rpxF-6C*r zG-;f;uEk28zejszLtVTUpQq_Hy(urz&xzXWUwmVP!W9iMo7*hYv%To-kiu0NUbdg? z!i7(GTq$v+Rf%x)YXe>`+9v37uKl#pyB#1%%>7PfF%%*eF6L}*n(WA^`zlEvmdUYO zEwCa(3}o*1((H$xH;ljd``^a{OXnY!ypYfx#Pl_Efq*d4P=FH4_fPN!tj*dz!M6-~ z@|E+teY>r6^93ryLJ7~W7N#P<`?PVL^5SIjxTKon+Ha4OOxXf zvZ8Wx^NGV@!`26*k$PS_TdX1G^yQWl-4O!n2RhgHI4~(+)^Ewbfpw?rdNrGeknb#& zExX%}3$x}qX{yebOxIClO*8U}lw0a&#~Zx)3x+$OJu%>PMHGC_1ym?f zw|HpfjpoMrL@o?IM-n*+3YtD%TnLS%oBenh6dL+#KHWqKHwwT)H>u6a1C{~ABSuRQ z(wae00eHg3{p5sRV5KScDxY^tfbRGRgxoBiUDlrT@7)49d zc7bk~3Cz@r`F`9C3y1YIs--nK#0_bGmD;E)9ji+2BI0}EpOD~M1@3Dha=rFL8|N4V zK+q&eZX9V}WtJrUFVMl2SO0e%%m52uLoEgzVr4zOtTU$F=k`B3o}bdVE6(`1V7xJv6?^nJlc)kIHhT5GbdsVZXgtwkVHB*ljo-=w{L13HSAi|V$X5kwPDZDlaqWac0UdYA)jQ1BsVd_0hyxaw zP<_l+8Wke%L$a(xS1;XTn?h62qbgAPKN~bvs`NgK4Pl!;|1e!N$K14|^xy9;s5fgT zv{u!vWra&0wrR<1iV2>7;l7j2O-lKzo~QER-?gKXgFM^B@j8_k+SmJV=jR`Rnq2h* z#2VY&9GA0uhaV4!FmO102=C#cBRJVZ4lO~LQ{elEGT!n_>)u}G!^;(W?d;&=MOd7Gs}?w3uG*rq{QX9|PL*AzG?xujPQ8r3a6uHMNboVd+TCVkntZ0jY4PX0 zwdV;^uSF!obMK*5Ax{mTH(I+)k6HEh#h_5(CD%0bH|5HfBJa*ME04iT0-kk_qX!Nc zKOYFxxi__bJ+)hG-0A$3*pVl!p)a%-?sb-|Y`!P5mpQ!=>?-4Kfa!lEc@k+gE>Et| z?EN9(=L65?c#si(W0+5n%`)~&{{-hBXJD#!`)=*@8TY3JKBqqTRB7-1U;HnQZ(-=h zHPh%5dW0(8wq<&oQz*5Q7+t6=G6op#Mz`qa@nS+oi@Lq0<8$})JGi6xd>AK7`U2U6 zydnLZA?m3W3dke4vCA84+1V~&(rQ8`q1$#v0$YSJP!-(K-;v@-N2*(KZ?qgu)w4)I za4)XH0E=TM6r@D~Z@r>KP4;g@`T~16Ryapoulz;v7Z=MD{NVm$9gcZ_{_||E+m5tR zU&WGEg`Pa0MWJxM3ua2}e>24#&MK$Ygt6F4?^ z?D^Hq%dHrP!NrB9jNAKHvzU|6WxT|EqxjM}Wk%&yYr)JO$MU%DszeG%$d&WyPy`PA z7uf3lEZGvUg?4k)Y+ahSYHrZ$fD{5LFbW_!v*>3ckB^Z7!8b{8(_|x#q~=Dusd?G{ zQ33q}esXK>5k**A+pEmOjpwEaC#PBqfR84C0LiXda{FD?nAcWzGSA68 z6$iGN-1HqMmo{?GZ>K5|OW*I5T!h^};Lo2a!IlH{VGGeS3(a*~Q)LbP))R{s*l)2T z4}Mc3G)VJ8&?1;9ZWLlMQ2I|eEa?}mEC?l(J3iDGhT!g)@5%x_AK}zeDYdt|>VS-R zv!AZ>Q73$7btrncs{iJsP_og>Fz2_5|4Q4)l;9b78_54<*?)22jJLc(;gtg=Yr6;-z17vXvF^xiu1WDeZ00EArZr$PA*?0qu7>}^JZ z&Vu-XmYSDW9g zNY3~Q(;;zxzWOZ`*{u!p7#{kRBpu1`by*YxD=+tHiJxKz9cxGt{|k%Z7AA{ffnhK) z1cR&9s!)?AVtI``z!@;b^Z_^?5Y|%u?*=C3gRmE4a|?V$g93`S+eDL30QCTLfk7Si zCOPUpY3iIm7lq$L1@A(IFrN&pkw0@!#&0QD3Sxd02t^Tg1&-HGpbI<&=5BaJ#>{01 z4Jja>Y_nba#HBw^UQc#9`{~>^=sNFozglO4uws$-XD!toWEZfMXs(qPx>%LF-cDMS zGBpzt=rH#_$x<($5YlrAO#Y706cU7xhV7@c<)o*ya<@?r8W-+Qa20R4QmA{th}Ot) z{CR$EE#Y2w#MEzrP%V0)UdFAM!FOQYuGyHRaV)zQ1#dEoO8UqJ1DxZf zLPHVWzOBBPIoO(XvN zUd8Xjo{vZG;YHQ6T=k=h**x(W=%jqNhT2DZLyI z90X}72|pXYXM=xP6YwOAD}MIdxr2G`46CbWlIkht^zzfA-oA|cw^v#DUs%%+Oz$;K z?a1~=f^qd{IaCo}_HhVpbs44t0-$PEHaWFww~}ifXU}Ya?p5+SizzpRph7Tn0F6Lp znn{q8$-IBMN+g`{3TnlVWHOZ?zFB#ghVu>?`JD}R z?SrYJWV6yj>nTMTwu|@?Xld{w_NNf9DRIrZz4kPbYr@7z zCYv6SP9t8NO!3Y(h}Q`E8HOKUv`_OMSmU(!Q~te`1x!Cdxlpo82kVq8jY)C;QjSB%nDeM)i5HkT3CR+Rlrc95o6>CFI>QQpVd4jxxv?OEHQ_>QM6O)C0EoG)oGg7F-|98$M2M&)43QKH`(+j$5-wVlZ~_qx@VLvo@2VF z#{K%-DY12&uTugDNFXsl)<4*A76V@AL>XUt(qYu6F8l<`ixjySvJ*0e4`PF#)avOQ z|9IpYzr{%r4wp>>EaEDt%Un01jzW1OC1DZbgWK6xvm3Ml6IhnrzNE?kra!QAtn?3a zO)xENUbL1PRlyx`2cDAs^WR#bkB5!q#SDzXn2eT zq)FlJDsu8%zLs9f%e_G#CczFsQ}L$<UVf(drkmc=Aw%f2$B=3=bvew<`0TwmW6(q@{ zB(jG_1C+4W2O7*>pW!2cnPK0Zqyrsp5g$mbK6~2OAm8-XUa=hQnF6zXE^CCKONTRj zxZCVpB>CR)kUHbk^pZ#FoE26Dih^!%MyqC%-ydAsoq-;NXz{SZr8h$Pn<;ht75~&n zJi5si+1{3vxP_*-w94+bP6^5_e0jDyfRIJwbR|~gy+&}alt-By#Mw1!qQDO3>Gn2e z`M7cm*?8*#!bAqtjYAAwUmVg%1P%MZ-shsSh&uby&+h&UqrcKAJAg`M8wXSsrQT_9 zo>SY-kZX9eQC|+88#2HG)C;z)`)2@X9@=!IZ&NQ`I<%Ukm=(0WWPeTiEncg>CO!J{ zVl#(L>NNb-5?{%Lf=tpIsU#06O=`M+^ZrwBQ%qkVu6t}zthgI7&`yl=DffBL2_4 zxBmh>5;+UM4>%{G9re*iX*fWx?oX-ODvAc7r*wZs9*wd1Dd#Yw%C6Y$< zOqCr?l|L>Zt_){aFlK8Rj+it`+sE(;biBR*O6TW#!<8_82?*Qpoqe6ALp=BL|1k=wLmU`8Jz~2&o>3>6NOsT2|MNwBkiFlU?rY`{193h{`5&tu$Pql+?#th%!#STVMNyC5J%P5Lho*duA;@c1qB;zM8A_E`KCO zv5$c&$e_RQz5n)C{EJ*J%V5NSn-|*TKl|CSs&RWmey|u4-XCE-4%PGgJZFV=15A+C zWKox?cSCjdFjQsuA|>_?5Us@e$B2%3pu24UPPusGL^wm264(7J;%B=~j1#LDk`kFu z(RT7iv)k}HUpmF6i*)V{0quJAZTU-nwDzzb-Uzql<7VCiZLU=|Qk&B8()NpMw`maGZ<{AT!#`#ERA0c1S%%5kx{y0DP1SXtcNEjlXYE z-y)QZ-9r;-`a6IPZcJ-th0F5nDHU+A{f<8``-~&lCpG6^D%o8HX9=v)_L7T*G!mCR zW4`syu#C1wO(D(%mDUghT1X&&!D{q^GtlBg(} z!drXM7_#=l3=ynmyFpsonUMMd54j97&G7^CzS|)mF!UYGnQ{*YcbZZ$ci{BT@O>%u zRWz+$BPYX+Tdu1a!0AkU8lh;xWO=vGTw8|ucAl6AIdQ8+MsW)7qJSCTOEX4|e)JOe+VBWm2; zr@+sw#G5zX+E&fqq>yV?kl>=H84d-ezgwP8p>@mWfxid-eDo%_+}hF4LN_PlsRo59 zq`=^R5Q<6zC=k-nDs_ZGlv7TVPc228G=zkRDvvgBVK zA~d_wATKYCv*%L|?`B9kGzO2Z!ur3WzUWO?uN9~wb{d0g%dCHtf||aA`0c=AjOE6c zDPrVr|FAD^qCMRoJ8mCdIH=K@3s9>XLammNJLa`n-Y!FOhba;I1@>)Q9t^hkZ^G80 zN9nq$cD0z0Zj{BD@HJ3(yK?CMc}Fxa8{EkgIV!KCu%U+&a zU-~)DO#{ReD70#>yQnA5PU?t+;>#IxCe<@ad&buPE!6*&mYLskSR**k!XkjR(Ke2^ z#AA6rAV|P%|I(PL#<}*MMU7qdeh+}=lxBwKo&G(2ZKL_Hv=kqaCj;QCeIs>VR3?LG zn7dgsdQBdQ`@(R#H+NNadeAZ(U!K{Go|ZSO0r@&@2?W?bbYa*A&C0e+o%YYS+ZG~)kmMAK1Y~US{drh=C zc2!MpIbEw$d{XKxUr7IlE+VoSXeGrwNany)Wh1Rt*(g3{F_(0Z9L@5y-(V+ov<7+5 zxK5lEoqpEKS82AMdc1VdR`_dCUs}T#GP`={JMVH_gc-Y6mdLKZtnCxM{#gBG69GiC z>uzxEu{_=H`PuY8m9OFlM}9~@>07U$_MrzZye`0X_V3)ab8kMq`y+r=9F+~edR94d zAKGJpP*xCYPuBO!D^sAyH*U{pW*SPlZOSz-_M@#Erg-X*-ib zq(dJx!^xD}oL<-3>F~aFF1q)55f`FiRA88UiWnAS#qfT_nVq`{w+(Pm-RZ~C~S$qCSupUKH%fBmPSt$Qq*EJSd_j-->Sd+ZU#V<=NGB8K~ zk^S!)QiH4&g*PelzV=uHe+m}~4bqiAbl8Pr8p$vXEBk7u=Ot`(wWX`Y6C(Z6Lpsd< z#c5ujYa@q6RoACe@z6$;9Hf_5NV@%QozE-8MJ#Qjo1(4Hp$W4M&C`#IUk<&YG+(j| z-UMLUQR;_CwBoiPuK#??@Vxe}Tn?RN`|^3-P+hfEn#qPmgOONuDE#HRcrNyLhPw=)eQwDYRkGV zS&SuVgUc$Aw}yd~R$o^y0x>PVhB|ZX&5g&Zt0#{*g6AJ2DE@bF?kce7F3ERyvC?(U znA+PJhx_`@EwMakb9mgl;W)QS2(D2GG5PVGRs21xQT^MOJWf2jy>_?IGDP^%@1}te zi{@r%`SajsA!(~&%;vBqJ>TQQy?8m}=;^``dz~-id2yOrgpevl09i=>EjRLNwE9}26wOodPh zZIN!;M7>-~^|a^a>C{=P;5BmC{+A*nM68h&>9aspq~$v|#*!mT=C^nJa$kg&v;`|j zZ?Lp)dSM%Nuy9~{;FHfF&9JgFG|OEy4e{apfx_5vm&X{Tan{q#LvI_ zrEwOdB}w@5iojX9+QIsIV1%;oI;pJ3H0qKNXreX)BhTBATl&sLI+eVx*V3FX!L+wO zx}B$2dx1Kj{8^TEa5S~=eRO+R(R&Yzu3d1o=*HL2i+=&-|8T@`L?7yhh%64?J7e!Y zkZ4^>*qwqvu-09SSPgB%4)0Bnu zs0j>TiI>(xS+k5)bPgHf+AeqK;TQ%VDQrB%+m;);j%Mj$zqh|R+m95gD&zUt+c~;k zxvj;bA0lzZzkB62i4E6rY!=3o9sY+0eUbCVac;NK=?Ha^{n8{&Rd;w<5ki}_mo&Jo zW$ha^y0c53pVw@kB(*;I(gu`p{xTx}@=K3Z@WaW|2$YtZd}{LK!=rRPTbrGPpp@2? zr!%%nzM4yt3gWKs)>R_zn1P5r;wSC<+k+-ev*0dp&`%K&xcrs%^zahx8BtdHdUUz5 zAyKx@00ogzJj#D0sd^l$(E01+LSm-<&Y>qi&?zD@&6>&T zI|^SC#Qds3?NRG2ttU8L4bbXh!G1-Wgbi=44# zC($5uuBFSR@oOZHI188hL?)qs*gfga=RxLjsL}Ae4T&nLVFd-cvs3cB1DhYAi#zfQ z`-D%^<_$rdH+yi4)4y1Sjf4@cc^);A%KQa!4OBp!Mt`#lYQno?{KKw=A;gz#YkBxS zDr9MjHclB_=sPn=Z!B{PyMR@;wOVcM>ayl5>GRoR4Bv$$PQYd&ClMP=FKozd86&Nb zTZ{yZe)8YDAW^zg?t9}Q=}pRtnoZg=s&enr8~#uY(iCy8FT=P~d9xb@{Vu1zW3xZF zXQ8WJJ>|k_EvsH#2iuz-LNLNYf+IC3yI02Ef3`|f5mfAdMZKeoZ`S3F>aW69k+1B% zuNzkZ?{v22SiA23{paf=POyClQvoW(-!U*LariOP#hIbD4-{Bocg$@Tg`#%rs-!^v z;Za2gk{X^$ZB@Uk9Y82{;swd;Z;2Nr5|*jIlHP&FVx%A9rH$*yM11c>lPW|QR?{JW|J6MbE;Rc(k5PEzq_?z#m zbS6H~EVK!cM>$IVC%(JG6=eD}K%m!-01_qkBz~%I$H9vG5vS@#=XDcoiMCsFijZ{~ z>0S3){$RJJM9)UokX|_Z#kB2BN)YdI7K%9>z{6Kt)>JBftM4VV)OdkKf+p;?g>&xO z8&hw!tdNoGs_Pa)&VA-Zrq28N<704MJ6`X!heSn$IlRGfZ_TW*pMn44wEXolRK>GT zX2%-Ls0r4;bSJDud^fN*ukg&kpsU~8YhrF#uIA(Kih1@H`m?C0FrGN{WYwg6k#f2$ z9 zN{#u6Vkp$5I5*~$$y|0i3AV=n5Mp3djj>80q;^6fBsW9aXl-VFlt7VCY@_ry&M`z$ zbgQn^TE-TQJomKaJj;g)%IcdLAtuS|5_+RQ^>##9_064m*!PNJRQcU4>I|)h{0#$(t~}0+FpDa_ei=`@Zb@PBNlD4B9sTKqwcfoN>L!7kZwS8^ z>v$G)Igf|a^&!ac$9EPl2@zJ27KTqJ2a%A+I*-~EM+v$NQ5 zABqXr-0MFc9>Zaa*U%$`u*wd1QMooZcN{h1mP?P93n^d3-0cTVEauSvy^fRCWe6VD zfX3oP@jKxcv$m$GMX1A)!d>s8!lKKuRn&Lm2F;}1y#Ky?s94AApv$?(pMnfYpFO2? zqd0VZ0F-1s_wv=`Sagkbrv=fU8+wxJdtlwuLd3$Cz;KlRnp|-3w!cs#<%{`J=*xXCet5zWzd{+x zu^~xRBLbNMXJu8*Wt7vUNfZLw4fs;@?YLvLrELaO>tTXA&}69+Y=!*geGEZqrmBV^`!bRmm&p0<#l z7vkJsblqDDdK`Mj8Y4*S&3t!B*Hj3#L7@ftFXew_ZCAMAo7z(2pHb~$(^|fto+_rJ z$1LDBnW?T@?!U_nn+d9}tO!dYxY-;Am~JL1r?9|~W~>bvB^;*Uek>^|en4VKH0tl# zl#B+6V(4q}OjcDf^m;fqS7ru0R^HS%*JulAk6LWq*X7SwLb~*sn2|xc&Uu>lA~V#6 z$3J3^(CD~@TL~e&XH(1mP7?oAv#L6BRel>=tBqC`Y!l@CXBcZu{PEKm{xai7KdgzZ z_Umf$7FWl@BGbkE!ASde{tv>!s$M4Keh@!e9Ovy-(^#nP+hJ(75U5j6Nglyc|H_Sy z+o6AYVoZmJZ1?WpZGVf3b*5Nhnmma2ENsDjkF22+l(1PuEeuM5!($N0i$1X3$%I&x z>{!*6P?*Gn8hCsB%&w-!%(PXzUf1=#4QEQOE6RV`sO(zy$Q&=^Ary-w`}b~&%JoV{ zznOV98f0B8Tk(3h3%W^Xjbx;>y2jwp3t=PAWT9>Q0oHw#rC0sF^5yY7eW|50FZ(K` zIZb)p#8@L3=l|hY8YbM2M?uAn3Bv0vd{K1Z;3{~lZ$ zN@m{%zoYu3LYf;A_EI=|ZHi>_>}n4yrMXJFwW#k5N&Q*^LeD~qyM@n$kcHZ4q*M^K zT-OxW<`^k~Q zAl)S(-Q8&cDj))abTbPP2NNI7&PecuEA?z;E;$F;t7hV$m$&wlo^_tDff zfa=m*dOra_1pW&)Gx)D6%Wcy{x2@}|xNmiiaYoo4;<>p-gqf^50&+CjDK zb1pcvC&BJGoqy~#^UZ3dQxIgU*5T-OCzKx<0ai4M83yyd{h@pLSzn(WssxvcxPHuq z4ttj<)ctTnyP%!E!d+Ry??}^#FYmq2_27s8fli3I#W&ZjH5(5Qg>@%r5^Z`ZPaCS` zAu<&dTq{Kh=k{C@OLA#ZZc0?y5iE)|Q%*A?C_s-msDH4*gCZq8`jCXP1axr-7W`jE zd|C}asiOV;-8D7aRoAMJr}m3TY16?gg9)9jnqLM8wvq%I#) zx>M#HE*L~hK)~w-?oT<^J$~1*)%TAh9wKj06FaOsAh&WzIMxU-oewe_)_eCR1|>{v zP-DVxLC=cF9DZ=T+$H`R?Qxv@d|%cHoN3ky>}+N#mIz+0XfP|C-q~HVrCL@QqeMP4 z$ahNKgufZV4t_ScwM|ICw_aOiIsUt#P|PC^{2hP^@Bj0gEq8#EF_;Jds36|+;LpNW z@i^l@37G9ELRv{vru%bsIB&OR!Vmf1vh}*$L-0d0X)%!DxeoRUSikoJ1Nk#AL9E^! zdHs3<$P@DDMh@lF|J0i3AF?g%o#+KO|K>pnOU=qRrM0-;kainn3cVpyY4ke+?B-MflwYUOLA|9-MMRy9piu)X1(11?na#ksb95G#OYlCp974FsfJ;IW36Vr`bji%&1mO0>|~VrmlF@J*=$( zZW*Ad$gzTh!88$+9PP(Ua+V&Sb0ys-TDcCRNjlT=DgqWvVXR8a_`vORYJ44-LMSo+ zZneip%yFrBO25l8UYGm1FtWY%zbh~&Zd=NU)7sDPL+-{Yeda}G9TBCniQY+o<^U>M zE{3F6)4Cqn`>14Wq-5zfV(<65qr-;D6I7Pgs)zL>3;3c|_>^)!=Wu}=b2RP+K7D!= zZil}gP945=#5skeK7N!+4})#ikbfS*?5Eg0>lvJY2mEr4n;c#o3O#R}-foEREg8&H zPI=*D1nY3WmK0ZXh@^iu=JGOe#YwGmJWXES0XBZm{_*a>(s`kzK(W#vA&+`l{lIT5 zS}BS`w_!99k)K8R>uS3PI}^8GTDVdyZ9#IC@c+f09VWz#g-HFQ>dbO>y5#cAa)Y%d zsftMjJVJz0`v|G^`24LDv3jP!4P5;Dl=$=~R3FJ^sfIZ8gD5ibtd1Mlwf^HxJ3G=3xjzGdsB=)aXTv|g6Yk{!REkv@Hj z(4xMfTPy1DtN6Xx>DI*C`l0-DFxVR{w1S zp?CFB!+s18vJ|=3*VWE9Z)Xq*RTN%T5#yNKYYG49A=|Y>HhSroOEl{)*1r{I;{{R(c#8J9J*0Z^+gi#QA8l+&)U9u%&rK z4mv3JAEJdk1rW(U`59kQVqfKX@rVjrhKKM!t1&m+!(<|ukgX5bM@tGVYejURMr94$ z4)jyf`-7$%FJ!6l($ycbsl}wOs0@d{_rIL2hoIdCQJ>Baq)mVeLZ(^}m64gFqi1|u zYV;jth{WHIqPKwS6>fg4l6sBe9N549_%8>;Q^`>XUixJ3g~MP6^5omj{$t^ARTDe-Azsnpf^5(7{Z; z{#@|LmEjZEtbgQNtWix8;$O2g`c_v5cMu4EQghb0EVYbCBkc9tWXty9L9`gb+7{CBpNqZkuubY- z+@bu*pA~9OsuG2YNm|vu0@Jfs(@QxUHgWv)BP3M-L?g=v$f=^+=i7w~9+GYongQGM ztQRx2{O@}8!ISv={T1U!zX{`+1CN~8blvNrspdF+-G1XZzV9mCURUQ>vo5ytn$2~I z$(;t)!6#n2_NloVS>gMcrB8EfN~C)kT)vq<|J%v;2rDYH>S%o%f({<}Zjw2pB}g%dE1R_PSzlXy=R<({j!Y z{mW@Q{iNHk1V1x-!#Gjv|Jq`&@$=ilubbYN^BemIP^6*>u75vjhjWQHmB@T_K$Wi1 zzV38nwNPT~9H`LMZJYnpA1(_llZ$XP3S>(G8gW#W^NM-3CQ3Tdp#AAoGzTsxi|}Hf zLKf4`N>vy6*!1n_}|_QnLZEY zw_T>Ksd5a^gP*ygUI^22c>Wh`CcIz1aSlh$gtT*{@g)tFyc3j6Kf$8dd;=@{(R-#G z;QiAGCrRmJYr(nb-szF*>2DANz!Hk>dbqFhx|LP(dp%O z@nEXLLKV{I(DJmo}P{|67uFyNq4_#U3KLsM)7UwPK5tL3MQCAOb* znF&1ZI6b>AD>KTqDs%mfS?q9^o+hJJ=bOJ@S6Tv2n$2b@W%=Kj0nn_@@i$YT_CDPk z)3%UQ?G!a5Cp}|%@w6ZljgzU!&)edJ;+q5a*g8F+fQskzi~Mm)n$EeRm%j0orN)|) z*Uj`REj%)_>C?x%8zX7ybJZlScpd-`tYp7`ic>%YOW?YNU$l^*6gN^N>;~UnR^S97 zIB;11g9CFd4>Ey+DZ&=4#$@!YP4q9DO`}1$xu`>7JHGOgDn^kH!DAdbcEM}FWUu>? zTB8JRMQt6G1cY7%`$?c%#ZEq$n3XOKA8hcJ4=I{&3BY6=L?(YXuf>5$DI^)IJBU0s zGgG;Jhni}2^t0dI2MSMv=LbYcj^W1TtKGnRFt7OkJzfsnw!$eeI-NBx+{`|>mn{&y zy8Bj(Xt_Fd^v&>dbqTeDG{4ujuEP7+Ftd?*+q~}*cCR8E$=()9_;INKWFr;*zq|Je zAFI{Z_DLSy)G!@Uuk``emz8U_9~$*VFu-5F;DoblH-)y6LR(gmM7+yB*0f_HFE<0? z22WZ46u`-K{&w!t5}S2seVF`{#N5lrI}-2=HjkcG11Hv_89^ZKAVx&B{IkvRbVnSr zn-N|;!(@2298i2;yp({>pL04-jhZ0S2{g!Anu?8@fK?0)ae_9tHSr|s#|=k6k9t@) zVvraG`2bkQ^?|UAeTo_bZKvN0A1u5?Ac6!$$st0o075Y(h?JDwh(!}okKH;H^ z+-RQ@d@>`3nVkMsqMAW$%-#+Z&M=6|MubVH_z@Cx3f+t)UY=l=%>v1hxnHRUOgiJ{ z^n67w1jcvKEYFL%dn6SFl6tm5 zh@;eCJXxfQj=EQ^5Sf(y{fJG`7Tl{Antx%zgFDm_wrJf5z@d@`U+`RUpHVen<%~f0vz9!;W7X~K4LM=})%6=Ea)BCQbdPnDw zw7xs9L+hc({K~b1@+f?SD9bn}=s%yumsbb8f|4Ac$fEEH9$}GBnF`6yhlB(Cn~{_a zM;}+J%6qJAP`_#7>FQ>gdJAAJ4=Kks>k@;u&fd_tm5-#c9{U9{C8hM-f$3U#v_C&} z-`R;%w#n!&dNS>irCgF|YXe}rc2mmQcCB8ei~fjBq!hV_4#U$N&ZbLS*kQwy=%o>TBE@!SFX`t4>0cvBdAO zEZhZu*5oO8exStOZ;Y0?#?zJ8rA3U_@rBKcgQ+#CZwI_9q{F3wFRRRO@<`;_$oSZ# zLYQ30-$9wAy+5O;aidk^*^)ulpDZ;uC2qkYM`z-!hIfY!?o$)$OWIW9S)8{xnY-2b zNW*lQsUwyZi|DmA)fBJsr8aFcb8?kd7K1~Ls39}pD46T;C|a%>j~fP8D472r=AOAh zjR@K8U#tN^wj^}}sX&FDi>AY;13+B?p9To3PDebj>o5*7s#RK?jxa2XAJRN8u25m0 ziB3&+*;`$C@~Ga>B4&XtiO=-|mIciX;Oc4WWiL{d;aSk}yg*-=%m(4Oy*KlJM?N6- zhR7n~kf&!q-P~ta6(F0KOFeP+YX9tyx~xoV9I{61SJdeQx_hy=x)B)0ebpgLab#0> zg)d#G22rZlHSqlykT+IN^B0}uEYNaWG!$;+y1a7K|FE=kFyP;E>jgosY9?2}!Ftc4 zXR3}y09eCOR*h-!O>g$zr=AATv`ji9gFd~4zrI3M?i}9#@^~z$ola)4LKx*5r|i$j zz!>PKVIqflJTA@=3+}X&uBv*L-@g3={(HB5ZKYD^`@7W|(X3+_62eHHRfQm7}WsZULaI+A`t+ozWB2V4_#DOG$6>VgM6Nx4^E1 z-}DLmJB=1clH*fOFf85BOQ$TQeH;JH9x#RCRrN+e=nCVtrr?T4sguM+zk~wx6O_~R z3v*%=W9M9%5YW$Hly0hWb)}r(Gds7UfY|`|%n5(*VI@`9Cu0Fyp^%-uqE!qWpc!fwl*jdX42)+qR99j%gQRiud`hkj&7p+AW<{# z{e7NWGNZ=&9qA3d=YaS@cpS4itW7%UeGL*{r}^sDD9PWYcYrle?Ckf!EDtmjlPIonV$-u`OKz?+lahfGNI>2s_)U&f_TZa_qqmu$p}fO)UJ+W}X%jqDkNv zFnK)UpypR4lK@`inoQP!l;{D3E{X*K%LXHRTrvdWWeqIA&w6D*DuTBHvWWTTb%?bA}0K=SX3-u{n(rO9PXAI zk4dIlNYqbMw1()H;8-#cpDs@YJh(sC^O9<|sm8n(n|*B6HEtN%xgPK}f>M^QPZ}MD zpxP&v#2{TDFJEB=agb10SzY@2W(41@2)VsZgz*sYGI7YWt%V);(OuUIgF)puogdJA zmVQ;K^qwUwNvS&RBx{=@Iql04i~;l=9X%yD-iq$lis^o+2`ketbCM63)bF9Fn1r%j z?E1g}N?rYj?mH-Zz6OHpB~F*T1!5Ph?;$)|HgI;jL>JFfzypJnzBC}?fUl)7^OkxU zkVH0C`}8HqWN#hsr>HZ>cPaXw)(t_}S0r1+%yXr2SZ)0`PLmu7 zmI7nr>5_AJGh+I*A>A_$B)H21H=n%@Y|b6uZV=>3$W}=c_z77KoA!?)bblio=+0fF zKT>D~ZCbJE`)Wa)FvG1;8Kor7gs?HIcORcgetL=({NJBI3oj0(A@`I)EbzXr>TDJ; zPTh}1anN{SHr?PfPEO^83e^65&3kbgw}sY3%=8PmzGxM=S;el@B}$L4fjznK&n^S< zy5II`sU0WMrFJ?v_dW8>D0pMx_5#9+%kzh<9J32mT9*RmleYNSAl896agc7Bur{cIpVWsmhK1vhYO$+!}i| zKAL&VQ{KoEo_e<2uf(y$h#+x}Ec@tHE|m%cz_!W17naomb_3Y4r`+&1{C&?9tSv7oP`-J(vSgh_COH zypYxq9<|7BxK~ec|2?(rqmudB=}6Gt z0?@A42>x4Kmd^XB90nN8@vhu14PFh6+&*(I@DjO)i2T9bM*FoD6Gsh0MRXb0Im-4> zW;E4{t z`xA&K&G&xlIi#)Oz~I1~>r%lUJySeUk>C_c&}Tvh7ZXbE;JBwQ0rrOr* z0Z9sJ-l-AZxr4}8+ka*K{r0QbZ{GO2B}zx30z63z@vr+F@k#=+WVAap)yDzLIQRuW zWK2BSaAiRjv%T6DLtj$`2eB?%!Zp)UdmaiY2leWjWsE+a>xy>=ahZ(?I?j=&cz!LE zJi#Luyh=&YS1VY^`@p)PcWE&d&f}DNPy7mu5Lr3!YA4Mzsf%36)E6wPI?ZLlXmIQU zQhd9^edSBvJP02>3+5Sd;eU($inq}Hg;6=^M*kJs>nLwE#>|;BBiz-=Y^j~LYcHv8 zlUYwPQxT`W(p+-xGJ2@AzO1(ujIRPjBT}j%mF8FRq`}WP<~HerMy+&*F@iuGo{h>#epd8~VYm3T`FvI-GaHJQU~uS;^9&71c@#V%}__e>d&vkT~gZ zYtp0#Iz&Lc$)Wk=4kKe)#2DPnYBU}L!lk)cDh-8k52%z&>i1&;nT*J`Pi5hPX#H9? z>I%}hwChJMC^H-yKG%aQ>|2rLFk=BHP5Q)9XGVnZfA zDDG5YC3*unYh2jl)<+)P=Wl@>tO?JUc1eqR^;0K*pCl-%XMLs<_P~*DvUbEhLRj{5 zqLHD^Q%NwDJ$vg4bA1JsFW+~;xyrusZxVBb^~h3eff`IxgoAHQh9WUz;-@|-mqD0!!8EhIu5d`C>9-5!8eMP{9o zCLMOfA zdEV?0_i)urStF{_{dbZy&ocT(cYbj^Dk*JsA3SIBef2K8G~d2ek6Zt!WHGm7qh63c zVuLnFfco?Ei6N3^AgXj|I1INrI-wehO`CZT!7W}lzFl@uc=aP)!&dg21438QLE&TP~XDdZh<`HW&fQ)75}T`y)g}RN@49ZaRZYg3c78~ zRA+(Ayr0KtT|cY518gaA+(TEl&nll$Yf7G>;6eG>n-SNI<6nI$n~MC^^lW21C8c5c zqg{0ngZHoINq*8nLAuFY@TKs+a)`EM|A4xKhFvx9FyYHN#1kjcs)7WUTKJ&9sr* zo&+Ds;~tu?t+!K(CJk}w{Hy|uKj9n8x-RDKMfZna!Qa!%pGwLDx-F4wug&1Z@2Dp* zE5lR2Z7mAhYu1rDwQ%(xVTpx9gObJFjz&4RU)Fk@?Um}v-Jwum%=Dr#CxhY_l zBVgtYPTc5!!R~`VJ2(uqgUk*mQ^Hn7z>fEOdw4gdcX5b8(fvj#9v>F zHT~xD5=zixpdFigeHnzds9|x$>Jf86fHa9%2g;(tJPW^7Ui}D@W^52I= zRX3!ac?@3VbG1q3YEikffSn<;=0E< zY$giRxk90c z)Ck|vAfrYy(08icBW}2Gxsblld3=P1;%b!ZbIi0=L)-TXeMJDZXt>4%nSu<`Fa@lM zdaW0}9h5$U;Q}QLXHz*`f{lFd5&am*wFTI2B*qV2Zwfx(jSa|ugIT*r($X5{h&s4^~tAnX9s2zD# z>8s~OK{qY9O6F3vU|udY)}JNnw_D3;S0lPXM}fC5^2FwMPKC=m$JX zu&-dX{f)0MWKw!E8|9kJW$_ABVW0zDkLx}j2eBU%x;@Ajm8_B%=4vMn;-VDzeT8~b zw3s0Ld{A$%!9TDXO0=b{o_EKC&|aL&46yA5Clx^&12#yK)GIR&226AZ%I+8_Z=;Q$ z;LDWm>E6s#gCr$P@12l=L;O=e>$kPZ^?@S;}Wm#GNZ zNob%`+FJrS2flJ$PXy;Bf}zA7$iL&Up;qViVZzvYcyk&k@8yj}cO-l5xk|;VcL+fp z$tY)3R{r}k==NpbD;>&t=Oa5II%Zd>{JK1oWsYKCquSE4vsv=Dc0 zeF}0;^!wg?)FVnkeo7!)Tj4&Xkr(?%k~`JGMhPa*a_u&qP9JYAlRu4)#n&$G!@~C} z$7S}tt>?YhI+W!e6i=6ME~AFCo5X2(jzXW%Igc%H_p0@>R%+IGC(^8M8BkHctLHu*ijz%_*2t}a2ia`5IQq>H-uLiF{i,zdyuYV%5MEd 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

#EO+g(6$LB&}TA&q4Hw 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

37dPV9LCK=i3f{- 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

o@FNv(u+?D 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 bT*Rza@O-6b0um8;c-T8Mw`Xkq{mlOX`$qo^ diff --git a/examples/README.md b/examples/README.md index da37f81..1ec42ec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -35,7 +35,7 @@ uv run --extra dev python examples/soi_hybridization_sweep.py ``` That example sweeps a 220 nm fully etched SOI ridge width and writes the -effective-index and TE-fraction plots to +effective-index, TE-fraction, and field-profile plots to `examples/soi_hybridization_outputs/`. Recreate the Tidy3D modal sources/monitors mode plot: diff --git a/examples/soi_hybridization_sweep.py b/examples/soi_hybridization_sweep.py index a650785..a7bd34d 100644 --- a/examples/soi_hybridization_sweep.py +++ b/examples/soi_hybridization_sweep.py @@ -74,8 +74,10 @@ def main() -> None: sweep = mm.Sweep(values=widths, results=sorted_results, parameter_name="width_um") summary = write_summary(args.output_dir / "summary.json", sweep, args) plot_sweep(args.output_dir / "hybridization_sweep.png", sweep) + plot_te_fraction(args.output_dir / "hybridization_te_fraction.png", sweep) plot_profiles(args.output_dir / "hybridization_profiles.png", widths, sorted_results, eps_grids) print(f"Wrote {args.output_dir / 'hybridization_sweep.png'}") + print(f"Wrote {args.output_dir / 'hybridization_te_fraction.png'}") print(f"Wrote {args.output_dir / 'hybridization_profiles.png'}") print(f"Wrote {summary}") @@ -195,56 +197,59 @@ def sort_result_by_neff(result: mm.Result) -> mm.Result: def plot_sweep(path: Path, sweep: mm.Sweep) -> None: - pol = sweep.pol_fraction with plt.rc_context(publication_style()): - fig, axes = plt.subplots( - 1, - 2, - figsize=(7.2, 3.0), - constrained_layout=True, - gridspec_kw={"width_ratios": (1.08, 1.0)}, - ) + fig, ax = plt.subplots(figsize=(4.2, 3.0), constrained_layout=True) colors = [MODE_COLORS[index % len(MODE_COLORS)] for index in range(sweep.num_modes)] plotted_modes = list(range(min(4, sweep.num_modes))) line_width = 2.025 for mode_index in plotted_modes: x_smooth, y_smooth = smooth_line(sweep.values, sweep.n_eff[:, mode_index]) - axes[0].plot( + ax.plot( x_smooth, y_smooth, color=colors[mode_index], linewidth=line_width, label=f"mode {mode_index}", ) - axes[0].set_xlabel("ridge width (um)") - axes[0].set_ylabel("effective index") - axes[0].grid(color="#d9dde3", linewidth=0.55) - axes[0].legend(ncol=2, frameon=False, handlelength=1.8, columnspacing=1.1) + ax.set_xlabel("ridge width (um)") + ax.set_ylabel("effective index") + ax.set_xlim(float(sweep.values[0]), float(sweep.values[-1])) + ax.margins(x=0) + ax.grid(color="#d9dde3", linewidth=0.55) + ax.legend(ncol=2, frameon=False, handlelength=1.8, columnspacing=1.1) + + save_figure(fig, path) + plt.close(fig) + +def plot_te_fraction(path: Path, sweep: mm.Sweep) -> None: + pol = sweep.pol_fraction + with plt.rc_context(publication_style()): + fig, ax = plt.subplots(figsize=(4.2, 3.0), constrained_layout=True) + colors = [MODE_COLORS[index % len(MODE_COLORS)] for index in range(sweep.num_modes)] + line_width = 2.025 highlighted = [index for index in (0, 1, 2, 3) if index < sweep.num_modes] if not highlighted: highlighted = [0] for mode_index in highlighted: x_smooth, y_smooth = smooth_line(sweep.values, pol["te"][:, mode_index], y_limits=(0.0, 1.0)) - axes[1].plot( + ax.plot( x_smooth, y_smooth, linewidth=line_width, color=colors[mode_index], label=f"mode {mode_index}", ) - axes[1].set_xlabel("ridge width (um)") - axes[1].set_ylabel("TE fraction") - axes[1].set_ylim(-0.04, 1.04) - axes[1].grid(color="#d9dde3", linewidth=0.55) - - for ax in axes: - ax.set_xlim(float(sweep.values[0]), float(sweep.values[-1])) - ax.margins(x=0) + ax.set_xlabel("ridge width (um)") + ax.set_ylabel("TE fraction") + ax.set_xlim(float(sweep.values[0]), float(sweep.values[-1])) + ax.set_ylim(-0.04, 1.04) + ax.margins(x=0) + ax.grid(color="#d9dde3", linewidth=0.55) mode_handles = [Line2D([0], [0], color=colors[index], lw=2.4) for index in highlighted] - axes[1].legend( + ax.legend( mode_handles, [f"mode {index}" for index in highlighted], frameon=False, From 89fc2b7da5ffe198de9f733140e226b81a3b3cd0 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 21:59:20 +0200 Subject: [PATCH 13/16] Fix release docs and formatting --- docs/release.md | 2 +- python/micromode/raster.py | 1 + python/micromode/scipy_reference.py | 4 +--- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/release.md b/docs/release.md index c4d7c73..6fbe625 100644 --- a/docs/release.md +++ b/docs/release.md @@ -63,7 +63,7 @@ python -m venv /tmp/micromode-testpypi 4. Tag the release with the same version from `pyproject.toml`: ```bash -git tag v0.1.0a3 +git tag v$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") git push origin main --tags ``` diff --git a/python/micromode/raster.py b/python/micromode/raster.py index 7d01c37..6abacfe 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -507,6 +507,7 @@ def _solve_one_frequency_scipy_tensorial( ), ) + def _transformed_material_tensors( eps: np.ndarray, mu: np.ndarray, diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 3ed8511..5090722 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -89,9 +89,7 @@ def solve_diagonal_scipy_reference( operators = _assemble_diagonal_operators(sparse, eps_tensor, mu_tensor, derivatives) operator = cast(Any, operators["mat"]) eig_guess = complex(-(neff_guess * neff_guess), 0.0) - operator, arpack_initial_vector, arpack_guess = _real_arpack_problem_if_close( - operator, initial_vector, eig_guess - ) + operator, arpack_initial_vector, arpack_guess = _real_arpack_problem_if_close(operator, initial_vector, eig_guess) values, vectors = _selected_eigenpairs( operator, num_modes=num_modes, From c5e186c05794871ca1a6acb20d3383045295637e Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 22:00:32 +0200 Subject: [PATCH 14/16] Harden PML validation and preserve sweep diagnostics --- python/micromode/models.py | 29 +++++++++++++++++++++++------ python/micromode/sweep.py | 1 + tests/test_micromode_api.py | 7 +++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/python/micromode/models.py b/python/micromode/models.py index e4aaa76..31d1718 100644 --- a/python/micromode/models.py +++ b/python/micromode/models.py @@ -4,7 +4,8 @@ from collections.abc import Sequence from dataclasses import dataclass -from typing import Literal +from operator import index +from typing import Literal, SupportsIndex, cast import numpy as np @@ -24,9 +25,13 @@ class PmlSpec: order: int = 3 def __post_init__(self) -> None: - if len(self.num_cells) != 2 or any(int(value) < 0 for value in self.num_cells): + if len(self.num_cells) != 2: raise ValueError("num_cells must contain two non-negative integers") - object.__setattr__(self, "num_cells", (int(self.num_cells[0]), int(self.num_cells[1]))) + num_cells = ( + _coerce_integral("num_cells", self.num_cells[0], minimum=0), + _coerce_integral("num_cells", self.num_cells[1], minimum=0), + ) + object.__setattr__(self, "num_cells", num_cells) for name in ("sigma_max", "kappa_min", "kappa_max"): value = float(getattr(self, name)) if not np.isfinite(value) or value <= 0.0: @@ -34,9 +39,7 @@ def __post_init__(self) -> None: object.__setattr__(self, name, value) if self.kappa_max < self.kappa_min: raise ValueError("kappa_max must be greater than or equal to kappa_min") - if int(self.order) <= 0: - raise ValueError("order must be positive") - object.__setattr__(self, "order", int(self.order)) + object.__setattr__(self, "order", _coerce_integral("order", self.order, minimum=1)) @classmethod def from_num_cells(cls, num_cells: tuple[int, int]) -> PmlSpec: @@ -483,6 +486,20 @@ def _stack_diagonal_components( return np.stack([xx_array, yy_array, zz_array], axis=0) +def _coerce_integral(name: str, value: object, *, minimum: int) -> int: + if isinstance(value, (bool, np.bool_)): + raise ValueError(f"{name} must contain integers") + try: + integer = index(cast(SupportsIndex, value)) + except TypeError as exc: + raise ValueError(f"{name} must contain integers") from exc + if integer < minimum: + if minimum == 0: + raise ValueError(f"{name} must contain non-negative integers") + raise ValueError(f"{name} must be positive") + return int(integer) + + def _normalize_slice_axis(axis: SliceAxis) -> int: if axis in {"x", 0}: return 0 diff --git a/python/micromode/sweep.py b/python/micromode/sweep.py index 9609816..aa6ac83 100644 --- a/python/micromode/sweep.py +++ b/python/micromode/sweep.py @@ -135,4 +135,5 @@ def _reorder_result_modes(result: Result, order: tuple[int, ...]) -> Result: field_components=field_components, n_group=n_group, dispersion=dispersion, + solver_info=result.solver_info, ) diff --git a/tests/test_micromode_api.py b/tests/test_micromode_api.py index 72ac4a3..51346ea 100644 --- a/tests/test_micromode_api.py +++ b/tests/test_micromode_api.py @@ -637,6 +637,7 @@ def result(n_values: list[float], order: tuple[str, str]) -> mm.Result: for mode_index, component in enumerate(order): fields[component][..., mode_index] = 1.0 arrays = {component: xr.DataArray(values, dims=dims, coords=coords) for component, values in fields.items()} + solver_info = {"backend": "synthetic", "n_values": n_values} return mm.Result( n_complex=xr.DataArray( [np.asarray(n_values, dtype=np.complex128)], @@ -644,6 +645,7 @@ def result(n_values: list[float], order: tuple[str, str]) -> mm.Result: coords={"f": coords["f"], "mode_index": coords["mode_index"]}, ), field_components=arrays, + solver_info=solver_info, ) first = result([2.2, 1.9], ("Ex", "Ey")) @@ -653,6 +655,7 @@ def result(n_values: list[float], order: tuple[str, str]) -> mm.Result: sweep = mm.Sweep(values=np.asarray([0.4, 0.5]), results=tracked, parameter_name="width") np.testing.assert_allclose(tracked[1].n_complex.values, [[2.18, 1.91]]) + assert tracked[1].solver_info == {"backend": "synthetic", "n_values": [1.91, 2.18]} np.testing.assert_allclose(sweep.n_eff[:, 0], [2.2, 2.18]) assert {"width", "mode_index", "n_eff", "te_fraction"} <= set(sweep.to_dataframe().columns) @@ -682,3 +685,7 @@ def test_spec_validation_for_core_options(): mm.Spec(num_modes=0) with pytest.raises(ValueError, match="bend_axis"): mm.Spec(bend_radius=5.0) + with pytest.raises(ValueError, match="num_cells"): + mm.PmlSpec(num_cells=(1.5, 0)) # type: ignore[arg-type] + with pytest.raises(ValueError, match="order"): + mm.PmlSpec(order=2.5) # type: ignore[arg-type] From 37d5d8883be0d8e516263ed61c55a5debd6981d6 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 22:14:07 +0200 Subject: [PATCH 15/16] Add docstrings to enhance clarity across benchmarks and examples --- benchmarks/compare_mode_solver_fixtures.py | 14 +++++++++ benchmarks/compare_tidy3d_backends.py | 16 ++++++++++ benchmarks/micromode_solver_benchmark.py | 6 ++++ benchmarks/mode_solver/fixtures.py | 13 ++++++++ examples/material_grid_demos.py | 19 ++++++++++++ examples/ridge_waveguide_readme.py | 11 +++++++ examples/soi_hybridization_sweep.py | 24 +++++++++++++++ examples/tidy3d_modal_sources_monitors.py | 8 +++++ python/micromode/__init__.py | 2 ++ python/micromode/models.py | 29 +++++++++++++++++ python/micromode/raster.py | 15 +++++++++ python/micromode/result.py | 23 ++++++++++++++ python/micromode/scipy_reference.py | 36 ++++++++++++++++++++++ python/micromode/sweep.py | 8 +++++ scripts/check_dist_artifacts.py | 2 ++ scripts/check_release_metadata.py | 4 +++ scripts/generate_coverage_badge.py | 4 +++ scripts/smoke_dist.py | 5 +++ scripts/smoke_wheel.py | 1 + tests/conftest.py | 2 ++ tests/test_micromode_api.py | 29 +++++++++++++++++ tests/test_mode_solver_fixtures.py | 14 +++++++++ 22 files changed, 285 insertions(+) diff --git a/benchmarks/compare_mode_solver_fixtures.py b/benchmarks/compare_mode_solver_fixtures.py index 2b4b2cf..2a33e08 100644 --- a/benchmarks/compare_mode_solver_fixtures.py +++ b/benchmarks/compare_mode_solver_fixtures.py @@ -1,3 +1,5 @@ +"""CLI and helpers for comparing committed mode-solver fixtures against MicroMode.""" + from __future__ import annotations import argparse @@ -28,6 +30,7 @@ def parse_args() -> argparse.Namespace: + """Parse fixture-comparison CLI options.""" parser = argparse.ArgumentParser(description="Inspect committed mode-solver reference fixtures.") parser.add_argument( "--suite", @@ -72,6 +75,7 @@ def parse_args() -> argparse.Namespace: def main() -> None: + """Inspect fixtures and optionally run local comparisons.""" args = parse_args() fixture_root = args.fixture_root or (DEFAULT_FIXTURE_ROOT / args.suite) manifest = read_json(manifest_path(fixture_root)) @@ -125,6 +129,7 @@ def main() -> None: def _compare_local_case(root: Path, entry: dict) -> dict: + """Run one reconstructable fixture recipe through MicroMode and compare outputs.""" case_id = entry["case_id"] try: import micromode as sm @@ -321,6 +326,7 @@ def _compare_local_case(root: Path, entry: dict) -> dict: def _solver_edges_from_field_coords( edges: tuple[np.ndarray, np.ndarray], recipe: dict ) -> tuple[np.ndarray, np.ndarray]: + """Derive solver edge coordinates from fixture field coordinates.""" dmin_pmc = tuple(bool(value) for value in recipe.get("dmin_pmc", (False, False))) trim_edges = tuple(recipe.get("trim_edges", ((0, 0), (0, 0)))) out = [] @@ -354,6 +360,7 @@ def _solve_recipe( normal_dim: str, normal_coord: float, ): + """Solve all frequencies described by a local fixture recipe.""" freqs = tuple(float(freq) for freq in ref_n.coords["f"].values) if recipe.get("solve_each_frequency"): rows = [] @@ -424,6 +431,7 @@ def _solve_recipe_for_freq( normal_dim: str, normal_coord: float, ): + """Solve one fixture recipe for one frequency or frequency tuple.""" freqs = (float(freq),) if np.isscalar(freq) else tuple(float(value) for value in freq) centers = tuple((axis_edges[:-1] + axis_edges[1:]) / 2 for axis_edges in edges) eps_xx, eps_yy, eps_zz = _eps_components_from_recipe(recipe, edges, centers, tangent_dims, freqs[0], sm) @@ -460,6 +468,7 @@ def _eps_components_from_recipe( freq: float, sm, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Rasterize diagonal epsilon components at Yee sample locations.""" if recipe.get("yee_staggered", True): return ( _eps_from_recipe(recipe, (coords[0], edges[1][:-1]), tangent_dims, freq, sm), @@ -477,6 +486,7 @@ def _eps_from_recipe( freq: float, sm, ) -> np.ndarray: + """Rasterize primitive boxes and circles onto one component grid.""" grids = np.meshgrid(*coords, indexing="ij") eps = np.full(tuple(len(coord) for coord in coords), recipe.get("clad_eps", 1.0), dtype=np.complex128) for box in recipe.get("boxes", ()): @@ -502,6 +512,7 @@ def _eps_from_recipe( def _reorder_modes(values: np.ndarray, recipe: dict) -> np.ndarray: + """Apply fixture-specific mode ordering before comparison.""" if recipe.get("sort_order") != "ascending": return values order = np.argsort(values.real, axis=1) @@ -509,6 +520,7 @@ def _reorder_modes(values: np.ndarray, recipe: dict) -> np.ndarray: def _reorder_field_modes(values: np.ndarray, recipe: dict) -> np.ndarray: + """Apply fixture-specific field ordering for diagnostic overlap checks.""" if recipe.get("sort_order") != "ascending": return values # Field reordering is only used for a coarse overlap diagnostic; n sorting is authoritative. @@ -516,10 +528,12 @@ def _reorder_field_modes(values: np.ndarray, recipe: dict) -> np.ndarray: def _status(status: str, summary: str, **details) -> dict: + """Build a normalized status dictionary for report output.""" return {"status": status, "failed": status == "fail", "summary": summary, **details} def _n_tolerance(entry: dict, recipe: dict | None = None) -> float: + """Resolve the effective-index tolerance for one fixture case.""" if recipe is not None: recipe_tolerance = recipe.get("n_tolerance") if recipe_tolerance is not None: diff --git a/benchmarks/compare_tidy3d_backends.py b/benchmarks/compare_tidy3d_backends.py index d37157b..3cd6d32 100644 --- a/benchmarks/compare_tidy3d_backends.py +++ b/benchmarks/compare_tidy3d_backends.py @@ -1,3 +1,5 @@ +"""Benchmark MicroMode against equivalent Tidy3D mode-solver setups.""" + from __future__ import annotations import argparse @@ -13,6 +15,8 @@ @dataclass(frozen=True) class BenchmarkCase: + """Configuration for one backend comparison problem.""" + case_id: str description: str ny: int @@ -53,6 +57,7 @@ class BenchmarkCase: def main() -> None: + """Run selected backend-comparison cases and print a markdown table.""" args = parse_args() cases = list(PRESETS[args.preset]) rows = [run_case(case, profile_source=args.profile_source) for case in cases] @@ -64,6 +69,7 @@ def main() -> None: def parse_args() -> argparse.Namespace: + """Parse backend benchmark CLI options.""" parser = argparse.ArgumentParser(description="Compare MicroMode SciPy and Tidy3D local solves.") parser.add_argument("--preset", choices=tuple(PRESETS), default="quick") parser.add_argument( @@ -77,6 +83,7 @@ def parse_args() -> argparse.Namespace: def run_case(case: BenchmarkCase, *, profile_source: str) -> dict[str, object]: + """Execute one benchmark case for MicroMode and Tidy3D.""" tidy3d_solver = make_tidy3d_solver(case) materials = ( micromode_materials_from_tidy3d_solver(tidy3d_solver, case) @@ -111,6 +118,7 @@ def run_case(case: BenchmarkCase, *, profile_source: str) -> dict[str, object]: def time_micromode(case: BenchmarkCase, materials: mm.Materials) -> tuple[float, np.ndarray]: + """Time the MicroMode solve for a prepared material grid.""" start = time.perf_counter() data = mm.solve_modes( material_grid=materials, @@ -124,12 +132,14 @@ def time_micromode(case: BenchmarkCase, materials: mm.Materials) -> tuple[float, def time_tidy3d(solver) -> tuple[float, np.ndarray]: + """Time a Tidy3D mode solve.""" start = time.perf_counter() data = solver.solve() return time.perf_counter() - start, np.asarray(data.n_eff.values[0], dtype=float) def make_tidy3d_solver(case: BenchmarkCase): + """Construct a Tidy3D mode solver for one benchmark case.""" try: import tidy3d as td from tidy3d.plugins.mode import ModeSolver @@ -157,6 +167,7 @@ def make_tidy3d_solver(case: BenchmarkCase): def micromode_materials_from_tidy3d_solver(solver, case: BenchmarkCase) -> mm.Materials: + """Rasterize Tidy3D solver materials into a MicroMode grid.""" eps = np.asarray(solver._solver_eps(tidy3d_frequency()), dtype=np.complex128) grid = solver._solver_grid return mm.Materials.from_components( @@ -176,12 +187,14 @@ def micromode_materials_from_tidy3d_solver(solver, case: BenchmarkCase) -> mm.Ma def tidy3d_frequency() -> float: + """Return the benchmark frequency in Hz.""" import tidy3d as td return float(td.C_0 / WAVELENGTH_UM) def micromode_materials(case: BenchmarkCase) -> mm.Materials: + """Build a direct MicroMode material grid for one benchmark case.""" y_edges = np.linspace(-0.5 * WIDTH_Y, 0.5 * WIDTH_Y, case.ny + 1) z_edges = np.linspace(-0.5 * WIDTH_Z, 0.5 * WIDTH_Z, case.nz + 1) y = 0.5 * (y_edges[:-1] + y_edges[1:]) @@ -200,6 +213,7 @@ def micromode_materials(case: BenchmarkCase) -> mm.Materials: def tidy3d_structures(td, problem: str): + """Return Tidy3D geometry structures for one benchmark problem.""" structures = [ td.Structure( geometry=td.Box(center=(0.0, 0.0, -0.5001), size=(td.inf, td.inf, 1.0002)), @@ -227,6 +241,7 @@ def tidy3d_structures(td, problem: str): def max_abs_delta(left: np.ndarray, right: np.ndarray) -> float: + """Return the maximum absolute difference between sorted mode arrays.""" count = min(left.size, right.size) if count == 0: return float("nan") @@ -234,6 +249,7 @@ def max_abs_delta(left: np.ndarray, right: np.ndarray) -> float: def markdown_table(rows: list[dict[str, object]]) -> str: + """Format benchmark rows as a markdown table.""" header = ( "| Problem | Grid | MicroMode SciPy (s) | Tidy3D local (s) | max abs Δn_eff SciPy/Tidy3D |\n" "|---|---:|---:|---:|---:|" diff --git a/benchmarks/micromode_solver_benchmark.py b/benchmarks/micromode_solver_benchmark.py index 8ec7410..0b8e16f 100644 --- a/benchmarks/micromode_solver_benchmark.py +++ b/benchmarks/micromode_solver_benchmark.py @@ -1,3 +1,5 @@ +"""Time MicroMode solves and record sparse-operator diagnostics.""" + from __future__ import annotations import argparse @@ -11,6 +13,7 @@ def main() -> None: + """Run timing cases and write a JSON benchmark report.""" args = parse_args() rows = [] grids = args.grid or ["20x14", "32x22", "48x32"] @@ -55,6 +58,7 @@ def main() -> None: def parse_args() -> argparse.Namespace: + """Parse solver benchmark CLI options.""" parser = argparse.ArgumentParser(description="Benchmark MicroMode sparse solves over grid sizes.") parser.add_argument( "--grid", @@ -73,6 +77,7 @@ def parse_args() -> argparse.Namespace: def parse_grid_sizes(values: list[str]) -> list[tuple[int, int]]: + """Parse grid-size strings like 20x14 into integer pairs.""" sizes = [] for value in values: left, sep, right = value.lower().partition("x") @@ -83,6 +88,7 @@ def parse_grid_sizes(values: list[str]) -> list[tuple[int, int]]: def strip_materials(*, nx: int, ny: int) -> mm.Materials: + """Build a simple strip-waveguide material grid for timing.""" x_edges = np.linspace(-1.2, 1.2, nx + 1) y_edges = np.linspace(-0.8, 0.8, ny + 1) x = 0.5 * (x_edges[:-1] + x_edges[1:]) diff --git a/benchmarks/mode_solver/fixtures.py b/benchmarks/mode_solver/fixtures.py index 501c328..051da7d 100644 --- a/benchmarks/mode_solver/fixtures.py +++ b/benchmarks/mode_solver/fixtures.py @@ -1,3 +1,5 @@ +"""Shared helpers for reading committed mode-solver reference fixtures.""" + from __future__ import annotations import hashlib @@ -15,22 +17,27 @@ def case_dir(root: Path, case_id: str) -> Path: + """Return the directory containing one fixture case.""" return root / case_id def data_path(root: Path, case_id: str) -> Path: + """Return the HDF5 path for one fixture case.""" return case_dir(root, case_id) / "mode_data.hdf5" def summary_path(root: Path, case_id: str) -> Path: + """Return the JSON summary path for one fixture case.""" return case_dir(root, case_id) / "summary.json" def manifest_path(root: Path) -> Path: + """Return the manifest path for a fixture suite.""" return root / "manifest.json" def sha256_file(path: Path) -> str: + """Return the SHA-256 digest for a file.""" digest = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): @@ -39,14 +46,17 @@ def sha256_file(path: Path) -> str: def read_json(path: Path) -> dict[str, Any]: + """Read a JSON file into a dictionary.""" return json.loads(path.read_text()) def write_json(path: Path, payload: dict[str, Any]) -> None: + """Write a dictionary as stable, pretty JSON.""" path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") def iter_manifest_entries(root: Path) -> tuple[dict[str, Any], ...]: + """Return all case entries from a suite manifest.""" return tuple(read_json(manifest_path(root))["cases"]) @@ -65,6 +75,7 @@ def load_data_array(path: Path, name: str) -> xr.DataArray: def _read_xarray_group(group: Any) -> xr.DataArray: + """Read a legacy xarray-style HDF5 group.""" if _XARRAY_VALUE_NAME not in group: raise KeyError(f"HDF5 group {group.name!r} is missing {_XARRAY_VALUE_NAME!r}") values = group[_XARRAY_VALUE_NAME][()] @@ -75,6 +86,7 @@ def _read_xarray_group(group: Any) -> xr.DataArray: def _infer_dims(shape: tuple[int, ...], coords: dict[str, np.ndarray]) -> tuple[str, ...]: + """Infer dimension names from HDF5 coordinate datasets.""" dims = tuple(dim for dim in _PREFERRED_DIMS if dim in coords) if len(dims) == len(shape) and all(len(coords[dim]) == size for dim, size in zip(dims, shape, strict=True)): return dims @@ -86,6 +98,7 @@ def _infer_dims(shape: tuple[int, ...], coords: dict[str, np.ndarray]) -> tuple[ def phase_aligned_relative_error(golden: np.ndarray, actual: np.ndarray) -> tuple[float, float]: + """Compare complex arrays after removing a global phase offset.""" g = np.asarray(golden).reshape(-1) a = np.asarray(actual).reshape(-1) norm_g = float(np.linalg.norm(g)) diff --git a/examples/material_grid_demos.py b/examples/material_grid_demos.py index 6b47ead..c2fe0c2 100644 --- a/examples/material_grid_demos.py +++ b/examples/material_grid_demos.py @@ -1,3 +1,5 @@ +"""Render several standalone material-grid demonstrations for MicroMode.""" + from __future__ import annotations import argparse @@ -23,6 +25,8 @@ @dataclass(frozen=True) class GridDemo: + """Definition of one material-grid demonstration case.""" + key: str title: str description: str @@ -37,6 +41,7 @@ class GridDemo: def main() -> None: + """Run the example script from parsed command-line options.""" args = parse_args() selected = select_demos(args.cases) args.output_dir.mkdir(parents=True, exist_ok=True) @@ -67,6 +72,7 @@ def main() -> None: def parse_args() -> argparse.Namespace: + """Parse command-line options for the example script.""" parser = argparse.ArgumentParser(description="Run pure Materials MicroMode demos.") parser.add_argument( "--output-dir", @@ -85,6 +91,7 @@ def parse_args() -> argparse.Namespace: def select_demos(case_keys: Sequence[str] | None) -> list[GridDemo]: + """Return the selected demo cases in display order.""" demos = grid_demos() if not case_keys: return demos @@ -97,6 +104,7 @@ def select_demos(case_keys: Sequence[str] | None) -> list[GridDemo]: def grid_demos() -> list[GridDemo]: + """Return all available material-grid demo definitions.""" return [ GridDemo( key="strip_grid", @@ -156,6 +164,7 @@ def grid_demos() -> list[GridDemo]: def make_strip_grid() -> tuple[sm.Materials, np.ndarray]: + """Build a rectangular strip-waveguide demo grid.""" x_edges, y_edges, xx, yy = demo_grid(nx=42, ny=30) eps = np.full(xx.shape, SIO2_EPS, dtype=np.complex128) eps[(np.abs(xx) <= 0.25) & (np.abs(yy) <= 0.11)] = SI_EPS @@ -163,6 +172,7 @@ def make_strip_grid() -> tuple[sm.Materials, np.ndarray]: def make_slot_grid() -> tuple[sm.Materials, np.ndarray]: + """Build a slot-waveguide demo grid.""" x_edges, y_edges, xx, yy = demo_grid(nx=42, ny=30) eps = np.full(xx.shape, SIO2_EPS, dtype=np.complex128) rail = np.abs(yy) <= 0.11 @@ -173,6 +183,7 @@ def make_slot_grid() -> tuple[sm.Materials, np.ndarray]: def make_rib_grid() -> tuple[sm.Materials, np.ndarray]: + """Build a rib-waveguide demo grid.""" x_edges, y_edges, xx, yy = demo_grid(nx=46, ny=30) eps = np.full(xx.shape, SIO2_EPS, dtype=np.complex128) slab = (np.abs(xx) <= 0.72) & (yy >= -0.16) & (yy <= -0.05) @@ -182,6 +193,7 @@ def make_rib_grid() -> tuple[sm.Materials, np.ndarray]: def make_circular_rod_grid() -> tuple[sm.Materials, np.ndarray]: + """Build a circular dielectric rod demo grid.""" x_edges, y_edges, xx, yy = demo_grid(nx=42, ny=34, width=2.2, height=1.8) eps = np.full(xx.shape, AIR_EPS, dtype=np.complex128) eps[xx**2 + yy**2 <= 0.32**2] = 2.6**2 @@ -189,6 +201,7 @@ def make_circular_rod_grid() -> tuple[sm.Materials, np.ndarray]: def make_anisotropic_tensor_grid() -> tuple[sm.Materials, np.ndarray]: + """Build a tensor-material demo grid.""" x_edges, y_edges, xx, yy = demo_grid(nx=28, ny=22, width=2.0, height=1.4) core = (np.abs(xx) <= 0.32) & (np.abs(yy) <= 0.16) eps_xx = np.full(xx.shape, SIO2_EPS, dtype=np.complex128) @@ -220,6 +233,7 @@ def demo_grid( width: float = 2.4, height: float = 1.6, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Build a rectangular grid with one or more filled boxes.""" x_edges = np.linspace(-width / 2, width / 2, nx + 1) y_edges = np.linspace(-height / 2, height / 2, ny + 1) x = 0.5 * (x_edges[:-1] + x_edges[1:]) @@ -235,6 +249,7 @@ def plot_demo( data: sm.Result, output_path: Path, ) -> None: + """Plot one demo result and save it.""" mode_count = int(data.n_complex.shape[1]) columns = ("eps_xx", "|E|", "Re(Ex)", "Re(Ey)", "Re(Ez)") fig, axes = plt.subplots( @@ -277,6 +292,7 @@ def plot_demo( def component_image( data: sm.Result, component: str, mode_index: int ) -> tuple[tuple[str, str], tuple[np.ndarray, np.ndarray], np.ndarray]: + """Extract one field component image for plotting.""" field = data.field_components[component] image = field.isel(f=0, mode_index=mode_index).squeeze(drop=True) spatial_dims = tuple(dim for dim in ("x", "y", "z") if dim in image.dims and image.sizes[dim] > 1) @@ -291,6 +307,7 @@ def component_image( def electric_magnitude_image( data: sm.Result, mode_index: int ) -> tuple[tuple[str, str], tuple[np.ndarray, np.ndarray], np.ndarray]: + """Compute electric-field magnitude for one mode image.""" dims, coords, ex = component_image(data, "Ex", mode_index) magnitude_squared = np.abs(ex) ** 2 for component in ("Ey", "Ez"): @@ -311,6 +328,7 @@ def draw_image( cmap: str, symmetric: bool, ): + """Draw a field image on an axes object with consistent scaling.""" x, y = coords dx = float(np.median(np.diff(x))) if len(x) > 1 else 1.0 dy = float(np.median(np.diff(y))) if len(y) > 1 else 1.0 @@ -345,6 +363,7 @@ def draw_image( def plot_eps_contours(ax: Axes, coords: tuple[np.ndarray, np.ndarray], eps: np.ndarray) -> None: + """Overlay material-index contours on a field plot.""" values = np.asarray(eps.real, dtype=float) if np.nanmax(values) - np.nanmin(values) < 1e-12: return diff --git a/examples/ridge_waveguide_readme.py b/examples/ridge_waveguide_readme.py index 0d1f39c..f806197 100644 --- a/examples/ridge_waveguide_readme.py +++ b/examples/ridge_waveguide_readme.py @@ -26,6 +26,7 @@ def publication_style() -> dict[str, object]: + """Return matplotlib rcParams for generated example figures.""" return { "font.family": "DejaVu Sans", "font.size": 9, @@ -46,6 +47,7 @@ def publication_style() -> dict[str, object]: def main() -> None: + """Run the example script from parsed command-line options.""" parser = argparse.ArgumentParser(description="Solve and plot a rasterized angled slab waveguide.") parser.add_argument("--step", type=float, default=0.02, help="Grid step in microns.") parser.add_argument("--subpixels", type=int, default=7, help="Subpixel samples per axis for material averaging.") @@ -102,6 +104,7 @@ def ridge_waveguide_materials(step: float = 0.02, subpixels: int = 7) -> tuple[m def centered_edges(*, width: float, step: float) -> np.ndarray: + """Return evenly spaced cell edges centered on zero.""" if step <= 0.0: raise ValueError("step must be positive") cells = round(width / step) @@ -111,6 +114,7 @@ def centered_edges(*, width: float, step: float) -> np.ndarray: def offset_edges(*, lower: float, upper: float, step: float) -> np.ndarray: + """Return evenly spaced cell edges covering an explicit interval.""" if step <= 0.0: raise ValueError("step must be positive") if upper <= lower: @@ -127,6 +131,7 @@ def subpixel_centers( *, subpixels: int, ) -> tuple[np.ndarray, np.ndarray]: + """Return subpixel sample coordinates inside each solver cell.""" if subpixels <= 0: raise ValueError("subpixels must be positive") offsets = (np.arange(subpixels, dtype=float) + 0.5) / subpixels @@ -139,6 +144,7 @@ def subpixel_centers( def plot_index(materials: mm.Materials, eps: np.ndarray, path: Path) -> None: + """Plot the material index profile.""" x_edges = np.asarray(materials.grid.x_edges, dtype=float) y_edges = np.asarray(materials.grid.y_edges, dtype=float) x = 0.5 * (x_edges[:-1] + x_edges[1:]) @@ -168,6 +174,7 @@ def plot_index(materials: mm.Materials, eps: np.ndarray, path: Path) -> None: def plot_modes(materials: mm.Materials, eps: np.ndarray, data: mm.Result, path: Path) -> None: + """Plot representative mode fields.""" components = ("Ex", "Ey", "Hx", "Hy") x_edges = np.asarray(materials.grid.x_edges, dtype=float) y_edges = np.asarray(materials.grid.y_edges, dtype=float) @@ -245,6 +252,7 @@ def plot_readme_figure(materials: mm.Materials, eps: np.ndarray, data: mm.Result def draw_material_outline(ax, x: np.ndarray, y: np.ndarray, eps: np.ndarray, **kwargs) -> None: + """Draw an epsilon contour outlining the waveguide material.""" if "color" in kwargs: kwargs["colors"] = kwargs.pop("color") if "linewidth" in kwargs: @@ -254,17 +262,20 @@ def draw_material_outline(ax, x: np.ndarray, y: np.ndarray, eps: np.ndarray, **k def format_complex(value: complex, *, precision: int) -> str: + """Format a complex number for compact plot annotations.""" sign = "+" if value.imag >= 0.0 else "-" return f"{value.real:.{precision}f}{sign}{abs(value.imag):.{precision}g}i" def normalize_signed(values: np.ndarray) -> np.ndarray: + """Normalize signed field data to unit peak magnitude.""" values = np.asarray(values, dtype=float) scale = max(float(np.nanmax(np.abs(values))), np.finfo(float).eps) return values / scale def save_figure(fig, path: Path) -> None: + """Write a figure to disk and close it.""" fig.savefig(path) fig.savefig(path.with_suffix(".pdf")) fig.savefig(path.with_suffix(".svg")) diff --git a/examples/soi_hybridization_sweep.py b/examples/soi_hybridization_sweep.py index a7bd34d..a7d56e3 100644 --- a/examples/soi_hybridization_sweep.py +++ b/examples/soi_hybridization_sweep.py @@ -1,3 +1,5 @@ +"""Sweep SOI ridge width and visualize mode hybridization.""" + from __future__ import annotations import argparse @@ -23,6 +25,7 @@ def publication_style() -> dict[str, object]: + """Return matplotlib rcParams for generated example figures.""" return { "font.family": "DejaVu Sans", "font.size": 9, @@ -43,6 +46,7 @@ def publication_style() -> dict[str, object]: def main() -> None: + """Run the example script from parsed command-line options.""" args = parse_args() args.output_dir.mkdir(parents=True, exist_ok=True) widths = np.arange(args.width_start, args.width_stop + 0.5 * args.width_step, args.width_step) @@ -83,6 +87,7 @@ def main() -> None: def parse_args() -> argparse.Namespace: + """Parse command-line options for the example script.""" parser = argparse.ArgumentParser(description="Run a MicroMode SOI mode-hybridization sweep.") parser.add_argument( "--output-dir", @@ -105,6 +110,7 @@ def parse_args() -> argparse.Namespace: def centered_edges(width: float, step: float) -> np.ndarray: + """Return evenly spaced cell edges centered on zero.""" half_cells = int(np.ceil(width / (2 * step))) return np.arange(-half_cells, half_cells + 1, dtype=float) * step @@ -116,6 +122,7 @@ def soi_ridge_materials( y_edges: np.ndarray, film_thickness: float, ) -> tuple[mm.Materials, np.ndarray]: + """Rasterize an SOI ridge for a requested top width.""" x = 0.5 * (x_edges[:-1] + x_edges[1:]) y = 0.5 * (y_edges[:-1] + y_edges[1:]) _xx, yy = np.meshgrid(x, y, indexing="ij") @@ -141,12 +148,14 @@ def rectangle_fill_fraction( y_min: float, y_max: float, ) -> np.ndarray: + """Compute cell fill fractions for a rectangle.""" x_overlap = interval_fill_fraction(x_edges, x_min, x_max) y_overlap = interval_fill_fraction(y_edges, y_min, y_max) return np.outer(x_overlap, y_overlap) def interval_fill_fraction(edges: np.ndarray, lower: float, upper: float) -> np.ndarray: + """Compute one-dimensional interval overlap fractions.""" cell_lower = edges[:-1] cell_upper = edges[1:] overlap = np.clip(np.minimum(cell_upper, upper) - np.maximum(cell_lower, lower), 0.0, None) @@ -155,6 +164,7 @@ def interval_fill_fraction(edges: np.ndarray, lower: float, upper: float) -> np. def write_summary(path: Path, sweep: mm.Sweep, args: argparse.Namespace) -> Path: + """Write a JSON summary of the sweep result.""" payload = { "wavelength_um": WAVELENGTH_UM, "n_si": N_SI, @@ -197,6 +207,7 @@ def sort_result_by_neff(result: mm.Result) -> mm.Result: def plot_sweep(path: Path, sweep: mm.Sweep) -> None: + """Plot effective index across the width sweep.""" with plt.rc_context(publication_style()): fig, ax = plt.subplots(figsize=(4.2, 3.0), constrained_layout=True) colors = [MODE_COLORS[index % len(MODE_COLORS)] for index in range(sweep.num_modes)] @@ -224,6 +235,7 @@ def plot_sweep(path: Path, sweep: mm.Sweep) -> None: def plot_te_fraction(path: Path, sweep: mm.Sweep) -> None: + """Plot TE fraction across the width sweep.""" pol = sweep.pol_fraction with plt.rc_context(publication_style()): fig, ax = plt.subplots(figsize=(4.2, 3.0), constrained_layout=True) @@ -297,6 +309,7 @@ def smooth_line( def pchip_slopes(x: np.ndarray, y: np.ndarray) -> np.ndarray: + """Compute monotone cubic Hermite slopes.""" h = np.diff(x) delta = np.diff(y) / h slopes = np.zeros_like(y) @@ -315,6 +328,7 @@ def pchip_slopes(x: np.ndarray, y: np.ndarray) -> np.ndarray: def pchip_endpoint_slope(h0: float, h1: float, delta0: float, delta1: float) -> float: + """Compute a shape-preserving endpoint slope.""" slope = ((2.0 * h0 + h1) * delta0 - h0 * delta1) / (h0 + h1) if np.sign(slope) != np.sign(delta0): return 0.0 @@ -324,6 +338,7 @@ def pchip_endpoint_slope(h0: float, h1: float, delta0: float, delta1: float) -> def evaluate_hermite(x: np.ndarray, y: np.ndarray, slopes: np.ndarray, x_dense: np.ndarray) -> np.ndarray: + """Evaluate cubic Hermite segments on a dense grid.""" interval_indices = np.searchsorted(x, x_dense, side="right") - 1 interval_indices = np.clip(interval_indices, 0, len(x) - 2) x0 = x[interval_indices] @@ -342,6 +357,7 @@ def evaluate_hermite(x: np.ndarray, y: np.ndarray, slopes: np.ndarray, x_dense: def plot_profiles(path: Path, widths: np.ndarray, results: tuple[mm.Result, ...], eps_grids: list[np.ndarray]) -> None: + """Plot selected field profiles from a sweep.""" selected_indices = unique_nearest_indices(widths, [0.5, 1.0, 2.0]) rows = len(selected_indices) columns = ("index", "mode0_abs", "mode0_ex", "mode1_abs", "mode1_ex") @@ -427,6 +443,7 @@ def plot_profiles(path: Path, widths: np.ndarray, results: tuple[mm.Result, ...] def unique_nearest_indices(values: np.ndarray, targets: list[float]) -> list[int]: + """Return unique indices nearest requested target values.""" indices = [] for target in targets: index = int(np.argmin(np.abs(values - target))) @@ -440,6 +457,7 @@ def component_image( component: str, mode_index: int, ) -> tuple[tuple[str, str], tuple[np.ndarray, np.ndarray], np.ndarray]: + """Extract one field component image for plotting.""" image = result.field_components[component].isel(f=0, mode_index=mode_index).squeeze(drop=True) spatial_dims = tuple(dim for dim in ("x", "y", "z") if dim in image.dims and image.sizes[dim] > 1) if len(spatial_dims) != 2: @@ -453,6 +471,7 @@ def electric_magnitude_image( result: mm.Result, mode_index: int, ) -> tuple[tuple[str, str], tuple[np.ndarray, np.ndarray], np.ndarray]: + """Compute electric-field magnitude for one mode image.""" dims, coords, ex = component_image(result, "Ex", mode_index) magnitude_squared = np.abs(ex) ** 2 for component in ("Ey", "Ez"): @@ -475,6 +494,7 @@ def draw_image( value_limits: tuple[float, float] | None = None, interpolation: str = "nearest", ): + """Draw a field image on an axes object with consistent scaling.""" x, y = coords dx = float(np.median(np.diff(x))) if len(x) > 1 else 1.0 dy = float(np.median(np.diff(y))) if len(y) > 1 else 1.0 @@ -500,18 +520,21 @@ def draw_image( def normalize_positive(values: np.ndarray) -> np.ndarray: + """Normalize nonnegative image data to unit peak magnitude.""" values = np.asarray(values, dtype=float) scale = max(float(np.nanmax(np.abs(values))), np.finfo(float).eps) return values / scale def normalize_signed(values: np.ndarray) -> np.ndarray: + """Normalize signed field data to unit peak magnitude.""" values = np.asarray(values, dtype=float) scale = max(float(np.nanmax(np.abs(values))), np.finfo(float).eps) return values / scale def plot_eps_contours(ax, coords: tuple[np.ndarray, np.ndarray], eps: np.ndarray) -> None: + """Overlay material-index contours on a field plot.""" values = np.asarray(eps.real, dtype=float) if np.nanmax(values) - np.nanmin(values) < 1e-12: return @@ -522,6 +545,7 @@ def plot_eps_contours(ax, coords: tuple[np.ndarray, np.ndarray], eps: np.ndarray def save_figure(fig, path: Path) -> None: + """Write a figure to disk and close it.""" fig.savefig(path) fig.savefig(path.with_suffix(".pdf")) fig.savefig(path.with_suffix(".svg")) diff --git a/examples/tidy3d_modal_sources_monitors.py b/examples/tidy3d_modal_sources_monitors.py index 887a773..893a324 100644 --- a/examples/tidy3d_modal_sources_monitors.py +++ b/examples/tidy3d_modal_sources_monitors.py @@ -32,6 +32,7 @@ def main() -> None: + """Run the example script from parsed command-line options.""" args = parse_args() args.output_dir.mkdir(parents=True, exist_ok=True) @@ -52,6 +53,7 @@ def main() -> None: def parse_args() -> argparse.Namespace: + """Parse command-line options for the example script.""" parser = argparse.ArgumentParser(description="Recreate the Tidy3D modal sources/monitors mode plot.") parser.add_argument( "--output-dir", @@ -91,6 +93,7 @@ def tidy3d_waveguide_materials(*, y_step: float, z_step: float) -> tuple[mm.Mate def centered_edges(*, width: float, step: float) -> np.ndarray: + """Return evenly spaced cell edges centered on zero.""" if step <= 0.0: raise ValueError("step must be positive") cells = round(width / step) @@ -100,6 +103,7 @@ def centered_edges(*, width: float, step: float) -> np.ndarray: def plot_tidy3d_mode_fields(materials: mm.Materials, eps: np.ndarray, data: mm.Result, path: Path) -> None: + """Plot Tidy3D-style modal field panels.""" y_edges = np.asarray(materials.grid.x_edges, dtype=float) z_edges = np.asarray(materials.grid.y_edges, dtype=float) y = 0.5 * (y_edges[:-1] + y_edges[1:]) @@ -135,17 +139,20 @@ def plot_tidy3d_mode_fields(materials: mm.Materials, eps: np.ndarray, data: mm.R def field_abs(data: mm.Result, *, component: str, mode_index: int) -> np.ndarray: + """Return the magnitude image for one field component and mode.""" field = data.field_components[component].isel(f=0, mode_index=mode_index).squeeze(drop=True) return np.abs(np.asarray(field.transpose("y", "z").values)) def draw_material_context(ax, y: np.ndarray, z: np.ndarray, eps: np.ndarray) -> None: + """Draw material outlines for the Tidy3D-style example.""" ax.axhline(0.0, color="#111111", linewidth=0.85, alpha=0.35) silicon_level = 0.5 * (N_AIR**2 + N_SI**2) ax.contour(y, z, eps.real.T, levels=[silicon_level], colors="#2f2f2f", linewidths=1.0, alpha=0.75) def publication_style() -> dict[str, object]: + """Return matplotlib rcParams for generated example figures.""" return { "font.family": "DejaVu Sans", "font.size": 10, @@ -161,6 +168,7 @@ def publication_style() -> dict[str, object]: def save_figure(fig, path: Path) -> None: + """Write a figure to disk and close it.""" fig.savefig(path) fig.savefig(path.with_suffix(".pdf")) fig.savefig(path.with_suffix(".svg")) diff --git a/python/micromode/__init__.py b/python/micromode/__init__.py index fa3d757..c4868ae 100644 --- a/python/micromode/__init__.py +++ b/python/micromode/__init__.py @@ -1,3 +1,5 @@ +"""Public package exports for the MicroMode Python API.""" + from __future__ import annotations from .constants import C_0, EPSILON_0 diff --git a/python/micromode/models.py b/python/micromode/models.py index 31d1718..9706b09 100644 --- a/python/micromode/models.py +++ b/python/micromode/models.py @@ -25,6 +25,7 @@ class PmlSpec: order: int = 3 def __post_init__(self) -> None: + """Normalize constructor inputs and enforce model invariants.""" if len(self.num_cells) != 2: raise ValueError("num_cells must contain two non-negative integers") num_cells = ( @@ -43,9 +44,11 @@ def __post_init__(self) -> None: @classmethod def from_num_cells(cls, num_cells: tuple[int, int]) -> PmlSpec: + """Build a PML specification from x/y cell counts.""" return cls(num_cells=num_cells) def as_dict(self) -> dict[str, float | int | tuple[int, int]]: + """Return a JSON-friendly representation of the specification.""" return { "num_cells": self.num_cells, "sigma_max": self.sigma_max, @@ -55,6 +58,7 @@ def as_dict(self) -> dict[str, float | int | tuple[int, int]]: } def profile_dict(self) -> dict[str, float | int]: + """Return only the scalar stretch-profile settings used by the solver.""" return { "sigma_max": self.sigma_max, "kappa_min": self.kappa_min, @@ -70,6 +74,7 @@ class BoundarySpec: low: tuple[BoundaryCondition, BoundaryCondition] = ("pec", "pec") def __post_init__(self) -> None: + """Normalize constructor inputs and enforce model invariants.""" if len(self.low) != 2: raise ValueError("low must contain two boundary conditions") normalized = tuple(str(value).lower() for value in self.low) @@ -80,13 +85,16 @@ def __post_init__(self) -> None: @property def dmin_pmc(self) -> tuple[bool, bool]: + """Return whether each low-edge boundary uses magnetic symmetry.""" return self.low[0] == "pmc", self.low[1] == "pmc" @property def dmin_pml(self) -> tuple[bool, bool]: + """Return whether each low-edge boundary is open to PML stretching.""" return self.low[0] == "pec", self.low[1] == "pec" def as_dict(self) -> dict[str, tuple[str, str]]: + """Return a JSON-friendly representation of the specification.""" return {"low": self.low} @@ -105,6 +113,7 @@ class Grid: normal_coordinate: float = 0.0 def __post_init__(self) -> None: + """Normalize constructor inputs and enforce model invariants.""" x_edges = tuple(float(value) for value in self.x_edges) y_edges = tuple(float(value) for value in self.y_edges) object.__setattr__(self, "x_edges", x_edges) @@ -120,6 +129,7 @@ def __post_init__(self) -> None: @property def shape(self) -> tuple[int, int]: + """Return the number of cells along the two mode-plane axes.""" return len(self.x_edges) - 1, len(self.y_edges) - 1 @@ -137,6 +147,7 @@ class Materials: mu_tensor: np.ndarray | None = None def __post_init__(self) -> None: + """Normalize constructor inputs and enforce model invariants.""" eps_tensor = np.asarray(self.eps_tensor, dtype=np.complex128) if eps_tensor.shape != (3, 3, *self.grid.shape): raise ValueError("eps_tensor must have shape (3, 3, nx, ny) matching the grid") @@ -168,6 +179,7 @@ def from_diagonal( normal_axis: Literal[0, 1, 2] = 2, normal_coordinate: float = 0.0, ) -> Materials: + """Build a material grid from scalar or diagonal tensor components.""" grid = Grid( tuple(float(value) for value in x_edges), tuple(float(value) for value in y_edges), @@ -213,6 +225,7 @@ def from_components( normal_axis: Literal[0, 1, 2] = 2, normal_coordinate: float = 0.0, ) -> Materials: + """Build a material grid from diagonal and off-diagonal tensor components.""" grid = Grid( tuple(float(value) for value in x_edges), tuple(float(value) for value in y_edges), @@ -306,6 +319,7 @@ def from_slice( cell_count = len(edge_values) - 1 def expand(label: str, values: np.ndarray | None) -> np.ndarray | None: + """Expand a one-dimensional component onto the padded mode-plane grid.""" if values is None: return None array = np.asarray(values, dtype=np.complex128) @@ -443,10 +457,12 @@ def average_subpixels( @property def shape(self) -> tuple[int, int]: + """Return the number of cells along the two mode-plane axes.""" return self.grid.shape @property def is_diagonal(self) -> bool: + """Return whether epsilon and mu contain only diagonal components.""" off_diagonal = np.ones((3, 3), dtype=bool) np.fill_diagonal(off_diagonal, False) mu_tensor = self._resolved_mu_tensor() @@ -455,17 +471,21 @@ def is_diagonal(self) -> bool: ) def flat_eps_tensor(self) -> np.ndarray: + """Return epsilon as flattened per-cell 3x3 tensors.""" return self.eps_tensor.reshape(3, 3, -1) def flat_mu_tensor(self) -> np.ndarray: + """Return mu as flattened per-cell 3x3 tensors.""" return self._resolved_mu_tensor().reshape(3, 3, -1) def _resolved_mu_tensor(self) -> np.ndarray: + """Return the initialized permeability tensor.""" if self.mu_tensor is None: raise RuntimeError("mu_tensor was not initialized") return self.mu_tensor def diagonal_eps(self) -> np.ndarray: + """Return the three diagonal epsilon components as a stacked array.""" return np.stack([self.eps_tensor[axis, axis] for axis in range(3)], axis=0) @@ -476,6 +496,7 @@ def _stack_diagonal_components( yy: np.ndarray | None, zz: np.ndarray | None, ) -> np.ndarray: + """Validate and stack x/y/z diagonal tensor components.""" xx_array = np.asarray(xx, dtype=np.complex128) if xx_array.shape != shape: raise ValueError(f"{label}_xx must have shape {shape}") @@ -487,6 +508,7 @@ def _stack_diagonal_components( def _coerce_integral(name: str, value: object, *, minimum: int) -> int: + """Coerce index-like values while rejecting floats and booleans.""" if isinstance(value, (bool, np.bool_)): raise ValueError(f"{name} must contain integers") try: @@ -501,6 +523,7 @@ def _coerce_integral(name: str, value: object, *, minimum: int) -> int: def _normalize_slice_axis(axis: SliceAxis) -> int: + """Map slice-axis aliases onto a zero-based axis index.""" if axis in {"x", 0}: return 0 if axis in {"y", 1}: @@ -509,6 +532,7 @@ def _normalize_slice_axis(axis: SliceAxis) -> int: def _diagonal_to_full_tensor(diagonal: np.ndarray) -> np.ndarray: + """Expand diagonal components into a full 3x3 tensor grid.""" tensor = np.zeros((3, 3, *diagonal.shape[1:]), dtype=np.complex128) for axis in range(3): tensor[axis, axis, :, :] = diagonal[axis] @@ -527,6 +551,7 @@ def _assign_tensor_offdiagonal( zx: np.ndarray | None, zy: np.ndarray | None, ) -> None: + """Validate and assign optional off-diagonal tensor components.""" for (row, col), suffix, values in [ ((0, 1), "xy", xy), ((0, 2), "xz", xz), @@ -557,6 +582,7 @@ class Spec: bend_axis: Literal[0, 1] | None = None def __post_init__(self) -> None: + """Normalize constructor inputs and enforce model invariants.""" if self.num_modes <= 0: raise ValueError("num_modes must be positive") if self.target_neff is not None and self.target_neff <= 0: @@ -579,12 +605,15 @@ def __post_init__(self) -> None: @property def has_angle(self) -> bool: + """Return whether an angular coordinate transform is requested.""" return abs(float(self.angle_theta)) > 0.0 @property def has_bend(self) -> bool: + """Return whether a bend transform is requested.""" return self.bend_radius is not None @property def has_transform(self) -> bool: + """Return whether any coordinate transform is requested.""" return self.has_angle or self.has_bend diff --git a/python/micromode/raster.py b/python/micromode/raster.py index 6abacfe..b5707f7 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -329,6 +329,7 @@ def _solve_one_frequency( bend_radius: float | None, bend_axis: int, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: + """Select the sparse formulation and solve a single frequency.""" # Forward and backward spacings represent the local Yee grid. The derivative # builders need both because E and H components are staggered. dlf = (np.diff(x_edges), np.diff(y_edges)) @@ -430,6 +431,7 @@ def _solve_one_frequency_scipy( krylov_dim: int | None, boundary_spec: BoundarySpec, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: + """Run the diagonal SciPy solver and reshape its fields.""" nx = len(dlf[0]) ny = len(dlf[1]) actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) @@ -476,6 +478,7 @@ def _solve_one_frequency_scipy_tensorial( krylov_dim: int | None, boundary_spec: BoundarySpec, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: + """Run the tensorial SciPy solver and reshape its fields.""" nx = len(dlf[0]) ny = len(dlf[1]) actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) @@ -519,6 +522,7 @@ def _transformed_material_tensors( bend_radius: float | None, bend_axis: int, ) -> tuple[np.ndarray, np.ndarray]: + """Apply angle and bend coordinate transforms to material tensors.""" # Transformation-optics material update: # eps' = J eps J.T / det(J) # mu' = J mu J.T / det(J) @@ -578,6 +582,7 @@ def _transformed_material_tensors( def _resolve_pml_spec( pml: PmlSpec | tuple[int, int] | None, ) -> PmlSpec: + """Normalize user PML input into a PmlSpec.""" if pml is None: return PmlSpec() if isinstance(pml, PmlSpec): @@ -588,6 +593,7 @@ def _resolve_pml_spec( def _resolve_boundary_spec( boundary: BoundarySpec | tuple[str, str] | None, ) -> BoundarySpec: + """Normalize user boundary input into a BoundarySpec.""" if boundary is None: return BoundarySpec() if isinstance(boundary, BoundarySpec): @@ -602,6 +608,7 @@ def _solver_info_with_context( shape: tuple[int, int], krylov_dim: int, ) -> dict[str, object]: + """Attach Python-side dispatch metadata to solver diagnostics.""" # Add enough Python-side context for saved Result files and benchmark # reports to be self-describing. out = dict(solver_info) @@ -614,12 +621,14 @@ def _solver_info_with_context( def _is_diagonal_tensor(tensor: np.ndarray, *, atol: float = 1e-12) -> bool: + """Return whether a flattened tensor has negligible off-diagonal terms.""" off_diagonal = np.ones((3, 3), dtype=bool) np.fill_diagonal(off_diagonal, False) return bool(np.all(np.abs(tensor[off_diagonal]) <= atol)) def _shift_target_neff(target_neff: float) -> float: + """Nudge the shift target away from exact eigenvalues for ARPACK stability.""" target_shift = float(10 * np.finfo(np.float32).eps) if abs(target_shift) > abs(target_neff * target_shift): return target_neff + target_shift @@ -627,6 +636,7 @@ def _shift_target_neff(target_neff: float) -> float: def _validate_edges(name: str, values: Sequence[float], cell_count: int) -> np.ndarray: + """Validate mode-plane edge coordinates against a cell count.""" edges = np.asarray(values, dtype=float) if edges.shape != (cell_count + 1,): raise ValueError(f"{name} must have length {cell_count + 1}") @@ -640,6 +650,7 @@ def _resolve_freqs( freqs: Sequence[float] | None, wavelength: float | Sequence[float] | None, ) -> tuple[float, ...]: + """Resolve exactly one frequency or wavelength input into frequencies.""" if (freqs is None) == (wavelength is None): raise ValueError("provide exactly one of freqs or wavelength") if freqs is not None: @@ -653,12 +664,14 @@ def _resolve_freqs( def _dual_steps(primal_steps: np.ndarray) -> np.ndarray: + """Return backward-grid step sizes from primal cell widths.""" if len(primal_steps) == 1: return primal_steps.copy() return np.hstack((primal_steps[0], (primal_steps[:-1] + primal_steps[1:]) / 2)) def _fields_to_grid(fields: list[np.ndarray], shape: tuple[int, int]) -> dict[str, np.ndarray]: + """Reshape flattened solver fields into x/y/mode arrays.""" nx, ny = shape mode_count = fields[0].shape[0] out = {} @@ -702,6 +715,7 @@ def _field_data_arrays( normal_axis: int, normal_coordinate: float, ) -> dict[str, xr.DataArray]: + """Wrap solved field arrays in coordinate-aware xarray objects.""" axis_names = ("x", "y", "z") if normal_axis not in {0, 1, 2}: raise ValueError("normal_axis must be 0, 1, or 2") @@ -740,6 +754,7 @@ def _field_data_arrays( def _default_initial_vector(size: int, shape: tuple[int, int] | None = None) -> np.ndarray: + """Build a deterministic ARPACK seed vector for reproducible solves.""" if shape is not None and size % (shape[0] * shape[1]) == 0: nx, ny = shape multiplier = size // (nx * ny) diff --git a/python/micromode/result.py b/python/micromode/result.py index a9d05db..9145e6c 100644 --- a/python/micromode/result.py +++ b/python/micromode/result.py @@ -344,11 +344,13 @@ def from_hdf5(cls, path: str | Path) -> Result: ) def _require_components(self, components: Iterable[str]) -> None: + """Raise if required field components are absent.""" missing = [component for component in components if component not in self.field_components] if missing: raise ValueError(f"field component(s) required but missing: {', '.join(missing)}") def _normal_dim(self) -> str: + """Infer the singleton propagation-normal dimension.""" reference = self._reference_field() attr_normal = reference.attrs.get("normal_dim") if attr_normal in _SPATIAL_DIMS and attr_normal in reference.dims: @@ -362,19 +364,23 @@ def _normal_dim(self) -> str: raise ValueError("fields must contain at least one spatial dimension") def _tangential_dims(self) -> tuple[str, ...]: + """Return the spatial dimensions transverse to propagation.""" normal = self._normal_dim() reference = self._reference_field() return tuple(dim for dim in _SPATIAL_DIMS if dim in reference.dims and dim != normal) def _reference_field(self) -> xr.DataArray: + """Return one field array used for dimension inference.""" if not self.field_components: raise ValueError("at least one field component is required") return next(iter(self.field_components.values())) def _spatial_axes(self, data_array: xr.DataArray) -> tuple[int, ...]: + """Return axis indices corresponding to spatial dimensions.""" return tuple(axis for axis, dim in enumerate(data_array.dims) if dim in _SPATIAL_DIMS) def _integrated_power(self, component: str) -> np.ndarray: + """Integrate squared field magnitude over the spatial grid.""" # Used by mode metrics such as polarization fraction and mode area. The # integration weights come from the mode-plane cell widths stored on each # field DataArray. @@ -385,6 +391,7 @@ def _integrated_power(self, component: str) -> np.ndarray: ) def _spatial_weights(self, data_array: xr.DataArray) -> np.ndarray: + """Build broadcastable cell-area weights from width coordinates.""" # Prefer explicit width coordinates written by the solver. Fall back to # midpoint-derived widths so externally constructed Results still work. weights: np.ndarray | float = 1.0 @@ -403,6 +410,7 @@ def _spatial_weights(self, data_array: xr.DataArray) -> np.ndarray: @staticmethod def _cell_widths(values: np.ndarray) -> np.ndarray: + """Infer cell widths from coordinate midpoints.""" if values.size <= 1: return np.ones(values.shape, dtype=float) midpoints = 0.5 * (values[1:] + values[:-1]) @@ -416,9 +424,11 @@ def _cell_widths(values: np.ndarray) -> np.ndarray: return np.abs(np.diff(edges)) def _fraction_dataset(self, te: np.ndarray, tm: np.ndarray) -> xr.Dataset: + """Package TE/TM fractions as a dataset aligned to modes.""" return xr.Dataset({"te": self._mode_data_array(te), "tm": self._mode_data_array(tm)}) def _mode_data_array(self, values: np.ndarray) -> xr.DataArray: + """Wrap per-mode values with the n_complex coordinates.""" return xr.DataArray(values, dims=self.n_complex.dims, coords=self.n_complex.coords) def _add_optional_metric( @@ -427,22 +437,26 @@ def _add_optional_metric( name: str, getter: Any, ) -> None: + """Add a metric if its required field components are available.""" try: metrics[name] = getter() except ValueError: return def _select_field(self, component: str, *, f: int | float, mode_index: int) -> xr.DataArray: + """Select one component at a frequency and mode index.""" data_array = self.field_components[component] data_array = data_array.isel(f=f) if isinstance(f, int) else data_array.sel(f=f, method="nearest") return data_array.isel(mode_index=mode_index) def _selected_mode_fields(self, *, f: int | float, mode_index: int) -> dict[str, xr.DataArray]: + """Select all available fields for one mode.""" return { component: self._select_field(component, f=f, mode_index=mode_index) for component in self.field_components } def _selected_frequency(self, f: int | float) -> float: + """Return the numeric frequency selected by index or nearest value.""" freq = self.n_complex.coords["f"] if isinstance(f, int): return float(freq.values[f]) @@ -450,6 +464,7 @@ def _selected_frequency(self, f: int | float) -> float: @staticmethod def _write_data_array(parent: Any, name: str, data_array: xr.DataArray) -> None: + """Write an xarray DataArray to the compact HDF5 layout.""" group = parent.create_group(name) group.attrs["dims"] = json.dumps(list(data_array.dims)) for attr_name, attr_value in data_array.attrs.items(): @@ -464,6 +479,7 @@ def _write_data_array(parent: Any, name: str, data_array: xr.DataArray) -> None: @staticmethod def _read_data_array(group: Any) -> xr.DataArray: + """Read an xarray DataArray from the compact HDF5 layout.""" dims = tuple(json.loads(group.attrs["dims"])) attrs = { key.removeprefix("attr:"): value @@ -489,6 +505,7 @@ def overlap( def _json_safe(value: Any) -> Any: + """Convert NumPy and complex values into JSON-safe objects.""" if isinstance(value, np.ndarray): return _json_safe(value.tolist()) if isinstance(value, (complex, np.complexfloating)): @@ -504,6 +521,7 @@ def _json_safe(value: Any) -> Any: def _loads_hdf5_json_attr(value: Any) -> Any: + """Decode a JSON HDF5 attribute stored as bytes or text.""" if isinstance(value, bytes): value = value.decode("utf-8") return json.loads(value) @@ -517,6 +535,7 @@ def _overlap_value( *, kind: str, ) -> complex: + """Compute one unnormalized field overlap integral.""" if kind not in _OVERLAP_KINDS: raise ValueError("kind must be 'electric', 'power', or 'lorentz'") if kind == "electric": @@ -554,6 +573,7 @@ def _normal_power_integrand( right: dict[str, xr.DataArray], normal: str, ) -> np.ndarray: + """Return the normal Poynting-flux integrand.""" if normal == "x": return left["Ey"].values * np.conj(right["Hz"].values) - left["Ez"].values * np.conj(right["Hy"].values) if normal == "y": @@ -568,6 +588,7 @@ def _normal_lorentz_integrand( right: dict[str, xr.DataArray], normal: str, ) -> np.ndarray: + """Return the symmetrized unconjugated Lorentz integrand.""" left_cross_right = _normal_unconjugated_cross_integrand(left, right, normal) right_cross_left = _normal_unconjugated_cross_integrand(right, left, normal) return 0.5 * (left_cross_right + right_cross_left) @@ -578,6 +599,7 @@ def _normal_unconjugated_cross_integrand( right: dict[str, xr.DataArray], normal: str, ) -> np.ndarray: + """Return one unconjugated cross-product component.""" if normal == "x": return left["Ey"].values * right["Hz"].values - left["Ez"].values * right["Hy"].values if normal == "y": @@ -593,6 +615,7 @@ def _validate_overlap_grid( right_result: Result, right: dict[str, xr.DataArray], ) -> None: + """Ensure two modes live on compatible spatial grids.""" if left_result._normal_dim() != right_result._normal_dim(): raise ValueError("mode overlap requires matching mode-plane normal dimensions") reference_left = left["Ex"] diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 5090722..317a26a 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -24,6 +24,8 @@ @dataclass class _ModeFields: + """Mutable six-component field container used during normalization.""" + ex: np.ndarray ey: np.ndarray ez: np.ndarray @@ -32,9 +34,11 @@ class _ModeFields: hz: np.ndarray def components(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Return field components in electric-then-magnetic order.""" return self.ex, self.ey, self.ez, self.hx, self.hy, self.hz def add_scaled(self, other: _ModeFields, scale: complex) -> None: + """Add another mode into this mode with a complex scale factor.""" for left, right in zip(self.components(), other.components(), strict=True): left += scale * right @@ -293,6 +297,7 @@ def solve_tensorial_scipy_reference( def _import_scipy(): + """Import SciPy modules lazily so package import stays lightweight.""" try: import scipy.linalg as scipy_linalg import scipy.sparse as sparse @@ -317,6 +322,7 @@ def _create_derivative_matrices( dmin_pmc: tuple[bool, bool], scale: float, ): + """Assemble scaled Yee derivative matrices with optional PML stretching.""" matrices = ( _make_dxf(sparse, np.asarray(dlf[0], dtype=float), shape, dmin_pmc[0]), _make_dxb(sparse, np.asarray(dlb[0], dtype=float), shape, dmin_pmc[0]), @@ -344,6 +350,7 @@ def _create_derivative_matrices( def _make_dxf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + """Build the forward x derivative on the local Yee grid.""" nx, ny = shape if nx == 1: return sparse.csc_matrix((ny, ny), dtype=np.complex128) @@ -367,6 +374,7 @@ def _make_dxf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): def _make_dxb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + """Build the backward x derivative on the local Yee grid.""" nx, ny = shape if nx == 1: return sparse.csc_matrix((ny, ny), dtype=np.complex128) @@ -390,6 +398,7 @@ def _make_dxb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): def _make_dyf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + """Build the forward y derivative on the local Yee grid.""" nx, ny = shape if ny == 1: return sparse.csc_matrix((nx, nx), dtype=np.complex128) @@ -413,6 +422,7 @@ def _make_dyf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): def _make_dyb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): + """Build the backward y derivative on the local Yee grid.""" nx, ny = shape if ny == 1: return sparse.csc_matrix((nx, nx), dtype=np.complex128) @@ -436,12 +446,15 @@ def _make_dyb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_mats) -> dict[str, object]: + """Assemble the reduced diagonal-material eigen-operator blocks.""" n = eps.shape[-1] zero = sparse.csc_matrix((n, n), dtype=np.complex128) dxf, dxb, dyf, dyb = der_mats inv_eps_zz = sparse.diags(1.0 / eps[2, 2, :], format="csc") inv_mu_zz = sparse.diags(1.0 / mu[2, 2, :], format="csc") + # The block names mirror docs/physics-model.md: + # A_diag = P_mu Q + P_partial Q_epsilon, with Q = Q_epsilon + Q_partial. p_mu = sparse.bmat( [[zero, sparse.diags(mu[1, 1, :], format="csc")], [-sparse.diags(mu[0, 0, :], format="csc"), zero]], format="csc", @@ -470,11 +483,13 @@ def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_ma def _assemble_tensorial_operator(sparse, eps: np.ndarray, mu: np.ndarray, der_mats): + """Assemble the full tensorial first-order eigen-operator.""" dxf, dxb, dyf, dyb = der_mats inv_eps_22 = sparse.diags(1.0 / eps[2, 2, :], format="csc") inv_mu_22 = sparse.diags(1.0 / mu[2, 2, :], format="csc") def diag(values): + """Return a sparse diagonal matrix for one flattened tensor component.""" return sparse.diags(values, format="csc") eps_20_over_22 = eps[2, 0, :] / eps[2, 2, :] @@ -486,6 +501,9 @@ def diag(values): mu_02_over_22 = mu[0, 2, :] / mu[2, 2, :] mu_12_over_22 = mu[1, 2, :] / mu[2, 2, :] + # Schur-complement terms eliminate Ez and Hz before solving the first-order + # transverse system [Ex, Ey, Hx, Hy]. Suffix "_s" means the component has + # been corrected by its coupling through the local z component. mu_10_s = mu[1, 0, :] - mu[1, 2, :] * mu_20_over_22 mu_11_s = mu[1, 1, :] - mu[1, 2, :] * mu_21_over_22 mu_00_s = mu[0, 0, :] - mu[0, 2, :] * mu_20_over_22 @@ -495,6 +513,9 @@ def diag(values): eps_00_s = eps[0, 0, :] - eps[0, 2, :] * eps_20_over_22 eps_01_s = eps[0, 1, :] - eps[0, 2, :] * eps_21_over_22 + # Block names encode output row and input column: + # a = electric transverse rows, b = magnetic transverse rows, x/y = local + # transverse components. For example, axby maps Hy into the Ex equation. axax = -(dxf @ diag(eps_20_over_22)) - diag(mu_12_over_22) @ dyf axay = -(dxf @ diag(eps_21_over_22)) + diag(mu_12_over_22) @ dxf axbx = -(dxf @ inv_eps_22 @ dyb) + diag(mu_10_s) @@ -530,12 +551,14 @@ def diag(values): def _canonical_sparse(matrix): + """Return a cleaned CSC sparse matrix.""" matrix = matrix.tocsc(copy=True) matrix.eliminate_zeros() return matrix def _real_arpack_problem_if_close(matrix, initial_vector: np.ndarray | None, guess: complex): + """Use a real ARPACK problem when the operator and shift are effectively real.""" if matrix.nnz == 0: return matrix, initial_vector, guess matrix_imag = matrix.data.imag @@ -557,6 +580,7 @@ def _selected_eigenpairs( spla, scipy_linalg, ) -> tuple[np.ndarray, np.ndarray]: + """Select eigenpairs nearest the requested shift.""" size = mat.shape[0] if num_modes >= size - 1: values, vectors = scipy_linalg.eig(mat.toarray()) @@ -592,6 +616,7 @@ def _create_s_diagonal_values( omega: float, profile: dict[str, float | int] | None, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Build inverse PML stretch vectors for every derivative stencil.""" nx, ny = shape avg_speed = _average_relative_speed(shape, num_pml, eps_tensor, mu_tensor) sx_f = _create_sfactor( @@ -627,12 +652,14 @@ def _average_relative_speed( eps_tensor: np.ndarray, mu_tensor: np.ndarray, ) -> np.ndarray: + """Estimate relative wave speed near PML boundaries.""" eps_avg = _pml_average_all_sides(shape, num_pml, eps_tensor) mu_avg = _pml_average_all_sides(shape, num_pml, mu_tensor) return 1.0 / np.sqrt(eps_avg * mu_avg) def _pml_average_all_sides(shape: tuple[int, int], num_pml: tuple[int, int], tensor: np.ndarray) -> np.ndarray: + """Average diagonal material components on each boundary side.""" nx, ny = shape regions: list[list[complex]] = [[], [], [], []] for comp in range(3): @@ -664,6 +691,7 @@ def _create_sfactor( avg_speed: np.ndarray, profile: dict[str, float | int] | None, ) -> np.ndarray: + """Dispatch PML stretch-factor construction for a derivative direction.""" if n_pml == 0: return np.ones(n, dtype=np.complex128) if direction == "f": @@ -682,6 +710,7 @@ def _create_sfactor_f( avg_speed: np.ndarray, profile: dict[str, float | int] | None, ) -> np.ndarray: + """Build forward-grid PML stretch factors.""" sfactor = np.ones(n, dtype=np.complex128) for i in range(n): if i < n_pml and dmin_pml: @@ -700,6 +729,7 @@ def _create_sfactor_b( avg_speed: np.ndarray, profile: dict[str, float | int] | None, ) -> np.ndarray: + """Build backward-grid PML stretch factors.""" sfactor = np.ones(n, dtype=np.complex128) for i in range(n): if i < n_pml and dmin_pml: @@ -716,6 +746,7 @@ def _s_value( avg_speed: complex, profile: dict[str, float | int] | None, ) -> complex: + """Evaluate one polynomial PML stretch factor.""" values = { "sigma_max": 2.0, "kappa_min": 1.0, @@ -731,6 +762,7 @@ def _s_value( def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: np.ndarray) -> dict[str, object]: + """Normalize modes and enforce Lorentz orthogonality.""" for mode in modes: _normalize_to_unit_power(mode, cell_areas) @@ -763,6 +795,7 @@ def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: n def _normalize_to_unit_power(mode: _ModeFields, cell_areas: np.ndarray) -> float: + """Scale a mode to unit transverse power.""" norm = abs(_transverse_power(mode, cell_areas)) if norm <= np.finfo(float).eps: return 0.0 @@ -773,16 +806,19 @@ def _normalize_to_unit_power(mode: _ModeFields, cell_areas: np.ndarray) -> float def _transverse_power(mode: _ModeFields, cell_areas: np.ndarray) -> complex: + """Compute conjugated transverse power flux.""" return complex(np.sum((mode.ex * np.conj(mode.hy) - mode.ey * np.conj(mode.hx)) * cell_areas)) def _lorentz_overlap(left: _ModeFields, right: _ModeFields, cell_areas: np.ndarray) -> complex: + """Compute the unconjugated reciprocal-product overlap.""" left_cross_right = np.sum((left.ex * right.hy - left.ey * right.hx) * cell_areas) right_cross_left = np.sum((right.ex * left.hy - right.ey * left.hx) * cell_areas) return complex(0.5 * (left_cross_right + right_cross_left)) def _apply_dominant_e_phase_convention(mode: _ModeFields) -> None: + """Rotate a mode so its largest electric component is real-positive.""" electric = np.concatenate((mode.ex, mode.ey, mode.ez)) if electric.size == 0: return diff --git a/python/micromode/sweep.py b/python/micromode/sweep.py index aa6ac83..b862582 100644 --- a/python/micromode/sweep.py +++ b/python/micromode/sweep.py @@ -20,6 +20,7 @@ class Sweep: parameter_name: str = "parameter" def __post_init__(self) -> None: + """Validate sweep lengths and mode-count consistency.""" values = np.asarray(self.values, dtype=float) if values.ndim != 1: raise ValueError("values must be one-dimensional") @@ -35,18 +36,22 @@ def __post_init__(self) -> None: @property def num_modes(self) -> int: + """Return the shared number of modes in the sweep.""" return int(self.results[0].n_complex.sizes["mode_index"]) @property def n_eff(self) -> np.ndarray: + """Return real effective indices arranged by sweep step and mode.""" return np.vstack([np.asarray(result.n_eff.values)[0] for result in self.results]) @property def n_complex(self) -> np.ndarray: + """Return complex effective indices arranged by sweep step and mode.""" return np.vstack([np.asarray(result.n_complex.values)[0] for result in self.results]) @property def pol_fraction(self) -> dict[str, np.ndarray]: + """Return TE/TM fractions arranged by sweep step and mode.""" return { "te": np.vstack([np.asarray(result.pol_fraction["te"].values)[0] for result in self.results]), "tm": np.vstack([np.asarray(result.pol_fraction["tm"].values)[0] for result in self.results]), @@ -54,6 +59,7 @@ def pol_fraction(self) -> dict[str, np.ndarray]: @property def pol_fraction_waveguide(self) -> dict[str, np.ndarray]: + """Return waveguide TE/TM fractions arranged by sweep step and mode.""" return { "te": np.vstack([np.asarray(result.pol_fraction_waveguide["te"].values)[0] for result in self.results]), "tm": np.vstack([np.asarray(result.pol_fraction_waveguide["tm"].values)[0] for result in self.results]), @@ -114,10 +120,12 @@ def track_modes_by_overlap( def _assignment_score(overlaps: np.ndarray, order: tuple[int, ...]) -> float: + """Score a proposed mode assignment by total overlap magnitude.""" return float(sum(overlaps[mode_index, source_index] for mode_index, source_index in enumerate(order))) def _reorder_result_modes(result: Result, order: tuple[int, ...]) -> Result: + """Return a result with all mode-indexed arrays reordered together.""" mode_coord = np.arange(len(order)) n_complex = result.n_complex.isel(mode_index=list(order)).assign_coords(mode_index=mode_coord) field_components = { diff --git a/scripts/check_dist_artifacts.py b/scripts/check_dist_artifacts.py index 9501c7d..9955e0d 100644 --- a/scripts/check_dist_artifacts.py +++ b/scripts/check_dist_artifacts.py @@ -12,6 +12,7 @@ def main() -> None: + """Validate wheel and source-distribution artifact coverage.""" parser = argparse.ArgumentParser() parser.add_argument("dist", nargs="?", default="dist", help="Directory containing release artifacts") parser.add_argument( @@ -72,6 +73,7 @@ def main() -> None: def require(condition: object, message: str) -> None: + """Raise SystemExit with a message when a condition is false.""" if not condition: raise SystemExit(message) diff --git a/scripts/check_release_metadata.py b/scripts/check_release_metadata.py index 469d21c..2a46a02 100644 --- a/scripts/check_release_metadata.py +++ b/scripts/check_release_metadata.py @@ -16,6 +16,7 @@ def main() -> None: + """Validate release metadata and workflow expectations.""" pyproject = load_toml("pyproject.toml") project = pyproject["project"] publish_workflow = (ROOT / ".github/workflows/publish.yml").read_text(encoding="utf-8") @@ -48,11 +49,13 @@ def main() -> None: def load_toml(path: str) -> dict: + """Load a TOML file relative to the repository root.""" with (ROOT / path).open("rb") as handle: return tomllib.load(handle) def require_tag_matches_version(version: str) -> None: + """Ensure a tag-triggered workflow uses the package version.""" if os.environ.get("GITHUB_REF_TYPE") != "tag": return @@ -61,6 +64,7 @@ def require_tag_matches_version(version: str) -> None: def require(condition: bool, message: str) -> None: + """Raise SystemExit with a message when a condition is false.""" if not condition: raise SystemExit(message) diff --git a/scripts/generate_coverage_badge.py b/scripts/generate_coverage_badge.py index 400d0cf..6938d4e 100644 --- a/scripts/generate_coverage_badge.py +++ b/scripts/generate_coverage_badge.py @@ -10,6 +10,7 @@ def main() -> None: + """Generate an SVG coverage badge from coverage XML.""" parser = argparse.ArgumentParser() parser.add_argument("coverage_xml", type=Path, nargs="?", default=Path("coverage.xml")) parser.add_argument("output_svg", type=Path, nargs="?", default=Path("docs/assets/coverage.svg")) @@ -23,6 +24,7 @@ def main() -> None: def coverage_percent(path: Path) -> float: + """Extract line coverage percentage from coverage.py XML.""" root = ET.parse(path).getroot() line_rate = root.attrib.get("line-rate") if line_rate is not None: @@ -36,6 +38,7 @@ def coverage_percent(path: Path) -> float: def color_for_percent(percent: float) -> str: + """Choose a badge color for a coverage percentage.""" if percent >= 90.0: return "#4c1" if percent >= 80.0: @@ -48,6 +51,7 @@ def color_for_percent(percent: float) -> str: def render_badge(label: str, message: str, color: str) -> str: + """Render a compact shields-style SVG badge.""" label = html.escape(label) message = html.escape(message) label_width = max(50, len(label) * 7 + 10) diff --git a/scripts/smoke_dist.py b/scripts/smoke_dist.py index f44f3b0..24d9bd8 100644 --- a/scripts/smoke_dist.py +++ b/scripts/smoke_dist.py @@ -13,6 +13,7 @@ def main() -> None: + """Install a built wheel into a temporary environment and smoke-test it.""" parser = argparse.ArgumentParser() parser.add_argument( "wheel", @@ -37,6 +38,7 @@ def main() -> None: def latest_wheel(required_tag: str) -> Path: + """Find the newest compatible wheel in dist.""" wheels = sorted((ROOT / "dist").glob(f"micromode-*-{required_tag}-*.whl"), key=_mtime, reverse=True) if not wheels: wheels = sorted((ROOT / "dist").glob("micromode-*-py3-none-any.whl"), key=_mtime, reverse=True) @@ -46,10 +48,12 @@ def latest_wheel(required_tag: str) -> Path: def _mtime(path: Path) -> float: + """Return a path modification timestamp for sorting.""" return path.stat().st_mtime def python_tag(python: str) -> str: + """Return the CPython wheel tag for an interpreter.""" command = [ python, "-c", @@ -60,6 +64,7 @@ def python_tag(python: str) -> str: def run(command: list[str]) -> None: + """Run a subprocess command and fail on nonzero exit.""" subprocess.run(command, check=True) diff --git a/scripts/smoke_wheel.py b/scripts/smoke_wheel.py index 24beac1..90a4d84 100644 --- a/scripts/smoke_wheel.py +++ b/scripts/smoke_wheel.py @@ -11,6 +11,7 @@ def main() -> None: + """Solve a tiny problem and round-trip HDF5 in an installed wheel.""" x_edges = np.linspace(-0.8, 0.8, 7) y_edges = np.linspace(-0.6, 0.6, 6) x = 0.5 * (x_edges[:-1] + x_edges[1:]) diff --git a/tests/conftest.py b/tests/conftest.py index da5833f..f464adc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +"""Pytest configuration for importing the local source tree.""" + from __future__ import annotations import sys diff --git a/tests/test_micromode_api.py b/tests/test_micromode_api.py index 51346ea..1ad5caf 100644 --- a/tests/test_micromode_api.py +++ b/tests/test_micromode_api.py @@ -1,3 +1,5 @@ +"""Integration-style tests for the public MicroMode API.""" + from __future__ import annotations from collections.abc import Sequence @@ -11,20 +13,24 @@ def _linspace_edges(start: float, stop: float, count: int) -> tuple[float, ...]: + """Return linspace edges used by tests.""" return tuple(float(value) for value in np.linspace(start, stop, count)) def _edge_centers(edges: Sequence[float]) -> np.ndarray: + """Return edge centers used by tests.""" edge_array = np.asarray(edges, dtype=float) return (edge_array[:-1] + edge_array[1:]) / 2 def _solver_info(data: mm.Result) -> dict[str, Any]: + """Return solver info used by tests.""" assert data.solver_info is not None return data.solver_info def _strip_grid(nx: int = 5, ny: int = 4) -> tuple[np.ndarray, tuple[float, ...], tuple[float, ...]]: + """Return strip grid used by tests.""" x_edges = _linspace_edges(-1.0, 1.0, nx + 1) y_edges = _linspace_edges(-0.8, 0.8, ny + 1) x_centers = _edge_centers(x_edges) @@ -35,6 +41,7 @@ def _strip_grid(nx: int = 5, ny: int = 4) -> tuple[np.ndarray, tuple[float, ...] def test_grid_api_solves_with_scipy_solver(): + """Verify grid api solves with scipy solver.""" eps, x_edges, y_edges = _strip_grid(6, 5) freq = mm.C_0 / 1.55 @@ -71,6 +78,7 @@ def test_grid_api_solves_with_scipy_solver(): def test_scipy_solver_reports_operator_diagnostics(): + """Verify scipy solver reports operator diagnostics.""" eps, x_edges, y_edges = _strip_grid(5, 4) data = mm.solve_grid( @@ -90,6 +98,7 @@ def test_scipy_solver_reports_operator_diagnostics(): def test_materials_api_matches_component_api_for_diagonal_grid(): + """Verify materials api matches component api for diagonal grid.""" eps, x_edges, y_edges = _strip_grid() freq = mm.C_0 / 1.55 materials = mm.Materials.from_diagonal(eps_xx=eps, x_edges=x_edges, y_edges=y_edges) @@ -116,6 +125,7 @@ def test_materials_api_matches_component_api_for_diagonal_grid(): def test_scipy_solver_handles_diagonal_grid(): + """Verify scipy solver handles diagonal grid.""" eps, x_edges, y_edges = _strip_grid(5, 4) freq = mm.C_0 / 1.55 @@ -139,6 +149,7 @@ def test_scipy_solver_handles_diagonal_grid(): def test_scipy_solver_handles_pml_and_tensorial_paths(): + """Verify scipy solver handles pml and tensorial paths.""" eps, x_edges, y_edges = _strip_grid(4, 3) pml_common = { @@ -180,6 +191,7 @@ def test_scipy_solver_handles_pml_and_tensorial_paths(): def test_scipy_solver_handles_transformed_grid(): + """Verify scipy solver handles transformed grid.""" eps, x_edges, y_edges = _strip_grid(4, 3) data = mm.solve_grid( @@ -198,6 +210,7 @@ def test_scipy_solver_handles_transformed_grid(): def test_materials_api_accepts_full_tensor_grid(): + """Verify materials api accepts full tensor grid.""" x_edges = _linspace_edges(-1.0, 1.0, 5) y_edges = _linspace_edges(-0.8, 0.8, 4) shape = (len(x_edges) - 1, len(y_edges) - 1) @@ -231,6 +244,7 @@ def test_materials_api_accepts_full_tensor_grid(): def test_solver_specs_report_diagnostics_and_persist_to_hdf5(tmp_path): + """Verify solver specs report diagnostics and persist to hdf5.""" eps, x_edges, y_edges = _strip_grid(6, 5) pml = mm.PmlSpec(num_cells=(1, 1), sigma_max=1.6, kappa_min=1.0, kappa_max=2.2, order=2) boundary = mm.BoundarySpec(low=("pec", "pmc")) @@ -267,6 +281,7 @@ def test_solver_specs_report_diagnostics_and_persist_to_hdf5(tmp_path): def test_slice_api_solves_1d_x_slice_and_matches_single_cell_grid(tmp_path): + """Verify slice api solves 1d x slice and matches single cell grid.""" x_edges = _linspace_edges(-1.0, 1.0, 9) x_centers = _edge_centers(x_edges) eps = np.where(np.abs(x_centers) <= 0.3, 3.4**2, 1.44**2) @@ -314,6 +329,7 @@ def test_slice_api_solves_1d_x_slice_and_matches_single_cell_grid(tmp_path): def test_materials_slice_api_supports_y_axis_and_tensor_components(): + """Verify materials slice api supports y axis and tensor components.""" y_edges = _linspace_edges(-0.8, 0.8, 7) shape = (len(y_edges) - 1,) eps_xx = np.full(shape, 2.2**2) @@ -347,6 +363,7 @@ def test_materials_slice_api_supports_y_axis_and_tensor_components(): def test_y_normal_solve_returns_global_component_names(): + """Verify y normal solve returns global component names.""" z_edges = _linspace_edges(-1.0, 1.0, 9) z_centers = _edge_centers(z_edges) eps = np.where(np.abs(z_centers) <= 0.3, 3.4**2, 1.44**2) @@ -372,6 +389,7 @@ def test_y_normal_solve_returns_global_component_names(): def test_x_normal_solve_returns_global_component_names_and_positive_power(): + """Verify x normal solve returns global component names and positive power.""" y_edges = _linspace_edges(-1.0, 1.0, 7) z_edges = _linspace_edges(-0.8, 0.8, 6) y_centers = _edge_centers(y_edges) @@ -401,6 +419,7 @@ def test_x_normal_solve_returns_global_component_names_and_positive_power(): def test_y_normal_power_overlap_is_positive(): + """Verify y normal power overlap is positive.""" z_edges = _linspace_edges(-1.0, 1.0, 9) z_centers = _edge_centers(z_edges) eps = np.where(np.abs(z_centers) <= 0.3, 3.4**2, 1.44**2) @@ -423,6 +442,7 @@ def test_y_normal_power_overlap_is_positive(): def test_materials_subpixel_averaging_helpers(): + """Verify materials subpixel averaging helpers.""" x_edges = _linspace_edges(-1.0, 1.0, 3) y_edges = _linspace_edges(-0.5, 0.5, 3) samples = np.asarray( @@ -456,6 +476,7 @@ def test_materials_subpixel_averaging_helpers(): def test_angle_and_bend_use_tensorial_path(): + """Verify angle and bend use tensorial path.""" eps, x_edges, y_edges = _strip_grid() data = mm.solve_grid( @@ -478,6 +499,7 @@ def test_angle_and_bend_use_tensorial_path(): def test_full_tensor_grid_supports_angle_and_bend_transform(): + """Verify full tensor grid supports angle and bend transform.""" x_edges = _linspace_edges(-1.0, 1.0, 5) y_edges = _linspace_edges(-0.8, 0.8, 4) shape = (len(x_edges) - 1, len(y_edges) - 1) @@ -509,6 +531,7 @@ def test_full_tensor_grid_supports_angle_and_bend_transform(): def test_spec_can_drive_grid_solve_options(): + """Verify spec can drive grid solve options.""" eps, x_edges, y_edges = _strip_grid() spec = mm.Spec(num_modes=1, target_neff=2.5) @@ -526,6 +549,7 @@ def test_spec_can_drive_grid_solve_options(): def test_result_metrics_io_plotting_and_overlap(tmp_path): + """Verify result metrics io plotting and overlap.""" eps, x_edges, y_edges = _strip_grid(6, 5) data = mm.solve_grid( eps_xx=eps, @@ -568,6 +592,7 @@ def test_result_metrics_io_plotting_and_overlap(tmp_path): def test_result_from_hdf5_accepts_bytes_solver_info(tmp_path): + """Verify result from hdf5 accepts bytes solver info.""" eps, x_edges, y_edges = _strip_grid(6, 5) data = mm.solve_grid( eps_xx=eps, @@ -589,6 +614,7 @@ def test_result_from_hdf5_accepts_bytes_solver_info(tmp_path): def test_result_overlap_for_synthetic_orthogonal_modes(): + """Verify result overlap for synthetic orthogonal modes.""" dims = ("x", "y", "z", "f", "mode_index") coords = { "x": np.asarray([-0.5, 0.5]), @@ -622,6 +648,7 @@ def test_result_overlap_for_synthetic_orthogonal_modes(): def test_sweep_tracks_modes_by_overlap(): + """Verify sweep tracks modes by overlap.""" dims = ("x", "y", "z", "f", "mode_index") coords = { "x": np.asarray([-0.5, 0.5]), @@ -633,6 +660,7 @@ def test_sweep_tracks_modes_by_overlap(): shape = (2, 2, 1, 1, 2) def result(n_values: list[float], order: tuple[str, str]) -> mm.Result: + """Return result used by tests.""" fields = {component: np.zeros(shape, dtype=np.complex128) for component in ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz")} for mode_index, component in enumerate(order): fields[component][..., mode_index] = 1.0 @@ -661,6 +689,7 @@ def result(n_values: list[float], order: tuple[str, str]) -> mm.Result: def test_spec_validation_for_core_options(): + """Verify spec validation for core options.""" spec = mm.Spec( num_modes=2, target_neff=2.0, diff --git a/tests/test_mode_solver_fixtures.py b/tests/test_mode_solver_fixtures.py index 5baf67e..fc284f9 100644 --- a/tests/test_mode_solver_fixtures.py +++ b/tests/test_mode_solver_fixtures.py @@ -1,3 +1,5 @@ +"""Tests for committed mode-solver fixture integrity and local comparisons.""" + from __future__ import annotations from pathlib import Path @@ -21,14 +23,17 @@ def test_smoke_fixture_manifest_and_hashes_are_current(): + """Verify smoke fixture manifest and hashes are current.""" _assert_fixture_manifest(SMOKE_FIXTURE_ROOT) def test_extended_fixture_manifest_and_hashes_are_current(): + """Verify extended fixture manifest and hashes are current.""" _assert_fixture_manifest(EXTENDED_FIXTURE_ROOT) def test_reference_fixture_files_do_not_embed_external_package_name(): + """Verify reference fixture files do not embed external package name.""" needle = bytes.fromhex("746964793364") for root in (SMOKE_FIXTURE_ROOT, EXTENDED_FIXTURE_ROOT): for path in root.rglob("*"): @@ -37,6 +42,7 @@ def test_reference_fixture_files_do_not_embed_external_package_name(): def test_reference_hdf5_files_do_not_store_serialized_solver_metadata(): + """Verify reference hdf5 files do not store serialized solver metadata.""" import h5py for root in (SMOKE_FIXTURE_ROOT, EXTENDED_FIXTURE_ROOT): @@ -46,6 +52,7 @@ def test_reference_hdf5_files_do_not_store_serialized_solver_metadata(): def test_reference_n_complex_matches_summary_payload(): + """Verify reference n complex matches summary payload.""" for root in (SMOKE_FIXTURE_ROOT, EXTENDED_FIXTURE_ROOT): for entry in read_json(manifest_path(root))["cases"]: case_id = entry["case_id"] @@ -60,6 +67,7 @@ def test_reference_n_complex_matches_summary_payload(): def test_reference_field_signatures_match_summary_payload(): + """Verify reference field signatures match summary payload.""" for root in (SMOKE_FIXTURE_ROOT, EXTENDED_FIXTURE_ROOT): for entry in read_json(manifest_path(root))["cases"]: case_id = entry["case_id"] @@ -73,6 +81,7 @@ def test_reference_field_signatures_match_summary_payload(): def test_phase_aligned_relative_error_accepts_global_complex_phase(): + """Verify phase aligned relative error accepts global complex phase.""" golden = np.array([1 + 2j, -3 + 1j, 0.5 - 0.25j]) actual = golden * np.exp(1j * 0.73) @@ -84,6 +93,7 @@ def test_phase_aligned_relative_error_accepts_global_complex_phase(): @pytest.mark.slow def test_local_fixture_comparison_uses_staggered_rasterization_for_z_strips(): + """Verify local fixture comparison uses staggered rasterization for z strips.""" manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) entries = {entry["case_id"]: entry for entry in manifest["cases"]} @@ -95,6 +105,7 @@ def test_local_fixture_comparison_uses_staggered_rasterization_for_z_strips(): @pytest.mark.slow def test_local_production_fixture_matrix_passes(): + """Verify local production fixture matrix passes.""" from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) @@ -115,6 +126,7 @@ def test_local_production_fixture_matrix_passes(): @pytest.mark.slow def test_unsupported_fixture_matrix_is_explicit(): + """Verify unsupported fixture matrix is explicit.""" from benchmarks.compare_mode_solver_fixtures import _LOCAL_CASES manifest = read_json(manifest_path(EXTENDED_FIXTURE_ROOT)) @@ -137,6 +149,7 @@ def test_unsupported_fixture_matrix_is_explicit(): def _assert_fixture_manifest(root: Path) -> None: + """Return assert fixture manifest used by tests.""" manifest = read_json(manifest_path(root)) expected_ids = [case["case_id"] for case in manifest["registered_cases"]] actual_ids = [case["case_id"] for case in manifest["cases"]] @@ -156,6 +169,7 @@ def _assert_fixture_manifest(root: Path) -> None: def _array_from_summary_values(payload: dict) -> np.ndarray: + """Return array from summary values used by tests.""" if "real" in payload: return np.asarray(payload["real"]) + 1j * np.asarray(payload["imag"]) return np.asarray(payload["values"]) From 2dbbef245c794df338ddace3a647fcb41f8a5405 Mon Sep 17 00:00:00 2001 From: Quentin Wach Date: Sat, 23 May 2026 22:25:41 +0200 Subject: [PATCH 16/16] Enhance documentation and validation across core modules - Re-export public API in `__init__.py` for user convenience. - Add detailed comments in `constants.py`, `models.py`, `raster.py`, `result.py`, `scipy_reference.py`, and `sweep.py` to clarify functionality and improve maintainability. - Ensure explicit handling of public interfaces and validation checks in data classes and solver functions. --- python/micromode/__init__.py | 4 + python/micromode/constants.py | 4 + python/micromode/models.py | 92 ++++++++++++++++++ python/micromode/raster.py | 85 ++++++++++++++++ python/micromode/result.py | 102 +++++++++++++++++++ python/micromode/scipy_reference.py | 145 ++++++++++++++++++++++++++++ python/micromode/sweep.py | 27 ++++++ 7 files changed, 459 insertions(+) diff --git a/python/micromode/__init__.py b/python/micromode/__init__.py index c4868ae..4a1590a 100644 --- a/python/micromode/__init__.py +++ b/python/micromode/__init__.py @@ -2,12 +2,16 @@ from __future__ import annotations +# Re-export the small public API from package root so users do not need to know +# the internal module split. from .constants import C_0, EPSILON_0 from .models import BoundarySpec, Grid, Materials, PmlSpec, Spec from .raster import solve_grid, solve_modes, solve_slice from .result import Result, overlap from .sweep import Sweep, track_modes_by_overlap +# Keep __all__ explicit so documentation and static analysis show the intended +# public surface. __all__ = [ "C_0", "EPSILON_0", diff --git a/python/micromode/constants.py b/python/micromode/constants.py index b6af4f9..1ff7e54 100644 --- a/python/micromode/constants.py +++ b/python/micromode/constants.py @@ -1,4 +1,8 @@ """Physical constants used by MicroMode.""" +# Speed of light in microns per second, matching the coordinate unit used by the +# raster API. C_0 = 2.997_924_58e14 + +# Vacuum permittivity in solver units. EPSILON_0 = 8.854_187_812_800_384e-18 diff --git a/python/micromode/models.py b/python/micromode/models.py index 9706b09..818d871 100644 --- a/python/micromode/models.py +++ b/python/micromode/models.py @@ -14,6 +14,8 @@ BoundaryCondition = Literal["pec", "pmc"] +# These dataclasses are intentionally lightweight: they validate user-facing +# arrays and options before the numerical code receives them. @dataclass(frozen=True) class PmlSpec: """Perfectly matched layer settings for mode solves.""" @@ -26,6 +28,7 @@ class PmlSpec: def __post_init__(self) -> None: """Normalize constructor inputs and enforce model invariants.""" + # First normalize the two edge counts because they control PML indexing. if len(self.num_cells) != 2: raise ValueError("num_cells must contain two non-negative integers") num_cells = ( @@ -33,11 +36,16 @@ def __post_init__(self) -> None: _coerce_integral("num_cells", self.num_cells[1], minimum=0), ) object.__setattr__(self, "num_cells", num_cells) + + # Stretch parameters must be positive scalars; zero would remove the + # damping profile or create invalid divisions in the PML factors. for name in ("sigma_max", "kappa_min", "kappa_max"): value = float(getattr(self, name)) if not np.isfinite(value) or value <= 0.0: raise ValueError(f"{name} must be finite and positive") object.__setattr__(self, name, value) + + # The outer PML stretch should never be weaker than the inner stretch. if self.kappa_max < self.kappa_min: raise ValueError("kappa_max must be greater than or equal to kappa_min") object.__setattr__(self, "order", _coerce_integral("order", self.order, minimum=1)) @@ -75,9 +83,13 @@ class BoundarySpec: def __post_init__(self) -> None: """Normalize constructor inputs and enforce model invariants.""" + # Keep only the low-edge symmetry flags; high edges are handled by the + # open-domain derivative/PML construction. if len(self.low) != 2: raise ValueError("low must contain two boundary conditions") normalized = tuple(str(value).lower() for value in self.low) + + # Reject misspellings early so they do not silently become PEC walls. unknown = set(normalized).difference({"pec", "pmc"}) if unknown: raise ValueError("boundary conditions must be 'pec' or 'pmc'") @@ -114,12 +126,20 @@ class Grid: def __post_init__(self) -> None: """Normalize constructor inputs and enforce model invariants.""" + # Store immutable float tuples so downstream validation and metadata + # serialization see one consistent representation. x_edges = tuple(float(value) for value in self.x_edges) y_edges = tuple(float(value) for value in self.y_edges) object.__setattr__(self, "x_edges", x_edges) object.__setattr__(self, "y_edges", y_edges) + + # The normal axis determines how local solver fields map back to global + # x/y/z components, so it must be one of the Cartesian axes. if self.normal_axis not in {0, 1, 2}: raise ValueError("normal_axis must be 0, 1, or 2") + + # Edge arrays define cell widths; non-monotonic values would produce + # negative or zero finite-difference spacings. for name, values in {"x_edges": x_edges, "y_edges": y_edges}.items(): if len(values) < 2: raise ValueError(f"{name} must contain at least two values") @@ -148,9 +168,13 @@ class Materials: def __post_init__(self) -> None: """Normalize constructor inputs and enforce model invariants.""" + # Epsilon is required and must already be sampled on the solver cells. eps_tensor = np.asarray(self.eps_tensor, dtype=np.complex128) if eps_tensor.shape != (3, 3, *self.grid.shape): raise ValueError("eps_tensor must have shape (3, 3, nx, ny) matching the grid") + + # Permeability defaults to identity so non-magnetic material grids stay + # concise at call sites while the solver always receives full tensors. if self.mu_tensor is None: mu_tensor = np.zeros_like(eps_tensor) for axis in range(3): @@ -159,6 +183,9 @@ def __post_init__(self) -> None: mu_tensor = np.asarray(self.mu_tensor, dtype=np.complex128) if mu_tensor.shape != eps_tensor.shape: raise ValueError("mu_tensor must have the same shape as eps_tensor") + + # NaNs and infinities poison sparse assembly, so reject them at the data + # model boundary instead of deep inside ARPACK. if not np.all(np.isfinite(eps_tensor)) or not np.all(np.isfinite(mu_tensor)): raise ValueError("material tensors must contain finite values") object.__setattr__(self, "eps_tensor", eps_tensor) @@ -180,12 +207,16 @@ def from_diagonal( normal_coordinate: float = 0.0, ) -> Materials: """Build a material grid from scalar or diagonal tensor components.""" + # Build and validate grid metadata before interpreting any component + # arrays, because the grid shape drives every component check. grid = Grid( tuple(float(value) for value in x_edges), tuple(float(value) for value in y_edges), normal_axis=normal_axis, normal_coordinate=normal_coordinate, ) + + # Missing yy/zz components inherit xx, matching isotropic scalar input. eps_diag = _stack_diagonal_components("eps", grid.shape, eps_xx, eps_yy, eps_zz) mu_diag = _stack_diagonal_components( "mu", @@ -194,6 +225,9 @@ def from_diagonal( np.ones(grid.shape, dtype=np.complex128) if mu_yy is None else mu_yy, np.ones(grid.shape, dtype=np.complex128) if mu_zz is None else mu_zz, ) + + # Store the result in the same full-tensor layout used by tensorial + # inputs; diagonal detection later chooses the faster solver path. return cls( grid=grid, eps_tensor=_diagonal_to_full_tensor(eps_diag), mu_tensor=_diagonal_to_full_tensor(mu_diag) ) @@ -226,12 +260,17 @@ def from_components( normal_coordinate: float = 0.0, ) -> Materials: """Build a material grid from diagonal and off-diagonal tensor components.""" + # Full component construction shares the same grid validation as the + # diagonal constructor, then overlays optional off-diagonal entries. grid = Grid( tuple(float(value) for value in x_edges), tuple(float(value) for value in y_edges), normal_axis=normal_axis, normal_coordinate=normal_coordinate, ) + + # Start with diagonal tensors so scalar and anisotropic grids follow one + # predictable path before optional coupling components are assigned. eps_diag = _stack_diagonal_components("eps", grid.shape, eps_xx, eps_yy, eps_zz) eps_tensor = _diagonal_to_full_tensor(eps_diag) _assign_tensor_offdiagonal( @@ -245,6 +284,9 @@ def from_components( zx=eps_zx, zy=eps_zy, ) + + # Magnetic coupling components are optional; absent mu entries represent + # identity permeability on the diagonal and zero off-diagonal coupling. mu_diag = _stack_diagonal_components( "mu", grid.shape, @@ -264,6 +306,9 @@ def from_components( zx=mu_zx, zy=mu_zy, ) + + # The dataclass constructor performs the final finite-value and shape + # validation on the assembled full tensors. return cls(grid=grid, eps_tensor=eps_tensor, mu_tensor=mu_tensor) @classmethod @@ -306,6 +351,9 @@ def from_slice( axis_index = _normalize_slice_axis(axis) edge_values = tuple(float(value) for value in coord_edges) + + # A slice is still represented as a 2D mode plane, so one axis gets the + # user coordinates and the other gets a single finite-width cell. if len(edge_values) < 2: raise ValueError("coord_edges must contain at least two values") if invariant_width <= 0.0 or not np.isfinite(invariant_width): @@ -320,16 +368,24 @@ def from_slice( def expand(label: str, values: np.ndarray | None) -> np.ndarray | None: """Expand a one-dimensional component onto the padded mode-plane grid.""" + # Optional components stay absent so from_components can apply its + # diagonal/permeability defaults in one place. if values is None: return None array = np.asarray(values, dtype=np.complex128) if array.shape != (cell_count,): raise ValueError(f"{label} must have shape {(cell_count,)} for a one-dimensional slice") + + # Insert the singleton invariant axis on the side implied by the + # requested varying axis. return array[:, None] if axis_index == 0 else array[None, :] expanded_eps_xx = expand("eps_xx", eps_xx) if expanded_eps_xx is None: raise ValueError("eps_xx is required") + + # Delegate to the full component constructor so slice and grid inputs + # share tensor assembly and validation behavior. return cls.from_components( x_edges=x_edges, y_edges=y_edges, @@ -383,6 +439,9 @@ def from_subpixel_diagonal( x_edge_values = tuple(float(value) for value in x_edges) y_edge_values = tuple(float(value) for value in y_edges) shape = (len(x_edge_values) - 1, len(y_edge_values) - 1) + + # Average each supplied high-resolution component independently before + # reusing the ordinary diagonal tensor constructor. averaged = { "eps_xx": cls.average_subpixels(eps_xx, shape=shape, subpixel_shape=subpixel_shape, method=average), "eps_yy": None @@ -427,11 +486,17 @@ def average_subpixels( nx, ny = (int(shape[0]), int(shape[1])) sx, sy = (int(subpixel_shape[0]), int(subpixel_shape[1])) + + # Validate the target grouping before reshaping so shape errors are + # reported in terms of solver cells and subpixel samples. if nx <= 0 or ny <= 0: raise ValueError("shape must contain positive cell counts") if sx <= 0 or sy <= 0: raise ValueError("subpixel_shape must contain positive sample counts") array = np.asarray(values, dtype=np.complex128) + + # Accept either a dense raster or an already grouped raster; both become + # (nx, ny, sx, sy) before the selected averaging rule is applied. if array.shape == (nx * sx, ny * sy): grouped = array.reshape(nx, sx, ny, sy).transpose(0, 2, 1, 3) elif array.shape == (nx, ny, sx, sy): @@ -439,6 +504,8 @@ def average_subpixels( else: raise ValueError(f"subpixel values must have shape {(nx * sx, ny * sy)} or {(nx, ny, sx, sy)}") + # Choose the averaging rule explicitly so interface-sensitive callers can + # request harmonic/geometric behavior without changing solver code. if method == "arithmetic": return grouped.mean(axis=(2, 3)) if method == "harmonic": @@ -463,6 +530,8 @@ def shape(self) -> tuple[int, int]: @property def is_diagonal(self) -> bool: """Return whether epsilon and mu contain only diagonal components.""" + # A boolean mask lets the check stay independent of grid shape and + # catches off-diagonal entries across every solver cell. off_diagonal = np.ones((3, 3), dtype=bool) np.fill_diagonal(off_diagonal, False) mu_tensor = self._resolved_mu_tensor() @@ -497,11 +566,15 @@ def _stack_diagonal_components( zz: np.ndarray | None, ) -> np.ndarray: """Validate and stack x/y/z diagonal tensor components.""" + # xx is the required scalar/diagonal component; yy and zz inherit it unless + # anisotropic values are provided. xx_array = np.asarray(xx, dtype=np.complex128) if xx_array.shape != shape: raise ValueError(f"{label}_xx must have shape {shape}") yy_array = xx_array if yy is None else np.asarray(yy, dtype=np.complex128) zz_array = xx_array if zz is None else np.asarray(zz, dtype=np.complex128) + + # All diagonal components must be cell-centered arrays matching the grid. if yy_array.shape != shape or zz_array.shape != shape: raise ValueError(f"{label}_xx, {label}_yy, and {label}_zz must have shape {shape}") return np.stack([xx_array, yy_array, zz_array], axis=0) @@ -509,12 +582,17 @@ def _stack_diagonal_components( def _coerce_integral(name: str, value: object, *, minimum: int) -> int: """Coerce index-like values while rejecting floats and booleans.""" + # bool implements the integer protocol, but accepting it would make option + # typos like num_cells=(True, False) look valid. if isinstance(value, (bool, np.bool_)): raise ValueError(f"{name} must contain integers") try: integer = index(cast(SupportsIndex, value)) except TypeError as exc: raise ValueError(f"{name} must contain integers") from exc + + # Keep the minimum check here so every PML/order caller reports consistent + # validation messages. if integer < minimum: if minimum == 0: raise ValueError(f"{name} must contain non-negative integers") @@ -533,6 +611,8 @@ def _normalize_slice_axis(axis: SliceAxis) -> int: def _diagonal_to_full_tensor(diagonal: np.ndarray) -> np.ndarray: """Expand diagonal components into a full 3x3 tensor grid.""" + # The solver consumes a full tensor layout even for diagonal materials; all + # off-diagonal entries remain zero. tensor = np.zeros((3, 3, *diagonal.shape[1:]), dtype=np.complex128) for axis in range(3): tensor[axis, axis, :, :] = diagonal[axis] @@ -552,6 +632,8 @@ def _assign_tensor_offdiagonal( zy: np.ndarray | None, ) -> None: """Validate and assign optional off-diagonal tensor components.""" + # Iterate through physical tensor suffixes instead of open-coding each + # assignment, which keeps validation messages and row/column mapping aligned. for (row, col), suffix, values in [ ((0, 1), "xy", xy), ((0, 2), "xz", xz), @@ -563,6 +645,9 @@ def _assign_tensor_offdiagonal( if values is None: continue array = np.asarray(values, dtype=np.complex128) + + # Off-diagonal components are cell-centered just like the diagonal + # components; no broadcasting is allowed because that can hide mistakes. if array.shape != shape: raise ValueError(f"{label}_{suffix} must have shape {shape}") tensor[row, col, :, :] = array @@ -583,10 +668,14 @@ class Spec: def __post_init__(self) -> None: """Normalize constructor inputs and enforce model invariants.""" + # Validate simple scalar controls before expanding nested model objects. if self.num_modes <= 0: raise ValueError("num_modes must be positive") if self.target_neff is not None and self.target_neff <= 0: raise ValueError("target_neff must be positive") + + # Accept compact tuple inputs at the API boundary, but store canonical + # model objects so solve_modes can unpack a Spec without extra branches. pml = self.pml pml = PmlSpec() if pml is None else pml if not isinstance(pml, PmlSpec): @@ -596,6 +685,9 @@ def __post_init__(self) -> None: if not isinstance(boundary, BoundarySpec): boundary = BoundarySpec(low=boundary) object.__setattr__(self, "boundary", boundary) + + # Bend transforms need both a radius and an axis because the Jacobian + # depends on which transverse coordinate measures curvature. if self.bend_radius is not None and np.isclose(self.bend_radius, 0.0): raise ValueError("bend_radius magnitude must be larger than 0") if self.bend_radius is not None and self.bend_axis is None: diff --git a/python/micromode/raster.py b/python/micromode/raster.py index b5707f7..446fa49 100644 --- a/python/micromode/raster.py +++ b/python/micromode/raster.py @@ -16,6 +16,8 @@ _COMPONENTS = ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz") +# Public entry points keep geometry handling out of the solver core: callers +# supply already-rasterized material arrays and grid coordinates. def solve_grid( *, eps_xx: np.ndarray, @@ -85,6 +87,9 @@ def solve_grid( normal_axis=normal_axis, normal_coordinate=normal_coordinate, ) + + # Once the component arrays have been normalized into Materials, the full + # solve path is identical to solve_modes. return solve_modes( material_grid=material_grid, freqs=freqs, @@ -153,6 +158,8 @@ def solve_slice( as ``solve_modes``. """ + # A one-dimensional slice is converted into a thin 2D Materials object so it + # can reuse the same validation, solver dispatch, and result wrapping. material_grid = Materials.from_slice( eps_xx=eps_xx, eps_yy=eps_yy, @@ -179,6 +186,8 @@ def solve_slice( normal_axis=normal_axis, normal_coordinate=normal_coordinate, ) + + # Delegate to solve_modes after the slice-specific padding is complete. return solve_modes( material_grid=material_grid, freqs=freqs, @@ -227,10 +236,14 @@ def solve_modes( # coordinate-aware xarray arrays. if not isinstance(material_grid, Materials): raise TypeError("material_grid must be a Materials") + + # Validate edge arrays against the tensor shape and resolve the frequency + # input before any expensive sparse work starts. shape = material_grid.shape x_edges_arr = _validate_edges("x_edges", material_grid.grid.x_edges, shape[0]) y_edges_arr = _validate_edges("y_edges", material_grid.grid.y_edges, shape[1]) solve_freqs = _resolve_freqs(freqs=freqs, wavelength=wavelength) + if spec is not None: # Spec is a convenience bundle. Once unpacked, the rest of the function # follows exactly the same path as explicit keyword arguments. @@ -242,6 +255,8 @@ def solve_modes( angle_phi = spec.angle_phi bend_radius = spec.bend_radius bend_axis = 0 if spec.bend_axis is None else spec.bend_axis + + # Normalize scalar options and compact model objects at the API boundary. if num_modes <= 0: raise ValueError("num_modes must be positive") if direction not in {"+", "-"}: @@ -252,6 +267,9 @@ def solve_modes( raise ValueError("bend_radius magnitude must be larger than 0") if bend_axis not in {0, 1}: raise ValueError("bend_axis must be 0 or 1") + + # Component filtering happens after solving so field reconstruction still has + # every component needed for normalization and coordinate conversion. requested_components = tuple(components or _COMPONENTS) unknown = set(requested_components).difference(_COMPONENTS) if unknown: @@ -263,6 +281,8 @@ def solve_modes( solver_runs = [] fields_by_component: dict[str, list[np.ndarray]] = {component: [] for component in requested_components} for freq in solve_freqs: + # The sparse solver is frequency-local; multi-frequency solves are a + # deterministic loop plus a final stack into xarray dimensions. n_complex, fields, solver_info = _solve_one_frequency( x_edges=x_edges_arr, y_edges=y_edges_arr, @@ -289,6 +309,7 @@ def solve_modes( if component in fields_by_component: fields_by_component[component].append(values) + # Convert accumulated lists into the public Result layout only once. n_values = np.asarray(n_rows, dtype=np.complex128) field_components = _field_data_arrays( fields_by_component, @@ -334,6 +355,9 @@ def _solve_one_frequency( # builders need both because E and H components are staggered. dlf = (np.diff(x_edges), np.diff(y_edges)) dlb = (_dual_steps(dlf[0]), _dual_steps(dlf[1])) + + # Flatten tensors to the layout expected by scipy_reference while preserving + # the original grid metadata for result coordinates. eps_tensor = material_grid.flat_eps_tensor() mu_tensor = material_grid.flat_mu_tensor() if target_neff is None: @@ -341,6 +365,9 @@ def _solve_one_frequency( # explicitly when hunting for modes near another branch. target_neff = float(np.sqrt(np.max(np.abs(eps_tensor)))) target_neff = _shift_target_neff(float(target_neff)) + + # Coordinate transforms can introduce off-diagonal coupling, so diagonal + # dispatch must be decided after any transform is applied. has_transform = abs(angle_theta) > 0.0 or bend_radius is not None is_diagonal = material_grid.is_diagonal if has_transform: @@ -432,9 +459,13 @@ def _solve_one_frequency_scipy( boundary_spec: BoundarySpec, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: """Run the diagonal SciPy solver and reshape its fields.""" + # The diagonal solver has two transverse unknowns per cell: Ex and Ey. nx = len(dlf[0]) ny = len(dlf[1]) actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) + + # Convert frequency into the nondimensional derivative scale and angular + # frequency expected by the sparse reference implementation. n_complex, fields, solver_info = solve_diagonal_scipy_reference( eps_tensor=eps_tensor, mu_tensor=mu_tensor, @@ -452,6 +483,9 @@ def _solve_one_frequency_scipy( krylov_dim=actual_krylov_dim, initial_vector=_default_initial_vector(2 * nx * ny, shape=(nx, ny)), ) + + # Solver fields are flattened by component; reshape them back to grid + # tensors and attach dispatch metadata for diagnostics. return ( n_complex, _fields_to_grid(fields, (nx, ny)), @@ -479,9 +513,14 @@ def _solve_one_frequency_scipy_tensorial( boundary_spec: BoundarySpec, ) -> tuple[np.ndarray, dict[str, np.ndarray], dict[str, object]]: """Run the tensorial SciPy solver and reshape its fields.""" + # The tensorial solver keeps four transverse unknowns per cell: + # Ex, Ey, Hx, and Hy. nx = len(dlf[0]) ny = len(dlf[1]) actual_krylov_dim = 32 if krylov_dim is None else int(krylov_dim) + + # Full tensor coupling uses the first-order operator and solves directly for + # effective index rather than n_eff squared. n_complex, fields, solver_info = solve_tensorial_scipy_reference( eps_tensor=eps_tensor, mu_tensor=mu_tensor, @@ -499,6 +538,9 @@ def _solve_one_frequency_scipy_tensorial( krylov_dim=actual_krylov_dim, initial_vector=_default_initial_vector(4 * nx * ny, shape=(nx, ny)), ) + + # Keep return structure identical to the diagonal path so solve_modes does + # not need backend-specific field handling. return ( n_complex, _fields_to_grid(fields, (nx, ny)), @@ -530,10 +572,16 @@ def _transformed_material_tensors( # passing the resulting grid to the same tensorial operator family. if eps.shape != mu.shape or eps.shape[:2] != (3, 3): raise ValueError("eps and mu tensors must both have shape (3, 3, nx, ny)") + + # Work in flattened cell order because the sparse solvers consume per-cell + # 3x3 material matrices. nx, ny = eps.shape[2:] n = nx * ny eps_tensor = np.zeros((3, 3, n), dtype=np.complex128) mu_tensor = np.zeros((3, 3, n), dtype=np.complex128) + + # Cell centers define material sample locations for affine angle transforms + # and H-field bend samples. x_centers = (x_edges[:-1] + x_edges[1:]) / 2 y_centers = (y_edges[:-1] + y_edges[1:]) / 2 # Slopes of the tilted propagation coordinate in the local x/y directions. @@ -542,6 +590,8 @@ def _transformed_material_tensors( for ix, x_value in enumerate(x_centers): for iy, y_value in enumerate(y_centers): + # Convert the 2D cell index into the flattened tensor column used by + # the sparse assembly layer. flat = ix * ny + iy local_eps = eps[:, :, ix, iy] local_mu = mu[:, :, ix, iy] @@ -574,6 +624,9 @@ def _transformed_material_tensors( bend_jac_h = np.diag([1.0, 1.0, dwdz_h]).astype(np.complex128) jac_e = bend_jac_e @ jac_e jac_h = bend_jac_h @ jac_h + + # Apply the transformation-optics tensor update separately for E and + # H because their Yee-grid sample locations can differ under bend. eps_tensor[:, :, flat] = jac_e @ local_eps @ jac_e.T / np.linalg.det(jac_e) mu_tensor[:, :, flat] = jac_h @ local_mu @ jac_h.T / np.linalg.det(jac_h) return eps_tensor, mu_tensor @@ -583,6 +636,8 @@ def _resolve_pml_spec( pml: PmlSpec | tuple[int, int] | None, ) -> PmlSpec: """Normalize user PML input into a PmlSpec.""" + # The public API accepts compact tuples, but the solver only sees validated + # PmlSpec objects. if pml is None: return PmlSpec() if isinstance(pml, PmlSpec): @@ -594,6 +649,7 @@ def _resolve_boundary_spec( boundary: BoundarySpec | tuple[str, str] | None, ) -> BoundarySpec: """Normalize user boundary input into a BoundarySpec.""" + # BoundarySpec handles case normalization and allowed-value validation. if boundary is None: return BoundarySpec() if isinstance(boundary, BoundarySpec): @@ -622,6 +678,8 @@ def _solver_info_with_context( def _is_diagonal_tensor(tensor: np.ndarray, *, atol: float = 1e-12) -> bool: """Return whether a flattened tensor has negligible off-diagonal terms.""" + # Only the first two tensor axes are structural; the remaining axis is the + # flattened cell index. off_diagonal = np.ones((3, 3), dtype=bool) np.fill_diagonal(off_diagonal, False) return bool(np.all(np.abs(tensor[off_diagonal]) <= atol)) @@ -629,6 +687,8 @@ def _is_diagonal_tensor(tensor: np.ndarray, *, atol: float = 1e-12) -> bool: def _shift_target_neff(target_neff: float) -> float: """Nudge the shift target away from exact eigenvalues for ARPACK stability.""" + # Exact shifts can make shift-invert factorization singular; the tiny nudge + # preserves the requested target while avoiding exact equality. target_shift = float(10 * np.finfo(np.float32).eps) if abs(target_shift) > abs(target_neff * target_shift): return target_neff + target_shift @@ -637,6 +697,8 @@ def _shift_target_neff(target_neff: float) -> float: def _validate_edges(name: str, values: Sequence[float], cell_count: int) -> np.ndarray: """Validate mode-plane edge coordinates against a cell count.""" + # Edge validation is repeated here because callers may provide an existing + # Materials object from outside the dataclass constructors. edges = np.asarray(values, dtype=float) if edges.shape != (cell_count + 1,): raise ValueError(f"{name} must have length {cell_count + 1}") @@ -651,11 +713,14 @@ def _resolve_freqs( wavelength: float | Sequence[float] | None, ) -> tuple[float, ...]: """Resolve exactly one frequency or wavelength input into frequencies.""" + # Accept either frequencies or wavelengths, never both, so the solve axis is + # unambiguous. if (freqs is None) == (wavelength is None): raise ValueError("provide exactly one of freqs or wavelength") if freqs is not None: values = tuple(float(freq) for freq in freqs) else: + # Wavelengths are in microns because C_0 is stored in um/s. wavelengths = np.asarray(wavelength, dtype=float).reshape(-1) values = tuple(float(C_0 / value) for value in wavelengths) if not values or any(not np.isfinite(freq) or freq <= 0 for freq in values): @@ -665,6 +730,7 @@ def _resolve_freqs( def _dual_steps(primal_steps: np.ndarray) -> np.ndarray: """Return backward-grid step sizes from primal cell widths.""" + # Interior backward steps live halfway between adjacent primal cells. if len(primal_steps) == 1: return primal_steps.copy() return np.hstack((primal_steps[0], (primal_steps[:-1] + primal_steps[1:]) / 2)) @@ -672,6 +738,8 @@ def _dual_steps(primal_steps: np.ndarray) -> np.ndarray: def _fields_to_grid(fields: list[np.ndarray], shape: tuple[int, int]) -> dict[str, np.ndarray]: """Reshape flattened solver fields into x/y/mode arrays.""" + # scipy_reference returns [mode, flattened_cell]; public results use + # [x, y, mode] per component. nx, ny = shape mode_count = fields[0].shape[0] out = {} @@ -692,6 +760,8 @@ def _local_fields_to_global(fields: dict[str, np.ndarray], *, normal_axis: int) axis_names = ("x", "y", "z") if normal_axis not in {0, 1, 2}: raise ValueError("normal_axis must be 0, 1, or 2") + + # Local solver axes are always tangential-x, tangential-y, normal-z. local_to_global = (*(axis for axis in range(3) if axis != normal_axis), normal_axis) out: dict[str, np.ndarray] = {} for prefix in ("E", "H"): @@ -719,9 +789,15 @@ def _field_data_arrays( axis_names = ("x", "y", "z") if normal_axis not in {0, 1, 2}: raise ValueError("normal_axis must be 0, 1, or 2") + + # Determine which two global coordinates correspond to the solver's local + # x/y plane. tangential = tuple(axis for axis in range(3) if axis != normal_axis) coord0 = (x_edges[:-1] + x_edges[1:]) / 2 coord1 = (y_edges[:-1] + y_edges[1:]) / 2 + + # Store width coordinates beside center coordinates so integrations can use + # physical cell areas without reconstructing the original edges. coords = { axis_names[tangential[0]]: coord0, f"{axis_names[tangential[0]]}_width": (axis_names[tangential[0]], np.diff(x_edges)), @@ -741,6 +817,8 @@ def _field_data_arrays( ) out = {} for component, rows in fields_by_component.items(): + # Stack frequencies on axis 2, then insert the singleton normal axis so + # every component has x/y/z/f/mode_index dimensions. values = np.stack(rows, axis=2)[:, :, None, :, :] component_coords = dict(coords) component_coords["mode_index"] = np.arange(values.shape[-1]) @@ -755,16 +833,23 @@ def _field_data_arrays( def _default_initial_vector(size: int, shape: tuple[int, int] | None = None) -> np.ndarray: """Build a deterministic ARPACK seed vector for reproducible solves.""" + # When grid shape is known, build a seed with the same flattened cell order + # as the solver state vector. if shape is not None and size % (shape[0] * shape[1]) == 0: nx, ny = shape multiplier = size // (nx * ny) rng = np.random.default_rng(0) vector = rng.random((nx, ny, multiplier)) + 1j * rng.random((nx, ny, multiplier)) + + # Zero low-edge rows when possible so the initial guess is compatible + # with symmetry-wall derivative stencils. if nx > 1: vector[0, :, :] = 0 if ny > 1: vector[:, 0, :] = 0 stacked = np.concatenate(tuple(vector[ix, :, :] for ix in range(nx)), axis=0) return stacked.flatten("F") + + # Fallback for callers that only know the operator size. index = np.arange(1, size + 1, dtype=float) return np.sin(0.37 * index) + 1j * np.cos(0.53 * index) diff --git a/python/micromode/result.py b/python/micromode/result.py index 9145e6c..47d74b3 100644 --- a/python/micromode/result.py +++ b/python/micromode/result.py @@ -20,6 +20,8 @@ _OVERLAP_KINDS = {"electric", "power", "lorentz"} +# Result is the post-processing boundary: solver outputs are immutable xarray +# arrays, and convenience methods derive metrics without mutating fields. @dataclass(frozen=True) class Result: """Mode solver result data. @@ -39,12 +41,15 @@ class Result: def n_eff(self) -> xr.DataArray: """Real effective index.""" + # Keep n_complex as the source of truth; derived views preserve xarray + # coordinates for frequency and mode index. return self.n_complex.real @property def k_eff(self) -> xr.DataArray: """Imaginary part of the complex effective index.""" + # The imaginary part is used by loss calculations and release summaries. return self.n_complex.imag @cached_property @@ -54,9 +59,14 @@ def pol_fraction(self) -> xr.Dataset: tangential_dims = self._tangential_dims() if len(tangential_dims) != 2: raise ValueError("exactly two tangential field dimensions are required") + + # The two transverse electric components define the TE/TM split in the + # local mode plane. first, second = (f"E{dim}" for dim in tangential_dims) self._require_components((first, second)) + # Integrate component intensities over the spatial grid, then normalize + # to avoid amplitude-dependent fractions. first_power = self._integrated_power(first) second_power = self._integrated_power(second) total = np.maximum(first_power + second_power, np.finfo(float).eps) @@ -77,6 +87,8 @@ def pol_fraction_waveguide(self) -> xr.Dataset: normal_h = f"H{normal_dim}" self._require_components((*_E_COMPONENTS, *_H_COMPONENTS)) + # Waveguide fractions measure how much total E/H energy is transverse to + # the propagation normal. total_e = sum(self._integrated_power(component) for component in _E_COMPONENTS) total_h = sum(self._integrated_power(component) for component in _H_COMPONENTS) te = 1.0 - self._integrated_power(normal_e) / np.maximum(total_e, np.finfo(float).eps) @@ -88,9 +100,14 @@ def mode_area(self) -> xr.DataArray: """Effective mode area from electric-field intensity.""" self._require_components(_E_COMPONENTS) + + # Compute |E|^2 on the full grid before applying area weights. intensity = sum(np.abs(self.field_components[name].values) ** 2 for name in _E_COMPONENTS) axes = self._spatial_axes(self.field_components["Ex"]) weights = self._spatial_weights(self.field_components["Ex"]) + + # Standard effective-area definition: (integral I dA)^2 / + # integral(I^2 dA), guarded against empty or zero fields. numerator = np.sum(intensity * weights, axis=axes) ** 2 denominator = np.maximum(np.sum(intensity**2 * weights, axis=axes), np.finfo(float).eps) return self._mode_data_array(numerator / denominator) @@ -102,16 +119,24 @@ def modes_info(self) -> xr.Dataset: freq = self.n_complex.coords["f"] wavelength = xr.DataArray(C_0 / freq.values, dims=("f",), coords={"f": freq}) wavelength_cm = wavelength / 1e4 + + # Start with metrics that require no optional field components. metrics: dict[str, xr.DataArray] = { "wavelength": wavelength, "n eff": self.n_eff, "k eff": self.k_eff, "loss (dB/cm)": 20 * 2 * np.pi * np.log10(np.e) * self.k_eff / wavelength_cm, } + + # Optional arrays are preserved when callers provide group index or + # dispersion calculations externally. if self.n_group is not None: metrics["group index"] = self.n_group if self.dispersion is not None: metrics["dispersion"] = self.dispersion + + # Field-derived metrics are best-effort because users may request only a + # subset of components from solve_modes. self._add_optional_metric(metrics, "TE fraction", lambda: self.pol_fraction["te"]) self._add_optional_metric(metrics, "TM fraction", lambda: self.pol_fraction["tm"]) self._add_optional_metric(metrics, "wg TE fraction", lambda: self.pol_fraction_waveguide["te"]) @@ -142,13 +167,19 @@ def plot_field( raise ValueError(f"field component {component!r} is not available") import matplotlib.pyplot as plt + # Select a single frequency/mode slice before deciding how many spatial + # dimensions need to be shown. data_array = self._select_field(component, f=f, mode_index=mode_index) plane_dims = [dim for dim in _SPATIAL_DIMS if dim in data_array.dims and data_array.sizes[dim] > 1] if len(plane_dims) not in {1, 2}: raise ValueError("field plotting requires one or two non-singleton spatial dimensions") + # Put spatial axes in canonical x/y/z order so 1D and 2D plotting code + # does not depend on solver normal direction. data_array = data_array.transpose(*[dim for dim in _SPATIAL_DIMS if dim in data_array.dims]) values = np.asarray(data_array.values).squeeze() + + # Convert complex fields to the requested real-valued plotting channel. if val == "real": values = np.real(values) default_cmap = "RdBu_r" @@ -163,6 +194,8 @@ def plot_field( if ax is None: _, ax = plt.subplots() + + # Thin slice results become a line plot along the one non-singleton axis. if len(plane_dims) == 1: coord = np.asarray(data_array.coords[plane_dims[0]].values) ax.plot(coord, values) @@ -171,6 +204,7 @@ def plot_field( ax.set_title(f"{component}, f={self._selected_frequency(f):.6g}, mode={mode_index}") return ax + # Full 2D mode planes render with physical coordinate extents. x_coord = np.asarray(data_array.coords[plane_dims[0]].values) y_coord = np.asarray(data_array.coords[plane_dims[1]].values) extent = [x_coord.min(), x_coord.max(), y_coord.min(), y_coord.max()] @@ -202,9 +236,12 @@ def plot_field_components( import matplotlib.pyplot as plt + # Plot only requested components that are present in this Result. available = [component for component in components if component in self.field_components] if not available: raise ValueError("none of the requested field components are available") + + # Use a compact grid with up to three columns and hide unused axes. ncols = min(3, len(available)) nrows = int(np.ceil(len(available) / ncols)) fig, axes = plt.subplots(nrows, ncols, squeeze=False, figsize=(4.0 * ncols, 3.2 * nrows)) @@ -245,6 +282,9 @@ def overlap( other = self if other is None else other other_mode_index = mode_index if other_mode_index is None else other_mode_index other_f = f if other_f is None else other_f + + # Extract exactly one mode from each result before computing the selected + # overlap product. selected_self = self._selected_mode_fields(f=f, mode_index=mode_index) selected_other = other._selected_mode_fields(f=other_f, mode_index=other_mode_index) value = _overlap_value(self, selected_self, other, selected_other, kind=kind) @@ -272,6 +312,9 @@ def overlap_matrix( other = self if other is None else other other_f = f if other_f is None else other_f + + # Fill the dense pairwise matrix by delegating to the single-overlap + # method so normalization and validation behavior stays identical. values = np.empty( (self.n_complex.sizes["mode_index"], other.n_complex.sizes["mode_index"]), dtype=np.complex128 ) @@ -305,15 +348,22 @@ def to_hdf5(self, path: str | Path) -> Path: destination = Path(path) with h5py.File(destination, "w") as handle: + # Store a tiny file-level marker before writing xarray-shaped groups. handle.attrs["format"] = "micromode.Result" handle.attrs["version"] = 1 self._write_data_array(handle, "n_complex", self.n_complex) + + # Optional scalar arrays are omitted when absent rather than storing + # empty sentinel datasets. if self.n_group is not None: self._write_data_array(handle, "n_group", self.n_group) if self.dispersion is not None: self._write_data_array(handle, "dispersion", self.dispersion) if self.solver_info is not None: handle.attrs["solver_info"] = json.dumps(_json_safe(self.solver_info)) + + # Field components live under one group so readers can discover all + # available components without knowing what the solve requested. fields_group = handle.create_group("field_components") for component, data_array in self.field_components.items(): self._write_data_array(fields_group, component, data_array) @@ -329,11 +379,15 @@ def from_hdf5(cls, path: str | Path) -> Result: raise ImportError("h5py is required for Result.from_hdf5()") from exc with h5py.File(path, "r") as handle: + # Rehydrate required and optional DataArrays from the compact group + # layout written by to_hdf5. n_complex = cls._read_data_array(handle["n_complex"]) n_group = cls._read_data_array(handle["n_group"]) if "n_group" in handle else None dispersion = cls._read_data_array(handle["dispersion"]) if "dispersion" in handle else None solver_info = _loads_hdf5_json_attr(handle.attrs["solver_info"]) if "solver_info" in handle.attrs else None field_group: Any = handle["field_components"] + + # Preserve whatever subset of fields was saved. field_components = {component: cls._read_data_array(group) for component, group in field_group.items()} return cls( n_complex=n_complex, @@ -345,6 +399,8 @@ def from_hdf5(cls, path: str | Path) -> Result: def _require_components(self, components: Iterable[str]) -> None: """Raise if required field components are absent.""" + # Centralize missing-component errors so derived metrics and overlaps use + # consistent messages. missing = [component for component in components if component not in self.field_components] if missing: raise ValueError(f"field component(s) required but missing: {', '.join(missing)}") @@ -352,9 +408,14 @@ def _require_components(self, components: Iterable[str]) -> None: def _normal_dim(self) -> str: """Infer the singleton propagation-normal dimension.""" reference = self._reference_field() + + # Solver-created results annotate the normal dimension explicitly. attr_normal = reference.attrs.get("normal_dim") if attr_normal in _SPATIAL_DIMS and attr_normal in reference.dims: return str(attr_normal) + + # External Results may not carry attrs, so fall back to singleton spatial + # dimensions and then the smallest spatial dimension. spatial_dims = [dim for dim in _SPATIAL_DIMS if dim in reference.dims] singleton_dims = [dim for dim in spatial_dims if reference.sizes[dim] == 1] if len(singleton_dims) == 1: @@ -400,9 +461,14 @@ def _spatial_weights(self, data_array: xr.DataArray) -> np.ndarray: continue width_coord = f"{dim}_width" if width_coord in data_array.coords: + # Width coordinates are written by solve_modes and preserve the + # original nonuniform grid exactly. widths = np.asarray(data_array.coords[width_coord].values, dtype=float) else: widths = self._cell_widths(np.asarray(data_array.coords[dim].values, dtype=float)) + + # Reshape each 1D width vector so NumPy broadcasting multiplies it + # along the matching spatial axis only. shape = [1] * data_array.ndim shape[axis] = len(widths) weights = weights * widths.reshape(shape) @@ -411,8 +477,13 @@ def _spatial_weights(self, data_array: xr.DataArray) -> np.ndarray: @staticmethod def _cell_widths(values: np.ndarray) -> np.ndarray: """Infer cell widths from coordinate midpoints.""" + # A singleton dimension represents the propagation normal and contributes + # unit width to area/volume weights. if values.size <= 1: return np.ones(values.shape, dtype=float) + + # Reconstruct edges halfway between adjacent centers, with endpoint + # extrapolation matching the nearest cell spacing. midpoints = 0.5 * (values[1:] + values[:-1]) edges = np.concatenate( ( @@ -438,6 +509,8 @@ def _add_optional_metric( getter: Any, ) -> None: """Add a metric if its required field components are available.""" + # Missing field components raise ValueError in the metric getter; skip + # those metrics while letting unexpected exceptions surface. try: metrics[name] = getter() except ValueError: @@ -445,6 +518,8 @@ def _add_optional_metric( def _select_field(self, component: str, *, f: int | float, mode_index: int) -> xr.DataArray: """Select one component at a frequency and mode index.""" + # Integer f means positional selection; float f means nearest frequency + # coordinate, which is friendlier for wavelength-derived frequencies. data_array = self.field_components[component] data_array = data_array.isel(f=f) if isinstance(f, int) else data_array.sel(f=f, method="nearest") return data_array.isel(mode_index=mode_index) @@ -466,11 +541,16 @@ def _selected_frequency(self, f: int | float) -> float: def _write_data_array(parent: Any, name: str, data_array: xr.DataArray) -> None: """Write an xarray DataArray to the compact HDF5 layout.""" group = parent.create_group(name) + + # Store dimensions as metadata and raw values as a single dataset. group.attrs["dims"] = json.dumps(list(data_array.dims)) for attr_name, attr_value in data_array.attrs.items(): if isinstance(attr_value, (str, int, float, bool, np.integer, np.floating, np.bool_)): group.attrs[f"attr:{attr_name}"] = attr_value group.create_dataset("values", data=data_array.values) + + # Coordinates keep their own dimensions because xarray coordinates can be + # scalar, 1D, or attached to nonmatching axes. coords = group.create_group("coords") for coord_name, coord in data_array.coords.items(): coord_values = np.asarray(coord.values) @@ -480,6 +560,8 @@ def _write_data_array(parent: Any, name: str, data_array: xr.DataArray) -> None: @staticmethod def _read_data_array(group: Any) -> xr.DataArray: """Read an xarray DataArray from the compact HDF5 layout.""" + # Rebuild attrs and coordinates first, then hand them to xarray with the + # stored value dataset. dims = tuple(json.loads(group.attrs["dims"])) attrs = { key.removeprefix("attr:"): value @@ -506,6 +588,8 @@ def overlap( def _json_safe(value: Any) -> Any: """Convert NumPy and complex values into JSON-safe objects.""" + # solver_info may contain NumPy arrays/scalars, complex values, and nested + # containers; JSON attributes require plain Python data. if isinstance(value, np.ndarray): return _json_safe(value.tolist()) if isinstance(value, (complex, np.complexfloating)): @@ -513,6 +597,8 @@ def _json_safe(value: Any) -> Any: return {"real": complex_value.real, "imag": complex_value.imag} if isinstance(value, dict): return {str(key): _json_safe(item) for key, item in value.items()} + + # Lists and tuples are both serialized as JSON arrays. if isinstance(value, (list, tuple)): return [_json_safe(item) for item in value] if isinstance(value, (np.integer, np.floating, np.bool_)): @@ -536,6 +622,7 @@ def _overlap_value( kind: str, ) -> complex: """Compute one unnormalized field overlap integral.""" + # Validate the requested overlap family before checking required components. if kind not in _OVERLAP_KINDS: raise ValueError("kind must be 'electric', 'power', or 'lorentz'") if kind == "electric": @@ -545,6 +632,9 @@ def _overlap_value( if missing: raise ValueError(f"electric overlap requires field component(s): {', '.join(missing)}") _validate_overlap_grid(left_result, left, right_result, right) + + # Electric overlap is a weighted dot product across all electric + # components. weights = left_result._spatial_weights(left["Ex"]) integrand = sum(left[component].values * np.conj(right[component].values) for component in _E_COMPONENTS) return complex(np.sum(integrand * weights)) @@ -561,6 +651,8 @@ def _overlap_value( _validate_overlap_grid(left_result, left, right_result, right) weights = left_result._spatial_weights(left["Ex"]) normal = left_result._normal_dim() + + # Dispatch to the cross-product component aligned with the mode normal. if kind == "lorentz": integrand = _normal_lorentz_integrand(left, right, normal) else: @@ -574,6 +666,8 @@ def _normal_power_integrand( normal: str, ) -> np.ndarray: """Return the normal Poynting-flux integrand.""" + # Compute (E_left x H_right*) . n for whichever global axis is normal to the + # mode plane. if normal == "x": return left["Ey"].values * np.conj(right["Hz"].values) - left["Ez"].values * np.conj(right["Hy"].values) if normal == "y": @@ -589,6 +683,7 @@ def _normal_lorentz_integrand( normal: str, ) -> np.ndarray: """Return the symmetrized unconjugated Lorentz integrand.""" + # Lorentz reciprocity uses both E_left x H_right and E_right x H_left. left_cross_right = _normal_unconjugated_cross_integrand(left, right, normal) right_cross_left = _normal_unconjugated_cross_integrand(right, left, normal) return 0.5 * (left_cross_right + right_cross_left) @@ -616,15 +711,22 @@ def _validate_overlap_grid( right: dict[str, xr.DataArray], ) -> None: """Ensure two modes live on compatible spatial grids.""" + # Overlap values are meaningful only when both fields use the same normal and + # spatial coordinate system. if left_result._normal_dim() != right_result._normal_dim(): raise ValueError("mode overlap requires matching mode-plane normal dimensions") reference_left = left["Ex"] reference_right = right["Ex"] + + # Dimension order must match before comparing coordinate values. if reference_left.dims != reference_right.dims: raise ValueError("mode overlap requires matching field dimensions") for dim in reference_left.dims: if dim not in _SPATIAL_DIMS: continue + + # Compare each spatial coordinate axis with tolerance to allow harmless + # floating-point roundoff in externally constructed Results. left_coord = np.asarray(reference_left.coords[dim].values, dtype=float) right_coord = np.asarray(reference_right.coords[dim].values, dtype=float) if left_coord.shape != right_coord.shape or not np.allclose(left_coord, right_coord): diff --git a/python/micromode/scipy_reference.py b/python/micromode/scipy_reference.py index 317a26a..f623a78 100644 --- a/python/micromode/scipy_reference.py +++ b/python/micromode/scipy_reference.py @@ -22,6 +22,8 @@ SparseSolveResult = tuple[np.ndarray, list[np.ndarray], dict[str, object]] +# The SciPy reference layer stays deliberately explicit so finite-difference +# assembly can be audited against the physics model. @dataclass class _ModeFields: """Mutable six-component field container used during normalization.""" @@ -39,6 +41,8 @@ def components(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np def add_scaled(self, other: _ModeFields, scale: complex) -> None: """Add another mode into this mode with a complex scale factor.""" + # Orthogonalization operates in-place to avoid repeatedly allocating six + # large component arrays per mode. for left, right in zip(self.components(), other.components(), strict=True): left += scale * right @@ -68,14 +72,20 @@ def solve_diagonal_scipy_reference( """ sparse, spla, scipy_linalg = _import_scipy() + + # The diagonal formulation has two transverse electric unknowns per cell. nx = len(dlf[0]) ny = len(dlf[1]) n = nx * ny + + # Validate flattened tensor layout before assembling sparse operators. if eps_tensor.shape != mu_tensor.shape or eps_tensor.shape[:2] != (3, 3) or eps_tensor.shape[-1] != n: raise ValueError("eps_tensor and mu_tensor must both have shape (3, 3, len(dlf[0]) * len(dlf[1]))") if num_modes <= 0: raise ValueError("num_modes must be positive") + # Build finite-difference derivative matrices, including optional PML + # complex-coordinate stretching. derivatives = _create_derivative_matrices( sparse, eps_tensor=eps_tensor, @@ -90,9 +100,14 @@ def solve_diagonal_scipy_reference( dmin_pmc=dmin_pmc, scale=float(derivative_scale), ) + + # Assemble the reduced Ex/Ey eigen-operator for diagonal material tensors. operators = _assemble_diagonal_operators(sparse, eps_tensor, mu_tensor, derivatives) operator = cast(Any, operators["mat"]) eig_guess = complex(-(neff_guess * neff_guess), 0.0) + + # ARPACK can solve a real problem faster when the operator and shift are + # numerically real; otherwise it keeps the complex path. operator, arpack_initial_vector, arpack_guess = _real_arpack_problem_if_close(operator, initial_vector, eig_guess) values, vectors = _selected_eigenpairs( operator, @@ -103,6 +118,9 @@ def solve_diagonal_scipy_reference( spla=spla, scipy_linalg=scipy_linalg, ) + + # Normalize residuals by eigenvector norm so diagnostics are comparable + # across modes. residuals = np.asarray( [ np.linalg.norm(operator @ vectors[:, index] - values[index] * vectors[:, index]) @@ -113,48 +131,64 @@ def solve_diagonal_scipy_reference( vector_norms = np.maximum(np.linalg.norm(vectors, axis=0), np.finfo(float).eps) residuals = residuals / vector_norms + # Convert eigenvalues of -n_eff^2 back to n_eff and sort by real index. modes: list[tuple[complex, np.ndarray, float]] = [] for value, vector, residual in zip(values, vectors.T, residuals, strict=True): modes.append((complex(np.sqrt(-value + 0j)), np.asarray(vector, dtype=np.complex128), float(residual))) modes.sort(key=lambda item: item[0].real, reverse=True) + # Precompute z-component inverse material matrices used during field + # reconstruction. inv_eps_zz = sparse.diags(1.0 / eps_tensor[2, 2, :], format="csc") inv_mu_zz = sparse.diags(1.0 / mu_tensor[2, 2, :], format="csc") dxf, dxb, dyf, dyb = derivatives cell_areas = np.repeat(np.asarray(dlf[0], dtype=float), ny) * np.tile(np.asarray(dlf[1], dtype=float), nx) + # Reconstruct all six field components from the transverse eigenvectors. n_complex: list[complex] = [] mode_fields: list[_ModeFields] = [] sorted_residuals: list[float] = [] for mode_n, vector, residual in modes: + # The eigenvector stores Ex then Ey in flattened cell order. ex = vector[:n].copy() ey = vector[n:].copy() denom = complex(-mode_n.imag, mode_n.real) + # Magnetic transverse components come from the Q operator divided by the + # effective-index factor. h_field = operators["qmat"] @ vector hx = np.asarray(h_field[:n] / denom, dtype=np.complex128) hy = np.asarray(h_field[n:] / denom, dtype=np.complex128) + # Longitudinal H follows from the curl of transverse E. hz_source = dxf @ ey - dyf @ ex hz = np.asarray(inv_mu_zz @ hz_source, dtype=np.complex128) + # Longitudinal E follows from the curl of the material electric block. h_partial = np.asarray((operators["q_ep"] @ vector) / denom, dtype=np.complex128) ez_source = dxb @ h_partial[n:] - dyb @ h_partial[:n] ez = np.asarray(inv_eps_zz @ ez_source, dtype=np.complex128) + # Convert magnetic fields to the same physical normalization convention + # used by Result overlap calculations. h_scale = -1j / ETA0 hx *= h_scale hy *= h_scale hz *= h_scale + + # Backward propagation flips transverse H and longitudinal E. if direction == "-": hx *= -1.0 hy *= -1.0 ez *= -1.0 + # Accumulate raw modes first; normalization is applied to the full set + # so modes can be Lorentz-orthogonalized together. n_complex.append(mode_n) sorted_residuals.append(residual) mode_fields.append(_ModeFields(ex=ex, ey=ey, ez=ez, hx=hx, hy=hy, hz=hz)) + # Apply final normalization and package field components by component name. orthogonalization = _lorentz_orthogonalize_and_normalize(mode_fields, cell_areas) fields = [ np.asarray([getattr(mode, component) for mode in mode_fields], dtype=np.complex128) @@ -196,14 +230,19 @@ def solve_tensorial_scipy_reference( """Solve the first-order tensorial sparse eigenproblem with SciPy/ARPACK.""" sparse, spla, scipy_linalg = _import_scipy() + + # The tensorial formulation keeps Ex, Ey, Hx, and Hy as unknowns per cell. nx = len(dlf[0]) ny = len(dlf[1]) n = nx * ny + + # Full tensors are already flattened by the raster layer. if eps_tensor.shape != mu_tensor.shape or eps_tensor.shape[:2] != (3, 3) or eps_tensor.shape[-1] != n: raise ValueError("eps_tensor and mu_tensor must both have shape (3, 3, len(dlf[0]) * len(dlf[1]))") if num_modes <= 0: raise ValueError("num_modes must be positive") + # Reuse the same Yee derivative matrices as the diagonal solver. derivatives = _create_derivative_matrices( sparse, eps_tensor=eps_tensor, @@ -218,6 +257,8 @@ def solve_tensorial_scipy_reference( dmin_pmc=dmin_pmc, scale=float(derivative_scale), ) + + # Assemble the first-order operator that directly returns n_eff eigenvalues. operator = _assemble_tensorial_operator(sparse, eps_tensor, mu_tensor, derivatives) values, vectors = _selected_eigenpairs( operator, @@ -228,6 +269,8 @@ def solve_tensorial_scipy_reference( spla=spla, scipy_linalg=scipy_linalg, ) + + # Record normalized ARPACK residuals for solver diagnostics. residuals = np.asarray( [ np.linalg.norm(operator @ vectors[:, index] - values[index] * vectors[:, index]) @@ -237,12 +280,14 @@ def solve_tensorial_scipy_reference( ) residuals = residuals / np.maximum(np.linalg.norm(vectors, axis=0), np.finfo(float).eps) + # Sort modes in descending real effective index, matching diagonal behavior. modes = [ (complex(value), np.asarray(vector, dtype=np.complex128), float(residual)) for value, vector, residual in zip(values, vectors.T, residuals, strict=True) ] modes.sort(key=lambda item: item[0].real, reverse=True) + # z-component inverses reconstruct Ez and Hz after the transverse solve. inv_eps_zz = sparse.diags(1.0 / eps_tensor[2, 2, :], format="csc") inv_mu_zz = sparse.diags(1.0 / mu_tensor[2, 2, :], format="csc") dxf, dxb, dyf, dyb = derivatives @@ -252,30 +297,38 @@ def solve_tensorial_scipy_reference( mode_fields: list[_ModeFields] = [] sorted_residuals: list[float] = [] for mode_n, vector, residual in modes: + # The first-order eigenvector stores transverse E followed by transverse H. ex = vector[:n].copy() ey = vector[n : 2 * n].copy() hx = vector[2 * n : 3 * n].copy() hy = vector[3 * n : 4 * n].copy() + # Longitudinal fields include off-diagonal material coupling through the + # z-row of the tensor. hz_source = dxf @ ey - dyf @ ex - mu_tensor[2, 0, :] * hx - mu_tensor[2, 1, :] * hy hz = np.asarray(inv_mu_zz @ hz_source, dtype=np.complex128) ez_source = dxb @ hy - dyb @ hx - eps_tensor[2, 0, :] * ex - eps_tensor[2, 1, :] * ey ez = np.asarray(inv_eps_zz @ ez_source, dtype=np.complex128) + # Match the magnetic-field convention used by the diagonal solver. h_scale = -1j / ETA0 hx *= h_scale hy *= h_scale hz *= h_scale + + # Backward propagation uses the same sign convention in both solvers. if direction == "-": hx *= -1.0 hy *= -1.0 ez *= -1.0 + # Store modes for the shared Lorentz orthogonalization pass. n_complex.append(mode_n) sorted_residuals.append(residual) mode_fields.append(_ModeFields(ex=ex, ey=ey, ez=ez, hx=hx, hy=hy, hz=hz)) + # Normalize the full set, then return component-major field arrays. orthogonalization = _lorentz_orthogonalize_and_normalize(mode_fields, cell_areas) fields = [ np.asarray([getattr(mode, component) for mode in mode_fields], dtype=np.complex128) @@ -323,6 +376,8 @@ def _create_derivative_matrices( scale: float, ): """Assemble scaled Yee derivative matrices with optional PML stretching.""" + # Build raw finite-difference stencils first; optional PML scaling is a + # diagonal pre-multiplication applied afterward. matrices = ( _make_dxf(sparse, np.asarray(dlf[0], dtype=float), shape, dmin_pmc[0]), _make_dxb(sparse, np.asarray(dlb[0], dtype=float), shape, dmin_pmc[0]), @@ -330,6 +385,7 @@ def _create_derivative_matrices( _make_dyb(sparse, np.asarray(dlb[1], dtype=float), shape, dmin_pmc[1]), ) if num_pml[0] > 0 or num_pml[1] > 0: + # Complex stretch factors require angular frequency to scale sigma. if omega is None: raise ValueError("omega is required when num_pml is nonzero") pml_values = _create_s_diagonal_values( @@ -346,6 +402,8 @@ def _create_derivative_matrices( matrices = tuple( sparse.diags(values, format="csc") @ matrix for values, matrix in zip(pml_values, matrices, strict=True) ) + + # derivative_scale nondimensionalizes spatial derivatives by wavelength. return tuple(matrix * complex(scale, 0.0) for matrix in matrices) @@ -354,6 +412,8 @@ def _make_dxf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): nx, ny = shape if nx == 1: return sparse.csc_matrix((ny, ny), dtype=np.complex128) + + # Assemble in coordinate-list form, then convert to CSC for sparse algebra. rows: list[int] = [] cols: list[int] = [] data: list[complex] = [] @@ -361,12 +421,16 @@ def _make_dxf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): for iy in range(ny): row = ix * ny + iy value = 1.0 / dls[ix] + + # At the low edge, PEC-style symmetry drops the diagonal term while + # PMC keeps the one-sided stencil. diagonal = 0.0 if ix == 0 and not pmc else -value if diagonal != 0.0: rows.append(row) cols.append(row) data.append(diagonal) if ix + 1 < nx: + # Interior forward difference points to the next x cell. rows.append(row) cols.append((ix + 1) * ny + iy) data.append(value) @@ -378,6 +442,8 @@ def _make_dxb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): nx, ny = shape if nx == 1: return sparse.csc_matrix((ny, ny), dtype=np.complex128) + + # Backward stencils share the same flattened row order as forward stencils. rows: list[int] = [] cols: list[int] = [] data: list[complex] = [] @@ -385,12 +451,16 @@ def _make_dxb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): for iy in range(ny): row = ix * ny + iy value = 1.0 / dls[ix] + + # PMC at the low boundary mirrors the field and doubles the diagonal + # contribution; otherwise the first row has no backward neighbor. diagonal = 2.0 * value if ix == 0 and pmc else (0.0 if ix == 0 else value) if diagonal != 0.0: rows.append(row) cols.append(row) data.append(diagonal) if ix > 0: + # Interior backward difference points to the previous x cell. rows.append(row) cols.append((ix - 1) * ny + iy) data.append(-value) @@ -402,6 +472,8 @@ def _make_dyf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): nx, ny = shape if ny == 1: return sparse.csc_matrix((nx, nx), dtype=np.complex128) + + # y derivatives use the same flat index but move along the inner y axis. rows: list[int] = [] cols: list[int] = [] data: list[complex] = [] @@ -409,12 +481,15 @@ def _make_dyf(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): for iy in range(ny): row = ix * ny + iy value = 1.0 / dls[iy] + + # Low-edge y boundary uses the same PEC/PMC convention as x. diagonal = 0.0 if iy == 0 and not pmc else -value if diagonal != 0.0: rows.append(row) cols.append(row) data.append(diagonal) if iy + 1 < ny: + # Interior forward y difference points to the next y cell. rows.append(row) cols.append(ix * ny + iy + 1) data.append(value) @@ -426,6 +501,8 @@ def _make_dyb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): nx, ny = shape if ny == 1: return sparse.csc_matrix((nx, nx), dtype=np.complex128) + + # Backward y differences mirror _make_dxb on the inner axis. rows: list[int] = [] cols: list[int] = [] data: list[complex] = [] @@ -433,12 +510,15 @@ def _make_dyb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): for iy in range(ny): row = ix * ny + iy value = 1.0 / dls[iy] + + # A PMC low edge doubles the reflected diagonal contribution. diagonal = 2.0 * value if iy == 0 and pmc else (0.0 if iy == 0 else value) if diagonal != 0.0: rows.append(row) cols.append(row) data.append(diagonal) if iy > 0: + # Interior backward y difference points to the previous y cell. rows.append(row) cols.append(ix * ny + iy - 1) data.append(-value) @@ -447,9 +527,12 @@ def _make_dyb(sparse, dls: np.ndarray, shape: tuple[int, int], pmc: bool): def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_mats) -> dict[str, object]: """Assemble the reduced diagonal-material eigen-operator blocks.""" + # All blocks operate on flattened cell vectors. n = eps.shape[-1] zero = sparse.csc_matrix((n, n), dtype=np.complex128) dxf, dxb, dyf, dyb = der_mats + + # Longitudinal component elimination requires inverse zz material terms. inv_eps_zz = sparse.diags(1.0 / eps[2, 2, :], format="csc") inv_mu_zz = sparse.diags(1.0 / mu[2, 2, :], format="csc") @@ -459,6 +542,8 @@ def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_ma [[zero, sparse.diags(mu[1, 1, :], format="csc")], [-sparse.diags(mu[0, 0, :], format="csc"), zero]], format="csc", ) + + # P_partial captures derivative coupling introduced by eliminating Ez. p_partial = sparse.bmat( [ [-(dxf @ inv_eps_zz @ dyb), dxf @ inv_eps_zz @ dxb], @@ -470,6 +555,8 @@ def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_ma [[zero, sparse.diags(eps[1, 1, :], format="csc")], [-sparse.diags(eps[0, 0, :], format="csc"), zero]], format="csc", ) + + # Q_partial captures derivative coupling introduced by eliminating Hz. q_partial = sparse.bmat( [ [-(dxb @ inv_mu_zz @ dyf), dxb @ inv_mu_zz @ dxf], @@ -478,12 +565,15 @@ def _assemble_diagonal_operators(sparse, eps: np.ndarray, mu: np.ndarray, der_ma format="csc", ) qmat = q_ep + q_partial + + # The final reduced operator acts on [Ex, Ey]. mat = p_mu @ qmat + p_partial @ q_ep return {"q_ep": _canonical_sparse(q_ep), "qmat": _canonical_sparse(qmat), "mat": _canonical_sparse(mat)} def _assemble_tensorial_operator(sparse, eps: np.ndarray, mu: np.ndarray, der_mats): """Assemble the full tensorial first-order eigen-operator.""" + # The tensorial operator acts on [Ex, Ey, Hx, Hy] and leaves Ez/Hz implicit. dxf, dxb, dyf, dyb = der_mats inv_eps_22 = sparse.diags(1.0 / eps[2, 2, :], format="csc") inv_mu_22 = sparse.diags(1.0 / mu[2, 2, :], format="csc") @@ -492,6 +582,7 @@ def diag(values): """Return a sparse diagonal matrix for one flattened tensor component.""" return sparse.diags(values, format="csc") + # Precompute tensor ratios that appear repeatedly after eliminating z fields. eps_20_over_22 = eps[2, 0, :] / eps[2, 2, :] eps_21_over_22 = eps[2, 1, :] / eps[2, 2, :] eps_02_over_22 = eps[0, 2, :] / eps[2, 2, :] @@ -516,6 +607,7 @@ def diag(values): # Block names encode output row and input column: # a = electric transverse rows, b = magnetic transverse rows, x/y = local # transverse components. For example, axby maps Hy into the Ex equation. + # Electric rows include derivative terms and magnetic tensor corrections. axax = -(dxf @ diag(eps_20_over_22)) - diag(mu_12_over_22) @ dyf axay = -(dxf @ diag(eps_21_over_22)) + diag(mu_12_over_22) @ dxf axbx = -(dxf @ inv_eps_22 @ dyb) + diag(mu_10_s) @@ -526,6 +618,7 @@ def diag(values): aybx = -(dyf @ inv_eps_22 @ dyb) - diag(mu_00_s) ayby = dyf @ inv_eps_22 @ dxb - diag(mu_01_s) + # Magnetic rows mirror the electric rows with eps/mu roles swapped. bxax = -(dxb @ inv_mu_22 @ dyf) + diag(eps_10_s) bxay = dxb @ inv_mu_22 @ dxf + diag(eps_11_s) bxbx = -(dxb @ diag(mu_20_over_22)) - diag(eps_12_over_22) @ dyb @@ -536,6 +629,7 @@ def diag(values): bybx = -(dyb @ diag(mu_20_over_22)) + diag(eps_02_over_22) @ dyb byby = -(dyb @ diag(mu_21_over_22)) - diag(eps_02_over_22) @ dxb + # The leading -i matches the chosen time-harmonic convention. return _canonical_sparse( -1j * sparse.bmat( @@ -552,6 +646,7 @@ def diag(values): def _canonical_sparse(matrix): """Return a cleaned CSC sparse matrix.""" + # CSC is the format used by ARPACK and sparse block algebra here. matrix = matrix.tocsc(copy=True) matrix.eliminate_zeros() return matrix @@ -559,11 +654,15 @@ def _canonical_sparse(matrix): def _real_arpack_problem_if_close(matrix, initial_vector: np.ndarray | None, guess: complex): """Use a real ARPACK problem when the operator and shift are effectively real.""" + # Empty operators cannot be inspected for imaginary scale. if matrix.nnz == 0: return matrix, initial_vector, guess matrix_imag = matrix.data.imag matrix_scale = max(float(np.max(np.abs(matrix.data))), 1.0) guess_is_real = abs(guess.imag) <= 1e-14 * max(abs(guess), 1.0) + + # If the complex part is numerical noise, cast to real to avoid SciPy's + # ComplexWarning path and improve reproducibility. if np.max(np.abs(matrix_imag)) <= 1e-14 * matrix_scale and guess_is_real: real_vector = None if initial_vector is None else np.asarray(initial_vector.real, dtype=float) return matrix.real.astype(float), real_vector, float(guess.real) @@ -582,14 +681,21 @@ def _selected_eigenpairs( ) -> tuple[np.ndarray, np.ndarray]: """Select eigenpairs nearest the requested shift.""" size = mat.shape[0] + + # Very small operators cannot use sparse eigs with k >= N - 1, so fall back + # to dense eig and select the nearest modes manually. if num_modes >= size - 1: values, vectors = scipy_linalg.eig(mat.toarray()) order = np.argsort(np.abs(values - sigma))[:num_modes] return values[order], vectors[:, order] + # Keep ARPACK's Krylov subspace large enough for the requested number of + # modes but bounded by the matrix size. ncv = min(size, max(int(krylov_dim), num_modes + 2)) if ncv <= num_modes + 1: ncv = min(size, num_modes + 2) + + # Shift-invert mode returns eigenvalues closest to sigma. with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=np.exceptions.ComplexWarning, module="scipy") values, vectors = spla.eigs( @@ -618,7 +724,12 @@ def _create_s_diagonal_values( ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Build inverse PML stretch vectors for every derivative stencil.""" nx, ny = shape + + # Estimate boundary-relative speed so PML sigma scales with local material. avg_speed = _average_relative_speed(shape, num_pml, eps_tensor, mu_tensor) + + # Forward and backward derivative stencils sample stretch factors at + # different Yee locations. sx_f = _create_sfactor( "f", omega, np.asarray(dlf[0], dtype=float), nx, num_pml[0], dmin_pml[0], avg_speed[:2], profile ) @@ -632,6 +743,7 @@ def _create_s_diagonal_values( "b", omega, np.asarray(dlb[1], dtype=float), ny, num_pml[1], dmin_pml[1], avg_speed[2:], profile ) + # Expand one-dimensional stretch factors into flattened cell vectors. sx_f_vec = np.empty(nx * ny, dtype=np.complex128) sx_b_vec = np.empty(nx * ny, dtype=np.complex128) sy_f_vec = np.empty(nx * ny, dtype=np.complex128) @@ -653,6 +765,8 @@ def _average_relative_speed( mu_tensor: np.ndarray, ) -> np.ndarray: """Estimate relative wave speed near PML boundaries.""" + # Average diagonal epsilon and mu on each side, then convert to relative + # speed for damping-strength scaling. eps_avg = _pml_average_all_sides(shape, num_pml, eps_tensor) mu_avg = _pml_average_all_sides(shape, num_pml, mu_tensor) return 1.0 / np.sqrt(eps_avg * mu_avg) @@ -661,11 +775,15 @@ def _average_relative_speed( def _pml_average_all_sides(shape: tuple[int, int], num_pml: tuple[int, int], tensor: np.ndarray) -> np.ndarray: """Average diagonal material components on each boundary side.""" nx, ny = shape + + # Region order is xmin, xmax, ymin, ymax. regions: list[list[complex]] = [[], [], [], []] for comp in range(3): for ix in range(nx): for iy in range(ny): value = complex(tensor[comp, comp, ix * ny + iy]) + + # Collect only cells that lie inside the requested PML thickness. if ix < num_pml[0]: regions[0].append(value) if ix >= max(nx - num_pml[0], 0) + 1: @@ -675,6 +793,9 @@ def _pml_average_all_sides(shape: tuple[int, int], num_pml: tuple[int, int], ten if iy >= max(ny - num_pml[1], 0) + 1: regions[3].append(value) out = np.ones(4, dtype=np.complex128) + + # If a side has no PML cells, leave its average at one so it does not affect + # unused stretch factors. for index, values in enumerate(regions): if values: out[index] = sum(values) / len(values) @@ -692,8 +813,11 @@ def _create_sfactor( profile: dict[str, float | int] | None, ) -> np.ndarray: """Dispatch PML stretch-factor construction for a derivative direction.""" + # No PML means identity stretching along this axis. if n_pml == 0: return np.ones(n, dtype=np.complex128) + + # Forward and backward stencils use half-cell-shifted PML depths. if direction == "f": return _create_sfactor_f(omega, dls, n, n_pml, dmin_pml, avg_speed, profile) if direction == "b": @@ -711,8 +835,10 @@ def _create_sfactor_f( profile: dict[str, float | int] | None, ) -> np.ndarray: """Build forward-grid PML stretch factors.""" + # Start with identity stretch and overwrite only PML cells. sfactor = np.ones(n, dtype=np.complex128) for i in range(n): + # Low-edge PML can be disabled by symmetry walls. if i < n_pml and dmin_pml: sfactor[i] = _s_value(dls[0], (n_pml - i - 0.5) / n_pml, omega, avg_speed[0], profile) elif i >= n - n_pml: @@ -730,6 +856,7 @@ def _create_sfactor_b( profile: dict[str, float | int] | None, ) -> np.ndarray: """Build backward-grid PML stretch factors.""" + # Backward stencils sample exactly on the low edge, not half a cell inward. sfactor = np.ones(n, dtype=np.complex128) for i in range(n): if i < n_pml and dmin_pml: @@ -747,6 +874,7 @@ def _s_value( profile: dict[str, float | int] | None, ) -> complex: """Evaluate one polynomial PML stretch factor.""" + # Defaults match PmlSpec and are overridden by a validated profile dict. values = { "sigma_max": 2.0, "kappa_min": 1.0, @@ -755,6 +883,8 @@ def _s_value( } if profile is not None: values.update(profile) + + # Polynomial depth controls both attenuation sigma and coordinate kappa. step_power = step ** int(values["order"]) kappa = float(values["kappa_min"]) + (float(values["kappa_max"]) - float(values["kappa_min"])) * step_power sigma = avg_speed * (float(values["sigma_max"]) / (ETA0 * dl) * step_power) @@ -763,9 +893,11 @@ def _s_value( def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: np.ndarray) -> dict[str, object]: """Normalize modes and enforce Lorentz orthogonality.""" + # Begin with unit transverse power so projection coefficients are stable. for mode in modes: _normalize_to_unit_power(mode, cell_areas) + # Modified Gram-Schmidt using the unconjugated Lorentz overlap. for mode_index, mode in enumerate(modes): for previous in modes[:mode_index]: denom = _lorentz_overlap(previous, previous, cell_areas) @@ -773,9 +905,12 @@ def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: n continue coeff = _lorentz_overlap(previous, mode, cell_areas) / denom mode.add_scaled(previous, -coeff) + + # Renormalize and pin phase after each projection step. _normalize_to_unit_power(mode, cell_areas) _apply_dominant_e_phase_convention(mode) + # Diagnostics expose both power normalization and residual Lorentz coupling. power_norms = np.asarray([abs(_transverse_power(mode, cell_areas)) for mode in modes], dtype=float) lorentz_norms = np.asarray([_lorentz_overlap(mode, mode, cell_areas) for mode in modes], dtype=np.complex128) error = 0.0 @@ -786,6 +921,7 @@ def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: n denom = float(np.sqrt(abs(lorentz_norms[left_index]) * abs(lorentz_norms[right_index]))) if denom <= np.finfo(float).eps: continue + # Track the largest normalized off-diagonal overlap. error = max(error, abs(_lorentz_overlap(left, right, cell_areas)) / denom) return { "power_norms": power_norms, @@ -796,10 +932,13 @@ def _lorentz_orthogonalize_and_normalize(modes: list[_ModeFields], cell_areas: n def _normalize_to_unit_power(mode: _ModeFields, cell_areas: np.ndarray) -> float: """Scale a mode to unit transverse power.""" + # Use magnitude so forward/backward sign conventions do not prevent scaling. norm = abs(_transverse_power(mode, cell_areas)) if norm <= np.finfo(float).eps: return 0.0 scale = 1.0 / np.sqrt(norm) + + # Apply one scalar to all six components to preserve field ratios. for component in mode.components(): component *= scale return abs(_transverse_power(mode, cell_areas)) @@ -807,11 +946,13 @@ def _normalize_to_unit_power(mode: _ModeFields, cell_areas: np.ndarray) -> float def _transverse_power(mode: _ModeFields, cell_areas: np.ndarray) -> complex: """Compute conjugated transverse power flux.""" + # For local z propagation, transverse flux is (E x H*) . z. return complex(np.sum((mode.ex * np.conj(mode.hy) - mode.ey * np.conj(mode.hx)) * cell_areas)) def _lorentz_overlap(left: _ModeFields, right: _ModeFields, cell_areas: np.ndarray) -> complex: """Compute the unconjugated reciprocal-product overlap.""" + # Symmetrize the unconjugated cross product to match reciprocal-mode theory. left_cross_right = np.sum((left.ex * right.hy - left.ey * right.hx) * cell_areas) right_cross_left = np.sum((right.ex * left.hy - right.ey * left.hx) * cell_areas) return complex(0.5 * (left_cross_right + right_cross_left)) @@ -819,12 +960,16 @@ def _lorentz_overlap(left: _ModeFields, right: _ModeFields, cell_areas: np.ndarr def _apply_dominant_e_phase_convention(mode: _ModeFields) -> None: """Rotate a mode so its largest electric component is real-positive.""" + # Choose a robust phase anchor from the largest electric-field entry. electric = np.concatenate((mode.ex, mode.ey, mode.ez)) if electric.size == 0: return anchor = electric[int(np.argmax(np.abs(electric) ** 2))] if abs(anchor) <= np.finfo(float).eps: return + + # Multiplying by conj(anchor)/|anchor| rotates the anchor onto the positive + # real axis without changing norms. phase = np.conj(anchor) / abs(anchor) for component in mode.components(): component *= phase diff --git a/python/micromode/sweep.py b/python/micromode/sweep.py index b862582..3bcb1da 100644 --- a/python/micromode/sweep.py +++ b/python/micromode/sweep.py @@ -21,6 +21,8 @@ class Sweep: def __post_init__(self) -> None: """Validate sweep lengths and mode-count consistency.""" + # Sweep values become a fixed one-dimensional float axis for all summary + # arrays and data-frame exports. values = np.asarray(self.values, dtype=float) if values.ndim != 1: raise ValueError("values must be one-dimensional") @@ -28,6 +30,9 @@ def __post_init__(self) -> None: raise ValueError("values and results must have the same length") if not self.results: raise ValueError("at least one result is required") + + # Mode tracking and stacked metrics assume every sweep step exposes the + # same number of modes. mode_counts = {int(result.n_complex.sizes["mode_index"]) for result in self.results} if len(mode_counts) != 1: raise ValueError("all sweep results must have the same number of modes") @@ -42,16 +47,22 @@ def num_modes(self) -> int: @property def n_eff(self) -> np.ndarray: """Return real effective indices arranged by sweep step and mode.""" + # Each Result stores one or more frequencies; sweep helpers currently + # summarize the first frequency for every step. return np.vstack([np.asarray(result.n_eff.values)[0] for result in self.results]) @property def n_complex(self) -> np.ndarray: """Return complex effective indices arranged by sweep step and mode.""" + # Preserve the imaginary part here so loss/k_eff can be derived without + # re-reading individual Result objects. return np.vstack([np.asarray(result.n_complex.values)[0] for result in self.results]) @property def pol_fraction(self) -> dict[str, np.ndarray]: """Return TE/TM fractions arranged by sweep step and mode.""" + # Keep TE and TM arrays parallel so callers can index by [step, mode] + # without touching xarray internals. return { "te": np.vstack([np.asarray(result.pol_fraction["te"].values)[0] for result in self.results]), "tm": np.vstack([np.asarray(result.pol_fraction["tm"].values)[0] for result in self.results]), @@ -70,6 +81,8 @@ def to_dataframe(self): import pandas as pd + # Precompute stacked arrays once; the nested loop below only packages + # scalar values into tabular records. rows = [] pol = self.pol_fraction wg_pol = self.pol_fraction_waveguide @@ -106,6 +119,9 @@ def track_modes_by_overlap( tracked = tuple(results) if not tracked: return () + + # The exhaustive assignment search is factorial, so keep it explicitly + # limited to small mode sets where it is predictable and readable. mode_count = int(tracked[0].n_complex.sizes["mode_index"]) if mode_count > 8: raise ValueError("exhaustive overlap tracking is limited to at most 8 modes") @@ -113,6 +129,9 @@ def track_modes_by_overlap( for result in tracked[1:]: if int(result.n_complex.sizes["mode_index"]) != mode_count: raise ValueError("all results must have the same number of modes") + + # Compare the next raw result against the previously tracked result, then + # choose the permutation with the largest total normalized overlap. overlaps = np.abs(reordered[-1].overlap_matrix(result, kind=kind).values) best_order = max(permutations(range(mode_count)), key=lambda order: _assignment_score(overlaps, order)) reordered.append(_reorder_result_modes(result, best_order)) @@ -121,17 +140,25 @@ def track_modes_by_overlap( def _assignment_score(overlaps: np.ndarray, order: tuple[int, ...]) -> float: """Score a proposed mode assignment by total overlap magnitude.""" + # order maps tracked mode_index -> source mode_index in the candidate result. return float(sum(overlaps[mode_index, source_index] for mode_index, source_index in enumerate(order))) def _reorder_result_modes(result: Result, order: tuple[int, ...]) -> Result: """Return a result with all mode-indexed arrays reordered together.""" + # Reset mode coordinates after reordering so the output uses dense + # branch-tracking indices instead of the source result's raw order. mode_coord = np.arange(len(order)) n_complex = result.n_complex.isel(mode_index=list(order)).assign_coords(mode_index=mode_coord) + + # Every field component carries the same mode_index dimension and must move + # in lockstep with n_complex. field_components = { name: data_array.isel(mode_index=list(order)).assign_coords(mode_index=mode_coord) for name, data_array in result.field_components.items() } + + # Optional dispersion arrays are mode-indexed too; preserve them when present. n_group = None if result.n_group is not None: n_group = result.n_group.isel(mode_index=list(order)).assign_coords(mode_index=mode_coord)

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 Nc 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