job-shop-lib 1.0.2__py3-none-any.whl → 1.0.3__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 +26 -26
- job_shop_lib/_operation.py +2 -4
- 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 +3 -3
- job_shop_lib/dispatching/_factories.py +2 -3
- job_shop_lib/dispatching/_history_observer.py +1 -2
- job_shop_lib/dispatching/_optimal_operations_observer.py +3 -4
- job_shop_lib/dispatching/_ready_operation_filters.py +18 -19
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +3 -2
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +10 -12
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +1 -3
- job_shop_lib/dispatching/feature_observers/_factory.py +11 -12
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +8 -9
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -4
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +2 -4
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +2 -4
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +15 -20
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -10
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +2 -3
- job_shop_lib/dispatching/rules/_utils.py +7 -8
- job_shop_lib/generation/_general_instance_generator.py +22 -20
- job_shop_lib/generation/_instance_generator.py +8 -8
- job_shop_lib/generation/_transformations.py +4 -5
- job_shop_lib/generation/_utils.py +16 -8
- job_shop_lib/graphs/_job_shop_graph.py +13 -14
- job_shop_lib/graphs/_node.py +6 -12
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +2 -4
- job_shop_lib/reinforcement_learning/__init__.py +2 -1
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +17 -17
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +1 -1
- job_shop_lib/reinforcement_learning/_reward_observers.py +1 -3
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +22 -24
- job_shop_lib/reinforcement_learning/_utils.py +2 -2
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +11 -11
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +21 -21
- job_shop_lib/visualization/gantt/_plot_gantt_chart.py +12 -14
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +15 -17
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +22 -24
- {job_shop_lib-1.0.2.dist-info → job_shop_lib-1.0.3.dist-info}/METADATA +1 -1
- {job_shop_lib-1.0.2.dist-info → job_shop_lib-1.0.3.dist-info}/RECORD +46 -46
- {job_shop_lib-1.0.2.dist-info → job_shop_lib-1.0.3.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.2.dist-info → job_shop_lib-1.0.3.dist-info}/WHEEL +0 -0
@@ -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
|
6
6
|
|
7
7
|
import matplotlib.pyplot as plt
|
8
8
|
import gymnasium as gym
|
@@ -140,24 +140,22 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
140
140
|
self,
|
141
141
|
job_shop_graph: JobShopGraph,
|
142
142
|
feature_observer_configs: Sequence[
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
FeatureObserverConfig,
|
148
|
-
],
|
143
|
+
str
|
144
|
+
| FeatureObserverType
|
145
|
+
| type[FeatureObserver]
|
146
|
+
| FeatureObserverConfig
|
149
147
|
],
|
150
148
|
reward_function_config: DispatcherObserverConfig[
|
151
|
-
|
149
|
+
type[RewardObserver]
|
152
150
|
] = DispatcherObserverConfig(class_type=MakespanReward),
|
153
151
|
graph_updater_config: DispatcherObserverConfig[
|
154
|
-
|
152
|
+
type[GraphUpdater]
|
155
153
|
] = DispatcherObserverConfig(class_type=ResidualGraphUpdater),
|
156
|
-
ready_operations_filter:
|
157
|
-
Callable[[Dispatcher,
|
158
|
-
|
159
|
-
render_mode:
|
160
|
-
render_config:
|
154
|
+
ready_operations_filter: (
|
155
|
+
Callable[[Dispatcher, list[Operation]], list[Operation]] | None
|
156
|
+
) = filter_dominated_operations,
|
157
|
+
render_mode: str | None = None,
|
158
|
+
render_config: RenderConfig | None = None,
|
161
159
|
use_padding: bool = True,
|
162
160
|
) -> None:
|
163
161
|
super().__init__()
|
@@ -224,7 +222,7 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
224
222
|
def _get_observation_space(self) -> gym.spaces.Dict:
|
225
223
|
"""Returns the observation space dictionary."""
|
226
224
|
num_edges = self.job_shop_graph.num_edges
|
227
|
-
dict_space:
|
225
|
+
dict_space: dict[str, gym.Space] = {
|
228
226
|
ObservationSpaceKey.REMOVED_NODES.value: gym.spaces.MultiBinary(
|
229
227
|
len(self.job_shop_graph.nodes)
|
230
228
|
),
|
@@ -250,9 +248,9 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
250
248
|
def reset(
|
251
249
|
self,
|
252
250
|
*,
|
253
|
-
seed:
|
254
|
-
options:
|
255
|
-
) ->
|
251
|
+
seed: int | None = None,
|
252
|
+
options: dict[str, Any] | None = None,
|
253
|
+
) -> tuple[ObservationDict, dict[str, Any]]:
|
256
254
|
"""Resets the environment.
|
257
255
|
|
258
256
|
Args:
|
@@ -284,8 +282,8 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
284
282
|
}
|
285
283
|
|
286
284
|
def step(
|
287
|
-
self, action:
|
288
|
-
) ->
|
285
|
+
self, action: tuple[int, int]
|
286
|
+
) -> tuple[ObservationDict, float, bool, bool, dict[str, Any]]:
|
289
287
|
"""Takes a step in the environment.
|
290
288
|
|
291
289
|
Args:
|
@@ -319,7 +317,7 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
319
317
|
reward = self.reward_function.last_reward
|
320
318
|
done = self.dispatcher.schedule.is_complete()
|
321
319
|
truncated = False
|
322
|
-
info:
|
320
|
+
info: dict[str, Any] = {
|
323
321
|
"feature_names": self.composite_observer.column_names,
|
324
322
|
"available_operations_with_ids": (
|
325
323
|
self.get_available_actions_with_ids()
|
@@ -374,7 +372,7 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
374
372
|
elif self.render_mode == "save_gif":
|
375
373
|
self.gantt_chart_creator.create_gif()
|
376
374
|
|
377
|
-
def get_available_actions_with_ids(self) ->
|
375
|
+
def get_available_actions_with_ids(self) -> list[tuple[int, int, int]]:
|
378
376
|
"""Returns a list of available actions in the form of
|
379
377
|
(operation_id, machine_id, job_id)."""
|
380
378
|
available_operations = self.dispatcher.available_operations()
|
@@ -388,7 +386,7 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
388
386
|
)
|
389
387
|
return available_operations_with_ids
|
390
388
|
|
391
|
-
def validate_action(self, action:
|
389
|
+
def validate_action(self, action: tuple[int, int]) -> None:
|
392
390
|
"""Validates that the action is legal in the current state.
|
393
391
|
|
394
392
|
Args:
|
@@ -428,7 +426,7 @@ if __name__ == "__main__":
|
|
428
426
|
|
429
427
|
instance = load_benchmark_instance("ft06")
|
430
428
|
job_shop_graph_ = build_disjunctive_graph(instance)
|
431
|
-
feature_observer_configs_:
|
429
|
+
feature_observer_configs_: list[DispatcherObserverConfig] = [
|
432
430
|
DispatcherObserverConfig(
|
433
431
|
FeatureObserverType.IS_READY,
|
434
432
|
kwargs={"feature_types": [FeatureType.JOBS]},
|
@@ -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
|
4
4
|
|
5
5
|
import numpy as np
|
6
6
|
from numpy.typing import NDArray
|
@@ -15,7 +15,7 @@ def add_padding(
|
|
15
15
|
array: NDArray[Any],
|
16
16
|
output_shape: tuple[int, ...],
|
17
17
|
padding_value: float = -1,
|
18
|
-
dtype:
|
18
|
+
dtype: type[T] | None = None,
|
19
19
|
) -> NDArray[T]:
|
20
20
|
"""Adds padding to the array.
|
21
21
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Home of the `GanttChartCreator` class and its configuration types."""
|
2
2
|
|
3
|
-
from typing import TypedDict
|
3
|
+
from typing import TypedDict
|
4
4
|
import matplotlib.pyplot as plt
|
5
5
|
|
6
6
|
from job_shop_lib.dispatching import (
|
@@ -24,7 +24,7 @@ class PartialGanttChartPlotterConfig(TypedDict, total=False):
|
|
24
24
|
- :func:`get_partial_gantt_chart_plotter`
|
25
25
|
"""
|
26
26
|
|
27
|
-
title:
|
27
|
+
title: str | None
|
28
28
|
"""The title of the Gantt chart."""
|
29
29
|
|
30
30
|
cmap: str
|
@@ -43,7 +43,7 @@ class GifConfig(TypedDict, total=False):
|
|
43
43
|
:func:`create_gantt_chart_gif`
|
44
44
|
"""
|
45
45
|
|
46
|
-
gif_path:
|
46
|
+
gif_path: str | None
|
47
47
|
"""The path to save the GIF. It must end with '.gif'."""
|
48
48
|
|
49
49
|
fps: int
|
@@ -52,7 +52,7 @@ class GifConfig(TypedDict, total=False):
|
|
52
52
|
remove_frames: bool
|
53
53
|
"""Whether to remove the frames after creating the GIF."""
|
54
54
|
|
55
|
-
frames_dir:
|
55
|
+
frames_dir: str | None
|
56
56
|
"""The directory to store the frames."""
|
57
57
|
|
58
58
|
plot_current_time: bool
|
@@ -68,7 +68,7 @@ class VideoConfig(TypedDict, total=False):
|
|
68
68
|
:func:`create_gantt_chart_video`
|
69
69
|
"""
|
70
70
|
|
71
|
-
video_path:
|
71
|
+
video_path: str | None
|
72
72
|
"""The path to save the video. It must end with a valid video extension
|
73
73
|
(e.g., '.mp4')."""
|
74
74
|
|
@@ -78,7 +78,7 @@ class VideoConfig(TypedDict, total=False):
|
|
78
78
|
remove_frames: bool
|
79
79
|
"""Whether to remove the frames after creating the video."""
|
80
80
|
|
81
|
-
frames_dir:
|
81
|
+
frames_dir: str | None
|
82
82
|
"""The directory to store the frames."""
|
83
83
|
|
84
84
|
plot_current_time: bool
|
@@ -164,11 +164,11 @@ class GanttChartCreator:
|
|
164
164
|
def __init__(
|
165
165
|
self,
|
166
166
|
dispatcher: Dispatcher,
|
167
|
-
partial_gantt_chart_plotter_config:
|
168
|
-
PartialGanttChartPlotterConfig
|
169
|
-
|
170
|
-
gif_config:
|
171
|
-
video_config:
|
167
|
+
partial_gantt_chart_plotter_config: (
|
168
|
+
PartialGanttChartPlotterConfig | None
|
169
|
+
) = None,
|
170
|
+
gif_config: GifConfig | None = None,
|
171
|
+
video_config: VideoConfig | None = None,
|
172
172
|
):
|
173
173
|
if gif_config is None:
|
174
174
|
gif_config = {}
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
5
|
import shutil
|
6
|
-
from typing import Sequence, Protocol,
|
6
|
+
from typing import Sequence, Protocol, Any
|
7
7
|
|
8
8
|
import imageio
|
9
9
|
import matplotlib.pyplot as plt
|
@@ -43,9 +43,9 @@ class PartialGanttChartPlotter(Protocol):
|
|
43
43
|
def __call__(
|
44
44
|
self,
|
45
45
|
schedule: Schedule,
|
46
|
-
makespan:
|
47
|
-
available_operations:
|
48
|
-
current_time:
|
46
|
+
makespan: int | None = None,
|
47
|
+
available_operations: list[Operation] | None = None,
|
48
|
+
current_time: int | None = None,
|
49
49
|
) -> Figure:
|
50
50
|
"""Plots a Gantt chart for an unfinished schedule.
|
51
51
|
|
@@ -65,7 +65,7 @@ class PartialGanttChartPlotter(Protocol):
|
|
65
65
|
|
66
66
|
|
67
67
|
def get_partial_gantt_chart_plotter(
|
68
|
-
title:
|
68
|
+
title: str | None = None,
|
69
69
|
cmap: str = "viridis",
|
70
70
|
show_available_operations: bool = False,
|
71
71
|
**kwargs: Any,
|
@@ -95,9 +95,9 @@ def get_partial_gantt_chart_plotter(
|
|
95
95
|
|
96
96
|
def plot_function(
|
97
97
|
schedule: Schedule,
|
98
|
-
makespan:
|
99
|
-
available_operations:
|
100
|
-
current_time:
|
98
|
+
makespan: int | None = None,
|
99
|
+
available_operations: list[Operation] | None = None,
|
100
|
+
current_time: int | None = None,
|
101
101
|
) -> Figure:
|
102
102
|
fig, ax = plot_gantt_chart(
|
103
103
|
schedule, title=title, cmap_name=cmap, xlim=makespan, **kwargs
|
@@ -136,14 +136,14 @@ def get_partial_gantt_chart_plotter(
|
|
136
136
|
# pylint: disable=too-many-arguments
|
137
137
|
def create_gantt_chart_gif(
|
138
138
|
instance: JobShopInstance,
|
139
|
-
gif_path:
|
140
|
-
solver:
|
141
|
-
plot_function:
|
139
|
+
gif_path: str | None = None,
|
140
|
+
solver: DispatchingRuleSolver | None = None,
|
141
|
+
plot_function: PartialGanttChartPlotter | None = None,
|
142
142
|
fps: int = 1,
|
143
143
|
remove_frames: bool = True,
|
144
|
-
frames_dir:
|
144
|
+
frames_dir: str | None = None,
|
145
145
|
plot_current_time: bool = True,
|
146
|
-
schedule_history:
|
146
|
+
schedule_history: Sequence[ScheduledOperation] | None = None,
|
147
147
|
) -> None:
|
148
148
|
"""Creates a GIF of the schedule being built.
|
149
149
|
|
@@ -205,14 +205,14 @@ def create_gantt_chart_gif(
|
|
205
205
|
# pylint: disable=too-many-arguments
|
206
206
|
def create_gantt_chart_video(
|
207
207
|
instance: JobShopInstance,
|
208
|
-
video_path:
|
209
|
-
solver:
|
210
|
-
plot_function:
|
208
|
+
video_path: str | None = None,
|
209
|
+
solver: DispatchingRuleSolver | None = None,
|
210
|
+
plot_function: PartialGanttChartPlotter | None = None,
|
211
211
|
fps: int = 1,
|
212
212
|
remove_frames: bool = True,
|
213
|
-
frames_dir:
|
213
|
+
frames_dir: str | None = None,
|
214
214
|
plot_current_time: bool = True,
|
215
|
-
schedule_history:
|
215
|
+
schedule_history: Sequence[ScheduledOperation] | None = None,
|
216
216
|
) -> None:
|
217
217
|
"""Creates a video of the schedule being built.
|
218
218
|
|
@@ -271,10 +271,10 @@ def create_gantt_chart_video(
|
|
271
271
|
def create_gantt_chart_frames(
|
272
272
|
frames_dir: str,
|
273
273
|
instance: JobShopInstance,
|
274
|
-
solver:
|
274
|
+
solver: DispatchingRuleSolver | None,
|
275
275
|
plot_function: PartialGanttChartPlotter,
|
276
276
|
plot_current_time: bool = True,
|
277
|
-
schedule_history:
|
277
|
+
schedule_history: Sequence[ScheduledOperation] | None = None,
|
278
278
|
) -> None:
|
279
279
|
"""Creates frames of the Gantt chart for the schedule being built.
|
280
280
|
|
@@ -414,7 +414,7 @@ def resize_image_to_macro_block(
|
|
414
414
|
return image
|
415
415
|
|
416
416
|
|
417
|
-
def _load_images(frames_dir: str) ->
|
417
|
+
def _load_images(frames_dir: str) -> list:
|
418
418
|
frames = [
|
419
419
|
os.path.join(frames_dir, frame)
|
420
420
|
for frame in sorted(os.listdir(frames_dir))
|
@@ -1,7 +1,5 @@
|
|
1
1
|
"""Module for plotting static Gantt charts for job shop schedules."""
|
2
2
|
|
3
|
-
from typing import Optional, List, Tuple, Dict
|
4
|
-
|
5
3
|
from matplotlib.figure import Figure
|
6
4
|
import matplotlib.pyplot as plt
|
7
5
|
from matplotlib.colors import Normalize
|
@@ -15,16 +13,16 @@ _Y_POSITION_INCREMENT = 10
|
|
15
13
|
|
16
14
|
def plot_gantt_chart(
|
17
15
|
schedule: Schedule,
|
18
|
-
title:
|
16
|
+
title: str | None = None,
|
19
17
|
cmap_name: str = "viridis",
|
20
|
-
xlim:
|
18
|
+
xlim: int | None = None,
|
21
19
|
number_of_x_ticks: int = 15,
|
22
|
-
job_labels:
|
23
|
-
machine_labels:
|
20
|
+
job_labels: list[str] | None = None,
|
21
|
+
machine_labels: list[str] | None = None,
|
24
22
|
legend_title: str = "",
|
25
23
|
x_label: str = "Time units",
|
26
24
|
y_label: str = "Machines",
|
27
|
-
) ->
|
25
|
+
) -> tuple[Figure, plt.Axes]:
|
28
26
|
"""Plots a Gantt chart for the schedule.
|
29
27
|
|
30
28
|
This function generates a Gantt chart that visualizes the schedule of jobs
|
@@ -90,10 +88,10 @@ def plot_gantt_chart(
|
|
90
88
|
|
91
89
|
def _initialize_plot(
|
92
90
|
schedule: Schedule,
|
93
|
-
title:
|
91
|
+
title: str | None,
|
94
92
|
x_label: str = "Time units",
|
95
93
|
y_label: str = "Machines",
|
96
|
-
) ->
|
94
|
+
) -> tuple[Figure, plt.Axes]:
|
97
95
|
"""Initializes the plot."""
|
98
96
|
fig, ax = plt.subplots()
|
99
97
|
ax.set_xlabel(x_label)
|
@@ -110,8 +108,8 @@ def _plot_machine_schedules(
|
|
110
108
|
schedule: Schedule,
|
111
109
|
ax: plt.Axes,
|
112
110
|
cmap_name: str,
|
113
|
-
job_labels:
|
114
|
-
) ->
|
111
|
+
job_labels: list[str] | None,
|
112
|
+
) -> dict[int, Patch]:
|
115
113
|
"""Plots the schedules for each machine."""
|
116
114
|
max_job_id = schedule.instance.num_jobs - 1
|
117
115
|
cmap = plt.get_cmap(cmap_name, max_job_id + 1)
|
@@ -137,7 +135,7 @@ def _plot_machine_schedules(
|
|
137
135
|
return legend_handles
|
138
136
|
|
139
137
|
|
140
|
-
def _get_job_label(job_labels:
|
138
|
+
def _get_job_label(job_labels: list[str] | None, job_id: int) -> str:
|
141
139
|
"""Returns the label for the job."""
|
142
140
|
if job_labels is None:
|
143
141
|
return f"Job {job_id}"
|
@@ -161,7 +159,7 @@ def _plot_scheduled_operation(
|
|
161
159
|
|
162
160
|
|
163
161
|
def _configure_legend(
|
164
|
-
ax: plt.Axes, legend_handles:
|
162
|
+
ax: plt.Axes, legend_handles: dict[int, Patch], legend_title: str
|
165
163
|
):
|
166
164
|
"""Configures the legend for the plot."""
|
167
165
|
sorted_legend_handles = [
|
@@ -178,7 +176,7 @@ def _configure_legend(
|
|
178
176
|
def _configure_axes(
|
179
177
|
schedule: Schedule,
|
180
178
|
ax: plt.Axes,
|
181
|
-
xlim:
|
179
|
+
xlim: int | None,
|
182
180
|
number_of_x_ticks: int,
|
183
181
|
machine_labels: list[str] | None,
|
184
182
|
):
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Module for visualizing the disjunctive graph of a job shop instance."""
|
2
2
|
|
3
3
|
import functools
|
4
|
-
from typing import Any
|
4
|
+
from typing import Any
|
5
5
|
from collections.abc import Callable, Sequence, Iterable
|
6
6
|
import warnings
|
7
7
|
import copy
|
@@ -23,7 +23,7 @@ from job_shop_lib.graphs import (
|
|
23
23
|
from job_shop_lib.exceptions import ValidationError
|
24
24
|
|
25
25
|
|
26
|
-
Layout = Callable[[nx.Graph],
|
26
|
+
Layout = Callable[[nx.Graph], dict[str, tuple[float, float]]]
|
27
27
|
|
28
28
|
|
29
29
|
def duration_labeler(node: Node) -> str:
|
@@ -56,10 +56,10 @@ def duration_labeler(node: Node) -> str:
|
|
56
56
|
# pylint: disable=too-many-arguments, too-many-locals, too-many-statements
|
57
57
|
# pylint: disable=too-many-branches, line-too-long
|
58
58
|
def plot_disjunctive_graph(
|
59
|
-
job_shop:
|
59
|
+
job_shop: JobShopGraph | JobShopInstance,
|
60
60
|
*,
|
61
|
-
title:
|
62
|
-
figsize:
|
61
|
+
title: str | None = None,
|
62
|
+
figsize: tuple[float, float] = (6, 4),
|
63
63
|
node_size: int = 1600,
|
64
64
|
edge_width: int = 2,
|
65
65
|
font_size: int = 10,
|
@@ -67,27 +67,25 @@ def plot_disjunctive_graph(
|
|
67
67
|
alpha: float = 0.95,
|
68
68
|
operation_node_labeler: Callable[[Node], str] = duration_labeler,
|
69
69
|
node_font_color: str = "white",
|
70
|
-
machine_colors:
|
71
|
-
Dict[int, Tuple[float, float, float, float]]
|
72
|
-
] = None,
|
70
|
+
machine_colors: dict[int, tuple[float, float, float, float]] | None = None,
|
73
71
|
color_map: str = "Dark2_r",
|
74
72
|
disjunctive_edge_color: str = "red",
|
75
73
|
conjunctive_edge_color: str = "black",
|
76
|
-
layout:
|
77
|
-
draw_disjunctive_edges:
|
78
|
-
conjunctive_edges_additional_params:
|
79
|
-
disjunctive_edges_additional_params:
|
74
|
+
layout: Layout | None = None,
|
75
|
+
draw_disjunctive_edges: bool | str = True,
|
76
|
+
conjunctive_edges_additional_params: dict[str, Any] | None = None,
|
77
|
+
disjunctive_edges_additional_params: dict[str, Any] | None = None,
|
80
78
|
conjunctive_patch_label: str = "Conjunctive edges",
|
81
79
|
disjunctive_patch_label: str = "Disjunctive edges",
|
82
80
|
legend_text: str = "$p_{ij}=$duration of $O_{ij}$",
|
83
81
|
show_machine_colors_in_legend: bool = True,
|
84
|
-
machine_labels:
|
82
|
+
machine_labels: Sequence[str] | None = None,
|
85
83
|
legend_location: str = "upper left",
|
86
|
-
legend_bbox_to_anchor:
|
84
|
+
legend_bbox_to_anchor: tuple[float, float] = (1.01, 1),
|
87
85
|
start_node_label: str = "$S$",
|
88
86
|
end_node_label: str = "$T$",
|
89
87
|
font_family: str = "sans-serif",
|
90
|
-
) ->
|
88
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
91
89
|
r"""Plots the disjunctive graph of the given job shop instance or graph.
|
92
90
|
|
93
91
|
Args:
|
@@ -240,7 +238,7 @@ def plot_disjunctive_graph(
|
|
240
238
|
# Draw nodes
|
241
239
|
# ----------
|
242
240
|
operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
243
|
-
cmap_func:
|
241
|
+
cmap_func: matplotlib.colors.Colormap | None = None
|
244
242
|
if machine_colors is None:
|
245
243
|
machine_colors = {}
|
246
244
|
cmap_func = matplotlib.colormaps.get_cmap(color_map)
|
@@ -289,7 +287,7 @@ def plot_disjunctive_graph(
|
|
289
287
|
for u, v, d in job_shop_graph.graph.edges(data=True)
|
290
288
|
if d["type"] == EdgeType.CONJUNCTIVE
|
291
289
|
]
|
292
|
-
disjunctive_edges: Iterable[
|
290
|
+
disjunctive_edges: Iterable[tuple[int, int]] = [
|
293
291
|
(u, v)
|
294
292
|
for u, v, d in job_shop_graph.graph.edges(data=True)
|
295
293
|
if d["type"] == EdgeType.DISJUNCTIVE
|
@@ -10,7 +10,7 @@ all operation nodes from the same job are connected by non-directed edges too.
|
|
10
10
|
|
11
11
|
from collections.abc import Callable
|
12
12
|
from copy import deepcopy
|
13
|
-
from typing import
|
13
|
+
from typing import Any
|
14
14
|
|
15
15
|
import matplotlib.pyplot as plt
|
16
16
|
import matplotlib.colors as mcolors
|
@@ -22,24 +22,22 @@ from job_shop_lib.graphs import NodeType, JobShopGraph, Node
|
|
22
22
|
def plot_resource_task_graph(
|
23
23
|
job_shop_graph: JobShopGraph,
|
24
24
|
*,
|
25
|
-
title:
|
26
|
-
figsize:
|
27
|
-
layout:
|
25
|
+
title: str | None = None,
|
26
|
+
figsize: tuple[int, int] = (10, 10),
|
27
|
+
layout: dict[Node, tuple[float, float]] | None = None,
|
28
28
|
node_size: int = 1200,
|
29
29
|
node_font_color: str = "k",
|
30
30
|
font_size: int = 10,
|
31
31
|
alpha: float = 0.95,
|
32
32
|
add_legend: bool = False,
|
33
|
-
node_shapes:
|
34
|
-
node_color_map:
|
35
|
-
Callable[[Node],
|
36
|
-
|
37
|
-
default_node_color:
|
38
|
-
str, Tuple[float, float, float, float]
|
39
|
-
] = "lightblue",
|
33
|
+
node_shapes: dict[str, str] | None = None,
|
34
|
+
node_color_map: (
|
35
|
+
Callable[[Node], tuple[float, float, float, float]] | None
|
36
|
+
) = None,
|
37
|
+
default_node_color: str | tuple[float, float, float, float] = "lightblue",
|
40
38
|
machine_color_map_name: str = "tab10",
|
41
39
|
legend_text: str = "$p_{ij}$ = duration of $O_{ij}$",
|
42
|
-
edge_additional_params:
|
40
|
+
edge_additional_params: dict[str, Any] | None = None,
|
43
41
|
draw_only_one_edge: bool = False,
|
44
42
|
) -> plt.Figure:
|
45
43
|
"""Returns a plot of the hetereogeneous graph of the instance.
|
@@ -203,17 +201,17 @@ def _get_node_label(node: Node) -> str:
|
|
203
201
|
|
204
202
|
|
205
203
|
def _color_to_rgba(
|
206
|
-
color:
|
207
|
-
) ->
|
204
|
+
color: str | tuple[float, float, float, float],
|
205
|
+
) -> tuple[float, float, float, float]:
|
208
206
|
if isinstance(color, str):
|
209
207
|
return mcolors.to_rgba(color)
|
210
208
|
return color
|
211
209
|
|
212
210
|
|
213
211
|
def color_nodes_by_machine(
|
214
|
-
machine_colors:
|
215
|
-
default_color:
|
216
|
-
) -> Callable[[Node],
|
212
|
+
machine_colors: dict[int, tuple[float, float, float, float]],
|
213
|
+
default_color: str | tuple[float, float, float, float],
|
214
|
+
) -> Callable[[Node], tuple[float, float, float, float]]:
|
217
215
|
"""Returns a function that assigns a color to a node based on its type.
|
218
216
|
|
219
217
|
The function returns a color based on the node type. If the node is an
|
@@ -234,7 +232,7 @@ def color_nodes_by_machine(
|
|
234
232
|
values of the color to use in the plot.
|
235
233
|
"""
|
236
234
|
|
237
|
-
def _get_node_color(node: Node) ->
|
235
|
+
def _get_node_color(node: Node) -> tuple[float, float, float, float]:
|
238
236
|
if node.node_type == NodeType.OPERATION:
|
239
237
|
return machine_colors[node.operation.machine_id]
|
240
238
|
if node.node_type == NodeType.MACHINE:
|
@@ -252,7 +250,7 @@ def three_columns_layout(
|
|
252
250
|
rightmost_position: float = 0.9,
|
253
251
|
topmost_position: float = 1.0,
|
254
252
|
bottommost_position: float = 0.0,
|
255
|
-
) ->
|
253
|
+
) -> dict[Node, tuple[float, float]]:
|
256
254
|
"""Generates coordinates for a three-column grid layout.
|
257
255
|
|
258
256
|
1. Left column: Machine nodes (M1, M2, etc.)
|
@@ -321,7 +319,7 @@ def three_columns_layout(
|
|
321
319
|
total_positions = len(operation_nodes) + len(global_nodes) * 2
|
322
320
|
y_spacing = (topmost_position - bottommost_position) / total_positions
|
323
321
|
|
324
|
-
layout:
|
322
|
+
layout: dict[Node, tuple[float, float]] = {}
|
325
323
|
|
326
324
|
machines_spacing_multiplier = len(operation_nodes) // len(machine_nodes)
|
327
325
|
layout.update(
|
@@ -364,7 +362,7 @@ def three_columns_layout(
|
|
364
362
|
|
365
363
|
def _get_x_positions(
|
366
364
|
leftmost_position: float, rightmost_position: float
|
367
|
-
) ->
|
365
|
+
) -> dict[str, float]:
|
368
366
|
center_position = (
|
369
367
|
leftmost_position + (rightmost_position - leftmost_position) / 2
|
370
368
|
)
|
@@ -376,12 +374,12 @@ def _get_x_positions(
|
|
376
374
|
|
377
375
|
|
378
376
|
def _assign_positions_from_top(
|
379
|
-
nodes:
|
377
|
+
nodes: list[Node],
|
380
378
|
x: float,
|
381
379
|
top: float,
|
382
380
|
y_spacing: float,
|
383
|
-
) ->
|
384
|
-
layout:
|
381
|
+
) -> dict[Node, tuple[float, float]]:
|
382
|
+
layout: dict[Node, tuple[float, float]] = {}
|
385
383
|
for i, node in enumerate(nodes):
|
386
384
|
y = top - (i + 1) * y_spacing
|
387
385
|
layout[node] = (x, y)
|