job-shop-lib 1.3.0__py3-none-any.whl → 1.6.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 +1 -1
- job_shop_lib/_base_solver.py +7 -7
- job_shop_lib/_job_shop_instance.py +158 -26
- job_shop_lib/_operation.py +48 -5
- job_shop_lib/_schedule.py +137 -18
- job_shop_lib/constraint_programming/_ortools_solver.py +12 -13
- job_shop_lib/dispatching/_dispatcher.py +11 -5
- job_shop_lib/dispatching/_ready_operation_filters.py +2 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +3 -0
- job_shop_lib/dispatching/feature_observers/_dates_observer.py +162 -0
- job_shop_lib/dispatching/feature_observers/_duration_observer.py +6 -5
- job_shop_lib/dispatching/rules/__init__.py +6 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +5 -0
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +31 -0
- job_shop_lib/generation/_utils.py +1 -3
- job_shop_lib/metaheuristics/__init__.py +61 -0
- job_shop_lib/metaheuristics/_job_shop_annealer.py +229 -0
- job_shop_lib/metaheuristics/_neighbor_generators.py +182 -0
- job_shop_lib/metaheuristics/_objective_functions.py +73 -0
- job_shop_lib/metaheuristics/_simulated_annealing_solver.py +163 -0
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.6.0.dist-info}/METADATA +21 -32
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.6.0.dist-info}/RECORD +24 -18
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.6.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.6.0.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
job_shop_lib/_base_solver.py
CHANGED
@@ -15,13 +15,13 @@ Solver = Callable[[JobShopInstance], Schedule]
|
|
15
15
|
class BaseSolver(abc.ABC):
|
16
16
|
"""Base class for all solvers implemented as classes.
|
17
17
|
|
18
|
-
A
|
19
|
-
|
20
|
-
classes. This class is provided as a base class for solvers
|
21
|
-
classes. It provides a default implementation of the
|
22
|
-
measures the time taken to solve the instance
|
23
|
-
schedule's metadata under the key "elapsed_time" if
|
24
|
-
|
18
|
+
A ``Solver`` is any ``Callable`` that takes a :class:`JobShopInstance` and
|
19
|
+
returns a :class:`Schedule`. Therefore, solvers can be implemented as
|
20
|
+
functions or as classes. This class is provided as a base class for solvers
|
21
|
+
implemented as classes. It provides a default implementation of the
|
22
|
+
``__call__`` method that measures the time taken to solve the instance
|
23
|
+
and stores it in the schedule's metadata under the key "elapsed_time" if
|
24
|
+
it is not alreadypresent.
|
25
25
|
"""
|
26
26
|
|
27
27
|
@abc.abstractmethod
|
@@ -3,8 +3,10 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import os
|
6
|
+
from collections.abc import Sequence
|
6
7
|
import functools
|
7
8
|
from typing import Any
|
9
|
+
import warnings
|
8
10
|
|
9
11
|
import numpy as np
|
10
12
|
from numpy.typing import NDArray
|
@@ -40,6 +42,9 @@ class JobShopInstance:
|
|
40
42
|
is_flexible
|
41
43
|
durations_matrix
|
42
44
|
machines_matrix
|
45
|
+
release_dates_matrix
|
46
|
+
deadlines_matrix
|
47
|
+
due_dates_matrix
|
43
48
|
durations_matrix_array
|
44
49
|
machines_matrix_array
|
45
50
|
operations_by_machine
|
@@ -92,6 +97,24 @@ class JobShopInstance:
|
|
92
97
|
self.name: str = name
|
93
98
|
self.metadata: dict[str, Any] = metadata
|
94
99
|
|
100
|
+
deprecated_keys = {
|
101
|
+
"release_dates_matrix",
|
102
|
+
"deadlines_matrix",
|
103
|
+
"due_dates_matrix",
|
104
|
+
}
|
105
|
+
if any(key in self.metadata for key in deprecated_keys):
|
106
|
+
warnings.warn(
|
107
|
+
"The use of 'release_dates_matrix', 'deadlines_matrix', or "
|
108
|
+
"'due_dates_matrix' in metadata is deprecated."
|
109
|
+
"Please add these attributes "
|
110
|
+
"directly to the Operation class. Not doing so may cause bugs "
|
111
|
+
"when using the dispatching module.You can use the "
|
112
|
+
"`JobShopInstance.from_matrices` method to create an "
|
113
|
+
"instance from 2D sequences. ",
|
114
|
+
DeprecationWarning,
|
115
|
+
stacklevel=2,
|
116
|
+
)
|
117
|
+
|
95
118
|
def set_operation_attributes(self):
|
96
119
|
"""Sets the ``job_id``, ``position_in_job``, and ``operation_id``
|
97
120
|
attributes for each operation in the instance.
|
@@ -196,14 +219,25 @@ class JobShopInstance:
|
|
196
219
|
"duration_matrix": self.durations_matrix,
|
197
220
|
"machines_matrix": self.machines_matrix,
|
198
221
|
"metadata": self.metadata,
|
222
|
+
# Optionally (if the instance has them):
|
223
|
+
"release_dates_matrix": self.release_dates_matrix,
|
224
|
+
"deadlines_matrix": self.deadlines_matrix,
|
225
|
+
"due_dates_matrix": self.due_dates_matrix,
|
199
226
|
}
|
200
227
|
"""
|
201
|
-
|
228
|
+
data = {
|
202
229
|
"name": self.name,
|
203
230
|
"duration_matrix": self.durations_matrix,
|
204
231
|
"machines_matrix": self.machines_matrix,
|
205
232
|
"metadata": self.metadata,
|
206
233
|
}
|
234
|
+
if self.has_release_dates:
|
235
|
+
data["release_dates_matrix"] = self.release_dates_matrix
|
236
|
+
if self.has_deadlines:
|
237
|
+
data["deadlines_matrix"] = self.deadlines_matrix
|
238
|
+
if self.has_due_dates:
|
239
|
+
data["due_dates_matrix"] = self.due_dates_matrix
|
240
|
+
return data
|
207
241
|
|
208
242
|
@classmethod
|
209
243
|
def from_matrices(
|
@@ -212,6 +246,9 @@ class JobShopInstance:
|
|
212
246
|
machines_matrix: list[list[list[int]]] | list[list[int]],
|
213
247
|
name: str = "JobShopInstance",
|
214
248
|
metadata: dict[str, Any] | None = None,
|
249
|
+
release_dates_matrix: list[list[int]] | None = None,
|
250
|
+
deadlines_matrix: list[list[int | None]] | None = None,
|
251
|
+
due_dates_matrix: list[list[int | None]] | None = None,
|
215
252
|
) -> JobShopInstance:
|
216
253
|
"""Creates a :class:`JobShopInstance` from duration and machines
|
217
254
|
matrices.
|
@@ -219,16 +256,26 @@ class JobShopInstance:
|
|
219
256
|
Args:
|
220
257
|
duration_matrix:
|
221
258
|
A list of lists of integers. The i-th list contains the
|
222
|
-
durations of the operations of the job with id i
|
259
|
+
durations of the operations of the job with id ``i``.
|
223
260
|
machines_matrix:
|
224
261
|
A list of lists of lists of integers if the
|
225
262
|
instance is flexible, or a list of lists of integers if the
|
226
263
|
instance is not flexible. The i-th list contains the machines
|
227
|
-
in which the operations of the job with id i can be
|
264
|
+
in which the operations of the job with id ``i`` can be
|
265
|
+
processed.
|
228
266
|
name:
|
229
267
|
A string with the name of the instance.
|
230
268
|
metadata:
|
231
269
|
A dictionary with additional information about the instance.
|
270
|
+
release_dates_matrix:
|
271
|
+
A list of lists of integers. The i-th list contains the
|
272
|
+
release dates of the operations of the job with id ``i``.
|
273
|
+
deadlines_matrix:
|
274
|
+
A list of lists of optional integers. The i-th list contains
|
275
|
+
the deadlines of the operations of the job with id ``i``.
|
276
|
+
due_dates_matrix:
|
277
|
+
A list of lists of optional integers. The i-th list contains
|
278
|
+
the due dates of the operations of the job with id ``i``.
|
232
279
|
|
233
280
|
Returns:
|
234
281
|
A :class:`JobShopInstance` object.
|
@@ -241,8 +288,29 @@ class JobShopInstance:
|
|
241
288
|
for position_in_job in range(num_operations):
|
242
289
|
duration = duration_matrix[job_id][position_in_job]
|
243
290
|
machines = machines_matrix[job_id][position_in_job]
|
291
|
+
release_date = (
|
292
|
+
release_dates_matrix[job_id][position_in_job]
|
293
|
+
if release_dates_matrix
|
294
|
+
else 0
|
295
|
+
)
|
296
|
+
deadline = (
|
297
|
+
deadlines_matrix[job_id][position_in_job]
|
298
|
+
if deadlines_matrix
|
299
|
+
else None
|
300
|
+
)
|
301
|
+
due_date = (
|
302
|
+
due_dates_matrix[job_id][position_in_job]
|
303
|
+
if due_dates_matrix
|
304
|
+
else None
|
305
|
+
)
|
244
306
|
jobs[job_id].append(
|
245
|
-
Operation(
|
307
|
+
Operation(
|
308
|
+
duration=duration,
|
309
|
+
machines=machines,
|
310
|
+
release_date=release_date,
|
311
|
+
deadline=deadline,
|
312
|
+
due_date=due_date,
|
313
|
+
)
|
246
314
|
)
|
247
315
|
|
248
316
|
metadata = {} if metadata is None else metadata
|
@@ -289,6 +357,21 @@ class JobShopInstance:
|
|
289
357
|
for job in self.jobs
|
290
358
|
)
|
291
359
|
|
360
|
+
@functools.cached_property
|
361
|
+
def has_release_dates(self) -> bool:
|
362
|
+
"""Returns ``True`` if any operation has a release date > 0."""
|
363
|
+
return any(op.release_date > 0 for job in self.jobs for op in job)
|
364
|
+
|
365
|
+
@functools.cached_property
|
366
|
+
def has_deadlines(self) -> bool:
|
367
|
+
"""Returns ``True`` if any operation has a deadline."""
|
368
|
+
return any(op.deadline is not None for job in self.jobs for op in job)
|
369
|
+
|
370
|
+
@functools.cached_property
|
371
|
+
def has_due_dates(self) -> bool:
|
372
|
+
"""Returns ``True`` if any operation has a due date."""
|
373
|
+
return any(op.due_date is not None for job in self.jobs for op in job)
|
374
|
+
|
292
375
|
@functools.cached_property
|
293
376
|
def durations_matrix(self) -> list[list[int]]:
|
294
377
|
"""Returns the duration matrix of the instance.
|
@@ -330,23 +413,75 @@ class JobShopInstance:
|
|
330
413
|
[operation.machine_id for operation in job] for job in self.jobs
|
331
414
|
]
|
332
415
|
|
416
|
+
@functools.cached_property
|
417
|
+
def release_dates_matrix(self) -> list[list[int]]:
|
418
|
+
"""Returns the release dates matrix of the instance.
|
419
|
+
|
420
|
+
The release date of the operation with ``job_id`` i and
|
421
|
+
``position_in_job`` j is stored in the i-th position of the j-th list
|
422
|
+
of the returned matrix.
|
423
|
+
"""
|
424
|
+
return [
|
425
|
+
[operation.release_date for operation in job] for job in self.jobs
|
426
|
+
]
|
427
|
+
|
428
|
+
@functools.cached_property
|
429
|
+
def deadlines_matrix(self) -> list[list[int | None]]:
|
430
|
+
"""Returns the deadlines matrix of the instance.
|
431
|
+
|
432
|
+
The deadline of the operation with ``job_id`` i and
|
433
|
+
``position_in_job`` j is stored in the i-th position of the j-th list
|
434
|
+
of the returned matrix.
|
435
|
+
"""
|
436
|
+
return [[operation.deadline for operation in job] for job in self.jobs]
|
437
|
+
|
438
|
+
@functools.cached_property
|
439
|
+
def due_dates_matrix(self) -> list[list[int | None]]:
|
440
|
+
"""Returns the due dates matrix of the instance.
|
441
|
+
|
442
|
+
The due date of the operation with ``job_id`` i and
|
443
|
+
``position_in_job`` j is stored in the i-th position of the j-th list
|
444
|
+
of the returned matrix.
|
445
|
+
"""
|
446
|
+
return [[operation.due_date for operation in job] for job in self.jobs]
|
447
|
+
|
333
448
|
@functools.cached_property
|
334
449
|
def durations_matrix_array(self) -> NDArray[np.float32]:
|
335
450
|
"""Returns the duration matrix of the instance as a numpy array.
|
336
451
|
|
337
|
-
|
338
|
-
``
|
339
|
-
|
452
|
+
If the jobs have different number of operations, the matrix is
|
453
|
+
padded with ``np.nan`` to make it rectangular.
|
454
|
+
"""
|
455
|
+
return self._fill_matrix_with_nans_2d(self.durations_matrix)
|
340
456
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
457
|
+
@functools.cached_property
|
458
|
+
def release_dates_matrix_array(self) -> NDArray[np.float32]:
|
459
|
+
"""Returns the release dates matrix of the instance as a numpy array.
|
460
|
+
|
461
|
+
If the jobs have different number of operations, the matrix is
|
462
|
+
padded with ``np.nan`` to make it rectangular.
|
347
463
|
"""
|
348
|
-
|
349
|
-
|
464
|
+
return self._fill_matrix_with_nans_2d(self.release_dates_matrix)
|
465
|
+
|
466
|
+
@functools.cached_property
|
467
|
+
def deadlines_matrix_array(self) -> NDArray[np.float32]:
|
468
|
+
"""Returns the deadlines matrix of the instance as a numpy array.
|
469
|
+
|
470
|
+
If the jobs have different number of operations, the matrix is
|
471
|
+
padded with ``np.nan`` to make it rectangular. None values are also
|
472
|
+
converted to ``np.nan``.
|
473
|
+
"""
|
474
|
+
return self._fill_matrix_with_nans_2d(self.deadlines_matrix)
|
475
|
+
|
476
|
+
@functools.cached_property
|
477
|
+
def due_dates_matrix_array(self) -> NDArray[np.float32]:
|
478
|
+
"""Returns the due dates matrix of the instance as a numpy array.
|
479
|
+
|
480
|
+
If the jobs have different number of operations, the matrix is
|
481
|
+
padded with ``np.nan`` to make it rectangular. None values are also
|
482
|
+
converted to ``np.nan``.
|
483
|
+
"""
|
484
|
+
return self._fill_matrix_with_nans_2d(self.due_dates_matrix)
|
350
485
|
|
351
486
|
@functools.cached_property
|
352
487
|
def machines_matrix_array(self) -> NDArray[np.float32]:
|
@@ -474,13 +609,13 @@ class JobShopInstance:
|
|
474
609
|
|
475
610
|
@staticmethod
|
476
611
|
def _fill_matrix_with_nans_2d(
|
477
|
-
matrix:
|
612
|
+
matrix: Sequence[Sequence[int | None]],
|
478
613
|
) -> NDArray[np.float32]:
|
479
|
-
"""
|
614
|
+
"""Creates a 2D numpy array padded with ``np.nan`` values.
|
480
615
|
|
481
616
|
Args:
|
482
617
|
matrix:
|
483
|
-
A list of lists of integers.
|
618
|
+
A list of lists of integers or Nones.
|
484
619
|
|
485
620
|
Returns:
|
486
621
|
A numpy array with the same shape as the input matrix, filled with
|
@@ -491,14 +626,17 @@ class JobShopInstance:
|
|
491
626
|
(len(matrix), max_length), np.nan, dtype=np.float32
|
492
627
|
)
|
493
628
|
for i, row in enumerate(matrix):
|
494
|
-
|
629
|
+
processed_row = [
|
630
|
+
item if item is not None else np.nan for item in row
|
631
|
+
]
|
632
|
+
squared_matrix[i, : len(processed_row)] = processed_row
|
495
633
|
return squared_matrix
|
496
634
|
|
497
635
|
@staticmethod
|
498
636
|
def _fill_matrix_with_nans_3d(
|
499
637
|
matrix: list[list[list[int]]],
|
500
638
|
) -> NDArray[np.float32]:
|
501
|
-
"""
|
639
|
+
"""Creates a 3D numpy array padded with ``np.nan`` values.
|
502
640
|
|
503
641
|
Args:
|
504
642
|
matrix:
|
@@ -522,9 +660,3 @@ class JobShopInstance:
|
|
522
660
|
for j, inner_row in enumerate(row):
|
523
661
|
squared_matrix[i, j, : len(inner_row)] = inner_row
|
524
662
|
return squared_matrix
|
525
|
-
|
526
|
-
|
527
|
-
if __name__ == "__main__":
|
528
|
-
import doctest
|
529
|
-
|
530
|
-
doctest.testmod()
|
job_shop_lib/_operation.py
CHANGED
@@ -30,6 +30,16 @@ class Operation:
|
|
30
30
|
an integer.
|
31
31
|
duration:
|
32
32
|
The time it takes to perform the operation.
|
33
|
+
release_date:
|
34
|
+
The earliest moment this operation can be scheduled to start.
|
35
|
+
Defaults to ``0``.
|
36
|
+
deadline:
|
37
|
+
A hard cutoff time by which the job must be finished. A schedule
|
38
|
+
is invalid if the job completes after this time. Defaults to
|
39
|
+
``None``.
|
40
|
+
due_date:
|
41
|
+
The target completion time for the job. Finishing late is allowed
|
42
|
+
but incurs a penalty (e.g., tardiness). Defaults to ``None``.
|
33
43
|
"""
|
34
44
|
|
35
45
|
__slots__ = {
|
@@ -42,6 +52,20 @@ class Operation:
|
|
42
52
|
"The time it takes to perform the operation. Often referred"
|
43
53
|
" to as the processing time."
|
44
54
|
),
|
55
|
+
"release_date": (
|
56
|
+
"The earliest moment this operation can be scheduled to start. "
|
57
|
+
"Defaults to ``0``."
|
58
|
+
),
|
59
|
+
"deadline": (
|
60
|
+
"A hard cutoff time by which the job must be finished. A schedule "
|
61
|
+
"is invalid if the job completes after this time. Defaults to "
|
62
|
+
"``None``."
|
63
|
+
),
|
64
|
+
"due_date": (
|
65
|
+
"The target completion time for the job. Finishing late is "
|
66
|
+
"allowed but incurs a penalty (e.g., tardiness). Defaults to "
|
67
|
+
"``None``."
|
68
|
+
),
|
45
69
|
"job_id": (
|
46
70
|
"The id of the job the operation belongs to. Defaults to -1. "
|
47
71
|
"It is usually set by the :class:`JobShopInstance` class after "
|
@@ -59,11 +83,21 @@ class Operation:
|
|
59
83
|
),
|
60
84
|
}
|
61
85
|
|
62
|
-
def __init__(
|
86
|
+
def __init__(
|
87
|
+
self,
|
88
|
+
machines: int | list[int],
|
89
|
+
duration: int,
|
90
|
+
release_date: int = 0,
|
91
|
+
deadline: int | None = None,
|
92
|
+
due_date: int | None = None,
|
93
|
+
):
|
63
94
|
self.machines: list[int] = (
|
64
95
|
[machines] if isinstance(machines, int) else machines
|
65
96
|
)
|
66
97
|
self.duration: int = duration
|
98
|
+
self.release_date: int = release_date
|
99
|
+
self.deadline: int | None = deadline
|
100
|
+
self.due_date: int | None = due_date
|
67
101
|
|
68
102
|
# Defined outside the class by the JobShopInstance class:
|
69
103
|
self.job_id: int = -1
|
@@ -93,9 +127,9 @@ class Operation:
|
|
93
127
|
def is_initialized(self) -> bool:
|
94
128
|
"""Returns whether the operation has been initialized."""
|
95
129
|
return (
|
96
|
-
self.job_id
|
97
|
-
|
98
|
-
|
130
|
+
self.job_id != -1
|
131
|
+
and self.position_in_job != -1
|
132
|
+
and self.operation_id != -1
|
99
133
|
)
|
100
134
|
|
101
135
|
def __hash__(self) -> int:
|
@@ -104,7 +138,16 @@ class Operation:
|
|
104
138
|
def __eq__(self, value: object) -> bool:
|
105
139
|
if not isinstance(value, Operation):
|
106
140
|
return False
|
107
|
-
return
|
141
|
+
return (
|
142
|
+
self.machines == value.machines
|
143
|
+
and self.duration == value.duration
|
144
|
+
and self.release_date == value.release_date
|
145
|
+
and self.deadline == value.deadline
|
146
|
+
and self.due_date == value.due_date
|
147
|
+
and self.job_id == value.job_id
|
148
|
+
and self.position_in_job == value.position_in_job
|
149
|
+
and self.operation_id == value.operation_id
|
150
|
+
)
|
108
151
|
|
109
152
|
def __repr__(self) -> str:
|
110
153
|
machines = (
|
job_shop_lib/_schedule.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from typing import Any, TYPE_CHECKING
|
6
6
|
from collections import deque
|
7
7
|
|
8
|
-
from job_shop_lib import ScheduledOperation, JobShopInstance
|
8
|
+
from job_shop_lib import ScheduledOperation, JobShopInstance, Operation
|
9
9
|
from job_shop_lib.exceptions import ValidationError
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
@@ -53,6 +53,19 @@ class Schedule:
|
|
53
53
|
"schedule. It can be used to store information about the "
|
54
54
|
"algorithm that generated the schedule, for example."
|
55
55
|
),
|
56
|
+
"operation_to_scheduled_operation": (
|
57
|
+
"A dictionary that maps an :class:`Operation` to its "
|
58
|
+
":class:`ScheduledOperation` in the schedule. This is used to "
|
59
|
+
"quickly find the scheduled operation associated with a given "
|
60
|
+
"operation."
|
61
|
+
),
|
62
|
+
"num_scheduled_operations": (
|
63
|
+
"The number of operations that have been scheduled so far."
|
64
|
+
),
|
65
|
+
"operation_with_latest_end_time": (
|
66
|
+
"The :class:`ScheduledOperation` with the latest end time. "
|
67
|
+
"This is used to quickly find the last operation in the schedule."
|
68
|
+
),
|
56
69
|
}
|
57
70
|
|
58
71
|
def __init__(
|
@@ -69,6 +82,25 @@ class Schedule:
|
|
69
82
|
self.instance: JobShopInstance = instance
|
70
83
|
self._schedule = schedule
|
71
84
|
self.metadata: dict[str, Any] = metadata
|
85
|
+
self.operation_to_scheduled_operation: dict[
|
86
|
+
Operation, ScheduledOperation
|
87
|
+
] = {
|
88
|
+
scheduled_op.operation: scheduled_op
|
89
|
+
for machine_schedule in schedule
|
90
|
+
for scheduled_op in machine_schedule
|
91
|
+
}
|
92
|
+
self.num_scheduled_operations = sum(
|
93
|
+
len(machine_schedule) for machine_schedule in schedule
|
94
|
+
)
|
95
|
+
self.operation_with_latest_end_time: ScheduledOperation | None = max(
|
96
|
+
(
|
97
|
+
scheduled_op
|
98
|
+
for machine_schedule in schedule
|
99
|
+
for scheduled_op in machine_schedule
|
100
|
+
),
|
101
|
+
key=lambda op: op.end_time, # type: ignore[union-attr]
|
102
|
+
default=None,
|
103
|
+
)
|
72
104
|
|
73
105
|
def __repr__(self) -> str:
|
74
106
|
return str(self.schedule)
|
@@ -84,11 +116,6 @@ class Schedule:
|
|
84
116
|
Schedule.check_schedule(new_schedule)
|
85
117
|
self._schedule = new_schedule
|
86
118
|
|
87
|
-
@property
|
88
|
-
def num_scheduled_operations(self) -> int:
|
89
|
-
"""The number of operations that have been scheduled so far."""
|
90
|
-
return sum(len(machine_schedule) for machine_schedule in self.schedule)
|
91
|
-
|
92
119
|
def to_dict(self) -> dict:
|
93
120
|
"""Returns a dictionary representation of the schedule.
|
94
121
|
|
@@ -106,15 +133,9 @@ class Schedule:
|
|
106
133
|
- **"metadata"**: A dictionary with additional information
|
107
134
|
about the schedule.
|
108
135
|
"""
|
109
|
-
job_sequences: list[list[int]] = []
|
110
|
-
for machine_schedule in self.schedule:
|
111
|
-
job_sequences.append(
|
112
|
-
[operation.job_id for operation in machine_schedule]
|
113
|
-
)
|
114
|
-
|
115
136
|
return {
|
116
137
|
"instance": self.instance.to_dict(),
|
117
|
-
"job_sequences": job_sequences,
|
138
|
+
"job_sequences": self.job_sequences(),
|
118
139
|
"metadata": self.metadata,
|
119
140
|
}
|
120
141
|
|
@@ -211,20 +232,35 @@ class Schedule:
|
|
211
232
|
)
|
212
233
|
return dispatcher.schedule
|
213
234
|
|
235
|
+
def job_sequences(self) -> list[list[int]]:
|
236
|
+
"""Returns the sequence of jobs for each machine in the schedule.
|
237
|
+
|
238
|
+
This method returns a list of lists, where each sublist contains the
|
239
|
+
job ids of the operations scheduled on that machine.
|
240
|
+
"""
|
241
|
+
job_sequences: list[list[int]] = []
|
242
|
+
for machine_schedule in self.schedule:
|
243
|
+
job_sequences.append(
|
244
|
+
[operation.job_id for operation in machine_schedule]
|
245
|
+
)
|
246
|
+
return job_sequences
|
247
|
+
|
214
248
|
def reset(self):
|
215
249
|
"""Resets the schedule to an empty state."""
|
216
250
|
self.schedule = [[] for _ in range(self.instance.num_machines)]
|
251
|
+
self.operation_to_scheduled_operation = {}
|
252
|
+
self.num_scheduled_operations = 0
|
253
|
+
self.operation_with_latest_end_time = None
|
217
254
|
|
218
255
|
def makespan(self) -> int:
|
219
256
|
"""Returns the makespan of the schedule.
|
220
257
|
|
221
258
|
The makespan is the time at which all operations are completed.
|
222
259
|
"""
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
return max_end_time
|
260
|
+
last_operation = self.operation_with_latest_end_time
|
261
|
+
if last_operation is None:
|
262
|
+
return 0
|
263
|
+
return last_operation.end_time
|
228
264
|
|
229
265
|
def is_complete(self) -> bool:
|
230
266
|
"""Returns ``True`` if all operations have been scheduled."""
|
@@ -245,10 +281,25 @@ class Schedule:
|
|
245
281
|
constraints.
|
246
282
|
"""
|
247
283
|
self._check_start_time_of_new_operation(scheduled_operation)
|
284
|
+
|
285
|
+
# Update attributes:
|
248
286
|
self.schedule[scheduled_operation.machine_id].append(
|
249
287
|
scheduled_operation
|
250
288
|
)
|
251
289
|
|
290
|
+
self.operation_to_scheduled_operation[
|
291
|
+
scheduled_operation.operation
|
292
|
+
] = scheduled_operation
|
293
|
+
|
294
|
+
self.num_scheduled_operations += 1
|
295
|
+
|
296
|
+
if (
|
297
|
+
self.operation_with_latest_end_time is None
|
298
|
+
or scheduled_operation.end_time
|
299
|
+
> self.operation_with_latest_end_time.end_time
|
300
|
+
):
|
301
|
+
self.operation_with_latest_end_time = scheduled_operation
|
302
|
+
|
252
303
|
def _check_start_time_of_new_operation(
|
253
304
|
self,
|
254
305
|
new_operation: ScheduledOperation,
|
@@ -333,3 +384,71 @@ class Schedule:
|
|
333
384
|
[machine_schedule.copy() for machine_schedule in self.schedule],
|
334
385
|
**self.metadata,
|
335
386
|
)
|
387
|
+
|
388
|
+
def critical_path(self) -> list[ScheduledOperation]:
|
389
|
+
"""Returns the critical path of the schedule.
|
390
|
+
|
391
|
+
The critical path is the longest path of dependent operations through
|
392
|
+
the schedule, which determines the makespan. This implementation
|
393
|
+
correctly identifies the path even in non-compact schedules where
|
394
|
+
idle time may exist.
|
395
|
+
|
396
|
+
It works by starting from an operation that determines the makespan
|
397
|
+
and tracing backwards, at each step choosing the predecessor (either
|
398
|
+
from the same job or the same machine) that finished latest.
|
399
|
+
"""
|
400
|
+
# 1. Start from the operation that determines the makespan
|
401
|
+
last_scheduled_op = self.operation_with_latest_end_time
|
402
|
+
if last_scheduled_op is None:
|
403
|
+
return []
|
404
|
+
|
405
|
+
critical_path = deque([last_scheduled_op])
|
406
|
+
current_scheduled_op = last_scheduled_op
|
407
|
+
|
408
|
+
# 2. Trace backwards from the last operation
|
409
|
+
while True:
|
410
|
+
job_pred = None
|
411
|
+
machine_pred = None
|
412
|
+
|
413
|
+
# Find job predecessor (the previous operation in the same job)
|
414
|
+
op_idx_in_job = current_scheduled_op.operation.position_in_job
|
415
|
+
if op_idx_in_job > 0:
|
416
|
+
prev_op_in_job = self.instance.jobs[
|
417
|
+
current_scheduled_op.job_id
|
418
|
+
][op_idx_in_job - 1]
|
419
|
+
job_pred = self.operation_to_scheduled_operation[
|
420
|
+
prev_op_in_job
|
421
|
+
]
|
422
|
+
|
423
|
+
# Find machine predecessor (the previous operation on the same
|
424
|
+
# machine)
|
425
|
+
machine_schedule = self.schedule[current_scheduled_op.machine_id]
|
426
|
+
op_idx_on_machine = machine_schedule.index(current_scheduled_op)
|
427
|
+
if op_idx_on_machine > 0:
|
428
|
+
machine_pred = machine_schedule[op_idx_on_machine - 1]
|
429
|
+
|
430
|
+
# 3. Determine the critical predecessor
|
431
|
+
# The critical predecessor is the one that finished latest, as it
|
432
|
+
# determined the start time of the current operation.
|
433
|
+
|
434
|
+
if job_pred is None and machine_pred is None:
|
435
|
+
# Reached the beginning of the schedule, no more predecessors
|
436
|
+
break
|
437
|
+
|
438
|
+
job_pred_end_time = (
|
439
|
+
job_pred.end_time if job_pred is not None else -1
|
440
|
+
)
|
441
|
+
machine_pred_end_time = (
|
442
|
+
machine_pred.end_time if machine_pred is not None else -1
|
443
|
+
)
|
444
|
+
critical_pred = (
|
445
|
+
job_pred
|
446
|
+
if job_pred_end_time >= machine_pred_end_time
|
447
|
+
else machine_pred
|
448
|
+
)
|
449
|
+
assert critical_pred is not None
|
450
|
+
# Prepend the critical predecessor to the path and continue tracing
|
451
|
+
critical_path.appendleft(critical_pred)
|
452
|
+
current_scheduled_op = critical_pred
|
453
|
+
|
454
|
+
return list(critical_path)
|
@@ -112,16 +112,12 @@ class ORToolsSolver(BaseSolver):
|
|
112
112
|
problem.
|
113
113
|
|
114
114
|
Args:
|
115
|
-
instance:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
deadlines:
|
122
|
-
Optional deadlines for each operation.
|
123
|
-
If provided, the solver will ensure that operations are
|
124
|
-
completed before their respective deadlines.
|
115
|
+
instance: The job shop instance to be solved.
|
116
|
+
|
117
|
+
.. note::
|
118
|
+
If provided, `arrival_times` and `deadlines` will override the
|
119
|
+
`release_date` and `deadline` attributes of the operations,
|
120
|
+
respectively.
|
125
121
|
|
126
122
|
Returns:
|
127
123
|
The best schedule found by the solver.
|
@@ -225,9 +221,12 @@ class ORToolsSolver(BaseSolver):
|
|
225
221
|
"""Creates two variables for each operation: start and end time."""
|
226
222
|
for job in instance.jobs:
|
227
223
|
for operation in job:
|
228
|
-
|
229
|
-
|
230
|
-
|
224
|
+
lower_bound = operation.release_date
|
225
|
+
upper_bound = (
|
226
|
+
instance.total_duration
|
227
|
+
if operation.deadline is None
|
228
|
+
else operation.deadline
|
229
|
+
)
|
231
230
|
|
232
231
|
if arrival_times is not None:
|
233
232
|
lower_bound = arrival_times[operation.job_id][
|