meanas/meanas/fdfd/solvers.py

154 lines
4.7 KiB
Python
Raw Normal View History

"""
Solvers and solver interface for FDFD problems.
"""
2024-07-29 00:29:39 -07:00
from typing import Any
from collections.abc import Callable
import logging
2022-10-04 14:32:40 -07:00
import numpy
from numpy.typing import ArrayLike, NDArray
from numpy.linalg import norm
import scipy.sparse.linalg
from ..fdmath import dx_lists_t, vfdfield, vcfdfield, vcfdfield_t
from . import operators
logger = logging.getLogger(__name__)
2022-10-04 14:32:40 -07:00
def _scipy_qmr(
A: scipy.sparse.csr_array,
2022-10-04 14:32:40 -07:00
b: ArrayLike,
**kwargs: Any,
) -> NDArray[numpy.float64]:
"""
Wrapper for scipy.sparse.linalg.qmr
Args:
A: Sparse matrix
b: Right-hand-side vector
kwargs: Passed as **kwargs to the wrapped function
Returns:
Converged solution. Raises RuntimeError on breakdown or nonconvergence.
"""
2025-01-07 00:04:01 -08:00
#
#Report on our progress
#
ii = 0
2022-10-04 14:32:40 -07:00
def log_residual(xk: ArrayLike) -> None:
nonlocal ii
ii += 1
if ii % 100 == 0:
cur_norm = norm(A @ xk - b) / norm(b)
2024-07-17 23:15:34 -07:00
logger.info(f'Solver residual at iteration {ii} : {cur_norm}')
if 'callback' in kwargs:
2026-04-17 21:34:36 -07:00
callback = kwargs['callback']
2022-10-04 14:32:40 -07:00
def augmented_callback(xk: ArrayLike) -> None:
log_residual(xk)
2026-04-17 21:34:36 -07:00
callback(xk)
kwargs['callback'] = augmented_callback
else:
kwargs['callback'] = log_residual
2025-01-07 00:04:01 -08:00
#
# Run the actual solve
#
x, info = scipy.sparse.linalg.qmr(A, b, **kwargs)
if info != 0:
residual = norm(A @ x - b)
raise RuntimeError(f'QMR failed to converge ({info=}, residual={residual:.6g})')
return x
2022-10-04 14:32:40 -07:00
def generic(
omega: complex,
dxes: dx_lists_t,
J: vcfdfield,
epsilon: vfdfield,
mu: vfdfield | None = None,
*,
pec: vfdfield | None = None,
pmc: vfdfield | None = None,
2022-10-04 14:32:40 -07:00
adjoint: bool = False,
matrix_solver: Callable[..., ArrayLike] = _scipy_qmr,
2024-07-29 00:29:39 -07:00
matrix_solver_opts: dict[str, Any] | None = None,
E_guess: vcfdfield | None = None,
) -> vcfdfield_t:
"""
Preconditioned FDFD solver using CSR sparse matrices.
All ndarray arguments should be 1D arrays, as returned by `meanas.fdmath.vectorization.vec()`.
Args:
omega: Complex frequency to solve at.
dxes: `[[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]]` (complex cell sizes) as
2019-11-27 22:59:52 -08:00
discussed in `meanas.fdmath.types`
J: Electric current distribution (at E-field locations)
epsilon: Dielectric constant distribution (at E-field locations)
mu: Magnetic permeability distribution (at H-field locations)
pec: Perfect electric conductor distribution
(at E-field locations; non-zero value indicates PEC is present).
Sources and initial guesses are zeroed at PEC locations.
pmc: Perfect magnetic conductor distribution
(at H-field locations; non-zero value indicates PMC is present)
adjoint: If true, solves the adjoint problem.
matrix_solver: Called as `matrix_solver(A, b, **matrix_solver_opts) -> x`,
where `A`: `scipy.sparse.csr_array`;
2022-10-04 14:32:40 -07:00
`b`: `ArrayLike`;
`x`: `ArrayLike`;
Default is a wrapped version of `scipy.sparse.linalg.qmr()`
which raises on nonconvergence and logs the residual every
100 iterations.
matrix_solver_opts: Passed as kwargs to `matrix_solver(...)`
E_guess: Guess at the solution E-field. `matrix_solver` must accept an
`x0` argument with the same purpose.
Returns:
E-field which solves the system.
"""
matrix_solver_opts = {} if matrix_solver_opts is None else dict(matrix_solver_opts)
b0 = -1j * omega * J
if pec is not None:
b0 = numpy.where(pec, 0, b0)
A0 = operators.e_full(omega, dxes, epsilon=epsilon, mu=mu, pec=pec, pmc=pmc)
Pl, Pr = operators.e_full_preconditioners(dxes)
if adjoint:
2026-04-17 21:34:36 -07:00
A = (Pl @ A0 @ Pr).T.conjugate()
b = Pr.T.conjugate() @ b0
else:
A = Pl @ A0 @ Pr
b = Pl @ b0
if E_guess is not None:
if pec is not None:
E_guess = numpy.where(pec, 0, E_guess)
if adjoint:
2026-04-17 21:34:36 -07:00
x0 = Pr.T.conjugate() @ E_guess
else:
x0 = Pl @ E_guess
matrix_solver_opts['x0'] = x0
x = numpy.asarray(matrix_solver(A.tocsr(), b, **matrix_solver_opts))
if x.shape != b.shape or not numpy.isfinite(x).all():
raise RuntimeError('matrix_solver must return a finite solution vector matching the source shape')
if adjoint:
2026-04-17 21:34:36 -07:00
x0 = Pl.T.conjugate() @ x
else:
x0 = Pr @ x
if pec is not None:
x0 = numpy.where(pec, 0, x0)
return vcfdfield_t(x0)