From 5c327446281f76406fc84ddc05e4e97f7bb88f46 Mon Sep 17 00:00:00 2001 From: matbesancon Date: Tue, 10 Mar 2026 07:05:05 +0100 Subject: [PATCH 1/2] bounded LMO version --- src/matchings.jl | 37 +++++++++++++++++++++++++++++++++++ test/runtests.jl | 51 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/matchings.jl b/src/matchings.jl index 8335aa5..610d891 100644 --- a/src/matchings.jl +++ b/src/matchings.jl @@ -61,6 +61,43 @@ function FrankWolfe.compute_extreme_point( return v end +function Boscia.bounded_compute_extreme_point(lmo::MatchingLMO, direction, lb, ub, int_vars; kwargs...) + # any entry i fixed to zero -> use a positive direction, ensuring the edge is not taken + # any entry i fixed to one with neighbors (u, v) -> use positive direction for all other neighbors of u, of v + corrected_direction = copy(direction) + for (idx, edge) in enumerate(edges(lmo.original_graph)) + if ub[idx] ≈ 0 + @assert lb[idx] ≈ 0 + corrected_direction[idx] = 1 + elseif lb[idx] ≈ 1 + @assert ub[idx] ≈ 1 + (vtx1, vtx2) = Tuple(edge) + # negative cost ensures the edge is taken, since no neighbor will be in the matching + corrected_direction[idx] = -1 + for (idx2, e2) in enumerate(edges(lmo.original_graph)) + # check if e2 is adjacent to edge + # we want one of the two nodes to be the same + if xor(src(e2) in (vtx1, vtx2), dst(e2) in (vtx1, vtx2)) + # we should not have incompatible edges fixed to one + @assert lb[idx2] ≈ 0 "incompatible edges $edge $e2" + corrected_direction[idx2] = 1 + end + end + end + end + v = FrankWolfe.compute_extreme_point(lmo, corrected_direction) + @debug begin + for idx in eachindex(direction) + if ub[idx] ≈ 0 + @assert v[idx] ≈ 0 + elseif lb[idx] ≈ 1 + @assert v[idx] ≈ 1 + end + end + end + return v +end + """ PerfectMatchingLMO{G}(g::Graphs) diff --git a/test/runtests.jl b/test/runtests.jl index b0cb9a2..e48a457 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -47,22 +47,61 @@ end lmo = CO.MatchingLMO(g) v = FrankWolfe.compute_extreme_point(lmo, direction) adj_mat = spzeros(M, M) - for i in 1:M - adj_mat[src(iter[i]), dst(iter[i])] = direction[i] + for (i, edge) in enumerate(edges(g)) + adj_mat[src(edge), dst(edge)] = direction[i] end match_result = GraphsMatching.maximum_weight_matching(g, HiGHS.Optimizer, -adj_mat) v_sol = spzeros(M) K = length(match_result.mate) - for i in 1:K - for j in 1:M - if (match_result.mate[i] == src(iter[j]) && dst(iter[j]) == i) - v_sol[j] = 1 + for k in 1:K + for (i, edge) in enumerate(edges(g)) + if (match_result.mate[k] == src(edge) && dst(edge) == k) + v_sol[i] = 1 end end end @test v_sol == v v2 = FrankWolfe.compute_extreme_point(lmo, ones(M)) @test norm(v2) == 0 + @test v == Boscia.bounded_compute_extreme_point(lmo, direction, zeros(M), ones(M), 1:M) + @testset "Fix one entry to zero" begin + for one_idx in SparseArrays.nonzeroinds(v) + # upperbound one everywhere except one_idx fixed to zero + v_fixed1 = Boscia.bounded_compute_extreme_point(lmo, direction, zeros(M), (1:M) .!= one_idx, 1:M) + @test v_fixed1[one_idx] == 0 + end + end + @testset "Fix a single entry to one" begin + for idx in rand(1:M, 100) + # skip if entry already at one + if v[idx] == 1 + continue + end + lb = (1:M) .== idx + ub = ones(M) + v_fixed2 = Boscia.bounded_compute_extreme_point(lmo, direction, lb, ub, 1:M) + @test v_fixed2[idx] == 1 + end + end + @testset "Fix two entries to one" begin + for (i1, e1) in enumerate(edges(g)) + for (i2, e2) in enumerate(edges(g)) + # lighter on computation + if i1 ÷ 1000 + i2 ÷ 1000 != 0 + continue + end + # non-adjacent edges + if isempty(intersect(Tuple(e1), Tuple(e2))) + lb = zeros(M) + ub = ones(M) + lb[i1] = lb[i2] = 1 + v_fixed3 = Boscia.bounded_compute_extreme_point(lmo, direction, lb, ub, 1:M) + @test v_fixed3[i1] == 1 + @test v_fixed3[i2] == 1 + end + end + end + end end From fb9d02970f26d679bc403d69293eef6087813ab8 Mon Sep 17 00:00:00 2001 From: matbesancon Date: Tue, 10 Mar 2026 07:33:37 +0100 Subject: [PATCH 2/2] added simple feasible test --- src/matchings.jl | 16 ++++++++++++++++ test/runtests.jl | 15 +++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/matchings.jl b/src/matchings.jl index 610d891..022a059 100644 --- a/src/matchings.jl +++ b/src/matchings.jl @@ -98,6 +98,22 @@ function Boscia.bounded_compute_extreme_point(lmo::MatchingLMO, direction, lb, u return v end +function Boscia.is_simple_linear_feasible(lmo::MatchingLMO, v) + vertex_matching = zeros(Graphs.nv(lmo.original_graph)) + for (idx, edge) in enumerate(edges(lmo.original_graph)) + if v[idx] ≈ 0 + continue + end + vtx1, vtx2 = Tuple(edge) + vertex_matching[vtx1] += v[idx] + vertex_matching[vtx2] += v[idx] + end + # one vertex fractionally matched to multiple edges + if maximum(vertex_matching) >= 1 + 1e-4 + return false + end + return true +end """ PerfectMatchingLMO{G}(g::Graphs) diff --git a/test/runtests.jl b/test/runtests.jl index e48a457..3d6cc68 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -38,7 +38,7 @@ Random.seed!(StableRNG(42), 42) end @testset "Matching LMO" begin - N = 200 + N = 50 Random.seed!(9754) g = Graphs.complete_graph(N) iter = collect(Graphs.edges(g)) @@ -46,6 +46,7 @@ end direction = randn(M) lmo = CO.MatchingLMO(g) v = FrankWolfe.compute_extreme_point(lmo, direction) + @test Boscia.is_simple_linear_feasible(lmo, v) adj_mat = spzeros(M, M) for (i, edge) in enumerate(edges(g)) adj_mat[src(edge), dst(edge)] = direction[i] @@ -69,6 +70,7 @@ end # upperbound one everywhere except one_idx fixed to zero v_fixed1 = Boscia.bounded_compute_extreme_point(lmo, direction, zeros(M), (1:M) .!= one_idx, 1:M) @test v_fixed1[one_idx] == 0 + @test Boscia.is_simple_linear_feasible(lmo, v_fixed1) end end @testset "Fix a single entry to one" begin @@ -81,13 +83,14 @@ end ub = ones(M) v_fixed2 = Boscia.bounded_compute_extreme_point(lmo, direction, lb, ub, 1:M) @test v_fixed2[idx] == 1 + @test Boscia.is_simple_linear_feasible(lmo, v_fixed2) end end @testset "Fix two entries to one" begin for (i1, e1) in enumerate(edges(g)) for (i2, e2) in enumerate(edges(g)) # lighter on computation - if i1 ÷ 1000 + i2 ÷ 1000 != 0 + if i1 ÷ 2 + i2 ÷ 2 > 0 continue end # non-adjacent edges @@ -98,10 +101,18 @@ end v_fixed3 = Boscia.bounded_compute_extreme_point(lmo, direction, lb, ub, 1:M) @test v_fixed3[i1] == 1 @test v_fixed3[i2] == 1 + @test Boscia.is_simple_linear_feasible(lmo, v_fixed3) end end end end + # non-matching vector + v_wrong = 1.0 * copy(v) + idx1 = findfirst(==(Edge(1, 2)), collect(edges(g))) + idx2 = findfirst(==(Edge(1, 3)), collect(edges(g))) + v_wrong[idx1] = 0.75 + v_wrong[idx2] = 0.5 + @test !Boscia.is_simple_linear_feasible(lmo, v_wrong) end