From be9af7f429ec68ab0c4733625cc2e0216135ed7c Mon Sep 17 00:00:00 2001 From: Matthew Plough Date: Tue, 20 Aug 2024 13:39:38 -0400 Subject: [PATCH] Convert tests to pytest --- .github/workflows/python-package-conda.yml | 2 +- .gitignore | 2 + Makefile | 5 +- README.rst | 2 +- pymatsolver/mumps/Makefile | 2 +- tests/test_Basic.py | 32 +++---- tests/test_BicgJacobi.py | 35 ++++---- tests/test_Mumps.py | 43 ++++----- tests/test_Pardiso.py | 100 ++++++++++----------- tests/test_Scipy.py | 62 ++++++------- tests/test_Triangle.py | 25 +++--- 11 files changed, 139 insertions(+), 171 deletions(-) diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 3fa8607..c8f9fcb 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -46,7 +46,7 @@ jobs: - name: Run Tests run: | - pytest --cov-config=.coveragerc --cov-report=xml --cov=pymatsolver -s -v + make coverage - name: Test Documentation if: ${{ matrix.os == 'ubuntu-latest' }} and {{ matrix.python-version == '3.8' }} diff --git a/.gitignore b/.gitignore index c2e9891..75dbfbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.coverage +coverage.xml *.pyc *.so build/ diff --git a/Makefile b/Makefile index 08a96fe..88a3dd5 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,7 @@ build: python setup.py build_ext --inplace coverage: - nosetests --logging-level=INFO --with-coverage --cover-package=pymatsolver --cover-html - open cover/index.html + pytest --cov-config=.coveragerc --cov-report=xml --cov=pymatsolver -s -v lint: pylint --output-format=html pymatsolver > pylint.html @@ -14,7 +13,7 @@ graphs: pyreverse -my -A -o pdf -p pymatsolver pymatsolver/**.py pymatsolver/**/**.py tests: - nosetests --logging-level=INFO + pytest docs: cd docs;make html diff --git a/README.rst b/README.rst index c00d431..450e634 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ From a clean install on Ubuntu: ./miniconda.sh -b export PATH=/root/anaconda/bin:/root/miniconda/bin:$PATH conda update --yes conda - conda install --yes numpy scipy matplotlib cython ipython nose + conda install --yes numpy scipy matplotlib cython ipython pytest coverage git clone https://github.com/rowanc1/pymatsolver.git cd pymatsolver diff --git a/pymatsolver/mumps/Makefile b/pymatsolver/mumps/Makefile index e6501e4..c29b41a 100644 --- a/pymatsolver/mumps/Makefile +++ b/pymatsolver/mumps/Makefile @@ -43,4 +43,4 @@ clean: rm -rf *.dSYM test: - cd ..;nosetests + cd ../..;pytest diff --git a/tests/test_Basic.py b/tests/test_Basic.py index adcd4c5..0fc9c5f 100644 --- a/tests/test_Basic.py +++ b/tests/test_Basic.py @@ -1,30 +1,24 @@ -import unittest import numpy as np +import pytest import scipy.sparse as sp from pymatsolver import Diagonal TOL = 1e-12 -class TestBasic(unittest.TestCase): +def test_DiagonalSolver(): - def test_DiagonalSolver(self): + A = sp.identity(5)*2.0 + rhs = np.c_[np.arange(1, 6), np.arange(2, 11, 2)] + X = Diagonal(A) * rhs + x = Diagonal(A) * rhs[:, 0] - A = sp.identity(5)*2.0 - rhs = np.c_[np.arange(1, 6), np.arange(2, 11, 2)] - X = Diagonal(A) * rhs - x = Diagonal(A) * rhs[:, 0] + sol = rhs/2.0 - sol = rhs/2.0 + with pytest.raises(TypeError): + Diagonal(A, check_accuracy=np.array([1, 2, 3])) + with pytest.raises(TypeError): + Diagonal(A, accuracy_tol=0) - with self.assertRaises(TypeError): - Diagonal(A, check_accuracy=np.array([1, 2, 3])) - with self.assertRaises(TypeError): - Diagonal(A, accuracy_tol=0) - - self.assertLess(np.linalg.norm(sol-X, np.inf), TOL) - self.assertLess(np.linalg.norm(sol[:, 0]-x, np.inf), TOL) - - -if __name__ == '__main__': - unittest.main() + assert np.linalg.norm(sol-X, np.inf) < TOL + assert np.linalg.norm(sol[:, 0]-x, np.inf) < TOL diff --git a/tests/test_BicgJacobi.py b/tests/test_BicgJacobi.py index 24e41a5..cd01b3f 100644 --- a/tests/test_BicgJacobi.py +++ b/tests/test_BicgJacobi.py @@ -1,4 +1,3 @@ -import unittest from pymatsolver import BicgJacobi import numpy as np import scipy.sparse as sp @@ -6,9 +5,10 @@ TOL = 1e-6 -class TestBicgJacobi(unittest.TestCase): +class TestBicgJacobi: - def setUp(self): + @classmethod + def setup_class(cls): nSize = 100 A = sp.rand(nSize, nSize, 0.05, format='csr', random_state=100) @@ -19,9 +19,9 @@ def setUp(self): sol = np.random.rand(nSize, 4) rhs = A.dot(sol) - self.A = A - self.rhs = rhs - self.sol = sol + cls.A = A + cls.rhs = rhs + cls.sol = sol def test(self): rhs = self.rhs @@ -30,7 +30,7 @@ def test(self): for i in range(3): err = np.linalg.norm( self.A*solb[:, i] - rhs[:, i]) / np.linalg.norm(rhs[:, i]) - self.assertLess(err, TOL) + assert err < TOL Ainv.clean() def test_T(self): @@ -42,13 +42,14 @@ def test_T(self): for i in range(3): err = np.linalg.norm( self.A.T*solb[:, i] - rhs[:, i]) / np.linalg.norm(rhs[:, i]) - self.assertLess(err, TOL) + assert err < TOL Ainv.clean() -class TestPardisoComplex(unittest.TestCase): +class TestBicgJacobiComplex: - def setUp(self): + @classmethod + def setup_class(cls): nSize = 100 A = sp.rand(nSize, nSize, 0.05, format='csr', random_state=100) A.data = A.data + 1j*np.random.rand(A.nnz) @@ -59,9 +60,9 @@ def setUp(self): sol = np.random.rand(nSize, 5) + 1j*np.random.rand(nSize, 5) rhs = A.dot(sol) - self.A = A - self.rhs = rhs - self.sol = sol + cls.A = A + cls.rhs = rhs + cls.sol = sol def test(self): rhs = self.rhs @@ -70,7 +71,7 @@ def test(self): for i in range(3): err = np.linalg.norm( self.A*solb[:, i] - rhs[:, i]) / np.linalg.norm(rhs[:, i]) - self.assertLess(err, TOL) + assert err < TOL Ainv.clean() def test_T(self): @@ -82,9 +83,5 @@ def test_T(self): for i in range(3): err = np.linalg.norm( self.A.T*solb[:, i] - rhs[:, i]) / np.linalg.norm(rhs[:, i]) - self.assertLess(err, TOL) + assert err < TOL Ainv.clean() - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_Mumps.py b/tests/test_Mumps.py index 1bf4afc..1f84278 100644 --- a/tests/test_Mumps.py +++ b/tests/test_Mumps.py @@ -1,4 +1,3 @@ -import unittest import numpy as np import scipy.sparse as sp import pymatsolver @@ -6,11 +5,12 @@ TOL = 1e-11 """ -class TestMumps(unittest.TestCase): +class TestMumps: if pymatsolver.AvailableSolvers['Mumps']: - def setUp(self): + @classmethod + def setup_class(cls): n = 5 irn = np.r_[0, 1, 3, 4, 1, 0, 4, 2, 1, 2, 0, 2] jcn = np.r_[1, 2, 2, 4, 0, 0, 1, 3, 4, 1, 2, 2] @@ -23,19 +23,17 @@ def setUp(self): sol = np.r_[1., 2., 3., 4., 5.] sol = np.c_[sol, 10*sol, 100*sol] A = sp.coo_matrix((a, (irn, jcn)), shape=(n, n)) - self.A = A - self.rhs = rhs - self.sol = sol + cls.A = A + cls.rhs = rhs + cls.sol = sol def test_1to5(self): rhs = self.rhs sol = self.sol Ainv = pymatsolver.Mumps(self.A) for i in range(3): - self.assertLess( - np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]), TOL - ) - self.assertLess(np.linalg.norm(Ainv * rhs - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs - sol, np.inf) < TOL def test_1to5_cmplx(self): rhs = self.rhs.astype(complex) @@ -43,10 +41,8 @@ def test_1to5_cmplx(self): self.A = self.A.astype(complex) Ainv = pymatsolver.Mumps(self.A) for i in range(3): - self.assertLess( - np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]), TOL - ) - self.assertLess(np.linalg.norm(Ainv * rhs - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs - sol, np.inf) < TOL def test_1to5_T(self): rhs = self.rhs @@ -54,15 +50,14 @@ def test_1to5_T(self): Ainv = pymatsolver.Mumps(self.A) AinvT = Ainv.T for i in range(3): - self.assertLess( - np.linalg.norm(AinvT.T * rhs[:, i] - sol[:, i]), TOL - ) - self.assertLess(np.linalg.norm(AinvT.T * rhs - sol, np.inf), TOL) + assert np.linalg.norm(AinvT.T * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(AinvT.T * rhs - sol, np.inf) < TOL # def test_singular(self): # A = sp.identity(5).tocsr() # A[-1, -1] = 0 - # self.assertRaises(Exception, pymatsolver.Mumps, A) + # with pytest.raises(Exception): + # pymatsolver.Mumps(A) def test_multiFactorsInMem(self): n = 100 @@ -72,15 +67,9 @@ def test_multiFactorsInMem(self): solvers = [pymatsolver.Mumps(A) for _ in range(20)] for Ainv in solvers: - self.assertLess( - np.linalg.norm(Ainv * rhs - x)/np.linalg.norm(rhs), TOL) + assert np.linalg.norm(Ainv * rhs - x)/np.linalg.norm(rhs) < TOL Ainv.clean() for Ainv in solvers: - self.assertLess( - np.linalg.norm(Ainv * rhs - x)/np.linalg.norm(rhs), TOL - ) - -if __name__ == '__main__': - unittest.main() + assert np.linalg.norm(Ainv * rhs - x)/np.linalg.norm(rhs) < TOL """ diff --git a/tests/test_Pardiso.py b/tests/test_Pardiso.py index da10975..40a946d 100644 --- a/tests/test_Pardiso.py +++ b/tests/test_Pardiso.py @@ -1,19 +1,20 @@ -import unittest from pymatsolver import Pardiso from pydiso.mkl_solver import ( get_mkl_pardiso_max_threads, PardisoTypeConversionWarning ) import numpy as np +import pytest import scipy.sparse as sp import os TOL = 1e-10 -class TestPardiso(unittest.TestCase): +class TestPardiso: - def setUp(self): + @classmethod + def setup_class(cls): nSize = 100 A = sp.rand(nSize, nSize, 0.05, format='csr', random_state=100) @@ -24,17 +25,17 @@ def setUp(self): sol = np.random.rand(nSize, 5) rhs = A.dot(sol) - self.A = A - self.rhs = rhs - self.sol = sol + cls.A = A + cls.rhs = rhs + cls.sol = sol def test(self): rhs = self.rhs sol = self.sol Ainv = Pardiso(self.A, is_symmetric=True) for i in range(3): - self.assertLess(np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]), TOL) - self.assertLess(np.linalg.norm(Ainv * rhs - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs - sol, np.inf) < TOL def test_refactor(self): rhs = self.rhs @@ -42,53 +43,54 @@ def test_refactor(self): A = self.A Ainv = Pardiso(A, is_symmetric=True) for i in range(3): - self.assertLess(np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]), TOL) - self.assertLess(np.linalg.norm(Ainv * rhs - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs - sol, np.inf) < TOL - # scale rows and collumns + # scale rows and columns D = sp.diags(np.random.rand(A.shape[0]) + 1.0) A2 = D.T @ A @ D rhs2 = A2 @ sol Ainv.factor(A2) for i in range(3): - self.assertLess(np.linalg.norm(Ainv * rhs2[:, i] - sol[:, i]), TOL) - self.assertLess(np.linalg.norm(Ainv * rhs2 - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs2[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs2 - sol, np.inf) < TOL def test_T(self): rhs = self.rhs sol = self.sol Ainv = Pardiso(self.A, is_symmetric=True) - with self.assertWarns(PardisoTypeConversionWarning): + with pytest.warns(PardisoTypeConversionWarning): AinvT = Ainv.T x = AinvT * rhs for i in range(3): - self.assertLess(np.linalg.norm(x[:, i] - sol[:, i]), TOL) - self.assertLess(np.linalg.norm(x - sol, np.inf), TOL) + assert np.linalg.norm(x[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(x - sol, np.inf) < TOL def test_n_threads(self): max_threads = get_mkl_pardiso_max_threads() print(f'testing setting n_threads to 1 and {max_threads}') Ainv = Pardiso(self.A, is_symmetric=True, n_threads=1) - self.assertEqual(Ainv.n_threads, 1) + assert Ainv.n_threads == 1 Ainv2 = Pardiso(self.A, is_symmetric=True, n_threads=max_threads) - self.assertEqual(Ainv2.n_threads, max_threads) - self.assertEqual(Ainv2.n_threads, Ainv.n_threads) + assert Ainv2.n_threads == max_threads + assert Ainv2.n_threads == Ainv.n_threads Ainv.n_threads = 1 - self.assertEqual(Ainv.n_threads, 1) - self.assertEqual(Ainv2.n_threads, Ainv.n_threads) + assert Ainv.n_threads == 1 + assert Ainv2.n_threads == Ainv.n_threads - with self.assertRaises(TypeError): + with pytest.raises(TypeError): Ainv.n_threads = "2" -class TestPardisoNotSymmetric(unittest.TestCase): +class TestPardisoNotSymmetric: - def setUp(self): + @classmethod + def setup_class(cls): nSize = 100 A = sp.rand(nSize, nSize, 0.05, format='csr', random_state=100) @@ -98,27 +100,29 @@ def setUp(self): sol = np.random.rand(nSize, 5) rhs = A.dot(sol) - self.A = A - self.rhs = rhs - self.sol = sol + cls.A = A + cls.rhs = rhs + cls.sol = sol def test(self): rhs = self.rhs sol = self.sol Ainv = Pardiso(self.A, is_symmetric=True, check_accuracy=True) - self.assertRaises(Exception, lambda: Ainv * rhs) + with pytest.raises(Exception): + Ainv * rhs Ainv.clean() Ainv = Pardiso(self.A) for i in range(3): - self.assertLess(np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]), TOL) - self.assertLess(np.linalg.norm(Ainv * rhs - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs - sol, np.inf) < TOL Ainv.clean() -class TestPardisoFDEM(unittest.TestCase): +class TestPardisoFDEM: - def setUp(self): + @classmethod + def setup_class(cls): base_path = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'fdem') @@ -126,20 +130,21 @@ def setUp(self): indices = np.load(os.path.join(base_path, 'A_indices.npy')) indptr = np.load(os.path.join(base_path, 'A_indptr.npy')) - self.A = sp.csr_matrix((data, indices, indptr), shape=(13872, 13872)) - self.rhs = np.load(os.path.join(base_path, 'RHS.npy')) + cls.A = sp.csr_matrix((data, indices, indptr), shape=(13872, 13872)) + cls.rhs = np.load(os.path.join(base_path, 'RHS.npy')) def test(self): rhs = self.rhs Ainv = Pardiso(self.A, check_accuracy=True) sol = Ainv * rhs - with self.assertWarns(PardisoTypeConversionWarning): + with pytest.warns(PardisoTypeConversionWarning): sol = Ainv * rhs.real -class TestPardisoComplex(unittest.TestCase): +class TestPardisoComplex: - def setUp(self): + @classmethod + def setup_class(cls): nSize = 100 A = sp.rand(nSize, nSize, 0.05, format='csr', random_state=100) A.data = A.data + 1j*np.random.rand(A.nnz) @@ -150,31 +155,26 @@ def setUp(self): sol = np.random.rand(nSize, 5) + 1j*np.random.rand(nSize, 5) rhs = A.dot(sol) - self.A = A - self.rhs = rhs - self.sol = sol + cls.A = A + cls.rhs = rhs + cls.sol = sol def test(self): rhs = self.rhs sol = self.sol Ainv = Pardiso(self.A, is_symmetric=True) for i in range(3): - self.assertLess(np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]), TOL) - self.assertLess(np.linalg.norm(Ainv * rhs - sol, np.inf), TOL) + assert np.linalg.norm(Ainv * rhs[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(Ainv * rhs - sol, np.inf) < TOL Ainv.clean() def test_T(self): rhs = self.rhs sol = self.sol Ainv = Pardiso(self.A, is_symmetric=True) - with self.assertWarns(PardisoTypeConversionWarning): + with pytest.warns(PardisoTypeConversionWarning): AinvT = Ainv.T x = AinvT * rhs for i in range(3): - self.assertLess( - np.linalg.norm(x[:, i] - sol[:, i]), TOL - ) - self.assertLess(np.linalg.norm(x - sol, np.inf), TOL) - -if __name__ == '__main__': - unittest.main() + assert np.linalg.norm(x[:, i] - sol[:, i]) < TOL + assert np.linalg.norm(x - sol, np.inf) < TOL diff --git a/tests/test_Scipy.py b/tests/test_Scipy.py index eb77135..dec6a58 100644 --- a/tests/test_Scipy.py +++ b/tests/test_Scipy.py @@ -1,7 +1,8 @@ -import unittest from pymatsolver import Solver, Diagonal, SolverCG, SolverLU import scipy.sparse as sp import numpy as np +import pytest + TOLD = 1e-10 TOLI = 1e-3 @@ -50,38 +51,27 @@ def dotest(MYSOLVER, multi=False, A=None, **solverOpts): return np.linalg.norm(e-x, np.inf) -class TestSolver(unittest.TestCase): - - def test_direct_spsolve_1(self): - self.assertLess(dotest(Solver, False), TOLD) - - def test_direct_spsolve_M(self): - self.assertLess(dotest(Solver, True), TOLD) - - def test_direct_splu_1(self): - self.assertLess(dotest(SolverLU, False), TOLD) - - def test_direct_splu_M(self): - self.assertLess(dotest(SolverLU, True), TOLD) - - def test_iterative_diag_1(self): - self.assertLess(dotest( - Diagonal, False, - A=sp.diags(np.random.rand(10)+1.0) - ), TOLI) - - def test_iterative_diag_M(self): - self.assertLess(dotest( - Diagonal, True, - A=sp.diags(np.random.rand(10)+1.0) - ), TOLI) - - def test_iterative_cg_1(self): - self.assertLess(dotest(SolverCG, False), TOLI) - - def test_iterative_cg_M(self): - self.assertLess(dotest(SolverCG, True), TOLI) - - -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize( + ["solver", "multi"], + [ + pytest.param(Solver, False), + pytest.param(Solver, True), + pytest.param(SolverLU, False), + pytest.param(SolverLU, True), + ] +) +def test_direct(solver, multi): + assert dotest(solver, multi) < TOLD + + +@pytest.mark.parametrize( + ["solver", "multi", "A"], + [ + pytest.param(Diagonal, False, sp.diags(np.random.rand(10)+1.0)), + pytest.param(Diagonal, True, sp.diags(np.random.rand(10)+1.0)), + pytest.param(SolverCG, False, None), + pytest.param(SolverCG, True, None), + ] +) +def test_iterative(solver, multi, A): + assert dotest(solver, multi, A) < TOLI diff --git a/tests/test_Triangle.py b/tests/test_Triangle.py index 241d1e0..98ee279 100644 --- a/tests/test_Triangle.py +++ b/tests/test_Triangle.py @@ -1,4 +1,3 @@ -import unittest import numpy as np import scipy.sparse as sp import pymatsolver @@ -6,29 +5,27 @@ TOL = 1e-12 -class TestTriangle(unittest.TestCase): +class TestTriangle: - def setUp(self): + @classmethod + def setup_class(cls): n = 50 nrhs = 20 - self.A = sp.rand(n, n, 0.4) + sp.identity(n) - self.sol = np.ones((n, nrhs)) - self.rhsU = sp.triu(self.A) * self.sol - self.rhsL = sp.tril(self.A) * self.sol + cls.A = sp.rand(n, n, 0.4) + sp.identity(n) + cls.sol = np.ones((n, nrhs)) + cls.rhsU = sp.triu(cls.A) * cls.sol + cls.rhsL = sp.tril(cls.A) * cls.sol def test_directLower(self): ALinv = pymatsolver.Forward(sp.tril(self.A)) X = ALinv * self.rhsL x = ALinv * self.rhsL[:, 0] - self.assertLess(np.linalg.norm(self.sol-X, np.inf), TOL) - self.assertLess(np.linalg.norm(self.sol[:, 0]-x, np.inf), TOL) + assert np.linalg.norm(self.sol-X, np.inf) < TOL + assert np.linalg.norm(self.sol[:, 0]-x, np.inf) < TOL def test_directLower_1(self): AUinv = pymatsolver.Backward(sp.triu(self.A)) X = AUinv * self.rhsU x = AUinv * self.rhsU[:, 0] - self.assertLess(np.linalg.norm(self.sol-X, np.inf), TOL) - self.assertLess(np.linalg.norm(self.sol[:, 0]-x, np.inf), TOL) - -if __name__ == '__main__': - unittest.main() + assert np.linalg.norm(self.sol-X, np.inf) < TOL + assert np.linalg.norm(self.sol[:, 0]-x, np.inf) < TOL