job-shop-lib 0.1.0__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 +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.
|