From 29e075d930cebc74942b1869f7724b0c1da19cd8 Mon Sep 17 00:00:00 2001 From: Royi Date: Fri, 16 Jan 2026 14:02:49 +0200 Subject: [PATCH 1/8] Add Support for Sparse Matrices in `quadform()` Add support for `SparseMatrixCSC` in `quadform()`. Fixes #725 . --- src/reformulations/quadform.jl | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/reformulations/quadform.jl b/src/reformulations/quadform.jl index 5b05969d2..62cc99653 100644 --- a/src/reformulations/quadform.jl +++ b/src/reformulations/quadform.jl @@ -121,10 +121,31 @@ function quadform(x::AbstractExpr, A::Value; assume_psd = false) else error("Quadratic forms supported only for semidefinite matrices") end - P = sqrt(LinearAlgebra.Hermitian(factor * A)) + # The term `factor * A` is SPSD + P = _squareroot(factor * A) return factor * square(norm2(P * x)) end +function _squareroot(x::AbstractExpr, A::Value;) + # Caluclates matrix `P` such that `A = P' * P` + P = sqrt(LinearAlgebra.Hermitian(A)) + return P +end + +function _squareroot(x::AbstractExpr, A::SparseArrays.SparseMatrixCSC; shift = 1e-10) + # Caluclates matrix `P` such that `A = P' * P` + _chol = cholesky(A; shift = shift, check = false) + if !issuccess(_chol) + error("The Shifted Cholesky decomposition failed. The matrix may not be an SPSD."); + end + # Extract sparse L and permutation + L = sparse(_chol.L); + p = _chol.p; + pinv = invperm(p); + P = L'[:, pinv]; + return P +end + # These `evaluate` calls are safe since they are not a `fix!`ed variable # (must be a constant): function quadform(x::Constant, A::AbstractExpr; kwargs...) From 0bba291776e2978b09d4077c6ff563a2517106fc Mon Sep 17 00:00:00 2001 From: Royi Date: Fri, 16 Jan 2026 17:39:29 +0200 Subject: [PATCH 2/8] Add Support for Sparse Matrices in `quadform()` Add support for `SparseMatrixCSC` in `quadform()`. Fixes #725 . --- src/reformulations/quadform.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reformulations/quadform.jl b/src/reformulations/quadform.jl index 62cc99653..66a74b494 100644 --- a/src/reformulations/quadform.jl +++ b/src/reformulations/quadform.jl @@ -126,13 +126,13 @@ function quadform(x::AbstractExpr, A::Value; assume_psd = false) return factor * square(norm2(P * x)) end -function _squareroot(x::AbstractExpr, A::Value;) +function _squareroot(A::Value) # Caluclates matrix `P` such that `A = P' * P` P = sqrt(LinearAlgebra.Hermitian(A)) return P end -function _squareroot(x::AbstractExpr, A::SparseArrays.SparseMatrixCSC; shift = 1e-10) +function _squareroot(A::SparseArrays.SparseMatrixCSC; shift = 1e-10) # Caluclates matrix `P` such that `A = P' * P` _chol = cholesky(A; shift = shift, check = false) if !issuccess(_chol) From d9f2ae543845c07b32aee70b64f7ee2d5ea4e7fa Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 17 Jan 2026 09:32:55 +1300 Subject: [PATCH 3/8] Refactor square root functions and improve error handling --- src/reformulations/quadform.jl | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/reformulations/quadform.jl b/src/reformulations/quadform.jl index 66a74b494..f97e2beb1 100644 --- a/src/reformulations/quadform.jl +++ b/src/reformulations/quadform.jl @@ -121,29 +121,22 @@ function quadform(x::AbstractExpr, A::Value; assume_psd = false) else error("Quadratic forms supported only for semidefinite matrices") end - # The term `factor * A` is SPSD - P = _squareroot(factor * A) + # Caluclates matrix `P` such that `A = P' * P` + P = _square_root(factor * A) return factor * square(norm2(P * x)) end -function _squareroot(A::Value) - # Caluclates matrix `P` such that `A = P' * P` - P = sqrt(LinearAlgebra.Hermitian(A)) - return P -end +_square_root(A::Value) = sqrt(LinearAlgebra.Hermitian(A)) function _squareroot(A::SparseArrays.SparseMatrixCSC; shift = 1e-10) - # Caluclates matrix `P` such that `A = P' * P` - _chol = cholesky(A; shift = shift, check = false) - if !issuccess(_chol) - error("The Shifted Cholesky decomposition failed. The matrix may not be an SPSD."); + chol = cholesky(A; shift, check = false) + if !issuccess(chol) + error( + "The Shifted Cholesky decomposition failed. The matrix may not be an SPSD.", + ) end - # Extract sparse L and permutation - L = sparse(_chol.L); - p = _chol.p; - pinv = invperm(p); - P = L'[:, pinv]; - return P + L = sparse(chol.L) + return L'[:, invperm(chol.p)] end # These `evaluate` calls are safe since they are not a `fix!`ed variable From 2b9ab0296a329e3b48f6628849b24dc11355f6e6 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Sat, 17 Jan 2026 09:33:15 +1300 Subject: [PATCH 4/8] Update src/reformulations/quadform.jl --- src/reformulations/quadform.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reformulations/quadform.jl b/src/reformulations/quadform.jl index f97e2beb1..2c1aa0891 100644 --- a/src/reformulations/quadform.jl +++ b/src/reformulations/quadform.jl @@ -121,7 +121,7 @@ function quadform(x::AbstractExpr, A::Value; assume_psd = false) else error("Quadratic forms supported only for semidefinite matrices") end - # Caluclates matrix `P` such that `A = P' * P` + # Calculates matrix `P` such that `A = P' * P` P = _square_root(factor * A) return factor * square(norm2(P * x)) end From eb72f3fc4bff0efdf6fa8600f29878680903dec7 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 19 Jan 2026 16:18:36 +1300 Subject: [PATCH 5/8] Add test --- test/test_atoms.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_atoms.jl b/test/test_atoms.jl index 1f48f325a..3cb32c4d9 100644 --- a/test/test_atoms.jl +++ b/test/test_atoms.jl @@ -12,6 +12,7 @@ using Test import LinearAlgebra import MathOptInterface as MOI +import SparseArrays function runtests() for name in names(@__MODULE__; all = true) @@ -2418,6 +2419,12 @@ function test_quadform() _test_reformulation(target) do context return quadform(Variable(2), [20.0 16.0; 16.0 20.0]) end + _test_reformulation(target) do context + return quadform( + Variable(2), + SparseArrays.sparse([20.0 16.0; 16.0 20.0]), + ) + end _test_reformulation(target) do context return quadform(Variable(2), [20.0 16.0; 16.0 20.0]; assume_psd = true) end From b46347cde5fba8ba67e094846a3552677cf45f41 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 19 Jan 2026 16:43:08 +1300 Subject: [PATCH 6/8] Rename and refactor square root function Refactor _squareroot function to _square_root and improve error handling. --- src/reformulations/quadform.jl | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/reformulations/quadform.jl b/src/reformulations/quadform.jl index 2c1aa0891..6e6840cad 100644 --- a/src/reformulations/quadform.jl +++ b/src/reformulations/quadform.jl @@ -128,14 +128,9 @@ end _square_root(A::Value) = sqrt(LinearAlgebra.Hermitian(A)) -function _squareroot(A::SparseArrays.SparseMatrixCSC; shift = 1e-10) - chol = cholesky(A; shift, check = false) - if !issuccess(chol) - error( - "The Shifted Cholesky decomposition failed. The matrix may not be an SPSD.", - ) - end - L = sparse(chol.L) +function _square_root(A::SparseArrays.SparseMatrixCSC) + chol = LinearAlgebra.cholesky(A; shift = 1e-10) + L = SparseArrays.sparse(chol.L) return L'[:, invperm(chol.p)] end From 19a8d0e5702445acaf7808c0ac69b683fb101638 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 19 Jan 2026 16:43:40 +1300 Subject: [PATCH 7/8] Refactor quadform tests and add new reformulation case Removed sparse matrix usage in quadform tests and added a new test case with a target reformulation. --- test/test_atoms.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/test_atoms.jl b/test/test_atoms.jl index 3cb32c4d9..f843cc52f 100644 --- a/test/test_atoms.jl +++ b/test/test_atoms.jl @@ -2419,12 +2419,6 @@ function test_quadform() _test_reformulation(target) do context return quadform(Variable(2), [20.0 16.0; 16.0 20.0]) end - _test_reformulation(target) do context - return quadform( - Variable(2), - SparseArrays.sparse([20.0 16.0; 16.0 20.0]), - ) - end _test_reformulation(target) do context return quadform(Variable(2), [20.0 16.0; 16.0 20.0]; assume_psd = true) end @@ -2466,6 +2460,15 @@ function test_quadform() quadform(Variable(2), [1 0; -2 1]), ) @test quadform(constant([1, 2]), constant([1 2; 2 3])) == 21 + target = """ + variables: u, t, x1, x2 + minobjective: 1.0 * u + [t, 2.000000000025 * x1, 3.0000000000166667*x2] in SecondOrderCone(3) + [u, 0.5, t] in RotatedSecondOrderCone(3) + """ + _test_reformulation(target) do context + return quadform(Variable(2), A) + end return end From 9235307104fa94f447cb55aee308f32ba4815027 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 19 Jan 2026 20:37:49 +1300 Subject: [PATCH 8/8] Update test/test_atoms.jl --- test/test_atoms.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_atoms.jl b/test/test_atoms.jl index f843cc52f..09efd5097 100644 --- a/test/test_atoms.jl +++ b/test/test_atoms.jl @@ -2466,6 +2466,7 @@ function test_quadform() [t, 2.000000000025 * x1, 3.0000000000166667*x2] in SecondOrderCone(3) [u, 0.5, t] in RotatedSecondOrderCone(3) """ + A = SparseArrays.sparse([4.0 0.0; 0.0 9.0]) _test_reformulation(target) do context return quadform(Variable(2), A) end