|  | 
| 1 |  | -# import cvxpy as cp | 
|  | 1 | +import cvxpy as cp | 
| 2 | 2 | import numpy as np | 
| 3 | 3 | from scipy.optimize import minimize | 
| 4 |  | -from scipy.sparse import coo_matrix, csc_matrix, diags | 
|  | 4 | +from scipy.sparse import coo_matrix, diags | 
| 5 | 5 | 
 | 
| 6 | 6 | 
 | 
| 7 | 7 | class SNMFOptimizer: | 
| @@ -451,56 +451,48 @@ def apply_transformation_matrix(self, stretch=None, weights=None, residuals=None | 
| 451 | 451 | 
 | 
| 452 | 452 |         return stretch_transformed | 
| 453 | 453 | 
 | 
| 454 |  | -    def solve_quadratic_program(self, t, m, alg="trust-constr"): | 
|  | 454 | +    def solve_quadratic_program(self, t, m): | 
| 455 | 455 |         """ | 
| 456 |  | -        Solves the quadratic program for updating y in stretched NMF using scipy.optimize: | 
|  | 456 | +        Solves the quadratic program for updating y in stretched NMF: | 
| 457 | 457 | 
 | 
| 458 |  | -            min J(y) = 0.5 * y^T Q y + d^T y | 
|  | 458 | +            min J(y) = 0.5 * y^T q y + d^T y | 
| 459 | 459 |             subject to: 0 ≤ y ≤ 1 | 
| 460 | 460 | 
 | 
| 461 |  | -        Uses the 'trust-constr' solver with the analytical gradient and Hessian. | 
| 462 |  | -        Alternatively, can use scipy's L-BFGS-B algorithm, which supports bound | 
| 463 |  | -        constraints. | 
| 464 |  | -
 | 
| 465 | 461 |         Parameters: | 
| 466 |  | -        - t: (N, K) ndarray | 
| 467 |  | -            Matrix computed from getAfun(A(k, m), X[:, k]). | 
| 468 |  | -        - m: int | 
| 469 |  | -            Index of the current column in source_matrix. | 
|  | 462 | +        - t: (N, k) ndarray | 
|  | 463 | +        - source_matrix_col: (N,) column of source_matrix for the corresponding m | 
| 470 | 464 | 
 | 
| 471 | 465 |         Returns: | 
| 472 |  | -        - y: (k,) ndarray | 
| 473 |  | -            Optimal solution for y, clipped to ensure non-negativity. | 
|  | 466 | +        - y: (k,) optimal solution | 
| 474 | 467 |         """ | 
|  | 468 | + | 
| 475 | 469 |         source_matrix_col = self.source_matrix[:, m] | 
| 476 |  | -        q = t.T @ t | 
| 477 |  | -        d = -t.T @ source_matrix_col | 
| 478 |  | -        k = q.shape[0] | 
| 479 |  | -        reg_factor = 1e-8 * np.linalg.norm(q, ord="fro") | 
| 480 |  | -        q += np.eye(k) * reg_factor | 
| 481 | 470 | 
 | 
| 482 |  | -        def objective(y): | 
| 483 |  | -            return 0.5 * y @ q @ y + d @ y | 
|  | 471 | +        # Compute q and d | 
|  | 472 | +        q = t.T @ t  # Gram matrix (k x k) | 
|  | 473 | +        d = -t.T @ source_matrix_col  # Linear term (k,) | 
| 484 | 474 | 
 | 
| 485 |  | -        def grad(y): | 
| 486 |  | -            return q @ y + d | 
|  | 475 | +        k = q.shape[0]  # Number of variables | 
| 487 | 476 | 
 | 
| 488 |  | -        if alg == "trust-constr": | 
|  | 477 | +        # Regularize q to ensure positive semi-definiteness | 
|  | 478 | +        reg_factor = 1e-8 * np.linalg.norm(q, ord="fro")  # Adaptive regularization, original was fixed | 
|  | 479 | +        q += np.eye(k) * reg_factor | 
| 489 | 480 | 
 | 
| 490 |  | -            def hess(y): | 
| 491 |  | -                return csc_matrix(q)  # sparse format for efficiency | 
|  | 481 | +        # Define optimization variable | 
|  | 482 | +        y = cp.Variable(k) | 
| 492 | 483 | 
 | 
| 493 |  | -            bounds = [(0, 1)] * k | 
| 494 |  | -            y0 = np.clip(-np.linalg.solve(q + np.eye(k) * 1e-5, d), 0, 1) | 
| 495 |  | -            result = minimize( | 
| 496 |  | -                objective, y0, method="trust-constr", jac=grad, hess=hess, bounds=bounds, options={"verbose": 0} | 
| 497 |  | -            ) | 
| 498 |  | -        elif alg == "L-BFGS-B": | 
| 499 |  | -            bounds = [(0, 1) for _ in range(k)]  # per-variable bounds | 
| 500 |  | -            y0 = np.clip(-np.linalg.solve(q + np.eye(k) * 1e-5, d), 0, 1)  # Initial guess | 
| 501 |  | -            result = minimize(objective, y0, method="L-BFGS-B", jac=grad, bounds=bounds) | 
|  | 484 | +        # Define quadratic objective | 
|  | 485 | +        objective = cp.Minimize(0.5 * cp.quad_form(y, q) + d.T @ y) | 
|  | 486 | + | 
|  | 487 | +        # Define constraints (0 ≤ y ≤ 1) | 
|  | 488 | +        constraints = [y >= 0, y <= 1] | 
|  | 489 | + | 
|  | 490 | +        # Solve using a QP solver | 
|  | 491 | +        prob = cp.Problem(objective, constraints) | 
|  | 492 | +        prob.solve(solver=cp.OSQP, verbose=False) | 
| 502 | 493 | 
 | 
| 503 |  | -        return np.maximum(result.x, 0) | 
|  | 494 | +        # Get the solution | 
|  | 495 | +        return np.maximum(y.value, 0)  # Ensure non-negative values in case of solver tolerance issues | 
| 504 | 496 | 
 | 
| 505 | 497 |     def update_components(self): | 
| 506 | 498 |         """ | 
|  | 
0 commit comments