job-shop-lib 1.0.0a4__py3-none-any.whl → 1.0.0b1__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 (52) hide show
  1. job_shop_lib/__init__.py +3 -0
  2. job_shop_lib/_job_shop_instance.py +36 -31
  3. job_shop_lib/_operation.py +4 -2
  4. job_shop_lib/_schedule.py +11 -11
  5. job_shop_lib/benchmarking/_load_benchmark.py +3 -3
  6. job_shop_lib/constraint_programming/_ortools_solver.py +6 -5
  7. job_shop_lib/dispatching/_dispatcher.py +58 -20
  8. job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
  9. job_shop_lib/dispatching/_factories.py +8 -6
  10. job_shop_lib/dispatching/_history_observer.py +2 -1
  11. job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
  12. job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
  13. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
  14. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
  15. job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
  16. job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
  17. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
  18. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
  19. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
  20. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +23 -15
  21. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
  22. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
  23. job_shop_lib/dispatching/rules/_utils.py +9 -8
  24. job_shop_lib/generation/__init__.py +8 -0
  25. job_shop_lib/generation/_general_instance_generator.py +42 -64
  26. job_shop_lib/generation/_instance_generator.py +11 -7
  27. job_shop_lib/generation/_transformations.py +5 -4
  28. job_shop_lib/generation/_utils.py +124 -0
  29. job_shop_lib/graphs/__init__.py +7 -7
  30. job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
  31. job_shop_lib/graphs/_job_shop_graph.py +17 -13
  32. job_shop_lib/graphs/_node.py +6 -4
  33. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
  34. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
  35. job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
  36. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
  37. job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
  38. job_shop_lib/reinforcement_learning/_utils.py +3 -3
  39. job_shop_lib/visualization/__init__.py +0 -60
  40. job_shop_lib/visualization/gantt/__init__.py +48 -0
  41. job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
  42. job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
  43. job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
  44. job_shop_lib/visualization/graphs/__init__.py +29 -0
  45. job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
  46. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  47. {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/METADATA +26 -24
  48. job_shop_lib-1.0.0b1.dist-info/RECORD +69 -0
  49. job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
  50. job_shop_lib-1.0.0a4.dist-info/RECORD +0 -66
  51. {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/LICENSE +0 -0
  52. {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py CHANGED
@@ -19,6 +19,8 @@ from job_shop_lib._schedule import Schedule
19
19
  from job_shop_lib._base_solver import BaseSolver, Solver
20
20
 
21
21
 
22
+ __version__ = "1.0.0-b.1"
23
+
22
24
  __all__ = [
23
25
  "Operation",
24
26
  "JobShopInstance",
@@ -26,4 +28,5 @@ __all__ = [
26
28
  "Schedule",
27
29
  "Solver",
28
30
  "BaseSolver",
31
+ "__version__",
29
32
  ]
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import functools
7
- from typing import Any
7
+ from typing import Any, List, Union, Dict
8
8
 
9
9
  import numpy as np
10
10
  from numpy.typing import NDArray
@@ -51,14 +51,14 @@ class JobShopInstance:
51
51
  total_duration
52
52
 
53
53
  Attributes:
54
- jobs (list[list[Operation]]):
54
+ jobs (List[List[Operation]]):
55
55
  A list of lists of operations. Each list of operations represents
56
56
  a job, and the operations are ordered by their position in the job.
57
- The ``job_id``, ``position_in_job``, and `operation_id` attributes
58
- of the operations are set when the instance is created.
57
+ The ``job_id``, ``position_in_job``, and ``operation_id``
58
+ attributes of the operations are set when the instance is created.
59
59
  name (str):
60
60
  A string with the name of the instance.
61
- metadata (dict[str, Any]):
61
+ metadata (Dict[str, Any]):
62
62
  A dictionary with additional information about the instance.
63
63
 
64
64
  Args:
@@ -81,16 +81,16 @@ class JobShopInstance:
81
81
 
82
82
  def __init__(
83
83
  self,
84
- jobs: list[list[Operation]],
84
+ jobs: List[List[Operation]],
85
85
  name: str = "JobShopInstance",
86
86
  set_operation_attributes: bool = True,
87
87
  **metadata: Any,
88
88
  ):
89
- self.jobs: list[list[Operation]] = jobs
89
+ self.jobs: List[List[Operation]] = jobs
90
90
  if set_operation_attributes:
91
91
  self.set_operation_attributes()
92
92
  self.name: str = name
93
- self.metadata: dict[str, Any] = metadata
93
+ self.metadata: Dict[str, Any] = metadata
94
94
 
95
95
  def set_operation_attributes(self):
96
96
  """Sets the ``job_id``, ``position_in_job``, and ``operation_id``
@@ -125,10 +125,10 @@ class JobShopInstance:
125
125
  @classmethod
126
126
  def from_taillard_file(
127
127
  cls,
128
- file_path: os.PathLike | str | bytes,
128
+ file_path: Union[os.PathLike, str, bytes],
129
129
  encoding: str = "utf-8",
130
130
  comment_symbol: str = "#",
131
- name: str | None = None,
131
+ name: Union[str, None] = None,
132
132
  **metadata: Any,
133
133
  ) -> JobShopInstance:
134
134
  """Creates a JobShopInstance from a file following Taillard's format.
@@ -178,7 +178,7 @@ class JobShopInstance:
178
178
  name = name.split(".")[0]
179
179
  return cls(jobs=jobs, name=name, **metadata)
180
180
 
181
- def to_dict(self) -> dict[str, Any]:
181
+ def to_dict(self) -> Dict[str, Any]:
182
182
  """Returns a dictionary representation of the instance.
183
183
 
184
184
  This representation is useful for saving the instance to a JSON file,
@@ -186,7 +186,7 @@ class JobShopInstance:
186
186
  like Taillard's.
187
187
 
188
188
  Returns:
189
- dict[str, Any]: The returned dictionary has the following
189
+ Dict[str, Any]: The returned dictionary has the following
190
190
  structure:
191
191
 
192
192
  .. code-block:: python
@@ -208,10 +208,10 @@ class JobShopInstance:
208
208
  @classmethod
209
209
  def from_matrices(
210
210
  cls,
211
- duration_matrix: list[list[int]],
212
- machines_matrix: list[list[list[int]]] | list[list[int]],
211
+ duration_matrix: List[List[int]],
212
+ machines_matrix: List[List[List[int]]] | List[List[int]],
213
213
  name: str = "JobShopInstance",
214
- metadata: dict[str, Any] | None = None,
214
+ metadata: Dict[str, Any] | None = None,
215
215
  ) -> JobShopInstance:
216
216
  """Creates a :class:`JobShopInstance` from duration and machines
217
217
  matrices.
@@ -233,7 +233,7 @@ class JobShopInstance:
233
233
  Returns:
234
234
  A :class:`JobShopInstance` object.
235
235
  """
236
- jobs: list[list[Operation]] = [[] for _ in range(len(duration_matrix))]
236
+ jobs: List[List[Operation]] = [[] for _ in range(len(duration_matrix))]
237
237
 
238
238
  num_jobs = len(duration_matrix)
239
239
  for job_id in range(num_jobs):
@@ -290,7 +290,7 @@ class JobShopInstance:
290
290
  )
291
291
 
292
292
  @functools.cached_property
293
- def durations_matrix(self) -> list[list[int]]:
293
+ def durations_matrix(self) -> List[List[int]]:
294
294
  """Returns the duration matrix of the instance.
295
295
 
296
296
  The duration of the operation with ``job_id`` i and ``position_in_job``
@@ -305,7 +305,7 @@ class JobShopInstance:
305
305
  return [[operation.duration for operation in job] for job in self.jobs]
306
306
 
307
307
  @functools.cached_property
308
- def machines_matrix(self) -> list[list[list[int]]] | list[list[int]]:
308
+ def machines_matrix(self) -> Union[List[List[List[int]]], List[List[int]]]:
309
309
  """Returns the machines matrix of the instance.
310
310
 
311
311
  If the instance is flexible (i.e., if any operation has more than one
@@ -342,7 +342,7 @@ class JobShopInstance:
342
342
  >>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
343
343
  >>> instance = JobShopInstance(jobs)
344
344
  >>> instance.durations_matrix_array
345
- array([[ 2., 2.],
345
+ array([[ 2., 3.],
346
346
  [ 4., nan]], dtype=float32)
347
347
  """
348
348
  duration_matrix = self.durations_matrix
@@ -358,8 +358,7 @@ class JobShopInstance:
358
358
 
359
359
  Example:
360
360
  >>> jobs = [
361
- ... [Operation(machines=[0, 1], 2), Operation(machines=1, 3)],
362
- ... [Operation(machines=0, 6)],
361
+ ... [Operation([0, 1], 2), Operation(1, 3)], [Operation(0, 6)]
363
362
  ... ]
364
363
  >>> instance = JobShopInstance(jobs)
365
364
  >>> instance.machines_matrix_array
@@ -372,25 +371,25 @@ class JobShopInstance:
372
371
  machines_matrix = self.machines_matrix
373
372
  if self.is_flexible:
374
373
  # False positive from mypy, the type of machines_matrix is
375
- # list[list[list[int]]] here
374
+ # List[List[List[int]]] here
376
375
  return self._fill_matrix_with_nans_3d(
377
376
  machines_matrix # type: ignore[arg-type]
378
377
  )
379
378
 
380
379
  # False positive from mypy, the type of machines_matrix is
381
- # list[list[int]] here
380
+ # List[List[int]] here
382
381
  return self._fill_matrix_with_nans_2d(
383
382
  machines_matrix # type: ignore[arg-type]
384
383
  )
385
384
 
386
385
  @functools.cached_property
387
- def operations_by_machine(self) -> list[list[Operation]]:
386
+ def operations_by_machine(self) -> List[List[Operation]]:
388
387
  """Returns a list of lists of operations.
389
388
 
390
389
  The i-th list contains the operations that can be processed in the
391
390
  machine with id i.
392
391
  """
393
- operations_by_machine: list[list[Operation]] = [
392
+ operations_by_machine: List[List[Operation]] = [
394
393
  [] for _ in range(self.num_machines)
395
394
  ]
396
395
  for job in self.jobs:
@@ -410,7 +409,7 @@ class JobShopInstance:
410
409
  )
411
410
 
412
411
  @functools.cached_property
413
- def max_duration_per_job(self) -> list[float]:
412
+ def max_duration_per_job(self) -> List[float]:
414
413
  """Returns the maximum duration of each job in the instance.
415
414
 
416
415
  The maximum duration of the job with id i is stored in the i-th
@@ -421,7 +420,7 @@ class JobShopInstance:
421
420
  return [max(op.duration for op in job) for job in self.jobs]
422
421
 
423
422
  @functools.cached_property
424
- def max_duration_per_machine(self) -> list[int]:
423
+ def max_duration_per_machine(self) -> List[int]:
425
424
  """Returns the maximum duration of each machine in the instance.
426
425
 
427
426
  The maximum duration of the machine with id i is stored in the i-th
@@ -440,7 +439,7 @@ class JobShopInstance:
440
439
  return max_duration_per_machine
441
440
 
442
441
  @functools.cached_property
443
- def job_durations(self) -> list[int]:
442
+ def job_durations(self) -> List[int]:
444
443
  """Returns a list with the duration of each job in the instance.
445
444
 
446
445
  The duration of a job is the sum of the durations of its operations.
@@ -451,7 +450,7 @@ class JobShopInstance:
451
450
  return [sum(op.duration for op in job) for job in self.jobs]
452
451
 
453
452
  @functools.cached_property
454
- def machine_loads(self) -> list[int]:
453
+ def machine_loads(self) -> List[int]:
455
454
  """Returns the total machine load of each machine in the instance.
456
455
 
457
456
  The total machine load of a machine is the sum of the durations of the
@@ -475,7 +474,7 @@ class JobShopInstance:
475
474
 
476
475
  @staticmethod
477
476
  def _fill_matrix_with_nans_2d(
478
- matrix: list[list[int]],
477
+ matrix: List[List[int]],
479
478
  ) -> NDArray[np.float32]:
480
479
  """Fills a matrix with ``np.nan`` values.
481
480
 
@@ -497,7 +496,7 @@ class JobShopInstance:
497
496
 
498
497
  @staticmethod
499
498
  def _fill_matrix_with_nans_3d(
500
- matrix: list[list[list[int]]],
499
+ matrix: List[List[List[int]]],
501
500
  ) -> NDArray[np.float32]:
502
501
  """Fills a 3D matrix with ``np.nan`` values.
503
502
 
@@ -523,3 +522,9 @@ class JobShopInstance:
523
522
  for j, inner_row in enumerate(row):
524
523
  squared_matrix[i, j, : len(inner_row)] = inner_row
525
524
  return squared_matrix
525
+
526
+
527
+ if __name__ == "__main__":
528
+ import doctest
529
+
530
+ doctest.testmod()
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Union, List
6
+
5
7
  from job_shop_lib.exceptions import UninitializedAttributeError
6
8
 
7
9
 
@@ -59,8 +61,8 @@ class Operation:
59
61
  ),
60
62
  }
61
63
 
62
- def __init__(self, machines: int | list[int], duration: int):
63
- self.machines: list[int] = (
64
+ def __init__(self, machines: Union[int, List[int]], duration: int):
65
+ self.machines: List[int] = (
64
66
  [machines] if isinstance(machines, int) else machines
65
67
  )
66
68
  self.duration: int = duration
job_shop_lib/_schedule.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
5
+ from typing import Any, List, Union, Dict, Optional
6
6
  from collections import deque
7
7
 
8
8
  from job_shop_lib import ScheduledOperation, JobShopInstance
@@ -55,7 +55,7 @@ class Schedule:
55
55
  def __init__(
56
56
  self,
57
57
  instance: JobShopInstance,
58
- schedule: list[list[ScheduledOperation]] | None = None,
58
+ schedule: Optional[List[List[ScheduledOperation]]] = None,
59
59
  **metadata: Any,
60
60
  ):
61
61
  if schedule is None:
@@ -65,19 +65,19 @@ class Schedule:
65
65
 
66
66
  self.instance: JobShopInstance = instance
67
67
  self._schedule = schedule
68
- self.metadata: dict[str, Any] = metadata
68
+ self.metadata: Dict[str, Any] = metadata
69
69
 
70
70
  def __repr__(self) -> str:
71
71
  return str(self.schedule)
72
72
 
73
73
  @property
74
- def schedule(self) -> list[list[ScheduledOperation]]:
74
+ def schedule(self) -> List[List[ScheduledOperation]]:
75
75
  """A list of lists of :class:`ScheduledOperation` objects. Each list
76
76
  represents the order of operations on a machine."""
77
77
  return self._schedule
78
78
 
79
79
  @schedule.setter
80
- def schedule(self, new_schedule: list[list[ScheduledOperation]]):
80
+ def schedule(self, new_schedule: List[List[ScheduledOperation]]):
81
81
  Schedule.check_schedule(new_schedule)
82
82
  self._schedule = new_schedule
83
83
 
@@ -103,7 +103,7 @@ class Schedule:
103
103
  - **"metadata"**: A dictionary with additional information
104
104
  about the schedule.
105
105
  """
106
- job_sequences: list[list[int]] = []
106
+ job_sequences: List[List[int]] = []
107
107
  for machine_schedule in self.schedule:
108
108
  job_sequences.append(
109
109
  [operation.job_id for operation in machine_schedule]
@@ -117,9 +117,9 @@ class Schedule:
117
117
 
118
118
  @staticmethod
119
119
  def from_dict(
120
- instance: dict[str, Any] | JobShopInstance,
121
- job_sequences: list[list[int]],
122
- metadata: dict[str, Any] | None = None,
120
+ instance: Union[Dict[str, Any], JobShopInstance],
121
+ job_sequences: List[List[int]],
122
+ metadata: Optional[Dict[str, Any]] = None,
123
123
  ) -> Schedule:
124
124
  """Creates a schedule from a dictionary representation."""
125
125
  if isinstance(instance, dict):
@@ -131,7 +131,7 @@ class Schedule:
131
131
  @staticmethod
132
132
  def from_job_sequences(
133
133
  instance: JobShopInstance,
134
- job_sequences: list[list[int]],
134
+ job_sequences: List[List[int]],
135
135
  ) -> Schedule:
136
136
  """Creates an active schedule from a list of job sequences.
137
137
 
@@ -240,7 +240,7 @@ class Schedule:
240
240
  return previous_operation.end_time <= scheduled_operation.start_time
241
241
 
242
242
  @staticmethod
243
- def check_schedule(schedule: list[list[ScheduledOperation]]):
243
+ def check_schedule(schedule: List[List[ScheduledOperation]]):
244
244
  """Checks if a schedule is valid and raises a
245
245
  :class:`~exceptions.ValidationError` if it is not.
246
246
 
@@ -1,6 +1,6 @@
1
1
  """Contains functions to load benchmark instances from a JSON file."""
2
2
 
3
- from typing import Any
3
+ from typing import Any, Dict
4
4
 
5
5
  import functools
6
6
  import json
@@ -10,7 +10,7 @@ from job_shop_lib import JobShopInstance
10
10
 
11
11
 
12
12
  @functools.cache
13
- def load_all_benchmark_instances() -> dict[str, JobShopInstance]:
13
+ def load_all_benchmark_instances() -> Dict[str, JobShopInstance]:
14
14
  """Loads all benchmark instances available.
15
15
 
16
16
  Returns:
@@ -48,7 +48,7 @@ def load_benchmark_instance(name: str) -> JobShopInstance:
48
48
 
49
49
 
50
50
  @functools.cache
51
- def load_benchmark_json() -> dict[str, dict[str, Any]]:
51
+ def load_benchmark_json() -> Dict[str, Dict[str, Any]]:
52
52
  """Loads the raw JSON file containing the benchmark instances.
53
53
 
54
54
  Results are cached to avoid reading the file multiple times.
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Any, Dict, List, Tuple
5
6
  import time
6
7
 
7
8
  from ortools.sat.python import cp_model
@@ -61,7 +62,7 @@ class ORToolsSolver(BaseSolver):
61
62
  self._makespan: cp_model.IntVar | None = None
62
63
  self.model = cp_model.CpModel()
63
64
  self.solver = cp_model.CpSolver()
64
- self._operations_start: dict[Operation, tuple[IntVar, IntVar]] = {}
65
+ self._operations_start: Dict[Operation, Tuple[IntVar, IntVar]] = {}
65
66
 
66
67
  def __call__(self, instance: JobShopInstance) -> Schedule:
67
68
  """Equivalent to calling the :meth:`~ORToolsSolver.solve` method.
@@ -151,15 +152,15 @@ class ORToolsSolver(BaseSolver):
151
152
  self._set_objective(instance)
152
153
 
153
154
  def _create_schedule(
154
- self, instance: JobShopInstance, metadata: dict[str, object]
155
+ self, instance: JobShopInstance, metadata: Dict[str, Any]
155
156
  ) -> Schedule:
156
157
  """Creates a Schedule object from the solution."""
157
- operations_start: dict[Operation, int] = {
158
+ operations_start: Dict[Operation, int] = {
158
159
  operation: self.solver.Value(start_var)
159
160
  for operation, (start_var, _) in self._operations_start.items()
160
161
  }
161
162
 
162
- unsorted_schedule: list[list[ScheduledOperation]] = [
163
+ unsorted_schedule: List[List[ScheduledOperation]] = [
163
164
  [] for _ in range(instance.num_machines)
164
165
  ]
165
166
  for operation, start_time in operations_start.items():
@@ -234,7 +235,7 @@ class ORToolsSolver(BaseSolver):
234
235
  each machine."""
235
236
 
236
237
  # Create interval variables for each operation on each machine
237
- machines_operations: list[list[tuple[tuple[IntVar, IntVar], int]]] = [
238
+ machines_operations: List[List[Tuple[Tuple[IntVar, IntVar], int]]] = [
238
239
  [] for _ in range(instance.num_machines)
239
240
  ]
240
241
  for job in instance.jobs:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
- from typing import Any, TypeVar
6
+ from typing import Any, TypeVar, List, Optional, Type, Set
7
7
  from collections.abc import Callable
8
8
  from functools import wraps
9
9
 
@@ -52,7 +52,7 @@ class DispatcherObserver(abc.ABC):
52
52
  class HistoryObserver(DispatcherObserver):
53
53
  def __init__(self, dispatcher: Dispatcher):
54
54
  super().__init__(dispatcher)
55
- self.history: list[ScheduledOperation] = []
55
+ self.history: List[ScheduledOperation] = []
56
56
 
57
57
  def update(self, scheduled_operation: ScheduledOperation):
58
58
  self.history.append(scheduled_operation)
@@ -153,6 +153,36 @@ class Dispatcher:
153
153
  responsible for scheduling the operations on the machines and keeping
154
154
  track of the next available time for each machine and job.
155
155
 
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
+
156
186
  Args:
157
187
  instance:
158
188
  The instance of the job shop problem to be solved.
@@ -182,18 +212,18 @@ class Dispatcher:
182
212
  self,
183
213
  instance: JobShopInstance,
184
214
  ready_operations_filter: (
185
- Callable[[Dispatcher, list[Operation]], list[Operation]] | None
215
+ Optional[Callable[[Dispatcher, List[Operation]], List[Operation]]]
186
216
  ) = None,
187
217
  ) -> None:
188
218
 
189
219
  self.instance = instance
190
220
  self.schedule = Schedule(self.instance)
191
221
  self.ready_operations_filter = ready_operations_filter
222
+ self.subscribers: List[DispatcherObserver] = []
192
223
 
193
224
  self._machine_next_available_time = [0] * self.instance.num_machines
194
225
  self._job_next_operation_index = [0] * self.instance.num_jobs
195
226
  self._job_next_available_time = [0] * self.instance.num_jobs
196
- self.subscribers: list[DispatcherObserver] = []
197
227
  self._cache: dict[str, Any] = {}
198
228
 
199
229
  def __str__(self) -> str:
@@ -203,18 +233,18 @@ class Dispatcher:
203
233
  return str(self)
204
234
 
205
235
  @property
206
- def machine_next_available_time(self) -> list[int]:
236
+ def machine_next_available_time(self) -> List[int]:
207
237
  """Returns the next available time for each machine."""
208
238
  return self._machine_next_available_time
209
239
 
210
240
  @property
211
- def job_next_operation_index(self) -> list[int]:
241
+ def job_next_operation_index(self) -> List[int]:
212
242
  """Returns the index of the next operation to be scheduled for each
213
243
  job."""
214
244
  return self._job_next_operation_index
215
245
 
216
246
  @property
217
- def job_next_available_time(self) -> list[int]:
247
+ def job_next_available_time(self) -> List[int]:
218
248
  """Returns the next available time for each job."""
219
249
  return self._job_next_available_time
220
250
 
@@ -236,7 +266,9 @@ class Dispatcher:
236
266
  for subscriber in self.subscribers:
237
267
  subscriber.reset()
238
268
 
239
- def dispatch(self, operation: Operation, machine_id: int) -> None:
269
+ def dispatch(
270
+ self, operation: Operation, machine_id: Optional[int] = None
271
+ ) -> None:
240
272
  """Schedules the given operation on the given machine.
241
273
 
242
274
  The start time of the operation is computed based on the next
@@ -249,15 +281,21 @@ class Dispatcher:
249
281
  The operation to be scheduled.
250
282
  machine_id:
251
283
  The id of the machine on which the operation is to be
252
- scheduled.
284
+ scheduled. If ``None``, the :class:`~job_shop_lib.Operation`'s
285
+ :attr:`~job_shop_lib.Operation.machine_id` attribute is used.
253
286
 
254
287
  Raises:
255
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.
256
291
  """
257
292
 
258
293
  if not self.is_operation_ready(operation):
259
294
  raise ValidationError("Operation is not ready to be scheduled.")
260
295
 
296
+ if machine_id is None:
297
+ machine_id = operation.machine_id
298
+
261
299
  start_time = self.start_time(operation, machine_id)
262
300
 
263
301
  scheduled_operation = ScheduledOperation(
@@ -325,7 +363,7 @@ class Dispatcher:
325
363
 
326
364
  def create_or_get_observer(
327
365
  self,
328
- observer: type[ObserverType],
366
+ observer: Type[ObserverType],
329
367
  condition: Callable[[DispatcherObserver], bool] = lambda _: True,
330
368
  **kwargs,
331
369
  ) -> ObserverType:
@@ -364,7 +402,7 @@ class Dispatcher:
364
402
  current_time = self.min_start_time(available_operations)
365
403
  return current_time
366
404
 
367
- def min_start_time(self, operations: list[Operation]) -> int:
405
+ def min_start_time(self, operations: List[Operation]) -> int:
368
406
  """Returns the minimum start time of the available operations."""
369
407
  if not operations:
370
408
  return self.schedule.makespan()
@@ -376,7 +414,7 @@ class Dispatcher:
376
414
  return int(min_start_time)
377
415
 
378
416
  @_dispatcher_cache
379
- def available_operations(self) -> list[Operation]:
417
+ def available_operations(self) -> List[Operation]:
380
418
  """Returns a list of available operations for processing, optionally
381
419
  filtering out operations using the filter function.
382
420
 
@@ -396,7 +434,7 @@ class Dispatcher:
396
434
  return available_operations
397
435
 
398
436
  @_dispatcher_cache
399
- def raw_ready_operations(self) -> list[Operation]:
437
+ def raw_ready_operations(self) -> List[Operation]:
400
438
  """Returns a list of available operations for processing without
401
439
  applying the filter function.
402
440
 
@@ -412,7 +450,7 @@ class Dispatcher:
412
450
  return available_operations
413
451
 
414
452
  @_dispatcher_cache
415
- def unscheduled_operations(self) -> list[Operation]:
453
+ def unscheduled_operations(self) -> List[Operation]:
416
454
  """Returns the list of operations that have not been scheduled."""
417
455
  unscheduled_operations = []
418
456
  for job_id, next_position in enumerate(self._job_next_operation_index):
@@ -421,7 +459,7 @@ class Dispatcher:
421
459
  return unscheduled_operations
422
460
 
423
461
  @_dispatcher_cache
424
- def scheduled_operations(self) -> list[Operation]:
462
+ def scheduled_operations(self) -> List[Operation]:
425
463
  """Returns the list of operations that have been scheduled."""
426
464
  scheduled_operations = []
427
465
  for job_id, next_position in enumerate(self._job_next_operation_index):
@@ -430,7 +468,7 @@ class Dispatcher:
430
468
  return scheduled_operations
431
469
 
432
470
  @_dispatcher_cache
433
- def available_machines(self) -> list[int]:
471
+ def available_machines(self) -> List[int]:
434
472
  """Returns the list of ready machines."""
435
473
  available_operations = self.available_operations()
436
474
  available_machines = set()
@@ -439,7 +477,7 @@ class Dispatcher:
439
477
  return list(available_machines)
440
478
 
441
479
  @_dispatcher_cache
442
- def available_jobs(self) -> list[int]:
480
+ def available_jobs(self) -> List[int]:
443
481
  """Returns the list of ready jobs."""
444
482
  available_operations = self.available_operations()
445
483
  available_jobs = set(
@@ -494,7 +532,7 @@ class Dispatcher:
494
532
  return scheduled_operation.end_time - adjusted_start_time
495
533
 
496
534
  @_dispatcher_cache
497
- def completed_operations(self) -> set[Operation]:
535
+ def completed_operations(self) -> Set[Operation]:
498
536
  """Returns the set of operations that have been completed.
499
537
 
500
538
  This method returns the operations that have been scheduled and the
@@ -511,7 +549,7 @@ class Dispatcher:
511
549
  return completed_operations
512
550
 
513
551
  @_dispatcher_cache
514
- def uncompleted_operations(self) -> list[Operation]:
552
+ def uncompleted_operations(self) -> List[Operation]:
515
553
  """Returns the list of operations that have not been completed yet.
516
554
 
517
555
  This method checks for operations that either haven't been scheduled
@@ -530,7 +568,7 @@ class Dispatcher:
530
568
  return uncompleted_operations
531
569
 
532
570
  @_dispatcher_cache
533
- def ongoing_operations(self) -> list[ScheduledOperation]:
571
+ def ongoing_operations(self) -> List[ScheduledOperation]:
534
572
  """Returns the list of operations that are currently being processed.
535
573
 
536
574
  This method returns the operations that have been scheduled and are
@@ -5,7 +5,7 @@ The factory functions create and return the appropriate functions based on the
5
5
  specified names or enums.
6
6
  """
7
7
 
8
- from typing import TypeVar, Generic, Any
8
+ from typing import TypeVar, Generic, Any, Dict
9
9
 
10
10
  from dataclasses import dataclass, field
11
11
 
@@ -17,7 +17,7 @@ from job_shop_lib.exceptions import ValidationError
17
17
  T = TypeVar("T")
18
18
 
19
19
 
20
- @dataclass(slots=True, frozen=True)
20
+ @dataclass(frozen=True)
21
21
  class DispatcherObserverConfig(Generic[T]):
22
22
  """Configuration for initializing any type of class.
23
23
 
@@ -46,7 +46,7 @@ class DispatcherObserverConfig(Generic[T]):
46
46
  # We use the type hint T, instead of ObserverType, to allow for string or
47
47
  # specific Enum values to be passed as the type argument. For example:
48
48
  # FeatureObserverConfig = DispatcherObserverConfig[
49
- # type[FeatureObserver] | FeatureObserverType | str
49
+ # Type[FeatureObserver] | FeatureObserverType | str
50
50
  # ]
51
51
  # This allows for the creation of a FeatureObserver instance
52
52
  # from the factory function.
@@ -55,7 +55,7 @@ class DispatcherObserverConfig(Generic[T]):
55
55
  enum value, or a string. This is useful for the creation of
56
56
  :class:`DispatcherObserver` instances from the factory functions."""
57
57
 
58
- kwargs: dict[str, Any] = field(default_factory=dict)
58
+ kwargs: Dict[str, Any] = field(default_factory=dict)
59
59
  """Keyword arguments needed to initialize the class. It must not
60
60
  contain the ``dispatcher`` argument."""
61
61