Skip to content

Commit 778dc20

Browse files
authored
Merge pull request #116 from PyLops/dottest
feat: added dottest
2 parents 57b793e + b9857e5 commit 778dc20

10 files changed

+198
-6
lines changed

docs/source/api/index.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,22 @@ Basic
104104

105105
cg
106106
cgls
107+
108+
109+
Utils
110+
-----
111+
112+
.. currentmodule:: pylops_mpi.DistributedArray
113+
114+
.. autosummary::
115+
:toctree: generated/
116+
117+
local_split
118+
119+
120+
.. currentmodule:: pylops_mpi.utils.dottest
121+
122+
.. autosummary::
123+
:toctree: generated/
124+
125+
dottest

pylops_mpi/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# isort: skip_file
2+
from .dottest import *

pylops_mpi/utils/dottest.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
__all__ = ["dottest"]
2+
3+
from typing import Optional
4+
5+
import numpy as np
6+
7+
from pylops_mpi.DistributedArray import DistributedArray
8+
from pylops.utils.backend import to_numpy
9+
10+
11+
def dottest(
12+
Op,
13+
u: DistributedArray,
14+
v: DistributedArray,
15+
nr: Optional[int] = None,
16+
nc: Optional[int] = None,
17+
rtol: float = 1e-6,
18+
atol: float = 1e-21,
19+
raiseerror: bool = True,
20+
verb: bool = False,
21+
) -> bool:
22+
r"""Dot test.
23+
24+
Perform dot-test to verify the validity of forward and adjoint
25+
operators using user-provided random vectors :math:`\mathbf{u}`
26+
and :math:`\mathbf{v}` (whose Partition must be consistent with
27+
the operator being tested). This test can help to detect errors
28+
in the operator ximplementation.
29+
30+
Parameters
31+
----------
32+
Op : :obj:`pylops_mpi.LinearOperator`
33+
Linear operator to test.
34+
u : :obj:`pylops_mpi.DistributedArray`
35+
Distributed array of size equal to the number of columns of operator
36+
v : :obj:`pylops_mpi.DistributedArray`
37+
Distributed array of size equal to the number of rows of operator
38+
nr : :obj:`int`
39+
Number of rows of operator (i.e., elements in data)
40+
nc : :obj:`int`
41+
Number of columns of operator (i.e., elements in model)
42+
rtol : :obj:`float`, optional
43+
Relative dottest tolerance
44+
atol : :obj:`float`, optional
45+
Absolute dottest tolerance
46+
.. versionadded:: 2.0.0
47+
raiseerror : :obj:`bool`, optional
48+
Raise error or simply return ``False`` when dottest fails
49+
verb : :obj:`bool`, optional
50+
Verbosity
51+
52+
Returns
53+
-------
54+
passed : :obj:`bool`
55+
Passed flag.
56+
57+
Raises
58+
------
59+
AssertionError
60+
If dot-test is not verified within chosen tolerances.
61+
62+
Notes
63+
-----
64+
A dot-test is mathematical tool used in the development of numerical
65+
linear operators.
66+
67+
More specifically, a correct implementation of forward and adjoint for
68+
a linear operator should verify the following *equality*
69+
within a numerical tolerance:
70+
71+
.. math::
72+
(\mathbf{Op}\,\mathbf{u})^H\mathbf{v} =
73+
\mathbf{u}^H(\mathbf{Op}^H\mathbf{v})
74+
75+
"""
76+
if nr is None:
77+
nr = Op.shape[0]
78+
if nc is None:
79+
nc = Op.shape[1]
80+
81+
if (nr, nc) != Op.shape:
82+
raise AssertionError("Provided nr and nc do not match operator shape")
83+
84+
y = Op.matvec(u) # Op * u
85+
x = Op.rmatvec(v) # Op'* v
86+
87+
yy = np.vdot(y.asarray(), v.asarray()) # (Op * u)' * v
88+
xx = np.vdot(u.asarray(), x.asarray()) # u' * (Op' * v)
89+
90+
# convert back to numpy (in case cupy arrays were used), make into a numpy
91+
# array and extract the first element. This is ugly but allows to handle
92+
# complex numbers in subsequent prints also when using cupy arrays.
93+
xx, yy = np.array([to_numpy(xx)])[0], np.array([to_numpy(yy)])[0]
94+
95+
# evaluate if dot test passed
96+
passed = np.isclose(xx, yy, rtol, atol)
97+
98+
# verbosity or error raising
99+
if (not passed and raiseerror) or verb:
100+
passed_status = "passed" if passed else "failed"
101+
msg = f"Dot test {passed_status}, v^H(Opu)={yy} - u^H(Op^Hv)={xx}"
102+
if not passed and raiseerror:
103+
raise AssertionError(msg)
104+
else:
105+
print(msg)
106+
107+
return passed

