From 0e4f2a3798c8c2503c3dccb2a00d3061540bb259 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:08:07 -0800 Subject: [PATCH 01/13] Added convergence flag to fit() --- src/diffpy/stretched_nmf/snmf_class.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index d5f6603..566e965 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -210,7 +210,8 @@ def fit(self, rho=0, eta=0, reset=True): the output of the previous fit() as their input. """ - + self.converged_ = False + if reset: self.components_ = self.init_components.copy() self.weights_ = self.init_weights.copy() @@ -294,6 +295,7 @@ def fit(self, rho=0, eta=0, reset=True): self.objective_difference < self.objective_function * self.tol and outiter >= self.min_iter ): + self.converged_ = True break self.normalize_results() From c2a825fb1dcb07bf7deadb1da0f8dd0378e81cd9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:14:09 +0000 Subject: [PATCH 02/13] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/stretched_nmf/snmf_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 566e965..f96c854 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -211,7 +211,7 @@ def fit(self, rho=0, eta=0, reset=True): fit() as their input. """ self.converged_ = False - + if reset: self.components_ = self.init_components.copy() self.weights_ = self.init_weights.copy() From fe8102411aacbf4e09d22d2bd8afbe5c50c5cd9c Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:17:56 -0800 Subject: [PATCH 03/13] Added news --- news/add-convergence-flag.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 news/add-convergence-flag.rst diff --git a/news/add-convergence-flag.rst b/news/add-convergence-flag.rst new file mode 100644 index 0000000..e4776b7 --- /dev/null +++ b/news/add-convergence-flag.rst @@ -0,0 +1,25 @@ +**Added:** + +* SNMFOptimizer.converged_ attribute to indicate whether the optimization + successfully reached the convergence tolerance (True) or stopped because the + maximum number of iterations was reached (False). + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From f24c5e3b8e6cbcec134b0c201933b1f999014718 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:36:50 -0800 Subject: [PATCH 04/13] Update snmf_class.py Was giving me errors due to use of python 3.11 --- src/diffpy/stretched_nmf/snmf_class.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index f96c854..35692b9 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -252,11 +252,12 @@ def fit(self, rho=0, eta=0, reset=True): sparsity_term = self.eta * np.sum( np.sqrt(self.components_) ) # Square root penalty + obj_diff = ( + self.objective_function - regularization_term - sparsity_term + ) print( f"Start, Objective function: {self.objective_function:.5e}" - f", Obj - reg/sparse: {self.objective_function - - regularization_term - - sparsity_term:.5e}" + f", Obj - reg/sparse: {obj_diff:.5e}" ) # Main optimization loop @@ -275,11 +276,12 @@ def fit(self, rho=0, eta=0, reset=True): sparsity_term = self.eta * np.sum( np.sqrt(self.components_) ) # Square root penalty + obj_diff = ( + self.objective_function - regularization_term - sparsity_term + ) print( f"Obj fun: {self.objective_function:.5e}, " - f"Obj - reg/sparse: {self.objective_function - - regularization_term - - sparsity_term:.5e}, " + f", Obj - reg/sparse: {obj_diff:.5e}" f"Iter: {self.outiter}" ) From 7eca89d3f7acd604a3eefc68ff9d51a7739abecc Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:14:39 -0700 Subject: [PATCH 05/13] Update snmf_class.py Optimized print messages Added verbose option Added objective_log attribute to track objective function updates, with associated time stamps and specifying what matrix has been updated --- src/diffpy/stretched_nmf/snmf_class.py | 120 ++++++++++++++++++------- 1 file changed, 86 insertions(+), 34 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 35692b9..1270bf2 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -1,3 +1,5 @@ +import time + import cvxpy as cp import numpy as np from scipy.optimize import minimize @@ -82,6 +84,7 @@ def __init__( n_components=None, random_state=None, show_plots=False, + verbose=True, ): """Initialize an instance of sNMF. @@ -131,6 +134,7 @@ def __init__( self.num_updates = 0 self._rng = np.random.default_rng(random_state) self.plotter = SNMFPlotter() if show_plots else None + self.verbose = verbose # Enforce exclusive specification of n_components or init_weights if (n_components is None and init_weights is None) or ( @@ -183,6 +187,7 @@ def __init__( [1, -2, 1], offsets=[0, 1, 2], shape=(self.n_signals - 2, self.n_signals), + dtype=float, ) def fit(self, rho=0, eta=0, reset=True): @@ -235,6 +240,13 @@ def fit(self, rho=0, eta=0, reset=True): ] self.objective_difference = None self._objective_history = [self.objective_function] + self.objective_log = [ + { + "step": "start", + "objective": self.objective_function, + "timestamp": time.time(), + } + ] # Set up tracking variables for update_components() self._prev_components = None @@ -255,10 +267,11 @@ def fit(self, rho=0, eta=0, reset=True): obj_diff = ( self.objective_function - regularization_term - sparsity_term ) - print( - f"Start, Objective function: {self.objective_function:.5e}" - f", Obj - reg/sparse: {obj_diff:.5e}" - ) + if self.verbose: + print( + f"Start, Objective function: {self.objective_function:.5e}" + f", Obj - reg/sparse: {obj_diff:.5e}" + ) # Main optimization loop for outiter in range(self.max_iter): @@ -279,22 +292,19 @@ def fit(self, rho=0, eta=0, reset=True): obj_diff = ( self.objective_function - regularization_term - sparsity_term ) - print( - f"Obj fun: {self.objective_function:.5e}, " - f", Obj - reg/sparse: {obj_diff:.5e}" - f"Iter: {self.outiter}" - ) - + convergence_threshold = self.objective_function * self.tol # Convergence check: Stop if diffun is small # and at least min_iter iterations have passed - print( - "Checking if ", - self.objective_difference, - " < ", - self.objective_function * self.tol, - ) + if self.verbose: + print( + f"\n--- Iteration {self.outiter} ---" + f"\nTotal Objective : {self.objective_function:.5e}" + f"\nBase Obj (No Reg) : {obj_diff:.5e}" + f"\nConvergence Check : Δ {self.objective_difference:.5e}" + f" < {convergence_threshold:.5e} (Threshold)\n" + ) if ( - self.objective_difference < self.objective_function * self.tol + self.objective_difference < convergence_threshold and outiter >= self.min_iter ): self.converged_ = True @@ -305,6 +315,8 @@ def fit(self, rho=0, eta=0, reset=True): return self def normalize_results(self): + if self.verbose: + print("\nNormalizing results after convergence...") # Select our best results for normalization self.components_ = self.best_matrices[0] self.weights_ = self.best_matrices[1] @@ -335,11 +347,18 @@ def normalize_results(self): self.update_components() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - print( - f"Objective function after normalize_components: " - f"{self.objective_function:.5e}" - ) + # print( + # f"Objective function after normalize_components: " + # f"{self.objective_function:.5e}" + # ) self._objective_history.append(self.objective_function) + self.objective_log = [ + { + "step": "c_norm", + "objective": self.objective_function, + "timestamp": time.time(), + } + ] self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] ) @@ -357,16 +376,25 @@ def normalize_results(self): break def outer_loop(self): + if self.verbose: + print("Updating components and weights in outer loop...") for iter in range(4): self.iter = iter self._prev_grad_components = self._grad_components.copy() self.update_components() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - print( - f"Objective function after update_components: " - f"{self.objective_function:.5e}" - ) + self.objective_log = [ + { + "step": "c", + "objective": self.objective_function, + "timestamp": time.time(), + } + ] + # print( + # f"Objective function after update_components: " + # f"{self.objective_function:.5e}" + # ) self._objective_history.append(self.objective_function) self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] @@ -389,11 +417,19 @@ def outer_loop(self): self.update_weights() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - print( - f"Objective function after update_weights: " - f"{self.objective_function:.5e}" - ) + # print( + # f"Objective function after update_weights: " + # f"{self.objective_function:.5e}" + # ) self._objective_history.append(self.objective_function) + self.objective_log = [ + { + "step": "w", + "objective": self.objective_function, + "timestamp": time.time(), + } + ] + self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] ) @@ -426,11 +462,18 @@ def outer_loop(self): self.update_stretch() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - print( - f"Objective function after update_stretch: " - f"{self.objective_function:.5e}" - ) + # print( + # f"Objective function after update_stretch: " + # f"{self.objective_function:.5e}" + # ) self._objective_history.append(self.objective_function) + self.objective_log = [ + { + "step": "s", + "objective": self.objective_function, + "timestamp": time.time(), + } + ] self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] ) @@ -712,7 +755,12 @@ def solve_quadratic_program(self, t, m): # Solve using a QP solver prob = cp.Problem(objective, constraints) - prob.solve(solver=cp.OSQP, verbose=False) + prob.solve( + solver=cp.OSQP, + verbose=False, + polish=False, # TODO keep? removes polish message + # solver_verbose=False + ) # Get the solution return np.maximum( @@ -722,6 +770,7 @@ def solve_quadratic_program(self, t, m): def update_components(self): """Updates `components` using gradient-based optimization with adaptive step size.""" + # Compute stretched components using the interpolation function stretched_components, _, _ = ( self.compute_stretched_components() @@ -868,6 +917,9 @@ def update_stretch(self): """Updates stretching matrix using constrained optimization (equivalent to fmincon in MATLAB).""" + if self.verbose: + print("Updating stretch factors...") + # Flatten stretch for compatibility with the optimizer # (since SciPy expects 1D input) stretch_flat_initial = self.stretch_.flatten() From d5a35687604c5a1f10769f0e072c70ebb90e9db4 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:26:41 -0700 Subject: [PATCH 06/13] Update snmf_class.py --- src/diffpy/stretched_nmf/snmf_class.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 1270bf2..3782673 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -269,8 +269,9 @@ def fit(self, rho=0, eta=0, reset=True): ) if self.verbose: print( - f"Start, Objective function: {self.objective_function:.5e}" - f", Obj - reg/sparse: {obj_diff:.5e}" + f"\n--- Start ---" + f"\nTotal Objective : {self.objective_function:.5e}" + f"\nBase Obj (No Reg) : {obj_diff:.5e}" ) # Main optimization loop @@ -300,8 +301,9 @@ def fit(self, rho=0, eta=0, reset=True): f"\n--- Iteration {self.outiter} ---" f"\nTotal Objective : {self.objective_function:.5e}" f"\nBase Obj (No Reg) : {obj_diff:.5e}" - f"\nConvergence Check : Δ {self.objective_difference:.5e}" - f" < {convergence_threshold:.5e} (Threshold)\n" + "\nConvergence Check : Δ " + f"({self.objective_difference:.2e})" + f" < Threshold ({convergence_threshold:.2e})\n" ) if ( self.objective_difference < convergence_threshold From 5662d605f283a5e444d222904b69bbeead925090 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:55:18 -0700 Subject: [PATCH 07/13] Update snmf_class.py Fixed wrong setting of objective_log --- src/diffpy/stretched_nmf/snmf_class.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 3782673..2815707 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -354,13 +354,13 @@ def normalize_results(self): # f"{self.objective_function:.5e}" # ) self._objective_history.append(self.objective_function) - self.objective_log = [ + self.objective_log.append( { "step": "c_norm", "objective": self.objective_function, "timestamp": time.time(), } - ] + ) self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] ) @@ -386,13 +386,13 @@ def outer_loop(self): self.update_components() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - self.objective_log = [ + self.objective_log.append( { "step": "c", "objective": self.objective_function, "timestamp": time.time(), } - ] + ) # print( # f"Objective function after update_components: " # f"{self.objective_function:.5e}" @@ -424,13 +424,13 @@ def outer_loop(self): # f"{self.objective_function:.5e}" # ) self._objective_history.append(self.objective_function) - self.objective_log = [ + self.objective_log.append( { "step": "w", "objective": self.objective_function, "timestamp": time.time(), } - ] + ) self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] @@ -469,13 +469,13 @@ def outer_loop(self): # f"{self.objective_function:.5e}" # ) self._objective_history.append(self.objective_function) - self.objective_log = [ + self.objective_log.append( { "step": "s", "objective": self.objective_function, "timestamp": time.time(), } - ] + ) self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] ) From f69c1a16ea47650f96e77901639db720e776795d Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:30:04 -0700 Subject: [PATCH 08/13] Update snmf_class.py remove commented print messages --- src/diffpy/stretched_nmf/snmf_class.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 2815707..28ca6c7 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -349,10 +349,6 @@ def normalize_results(self): self.update_components() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - # print( - # f"Objective function after normalize_components: " - # f"{self.objective_function:.5e}" - # ) self._objective_history.append(self.objective_function) self.objective_log.append( { @@ -393,10 +389,6 @@ def outer_loop(self): "timestamp": time.time(), } ) - # print( - # f"Objective function after update_components: " - # f"{self.objective_function:.5e}" - # ) self._objective_history.append(self.objective_function) self.objective_difference = ( self._objective_history[-2] - self._objective_history[-1] @@ -419,10 +411,6 @@ def outer_loop(self): self.update_weights() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - # print( - # f"Objective function after update_weights: " - # f"{self.objective_function:.5e}" - # ) self._objective_history.append(self.objective_function) self.objective_log.append( { @@ -464,10 +452,6 @@ def outer_loop(self): self.update_stretch() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - # print( - # f"Objective function after update_stretch: " - # f"{self.objective_function:.5e}" - # ) self._objective_history.append(self.objective_function) self.objective_log.append( { From 90c81a8799b7e139471da22f0c3f5e403f6218d7 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:43:58 -0700 Subject: [PATCH 09/13] Update snmf_class.py Added "iteration" key to objective_log Removed redundant _objective_history attribute --- src/diffpy/stretched_nmf/snmf_class.py | 39 +++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 28ca6c7..39a4324 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -239,10 +239,10 @@ def fit(self, rho=0, eta=0, reset=True): self.stretch_.copy(), ] self.objective_difference = None - self._objective_history = [self.objective_function] self.objective_log = [ { "step": "start", + "iteration": 0, "objective": self.objective_function, "timestamp": time.time(), } @@ -340,7 +340,6 @@ def normalize_results(self): self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() self.objective_difference = None - self._objective_history = [self.objective_function] self.outiter = 0 self.iter = 0 for outiter in range(self.max_iter): @@ -349,16 +348,17 @@ def normalize_results(self): self.update_components() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - self._objective_history.append(self.objective_function) self.objective_log.append( { "step": "c_norm", + "iteration": outiter, "objective": self.objective_function, "timestamp": time.time(), } ) self.objective_difference = ( - self._objective_history[-2] - self._objective_history[-1] + self.objective_log[-2]["objective"] + - self.objective_log[-1]["objective"] ) if self.plotter is not None: self.plotter.update( @@ -385,13 +385,14 @@ def outer_loop(self): self.objective_log.append( { "step": "c", + "iteration": self.outiter, "objective": self.objective_function, "timestamp": time.time(), } ) - self._objective_history.append(self.objective_function) self.objective_difference = ( - self._objective_history[-2] - self._objective_history[-1] + self.objective_log[-2]["objective"] + - self.objective_log[-1]["objective"] ) if self.objective_function < self.best_objective: self.best_objective = self.objective_function @@ -411,17 +412,18 @@ def outer_loop(self): self.update_weights() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - self._objective_history.append(self.objective_function) self.objective_log.append( { "step": "w", + "iteration": self.outiter, "objective": self.objective_function, "timestamp": time.time(), } ) self.objective_difference = ( - self._objective_history[-2] - self._objective_history[-1] + self.objective_log[-2]["objective"] + - self.objective_log[-1]["objective"] ) if self.objective_function < self.best_objective: self.best_objective = self.objective_function @@ -439,10 +441,11 @@ def outer_loop(self): ) self.objective_difference = ( - self._objective_history[-2] - self._objective_history[-1] + self.objective_log[-2]["objective"] + - self.objective_log[-1]["objective"] ) if ( - self._objective_history[-3] - self.objective_function + self.objective_log[-3]["objective"] - self.objective_function < self.objective_difference * 1e-3 ): break @@ -452,16 +455,17 @@ def outer_loop(self): self.update_stretch() self.residuals = self.get_residual_matrix() self.objective_function = self.get_objective_function() - self._objective_history.append(self.objective_function) self.objective_log.append( { "step": "s", + "iteration": self.outiter, "objective": self.objective_function, "timestamp": time.time(), } ) self.objective_difference = ( - self._objective_history[-2] - self._objective_history[-1] + self.objective_log[-2]["objective"] + - self.objective_log[-1]["objective"] ) if self.objective_function < self.best_objective: self.best_objective = self.objective_function @@ -820,10 +824,13 @@ def update_components(self): ) self.components_ = mask * self.components_ - objective_improvement = self._objective_history[ - -1 - ] - self.get_objective_function( - residuals=self.get_residual_matrix() + # self.objective_function is the baseline value cached right + # before we called update_components() + objective_improvement = ( + self.objective_function + - self.get_objective_function( + residuals=self.get_residual_matrix() + ) ) # Check if objective function improves From 128cb5baba5239deb0180506b5787b6239466cf5 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:52:50 -0700 Subject: [PATCH 10/13] Create optimize-messages.rst --- news/optimize-messages.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 news/optimize-messages.rst diff --git a/news/optimize-messages.rst b/news/optimize-messages.rst new file mode 100644 index 0000000..6ec132f --- /dev/null +++ b/news/optimize-messages.rst @@ -0,0 +1,32 @@ +**Added:** + +* 'SNMFOptimizer.objective_log' attr: dictionary list to track the optimization + process, recording the step, iteration, objective, and timestamp at each update. + Uses the 'step', 'iteration', 'objective' and 'timestamp' keys. +* 'SNMFOptimizer(verbose : Optional[bool])' option and SNMFOptimizer.verbose + attribute to allow users to toggle diagnostic console output. + +**Changed:** + +* Modified all print messages for improved readability and tied them to the new + verbose flag. +* Refactored convergence checks and step-size calculations to pull objective + values directly from objective_log instead of relying on a separate history + array. + +**Deprecated:** + +* + +**Removed:** + +* Removed the 'SNMFOptimizer._objective_history' list, which was made redundant + by the comprehensive 'SNMFOptimizer.objective_log' tracking system. + +**Fixed:** + +* + +**Security:** + +* From 8ea3998837b8eca1c73bb4dac091d544c1e55866 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:35:51 -0700 Subject: [PATCH 11/13] Update snmf_class.py --- src/diffpy/stretched_nmf/snmf_class.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 39a4324..0af3e2e 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -264,14 +264,14 @@ def fit(self, rho=0, eta=0, reset=True): sparsity_term = self.eta * np.sum( np.sqrt(self.components_) ) # Square root penalty - obj_diff = ( + base_obj = ( self.objective_function - regularization_term - sparsity_term ) if self.verbose: print( f"\n--- Start ---" f"\nTotal Objective : {self.objective_function:.5e}" - f"\nBase Obj (No Reg) : {obj_diff:.5e}" + f"\nBase Obj (No Reg) : {base_obj:.5e}" ) # Main optimization loop @@ -290,7 +290,7 @@ def fit(self, rho=0, eta=0, reset=True): sparsity_term = self.eta * np.sum( np.sqrt(self.components_) ) # Square root penalty - obj_diff = ( + base_obj = ( self.objective_function - regularization_term - sparsity_term ) convergence_threshold = self.objective_function * self.tol @@ -300,7 +300,7 @@ def fit(self, rho=0, eta=0, reset=True): print( f"\n--- Iteration {self.outiter} ---" f"\nTotal Objective : {self.objective_function:.5e}" - f"\nBase Obj (No Reg) : {obj_diff:.5e}" + f"\nBase Obj (No Reg) : {base_obj:.5e}" "\nConvergence Check : Δ " f"({self.objective_difference:.2e})" f" < Threshold ({convergence_threshold:.2e})\n" @@ -367,15 +367,24 @@ def normalize_results(self): stretch=self.stretch_, update_tag="normalize components", ) + convergence_threshold = self.objective_function * self.tol + if self.verbose: + print( + f"\n--- Iteration {outiter} after normalization---" + f"\nTotal Objective : {self.objective_function:.5e}" + "\nConvergence Check : Δ " + f"({self.objective_difference:.2e})" + f" < Threshold ({convergence_threshold:.2e})\n" + ) if ( - self.objective_difference < self.objective_function * self.tol + self.objective_difference < convergence_threshold and outiter >= 7 ): break def outer_loop(self): if self.verbose: - print("Updating components and weights in outer loop...") + print("Updating components and weights...") for iter in range(4): self.iter = iter self._prev_grad_components = self._grad_components.copy() From ab71e11dc0862a325f1d64b3c263858f898f996b Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:36:23 -0700 Subject: [PATCH 12/13] Update snmf_class.py --- src/diffpy/stretched_nmf/snmf_class.py | 55 ++++++++++++++------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 1e77c0f..9155b05 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -218,6 +218,7 @@ def _initialize_factors( [1, -2, 1], offsets=[0, 1, 2], shape=(self.n_signals_ - 2, self.n_signals_), + dtype=float, ) def fit( @@ -306,12 +307,12 @@ def fit( self.weights_.copy(), self.stretch_.copy(), ] - self.objective_difference = None + self.objective_difference_ = None self.objective_log = [ { "step": "start", "iteration": 0, - "objective": self.objective_function, + "objective": self.objective_function_, "timestamp": time.time(), } ] @@ -334,12 +335,12 @@ def fit( np.sqrt(self.components_) ) # Square root penalty base_obj = ( - self.objective_function - regularization_term - sparsity_term + self.objective_function_ - regularization_term - sparsity_term ) if self.verbose: print( f"\n--- Start ---" - f"\nTotal Objective : {self.objective_function:.5e}" + f"\nTotal Objective : {self.objective_function_:.5e}" f"\nBase Obj (No Reg) : {base_obj:.5e}" ) @@ -361,22 +362,22 @@ def fit( np.sqrt(self.components_) ) # Square root penalty base_obj = ( - self.objective_function - regularization_term - sparsity_term + self.objective_function_ - regularization_term - sparsity_term ) - convergence_threshold = self.objective_function * self.tol + convergence_threshold = self.objective_function_ * self.tol # Convergence check: Stop if diffun is small # and at least min_iter iterations have passed if self.verbose: print( - f"\n--- Iteration {self.outiter} ---" - f"\nTotal Objective : {self.objective_function:.5e}" + f"\n--- Iteration {self._outer_iter} ---" + f"\nTotal Objective : {self.objective_function_:.5e}" f"\nBase Obj (No Reg) : {base_obj:.5e}" "\nConvergence Check : Δ " - f"({self.objective_difference:.2e})" + f"({self.objective_difference_:.2e})" f" < Threshold ({convergence_threshold:.2e})\n" ) if ( - self.objective_difference < convergence_threshold + self.objective_difference_ < convergence_threshold and outiter >= self.min_iter ): self.converged_ = True @@ -427,13 +428,14 @@ def _normalize_results(self): { "step": "c_norm", "iteration": outiter, - "objective": self.objective_function, + "objective": self.objective_function_, "timestamp": time.time(), } ) - self.objective_difference = ( + self.objective_difference_ = ( self.objective_log[-2]["objective"] - self.objective_log[-1]["objective"] + ) if self._plotter is not None: self._plotter.update( components=self.components_, @@ -441,13 +443,13 @@ def _normalize_results(self): stretch=self.stretch_, update_tag="normalize components", ) - convergence_threshold = self.objective_function * self.tol + convergence_threshold = self.objective_function_ * self.tol if self.verbose: print( f"\n--- Iteration {outiter} after normalization---" - f"\nTotal Objective : {self.objective_function:.5e}" + f"\nTotal Objective : {self.objective_function_:.5e}" "\nConvergence Check : Δ " - f"({self.objective_difference:.2e})" + f"({self.objective_difference_:.2e})" f" < Threshold ({convergence_threshold:.2e})\n" ) if ( @@ -468,12 +470,12 @@ def _outer_loop(self): self.objective_log.append( { "step": "c", - "iteration": self.outiter, - "objective": self.objective_function, + "iteration": self._outer_iter, + "objective": self.objective_function_, "timestamp": time.time(), } ) - self.objective_difference = ( + self.objective_difference_ = ( self.objective_log[-2]["objective"] - self.objective_log[-1]["objective"] ) @@ -498,13 +500,13 @@ def _outer_loop(self): self.objective_log.append( { "step": "w", - "iteration": self.outiter, - "objective": self.objective_function, + "iteration": self._outer_iter, + "objective": self.objective_function_, "timestamp": time.time(), } ) - self.objective_difference = ( + self.objective_difference_ = ( self.objective_log[-2]["objective"] - self.objective_log[-1]["objective"] ) @@ -541,12 +543,12 @@ def _outer_loop(self): self.objective_log.append( { "step": "s", - "iteration": self.outiter, - "objective": self.objective_function, + "iteration": self._outer_iter, + "objective": self.objective_function_, "timestamp": time.time(), } ) - self.objective_difference = ( + self.objective_difference_ = ( self.objective_log[-2]["objective"] - self.objective_log[-1]["objective"] ) @@ -914,8 +916,9 @@ def _update_components(self): ) self.components_ = mask * self.components_ - objective_improvement = self.objective_log[-1]["objective"] - - self._get_objective_function( + objective_improvement = self.objective_log[-1][ + "objective" + ] - self._get_objective_function( residuals=self._get_residual_matrix() ) From f504ce6243c17c339420e105ad4b37aa4412af35 Mon Sep 17 00:00:00 2001 From: Andrea-gm <162758982+Andrea-gm@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:18:00 -0700 Subject: [PATCH 13/13] Update snmf_class.py --- src/diffpy/stretched_nmf/snmf_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffpy/stretched_nmf/snmf_class.py b/src/diffpy/stretched_nmf/snmf_class.py index 9155b05..3269570 100644 --- a/src/diffpy/stretched_nmf/snmf_class.py +++ b/src/diffpy/stretched_nmf/snmf_class.py @@ -84,7 +84,7 @@ def __init__( eta=0, random_state=None, show_plots=False, - verbose=True, + verbose=False, ): """Initialize an instance of sNMF with estimator hyperparameters.