Source code for discrete_optimization.binpack.transformations.to_facility

#  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 BinPack to Facility Location."""

from typing import Optional

from discrete_optimization.binpack.problem import BinPackProblem, BinPackSolution
from discrete_optimization.facility.problem import (
    Customer,
    Facility,
    FacilityProblem,
    FacilitySolution,
    Point,
)
from discrete_optimization.generic_tools.transformation.problem_transformation import (
    ProblemTransformation,
)
from discrete_optimization.generic_tools.transformation.transformation_metadata import (
    InformationLoss,
    LossImpact,
    LossType,
    TransformationMetadata,
    lossy_transformation,
)


[docs] class BinpackToFacilityTransformation( ProblemTransformation[ BinPackProblem, BinPackSolution, FacilityProblem, FacilitySolution ] ): """Transform BinPack problem to Facility Location problem. Mapping: - Items (with weights) → Customers (with demands) - Bins (with capacity) → Facilities (with capacity) - Opening a bin → Setup cost for facility (cost = 1) - Minimize bins → Minimize setup costs - No spatial component (all locations at origin) - No assignment costs (distance-based cost = 0) This transformation is ASYMMETRIC: - Forward (problem): LOSSY - incompatibility constraints lost - Backward (solution): EXACT - facility assignments map to bins """ def __init__(self, setup_cost_per_bin: float = 1.0): """Initialize transformation. Args: setup_cost_per_bin: Cost for opening each bin/facility (default: 1.0) """ self.setup_cost_per_bin = setup_cost_per_bin
[docs] def get_forward_metadata(self) -> TransformationMetadata: """Metadata for forward problem transformation (BinPack → Facility). This direction is LOSSY: incompatibility constraints cannot be represented. """ losses = [ InformationLoss( name="incompatibility_constraints", loss_type=LossType.CONSTRAINT, description="Item incompatibility constraints (items that cannot be in same bin)", reason="Facility Location has no concept of customer incompatibility or conflict constraints", impact=LossImpact.MAJOR, workaround="Use BinPack→RCPSP transformation which models incompatibility with virtual resources", ) ] return lossy_transformation( losses=losses, assumptions=[ "No item incompatibility constraints (or safe to ignore)", "Pure capacity allocation problem", ], use_cases=[ "Bin packing without incompatibility constraints", "Benchmarking Facility solvers on packing problems", ], warnings=[ "Solutions may violate incompatibility constraints if present in source problem", "Verify solution feasibility in original BinPack problem after solving", ], )
[docs] def transform_problem(self, source_problem: BinPackProblem) -> FacilityProblem: """Transform BinPack to Facility Location. Args: source_problem: BinPack problem instance Returns: Equivalent Facility Location problem """ # Create customers from items customers = [ Customer( index=item.index, demand=float(item.weight), location=Point(x=0.0, y=0.0), # No spatial component ) for item in source_problem.list_items ] # Create potential facilities (one per potential bin) # Upper bound: one facility per item (worst case) max_facilities = source_problem.nb_items facilities = [ Facility( index=i, setup_cost=self.setup_cost_per_bin, capacity=float(source_problem.capacity_bin), location=Point(x=0.0, y=0.0), # All at origin ) for i in range(max_facilities) ] # Create a concrete subclass since FacilityProblem is abstract class BinPackAsFacilityProblem(FacilityProblem): """Facility problem for bin packing transformation.""" def evaluate_customer_facility( self, facility: Facility, customer: Customer ) -> float: """No assignment cost (distance = 0 for bin packing). Args: facility: Facility customer: Customer Returns: Cost of 0 (no spatial component in bin packing) """ return 0.0 # No distance cost in bin packing return BinPackAsFacilityProblem( facility_count=len(facilities), customer_count=len(customers), facilities=facilities, customers=customers, )
[docs] def back_transform_solution( self, solution: FacilitySolution, source_problem: BinPackProblem ) -> BinPackSolution: """Transform Facility solution back to BinPack solution. Args: solution: Facility Location solution source_problem: Original BinPack problem Returns: Equivalent BinPack solution """ # Direct mapping: facility_for_customers → allocation # solution.facility_for_customers[i] gives facility index for customer i allocation = list(solution.facility_for_customers) return BinPackSolution(problem=source_problem, allocation=allocation)
[docs] def forward_transform_solution( self, solution: BinPackSolution, target_problem: FacilityProblem ) -> Optional[FacilitySolution]: """Transform BinPack solution to Facility solution (for warmstart). Args: solution: BinPack solution target_problem: Target Facility problem Returns: Equivalent Facility solution for warmstart """ # Direct mapping: allocation → facility_for_customers facility_for_customers = list(solution.allocation) return FacilitySolution( problem=target_problem, facility_for_customers=facility_for_customers )