From 401bea042d0c1db75345fd4be46d29629bee459e Mon Sep 17 00:00:00 2001 From: Samuel St-Jean <3030760+samuelstjean@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:28:19 -0500 Subject: [PATCH 1/4] use spams.lassoWeighted with scaling --- nlsam/denoiser.py | 52 +++++++++++++++++++++-------------------------- pyproject.toml | 4 ++-- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/nlsam/denoiser.py b/nlsam/denoiser.py index c0b513c..f5943ad 100644 --- a/nlsam/denoiser.py +++ b/nlsam/denoiser.py @@ -241,13 +241,12 @@ def local_denoise(data, block_size, overlap, variance, n_iter=10, mask=None, block_size, overlap, param_alpha, - param_D, current_slice, dtype, n_iter) for current_slice in progress_slicer) - logger.info(f'Multiprocessing done in {(time() - time_multi) / 60:.2f} mins.') + logger.info(f'Multiprocessing done in {int(time() - time_multi)}s') # Put together the multiprocessed results data_subset = np.zeros_like(data, dtype=np.float32) @@ -261,7 +260,7 @@ def local_denoise(data, block_size, overlap, variance, n_iter=10, mask=None, return data_subset -def processer(data, mask, variance, block_size, overlap, param_alpha, param_D, current_slice, +def processer(data, mask, variance, block_size, overlap, param_alpha, current_slice, dtype=np.float64, n_iter=10, gamma=3, tau=1, tolerance=1e-5): # Fetch the current slice for parallel processing since now the arrays are dumped and read from disk @@ -284,53 +283,48 @@ def processer(data, mask, variance, block_size, overlap, param_alpha, param_D, c X_full_shape = X.shape X = X[:, train_idx].astype(dtype) - param_alpha['L'] = int(0.5 * X.shape[0]) - + param_alpha['L'] = X.shape[0] // 2 D = param_alpha['D'] alpha = np.zeros((D.shape[1], X.shape[1]), dtype=dtype) - W = np.ones(alpha.shape, dtype=dtype) - temp = np.zeros([alpha.shape[0], 1], dtype=dtype) - - DtD = np.asfortranarray(D.T @ D) - DtX = np.asfortranarray(D.T @ X) - DtXW = np.empty_like(DtX, order='F') - DtDW = np.empty_like(DtD, order='F') + alpha_old = np.ones_like(alpha) + W = np.ones_like(alpha, order='F') - alpha_old = np.ones(alpha.shape, dtype=dtype) not_converged = np.ones(alpha.shape[1], dtype=bool) nonzero_ind = np.zeros(alpha.shape, dtype=bool) - arr = np.empty(alpha.shape) xi = np.random.randn(X.shape[0], X.shape[1]) * var_mat var_mat *= (X.shape[0] + gamma * np.sqrt(2 * X.shape[0])) eps = np.max(np.abs(D.T @ xi), axis=0) - for _ in range(n_iter): - DtXW[:, not_converged] = DtX[:, not_converged] / W[:, not_converged] + # Rescale to ensure lambda1=1 as a constant, this allows passing everything to spams.lassoWeighted + # but the output coefficients are scaled by an extra np.sqrt(var_mat) factor since we do not rescale D to keep it constant also + scale = np.sqrt(var_mat) + X = np.asfortranarray(X / scale) + param_alpha['lambda1'] = 1 - for i in range(alpha.shape[1]): - if not_converged[i]: - param_alpha['lambda1'] = var_mat[i] - DtDW[:] = (1 / W[..., None, i]) * DtD * (1 / W[:, i]) - spams.lasso(X[:, i:i+1], Q=DtDW, q=DtXW[:, i:i+1], **param_alpha).todense(out=temp) - alpha[:, i:i+1] = temp + for _ in range(n_iter): + Xi = X[:, not_converged] + Wi = W[:, not_converged] + alpha[:, not_converged] = spams.lassoWeighted(X=Xi, W=Wi, **param_alpha).toarray() * scale[not_converged] - arr[:] = alpha - nonzero_ind[:] = arr != 0 - arr[nonzero_ind] /= W[nonzero_ind] - not_converged[:] = np.max(np.abs(alpha_old - arr), axis=0) > tolerance + nonzero_ind[:] = alpha != 0 + not_converged[:] = np.max(np.abs(alpha_old - alpha), axis=0) > tolerance if not np.any(not_converged): break - alpha_old[:] = arr + alpha_old[:] = alpha W[:] = 1 / (np.abs(alpha_old**tau) + eps) + # print(f'{_} W', W.min(), W.max(), eps.min(), eps.max(), np.sum(not_converged), not_converged.size) + # print(f'{_} alpha', alpha.min(), alpha.max(), np.min(X * scale), np.max(X*scale), np.min(D@alpha), np.max(D@alpha)) weights = np.ones(X_full_shape[1], dtype=dtype) weights[train_idx] = 1 / (np.sum(alpha != 0, axis=0) + 1) X = np.zeros(X_full_shape, dtype=dtype, order='F') - X[:, train_idx] = D @ arr + X[:, train_idx] = D @ alpha out = col2im_nd(X, block_size, orig_shape, overlap, weights) - del X, W, alpha, alpha_old, DtX, DtXW, DtDW + # print('out', out.min(), out.max(), X.min(), X.max(), weights.min(), weights.max()) + del X, W, alpha, alpha_old + return out diff --git a/pyproject.toml b/pyproject.toml index faf434a..14bb167 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,10 +19,10 @@ keywords = ["MRI", "diffusion", "dmri", "denoising"] dependencies = [ 'numpy>=1.21.3', 'scipy>=1.5', - 'nibabel>=2.0', + 'nibabel>=4.0', 'joblib>=0.14.1', 'autodmri>=0.2.1', - 'spams-bin>=2.6.2', + 'spams-bin>=2.6.12', 'tqdm>=4.56'] [project.urls] From 7e13b2705ab48a2199e543dc40d811e6c2ab108b Mon Sep 17 00:00:00 2001 From: Samuel St-Jean <3030760+samuelstjean@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:44:08 -0500 Subject: [PATCH 2/4] cleanup --- CHANGELOG.md | 6 ++++++ docs/conf.py | 4 ++-- nlsam/denoiser.py | 44 ++++++++++++++++++---------------------- nlsam/script.py | 34 ++++++++----------------------- nlsam/tests/test_main.py | 28 ++++++++++++------------- pyproject.toml | 5 ++--- 6 files changed, 53 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff4478..dbe9a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.0] - 2025-xx-xx + +- Some internal rework for faster processing by an order of magnitude + - spams-bin 2.6.12 or higher is now *required* as it fixes a positivity bug needed in the solver +- Some cleanups of old deprecated command line options + ## [0.7.3] - 2025-10-06 - Removed `PIESNO` as a noise estimation method. Use `auto` instead, which is the new default since 0.7, as it will automatically estimate `N` for you. diff --git a/docs/conf.py b/docs/conf.py index cc2827d..b932fee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,9 +59,9 @@ # built documents. # # The short X.Y version. -version = '0.7.3' +version = '1.0' # The full version, including alpha/beta/rc tags. -release = '0.7.3' +release = '1.0' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/nlsam/denoiser.py b/nlsam/denoiser.py index f5943ad..aed5110 100644 --- a/nlsam/denoiser.py +++ b/nlsam/denoiser.py @@ -189,43 +189,39 @@ def local_denoise(data, block_size, overlap, variance, n_iter=10, mask=None, if mask is None: mask = np.ones(data.shape[:-1], dtype=bool) - X = extract_patches(data, block_size, [1, 1, 1, block_size[-1]]).reshape(-1, np.prod(block_size)).T + m = np.prod(block_size) + X = extract_patches(data, block_size, [1, 1, 1, block_size[-1]]).reshape(-1, m).T # Solving for D param_alpha = {} param_alpha['pos'] = True param_alpha['mode'] = 1 + param_alpha['numThreads'] = 1 param_D = {} param_D['verbose'] = False param_D['posAlpha'] = True param_D['posD'] = True param_D['mode'] = 2 - param_D['lambda1'] = 1.2 / np.sqrt(np.prod(block_size)) - param_D['K'] = int(2 * np.prod(block_size)) + param_D['lambda1'] = 1.2 / np.sqrt(m) + param_D['K'] = int(2 * m) param_D['iter'] = 150 param_D['batchsize'] = 500 param_D['numThreads'] = n_cores - if 'D' in param_alpha: - param_D['D'] = param_alpha['D'] - mask_col = extract_patches(mask, block_size[:-1], (1, 1, 1), flatten=False) axis = tuple(range(mask_col.ndim//2, mask_col.ndim)) train_idx = np.sum(mask_col, axis=axis).ravel() > (np.prod(block_size[:-1]) / 2) train_data = np.asfortranarray(X[:, train_idx]) - train_data /= np.sqrt(np.sum(train_data**2, axis=0, keepdims=True), dtype=dtype) + train_data /= np.linalg.norm(train_data, axis=0, keepdims=True).astype(dtype, copy=False) - param_alpha['D'] = spams.trainDL(train_data, **param_D) - param_alpha['D'] /= np.sqrt(np.sum(param_alpha['D']**2, axis=0, keepdims=True, dtype=dtype)) - param_D['D'] = param_alpha['D'] + D = spams.trainDL(train_data, **param_D) + D /= np.linalg.norm(D, axis=0, keepdims=True).astype(dtype, copy=False) + param_alpha['D'] = D del train_idx, train_data, X, mask_col - param_alpha['numThreads'] = 1 - param_D['numThreads'] = 1 - slicer = [np.index_exp[:, :, k:k + block_size[2]] for k in range((data.shape[2] - block_size[2] + 1))] if verbose: @@ -282,25 +278,27 @@ def processer(data, mask, variance, block_size, overlap, param_alpha, current_sl var_mat = np.median(im2col_nd(variance, block_size[:-1], overlap[:-1])[:, train_idx], axis=0) X_full_shape = X.shape X = X[:, train_idx].astype(dtype) - - param_alpha['L'] = X.shape[0] // 2 D = param_alpha['D'] - alpha = np.zeros((D.shape[1], X.shape[1]), dtype=dtype) + m, n = X.shape + p = D.shape[1] + param_alpha['L'] = m // 2 + + alpha = np.zeros((p, n), dtype=dtype) alpha_old = np.ones_like(alpha) - W = np.ones_like(alpha, order='F') + W = np.ones_like(alpha, order='F', dtype=dtype) - not_converged = np.ones(alpha.shape[1], dtype=bool) + not_converged = np.ones(n, dtype=bool) nonzero_ind = np.zeros(alpha.shape, dtype=bool) - xi = np.random.randn(X.shape[0], X.shape[1]) * var_mat - var_mat *= (X.shape[0] + gamma * np.sqrt(2 * X.shape[0])) + xi = np.random.randn(m, n) * var_mat + var_mat *= (m + gamma * np.sqrt(2 * m)) eps = np.max(np.abs(D.T @ xi), axis=0) # Rescale to ensure lambda1=1 as a constant, this allows passing everything to spams.lassoWeighted # but the output coefficients are scaled by an extra np.sqrt(var_mat) factor since we do not rescale D to keep it constant also scale = np.sqrt(var_mat) - X = np.asfortranarray(X / scale) + X = np.asfortranarray(X / scale, dtype=dtype) param_alpha['lambda1'] = 1 for _ in range(n_iter): @@ -316,15 +314,13 @@ def processer(data, mask, variance, block_size, overlap, param_alpha, current_sl alpha_old[:] = alpha W[:] = 1 / (np.abs(alpha_old**tau) + eps) - # print(f'{_} W', W.min(), W.max(), eps.min(), eps.max(), np.sum(not_converged), not_converged.size) - # print(f'{_} alpha', alpha.min(), alpha.max(), np.min(X * scale), np.max(X*scale), np.min(D@alpha), np.max(D@alpha)) weights = np.ones(X_full_shape[1], dtype=dtype) weights[train_idx] = 1 / (np.sum(alpha != 0, axis=0) + 1) X = np.zeros(X_full_shape, dtype=dtype, order='F') X[:, train_idx] = D @ alpha + out = col2im_nd(X, block_size, orig_shape, overlap, weights) - # print('out', out.min(), out.max(), X.min(), X.max(), weights.min(), weights.max()) del X, W, alpha, alpha_old return out diff --git a/nlsam/script.py b/nlsam/script.py index 438d19a..17d33c2 100644 --- a/nlsam/script.py +++ b/nlsam/script.py @@ -204,19 +204,19 @@ def buildArgsParser(): # Old stuff ############ - deprecated = p.add_argument_group('Deprecated options') + # deprecated = p.add_argument_group('Deprecated options') - deprecated.add_argument('--fix_implausible', action='store_true', dest='implausible_signal_fix', - help='This option has been removed and has no effect.') + # deprecated.add_argument('--fix_implausible', action='store_true', dest='implausible_signal_fix', + # help='This option has been removed and has no effect.') - deprecated.add_argument('--sh_order', metavar='int', default=0, type=int, choices=[0, 2, 4, 6, 8], - help='This option has been removed and has no effect.') + # deprecated.add_argument('--sh_order', metavar='int', default=0, type=int, choices=[0, 2, 4, 6, 8], + # help='This option has been removed and has no effect.') - deprecated.add_argument('--mp_method', metavar='string', - help='This option has been removed and has no effect.') + # deprecated.add_argument('--mp_method', metavar='string', + # help='This option has been removed and has no effect.') - deprecated.add_argument('--use_threading', action='store_true', - help='This option has been removed and has no effect.') + # deprecated.add_argument('--use_threading', action='store_true', + # help='This option has been removed and has no effect.') return p @@ -264,18 +264,6 @@ def main(): else: dtype = np.float64 - if args.implausible_signal_fix: - logger.warning('Option --implausible_signal_fix has been deprecated') - - if args.sh_order: - logger.warning('Option --sh_order has been deprecated') - - if args.mp_method: - logger.warning('Option --mp_method has been deprecated') - - if args.use_threading: - logger.warning('Option --use_threading has been deprecated') - ########################################## # Load up data and do some sanity checks ########################################## @@ -521,7 +509,3 @@ def main_workaround_joblib(): main() else: main() - - -if __name__ == "__main__": - main_workaround_joblib() diff --git a/nlsam/tests/test_main.py b/nlsam/tests/test_main.py index 9a1159a..c45e21c 100644 --- a/nlsam/tests/test_main.py +++ b/nlsam/tests/test_main.py @@ -24,20 +24,20 @@ def unzip(zip, file): commands = [ - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-f', '-N', '1', '--noise_est', 'local_std', '--sh_order', '0', '--cores', '1', '-m', 'mask_crop.nii.gz', '-v'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '-N', '1', '--noise_est', 'local_std', '--sh_order', '6', '--iterations', '5', '--verbose', '--save_sigma', 'sigma.nii.gz', '--log', 'log.txt'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--noise_est', 'auto', '--sh_order', '6', '--iterations', '5', '--verbose', '--save_sigma', 'sigma.nii.gz', '--save_N', 'N.nii.gz'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '--b0_threshold', '10', '--noise_mask', 'pmask.nii.gz'), - ('nlsam_denoising', 'dwi_crop.nii', 'dwi_nlsam.nii', 'bvals', 'bvecs', '-m', 'mask_crop.nii', '-f', '--verbose', '--sh_order', '0', '-N', '1', '--no_stabilization', '--load_sigma', 'sigma.nii', '--is_symmetric', '--use_threading', '--save_difference', 'diff.nii'), - ('nlsam_denoising', 'dwi_crop.nii', 'dwi_nlsam.nii', 'bvals', 'bvecs', '-m', 'mask_crop.nii', '-f', '--verbose', '--sh_order', '0', '--no_denoising', '--save_sigma', 'sigma.nii', '--save_stab', 'stab.nii', '--load_mhat', 'dwi_crop.nii', '--save_eta', 'eta.nii'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '-N', '1', '--noise_est', 'local_std', '--sh_order', '0', '--block_size', '2,2,2', '--save_sigma', 'sigma.nii.gz', '--cores', '1'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '-N', '1', '--load_sigma', 'sigma.nii.gz', '--no_subsample', '--fix_implausible', '--no_clip_eta'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '--noise_map', 'noise.nii.gz', '--noise_mask', 'pmask.nii.gz', '--use_f32'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '-N', '1', '--load_sigma', 'sigma.nii.gz', '--no_stabilization', '--no_denoising'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '-N', '1', '--load_sigma', 'sigma.nii.gz', '--no_denoising', '--no_clip_eta'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '-N', '1', '--noise_est', 'local_std', '--no_denoising'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '--noise_est', 'auto', '--no_denoising', '--cores', '4'), - ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--sh_order', '0', '--no_denoising') + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-f', '-N', '1', '--noise_est', 'local_std', '--cores', '1', '-m', 'mask_crop.nii.gz', '-v'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '-N', '1', '--noise_est', 'local_std', '--iterations', '5', '--verbose', '--save_sigma', 'sigma.nii.gz', '--log', 'log.txt'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--noise_est', 'auto', '--iterations', '5', '--verbose', '--save_sigma', 'sigma.nii.gz', '--save_N', 'N.nii.gz'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--b0_threshold', '10', '--noise_mask', 'pmask.nii.gz'), + ('nlsam_denoising', 'dwi_crop.nii', 'dwi_nlsam.nii', 'bvals', 'bvecs', '-m', 'mask_crop.nii', '-f', '--verbose', '-N', '1', '--no_stabilization', '--load_sigma', 'sigma.nii', '--is_symmetric', '--save_difference', 'diff.nii'), + ('nlsam_denoising', 'dwi_crop.nii', 'dwi_nlsam.nii', 'bvals', 'bvecs', '-m', 'mask_crop.nii', '-f', '--verbose', '--no_denoising', '--save_sigma', 'sigma.nii', '--save_stab', 'stab.nii', '--load_mhat', 'dwi_crop.nii', '--save_eta', 'eta.nii'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '-N', '1', '--noise_est', 'local_std', '--block_size', '2,2,2', '--save_sigma', 'sigma.nii.gz', '--cores', '1'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '-N', '1', '--load_sigma', 'sigma.nii.gz', '--no_subsample', '--no_clip_eta'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--noise_map', 'noise.nii.gz', '--noise_mask', 'pmask.nii.gz', '--use_f32'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '-N', '1', '--load_sigma', 'sigma.nii.gz', '--no_stabilization', '--no_denoising'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '-N', '1', '--load_sigma', 'sigma.nii.gz', '--no_denoising', '--no_clip_eta'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '-N', '1', '--noise_est', 'local_std', '--no_denoising'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--noise_est', 'auto', '--no_denoising', '--cores', '4'), + ('nlsam_denoising', 'dwi_crop.nii.gz', 'dwi_nlsam.nii.gz', 'bvals', 'bvecs', '-m', 'mask_crop.nii.gz', '-f', '--verbose', '--no_denoising') ] cwd = Path(__file__).parent / Path("datasets") diff --git a/pyproject.toml b/pyproject.toml index 14bb167..073ab12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,13 +2,12 @@ requires = ["Cython>=3.0", "scipy>=1.5", "numpy>=2.0", - "setuptools>=61.0", - "wheel"] + "setuptools>=78.0"] build-backend = "setuptools.build_meta" [project] name = "nlsam" -version = '0.7.3' +version = '1.0' authors = [{name = "Samuel St-Jean"}] description='Implementation of "Non Local Spatial and Angular Matching : Enabling higher spatial resolution diffusion MRI datasets through adaptive denoising"' readme = "README.md" From 290981f24cc9bb6f86ea1bb14ff55899c71095c7 Mon Sep 17 00:00:00 2001 From: Samuel St-Jean <3030760+samuelstjean@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:42:22 -0500 Subject: [PATCH 3/4] Add main_workaround_joblib function call --- nlsam/script.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nlsam/script.py b/nlsam/script.py index 17d33c2..31b1ef6 100644 --- a/nlsam/script.py +++ b/nlsam/script.py @@ -509,3 +509,7 @@ def main_workaround_joblib(): main() else: main() + + +if __name__ == "__main__": + main_workaround_joblib() From 494de6652d25ccd39e75724d095c1fcca5ae2f5c Mon Sep 17 00:00:00 2001 From: Samuel St-Jean <3030760+samuelstjean@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:55:47 -0500 Subject: [PATCH 4/4] Revise CHANGELOG for version updates and requirements Updated changelog with internal rework details and changes. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe9a82..57bdb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ - Some internal rework for faster processing by an order of magnitude - spams-bin 2.6.12 or higher is now *required* as it fixes a positivity bug needed in the solver - Some cleanups of old deprecated command line options - +- Frozen builds are now using the threading interface + ## [0.7.3] - 2025-10-06 - Removed `PIESNO` as a noise estimation method. Use `auto` instead, which is the new default since 0.7, as it will automatically estimate `N` for you.