job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b1__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/_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/_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/_dispatching_rule_factory.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +23 -15
- 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.0b1.dist-info}/METADATA +17 -15
- job_shop_lib-1.0.0b1.dist-info/RECORD +69 -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.0b1.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/WHEEL +0 -0
@@ -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
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Home of the `JobShopGraph` class."""
|
2
2
|
|
3
|
+
from typing import List, Union, Dict
|
3
4
|
import collections
|
4
5
|
import networkx as nx
|
5
6
|
|
@@ -62,23 +63,23 @@ class JobShopGraph:
|
|
62
63
|
self.graph = nx.DiGraph()
|
63
64
|
self.instance = instance
|
64
65
|
|
65
|
-
self._nodes:
|
66
|
-
self._nodes_by_type:
|
66
|
+
self._nodes: List[Node] = []
|
67
|
+
self._nodes_by_type: Dict[NodeType, List[Node]] = (
|
67
68
|
collections.defaultdict(list)
|
68
69
|
)
|
69
|
-
self._nodes_by_machine:
|
70
|
+
self._nodes_by_machine: List[List[Node]] = [
|
70
71
|
[] for _ in range(instance.num_machines)
|
71
72
|
]
|
72
|
-
self._nodes_by_job:
|
73
|
+
self._nodes_by_job: List[List[Node]] = [
|
73
74
|
[] for _ in range(instance.num_jobs)
|
74
75
|
]
|
75
76
|
self._next_node_id = 0
|
76
|
-
self.removed_nodes:
|
77
|
+
self.removed_nodes: List[bool] = []
|
77
78
|
if add_operation_nodes:
|
78
79
|
self.add_operation_nodes()
|
79
80
|
|
80
81
|
@property
|
81
|
-
def nodes(self) ->
|
82
|
+
def nodes(self) -> List[Node]:
|
82
83
|
"""List of all nodes added to the graph.
|
83
84
|
|
84
85
|
It may contain nodes that have been removed from the graph.
|
@@ -86,7 +87,7 @@ class JobShopGraph:
|
|
86
87
|
return self._nodes
|
87
88
|
|
88
89
|
@property
|
89
|
-
def nodes_by_type(self) ->
|
90
|
+
def nodes_by_type(self) -> Dict[NodeType, List[Node]]:
|
90
91
|
"""Dictionary mapping node types to lists of nodes.
|
91
92
|
|
92
93
|
It may contain nodes that have been removed from the graph.
|
@@ -94,7 +95,7 @@ class JobShopGraph:
|
|
94
95
|
return self._nodes_by_type
|
95
96
|
|
96
97
|
@property
|
97
|
-
def nodes_by_machine(self) ->
|
98
|
+
def nodes_by_machine(self) -> List[List[Node]]:
|
98
99
|
"""List of lists mapping machine ids to operation nodes.
|
99
100
|
|
100
101
|
It may contain nodes that have been removed from the graph.
|
@@ -102,7 +103,7 @@ class JobShopGraph:
|
|
102
103
|
return self._nodes_by_machine
|
103
104
|
|
104
105
|
@property
|
105
|
-
def nodes_by_job(self) ->
|
106
|
+
def nodes_by_job(self) -> List[List[Node]]:
|
106
107
|
"""List of lists mapping job ids to operation nodes.
|
107
108
|
|
108
109
|
It may contain nodes that have been removed from the graph.
|
@@ -163,7 +164,10 @@ class JobShopGraph:
|
|
163
164
|
self._nodes_by_machine[machine_id].append(node_for_adding)
|
164
165
|
|
165
166
|
def add_edge(
|
166
|
-
self,
|
167
|
+
self,
|
168
|
+
u_of_edge: Union[Node, int],
|
169
|
+
v_of_edge: Union[Node, int],
|
170
|
+
**attr,
|
167
171
|
) -> None:
|
168
172
|
"""Adds an edge to the graph.
|
169
173
|
|
@@ -177,7 +181,7 @@ class JobShopGraph:
|
|
177
181
|
**attr: Additional attributes to be added to the edge.
|
178
182
|
|
179
183
|
Raises:
|
180
|
-
|
184
|
+
ValidationError: If `u_of_edge` or `v_of_edge` are not in the
|
181
185
|
graph.
|
182
186
|
"""
|
183
187
|
if isinstance(u_of_edge, Node):
|
@@ -206,7 +210,7 @@ class JobShopGraph:
|
|
206
210
|
|
207
211
|
self.graph.remove_nodes_from(isolated_nodes)
|
208
212
|
|
209
|
-
def is_removed(self, node: int
|
213
|
+
def is_removed(self, node: Union[int, Node]) -> bool:
|
210
214
|
"""Returns whether the node is removed from the graph.
|
211
215
|
|
212
216
|
Args:
|
@@ -218,7 +222,7 @@ class JobShopGraph:
|
|
218
222
|
node = node.node_id
|
219
223
|
return self.removed_nodes[node]
|
220
224
|
|
221
|
-
def non_removed_nodes(self) ->
|
225
|
+
def non_removed_nodes(self) -> List[Node]:
|
222
226
|
"""Returns the nodes that are not removed from the graph."""
|
223
227
|
return [node for node in self._nodes if not self.is_removed(node)]
|
224
228
|
|
job_shop_lib/graphs/_node.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Home of the `Node` class."""
|
2
2
|
|
3
|
+
from typing import Optional
|
4
|
+
|
3
5
|
from job_shop_lib import Operation
|
4
6
|
from job_shop_lib.exceptions import (
|
5
7
|
UninitializedAttributeError,
|
@@ -80,9 +82,9 @@ class Node:
|
|
80
82
|
def __init__(
|
81
83
|
self,
|
82
84
|
node_type: NodeType,
|
83
|
-
operation: Operation
|
84
|
-
machine_id: int
|
85
|
-
job_id: int
|
85
|
+
operation: Optional[Operation] = None,
|
86
|
+
machine_id: Optional[int] = None,
|
87
|
+
job_id: Optional[int] = None,
|
86
88
|
):
|
87
89
|
if node_type == NodeType.OPERATION and operation is None:
|
88
90
|
raise ValidationError("Operation node must have an operation.")
|
@@ -94,7 +96,7 @@ class Node:
|
|
94
96
|
raise ValidationError("Job node must have a job_id.")
|
95
97
|
|
96
98
|
self.node_type: NodeType = node_type
|
97
|
-
self._node_id: int
|
99
|
+
self._node_id: Optional[int] = None
|
98
100
|
|
99
101
|
self._operation = operation
|
100
102
|
self._machine_id = machine_id
|
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Home of the `ResidualGraphUpdater` class."""
|
2
2
|
|
3
|
+
from typing import Optional, List
|
4
|
+
|
3
5
|
from job_shop_lib import ScheduledOperation
|
4
6
|
from job_shop_lib.exceptions import UninitializedAttributeError
|
5
7
|
from job_shop_lib.graphs import NodeType, JobShopGraph
|
@@ -54,7 +56,7 @@ class ResidualGraphUpdater(GraphUpdater):
|
|
54
56
|
remove_completed_machine_nodes: bool = True,
|
55
57
|
remove_completed_job_nodes: bool = True,
|
56
58
|
):
|
57
|
-
self._is_completed_observer:
|
59
|
+
self._is_completed_observer: Optional[IsCompletedObserver] = None
|
58
60
|
self.remove_completed_job_nodes = remove_completed_job_nodes
|
59
61
|
self.remove_completed_machine_nodes = remove_completed_machine_nodes
|
60
62
|
self._initialize_is_completed_observer_attribute(dispatcher)
|
@@ -80,7 +82,7 @@ class ResidualGraphUpdater(GraphUpdater):
|
|
80
82
|
return False
|
81
83
|
return True
|
82
84
|
|
83
|
-
feature_types:
|
85
|
+
feature_types: List[FeatureType] = []
|
84
86
|
if self.remove_completed_machine_nodes:
|
85
87
|
feature_types.append(FeatureType.MACHINES)
|
86
88
|
if self.remove_completed_job_nodes:
|