tests/test_blockdiag.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
"""Test the MPIBlockDiag and MPIStackedBlockDiag classes
2+
Designed to run with n processes
3+
$ mpiexec -n 10 pytest test_blockdiag.py --with-mpi
4+
"""
15
from mpi4py import MPI
26
import numpy as np
37
from numpy.testing import assert_allclose
4-
np.random.seed(42)
5-
68
import pytest
79

810
import pylops
911
import pylops_mpi
12+
from pylops_mpi.utils.dottest import dottest
1013

1114
par1 = {'ny': 101, 'nx': 101, 'dtype': np.float64}
1215
par1j = {'ny': 101, 'nx': 101, 'dtype': np.complex128}
1316
par2 = {'ny': 301, 'nx': 101, 'dtype': np.float64}
1417
par2j = {'ny': 301, 'nx': 101, 'dtype': np.complex128}
1518

19+
np.random.seed(42)
20+
1621

1722
@pytest.mark.mpi(min_size=2)
1823
@pytest.mark.parametrize("par", [(par1), (par1j), (par2), (par2j)])
@@ -30,10 +35,14 @@ def test_blockdiag(par):
3035
y[:] = np.ones(shape=par['ny'], dtype=par['dtype'])
3136
y_global = y.asarray()
3237

38+
# Forward
3339
x_mat = BDiag_MPI @ x
40+
# Adjoint
3441
y_rmat = BDiag_MPI.H @ y
3542
assert isinstance(x_mat, pylops_mpi.DistributedArray)
3643
assert isinstance(y_rmat, pylops_mpi.DistributedArray)
44+
# Dot test
45+
dottest(BDiag_MPI, x, y, size * par['ny'], size * par['nx'])
3746

3847
x_mat_mpi = x_mat.asarray()
3948
y_rmat_mpi = y_rmat.asarray()
@@ -73,10 +82,14 @@ def test_stacked_blockdiag(par):
7382
y = pylops_mpi.StackedDistributedArray(distarrays=[dist1, dist2])
7483
y_global = y.asarray()
7584

85+
# Forward
7686
x_mat = StackedBDiag_MPI @ x
87+
# Adjoint
7788
y_rmat = StackedBDiag_MPI.H @ y
7889
assert isinstance(x_mat, pylops_mpi.StackedDistributedArray)
7990
assert isinstance(y_rmat, pylops_mpi.StackedDistributedArray)
91+
# Dot test
92+
dottest(StackedBDiag_MPI, x, y, size * par['ny'] + par['nx'] * par['ny'], size * par['nx'] + par['nx'] * par['ny'])
8093

8194
x_mat_mpi = x_mat.asarray()
8295
y_rmat_mpi = y_rmat.asarray()

tests/test_derivative.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
"""Test the derivative classes
2+
Designed to run with n processes
3+
$ mpiexec -n 10 pytest test_derivative.py --with-mpi
4+
"""
15
import numpy as np
26
from mpi4py import MPI
37
from numpy.testing import assert_allclose
48
import pytest
59

610
import pylops
711
import pylops_mpi
12+
from pylops_mpi.utils.dottest import dottest
813

914
np.random.seed(42)
1015
rank = MPI.COMM_WORLD.Get_rank()
@@ -194,6 +199,8 @@ def test_first_derivative_forward(par):
194199
# Adjoint
195200
y_adj_dist = Fop_MPI.H @ x
196201
y_adj = y_adj_dist.asarray()
202+
# Dot test
203+
dottest(Fop_MPI, x, y_dist, np.prod(par['nz']), np.prod(par['nz']))
197204

