Source code for discrete_optimization.alb.salbp.transformations.to_rcalbp_l

#  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 SALBP to RCALBP_L (Resource-Constrained Assembly Line Balancing with Learning)."""

from typing import Optional

from discrete_optimization.alb.rcalbp_l.problem import (
    RCALBPLProblem,
    RCALBPLSolution,
)
from discrete_optimization.alb.salbp.problem import SalbpProblem, SalbpSolution
from discrete_optimization.generic_tools.transformation.problem_transformation import (
    ProblemTransformation,
)
from discrete_optimization.generic_tools.transformation.transformation_metadata import (
    TransformationMetadata,
    subset_transformation,
)


[docs] class SalbpToRcalbpLTransformation( ProblemTransformation[SalbpProblem, SalbpSolution, RCALBPLProblem, RCALBPLSolution] ): """Transform SALBP to RCALBP_L. Mapping: - SALBP tasks → RCALBP_L tasks (single period, no learning) - SALBP stations → RCALBP_L workstations - SALBP cycle time → RCALBP_L target cycle time - SALBP precedence → RCALBP_L precedence - No resources, no zones (empty sets) - Single period (no learning effect) - Task durations remain constant across workstations This is a SUBSET transformation: SALBP is a special case of RCALBP_L where: - nb_resources = 0 - nb_zones = 0 - nb_periods = 1 - No learning effect (durations constant) """ def __init__(self, nb_stations_upper_bound: Optional[int] = None): """Initialize transformation. Args: nb_stations_upper_bound: Upper bound on number of stations (default: number of tasks) """ self.nb_stations_upper_bound = nb_stations_upper_bound
[docs] def get_forward_metadata(self) -> TransformationMetadata: """Metadata for forward transformation (SALBP → RCALBP_L).""" return subset_transformation( use_cases=[ "Use RCALBP_L solvers for SALBP problems", "SALBP is a special case of RCALBP_L (no resources, single period)", "Benchmark RCALBP_L algorithms on simpler SALBP instances", ], assumptions=[ "No resource constraints", "No zone constraints", "Single period (no learning effect)", "Durations constant across workstations", ], )
[docs] def transform_problem(self, source_problem: SalbpProblem) -> RCALBPLProblem: """Transform SALBP to RCALBP_L. Args: source_problem: SALBP problem instance Returns: Equivalent RCALBP_L problem (no resources, single period) """ # Parameters nb_tasks = source_problem.nb_tasks nb_stations = ( self.nb_stations_upper_bound if self.nb_stations_upper_bound is not None else nb_tasks ) nb_periods = 1 # Single period (no learning) nb_resources = 1 # No resources nb_zones = 0 # No zones # Cycle time constraints c_target = source_problem.cycle_time c_max = source_problem.cycle_time # Same as target for SALBP # Precedence constraints # SALBP precedence: list of (pred, succ) pairs # RCALBP_L precedence: list of ((pred, period), (succ, period)) pairs # Since we have single period, all tasks are in period 0 precedences = [ (source_problem.tasks_to_index[pred], source_problem.tasks_to_index[succ]) for pred, succ in source_problem.precedence ] # Durations: RCALBP_L uses durations[task][experience_level] # For SALBP, no learning effect, so duration is constant # durations[t][0] = task time for task t (no experience) durations = [ [source_problem.task_times[task]] * nb_stations for task in source_problem.tasks ] # Empty resource/zone arrays capa_resources = [1] cons_resources = [[1 for i in range(nb_tasks)]] capa_zones = [] cons_zones = [] neutr_zones = [[] for i in range(nb_tasks)] return RCALBPLProblem( c_target=c_target, c_max=c_max, nb_stations=nb_stations, nb_periods=nb_periods, nb_tasks=nb_tasks, precedences=precedences, durations=durations, nb_resources=nb_resources, capa_resources=capa_resources, cons_resources=cons_resources, nb_zones=nb_zones, capa_zones=capa_zones, cons_zones=cons_zones, neutr_zones=neutr_zones, p_start=nb_stations - 1, p_end=nb_stations, # Single period [0, 1) )
[docs] def back_transform_solution( self, solution: RCALBPLSolution, source_problem: SalbpProblem ) -> SalbpSolution: """Transform RCALBP_L solution back to SALBP solution. Args: solution: RCALBP_L solution source_problem: Original SALBP problem Returns: Equivalent SALBP solution """ # Extract workstation assignments from RCALBP_L solution # RCALBP_L: wks[task_id] = workstation # SALBP: allocation_to_station[task_index] = station # Map task IDs to indices allocation_to_station = [ solution.wks[source_problem.tasks_to_index[task]] for task in source_problem.tasks ] return SalbpSolution( problem=source_problem, allocation_to_station=allocation_to_station, )
[docs] def forward_transform_solution( self, solution: SalbpSolution, target_problem: RCALBPLProblem ) -> Optional[RCALBPLSolution]: """Transform SALBP solution to RCALBP_L solution (for warmstart). Args: solution: SALBP solution target_problem: Target RCALBP_L problem Returns: Equivalent RCALBP_L solution """ # Build workstation assignments # SALBP: allocation_to_station[task_index] = station # RCALBP_L: wks[task_id] = workstation wks = { target_problem.tasks[i]: solution.allocation_to_station[i] for i in range(len(solution.allocation_to_station)) } # Empty resource allocation (no resources) raw = {} # Build schedule using RCALBP_L's scheduling algorithm # We use a simple greedy schedule for period 0 # Target starts: use station assignment as priority target_starts = {task: wks[task] for task in wks} # Build full solution using RCALBP_L's built-in scheduler return target_problem.build_full_solution( wks=wks, raw=raw, target_starts=target_starts, )