Source code for discrete_optimization.tsptw.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 TSPTW to GPDP (General Pickup and Delivery Problem).

TSPTW can be modeled as a GPDP with:
- Single vehicle
- No pickup/delivery pairs
- Time windows for all nodes
- 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.tsptw.problem import TSPTWProblem, TSPTWSolution


[docs] class TsptwToGpdpTransformation( ProblemTransformation[TSPTWProblem, TSPTWSolution, GpdpProblem, GpdpSolution] ): """Transform TSPTW to GPDP. TSPTW is a special case of GPDP with: - Single vehicle - Time windows for each node - No pickup/delivery pairs - No capacity constraints - All nodes must be visited This transformation is EXACT: - All TSPTW constraints are preserved in GPDP - Time windows are directly mapped - 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 (TSPTW → GPDP). This direction is EXACT: TSPTW is a special case of GPDP. """ return exact_transformation( use_cases=[ "Use GPDP solvers to solve TSPTW problems", "TSPTW is exactly GPDP with 1 vehicle and time windows", "All TSPTW constraints preserved in GPDP formulation", "Time windows constrain node visit times", ] )
[docs] def transform_problem(self, source_problem: TSPTWProblem) -> GpdpProblem: """Transform TSPTW to GPDP using the existing ProxyClass implementation. Args: source_problem: TSPTW problem instance Returns: Equivalent GPDP problem with 1 vehicle and time windows """ from discrete_optimization.gpdp.problem import ProxyClass return ProxyClass.from_tsptw_to_gpdp( source_problem, compute_graph=self.compute_graph )
[docs] def back_transform_solution( self, solution: GpdpSolution, source_problem: TSPTWProblem ) -> TSPTWSolution: """Transform GPDP solution back to TSPTW solution. Args: solution: GPDP solution (should have 1 vehicle) source_problem: Original TSPTW problem Returns: Equivalent TSPTW solution """ # Extract the single vehicle trajectory (vehicle 0) if 0 not in solution.trajectories: # Empty solution - use customer order permutation = list(source_problem.customers) else: trajectory = solution.trajectories[0] # The trajectory includes: [origin_node, customer1, customer2, ..., target_node] # We need to extract only the customer nodes and map them back # In the ProxyClass.from_tsptw_to_gpdp transformation: # - GPDP customers are indexed 0..nb_customers-1 # - These correspond to TSPTW customers (which are source_problem.customers) # - Origin node = nb_customers # - Target node = nb_customers + 1 # # Simply skip first and last nodes (origin/target) and map the rest permutation = [] for node in trajectory[1:-1]: # Skip origin and target # Map GPDP customer index to TSPTW customer index permutation.append(source_problem.customers[node]) return TSPTWSolution( problem=source_problem, permutation=permutation, )
[docs] def forward_transform_solution( self, solution: TSPTWSolution, target_problem: GpdpProblem ) -> Optional[GpdpSolution]: """Transform TSPTW solution to GPDP solution (for warmstart). Args: solution: TSPTW solution target_problem: Target GPDP problem Returns: Equivalent GPDP solution with single vehicle trajectory """ # Build trajectory for vehicle 0 # GPDP uses virtual nodes: customers are 0..nb_customers-1, # origin is nb_customers, target is nb_customers+1 nb_customers = len(solution.problem.customers) # Map TSPTW customer indices to GPDP customer indices # TSPTW permutation contains original customer indices # GPDP uses sequential indices 0..nb_customers-1 gpdp_customer_indices = [] for tsptw_customer in solution.permutation: # Find the position of this customer in the customers list gpdp_index = solution.problem.customers.index(tsptw_customer) 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] else: # No direct edge - this shouldn't happen in valid solutions current_time += 0.0 # Add service time at current node if node in target_problem.time_delta_node: current_time += target_problem.time_delta_node[node] return GpdpSolution( problem=target_problem, trajectories=trajectories, times=times, resource_evolution={}, )