Source code for discrete_optimization.tsp.transformations.to_gpdp
# Copyright (c) 2026 AIRBUS and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""Transformation from TSP to GPDP (General Pickup and Delivery Problem).
TSP can be modeled as a GPDP with:
- Single vehicle
- No pickup/delivery pairs
- No time windows
- No capacity constraints
"""
from typing import Optional
from discrete_optimization.generic_tools.transformation.problem_transformation import (
ProblemTransformation,
)
from discrete_optimization.generic_tools.transformation.transformation_metadata import (
TransformationMetadata,
exact_transformation,
)
from discrete_optimization.gpdp.problem import GpdpProblem, GpdpSolution
from discrete_optimization.tsp.problem import TspProblem, TspSolution
[docs]
class TspToGpdpTransformation(
ProblemTransformation[TspProblem, TspSolution, GpdpProblem, GpdpSolution]
):
"""Transform TSP to GPDP.
TSP is a special case of GPDP with:
- Single vehicle
- No pickup/delivery pairs
- No time windows
- No capacity constraints
- All nodes must be visited
This transformation is EXACT:
- All TSP constraints are preserved in GPDP
- Solution quality is preserved in both directions
"""
def __init__(self, compute_graph: bool = False):
"""Initialize transformation.
Args:
compute_graph: Whether to compute the GPDP graph structure (default: False).
Set to True if using graph-based GPDP solvers.
"""
self.compute_graph = compute_graph
[docs]
def get_forward_metadata(self) -> TransformationMetadata:
"""Metadata for forward problem transformation (TSP → GPDP).
This direction is EXACT: TSP is a special case of GPDP.
"""
return exact_transformation(
use_cases=[
"Use GPDP solvers to solve TSP problems",
"TSP is exactly GPDP with 1 vehicle and no constraints",
"All TSP constraints preserved in GPDP formulation",
"No time windows, pickup/delivery, or capacity constraints",
]
)
[docs]
def transform_problem(self, source_problem: TspProblem) -> GpdpProblem:
"""Transform TSP to GPDP using the existing ProxyClass implementation.
Args:
source_problem: TSP problem instance
Returns:
Equivalent GPDP problem with 1 vehicle and no constraints
"""
from discrete_optimization.gpdp.problem import ProxyClass
return ProxyClass.from_tsp_to_gpdp(
source_problem, compute_graph=self.compute_graph
)
[docs]
def back_transform_solution(
self, solution: GpdpSolution, source_problem: TspProblem
) -> TspSolution:
"""Transform GPDP solution back to TSP solution.
Args:
solution: GPDP solution (should have 1 vehicle)
source_problem: Original TSP problem
Returns:
Equivalent TSP solution
"""
# Extract the single vehicle trajectory (vehicle 0)
if 0 not in solution.trajectories:
# Empty solution - use default permutation
permutation = list(source_problem.ind_in_permutation)
else:
trajectory = solution.trajectories[0]
# The trajectory includes: [origin_node, customer1, customer2, ..., target_node]
# We need to extract only the customer nodes (skip first and last)
# In ProxyClass.from_tsp_to_gpdp, customers are mapped to indices 0..nb_customers-1
permutation = trajectory[1:-1] # Skip origin and target nodes
# Convert from GPDP indices to TSP indices
# In the transformation, GPDP indices match TSP indices for customers
permutation = [source_problem.ind_in_permutation[i] for i in permutation]
return TspSolution(
problem=source_problem,
permutation=permutation,
start_index=source_problem.start_index,
end_index=source_problem.end_index,
)
[docs]
def forward_transform_solution(
self, solution: TspSolution, target_problem: GpdpProblem
) -> Optional[GpdpSolution]:
"""Transform TSP solution to GPDP solution (for warmstart).
Args:
solution: TSP solution
target_problem: Target GPDP problem
Returns:
Equivalent GPDP solution with single vehicle trajectory
"""
# Convert TSP permutation to GPDP customer indices
# TSP permutation contains original node indices
# GPDP uses sequential indices 0..nb_customers-1
gpdp_customer_indices = []
for tsp_node in solution.permutation:
# Find index in ind_in_permutation
gpdp_index = solution.problem.ind_in_permutation.index(tsp_node)
gpdp_customer_indices.append(gpdp_index)
# Build complete trajectory: origin -> customers -> target
origin_node = target_problem.origin_vehicle[0]
target_node = target_problem.target_vehicle[0]
trajectory = [origin_node] + gpdp_customer_indices + [target_node]
trajectories = {0: trajectory}
# Compute times along the trajectory
times = {}
current_time = 0.0
for i, node in enumerate(trajectory):
times[node] = current_time
if i < len(trajectory) - 1:
next_node = trajectory[i + 1]
# Add travel time to next node
if node in target_problem.time_delta:
if next_node in target_problem.time_delta[node]:
current_time += target_problem.time_delta[node][next_node]
return GpdpSolution(
problem=target_problem,
trajectories=trajectories,
times=times,
resource_evolution={},
)