From a77f692abfc3502c1b903a2dadb6904e0ed85606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 29 Aug 2026 16:33:11 +0200 Subject: [PATCH] sum_distances takes start and end, not a tour --- ext/MathOptVRPTestExt.jl | 12 +++++++++--- src/operators.jl | 10 ++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ext/MathOptVRPTestExt.jl b/ext/MathOptVRPTestExt.jl index def8fc1..d37b0fa 100644 --- a/ext/MathOptVRPTestExt.jl +++ b/ext/MathOptVRPTestExt.jl @@ -326,11 +326,17 @@ function MathOptVRP.Tests.test_tsp( ) inst = _tsp_instance(; seed, n) model = _model(optimizer_factory; time_limit) - JuMP.@variable(model, nodes[1:n] in MathOptVRP.Permutation(n)) - JuMP.@objective(model, Min, MathOptVRP.op_sum_distances(inst.dist, nodes)) + # Fix node `n` as the start/end of the tour. This breaks rotational + # symmetry and expresses TSP as the one-vehicle VRP special case. + JuMP.@variable(model, nodes[1:(n-1)] in MathOptVRP.Permutation(n - 1)) + JuMP.@objective( + model, + Min, + MathOptVRP.op_sum_distances(inst.dist, [n; nodes; n]), + ) JuMP.optimize!(model) @test JuMP.termination_status(model) in _OPTIMAL_STATUSES - tour = [round(Int, JuMP.value(v)) for v in nodes] + tour = [n; [round(Int, JuMP.value(v)) for v in nodes]] @test sort(tour) == collect(1:n) expected = sum(inst.dist[tour[k], tour[mod1(k + 1, n)]] for k = 1:n) @test round(Int, JuMP.objective_value(model)) == expected diff --git a/src/operators.jl b/src/operators.jl index a1617ce..62178a8 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -1,13 +1,15 @@ -# Solver-agnostic JuMP nonlinear operator for closed-tour distance. +# Solver-agnostic JuMP nonlinear operator for sequence distance. # A solver wrapper recognises `MOI.ScalarNonlinearFunction` with head # `:sum_distances` and lowers it into its own modelling primitives. """ sum_distances(dist_matrix, nodes) -Build a JuMP nonlinear expression representing the closed-tour cost over -`nodes` using `dist_matrix` as the edge weights. Has no scalar fallback -method — solvers that consume `:sum_distances` provide the lowering. +Build a JuMP nonlinear expression equal to +`sum(dist_matrix[nodes[i], nodes[i+1]])` over consecutive entries of `nodes`. +Closure is explicit: pass `[first; route; first]` for a closed tour. Has no +scalar fallback method — solvers that consume `:sum_distances` provide the +lowering. """ function sum_distances end