Source code for discrete_optimization.facility.transformations.to_salbp
# 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 Facility Location to SALBP.
Note: This transformation DISCARDS facility setup costs and distances!
Assumes uniform facility costs and no precedence.
"""
from typing import Optional
from discrete_optimization.alb.base.problem import TaskData
from discrete_optimization.alb.salbp.problem import SalbpProblem, SalbpSolution
from discrete_optimization.facility.problem import FacilityProblem, FacilitySolution
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 FacilityToSalbpTransformation(
ProblemTransformation[
FacilityProblem, FacilitySolution, SalbpProblem, SalbpSolution
]
):
"""Transform Facility Location to SALBP.
Mapping:
- Customers (with demands) → Tasks (with processing times)
- Facility capacity → Cycle time
- Facilities → Stations
- Minimize facilities → Minimize stations
- **Setup costs and distances DISCARDED**
- **No precedence constraints** (Facility has none)
This transformation is ASYMMETRIC:
- Forward (problem): LOSSY - setup costs and assignment costs lost
- Backward (solution): EXACT - station assignments map to facility assignments
Assumptions:
- All facilities have SAME capacity (uses first facility's capacity)
- Setup costs IGNORED
- Distances/allocation costs IGNORED
Use case:
- Facility location viewed as line balancing problem
- Capacitated facility location simplified to SALBP
"""
[docs]
def get_forward_metadata(self) -> TransformationMetadata:
"""Metadata for forward problem transformation (Facility → SALBP).
This direction is LOSSY: setup costs and assignment costs lost.
"""
losses = [
InformationLoss(
name="setup_costs",
loss_type=LossType.OBJECTIVE,
description="Facility setup costs (opening costs)",
reason="SALBP has no concept of setup/opening costs - all stations have implicit uniform cost",
impact=LossImpact.MAJOR,
workaround="Cannot be modeled in SALBP. Use Facility solvers directly if setup costs matter.",
),
InformationLoss(
name="assignment_costs",
loss_type=LossType.OBJECTIVE,
description="Customer-to-facility assignment costs (distances)",
reason="SALBP has no concept of assignment costs - tasks have no location/distance",
impact=LossImpact.MAJOR,
workaround="Cannot be modeled in SALBP. Use Facility solvers directly if distances matter.",
),
InformationLoss(
name="heterogeneous_capacities",
loss_type=LossType.PARAMETER,
description="Different facility capacities (if facilities have different capacities)",
reason="SALBP assumes uniform cycle time. Transformation uses first facility's capacity.",
impact=LossImpact.MODERATE,
workaround="Verify all facilities have same capacity.",
),
]
return lossy_transformation(
losses=losses,
assumptions=[
"Setup costs are uniform or negligible",
"Assignment costs (distances) are zero or negligible",
"All facilities have same capacity",
],
use_cases=[
"Facility location problem without distance costs",
"When only capacity allocation matters",
],
warnings=[
"Solutions may be suboptimal in original Facility problem due to ignored costs",
"Verify that setup costs and assignment costs are truly negligible",
],
)
[docs]
def transform_problem(self, source_problem: FacilityProblem) -> SalbpProblem:
"""Transform Facility Location to SALBP.
Args:
source_problem: Facility Location problem instance
Returns:
Equivalent SALBP problem (without distances/costs)
"""
# Map customers to tasks with TaskData
tasks_data = [
TaskData(task_id=customer.index, processing_time=int(customer.demand))
for customer in source_problem.customers
]
# Use first facility's capacity as cycle time
cycle_time = int(source_problem.facilities[0].capacity)
# No precedence constraints (Facility Location has none)
precedences = []
return SalbpProblem(
tasks_data=tasks_data,
cycle_time=cycle_time,
precedences=precedences,
)
[docs]
def back_transform_solution(
self, solution: SalbpSolution, source_problem: FacilityProblem
) -> FacilitySolution:
"""Transform SALBP solution back to Facility Location solution.
Args:
solution: SALBP solution
source_problem: Original Facility Location problem
Returns:
Equivalent Facility Location solution
"""
# Direct mapping: station allocation → facility allocation
facility_for_customers = list(solution.allocation_to_station)
return FacilitySolution(
problem=source_problem,
facility_for_customers=facility_for_customers,
)
[docs]
def forward_transform_solution(
self, solution: FacilitySolution, target_problem: SalbpProblem
) -> Optional[SalbpSolution]:
"""Transform Facility solution to SALBP solution (for warmstart).
Args:
solution: Facility Location solution
target_problem: Target SALBP problem
Returns:
Equivalent SALBP solution for warmstart
"""
# Direct mapping: facility allocation → station allocation
allocation_to_station = list(solution.facility_for_customers)
return SalbpSolution(
problem=target_problem,
allocation_to_station=allocation_to_station,
)