job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__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 +34 -29
- job_shop_lib/_operation.py +4 -2
- job_shop_lib/_schedule.py +11 -11
- job_shop_lib/benchmarking/_load_benchmark.py +3 -3
- job_shop_lib/constraint_programming/_ortools_solver.py +6 -6
- job_shop_lib/dispatching/__init__.py +4 -3
- job_shop_lib/dispatching/_dispatcher.py +19 -19
- job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
- job_shop_lib/dispatching/_factories.py +4 -2
- job_shop_lib/dispatching/_history_observer.py +2 -1
- job_shop_lib/dispatching/_optimal_operations_observer.py +115 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
- job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
- job_shop_lib/dispatching/rules/__init__.py +37 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +50 -20
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
- job_shop_lib/dispatching/rules/_utils.py +9 -8
- job_shop_lib/generation/__init__.py +8 -0
- job_shop_lib/generation/_general_instance_generator.py +42 -64
- job_shop_lib/generation/_instance_generator.py +11 -7
- job_shop_lib/generation/_transformations.py +5 -4
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +7 -7
- job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
- job_shop_lib/graphs/_job_shop_graph.py +17 -13
- job_shop_lib/graphs/_node.py +6 -4
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
- job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
- job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
- job_shop_lib/reinforcement_learning/_utils.py +3 -3
- job_shop_lib/visualization/__init__.py +0 -60
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
- job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
- job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/METADATA +21 -15
- job_shop_lib-1.0.0b2.dist-info/RECORD +70 -0
- job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
- job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/WHEEL +0 -0
@@ -6,6 +6,7 @@ which operations are selected for execution based on certain criteria such as
|
|
6
6
|
shortest processing time, first come first served, etc.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from typing import List, Optional
|
9
10
|
from collections.abc import Callable, Sequence
|
10
11
|
import random
|
11
12
|
|
@@ -88,7 +89,7 @@ def score_based_rule(
|
|
88
89
|
|
89
90
|
|
90
91
|
def score_based_rule_with_tie_breaker(
|
91
|
-
score_functions:
|
92
|
+
score_functions: List[Callable[[Dispatcher], Sequence[int]]],
|
92
93
|
) -> Callable[[Dispatcher], Operation]:
|
93
94
|
"""Creates a dispatching rule based on multiple scoring functions.
|
94
95
|
|
@@ -122,7 +123,7 @@ def score_based_rule_with_tie_breaker(
|
|
122
123
|
# -----------------
|
123
124
|
|
124
125
|
|
125
|
-
def shortest_processing_time_score(dispatcher: Dispatcher) ->
|
126
|
+
def shortest_processing_time_score(dispatcher: Dispatcher) -> List[int]:
|
126
127
|
"""Scores each job based on the duration of the next operation."""
|
127
128
|
num_jobs = dispatcher.instance.num_jobs
|
128
129
|
scores = [0] * num_jobs
|
@@ -131,7 +132,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
|
131
132
|
return scores
|
132
133
|
|
133
134
|
|
134
|
-
def first_come_first_served_score(dispatcher: Dispatcher) ->
|
135
|
+
def first_come_first_served_score(dispatcher: Dispatcher) -> List[int]:
|
135
136
|
"""Scores each job based on the position of the next operation."""
|
136
137
|
num_jobs = dispatcher.instance.num_jobs
|
137
138
|
scores = [0] * num_jobs
|
@@ -153,9 +154,9 @@ class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
|
|
153
154
|
"""
|
154
155
|
|
155
156
|
def __init__(self) -> None:
|
156
|
-
self._duration_observer: DurationObserver
|
157
|
-
self._is_ready_observer: IsReadyObserver
|
158
|
-
self._current_dispatcher: Dispatcher
|
157
|
+
self._duration_observer: Optional[DurationObserver] = None
|
158
|
+
self._is_ready_observer: Optional[IsReadyObserver] = None
|
159
|
+
self._current_dispatcher: Optional[Dispatcher] = None
|
159
160
|
|
160
161
|
def __call__(self, dispatcher: Dispatcher) -> Sequence[int]:
|
161
162
|
"""Scores each job based on the remaining work in the job."""
|
@@ -197,7 +198,7 @@ observer_based_most_work_remaining_rule = score_based_rule(
|
|
197
198
|
)
|
198
199
|
|
199
200
|
|
200
|
-
def most_operations_remaining_score(dispatcher: Dispatcher) ->
|
201
|
+
def most_operations_remaining_score(dispatcher: Dispatcher) -> List[int]:
|
201
202
|
"""Scores each job based on the remaining operations in the job."""
|
202
203
|
num_jobs = dispatcher.instance.num_jobs
|
203
204
|
scores = [0] * num_jobs
|
@@ -206,7 +207,7 @@ def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
|
|
206
207
|
return scores
|
207
208
|
|
208
209
|
|
209
|
-
def random_score(dispatcher: Dispatcher) ->
|
210
|
+
def random_score(dispatcher: Dispatcher) -> List[int]:
|
210
211
|
"""Scores each job randomly."""
|
211
212
|
return [
|
212
213
|
random.randint(0, 100) for _ in range(dispatcher.instance.num_jobs)
|
@@ -5,6 +5,7 @@ The factory functions create and return the appropriate functions based on the
|
|
5
5
|
specified names or enums.
|
6
6
|
"""
|
7
7
|
|
8
|
+
from typing import Union, Dict
|
8
9
|
from enum import Enum
|
9
10
|
from collections.abc import Callable
|
10
11
|
import random
|
@@ -25,7 +26,7 @@ MachineChooser = Callable[[Dispatcher, Operation], int]
|
|
25
26
|
|
26
27
|
|
27
28
|
def machine_chooser_factory(
|
28
|
-
machine_chooser: str
|
29
|
+
machine_chooser: Union[str, MachineChooser],
|
29
30
|
) -> MachineChooser:
|
30
31
|
"""Creates and returns a machine chooser function based on the specified
|
31
32
|
machine chooser strategy name.
|
@@ -36,7 +37,7 @@ def machine_chooser_factory(
|
|
36
37
|
machine randomly.
|
37
38
|
|
38
39
|
Args:
|
39
|
-
machine_chooser
|
40
|
+
machine_chooser: The name of the machine chooser strategy to be
|
40
41
|
used. Supported values are 'first' and 'random'.
|
41
42
|
|
42
43
|
Returns:
|
@@ -49,7 +50,7 @@ def machine_chooser_factory(
|
|
49
50
|
ValueError: If the machine_chooser argument is not recognized or is
|
50
51
|
not supported.
|
51
52
|
"""
|
52
|
-
machine_choosers:
|
53
|
+
machine_choosers: Dict[str, Callable[[Dispatcher, Operation], int]] = {
|
53
54
|
MachineChooserType.FIRST: lambda _, operation: operation.machines[0],
|
54
55
|
MachineChooserType.RANDOM: lambda _, operation: random.choice(
|
55
56
|
operation.machines
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Utility functions."""
|
2
2
|
|
3
|
+
from typing import Union, List
|
3
4
|
import time
|
4
5
|
from collections.abc import Callable
|
5
6
|
import pandas as pd
|
@@ -10,12 +11,12 @@ from job_shop_lib.dispatching import Dispatcher
|
|
10
11
|
|
11
12
|
|
12
13
|
def benchmark_dispatching_rules(
|
13
|
-
dispatching_rules:
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
instances:
|
14
|
+
dispatching_rules: Union[
|
15
|
+
List[Union[str, Callable[[Dispatcher], Operation]]],
|
16
|
+
List[str],
|
17
|
+
List[Callable[[Dispatcher], Operation]]
|
18
|
+
],
|
19
|
+
instances: List[JobShopInstance],
|
19
20
|
) -> pd.DataFrame:
|
20
21
|
"""Benchmark multiple dispatching rules on multiple JobShopInstances.
|
21
22
|
|
@@ -30,7 +31,7 @@ def benchmark_dispatching_rules(
|
|
30
31
|
either a string (name of a built-in rule) or a callable
|
31
32
|
(custom rule function).
|
32
33
|
instances:
|
33
|
-
|
34
|
+
List of :class:`JobShopInstance` objects to be solved.
|
34
35
|
|
35
36
|
Returns:
|
36
37
|
A pandas DataFrame with columns:
|
@@ -111,7 +112,7 @@ if __name__ == "__main__":
|
|
111
112
|
instances_ = [load_benchmark_instance(f"ta{i:02d}") for i in range(1, 3)]
|
112
113
|
|
113
114
|
# Define rules
|
114
|
-
rules_:
|
115
|
+
rules_: List[str | Callable[[Dispatcher], Operation]] = [
|
115
116
|
"most_work_remaining",
|
116
117
|
"shortest_processing_time",
|
117
118
|
most_work_remaining_rule,
|
@@ -1,5 +1,10 @@
|
|
1
1
|
"""Package for generating job shop instances."""
|
2
2
|
|
3
|
+
from job_shop_lib.generation._utils import (
|
4
|
+
generate_duration_matrix,
|
5
|
+
generate_machine_matrix_with_recirculation,
|
6
|
+
generate_machine_matrix_without_recirculation,
|
7
|
+
)
|
3
8
|
from job_shop_lib.generation._instance_generator import InstanceGenerator
|
4
9
|
from job_shop_lib.generation._general_instance_generator import (
|
5
10
|
GeneralInstanceGenerator,
|
@@ -8,4 +13,7 @@ from job_shop_lib.generation._general_instance_generator import (
|
|
8
13
|
__all__ = [
|
9
14
|
"InstanceGenerator",
|
10
15
|
"GeneralInstanceGenerator",
|
16
|
+
"generate_duration_matrix",
|
17
|
+
"generate_machine_matrix_with_recirculation",
|
18
|
+
"generate_machine_matrix_without_recirculation",
|
11
19
|
]
|
@@ -1,10 +1,16 @@
|
|
1
1
|
"""Home of the `BasicGenerator` class."""
|
2
2
|
|
3
|
+
from typing import Optional, Tuple, Union
|
3
4
|
import random
|
4
5
|
|
5
|
-
from job_shop_lib import JobShopInstance
|
6
|
+
from job_shop_lib import JobShopInstance
|
6
7
|
from job_shop_lib.exceptions import ValidationError
|
7
|
-
from job_shop_lib.generation import
|
8
|
+
from job_shop_lib.generation import (
|
9
|
+
InstanceGenerator,
|
10
|
+
generate_duration_matrix,
|
11
|
+
generate_machine_matrix_with_recirculation,
|
12
|
+
generate_machine_matrix_without_recirculation,
|
13
|
+
)
|
8
14
|
|
9
15
|
|
10
16
|
class GeneralInstanceGenerator(InstanceGenerator):
|
@@ -20,6 +26,8 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
20
26
|
multiple instances, controlled by the ``iteration_limit`` parameter. It
|
21
27
|
implements the iterator protocol, allowing it to be used in a ``for`` loop.
|
22
28
|
|
29
|
+
The number of operations per machine is equal to the number of machines
|
30
|
+
|
23
31
|
Note:
|
24
32
|
When used as an iterator, the generator will produce instances until it
|
25
33
|
reaches the specified ``iteration_limit``. If ``iteration_limit`` is
|
@@ -73,15 +81,15 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
73
81
|
|
74
82
|
def __init__( # pylint: disable=too-many-arguments
|
75
83
|
self,
|
76
|
-
num_jobs: int
|
77
|
-
num_machines: int
|
78
|
-
duration_range:
|
84
|
+
num_jobs: Union[int, Tuple[int, int]] = (10, 20),
|
85
|
+
num_machines: Union[int, Tuple[int, int]] = (5, 10),
|
86
|
+
duration_range: Tuple[int, int] = (1, 99),
|
79
87
|
allow_less_jobs_than_machines: bool = True,
|
80
88
|
allow_recirculation: bool = False,
|
81
|
-
machines_per_operation: int
|
89
|
+
machines_per_operation: Union[int, Tuple[int, int]] = 1,
|
82
90
|
name_suffix: str = "classic_generated_instance",
|
83
|
-
seed: int
|
84
|
-
iteration_limit: int
|
91
|
+
seed: Optional[int] = None,
|
92
|
+
iteration_limit: Optional[int] = None,
|
85
93
|
):
|
86
94
|
super().__init__(
|
87
95
|
num_jobs=num_jobs,
|
@@ -96,6 +104,10 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
96
104
|
machines_per_operation,
|
97
105
|
machines_per_operation,
|
98
106
|
)
|
107
|
+
if machines_per_operation != (1, 1):
|
108
|
+
raise NotImplementedError(
|
109
|
+
"The number of machines per operation must be 1 for now."
|
110
|
+
)
|
99
111
|
self.machines_per_operation = machines_per_operation
|
100
112
|
|
101
113
|
self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
|
@@ -114,7 +126,9 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
114
126
|
)
|
115
127
|
|
116
128
|
def generate(
|
117
|
-
self,
|
129
|
+
self,
|
130
|
+
num_jobs: Optional[int] = None,
|
131
|
+
num_machines: Optional[int] = None,
|
118
132
|
) -> JobShopInstance:
|
119
133
|
if num_jobs is None:
|
120
134
|
num_jobs = random.randint(*self.num_jobs_range)
|
@@ -128,60 +142,24 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
128
142
|
not self.allow_less_jobs_than_machines and num_jobs < num_machines
|
129
143
|
):
|
130
144
|
raise ValidationError(
|
131
|
-
"
|
132
|
-
"when `allow_less_jobs_than_machines` attribute is False."
|
145
|
+
"There are fewer jobs than machines, which is not allowed "
|
146
|
+
" when `allow_less_jobs_than_machines` attribute is False."
|
147
|
+
)
|
148
|
+
|
149
|
+
duration_matrix = generate_duration_matrix(
|
150
|
+
num_jobs, num_machines, self.duration_range
|
151
|
+
)
|
152
|
+
if self.allow_recirculation:
|
153
|
+
machine_matrix = generate_machine_matrix_with_recirculation(
|
154
|
+
num_jobs, num_machines
|
155
|
+
)
|
156
|
+
else:
|
157
|
+
machine_matrix = generate_machine_matrix_without_recirculation(
|
158
|
+
num_jobs, num_machines
|
133
159
|
)
|
134
160
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
operation = self.create_random_operation(available_machines)
|
141
|
-
job.append(operation)
|
142
|
-
jobs.append(job)
|
143
|
-
available_machines = list(range(num_machines))
|
144
|
-
|
145
|
-
return JobShopInstance(jobs=jobs, name=self._next_name())
|
146
|
-
|
147
|
-
def create_random_operation(
|
148
|
-
self, available_machines: list[int] | None = None
|
149
|
-
) -> Operation:
|
150
|
-
"""Creates a random operation with the given available machines.
|
151
|
-
|
152
|
-
Args:
|
153
|
-
available_machines:
|
154
|
-
A list of available machine_ids to choose from.
|
155
|
-
If ``None``, all machines are available.
|
156
|
-
"""
|
157
|
-
duration = random.randint(*self.duration_range)
|
158
|
-
|
159
|
-
if self.machines_per_operation[1] > 1:
|
160
|
-
machines = self._choose_multiple_machines()
|
161
|
-
return Operation(machines=machines, duration=duration)
|
162
|
-
|
163
|
-
machine_id = self._choose_one_machine(available_machines)
|
164
|
-
return Operation(machines=machine_id, duration=duration)
|
165
|
-
|
166
|
-
def _choose_multiple_machines(self) -> list[int]:
|
167
|
-
num_machines = random.randint(*self.machines_per_operation)
|
168
|
-
available_machines = list(range(num_machines))
|
169
|
-
machines = []
|
170
|
-
for _ in range(num_machines):
|
171
|
-
machine = random.choice(available_machines)
|
172
|
-
machines.append(machine)
|
173
|
-
available_machines.remove(machine)
|
174
|
-
return machines
|
175
|
-
|
176
|
-
def _choose_one_machine(
|
177
|
-
self, available_machines: list[int] | None = None
|
178
|
-
) -> int:
|
179
|
-
if available_machines is None:
|
180
|
-
_, max_num_machines = self.num_machines_range
|
181
|
-
available_machines = list(range(max_num_machines))
|
182
|
-
|
183
|
-
machine_id = random.choice(available_machines)
|
184
|
-
if not self.allow_recirculation:
|
185
|
-
available_machines.remove(machine_id)
|
186
|
-
|
187
|
-
return machine_id
|
161
|
+
return JobShopInstance.from_matrices(
|
162
|
+
duration_matrix.tolist(),
|
163
|
+
machine_matrix.tolist(),
|
164
|
+
name=self._next_name(),
|
165
|
+
)
|
@@ -1,7 +1,9 @@
|
|
1
|
+
"""Home of the `InstanceGenerator` class."""
|
2
|
+
|
1
3
|
import abc
|
2
4
|
|
3
5
|
import random
|
4
|
-
from typing import Iterator
|
6
|
+
from typing import Iterator, Optional, Tuple, Union
|
5
7
|
|
6
8
|
from job_shop_lib import JobShopInstance
|
7
9
|
from job_shop_lib.exceptions import UninitializedAttributeError
|
@@ -50,12 +52,12 @@ class InstanceGenerator(abc.ABC):
|
|
50
52
|
|
51
53
|
def __init__( # pylint: disable=too-many-arguments
|
52
54
|
self,
|
53
|
-
num_jobs: int
|
54
|
-
num_machines: int
|
55
|
-
duration_range:
|
55
|
+
num_jobs: Union[int, Tuple[int, int]] = (10, 20),
|
56
|
+
num_machines: Union[int, Tuple[int, int]] = (5, 10),
|
57
|
+
duration_range: Tuple[int, int] = (1, 99),
|
56
58
|
name_suffix: str = "generated_instance",
|
57
|
-
seed: int
|
58
|
-
iteration_limit: int
|
59
|
+
seed: Optional[int] = None,
|
60
|
+
iteration_limit: Optional[int] = None,
|
59
61
|
):
|
60
62
|
if isinstance(num_jobs, int):
|
61
63
|
num_jobs = (num_jobs, num_jobs)
|
@@ -75,7 +77,9 @@ class InstanceGenerator(abc.ABC):
|
|
75
77
|
|
76
78
|
@abc.abstractmethod
|
77
79
|
def generate(
|
78
|
-
self,
|
80
|
+
self,
|
81
|
+
num_jobs: Optional[int] = None,
|
82
|
+
num_machines: Optional[int] = None,
|
79
83
|
) -> JobShopInstance:
|
80
84
|
"""Generates a single job shop instance
|
81
85
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import abc
|
4
4
|
import copy
|
5
5
|
import random
|
6
|
+
from typing import Optional
|
6
7
|
|
7
8
|
from job_shop_lib import JobShopInstance, Operation
|
8
9
|
|
@@ -38,7 +39,7 @@ class RemoveMachines(Transformation):
|
|
38
39
|
"""Removes operations associated with randomly selected machines until
|
39
40
|
there are exactly num_machines machines left."""
|
40
41
|
|
41
|
-
def __init__(self, num_machines: int, suffix: str
|
42
|
+
def __init__(self, num_machines: int, suffix: Optional[str] = None):
|
42
43
|
if suffix is None:
|
43
44
|
suffix = f"_machines={num_machines}"
|
44
45
|
super().__init__(suffix=suffix)
|
@@ -83,7 +84,7 @@ class AddDurationNoise(Transformation):
|
|
83
84
|
min_duration: int = 1,
|
84
85
|
max_duration: int = 100,
|
85
86
|
noise_level: int = 10,
|
86
|
-
suffix: str
|
87
|
+
suffix: Optional[str] = None,
|
87
88
|
):
|
88
89
|
if suffix is None:
|
89
90
|
suffix = f"_noise={noise_level}"
|
@@ -127,8 +128,8 @@ class RemoveJobs(Transformation):
|
|
127
128
|
self,
|
128
129
|
min_jobs: int,
|
129
130
|
max_jobs: int,
|
130
|
-
target_jobs: int
|
131
|
-
suffix: str
|
131
|
+
target_jobs: Optional[int] = None,
|
132
|
+
suffix: Optional[str] = None,
|
132
133
|
):
|
133
134
|
if suffix is None:
|
134
135
|
suffix = f"_jobs={min_jobs}-{max_jobs}"
|
@@ -0,0 +1,124 @@
|
|
1
|
+
from typing import Tuple
|
2
|
+
import numpy as np
|
3
|
+
from numpy.typing import NDArray
|
4
|
+
|
5
|
+
from job_shop_lib.exceptions import ValidationError
|
6
|
+
|
7
|
+
|
8
|
+
def generate_duration_matrix(
|
9
|
+
num_jobs: int, num_machines: int, duration_range: Tuple[int, int]
|
10
|
+
) -> NDArray[np.int32]:
|
11
|
+
"""Generates a duration matrix.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
num_jobs: The number of jobs.
|
15
|
+
num_machines: The number of machines.
|
16
|
+
duration_range: The range of the duration values.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
A duration matrix with shape (num_jobs, num_machines).
|
20
|
+
"""
|
21
|
+
if duration_range[0] > duration_range[1]:
|
22
|
+
raise ValidationError(
|
23
|
+
"The lower bound of the duration range must be less than or equal "
|
24
|
+
"to the upper bound."
|
25
|
+
)
|
26
|
+
if num_jobs <= 0:
|
27
|
+
raise ValidationError("The number of jobs must be greater than 0.")
|
28
|
+
if num_machines <= 0:
|
29
|
+
raise ValidationError("The number of machines must be greater than 0.")
|
30
|
+
|
31
|
+
return np.random.randint(
|
32
|
+
duration_range[0],
|
33
|
+
duration_range[1] + 1,
|
34
|
+
size=(num_jobs, num_machines),
|
35
|
+
dtype=np.int32,
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
def generate_machine_matrix_with_recirculation(
|
40
|
+
num_jobs: int, num_machines: int
|
41
|
+
) -> NDArray[np.int32]:
|
42
|
+
"""Generate a machine matrix with recirculation.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
num_jobs: The number of jobs.
|
46
|
+
num_machines: The number of machines.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
A machine matrix with recirculation with shape (num_machines,
|
50
|
+
num_jobs).
|
51
|
+
"""
|
52
|
+
if num_jobs <= 0:
|
53
|
+
raise ValidationError("The number of jobs must be greater than 0.")
|
54
|
+
if num_machines <= 0:
|
55
|
+
raise ValidationError("The number of machines must be greater than 0.")
|
56
|
+
num_machines_is_correct = False
|
57
|
+
while not num_machines_is_correct:
|
58
|
+
machine_matrix: np.ndarray = np.random.randint(
|
59
|
+
0, num_machines, size=(num_machines, num_jobs)
|
60
|
+
)
|
61
|
+
num_machines_is_correct = (
|
62
|
+
len(np.unique(machine_matrix)) == num_machines
|
63
|
+
)
|
64
|
+
|
65
|
+
return machine_matrix
|
66
|
+
|
67
|
+
|
68
|
+
def generate_machine_matrix_without_recirculation(
|
69
|
+
num_jobs: int, num_machines: int
|
70
|
+
) -> NDArray[np.int32]:
|
71
|
+
"""Generate a machine matrix without recirculation.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
num_jobs: The number of jobs.
|
75
|
+
num_machines: The number of machines.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
A machine matrix without recirculation.
|
79
|
+
"""
|
80
|
+
if num_jobs <= 0:
|
81
|
+
raise ValidationError("The number of jobs must be greater than 0.")
|
82
|
+
if num_machines <= 0:
|
83
|
+
raise ValidationError("The number of machines must be greater than 0.")
|
84
|
+
# Start with an arange repeated:
|
85
|
+
# m1: [0, 1, 2]
|
86
|
+
# m2: [0, 1, 2]
|
87
|
+
# m3: [0, 1, 2]
|
88
|
+
machine_matrix = np.tile(
|
89
|
+
np.arange(num_machines).reshape(1, num_machines),
|
90
|
+
(num_jobs, 1),
|
91
|
+
)
|
92
|
+
# Shuffle the columns:
|
93
|
+
machine_matrix = np.apply_along_axis(
|
94
|
+
np.random.permutation, 1, machine_matrix
|
95
|
+
)
|
96
|
+
return machine_matrix
|
97
|
+
|
98
|
+
|
99
|
+
if __name__ == "__main__":
|
100
|
+
|
101
|
+
NUM_JOBS = 3
|
102
|
+
NUM_MACHINES = 3
|
103
|
+
DURATION_RANGE = (1, 10)
|
104
|
+
|
105
|
+
duration_matrix = generate_duration_matrix(
|
106
|
+
num_jobs=NUM_JOBS,
|
107
|
+
num_machines=NUM_MACHINES,
|
108
|
+
duration_range=DURATION_RANGE,
|
109
|
+
)
|
110
|
+
print(duration_matrix)
|
111
|
+
|
112
|
+
machine_matrix_with_recirculation = (
|
113
|
+
generate_machine_matrix_with_recirculation(
|
114
|
+
num_jobs=NUM_JOBS, num_machines=NUM_MACHINES
|
115
|
+
)
|
116
|
+
)
|
117
|
+
print(machine_matrix_with_recirculation)
|
118
|
+
|
119
|
+
machine_matrix_without_recirculation = (
|
120
|
+
generate_machine_matrix_without_recirculation(
|
121
|
+
num_jobs=NUM_JOBS, num_machines=NUM_MACHINES
|
122
|
+
)
|
123
|
+
)
|
124
|
+
print(machine_matrix_without_recirculation)
|
job_shop_lib/graphs/__init__.py
CHANGED
@@ -25,10 +25,10 @@ from job_shop_lib.graphs._build_disjunctive_graph import (
|
|
25
25
|
add_source_sink_nodes,
|
26
26
|
add_source_sink_edges,
|
27
27
|
)
|
28
|
-
from job_shop_lib.graphs.
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
from job_shop_lib.graphs._build_resource_task_graphs import (
|
29
|
+
build_resource_task_graph,
|
30
|
+
build_complete_resource_task_graph,
|
31
|
+
build_resource_task_graph_with_jobs,
|
32
32
|
add_same_job_operations_edges,
|
33
33
|
add_machine_nodes,
|
34
34
|
add_operation_machine_edges,
|
@@ -52,9 +52,9 @@ __all__ = [
|
|
52
52
|
"add_conjunctive_edges",
|
53
53
|
"add_source_sink_nodes",
|
54
54
|
"add_source_sink_edges",
|
55
|
-
"
|
56
|
-
"
|
57
|
-
"
|
55
|
+
"build_resource_task_graph",
|
56
|
+
"build_complete_resource_task_graph",
|
57
|
+
"build_resource_task_graph_with_jobs",
|
58
58
|
"add_same_job_operations_edges",
|
59
59
|
"add_machine_nodes",
|
60
60
|
"add_operation_machine_edges",
|
@@ -1,16 +1,13 @@
|
|
1
|
-
"""Contains helper functions to build the
|
2
|
-
|
1
|
+
"""Contains helper functions to build the resource-task graphs from a job shop
|
2
|
+
instance.
|
3
3
|
|
4
|
-
The agent-task graph
|
4
|
+
The agent-task graph (renamed to resource-task graph) was introduced by
|
5
|
+
Junyoung Park et al. (2021).
|
5
6
|
In contrast to the disjunctive graph, instead of connecting operations that
|
6
7
|
share the same resources directly by disjunctive edges, operation nodes are
|
7
8
|
connected with machine ones. All machine nodes are connected between them, and
|
8
9
|
all operation nodes from the same job are connected by non-directed edges too.
|
9
10
|
|
10
|
-
We also support a generalization of this approach by the addition of job nodes
|
11
|
-
and a global node. Job nodes are connected to all operation nodes of the same
|
12
|
-
job, and the global node is connected to all machine and job nodes.
|
13
|
-
|
14
11
|
References:
|
15
12
|
- Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
|
16
13
|
solve multi-agent scheduling problems with reinforcement learning. ArXiv,
|
@@ -23,11 +20,13 @@ from job_shop_lib import JobShopInstance
|
|
23
20
|
from job_shop_lib.graphs import JobShopGraph, NodeType, Node
|
24
21
|
|
25
22
|
|
26
|
-
def
|
27
|
-
|
23
|
+
def build_complete_resource_task_graph(
|
24
|
+
instance: JobShopInstance,
|
25
|
+
) -> JobShopGraph:
|
26
|
+
"""Builds the resource-task graph of the instance with job and global
|
27
|
+
nodes.
|
28
28
|
|
29
|
-
The complete
|
30
|
-
that includes job nodes and a global node.
|
29
|
+
The complete resource-task includes job nodes and a global node.
|
31
30
|
|
32
31
|
Job nodes are connected to all operation nodes of the same job, and the
|
33
32
|
global node is connected to all machine and job nodes.
|
@@ -38,10 +37,11 @@ def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
|
38
37
|
|
39
38
|
Args:
|
40
39
|
instance:
|
41
|
-
The job shop instance in which the
|
40
|
+
The job shop instance in which the resource-task graph will be
|
41
|
+
built.
|
42
42
|
|
43
43
|
Returns:
|
44
|
-
The complete
|
44
|
+
The complete resource-task graph of the instance.
|
45
45
|
"""
|
46
46
|
graph = JobShopGraph(instance)
|
47
47
|
|
@@ -58,23 +58,23 @@ def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
|
58
58
|
return graph
|
59
59
|
|
60
60
|
|
61
|
-
def
|
61
|
+
def build_resource_task_graph_with_jobs(
|
62
62
|
instance: JobShopInstance,
|
63
63
|
) -> JobShopGraph:
|
64
|
-
"""Builds the
|
64
|
+
"""Builds the resource-task graph of the instance with job nodes.
|
65
65
|
|
66
|
-
The
|
67
|
-
graph that includes job nodes.
|
66
|
+
The resource-task graph that includes job nodes.
|
68
67
|
|
69
68
|
Job nodes are connected to all operation nodes of the same job, and their
|
70
69
|
are connected between them.
|
71
70
|
|
72
71
|
Args:
|
73
72
|
instance:
|
74
|
-
The job shop instance in which the
|
73
|
+
The job shop instance in which the resource-task graph will be
|
74
|
+
built.
|
75
75
|
|
76
76
|
Returns:
|
77
|
-
The
|
77
|
+
The resource-task graph of the instance with job nodes.
|
78
78
|
"""
|
79
79
|
graph = JobShopGraph(instance)
|
80
80
|
|
@@ -89,10 +89,11 @@ def build_agent_task_graph_with_jobs(
|
|
89
89
|
return graph
|
90
90
|
|
91
91
|
|
92
|
-
def
|
93
|
-
"""Builds the
|
92
|
+
def build_resource_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
93
|
+
"""Builds the resource-task graph of the instance.
|
94
94
|
|
95
|
-
The
|
95
|
+
The JSSP resource-task graph representation was introduced by Junyoung
|
96
|
+
Park et al. (2021) (named agent-task graph in the original paper).
|
96
97
|
|
97
98
|
In contrast to the disjunctive graph, instead of connecting operations
|
98
99
|
that share the same resources directly by disjunctive edges, operation
|
@@ -103,10 +104,11 @@ def build_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
|
103
104
|
|
104
105
|
Args:
|
105
106
|
instance:
|
106
|
-
The job shop instance in which the
|
107
|
+
The job shop instance in which the resource-task graph will be
|
108
|
+
built.
|
107
109
|
|
108
110
|
Returns:
|
109
|
-
The
|
111
|
+
The resource-task graph of the instance.
|
110
112
|
"""
|
111
113
|
graph = JobShopGraph(instance)
|
112
114
|
|