Source code for LION.classical_algorithms.spgl1_torch

"""SPGL1 sparse reconstruction with torch operators."""

import numpy as np
import torch
from scipy.sparse.linalg import LinearOperator
from spgl1 import spgl1

from LION.operators.Operator import Operator


class _RealLinearOperator(LinearOperator):
    """Linear operator with stable NumPy real-valued introspection.

    SPGL1 0.0.3 calls ``np.isreal(A).all()``.  Some NumPy/SciPy combinations
    return a Python ``bool`` for an abstract ``LinearOperator``, which lacks
    ``.all``.  A scalar array representation keeps that dependency check
    version-independent without materialising the operator matrix.
    """

    def __init__(self, *, shape, matvec, rmatvec, dtype):
        super().__init__(dtype=np.dtype(dtype), shape=shape)
        self._matvec_callback = matvec
        self._rmatvec_callback = rmatvec

    def _matvec(self, value):
        return self._matvec_callback(value)

    def _rmatvec(self, value):
        return self._rmatvec_callback(value)

    def __array__(self, dtype=None, copy=None):
        del copy
        return np.asarray(0.0, dtype=dtype or self.dtype)


[docs] def spgl1_torch(op: Operator, y: torch.Tensor, **spgl1_kwargs) -> torch.Tensor: r"""Solve an l1 sparse reconstruction using SPGL1, wrapping torch operators. This is a thin wrapper around the Python SPGL1 solver ``spgl1.spgl1`` that uses torch operators for matrix-vector products. SPGL1 is a spectral projected-gradient method for constrained l1 problems; see [BergFriedlander2008]_ and [BergFriedlander2010]_. This wrapper is built on top of the Python implementation ``spgl1.spgl1`` and uses the same calling convention (argument names and behaviour); see [SPGL1Python]_ for details. Parameters ---------- op : Operator Linear operator implementing the forward map and its adjoint. It is called as ``op(w)`` and ``op.adjoint(r)``. y : torch.Tensor Measurements, shape ``(M,)``. spgl1_kwargs : dict Extra keyword args forwarded to ``spgl1.spgl1`` (for example tolerances or iteration limits; see [SPGL1Python]_). Returns ------- w_hat : torch.Tensor Estimated coefficient vector in the same shape as ``op.adjoint(y*0)``. References ---------- .. [BergFriedlander2008] E. van den Berg and M. P. Friedlander, "Probing the Pareto frontier for basis pursuit solutions", SIAM Journal on Scientific Computing, 31(2):890-912, 2008. .. [BergFriedlander2010] E. van den Berg and M. P. Friedlander, "Sparse optimisation with least-squares constraints", TR-2010-02, Department of Computer Science, University of British Columbia, 2010. .. [SPGL1Python] SPGL1: Spectral Projected Gradient for L1 minimisation, Python package documentation, https://spgl1.readthedocs.io/ """ device = y.device y = y.detach() # Infer coefficient shape from one adjoint call with torch.no_grad(): w0 = op.adjoint(torch.zeros_like(y)) w0 = w0.detach() n_w = w0.numel() n_y = y.numel() def matvec(w_np: np.ndarray) -> np.ndarray: w_t = torch.from_numpy(w_np.astype(np.float32)).to(device).view_as(w0) y_t = op(w_t) return y_t.detach().cpu().numpy().ravel() def rmatvec(r_np: np.ndarray) -> np.ndarray: r_t = torch.from_numpy(r_np.astype(np.float32)).to(device).view_as(y) g_t = op.adjoint(r_t) return g_t.detach().cpu().numpy().ravel() A_linop = _RealLinearOperator( shape=(n_y, n_w), matvec=matvec, rmatvec=rmatvec, dtype=np.float32, ) y_np = y.detach().cpu().numpy().ravel() x0_np = np.zeros(n_w, dtype=np.float32) x_np, _, _, _ = spgl1(A_linop, y_np, x0=x0_np, **spgl1_kwargs) w_hat = torch.from_numpy(x_np.astype(np.float32)).to(device).view_as(w0) return w_hat