Skip to content
Open
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
128 changes: 128 additions & 0 deletions src/MOF.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Copyright (c) 2025: Benoît Legat and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

# Experimental MathOptFormat serialization. MOF's generic set writer serializes
# set fields after dispatching through head_name. The matching set_to_moi
# methods reconstruct the MathOptVRP sets on read.

const _MOF = MOI.FileFormats.MOF

_MOF.head_name(::Type{Permutation}) = "MathOptVRPPermutation"
_MOF.head_name(::Type{Partition}) = "MathOptVRPPartition"
_MOF.head_name(::Type{PartitionPD}) = "MathOptVRPPartitionPD"
function _MOF.head_name(::Type{<:TimeWindows{WITHOUT_START_TIME}})
return "MathOptVRPTimeWindowsWithoutStartTime"
end
function _MOF.head_name(::Type{<:TimeWindows{WITH_START_TIME}})
return "MathOptVRPTimeWindowsWithStartTime"
end
_MOF.head_name(::Type{<:Capacity}) = "MathOptVRPCapacity"
function _MOF.head_name(::Type{<:CapacitatedTimeWindows})
return "MathOptVRPCapacitatedTimeWindows"
end
function _MOF.head_name(::Type{RouteCompatibility})
return "MathOptVRPRouteCompatibility"
end
_MOF.head_name(::Type{RouteOrder}) = "MathOptVRPRouteOrder"
_MOF.head_name(::Type{RouteExtremities}) = "MathOptVRPRouteExtremities"
_MOF.head_name(::Type{IsEmpty}) = "MathOptVRPIsEmpty"
_MOF.head_name(::Type{<:SumGetIndex}) = "MathOptVRPSumGetIndex"

function _MOF.set_to_moi(::Val{:MathOptVRPPermutation}, ::Type{T}, object::Dict) where {T}
return Permutation(Int(object["dimension"]))
end

function _MOF.set_to_moi(::Val{:MathOptVRPPartition}, ::Type{T}, object::Dict) where {T}
return Partition(Int(object["num_clients"]), Int(object["num_trucks"]))
end

function _MOF.set_to_moi(::Val{:MathOptVRPPartitionPD}, ::Type{T}, object::Dict) where {T}
return PartitionPD(
Int(object["num_services"]),
Int(object["num_pickup_deliveries"]),
Int(object["num_trucks"]),
)
end

function _mof_matrix(::Type{T}, columns::Vector) where {T}
return convert(Matrix{T}, hcat(columns...))
end

function _time_windows_from_mof(::Type{T}, ::Val{W}, object::Dict) where {T,W}
return TimeWindows{W}(
_mof_matrix(T, object["travel"]),
convert(Vector{T}, object["earliest"]),
convert(Vector{T}, object["latest"]),
convert(Vector{T}, object["service"]),
Int(object["num_items"]),
)
end

function _MOF.set_to_moi(
::Val{:MathOptVRPTimeWindowsWithoutStartTime},
::Type{T},
object::Dict,
) where {T}
return _time_windows_from_mof(T, Val(WITHOUT_START_TIME), object)
end

function _MOF.set_to_moi(
::Val{:MathOptVRPTimeWindowsWithStartTime},
::Type{T},
object::Dict,
) where {T}
return _time_windows_from_mof(T, Val(WITH_START_TIME), object)
end

function _MOF.set_to_moi(::Val{:MathOptVRPCapacity}, ::Type{T}, object::Dict) where {T}
return Capacity(convert(Vector{T}, object["delta"]), convert(T, object["capacity"]))
end

function _MOF.set_to_moi(
::Val{:MathOptVRPCapacitatedTimeWindows},
::Type{T},
object::Dict,
) where {T}
return CapacitatedTimeWindows(
_mof_matrix(T, object["travel"]),
convert(Vector{T}, object["earliest"]),
convert(Vector{T}, object["latest"]),
convert(T, object["fixed_time"]),
convert(T, object["slope"]),
convert(Vector{T}, object["delta"]),
convert(T, object["capacity"]),
)
end

