"""Home of the `Schedule` class."""from__future__importannotationsfromtypingimportAnyfromcollectionsimportdequefromjob_shop_libimportScheduledOperation,JobShopInstancefromjob_shop_lib.exceptionsimportValidationError
[docs]classSchedule:"""Data structure to store a complete or partial solution for a particular :class:`JobShopInstance`. A schedule is a list of lists of :class:`ScheduledOperation` objects. Each list represents the order of operations on a machine. The main methods of this class are: .. autosummary:: :nosignatures: makespan is_complete add reset Args: instance: The :class:`JobShopInstance` object that the schedule is for. schedule: A list of lists of :class:`ScheduledOperation` objects. Each list represents the order of operations on a machine. If not provided, the schedule is initialized as an empty schedule. **metadata: Additional information about the schedule. """__slots__={"instance":("The :class:`JobShopInstance` object that the schedule is for."),"_schedule":("A list of lists of :class:`ScheduledOperation` objects. ""Each list represents the order of operations on a machine."),"metadata":("A dictionary with additional information about the ""schedule. It can be used to store information about the ""algorithm that generated the schedule, for example."),}def__init__(self,instance:JobShopInstance,schedule:list[list[ScheduledOperation]]|None=None,**metadata:Any,):ifscheduleisNone:schedule=[[]for_inrange(instance.num_machines)]Schedule.check_schedule(schedule)self.instance:JobShopInstance=instanceself._schedule=scheduleself.metadata:dict[str,Any]=metadatadef__repr__(self)->str:returnstr(self.schedule)@propertydefschedule(self)->list[list[ScheduledOperation]]:"""A list of lists of :class:`ScheduledOperation` objects. Each list represents the order of operations on a machine."""returnself._schedule@schedule.setterdefschedule(self,new_schedule:list[list[ScheduledOperation]]):Schedule.check_schedule(new_schedule)self._schedule=new_schedule@propertydefnum_scheduled_operations(self)->int:"""The number of operations that have been scheduled so far."""returnsum(len(machine_schedule)formachine_scheduleinself.schedule)
[docs]defto_dict(self)->dict:"""Returns a dictionary representation of the schedule. This representation is useful for saving the instance to a JSON file. Returns: A dictionary representation of the schedule with the following keys: - **"instance"**: A dictionary representation of the instance. - **"job_sequences"**: A list of lists of job ids. Each list of job ids represents the order of operations on the machine. The machine that the list corresponds to is determined by the index of the list. - **"metadata"**: A dictionary with additional information about the schedule. """job_sequences:list[list[int]]=[]formachine_scheduleinself.schedule:job_sequences.append([operation.job_idforoperationinmachine_schedule])return{"instance":self.instance.to_dict(),"job_sequences":job_sequences,"metadata":self.metadata,}
[docs]@staticmethoddeffrom_dict(instance:dict[str,Any]|JobShopInstance,job_sequences:list[list[int]],metadata:dict[str,Any]|None=None,)->Schedule:"""Creates a schedule from a dictionary representation. Args: instance: The instance to create the schedule for. Can be a dictionary representation of a :class:`JobShopInstance` or a :class:`JobShopInstance` object. job_sequences: A list of lists of job ids. Each list of job ids represents the order of operations on the machine. The machine that the list corresponds to is determined by the index of the list. metadata: A dictionary with additional information about the schedule. Returns: A :class:`Schedule` object with the given job sequences. """ifisinstance(instance,dict):instance=JobShopInstance.from_matrices(**instance)schedule=Schedule.from_job_sequences(instance,job_sequences)schedule.metadata=metadataifmetadataisnotNoneelse{}returnschedule
[docs]@staticmethoddeffrom_job_sequences(instance:JobShopInstance,job_sequences:list[list[int]],)->Schedule:"""Creates an active schedule from a list of job sequences. An active schedule is the optimal schedule for the given job sequences. In other words, it is not possible to construct another schedule, through changes in the order of processing on the machines, with at least one operation finishing earlier and no operation finishing later. Args: instance: The :class:`JobShopInstance` object that the schedule is for. job_sequences: A list of lists of job ids. Each list of job ids represents the order of operations on the machine. The machine that the list corresponds to is determined by the index of the list. Returns: A :class:`Schedule` object with the given job sequences. """fromjob_shop_lib.dispatchingimportDispatcherdispatcher=Dispatcher(instance)dispatcher.reset()raw_solution_deques=[deque(job_ids)forjob_idsinjob_sequences]whileany(job_seqforjob_seqinraw_solution_deques):at_least_one_operation_scheduled=Falseformachine_id,job_idsinenumerate(raw_solution_deques):ifnotjob_ids:continuejob_id=job_ids[0]operation_index=dispatcher.job_next_operation_index[job_id]operation=instance.jobs[job_id][operation_index]is_ready=dispatcher.is_operation_ready(operation)ifis_readyandmachine_idinoperation.machines:dispatcher.dispatch(operation,machine_id)job_ids.popleft()at_least_one_operation_scheduled=Trueifnotat_least_one_operation_scheduled:raiseValidationError("Invalid job sequences. No valid operation to schedule.")returndispatcher.schedule
[docs]defreset(self):"""Resets the schedule to an empty state."""self.schedule=[[]for_inrange(self.instance.num_machines)]
[docs]defmakespan(self)->int:"""Returns the makespan of the schedule. The makespan is the time at which all operations are completed. """max_end_time=0formachine_scheduleinself.schedule:ifmachine_schedule:max_end_time=max(max_end_time,machine_schedule[-1].end_time)returnmax_end_time
[docs]defis_complete(self)->bool:"""Returns ``True`` if all operations have been scheduled."""returnself.num_scheduled_operations==self.instance.num_operations
[docs]defadd(self,scheduled_operation:ScheduledOperation):"""Adds a new :class:`ScheduledOperation` to the schedule. Args: scheduled_operation: The :class:`ScheduledOperation` to add to the schedule. Raises: ValidationError: If the start time of the new operation is before the end time of the last operation on the same machine. In favor of performance, this method does not checks precedence constraints. """self._check_start_time_of_new_operation(scheduled_operation)self.schedule[scheduled_operation.machine_id].append(scheduled_operation)
def_check_start_time_of_new_operation(self,new_operation:ScheduledOperation,):is_first_operation=notself.schedule[new_operation.machine_id]ifis_first_operation:returnlast_operation=self.schedule[new_operation.machine_id][-1]ifnotself._is_valid_start_time(new_operation,last_operation):raiseValidationError("Operation cannot be scheduled before the last operation on ""the same machine: end time of last operation "f"({last_operation.end_time}) > start time of new operation "f"({new_operation.start_time}).")@staticmethoddef_is_valid_start_time(scheduled_operation:ScheduledOperation,previous_operation:ScheduledOperation,):returnprevious_operation.end_time<=scheduled_operation.start_time
[docs]@staticmethoddefcheck_schedule(schedule:list[list[ScheduledOperation]]):"""Checks if a schedule is valid and raises a :class:`~exceptions.ValidationError` if it is not. A schedule is considered invalid if: - A :class:`ScheduledOperation` has a machine id that does not match the machine id of the machine schedule (the list of :class:`ScheduledOperation` objects) that it belongs to. - The start time of a :class:`ScheduledOperation` is before the end time of the last operation on the same machine. Args: schedule: The schedule (a list of lists of :class:`ScheduledOperation` objects) to check. Raises: ValidationError: If the schedule is invalid. """formachine_id,scheduled_operationsinenumerate(schedule):fori,scheduled_operationinenumerate(scheduled_operations):ifscheduled_operation.machine_id!=machine_id:raiseValidationError("The machine id of the scheduled operation "f"({ScheduledOperation.machine_id}) does not match "f"the machine id of the machine schedule ({machine_id}"f"). Index of the operation: [{machine_id}][{i}].")ifi==0:continueifnotSchedule._is_valid_start_time(scheduled_operation,scheduled_operations[i-1]):raiseValidationError("Invalid schedule. The start time of the new ""operation is before the end time of the last ""operation on the same machine.""End time of last operation: "f"{scheduled_operations[i-1].end_time}. "f"Start time of new operation: "f"{scheduled_operation.start_time}. At index "f"[{machine_id}][{i}].")