From f6d9ebd0c7585d5871bade46639a9c2507f8adc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 5 Aug 2026 08:26:41 +0200 Subject: [PATCH 1/3] Add bridges to map to zero-based --- src/Hexaly.jl | 2 + src/MOI/bridges.jl | 120 +++++++++++++++++++++++++++++ src/MOI/list.jl | 39 ++++++---- src/MOI/sets.jl | 46 +++++++++++ src/MOI/sum_distances_objective.jl | 28 ++++++- src/MOI/wrapper.jl | 7 ++ 6 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 src/MOI/bridges.jl create mode 100644 src/MOI/sets.jl diff --git a/src/Hexaly.jl b/src/Hexaly.jl index 609c977..b3cb733 100644 --- a/src/Hexaly.jl +++ b/src/Hexaly.jl @@ -73,6 +73,8 @@ include("MOI/wrapper_constraints.jl") include("MOI/wrapper_constraints_singlevar.jl") include("MOI/wrapper_constraints_linear.jl") include("MOI/wrapper_constraints_cp.jl") +include("MOI/sets.jl") +include("MOI/bridges.jl") include("MOI/list.jl") include("MOI/sum_distances_objective.jl") diff --git a/src/MOI/bridges.jl b/src/MOI/bridges.jl new file mode 100644 index 0000000..765796e --- /dev/null +++ b/src/MOI/bridges.jl @@ -0,0 +1,120 @@ +# `MathOptVRP` sets are 1-based, Hexaly's `list` decision variables are +# 0-based. Rather than special-casing the offset everywhere, we let a +# variable bridge do it once: the model sent to Hexaly is written with the +# 0-based sets of `sets.jl` and every occurrence of a node variable in the +# user's functions is substituted by `y + 1`. +# +# `_shift_to_zero_based` in `sum_distances_objective.jl` takes the `-1` back +# out when lowering `:sum_distances` and the routing constraints, so the two +# offsets cancel and Hexaly indexes its arrays 0-based as it expects. + +""" + ZeroBasedBridge{T,S1,S2} <: MOI.Bridges.Variable.SetMapBridge{T,S1,S2} + +Bridges the 1-based `MathOptVRP` set `S2` into its 0-based Hexaly +counterpart `S1` with the substitution rule `x = y + 1`. + +Because the map is a translation, its linear part is the identity, so both +adjoint maps are the identity as well. +""" +struct ZeroBasedBridge{T,S1,S2} <: + MOI.Bridges.Variable.SetMapBridge{T,S1,S2} + variables::Vector{MOI.VariableIndex} + constraint::MOI.ConstraintIndex{MOI.VectorOfVariables,S1} +end + +""" + ListBridge{T} = ZeroBasedBridge{T,List,MathOptVRP.List} + +Bridges `MathOptVRP.List(n)` into [`List(n)`](@ref). +""" +const ListBridge{T} = ZeroBasedBridge{T,List,MathOptVRP.List} + +""" + PartitionBridge{T} = ZeroBasedBridge{T,Partition,MathOptVRP.Partition} + +Bridges `MathOptVRP.Partition` into [`Partition`](@ref). +""" +const PartitionBridge{T} = ZeroBasedBridge{T,Partition,MathOptVRP.Partition} + +""" + PartitionPDBridge{T} = ZeroBasedBridge{T,PartitionPD,MathOptVRP.PartitionPD} + +Bridges `MathOptVRP.PartitionPD` into [`PartitionPD`](@ref). +""" +const PartitionPDBridge{T} = + ZeroBasedBridge{T,PartitionPD,MathOptVRP.PartitionPD} + +# `map_set` goes Hexaly -> MathOptVRP, `inverse_map_set` the other way; the +# shape is unchanged, only the value convention differs. + +MOI.Bridges.map_set(::Type{<:ListBridge}, s::List) = MathOptVRP.List(s.dimension) + +function MOI.Bridges.inverse_map_set(::Type{<:ListBridge}, s::MathOptVRP.List) + return List(s.dimension) +end + +function MOI.Bridges.map_set(::Type{<:PartitionBridge}, s::Partition) + return MathOptVRP.Partition(s.num_clients, s.num_trucks) +end + +function MOI.Bridges.inverse_map_set( + ::Type{<:PartitionBridge}, + s::MathOptVRP.Partition, +) + return Partition(s.num_clients, s.num_trucks) +end + +function MOI.Bridges.map_set(::Type{<:PartitionPDBridge}, s::PartitionPD) + return MathOptVRP.PartitionPD( + s.num_services, + s.num_pickup_deliveries, + s.num_trucks, + ) +end + +function MOI.Bridges.inverse_map_set( + ::Type{<:PartitionPDBridge}, + s::MathOptVRP.PartitionPD, +) + return PartitionPD( + s.num_services, + s.num_pickup_deliveries, + s.num_trucks, + ) +end + +# `MOI.Utilities.operate` wants the shift to match the shape of `func`: a +# scalar for `unbridged_map`'s `MOI.VariableIndex`, a vector for the +# functions and for the solution vectors of `MOI.VariablePrimal`. +function _translate(::Type{T}, func::Vector, α::T) where {T} + return func .+ α +end + +function _translate(::Type{T}, func::MOI.AbstractVectorFunction, α::T) where {T} + return MOI.Utilities.operate(+, T, func, fill(α, MOI.output_dimension(func))) +end + +function _translate(::Type{T}, func, α::T) where {T} + return MOI.Utilities.operate(+, T, func, α) +end + +function MOI.Bridges.map_function(::Type{<:ZeroBasedBridge{T}}, func) where {T} + return _translate(T, func, one(T)) +end + +function MOI.Bridges.inverse_map_function( + ::Type{<:ZeroBasedBridge{T}}, + func, +) where {T} + return _translate(T, func, -one(T)) +end + +MOI.Bridges.adjoint_map_function(::Type{<:ZeroBasedBridge}, func) = func + +function MOI.Bridges.inverse_adjoint_map_function( + ::Type{<:ZeroBasedBridge}, + func, +) + return func +end diff --git a/src/MOI/list.jl b/src/MOI/list.jl index 17d7933..685acde 100644 --- a/src/MOI/list.jl +++ b/src/MOI/list.jl @@ -1,7 +1,7 @@ -# Hexaly-specific implementations of the `MathOptVRP` vector sets. The set -# definitions, JuMP `build_variable` overloads and `MOI.dimension` methods -# live in `MathOptVRP`; here we only provide the `MOI.add_constrained_variables` -# / `MOI.add_constraint` methods that realise each set with Hexaly's C API. +# Realisation of the routing sets with Hexaly's C API. The variable sets are +# the 0-based ones of `sets.jl` (the `MathOptVRP` ones reach them through the +# bridges of `bridges.jl`); the constraint sets are `MathOptVRP`'s, and their +# node items are shifted back to 0-based by `_shift_to_zero_based!`. # Create `n` MOI variables backed by `hx_list[0..n-1]`, each tagged with # `parent_list = hx_list` so the objective handler can recover the list. @@ -24,22 +24,22 @@ function _add_list_variables!(m::Optimizer, hx_list::HxExpression, n::Int) return indices end -# ── MathOptVRP.List ──────────────────────────────────────────────────── +# ── Hexaly.List ──────────────────────────────────────────────────── function MOI.supports_add_constrained_variables( ::Optimizer, - ::Type{MathOptVRP.List}, + ::Type{List}, ) return true end -function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.List) +function MOI.add_constrained_variables(m::Optimizer, set::List) n = set.dimension hx_list = list!(m.model, n) # Pin the list's count so the solver can't pick a shorter list. _add_hexaly_constraint!(m, eq(m.model, count_(m.model, hx_list), n)) indices = _add_list_variables!(m, hx_list, n) - cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,MathOptVRP.List}( + cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,List}( length(m.constraint_info) + 1, ) m.constraint_info[cindex] = @@ -47,16 +47,16 @@ function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.List) return indices, cindex end -# ── MathOptVRP.Partition ─────────────────────────────────────────────── +# ── Hexaly.Partition ─────────────────────────────────────────────── function MOI.supports_add_constrained_variables( ::Optimizer, - ::Type{MathOptVRP.Partition}, + ::Type{Partition}, ) return true end -function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.Partition) +function MOI.add_constrained_variables(m::Optimizer, set::Partition) md = m.model lists = HxExpression[list!(md, set.num_clients) for _ = 1:(set.num_trucks)] _add_hexaly_constraint!(m, partition(md, lists)) @@ -65,7 +65,7 @@ function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.Partition) col_indices = _add_list_variables!(m, hx_list, set.num_clients) append!(indices, col_indices) end - cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,MathOptVRP.Partition}( + cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,Partition}( length(m.constraint_info) + 1, ) m.constraint_info[cindex] = @@ -73,18 +73,18 @@ function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.Partition) return indices, cindex end -# ── MathOptVRP.PartitionPD ───────────────────────────────────────────── +# ── Hexaly.PartitionPD ───────────────────────────────────────────── function MOI.supports_add_constrained_variables( ::Optimizer, - ::Type{MathOptVRP.PartitionPD}, + ::Type{PartitionPD}, ) return true end -function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.PartitionPD) +function MOI.add_constrained_variables(m::Optimizer, set::PartitionPD) md = m.model - n_total = MathOptVRP._pd_n_total(set) + n_total = _pd_n_total(set) lists = HxExpression[list!(md, n_total) for _ = 1:(set.num_trucks)] _add_hexaly_constraint!(m, partition(md, lists)) for k = 0:(set.num_pickup_deliveries-1) @@ -102,7 +102,7 @@ function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.PartitionPD col_indices = _add_list_variables!(m, hx_list, n_total) append!(indices, col_indices) end - cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,MathOptVRP.PartitionPD}( + cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,PartitionPD}( length(m.constraint_info) + 1, ) m.constraint_info[cindex] = @@ -132,6 +132,8 @@ function MOI.add_constraint( "MathOptVRP.TimeWindows expected `length([t; depot_start; nodes; depot_end]) ", "== $(MOI.dimension(s))`; got $(length(items)).", ) + # Item 1 is the user's `t` variable, items 2..end are node values. + _shift_to_zero_based!(items, 2) items[1] isa MOI.VariableIndex || error( "MathOptVRP.TimeWindows: first item must be the total-time `t` variable.", ) @@ -239,6 +241,7 @@ function MOI.add_constraint( "MathOptVRP.Capacity expected `length(nodes) == $(MOI.dimension(s))`; ", "got $(length(items)).", ) + _shift_to_zero_based!(items) all(it -> it isa MOI.VariableIndex, items) || error( "MathOptVRP.Capacity: every item must be a `MOI.VariableIndex` backed by ", "a Hexaly list.", @@ -306,6 +309,8 @@ function MOI.add_constraint( "`length([t; depot_start; nodes; depot_end]) == $(MOI.dimension(s))`; ", "got $(length(items)).", ) + # Item 1 is the user's `t` variable, items 2..end are node values. + _shift_to_zero_based!(items, 2) items[1] isa MOI.VariableIndex || error( "MathOptVRP.CapacitatedTimeWindows: first item must be the total-time ", "`t` variable.", diff --git a/src/MOI/sets.jl b/src/MOI/sets.jl new file mode 100644 index 0000000..5a33743 --- /dev/null +++ b/src/MOI/sets.jl @@ -0,0 +1,46 @@ +# Hexaly's counterparts of the `MathOptVRP` variable sets. They have the +# same shape, the difference is the value convention: a Hexaly `list` +# decision variable takes values in `0:n-1` and Hexaly indexes arrays +# 0-based, while `MathOptVRP` is 1-based like the rest of MOI. The +# `ZeroBasedBridge`s of `bridges.jl` convert between the two, so a model +# written against `MathOptVRP` never sees these sets. + +""" + List(dimension::Int) + +The 0-based counterpart of [`MathOptVRP.List`](@ref): the `dimension` +variables form a permutation of `0:dimension-1`. +""" +struct List <: MOI.AbstractVectorSet + dimension::Int +end + +MOI.dimension(s::List) = s.dimension + +""" + Partition(num_clients::Int, num_trucks::Int) + +The 0-based counterpart of [`MathOptVRP.Partition`](@ref): the columns +together partition `0:num_clients-1`. +""" +struct Partition <: MOI.AbstractVectorSet + num_clients::Int + num_trucks::Int +end + +MOI.dimension(s::Partition) = s.num_clients * s.num_trucks + +""" + PartitionPD(num_services::Int, num_pickup_deliveries::Int, num_trucks::Int) + +The 0-based counterpart of [`MathOptVRP.PartitionPD`](@ref). +""" +struct PartitionPD <: MOI.AbstractVectorSet + num_services::Int + num_pickup_deliveries::Int + num_trucks::Int +end + +_pd_n_total(s::PartitionPD) = s.num_services + 2 * s.num_pickup_deliveries + +MOI.dimension(s::PartitionPD) = _pd_n_total(s) * s.num_trucks diff --git a/src/MOI/sum_distances_objective.jl b/src/MOI/sum_distances_objective.jl index a269e83..7324e37 100644 --- a/src/MOI/sum_distances_objective.jl +++ b/src/MOI/sum_distances_objective.jl @@ -36,7 +36,7 @@ function _build_sum_distances_expression(m::Optimizer, f::MOI.ScalarNonlinearFun ) dist_matrix = f.args[1]::AbstractMatrix{<:Real} nodes_raw = f.args[2] - items = _normalize_sum_distances_items(nodes_raw) + items = _shift_to_zero_based!(_normalize_sum_distances_items(nodes_raw)) md = m.model n_rows = size(dist_matrix, 1) @@ -117,6 +117,32 @@ function _vector_affine_to_items(f::MOI.VectorAffineFunction) return items end +# `MathOptVRP` node values are 1-based, like the rest of MOI, while Hexaly's +# `list` decision variables take values in `0:n-1` and index arrays 0-based. +# The `ZeroBasedBridge` of `bridges.jl` substituted every node variable by +# `y + 1`, so taking that one back out recovers the raw Hexaly variable and, +# for a constant node such as a depot, the 0-based index. +function _shift_to_zero_based!(items, from::Int = 1) + for k = from:length(items) + items[k] = _zero_based(items[k]) + end + return items +end + +_zero_based(x::Real) = x - 1 + +function _zero_based(f::MOI.ScalarAffineFunction) + return _simplify_item(MOI.ScalarAffineFunction(f.terms, f.constant - 1)) +end + +function _zero_based(vi::MOI.VariableIndex) + return error( + "Hexaly: the node variable `$vi` was not offset by `Hexaly.ZeroBasedBridge`. ", + "Use `MOI.instantiate(Hexaly.Optimizer; with_bridge_type = Float64)` or ", + "`JuMP.Model(Hexaly.Optimizer)` so that the bridge is added.", + ) +end + _simplify_item(x) = x function _simplify_item(f::MOI.ScalarAffineFunction) if isempty(f.terms) diff --git a/src/MOI/wrapper.jl b/src/MOI/wrapper.jl index b0403a0..631e679 100644 --- a/src/MOI/wrapper.jl +++ b/src/MOI/wrapper.jl @@ -139,6 +139,13 @@ function MOI.get(::Optimizer, ::MOI.SolverVersion) return string(version()) end +# The optimizer only supports the 0-based sets of `sets.jl`; users write +# their model with the 1-based `MathOptVRP` sets and these bridges do the +# conversion. See `bridges.jl`. +function MOI.get(::Optimizer, ::MOI.Bridges.ListOfNonstandardBridges{T}) where {T} + return Type[ListBridge{T}, PartitionBridge{T}, PartitionPDBridge{T}] +end + # Name function MOI.supports(::Optimizer, ::MOI.Name) From 7cdebce2b7a816e6f7feec9a516faf293563e7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 7 Aug 2026 18:26:14 +0200 Subject: [PATCH 2/3] Pad with -1 --- src/MOI/list.jl | 53 +++++++++++++++++++++++++++++++++++++++++++++++-- test/jump.jl | 25 ++++++----------------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/MOI/list.jl b/src/MOI/list.jl index 685acde..ef5408d 100644 --- a/src/MOI/list.jl +++ b/src/MOI/list.jl @@ -5,17 +5,30 @@ # Create `n` MOI variables backed by `hx_list[0..n-1]`, each tagged with # `parent_list = hx_list` so the objective handler can recover the list. +# +# A list may be shorter than `n` (the parts of a partition have a variable +# count), but the MOI variables exist for every position, so the ones past +# `count()` have to report something. Hexaly's `at` answers `0` there, which +# is a node like any other, so we substitute `-1`: the `ZeroBasedBridge` +# adds one to it and `MathOptVRP` sees the `0` that marks the end of the +# sequence. function _add_list_variables!(m::Optimizer, hx_list::HxExpression, n::Int) + md = m.model indices = MOI.VariableIndex[] for i = 0:(n-1) - elem = at(m.model, hx_list, i) + elem = iif( + md, + lt(md, i, count_(md, hx_list)), + at(md, hx_list, i), + -1, + ) info = VariableInfo( MOI.VariableIndex(0), elem; is_integer = true, parent_list = hx_list, ) - info.lb = 0.0 + info.lb = -1.0 info.ub = Float64(n - 1) idx = MOI.Utilities.CleverDicts.add_item(m.variable_info, info) _info(m, idx).index = idx @@ -122,6 +135,18 @@ function MOI.supports_constraint( return true end +# These sets constrain node variables that a `Partition` / `PartitionPD` +# already created; they cannot create any. Without this, MOI's default +# infers `supports_add_constrained_variables` from the `VectorOfVariables` +# constraint above and `copy_to` builds the variables from this set instead +# of from the partition, leaving them without a backing Hexaly list. +function MOI.supports_add_constrained_variables( + ::Optimizer, + ::Type{<:MathOptVRP.TimeWindows}, +) + return false +end + function MOI.add_constraint( m::Optimizer, f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, @@ -231,6 +256,18 @@ function MOI.supports_constraint( return true end +# These sets constrain node variables that a `Partition` / `PartitionPD` +# already created; they cannot create any. Without this, MOI's default +# infers `supports_add_constrained_variables` from the `VectorOfVariables` +# constraint above and `copy_to` builds the variables from this set instead +# of from the partition, leaving them without a backing Hexaly list. +function MOI.supports_add_constrained_variables( + ::Optimizer, + ::Type{<:MathOptVRP.Capacity}, +) + return false +end + function MOI.add_constraint( m::Optimizer, f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, @@ -298,6 +335,18 @@ function MOI.supports_constraint( return true end +# These sets constrain node variables that a `Partition` / `PartitionPD` +# already created; they cannot create any. Without this, MOI's default +# infers `supports_add_constrained_variables` from the `VectorOfVariables` +# constraint above and `copy_to` builds the variables from this set instead +# of from the partition, leaving them without a backing Hexaly list. +function MOI.supports_add_constrained_variables( + ::Optimizer, + ::Type{<:MathOptVRP.CapacitatedTimeWindows}, +) + return false +end + function MOI.add_constraint( m::Optimizer, f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, diff --git a/test/jump.jl b/test/jump.jl index a3c05a2..6e2174a 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -1,23 +1,10 @@ -using JuMP using Hexaly using Test -import MathOptInterface as MOI import MathOptVRP -# Hexaly-specific recovery of each truck's variable-length list from the -# `nodes` partition variables. Reaches through `JuMP.unsafe_backend` into -# the Hexaly `Optimizer` to read each list's solved value (its -# `count()` + per-position values), which can't be inferred from the -# JuMP variable values alone since trucks have variable length. -function _hexaly_read_routes(model, nodes) - inner = JuMP.unsafe_backend(model) - routes = Vector{Int}[] - for i = 1:size(nodes, 2) - vi = JuMP.index(nodes[1, i]) - list_expr = inner.variable_info[vi].parent_list::Hexaly.HxExpression - push!(routes, Hexaly.collection_value(list_expr)) - end - return routes -end - -MathOptVRP.Tests.runtests(Hexaly.Optimizer; read_routes = _hexaly_read_routes) +# The routes are read back from the `Partition` variables themselves: a +# truck's column holds the clients it visits followed by `0`s, so +# `MathOptVRP.Tests` needs nothing Hexaly-specific to recover them. See +# `Hexaly._add_list_variables!` for the `-1` sentinel that the +# `ZeroBasedBridge` turns into that `0`. +MathOptVRP.Tests.runtests(Hexaly.Optimizer) From 2553e24d862f24fa0262684d8e676ee58e56c4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 15 Aug 2026 18:15:14 +0200 Subject: [PATCH 3/3] Simplify --- src/Hexaly.jl | 2 - src/MOI/bridges.jl | 120 ----------------------------- src/MOI/list.jl | 46 ++++++----- src/MOI/sets.jl | 46 ----------- src/MOI/sum_distances_objective.jl | 17 ++-- src/MOI/wrapper.jl | 7 -- test/jump.jl | 3 +- 7 files changed, 28 insertions(+), 213 deletions(-) delete mode 100644 src/MOI/bridges.jl delete mode 100644 src/MOI/sets.jl diff --git a/src/Hexaly.jl b/src/Hexaly.jl index b3cb733..609c977 100644 --- a/src/Hexaly.jl +++ b/src/Hexaly.jl @@ -73,8 +73,6 @@ include("MOI/wrapper_constraints.jl") include("MOI/wrapper_constraints_singlevar.jl") include("MOI/wrapper_constraints_linear.jl") include("MOI/wrapper_constraints_cp.jl") -include("MOI/sets.jl") -include("MOI/bridges.jl") include("MOI/list.jl") include("MOI/sum_distances_objective.jl") diff --git a/src/MOI/bridges.jl b/src/MOI/bridges.jl deleted file mode 100644 index 765796e..0000000 --- a/src/MOI/bridges.jl +++ /dev/null @@ -1,120 +0,0 @@ -# `MathOptVRP` sets are 1-based, Hexaly's `list` decision variables are -# 0-based. Rather than special-casing the offset everywhere, we let a -# variable bridge do it once: the model sent to Hexaly is written with the -# 0-based sets of `sets.jl` and every occurrence of a node variable in the -# user's functions is substituted by `y + 1`. -# -# `_shift_to_zero_based` in `sum_distances_objective.jl` takes the `-1` back -# out when lowering `:sum_distances` and the routing constraints, so the two -# offsets cancel and Hexaly indexes its arrays 0-based as it expects. - -""" - ZeroBasedBridge{T,S1,S2} <: MOI.Bridges.Variable.SetMapBridge{T,S1,S2} - -Bridges the 1-based `MathOptVRP` set `S2` into its 0-based Hexaly -counterpart `S1` with the substitution rule `x = y + 1`. - -Because the map is a translation, its linear part is the identity, so both -adjoint maps are the identity as well. -""" -struct ZeroBasedBridge{T,S1,S2} <: - MOI.Bridges.Variable.SetMapBridge{T,S1,S2} - variables::Vector{MOI.VariableIndex} - constraint::MOI.ConstraintIndex{MOI.VectorOfVariables,S1} -end - -""" - ListBridge{T} = ZeroBasedBridge{T,List,MathOptVRP.List} - -Bridges `MathOptVRP.List(n)` into [`List(n)`](@ref). -""" -const ListBridge{T} = ZeroBasedBridge{T,List,MathOptVRP.List} - -""" - PartitionBridge{T} = ZeroBasedBridge{T,Partition,MathOptVRP.Partition} - -Bridges `MathOptVRP.Partition` into [`Partition`](@ref). -""" -const PartitionBridge{T} = ZeroBasedBridge{T,Partition,MathOptVRP.Partition} - -""" - PartitionPDBridge{T} = ZeroBasedBridge{T,PartitionPD,MathOptVRP.PartitionPD} - -Bridges `MathOptVRP.PartitionPD` into [`PartitionPD`](@ref). -""" -const PartitionPDBridge{T} = - ZeroBasedBridge{T,PartitionPD,MathOptVRP.PartitionPD} - -# `map_set` goes Hexaly -> MathOptVRP, `inverse_map_set` the other way; the -# shape is unchanged, only the value convention differs. - -MOI.Bridges.map_set(::Type{<:ListBridge}, s::List) = MathOptVRP.List(s.dimension) - -function MOI.Bridges.inverse_map_set(::Type{<:ListBridge}, s::MathOptVRP.List) - return List(s.dimension) -end - -function MOI.Bridges.map_set(::Type{<:PartitionBridge}, s::Partition) - return MathOptVRP.Partition(s.num_clients, s.num_trucks) -end - -function MOI.Bridges.inverse_map_set( - ::Type{<:PartitionBridge}, - s::MathOptVRP.Partition, -) - return Partition(s.num_clients, s.num_trucks) -end - -function MOI.Bridges.map_set(::Type{<:PartitionPDBridge}, s::PartitionPD) - return MathOptVRP.PartitionPD( - s.num_services, - s.num_pickup_deliveries, - s.num_trucks, - ) -end - -function MOI.Bridges.inverse_map_set( - ::Type{<:PartitionPDBridge}, - s::MathOptVRP.PartitionPD, -) - return PartitionPD( - s.num_services, - s.num_pickup_deliveries, - s.num_trucks, - ) -end - -# `MOI.Utilities.operate` wants the shift to match the shape of `func`: a -# scalar for `unbridged_map`'s `MOI.VariableIndex`, a vector for the -# functions and for the solution vectors of `MOI.VariablePrimal`. -function _translate(::Type{T}, func::Vector, α::T) where {T} - return func .+ α -end - -function _translate(::Type{T}, func::MOI.AbstractVectorFunction, α::T) where {T} - return MOI.Utilities.operate(+, T, func, fill(α, MOI.output_dimension(func))) -end - -function _translate(::Type{T}, func, α::T) where {T} - return MOI.Utilities.operate(+, T, func, α) -end - -function MOI.Bridges.map_function(::Type{<:ZeroBasedBridge{T}}, func) where {T} - return _translate(T, func, one(T)) -end - -function MOI.Bridges.inverse_map_function( - ::Type{<:ZeroBasedBridge{T}}, - func, -) where {T} - return _translate(T, func, -one(T)) -end - -MOI.Bridges.adjoint_map_function(::Type{<:ZeroBasedBridge}, func) = func - -function MOI.Bridges.inverse_adjoint_map_function( - ::Type{<:ZeroBasedBridge}, - func, -) - return func -end diff --git a/src/MOI/list.jl b/src/MOI/list.jl index ef5408d..d3798f5 100644 --- a/src/MOI/list.jl +++ b/src/MOI/list.jl @@ -1,35 +1,30 @@ -# Realisation of the routing sets with Hexaly's C API. The variable sets are -# the 0-based ones of `sets.jl` (the `MathOptVRP` ones reach them through the -# bridges of `bridges.jl`); the constraint sets are `MathOptVRP`'s, and their -# node items are shifted back to 0-based by `_shift_to_zero_based!`. +# Realisation of the 1-based MathOptVRP routing sets with Hexaly's C API. # Create `n` MOI variables backed by `hx_list[0..n-1]`, each tagged with # `parent_list = hx_list` so the objective handler can recover the list. # # A list may be shorter than `n` (the parts of a partition have a variable -# count), but the MOI variables exist for every position, so the ones past -# `count()` have to report something. Hexaly's `at` answers `0` there, which -# is a node like any other, so we substitute `-1`: the `ZeroBasedBridge` -# adds one to it and `MathOptVRP` sees the `0` that marks the end of the -# sequence. +# count), but the MOI variables exist for every position. The trailing zero +# is MathOptVRP's sentinel; real Hexaly list elements are offset by one. function _add_list_variables!(m::Optimizer, hx_list::HxExpression, n::Int) md = m.model indices = MOI.VariableIndex[] for i = 0:(n-1) - elem = iif( + raw_elem = iif( md, lt(md, i, count_(md, hx_list)), at(md, hx_list, i), -1, ) + elem = sum(md, raw_elem, 1) info = VariableInfo( MOI.VariableIndex(0), elem; is_integer = true, parent_list = hx_list, ) - info.lb = -1.0 - info.ub = Float64(n - 1) + info.lb = 0.0 + info.ub = Float64(n) idx = MOI.Utilities.CleverDicts.add_item(m.variable_info, info) _info(m, idx).index = idx push!(indices, idx) @@ -37,22 +32,22 @@ function _add_list_variables!(m::Optimizer, hx_list::HxExpression, n::Int) return indices end -# ── Hexaly.List ──────────────────────────────────────────────────── +# ── MathOptVRP.List ───────────────────────────────────────────────── function MOI.supports_add_constrained_variables( ::Optimizer, - ::Type{List}, + ::Type{MathOptVRP.List}, ) return true end -function MOI.add_constrained_variables(m::Optimizer, set::List) +function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.List) n = set.dimension hx_list = list!(m.model, n) # Pin the list's count so the solver can't pick a shorter list. _add_hexaly_constraint!(m, eq(m.model, count_(m.model, hx_list), n)) indices = _add_list_variables!(m, hx_list, n) - cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,List}( + cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,MathOptVRP.List}( length(m.constraint_info) + 1, ) m.constraint_info[cindex] = @@ -60,16 +55,16 @@ function MOI.add_constrained_variables(m::Optimizer, set::List) return indices, cindex end -# ── Hexaly.Partition ─────────────────────────────────────────────── +# ── MathOptVRP.Partition ──────────────────────────────────────────── function MOI.supports_add_constrained_variables( ::Optimizer, - ::Type{Partition}, + ::Type{MathOptVRP.Partition}, ) return true end -function MOI.add_constrained_variables(m::Optimizer, set::Partition) +function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.Partition) md = m.model lists = HxExpression[list!(md, set.num_clients) for _ = 1:(set.num_trucks)] _add_hexaly_constraint!(m, partition(md, lists)) @@ -78,7 +73,7 @@ function MOI.add_constrained_variables(m::Optimizer, set::Partition) col_indices = _add_list_variables!(m, hx_list, set.num_clients) append!(indices, col_indices) end - cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,Partition}( + cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,MathOptVRP.Partition}( length(m.constraint_info) + 1, ) m.constraint_info[cindex] = @@ -86,16 +81,19 @@ function MOI.add_constrained_variables(m::Optimizer, set::Partition) return indices, cindex end -# ── Hexaly.PartitionPD ───────────────────────────────────────────── +# ── MathOptVRP.PartitionPD ────────────────────────────────────────── + +_pd_n_total(s::MathOptVRP.PartitionPD) = + s.num_services + 2 * s.num_pickup_deliveries function MOI.supports_add_constrained_variables( ::Optimizer, - ::Type{PartitionPD}, + ::Type{MathOptVRP.PartitionPD}, ) return true end -function MOI.add_constrained_variables(m::Optimizer, set::PartitionPD) +function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.PartitionPD) md = m.model n_total = _pd_n_total(set) lists = HxExpression[list!(md, n_total) for _ = 1:(set.num_trucks)] @@ -115,7 +113,7 @@ function MOI.add_constrained_variables(m::Optimizer, set::PartitionPD) col_indices = _add_list_variables!(m, hx_list, n_total) append!(indices, col_indices) end - cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,PartitionPD}( + cindex = MOI.ConstraintIndex{MOI.VectorOfVariables,MathOptVRP.PartitionPD}( length(m.constraint_info) + 1, ) m.constraint_info[cindex] = diff --git a/src/MOI/sets.jl b/src/MOI/sets.jl deleted file mode 100644 index 5a33743..0000000 --- a/src/MOI/sets.jl +++ /dev/null @@ -1,46 +0,0 @@ -# Hexaly's counterparts of the `MathOptVRP` variable sets. They have the -# same shape, the difference is the value convention: a Hexaly `list` -# decision variable takes values in `0:n-1` and Hexaly indexes arrays -# 0-based, while `MathOptVRP` is 1-based like the rest of MOI. The -# `ZeroBasedBridge`s of `bridges.jl` convert between the two, so a model -# written against `MathOptVRP` never sees these sets. - -""" - List(dimension::Int) - -The 0-based counterpart of [`MathOptVRP.List`](@ref): the `dimension` -variables form a permutation of `0:dimension-1`. -""" -struct List <: MOI.AbstractVectorSet - dimension::Int -end - -MOI.dimension(s::List) = s.dimension - -""" - Partition(num_clients::Int, num_trucks::Int) - -The 0-based counterpart of [`MathOptVRP.Partition`](@ref): the columns -together partition `0:num_clients-1`. -""" -struct Partition <: MOI.AbstractVectorSet - num_clients::Int - num_trucks::Int -end - -MOI.dimension(s::Partition) = s.num_clients * s.num_trucks - -""" - PartitionPD(num_services::Int, num_pickup_deliveries::Int, num_trucks::Int) - -The 0-based counterpart of [`MathOptVRP.PartitionPD`](@ref). -""" -struct PartitionPD <: MOI.AbstractVectorSet - num_services::Int - num_pickup_deliveries::Int - num_trucks::Int -end - -_pd_n_total(s::PartitionPD) = s.num_services + 2 * s.num_pickup_deliveries - -MOI.dimension(s::PartitionPD) = _pd_n_total(s) * s.num_trucks diff --git a/src/MOI/sum_distances_objective.jl b/src/MOI/sum_distances_objective.jl index 7324e37..747c98e 100644 --- a/src/MOI/sum_distances_objective.jl +++ b/src/MOI/sum_distances_objective.jl @@ -117,11 +117,10 @@ function _vector_affine_to_items(f::MOI.VectorAffineFunction) return items end -# `MathOptVRP` node values are 1-based, like the rest of MOI, while Hexaly's -# `list` decision variables take values in `0:n-1` and index arrays 0-based. -# The `ZeroBasedBridge` of `bridges.jl` substituted every node variable by -# `y + 1`, so taking that one back out recovers the raw Hexaly variable and, -# for a constant node such as a depot, the 0-based index. +# `MathOptVRP` node values are 1-based, while Hexaly lists and arrays are +# 0-based. List variables retain their identity here because the specialized +# lowering paths below access their raw parent list; constants are shifted for +# Hexaly array indexing. function _shift_to_zero_based!(items, from::Int = 1) for k = from:length(items) items[k] = _zero_based(items[k]) @@ -135,13 +134,7 @@ function _zero_based(f::MOI.ScalarAffineFunction) return _simplify_item(MOI.ScalarAffineFunction(f.terms, f.constant - 1)) end -function _zero_based(vi::MOI.VariableIndex) - return error( - "Hexaly: the node variable `$vi` was not offset by `Hexaly.ZeroBasedBridge`. ", - "Use `MOI.instantiate(Hexaly.Optimizer; with_bridge_type = Float64)` or ", - "`JuMP.Model(Hexaly.Optimizer)` so that the bridge is added.", - ) -end +_zero_based(vi::MOI.VariableIndex) = vi _simplify_item(x) = x function _simplify_item(f::MOI.ScalarAffineFunction) diff --git a/src/MOI/wrapper.jl b/src/MOI/wrapper.jl index 631e679..b0403a0 100644 --- a/src/MOI/wrapper.jl +++ b/src/MOI/wrapper.jl @@ -139,13 +139,6 @@ function MOI.get(::Optimizer, ::MOI.SolverVersion) return string(version()) end -# The optimizer only supports the 0-based sets of `sets.jl`; users write -# their model with the 1-based `MathOptVRP` sets and these bridges do the -# conversion. See `bridges.jl`. -function MOI.get(::Optimizer, ::MOI.Bridges.ListOfNonstandardBridges{T}) where {T} - return Type[ListBridge{T}, PartitionBridge{T}, PartitionPDBridge{T}] -end - # Name function MOI.supports(::Optimizer, ::MOI.Name) diff --git a/test/jump.jl b/test/jump.jl index 6e2174a..7667d34 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -5,6 +5,5 @@ import MathOptVRP # The routes are read back from the `Partition` variables themselves: a # truck's column holds the clients it visits followed by `0`s, so # `MathOptVRP.Tests` needs nothing Hexaly-specific to recover them. See -# `Hexaly._add_list_variables!` for the `-1` sentinel that the -# `ZeroBasedBridge` turns into that `0`. +# `Hexaly._add_list_variables!` for the trailing `0` sentinel. MathOptVRP.Tests.runtests(Hexaly.Optimizer)