Skip to content

Commit fd18698

Browse files
committed
test: added more tests to eigs/cond
1 parent 770b187 commit fd18698

2 files changed

Lines changed: 121 additions & 10 deletions

File tree

‎pylops/linearoperator.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,8 @@ def eigs(
10551055
niter : :obj:`int`, optional
10561056
Number of iterations for eigenvalue estimation
10571057
uselobpcg : :obj:`bool`, optional
1058-
Use :func:`scipy.sparse.linalg.lobpcg`
1058+
Use :func:`scipy.sparse.linalg.lobpcg`. Note that this method
1059+
is only available for symmetric operators.
10591060
backend : :obj:`str`, optional
10601061
.. versionadded:: 2.9.0
10611062
@@ -1141,13 +1142,13 @@ def _check_lobpcg_size(n: int, neigs: int, Y: NDArray | None = None) -> None:
11411142
# All eigenvalues
11421143
eigenvalues = get_eigvals(ncp.ones(1))(self.A)
11431144
else:
1145+
# Limited number of eigenvalues
11441146
if not symmetric and uselobpcg:
11451147
msg = (
11461148
"Cannot use scipy.sparse.linalg.lobpcg "
11471149
"for non real-symmetric or complex-hermitian matrices..."
11481150
)
11491151
raise ValueError(msg)
1150-
# Limited number of eigenvalues
11511152
if symmetric and uselobpcg:
11521153
X = ncp.random.rand(self.shape[0], neigs).astype(self.dtype)
11531154
eigenvalues = get_lobpcg(ncp.ones(1))(
@@ -1264,8 +1265,8 @@ def cond(
12641265
12651266
.. note::
12661267
If ``backend="cupy"``, the condition number can only be computed
1267-
with the ``uselobpcg=True`` option (and therefore for symmetric
1268-
operators).
1268+
with the ``uselobpcg=True`` option (and therefore for square,
1269+
symmetric operators only).
12691270
12701271
Parameters
12711272
----------
@@ -1274,7 +1275,7 @@ def cond(
12741275
backend : :obj:`str`, optional
12751276
.. versionadded:: 2.9.0
12761277
1277-
Backend to use (`numpy` or `cupy`)
1278+
Backend to use (`numpy` or `cupy`).
12781279
**kwargs_eig
12791280
Arbitrary keyword arguments for :func:`scipy.sparse.linalg.eigs`,
12801281
:func:`scipy.sparse.linalg.eigsh`, or

‎pytests/test_linearoperator.py‎

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,14 @@ def test_eigs_lobpcg_failures(par):
147147
when neigs is too large to avoid lobpcg dense solver
148148
"""
149149
nx = 10 * par["nx"]
150-
Op = Diagonal(np.arange(nx, 0, -1), dtype=par["dtype"])
151150

152-
# lobpcg cannot be used for non-symmetric operators
151+
# lobpcg cannot be used for non-symmetric explicit operators
152+
Op = MatrixMult(np.diag(np.arange(nx, 0.0, -1.0)), dtype=np.dtype(par["dtype"]))
153+
with pytest.raises(ValueError, match="non real-symmetric or complex-hermitian"):
154+
Op.eigs(neigs=2, symmetric=False, uselobpcg=True, backend=backend)
155+
156+
# lobpcg cannot be used for non-symmetric linear operators
157+
Op = Diagonal(np.arange(nx, 0, -1), dtype=par["dtype"])
153158
with pytest.raises(ValueError, match="non real-symmetric or complex-hermitian"):
154159
Op.eigs(neigs=2, symmetric=False, uselobpcg=True, backend=backend)
155160

@@ -166,6 +171,36 @@ def test_eigs_lobpcg_failures(par):
166171
backend=backend,
167172
)
168173

174+
# lobpcg size check for non-square operators (based on number of columns)
175+
Op = VStack([Op, Zero(par["ny"], nx)])
176+
with pytest.raises(ValueError, match="uselobpcg=True requires the operator size"):
177+
Op.eigs(neigs=nx // 5 + 1, uselobpcg=True, backend=backend)
178+
with pytest.raises(ValueError, match="uselobpcg=True requires the operator size"):
179+
Op.eigs(
180+
neigs=nx // 5,
181+
uselobpcg=True,
182+
Y=np.eye(nx, 5, dtype=par["dtype"]),
183+
backend=backend,
184+
)
185+
186+
187+
@pytest.mark.skipif(int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 0, reason="CuPy only")
188+
@pytest.mark.parametrize("par", [(par1), (par1j)])
189+
def test_eigs_cupy_nonsymmetric_failures(par):
190+
"""Fail non-symmetric square operators with CuPy backend"""
191+
nx = 10 * par["nx"]
192+
diag = np.arange(nx, 0.0, -1.0)
193+
194+
# explicit=True (limited number of eigenvalues)
195+
Op = MatrixMult(np.diag(diag), dtype=np.dtype(par["dtype"]))
196+
with pytest.raises(NotImplementedError, match="not available with CuPy"):
197+
Op.eigs(neigs=3, symmetric=False, backend="cupy")
198+
199+
# explicit=False
200+
Op = Diagonal(diag, dtype=np.dtype(par["dtype"]))
201+
with pytest.raises(NotImplementedError, match="not available with CuPy"):
202+
Op.eigs(neigs=3, symmetric=False, backend="cupy")
203+
169204

170205
@pytest.mark.parametrize("par", [(par1), (par1j)])
171206
@pytest.mark.parametrize("symmetric", [True, False])
@@ -190,25 +225,43 @@ def test_eigs_square(par, symmetric):
190225
Op = MatrixMult(np.diag(diag), dtype=np.dtype(par["dtype"]))
191226

192227
eigs = Op.eigs(symmetric=symmetric, backend=backend) # all
193-
assert_array_almost_equal(eigs, diag[: eigs.size], decimal=3)
228+
assert_array_almost_equal(eigs, diag, decimal=3)
194229

195230
eigs = Op.eigs(neigs=3, symmetric=symmetric, backend=backend) # top-k
196231
assert_array_almost_equal(eigs, diag[:3], decimal=3)
197232

233+
# neigs equal to size of operator returns all eigenvalues
234+
eigs = Op.eigs(neigs=nx, symmetric=symmetric, backend=backend)
235+
assert_array_almost_equal(eigs, diag, decimal=3)
236+
198237
if backend == "numpy":
199238
# CuPy cannot compute a single eigenvalue for square, non-symmetric matrix
200-
cond = Op.cond(symmetric=symmetric, backend=backend)
239+
cond = Op.cond(backend=backend)
201240
assert_array_almost_equal(np.real(cond), nx, decimal=3)
202241

242+
if symmetric:
243+
# lobpcg
244+
eigs = Op.eigs(
245+
neigs=3, symmetric=True, uselobpcg=True, niter=50, backend=backend
246+
)
247+
assert_array_almost_equal(eigs, diag[:3], decimal=3)
248+
203249
# explicit=False
204250
Op = Diagonal(diag, dtype=np.dtype(par["dtype"]))
205251

206252
if backend == "numpy":
207253
# CuPy cannot compute neigs-2 eigenvalues (seems to be
208254
# a bug in CuPy's eigsh)
209255
eigs = Op.eigs(symmetric=symmetric, backend=backend)
256+
assert eigs.size == nx - 2 # neigs=None is clamped to N-2 (no warning)
210257
assert_array_almost_equal(eigs, diag[: eigs.size], decimal=3)
211258

259+
# neigs too large is clamped to N-2 (with warning)
260+
with pytest.warns(UserWarning, match="exceeds the number of columns"):
261+
eigs = Op.eigs(neigs=nx, symmetric=symmetric, backend=backend)
262+
assert eigs.size == nx - 2
263+
assert_array_almost_equal(eigs, diag[: nx - 2], decimal=3)
264+
212265
eigs = Op.eigs(neigs=3, symmetric=symmetric, backend=backend) # top-k
213266
assert_array_almost_equal(eigs, diag[:3], decimal=3)
214267

@@ -218,8 +271,8 @@ def test_eigs_square(par, symmetric):
218271
cond = Op.cond(backend=backend)
219272
assert_array_almost_equal(np.real(cond), nx, decimal=3)
220273

221-
# use lobpcg for symmetric case
222274
if symmetric:
275+
# lobpcg
223276
eigs1 = Op.eigs(
224277
neigs=3, symmetric=True, uselobpcg=True, niter=50, backend=backend
225278
)
@@ -229,6 +282,53 @@ def test_eigs_square(par, symmetric):
229282
assert_array_almost_equal(np.real(cond), nx, decimal=3)
230283

231284

285+
@pytest.mark.parametrize("par", [(par1j)])
286+
@pytest.mark.parametrize("explicit", [True, False])
287+
def test_eigs_square_hermitian(par, explicit):
288+
"""Eigenvalues and conditioning number estimate of complex-valued
289+
hermitian matrix with known (real-valued) eigenvalues
290+
"""
291+
nx = 5 * par["nx"]
292+
293+
# hermitian matrix A = U diag(eigs) U^H with U unitary
294+
np.random.seed(0)
295+
U, _ = np.linalg.qr(
296+
np.random.normal(size=(nx, nx)) + 1j * np.random.normal(size=(nx, nx))
297+
)
298+
eigs_true = np.arange(nx, 0.0, -1.0)
299+
A = (U * eigs_true) @ np.conj(U.T)
300+
A = (A + np.conj(A.T)) / 2 # remove round-off asymmetry
301+
Op = MatrixMult(A, dtype=np.dtype(par["dtype"]))
302+
Op.explicit = explicit
303+
304+
# hermitian operator
305+
assert_array_almost_equal(Op.H.todense(), Op.todense(), decimal=6)
306+
307+
# hermitian solver
308+
if explicit:
309+
eigs = Op.eigs(symmetric=True, backend=backend) # all
310+
assert_array_almost_equal(eigs, eigs_true, decimal=3)
311+
312+
eigs = Op.eigs(neigs=3, symmetric=True, backend=backend) # top-k
313+
assert_array_almost_equal(eigs, eigs_true[:3], decimal=3)
314+
315+
cond = Op.cond(symmetric=True, backend=backend)
316+
assert_array_almost_equal(np.real(cond), nx, decimal=3)
317+
318+
# lobpcg
319+
eigs = Op.eigs(neigs=3, symmetric=True, uselobpcg=True, niter=100, backend=backend)
320+
assert_array_almost_equal(eigs, eigs_true[:3], decimal=3)
321+
322+
cond = Op.cond(uselobpcg=True, niter=200, backend=backend)
323+
assert_array_almost_equal(np.real(cond), nx, decimal=3)
324+
325+
# general solver (not available for CuPy)
326+
if backend == "numpy":
327+
eigs = Op.eigs(neigs=3, symmetric=False, backend=backend)
328+
assert_array_almost_equal(np.real(eigs), eigs_true[:3], decimal=3)
329+
assert_array_almost_equal(np.imag(eigs), np.zeros(3), decimal=3)
330+
331+
232332
@pytest.mark.parametrize("par", [(par2), (par2j)])
233333
def test_eigs_non_square(par):
234334
"""Eigenvalues and conditioning number estimate of non-square matrix"""
@@ -248,6 +348,9 @@ def test_eigs_non_square(par):
248348
eigs = Op.eigs(neigs=3, backend=backend) # top-k
249349
assert_array_almost_equal(eigs, np.abs(diag)[:3], decimal=3)
250350

351+
eigs = Op.eigs(neigs=2, uselobpcg=True, niter=100, backend=backend) # lobpcg
352+
assert_array_almost_equal(eigs, np.abs(diag)[:2], decimal=3)
353+
251354
if backend == "numpy":
252355
# CuPy cannot compute a single eigenvalue for square,
253356
# non-symmetric matrix
@@ -273,6 +376,13 @@ def test_eigs_non_square(par):
273376
cond = Op.cond(backend=backend)
274377
assert_array_almost_equal(np.real(cond), nx, decimal=3)
275378

379+
# use lobpcg
380+
eigs = Op.eigs(neigs=2, uselobpcg=True, niter=100, backend=backend)
381+
assert_array_almost_equal(eigs, np.abs(diag)[:2], decimal=3)
382+
383+
cond = Op.cond(uselobpcg=True, niter=200, backend=backend)
384+
assert_array_almost_equal(np.real(cond), nx, decimal=3)
385+
276386

277387
@pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)])
278388
def test_conj(par):

0 commit comments

Comments
 (0)