Skip to content

Commit 1d78e06

Browse files
authored
bounded LMO version for the matching polytope (#24)
This PR adds the simple BLMO functions for the matching polytope. The overhead cost of creating the perturbed direction doesn't seem to be high so this can stay the default, as opposed to creating a fully-fledged BLMO with bound management
1 parent 58ccca9 commit 1d78e06

2 files changed

Lines changed: 110 additions & 7 deletions

File tree

src/matchings.jl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,59 @@ function FrankWolfe.compute_extreme_point(
6161
return v
6262
end
6363

64+
function Boscia.bounded_compute_extreme_point(lmo::MatchingLMO, direction, lb, ub, int_vars; kwargs...)
65+
# any entry i fixed to zero -> use a positive direction, ensuring the edge is not taken
66+
# any entry i fixed to one with neighbors (u, v) -> use positive direction for all other neighbors of u, of v
67+
corrected_direction = copy(direction)
68+
for (idx, edge) in enumerate(edges(lmo.original_graph))
69+
if ub[idx] 0
70+
@assert lb[idx] 0
71+
corrected_direction[idx] = 1
72+
elseif lb[idx] 1
73+
@assert ub[idx] 1
74+
(vtx1, vtx2) = Tuple(edge)
75+
# negative cost ensures the edge is taken, since no neighbor will be in the matching
76+
corrected_direction[idx] = -1
77+
for (idx2, e2) in enumerate(edges(lmo.original_graph))
78+
# check if e2 is adjacent to edge
79+
# we want one of the two nodes to be the same
80+
if xor(src(e2) in (vtx1, vtx2), dst(e2) in (vtx1, vtx2))
81+
# we should not have incompatible edges fixed to one
82+
@assert lb[idx2] 0 "incompatible edges $edge $e2"
83+
corrected_direction[idx2] = 1
84+
end
85+
end
86+
end
87+
end
88+
v = FrankWolfe.compute_extreme_point(lmo, corrected_direction)
89+
@debug begin
90+
for idx in eachindex(direction)
91+
if ub[idx] 0
92+
@assert v[idx] 0
93+
elseif lb[idx] 1
94+
@assert v[idx] 1
95+
end
96+
end
97+
end
98+
return v
99+
end
100+
101+
function Boscia.is_simple_linear_feasible(lmo::MatchingLMO, v)
102+
vertex_matching = zeros(Graphs.nv(lmo.original_graph))
103+
for (idx, edge) in enumerate(edges(lmo.original_graph))
104+
if v[idx] 0
105+
continue
106+
end
107+
vtx1, vtx2 = Tuple(edge)
108+
vertex_matching[vtx1] += v[idx]
109+
vertex_matching[vtx2] += v[idx]
110+
end
111+
# one vertex fractionally matched to multiple edges
112+
if maximum(vertex_matching) >= 1 + 1e-4
113+
return false
114+
end
115+
return true
116+
end
64117

65118
"""
66119
PerfectMatchingLMO{G}(g::Graphs)

test/runtests.jl

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,81 @@ Random.seed!(StableRNG(42), 42)
3838
end
3939

4040
@testset "Matching LMO" begin
41-
N = 200
41+
N = 50
4242
Random.seed!(9754)
4343
g = Graphs.complete_graph(N)
4444
iter = collect(Graphs.edges(g))
4545
M = length(iter)
4646
direction = randn(M)
4747
lmo = CO.MatchingLMO(g)
4848
v = FrankWolfe.compute_extreme_point(lmo, direction)
49+
@test Boscia.is_simple_linear_feasible(lmo, v)
4950
adj_mat = spzeros(M, M)
50-
for i in 1:M
51-
adj_mat[src(iter[i]), dst(iter[i])] = direction[i]
51+
for (i, edge) in enumerate(edges(g))
52+
adj_mat[src(edge), dst(edge)] = direction[i]
5253
end
5354
match_result = GraphsMatching.maximum_weight_matching(g, HiGHS.Optimizer, -adj_mat)
5455
v_sol = spzeros(M)
5556
K = length(match_result.mate)
56-
for i in 1:K
57-
for j in 1:M
58-
if (match_result.mate[i] == src(iter[j]) && dst(iter[j]) == i)
59-
v_sol[j] = 1
57+
for k in 1:K
58+
for (i, edge) in enumerate(edges(g))
59+
if (match_result.mate[k] == src(edge) && dst(edge) == k)
60+
v_sol[i] = 1
6061
end
6162
end
6263
end
6364
@test v_sol == v
6465
v2 = FrankWolfe.compute_extreme_point(lmo, ones(M))
6566
@test norm(v2) == 0
67+
@test v == Boscia.bounded_compute_extreme_point(lmo, direction, zeros(M), ones(M), 1:M)
68+
@testset "Fix one entry to zero" begin
69+
for one_idx in SparseArrays.nonzeroinds(v)
70+
# upperbound one everywhere except one_idx fixed to zero
71+
v_fixed1 = Boscia.bounded_compute_extreme_point(lmo, direction, zeros(M), (1:M) .!= one_idx, 1:M)
72+
@test v_fixed1[one_idx] == 0
73+
@test Boscia.is_simple_linear_feasible(lmo, v_fixed1)
74+
end
75+
end
76+
@testset "Fix a single entry to one" begin
77+
for idx in rand(1:M, 100)
78+
# skip if entry already at one
79+
if v[idx] == 1
80+
continue
81+
end
82+
lb = (1:M) .== idx
83+
ub = ones(M)
84+
v_fixed2 = Boscia.bounded_compute_extreme_point(lmo, direction, lb, ub, 1:M)
85+
@test v_fixed2[idx] == 1
86+
@test Boscia.is_simple_linear_feasible(lmo, v_fixed2)
87+
end
88+
end
89+
@testset "Fix two entries to one" begin
90+
for (i1, e1) in enumerate(edges(g))
91+
for (i2, e2) in enumerate(edges(g))
92+
# lighter on computation
93+
if i1 ÷ 2 + i2 ÷ 2 > 0
94+
continue
95+
end
96+
# non-adjacent edges
97+
if isempty(intersect(Tuple(e1), Tuple(e2)))
98+
lb = zeros(M)
99+
ub = ones(M)
100+
lb[i1] = lb[i2] = 1
101+
v_fixed3 = Boscia.bounded_compute_extreme_point(lmo, direction, lb, ub, 1:M)
102+
@test v_fixed3[i1] == 1
103+
@test v_fixed3[i2] == 1
104+
@test Boscia.is_simple_linear_feasible(lmo, v_fixed3)
105+
end
106+
end
107+
end
108+
end
109+
# non-matching vector
110+
v_wrong = 1.0 * copy(v)
111+
idx1 = findfirst(==(Edge(1, 2)), collect(edges(g)))
112+
idx2 = findfirst(==(Edge(1, 3)), collect(edges(g)))
113+
v_wrong[idx1] = 0.75
114+
v_wrong[idx2] = 0.5
115+
@test !Boscia.is_simple_linear_feasible(lmo, v_wrong)
66116
end
67117

68118

0 commit comments

Comments
 (0)