From aaafc619b2425e4c2c5887bac0e507f1549c1914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 10 Jul 2024 19:08:42 +0200 Subject: [PATCH 01/28] Add support for low rank constraint --- .github/workflows/ci.yml | 7 ++++ Project.toml | 1 + src/MOI_wrapper.jl | 61 ++++++++++++++++++++++++++----- src/SDPLR.jl | 10 ++++-- test/Project.toml | 1 + test/runtests.jl | 78 ++++++++++++++++++++++++++++++++++------ test/simple_lowrank.jl | 8 +++++ 7 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 test/simple_lowrank.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2a6e76..7f74399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,13 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 + - name: LRO + shell: julia --project=@. {0} + run: | + using Pkg + Pkg.add([ + PackageSpec(url="https://github.com/blegat/LowRankOpt.jl/"), + ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 diff --git a/Project.toml b/Project.toml index b5b4d53..8ea7793 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Benoît Legat "] version = "0.1.2" [deps] +LowRankOpt = "607ca3ad-272e-43c8-bcbe-fc71b56c935c" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" SDPLR_jll = "3a057b76-36a0-51f0-a66f-6d580b8e8efd" diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 27ff835..9d90c9b 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,12 +17,21 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) +const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.Factorization{Cdouble,F,D} + +const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ + MOI.PositiveSemidefiniteConeTriangle, + LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, + <:AbstractVector{LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}} +} + const SupportedSets = - Union{MOI.Nonnegatives,MOI.PositiveSemidefiniteConeTriangle} + Union{MOI.Nonnegatives,MOI.PositiveSemidefiniteConeTriangle,_SetDotProd} mutable struct Optimizer <: MOI.AbstractOptimizer objective_constant::Float64 objective_sign::Int + dot_product::Vector{Union{Nothing,_LowRankMatrix}} blksz::Vector{Cptrdiff_t} blktype::Vector{Cchar} varmap::Vector{Tuple{Int,Int,Int}} # Variable Index vi -> blk, i, j @@ -51,6 +60,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer return new( 0.0, 1, + Union{Nothing,_LowRankMatrix}[], Cptrdiff_t[], Cchar[], Tuple{Int,Int,Int}[], @@ -151,6 +161,7 @@ function _new_block(model::Optimizer, set::MOI.Nonnegatives) blk = length(model.blksz) for i in 1:MOI.dimension(set) push!(model.varmap, (blk, i, i)) + push!(model.dot_product, nothing) end return end @@ -162,11 +173,22 @@ function _new_block(model::Optimizer, set::MOI.PositiveSemidefiniteConeTriangle) for j in 1:set.side_dimension for i in 1:j push!(model.varmap, (blk, i, j)) + push!(model.dot_product, nothing) end end return end +function _new_block(model::Optimizer, set::_SetDotProd) + blk = length(model.blksz) + 1 + for i in eachindex(set.vectors) + push!(model.varmap, (-blk, i, i)) + push!(model.dot_product, set.vectors[i].matrix) + end + _new_block(model, set.set) + return +end + function MOI.add_constrained_variables(model::Optimizer, set::SupportedSets) reset_solution!(model) offset = length(model.varmap) @@ -237,14 +259,36 @@ function _fill!( ) for t in MOI.Utilities.canonical(func).terms blk, i, j = model.varmap[t.variable.value] - _fill_until(model, blk, entptr, type, length(ent)) - coef = t.coefficient - if i != j - coef /= 2 + _fill_until(model, abs(blk), entptr, type, length(ent)) + if type[end] == Cchar('l') + error( + "Can either have one dot product variable or several normal variables in the same constraint", + ) + end + if blk < 0 + type[end] = Cchar('l') + mat = model.dot_product[t.variable.value] + for i in eachindex(mat.scaling) + push!(ent, mat.scaling[i]) + push!(row, i) + push!(col, i) + end + for j in axes(mat.factor, 2) + for i in axes(mat.factor, 1) + push!(ent, mat.factor[i, j]) + push!(row, i) + push!(col, j) + end + end + else + coef = t.coefficient + if i != j + coef /= 2 + end + push!(ent, coef) + push!(row, i) + push!(col, j) end - push!(ent, coef) - push!(row, i) - push!(col, j) end _fill_until(model, length(model.blksz), entptr, type, length(ent)) @assert length(entptr) == length(model.blksz) @@ -366,6 +410,7 @@ end function MOI.empty!(optimizer::Optimizer) optimizer.objective_constant = 0.0 optimizer.objective_sign = 1 + empty!(optimizer.dot_product) empty!(optimizer.blksz) empty!(optimizer.blktype) empty!(optimizer.varmap) diff --git a/src/SDPLR.jl b/src/SDPLR.jl index ad96885..79310cb 100644 --- a/src/SDPLR.jl +++ b/src/SDPLR.jl @@ -6,6 +6,7 @@ module SDPLR import MathOptInterface as MOI +import LowRankOpt as LRO import SDPLR_jll include("bounds.jl") @@ -138,14 +139,17 @@ function solve( k += 1 @assert CAinfo_entptr[k] <= CAinfo_entptr[k+1] for j in ((CAinfo_entptr[k]+1):CAinfo_entptr[k+1]) - @assert blktype[blk] == CAinfo_type[k] @assert 1 <= CArow[j] <= blksz[blk] @assert 1 <= CAcol[j] <= blksz[blk] if CAinfo_type[k] == Cchar('s') + @assert blktype[blk] == Cchar('s') @assert CArow[j] <= CAcol[j] - else - @assert CAinfo_type[k] == Cchar('d') + elseif CAinfo_type[k] == Cchar('d') + @assert blktype[blk] == Cchar('d') @assert CArow[j] == CAcol[j] + else + @assert CAinfo_type[k] == Cchar('l') + @assert blktype[blk] == Cchar('s') end end end diff --git a/test/Project.toml b/test/Project.toml index b0cb0ca..40f3a37 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,6 @@ [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LowRankOpt = "607ca3ad-272e-43c8-bcbe-fc71b56c935c" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SDPLR = "56161740-ea4e-4253-9d15-43c62ff94d95" diff --git a/test/runtests.jl b/test/runtests.jl index 0077500..595e8b6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ module TestSDPLR using Test import LinearAlgebra import MathOptInterface as MOI +import LowRankOpt as LRO import Random import SDPLR @@ -160,7 +161,7 @@ function test_factor() return end -function _build_simple_model() +function _build_simple_sparse_model() model = SDPLR.Optimizer() X, _ = MOI.add_constrained_variables( model, @@ -174,6 +175,31 @@ function _build_simple_model() return model, X, c end +function _build_simple_lowrank_model() + model = SDPLR.Optimizer() + A = LRO.Factorization( + [ + -1.0 1.0 + 1.0 1.0 + ], + [-1 / 4, 1 / 4], + ) + set = LRO.SetDotProducts( + MOI.PositiveSemidefiniteConeTriangle(2), + [LRO.TriangleVectorization(A)], + ) + @test set isa SDPLR._SetDotProd + @test set isa SDPLR._SetDotProd{Matrix{Float64},Vector{Float64}} + @test MOI.supports_add_constrained_variables(model, typeof(set)) + X, _ = MOI.add_constrained_variables(model, set) + c = MOI.add_constraint(model, 1.0 * X[1], MOI.EqualTo(1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + obj = 1.0 * X[2] + 1.0 * X[4] + MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + return model, X[2:4], c +end + function _test_simple_model(model, X, c) atol = rtol = 1e-2 @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED @@ -189,15 +215,22 @@ function _test_simple_model(model, X, c) return end -function test_simple_MOI_wrapper() - model, X, c = _build_simple_model() +function test_simple_sparse_MOI_wrapper() + model, X, c = _build_simple_sparse_model() + MOI.optimize!(model) + _test_simple_model(model, X, c) + return +end + +function test_simple_lowrank_MOI_wrapper() + model, X, c = _build_simple_lowrank_model() MOI.optimize!(model) _test_simple_model(model, X, c) return end function _test_limit(attr, val, term) - model, _, _ = _build_simple_model() + model, _, _ = _build_simple_sparse_model() MOI.set(model, MOI.RawOptimizerAttribute(attr), val) MOI.optimize!(model) @test MOI.get(model, MOI.TerminationStatus()) == term @@ -229,7 +262,7 @@ function totaltime() end function test_continuity_between_solve() - model, X, c = _build_simple_model() + model, X, c = _build_simple_sparse_model() MOI.set(model, MOI.RawOptimizerAttribute("majiter"), SDPLR.MAX_MAJITER - 2) @test MOI.get(model, MOI.RawOptimizerAttribute("majiter")) == SDPLR.MAX_MAJITER - 2 @@ -307,15 +340,16 @@ function test_bounds() return end -function test_solve_simple_with_sdplrlib() +function _test_solve_simple_with_sdplrlib(; + CAinfo_entptr, + CAent, + CArow, + CAcol, + CAinfo_type, +) blksz = Cptrdiff_t[2] blktype = Cchar['s'] b = Cdouble[1] - CAinfo_entptr = Csize_t[0, 2, 3] - CAent = Cdouble[1, 1, 0.5] - CArow = Csize_t[1, 2, 1] - CAcol = Csize_t[1, 2, 2] - CAinfo_type = Cchar['s', 's'] # The `925` seed is taken from SDPLR's `main.c` Random.seed!(925) ret, R, lambda, ranks, pieces = SDPLR.solve( @@ -337,6 +371,28 @@ function test_solve_simple_with_sdplrlib() return end +function test_solve_simple_sparse_with_sdplrlib() + _test_solve_simple_with_sdplrlib( + CAinfo_entptr = Csize_t[0, 2, 3], + CAent = Cdouble[1, 1, 0.5], + CArow = Csize_t[1, 2, 1], + CAcol = Csize_t[1, 2, 2], + CAinfo_type = Cchar['s', 's'], + ) + return +end + +function test_solve_simple_lowrank_with_sdplrlib() + _test_solve_simple_with_sdplrlib( + CAinfo_entptr = Csize_t[0, 2, 8], + CAent = Cdouble[1, 1, -0.25, 0.25, -1, 1, 1, 1], + CArow = Csize_t[1, 2, 1, 2, 1, 2, 1, 2], + CAcol = Csize_t[1, 2, 1, 2, 1, 1, 2, 2], + CAinfo_type = Cchar['s', 'l'], + ) + return +end + function test_solve_vibra_with_sdplr_executable() SDPLR.solve_sdpa_file("vibra1.dat-s") return diff --git a/test/simple_lowrank.jl b/test/simple_lowrank.jl new file mode 100644 index 0000000..3149e01 --- /dev/null +++ b/test/simple_lowrank.jl @@ -0,0 +1,8 @@ +blksz = Cptrdiff_t[2] +blktype = Cchar['s'] +b = Cdouble[1] +CAinfo_entptr = Csize_t[0, 2, 8] +CAent = Cdouble[1, 1, -0.25, 0.25, -1, 1, 1, 1] +CArow = Csize_t[1, 2, 1, 2, 1, 2, 1, 2] +CAcol = Csize_t[1, 2, 1, 2, 1, 1, 2, 2] +CAinfo_type = Cchar['s', 'l'] From 8932e31c3f651961db31a0324d6c59beb4188108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 4 Apr 2025 10:44:34 +0200 Subject: [PATCH 02/28] Fix format --- src/MOI_wrapper.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 9d90c9b..c20cb31 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,13 +17,17 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) -const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.Factorization{Cdouble,F,D} - -const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ - MOI.PositiveSemidefiniteConeTriangle, - LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, - <:AbstractVector{LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}} -} +const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = + LRO.Factorization{Cdouble,F,D} + +const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = + LRO.SetDotProducts{ + MOI.PositiveSemidefiniteConeTriangle, + LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, + <:AbstractVector{ + LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, + }, + } const SupportedSets = Union{MOI.Nonnegatives,MOI.PositiveSemidefiniteConeTriangle,_SetDotProd} From bb2753ff1abf7574473989af0a6ef267e95f4c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 4 Apr 2025 10:49:29 +0200 Subject: [PATCH 03/28] Fixes --- src/MOI_wrapper.jl | 1 + test/runtests.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index c20cb31..3371bf8 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -22,6 +22,7 @@ const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ + LRO.WITH_SET, MOI.PositiveSemidefiniteConeTriangle, LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, <:AbstractVector{ diff --git a/test/runtests.jl b/test/runtests.jl index 595e8b6..80086de 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -184,7 +184,7 @@ function _build_simple_lowrank_model() ], [-1 / 4, 1 / 4], ) - set = LRO.SetDotProducts( + set = LRO.SetDotProducts{LRO.WITH_SET}( MOI.PositiveSemidefiniteConeTriangle(2), [LRO.TriangleVectorization(A)], ) From db41d20f7930cde43b412071774a083a09c73ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 4 Apr 2025 11:09:44 +0200 Subject: [PATCH 04/28] Drops Julia v1.6 --- .github/workflows/ci.yml | 4 ++-- Project.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f74399..b162f49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,12 @@ jobs: strategy: fail-fast: false matrix: - version: ['1.6', '1'] # Test against LTS + version: ['1.10', '1'] # Test against LTS os: [ubuntu-latest, macOS-latest, windows-latest] arch: [x64] include: # Also test against 32-bit Linux on LTS. - - version: '1.6' + - version: '1.10' os: ubuntu-latest arch: x86 steps: diff --git a/Project.toml b/Project.toml index 8ea7793..dd4e828 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SDPLR" uuid = "56161740-ea4e-4253-9d15-43c62ff94d95" authors = ["Benoît Legat "] -version = "0.1.2" +version = "0.2.0" [deps] LowRankOpt = "607ca3ad-272e-43c8-bcbe-fc71b56c935c" @@ -11,4 +11,4 @@ SDPLR_jll = "3a057b76-36a0-51f0-a66f-6d580b8e8efd" [compat] MathOptInterface = "1.24" SDPLR_jll = "=100.2.300" -julia = "1.6" +julia = "1.10" From 1b756d61b7c8f42875b61c3bd2d56e4333d4c271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 4 Apr 2025 16:19:26 +0200 Subject: [PATCH 05/28] Add LRO tests --- test/runtests.jl | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 80086de..9c4ca6d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,13 +13,7 @@ import Random import SDPLR function test_runtests() - model = MOI.Bridges.full_bridge_optimizer( - MOI.Utilities.CachingOptimizer( - MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - SDPLR.Optimizer(), - ), - Float64, - ) + model = MOI.instantiate(SDPLR.Optimizer, with_bridge_type = Float64, with_cache_type = Float64) MOI.set(model, MOI.Silent(), true) MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) config = MOI.Test.Config( @@ -50,6 +44,30 @@ function test_runtests() return end +function test_LRO_runtests() + model = MOI.instantiate(SDPLR.Optimizer, with_bridge_type = Float64, with_cache_type = Float64) + MOI.set(model, MOI.Silent(), true) + MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) + config = MOI.Test.Config( + rtol = 1e-1, + atol = 1e-1, + exclude = Any[ + MOI.ConstraintBasisStatus, + MOI.VariableBasisStatus, + MOI.ConstraintName, + MOI.VariableName, + MOI.ObjectiveBound, + MOI.SolverVersion, + ], + optimal_status = MOI.LOCALLY_SOLVED, + ) + LRO.Test.runtests( + model, + config, + ) + return +end + function test_RawOptimizerAttribute_UnsupportedAttribute() model = SDPLR.Optimizer() attr = MOI.RawOptimizerAttribute("FooBarBaz") From 52e357548bd10a26c73a96b1afccbb14fc717676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 4 Apr 2025 16:32:03 +0200 Subject: [PATCH 06/28] Fix format --- test/runtests.jl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9c4ca6d..2b49026 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,7 +13,11 @@ import Random import SDPLR function test_runtests() - model = MOI.instantiate(SDPLR.Optimizer, with_bridge_type = Float64, with_cache_type = Float64) + model = MOI.instantiate( + SDPLR.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) MOI.set(model, MOI.Silent(), true) MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) config = MOI.Test.Config( @@ -45,7 +49,11 @@ function test_runtests() end function test_LRO_runtests() - model = MOI.instantiate(SDPLR.Optimizer, with_bridge_type = Float64, with_cache_type = Float64) + model = MOI.instantiate( + SDPLR.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) MOI.set(model, MOI.Silent(), true) MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) config = MOI.Test.Config( @@ -61,10 +69,7 @@ function test_LRO_runtests() ], optimal_status = MOI.LOCALLY_SOLVED, ) - LRO.Test.runtests( - model, - config, - ) + LRO.Test.runtests(model, config) return end From b3f60d368db5d029e4d81988b03b5016db8e76c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 7 Apr 2025 18:53:40 +0200 Subject: [PATCH 07/28] Fixes --- src/MOI_wrapper.jl | 8 +------- test/runtests.jl | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 3371bf8..bb147fc 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,17 +17,11 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) -const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = - LRO.Factorization{Cdouble,F,D} - const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ LRO.WITH_SET, MOI.PositiveSemidefiniteConeTriangle, - LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, - <:AbstractVector{ - LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, - }, + LRO.TriangleVectorization{Cdouble,LRO.Factorization{Cdouble,F,D}}, } const SupportedSets = diff --git a/test/runtests.jl b/test/runtests.jl index 2b49026..fc1ba71 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,6 +54,7 @@ function test_LRO_runtests() with_bridge_type = Float64, with_cache_type = Float64, ) + LRO.Bridges.add_all_bridges(model, Float64) MOI.set(model, MOI.Silent(), true) MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) config = MOI.Test.Config( From d85b8c978b84620eeb623ca74d67d030a171cde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 8 Apr 2025 08:30:24 +0200 Subject: [PATCH 08/28] Renam --- src/MOI_wrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index bb147fc..387affe 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,7 +17,7 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) -const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = +const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ LRO.WITH_SET, MOI.PositiveSemidefiniteConeTriangle, From a9185e71bfd1c0eeff793e2f6a4c733fe326f95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 8 Apr 2025 16:21:02 +0200 Subject: [PATCH 09/28] Fix --- src/MOI_wrapper.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 387affe..7e47652 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,7 +17,7 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) -const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = +const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ LRO.WITH_SET, MOI.PositiveSemidefiniteConeTriangle, @@ -30,7 +30,7 @@ const SupportedSets = mutable struct Optimizer <: MOI.AbstractOptimizer objective_constant::Float64 objective_sign::Int - dot_product::Vector{Union{Nothing,_LowRankMatrix}} + dot_product::Vector{Union{Nothing,_SetDotProd}} blksz::Vector{Cptrdiff_t} blktype::Vector{Cchar} varmap::Vector{Tuple{Int,Int,Int}} # Variable Index vi -> blk, i, j @@ -59,7 +59,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer return new( 0.0, 1, - Union{Nothing,_LowRankMatrix}[], + Union{Nothing,_SetDotProd}[], Cptrdiff_t[], Cchar[], Tuple{Int,Int,Int}[], From 410ffda82c0b027cdfafdc479247819fc16058b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 8 Apr 2025 16:45:00 +0200 Subject: [PATCH 10/28] Fixes --- test/runtests.jl | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index fc1ba71..7744ef9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,25 +49,18 @@ function test_runtests() end function test_LRO_runtests() + T = Float64 model = MOI.instantiate( SDPLR.Optimizer, - with_bridge_type = Float64, - with_cache_type = Float64, + with_bridge_type = T, + with_cache_type = T, ) - LRO.Bridges.add_all_bridges(model, Float64) + LRO.Bridges.add_all_bridges(model, T) MOI.set(model, MOI.Silent(), true) MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) config = MOI.Test.Config( rtol = 1e-1, atol = 1e-1, - exclude = Any[ - MOI.ConstraintBasisStatus, - MOI.VariableBasisStatus, - MOI.ConstraintName, - MOI.VariableName, - MOI.ObjectiveBound, - MOI.SolverVersion, - ], optimal_status = MOI.LOCALLY_SOLVED, ) LRO.Test.runtests(model, config) From 9ea2d1e0f0900b6936b15a1284aee99afb782a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 9 Apr 2025 09:21:13 +0200 Subject: [PATCH 11/28] Fix --- src/MOI_wrapper.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 7e47652..d06ec64 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,11 +17,13 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) +const _LowRankMatrix = LRO.Factorization{Cdouble,F,D} + const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ LRO.WITH_SET, MOI.PositiveSemidefiniteConeTriangle, - LRO.TriangleVectorization{Cdouble,LRO.Factorization{Cdouble,F,D}}, + LRO.TriangleVectorization{Cdouble,_LowRankMatrix}, } const SupportedSets = @@ -30,7 +32,7 @@ const SupportedSets = mutable struct Optimizer <: MOI.AbstractOptimizer objective_constant::Float64 objective_sign::Int - dot_product::Vector{Union{Nothing,_SetDotProd}} + dot_product::Vector{Union{Nothing,_LowRankMatrix}} blksz::Vector{Cptrdiff_t} blktype::Vector{Cchar} varmap::Vector{Tuple{Int,Int,Int}} # Variable Index vi -> blk, i, j @@ -59,7 +61,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer return new( 0.0, 1, - Union{Nothing,_SetDotProd}[], + Union{Nothing,_LowRankMatrix}[], Cptrdiff_t[], Cchar[], Tuple{Int,Int,Int}[], From de65736d2c52646a5ced8ecce8909b1e23513e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 9 Apr 2025 09:22:07 +0200 Subject: [PATCH 12/28] Fix --- src/MOI_wrapper.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index d06ec64..c7f5104 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,13 +17,13 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) -const _LowRankMatrix = LRO.Factorization{Cdouble,F,D} +const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.Factorization{Cdouble,F,D} const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ LRO.WITH_SET, MOI.PositiveSemidefiniteConeTriangle, - LRO.TriangleVectorization{Cdouble,_LowRankMatrix}, + LRO.TriangleVectorization{Cdouble,_LowRankMatrix{F,D}}, } const SupportedSets = From 045f9db10045b240f3652af5f3bd088226063dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 9 Apr 2025 10:12:34 +0200 Subject: [PATCH 13/28] Debug --- test/runtests.jl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 7744ef9..698adb6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -435,6 +435,35 @@ function test_solve_vibra_with_sdplrlib() @test ranks == Csize_t[9, 9, 1] end +# Test of LowRankOpt's test `test_conic_PositiveSemidefinite_RankOne_polynomial` in low-level SDPLR version +function test_solve_conic_PositiveSemidefinite_RankOne_polynomial() + blksz = [2, 2] + blktype = Int8['d', 's'] + b = [-3.0, 1.0] + CAent = [-1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0] + CArow = UInt64[0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002] + CAcol = UInt64[0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001] + CAinfo_entptr = UInt64[0x0000000000000000, 0x0000000000000002, 0x0000000000000002, 0x0000000000000004, 0x0000000000000007, 0x0000000000000009, 0x000000000000000c] + CAinfo_type = Int8[100, 115, 100, 108, 100, 108] + # The `925` seed is taken from SDPLR's `main.c` + Random.seed!(925) + ret, R, lambda, ranks, pieces = SDPLR.solve( + blksz, + blktype, + b, + CAent, + CArow, + CAcol, + CAinfo_entptr, + CAinfo_type, + ) + @test iszero(ret) + @show R + @show lambda + @show pieces + @show ranks +end + function runtests() for name in names(@__MODULE__; all = true) if startswith("$(name)", "test_") From d7ff9828a7d6358de1000d34960cc1a4bda16fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 9 Apr 2025 10:14:19 +0200 Subject: [PATCH 14/28] Cchar --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 698adb6..1847305 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -438,13 +438,13 @@ end # Test of LowRankOpt's test `test_conic_PositiveSemidefinite_RankOne_polynomial` in low-level SDPLR version function test_solve_conic_PositiveSemidefinite_RankOne_polynomial() blksz = [2, 2] - blktype = Int8['d', 's'] + blktype = Cchar['d', 's'] b = [-3.0, 1.0] CAent = [-1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0] CArow = UInt64[0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002] CAcol = UInt64[0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001] CAinfo_entptr = UInt64[0x0000000000000000, 0x0000000000000002, 0x0000000000000002, 0x0000000000000004, 0x0000000000000007, 0x0000000000000009, 0x000000000000000c] - CAinfo_type = Int8[100, 115, 100, 108, 100, 108] + CAinfo_type = Cchar['d', 's', 'd', 'l', 'd', 'l'] # The `925` seed is taken from SDPLR's `main.c` Random.seed!(925) ret, R, lambda, ranks, pieces = SDPLR.solve( From f4f1316fe4694d26f43833a300bc430cf66988d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 10 Apr 2025 23:12:37 +0200 Subject: [PATCH 15/28] Fix --- src/MOI_wrapper.jl | 19 +++++++++++++++---- test/runtests.jl | 17 +++++++++-------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index c7f5104..d82b523 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -270,7 +270,7 @@ function _fill!( type[end] = Cchar('l') mat = model.dot_product[t.variable.value] for i in eachindex(mat.scaling) - push!(ent, mat.scaling[i]) + push!(ent, t.coefficient * mat.scaling[i]) push!(row, i) push!(col, i) end @@ -543,12 +543,23 @@ function MOI.get( vi::MOI.VariableIndex, ) MOI.check_result_index_bounds(optimizer, attr) - blk, i, j = optimizer.varmap[vi.value] - I = (optimizer.Rmap[blk]+1):optimizer.Rmap[blk+1] + _blk, i, j = optimizer.varmap[vi.value] + blk = abs(_blk) + I = (optimizer.Rmap[abs(blk)]+1):optimizer.Rmap[abs(blk)+1] if optimizer.blktype[blk] == Cchar('s') d = optimizer.blksz[blk] U = reshape(optimizer.R[I], d, div(length(I), d)) - return U[i, :]' * U[j, :] + if _blk < 0 + # result of dot product + mat = optimizer.dot_product[vi.value] + return sum(eachindex(mat.scaling); init = 0.0) do i + v = mat.factor[:, i] + vU = U' * v + return mat.scaling[i] * (vU' * vU) + end + else + return U[i, :]' * U[j, :] + end else @assert optimizer.blktype[blk] == Cchar('d') return optimizer.R[I[i]]^2 diff --git a/test/runtests.jl b/test/runtests.jl index 1847305..2902108 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -440,10 +440,10 @@ function test_solve_conic_PositiveSemidefinite_RankOne_polynomial() blksz = [2, 2] blktype = Cchar['d', 's'] b = [-3.0, 1.0] - CAent = [-1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0] - CArow = UInt64[0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002] - CAcol = UInt64[0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001, 0x0000000000000002, 0x0000000000000001, 0x0000000000000001, 0x0000000000000001] - CAinfo_entptr = UInt64[0x0000000000000000, 0x0000000000000002, 0x0000000000000002, 0x0000000000000004, 0x0000000000000007, 0x0000000000000009, 0x000000000000000c] + CAent = [-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0] + CArow = UInt64[1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 1, 2] + CAcol = UInt64[1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1] + CAinfo_entptr = UInt64[0, 2, 2, 4, 7, 9, 12] CAinfo_type = Cchar['d', 's', 'd', 'l', 'd', 'l'] # The `925` seed is taken from SDPLR's `main.c` Random.seed!(925) @@ -458,10 +458,11 @@ function test_solve_conic_PositiveSemidefinite_RankOne_polynomial() CAinfo_type, ) @test iszero(ret) - @show R - @show lambda - @show pieces - @show ranks + U = reshape(R[3:end], 2, 2) + @test U * U' ≈ [1 -1; -1 1] rtol = 1e-3 + @test lambda ≈ [0, 1] atol = 1e-3 + @test pieces == [7, 20, 1, 0, 0, 0, 16, 1] + @test ranks == [1, 2] end function runtests() From f287f4d1410bf77e99973138882abe93a44b29dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 10 Apr 2025 23:19:05 +0200 Subject: [PATCH 16/28] Fix format --- src/MOI_wrapper.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index d82b523..d343510 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -17,7 +17,8 @@ const PIECES_MAP = Dict{String,Int}( "overallsc" => 8, ) -const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.Factorization{Cdouble,F,D} +const _LowRankMatrix{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = + LRO.Factorization{Cdouble,F,D} const _SetDotProd{F<:AbstractMatrix{Cdouble},D<:AbstractVector{Cdouble}} = LRO.SetDotProducts{ From dedd5af43621a46717b79e4e1e9e221fdcdf9e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 11 Apr 2025 16:56:49 +0200 Subject: [PATCH 17/28] Fix --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 2902108..69fbfc9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -441,9 +441,9 @@ function test_solve_conic_PositiveSemidefinite_RankOne_polynomial() blktype = Cchar['d', 's'] b = [-3.0, 1.0] CAent = [-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0] - CArow = UInt64[1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 1, 2] - CAcol = UInt64[1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1] - CAinfo_entptr = UInt64[0, 2, 2, 4, 7, 9, 12] + CArow = Csize_t[1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 1, 2] + CAcol = Csize_t[1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1] + CAinfo_entptr = Csize_t[0, 2, 2, 4, 7, 9, 12] CAinfo_type = Cchar['d', 's', 'd', 'l', 'd', 'l'] # The `925` seed is taken from SDPLR's `main.c` Random.seed!(925) From ae0e8d6bb1bd935af29f2f15a1d877892d8983c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 11 Apr 2025 16:59:33 +0200 Subject: [PATCH 18/28] Fix --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 69fbfc9..cd1e809 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,7 +63,7 @@ function test_LRO_runtests() atol = 1e-1, optimal_status = MOI.LOCALLY_SOLVED, ) - LRO.Test.runtests(model, config) + MOI.Test.runtests(model, config, test_module = LRO.Test) return end From e08783152dae7b3ce3edc28e6aa5f1a8322a088f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 7 May 2025 14:35:56 +0200 Subject: [PATCH 19/28] Add def of N in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0fd391b..2b58d92 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,10 @@ julia> F = MOI.get(model, SDPLR.Factor(), VariableInSetRef(X)) -0.750538 -0.558704 0.352905 ``` -`JuMP.value(X)` is internally computed from the factor so this will always hold: +`JuMP.value.(X)` is internally computed from the factor so this will always hold: ```julia-repl -julia> F * F' ≈ value(X) +julia> F * F' ≈ value.(X) true ``` From f2fa01dc6579f2339c51aaf91ad39a47f85ee55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 7 May 2025 14:37:03 +0200 Subject: [PATCH 20/28] Add support for Factor in low-rank case --- src/MOI_wrapper.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index d343510..7c8e6f1 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -526,9 +526,10 @@ function MOI.get( # The constraint index corresponds to the variable index of the `1, 1` entry blk, i, j = optimizer.varmap[ci.value] @assert i == j == 1 + blk = abs(blk) # In the Low-Rank case, we just take the factor of the PSD matrix I = (optimizer.Rmap[blk]+1):optimizer.Rmap[blk+1] r = optimizer.R[I] - if S === MOI.PositiveSemidefiniteConeTriangle + if S === MOI.PositiveSemidefiniteConeTriangle || S <: _SetDotProd @assert optimizer.blktype[blk] == Cchar('s') d = optimizer.blksz[blk] return reshape(r, d, div(length(I), d)) From cd7a61a0f84a847f35c83d61117d4d075260ce4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 7 May 2025 15:47:34 +0200 Subject: [PATCH 21/28] Fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b58d92..0fd391b 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,10 @@ julia> F = MOI.get(model, SDPLR.Factor(), VariableInSetRef(X)) -0.750538 -0.558704 0.352905 ``` -`JuMP.value.(X)` is internally computed from the factor so this will always hold: +`JuMP.value(X)` is internally computed from the factor so this will always hold: ```julia-repl -julia> F * F' ≈ value.(X) +julia> F * F' ≈ value(X) true ``` From fb7941dcafef4ae0b1474b13b3bcc1ad33b4ec87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 7 May 2025 22:35:00 +0200 Subject: [PATCH 22/28] Add tests for sum of low rank --- test/runtests.jl | 98 +++++++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index cd1e809..bd15d5e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,41 +12,41 @@ import LowRankOpt as LRO import Random import SDPLR -function test_runtests() - model = MOI.instantiate( - SDPLR.Optimizer, - with_bridge_type = Float64, - with_cache_type = Float64, - ) - MOI.set(model, MOI.Silent(), true) - MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) - config = MOI.Test.Config( - rtol = 1e-1, - atol = 1e-1, - exclude = Any[ - MOI.ConstraintBasisStatus, - MOI.VariableBasisStatus, - MOI.ObjectiveBound, - MOI.SolverVersion, - ], - optimal_status = MOI.LOCALLY_SOLVED, - ) - MOI.Test.runtests( - model, - config, - exclude = [ - # Detecting infeasibility or unboundedness not supported - "INFEAS", - # These three are unbounded even if it's not in the name - r"test_conic_SecondOrderCone_negative_post_bound_2$", - r"test_conic_SecondOrderCone_negative_post_bound_3$", - r"test_conic_SecondOrderCone_no_initial_bound$", - # Incorrect `ConstraintDual` for `vc2` for MacOS in CI - r"test_linear_integration$", - ], - ) - return -end +#function test_runtests() +# model = MOI.instantiate( +# SDPLR.Optimizer, +# with_bridge_type = Float64, +# with_cache_type = Float64, +# ) +# MOI.set(model, MOI.Silent(), true) +# MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) +# config = MOI.Test.Config( +# rtol = 1e-1, +# atol = 1e-1, +# exclude = Any[ +# MOI.ConstraintBasisStatus, +# MOI.VariableBasisStatus, +# MOI.ObjectiveBound, +# MOI.SolverVersion, +# ], +# optimal_status = MOI.LOCALLY_SOLVED, +# ) +# MOI.Test.runtests( +# model, +# config, +# exclude = [ +# # Detecting infeasibility or unboundedness not supported +# "INFEAS", +# # These three are unbounded even if it's not in the name +# r"test_conic_SecondOrderCone_negative_post_bound_2$", +# r"test_conic_SecondOrderCone_negative_post_bound_3$", +# r"test_conic_SecondOrderCone_no_initial_bound$", +# # Incorrect `ConstraintDual` for `vc2` for MacOS in CI +# r"test_linear_integration$", +# ], +# ) +# return +#end function test_LRO_runtests() T = Float64 @@ -192,6 +192,27 @@ function _build_simple_sparse_model() return model, X, c end +function _build_simple_rankone_model() + model = SDPLR.Optimizer() + A1 = LRO.positive_semidefinite_factorization([-1.0, 1.0]) + A2 = LRO.positive_semidefinite_factorization([1.0, 1.0]) + set = LRO.SetDotProducts{LRO.WITH_SET}( + MOI.PositiveSemidefiniteConeTriangle(2), + LRO.TriangleVectorization.([A1, A2]), + ) + @test set isa SDPLR._SetDotProd + @test MOI.supports_add_constrained_variables(model, typeof(set)) + dot_prods_X, _ = MOI.add_constrained_variables(model, set) + dot_prods = dot_prods_X[1:2] + X = dot_prods_X[3:end] + c = MOI.add_constraint(model, -1/4 * dot_prods[1] + 1/4 * dot_prods[2], MOI.EqualTo(1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + obj = 1.0 * X[1] + 1.0 * X[3] + MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED + return model, X, c +end + function _build_simple_lowrank_model() model = SDPLR.Optimizer() A = LRO.Factorization( @@ -246,6 +267,13 @@ function test_simple_lowrank_MOI_wrapper() return end +function test_simple_rankone_MOI_wrapper() + model, X, c = _build_simple_rankone_model() + MOI.optimize!(model) + _test_simple_model(model, X, c) + return +end + function _test_limit(attr, val, term) model, _, _ = _build_simple_sparse_model() MOI.set(model, MOI.RawOptimizerAttribute(attr), val) From 93494631cbb84ff19005cc2ad6847c0dfaf84ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 7 May 2025 23:20:28 +0200 Subject: [PATCH 23/28] Add support for multiple factor in same block --- src/MOI_wrapper.jl | 65 ++++++++++++++++++++++++++++++++-------------- test/runtests.jl | 4 +-- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 7c8e6f1..c27ba14 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -232,6 +232,9 @@ function _next(model::Optimizer, i) return length(model.Aent) end +# For the variables that are not in the dot product, we need to fill the +# `entptr` and `type`. This is because the solver needs to have an entry +# for each block. function _fill_until( model::Optimizer, numblk, @@ -250,6 +253,41 @@ function _fill_until( return end +# We have ∑ αᵢ' * Fᵢ' * Fᵢ' = [F₁ ... Fₖ] * Diag(α₁, .., αₖ) * [F₁' .. Fₖ'] +function merge_low_rank_terms( + ent, + row, + col, + type::Vector{Cchar}, + mats::Vector{Tuple{Cdouble,_LowRankMatrix}}, +) + if isempty(mats) + return + end + type[end] = Cchar('l') + offset = 0 + for (coef, mat) in mats + for i in eachindex(mat.scaling) + push!(ent, coef * mat.scaling[i]) + push!(row, offset + i) + push!(col, offset + i) + end + offset += length(mat.scaling) + end + offset = 0 + for (_, mat) in mats + for j in axes(mat.factor, 2) + for i in axes(mat.factor, 1) + push!(ent, mat.factor[i, j]) + push!(row, offset + i) + push!(col, offset + j) + end + offset += length(mat.scaling) + end + end + empty!(mats) +end + function _fill!( model, ent, @@ -259,29 +297,17 @@ function _fill!( type::Vector{Cchar}, func, ) + prev_blk = 0 + mats = Tuple{Cdouble,_LowRankMatrix}[] for t in MOI.Utilities.canonical(func).terms blk, i, j = model.varmap[t.variable.value] - _fill_until(model, abs(blk), entptr, type, length(ent)) - if type[end] == Cchar('l') - error( - "Can either have one dot product variable or several normal variables in the same constraint", - ) + if blk != prev_blk + merge_low_rank_terms(ent, row, col, type, mats) end + _fill_until(model, abs(blk), entptr, type, length(ent)) if blk < 0 - type[end] = Cchar('l') - mat = model.dot_product[t.variable.value] - for i in eachindex(mat.scaling) - push!(ent, t.coefficient * mat.scaling[i]) - push!(row, i) - push!(col, i) - end - for j in axes(mat.factor, 2) - for i in axes(mat.factor, 1) - push!(ent, mat.factor[i, j]) - push!(row, i) - push!(col, j) - end - end + prev_blk = blk + push!(mats, (t.coefficient, model.dot_product[t.variable.value])) else coef = t.coefficient if i != j @@ -292,6 +318,7 @@ function _fill!( push!(col, j) end end + merge_low_rank_terms(ent, row, col, type, mats) _fill_until(model, length(model.blksz), entptr, type, length(ent)) @assert length(entptr) == length(model.blksz) @assert length(type) == length(model.blksz) diff --git a/test/runtests.jl b/test/runtests.jl index bd15d5e..6c0d994 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -194,8 +194,8 @@ end function _build_simple_rankone_model() model = SDPLR.Optimizer() - A1 = LRO.positive_semidefinite_factorization([-1.0, 1.0]) - A2 = LRO.positive_semidefinite_factorization([1.0, 1.0]) + A1 = LRO.positive_semidefinite_factorization([-1.0; 1.0;;]) + A2 = LRO.positive_semidefinite_factorization([1.0; 1.0;;]) set = LRO.SetDotProducts{LRO.WITH_SET}( MOI.PositiveSemidefiniteConeTriangle(2), LRO.TriangleVectorization.([A1, A2]), From dbdabeb287fcb2eadf93b3414426717077a6904a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 8 May 2025 09:57:47 +0200 Subject: [PATCH 24/28] Fixes --- src/MOI_wrapper.jl | 4 +-- test/runtests.jl | 70 +++++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index c27ba14..58d4b4d 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -279,11 +279,11 @@ function merge_low_rank_terms( for j in axes(mat.factor, 2) for i in axes(mat.factor, 1) push!(ent, mat.factor[i, j]) - push!(row, offset + i) + push!(row, i) push!(col, offset + j) end - offset += length(mat.scaling) end + offset += length(mat.scaling) end empty!(mats) end diff --git a/test/runtests.jl b/test/runtests.jl index 6c0d994..2d83182 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,41 +12,41 @@ import LowRankOpt as LRO import Random import SDPLR -#function test_runtests() -# model = MOI.instantiate( -# SDPLR.Optimizer, -# with_bridge_type = Float64, -# with_cache_type = Float64, -# ) -# MOI.set(model, MOI.Silent(), true) -# MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) -# config = MOI.Test.Config( -# rtol = 1e-1, -# atol = 1e-1, -# exclude = Any[ -# MOI.ConstraintBasisStatus, -# MOI.VariableBasisStatus, -# MOI.ObjectiveBound, -# MOI.SolverVersion, -# ], -# optimal_status = MOI.LOCALLY_SOLVED, -# ) -# MOI.Test.runtests( -# model, -# config, -# exclude = [ -# # Detecting infeasibility or unboundedness not supported -# "INFEAS", -# # These three are unbounded even if it's not in the name -# r"test_conic_SecondOrderCone_negative_post_bound_2$", -# r"test_conic_SecondOrderCone_negative_post_bound_3$", -# r"test_conic_SecondOrderCone_no_initial_bound$", -# # Incorrect `ConstraintDual` for `vc2` for MacOS in CI -# r"test_linear_integration$", -# ], -# ) -# return -#end +function test_runtests() + model = MOI.instantiate( + SDPLR.Optimizer, + with_bridge_type = Float64, + with_cache_type = Float64, + ) + MOI.set(model, MOI.Silent(), true) + MOI.set(model, MOI.RawOptimizerAttribute("timelim"), 10) + config = MOI.Test.Config( + rtol = 1e-1, + atol = 1e-1, + exclude = Any[ + MOI.ConstraintBasisStatus, + MOI.VariableBasisStatus, + MOI.ObjectiveBound, + MOI.SolverVersion, + ], + optimal_status = MOI.LOCALLY_SOLVED, + ) + MOI.Test.runtests( + model, + config, + exclude = [ + # Detecting infeasibility or unboundedness not supported + "INFEAS", + # These three are unbounded even if it's not in the name + r"test_conic_SecondOrderCone_negative_post_bound_2$", + r"test_conic_SecondOrderCone_negative_post_bound_3$", + r"test_conic_SecondOrderCone_no_initial_bound$", + # Incorrect `ConstraintDual` for `vc2` for MacOS in CI + r"test_linear_integration$", + ], + ) + return +end function test_LRO_runtests() T = Float64 From 8bb83e30b5089805b4b81b3b9bf9bdfd37547922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 10 May 2025 23:43:04 +0200 Subject: [PATCH 25/28] Use LowRankOpt v0.1 --- .github/workflows/ci.yml | 7 ------- Project.toml | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b162f49..ef4350f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,13 +30,6 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - - name: LRO - shell: julia --project=@. {0} - run: | - using Pkg - Pkg.add([ - PackageSpec(url="https://github.com/blegat/LowRankOpt.jl/"), - ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 diff --git a/Project.toml b/Project.toml index dd4e828..e0c3e91 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" SDPLR_jll = "3a057b76-36a0-51f0-a66f-6d580b8e8efd" [compat] +LowRankOpt = "0.1" MathOptInterface = "1.24" SDPLR_jll = "=100.2.300" julia = "1.10" From 6362c94984ee8b890a87a186530d2498be7783ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 11 May 2025 07:32:02 +0200 Subject: [PATCH 26/28] Fix format --- src/MOI_wrapper.jl | 1 + test/runtests.jl | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 58d4b4d..1ab5f20 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -286,6 +286,7 @@ function merge_low_rank_terms( offset += length(mat.scaling) end empty!(mats) + return end function _fill!( diff --git a/test/runtests.jl b/test/runtests.jl index 2d83182..c0ff5fd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -205,7 +205,11 @@ function _build_simple_rankone_model() dot_prods_X, _ = MOI.add_constrained_variables(model, set) dot_prods = dot_prods_X[1:2] X = dot_prods_X[3:end] - c = MOI.add_constraint(model, -1/4 * dot_prods[1] + 1/4 * dot_prods[2], MOI.EqualTo(1.0)) + c = MOI.add_constraint( + model, + -1/4 * dot_prods[1] + 1/4 * dot_prods[2], + MOI.EqualTo(1.0), + ) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) obj = 1.0 * X[1] + 1.0 * X[3] MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj) From 47e6f7685e91e447ea2fdb4f0544bf6eb29d3c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 11 May 2025 07:34:08 +0200 Subject: [PATCH 27/28] Mention in README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0fd391b..6241e2a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ To use SDPLR with [JuMP](https://github.com/jump-dev/JuMP.jl), use using JuMP, SDPLR model = Model(SDPLR.Optimizer) ``` +In order to use low-rank **constraints** (not to be confused with low-rank +**solutions** detailed in the next section), use the +[LowRankOpt](https://github.com/blegat/LowRankOpt.jl/) extension of JuMP. ## Example: modifying the rank and checking optimality From eea5ee085abf9eb6281846a9b5b8e3fa379cad2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 11 May 2025 19:36:59 +0200 Subject: [PATCH 28/28] Fix for mac os --- test/runtests.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index c0ff5fd..74b5c46 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -493,7 +493,8 @@ function test_solve_conic_PositiveSemidefinite_RankOne_polynomial() U = reshape(R[3:end], 2, 2) @test U * U' ≈ [1 -1; -1 1] rtol = 1e-3 @test lambda ≈ [0, 1] atol = 1e-3 - @test pieces == [7, 20, 1, 0, 0, 0, 16, 1] + @test pieces[1:5] == [7, 20, 1, 0, 0] + @test pieces[7:8] == [16, 1] @test ranks == [1, 2] end