Repository navigation
🐛 Replace nalgebra's SVD with a null-space KKT solve and Jacobi eigendecomposition - #12
Merged
Merged
Conversation
thinkjrs
commented
Oct 27, 2026
thinkjrs
commented
Oct 27, 2026
thinkjrs
commented
Oct 27, 2026
thinkjrs
commented
Oct 27, 2026
thinkjrs
commented
Oct 27, 2026
| if sigma <= cutoff { | ||
| let v = v_t.row(k).transpose(); | ||
| let problem = Scaled::new(q, e)?; | ||
| let z = Constraints::new(&problem.e).null; |
Owner
Author
There was a problem hiding this comment.
Duplicate decomposition. When solve_noisy_kkt returns None in the active-set loop, descent_ray is called on the same q_free/e_free and rebuilds everything: Scaled::new, Gram–Schmidt, the full Householder QR, ZᵀQZ, the nalgebra eigen warm start and the Jacobi sweeps. That is O(n³) work done twice per zero-curvature step. Return (z, eigen, cutoff) from the failed solve, or factor a shared struct, so the ray reuses it.
Owner
Author
There was a problem hiding this comment.
Deferred. This is the same work as finding 7 on #11, reusing factorizations across active-set steps, so it goes with that. It only runs on zero-curvature steps, which plain min variance never takes.
thinkjrs
commented
Oct 27, 2026
thinkjrs
commented
Oct 27, 2026
…SingularCovariance on decomposition failure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes nalgebra's SVD from cutup. On rank-deficient and heavily shrunk covariances it could return factors that don't reconstruct the matrix, so min variance, mean-variance, max Sharpe and long-only min variance returned
SingularCovariance, or slightly wrong weights, on valid input. Equality-constrained problems are now solved by the null-space method, and the decompositions that need a pseudo-inverse or a null space use a Jacobi eigensolver written for cutup. Weights are unchanged up to rounding, including the minimum-norm weights on a singular covariance. No API changes.The problem
PR #11 moved the SVD to nalgebra's default tolerance and added iterative refinement. That fixed the shrunk-covariance case, but not rank-deficient ones:
try_svdat5εstill reconstructed with errors as large as 1.13, on 665 of 200k random matrices. Atεthe worst error was 3.5, on 4,594 of them.symmetric_eigendoes better, but still reaches errors of 1e-9, on 61 of 200k.ColPivQRpivots on the largest single entry rather than the largest column norm, so it doesn't reveal rank and can't stand in.On a stress run of 60k random singular problems, about 4 failed with
SingularCovariance. One failure is plain min variance on 3 observations of 4 assets, which is now a regression test.Upgrading nalgebra isn't an option in a patch release, since nalgebra types are in cutup's public API.
Jacobi eigensolver (
src/eigen.rs)SymmetricEigen::newuses cyclic Jacobi (Golub and Van Loan, Algorithm 8.5.3). Jacobi rotations are backward stable: the factors reconstruct the matrix to within rounding of its norm.VᵀAV, whereVholds nalgebra's eigenvectors made exactly orthonormal by a QR decomposition. Any orthonormalVgives the same eigenvalues, and a good one leaves only a sweep or two of work. nalgebra runs at5εwith a cap of30·nQR iterations, as in LAPACK; if it doesn't converge,Vis the identity.Ais divided by its largest entry first, so the stopping test can't overflow or underflow at extreme scales.ε‖A‖. Non-finite input, or 100 sweeps without converging, returnsNone, which callers report asSingularCovariance, the error the SVD path returned.split(cutoff)separates the range from the null space for positive semidefinite matrices. Eigenvalues at or below the cutoff, negative rounding included, go to the null space. The KKT solve, the zero-curvature direction and max Sharpe all use it.τ²would overflow, the rotation usest ≈ 1/2τ.Null-space KKT solve (
src/qp.rs)solve_kktsolvesmin ½xᵀKx − cᵀxsubject toEᵀx = bby the null-space method (Nocedal and Wright, §16.2), in place of a pseudo-inverse of the whole KKT matrix:Kis divided by its average diagonal entry, and each constraint column by its largest entry.E = YT, whereYis an orthonormal basis ofE's range. Dependent columns are dropped, which still detectsDegenerateExpectedReturns. Householder QR ofYcompletes the null-space basisZ.x₀ = Yuis the smallest point satisfying the constraints, found by forward substitution onT. Inconsistent constraints fail the residual check below.ZᵀKZis decomposed by the Jacobi solver. Its pseudo-inverse, over eigenvalues above the cutoffmax(|λ|, |K|)·(n+1)·ε, givesv, andx = x₀ + Zvis the minimum-norm optimum. That's the same answer the pseudo-inverse of the KKT matrix gave.λcomes from back substitution onTagainstc − Kx, with zeros for dependent constraints.1e-8of the problem's size, there is no optimum and the solve returnsNone, as before. Anoisefloor lets the active-set solver pass its own rounding.The SVD tolerance constant and the iterative refinement are gone. The active-set solver's zero-curvature direction, used when
Qis singular, is built from the sameZand the null eigenvectors of the reduced system.Max Sharpe (
src/lib.rs)Max Sharpe splits Σ into its range and null space with
SymmetricEigen::splitinstead of the SVD. Σ is positive semidefinite, so its eigenvalues are its singular values and the cutoff is unchanged.Tests
split.1e-14·‖A‖·nand thatVis orthonormal.test_min_variance_on_rank_deficient_kkt. Min variance on 3 observations of 4 assets matches NumPy's minimum-norm answer to 1e-9, and long-only min variance reaches zero variance. Plain min variance fails on this input ondevelop.PROPTEST_CASES=50000pass. That includesmax_sharpe_ignores_column_order_and_joint_scale, which used to fail about one run in three.Performance
Long-only problems still refactor the KKT system at every active-set step. Updating the factorization instead is review finding 7 from #11, left for later.