File size: 11,200 Bytes
7885a28 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
"""
Python wrapper for PROPACK
--------------------------
PROPACK is a collection of Fortran routines for iterative computation
of partial SVDs of large matrices or linear operators.
Based on BSD licensed pypropack project:
http://github.com/jakevdp/pypropack
Author: Jake Vanderplas <[email protected]>
PROPACK source is BSD licensed, and available at
http://soi.stanford.edu/~rmunk/PROPACK/
"""
__all__ = ['_svdp']
import numpy as np
from scipy.sparse.linalg import aslinearoperator
from scipy.linalg import LinAlgError
from ._propack import _spropack # type: ignore[attr-defined]
from ._propack import _dpropack # type: ignore[attr-defined]
from ._propack import _cpropack # type: ignore[attr-defined]
from ._propack import _zpropack # type: ignore[attr-defined]
_lansvd_dict = {
'f': _spropack.slansvd,
'd': _dpropack.dlansvd,
'F': _cpropack.clansvd,
'D': _zpropack.zlansvd,
}
_lansvd_irl_dict = {
'f': _spropack.slansvd_irl,
'd': _dpropack.dlansvd_irl,
'F': _cpropack.clansvd_irl,
'D': _zpropack.zlansvd_irl,
}
_which_converter = {
'LM': 'L',
'SM': 'S',
}
class _AProd:
"""
Wrapper class for linear operator
The call signature of the __call__ method matches the callback of
the PROPACK routines.
"""
def __init__(self, A):
try:
self.A = aslinearoperator(A)
except TypeError:
self.A = aslinearoperator(np.asarray(A))
def __call__(self, transa, m, n, x, y, sparm, iparm):
if transa == 'n':
y[:] = self.A.matvec(x)
else:
y[:] = self.A.rmatvec(x)
@property
def shape(self):
return self.A.shape
@property
def dtype(self):
try:
return self.A.dtype
except AttributeError:
return self.A.matvec(np.zeros(self.A.shape[1])).dtype
def _svdp(A, k, which='LM', irl_mode=True, kmax=None,
compute_u=True, compute_v=True, v0=None, full_output=False, tol=0,
delta=None, eta=None, anorm=0, cgs=False, elr=True,
min_relgap=0.002, shifts=None, maxiter=None, rng=None):
"""
Compute the singular value decomposition of a linear operator using PROPACK
Parameters
----------
A : array_like, sparse matrix, or LinearOperator
Operator for which SVD will be computed. If `A` is a LinearOperator
object, it must define both ``matvec`` and ``rmatvec`` methods.
k : int
Number of singular values/vectors to compute
which : {"LM", "SM"}
Which singular triplets to compute:
- 'LM': compute triplets corresponding to the `k` largest singular
values
- 'SM': compute triplets corresponding to the `k` smallest singular
values
`which='SM'` requires `irl_mode=True`. Computes largest singular
values by default.
irl_mode : bool, optional
If `True`, then compute SVD using IRL (implicitly restarted Lanczos)
mode. Default is `True`.
kmax : int, optional
Maximal number of iterations / maximal dimension of the Krylov
subspace. Default is ``10 * k``.
compute_u : bool, optional
If `True` (default) then compute left singular vectors, `u`.
compute_v : bool, optional
If `True` (default) then compute right singular vectors, `v`.
tol : float, optional
The desired relative accuracy for computed singular values.
If not specified, it will be set based on machine precision.
v0 : array_like, optional
Starting vector for iterations: must be of length ``A.shape[0]``.
If not specified, PROPACK will generate a starting vector.
full_output : bool, optional
If `True`, then return sigma_bound. Default is `False`.
delta : float, optional
Level of orthogonality to maintain between Lanczos vectors.
Default is set based on machine precision.
eta : float, optional
Orthogonality cutoff. During reorthogonalization, vectors with
component larger than `eta` along the Lanczos vector will be purged.
Default is set based on machine precision.
anorm : float, optional
Estimate of ``||A||``. Default is ``0``.
cgs : bool, optional
If `True`, reorthogonalization is done using classical Gram-Schmidt.
If `False` (default), it is done using modified Gram-Schmidt.
elr : bool, optional
If `True` (default), then extended local orthogonality is enforced
when obtaining singular vectors.
min_relgap : float, optional
The smallest relative gap allowed between any shift in IRL mode.
Default is ``0.001``. Accessed only if ``irl_mode=True``.
shifts : int, optional
Number of shifts per restart in IRL mode. Default is determined
to satisfy ``k <= min(kmax-shifts, m, n)``. Must be
>= 0, but choosing 0 might lead to performance degradation.
Accessed only if ``irl_mode=True``.
maxiter : int, optional
Maximum number of restarts in IRL mode. Default is ``1000``.
Accessed only if ``irl_mode=True``.
rng : `numpy.random.Generator`, optional
Pseudorandom number generator state. When `rng` is None, a new
`numpy.random.Generator` is created using entropy from the
operating system. Types other than `numpy.random.Generator` are
passed to `numpy.random.default_rng` to instantiate a ``Generator``.
Returns
-------
u : ndarray
The `k` largest (``which="LM"``) or smallest (``which="SM"``) left
singular vectors, ``shape == (A.shape[0], 3)``, returned only if
``compute_u=True``.
sigma : ndarray
The top `k` singular values, ``shape == (k,)``
vt : ndarray
The `k` largest (``which="LM"``) or smallest (``which="SM"``) right
singular vectors, ``shape == (3, A.shape[1])``, returned only if
``compute_v=True``.
sigma_bound : ndarray
the error bounds on the singular values sigma, returned only if
``full_output=True``.
"""
if rng is None:
raise ValueError("`rng` must be a normalized numpy.random.Generator instance")
which = which.upper()
if which not in {'LM', 'SM'}:
raise ValueError("`which` must be either 'LM' or 'SM'")
if not irl_mode and which == 'SM':
raise ValueError("`which`='SM' requires irl_mode=True")
aprod = _AProd(A)
typ = aprod.dtype.char
try:
lansvd_irl = _lansvd_irl_dict[typ]
lansvd = _lansvd_dict[typ]
except KeyError:
# work with non-supported types using native system precision
if np.iscomplexobj(np.empty(0, dtype=typ)):
typ = np.dtype(complex).char
else:
typ = np.dtype(float).char
lansvd_irl = _lansvd_irl_dict[typ]
lansvd = _lansvd_dict[typ]
m, n = aprod.shape
if (k < 1) or (k > min(m, n)):
raise ValueError("k must be positive and not greater than m or n")
if kmax is None:
kmax = 10*k
if maxiter is None:
maxiter = 1000
# guard against unnecessarily large kmax
kmax = min(m + 1, n + 1, kmax)
if kmax < k:
raise ValueError(
"kmax must be greater than or equal to k, "
f"but kmax ({kmax}) < k ({k})")
# convert python args to fortran args
jobu = 'y' if compute_u else 'n'
jobv = 'y' if compute_v else 'n'
# these will be the output arrays
u = np.zeros((m, kmax + 1), order='F', dtype=typ)
v = np.zeros((n, kmax), order='F', dtype=typ)
# Specify the starting vector. if v0 is all zero, PROPACK will generate
# a random starting vector: the random seed cannot be controlled in that
# case, so we'll instead use numpy to generate a random vector
if v0 is None:
u[:, 0] = rng.uniform(size=m)
if np.iscomplexobj(np.empty(0, dtype=typ)): # complex type
u[:, 0] += 1j * rng.uniform(size=m)
else:
try:
u[:, 0] = v0
except ValueError:
raise ValueError(f"v0 must be of length {m}")
# process options for the fit
if delta is None:
delta = np.sqrt(np.finfo(typ).eps)
if eta is None:
eta = np.finfo(typ).eps ** 0.75
if irl_mode:
doption = np.array((delta, eta, anorm, min_relgap), dtype=typ.lower())
# validate or find default shifts
if shifts is None:
shifts = kmax - k
if k > min(kmax - shifts, m, n):
raise ValueError('shifts must satisfy '
'k <= min(kmax-shifts, m, n)!')
elif shifts < 0:
raise ValueError('shifts must be >= 0!')
else:
doption = np.array((delta, eta, anorm), dtype=typ.lower())
ioption = np.array((int(bool(cgs)), int(bool(elr))), dtype='i')
# If computing `u` or `v` (left and right singular vectors,
# respectively), `blocksize` controls how large a fraction of the
# work is done via fast BLAS level 3 operations. A larger blocksize
# may lead to faster computation at the expense of greater memory
# consumption. `blocksize` must be ``>= 1``. Choosing blocksize
# of 16, but docs don't specify; it's almost surely a
# power of 2.
blocksize = 16
# Determine lwork & liwork:
# the required lengths are specified in the PROPACK documentation
if compute_u or compute_v:
lwork = m + n + 9*kmax + 5*kmax*kmax + 4 + max(
3*kmax*kmax + 4*kmax + 4,
blocksize*max(m, n))
liwork = 8*kmax
else:
lwork = m + n + 9*kmax + 2*kmax*kmax + 4 + max(m + n, 4*kmax + 4)
liwork = 2*kmax + 1
work = np.empty(lwork, dtype=typ.lower())
iwork = np.empty(liwork, dtype=np.int32)
# dummy arguments: these are passed to aprod, and not used in this wrapper
dparm = np.empty(1, dtype=typ.lower())
iparm = np.empty(1, dtype=np.int32)
if typ.isupper():
# PROPACK documentation is unclear on the required length of zwork.
# Use the same length Julia's wrapper uses
# see https://github.com/JuliaSmoothOptimizers/PROPACK.jl/
zwork = np.empty(m + n + 32*m, dtype=typ)
works = work, zwork, iwork
else:
works = work, iwork
if irl_mode:
u, sigma, bnd, v, info = lansvd_irl(_which_converter[which], jobu,
jobv, m, n, shifts, k, maxiter,
aprod, u, v, tol, *works, doption,
ioption, dparm, iparm)
else:
u, sigma, bnd, v, info = lansvd(jobu, jobv, m, n, k, aprod, u, v, tol,
*works, doption, ioption, dparm, iparm)
if info > 0:
raise LinAlgError(
f"An invariant subspace of dimension {info} was found.")
elif info < 0:
raise LinAlgError(
f"k={k} singular triplets did not converge within "
f"kmax={kmax} iterations")
# info == 0: The K largest (or smallest) singular triplets were computed
# successfully!
return u[:, :k], sigma, v[:, :k].conj().T, bnd
|