job-shop-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. job_shop_lib/__init__.py +20 -0
  2. job_shop_lib/base_solver.py +37 -0
  3. job_shop_lib/benchmarking/__init__.py +78 -0
  4. job_shop_lib/benchmarking/benchmark_instances.json +1 -0
  5. job_shop_lib/benchmarking/load_benchmark.py +142 -0
  6. job_shop_lib/cp_sat/__init__.py +5 -0
  7. job_shop_lib/cp_sat/ortools_solver.py +201 -0
  8. job_shop_lib/dispatching/__init__.py +49 -0
  9. job_shop_lib/dispatching/dispatcher.py +269 -0
  10. job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
  11. job_shop_lib/dispatching/dispatching_rules.py +160 -0
  12. job_shop_lib/dispatching/factories.py +206 -0
  13. job_shop_lib/dispatching/pruning_functions.py +116 -0
  14. job_shop_lib/exceptions.py +26 -0
  15. job_shop_lib/generators/__init__.py +7 -0
  16. job_shop_lib/generators/basic_generator.py +197 -0
  17. job_shop_lib/graphs/__init__.py +52 -0
  18. job_shop_lib/graphs/build_agent_task_graph.py +209 -0
  19. job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
  20. job_shop_lib/graphs/constants.py +21 -0
  21. job_shop_lib/graphs/job_shop_graph.py +159 -0
  22. job_shop_lib/graphs/node.py +147 -0
  23. job_shop_lib/job_shop_instance.py +355 -0
  24. job_shop_lib/operation.py +120 -0
  25. job_shop_lib/schedule.py +180 -0
  26. job_shop_lib/scheduled_operation.py +97 -0
  27. job_shop_lib/visualization/__init__.py +25 -0
  28. job_shop_lib/visualization/agent_task_graph.py +257 -0
  29. job_shop_lib/visualization/create_gif.py +191 -0
  30. job_shop_lib/visualization/disjunctive_graph.py +206 -0
  31. job_shop_lib/visualization/gantt_chart.py +147 -0
  32. job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
  33. job_shop_lib-0.1.0.dist-info/METADATA +363 -0
  34. job_shop_lib-0.1.0.dist-info/RECORD +35 -0
  35. job_shop_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,142 @@
