job-shop-lib 1.6.1__py3-none-any.whl → 1.7.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 +37 -5
- job_shop_lib/benchmarking/__init__.py +3 -0
- job_shop_lib/benchmarking/_load_benchmark.py +25 -0
- job_shop_lib/dispatching/feature_observers/_duration_observer.py +1 -1
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +1 -1
- job_shop_lib/generation/__init__.py +44 -3
- job_shop_lib/generation/_duration_matrix.py +83 -0
- job_shop_lib/generation/_instance_generator.py +8 -6
- job_shop_lib/generation/{_utils.py → _machine_matrix.py} +50 -59
- job_shop_lib/generation/_modular_instance_generator.py +116 -0
- job_shop_lib/generation/_release_date_matrix.py +160 -0
- job_shop_lib/generation/_size_selectors.py +58 -0
- job_shop_lib/metaheuristics/__init__.py +6 -0
- job_shop_lib/metaheuristics/_objective_functions.py +63 -22
- job_shop_lib/reinforcement_learning/__init__.py +9 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +76 -0
- job_shop_lib/reinforcement_learning/_utils.py +63 -4
- {job_shop_lib-1.6.1.dist-info → job_shop_lib-1.7.0.dist-info}/METADATA +3 -3
- {job_shop_lib-1.6.1.dist-info → job_shop_lib-1.7.0.dist-info}/RECORD +22 -18
- {job_shop_lib-1.6.1.dist-info → job_shop_lib-1.7.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.6.1.dist-info → job_shop_lib-1.7.0.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
@@ -40,12 +40,12 @@ class JobShopInstance:
|
|
40
40
|
num_machines
|
41
41
|
num_operations
|
42
42
|
is_flexible
|
43
|
-
|
43
|
+
duration_matrix
|
44
44
|
machines_matrix
|
45
45
|
release_dates_matrix
|
46
46
|
deadlines_matrix
|
47
47
|
due_dates_matrix
|
48
|
-
|
48
|
+
duration_matrix_array
|
49
49
|
machines_matrix_array
|
50
50
|
operations_by_machine
|
51
51
|
max_duration
|
@@ -216,7 +216,7 @@ class JobShopInstance:
|
|
216
216
|
|
217
217
|
{
|
218
218
|
"name": self.name,
|
219
|
-
"duration_matrix": self.
|
219
|
+
"duration_matrix": self.duration_matrix,
|
220
220
|
"machines_matrix": self.machines_matrix,
|
221
221
|
"metadata": self.metadata,
|
222
222
|
# Optionally (if the instance has them):
|
@@ -227,7 +227,7 @@ class JobShopInstance:
|
|
227
227
|
"""
|
228
228
|
data = {
|
229
229
|
"name": self.name,
|
230
|
-
"duration_matrix": self.
|
230
|
+
"duration_matrix": self.duration_matrix,
|
231
231
|
"machines_matrix": self.machines_matrix,
|
232
232
|
"metadata": self.metadata,
|
233
233
|
}
|
@@ -374,6 +374,22 @@ class JobShopInstance:
|
|
374
374
|
|
375
375
|
@functools.cached_property
|
376
376
|
def durations_matrix(self) -> list[list[int]]:
|
377
|
+
"""Another name for `duration_matrix` attribute, kept for
|
378
|
+
backward compatibility.
|
379
|
+
|
380
|
+
It may be removed in future versions.
|
381
|
+
"""
|
382
|
+
warnings.warn(
|
383
|
+
"`duration_matrix` attribute is deprecated and will be "
|
384
|
+
"removed in future versions. Please use `duration_matrix` "
|
385
|
+
"property instead.",
|
386
|
+
DeprecationWarning,
|
387
|
+
stacklevel=2,
|
388
|
+
)
|
389
|
+
return self.duration_matrix
|
390
|
+
|
391
|
+
@functools.cached_property
|
392
|
+
def duration_matrix(self) -> list[list[int]]:
|
377
393
|
"""Returns the duration matrix of the instance.
|
378
394
|
|
379
395
|
The duration of the operation with ``job_id`` i and ``position_in_job``
|
@@ -452,7 +468,23 @@ class JobShopInstance:
|
|
452
468
|
If the jobs have different number of operations, the matrix is
|
453
469
|
padded with ``np.nan`` to make it rectangular.
|
454
470
|
"""
|
455
|
-
|
471
|
+
warnings.warn(
|
472
|
+
"`durations_matrix_array` attribute is deprecated and will be "
|
473
|
+
"removed in future versions. Please use `duration_matrix_array` "
|
474
|
+
"property instead.",
|
475
|
+
DeprecationWarning,
|
476
|
+
stacklevel=2,
|
477
|
+
)
|
478
|
+
return self.duration_matrix_array
|
479
|
+
|
480
|
+
@property
|
481
|
+
def duration_matrix_array(self) -> NDArray[np.float32]:
|
482
|
+
"""Returns the duration matrix of the instance as a numpy array.
|
483
|
+
|
484
|
+
If the jobs have different number of operations, the matrix is
|
485
|
+
padded with ``np.nan`` to make it rectangular.
|
486
|
+
"""
|
487
|
+
return self._fill_matrix_with_nans_2d(self.duration_matrix)
|
456
488
|
|
457
489
|
@functools.cached_property
|
458
490
|
def release_dates_matrix_array(self) -> NDArray[np.float32]:
|
@@ -6,6 +6,7 @@
|
|
6
6
|
load_all_benchmark_instances
|
7
7
|
load_benchmark_instance
|
8
8
|
load_benchmark_json
|
9
|
+
load_benchmark_group
|
9
10
|
|
10
11
|
You can load a benchmark instance from the library:
|
11
12
|
|
@@ -93,10 +94,12 @@ from job_shop_lib.benchmarking._load_benchmark import (
|
|
93
94
|
load_all_benchmark_instances,
|
94
95
|
load_benchmark_instance,
|
95
96
|
load_benchmark_json,
|
97
|
+
load_benchmark_group,
|
96
98
|
)
|
97
99
|
|
98
100
|
__all__ = [
|
99
101
|
"load_all_benchmark_instances",
|
100
102
|
"load_benchmark_instance",
|
101
103
|
"load_benchmark_json",
|
104
|
+
"load_benchmark_group",
|
102
105
|
]
|
@@ -86,3 +86,28 @@ def load_benchmark_json() -> dict[str, dict[str, Any]]:
|
|
86
86
|
|
87
87
|
with benchmark_file.open("r", encoding="utf-8") as f:
|
88
88
|
return json.load(f)
|
89
|
+
|
90
|
+
|
91
|
+
@functools.cache
|
92
|
+
def load_benchmark_group(group_prefix: str) -> list[JobShopInstance]:
|
93
|
+
"""Loads a group of benchmark instances whose names start with the given
|
94
|
+
prefix.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
group_prefix:
|
98
|
+
The prefix of the benchmark instances to load. For example,
|
99
|
+
if the prefix is "la", all instances whose names start with "la"
|
100
|
+
(e.g., "la01-40", "la02-40", etc.) will be loaded.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
A list of :class:`JobShopInstance` objects whose names start with
|
104
|
+
the given prefix.
|
105
|
+
|
106
|
+
.. versionadded:: 1.7.0
|
107
|
+
"""
|
108
|
+
all_instances = load_all_benchmark_instances()
|
109
|
+
return [
|
110
|
+
instance
|
111
|
+
for name, instance in all_instances.items()
|
112
|
+
if name.startswith(group_prefix)
|
113
|
+
]
|
@@ -58,7 +58,7 @@ class DurationObserver(FeatureObserver):
|
|
58
58
|
mapping[feature_type](scheduled_operation)
|
59
59
|
|
60
60
|
def _initialize_operation_durations(self):
|
61
|
-
duration_matrix = self.dispatcher.instance.
|
61
|
+
duration_matrix = self.dispatcher.instance.duration_matrix_array
|
62
62
|
operation_durations = np.array(duration_matrix).reshape(-1, 1)
|
63
63
|
# Drop the NaN values
|
64
64
|
operation_durations = operation_durations[
|
@@ -80,7 +80,7 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
80
80
|
|
81
81
|
# Earliest start times initialization
|
82
82
|
# -------------------------------
|
83
|
-
squared_duration_matrix = dispatcher.instance.
|
83
|
+
squared_duration_matrix = dispatcher.instance.duration_matrix_array
|
84
84
|
self.earliest_start_times: NDArray[np.float32] = np.hstack(
|
85
85
|
(
|
86
86
|
np.zeros((squared_duration_matrix.shape[0], 1), dtype=float),
|
@@ -5,16 +5,46 @@
|
|
5
5
|
|
6
6
|
InstanceGenerator
|
7
7
|
GeneralInstanceGenerator
|
8
|
-
|
8
|
+
modular_instance_generator
|
9
9
|
generate_machine_matrix_with_recirculation
|
10
10
|
generate_machine_matrix_without_recirculation
|
11
|
+
generate_duration_matrix
|
12
|
+
range_size_selector
|
13
|
+
choice_size_selector
|
14
|
+
get_default_machine_matrix_creator
|
15
|
+
get_default_duration_matrix_creator
|
16
|
+
ReleaseDateStrategy
|
17
|
+
create_release_dates_matrix
|
18
|
+
get_independent_release_date_strategy
|
19
|
+
get_cumulative_release_date_strategy
|
20
|
+
get_mixed_release_date_strategy
|
21
|
+
compute_horizon_proxy
|
11
22
|
|
12
23
|
"""
|
13
24
|
|
14
|
-
from job_shop_lib.generation.
|
15
|
-
|
25
|
+
from job_shop_lib.generation._size_selectors import (
|
26
|
+
range_size_selector,
|
27
|
+
choice_size_selector,
|
28
|
+
)
|
29
|
+
from job_shop_lib.generation._machine_matrix import (
|
16
30
|
generate_machine_matrix_with_recirculation,
|
17
31
|
generate_machine_matrix_without_recirculation,
|
32
|
+
get_default_machine_matrix_creator,
|
33
|
+
)
|
34
|
+
from job_shop_lib.generation._duration_matrix import (
|
35
|
+
get_default_duration_matrix_creator,
|
36
|
+
generate_duration_matrix,
|
37
|
+
)
|
38
|
+
from job_shop_lib.generation._release_date_matrix import (
|
39
|
+
ReleaseDateStrategy,
|
40
|
+
create_release_dates_matrix,
|
41
|
+
get_independent_release_date_strategy,
|
42
|
+
get_cumulative_release_date_strategy,
|
43
|
+
get_mixed_release_date_strategy,
|
44
|
+
compute_horizon_proxy,
|
45
|
+
)
|
46
|
+
from job_shop_lib.generation._modular_instance_generator import (
|
47
|
+
modular_instance_generator,
|
18
48
|
)
|
19
49
|
from job_shop_lib.generation._instance_generator import InstanceGenerator
|
20
50
|
from job_shop_lib.generation._general_instance_generator import (
|
@@ -27,4 +57,15 @@ __all__ = [
|
|
27
57
|
"generate_duration_matrix",
|
28
58
|
"generate_machine_matrix_with_recirculation",
|
29
59
|
"generate_machine_matrix_without_recirculation",
|
60
|
+
"modular_instance_generator",
|
61
|
+
"range_size_selector",
|
62
|
+
"choice_size_selector",
|
63
|
+
"get_default_machine_matrix_creator",
|
64
|
+
"get_default_duration_matrix_creator",
|
65
|
+
"ReleaseDateStrategy",
|
66
|
+
"create_release_dates_matrix",
|
67
|
+
"get_independent_release_date_strategy",
|
68
|
+
"get_cumulative_release_date_strategy",
|
69
|
+
"get_mixed_release_date_strategy",
|
70
|
+
"compute_horizon_proxy",
|
30
71
|
]
|
@@ -0,0 +1,83 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import random
|
3
|
+
|
4
|
+
import numpy as np
|
5
|
+
from numpy.typing import NDArray
|
6
|
+
|
7
|
+
from job_shop_lib.exceptions import ValidationError
|
8
|
+
|
9
|
+
|
10
|
+
def get_default_duration_matrix_creator(
|
11
|
+
duration_range: tuple[int, int] = (1, 99),
|
12
|
+
) -> Callable[
|
13
|
+
[list[list[list[int]]] | list[list[int]], random.Random],
|
14
|
+
list[list[int]],
|
15
|
+
]:
|
16
|
+
"""Creates a duration matrix generator function.
|
17
|
+
|
18
|
+
Internally, it wraps :func:`generate_duration_matrix`.
|
19
|
+
|
20
|
+
.. note::
|
21
|
+
|
22
|
+
This function assumes that the machine matrix has the shape (num_jobs,
|
23
|
+
num_machines).
|
24
|
+
|
25
|
+
Args:
|
26
|
+
duration_range:
|
27
|
+
A tuple specifying the inclusive range for operation durations.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
A callable that generates a duration matrix of shape (num_jobs,
|
31
|
+
num_machines) when called with a machine matrix and a
|
32
|
+
`random.Random` instance.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def duration_matrix_creator(
|
36
|
+
machine_matrix: list[list[list[int]]] | list[list[int]],
|
37
|
+
rng: random.Random,
|
38
|
+
) -> list[list[int]]:
|
39
|
+
seed_for_np = rng.randint(0, 2**16 - 1)
|
40
|
+
numpy_rng = np.random.default_rng(seed_for_np)
|
41
|
+
num_jobs = len(machine_matrix)
|
42
|
+
num_machines = len(machine_matrix[0])
|
43
|
+
return generate_duration_matrix(
|
44
|
+
num_jobs, num_machines, duration_range, numpy_rng
|
45
|
+
).tolist()
|
46
|
+
|
47
|
+
return duration_matrix_creator
|
48
|
+
|
49
|
+
|
50
|
+
def generate_duration_matrix(
|
51
|
+
num_jobs: int,
|
52
|
+
num_machines: int,
|
53
|
+
duration_range: tuple[int, int],
|
54
|
+
rng: np.random.Generator | None = None,
|
55
|
+
) -> NDArray[np.int32]:
|
56
|
+
"""Generates a duration matrix.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
num_jobs: The number of jobs.
|
60
|
+
num_machines: The number of machines.
|
61
|
+
duration_range: The range of the duration values.
|
62
|
+
rng: A numpy random number generator.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
A duration matrix with shape (num_jobs, num_machines).
|
66
|
+
"""
|
67
|
+
rng = rng or np.random.default_rng()
|
68
|
+
if duration_range[0] > duration_range[1]:
|
69
|
+
raise ValidationError(
|
70
|
+
"The lower bound of the duration range must be less than or equal "
|
71
|
+
"to the upper bound."
|
72
|
+
)
|
73
|
+
if num_jobs <= 0:
|
74
|
+
raise ValidationError("The number of jobs must be greater than 0.")
|
75
|
+
if num_machines <= 0:
|
76
|
+
raise ValidationError("The number of machines must be greater than 0.")
|
77
|
+
|
78
|
+
return rng.integers(
|
79
|
+
duration_range[0],
|
80
|
+
duration_range[1] + 1,
|
81
|
+
size=(num_jobs, num_machines),
|
82
|
+
dtype=np.int32,
|
83
|
+
)
|
@@ -13,13 +13,13 @@ class InstanceGenerator(ABC):
|
|
13
13
|
"""Common interface for all generators.
|
14
14
|
|
15
15
|
The class supports both single instance generation and iteration over
|
16
|
-
multiple instances, controlled by the
|
17
|
-
implements the iterator protocol, allowing it to be used in a
|
16
|
+
multiple instances, controlled by the ``iteration_limit`` parameter. It
|
17
|
+
implements the iterator protocol, allowing it to be used in a ``for`` loop.
|
18
18
|
|
19
19
|
Note:
|
20
20
|
When used as an iterator, the generator will produce instances until it
|
21
|
-
reaches the specified
|
22
|
-
it will continue indefinitely.
|
21
|
+
reaches the specified ``iteration_limit``. If ``iteration_limit`` is
|
22
|
+
``None``, it will continue indefinitely.
|
23
23
|
|
24
24
|
Attributes:
|
25
25
|
num_jobs_range:
|
@@ -84,9 +84,11 @@ class InstanceGenerator(ABC):
|
|
84
84
|
"""Generates a single job shop instance
|
85
85
|
|
86
86
|
Args:
|
87
|
-
num_jobs:
|
87
|
+
num_jobs:
|
88
|
+
The number of jobs to generate. If None, a random value
|
88
89
|
within the specified range will be used.
|
89
|
-
num_machines:
|
90
|
+
num_machines:
|
91
|
+
The number of machines to generate. If None, a random
|
90
92
|
value within the specified range will be used.
|
91
93
|
"""
|
92
94
|
|
@@ -1,43 +1,62 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import random
|
3
|
+
|
1
4
|
import numpy as np
|
2
5
|
from numpy.typing import NDArray
|
3
6
|
|
4
7
|
from job_shop_lib.exceptions import ValidationError
|
5
8
|
|
6
9
|
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
) ->
|
13
|
-
"""
|
10
|
+
def get_default_machine_matrix_creator(
|
11
|
+
size_selector: Callable[[random.Random], tuple[int, int]] = (
|
12
|
+
lambda _: (10, 10)
|
13
|
+
),
|
14
|
+
with_recirculation: bool = True,
|
15
|
+
) -> Callable[[random.Random], list[list[list[int]]] | list[list[int]]]:
|
16
|
+
"""Creates a machine matrix generator function.
|
17
|
+
|
18
|
+
Internally, it wraps either
|
19
|
+
:func:`generate_machine_matrix_with_recirculation`
|
20
|
+
or :func:`generate_machine_matrix_without_recirculation`
|
21
|
+
based on the `with_recirculation` parameter.
|
22
|
+
|
23
|
+
.. note::
|
24
|
+
|
25
|
+
The generated machine matrix will have the shape (num_jobs,
|
26
|
+
num_machines).
|
14
27
|
|
15
28
|
Args:
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
29
|
+
rng:
|
30
|
+
A random.Random instance.
|
31
|
+
size_selector:
|
32
|
+
A callable that takes a random.Random instance and returns a
|
33
|
+
tuple (num_jobs, num_machines).
|
34
|
+
with_recirculation:
|
35
|
+
If ``True``, generates a machine matrix with recirculation;
|
36
|
+
otherwise, without recirculation. Recirculation means that a job
|
37
|
+
can visit the same machine more than once.
|
20
38
|
|
21
39
|
Returns:
|
22
|
-
A
|
40
|
+
A callable that generates a machine matrix when called with a
|
41
|
+
random.Random instance.
|
23
42
|
"""
|
24
|
-
rng = rng or np.random.default_rng()
|
25
|
-
if duration_range[0] > duration_range[1]:
|
26
|
-
raise ValidationError(
|
27
|
-
"The lower bound of the duration range must be less than or equal "
|
28
|
-
"to the upper bound."
|
29
|
-
)
|
30
|
-
if num_jobs <= 0:
|
31
|
-
raise ValidationError("The number of jobs must be greater than 0.")
|
32
|
-
if num_machines <= 0:
|
33
|
-
raise ValidationError("The number of machines must be greater than 0.")
|
34
43
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
def generator(
|
45
|
+
rng: random.Random,
|
46
|
+
) -> list[list[list[int]]]:
|
47
|
+
num_jobs, num_machines = size_selector(rng)
|
48
|
+
seed_for_np = rng.randint(0, 2**16 - 1)
|
49
|
+
numpy_rng = np.random.default_rng(seed_for_np)
|
50
|
+
if with_recirculation:
|
51
|
+
return generate_machine_matrix_with_recirculation(
|
52
|
+
num_jobs, num_machines, numpy_rng
|
53
|
+
).tolist()
|
54
|
+
|
55
|
+
return generate_machine_matrix_without_recirculation(
|
56
|
+
num_jobs, num_machines, numpy_rng
|
57
|
+
).tolist()
|
58
|
+
|
59
|
+
return generator
|
41
60
|
|
42
61
|
|
43
62
|
def generate_machine_matrix_with_recirculation(
|
@@ -51,8 +70,8 @@ def generate_machine_matrix_with_recirculation(
|
|
51
70
|
rng: A numpy random number generator.
|
52
71
|
|
53
72
|
Returns:
|
54
|
-
A machine matrix with recirculation with shape (
|
55
|
-
|
73
|
+
A machine matrix with recirculation with shape (num_jobs,
|
74
|
+
num_machines).
|
56
75
|
"""
|
57
76
|
rng = rng or np.random.default_rng()
|
58
77
|
if num_jobs <= 0:
|
@@ -62,7 +81,7 @@ def generate_machine_matrix_with_recirculation(
|
|
62
81
|
num_machines_is_correct = False
|
63
82
|
while not num_machines_is_correct:
|
64
83
|
machine_matrix: np.ndarray = rng.integers(
|
65
|
-
0, num_machines, size=(
|
84
|
+
0, num_machines, size=(num_jobs, num_machines), dtype=np.int32
|
66
85
|
)
|
67
86
|
num_machines_is_correct = (
|
68
87
|
len(np.unique(machine_matrix)) == num_machines
|
@@ -100,31 +119,3 @@ def generate_machine_matrix_without_recirculation(
|
|
100
119
|
# Shuffle the columns:
|
101
120
|
machine_matrix = np.apply_along_axis(rng.permutation, 1, machine_matrix)
|
102
121
|
return machine_matrix
|
103
|
-
|
104
|
-
|
105
|
-
if __name__ == "__main__":
|
106
|
-
|
107
|
-
NUM_JOBS = 3
|
108
|
-
NUM_MACHINES = 3
|
109
|
-
DURATION_RANGE = (1, 10)
|
110
|
-
|
111
|
-
duration_matrix = generate_duration_matrix(
|
112
|
-
num_jobs=NUM_JOBS,
|
113
|
-
num_machines=NUM_MACHINES,
|
114
|
-
duration_range=DURATION_RANGE,
|
115
|
-
)
|
116
|
-
print(duration_matrix)
|
117
|
-
|
118
|
-
machine_matrix_with_recirculation = (
|
119
|
-
generate_machine_matrix_with_recirculation(
|
120
|
-
num_jobs=NUM_JOBS, num_machines=NUM_MACHINES
|
121
|
-
)
|
122
|
-
)
|
123
|
-
print(machine_matrix_with_recirculation)
|
124
|
-
|
125
|
-
machine_matrix_without_recirculation = (
|
126
|
-
generate_machine_matrix_without_recirculation(
|
127
|
-
num_jobs=NUM_JOBS, num_machines=NUM_MACHINES
|
128
|
-
)
|
129
|
-
)
|
130
|
-
print(machine_matrix_without_recirculation)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Generator
|
3
|
+
import random
|
4
|
+
|
5
|
+
from job_shop_lib import JobShopInstance
|
6
|
+
|
7
|
+
|
8
|
+
def modular_instance_generator(
|
9
|
+
machine_matrix_creator: Callable[
|
10
|
+
[random.Random], list[list[list[int]]] | list[list[int]]
|
11
|
+
],
|
12
|
+
duration_matrix_creator: Callable[
|
13
|
+
[list[list[list[int]]] | list[list[int]], random.Random],
|
14
|
+
list[list[int]],
|
15
|
+
],
|
16
|
+
*,
|
17
|
+
name_creator: Callable[[int], str] = lambda x: f"generated_instance_{x}",
|
18
|
+
release_dates_matrix_creator: (
|
19
|
+
Callable[
|
20
|
+
[list[list[int]], random.Random],
|
21
|
+
list[list[int]],
|
22
|
+
]
|
23
|
+
| None
|
24
|
+
) = None,
|
25
|
+
deadlines_matrix_creator: (
|
26
|
+
Callable[[list[list[int]], random.Random], list[list[int | None]]]
|
27
|
+
| None
|
28
|
+
) = None,
|
29
|
+
due_dates_matrix_creator: (
|
30
|
+
Callable[[list[list[int]], random.Random], list[list[int | None]]]
|
31
|
+
| None
|
32
|
+
) = None,
|
33
|
+
seed: int | None = None,
|
34
|
+
) -> Generator[JobShopInstance, None, None]:
|
35
|
+
"""Creates a generator function that produces job shop instances using
|
36
|
+
the provided components.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
machine_matrix_creator:
|
40
|
+
A callable that creates a machine matrix.
|
41
|
+
duration_matrix_creator:
|
42
|
+
A callable that creates a duration matrix.
|
43
|
+
name_creator:
|
44
|
+
A callable that generates unique names for instances.
|
45
|
+
release_dates_matrix_creator:
|
46
|
+
An optional callable that generates release dates for jobs.
|
47
|
+
deadlines_matrix_creator:
|
48
|
+
An optional callable that generates deadlines for jobs.
|
49
|
+
due_dates_matrix_creator:
|
50
|
+
An optional callable that generates due dates for jobs.
|
51
|
+
seed:
|
52
|
+
An optional seed for random number generation.
|
53
|
+
|
54
|
+
Yields:
|
55
|
+
JobShopInstance:
|
56
|
+
A generated job shop instance created using the generated matrices.
|
57
|
+
|
58
|
+
Example:
|
59
|
+
|
60
|
+
>>> from job_shop_lib.generation import (
|
61
|
+
... get_default_machine_matrix_creator,
|
62
|
+
... get_default_duration_matrix_creator,
|
63
|
+
... modular_instance_generator,
|
64
|
+
... )
|
65
|
+
>>> machine_matrix_gen = get_default_machine_matrix_creator(
|
66
|
+
... size_selector=lambda rng: (3, 3),
|
67
|
+
... with_recirculation=False,
|
68
|
+
... )
|
69
|
+
>>> duration_matrix_gen = get_default_duration_matrix_creator(
|
70
|
+
... duration_range=(1, 10),
|
71
|
+
... )
|
72
|
+
>>> instance_gen = modular_instance_generator(
|
73
|
+
... machine_matrix_creator=machine_matrix_gen,
|
74
|
+
... duration_matrix_creator=duration_matrix_gen,
|
75
|
+
... seed=42,
|
76
|
+
... )
|
77
|
+
>>> instance = next(instance_gen)
|
78
|
+
>>> print(instance)
|
79
|
+
JobShopInstance(name=generated_instance_0, num_jobs=3, num_machines=3)
|
80
|
+
>>> print(instance.duration_matrix_array)
|
81
|
+
[[ 5. 6. 4.]
|
82
|
+
[ 5. 7. 10.]
|
83
|
+
[ 9. 9. 5.]]
|
84
|
+
|
85
|
+
.. versionadded:: 1.7.0
|
86
|
+
"""
|
87
|
+
rng = random.Random(seed)
|
88
|
+
i = 0
|
89
|
+
while True:
|
90
|
+
machine_matrix = machine_matrix_creator(rng)
|
91
|
+
duration_matrix = duration_matrix_creator(machine_matrix, rng)
|
92
|
+
release_dates = (
|
93
|
+
release_dates_matrix_creator(duration_matrix, rng)
|
94
|
+
if release_dates_matrix_creator
|
95
|
+
else None
|
96
|
+
)
|
97
|
+
deadlines = (
|
98
|
+
deadlines_matrix_creator(duration_matrix, rng)
|
99
|
+
if deadlines_matrix_creator
|
100
|
+
else None
|
101
|
+
)
|
102
|
+
due_dates = (
|
103
|
+
due_dates_matrix_creator(duration_matrix, rng)
|
104
|
+
if due_dates_matrix_creator
|
105
|
+
else None
|
106
|
+
)
|
107
|
+
instance = JobShopInstance.from_matrices(
|
108
|
+
duration_matrix,
|
109
|
+
machine_matrix,
|
110
|
+
name=name_creator(i),
|
111
|
+
release_dates_matrix=release_dates,
|
112
|
+
deadlines_matrix=deadlines,
|
113
|
+
due_dates_matrix=due_dates,
|
114
|
+
)
|
115
|
+
i += 1
|
116
|
+
yield instance
|
@@ -0,0 +1,160 @@
|
|
1
|
+
from typing import Sequence, Callable
|
2
|
+
import random
|
3
|
+
|
4
|
+
from job_shop_lib.exceptions import ValidationError
|
5
|
+
|
6
|
+
|
7
|
+
ReleaseDateStrategy = Callable[[random.Random, int], int]
|
8
|
+
|
9
|
+
|
10
|
+
def create_release_dates_matrix(
|
11
|
+
duration_matrix: list[list[int]],
|
12
|
+
strategy: ReleaseDateStrategy | None = None,
|
13
|
+
rng: random.Random | None = None,
|
14
|
+
) -> list[list[int]]:
|
15
|
+
"""Generate per-operation release dates for ragged job durations.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
duration_matrix:
|
19
|
+
Ragged list of per-job operation durations.
|
20
|
+
strategy:
|
21
|
+
Callable implementing the release date policy. If ``None``
|
22
|
+
a default mixed strategy (alpha=0.7, beta=0.3) is built using the
|
23
|
+
computed horizon proxy.
|
24
|
+
rng:
|
25
|
+
Optional numpy random generator (one will be created if omitted).
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
A ragged list mirroring ``duration_matrix`` structure with per-
|
29
|
+
operation release dates.
|
30
|
+
|
31
|
+
.. versionadded:: 1.7.0
|
32
|
+
"""
|
33
|
+
rng = rng or random.Random()
|
34
|
+
|
35
|
+
num_jobs = len(duration_matrix)
|
36
|
+
if num_jobs == 0:
|
37
|
+
return []
|
38
|
+
|
39
|
+
if strategy is None:
|
40
|
+
horizon_proxy = compute_horizon_proxy(duration_matrix)
|
41
|
+
strategy = get_mixed_release_date_strategy(0.7, 0.3, horizon_proxy)
|
42
|
+
|
43
|
+
release_dates_matrix: list[list[int]] = []
|
44
|
+
for job_durations in duration_matrix:
|
45
|
+
job_release_dates: list[int] = []
|
46
|
+
cumulative_previous_duration = 0
|
47
|
+
for duration_value in job_durations:
|
48
|
+
release_date_value = strategy(rng, cumulative_previous_duration)
|
49
|
+
job_release_dates.append(release_date_value)
|
50
|
+
cumulative_previous_duration += int(duration_value)
|
51
|
+
release_dates_matrix.append(job_release_dates)
|
52
|
+
|
53
|
+
return release_dates_matrix
|
54
|
+
|
55
|
+
|
56
|
+
def compute_horizon_proxy(duration_matrix: Sequence[Sequence[int]]) -> int:
|
57
|
+
"""Compute the horizon proxy used previously for the mixed strategy.
|
58
|
+
|
59
|
+
It is defined as: round(total_duration / avg_operations_per_job)
|
60
|
+
with protections against division by zero.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
duration_matrix:
|
64
|
+
Ragged list of per-job operation durations.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
The computed horizon proxy.
|
68
|
+
|
69
|
+
.. seealso::
|
70
|
+
:meth:`job_shop_lib.JobShopInstance.duration_matrix`
|
71
|
+
|
72
|
+
.. versionadded:: 1.7.0
|
73
|
+
"""
|
74
|
+
num_jobs = len(duration_matrix)
|
75
|
+
if num_jobs == 0:
|
76
|
+
return 0
|
77
|
+
total_duration = sum(sum(job) for job in duration_matrix)
|
78
|
+
total_operations = sum(len(job) for job in duration_matrix)
|
79
|
+
avg_ops_per_job = total_operations / max(1, num_jobs)
|
80
|
+
return round(total_duration / max(1, avg_ops_per_job))
|
81
|
+
|
82
|
+
|
83
|
+
def get_independent_release_date_strategy(
|
84
|
+
max_release_time: int,
|
85
|
+
) -> ReleaseDateStrategy:
|
86
|
+
"""Factory for an independent (pure random) release date strategy.
|
87
|
+
|
88
|
+
The release date is drawn uniformly at random in the interval
|
89
|
+
``[0, max_release_time]``.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
max_release_time:
|
93
|
+
Inclusive upper bound for the random value.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
A callable implementing the independent release date strategy.
|
97
|
+
|
98
|
+
.. versionadded:: 1.7.0
|
99
|
+
"""
|
100
|
+
if max_release_time < 0:
|
101
|
+
raise ValidationError("'max_release_time' must be >= 0.")
|
102
|
+
|
103
|
+
def _strategy(rng: random.Random, unused_cumulative_prev: int) -> int:
|
104
|
+
return int(rng.randint(0, max_release_time))
|
105
|
+
|
106
|
+
return _strategy
|
107
|
+
|
108
|
+
|
109
|
+
def get_cumulative_release_date_strategy(
|
110
|
+
maximum_slack: int = 0,
|
111
|
+
) -> ReleaseDateStrategy:
|
112
|
+
"""Factory for a cumulative strategy allowing forward slack.
|
113
|
+
|
114
|
+
The release date is the cumulative previous processing time plus a
|
115
|
+
random slack in ``[0, maximum_slack]``.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
maximum_slack:
|
119
|
+
Non-negative integer defining the maximum forward slack to add.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
A callable implementing the cumulative release date strategy.
|
123
|
+
"""
|
124
|
+
if maximum_slack < 0:
|
125
|
+
raise ValidationError("'maximum_slack' must be >= 0.")
|
126
|
+
|
127
|
+
def _strategy(rng: random.Random, cumulative_prev: int) -> int:
|
128
|
+
return cumulative_prev + rng.randint(0, maximum_slack)
|
129
|
+
|
130
|
+
return _strategy
|
131
|
+
|
132
|
+
|
133
|
+
def get_mixed_release_date_strategy(
|
134
|
+
alpha: float,
|
135
|
+
beta: float,
|
136
|
+
horizon_proxy: int,
|
137
|
+
) -> ReleaseDateStrategy:
|
138
|
+
"""Factory for the mixed heuristic strategy.
|
139
|
+
|
140
|
+
release_date = alpha * cumulative_previous + U(0, beta * horizon_proxy)
|
141
|
+
|
142
|
+
Args:
|
143
|
+
alpha:
|
144
|
+
Weight for the proportional cumulative component.
|
145
|
+
beta:
|
146
|
+
Weight for the random component upper bound.
|
147
|
+
horizon_proxy:
|
148
|
+
Non-negative proxy for the time horizon (e.g. derived
|
149
|
+
from durations to scale the random component consistently).
|
150
|
+
"""
|
151
|
+
if horizon_proxy < 0:
|
152
|
+
raise ValidationError("'horizon_proxy' must be >= 0.")
|
153
|
+
|
154
|
+
random_component_upper = round(beta * horizon_proxy)
|
155
|
+
|
156
|
+
def _strategy(rng: random.Random, cumulative_prev: int) -> int:
|
157
|
+
random_component = rng.randint(0, random_component_upper)
|
158
|
+
return round(alpha * cumulative_prev) + random_component
|
159
|
+
|
160
|
+
return _strategy
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import random
|
2
|
+
|
3
|
+
|
4
|
+
def range_size_selector(
|
5
|
+
rng: random.Random,
|
6
|
+
num_jobs_range: tuple[int, int] = (10, 20),
|
7
|
+
num_machines_range: tuple[int, int] = (5, 10),
|
8
|
+
allow_less_jobs_than_machines: bool = True,
|
9
|
+
) -> tuple[int, int]:
|
10
|
+
"""Selects the number of jobs and machines based on the provided ranges
|
11
|
+
and constraints.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
rng:
|
15
|
+
A ``random.Random`` instance.
|
16
|
+
num_jobs_range:
|
17
|
+
A tuple specifying the inclusive range for the number of jobs.
|
18
|
+
num_machines_range:
|
19
|
+
A tuple specifying the inclusive range for the number of machines.
|
20
|
+
allow_less_jobs_than_machines:
|
21
|
+
If ``False``, ensures that the number of jobs is not less than the
|
22
|
+
number of machines.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
A tuple containing the selected number of jobs and machines.
|
26
|
+
"""
|
27
|
+
num_jobs = rng.randint(num_jobs_range[0], num_jobs_range[1])
|
28
|
+
|
29
|
+
min_num_machines, max_num_machines = num_machines_range
|
30
|
+
if not allow_less_jobs_than_machines:
|
31
|
+
# Cap the maximum machines to the sampled number of jobs.
|
32
|
+
max_num_machines = min(max_num_machines, num_jobs)
|
33
|
+
# If min > capped max, collapse interval so we return a valid value
|
34
|
+
# (e.g. jobs=3, range=(5,10)).
|
35
|
+
if min_num_machines > max_num_machines:
|
36
|
+
min_num_machines = max_num_machines
|
37
|
+
num_machines = rng.randint(min_num_machines, max_num_machines)
|
38
|
+
|
39
|
+
return num_jobs, num_machines
|
40
|
+
|
41
|
+
|
42
|
+
def choice_size_selector(
|
43
|
+
rng: random.Random,
|
44
|
+
options: list[tuple[int, int]],
|
45
|
+
) -> tuple[int, int]:
|
46
|
+
"""Selects the number of jobs and machines from a list of options.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
rng:
|
50
|
+
A ``random.Random`` instance.
|
51
|
+
options:
|
52
|
+
A list of tuples, where each tuple contains a pair of integers
|
53
|
+
representing the number of jobs and machines.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
A tuple containing the selected number of jobs and machines.
|
57
|
+
"""
|
58
|
+
return rng.choice(options)
|
@@ -31,12 +31,16 @@ contribute!
|
|
31
31
|
swap_random_operations
|
32
32
|
ObjectiveFunction
|
33
33
|
get_makespan_with_penalties_objective
|
34
|
+
compute_penalty_for_deadlines
|
35
|
+
compute_penalty_for_due_dates
|
34
36
|
|
35
37
|
"""
|
36
38
|
|
37
39
|
from job_shop_lib.metaheuristics._objective_functions import (
|
38
40
|
ObjectiveFunction,
|
39
41
|
get_makespan_with_penalties_objective,
|
42
|
+
compute_penalty_for_deadlines,
|
43
|
+
compute_penalty_for_due_dates,
|
40
44
|
)
|
41
45
|
from job_shop_lib.metaheuristics._neighbor_generators import (
|
42
46
|
NeighborGenerator,
|
@@ -58,4 +62,6 @@ __all__ = [
|
|
58
62
|
"swap_random_operations",
|
59
63
|
"ObjectiveFunction",
|
60
64
|
"get_makespan_with_penalties_objective",
|
65
|
+
"compute_penalty_for_deadlines",
|
66
|
+
"compute_penalty_for_due_dates",
|
61
67
|
]
|
@@ -45,29 +45,70 @@ def get_makespan_with_penalties_objective(
|
|
45
45
|
|
46
46
|
def objective(schedule: Schedule) -> float:
|
47
47
|
makespan = schedule.makespan()
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
penalty =
|
55
|
-
for machine_schedule in schedule.schedule:
|
56
|
-
for scheduled_op in machine_schedule:
|
57
|
-
op = scheduled_op.operation
|
58
|
-
# Deadline (hard) penalty
|
59
|
-
if (
|
60
|
-
op.deadline is not None
|
61
|
-
and scheduled_op.end_time > op.deadline
|
62
|
-
):
|
63
|
-
penalty += deadline_penalty_factor
|
64
|
-
# Due date (soft) penalty
|
65
|
-
if (
|
66
|
-
op.due_date is not None
|
67
|
-
and scheduled_op.end_time > op.due_date
|
68
|
-
):
|
69
|
-
penalty += due_date_penalty_factor
|
48
|
+
penalty_for_deadlines = compute_penalty_for_deadlines(
|
49
|
+
schedule, deadline_penalty_factor
|
50
|
+
)
|
51
|
+
penalty_for_due_dates = compute_penalty_for_due_dates(
|
52
|
+
schedule, due_date_penalty_factor
|
53
|
+
)
|
54
|
+
penalty = penalty_for_deadlines + penalty_for_due_dates
|
70
55
|
|
71
56
|
return makespan + penalty
|
72
57
|
|
73
58
|
return objective
|
59
|
+
|
60
|
+
|
61
|
+
def compute_penalty_for_deadlines(
|
62
|
+
schedule: Schedule, penalty_per_violation: float
|
63
|
+
) -> float:
|
64
|
+
"""Compute the total penalty for deadline violations in a schedule.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
schedule:
|
68
|
+
The schedule to evaluate.
|
69
|
+
penalty_per_violation:
|
70
|
+
The penalty to apply for each operation that
|
71
|
+
finishes after its deadline.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
The total penalty for deadline violations.
|
75
|
+
"""
|
76
|
+
if not schedule.instance.has_deadlines or penalty_per_violation == 0:
|
77
|
+
return 0.0
|
78
|
+
|
79
|
+
penalty = 0.0
|
80
|
+
for machine_schedule in schedule.schedule:
|
81
|
+
for scheduled_op in machine_schedule:
|
82
|
+
op = scheduled_op.operation
|
83
|
+
if op.deadline is not None and scheduled_op.end_time > op.deadline:
|
84
|
+
penalty += penalty_per_violation
|
85
|
+
|
86
|
+
return penalty
|
87
|
+
|
88
|
+
|
89
|
+
def compute_penalty_for_due_dates(
|
90
|
+
schedule: Schedule, penalty_per_violation: float
|
91
|
+
) -> float:
|
92
|
+
"""Compute the total penalty for due date violations in a schedule.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
schedule:
|
96
|
+
The schedule to evaluate.
|
97
|
+
penalty_per_violation:
|
98
|
+
The penalty to apply for each operation that
|
99
|
+
finishes after its due date.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
The total penalty for due date violations.
|
103
|
+
"""
|
104
|
+
if not schedule.instance.has_due_dates or penalty_per_violation == 0:
|
105
|
+
return 0.0
|
106
|
+
|
107
|
+
penalty = 0.0
|
108
|
+
for machine_schedule in schedule.schedule:
|
109
|
+
for scheduled_op in machine_schedule:
|
110
|
+
op = scheduled_op.operation
|
111
|
+
if op.due_date is not None and scheduled_op.end_time > op.due_date:
|
112
|
+
penalty += penalty_per_violation
|
113
|
+
|
114
|
+
return penalty
|
@@ -14,11 +14,14 @@
|
|
14
14
|
RewardObserver
|
15
15
|
MakespanReward
|
16
16
|
IdleTimeReward
|
17
|
+
RewardWithPenalties
|
17
18
|
RenderConfig
|
18
19
|
add_padding
|
19
20
|
create_edge_type_dict
|
20
21
|
map_values
|
21
22
|
get_optimal_actions
|
23
|
+
get_deadline_violation_penalty
|
24
|
+
get_due_date_violation_penalty
|
22
25
|
|
23
26
|
"""
|
24
27
|
|
@@ -32,6 +35,7 @@ from job_shop_lib.reinforcement_learning._reward_observers import (
|
|
32
35
|
RewardObserver,
|
33
36
|
MakespanReward,
|
34
37
|
IdleTimeReward,
|
38
|
+
RewardWithPenalties,
|
35
39
|
)
|
36
40
|
|
37
41
|
from job_shop_lib.reinforcement_learning._utils import (
|
@@ -39,6 +43,8 @@ from job_shop_lib.reinforcement_learning._utils import (
|
|
39
43
|
create_edge_type_dict,
|
40
44
|
map_values,
|
41
45
|
get_optimal_actions,
|
46
|
+
get_deadline_violation_penalty,
|
47
|
+
get_due_date_violation_penalty,
|
42
48
|
)
|
43
49
|
|
44
50
|
from job_shop_lib.reinforcement_learning._single_job_shop_graph_env import (
|
@@ -63,9 +69,12 @@ __all__ = [
|
|
63
69
|
"RewardObserver",
|
64
70
|
"MakespanReward",
|
65
71
|
"IdleTimeReward",
|
72
|
+
"RewardWithPenalties",
|
66
73
|
"RenderConfig",
|
67
74
|
"add_padding",
|
68
75
|
"create_edge_type_dict",
|
69
76
|
"map_values",
|
70
77
|
"get_optimal_actions",
|
78
|
+
"get_deadline_violation_penalty",
|
79
|
+
"get_due_date_violation_penalty",
|
71
80
|
]
|
@@ -1,6 +1,9 @@
|
|
1
1
|
"""Rewards functions are defined as `DispatcherObervers` and are used to
|
2
2
|
calculate the reward for a given state."""
|
3
3
|
|
4
|
+
from collections.abc import Callable
|
5
|
+
|
6
|
+
from job_shop_lib.exceptions import ValidationError
|
4
7
|
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
5
8
|
from job_shop_lib import ScheduledOperation
|
6
9
|
|
@@ -83,3 +86,76 @@ class IdleTimeReward(RewardObserver):
|
|
83
86
|
|
84
87
|
reward = -idle_time
|
85
88
|
self.rewards.append(reward)
|
89
|
+
|
90
|
+
|
91
|
+
class RewardWithPenalties(RewardObserver):
|
92
|
+
"""Reward function that adds penalties to another reward function.
|
93
|
+
|
94
|
+
The reward is calculated as the sum of the reward from another reward
|
95
|
+
function and a penalty for each constraint violation (due dates and
|
96
|
+
deadlines).
|
97
|
+
|
98
|
+
Attributes:
|
99
|
+
base_reward_observer:
|
100
|
+
The base reward observer to use for calculating the reward.
|
101
|
+
penalty_function:
|
102
|
+
A function that takes a scheduled operation and the dispatcher as
|
103
|
+
input and returns the penalty for that operation.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
dispatcher:
|
107
|
+
The dispatcher to observe.
|
108
|
+
base_reward_observer:
|
109
|
+
The base reward observer to use for calculating the reward. It
|
110
|
+
must use the same dispatcher as this reward observer. If it is
|
111
|
+
subscribed to the dispatcher, it will be unsubscribed.
|
112
|
+
penalty_function:
|
113
|
+
A function that takes a scheduled operation and the
|
114
|
+
dispatcher as input and returns the penalty for that operation.
|
115
|
+
subscribe:
|
116
|
+
Whether to subscribe to the dispatcher upon initialization.
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
ValidationError:
|
120
|
+
If the base reward observer does not use the same dispatcher as
|
121
|
+
this reward observer.
|
122
|
+
|
123
|
+
.. versionadded:: 1.7.0
|
124
|
+
|
125
|
+
.. seealso::
|
126
|
+
The following functions (along with ``functools.partial``) can be
|
127
|
+
used to create penalty functions:
|
128
|
+
|
129
|
+
- :func:`~job_shop_lib.reinforcement_learning.get_deadline_violation_penalty`
|
130
|
+
- :func:`~job_shop_lib.reinforcement_learning.get_due_date_violation_penalty`
|
131
|
+
|
132
|
+
""" # noqa: E501
|
133
|
+
|
134
|
+
def __init__(
|
135
|
+
self,
|
136
|
+
dispatcher: Dispatcher,
|
137
|
+
*,
|
138
|
+
base_reward_observer: RewardObserver,
|
139
|
+
penalty_function: Callable[[ScheduledOperation, Dispatcher], float],
|
140
|
+
subscribe: bool = True,
|
141
|
+
) -> None:
|
142
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
143
|
+
self.base_reward_observer = base_reward_observer
|
144
|
+
self.penalty_function = penalty_function
|
145
|
+
if base_reward_observer.dispatcher is not dispatcher:
|
146
|
+
raise ValidationError(
|
147
|
+
"The base reward observer must use the same "
|
148
|
+
"dispatcher as this reward observer."
|
149
|
+
)
|
150
|
+
if base_reward_observer in dispatcher.subscribers:
|
151
|
+
dispatcher.unsubscribe(base_reward_observer)
|
152
|
+
|
153
|
+
def reset(self) -> None:
|
154
|
+
super().reset()
|
155
|
+
self.base_reward_observer.reset()
|
156
|
+
|
157
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
158
|
+
self.base_reward_observer.update(scheduled_operation)
|
159
|
+
base_reward = self.base_reward_observer.last_reward
|
160
|
+
penalty = self.penalty_function(scheduled_operation, self.dispatcher)
|
161
|
+
self.rewards.append(base_reward - penalty)
|
@@ -5,8 +5,9 @@ from typing import TypeVar, Any
|
|
5
5
|
import numpy as np
|
6
6
|
from numpy.typing import NDArray
|
7
7
|
|
8
|
+
from job_shop_lib import ScheduledOperation
|
8
9
|
from job_shop_lib.exceptions import ValidationError
|
9
|
-
from job_shop_lib.dispatching import OptimalOperationsObserver
|
10
|
+
from job_shop_lib.dispatching import OptimalOperationsObserver, Dispatcher
|
10
11
|
|
11
12
|
T = TypeVar("T", bound=np.number)
|
12
13
|
|
@@ -193,7 +194,65 @@ def get_optimal_actions(
|
|
193
194
|
return optimal_actions
|
194
195
|
|
195
196
|
|
196
|
-
|
197
|
-
|
197
|
+
def get_deadline_violation_penalty(
|
198
|
+
scheduled_operation: ScheduledOperation,
|
199
|
+
unused_dispatcher: Dispatcher,
|
200
|
+
deadline_penalty_factor: float = 10_000,
|
201
|
+
) -> float:
|
202
|
+
"""Compute the penalty for a scheduled operation that violates its
|
203
|
+
deadline.
|
198
204
|
|
199
|
-
|
205
|
+
Args:
|
206
|
+
scheduled_operation:
|
207
|
+
The scheduled operation to evaluate.
|
208
|
+
unused_dispatcher:
|
209
|
+
This argument is unused but included for compatibility with the
|
210
|
+
penalty function signature.
|
211
|
+
deadline_penalty_factor:
|
212
|
+
Cost added for each operation that
|
213
|
+
finishes after its deadline. Defaults to 10_000.
|
214
|
+
Returns:
|
215
|
+
The penalty for the scheduled operation if it violates its deadline,
|
216
|
+
otherwise 0.
|
217
|
+
|
218
|
+
.. versionadded:: 1.7.0
|
219
|
+
"""
|
220
|
+
if (
|
221
|
+
scheduled_operation.operation.deadline is not None
|
222
|
+
and scheduled_operation.end_time
|
223
|
+
> scheduled_operation.operation.deadline
|
224
|
+
):
|
225
|
+
return deadline_penalty_factor
|
226
|
+
return 0.0
|
227
|
+
|
228
|
+
|
229
|
+
def get_due_date_violation_penalty(
|
230
|
+
scheduled_operation: ScheduledOperation,
|
231
|
+
unused_dispatcher: Dispatcher,
|
232
|
+
due_date_penalty_factor: float = 100,
|
233
|
+
) -> float:
|
234
|
+
"""Compute the penalty for a scheduled operation that violates its
|
235
|
+
due date.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
scheduled_operation:
|
239
|
+
The scheduled operation to evaluate.
|
240
|
+
unused_dispatcher:
|
241
|
+
This argument is unused but included for compatibility with the
|
242
|
+
penalty function signature.
|
243
|
+
due_date_penalty_factor:
|
244
|
+
Cost added for each operation that
|
245
|
+
finishes after its due date. Defaults to 100.
|
246
|
+
Returns:
|
247
|
+
The penalty for the scheduled operation if it violates its due date,
|
248
|
+
otherwise 0.
|
249
|
+
|
250
|
+
.. versionadded:: 1.7.0
|
251
|
+
"""
|
252
|
+
if (
|
253
|
+
scheduled_operation.operation.due_date is not None
|
254
|
+
and scheduled_operation.end_time
|
255
|
+
> scheduled_operation.operation.due_date
|
256
|
+
):
|
257
|
+
return due_date_penalty_factor
|
258
|
+
return 0.0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.7.0
|
4
4
|
Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
|
5
5
|
License: MIT
|
6
6
|
Author: Pabloo22
|
@@ -45,13 +45,13 @@ It follows a modular design, allowing users to easily extend the library with ne
|
|
45
45
|
We support multiple solvers, including:
|
46
46
|
- **Constraint Programming**: Based on OR-Tools' CP-SAT solver. It supports **release dates, deadlines, and due dates.** See the ["Solving the Problem" tutorial](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/02-Solving-the-Problem.ipynb) for an example.
|
47
47
|
- **Dispatching Rules**: A set of predefined rules and the ability to create custom ones. They support arbitrary **setup times, machine breakdowns, release dates, deadlines, and due dates**. See the [following example](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/03-Dispatching-Rules.ipynb). You can also create videos or GIFs of the scheduling process. For creating GIFs or videos, see the [Save Gif example](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/04-Save-Gif.ipynb).
|
48
|
-
- **Metaheuristics**: Currently, we have a **simulated annealing** implementation that supports **release dates, deadlines, and due dates**. We also support arbitrary neighborhood search strategies, including swapping operations in the critical path as described in the paper "Job Shop Scheduling by Simulated Annealing" by van Laarhoven et al. (1992); and energy functions. See our [simulated annealing tutorial](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/
|
48
|
+
- **Metaheuristics**: Currently, we have a **simulated annealing** implementation that supports **release dates, deadlines, and due dates**. We also support arbitrary neighborhood search strategies, including swapping operations in the critical path as described in the paper "Job Shop Scheduling by Simulated Annealing" by van Laarhoven et al. (1992); and energy functions. See our [simulated annealing tutorial](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/04-Simulated-Annealing.ipynb).
|
49
49
|
- **Reinforcement Learning**: Two Gymnasium environments for solving the problem with **graph neural networks** (GNNs) or any other method. The environments support **setup times, release dates, deadlines, and due dates.** We're currently building a tutorial on how to use them.
|
50
50
|
|
51
51
|
We also provide useful utilities, data structures, and visualization functions:
|
52
52
|
- **Intuitive Data Structures**: Easily create, manage, and manipulate job shop instances and solutions with user-friendly data structures. See [Getting Started](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/00-Getting-Started.ipynb) and [How Solutions are Represented](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/01-How-Solutions-are-Represented.ipynb).
|
53
53
|
- **Benchmark Instances**: Load well-known benchmark instances directly from the library without manual downloading. See [Load Benchmark Instances](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/05-Load-Benchmark-Instances.ipynb).
|
54
|
-
- **Random Instance Generation**: Create random instances with customizable sizes and properties. See [`
|
54
|
+
- **Random Instance Generation**: Create random instances with customizable sizes and properties. See [`this tutorial`](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/03-Generating-New-Instances.ipynb).
|
55
55
|
- **Gantt Charts**: Visualize final schedules and how they are created iteratively by dispatching rule solvers or sequences of scheduling decisions with GIFs or videos.
|
56
56
|
- **Graph Representations**: Represent and visualize instances as disjunctive graphs or agent-task graphs (introduced in the ScheduleNet paper). Build your own custom graphs with the `JobShopGraph` class. See the [Disjunctive Graphs](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/04-Disjunctive-Graphs.ipynb) and [Resource Task Graphs](https://job-shop-lib.readthedocs.io/en/stable/examples/07-Resource-Task-Graph.html) examples.
|
57
57
|
|
@@ -1,11 +1,11 @@
|
|
1
|
-
job_shop_lib/__init__.py,sha256=
|
1
|
+
job_shop_lib/__init__.py,sha256=2tXYQ5nb_6v3J7mpsXC1vRMI2khj2BTRijff-8j6ZLY,639
|
2
2
|
job_shop_lib/_base_solver.py,sha256=8CCSiA2-DegCKRXhMw7yYyI8iPauTSuLku2LQ8dU-9U,1382
|
3
|
-
job_shop_lib/_job_shop_instance.py,sha256=
|
3
|
+
job_shop_lib/_job_shop_instance.py,sha256=tQDJwKCgimzAcEK6vdAAePKiF_5AvOmzHZcOptfVkd0,25403
|
4
4
|
job_shop_lib/_operation.py,sha256=JI5WjvRXNBeSpPOv3ZwSrUJ4jsVDJYKfMaDHYOaFYts,5945
|
5
5
|
job_shop_lib/_schedule.py,sha256=jvidw6iIh05QGe2OeA6JkiQuCzLoOtgqny8zj95_sGA,18173
|
6
6
|
job_shop_lib/_scheduled_operation.py,sha256=czrGr87EOTlO2NPolIN5CDigeiCzvQEyra5IZPwSFZc,2801
|
7
|
-
job_shop_lib/benchmarking/__init__.py,sha256=
|
8
|
-
job_shop_lib/benchmarking/_load_benchmark.py,sha256
|
7
|
+
job_shop_lib/benchmarking/__init__.py,sha256=M4hGy3PbNoNtcYUAJkGTYfPnk3hFVkhg5BYr6g3iB6Q,3369
|
8
|
+
job_shop_lib/benchmarking/_load_benchmark.py,sha256=5NvDXKvwtff9Vw6y5R2f2I2r2YNekMbQsSt0GXzZeao,3684
|
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
11
|
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=trTQtqSL2F2PXxd9RPnFhxaY8blNcfFUhTdab5QP9VU,12585
|
@@ -21,8 +21,8 @@ job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=0he-j4OlvqtX
|
|
21
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
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=
|
25
|
-
job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=
|
24
|
+
job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=3Z8CA9k4OKKjVcclCnyUrtr7USM7M4rSvp2hhwUir3A,4224
|
25
|
+
job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=Jm6g3LUwVCiXVghSsJ1XyaxfAu5sYT1t8OunfVjHDqc,11451
|
26
26
|
job_shop_lib/dispatching/feature_observers/_factory.py,sha256=NyXYK5A1hXsYEeEqngwVRNAFkevY95DglheeqyfFv8s,3217
|
27
27
|
job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=qbgtMUicQ5FWS-Ql4Izjsj4QrevfOGlWzoJ0JlVSLH0,8668
|
28
28
|
job_shop_lib/dispatching/feature_observers/_is_completed_observer.py,sha256=EYJOyWL8ApUElLucoHnFlt0g2Ior_1yO7Q8V3FU_Qog,3576
|
@@ -37,10 +37,14 @@ job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=c-T6jUXZ2w
|
|
37
37
|
job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=CJ74ujgWXgG8cuULWY6VJkD_b3arTcOjTNLZJTAf8xE,2346
|
38
38
|
job_shop_lib/dispatching/rules/_utils.py,sha256=m5qw4qyfaIvVrkmv51nuhreizr98-cg8AJKt2VTd48w,4603
|
39
39
|
job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
|
40
|
-
job_shop_lib/generation/__init__.py,sha256=
|
40
|
+
job_shop_lib/generation/__init__.py,sha256=6H94ZsuvaRWIFzvDFPvCoaqNTLp_R2RL0DgLl47IC2s,2197
|
41
|
+
job_shop_lib/generation/_duration_matrix.py,sha256=l6T06SGUYQC0BFgQhw2j7RLi7yrK_CLunGuYmAyWglQ,2475
|
41
42
|
job_shop_lib/generation/_general_instance_generator.py,sha256=b_tnyP4H_buoN7b6lKQRLvDkeZDdys0mpqS3thB5-SQ,6544
|
42
|
-
job_shop_lib/generation/_instance_generator.py,sha256=
|
43
|
-
job_shop_lib/generation/
|
43
|
+
job_shop_lib/generation/_instance_generator.py,sha256=wnXk3dS7TQu5fNPsbkqbjvcuUdaZ2XE0w9ITsSVLiyY,4628
|
44
|
+
job_shop_lib/generation/_machine_matrix.py,sha256=l56h0b1I39_zTm5h0Hnc217V5vmnEB-k8_xoxGL4taw,3928
|
45
|
+
job_shop_lib/generation/_modular_instance_generator.py,sha256=pDwhbCKc85kTUCuQZEKirF-ILnv67Ln618jhTn_ltis,3900
|
46
|
+
job_shop_lib/generation/_release_date_matrix.py,sha256=rv-lcqg-z3uYgsoEKPf1r7iGZsKXf9tnSkvMxEmx-Oc,4981
|
47
|
+
job_shop_lib/generation/_size_selectors.py,sha256=y9jUOjR3hCk0dUU3H_Xce4Pok7X-YVybTuP_GK66CuQ,1969
|
44
48
|
job_shop_lib/graphs/__init__.py,sha256=wlYIiXTuZRE6Kx3K0RpPUoZikzoegBuN2hcdqMODtGk,2433
|
45
49
|
job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
|
46
50
|
job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=vIy_EkQjgQAd5YyJxKAuGf7CLTjgCfhz-fYrObF4DTU,6962
|
@@ -52,19 +56,19 @@ job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py,sha256=-t0T8W-J
|
|
52
56
|
job_shop_lib/graphs/graph_updaters/_graph_updater.py,sha256=j1f7iWsa62GVszK2BPaMxnKBCEGWa9owm8g4VWUje8w,1967
|
53
57
|
job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py,sha256=9NG3pu7Z5h-ZTfX8rRiZbI_NfNQi80h-XUHainshjZY,6064
|
54
58
|
job_shop_lib/graphs/graph_updaters/_utils.py,sha256=sdw2Vo75P9c6Fy-YBlfgpXb9gPwHUluTB1E-9WINm_g,730
|
55
|
-
job_shop_lib/metaheuristics/__init__.py,sha256=
|
59
|
+
job_shop_lib/metaheuristics/__init__.py,sha256=CfuajbzO1J21_FsA1J_6UpODvPSwzi_VNAh7xEEoe_Q,2096
|
56
60
|
job_shop_lib/metaheuristics/_job_shop_annealer.py,sha256=Ty4SLPZh1NrL-XRqU76EeN8fwUdKfqbphqfYEDje1lQ,9195
|
57
61
|
job_shop_lib/metaheuristics/_neighbor_generators.py,sha256=3RePlnYvJdpdhObmf0m_3NhyUM7avfNr4vOZT0PWTRQ,6563
|
58
|
-
job_shop_lib/metaheuristics/_objective_functions.py,sha256=
|
62
|
+
job_shop_lib/metaheuristics/_objective_functions.py,sha256=XovebJbvUyuwgcnS6C8P-hGlMXZ7bFxKOM2He5NUH0Q,3727
|
59
63
|
job_shop_lib/metaheuristics/_simulated_annealing_solver.py,sha256=EMCrFl2zzJubrvCMi5upm8lnUgtBizhZbi4EvbnIsM4,6200
|
60
64
|
job_shop_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
61
|
-
job_shop_lib/reinforcement_learning/__init__.py,sha256=
|
65
|
+
job_shop_lib/reinforcement_learning/__init__.py,sha256=aPB3XqOzRxtpjL5S3kjNXZa7L8Z2OFnO48J5wTc9DbQ,1831
|
62
66
|
job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=6nXw67Tfmim3LqlSuQ9Cfg3mMY-VmbMHuXfyOL90jng,15740
|
63
67
|
job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=ZqN6kzuXbO0BdA1UKrTEHeiHAKzRNIeuH-dBt90ttEc,12914
|
64
|
-
job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=
|
68
|
+
job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=CBQ-QrTs4ymYEqqIsAXnsiEXX5BgakOrT1FweM87l_4,5858
|
65
69
|
job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=MnQYCVpwX4WHiGhYguHziDUrPIrKmXsjOUDoTmuoCBc,16403
|
66
70
|
job_shop_lib/reinforcement_learning/_types_and_constants.py,sha256=6FpuQkZLV2H8_dXmax49OTgAw7dWQcUEWVWWdMLR7bs,1752
|
67
|
-
job_shop_lib/reinforcement_learning/_utils.py,sha256=
|
71
|
+
job_shop_lib/reinforcement_learning/_utils.py,sha256=sxKFulDrt1Aei3etLEgsdFVFRmXKNIhFLoOKOWqIz00,8022
|
68
72
|
job_shop_lib/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
69
73
|
job_shop_lib/visualization/gantt/__init__.py,sha256=xMvuNph6bfwulHYqqklCj_6SUQgRzvC92Yul75F3Zlg,1250
|
70
74
|
job_shop_lib/visualization/gantt/_gantt_chart_creator.py,sha256=FgE4SmKLYKnS7dfTFgnBklWhwGyIo0DKWVkmxusDmp8,8606
|
@@ -73,7 +77,7 @@ job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9G
|
|
73
77
|
job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
|
74
78
|
job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
|
75
79
|
job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
|
76
|
-
job_shop_lib-1.
|
77
|
-
job_shop_lib-1.
|
78
|
-
job_shop_lib-1.
|
79
|
-
job_shop_lib-1.
|
80
|
+
job_shop_lib-1.7.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
81
|
+
job_shop_lib-1.7.0.dist-info/METADATA,sha256=goMjnzDFnjTYw4BEwApgq5gSs9SZtFr98WR8qRakRs0,19151
|
82
|
+
job_shop_lib-1.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
83
|
+
job_shop_lib-1.7.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|