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
@@ -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:
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from collections import defaultdict
|
4
4
|
from collections.abc import Callable, Sequence
|
5
|
-
from typing import Any
|
5
|
+
from typing import Any, Tuple, Dict, List, Optional, Type
|
6
6
|
from copy import deepcopy
|
7
7
|
|
8
8
|
import gymnasium as gym
|
@@ -16,7 +16,7 @@ from job_shop_lib.dispatching import (
|
|
16
16
|
)
|
17
17
|
from job_shop_lib.dispatching.feature_observers import FeatureObserverConfig
|
18
18
|
from job_shop_lib.generation import InstanceGenerator
|
19
|
-
from job_shop_lib.graphs import JobShopGraph,
|
19
|
+
from job_shop_lib.graphs import JobShopGraph, build_resource_task_graph
|
20
20
|
from job_shop_lib.graphs.graph_updaters import (
|
21
21
|
GraphUpdater,
|
22
22
|
ResidualGraphUpdater,
|
@@ -160,18 +160,18 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
160
160
|
feature_observer_configs: Sequence[FeatureObserverConfig],
|
161
161
|
graph_initializer: Callable[
|
162
162
|
[JobShopInstance], JobShopGraph
|
163
|
-
] =
|
163
|
+
] = build_resource_task_graph,
|
164
164
|
graph_updater_config: DispatcherObserverConfig[
|
165
|
-
|
165
|
+
Type[GraphUpdater]
|
166
166
|
] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
|
167
167
|
ready_operations_filter: Callable[
|
168
|
-
[Dispatcher,
|
168
|
+
[Dispatcher, List[Operation]], List[Operation]
|
169
169
|
] = filter_dominated_operations,
|
170
170
|
reward_function_config: DispatcherObserverConfig[
|
171
|
-
|
171
|
+
Type[RewardObserver]
|
172
172
|
] = DispatcherObserverConfig(class_type=MakespanReward),
|
173
|
-
render_mode: str
|
174
|
-
render_config: RenderConfig
|
173
|
+
render_mode: Optional[str] = None,
|
174
|
+
render_config: Optional[RenderConfig] = None,
|
175
175
|
use_padding: bool = True,
|
176
176
|
) -> None:
|
177
177
|
super().__init__()
|
@@ -226,7 +226,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
226
226
|
@property
|
227
227
|
def ready_operations_filter(
|
228
228
|
self,
|
229
|
-
) -> Callable[[Dispatcher,
|
229
|
+
) -> Optional[Callable[[Dispatcher, List[Operation]], List[Operation]]]:
|
230
230
|
"""Returns the current ready operations filter."""
|
231
231
|
return (
|
232
232
|
self.single_job_shop_graph_env.dispatcher.ready_operations_filter
|
@@ -236,7 +236,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
236
236
|
def ready_operations_filter(
|
237
237
|
self,
|
238
238
|
pruning_function: Callable[
|
239
|
-
[Dispatcher,
|
239
|
+
[Dispatcher, List[Operation]], List[Operation]
|
240
240
|
],
|
241
241
|
) -> None:
|
242
242
|
"""Sets the ready operations filter."""
|
@@ -267,9 +267,9 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
267
267
|
def reset(
|
268
268
|
self,
|
269
269
|
*,
|
270
|
-
seed: int
|
271
|
-
options:
|
272
|
-
) ->
|
270
|
+
seed: Optional[int] = None,
|
271
|
+
options: Dict[str, Any] | None = None,
|
272
|
+
) -> Tuple[ObservationDict, Dict[str, Any]]:
|
273
273
|
"""Resets the environment and returns the initial observation.
|
274
274
|
|
275
275
|
Args:
|
@@ -303,8 +303,8 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
303
303
|
return obs, info
|
304
304
|
|
305
305
|
def step(
|
306
|
-
self, action:
|
307
|
-
) ->
|
306
|
+
self, action: Tuple[int, int]
|
307
|
+
) -> Tuple[ObservationDict, float, bool, bool, Dict[str, Any]]:
|
308
308
|
"""Takes a step in the environment.
|
309
309
|
|
310
310
|
Args:
|
@@ -322,9 +322,10 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
322
322
|
- Whether the environment is done.
|
323
323
|
- Whether the episode was truncated (always False).
|
324
324
|
- A dictionary with additional information. The dictionary
|
325
|
-
contains the following keys:
|
326
|
-
|
327
|
-
|
325
|
+
contains the following keys: "feature_names", the names of the
|
326
|
+
features in the observation; and "available_operations_with_ids",
|
327
|
+
a list of available actions in the form of (operation_id,
|
328
|
+
machine_id, job_id).
|
328
329
|
"""
|
329
330
|
obs, reward, done, truncated, info = (
|
330
331
|
self.single_job_shop_graph_env.step(action)
|
@@ -355,7 +356,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
355
356
|
input_shape: (num_machines, num_features)
|
356
357
|
output_shape: (max_num_machines, num_features) (padded with -1)
|
357
358
|
"""
|
358
|
-
padding_value:
|
359
|
+
padding_value: Dict[str, float | bool] = defaultdict(lambda: -1)
|
359
360
|
padding_value[ObservationSpaceKey.REMOVED_NODES.value] = True
|
360
361
|
for key, value in observation.items():
|
361
362
|
if not isinstance(value, np.ndarray): # Make mypy happy
|
@@ -368,7 +369,7 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
368
369
|
)
|
369
370
|
return observation
|
370
371
|
|
371
|
-
def _get_output_shape(self, key: str) ->
|
372
|
+
def _get_output_shape(self, key: str) -> Tuple[int, ...]:
|
372
373
|
"""Returns the output shape of the observation space key."""
|
373
374
|
output_shape = self.observation_space[key].shape
|
374
375
|
assert output_shape is not None # Make mypy happy
|
@@ -376,3 +377,22 @@ class MultiJobShopGraphEnv(gym.Env):
|
|
376
377
|
|
377
378
|
def render(self) -> None:
|
378
379
|
self.single_job_shop_graph_env.render()
|
380
|
+
|
381
|
+
def get_available_actions_with_ids(self) -> List[Tuple[int, int, int]]:
|
382
|
+
"""Returns a list of available actions in the form of
|
383
|
+
(operation_id, machine_id, job_id)."""
|
384
|
+
return self.single_job_shop_graph_env.get_available_actions_with_ids()
|
385
|
+
|
386
|
+
def validate_action(self, action: Tuple[int, int]) -> None:
|
387
|
+
"""Validates the action.
|
388
|
+
|
389
|
+
Args:
|
390
|
+
action:
|
391
|
+
The action to validate. The action is a tuple of two integers
|
392
|
+
(job_id, machine_id): the job ID and the machine ID in which
|
393
|
+
to schedule the operation.
|
394
|
+
|
395
|
+
Raises:
|
396
|
+
ValidationError: If the action is invalid.
|
397
|
+
"""
|
398
|
+
self.single_job_shop_graph_env.validate_action(action)
|
@@ -1,6 +1,8 @@
|
|
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 typing import List
|
5
|
+
|
4
6
|
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
5
7
|
from job_shop_lib import ScheduledOperation
|
6
8
|
|
@@ -18,7 +20,7 @@ class RewardObserver(DispatcherObserver):
|
|
18
20
|
self, dispatcher: Dispatcher, *, subscribe: bool = True
|
19
21
|
) -> None:
|
20
22
|
super().__init__(dispatcher, subscribe=subscribe)
|
21
|
-
self.rewards:
|
23
|
+
self.rewards: List[float] = []
|
22
24
|
|
23
25
|
@property
|
24
26
|
def last_reward(self) -> float:
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from copy import deepcopy
|
4
4
|
from collections.abc import Callable, Sequence
|
5
|
-
from typing import Any
|
5
|
+
from typing import Any, Dict, Tuple, List, Optional, Type
|
6
6
|
|
7
7
|
import matplotlib.pyplot as plt
|
8
8
|
import gymnasium as gym
|
@@ -15,6 +15,7 @@ from job_shop_lib.graphs.graph_updaters import (
|
|
15
15
|
GraphUpdater,
|
16
16
|
ResidualGraphUpdater,
|
17
17
|
)
|
18
|
+
from job_shop_lib.exceptions import ValidationError
|
18
19
|
from job_shop_lib.dispatching import (
|
19
20
|
Dispatcher,
|
20
21
|
filter_dominated_operations,
|
@@ -24,7 +25,7 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
24
25
|
FeatureObserverConfig,
|
25
26
|
CompositeFeatureObserver,
|
26
27
|
)
|
27
|
-
from job_shop_lib.visualization import GanttChartCreator
|
28
|
+
from job_shop_lib.visualization.gantt import GanttChartCreator
|
28
29
|
from job_shop_lib.reinforcement_learning import (
|
29
30
|
RewardObserver,
|
30
31
|
MakespanReward,
|
@@ -138,16 +139,16 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
138
139
|
job_shop_graph: JobShopGraph,
|
139
140
|
feature_observer_configs: Sequence[FeatureObserverConfig],
|
140
141
|
reward_function_config: DispatcherObserverConfig[
|
141
|
-
|
142
|
+
Type[RewardObserver]
|
142
143
|
] = DispatcherObserverConfig(class_type=MakespanReward),
|
143
144
|
graph_updater_config: DispatcherObserverConfig[
|
144
|
-
|
145
|
+
Type[GraphUpdater]
|
145
146
|
] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
|
146
|
-
ready_operations_filter:
|
147
|
-
Callable[[Dispatcher,
|
148
|
-
|
149
|
-
render_mode: str
|
150
|
-
render_config: RenderConfig
|
147
|
+
ready_operations_filter: Optional[
|
148
|
+
Callable[[Dispatcher, List[Operation]], List[Operation]]
|
149
|
+
] = filter_dominated_operations,
|
150
|
+
render_mode: Optional[str] = None,
|
151
|
+
render_config: Optional[RenderConfig] = None,
|
151
152
|
use_padding: bool = True,
|
152
153
|
) -> None:
|
153
154
|
super().__init__()
|
@@ -195,10 +196,26 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
195
196
|
"""Returns the job shop graph."""
|
196
197
|
return self.graph_updater.job_shop_graph
|
197
198
|
|
199
|
+
def current_makespan(self) -> int:
|
200
|
+
"""Returns current makespan of partial schedule."""
|
201
|
+
return self.dispatcher.schedule.makespan()
|
202
|
+
|
203
|
+
def machine_utilization(self) -> NDArray[np.float32]:
|
204
|
+
"""Returns utilization percentage for each machine."""
|
205
|
+
total_time = max(1, self.current_makespan()) # Avoid division by zero
|
206
|
+
machine_busy_time = np.zeros(self.instance.num_machines)
|
207
|
+
|
208
|
+
for m_id, m_schedule in enumerate(self.dispatcher.schedule.schedule):
|
209
|
+
machine_busy_time[m_id] = sum(
|
210
|
+
op.operation.duration for op in m_schedule
|
211
|
+
)
|
212
|
+
|
213
|
+
return machine_busy_time / total_time
|
214
|
+
|
198
215
|
def _get_observation_space(self) -> gym.spaces.Dict:
|
199
216
|
"""Returns the observation space dictionary."""
|
200
217
|
num_edges = self.job_shop_graph.num_edges
|
201
|
-
dict_space:
|
218
|
+
dict_space: Dict[str, gym.Space] = {
|
202
219
|
ObservationSpaceKey.REMOVED_NODES.value: gym.spaces.MultiBinary(
|
203
220
|
len(self.job_shop_graph.nodes)
|
204
221
|
),
|
@@ -224,18 +241,23 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
224
241
|
def reset(
|
225
242
|
self,
|
226
243
|
*,
|
227
|
-
seed: int
|
228
|
-
options:
|
229
|
-
) ->
|
244
|
+
seed: Optional[int] = None,
|
245
|
+
options: Optional[Dict[str, Any]] = None,
|
246
|
+
) -> Tuple[ObservationDict, dict]:
|
230
247
|
"""Resets the environment."""
|
231
248
|
super().reset(seed=seed, options=options)
|
232
249
|
self.dispatcher.reset()
|
233
250
|
obs = self.get_observation()
|
234
|
-
return obs, {
|
251
|
+
return obs, {
|
252
|
+
"feature_names": self.composite_observer.column_names,
|
253
|
+
"available_operations_with_ids": (
|
254
|
+
self.get_available_actions_with_ids()
|
255
|
+
),
|
256
|
+
}
|
235
257
|
|
236
258
|
def step(
|
237
|
-
self, action:
|
238
|
-
) ->
|
259
|
+
self, action: Tuple[int, int]
|
260
|
+
) -> Tuple[ObservationDict, float, bool, bool, Dict[str, Any]]:
|
239
261
|
"""Takes a step in the environment.
|
240
262
|
|
241
263
|
Args:
|
@@ -254,9 +276,9 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
254
276
|
- Whether the episode was truncated (always False).
|
255
277
|
- A dictionary with additional information. The dictionary
|
256
278
|
contains the following keys: "feature_names", the names of the
|
257
|
-
features in the observation; "
|
258
|
-
|
259
|
-
|
279
|
+
features in the observation; and "available_operations_with_ids",
|
280
|
+
a list of available actions in the form of (operation_id,
|
281
|
+
machine_id, job_id).
|
260
282
|
"""
|
261
283
|
job_id, machine_id = action
|
262
284
|
operation = self.dispatcher.next_operation(job_id)
|
@@ -269,9 +291,11 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
269
291
|
reward = self.reward_function.last_reward
|
270
292
|
done = self.dispatcher.schedule.is_complete()
|
271
293
|
truncated = False
|
272
|
-
info:
|
294
|
+
info: Dict[str, Any] = {
|
273
295
|
"feature_names": self.composite_observer.column_names,
|
274
|
-
"
|
296
|
+
"available_operations_with_ids": (
|
297
|
+
self.get_available_actions_with_ids()
|
298
|
+
),
|
275
299
|
}
|
276
300
|
return obs, reward, done, truncated, info
|
277
301
|
|
@@ -322,6 +346,49 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
322
346
|
elif self.render_mode == "save_gif":
|
323
347
|
self.gantt_chart_creator.create_gif()
|
324
348
|
|
349
|
+
def get_available_actions_with_ids(self) -> List[Tuple[int, int, int]]:
|
350
|
+
"""Returns a list of available actions in the form of
|
351
|
+
(operation_id, machine_id, job_id)."""
|
352
|
+
available_operations = self.dispatcher.available_operations()
|
353
|
+
available_operations_with_ids = []
|
354
|
+
for operation in available_operations:
|
355
|
+
job_id = operation.job_id
|
356
|
+
operation_id = operation.operation_id
|
357
|
+
for machine_id in operation.machines:
|
358
|
+
available_operations_with_ids.append(
|
359
|
+
(operation_id, machine_id, job_id)
|
360
|
+
)
|
361
|
+
return available_operations_with_ids
|
362
|
+
|
363
|
+
def validate_action(self, action: Tuple[int, int]) -> None:
|
364
|
+
"""Validates that the action is legal in the current state.
|
365
|
+
|
366
|
+
Args:
|
367
|
+
action:
|
368
|
+
The action to validate. The action is a tuple of two integers
|
369
|
+
(job_id, machine_id).
|
370
|
+
|
371
|
+
Raises:
|
372
|
+
ValidationError: If the action is invalid.
|
373
|
+
"""
|
374
|
+
job_id, machine_id = action
|
375
|
+
if not 0 <= job_id < self.instance.num_jobs:
|
376
|
+
raise ValidationError(f"Invalid job_id {job_id}")
|
377
|
+
|
378
|
+
if not -1 <= machine_id < self.instance.num_machines:
|
379
|
+
raise ValidationError(f"Invalid machine_id {machine_id}")
|
380
|
+
|
381
|
+
# Check if job has operations left
|
382
|
+
job = self.instance.jobs[job_id]
|
383
|
+
if self.dispatcher.job_next_operation_index[job_id] >= len(job):
|
384
|
+
raise ValidationError(f"Job {job_id} has no operations left")
|
385
|
+
|
386
|
+
next_operation = self.dispatcher.next_operation(job_id)
|
387
|
+
if machine_id == -1 and len(next_operation.machines) > 1:
|
388
|
+
raise ValidationError(
|
389
|
+
f"Operation {next_operation} requires a machine_id"
|
390
|
+
)
|
391
|
+
|
325
392
|
|
326
393
|
if __name__ == "__main__":
|
327
394
|
from job_shop_lib.dispatching.feature_observers import (
|
@@ -333,7 +400,7 @@ if __name__ == "__main__":
|
|
333
400
|
|
334
401
|
instance = load_benchmark_instance("ft06")
|
335
402
|
job_shop_graph_ = build_disjunctive_graph(instance)
|
336
|
-
feature_observer_configs_ = [
|
403
|
+
feature_observer_configs_: List[DispatcherObserverConfig] = [
|
337
404
|
DispatcherObserverConfig(
|
338
405
|
FeatureObserverType.IS_READY,
|
339
406
|
kwargs={"feature_types": [FeatureType.JOBS]},
|
@@ -7,7 +7,7 @@ from typing import TypedDict
|
|
7
7
|
import numpy as np
|
8
8
|
|
9
9
|
from job_shop_lib.dispatching.feature_observers import FeatureType
|
10
|
-
from job_shop_lib.visualization import (
|
10
|
+
from job_shop_lib.visualization.gantt import (
|
11
11
|
PartialGanttChartPlotterConfig,
|
12
12
|
GifConfig,
|
13
13
|
VideoConfig,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Utility functions for reinforcement learning."""
|
2
2
|
|
3
|
-
from typing import TypeVar, Any
|
3
|
+
from typing import TypeVar, Any, Tuple, Optional, Type
|
4
4
|
|
5
5
|
import numpy as np
|
6
6
|
from numpy.typing import NDArray
|
@@ -12,9 +12,9 @@ T = TypeVar("T", bound=np.number)
|
|
12
12
|
|
13
13
|
def add_padding(
|
14
14
|
array: NDArray[Any],
|
15
|
-
output_shape:
|
15
|
+
output_shape: Tuple[int, ...],
|
16
16
|
padding_value: float = -1,
|
17
|
-
dtype:
|
17
|
+
dtype: Optional[Type[T]] = None,
|
18
18
|
) -> NDArray[T]:
|
19
19
|
"""Adds padding to the array.
|
20
20
|
|
@@ -1,60 +0,0 @@
|
|
1
|
-
"""Contains functions and classes for visualizing job shop scheduling problems.
|
2
|
-
|
3
|
-
.. autosummary::
|
4
|
-
|
5
|
-
plot_gantt_chart
|
6
|
-
get_partial_gantt_chart_plotter
|
7
|
-
PartialGanttChartPlotter
|
8
|
-
create_gantt_chart_video
|
9
|
-
create_gantt_chart_gif
|
10
|
-
plot_disjunctive_graph
|
11
|
-
plot_agent_task_graph
|
12
|
-
GanttChartCreator
|
13
|
-
GifConfig
|
14
|
-
VideoConfig
|
15
|
-
|
16
|
-
"""
|
17
|
-
|
18
|
-
from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
|
19
|
-
from job_shop_lib.visualization._gantt_chart_video_and_gif_creation import (
|
20
|
-
create_gantt_chart_gif,
|
21
|
-
create_gantt_chart_video,
|
22
|
-
create_gantt_chart_frames,
|
23
|
-
get_partial_gantt_chart_plotter,
|
24
|
-
create_video_from_frames,
|
25
|
-
create_gif_from_frames,
|
26
|
-
PartialGanttChartPlotter,
|
27
|
-
)
|
28
|
-
from job_shop_lib.visualization._plot_disjunctive_graph import (
|
29
|
-
plot_disjunctive_graph,
|
30
|
-
duration_labeler,
|
31
|
-
)
|
32
|
-
from job_shop_lib.visualization._plot_agent_task_graph import (
|
33
|
-
plot_agent_task_graph,
|
34
|
-
three_columns_layout,
|
35
|
-
)
|
36
|
-
from job_shop_lib.visualization._gantt_chart_creator import (
|
37
|
-
GanttChartCreator,
|
38
|
-
PartialGanttChartPlotterConfig,
|
39
|
-
GifConfig,
|
40
|
-
VideoConfig,
|
41
|
-
)
|
42
|
-
|
43
|
-
__all__ = [
|
44
|
-
"plot_gantt_chart",
|
45
|
-
"create_gantt_chart_video",
|
46
|
-
"create_gantt_chart_gif",
|
47
|
-
"create_gantt_chart_frames",
|
48
|
-
"get_partial_gantt_chart_plotter",
|
49
|
-
"create_gif_from_frames",
|
50
|
-
"create_video_from_frames",
|
51
|
-
"plot_disjunctive_graph",
|
52
|
-
"plot_agent_task_graph",
|
53
|
-
"three_columns_layout",
|
54
|
-
"GanttChartCreator",
|
55
|
-
"PartialGanttChartPlotterConfig",
|
56
|
-
"GifConfig",
|
57
|
-
"VideoConfig",
|
58
|
-
"PartialGanttChartPlotter",
|
59
|
-
"duration_labeler",
|
60
|
-
]
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""Contains functions and classes for visualizing job shop scheduling problems.
|
2
|
+
|
3
|
+
.. autosummary::
|
4
|
+
|
5
|
+
plot_gantt_chart
|
6
|
+
get_partial_gantt_chart_plotter
|
7
|
+
PartialGanttChartPlotter
|
8
|
+
create_gantt_chart_video
|
9
|
+
create_gantt_chart_gif
|
10
|
+
GanttChartCreator
|
11
|
+
GifConfig
|
12
|
+
VideoConfig
|
13
|
+
PartialGanttChartPlotterConfig
|
14
|
+
|
15
|
+
"""
|
16
|
+
|
17
|
+
from ._plot_gantt_chart import plot_gantt_chart
|
18
|
+
from ._gantt_chart_video_and_gif_creation import (
|
19
|
+
create_gantt_chart_gif,
|
20
|
+
create_gantt_chart_video,
|
21
|
+
create_gantt_chart_frames,
|
22
|
+
get_partial_gantt_chart_plotter,
|
23
|
+
create_video_from_frames,
|
24
|
+
create_gif_from_frames,
|
25
|
+
PartialGanttChartPlotter,
|
26
|
+
)
|
27
|
+
|
28
|
+
from ._gantt_chart_creator import (
|
29
|
+
GanttChartCreator,
|
30
|
+
PartialGanttChartPlotterConfig,
|
31
|
+
GifConfig,
|
32
|
+
VideoConfig,
|
33
|
+
)
|
34
|
+
|
35
|
+
__all__ = [
|
36
|
+
"plot_gantt_chart",
|
37
|
+
"create_gantt_chart_video",
|
38
|
+
"create_gantt_chart_gif",
|
39
|
+
"create_gantt_chart_frames",
|
40
|
+
"get_partial_gantt_chart_plotter",
|
41
|
+
"create_gif_from_frames",
|
42
|
+
"create_video_from_frames",
|
43
|
+
"GanttChartCreator",
|
44
|
+
"PartialGanttChartPlotterConfig",
|
45
|
+
"GifConfig",
|
46
|
+
"VideoConfig",
|
47
|
+
"PartialGanttChartPlotter",
|
48
|
+
]
|