1
+ """Module for loading benchmark instances.
2
+
3
+ All benchmark instances are stored in a single JSON file. This module provides
4
+ functions to load the instances from the file and return them as
5
+ JobShopInstance objects.
6
+
7
+ The contributions to this benchmark dataset are as follows:
8
+
9
+ abz5-9: This subset, comprising five instances, was introduced by Adams et
10
+ al. (1988).
11
+ ft06, ft10, ft20: These three instances are attributed to the work of
12
+ Fisher and Thompson, as detailed in their 1963 work.
13
+ la01-40: A collection of forty instances, this group was contributed by
14
+ Lawrence, as referenced in his 1984 report.
15
+ orb01-10: Ten instances in this category were provided by Applegate and
16
+ Cook, as seen in their 1991 study.
17
+ swb01-20: This segment, encompassing twenty instances, was contributed by
18
+ Storer et al., as per their 1992 article.
19
+ yn1-4: Yamada and Nakano are credited with the addition of four instances
20
+ in this group, as found in their 1992 paper.
21
+ ta01-80: The largest contribution, consisting of eighty instances, was
22
+ made by Taillard, as documented in his 1993 paper.
23
+
24
+ The metadata from these instances has been updated using data from:
25
+
26
+ Thomas Weise. jsspInstancesAndResults. Accessed in January 2024.
27
+ Available at: https://github.com/thomasWeise/jsspInstancesAndResults
28
+
29
+ It includes the following information:
30
+ - "optimum" (int | None): The optimal makespan for the instance.
31
+ - "lower_bound" (int): The lower bound for the makespan. If
32
+ optimality is known, it is equal to the optimum.
33
+ - "upper_bound" (int): The upper bound for the makespan. If
34
+ optimality is known, it is equal to the optimum.
35
+ - "reference" (str): The paper or source where the instance was first
36
+ introduced.
37
+
38
+ References:
39
+ - J. Adams, E. Balas, and D. Zawack, "The shifting bottleneck procedure
40
+ for job shop scheduling," Management Science, vol. 34, no. 3,
41
+ pp. 391–401, 1988.
42
+
43
+ - J.F. Muth and G.L. Thompson, Industrial scheduling. Englewood Cliffs,
44
+ NJ: Prentice-Hall, 1963.
45
+
46
+ - S. Lawrence, "Resource constrained project scheduling: An experimental
47
+ investigation of heuristic scheduling techniques (Supplement),"
48
+ Carnegie-Mellon University, Graduate School of Industrial
49
+ Administration, Pittsburgh, Pennsylvania, 1984.
50
+
51
+ - D. Applegate and W. Cook, "A computational study of job-shop
52
+ scheduling," ORSA Journal on Computer, vol. 3, no. 2, pp. 149–156,
53
+ 1991.
54
+
55
+ - R.H. Storer, S.D. Wu, and R. Vaccari, "New search spaces for
56
+ sequencing problems with applications to job-shop scheduling,"
57
+ Management Science, vol. 38, no. 10, pp. 1495–1509, 1992.
58
+
59
+ - T. Yamada and R. Nakano, "A genetic algorithm applicable to
60
+ large-scale job-shop problems," in Proceedings of the Second
61
+ International Workshop on Parallel Problem Solving from Nature
62
+ (PPSN'2), Brussels, Belgium, pp. 281–290, 1992.
63
+
64
+ - E. Taillard, "Benchmarks for basic scheduling problems," European
65
+ Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
66
+ """
67
+
68
+ from typing import Any
69
+
70
+ import functools
71
+ import json
72
+ from importlib import resources
73
+
74
+ from job_shop_lib import JobShopInstance
75
+
76
+
77
+ @functools.cache
78
+ def load_all_benchmark_instances() -> dict[str, JobShopInstance]:
79
+ """Loads all benchmark instances available.
80
+
81
+ Returns:
82
+ A dictionary containing the names of the benchmark instances as keys
83
+ and the corresponding JobShopInstance objects as values.
84
+
85
+ """
86
+ benchmark_instances_dict = load_benchmark_json()
87
+ return {
88
+ name: load_benchmark_instance(name)
89
+ for name in benchmark_instances_dict
90
+ }
91
+
92
+
93
+ def load_benchmark_instance(name: str) -> JobShopInstance:
94
+ """Loads a specific benchmark instance.
95
+
96
+ Calls to `load_benchmark_json` to load the benchmark instances from the
97
+ JSON file. The instance is then loaded from the dictionary using the
98
+ provided name. Since `load_benchmark_json` is cached, the file is only
99
+ read once.
100
+
101
+ Args:
102
+ name: The name of the benchmark instance to load. Can be one of the
103
+ following: "abz5-9", "ft06", "ft10", "ft20", "la01-40", "orb01-10",
104
+ "swb01-20", "yn1-4", or "ta01-80".
105
+ """
106
+ benchmark_dict = load_benchmark_json()[name]
107
+ return JobShopInstance.from_matrices(
108
+ duration_matrix=benchmark_dict["duration_matrix"],
109
+ machines_matrix=benchmark_dict["machines_matrix"],
110
+ name=name,
111
+ metadata=benchmark_dict["metadata"],
112
+ )
113
+
114
+
115
+ @functools.cache
116
+ def load_benchmark_json() -> dict[str, dict[str, Any]]:
117
+ """Loads the raw JSON file containing the benchmark instances.
118
+
119
+ Results are cached to avoid reading the file multiple times.
120
+
121
+ Each instance is represented as a dictionary with the following keys
122
+ and values:
123
+ - "name" (str): The name of the instance.
124
+ - "duration_matrix" (list[list[int]]): The matrix containing the
125
+ durations for each operation.
126
+ - "machines_matrix" (list[list[int]]): The matrix containing the
127
+ machines for each operation.
128
+ - "metadata" (dict[str, Any]): A dictionary containing metadata
129
+ about the instance. The keys are "optimum" (int | None),
130
+ "lower_bound" (int), "upper_bound" (int),
131
+ and "reference" (str).
132
+
133
+ Returns:
134
+ The dictionary containing the benchmark instances.
135
+ """
136
+ benchmark_file = (
137
+ resources.files("job_shop_lib.benchmarking")
138
+ / "benchmark_instances.json"
139
+ )
140
+
141
+ with benchmark_file.open("r", encoding="utf-8") as f:
142
+ return json.load(f)
@@ -0,0 +1,5 @@
1
+ from job_shop_lib.cp_sat.ortools_solver import ORToolsSolver
2
+
3
+ __all__ = [
4
+ "ORToolsSolver",
5
+ ]
@@ -0,0 +1,201 @@
1
+ """Home of the ORToolsSolver class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from ortools.sat.python import cp_model
8
+ from ortools.sat.python.cp_model import IntVar
9
+
10
+ from job_shop_lib import (
11
+ JobShopInstance,
12
+ Schedule,
13
+ ScheduledOperation,
14
+ Operation,
15
+ )
16
+ from job_shop_lib import NoSolutionFoundError, BaseSolver
17
+
18
+
19
+ class ORToolsSolver(BaseSolver):
20
+ """A solver for the job shop scheduling problem using constraint
21
+ programming.
22
+
23
+ This solver uses the ortools library to solve the job shop scheduling
24
+ problem using constraint programming.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ max_time_in_seconds: float | None = None,
30
+ log_search_progress: bool = False,
31
+ ):
32
+ self.log_search_progress = log_search_progress
33
+ self.max_time_in_seconds = max_time_in_seconds
34
+
35
+ self.makespan: cp_model.IntVar | None = None
36
+ self.model = cp_model.CpModel()
37
+ self.solver = cp_model.CpSolver()
38
+ self._operations_start: dict[Operation, tuple[IntVar, IntVar]] = {}
39
+
40
+ def __call__(self, instance: JobShopInstance) -> Schedule:
41
+ # Re-defined here since we already add metadata to the schedule in
42
+ # the solve method.
43
+ return self.solve(instance)
44
+
45
+ def solve(self, instance: JobShopInstance) -> Schedule:
46
+ """Creates the variables, constraints and objective, and solves the
47
+ problem.
48
+
49
+ If a solution is found, it extracts and returns the start times of
50
+ each operation and the makespan. If no solution is found, it raises
51
+ a NoSolutionFound exception.
52
+ """
53
+ self._initialize_model(instance)
54
+
55
+ start_time = time.perf_counter()
56
+ status = self.solver.Solve(self.model)
57
+ elapsed_time = time.perf_counter() - start_time
58
+
59
+ if status not in {cp_model.OPTIMAL, cp_model.FEASIBLE}:
60
+ raise NoSolutionFoundError(
61
+ f"No solution could be found for the given problem. "
62
+ f"Elapsed time: {elapsed_time} seconds."
63
+ )
64
+ if self.makespan is None:
65
+ # Check added to satisfy mypy
66
+ raise ValueError("The makespan variable was not set.")
67
+
68
+ metadata = {
69
+ "status": "optimal" if status == cp_model.OPTIMAL else "feasible",
70
+ "elapsed_time": elapsed_time,
71
+ "makespan": self.solver.Value(self.makespan),
72
+ "solved_by": "ORToolsSolver",
73
+ }
74
+ return self._create_schedule(instance, metadata)
75
+
76
+ def _initialize_model(self, instance: JobShopInstance):
77
+ """Initializes the model with variables, constraints and objective.
78
+
79
+ The model is initialized with two variables for each operation: start
80
+ and end time. The constraints ensure that operations within a job are
81
+ performed in sequence and that operations assigned to the same machine
82
+ do not overlap. The objective is to minimize the makespan.
83
+
84
+ Args:
85
+ instance: The job shop instance to be solved.
86
+ """
87
+ self.model = cp_model.CpModel()
88
+ self.solver = cp_model.CpSolver()
89
+ self.solver.parameters.log_search_progress = self.log_search_progress
90
+ self._operations_start = {}
91
+ if self.max_time_in_seconds is not None:
92
+ self.solver.parameters.max_time_in_seconds = (
93
+ self.max_time_in_seconds
94
+ )
95
+ self._create_variables(instance)
96
+ self._add_constraints(instance)
97
+ self._set_objective(instance)
98
+
99
+ def _create_schedule(
100
+ self, instance: JobShopInstance, metadata: dict[str, object]
101
+ ) -> Schedule:
102
+ """Creates a Schedule object from the solution."""
103
+ operations_start: dict[Operation, int] = {
104
+ operation: self.solver.Value(start_var)
105
+ for operation, (start_var, _) in self._operations_start.items()
106
+ }
107
+
108
+ unsorted_schedule: list[list[ScheduledOperation]] = [
109
+ [] for _ in range(instance.num_machines)
110
+ ]
111
+ for operation, start_time in operations_start.items():
112
+ unsorted_schedule[operation.machine_id].append(
113
+ ScheduledOperation(operation, start_time, operation.machine_id)
114
+ )
115
+
116
+ sorted_schedule = [
117
+ sorted(scheduled_operation, key=lambda x: x.start_time)
118
+ for scheduled_operation in unsorted_schedule
119
+ ]
120
+
121
+ return Schedule(
122
+ instance=instance, schedule=sorted_schedule, **metadata
123
+ )
124
+
125
+ def _create_variables(self, instance: JobShopInstance):
126
+ """Creates two variables for each operation: start and end time."""
127
+ for job in instance.jobs:
128
+ for operation in job:
129
+ start_var = self.model.NewIntVar(
130
+ 0, instance.total_duration, f"start_{operation}"
131
+ )
132
+ end_var = self.model.NewIntVar(
133
+ 0, instance.total_duration, f"end_{operation}"
134
+ )
135
+ self._operations_start[operation] = (start_var, end_var)
136
+ self.model.Add(end_var == start_var + operation.duration)
137
+
138
+ def _add_constraints(self, instance: JobShopInstance):
139
+ """Adds job and machine constraints.
140
+
141
+ Job Constraints: Ensure that operations within a job are performed in
142
+ sequence. If operation A must precede operation B in a job, we ensure
143
+ A's end time is less than or equal to B's start time.
144
+
145
+ Machine Constraints: Operations assigned to the same machine cannot
146
+ overlap. This is ensured by creating interval variables (which
147
+ represent the duration an operation occupies a machine)
148
+ and adding a 'no overlap' constraint for these intervals on
149
+ each machine.
150
+ """
151
+ self._add_job_constraints(instance)
152
+ self._add_machine_constraints(instance)
153
+
154
+ def _set_objective(self, instance: JobShopInstance):
155
+ """The objective is to minimize the makespan, which is the total
156
+ duration of the schedule."""
157
+ self.makespan = self.model.NewIntVar(
158
+ 0, instance.total_duration, "makespan"
159
+ )
160
+ end_times = [end for _, end in self._operations_start.values()]
161
+ self.model.AddMaxEquality(self.makespan, end_times)
162
+ self.model.Minimize(self.makespan)
163
+
164
+ def _add_job_constraints(self, instance: JobShopInstance):
165
+ """Adds job constraints to the model. Operations within a job must be
166
+ performed in sequence. If operation A must precede operation B in a
167
+ job, we ensure A's end time is less than or equal to B's start time."""
168
+ for job in instance.jobs:
169
+ for position in range(1, len(job)):
170
+ self.model.Add(
171
+ self._operations_start[job[position - 1]][1]
172
+ <= self._operations_start[job[position]][0]
173
+ )
174
+
175
+ def _add_machine_constraints(self, instance: JobShopInstance):
176
+ """Adds machine constraints to the model. Operations assigned to the
177
+ same machine cannot overlap. This is ensured by creating interval
178
+ variables (which represent the duration an operation occupies a
179
+ machine) and adding a 'no overlap' constraint for these intervals on
180
+ each machine."""
181
+
182
+ # Create interval variables for each operation on each machine
183
+ machines_operations: list[list[tuple[tuple[IntVar, IntVar], int]]] = [
184
+ [] for _ in range(instance.num_machines)
185
+ ]
186
+ for job in instance.jobs:
187
+ for operation in job:
188
+ machines_operations[operation.machine_id].append(
189
+ (
190
+ self._operations_start[operation],
191
+ operation.duration,
192
+ )
193
+ )
194
+ for machine_id, operations in enumerate(machines_operations):
195
+ intervals = []
196
+ for (start_var, end_var), duration in operations:
197
+ interval_var = self.model.NewIntervalVar(
198
+ start_var, duration, end_var, f"interval_{machine_id}"
199
+ )
200
+ intervals.append(interval_var)
201
+ self.model.AddNoOverlap(intervals)
@@ -0,0 +1,49 @@
1
+ """Package containing all the functionality to solve the Job Shop Scheduling
2
+ Problem step-by-step."""
3
+
4
+ from job_shop_lib.dispatching.dispatcher import Dispatcher
5
+ from job_shop_lib.dispatching.dispatching_rules import (
6
+ shortest_processing_time_rule,
7
+ first_come_first_served_rule,
8
+ most_work_remaining_rule,
9
+ most_operations_remaining_rule,
10
+ random_operation_rule,
11
+ )
12
+ from job_shop_lib.dispatching.pruning_functions import (
13
+ prune_dominated_operations,
14
+ prune_non_immediate_machines,
15
+ create_composite_pruning_function,
16
+ )
17
+ from job_shop_lib.dispatching.factories import (
18
+ PruningFunction,
19
+ DispatchingRule,
20
+ MachineChooser,
21
+ dispatching_rule_factory,
22
+ machine_chooser_factory,
23
+ pruning_function_factory,
24
+ composite_pruning_function_factory,
25
+ )
26
+ from job_shop_lib.dispatching.dispatching_rule_solver import (
27
+ DispatchingRuleSolver,
28
+ )
29
+
30
+
31
+ __all__ = [
32
+ "dispatching_rule_factory",
33
+ "machine_chooser_factory",
34
+ "shortest_processing_time_rule",
35
+ "first_come_first_served_rule",
36
+ "most_work_remaining_rule",
37
+ "most_operations_remaining_rule",
38
+ "random_operation_rule",
39
+ "DispatchingRule",
40
+ "MachineChooser",
41
+ "Dispatcher",
42
+ "DispatchingRuleSolver",
43
+ "prune_dominated_operations",
44
+ "prune_non_immediate_machines",
45
+ "create_composite_pruning_function",
46
+ "PruningFunction",
47
+ "pruning_function_factory",
48
+ "composite_pruning_function_factory",
49
+ ]
@@ -0,0 +1,269 @@
1
+ """Home of the `Dispatcher` class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from collections import deque
7
+
8
+ from job_shop_lib import (
9
+ JobShopInstance,
10
+ Schedule,
11
+ ScheduledOperation,
12
+ Operation,
13
+ )
14
+
15
+
16
+ class Dispatcher:
17
+ """Handles the logic of scheduling operations on machines.
18
+
19
+ This class allow us to just define the order in which operations are
20
+ sequenced and the machines in which they are processed. It is then
21
+ responsible for scheduling the operations on the machines and keeping
22
+ track of the next available time for each machine and job.
23
+
24
+ Attributes:
25
+ instance:
26
+ The instance of the job shop problem to be scheduled.
27
+ schedule:
28
+ The schedule of operations on machines.
29
+ pruning_function:
30
+ The pipeline of pruning methods to be used to filter out
31
+ operations from the list of available operations.
32
+ """
33
+
34
+ __slots__ = (
35
+ "instance",
36
+ "schedule",
37
+ "_machine_next_available_time",
38
+ "_job_next_operation_index",
39
+ "_job_next_available_time",
40
+ "pruning_function",
41
+ )
42
+
43
+ def __init__(
44
+ self,
45
+ instance: JobShopInstance,
46
+ pruning_function: (
47
+ Callable[[Dispatcher, list[Operation]], list[Operation]] | None
48
+ ) = None,
49
+ ) -> None:
50
+ """Initializes the object with the given instance.
51
+
52
+ Args:
53
+ instance:
54
+ The instance of the job shop problem to be solved.
55
+ pruning_strategies:
56
+ A list of pruning strategies to be used to filter out
57
+ operations from the list of available operations. Supported
58
+ values are 'dominated_operations' and 'non_immediate_machines'.
59
+ Defaults to [PruningStrategy.DOMINATED_OPERATIONS]. To disable
60
+ pruning, pass an empty list.
61
+ """
62
+
63
+ self.instance = instance
64
+ self.schedule = Schedule(self.instance)
65
+ self._machine_next_available_time = [0] * self.instance.num_machines
66
+ self._job_next_operation_index = [0] * self.instance.num_jobs
67
+ self._job_next_available_time = [0] * self.instance.num_jobs
68
+ self.pruning_function = pruning_function
69
+
70
+ @property
71
+ def machine_next_available_time(self) -> list[int]:
72
+ """Returns the next available time for each machine."""
73
+ return self._machine_next_available_time
74
+
75
+ @property
76
+ def job_next_operation_index(self) -> list[int]:
77
+ """Returns the index of the next operation to be scheduled for each
78
+ job."""
79
+ return self._job_next_operation_index
80
+
81
+ @property
82
+ def job_next_available_time(self) -> list[int]:
83
+ """Returns the next available time for each job."""
84
+ return self._job_next_available_time
85
+
86
+ @classmethod
87
+ def create_schedule_from_raw_solution(
88
+ cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
89
+ ) -> Schedule:
90
+ """Creates a schedule from a raw solution.
91
+
92
+ A raw solution is a list of lists of operations, where each list
93
+ represents the order of operations for a machine.
94
+
95
+ Args:
96
+ instance:
97
+ The instance of the job shop problem to be solved.
98
+ raw_solution:
99
+ A list of lists of operations, where each list represents the
100
+ order of operations for a machine.
101
+
102
+ Returns:
103
+ A Schedule object representing the solution.
104
+ """
105
+ dispatcher = cls(instance)
106
+ dispatcher.reset()
107
+ raw_solution_deques = [
108
+ deque(operations) for operations in raw_solution
109
+ ]
110
+ while not dispatcher.schedule.is_complete():
111
+ for machine_id, operations in enumerate(raw_solution_deques):
112
+ if not operations:
113
+ continue
114
+ operation = operations[0]
115
+ if dispatcher.is_operation_ready(operation):
116
+ dispatcher.dispatch(operation, machine_id)
117
+ operations.popleft()
118
+ return dispatcher.schedule
119
+
120
+ def reset(self) -> None:
121
+ """Resets the dispatcher to its initial state."""
122
+ self.schedule.reset()
123
+ self._machine_next_available_time = [0] * self.instance.num_machines
124
+ self._job_next_operation_index = [0] * self.instance.num_jobs
125
+ self._job_next_available_time = [0] * self.instance.num_jobs
126
+
127
+ def dispatch(self, operation: Operation, machine_id: int) -> None:
128
+ """Schedules the given operation on the given machine.
129
+
130
+ The start time of the operation is computed based on the next
131
+ available time for the machine and the next available time for the
132
+ job to which the operation belongs. The operation is then scheduled
133
+ on the machine and the tracking attributes are updated.
134
+ Args:
135
+ operation:
136
+ The operation to be scheduled.
137
+ machine_id:
138
+ The id of the machine on which the operation is to be
139
+ scheduled.
140
+
141
+ Raises:
142
+ ValueError: If the operation is not ready to be scheduled.
143
+ """
144
+
145
+ if not self.is_operation_ready(operation):
146
+ raise ValueError("Operation is not ready to be scheduled.")
147
+
148
+ start_time = self.start_time(operation, machine_id)
149
+
150
+ scheduled_operation = ScheduledOperation(
151
+ operation, start_time, machine_id
152
+ )
153
+ self.schedule.add(scheduled_operation)
154
+ self._update_tracking_attributes(scheduled_operation)
155
+
156
+ def is_operation_ready(self, operation: Operation) -> bool:
157
+ """Returns True if the given operation is ready to be scheduled.
158
+
159
+ An operation is ready to be scheduled if it is the next operation
160
+ to be scheduled for its job.
161
+
162
+ Args:
163
+ operation:
164
+ The operation to be checked.
165
+ """
166
+ return (
167
+ self.job_next_operation_index[operation.job_id]
168
+ == operation.position_in_job
169
+ )
170
+
171
+ def start_time(self, operation: Operation, machine_id: int) -> int:
172
+ """Computes the start time for the given operation on the given
173
+ machine.
174
+
175
+ The start time is the maximum of the next available time for the
176
+ machine and the next available time for the job to which the
177
+ operation belongs.
178
+
179
+ Args:
180
+ operation:
181
+ The operation to be scheduled.
182
+ machine_id:
183
+ The id of the machine on which the operation is to be
184
+ scheduled.
185
+ """
186
+ return max(
187
+ self.machine_next_available_time[machine_id],
188
+ self.job_next_available_time[operation.job_id],
189
+ )
190
+
191
+ def _update_tracking_attributes(
192
+ self, scheduled_operation: ScheduledOperation
193
+ ) -> None:
194
+ # Variables defined here to make the lines shorter
195
+ job_id = scheduled_operation.job_id
196
+ machine_id = scheduled_operation.machine_id
197
+ end_time = scheduled_operation.end_time
198
+
199
+ self.machine_next_available_time[machine_id] = end_time
200
+ self.job_next_operation_index[job_id] += 1
201
+ self.job_next_available_time[job_id] = end_time
202
+
203
+ def current_time(self) -> int:
204
+ """Returns the current time of the schedule.
205
+
206
+ The current time is the minimum start time of the available
207
+ operations.
208
+ """
209
+ available_operations = self.available_operations()
210
+ return self.min_start_time(available_operations)
211
+
212
+ def min_start_time(self, operations: list[Operation]) -> int:
213
+ """Returns the minimum start time of the available operations."""
214
+ if not operations:
215
+ return self.schedule.makespan()
216
+ min_start_time = float("inf")
217
+ for op in operations:
218
+ for machine_id in op.machines:
219
+ start_time = self.start_time(op, machine_id)
220
+ min_start_time = min(min_start_time, start_time)
221
+ return int(min_start_time)
222
+
223
+ def uncompleted_operations(self) -> list[Operation]:
224
+ """Returns the list of operations that have not been scheduled.
225
+
226
+ An operation is uncompleted if it has not been scheduled yet.
227
+
228
+ It is more efficient than checking all operations in the instance.
229
+ """
230
+ uncompleted_operations = []
231
+ for job_id, next_position in enumerate(self.job_next_operation_index):
232
+ operations = self.instance.jobs[job_id][next_position:]
233
+ uncompleted_operations.extend(operations)
234
+ return uncompleted_operations
235
+
236
+ def available_operations(self) -> list[Operation]:
237
+ """Returns a list of available operations for processing, optionally
238
+ filtering out operations known to be bad choices.
239
+
240
+ This method first gathers all possible next operations from the jobs
241
+ being processed. It then optionally filters these operations to exclude
242
+ ones that are deemed inefficient or suboptimal choices.
243
+
244
+ An operation is sub-optimal if there is another operation that could
245
+ be scheduled in the same machine that would finish before the start
246
+ time of the sub-optimal operation.
247
+
248
+ Returns:
249
+ A list of Operation objects that are available for scheduling.
250
+
251
+ Raises:
252
+ ValueError: If using the filter_bad_choices option and one of the
253
+ available operations can be scheduled in more than one machine.
254
+ """
255
+ available_operations = self._available_operations()
256
+ if self.pruning_function is not None:
257
+ available_operations = self.pruning_function(
258
+ self, available_operations
259
+ )
260
+ return available_operations
261
+
262
+ def _available_operations(self) -> list[Operation]:
263
+ available_operations = []
264
+ for job_id, next_position in enumerate(self.job_next_operation_index):
265
+ if next_position == len(self.instance.jobs[job_id]):
266
+ continue
267
+ operation = self.instance.jobs[job_id][next_position]
268
+ available_operations.append(operation)
269
+ return available_operations