job-shop-lib 1.3.0__py3-none-any.whl → 1.5.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/_job_shop_instance.py +158 -26
- job_shop_lib/_operation.py +48 -5
- 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-1.3.0.dist-info → job_shop_lib-1.5.0.dist-info}/METADATA +1 -1
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.5.0.dist-info}/RECORD +17 -16
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.5.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.3.0.dist-info → job_shop_lib-1.5.0.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
@@ -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 = (
|
@@ -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][
|
@@ -25,9 +25,8 @@ def no_setup_time_calculator(
|
|
25
25
|
) -> int:
|
26
26
|
"""Default start time calculator that implements the standard behavior.
|
27
27
|
|
28
|
-
The start time is the maximum of the next available time
|
29
|
-
|
30
|
-
operation belongs.
|
28
|
+
The start time is the maximum of the machine's next available time, the
|
29
|
+
job's next available time, and the operation's release date.
|
31
30
|
|
32
31
|
Args:
|
33
32
|
dispatcher:
|
@@ -44,6 +43,7 @@ def no_setup_time_calculator(
|
|
44
43
|
return max(
|
45
44
|
dispatcher.machine_next_available_time[machine_id],
|
46
45
|
dispatcher.job_next_available_time[operation.job_id],
|
46
|
+
operation.release_date,
|
47
47
|
)
|
48
48
|
|
49
49
|
|
@@ -552,7 +552,9 @@ class Dispatcher:
|
|
552
552
|
for machine_id in operation.machines
|
553
553
|
)
|
554
554
|
job_start_time = self._job_next_available_time[operation.job_id]
|
555
|
-
return max(
|
555
|
+
return max(
|
556
|
+
machine_earliest_start_time, job_start_time, operation.release_date
|
557
|
+
)
|
556
558
|
|
557
559
|
def remaining_duration(
|
558
560
|
self, scheduled_operation: ScheduledOperation
|
@@ -632,7 +634,11 @@ class Dispatcher:
|
|
632
634
|
def is_ongoing(self, scheduled_operation: ScheduledOperation) -> bool:
|
633
635
|
"""Checks if the given operation is currently being processed."""
|
634
636
|
current_time = self.current_time()
|
635
|
-
return
|
637
|
+
return (
|
638
|
+
scheduled_operation.start_time
|
639
|
+
<= current_time
|
640
|
+
< scheduled_operation.end_time
|
641
|
+
)
|
636
642
|
|
637
643
|
def next_operation(self, job_id: int) -> Operation:
|
638
644
|
"""Returns the next operation to be scheduled for the given job.
|
@@ -88,6 +88,8 @@ def filter_non_immediate_operations(
|
|
88
88
|
min_start_time = dispatcher.min_start_time(operations)
|
89
89
|
immediate_operations: list[Operation] = []
|
90
90
|
for operation in operations:
|
91
|
+
if operation.release_date > min_start_time:
|
92
|
+
continue
|
91
93
|
start_time = dispatcher.earliest_start_time(operation)
|
92
94
|
if start_time == min_start_time:
|
93
95
|
immediate_operations.append(operation)
|
@@ -14,6 +14,7 @@ dispatcher.
|
|
14
14
|
PositionInJobObserver
|
15
15
|
RemainingOperationsObserver
|
16
16
|
IsCompletedObserver
|
17
|
+
DatesObserver
|
17
18
|
FeatureObserverType
|
18
19
|
feature_observer_factory
|
19
20
|
FeatureObserverConfig
|
@@ -41,6 +42,7 @@ from ._is_scheduled_observer import IsScheduledObserver
|
|
41
42
|
from ._position_in_job_observer import PositionInJobObserver
|
42
43
|
from ._remaining_operations_observer import RemainingOperationsObserver
|
43
44
|
from ._is_completed_observer import IsCompletedObserver
|
45
|
+
from ._dates_observer import DatesObserver
|
44
46
|
from ._factory import (
|
45
47
|
FeatureObserverType,
|
46
48
|
feature_observer_factory,
|
@@ -60,6 +62,7 @@ __all__ = [
|
|
60
62
|
"PositionInJobObserver",
|
61
63
|
"RemainingOperationsObserver",
|
62
64
|
"IsCompletedObserver",
|
65
|
+
"DatesObserver",
|
63
66
|
"FeatureObserverType",
|
64
67
|
"feature_observer_factory",
|
65
68
|
"FeatureObserverConfig",
|
@@ -0,0 +1,162 @@
|
|
1
|
+
"""Home of the `DatesObserver` class."""
|
2
|
+
|
3
|
+
from typing import Literal
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
|
7
|
+
from job_shop_lib import ScheduledOperation, JobShopInstance
|
8
|
+
from job_shop_lib.dispatching import Dispatcher
|
9
|
+
from job_shop_lib.dispatching.feature_observers import (
|
10
|
+
FeatureObserver,
|
11
|
+
FeatureType,
|
12
|
+
)
|
13
|
+
|
14
|
+
Attribute = Literal["release_date", "deadline", "due_date"]
|
15
|
+
|
16
|
+
|
17
|
+
class DatesObserver(FeatureObserver):
|
18
|
+
"""Observes time-related attributes of operations.
|
19
|
+
|
20
|
+
This observer tracks attributes like release date, deadline, and
|
21
|
+
due date for each operation in the job shop instance. The attributes to
|
22
|
+
be observed can be specified during initialization.
|
23
|
+
|
24
|
+
The values are stored in a numpy array of shape ``(num_operations,
|
25
|
+
num_attributes)``, where ``num_attributes`` is the number of attributes
|
26
|
+
being observed.
|
27
|
+
|
28
|
+
All attributes are updated based on the current time of the dispatcher so
|
29
|
+
that they reflect the relative time remaining until the event occurs:
|
30
|
+
(attribute - current_time). This means that the values will be negative if
|
31
|
+
the event is in the past.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
dispatcher:
|
35
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
36
|
+
subscribe:
|
37
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
38
|
+
initialization. Otherwise, the observer must be subscribed later
|
39
|
+
or manually updated.
|
40
|
+
attributes_to_observe:
|
41
|
+
A list of attributes to observe. If ``None``, all available time
|
42
|
+
attributes (release_date, deadline, due_date) will be
|
43
|
+
observed, provided they exist in the instance.
|
44
|
+
"""
|
45
|
+
|
46
|
+
_supported_feature_types = [FeatureType.OPERATIONS]
|
47
|
+
|
48
|
+
__slots__ = {
|
49
|
+
"attributes_to_observe": "List of attributes to observe.",
|
50
|
+
"_attribute_map": "Maps attributes to their column index.",
|
51
|
+
}
|
52
|
+
|
53
|
+
def __init__(
|
54
|
+
self,
|
55
|
+
dispatcher: Dispatcher,
|
56
|
+
*,
|
57
|
+
subscribe: bool = True,
|
58
|
+
attributes_to_observe: list[Attribute] | None = None,
|
59
|
+
feature_types: FeatureType | list[FeatureType] | None = None,
|
60
|
+
):
|
61
|
+
self.attributes_to_observe = self._determine_attributes_to_observe(
|
62
|
+
dispatcher.instance, attributes_to_observe
|
63
|
+
)
|
64
|
+
self._attribute_map = {
|
65
|
+
attr: i for i, attr in enumerate(self.attributes_to_observe)
|
66
|
+
}
|
67
|
+
self._previous_current_time = 0
|
68
|
+
super().__init__(
|
69
|
+
dispatcher,
|
70
|
+
subscribe=subscribe,
|
71
|
+
feature_types=feature_types,
|
72
|
+
)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def feature_sizes(self) -> dict[FeatureType, int]:
|
76
|
+
return {
|
77
|
+
FeatureType.OPERATIONS: len(self.attributes_to_observe),
|
78
|
+
}
|
79
|
+
|
80
|
+
@property
|
81
|
+
def attribute_map(self) -> dict[Attribute, int]:
|
82
|
+
"""Maps attributes to their column index in the features array."""
|
83
|
+
return self._attribute_map
|
84
|
+
|
85
|
+
def initialize_features(self):
|
86
|
+
"""Initializes the features for the operations.
|
87
|
+
|
88
|
+
This method sets up the features for the operations based on the
|
89
|
+
attributes to observe. It creates a numpy array with the shape
|
90
|
+
(num_operations, num_attributes) and fills it with the corresponding
|
91
|
+
values from the job shop instance. Note that the matrix may contain
|
92
|
+
``np.nan`` values for operations that do not have deadlines or
|
93
|
+
due dates.
|
94
|
+
|
95
|
+
.. seealso::
|
96
|
+
- :meth:`job_shop_lib.JobShopInstance.release_dates_matrix_array`
|
97
|
+
- :meth:`job_shop_lib.JobShopInstance.deadlines_matrix_array`
|
98
|
+
- :meth:`job_shop_lib.JobShopInstance.due_dates_matrix_array`
|
99
|
+
"""
|
100
|
+
self.features = {
|
101
|
+
FeatureType.OPERATIONS: np.zeros(
|
102
|
+
(
|
103
|
+
self.dispatcher.instance.num_operations,
|
104
|
+
len(self.attributes_to_observe),
|
105
|
+
),
|
106
|
+
dtype=np.float32,
|
107
|
+
)
|
108
|
+
}
|
109
|
+
self._previous_current_time = self.dispatcher.current_time()
|
110
|
+
release_dates_matrix = (
|
111
|
+
self.dispatcher.instance.release_dates_matrix_array
|
112
|
+
)
|
113
|
+
valid_operations_mask = ~np.isnan(release_dates_matrix.flatten())
|
114
|
+
|
115
|
+
for attr, col_idx in self._attribute_map.items():
|
116
|
+
matrix = getattr(self.dispatcher.instance, f"{attr}s_matrix_array")
|
117
|
+
values = np.array(matrix).flatten()
|
118
|
+
valid_values = values[valid_operations_mask]
|
119
|
+
self.features[FeatureType.OPERATIONS][:, col_idx] = valid_values
|
120
|
+
|
121
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
122
|
+
"""Updates the features based on the scheduled operation.
|
123
|
+
|
124
|
+
This method updates the features by subtracting the current time from
|
125
|
+
the initial release date, deadline, and due date attributes of the
|
126
|
+
operations.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
scheduled_operation:
|
130
|
+
The scheduled operation that has just been processed. It is
|
131
|
+
not used in this observer, but is required by the
|
132
|
+
:meth:`FeatureObserver.update` method.
|
133
|
+
"""
|
134
|
+
current_time = self.dispatcher.current_time()
|
135
|
+
elapsed_time = current_time - self._previous_current_time
|
136
|
+
self._previous_current_time = current_time
|
137
|
+
cols = [
|
138
|
+
self._attribute_map[attr]
|
139
|
+
for attr in self.attributes_to_observe
|
140
|
+
]
|
141
|
+
self.features[FeatureType.OPERATIONS][:, cols] -= elapsed_time
|
142
|
+
|
143
|
+
def _determine_attributes_to_observe(
|
144
|
+
self,
|
145
|
+
instance: JobShopInstance,
|
146
|
+
attributes_to_observe: list[Attribute] | None,
|
147
|
+
) -> list[Attribute]:
|
148
|
+
if attributes_to_observe:
|
149
|
+
return attributes_to_observe
|
150
|
+
|
151
|
+
default_attributes: list[Attribute] = []
|
152
|
+
if instance.has_release_dates:
|
153
|
+
default_attributes.append("release_date")
|
154
|
+
if instance.has_deadlines:
|
155
|
+
default_attributes.append("deadline")
|
156
|
+
if instance.has_due_dates:
|
157
|
+
default_attributes.append("due_date")
|
158
|
+
return default_attributes
|
159
|
+
|
160
|
+
def reset(self):
|
161
|
+
"""Calls :meth:`initialize_features`"""
|
162
|
+
self.initialize_features()
|
@@ -77,12 +77,13 @@ class DurationObserver(FeatureObserver):
|
|
77
77
|
self.features[FeatureType.JOBS][job_id, 0] = job_duration
|
78
78
|
|
79
79
|
def _update_operation_durations(
|
80
|
-
self,
|
80
|
+
self, unused_scheduled_operation: ScheduledOperation
|
81
81
|
):
|
82
|
-
|
83
|
-
|
84
|
-
self.
|
85
|
-
|
82
|
+
for scheduled_operation in self.dispatcher.ongoing_operations():
|
83
|
+
operation_id = scheduled_operation.operation.operation_id
|
84
|
+
self.features[FeatureType.OPERATIONS][operation_id, 0] = (
|
85
|
+
self.dispatcher.remaining_duration(scheduled_operation)
|
86
|
+
)
|
86
87
|
|
87
88
|
def _update_machine_durations(
|
88
89
|
self, scheduled_operation: ScheduledOperation
|
@@ -18,6 +18,7 @@ Dispatching rules:
|
|
18
18
|
:nosignatures:
|
19
19
|
|
20
20
|
shortest_processing_time_rule
|
21
|
+
largest_processing_time_rule
|
21
22
|
first_come_first_served_rule
|
22
23
|
most_work_remaining_rule
|
23
24
|
most_operations_remaining_rule
|
@@ -32,6 +33,7 @@ Dispatching rule scorers:
|
|
32
33
|
:nosignatures:
|
33
34
|
|
34
35
|
shortest_processing_time_score
|
36
|
+
largest_processing_time_score
|
35
37
|
first_come_first_served_score
|
36
38
|
MostWorkRemainingScorer
|
37
39
|
most_operations_remaining_score
|
@@ -45,6 +47,8 @@ from ._dispatching_rules_functions import (
|
|
45
47
|
most_work_remaining_rule,
|
46
48
|
most_operations_remaining_rule,
|
47
49
|
random_operation_rule,
|
50
|
+
largest_processing_time_score,
|
51
|
+
largest_processing_time_rule,
|
48
52
|
score_based_rule,
|
49
53
|
score_based_rule_with_tie_breaker,
|
50
54
|
shortest_processing_time_score,
|
@@ -78,6 +82,8 @@ __all__ = [
|
|
78
82
|
"most_work_remaining_rule",
|
79
83
|
"most_operations_remaining_rule",
|
80
84
|
"random_operation_rule",
|
85
|
+
"largest_processing_time_score",
|
86
|
+
"largest_processing_time_rule",
|
81
87
|
"score_based_rule",
|
82
88
|
"score_based_rule_with_tie_breaker",
|
83
89
|
"observer_based_most_work_remaining_rule",
|
@@ -17,6 +17,7 @@ from job_shop_lib.dispatching.rules import (
|
|
17
17
|
most_operations_remaining_rule,
|
18
18
|
random_operation_rule,
|
19
19
|
most_work_remaining_rule,
|
20
|
+
largest_processing_time_rule,
|
20
21
|
)
|
21
22
|
|
22
23
|
|
@@ -24,6 +25,7 @@ class DispatchingRuleType(str, Enum):
|
|
24
25
|
"""Enumeration of dispatching rules for the job shop scheduling problem."""
|
25
26
|
|
26
27
|
SHORTEST_PROCESSING_TIME = "shortest_processing_time"
|
28
|
+
LARGEST_PROCESSING_TIME = "largest_processing_time"
|
27
29
|
FIRST_COME_FIRST_SERVED = "first_come_first_served"
|
28
30
|
MOST_WORK_REMAINING = "most_work_remaining"
|
29
31
|
MOST_OPERATIONS_REMAINING = "most_operations_remaining"
|
@@ -62,6 +64,9 @@ def dispatching_rule_factory(
|
|
62
64
|
DispatchingRuleType.SHORTEST_PROCESSING_TIME: (
|
63
65
|
shortest_processing_time_rule
|
64
66
|
),
|
67
|
+
DispatchingRuleType.LARGEST_PROCESSING_TIME: (
|
68
|
+
largest_processing_time_rule
|
69
|
+
),
|
65
70
|
DispatchingRuleType.FIRST_COME_FIRST_SERVED: (
|
66
71
|
first_come_first_served_rule
|
67
72
|
),
|
@@ -26,6 +26,37 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
|
26
26
|
)
|
27
27
|
|
28
28
|
|
29
|
+
def largest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
30
|
+
"""Dispatches the operation with the longest duration."""
|
31
|
+
return max(
|
32
|
+
dispatcher.available_operations(),
|
33
|
+
key=lambda operation: operation.duration,
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
def largest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
38
|
+
"""Scores each job based on the duration of the next operation.
|
39
|
+
|
40
|
+
The score is the duration of the next operation in each job.
|
41
|
+
Jobs with longer next operations will have higher scores.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
dispatcher:
|
45
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` instance
|
46
|
+
containing the job shop instance and the current state of the
|
47
|
+
schedule.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
A list of scores for each job, where the score is the duration of
|
51
|
+
the next operation in that job.
|
52
|
+
"""
|
53
|
+
num_jobs = dispatcher.instance.num_jobs
|
54
|
+
scores = [0] * num_jobs
|
55
|
+
for operation in dispatcher.available_operations():
|
56
|
+
scores[operation.job_id] = operation.duration
|
57
|
+
return scores
|
58
|
+
|
59
|
+
|
29
60
|
def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
|
30
61
|
"""Dispatches the operation with the lowest position in job."""
|
31
62
|
return min(
|
@@ -98,9 +98,7 @@ def generate_machine_matrix_without_recirculation(
|
|
98
98
|
(num_jobs, 1),
|
99
99
|
)
|
100
100
|
# Shuffle the columns:
|
101
|
-
machine_matrix = np.apply_along_axis(
|
102
|
-
rng.permutation, 1, machine_matrix
|
103
|
-
)
|
101
|
+
machine_matrix = np.apply_along_axis(rng.permutation, 1, machine_matrix)
|
104
102
|
return machine_matrix
|
105
103
|
|
106
104
|
|
@@ -1,26 +1,27 @@
|
|
1
|
-
job_shop_lib/__init__.py,sha256=
|
1
|
+
job_shop_lib/__init__.py,sha256=vJ_HFv-mEpI6ejngfGDE4aTMgYvFR3kYm8nkFyGrMS8,639
|
2
2
|
job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
|
3
|
-
job_shop_lib/_job_shop_instance.py,sha256=
|
4
|
-
job_shop_lib/_operation.py,sha256=
|
3
|
+
job_shop_lib/_job_shop_instance.py,sha256=_92orxdi70645J7cQlRE1I0PebvpHRCP6958q9j2h18,24261
|
4
|
+
job_shop_lib/_operation.py,sha256=JI5WjvRXNBeSpPOv3ZwSrUJ4jsVDJYKfMaDHYOaFYts,5945
|
5
5
|
job_shop_lib/_schedule.py,sha256=3PgDZ-DZmlESh5TASNHTqW_8Z7XPVSF64knvXEGRIbM,12927
|
6
6
|
job_shop_lib/_scheduled_operation.py,sha256=czrGr87EOTlO2NPolIN5CDigeiCzvQEyra5IZPwSFZc,2801
|
7
7
|
job_shop_lib/benchmarking/__init__.py,sha256=JPnCw5mK7sADAW0HctVKHEDRw22afp9caNh2eUS36Ys,3290
|
8
8
|
job_shop_lib/benchmarking/_load_benchmark.py,sha256=-cgyx0Kn6uAc3KdGFSQb6eUVQjQggmpVKOH9qusNkXI,2930
|
9
9
|
job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMhsKstmyUmroyWduM7a00KQ,464841
|
10
10
|
job_shop_lib/constraint_programming/__init__.py,sha256=kKQRUxxS_nVFUdXGnf4bQOD9mqrXxZZWElS753A4YiA,454
|
11
|
-
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=
|
11
|
+
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=trTQtqSL2F2PXxd9RPnFhxaY8blNcfFUhTdab5QP9VU,12585
|
12
12
|
job_shop_lib/dispatching/__init__.py,sha256=gbgY1_lhergmXaDa-VYVUmxMpOKzYko0ONREVAt_QPc,2643
|
13
|
-
job_shop_lib/dispatching/_dispatcher.py,sha256=
|
13
|
+
job_shop_lib/dispatching/_dispatcher.py,sha256=A54Q3_hm9Qy5Vfzeudj2QFxd9SWo9lqqBN55S6z-Who,23976
|
14
14
|
job_shop_lib/dispatching/_dispatcher_observer_config.py,sha256=QF2d3rJWwmvutQBAkKxzQ1toJs6eMelT404LGS2z9HQ,2467
|
15
15
|
job_shop_lib/dispatching/_factories.py,sha256=j3MhIwVXiq-B8JMit72ObvXSa2sdgWNhUD86gghL6Gg,4689
|
16
16
|
job_shop_lib/dispatching/_history_observer.py,sha256=Vl8rQaxekUeEB-AyNxyC3c76zQakeh-rdri2iDnZvXw,610
|
17
17
|
job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=2EYxevjpeGMP3do-m0ZmtmjIjmNcxrWOSKzN_bW37gQ,4247
|
18
|
-
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=
|
18
|
+
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=DcwPDIF1eqVR6s3DV4J3cumii0N_apC13XA-50wjlI0,5822
|
19
19
|
job_shop_lib/dispatching/_start_time_calculators.py,sha256=N4kz3c4TmXbyFsY6ctxruYK2ucnjSVXWNMhvsUWFuDg,8192
|
20
20
|
job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=0he-j4OlvqtXAJZD5x1nuBnUKqZUfftVx9NT3CVxPyg,2708
|
21
|
-
job_shop_lib/dispatching/feature_observers/__init__.py,sha256=
|
21
|
+
job_shop_lib/dispatching/feature_observers/__init__.py,sha256=Pzud4tuO_t72d9KY_nEH-stGOvKUTNjo_6GeWDuJPvc,2322
|
22
22
|
job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=tpvqTLIcNmbYROSFT62LiUZ_tI4fHWL_qCULKK43BU4,6429
|
23
|
-
job_shop_lib/dispatching/feature_observers/
|
23
|
+
job_shop_lib/dispatching/feature_observers/_dates_observer.py,sha256=oCk1XAo_2mrgD0ckHQLw3dD7DSQVVg7xBKn7D_u1Dvc,6083
|
24
|
+
job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=pBsJjT-1pbSi32hoLppoqXCftBvJPSh7r7tl3m7etAQ,4225
|
24
25
|
job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=AQIjVp7VRDnb5GuYZlLUwk-xiXSqbsxJW-Ji7NjLoAw,11452
|
25
26
|
job_shop_lib/dispatching/feature_observers/_factory.py,sha256=NyXYK5A1hXsYEeEqngwVRNAFkevY95DglheeqyfFv8s,3217
|
26
27
|
job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=qbgtMUicQ5FWS-Ql4Izjsj4QrevfOGlWzoJ0JlVSLH0,8668
|
@@ -29,17 +30,17 @@ job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=wy_pA-1w
|
|
29
30
|
job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py,sha256=OcuMUB9_By6ZMtX-1_3z-xaxGbP85a5Zv0ywAv7XxWQ,1491
|
30
31
|
job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py,sha256=WRknpQBKXs6h6cXLFJW7ZCvjtU8CPL-iXXNPw3g-mLE,1303
|
31
32
|
job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py,sha256=5V87lCrJUabEe8AkTGXPu5yS8OGxeN8L3-xNyHmdmLs,1441
|
32
|
-
job_shop_lib/dispatching/rules/__init__.py,sha256=
|
33
|
-
job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=
|
33
|
+
job_shop_lib/dispatching/rules/__init__.py,sha256=u1XCRU4tVXJ2rdxXQabPIBaRly0PbBmBu1QcEKx9Z_Y,2396
|
34
|
+
job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=iwqGHtroZAuE90fKOAe79reHlJ8gCnJzMjCkI0lucMg,3101
|
34
35
|
job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=1_canC1lXZATrQCZaHOY3JOLmTuT6U0Z_QWzgTOLwqI,5917
|
35
|
-
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=
|
36
|
+
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=c-T6jUXZ2wjkmENBlDfNyWUA4T3inGll1E0jqAn6wd8,12052
|
36
37
|
job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=CJ74ujgWXgG8cuULWY6VJkD_b3arTcOjTNLZJTAf8xE,2346
|
37
38
|
job_shop_lib/dispatching/rules/_utils.py,sha256=m5qw4qyfaIvVrkmv51nuhreizr98-cg8AJKt2VTd48w,4603
|
38
39
|
job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
|
39
40
|
job_shop_lib/generation/__init__.py,sha256=QaWwuBfBNnOiG0OPiP_CV_flBu9dX7r2o_HwL47tREM,822
|
40
41
|
job_shop_lib/generation/_general_instance_generator.py,sha256=b_tnyP4H_buoN7b6lKQRLvDkeZDdys0mpqS3thB5-SQ,6544
|
41
42
|
job_shop_lib/generation/_instance_generator.py,sha256=doN6WySyI0k7wz3aKy_e6hj6t7WV3dNzve3YmTFShas,4584
|
42
|
-
job_shop_lib/generation/_utils.py,sha256=
|
43
|
+
job_shop_lib/generation/_utils.py,sha256=b3SVU5DY3-VHXX2yrOwM7ABDSexiSFSRbo1d5QjRfoI,3972
|
43
44
|
job_shop_lib/graphs/__init__.py,sha256=wlYIiXTuZRE6Kx3K0RpPUoZikzoegBuN2hcdqMODtGk,2433
|
44
45
|
job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
|
45
46
|
job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=vIy_EkQjgQAd5YyJxKAuGf7CLTjgCfhz-fYrObF4DTU,6962
|
@@ -67,7 +68,7 @@ job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9G
|
|
67
68
|
job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
|
68
69
|
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
|
69
70
|
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
|
70
|
-
job_shop_lib-1.
|
71
|
-
job_shop_lib-1.
|
72
|
-
job_shop_lib-1.
|
73
|
-
job_shop_lib-1.
|
71
|
+
job_shop_lib-1.5.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
72
|
+
job_shop_lib-1.5.0.dist-info/METADATA,sha256=ggq_EHjUpRr9jX_67Sc3Fhj7sFlOqOL1tWzqRY-4xzY,19130
|
73
|
+
job_shop_lib-1.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
74
|
+
job_shop_lib-1.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|