job-shop-lib 1.0.0a4__py3-none-any.whl → 1.0.0b1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|