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

Sign up to get free protection for your applications and to get access to all the features.
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