job-shop-lib 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +20 -0
- job_shop_lib/base_solver.py +37 -0
- job_shop_lib/benchmarking/__init__.py +78 -0
- job_shop_lib/benchmarking/benchmark_instances.json +1 -0
- job_shop_lib/benchmarking/load_benchmark.py +142 -0
- job_shop_lib/cp_sat/__init__.py +5 -0
- job_shop_lib/cp_sat/ortools_solver.py +201 -0
- job_shop_lib/dispatching/__init__.py +49 -0
- job_shop_lib/dispatching/dispatcher.py +269 -0
- job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
- job_shop_lib/dispatching/dispatching_rules.py +160 -0
- job_shop_lib/dispatching/factories.py +206 -0
- job_shop_lib/dispatching/pruning_functions.py +116 -0
- job_shop_lib/exceptions.py +26 -0
- job_shop_lib/generators/__init__.py +7 -0
- job_shop_lib/generators/basic_generator.py +197 -0
- job_shop_lib/graphs/__init__.py +52 -0
- job_shop_lib/graphs/build_agent_task_graph.py +209 -0
- job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
- job_shop_lib/graphs/constants.py +21 -0
- job_shop_lib/graphs/job_shop_graph.py +159 -0
- job_shop_lib/graphs/node.py +147 -0
- job_shop_lib/job_shop_instance.py +355 -0
- job_shop_lib/operation.py +120 -0
- job_shop_lib/schedule.py +180 -0
- job_shop_lib/scheduled_operation.py +97 -0
- job_shop_lib/visualization/__init__.py +25 -0
- job_shop_lib/visualization/agent_task_graph.py +257 -0
- job_shop_lib/visualization/create_gif.py +191 -0
- job_shop_lib/visualization/disjunctive_graph.py +206 -0
- job_shop_lib/visualization/gantt_chart.py +147 -0
- job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
- job_shop_lib-0.1.0.dist-info/METADATA +363 -0
- job_shop_lib-0.1.0.dist-info/RECORD +35 -0
- job_shop_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,191 @@
|
|
1
|
+
"""Module for creating a GIF of the schedule being built by a
|
2
|
+
dispatching rule solver."""
|
3
|
+
|
4
|
+
import os
|
5
|
+
import pathlib
|
6
|
+
import shutil
|
7
|
+
from typing import Callable
|
8
|
+
|
9
|
+
import imageio
|
10
|
+
import matplotlib.pyplot as plt
|
11
|
+
from matplotlib.figure import Figure
|
12
|
+
|
13
|
+
from job_shop_lib import JobShopInstance, Schedule, Operation
|
14
|
+
from job_shop_lib.dispatching import DispatchingRuleSolver, Dispatcher
|
15
|
+
from job_shop_lib.visualization.gantt_chart import plot_gantt_chart
|
16
|
+
|
17
|
+
|
18
|
+
# Most of the arguments are optional with default values. There is no way to
|
19
|
+
# reduce the number of arguments without losing functionality.
|
20
|
+
# pylint: disable=too-many-arguments
|
21
|
+
def create_gif(
|
22
|
+
gif_path: str,
|
23
|
+
instance: JobShopInstance,
|
24
|
+
solver: DispatchingRuleSolver,
|
25
|
+
plot_function: (
|
26
|
+
Callable[[Schedule, int, list[Operation] | None], Figure] | None
|
27
|
+
) = None,
|
28
|
+
fps: int = 1,
|
29
|
+
remove_frames: bool = True,
|
30
|
+
frames_dir: str | None = None,
|
31
|
+
plot_current_time: bool = True,
|
32
|
+
) -> None:
|
33
|
+
"""Creates a GIF of the schedule being built by the given solver.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
gif_path:
|
37
|
+
The path to save the GIF file. It should end with ".gif".
|
38
|
+
instance:
|
39
|
+
The instance of the job shop problem to be scheduled.
|
40
|
+
solver:
|
41
|
+
The dispatching rule solver to use.
|
42
|
+
plot_function:
|
43
|
+
A function that plots a Gantt chart for a schedule. It
|
44
|
+
should take a `Schedule` object and the makespan of the schedule as
|
45
|
+
input and return a `Figure` object. If not provided, a default
|
46
|
+
function is used.
|
47
|
+
fps:
|
48
|
+
The number of frames per second in the GIF.
|
49
|
+
remove_frames:
|
50
|
+
Whether to remove the frames after creating the GIF.
|
51
|
+
frames_dir:
|
52
|
+
The directory to save the frames in. If not provided,
|
53
|
+
`gif_path.replace(".gif", "") + "_frames"` is used.
|
54
|
+
plot_current_time:
|
55
|
+
Whether to plot a vertical line at the current time.
|
56
|
+
"""
|
57
|
+
if plot_function is None:
|
58
|
+
plot_function = plot_gantt_chart_wrapper()
|
59
|
+
|
60
|
+
if frames_dir is None:
|
61
|
+
# Use the name of the GIF file as the directory name
|
62
|
+
frames_dir = gif_path.replace(".gif", "") + "_frames"
|
63
|
+
path = pathlib.Path(frames_dir)
|
64
|
+
path.mkdir(exist_ok=True)
|
65
|
+
frames_dir = str(path)
|
66
|
+
create_gantt_chart_frames(
|
67
|
+
frames_dir, instance, solver, plot_function, plot_current_time
|
68
|
+
)
|
69
|
+
create_gif_from_frames(frames_dir, gif_path, fps)
|
70
|
+
|
71
|
+
if remove_frames:
|
72
|
+
shutil.rmtree(frames_dir)
|
73
|
+
|
74
|
+
|
75
|
+
def plot_gantt_chart_wrapper(
|
76
|
+
title: str | None = None,
|
77
|
+
cmap: str = "viridis",
|
78
|
+
show_available_operations: bool = False,
|
79
|
+
) -> Callable[[Schedule, int, list[Operation] | None], Figure]:
|
80
|
+
"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
title: The title of the Gantt chart.
|
84
|
+
cmap: The name of the colormap to use.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
A function that plots a Gantt chart for a schedule. The function takes
|
88
|
+
a `Schedule` object and the makespan of the schedule as input and
|
89
|
+
returns a `Figure` object.
|
90
|
+
"""
|
91
|
+
|
92
|
+
def plot_function(
|
93
|
+
schedule: Schedule,
|
94
|
+
makespan: int,
|
95
|
+
available_operations: list | None = None,
|
96
|
+
) -> Figure:
|
97
|
+
fig, ax = plot_gantt_chart(
|
98
|
+
schedule, title=title, cmap_name=cmap, xlim=makespan
|
99
|
+
)
|
100
|
+
|
101
|
+
if not show_available_operations or available_operations is None:
|
102
|
+
return fig
|
103
|
+
|
104
|
+
operations_text = "\n".join(
|
105
|
+
str(operation) for operation in available_operations
|
106
|
+
)
|
107
|
+
text = f"Available operations:\n{operations_text}"
|
108
|
+
# Print the available operations at the bottom right corner
|
109
|
+
# of the Gantt chart
|
110
|
+
fig.text(
|
111
|
+
1.25,
|
112
|
+
0.05,
|
113
|
+
text,
|
114
|
+
ha="right",
|
115
|
+
va="bottom",
|
116
|
+
transform=ax.transAxes,
|
117
|
+
bbox=dict(facecolor="white", alpha=0.5, boxstyle="round,pad=0.5"),
|
118
|
+
)
|
119
|
+
return fig
|
120
|
+
|
121
|
+
return plot_function
|
122
|
+
|
123
|
+
|
124
|
+
def create_gantt_chart_frames(
|
125
|
+
frames_dir: str,
|
126
|
+
instance: JobShopInstance,
|
127
|
+
solver: DispatchingRuleSolver,
|
128
|
+
plot_function: Callable[[Schedule, int, list[Operation] | None], Figure],
|
129
|
+
plot_current_time: bool = True,
|
130
|
+
) -> None:
|
131
|
+
"""Creates frames of the Gantt chart for the schedule being built.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
frames_dir:
|
135
|
+
The directory to save the frames in.
|
136
|
+
instance:
|
137
|
+
The instance of the job shop problem to be scheduled.
|
138
|
+
solver:
|
139
|
+
The dispatching rule solver to use.
|
140
|
+
plot_function:
|
141
|
+
A function that plots a Gantt chart for a schedule. It
|
142
|
+
should take a `Schedule` object and the makespan of the schedule as
|
143
|
+
input and return a `Figure` object.
|
144
|
+
plot_current_time:
|
145
|
+
Whether to plot a vertical line at the current time."""
|
146
|
+
dispatcher = Dispatcher(instance, pruning_function=solver.pruning_function)
|
147
|
+
schedule = dispatcher.schedule
|
148
|
+
makespan = solver(instance).makespan()
|
149
|
+
iteration = 0
|
150
|
+
|
151
|
+
while not schedule.is_complete():
|
152
|
+
solver.step(dispatcher)
|
153
|
+
iteration += 1
|
154
|
+
fig = plot_function(
|
155
|
+
schedule,
|
156
|
+
makespan,
|
157
|
+
dispatcher.available_operations(),
|
158
|
+
)
|
159
|
+
current_time = (
|
160
|
+
None if not plot_current_time else dispatcher.current_time()
|
161
|
+
)
|
162
|
+
_save_frame(fig, frames_dir, iteration, current_time)
|
163
|
+
|
164
|
+
|
165
|
+
def _save_frame(
|
166
|
+
figure: Figure, frames_dir: str, number: int, current_time: int | None
|
167
|
+
) -> None:
|
168
|
+
if current_time is not None:
|
169
|
+
figure.gca().axvline(current_time, color="red", linestyle="--")
|
170
|
+
|
171
|
+
figure.savefig(f"{frames_dir}/frame_{number:02d}.png", bbox_inches="tight")
|
172
|
+
plt.close(figure)
|
173
|
+
|
174
|
+
|
175
|
+
def create_gif_from_frames(frames_dir: str, gif_path: str, fps: int) -> None:
|
176
|
+
"""Creates a GIF from the frames in the given directory.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
frames_dir:
|
180
|
+
The directory containing the frames to be used in the GIF.
|
181
|
+
gif_path:
|
182
|
+
The path to save the GIF file. It should end with ".gif".
|
183
|
+
fps:
|
184
|
+
The number of frames per second in the GIF.
|
185
|
+
"""
|
186
|
+
frames = [
|
187
|
+
os.path.join(frames_dir, frame)
|
188
|
+
for frame in sorted(os.listdir(frames_dir))
|
189
|
+
]
|
190
|
+
images = [imageio.imread(frame) for frame in frames]
|
191
|
+
imageio.mimsave(gif_path, images, fps=fps, loop=0)
|
@@ -0,0 +1,206 @@
|
|
1
|
+
"""Module for visualizing the disjunctive graph of a job shop instance."""
|
2
|
+
|
3
|
+
import functools
|
4
|
+
from typing import Optional, Callable
|
5
|
+
import warnings
|
6
|
+
import copy
|
7
|
+
|
8
|
+
import matplotlib
|
9
|
+
import matplotlib.pyplot as plt
|
10
|
+
import networkx as nx
|
11
|
+
|
12
|
+
from job_shop_lib import JobShopInstance
|
13
|
+
from job_shop_lib.graphs import (
|
14
|
+
JobShopGraph,
|
15
|
+
EdgeType,
|
16
|
+
NodeType,
|
17
|
+
Node,
|
18
|
+
build_disjunctive_graph,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
Layout = Callable[[nx.Graph], dict[str, tuple[float, float]]]
|
23
|
+
|
24
|
+
|
25
|
+
# This function could be improved by a function extraction refactoring
|
26
|
+
# (see `plot_gantt_chart`
|
27
|
+
# function as a reference in how to do it). That would solve the
|
28
|
+
# "too many locals" warning. However, this refactoring is not a priority at
|
29
|
+
# the moment. To compensate, sections are separated by comments.
|
30
|
+
# For the "too many arguments" warning no satisfactory solution was
|
31
|
+
# found. I believe is still better than using `**kwargs` and losing the
|
32
|
+
# function signature or adding a dataclass for configuration (it would add
|
33
|
+
# unnecessary complexity).
|
34
|
+
# pylint: disable=too-many-arguments, too-many-locals
|
35
|
+
def plot_disjunctive_graph(
|
36
|
+
job_shop: JobShopGraph | JobShopInstance,
|
37
|
+
figsize: tuple[float, float] = (6, 4),
|
38
|
+
node_size: int = 1600,
|
39
|
+
title: Optional[str] = None,
|
40
|
+
layout: Optional[Layout] = None,
|
41
|
+
edge_width: int = 2,
|
42
|
+
font_size: int = 10,
|
43
|
+
arrow_size: int = 35,
|
44
|
+
alpha=0.95,
|
45
|
+
node_font_color: str = "white",
|
46
|
+
color_map: str = "Dark2_r",
|
47
|
+
draw_disjunctive_edges: bool = True,
|
48
|
+
) -> plt.Figure:
|
49
|
+
"""Returns a plot of the disjunctive graph of the instance."""
|
50
|
+
|
51
|
+
if isinstance(job_shop, JobShopInstance):
|
52
|
+
job_shop_graph = build_disjunctive_graph(job_shop)
|
53
|
+
else:
|
54
|
+
job_shop_graph = job_shop
|
55
|
+
|
56
|
+
# Set up the plot
|
57
|
+
# ----------------
|
58
|
+
plt.figure(figsize=figsize)
|
59
|
+
if title is None:
|
60
|
+
title = (
|
61
|
+
f"Disjunctive Graph Visualization: {job_shop_graph.instance.name}"
|
62
|
+
)
|
63
|
+
plt.title(title)
|
64
|
+
|
65
|
+
# Set up the layout
|
66
|
+
# -----------------
|
67
|
+
if layout is None:
|
68
|
+
try:
|
69
|
+
from networkx.drawing.nx_agraph import (
|
70
|
+
graphviz_layout,
|
71
|
+
)
|
72
|
+
|
73
|
+
layout = functools.partial(
|
74
|
+
graphviz_layout, prog="dot", args="-Grankdir=LR"
|
75
|
+
)
|
76
|
+
except ImportError:
|
77
|
+
warnings.warn(
|
78
|
+
"Could not import graphviz_layout. "
|
79
|
+
+ "Using spring_layout instead."
|
80
|
+
)
|
81
|
+
layout = nx.spring_layout
|
82
|
+
|
83
|
+
temp_graph = copy.deepcopy(job_shop_graph.graph)
|
84
|
+
# Remove disjunctive edges to get a better layout
|
85
|
+
temp_graph.remove_edges_from(
|
86
|
+
[
|
87
|
+
(u, v)
|
88
|
+
for u, v, d in job_shop_graph.graph.edges(data=True)
|
89
|
+
if d["type"] == EdgeType.DISJUNCTIVE
|
90
|
+
]
|
91
|
+
)
|
92
|
+
pos = layout(temp_graph) # type: ignore
|
93
|
+
|
94
|
+
# Draw nodes
|
95
|
+
# ----------
|
96
|
+
node_colors = [_get_node_color(node) for node in job_shop_graph.nodes]
|
97
|
+
|
98
|
+
nx.draw_networkx_nodes(
|
99
|
+
job_shop_graph.graph,
|
100
|
+
pos,
|
101
|
+
node_size=node_size,
|
102
|
+
node_color=node_colors,
|
103
|
+
alpha=alpha,
|
104
|
+
cmap=matplotlib.colormaps.get_cmap(color_map),
|
105
|
+
)
|
106
|
+
|
107
|
+
# Draw edges
|
108
|
+
# ----------
|
109
|
+
conjunctive_edges = [
|
110
|
+
(u, v)
|
111
|
+
for u, v, d in job_shop_graph.graph.edges(data=True)
|
112
|
+
if d["type"] == EdgeType.CONJUNCTIVE
|
113
|
+
]
|
114
|
+
disjunctive_edges = [
|
115
|
+
(u, v)
|
116
|
+
for u, v, d in job_shop_graph.graph.edges(data=True)
|
117
|
+
if d["type"] == EdgeType.DISJUNCTIVE
|
118
|
+
]
|
119
|
+
|
120
|
+
nx.draw_networkx_edges(
|
121
|
+
job_shop_graph.graph,
|
122
|
+
pos,
|
123
|
+
edgelist=conjunctive_edges,
|
124
|
+
width=edge_width,
|
125
|
+
edge_color="black",
|
126
|
+
arrowsize=arrow_size,
|
127
|
+
)
|
128
|
+
|
129
|
+
if draw_disjunctive_edges:
|
130
|
+
nx.draw_networkx_edges(
|
131
|
+
job_shop_graph.graph,
|
132
|
+
pos,
|
133
|
+
edgelist=disjunctive_edges,
|
134
|
+
width=edge_width,
|
135
|
+
edge_color="red",
|
136
|
+
arrowsize=arrow_size,
|
137
|
+
)
|
138
|
+
|
139
|
+
# Draw node labels
|
140
|
+
# ----------------
|
141
|
+
operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
142
|
+
|
143
|
+
labels = {}
|
144
|
+
source_node = job_shop_graph.nodes_by_type[NodeType.SOURCE][0]
|
145
|
+
labels[source_node] = "S"
|
146
|
+
|
147
|
+
sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
|
148
|
+
labels[sink_node] = "T"
|
149
|
+
for operation_node in operation_nodes:
|
150
|
+
labels[operation_node] = (
|
151
|
+
f"m={operation_node.operation.machine_id}\n"
|
152
|
+
f"d={operation_node.operation.duration}"
|
153
|
+
)
|
154
|
+
|
155
|
+
nx.draw_networkx_labels(
|
156
|
+
job_shop_graph.graph,
|
157
|
+
pos,
|
158
|
+
labels=labels,
|
159
|
+
font_color=node_font_color,
|
160
|
+
font_size=font_size,
|
161
|
+
font_family="sans-serif",
|
162
|
+
)
|
163
|
+
|
164
|
+
# Final touches
|
165
|
+
# -------------
|
166
|
+
plt.axis("off")
|
167
|
+
plt.tight_layout()
|
168
|
+
# Create a legend to indicate the meaning of the edge colors
|
169
|
+
conjunctive_patch = matplotlib.patches.Patch(
|
170
|
+
color="black", label="conjunctive edges"
|
171
|
+
)
|
172
|
+
disjunctive_patch = matplotlib.patches.Patch(
|
173
|
+
color="red", label="disjunctive edges"
|
174
|
+
)
|
175
|
+
|
176
|
+
# Add to the legend the meaning of m and d
|
177
|
+
text = "m = machine_id\nd = duration"
|
178
|
+
extra = matplotlib.patches.Rectangle(
|
179
|
+
(0, 0),
|
180
|
+
1,
|
181
|
+
1,
|
182
|
+
fc="w",
|
183
|
+
fill=False,
|
184
|
+
edgecolor="none",
|
185
|
+
linewidth=0,
|
186
|
+
label=text,
|
187
|
+
)
|
188
|
+
plt.legend(
|
189
|
+
handles=[conjunctive_patch, disjunctive_patch, extra],
|
190
|
+
loc="upper left",
|
191
|
+
bbox_to_anchor=(1.05, 1),
|
192
|
+
borderaxespad=0.0,
|
193
|
+
)
|
194
|
+
return plt.gcf()
|
195
|
+
|
196
|
+
|
197
|
+
def _get_node_color(node: Node) -> int:
|
198
|
+
"""Returns the color of the node."""
|
199
|
+
if node.node_type == NodeType.SOURCE:
|
200
|
+
return -1
|
201
|
+
if node.node_type == NodeType.SINK:
|
202
|
+
return -1
|
203
|
+
if node.node_type == NodeType.OPERATION:
|
204
|
+
return node.operation.machine_id
|
205
|
+
|
206
|
+
raise ValueError("Invalid node type.")
|
@@ -0,0 +1,147 @@
|
|
1
|
+
"""Module for plotting static Gantt charts for job shop schedules."""
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from matplotlib.figure import Figure
|
6
|
+
import matplotlib.pyplot as plt
|
7
|
+
from matplotlib.colors import Normalize
|
8
|
+
from matplotlib.patches import Patch
|
9
|
+
|
10
|
+
from job_shop_lib import Schedule, ScheduledOperation
|
11
|
+
|
12
|
+
|
13
|
+
_BASE_Y_POSITION = 1
|
14
|
+
_Y_POSITION_INCREMENT = 10
|
15
|
+
|
16
|
+
|
17
|
+
def plot_gantt_chart(
|
18
|
+
schedule: Schedule,
|
19
|
+
title: str | None = None,
|
20
|
+
cmap_name: str = "viridis",
|
21
|
+
xlim: int | None = None,
|
22
|
+
number_of_x_ticks: int = 15,
|
23
|
+
) -> tuple[Figure, plt.Axes]:
|
24
|
+
"""Plots a Gantt chart for the schedule.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
schedule:
|
28
|
+
The schedule to plot.
|
29
|
+
title:
|
30
|
+
The title of the plot. If not provided, the title:
|
31
|
+
`f"Gantt Chart for {schedule.instance.name} instance"`
|
32
|
+
is used.
|
33
|
+
cmap_name:
|
34
|
+
The name of the colormap to use. Default is "viridis".
|
35
|
+
xlim:
|
36
|
+
The maximum value for the x-axis. If not provided, the makespan of
|
37
|
+
the schedule is used.
|
38
|
+
number_of_x_ticks:
|
39
|
+
The number of ticks to use in the x-axis.
|
40
|
+
"""
|
41
|
+
fig, ax = _initialize_plot(schedule, title)
|
42
|
+
legend_handles = _plot_machine_schedules(schedule, ax, cmap_name)
|
43
|
+
_configure_legend(ax, legend_handles)
|
44
|
+
_configure_axes(schedule, ax, xlim, number_of_x_ticks)
|
45
|
+
return fig, ax
|
46
|
+
|
47
|
+
|
48
|
+
def _initialize_plot(
|
49
|
+
schedule: Schedule, title: str | None
|
50
|
+
) -> tuple[Figure, plt.Axes]:
|
51
|
+
"""Initializes the plot."""
|
52
|
+
fig, ax = plt.subplots()
|
53
|
+
ax.set_xlabel("Time units")
|
54
|
+
ax.set_ylabel("Machines")
|
55
|
+
ax.grid(True, which="both", axis="x", linestyle="--", linewidth=0.5)
|
56
|
+
ax.yaxis.grid(False)
|
57
|
+
if title is None:
|
58
|
+
title = f"Gantt Chart for {schedule.instance.name} instance"
|
59
|
+
plt.title(title)
|
60
|
+
return fig, ax
|
61
|
+
|
62
|
+
|
63
|
+
def _plot_machine_schedules(
|
64
|
+
schedule: Schedule, ax: plt.Axes, cmap_name: str
|
65
|
+
) -> dict[int, Patch]:
|
66
|
+
"""Plots the schedules for each machine."""
|
67
|
+
max_job_id = schedule.instance.num_jobs - 1
|
68
|
+
cmap = plt.cm.get_cmap(cmap_name, max_job_id + 1)
|
69
|
+
norm = Normalize(vmin=0, vmax=max_job_id)
|
70
|
+
legend_handles = {}
|
71
|
+
|
72
|
+
for machine_index, machine_schedule in enumerate(schedule.schedule):
|
73
|
+
y_position_for_machines = (
|
74
|
+
_BASE_Y_POSITION + _Y_POSITION_INCREMENT * machine_index
|
75
|
+
)
|
76
|
+
|
77
|
+
for scheduled_op in machine_schedule:
|
78
|
+
color = cmap(norm(scheduled_op.job_id))
|
79
|
+
_plot_scheduled_operation(
|
80
|
+
ax, scheduled_op, y_position_for_machines, color
|
81
|
+
)
|
82
|
+
if scheduled_op.job_id not in legend_handles:
|
83
|
+
legend_handles[scheduled_op.job_id] = Patch(
|
84
|
+
facecolor=color, label=f"Job {scheduled_op.job_id}"
|
85
|
+
)
|
86
|
+
|
87
|
+
return legend_handles
|
88
|
+
|
89
|
+
|
90
|
+
def _plot_scheduled_operation(
|
91
|
+
ax: plt.Axes,
|
92
|
+
scheduled_op: ScheduledOperation,
|
93
|
+
y_position_for_machines: int,
|
94
|
+
color,
|
95
|
+
):
|
96
|
+
"""Plots a single scheduled operation."""
|
97
|
+
start_time, end_time = scheduled_op.start_time, scheduled_op.end_time
|
98
|
+
duration = end_time - start_time
|
99
|
+
ax.broken_barh(
|
100
|
+
[(start_time, duration)],
|
101
|
+
(y_position_for_machines, 9),
|
102
|
+
facecolors=color,
|
103
|
+
)
|
104
|
+
|
105
|
+
|
106
|
+
def _configure_legend(ax: plt.Axes, legend_handles: dict[int, Patch]):
|
107
|
+
"""Configures the legend for the plot."""
|
108
|
+
sorted_legend_handles = [
|
109
|
+
legend_handles[job_id] for job_id in sorted(legend_handles)
|
110
|
+
]
|
111
|
+
ax.legend(
|
112
|
+
handles=sorted_legend_handles,
|
113
|
+
loc="upper left",
|
114
|
+
bbox_to_anchor=(1.01, 1),
|
115
|
+
)
|
116
|
+
|
117
|
+
|
118
|
+
def _configure_axes(
|
119
|
+
schedule: Schedule,
|
120
|
+
ax: plt.Axes,
|
121
|
+
xlim: Optional[int],
|
122
|
+
number_of_x_ticks: int,
|
123
|
+
):
|
124
|
+
"""Sets the limits and labels for the axes."""
|
125
|
+
num_machines = len(schedule.schedule)
|
126
|
+
ax.set_ylim(0, _BASE_Y_POSITION + _Y_POSITION_INCREMENT * num_machines)
|
127
|
+
ax.set_yticks(
|
128
|
+
[
|
129
|
+
_BASE_Y_POSITION
|
130
|
+
+ _Y_POSITION_INCREMENT // 2
|
131
|
+
+ _Y_POSITION_INCREMENT * i
|
132
|
+
for i in range(num_machines)
|
133
|
+
]
|
134
|
+
)
|
135
|
+
ax.set_yticklabels([str(i) for i in range(num_machines)])
|
136
|
+
makespan = schedule.makespan()
|
137
|
+
xlim = xlim if xlim is not None else makespan
|
138
|
+
ax.set_xlim(0, xlim)
|
139
|
+
|
140
|
+
tick_interval = max(1, xlim // number_of_x_ticks)
|
141
|
+
xticks = list(range(0, xlim + 1, tick_interval))
|
142
|
+
|
143
|
+
if xticks[-1] != xlim:
|
144
|
+
xticks.pop()
|
145
|
+
xticks.append(xlim)
|
146
|
+
|
147
|
+
ax.set_xticks(xticks)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Pablo Ariño
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|