Source code for labscheduler.solvers.cp_solver_structures

"""
Collection of utility structures used by the CP solver
"""

from dataclasses import dataclass, field
from datetime import datetime

try:
    from ortools.sat.cp_model_pb2 import CpSolverStatus
    from ortools.sat.python.cp_model import (
        FEASIBLE,
        OPTIMAL,
        Constraint,
        IntervalVar,
        IntVar,
        LinearExpr,
    )
except ModuleNotFoundError as e:
    msg = (
        "The required optional dependency 'ortools' is not installed. Please install it using "
        "'pip install .[cpsolver]'."
    )
    raise ModuleNotFoundError(msg) from e

from labscheduler.logging_manager import scheduler_logger as logger
from labscheduler.structures import JSSP, Operation, SolutionQuality


[docs] @dataclass class IntervalBundle: """ Contains all information about the interval variables corresponding to an operation. """ op_idx: str # name of the operation # list of interval vars together with a dictionary {tag: machine_name} to specify the executors and a is_present # variable(which might be a fix integer or an actual variable) interval_vars: list[tuple[IntervalVar, dict[str, str], bool | IntVar]] = field(default_factory=list) def __str__(self): return f"{self.op_idx}:\n" + "\n".join(str(tupl[1]) for tupl in self.interval_vars)
[docs] class CPVariables: operation_by_machine: dict[str, list[IntervalVar]] reference_time: datetime offset: int # some generous upper bound for the makespan horizon: int intervals: dict[str, IntervalBundle] # provides the is_present variable to interval variable_name presence = dict[str, int | IntVar] objective: LinearExpr | int hard_time_cons: list[Constraint] | None = None def __init__(self, inst: JSSP, offset: int): self.operation_by_machine = {name: [] for name in inst.machine_collection.machine_by_id} self.reference_time = datetime.now() self.horizon = 2**31 - 1 self.offset = offset self.intervals = {} self.presence = {} self.inst = inst self.objective = 0 self.aux_vars = [] if self.hard_time_cons is None: self.hard_time_cons = []
[docs] def intervals_by_id(self, idx: str) -> list[IntervalVar]: intervals = [] for interval, _roles, _is_present in self.intervals[idx].interval_vars: intervals.append(interval) return intervals
@property def all_intervals(self) -> list[IntervalVar]: intervals = [] for bundle in self.intervals.values(): for interval, _roles, _is_present in bundle.interval_vars: intervals.append(interval) return intervals
[docs] def add_interval(self, o: Operation, interval: IntervalVar, is_present: int | IntVar = 1, **kwargs): """ Adds the interval var into the correct bundle. In the kwargs must be specified which machine shall execute which required role if that is not already specified in the operation (i.e. for pooling). """ # create an entry if necessary if o.name not in self.intervals: self.intervals[o.name] = IntervalBundle(o.name) roles = {} for required in o.required_machines: # the executing machine is either fixed by the operation.... if required.preferred: roles[required.tag] = required.preferred # ... or specified in the kwargs elif required.tag not in kwargs: logger.error(f"The executor for {required} in operation {o.name} is unclear") else: roles[required.tag] = kwargs[required.tag] # add the tuple of interval-var and role assignments self.intervals[o.name].interval_vars.append((interval, roles, is_present)) self.operation_by_machine[roles["main"]].append(interval) # links variable name and is_present variable self.presence[interval.name] = is_present
[docs] def solver_status_to_solution_quality(status: CpSolverStatus) -> SolutionQuality: if status == OPTIMAL: return SolutionQuality.OPTIMAL if status == FEASIBLE: return SolutionQuality.FEASIBLE return SolutionQuality.INFEASIBLE