job-shop-lib 0.5.0__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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
job_shop_lib/__init__.py
CHANGED
@@ -1,13 +1,25 @@
|
|
1
1
|
"""Contains the main data structures and base classes.
|
2
|
+
|
3
|
+
.. autosummary::
|
4
|
+
:nosignatures:
|
5
|
+
|
6
|
+
Operation
|
7
|
+
JobShopInstance
|
8
|
+
ScheduledOperation
|
9
|
+
Schedule
|
10
|
+
Solver
|
11
|
+
BaseSolver
|
12
|
+
|
2
13
|
"""
|
3
14
|
|
4
|
-
from job_shop_lib.
|
5
|
-
from job_shop_lib.
|
6
|
-
from job_shop_lib.
|
7
|
-
from job_shop_lib.
|
8
|
-
from job_shop_lib.
|
9
|
-
|
15
|
+
from job_shop_lib._operation import Operation
|
16
|
+
from job_shop_lib._job_shop_instance import JobShopInstance
|
17
|
+
from job_shop_lib._scheduled_operation import ScheduledOperation
|
18
|
+
from job_shop_lib._schedule import Schedule
|
19
|
+
from job_shop_lib._base_solver import BaseSolver, Solver
|
20
|
+
|
10
21
|
|
22
|
+
__version__ = "1.0.0"
|
11
23
|
|
12
24
|
__all__ = [
|
13
25
|
"Operation",
|
@@ -16,6 +28,5 @@ __all__ = [
|
|
16
28
|
"Schedule",
|
17
29
|
"Solver",
|
18
30
|
"BaseSolver",
|
19
|
-
"
|
20
|
-
"NoSolutionFoundError",
|
31
|
+
"__version__",
|
21
32
|
]
|
@@ -33,5 +33,5 @@ class BaseSolver(abc.ABC):
|
|
33
33
|
schedule = self.solve(instance)
|
34
34
|
elapsed_time = time_start - time.perf_counter()
|
35
35
|
schedule.metadata["elapsed_time"] = elapsed_time
|
36
|
-
schedule.metadata["solved_by"] =
|
36
|
+
schedule.metadata["solved_by"] = self.__class__.__name__
|
37
37
|
return schedule
|
@@ -4,9 +4,10 @@ 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
|
+
from numpy.typing import NDArray
|
10
11
|
|
11
12
|
from job_shop_lib import Operation
|
12
13
|
|
@@ -14,49 +15,105 @@ from job_shop_lib import Operation
|
|
14
15
|
class JobShopInstance:
|
15
16
|
"""Data structure to store a Job Shop Scheduling Problem instance.
|
16
17
|
|
17
|
-
Additional attributes such as
|
18
|
-
from the instance and are cached for performance if they
|
19
|
-
computations.
|
18
|
+
Additional attributes such as ``num_machines`` or ``durations_matrix`` can
|
19
|
+
be computed from the instance and are cached for performance if they
|
20
|
+
require expensive computations.
|
21
|
+
|
22
|
+
Methods:
|
23
|
+
|
24
|
+
.. autosummary::
|
25
|
+
:nosignatures:
|
26
|
+
|
27
|
+
from_taillard_file
|
28
|
+
to_dict
|
29
|
+
from_matrices
|
30
|
+
set_operation_attributes
|
31
|
+
|
32
|
+
Properties:
|
33
|
+
|
34
|
+
.. autosummary::
|
35
|
+
:nosignatures:
|
36
|
+
|
37
|
+
num_jobs
|
38
|
+
num_machines
|
39
|
+
num_operations
|
40
|
+
is_flexible
|
41
|
+
durations_matrix
|
42
|
+
machines_matrix
|
43
|
+
durations_matrix_array
|
44
|
+
machines_matrix_array
|
45
|
+
operations_by_machine
|
46
|
+
max_duration
|
47
|
+
max_duration_per_job
|
48
|
+
max_duration_per_machine
|
49
|
+
job_durations
|
50
|
+
machine_loads
|
51
|
+
total_duration
|
20
52
|
|
21
53
|
Attributes:
|
22
|
-
jobs:
|
54
|
+
jobs (List[List[Operation]]):
|
23
55
|
A list of lists of operations. Each list of operations represents
|
24
56
|
a job, and the operations are ordered by their position in the job.
|
25
|
-
The
|
26
|
-
the operations are set when the instance is created.
|
27
|
-
name:
|
57
|
+
The ``job_id``, ``position_in_job``, and ``operation_id``
|
58
|
+
attributes of the operations are set when the instance is created.
|
59
|
+
name (str):
|
28
60
|
A string with the name of the instance.
|
29
|
-
metadata:
|
61
|
+
metadata (Dict[str, Any]):
|
30
62
|
A dictionary with additional information about the instance.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
jobs:
|
66
|
+
A list of lists of operations. Each list of operations
|
67
|
+
represents a job, and the operations are ordered by their
|
68
|
+
position in the job. The ``job_id``, ``position_in_job``, and
|
69
|
+
``operation_id`` attributes of the operations are set when the
|
70
|
+
instance is created.
|
71
|
+
name:
|
72
|
+
A string with the name of the instance.
|
73
|
+
set_operation_attributes:
|
74
|
+
If True, the ``job_id``, ``position_in_job``, and ``operation_id``
|
75
|
+
attributes of the operations are set when the instance is created.
|
76
|
+
See :meth:`set_operation_attributes` for more information. Defaults
|
77
|
+
to True.
|
78
|
+
**metadata:
|
79
|
+
Additional information about the instance.
|
31
80
|
"""
|
32
81
|
|
33
82
|
def __init__(
|
34
83
|
self,
|
35
|
-
jobs:
|
84
|
+
jobs: List[List[Operation]],
|
36
85
|
name: str = "JobShopInstance",
|
86
|
+
set_operation_attributes: bool = True,
|
37
87
|
**metadata: Any,
|
38
88
|
):
|
39
|
-
|
89
|
+
self.jobs: List[List[Operation]] = jobs
|
90
|
+
if set_operation_attributes:
|
91
|
+
self.set_operation_attributes()
|
92
|
+
self.name: str = name
|
93
|
+
self.metadata: Dict[str, Any] = metadata
|
94
|
+
|
95
|
+
def set_operation_attributes(self):
|
96
|
+
"""Sets the ``job_id``, ``position_in_job``, and ``operation_id``
|
97
|
+
attributes for each operation in the instance.
|
98
|
+
|
99
|
+
The ``job_id`` attribute is set to the id of the job to which the
|
100
|
+
operation belongs.
|
101
|
+
|
102
|
+
The ``position_in_job`` attribute is set to the
|
103
|
+
position of the operation in the job (starts from 0).
|
104
|
+
|
105
|
+
The ``operation_id`` attribute is set to a unique identifier for the
|
106
|
+
operation (starting from 0).
|
107
|
+
|
108
|
+
The formula to compute the ``operation_id`` in a job shop instance with
|
109
|
+
a fixed number of operations per job is:
|
110
|
+
|
111
|
+
.. code-block:: python
|
112
|
+
|
113
|
+
operation_id = job_id * num_operations_per_job + position_in_job
|
40
114
|
|
41
|
-
Args:
|
42
|
-
jobs:
|
43
|
-
A list of lists of operations. Each list of operations
|
44
|
-
represents a job, and the operations are ordered by their
|
45
|
-
position in the job. The `job_id`, `position_in_job`, and
|
46
|
-
`operation_id` attributes of the operations are set when the
|
47
|
-
instance is created.
|
48
|
-
name:
|
49
|
-
A string with the name of the instance.
|
50
|
-
**metadata:
|
51
|
-
Additional information about the instance.
|
52
115
|
"""
|
53
|
-
self.jobs = jobs
|
54
|
-
self.set_operation_attributes()
|
55
|
-
self.name = name
|
56
|
-
self.metadata = metadata
|
57
116
|
|
58
|
-
def set_operation_attributes(self):
|
59
|
-
"""Sets the job_id and position of each operation."""
|
60
117
|
operation_id = 0
|
61
118
|
for job_id, job in enumerate(self.jobs):
|
62
119
|
for position, operation in enumerate(job):
|
@@ -68,10 +125,10 @@ class JobShopInstance:
|
|
68
125
|
@classmethod
|
69
126
|
def from_taillard_file(
|
70
127
|
cls,
|
71
|
-
file_path: os.PathLike
|
128
|
+
file_path: Union[os.PathLike, str, bytes],
|
72
129
|
encoding: str = "utf-8",
|
73
130
|
comment_symbol: str = "#",
|
74
|
-
name: str
|
131
|
+
name: Union[str, None] = None,
|
75
132
|
**metadata: Any,
|
76
133
|
) -> JobShopInstance:
|
77
134
|
"""Creates a JobShopInstance from a file following Taillard's format.
|
@@ -91,8 +148,8 @@ class JobShopInstance:
|
|
91
148
|
Additional information about the instance.
|
92
149
|
|
93
150
|
Returns:
|
94
|
-
A JobShopInstance object with the operations read from the
|
95
|
-
and the name and metadata provided.
|
151
|
+
A :class:`JobShopInstance` object with the operations read from the
|
152
|
+
file, and the name and metadata provided.
|
96
153
|
"""
|
97
154
|
with open(file_path, "r", encoding=encoding) as file:
|
98
155
|
lines = file.readlines()
|
@@ -121,7 +178,7 @@ class JobShopInstance:
|
|
121
178
|
name = name.split(".")[0]
|
122
179
|
return cls(jobs=jobs, name=name, **metadata)
|
123
180
|
|
124
|
-
def to_dict(self) ->
|
181
|
+
def to_dict(self) -> Dict[str, Any]:
|
125
182
|
"""Returns a dictionary representation of the instance.
|
126
183
|
|
127
184
|
This representation is useful for saving the instance to a JSON file,
|
@@ -129,13 +186,17 @@ class JobShopInstance:
|
|
129
186
|
like Taillard's.
|
130
187
|
|
131
188
|
Returns:
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
189
|
+
Dict[str, Any]: The returned dictionary has the following
|
190
|
+
structure:
|
191
|
+
|
192
|
+
.. code-block:: python
|
193
|
+
|
194
|
+
{
|
195
|
+
"name": self.name,
|
196
|
+
"duration_matrix": self.durations_matrix,
|
197
|
+
"machines_matrix": self.machines_matrix,
|
198
|
+
"metadata": self.metadata,
|
199
|
+
}
|
139
200
|
"""
|
140
201
|
return {
|
141
202
|
"name": self.name,
|
@@ -147,12 +208,13 @@ class JobShopInstance:
|
|
147
208
|
@classmethod
|
148
209
|
def from_matrices(
|
149
210
|
cls,
|
150
|
-
duration_matrix:
|
151
|
-
machines_matrix:
|
211
|
+
duration_matrix: List[List[int]],
|
212
|
+
machines_matrix: List[List[List[int]]] | List[List[int]],
|
152
213
|
name: str = "JobShopInstance",
|
153
|
-
metadata:
|
214
|
+
metadata: Dict[str, Any] | None = None,
|
154
215
|
) -> JobShopInstance:
|
155
|
-
"""Creates a JobShopInstance from duration and machines
|
216
|
+
"""Creates a :class:`JobShopInstance` from duration and machines
|
217
|
+
matrices.
|
156
218
|
|
157
219
|
Args:
|
158
220
|
duration_matrix:
|
@@ -169,9 +231,9 @@ class JobShopInstance:
|
|
169
231
|
A dictionary with additional information about the instance.
|
170
232
|
|
171
233
|
Returns:
|
172
|
-
A JobShopInstance object.
|
234
|
+
A :class:`JobShopInstance` object.
|
173
235
|
"""
|
174
|
-
jobs:
|
236
|
+
jobs: List[List[Operation]] = [[] for _ in range(len(duration_matrix))]
|
175
237
|
|
176
238
|
num_jobs = len(duration_matrix)
|
177
239
|
for job_id in range(num_jobs):
|
@@ -221,27 +283,29 @@ class JobShopInstance:
|
|
221
283
|
|
222
284
|
@functools.cached_property
|
223
285
|
def is_flexible(self) -> bool:
|
224
|
-
"""Returns True if any operation has more than one machine."""
|
286
|
+
"""Returns ``True`` if any operation has more than one machine."""
|
225
287
|
return any(
|
226
288
|
any(len(operation.machines) > 1 for operation in job)
|
227
289
|
for job in self.jobs
|
228
290
|
)
|
229
291
|
|
230
292
|
@functools.cached_property
|
231
|
-
def durations_matrix(self) ->
|
293
|
+
def durations_matrix(self) -> List[List[int]]:
|
232
294
|
"""Returns the duration matrix of the instance.
|
233
295
|
|
234
|
-
The duration of the operation with
|
235
|
-
is stored in the i-th position of the j-th list of the returned
|
296
|
+
The duration of the operation with ``job_id`` i and ``position_in_job``
|
297
|
+
j is stored in the i-th position of the j-th list of the returned
|
298
|
+
matrix:
|
299
|
+
|
300
|
+
.. code-block:: python
|
301
|
+
|
302
|
+
duration = instance.durations_matrix[i][j]
|
236
303
|
|
237
|
-
```python
|
238
|
-
duration = instance.durations_matrix[i][j]
|
239
|
-
```
|
240
304
|
"""
|
241
305
|
return [[operation.duration for operation in job] for job in self.jobs]
|
242
306
|
|
243
307
|
@functools.cached_property
|
244
|
-
def machines_matrix(self) ->
|
308
|
+
def machines_matrix(self) -> Union[List[List[List[int]]], List[List[int]]]:
|
245
309
|
"""Returns the machines matrix of the instance.
|
246
310
|
|
247
311
|
If the instance is flexible (i.e., if any operation has more than one
|
@@ -253,9 +317,9 @@ class JobShopInstance:
|
|
253
317
|
To access the machines of the operation with position i in the job
|
254
318
|
with id j, the following code must be used:
|
255
319
|
|
256
|
-
|
257
|
-
|
258
|
-
|
320
|
+
.. code-block:: python
|
321
|
+
|
322
|
+
machines = instance.machines_matrix[j][i]
|
259
323
|
|
260
324
|
"""
|
261
325
|
if self.is_flexible:
|
@@ -267,34 +331,34 @@ class JobShopInstance:
|
|
267
331
|
]
|
268
332
|
|
269
333
|
@functools.cached_property
|
270
|
-
def durations_matrix_array(self) -> np.
|
334
|
+
def durations_matrix_array(self) -> NDArray[np.float32]:
|
271
335
|
"""Returns the duration matrix of the instance as a numpy array.
|
272
336
|
|
273
|
-
The returned array has shape (num_jobs
|
274
|
-
|
337
|
+
The returned array has shape (``num_jobs``,
|
338
|
+
``max_num_operations_per_job``).
|
339
|
+
Non-existing operations are filled with ``np.nan``.
|
275
340
|
|
276
341
|
Example:
|
277
342
|
>>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
|
278
343
|
>>> instance = JobShopInstance(jobs)
|
279
344
|
>>> instance.durations_matrix_array
|
280
|
-
array([[ 2.,
|
345
|
+
array([[ 2., 3.],
|
281
346
|
[ 4., nan]], dtype=float32)
|
282
347
|
"""
|
283
348
|
duration_matrix = self.durations_matrix
|
284
349
|
return self._fill_matrix_with_nans_2d(duration_matrix)
|
285
350
|
|
286
351
|
@functools.cached_property
|
287
|
-
def machines_matrix_array(self) -> np.
|
352
|
+
def machines_matrix_array(self) -> NDArray[np.float32]:
|
288
353
|
"""Returns the machines matrix of the instance as a numpy array.
|
289
354
|
|
290
|
-
The returned array has shape (num_jobs
|
291
|
-
max_num_machines_per_operation).
|
292
|
-
np.nan
|
355
|
+
The returned array has shape (``num_jobs``,
|
356
|
+
``max_num_operations_per_job``, ``max_num_machines_per_operation``).
|
357
|
+
Non-existing machines are filled with ``np.nan``.
|
293
358
|
|
294
359
|
Example:
|
295
360
|
>>> jobs = [
|
296
|
-
... [Operation(
|
297
|
-
... [Operation(machines=0, 6)],
|
361
|
+
... [Operation([0, 1], 2), Operation(1, 3)], [Operation(0, 6)]
|
298
362
|
... ]
|
299
363
|
>>> instance = JobShopInstance(jobs)
|
300
364
|
>>> instance.machines_matrix_array
|
@@ -307,25 +371,25 @@ class JobShopInstance:
|
|
307
371
|
machines_matrix = self.machines_matrix
|
308
372
|
if self.is_flexible:
|
309
373
|
# False positive from mypy, the type of machines_matrix is
|
310
|
-
#
|
374
|
+
# List[List[List[int]]] here
|
311
375
|
return self._fill_matrix_with_nans_3d(
|
312
376
|
machines_matrix # type: ignore[arg-type]
|
313
377
|
)
|
314
378
|
|
315
379
|
# False positive from mypy, the type of machines_matrix is
|
316
|
-
#
|
380
|
+
# List[List[int]] here
|
317
381
|
return self._fill_matrix_with_nans_2d(
|
318
382
|
machines_matrix # type: ignore[arg-type]
|
319
383
|
)
|
320
384
|
|
321
385
|
@functools.cached_property
|
322
|
-
def operations_by_machine(self) ->
|
386
|
+
def operations_by_machine(self) -> List[List[Operation]]:
|
323
387
|
"""Returns a list of lists of operations.
|
324
388
|
|
325
389
|
The i-th list contains the operations that can be processed in the
|
326
390
|
machine with id i.
|
327
391
|
"""
|
328
|
-
operations_by_machine:
|
392
|
+
operations_by_machine: List[List[Operation]] = [
|
329
393
|
[] for _ in range(self.num_machines)
|
330
394
|
]
|
331
395
|
for job in self.jobs:
|
@@ -345,7 +409,7 @@ class JobShopInstance:
|
|
345
409
|
)
|
346
410
|
|
347
411
|
@functools.cached_property
|
348
|
-
def max_duration_per_job(self) ->
|
412
|
+
def max_duration_per_job(self) -> List[float]:
|
349
413
|
"""Returns the maximum duration of each job in the instance.
|
350
414
|
|
351
415
|
The maximum duration of the job with id i is stored in the i-th
|
@@ -356,7 +420,7 @@ class JobShopInstance:
|
|
356
420
|
return [max(op.duration for op in job) for job in self.jobs]
|
357
421
|
|
358
422
|
@functools.cached_property
|
359
|
-
def max_duration_per_machine(self) ->
|
423
|
+
def max_duration_per_machine(self) -> List[int]:
|
360
424
|
"""Returns the maximum duration of each machine in the instance.
|
361
425
|
|
362
426
|
The maximum duration of the machine with id i is stored in the i-th
|
@@ -375,7 +439,7 @@ class JobShopInstance:
|
|
375
439
|
return max_duration_per_machine
|
376
440
|
|
377
441
|
@functools.cached_property
|
378
|
-
def job_durations(self) ->
|
442
|
+
def job_durations(self) -> List[int]:
|
379
443
|
"""Returns a list with the duration of each job in the instance.
|
380
444
|
|
381
445
|
The duration of a job is the sum of the durations of its operations.
|
@@ -386,7 +450,7 @@ class JobShopInstance:
|
|
386
450
|
return [sum(op.duration for op in job) for job in self.jobs]
|
387
451
|
|
388
452
|
@functools.cached_property
|
389
|
-
def machine_loads(self) ->
|
453
|
+
def machine_loads(self) -> List[int]:
|
390
454
|
"""Returns the total machine load of each machine in the instance.
|
391
455
|
|
392
456
|
The total machine load of a machine is the sum of the durations of the
|
@@ -409,8 +473,10 @@ class JobShopInstance:
|
|
409
473
|
return sum(self.job_durations)
|
410
474
|
|
411
475
|
@staticmethod
|
412
|
-
def _fill_matrix_with_nans_2d(
|
413
|
-
|
476
|
+
def _fill_matrix_with_nans_2d(
|
477
|
+
matrix: List[List[int]],
|
478
|
+
) -> NDArray[np.float32]:
|
479
|
+
"""Fills a matrix with ``np.nan`` values.
|
414
480
|
|
415
481
|
Args:
|
416
482
|
matrix:
|
@@ -418,7 +484,7 @@ class JobShopInstance:
|
|
418
484
|
|
419
485
|
Returns:
|
420
486
|
A numpy array with the same shape as the input matrix, filled with
|
421
|
-
np.nan values.
|
487
|
+
``np.nan`` values.
|
422
488
|
"""
|
423
489
|
max_length = max(len(row) for row in matrix)
|
424
490
|
squared_matrix = np.full(
|
@@ -429,8 +495,10 @@ class JobShopInstance:
|
|
429
495
|
return squared_matrix
|
430
496
|
|
431
497
|
@staticmethod
|
432
|
-
def _fill_matrix_with_nans_3d(
|
433
|
-
|
498
|
+
def _fill_matrix_with_nans_3d(
|
499
|
+
matrix: List[List[List[int]]],
|
500
|
+
) -> NDArray[np.float32]:
|
501
|
+
"""Fills a 3D matrix with ``np.nan`` values.
|
434
502
|
|
435
503
|
Args:
|
436
504
|
matrix:
|
@@ -438,7 +506,7 @@ class JobShopInstance:
|
|
438
506
|
|
439
507
|
Returns:
|
440
508
|
A numpy array with the same shape as the input matrix, filled with
|
441
|
-
np.nan values.
|
509
|
+
``np.nan`` values.
|
442
510
|
"""
|
443
511
|
max_length = max(len(row) for row in matrix)
|
444
512
|
max_inner_length = len(matrix[0][0])
|
@@ -454,3 +522,9 @@ class JobShopInstance:
|
|
454
522
|
for j, inner_row in enumerate(row):
|
455
523
|
squared_matrix[i, j, : len(inner_row)] = inner_row
|
456
524
|
return squared_matrix
|
525
|
+
|
526
|
+
|
527
|
+
if __name__ == "__main__":
|
528
|
+
import doctest
|
529
|
+
|
530
|
+
doctest.testmod()
|
@@ -0,0 +1,118 @@
|
|
1
|
+
"""Home of the `Operation` class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Union, List
|
6
|
+
|
7
|
+
from job_shop_lib.exceptions import ValidationError
|
8
|
+
|
9
|
+
|
10
|
+
class Operation:
|
11
|
+
"""Stores machine and duration information for a job operation.
|
12
|
+
|
13
|
+
An operation is a task that must be performed on a machine. It is part of a
|
14
|
+
job and has a duration that represents the time it takes to complete the
|
15
|
+
task.
|
16
|
+
|
17
|
+
Tip:
|
18
|
+
To use custom attributes, such as due dates or priorities, subclass
|
19
|
+
this class and add the desired attributes.
|
20
|
+
|
21
|
+
Note:
|
22
|
+
To increase performance, some solvers such as the CP-SAT solver use
|
23
|
+
only integers to represent the operation's attributes. Should a
|
24
|
+
problem involve operations with non-integer durations, it would be
|
25
|
+
necessary to multiply all durations by a sufficiently large integer so
|
26
|
+
that every duration is an integer.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
machines:
|
30
|
+
A list of machine ids that can perform the operation. If
|
31
|
+
only one machine can perform the operation, it can be passed as
|
32
|
+
an integer.
|
33
|
+
duration:
|
34
|
+
The time it takes to perform the operation.
|
35
|
+
"""
|
36
|
+
|
37
|
+
__slots__ = {
|
38
|
+
"machines": (
|
39
|
+
"A list of machine ids that can perform the operation. If "
|
40
|
+
"only one machine can perform the operation, it can be passed as "
|
41
|
+
"an integer."
|
42
|
+
),
|
43
|
+
"duration": (
|
44
|
+
"The time it takes to perform the operation. Often referred"
|
45
|
+
" to as the processing time."
|
46
|
+
),
|
47
|
+
"job_id": (
|
48
|
+
"The id of the job the operation belongs to. Defaults to -1. "
|
49
|
+
"It is usually set by the :class:`JobShopInstance` class after "
|
50
|
+
"initialization."
|
51
|
+
),
|
52
|
+
"position_in_job": (
|
53
|
+
"The index of the operation in the job. Defaults to -1. "
|
54
|
+
"It is usually set by the :class:`JobShopInstance` class after "
|
55
|
+
"initialization."
|
56
|
+
),
|
57
|
+
"operation_id": (
|
58
|
+
"The id of the operation. This is unique within a "
|
59
|
+
":class:`JobShopInstance`. Defaults to -1. It is usually set by "
|
60
|
+
"the :class:`JobShopInstance` class after initialization."
|
61
|
+
),
|
62
|
+
}
|
63
|
+
|
64
|
+
def __init__(self, machines: Union[int, List[int]], duration: int):
|
65
|
+
self.machines: List[int] = (
|
66
|
+
[machines] if isinstance(machines, int) else machines
|
67
|
+
)
|
68
|
+
self.duration: int = duration
|
69
|
+
|
70
|
+
# Defined outside the class by the JobShopInstance class:
|
71
|
+
self.job_id: int = -1
|
72
|
+
self.position_in_job: int = -1
|
73
|
+
self.operation_id: int = -1
|
74
|
+
|
75
|
+
@property
|
76
|
+
def machine_id(self) -> int:
|
77
|
+
"""Returns the id of the machine associated with the operation.
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
UninitializedAttributeError:
|
81
|
+
If the operation has multiple machines in its list.
|
82
|
+
"""
|
83
|
+
if len(self.machines) > 1:
|
84
|
+
raise ValidationError(
|
85
|
+
"Operation has multiple machines. The `machine_id` property "
|
86
|
+
"should only be used when working with a classic JSSP "
|
87
|
+
"instance. This error prevents silent bugs. To handle "
|
88
|
+
"operations with more machines you have to use the machines "
|
89
|
+
"attribute. If you get this error using `job_shop_lib` "
|
90
|
+
"objects, it means that that object does not support "
|
91
|
+
"operations with multiple machines yet."
|
92
|
+
)
|
93
|
+
return self.machines[0]
|
94
|
+
|
95
|
+
def is_initialized(self) -> bool:
|
96
|
+
"""Returns whether the operation has been initialized."""
|
97
|
+
return (
|
98
|
+
self.job_id == -1
|
99
|
+
or self.position_in_job == -1
|
100
|
+
or self.operation_id == -1
|
101
|
+
)
|
102
|
+
|
103
|
+
def __hash__(self) -> int:
|
104
|
+
return hash(self.operation_id)
|
105
|
+
|
106
|
+
def __eq__(self, value: object) -> bool:
|
107
|
+
if not isinstance(value, Operation):
|
108
|
+
return False
|
109
|
+
return self.__slots__ == value.__slots__
|
110
|
+
|
111
|
+
def __repr__(self) -> str:
|
112
|
+
machines = (
|
113
|
+
self.machines[0] if len(self.machines) == 1 else self.machines
|
114
|
+
)
|
115
|
+
return (
|
116
|
+
f"O(m={machines}, d={self.duration}, "
|
117
|
+
f"j={self.job_id}, p={self.position_in_job})"
|
118
|
+
)
|