198205
if rank == 0:
199206
Fop = pylops.FirstDerivative(dims=par['nz'], axis=0,
@@ -226,6 +233,8 @@ def test_first_derivative_backward(par):
226233
# Adjoint
227234
y_adj_dist = Fop_MPI.H @ x
228235
y_adj = y_adj_dist.asarray()
236+
# Dot test
237+
dottest(Fop_MPI, x, y_dist, np.prod(par['nz']), np.prod(par['nz']))
229238

230239
if rank == 0:
231240
Fop = pylops.FirstDerivative(dims=par['nz'], axis=0,
@@ -259,6 +268,9 @@ def test_first_derivative_centered(par):
259268
# Adjoint
260269
y_adj_dist = Fop_MPI.H @ x
261270
y_adj = y_adj_dist.asarray()
271+
# Dot test
272+
dottest(Fop_MPI, x, y_dist, np.prod(par['nz']), np.prod(par['nz']))
273+
262274
if rank == 0:
263275
Fop = pylops.FirstDerivative(dims=par['nz'], axis=0,
264276
sampling=par['dz'],
@@ -290,6 +302,8 @@ def test_second_derivative_forward(par):
290302
# Adjoint
291303
y_adj_dist = Sop_MPI.H @ x
292304
y_adj = y_adj_dist.asarray()
305+
# Dot test
306+
dottest(Sop_MPI, x, y_dist, np.prod(par['nz']), np.prod(par['nz']))
293307

294308
if rank == 0:
295309
Sop = pylops.SecondDerivative(dims=par['nz'], axis=0,
@@ -322,6 +336,8 @@ def test_second_derivative_backward(par):
322336
# Adjoint
323337
y_adj_dist = Sop_MPI.H @ x
324338
y_adj = y_adj_dist.asarray()
339+
# Dot test
340+
dottest(Sop_MPI, x, y_dist, np.prod(par['nz']), np.prod(par['nz']))
325341

326342
if rank == 0:
327343
Sop = pylops.SecondDerivative(dims=par['nz'], axis=0,
@@ -354,6 +370,8 @@ def test_second_derivative_centered(par):
354370
# Adjoint
355371
y_adj_dist = Sop_MPI.H @ x
356372
y_adj = y_adj_dist.asarray()
373+
# Dot test
374+
dottest(Sop_MPI, x, y_dist, np.prod(par['nz']), np.prod(par['nz']))
357375

358376
if rank == 0:
359377
Sop = pylops.SecondDerivative(dims=par['nz'], axis=0,
@@ -385,6 +403,8 @@ def test_laplacian(par):
385403
# Adjoint
386404
y_adj_dist = Lop_MPI.H @ x
387405
y_adj = y_adj_dist.asarray()
406+
# Dot test
407+
dottest(Lop_MPI, x, y_dist, np.prod(par['n']), np.prod(par['n']))
388408

389409
if rank == 0:
390410
Lop = pylops.Laplacian(dims=par['n'], axes=par['axes'],
@@ -409,6 +429,7 @@ def test_gradient(par):
409429
x_fwd = pylops_mpi.DistributedArray(global_shape=np.prod(par['n']), dtype=par['dtype'])
410430
x_fwd[:] = np.random.normal(rank, 10, x_fwd.local_shape)
411431
x_global = x_fwd.asarray()
432+
412433
# Forward
413434
y_dist = Gop_MPI @ x_fwd
414435
assert isinstance(y_dist, pylops_mpi.StackedDistributedArray)
@@ -421,15 +442,15 @@ def test_gradient(par):
421442
x_adj_dist2[:] = np.random.normal(rank, 20, x_adj_dist2.local_shape)
422443
x_adj_dist3 = pylops_mpi.DistributedArray(global_shape=int(np.prod(par['n'])), dtype=par['dtype'])
423444
x_adj_dist3[:] = np.random.normal(rank, 30, x_adj_dist3.local_shape)
424-
425445
x_adj = pylops_mpi.StackedDistributedArray(distarrays=[x_adj_dist1, x_adj_dist2, x_adj_dist3])
426-
427446
x_adj_global = x_adj.asarray()
428447
y_adj_dist = Gop_MPI.H @ x_adj
429448
assert isinstance(y_adj_dist, pylops_mpi.DistributedArray)
430-
431449
y_adj = y_adj_dist.asarray()
432450

451+
# Dot test
452+
dottest(Gop_MPI, x_fwd, y_dist, len(par['n']) * np.prod(par['n']), np.prod(par['n']))
453+
433454
if rank == 0:
434455
Gop = pylops.Gradient(dims=par['n'], sampling=par['sampling'],
435456
kind=kind, edge=par['edge'],

tests/test_fredholm.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""Test the MPIFredholm1 class
2+
Designed to run with n processes
3+
$ mpiexec -n 10 pytest test_fredholm.py --with-mpi
4+
"""
15
import numpy as np
26
from numpy.testing import assert_allclose
37
from mpi4py import MPI
@@ -9,12 +13,12 @@
913
from pylops_mpi import DistributedArray
1014
from pylops_mpi.DistributedArray import local_split, Partition
1115
from pylops_mpi.signalprocessing import MPIFredholm1
16+
from pylops_mpi.utils.dottest import dottest
1217

1318
np.random.seed(42)
1419
rank = MPI.COMM_WORLD.Get_rank()
1520
size = MPI.COMM_WORLD.Get_size()
1621

17-
1822
par1 = {
1923
"nsl": 6,
2024
"ny": 6,
@@ -130,6 +134,8 @@ def test_Fredholm1(par):
130134
# Adjoint
131135
y_adj_dist = Fop_MPI.H @ y_dist
132136
y_adj = y_adj_dist.asarray()
137+
# Dot test
138+
dottest(Fop_MPI, x, y_dist, par["nsl"] * par["nx"] * par["nz"],par["nsl"] * par["ny"] * par["nz"])
133139

134140
if rank == 0:
135141
Fop = pylops.signalprocessing.Fredholm1(

tests/test_linearop.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""Test the MPILinearOperator class
2+
Designed to run with n processes
3+
$ mpiexec -n 10 pytest test_linearop.py --with-mpi
4+
"""
15
import numpy as np
26
from numpy.testing import assert_allclose
37
from mpi4py import MPI
@@ -58,6 +62,7 @@ def test_transpose(par):
5862
"""Test the TransposeLinearOperator"""
5963
Op = pylops.MatrixMult(A=((rank + 1) * np.ones(shape=(par['ny'], par['nx']))).astype(par['dtype']))
6064
BDiag_MPI = MPIBlockDiag(ops=[Op, ])
65+
6166
# Tranposed Op
6267
Top_MPI = BDiag_MPI.T
6368

@@ -76,6 +81,7 @@ def test_transpose(par):
7681
Top_y = Top_MPI.H @ y
7782
assert isinstance(Top_y, DistributedArray)
7883
Top_y_np = Top_y.asarray()
84+
7985
if rank == 0:
8086
ops = [pylops.MatrixMult((i + 1) * np.ones(shape=(par['ny'], par['nx'])).astype(par['dtype'])) for i in
8187
range(size)]
@@ -109,6 +115,7 @@ def test_scaled(par):
109115
Sop_y = Sop_MPI.H @ y
110116
assert isinstance(Sop_y, DistributedArray)
111117
Sop_y_np = Sop_y.asarray()
118+
112119
if rank == 0:
113120
ops = [pylops.MatrixMult((i + 1) * np.ones(shape=(par['ny'], par['nx'])).astype(par['dtype'])) for i in
114121
range(size)]

tests/test_solver.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""Test solvers
2+
Designed to run with n processes
3+
$ mpiexec -n 10 pytest test_solver.py --with-mpi
4+
"""
15
import numpy as np
26
from numpy.testing import assert_allclose
37
from mpi4py import MPI

tests/test_stack.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
"""Test the stacking classes
2+
Designed to run with n processes
3+
$ mpiexec -n 10 pytest test_stack.py --with-mpi
4+
"""
15
import numpy as np
26
from numpy.testing import assert_allclose
37
from mpi4py import MPI
48
import pytest
59

610
import pylops
711
import pylops_mpi
12+
from pylops_mpi.utils.dottest import dottest
813

914
par1 = {'ny': 101, 'nx': 101, 'imag': 0, 'dtype': np.float64}
1015
par1j = {'ny': 101, 'nx': 101, 'imag': 1j, 'dtype': np.complex128}
@@ -36,10 +41,14 @@ def test_vstack(par):
3641
y[:] = np.ones(shape=par['ny'], dtype=par['dtype'])
3742
y_global = y.asarray()
3843

44+
# Forward
3945
x_mat = VStack_MPI @ x
46+
# Adjoint
4047
y_rmat = VStack_MPI.H @ y
4148
assert isinstance(x_mat, pylops_mpi.DistributedArray)
4249
assert isinstance(y_rmat, pylops_mpi.DistributedArray)
50+
# Dot test
51+
dottest(VStack_MPI, x, y, size * par['ny'], par['nx'])
4352

4453
x_mat_mpi = x_mat.asarray()
4554
y_rmat_mpi = y_rmat.asarray()

0 commit comments

Comments
 (0)