diff --git a/src/MOF.jl b/src/MOF.jl new file mode 100644 index 0000000..b675198 --- /dev/null +++ b/src/MOF.jl @@ -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 diff --git a/src/MathOptVRP.jl b/src/MathOptVRP.jl index 8c2190d..6c74fba 100644 --- a/src/MathOptVRP.jl +++ b/src/MathOptVRP.jl @@ -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") diff --git a/test/MOF.jl b/test/MOF.jl new file mode 100644 index 0000000..453b933 --- /dev/null +++ b/test/MOF.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 2945090..2cc2c02 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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")