Skip to content

Commit d3f4c03

Browse files
Sutekinaablegat
andauthored
Added bridge for List -> Partition conversion (#10)
* Added bridge for List -> Partition conversion * Added SetConversionBridge * removed bridge in test_tsp * Identity map * Simplify * simplify * Don't +1/-1 * Add test * Fix tests * Fix * Fix * Fix --------- Co-authored-by: Benoît Legat <benoit.legat@gmail.com>
1 parent 59e510a commit d3f4c03

5 files changed

Lines changed: 184 additions & 0 deletions

File tree

‎src/MathOptVRP.jl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import MathOptInterface as MOI
2121
import JuMP
2222

2323
include("sets.jl")
24+
include("bridges.jl")
2425
include("operators.jl")
2526
include("Tests.jl")
2627

‎src/bridges.jl‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# `SetConversionBridge{T,S2,S1}` lets a solver backend that only
2+
# implements `MOI.add_constrained_variables(model, ::S2)` transparently
3+
# also support `@variable(model, x[1:n] in S1(...))`, for any pair of
4+
# vector sets connected by a linear map (`S2` is the set actually added to
5+
# the underlying model, `S1` is the set exposed to the user). A new `S1 => S2` pair
6+
# only has to supply `map_set` / `inverse_map_set` (how the sets relate — generic here, via
7+
# `Base.convert`) and `map_function` / `inverse_map_function` (how the
8+
# *variables* relate — necessarily specific to the pair, since `S1` and
9+
# `S2` need not even share a dimension).
10+
#
11+
12+
"""
13+
SetConversionBridge{T,S2,S1} <: MOI.Bridges.Variable.SetMapBridge{T,S2,S1}
14+
15+
Bridges `MOI.VectorOfVariables`-in-`S1` (what the user asks for) to
16+
`MOI.VectorOfVariables`-in-`S2` (what gets added to the underlying model).
17+
"""
18+
struct SetConversionBridge{T,S2,S1} <: MOI.Bridges.Variable.SetMapBridge{T,S2,S1}
19+
variables::Vector{MOI.VariableIndex}
20+
constraint::MOI.ConstraintIndex{MOI.VectorOfVariables,S2}
21+
end
22+
23+
function MOI.Bridges.map_set(
24+
::Type{<:SetConversionBridge{T,S2,S1}},
25+
set::S2,
26+
) where {T,S2,S1}
27+
return convert(S1, set)
28+
end
29+
30+
function MOI.Bridges.inverse_map_set(
31+
::Type{<:SetConversionBridge{T,S2,S1}},
32+
set::S1,
33+
) where {T,S2,S1}
34+
return convert(S2, set)
35+
end
36+
37+
MOI.Bridges.map_function(::Type{<:SetConversionBridge}, func) = func
38+
39+
MOI.Bridges.inverse_map_function(::Type{<:SetConversionBridge}, func) = func
40+
41+
# The map is the identity so it is its own adjoint; without these, setting
42+
# `MOI.ConstraintDualStart` (or getting `MOI.ConstraintDual`) on a bridged
43+
# constraint errors.
44+
MOI.Bridges.adjoint_map_function(::Type{<:SetConversionBridge}, func) = func
45+
46+
function MOI.Bridges.inverse_adjoint_map_function(
47+
::Type{<:SetConversionBridge},
48+
func,
49+
)
50+
return func
51+
end
52+
53+
"""
54+
ListToPartitionBridge{T} = SetConversionBridge{T,Partition,List}
55+
56+
Bridges `List(n)` to `Partition(n, 1)`, for backends that support
57+
`Partition` but not `List`.
58+
"""
59+
const ListToPartitionBridge{T} = SetConversionBridge{T,Partition,List}

‎src/sets.jl‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ function JuMP.build_variable(
4646
)
4747
end
4848

49+
"""
50+
Base.convert(::Type{Partition}, s::List)
51+
52+
`List(n)` and `Partition(n, 1)` have the same flat dimension (`n`) and, for
53+
a single truck, the same "permutation of `0:n-1`" semantics, so this
54+
conversion always succeeds.
55+
"""
56+
Base.convert(::Type{Partition}, s::List) = Partition(s.dimension, 1)
57+
58+
"""
59+
Base.convert(::Type{List}, s::Partition)
60+
61+
Only defined when `s.num_trucks == 1`: a multi-truck `Partition` has no
62+
`List` equivalent.
63+
"""
64+
function Base.convert(::Type{List}, s::Partition)
65+
if s.num_trucks != 1
66+
# Julia v1.10's `InexactError` only accepts `(func, T, val)`; the
67+
# variadic constructor that takes a message is v1.11 or later.
68+
throw(InexactError(:convert, List, s))
69+
end
70+
return List(s.num_clients)
71+
end
72+
4973
"""
5074
PartitionPD(num_services::Int, num_pickup_deliveries::Int, num_trucks::Int)
5175

‎test/Bridges/set_conversion.jl‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
module TestSetConversion
2+
3+
using Test
4+
5+
import MathOptInterface as MOI
6+
import MathOptVRP
7+
8+
function runtests()
9+
for name in names(@__MODULE__; all = true)
10+
if startswith("$(name)", "test_")
11+
@testset "$(name)" begin
12+
getfield(@__MODULE__, name)()
13+
end
14+
end
15+
end
16+
return
17+
end
18+
19+
function test_runtests_ListToPartition()
20+
MOI.Bridges.runtests(
21+
MathOptVRP.ListToPartitionBridge,
22+
model -> MOI.add_constrained_variables(model, MathOptVRP.List(3)),
23+
model ->
24+
MOI.add_constrained_variables(model, MathOptVRP.Partition(3, 1)),
25+
)
26+
return
27+
end
28+
29+
function _set_objective(model, x)
30+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
31+
f = sum(Float64(i) * x[i] for i in eachindex(x))
32+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
33+
return
34+
end
35+
36+
# The variables are mapped through the bridge with the identity so the
37+
# objective must come out of the bridged model unchanged.
38+
function test_runtests_ListToPartition_objective()
39+
for n in (1, 4)
40+
MOI.Bridges.runtests(
41+
MathOptVRP.ListToPartitionBridge,
42+
model -> begin
43+
x, _ = MOI.add_constrained_variables(model, MathOptVRP.List(n))
44+
_set_objective(model, x)
45+
end,
46+
model -> begin
47+
x, _ = MOI.add_constrained_variables(
48+
model,
49+
MathOptVRP.Partition(n, 1),
50+
)
51+
_set_objective(model, x)
52+
end,
53+
)
54+
end
55+
return
56+
end
57+
58+
function test_supports_constrained_variable()
59+
BT = MathOptVRP.ListToPartitionBridge{Float64}
60+
@test MOI.Bridges.Variable.supports_constrained_variable(
61+
BT,
62+
MathOptVRP.List,
63+
)
64+
@test !MOI.Bridges.Variable.supports_constrained_variable(
65+
BT,
66+
MathOptVRP.Partition,
67+
)
68+
@test MOI.Bridges.added_constrained_variable_types(BT) ==
69+
[(MathOptVRP.Partition,)]
70+
@test isempty(MOI.Bridges.added_constraint_types(BT))
71+
return
72+
end
73+
74+
# `map_set` (bridge → user) and `inverse_map_set` (user → bridge) are the
75+
# `Base.convert` methods of `src/sets.jl`.
76+
function test_map_set()
77+
BT = MathOptVRP.ListToPartitionBridge{Float64}
78+
@test MOI.Bridges.inverse_map_set(BT, MathOptVRP.List(3)) ==
79+
MathOptVRP.Partition(3, 1)
80+
@test MOI.Bridges.map_set(BT, MathOptVRP.Partition(3, 1)) ==
81+
MathOptVRP.List(3)
82+
return
83+
end
84+
85+
end # module TestSetConversion
86+
87+
TestSetConversion.runtests()

‎test/runtests.jl‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ import MathOptInterface as MOI
2929
)
3030
end
3131

32+
@testset "List <=> Partition conversion" begin
33+
@test convert(MathOptVRP.Partition, MathOptVRP.List(5)) ==
34+
MathOptVRP.Partition(5, 1)
35+
@test convert(MathOptVRP.List, MathOptVRP.Partition(5, 1)) ==
36+
MathOptVRP.List(5)
37+
@test_throws InexactError convert(
38+
MathOptVRP.List,
39+
MathOptVRP.Partition(5, 2),
40+
)
41+
end
42+
3243
@testset "PartitionPD" begin
3344
s = MathOptVRP.PartitionPD(2, 3, 4) # 2 services + 2*3 pd = 8 rows, 4 trucks
3445
@test s.num_services == 2
@@ -107,6 +118,8 @@ import MathOptInterface as MOI
107118
@test expr.args[2] === nodes
108119
end
109120

121+
include("Bridges/set_conversion.jl")
122+
110123
include("test_ortools.jl")
111124

112125
@testset "JuMP overrides (type piracy)" begin

0 commit comments

Comments
 (0)