Skip to content

Commit 5a2ad66

Browse files
authored
Merge pull request #19 from ZIB-IOL/add_Birkhoff_tests
Add birkhoff tests and comments on the functions
2 parents 1025951 + 242af07 commit 5a2ad66

3 files changed

Lines changed: 221 additions & 45 deletions

File tree

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ julia = "1.10"
2828

2929
[extras]
3030
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
31+
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
3132
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3233

3334
[targets]
34-
test = ["Test", "HiGHS"]
35+
test = ["Test", "HiGHS", "StableRNGs"]

src/birkhoff_polytope.jl

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""
2-
BirkhoffLMO
2+
BirkhoffLMO(dim, int_vars; append_by_column=true, atol=1e-6, rtol=1e-3)
33
4-
A bounded LMO for the Birkhoff polytope. This oracle computes an extreme point subject to
5-
node-specific bounds on the integer variables.
4+
A bounded Linear Minimization Oracle (LMO) for the Birkhoff polytope. The oracle
5+
computes extreme points (permutation matrices) possibly under node-specific bound
6+
constraints on a subset of integer variables. It also supports mixed-integer
7+
variants, partial fixings (reduced maps), and in-face oracles used by DICG-like methods.
68
"""
79
mutable struct BirkhoffLMO <: FrankWolfe.LinearMinimizationOracle
810
append_by_column::Bool
@@ -17,10 +19,14 @@ mutable struct BirkhoffLMO <: FrankWolfe.LinearMinimizationOracle
1719
updated_lmo::Bool
1820
atol::Float64
1921
rtol::Float64
20-
boscia_use::Bool
2122
end
2223

23-
# return an integer-type BirkhoffLMO
24+
"""
25+
BirkhoffLMO(dim, int_vars; append_by_column=true, atol=1e-6, rtol=1e-3)
26+
27+
Constructor for a mixed-integer Birkhoff LMO. All variables listed in
28+
`int_vars` are treated as integer with default bounds `[0,1]`.
29+
"""
2430
BirkhoffLMO(dim, int_vars; append_by_column=true, atol=1e-6, rtol=1e-3) = BirkhoffLMO(
2531
append_by_column,
2632
dim,
@@ -34,10 +40,8 @@ BirkhoffLMO(dim, int_vars; append_by_column=true, atol=1e-6, rtol=1e-3) = Birkho
3440
true,
3541
atol,
3642
rtol,
37-
true,
3843
)
3944

40-
# return a continuous BirkhoffLMO
4145
BirkhoffLMO(dim; append_by_column=true, atol=1e-6, rtol=1e-3) = BirkhoffLMO(
4246
append_by_column,
4347
dim,
@@ -51,15 +55,15 @@ BirkhoffLMO(dim; append_by_column=true, atol=1e-6, rtol=1e-3) = BirkhoffLMO(
5155
true,
5256
atol,
5357
rtol,
54-
false,
5558
)
5659

5760
## Necessary
5861

59-
"""
60-
Computes the extreme point given an direction d, the current lower and upper bounds on the integer variables, and the set of integer variables.
61-
"""
62-
function Boscia.compute_extreme_point(lmo::BirkhoffLMO, d::AbstractMatrix{T}; kwargs...) where {T}
62+
function FrankWolfe.compute_extreme_point(
63+
lmo::BirkhoffLMO,
64+
d::AbstractMatrix{T};
65+
kwargs...,
66+
) where {T}
6367
n = lmo.dim
6468

6569
fixed_to_one_rows = lmo.fixed_to_one_rows
@@ -133,7 +137,11 @@ function Boscia.compute_extreme_point(lmo::BirkhoffLMO, d::AbstractMatrix{T}; kw
133137
return m
134138
end
135139

136-
function Boscia.compute_extreme_point(lmo::BirkhoffLMO, d::AbstractVector{T}; kwargs...) where {T}
140+
function FrankWolfe.compute_extreme_point(
141+
lmo::BirkhoffLMO,
142+
d::AbstractVector{T};
143+
kwargs...,
144+
) where {T}
137145
n = lmo.dim
138146
d = lmo.append_by_column ? reshape(d, (n, n)) : transpose(reshape(d, (n, n)))
139147
m = Boscia.compute_extreme_point(lmo, d; kwargs...)
@@ -152,9 +160,6 @@ function Boscia.compute_extreme_point(lmo::BirkhoffLMO, d::AbstractVector{T}; kw
152160
return m
153161
end
154162

155-
"""
156-
Computes the extreme point given an direction d, the current lower and upper bounds on the integer variables, and the set of integer variables.
157-
"""
158163
function FrankWolfe.compute_inface_extreme_point(
159164
lmo::BirkhoffLMO,
160165
direction::AbstractMatrix{T},
@@ -186,7 +191,7 @@ function FrankWolfe.compute_inface_extreme_point(
186191
for i in 1:nreduced
187192
row_orig = index_map_rows[i]
188193
col_orig = index_map_cols[j]
189-
if x[row_orig, col_orig] >= 1-eps()
194+
if x[row_orig, col_orig] >= 1 - eps()
190195
push!(fixed_to_one_rows, row_orig)
191196
push!(fixed_to_one_cols, col_orig)
192197

@@ -210,9 +215,9 @@ function FrankWolfe.compute_inface_extreme_point(
210215
for i in 1:nreduced
211216
row_orig = index_map_rows[i]
212217
if lmo.append_by_column
213-
orig_linear_idx = (col_orig-1)*n+row_orig
218+
orig_linear_idx = (col_orig - 1) * n + row_orig
214219
else
215-
orig_linear_idx = (row_orig-1)*n+col_orig
220+
orig_linear_idx = (row_orig - 1) * n + col_orig
216221
end
217222
if x[row_orig, col_orig] <= eps()
218223
if lmo.append_by_column
@@ -289,10 +294,6 @@ function FrankWolfe.compute_inface_extreme_point(
289294
return m
290295
end
291296

292-
"""
293-
LMO-like operation which computes a vertex minimizing in `direction` on the face defined by the current fixings.
294-
Fixings are maintained by the oracle (or deduced from `x` itself).
295-
"""
296297
function FrankWolfe.dicg_maximum_step(lmo::BirkhoffLMO, direction, x; kwargs...)
297298
n = lmo.dim
298299
T = promote_type(eltype(x), eltype(direction))
@@ -321,9 +322,6 @@ function FrankWolfe.is_decomposition_invariant_oracle(lmo::BirkhoffLMO)
321322
return true
322323
end
323324

324-
"""
325-
The sum of each row and column has to be equal to 1.
326-
"""
327325
function Boscia.is_linear_feasible(blmo::BirkhoffLMO, v::AbstractVector)
328326
for (i, int_var) in enumerate(blmo.int_vars)
329327
if !(
@@ -351,7 +349,6 @@ function Boscia.is_linear_feasible(blmo::BirkhoffLMO, v::AbstractVector)
351349
return true
352350
end
353351

354-
# Read global bounds from the problem.
355352
function Boscia.build_global_bounds(blmo::BirkhoffLMO, integer_variables)
356353
global_bounds = Boscia.IntegerBounds()
357354
for (idx, int_var) in enumerate(blmo.int_vars)
@@ -361,34 +358,27 @@ function Boscia.build_global_bounds(blmo::BirkhoffLMO, integer_variables)
361358
return global_bounds
362359
end
363360

364-
# Get list of variables indices.
365-
# If the problem has n variables, they are expected to contiguous and ordered from 1 to n.
366361
function Boscia.get_list_of_variables(blmo::BirkhoffLMO)
367362
n = blmo.dim^2
368363
return n, collect(1:n)
369364
end
370365

371-
# Get list of integer variables
372366
function Boscia.get_integer_variables(blmo::BirkhoffLMO)
373367
return blmo.int_vars
374368
end
375369

376-
# Get the index of the integer variable the bound is working on.
377370
function Boscia.get_int_var(blmo::BirkhoffLMO, cidx)
378371
return blmo.int_vars[cidx]
379372
end
380373

381-
# Get the list of lower bounds.
382374
function Boscia.get_lower_bound_list(blmo::BirkhoffLMO)
383375
return collect(1:length(blmo.lower_bounds))
384376
end
385377

386-
# Get the list of upper bounds.
387378
function Boscia.get_upper_bound_list(blmo::BirkhoffLMO)
388379
return collect(1:length(blmo.upper_bounds))
389380
end
390381

391-
# Read bound value for c_idx.
392382
function Boscia.get_bound(blmo::BirkhoffLMO, c_idx, sense::Symbol)
393383
if sense == :lessthan
394384
return blmo.upper_bounds[c_idx]
@@ -401,7 +391,6 @@ end
401391

402392
## Changing the bounds constraints.
403393

404-
# Change the value of the bound c_idx.
405394
function Boscia.set_bound!(blmo::BirkhoffLMO, c_idx, value, sense::Symbol)
406395
# Reset the lmo if necessary
407396
if blmo.updated_lmo
@@ -432,7 +421,12 @@ function Boscia.set_bound!(blmo::BirkhoffLMO, c_idx, value, sense::Symbol)
432421
end
433422
end
434423

435-
# Delete bounds.
424+
"""
425+
Boscia.delete_bounds!(blmo::BirkhoffLMO, cons_delete)
426+
427+
Delete a collection of bounds given as pairs `(idx, sense)` and rebuild the reduced index maps `index_map_rows`
428+
and `index_map_cols` based on entries fixed to one.
429+
"""
436430
function Boscia.delete_bounds!(blmo::BirkhoffLMO, cons_delete)
437431
for (d_idx, sense) in cons_delete
438432
if sense == :greaterthan
@@ -469,7 +463,12 @@ function Boscia.delete_bounds!(blmo::BirkhoffLMO, cons_delete)
469463
return true
470464
end
471465

472-
# Add bound constraint.
466+
"""
467+
Boscia.add_bound_constraint!(blmo::BirkhoffLMO, key, value, sense::Symbol)
468+
469+
Add or overwrite a single bound for the integer variable. If a lower bound is set to `1.0`,
470+
update `fixed_to_one_rows` and `fixed_to_one_cols`.
471+
"""
473472
function Boscia.add_bound_constraint!(blmo::BirkhoffLMO, key, value, sense::Symbol)
474473
idx = findfirst(x -> x == key, blmo.int_vars)
475474
if sense == :greaterthan
@@ -497,25 +496,20 @@ end
497496

498497
## Checks
499498

500-
# Check if the subject of the bound c_idx is an integer variable (recorded in int_vars).
501499
function Boscia.is_constraint_on_int_var(blmo::BirkhoffLMO, c_idx, int_vars)
502500
return blmo.int_vars[c_idx] in int_vars
503501
end
504502

505-
# To check if there is bound for the variable in the global or node bounds.
506503
function Boscia.is_bound_in(blmo::BirkhoffLMO, c_idx, bounds)
507504
return haskey(bounds, blmo.int_vars[c_idx])
508505
end
509506

510-
# Has variable an integer constraint?
511507
function Boscia.has_integer_constraint(blmo::BirkhoffLMO, idx)
512508
return idx in blmo.int_vars
513509
end
514510

515511
## Safety Functions
516512

517-
# Check if the bounds were set correctly in build_LMO.
518-
# Safety check only.
519513
function Boscia.build_LMO_correct(blmo::BirkhoffLMO, node_bounds)
520514
for key in keys(node_bounds.lower_bounds)
521515
idx = findfirst(x -> x == key, blmo.int_vars)
@@ -534,6 +528,13 @@ end
534528

535529
## Optional
536530

531+
"""
532+
Boscia.check_feasibility(blmo::BirkhoffLMO)
533+
534+
Quick feasibility test for the bounds alone (without a specific `x`). It validates
535+
that `ub ≥ lb` componentwise and that row/column sums can still achieve `1` given
536+
the current lower/upper bounds.
537+
"""
537538
function Boscia.check_feasibility(blmo::BirkhoffLMO)
538539
for (lb, ub) in zip(blmo.lower_bounds, blmo.upper_bounds)
539540
if ub < lb

0 commit comments

Comments
 (0)