job-shop-lib 0.5.1__py3-none-any.whl → 1.0.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 (95) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +10 -2
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generation/general_instance_generator.py +0 -169
  84. job_shop_lib/generation/transformations.py +0 -164
  85. job_shop_lib/generators/__init__.py +0 -8
  86. job_shop_lib/generators/basic_generator.py +0 -200
  87. job_shop_lib/graphs/constants.py +0 -21
  88. job_shop_lib/graphs/job_shop_graph.py +0 -202
  89. job_shop_lib/graphs/node.py +0 -166
  90. job_shop_lib/operation.py +0 -122
  91. job_shop_lib/visualization/agent_task_graph.py +0 -257
  92. job_shop_lib/visualization/create_gif.py +0 -209
  93. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  94. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  95. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -3,11 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
- from typing import Any
6
+ from typing import Any, TypeVar, List, Optional, Type, Set
7
7
  from collections.abc import Callable
8
- from collections import deque
9
8
  from functools import wraps
10
- from warnings import warn
11
9
 
12
10
  from job_shop_lib import (
13
11
  JobShopInstance,
@@ -15,40 +13,71 @@ from job_shop_lib import (
15
13
  ScheduledOperation,
16
14
  Operation,
17
15
  )
16
+ from job_shop_lib.exceptions import ValidationError
18
17
 
19
18
 
20
19
  # Added here to avoid circular imports
21
20
  class DispatcherObserver(abc.ABC):
22
- """Interface for classes that observe th"""
21
+ """Abstract class that allows objects to observe and respond to changes
22
+ within the :class:`Dispatcher`.
23
+
24
+ It follows the Observer design pattern, where observers subscribe to the
25
+ dispatcher and receive updates when certain events occur, such as when
26
+ an operation is scheduled or when the dispatcher is reset.
27
+
28
+ Attributes:
29
+ dispatcher:
30
+ The :class:`Dispatcher` instance to observe.
31
+
32
+ Args:
33
+ dispatcher:
34
+ The :class:`Dispatcher` instance to observe.
35
+ subscribe:
36
+ If ``True``, automatically subscribes the observer to the
37
+ dispatcher when it is initialized. Defaults to ``True``.
38
+
39
+ Raises:
40
+ ValidationError: If ``is_singleton`` is ``True`` and an observer of the
41
+ same type already exists in the dispatcher's list of
42
+ subscribers.
43
+
44
+ Example:
45
+
46
+ .. code-block:: python
47
+
48
+ from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
49
+ from job_shop_lib import ScheduledOperation
50
+
51
+
52
+ class HistoryObserver(DispatcherObserver):
53
+ def __init__(self, dispatcher: Dispatcher):
54
+ super().__init__(dispatcher)
55
+ self.history: List[ScheduledOperation] = []
56
+
57
+ def update(self, scheduled_operation: ScheduledOperation):
58
+ self.history.append(scheduled_operation)
59
+
60
+ def reset(self):
61
+ self.history = []
62
+
63
+ """
64
+
65
+ # Made read-only following Google Style Guide recommendation
66
+ _is_singleton = True
67
+ """If True, ensures only one instance of this observer type is subscribed
68
+ to the dispatcher."""
23
69
 
24
70
  def __init__(
25
71
  self,
26
72
  dispatcher: Dispatcher,
27
- is_singleton: bool = True,
73
+ *,
28
74
  subscribe: bool = True,
29
75
  ):
30
- """Initializes the observer with the `Dispatcher` and subscribes to
31
- it.
32
-
33
- Args:
34
- subject:
35
- The subject to observe.
36
- is_singleton:
37
- Whether the observer should be a singleton. If True, the
38
- observer will be the only instance of its class in the
39
- subject's list of subscribers. If False, the observer will
40
- be added to the subject's list of subscribers every time
41
- it is initialized.
42
- subscribe:
43
- Whether to subscribe the observer to the subject. If False,
44
- the observer will not be subscribed to the subject and will
45
- not receive automatic updates.
46
- """
47
- if is_singleton and any(
76
+ if self._is_singleton and any(
48
77
  isinstance(observer, self.__class__)
49
78
  for observer in dispatcher.subscribers
50
79
  ):
51
- raise ValueError(
80
+ raise ValidationError(
52
81
  f"An observer of type {self.__class__.__name__} already "
53
82
  "exists in the dispatcher's list of subscribers. If you want "
54
83
  "to create multiple instances of this observer, set "
@@ -59,6 +88,15 @@ class DispatcherObserver(abc.ABC):
59
88
  if subscribe:
60
89
  self.dispatcher.subscribe(self)
61
90
 
91
+ @property
92
+ def is_singleton(self) -> bool:
93
+ """Returns whether this observer is a singleton.
94
+
95
+ This is a class attribute that determines whether only one
96
+ instance of this observer type can be subscribed to the dispatcher.
97
+ """
98
+ return self._is_singleton
99
+
62
100
  @abc.abstractmethod
63
101
  def update(self, scheduled_operation: ScheduledOperation):
64
102
  """Called when an operation is scheduled on a machine."""
@@ -74,6 +112,11 @@ class DispatcherObserver(abc.ABC):
74
112
  return self.__class__.__name__
75
113
 
76
114
 
115
+ # Disable pylint's false positive
116
+ # pylint: disable=invalid-name
117
+ ObserverType = TypeVar("ObserverType", bound=DispatcherObserver)
118
+
119
+
77
120
  def _dispatcher_cache(method):
78
121
  """Decorator to cache results of a method based on its name.
79
122
 
@@ -110,54 +153,77 @@ class Dispatcher:
110
153
  responsible for scheduling the operations on the machines and keeping
111
154
  track of the next available time for each machine and job.
112
155
 
113
- Attributes:
156
+ The core method of the class are:
157
+
158
+ .. autosummary::
159
+
160
+ dispatch
161
+ reset
162
+
163
+ It also provides methods to query the state of the schedule and the
164
+ operations:
165
+
166
+ .. autosummary::
167
+
168
+ current_time
169
+ available_operations
170
+ available_machines
171
+ available_jobs
172
+ unscheduled_operations
173
+ scheduled_operations
174
+ ongoing_operations
175
+ completed_operations
176
+ uncompleted_operations
177
+ is_scheduled
178
+ is_ongoing
179
+ next_operation
180
+ earliest_start_time
181
+ remaining_duration
182
+
183
+ The above methods which do not take any arguments are cached to improve
184
+ performance. After each scheduling operation, the cache is cleared.
185
+
186
+ Args:
114
187
  instance:
115
- The instance of the job shop problem to be scheduled.
116
- schedule:
117
- The schedule of operations on machines.
118
- pruning_function:
119
- A function that filters out operations that are not ready to be
120
- scheduled.
188
+ The instance of the job shop problem to be solved.
189
+ ready_operations_filter:
190
+ A function that filters out operations that are not ready to
191
+ be scheduled. The function should take the dispatcher and a
192
+ list of operations as input and return a list of operations
193
+ that are ready to be scheduled. If ``None``, no filtering is
194
+ done.
121
195
  """
122
196
 
123
- __slots__ = (
124
- "instance",
125
- "schedule",
126
- "_machine_next_available_time",
127
- "_job_next_operation_index",
128
- "_job_next_available_time",
129
- "pruning_function",
130
- "subscribers",
131
- "_cache",
132
- )
197
+ __slots__ = {
198
+ "instance": "The instance of the job shop problem to be scheduled.",
199
+ "schedule": "The schedule of operations on machines.",
200
+ "_machine_next_available_time": "",
201
+ "_job_next_operation_index": "",
202
+ "_job_next_available_time": "",
203
+ "ready_operations_filter": (
204
+ "A function that filters out operations that are not ready to be "
205
+ "scheduled."
206
+ ),
207
+ "subscribers": "A list of observers subscribed to the dispatcher.",
208
+ "_cache": "A dictionary to cache the results of the cached methods.",
209
+ }
133
210
 
134
211
  def __init__(
135
212
  self,
136
213
  instance: JobShopInstance,
137
- pruning_function: (
138
- Callable[[Dispatcher, list[Operation]], list[Operation]] | None
214
+ ready_operations_filter: (
215
+ Optional[Callable[[Dispatcher, List[Operation]], List[Operation]]]
139
216
  ) = None,
140
217
  ) -> None:
141
- """Initializes the object with the given instance.
142
-
143
- Args:
144
- instance:
145
- The instance of the job shop problem to be solved.
146
- pruning_function:
147
- A function that filters out operations that are not ready to
148
- be scheduled. The function should take the dispatcher and a
149
- list of operations as input and return a list of operations
150
- that are ready to be scheduled. If `None`, no pruning is done.
151
- """
152
218
 
153
219
  self.instance = instance
154
220
  self.schedule = Schedule(self.instance)
155
- self.pruning_function = pruning_function
221
+ self.ready_operations_filter = ready_operations_filter
222
+ self.subscribers: List[DispatcherObserver] = []
156
223
 
157
224
  self._machine_next_available_time = [0] * self.instance.num_machines
158
225
  self._job_next_operation_index = [0] * self.instance.num_jobs
159
226
  self._job_next_available_time = [0] * self.instance.num_jobs
160
- self.subscribers: list[DispatcherObserver] = []
161
227
  self._cache: dict[str, Any] = {}
162
228
 
163
229
  def __str__(self) -> str:
@@ -167,18 +233,18 @@ class Dispatcher:
167
233
  return str(self)
168
234
 
169
235
  @property
170
- def machine_next_available_time(self) -> list[int]:
236
+ def machine_next_available_time(self) -> List[int]:
171
237
  """Returns the next available time for each machine."""
172
238
  return self._machine_next_available_time
173
239
 
174
240
  @property
175
- def job_next_operation_index(self) -> list[int]:
241
+ def job_next_operation_index(self) -> List[int]:
176
242
  """Returns the index of the next operation to be scheduled for each
177
243
  job."""
178
244
  return self._job_next_operation_index
179
245
 
180
246
  @property
181
- def job_next_available_time(self) -> list[int]:
247
+ def job_next_available_time(self) -> List[int]:
182
248
  """Returns the next available time for each job."""
183
249
  return self._job_next_available_time
184
250
 
@@ -200,26 +266,35 @@ class Dispatcher:
200
266
  for subscriber in self.subscribers:
201
267
  subscriber.reset()
202
268
 
203
- def dispatch(self, operation: Operation, machine_id: int) -> None:
269
+ def dispatch(
270
+ self, operation: Operation, machine_id: Optional[int] = None
271
+ ) -> None:
204
272
  """Schedules the given operation on the given machine.
205
273
 
206
274
  The start time of the operation is computed based on the next
207
275
  available time for the machine and the next available time for the
208
276
  job to which the operation belongs. The operation is then scheduled
209
277
  on the machine and the tracking attributes are updated.
278
+
210
279
  Args:
211
280
  operation:
212
281
  The operation to be scheduled.
213
282
  machine_id:
214
283
  The id of the machine on which the operation is to be
215
- scheduled.
284
+ scheduled. If ``None``, the :class:`~job_shop_lib.Operation`'s
285
+ :attr:`~job_shop_lib.Operation.machine_id` attribute is used.
216
286
 
217
287
  Raises:
218
- ValueError: If the operation is not ready to be scheduled.
288
+ ValidationError: If the operation is not ready to be scheduled.
289
+ UninitializedAttributeError: If the operation has multiple
290
+ machines in its list and no ``machine_id`` is provided.
219
291
  """
220
292
 
221
293
  if not self.is_operation_ready(operation):
222
- raise ValueError("Operation is not ready to be scheduled.")
294
+ raise ValidationError("Operation is not ready to be scheduled.")
295
+
296
+ if machine_id is None:
297
+ machine_id = operation.machine_id
223
298
 
224
299
  start_time = self.start_time(operation, machine_id)
225
300
 
@@ -229,10 +304,6 @@ class Dispatcher:
229
304
  self.schedule.add(scheduled_operation)
230
305
  self._update_tracking_attributes(scheduled_operation)
231
306
 
232
- # Notify subscribers
233
- for subscriber in self.subscribers:
234
- subscriber.update(scheduled_operation)
235
-
236
307
  def is_operation_ready(self, operation: Operation) -> bool:
237
308
  """Returns True if the given operation is ready to be scheduled.
238
309
 
@@ -265,8 +336,7 @@ class Dispatcher:
265
336
  The operation to be scheduled.
266
337
  machine_id:
267
338
  The id of the machine on which the operation is to be
268
- scheduled. If None, the start time is computed based on the
269
- next available time for the operation on any machine.
339
+ scheduled.
270
340
  """
271
341
  return max(
272
342
  self._machine_next_available_time[machine_id],
@@ -286,6 +356,40 @@ class Dispatcher:
286
356
  self._job_next_available_time[job_id] = end_time
287
357
  self._cache = {}
288
358
 
359
+ # Notify subscribers
360
+ for subscriber in self.subscribers:
361
+ subscriber.update(scheduled_operation)
362
+
363
+ def create_or_get_observer(
364
+ self,
365
+ observer: Type[ObserverType],
366
+ condition: Callable[[DispatcherObserver], bool] = lambda _: True,
367
+ **kwargs,
368
+ ) -> ObserverType:
369
+ """Creates a new observer of the specified type or returns an existing
370
+ observer of the same type if it already exists in the dispatcher's list
371
+ of observers.
372
+
373
+ Args:
374
+ observer:
375
+ The type of observer to be created or retrieved.
376
+ condition:
377
+ A function that takes an observer and returns True if it is
378
+ the observer to be retrieved. By default, it returns True for
379
+ all observers.
380
+ **kwargs:
381
+ Additional keyword arguments to be passed to the observer's
382
+ constructor.
383
+ """
384
+ for existing_observer in self.subscribers:
385
+ if isinstance(existing_observer, observer) and condition(
386
+ existing_observer
387
+ ):
388
+ return existing_observer
389
+
390
+ new_observer = observer(self, **kwargs)
391
+ return new_observer
392
+
289
393
  @_dispatcher_cache
290
394
  def current_time(self) -> int:
291
395
  """Returns the current time of the schedule.
@@ -297,7 +401,7 @@ class Dispatcher:
297
401
  current_time = self.min_start_time(available_operations)
298
402
  return current_time
299
403
 
300
- def min_start_time(self, operations: list[Operation]) -> int:
404
+ def min_start_time(self, operations: List[Operation]) -> int:
301
405
  """Returns the minimum start time of the available operations."""
302
406
  if not operations:
303
407
  return self.schedule.makespan()
@@ -309,43 +413,43 @@ class Dispatcher:
309
413
  return int(min_start_time)
310
414
 
311
415
  @_dispatcher_cache
312
- def available_operations(self) -> list[Operation]:
416
+ def available_operations(self) -> List[Operation]:
313
417
  """Returns a list of available operations for processing, optionally
314
- filtering out operations using the pruning function.
418
+ filtering out operations using the filter function.
315
419
 
316
420
  This method first gathers all possible next operations from the jobs
317
421
  being processed. It then optionally filters these operations using the
318
- pruning function.
422
+ filter function.
319
423
 
320
424
  Returns:
321
- A list of Operation objects that are available for scheduling.
425
+ A list of :class:`Operation` objects that are available for
426
+ scheduling.
322
427
  """
323
- available_operations = self.available_operations_without_pruning()
324
- if self.pruning_function is not None:
325
- available_operations = self.pruning_function(
428
+ available_operations = self.raw_ready_operations()
429
+ if self.ready_operations_filter is not None:
430
+ available_operations = self.ready_operations_filter(
326
431
  self, available_operations
327
432
  )
328
433
  return available_operations
329
434
 
330
435
  @_dispatcher_cache
331
- def available_operations_without_pruning(self) -> list[Operation]:
436
+ def raw_ready_operations(self) -> List[Operation]:
332
437
  """Returns a list of available operations for processing without
333
- applying the pruning function.
438
+ applying the filter function.
334
439
 
335
440
  Returns:
336
- A list of Operation objects that are available for scheduling
337
- based on precedence and machine constraints only.
441
+ A list of :class:`Operation` objects that are available for
442
+ scheduling based on precedence and machine constraints only.
338
443
  """
339
- available_operations = []
340
- for job_id, next_position in enumerate(self._job_next_operation_index):
341
- if next_position == len(self.instance.jobs[job_id]):
342
- continue
343
- operation = self.instance.jobs[job_id][next_position]
344
- available_operations.append(operation)
444
+ available_operations = [
445
+ self.instance.jobs[job_id][position]
446
+ for job_id, position in enumerate(self._job_next_operation_index)
447
+ if position < len(self.instance.jobs[job_id])
448
+ ]
345
449
  return available_operations
346
450
 
347
451
  @_dispatcher_cache
348
- def unscheduled_operations(self) -> list[Operation]:
452
+ def unscheduled_operations(self) -> List[Operation]:
349
453
  """Returns the list of operations that have not been scheduled."""
350
454
  unscheduled_operations = []
351
455
  for job_id, next_position in enumerate(self._job_next_operation_index):
@@ -354,17 +458,16 @@ class Dispatcher:
354
458
  return unscheduled_operations
355
459
 
356
460
  @_dispatcher_cache
357
- def scheduled_operations(self) -> list[Operation]:
461
+ def scheduled_operations(self) -> List[ScheduledOperation]:
358
462
  """Returns the list of operations that have been scheduled."""
359
463
  scheduled_operations = []
360
- for job_id, next_position in enumerate(self._job_next_operation_index):
361
- operations = self.instance.jobs[job_id][:next_position]
362
- scheduled_operations.extend(operations)
464
+ for machine_schedule in self.schedule.schedule:
465
+ scheduled_operations.extend(machine_schedule)
363
466
  return scheduled_operations
364
467
 
365
468
  @_dispatcher_cache
366
- def available_machines(self) -> list[int]:
367
- """Returns the list of available machines."""
469
+ def available_machines(self) -> List[int]:
470
+ """Returns the list of ready machines."""
368
471
  available_operations = self.available_operations()
369
472
  available_machines = set()
370
473
  for operation in available_operations:
@@ -372,8 +475,8 @@ class Dispatcher:
372
475
  return list(available_machines)
373
476
 
374
477
  @_dispatcher_cache
375
- def available_jobs(self) -> list[int]:
376
- """Returns the list of available jobs."""
478
+ def available_jobs(self) -> List[int]:
479
+ """Returns the list of ready jobs."""
377
480
  available_operations = self.available_operations()
378
481
  available_jobs = set(
379
482
  operation.job_id for operation in available_operations
@@ -384,7 +487,7 @@ class Dispatcher:
384
487
  """Calculates the earliest start time for a given operation based on
385
488
  machine and job constraints.
386
489
 
387
- This method is different from the `start_time` method in that it
490
+ This method is different from the ``start_time`` method in that it
388
491
  takes into account every machine that can process the operation, not
389
492
  just the one that will process it. However, it also assumes that
390
493
  the operation is ready to be scheduled in the job in favor of
@@ -427,24 +530,19 @@ class Dispatcher:
427
530
  return scheduled_operation.end_time - adjusted_start_time
428
531
 
429
532
  @_dispatcher_cache
430
- def completed_operations(self) -> set[Operation]:
533
+ def completed_operations(self) -> Set[ScheduledOperation]:
431
534
  """Returns the set of operations that have been completed.
432
535
 
433
536
  This method returns the operations that have been scheduled and the
434
537
  current time is greater than or equal to the end time of the operation.
435
538
  """
436
539
  scheduled_operations = set(self.scheduled_operations())
437
- ongoing_operations = set(
438
- map(
439
- lambda scheduled_op: scheduled_op.operation,
440
- self.ongoing_operations(),
441
- )
442
- )
540
+ ongoing_operations = set(self.ongoing_operations())
443
541
  completed_operations = scheduled_operations - ongoing_operations
444
542
  return completed_operations
445
543
 
446
544
  @_dispatcher_cache
447
- def uncompleted_operations(self) -> list[Operation]:
545
+ def uncompleted_operations(self) -> List[Operation]:
448
546
  """Returns the list of operations that have not been completed yet.
449
547
 
450
548
  This method checks for operations that either haven't been scheduled
@@ -463,7 +561,7 @@ class Dispatcher:
463
561
  return uncompleted_operations
464
562
 
465
563
  @_dispatcher_cache
466
- def ongoing_operations(self) -> list[ScheduledOperation]:
564
+ def ongoing_operations(self) -> List[ScheduledOperation]:
467
565
  """Returns the list of operations that are currently being processed.
468
566
 
469
567
  This method returns the operations that have been scheduled and are
@@ -489,28 +587,23 @@ class Dispatcher:
489
587
  current_time = self.current_time()
490
588
  return scheduled_operation.start_time <= current_time
491
589
 
492
- @classmethod
493
- def create_schedule_from_raw_solution(
494
- cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
495
- ) -> Schedule:
496
- """Deprecated method, use `Schedule.from_job_sequences` instead."""
497
- warn(
498
- "Dispatcher.create_schedule_from_raw_solution is deprecated. "
499
- "Use Schedule.from_job_sequences instead. It will be removed in "
500
- "version 1.0.0.",
501
- DeprecationWarning,
502
- )
503
- dispatcher = cls(instance)
504
- dispatcher.reset()
505
- raw_solution_deques = [
506
- deque(operations) for operations in raw_solution
590
+ def next_operation(self, job_id: int) -> Operation:
591
+ """Returns the next operation to be scheduled for the given job.
592
+
593
+ Args:
594
+ job_id:
595
+ The id of the job for which to return the next operation.
596
+
597
+ Raises:
598
+ ValidationError: If there are no more operations left for the job.
599
+ """
600
+ if (
601
+ len(self.instance.jobs[job_id])
602
+ <= self._job_next_operation_index[job_id]
603
+ ):
604
+ raise ValidationError(
605
+ f"No more operations left for job {job_id} to schedule."
606
+ )
607
+ return self.instance.jobs[job_id][
608
+ self._job_next_operation_index[job_id]
507
609
  ]
508
- while not dispatcher.schedule.is_complete():
509
- for machine_id, operations in enumerate(raw_solution_deques):
510
- if not operations:
511
- continue
512
- operation = operations[0]
513
- if dispatcher.is_operation_ready(operation):
514
- dispatcher.dispatch(operation, machine_id)
515
- operations.popleft()
516
- return dispatcher.schedule
@@ -0,0 +1,67 @@
1
+ """Contains factory functions for creating dispatching rules, machine choosers,
2
+ and pruning functions for the job shop scheduling problem.
3
+
4
+ The factory functions create and return the appropriate functions based on the
5
+ specified names or enums.
6
+ """
7
+
8
+ from typing import TypeVar, Generic, Any, Dict
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ from job_shop_lib.exceptions import ValidationError
13
+
14
+
15
+ # Disable pylint's false positive
16
+ # pylint: disable=invalid-name
17
+ T = TypeVar("T")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class DispatcherObserverConfig(Generic[T]):
22
+ """Configuration for initializing any type of class.
23
+
24
+ Useful for specifying the type of the
25
+ :class:`~job_shop_lib.dispatching.DispatcherObserver` and additional
26
+ keyword arguments to pass to the dispatcher observer constructor while
27
+ not containing the ``dispatcher`` argument.
28
+
29
+ Args:
30
+ class_type:
31
+ Type of the class to be initialized. It can be the class type, an
32
+ enum value, or a string. This is useful for the creation of
33
+ :class:`~job_shop_lib.dispatching.DispatcherObserver` instances
34
+ from the factory functions.
35
+ kwargs:
36
+ Keyword arguments needed to initialize the class. It must not
37
+ contain the ``dispatcher`` argument.
38
+
39
+ .. seealso::
40
+
41
+ - :class:`~job_shop_lib.dispatching.DispatcherObserver`
42
+ - :func:`job_shop_lib.dispatching.feature_observers.\\
43
+ feature_observer_factory`
44
+ """
45
+
46
+ # We use the type hint T, instead of ObserverType, to allow for string or
47
+ # specific Enum values to be passed as the type argument. For example:
48
+ # FeatureObserverConfig = DispatcherObserverConfig[
49
+ # Type[FeatureObserver] | FeatureObserverType | str
50
+ # ]
51
+ # This allows for the creation of a FeatureObserver instance
52
+ # from the factory function.
53
+ class_type: T
54
+ """Type of the class to be initialized. It can be the class type, an
55
+ enum value, or a string. This is useful for the creation of
56
+ :class:`DispatcherObserver` instances from the factory functions."""
57
+
58
+ kwargs: Dict[str, Any] = field(default_factory=dict)
59
+ """Keyword arguments needed to initialize the class. It must not
60
+ contain the ``dispatcher`` argument."""
61
+
62
+ def __post_init__(self):
63
+ if "dispatcher" in self.kwargs:
64
+ raise ValidationError(
65
+ "The 'dispatcher' argument should not be included in the "
66
+ "kwargs attribute."
67
+ )