function _MOF.set_to_moi(
::Val{:MathOptVRPRouteCompatibility},
::Type{T},
object::Dict,
) where {T}
return RouteCompatibility(convert(Vector{Bool}, object["allowed"]))
end

function _MOF.set_to_moi(::Val{:MathOptVRPRouteOrder}, ::Type{T}, object::Dict) where {T}
return RouteOrder(
convert(Vector{Bool}, object["before"]),
convert(Vector{Bool}, object["after"]),
)
end

function _MOF.set_to_moi(
::Val{:MathOptVRPRouteExtremities},
::Type{T},
object::Dict,
) where {T}
return RouteExtremities(convert(Vector{Bool}, object["members"]))
end

function _MOF.set_to_moi(::Val{:MathOptVRPIsEmpty}, ::Type{T}, object::Dict) where {T}
return IsEmpty(Int(object["num_items"]))
end

function _MOF.set_to_moi(::Val{:MathOptVRPSumGetIndex}, ::Type{T}, object::Dict) where {T}
return SumGetIndex(convert(Vector{T}, object["values"]))
end
1 change: 1 addition & 0 deletions src/MathOptVRP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import MathOptInterface as MOI
import JuMP

include("sets.jl")
include("MOF.jl")
include("Bridges/Bridges.jl")
include("operators.jl")
include("Tests.jl")
Expand Down
77 changes: 77 additions & 0 deletions test/MOF.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Test
import MathOptInterface as MOI

function _test_mof_set_fields(actual, expected)
@test typeof(actual) == typeof(expected)
for field in fieldnames(typeof(expected))
@test getfield(actual, field) == getfield(expected, field)
end
return
end

@testset "MOF round trip" begin
travel = Float64[0 1 2; 3 0 4; 5 6 0]
sets = MOI.AbstractVectorSet[
MathOptVRP.Permutation(3),
MathOptVRP.Partition(3, 2),
MathOptVRP.PartitionPD(1, 1, 2),
MathOptVRP.TimeWindows{MathOptVRP.WITHOUT_START_TIME}(
travel,
[0.0, 1.0, 2.0],
[10.0, 11.0, 12.0],
[0.5, 0.6, 0.7],
2,
),
MathOptVRP.TimeWindows{MathOptVRP.WITH_START_TIME}(
travel,
[0.0, 1.0, 2.0],
[10.0, 11.0, 12.0],
[0.5, 0.6, 0.7],
2,
),
MathOptVRP.Capacity([1.0, -1.0, 2.0], 4.0),
MathOptVRP.CapacitatedTimeWindows(
travel,
[0.0, 1.0, 2.0],
[10.0, 11.0, 12.0],
0.5,
1.5,
[1.0, -1.0, 0.0],
4.0,
),
MathOptVRP.RouteCompatibility(Bool[true, false, true]),
MathOptVRP.RouteOrder(Bool[true, false, false], Bool[false, false, true]),
MathOptVRP.RouteExtremities(Bool[true, false, true]),
MathOptVRP.IsEmpty(3),
MathOptVRP.SumGetIndex([2.0, 3.0, 5.0]),
]

source = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
next_variable = 0
for set in sets
variables = MOI.add_variables(source, MOI.dimension(set))
for variable in variables
next_variable += 1
MOI.set(source, MOI.VariableName(), variable, "x[$next_variable]")
end
MOI.add_constraint(source, MOI.VectorOfVariables(variables), set)
end

writer = MOI.FileFormats.MOF.Model(use_nlp_block = false)
MOI.copy_to(writer, source)
text = sprint(write, writer)
for set in sets
@test occursin(MOI.FileFormats.MOF.head_name(typeof(set)), text)
end

destination = MOI.FileFormats.MOF.Model(use_nlp_block = false)
read!(IOBuffer(text), destination)
for expected in sets
S = typeof(expected)
indices =
MOI.get(destination, MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}())
@test length(indices) == 1
actual = MOI.get(destination, MOI.ConstraintSet(), only(indices))
_test_mof_set_fields(actual, expected)
end
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ import MathOptInterface as MOI
@test copy(sum_set) === sum_set
end

include("MOF.jl")

include("Bridges/PermutationToPartitionBridge.jl")

include("test_ortools.jl")
Expand Down
Loading