Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions src/MOI/list.jl
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
# 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 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. 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 = at(m.model, hx_list, i)
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 = 0.0
info.ub = Float64(n - 1)
info.ub = Float64(n)
idx = MOI.Utilities.CleverDicts.add_item(m.variable_info, info)
_info(m, idx).index = idx
push!(indices, idx)
end
return indices
end

# ── MathOptVRP.List ────────────────────────────────────────────────────
# ── MathOptVRP.List ─────────────────────────────────────────────────

function MOI.supports_add_constrained_variables(
::Optimizer,
Expand All @@ -47,7 +55,7 @@ function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.List)
return indices, cindex
end

# ── MathOptVRP.Partition ───────────────────────────────────────────────
# ── MathOptVRP.Partition ────────────────────────────────────────────

function MOI.supports_add_constrained_variables(
::Optimizer,
Expand All @@ -73,7 +81,10 @@ function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.Partition)
return indices, cindex
end

# ── MathOptVRP.PartitionPD ─────────────────────────────────────────────
# ── MathOptVRP.PartitionPD ──────────────────────────────────────────

_pd_n_total(s::MathOptVRP.PartitionPD) =
s.num_services + 2 * s.num_pickup_deliveries

function MOI.supports_add_constrained_variables(
::Optimizer,
Expand All @@ -84,7 +95,7 @@ end

function MOI.add_constrained_variables(m::Optimizer, set::MathOptVRP.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)
Expand Down Expand Up @@ -122,6 +133,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},
Expand All @@ -132,6 +155,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.",
)
Expand Down Expand Up @@ -229,6 +254,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},
Expand All @@ -239,6 +276,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.",
Expand Down Expand Up @@ -295,6 +333,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},
Expand All @@ -306,6 +356,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.",
Expand Down
21 changes: 20 additions & 1 deletion src/MOI/sum_distances_objective.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -117,6 +117,25 @@ function _vector_affine_to_items(f::MOI.VectorAffineFunction)
return items
end

# `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])
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

_zero_based(vi::MOI.VariableIndex) = vi

_simplify_item(x) = x
function _simplify_item(f::MOI.ScalarAffineFunction)
if isempty(f.terms)
Expand Down
24 changes: 5 additions & 19 deletions test/jump.jl
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
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 trailing `0` sentinel.
MathOptVRP.Tests.runtests(Hexaly.Optimizer)