From 897838c26a490a6ad5c98c04078d925fb49acf86 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 26 Oct 2026 09:50:01 -0400 Subject: [PATCH 1/2] Remove workaround for older (<1.15) scipy --- pyproject.toml | 2 +- tests/test_basis_utils.py | 17 +---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f25484250..7eef0575d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "psutil", "pymanopt", "PyWavelets", - "scipy >= 1.10.0", + "scipy >= 1.15.0", "scikit-learn >= 1.5.0", "scikit-image", "setuptools >= 0.41", diff --git a/tests/test_basis_utils.py b/tests/test_basis_utils.py index e45e61d91f..ea669dd054 100644 --- a/tests/test_basis_utils.py +++ b/tests/test_basis_utils.py @@ -1,22 +1,7 @@ from unittest import TestCase import numpy as np - -# This can be removed when project requires scipy>=1.15.0 -# scipy<1.15.0 provide `sph_harm` -import scipy -from packaging.version import Version - -if Version(scipy.__version__) < Version("1.15.0"): - from scipy.special import sph_harm as sp_sph_harm - - # This has a different convention from upstream sph_harm_y - def sph_harm_y(j, m, x, y): - return sp_sph_harm(m, j, y, x) - -else: - # scipy>=1.15.0 provide `sph_harm_y` - from scipy.special import sph_harm_y +from scipy.special import sph_harm_y from aspire.basis.basis_utils import ( all_besselj_zeros, From 0f9a129606fbb77442fec15a2a01f4d693e3f5a9 Mon Sep 17 00:00:00 2001 From: Garrett Wright Date: Mon, 26 Oct 2026 09:58:01 -0400 Subject: [PATCH 2/2] Remove cg workaround for older (<1.14) scipy --- src/aspire/basis/basis.py | 3 +- src/aspire/covariance/covar.py | 6 +- src/aspire/numeric/scipy.py | 18 -- src/aspire/reconstruction/mean.py | 3 +- tests/test_covar3d.py | 354 +++++++++++++++--------------- 5 files changed, 178 insertions(+), 206 deletions(-) delete mode 100644 src/aspire/numeric/scipy.py diff --git a/src/aspire/basis/basis.py b/src/aspire/basis/basis.py index 8a9e46a1e3..c154636f78 100644 --- a/src/aspire/basis/basis.py +++ b/src/aspire/basis/basis.py @@ -2,10 +2,9 @@ import warnings import numpy as np -from scipy.sparse.linalg import LinearOperator +from scipy.sparse.linalg import LinearOperator, cg from aspire.image import Image -from aspire.numeric.scipy import cg from aspire.utils import mdim_mat_fun_conj from aspire.volume import Volume diff --git a/src/aspire/covariance/covar.py b/src/aspire/covariance/covar.py index cf48d43aa3..1d2c14c52c 100644 --- a/src/aspire/covariance/covar.py +++ b/src/aspire/covariance/covar.py @@ -2,12 +2,12 @@ from functools import partial import numpy as np +import scipy from scipy.linalg import norm from scipy.sparse.linalg import LinearOperator from aspire.nufft import anufft from aspire.numeric import fft -from aspire.numeric.scipy import cg from aspire.operators import evaluate_src_filters_on_grid from aspire.reconstruction import Estimator, FourierKernel, MeanEstimator from aspire.utils import make_symmat, symmat_to_vec_iso, trange, vec_to_symmat_iso @@ -123,7 +123,9 @@ def cb(xk): f"Delta {norm(b_coef - self.apply_kernel(xk, packed=True))} (target {target_residual})" ) - x, info = cg(operator, b_coef, M=M, callback=cb, rtol=tol, atol=0) + x, info = scipy.sparse.linalg.cg( + operator, b_coef, M=M, callback=cb, rtol=tol, atol=0 + ) if info != 0: raise RuntimeError("Unable to converge!") diff --git a/src/aspire/numeric/scipy.py b/src/aspire/numeric/scipy.py deleted file mode 100644 index c913e917a3..0000000000 --- a/src/aspire/numeric/scipy.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Utility wrappers for scipy methods. -""" - -import scipy -from packaging.version import Version - - -def cg(*args, **kwargs): - """ - Supports scipy cg before and after 1.12.0. - """ - - # older (<1.12.0) scipy cg interface uses `tol` instead of `rtol`. - # `tol` will be removed in scipy 1.14.0. - if Version(scipy.__version__) < Version("1.12.0"): - kwargs["tol"] = kwargs.pop("rtol", None) - return scipy.sparse.linalg.cg(*args, **kwargs) diff --git a/src/aspire/reconstruction/mean.py b/src/aspire/reconstruction/mean.py index 167769305f..869c5980bf 100644 --- a/src/aspire/reconstruction/mean.py +++ b/src/aspire/reconstruction/mean.py @@ -3,13 +3,12 @@ import numpy as np from scipy.linalg import norm -from scipy.sparse.linalg import LinearOperator +from scipy.sparse.linalg import LinearOperator, cg from aspire import config from aspire.basis import Coef from aspire.nufft import anufft from aspire.numeric import fft -from aspire.numeric.scipy import cg from aspire.operators import evaluate_src_filters_on_grid from aspire.reconstruction import Estimator, FourierKernel, FourierKernelMatrix from aspire.volume import Volume, rotated_grids diff --git a/tests/test_covar3d.py b/tests/test_covar3d.py index 5b50ec2c56..485558ac5b 100644 --- a/tests/test_covar3d.py +++ b/tests/test_covar3d.py @@ -163,101 +163,93 @@ def testCovar3D1(self, cg): covar_est = self.covar_estimator.estimate(self.mean_est, self.noise_variance) - # Since we're only mocking a linear system solver, ensure that we did return the solution - # for the argument we got called with. - # 'call_args' is a tuple with the first member being the ordered arguments of the Mock call - # In our case (in order) - the LinearOperator and 'b' (the RHS of the linear system) - op, b = cg.call_args[0] - - self.assertTrue( - np.allclose( - np.array( + np.testing.assert_allclose( + np.array( + [ [ - [ - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - -4.97200141e-17, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - ], - [ - 0.00000000e00, - 0.00000000e00, - -1.12423131e-02, - -7.82940670e-03, - -5.25730644e-02, - -5.12982911e-02, - 5.46448594e-03, - 0.00000000e00, - ], - [ - 0.00000000e00, - -1.45263802e-02, - 1.74773651e-02, - 2.37465013e-02, - -5.82802387e-02, - -2.18693634e-02, - -8.17610441e-03, - -5.34913194e-02, - ], - [ - 0.00000000e00, - -1.25388002e-02, - 3.32579746e-02, - -2.47232520e-02, - -1.45779671e-01, - -9.90902473e-02, - -1.21664500e-01, - -1.86567008e-01, - ], - [ - 5.23168570e-17, - 1.48361175e-02, - 6.39768940e-02, - 2.31061220e-01, - 1.14505159e-01, - -1.27282900e-01, - -1.20426781e-01, - -9.83754536e-02, - ], - [ - 0.00000000e00, - -2.77886166e-02, - -2.70706646e-02, - 3.27305040e-01, - 3.52852148e-01, - 1.95510582e-03, - -5.53571860e-02, - -2.08399248e-02, - ], - [ - 0.00000000e00, - -2.60699879e-02, - -1.84686293e-02, - 1.30268283e-01, - 1.36522253e-01, - 8.11090183e-02, - 3.50443711e-02, - -1.21283276e-02, - ], - [ - 0.00000000e00, - 0.00000000e00, - 6.67517637e-02, - 1.12721933e-01, - -8.87693429e-03, - 2.99613531e-02, - 4.14024319e-02, - 0.00000000e00, - ], - ] - ), - covar_est.T[:, :, 4, 4, 4, 4], # RCOPT - atol=1e-4, - ) + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + -4.97200141e-17, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + ], + [ + 0.00000000e00, + 0.00000000e00, + -1.12423131e-02, + -7.82940670e-03, + -5.25730644e-02, + -5.12982911e-02, + 5.46448594e-03, + 0.00000000e00, + ], + [ + 0.00000000e00, + -1.45263802e-02, + 1.74773651e-02, + 2.37465013e-02, + -5.82802387e-02, + -2.18693634e-02, + -8.17610441e-03, + -5.34913194e-02, + ], + [ + 0.00000000e00, + -1.25388002e-02, + 3.32579746e-02, + -2.47232520e-02, + -1.45779671e-01, + -9.90902473e-02, + -1.21664500e-01, + -1.86567008e-01, + ], + [ + 5.23168570e-17, + 1.48361175e-02, + 6.39768940e-02, + 2.31061220e-01, + 1.14505159e-01, + -1.27282900e-01, + -1.20426781e-01, + -9.83754536e-02, + ], + [ + 0.00000000e00, + -2.77886166e-02, + -2.70706646e-02, + 3.27305040e-01, + 3.52852148e-01, + 1.95510582e-03, + -5.53571860e-02, + -2.08399248e-02, + ], + [ + 0.00000000e00, + -2.60699879e-02, + -1.84686293e-02, + 1.30268283e-01, + 1.36522253e-01, + 8.11090183e-02, + 3.50443711e-02, + -1.21283276e-02, + ], + [ + 0.00000000e00, + 0.00000000e00, + 6.67517637e-02, + 1.12721933e-01, + -8.87693429e-03, + 2.99613531e-02, + 4.14024319e-02, + 0.00000000e00, + ], + ] + ), + covar_est.T[:, :, 4, 4, 4, 4], # RCOPT + atol=1e-4, ) @patch("scipy.sparse.linalg.cg") @@ -270,95 +262,93 @@ def testCovar3D2(self, cg): self.mean_est, self.noise_variance ) - self.assertTrue( - np.allclose( - np.array( + np.testing.assert_allclose( + np.array( + [ [ - [ - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - -4.97200141e-17, - 0.00000000e00, - 0.00000000e00, - 0.00000000e00, - ], - [ - 0.00000000e00, - 0.00000000e00, - -1.12423131e-02, - -7.82940670e-03, - -5.25730644e-02, - -5.12982911e-02, - 5.46448594e-03, - 0.00000000e00, - ], - [ - 0.00000000e00, - -1.45263802e-02, - 1.74773651e-02, - 2.37465013e-02, - -5.82802387e-02, - -2.18693634e-02, - -8.17610441e-03, - -5.34913194e-02, - ], - [ - 0.00000000e00, - -1.25388002e-02, - 3.32579746e-02, - -2.47232520e-02, - -1.45779671e-01, - -9.90902473e-02, - -1.21664500e-01, - -1.86567008e-01, - ], - [ - 5.23168570e-17, - 1.48361175e-02, - 6.39768940e-02, - 2.31061220e-01, - 1.14505159e-01, - -1.27282900e-01, - -1.20426781e-01, - -9.83754536e-02, - ], - [ - 0.00000000e00, - -2.77886166e-02, - -2.70706646e-02, - 3.27305040e-01, - 3.52852148e-01, - 1.95510582e-03, - -5.53571860e-02, - -2.08399248e-02, - ], - [ - 0.00000000e00, - -2.60699879e-02, - -1.84686293e-02, - 1.30268283e-01, - 1.36522253e-01, - 8.11090183e-02, - 3.50443711e-02, - -1.21283276e-02, - ], - [ - 0.00000000e00, - 0.00000000e00, - 6.67517637e-02, - 1.12721933e-01, - -8.87693429e-03, - 2.99613531e-02, - 4.14024319e-02, - 0.00000000e00, - ], - ] - ), - covar_est.T[:, :, 4, 4, 4, 4], # RCOPT - atol=1e-4, - ) + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + -4.97200141e-17, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + ], + [ + 0.00000000e00, + 0.00000000e00, + -1.12423131e-02, + -7.82940670e-03, + -5.25730644e-02, + -5.12982911e-02, + 5.46448594e-03, + 0.00000000e00, + ], + [ + 0.00000000e00, + -1.45263802e-02, + 1.74773651e-02, + 2.37465013e-02, + -5.82802387e-02, + -2.18693634e-02, + -8.17610441e-03, + -5.34913194e-02, + ], + [ + 0.00000000e00, + -1.25388002e-02, + 3.32579746e-02, + -2.47232520e-02, + -1.45779671e-01, + -9.90902473e-02, + -1.21664500e-01, + -1.86567008e-01, + ], + [ + 5.23168570e-17, + 1.48361175e-02, + 6.39768940e-02, + 2.31061220e-01, + 1.14505159e-01, + -1.27282900e-01, + -1.20426781e-01, + -9.83754536e-02, + ], + [ + 0.00000000e00, + -2.77886166e-02, + -2.70706646e-02, + 3.27305040e-01, + 3.52852148e-01, + 1.95510582e-03, + -5.53571860e-02, + -2.08399248e-02, + ], + [ + 0.00000000e00, + -2.60699879e-02, + -1.84686293e-02, + 1.30268283e-01, + 1.36522253e-01, + 8.11090183e-02, + 3.50443711e-02, + -1.21283276e-02, + ], + [ + 0.00000000e00, + 0.00000000e00, + 6.67517637e-02, + 1.12721933e-01, + -8.87693429e-03, + 2.99613531e-02, + 4.14024319e-02, + 0.00000000e00, + ], + ] + ), + covar_est.T[:, :, 4, 4, 4, 4], # RCOPT + atol=1e-4, ) def testMeanEvaluation(self):