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.
- job_shop_lib/__init__.py +3 -0
- job_shop_lib/_job_shop_instance.py +36 -31
- job_shop_lib/_operation.py +4 -2
- job_shop_lib/_schedule.py +11 -11
- job_shop_lib/benchmarking/_load_benchmark.py +3 -3
- job_shop_lib/constraint_programming/_ortools_solver.py +6 -5
- job_shop_lib/dispatching/_dispatcher.py +58 -20
- job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
- job_shop_lib/dispatching/_factories.py +8 -6
- job_shop_lib/dispatching/_history_observer.py +2 -1
- job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
- job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +23 -15
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
- job_shop_lib/dispatching/rules/_utils.py +9 -8
- job_shop_lib/generation/__init__.py +8 -0
- job_shop_lib/generation/_general_instance_generator.py +42 -64
- job_shop_lib/generation/_instance_generator.py +11 -7
- job_shop_lib/generation/_transformations.py +5 -4
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +7 -7
- job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
- job_shop_lib/graphs/_job_shop_graph.py +17 -13
- job_shop_lib/graphs/_node.py +6 -4
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
- job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
- job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
- job_shop_lib/reinforcement_learning/_utils.py +3 -3
- job_shop_lib/visualization/__init__.py +0 -60
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
- job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
- job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/METADATA +26 -24
- job_shop_lib-1.0.0b1.dist-info/RECORD +69 -0
- job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
- job_shop_lib-1.0.0a4.dist-info/RECORD +0 -66
- {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/LICENSE +0 -0
- {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 (
|
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
|
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 (
|
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:
|
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:
|
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:
|
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
|
128
|
+
file_path: Union[os.PathLike, str, bytes],
|
129
129
|
encoding: str = "utf-8",
|
130
130
|
comment_symbol: str = "#",
|
131
|
-
name: str
|
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) ->
|
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
|
-
|
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:
|
212
|
-
machines_matrix:
|
211
|
+
duration_matrix: List[List[int]],
|
212
|
+
machines_matrix: List[List[List[int]]] | List[List[int]],
|
213
213
|
name: str = "JobShopInstance",
|
214
|
-
metadata:
|
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:
|
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) ->
|
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) ->
|
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.,
|
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(
|
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
|
-
#
|
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
|
-
#
|
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) ->
|
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:
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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:
|
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:
|
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()
|
job_shop_lib/_operation.py
CHANGED
@@ -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
|
63
|
-
self.machines:
|
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:
|
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:
|
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) ->
|
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:
|
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:
|
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:
|
121
|
-
job_sequences:
|
122
|
-
metadata:
|
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:
|
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:
|
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() ->
|
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() ->
|
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:
|
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:
|
155
|
+
self, instance: JobShopInstance, metadata: Dict[str, Any]
|
155
156
|
) -> Schedule:
|
156
157
|
"""Creates a Schedule object from the solution."""
|
157
|
-
operations_start:
|
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:
|
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:
|
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:
|
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,
|
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) ->
|
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) ->
|
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) ->
|
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(
|
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:
|
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:
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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(
|
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
|
-
#
|
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:
|
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
|
|