Source code for spacecore.linalg._lsqr

from __future__ import annotations

from typing import Any

from ..linop import LinOp
from ._utils import DEFAULT_CONVERGENCE_CHECK_INTERVAL, check_interval
from ._utils import LinalgResult
from ._utils import check_maxiter, is_converged, require_linop
from ._utils import safe_inverse_nonneg, should_check_iteration, threshold


[docs] def lsqr( A: LinOp, b: Any, *, x0: Any | None = None, tol: float = 1e-6, atol: float = 0.0, maxiter: int | None = None, check_every: int = DEFAULT_CONVERGENCE_CHECK_INTERVAL, residual_mode: str = "exact", ) -> LinalgResult: r""" Solve :math:`\min_x \|A x - b\|` by LSQR. ``A`` may map between different spaces. Only ``A.apply`` and ``A.H.apply`` are used; the normal equations are never formed. Parameters ---------- A : LinOp Operator from ``A.domain`` to ``A.codomain``. b : array-like Right-hand side in ``A.codomain``. x0 : array-like or None, optional Initial guess; zero by default. tol : float, optional Converged when ``norm(A.H @ (A @ x - b))`` is below ``atol + tol * norm(b)``. atol : float, optional Absolute tolerance on that normal-equation residual. maxiter : int or None, optional Iteration cap; ``prod(A.domain.shape)`` by default. check_every : int, optional Refresh residual diagnostics every this many iterations, and on the last one. residual_mode : {"exact", "recurrence"}, optional ``"exact"`` recomputes both residuals at each check, costing one extra forward and adjoint apply; ``"recurrence"`` uses LSQR's scalar estimates for free. Returns ------- LinalgResult With ``normal_residual_norm`` set. Raises ------ TypeError If ``A`` is not a :class:`LinOp`. ValueError If an iteration parameter or ``residual_mode`` is invalid. See Also -------- cg : Square Hermitian positive-definite systems. Notes ----- JIT-compatible on JAX with ``maxiter``, ``check_every`` and ``residual_mode`` static. A small normal-equation residual does not imply a small error when ``A`` is ill-conditioned: the error solves ``A.H A e = A.H (A x - b)``. Golub & Van Loan, *Matrix Computations*, 4th ed., Sec. 11.4.2. Orthonormal bases in both spaces give ordinary LSQR; complex spaces replace transposes by conjugate transposes in the bidiagonalization. Examples -------- Solve a small overdetermined least-squares problem. >>> import numpy as np >>> import spacecore as sc >>> ctx = sc.Context(sc.NumpyOps(), dtype=np.float64) >>> X = sc.DenseCoordinateSpace((2,), ctx) >>> Y = sc.DenseCoordinateSpace((3,), ctx) >>> M = ctx.asarray([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]) >>> A = sc.DenseLinOp(M, X, Y, ctx) >>> b = ctx.asarray([1.0, 2.0, 3.0]) >>> result = sc.lsqr(A, b, tol=1e-10) >>> np.allclose(result.x, [1.0, 2.0]) True """ # Validate once, up front. A = require_linop(A) A.codomain.check_member(b) maxiter = check_maxiter(maxiter, A) check_every = check_interval(check_every) if residual_mode not in {"exact", "recurrence"}: raise ValueError("residual_mode must be 'exact' or 'recurrence'.") # Resolve check-free cores once; the hot loop then skips per-iteration # validation while honoring any custom geometry the spaces define. apply = A._apply_core radj = A._rapply_core dom = A.domain cod = A.codomain # Golub-Kahan start: beta u = b - A x and alpha v = A.H u. x = A.domain.zeros() if x0 is None else x0 A.domain.check_member(x) residual = cod._add_core(b, cod._scale_core(-1.0, apply(x))) beta = cod._norm_core(residual) u = cod._scale_core(safe_inverse_nonneg(A.ops, beta), residual) v = radj(u) alpha = dom._norm_core(v) # Initial normal-equation residual ||A.H r||, exact or from the recurrence. if residual_mode == "exact": normal_residual_norm = dom._norm_core(radj(residual)) else: normal_residual_norm = beta * alpha v = dom._scale_core(safe_inverse_nonneg(A.ops, alpha), v) # LSQR state: search direction w and the scalar recurrences phi_bar, rho_bar. w = v phi_bar = beta rho_bar = alpha residual_norm = beta threshold_value = threshold(A.codomain.norm(b), tol, atol) def cond_fun(carry: tuple[Any, ...]) -> Any: _x, _u, _v, _w, _alpha, _beta, _rho_bar, _phi_bar, _res_norm, norm_res, k = carry return (k < maxiter) & (norm_res > threshold_value) def body_fun(carry: tuple[Any, ...]) -> tuple[Any, ...]: x, u, v, w, alpha, _beta, rho_bar, phi_bar, _residual_norm, _normal_residual, k = carry # Bidiagonalization step: next unit vectors u and v. u_next = cod._axpy_core(-alpha, u, apply(v)) beta_next = cod._norm_core(u_next) u_next = cod._scale_core(safe_inverse_nonneg(A.ops, beta_next), u_next) v_next = dom._axpy_core(-beta_next, v, radj(u_next)) alpha_next = dom._norm_core(v_next) v_next = dom._scale_core(safe_inverse_nonneg(A.ops, alpha_next), v_next) # Givens rotation (c, s) that eliminates the new subdiagonal entry. rho = A.ops.sqrt(rho_bar * rho_bar + beta_next * beta_next) inv_rho = safe_inverse_nonneg(A.ops, rho) c = rho_bar * inv_rho s = beta_next * inv_rho theta = s * alpha_next rho_bar_next = -c * alpha_next phi = c * phi_bar phi_bar_next = s * phi_bar # Free residual estimates from the recurrence. recurrence_residual_norm = A.ops.abs(phi_bar_next) recurrence_normal_residual_norm = alpha_next * A.ops.abs(s * phi) # Update the solution and the search direction. x_next = dom._axpy_core(phi * inv_rho, w, x) w_next = dom._axpy_core(-(theta * inv_rho), w, v_next) k_next = k + 1 # Exact mode recomputes the residuals (one forward and one adjoint apply). if residual_mode == "exact": def refresh_residuals(payload: tuple[Any, Any, Any]) -> tuple[Any, Any]: x_candidate, _old_residual_norm, _old_normal_residual = payload residual_next = cod._add_core(apply(x_candidate), cod._scale_core(-1.0, b)) return cod._norm_core(residual_next), dom._norm_core(radj(residual_next)) else: def refresh_residuals(payload: tuple[Any, Any, Any]) -> tuple[Any, Any]: _x_candidate, _old_residual_norm, _old_normal_residual = payload return recurrence_residual_norm, recurrence_normal_residual_norm def keep_residuals(payload: tuple[Any, Any, Any]) -> tuple[Any, Any]: _x_candidate, old_residual_norm, old_normal_residual = payload return old_residual_norm, old_normal_residual # Refresh the reported residuals only on check iterations. residual_norm_next, normal_residual_norm_next = A.ops.cond( should_check_iteration(k_next, maxiter, check_every), refresh_residuals, keep_residuals, (x_next, _residual_norm, _normal_residual), ) return ( x_next, u_next, v_next, w_next, alpha_next, beta_next, rho_bar_next, phi_bar_next, residual_norm_next, normal_residual_norm_next, k_next, ) x, *_rest, residual_norm, normal_residual_norm, num_iters = A.ops.while_loop( cond_fun, body_fun, (x, u, v, w, alpha, beta, rho_bar, phi_bar, residual_norm, normal_residual_norm, 0), ) return LinalgResult( x, is_converged(normal_residual_norm, threshold_value), num_iters, residual_norm, normal_residual_norm=normal_residual_norm, )