job-shop-lib 1.0.0a2__py3-none-any.whl → 1.0.0a4__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/_job_shop_instance.py +119 -55
- job_shop_lib/_operation.py +18 -7
- job_shop_lib/_schedule.py +13 -15
- job_shop_lib/_scheduled_operation.py +17 -18
- job_shop_lib/dispatching/__init__.py +4 -0
- job_shop_lib/dispatching/_dispatcher.py +36 -47
- job_shop_lib/dispatching/_dispatcher_observer_config.py +15 -2
- job_shop_lib/dispatching/_factories.py +10 -2
- job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +0 -1
- job_shop_lib/dispatching/feature_observers/_factory.py +21 -18
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +1 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +1 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +44 -25
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
- job_shop_lib/generation/_general_instance_generator.py +33 -34
- job_shop_lib/generation/_instance_generator.py +14 -17
- job_shop_lib/generation/_transformations.py +11 -8
- job_shop_lib/graphs/__init__.py +3 -0
- job_shop_lib/graphs/_build_disjunctive_graph.py +41 -3
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +11 -13
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +17 -20
- job_shop_lib/reinforcement_learning/__init__.py +16 -7
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +69 -57
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +43 -32
- job_shop_lib/reinforcement_learning/_types_and_constants.py +2 -2
- job_shop_lib/visualization/__init__.py +29 -10
- job_shop_lib/visualization/_gantt_chart_creator.py +122 -84
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +68 -37
- job_shop_lib/visualization/_plot_disjunctive_graph.py +382 -0
- job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/METADATA +15 -3
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/RECORD +36 -36
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/WHEEL +1 -1
- job_shop_lib/visualization/_disjunctive_graph.py +0 -210
- /job_shop_lib/visualization/{_agent_task_graph.py → _plot_agent_task_graph.py} +0 -0
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/LICENSE +0 -0
@@ -0,0 +1,382 @@
|
|
1
|
+
"""Module for visualizing the disjunctive graph of a job shop instance."""
|
2
|
+
|
3
|
+
import functools
|
4
|
+
from typing import Any
|
5
|
+
from collections.abc import Callable, Sequence, Iterable
|
6
|
+
import warnings
|
7
|
+
import copy
|
8
|
+
|
9
|
+
import matplotlib
|
10
|
+
import matplotlib.pyplot as plt
|
11
|
+
import networkx as nx
|
12
|
+
from networkx.drawing.nx_agraph import graphviz_layout
|
13
|
+
|
14
|
+
from job_shop_lib import JobShopInstance
|
15
|
+
from job_shop_lib.graphs import (
|
16
|
+
JobShopGraph,
|
17
|
+
EdgeType,
|
18
|
+
NodeType,
|
19
|
+
Node,
|
20
|
+
build_disjunctive_graph,
|
21
|
+
)
|
22
|
+
from job_shop_lib.exceptions import ValidationError
|
23
|
+
|
24
|
+
|
25
|
+
Layout = Callable[[nx.Graph], dict[str, tuple[float, float]]]
|
26
|
+
|
27
|
+
|
28
|
+
def duration_labeler(node: Node) -> str:
|
29
|
+
"""Returns a label for the node with the processing time.
|
30
|
+
|
31
|
+
In the form ``"$p_{ij}=duration$"``, where $i$ is the job id and $j$ is
|
32
|
+
the position in the job.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
node:
|
36
|
+
The operation node to label. See
|
37
|
+
:class:`~job_shop_lib.graphs.Node`.
|
38
|
+
"""
|
39
|
+
return (
|
40
|
+
f"$p_{{{node.operation.job_id + 1}"
|
41
|
+
f"{node.operation.position_in_job + 1}}}={node.operation.duration}$"
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
# This function could be improved by a function extraction refactoring
|
46
|
+
# (see `plot_gantt_chart`
|
47
|
+
# function as a reference in how to do it). That would solve the
|
48
|
+
# "too many locals" warning. However, this refactoring is not a priority at
|
49
|
+
# the moment. To compensate, sections are separated by comments.
|
50
|
+
# For the "too many arguments" warning no satisfactory solution was
|
51
|
+
# found. I believe is still better than using `**kwargs` and losing the
|
52
|
+
# function signature or adding a dataclass for configuration (it would add
|
53
|
+
# unnecessary complexity). A TypedDict could be used too, but the default
|
54
|
+
# values would not be explicit.
|
55
|
+
# pylint: disable=too-many-arguments, too-many-locals, too-many-statements
|
56
|
+
# pylint: disable=too-many-branches, line-too-long
|
57
|
+
def plot_disjunctive_graph(
|
58
|
+
job_shop: JobShopGraph | JobShopInstance,
|
59
|
+
*,
|
60
|
+
title: str | None = None,
|
61
|
+
figsize: tuple[float, float] = (6, 4),
|
62
|
+
node_size: int = 1600,
|
63
|
+
edge_width: int = 2,
|
64
|
+
font_size: int = 10,
|
65
|
+
arrow_size: int = 35,
|
66
|
+
alpha: float = 0.95,
|
67
|
+
operation_node_labeler: Callable[[Node], str] = duration_labeler,
|
68
|
+
node_font_color: str = "white",
|
69
|
+
color_map: str = "Dark2_r",
|
70
|
+
disjunctive_edge_color: str = "red",
|
71
|
+
conjunctive_edge_color: str = "black",
|
72
|
+
layout: Layout | None = None,
|
73
|
+
draw_disjunctive_edges: bool | str = True,
|
74
|
+
conjunctive_edges_additional_params: dict[str, Any] | None = None,
|
75
|
+
disjunctive_edges_additional_params: dict[str, Any] | None = None,
|
76
|
+
conjunctive_patch_label: str = "Conjunctive edges",
|
77
|
+
disjunctive_patch_label: str = "Disjunctive edges",
|
78
|
+
legend_text: str = "$p_{ij}=$duration of $O_{ij}$",
|
79
|
+
show_machine_colors_in_legend: bool = True,
|
80
|
+
machine_labels: Sequence[str] | None = None,
|
81
|
+
legend_location: str = "upper left",
|
82
|
+
legend_bbox_to_anchor: tuple[float, float] = (1.01, 1),
|
83
|
+
start_node_label: str = "$S$",
|
84
|
+
end_node_label: str = "$T$",
|
85
|
+
font_family: str = "sans-serif",
|
86
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
87
|
+
r"""Plots the disjunctive graph of the given job shop instance or graph.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
job_shop:
|
91
|
+
The job shop instance or graph to plot. Can be either a
|
92
|
+
:class:`JobShopGraph` or a :class:`JobShopInstance`. If a job shop
|
93
|
+
instance is given, the disjunctive graph is built before plotting
|
94
|
+
using the :func:`~job_shop_lib.graphs.build_disjunctive_graph`.
|
95
|
+
title:
|
96
|
+
The title of the graph (default is ``"Disjunctive Graph
|
97
|
+
Visualization: {job_shop.instance.name}"``).
|
98
|
+
figsize:
|
99
|
+
The size of the figure (default is (6, 4)).
|
100
|
+
node_size:
|
101
|
+
The size of the nodes (default is 1600).
|
102
|
+
edge_width:
|
103
|
+
The width of the edges (default is 2).
|
104
|
+
font_size:
|
105
|
+
The font size of the node labels (default is 10).
|
106
|
+
arrow_size:
|
107
|
+
The size of the arrows (default is 35).
|
108
|
+
alpha:
|
109
|
+
The transparency level of the nodes and edges (default is 0.95).
|
110
|
+
operation_node_labeler:
|
111
|
+
A function that formats labels for operation nodes. Receives a
|
112
|
+
:class:`~job_shop_lib.graphs.Node` and returns a string.
|
113
|
+
The default is :func:`duration_labeler`, which labels the nodes
|
114
|
+
with their duration.
|
115
|
+
node_font_color:
|
116
|
+
The color of the node labels (default is ``"white"``).
|
117
|
+
color_map:
|
118
|
+
The color map to use for the nodes (default is ``"Dark2_r"``).
|
119
|
+
disjunctive_edge_color:
|
120
|
+
The color of the disjunctive edges (default is ``"red"``).
|
121
|
+
conjunctive_edge_color:
|
122
|
+
The color of the conjunctive edges (default is ``"black"``).
|
123
|
+
layout:
|
124
|
+
The layout of the graph (default is ``graphviz_layout`` with
|
125
|
+
``prog="dot"`` and ``args="-Grankdir=LR"``). If not available,
|
126
|
+
the spring layout is used. To install pygraphviz, check
|
127
|
+
`pygraphviz documentation
|
128
|
+
<https://pygraphviz.github.io/documentation/stable/install.html>`_.
|
129
|
+
draw_disjunctive_edges:
|
130
|
+
Whether to draw disjunctive edges (default is ``True``). If
|
131
|
+
``False``, only conjunctive edges are drawn. If ``"single_edge",``
|
132
|
+
the disjunctive edges are drawn as undirected edges by removing one
|
133
|
+
of the directions. If using this last option is recommended to set
|
134
|
+
the "arrowstyle" parameter to ``"-"`` or ``"<->"`` in the
|
135
|
+
``disjunctive_edges_additional_params`` to make the edges look
|
136
|
+
better. See `matplotlib documentation on arrow styles <https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.ArrowStyle.html#matplotlib.patches.ArrowStyle>`_
|
137
|
+
and `nx.draw_networkx_edges <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.nx_pylab.draw_networkx_edges.html>`_
|
138
|
+
for more information.
|
139
|
+
conjunctive_edges_additional_params:
|
140
|
+
Additional parameters to pass to the conjunctive edges when
|
141
|
+
drawing them (default is ``None``). See the documentation of
|
142
|
+
`nx.draw_networkx_edges <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.nx_pylab.draw_networkx_edges.html>`_
|
143
|
+
for more information. The parameters that are explicitly set by
|
144
|
+
this function and should not be part of this dictionary are
|
145
|
+
``edgelist``, ``pos``, ``width``, ``edge_color``, and
|
146
|
+
``arrowsize``.
|
147
|
+
disjunctive_edges_additional_params:
|
148
|
+
Same as ``conjunctive_edges_additional_params``, but for
|
149
|
+
disjunctive edges (default is ``None``).
|
150
|
+
conjunctive_patch_label:
|
151
|
+
The label for the conjunctive edges in the legend (default is
|
152
|
+
``"Conjunctive edges"``).
|
153
|
+
disjunctive_patch_label:
|
154
|
+
The label for the disjunctive edges in the legend (default is
|
155
|
+
``"Disjunctive edges"``).
|
156
|
+
legend_text:
|
157
|
+
Text to display in the legend after the conjunctive and
|
158
|
+
disjunctive edges labels (default is
|
159
|
+
``"$p_{ij}=$duration of $O_{ij}$"``).
|
160
|
+
show_machine_colors_in_legend:
|
161
|
+
Whether to show the colors of the machines in the legend
|
162
|
+
(default is ``True``).
|
163
|
+
machine_labels:
|
164
|
+
The labels for the machines (default is
|
165
|
+
``[f"Machine {i}" for i in range(num_machines)]``). Not used if
|
166
|
+
``show_machine_colors_in_legend`` is ``False``.
|
167
|
+
legend_location:
|
168
|
+
The location of the legend (default is "upper left").
|
169
|
+
legend_bbox_to_anchor:
|
170
|
+
The anchor of the legend box (default is ``(1.01, 1)``).
|
171
|
+
start_node_label:
|
172
|
+
The label for the start node (default is ``"$S$"``).
|
173
|
+
end_node_label:
|
174
|
+
The label for the end node (default is ``"$T$"``).
|
175
|
+
font_family:
|
176
|
+
The font family of the node labels (default is ``"sans-serif"``).
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
A matplotlib Figure object representing the disjunctive graph.
|
180
|
+
|
181
|
+
Example:
|
182
|
+
|
183
|
+
.. code-block:: python
|
184
|
+
|
185
|
+
job_shop_instance = JobShopInstance(...) # or a JobShopGraph
|
186
|
+
fig = plot_disjunctive_graph(job_shop_instance)
|
187
|
+
|
188
|
+
""" # noqa: E501
|
189
|
+
|
190
|
+
if isinstance(job_shop, JobShopInstance):
|
191
|
+
job_shop_graph = build_disjunctive_graph(job_shop)
|
192
|
+
else:
|
193
|
+
job_shop_graph = job_shop
|
194
|
+
|
195
|
+
# Set up the plot
|
196
|
+
# ----------------
|
197
|
+
plt.figure(figsize=figsize)
|
198
|
+
if title is None:
|
199
|
+
title = (
|
200
|
+
f"Disjunctive Graph Visualization: {job_shop_graph.instance.name}"
|
201
|
+
)
|
202
|
+
plt.title(title)
|
203
|
+
|
204
|
+
# Set up the layout
|
205
|
+
# -----------------
|
206
|
+
if layout is None:
|
207
|
+
layout = functools.partial(
|
208
|
+
graphviz_layout, prog="dot", args="-Grankdir=LR"
|
209
|
+
)
|
210
|
+
|
211
|
+
temp_graph = copy.deepcopy(job_shop_graph.graph)
|
212
|
+
# Remove disjunctive edges to get a better layout
|
213
|
+
temp_graph.remove_edges_from(
|
214
|
+
[
|
215
|
+
(u, v)
|
216
|
+
for u, v, d in job_shop_graph.graph.edges(data=True)
|
217
|
+
if d["type"] == EdgeType.DISJUNCTIVE
|
218
|
+
]
|
219
|
+
)
|
220
|
+
|
221
|
+
try:
|
222
|
+
pos = layout(temp_graph)
|
223
|
+
except ImportError:
|
224
|
+
warnings.warn(
|
225
|
+
"Default layout requires pygraphviz http://pygraphviz.github.io/. "
|
226
|
+
"Using spring layout instead.",
|
227
|
+
)
|
228
|
+
pos = nx.spring_layout(temp_graph)
|
229
|
+
|
230
|
+
# Draw nodes
|
231
|
+
# ----------
|
232
|
+
node_colors = [
|
233
|
+
_get_node_color(node)
|
234
|
+
for node in job_shop_graph.nodes
|
235
|
+
if not job_shop_graph.is_removed(node.node_id)
|
236
|
+
]
|
237
|
+
cmap_func = matplotlib.colormaps.get_cmap(color_map)
|
238
|
+
nx.draw_networkx_nodes(
|
239
|
+
job_shop_graph.graph,
|
240
|
+
pos,
|
241
|
+
node_size=node_size,
|
242
|
+
node_color=node_colors,
|
243
|
+
alpha=alpha,
|
244
|
+
cmap=cmap_func,
|
245
|
+
)
|
246
|
+
|
247
|
+
# Draw edges
|
248
|
+
# ----------
|
249
|
+
conjunctive_edges = [
|
250
|
+
(u, v)
|
251
|
+
for u, v, d in job_shop_graph.graph.edges(data=True)
|
252
|
+
if d["type"] == EdgeType.CONJUNCTIVE
|
253
|
+
]
|
254
|
+
disjunctive_edges: Iterable[tuple[int, int]] = [
|
255
|
+
(u, v)
|
256
|
+
for u, v, d in job_shop_graph.graph.edges(data=True)
|
257
|
+
if d["type"] == EdgeType.DISJUNCTIVE
|
258
|
+
]
|
259
|
+
if conjunctive_edges_additional_params is None:
|
260
|
+
conjunctive_edges_additional_params = {}
|
261
|
+
if disjunctive_edges_additional_params is None:
|
262
|
+
disjunctive_edges_additional_params = {}
|
263
|
+
|
264
|
+
nx.draw_networkx_edges(
|
265
|
+
job_shop_graph.graph,
|
266
|
+
pos,
|
267
|
+
edgelist=conjunctive_edges,
|
268
|
+
width=edge_width,
|
269
|
+
edge_color=conjunctive_edge_color,
|
270
|
+
arrowsize=arrow_size,
|
271
|
+
**conjunctive_edges_additional_params,
|
272
|
+
)
|
273
|
+
|
274
|
+
if draw_disjunctive_edges:
|
275
|
+
if draw_disjunctive_edges == "single_edge":
|
276
|
+
# Filter the disjunctive edges to remove one of the directions
|
277
|
+
disjunctive_edges_filtered = set()
|
278
|
+
for u, v in disjunctive_edges:
|
279
|
+
if u > v:
|
280
|
+
u, v = v, u
|
281
|
+
disjunctive_edges_filtered.add((u, v))
|
282
|
+
disjunctive_edges = disjunctive_edges_filtered
|
283
|
+
nx.draw_networkx_edges(
|
284
|
+
job_shop_graph.graph,
|
285
|
+
pos,
|
286
|
+
edgelist=disjunctive_edges,
|
287
|
+
width=edge_width,
|
288
|
+
edge_color=disjunctive_edge_color,
|
289
|
+
arrowsize=arrow_size,
|
290
|
+
**disjunctive_edges_additional_params,
|
291
|
+
)
|
292
|
+
|
293
|
+
# Draw node labels
|
294
|
+
# ----------------
|
295
|
+
operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
296
|
+
labels = {}
|
297
|
+
source_node = job_shop_graph.nodes_by_type[NodeType.SOURCE][0]
|
298
|
+
labels[source_node] = start_node_label
|
299
|
+
|
300
|
+
sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
|
301
|
+
labels[sink_node] = end_node_label
|
302
|
+
machine_colors: dict[int, tuple[float, float, float, float]] = {}
|
303
|
+
for operation_node in operation_nodes:
|
304
|
+
if job_shop_graph.is_removed(operation_node.node_id):
|
305
|
+
continue
|
306
|
+
labels[operation_node] = operation_node_labeler(operation_node)
|
307
|
+
machine_id = operation_node.operation.machine_id
|
308
|
+
if machine_id not in machine_colors:
|
309
|
+
machine_colors[machine_id] = cmap_func(
|
310
|
+
(_get_node_color(operation_node) + 1)
|
311
|
+
/ job_shop_graph.instance.num_machines
|
312
|
+
)
|
313
|
+
|
314
|
+
nx.draw_networkx_labels(
|
315
|
+
job_shop_graph.graph,
|
316
|
+
pos,
|
317
|
+
labels=labels,
|
318
|
+
font_color=node_font_color,
|
319
|
+
font_size=font_size,
|
320
|
+
font_family=font_family,
|
321
|
+
)
|
322
|
+
# Final touches
|
323
|
+
# -------------
|
324
|
+
plt.axis("off")
|
325
|
+
plt.tight_layout()
|
326
|
+
# Create a legend to indicate the meaning of the edge colors
|
327
|
+
conjunctive_patch = matplotlib.patches.Patch(
|
328
|
+
color=conjunctive_edge_color, label=conjunctive_patch_label
|
329
|
+
)
|
330
|
+
disjunctive_patch = matplotlib.patches.Patch(
|
331
|
+
color=disjunctive_edge_color, label=disjunctive_patch_label
|
332
|
+
)
|
333
|
+
handles = [conjunctive_patch, disjunctive_patch]
|
334
|
+
|
335
|
+
# Add machine colors to the legend
|
336
|
+
if show_machine_colors_in_legend:
|
337
|
+
machine_patches = [
|
338
|
+
matplotlib.patches.Patch(
|
339
|
+
color=color,
|
340
|
+
label=(
|
341
|
+
machine_labels[machine_id]
|
342
|
+
if machine_labels is not None
|
343
|
+
else f"Machine {machine_id}"
|
344
|
+
),
|
345
|
+
)
|
346
|
+
for machine_id, color in machine_colors.items()
|
347
|
+
]
|
348
|
+
handles.extend(machine_patches)
|
349
|
+
|
350
|
+
# Add to the legend the meaning of m and d
|
351
|
+
if legend_text:
|
352
|
+
extra = matplotlib.patches.Rectangle(
|
353
|
+
(0, 0),
|
354
|
+
1,
|
355
|
+
1,
|
356
|
+
fc="w",
|
357
|
+
fill=False,
|
358
|
+
edgecolor="none",
|
359
|
+
linewidth=0,
|
360
|
+
label=legend_text,
|
361
|
+
)
|
362
|
+
handles.append(extra)
|
363
|
+
|
364
|
+
plt.legend(
|
365
|
+
handles=handles,
|
366
|
+
loc=legend_location,
|
367
|
+
bbox_to_anchor=legend_bbox_to_anchor,
|
368
|
+
borderaxespad=0.0,
|
369
|
+
)
|
370
|
+
return plt.gcf(), plt.gca()
|
371
|
+
|
372
|
+
|
373
|
+
def _get_node_color(node: Node) -> int:
|
374
|
+
"""Returns the color of the node."""
|
375
|
+
if node.node_type == NodeType.SOURCE:
|
376
|
+
return -1
|
377
|
+
if node.node_type == NodeType.SINK:
|
378
|
+
return -1
|
379
|
+
if node.node_type == NodeType.OPERATION:
|
380
|
+
return node.operation.machine_id
|
381
|
+
|
382
|
+
raise ValidationError("Invalid node type.")
|
@@ -20,16 +20,39 @@ def plot_gantt_chart(
|
|
20
20
|
cmap_name: str = "viridis",
|
21
21
|
xlim: int | None = None,
|
22
22
|
number_of_x_ticks: int = 15,
|
23
|
+
job_labels: None | list[str] = None,
|
24
|
+
machine_labels: None | list[str] = None,
|
25
|
+
legend_title: str = "",
|
26
|
+
x_label: str = "Time units",
|
27
|
+
y_label: str = "Machines",
|
23
28
|
) -> tuple[Figure, plt.Axes]:
|
24
29
|
"""Plots a Gantt chart for the schedule.
|
25
30
|
|
31
|
+
This function generates a Gantt chart that visualizes the schedule of jobs
|
32
|
+
across multiple machines. Each job is represented with a unique color,
|
33
|
+
and operations are plotted as bars on the corresponding machines over time.
|
34
|
+
|
35
|
+
The Gantt chart helps to understand the flow of jobs on machines and
|
36
|
+
visualize the makespan of the schedule, i.e., the time it takes to
|
37
|
+
complete all jobs.
|
38
|
+
|
39
|
+
The Gantt chart includes:
|
40
|
+
|
41
|
+
- X-axis: Time units, representing the progression of the schedule.
|
42
|
+
- Y-axis: Machines, which are assigned jobs at various time slots.
|
43
|
+
- Legend: A list of jobs, labeled and color-coded for clarity.
|
44
|
+
|
45
|
+
.. note::
|
46
|
+
The last tick on the x-axis always represents the makespan for easy
|
47
|
+
identification of the completion time.
|
48
|
+
|
26
49
|
Args:
|
27
50
|
schedule:
|
28
51
|
The schedule to plot.
|
29
52
|
title:
|
30
53
|
The title of the plot. If not provided, the title:
|
31
|
-
|
32
|
-
is used.
|
54
|
+
``f"Gantt Chart for {schedule.instance.name} instance"``
|
55
|
+
is used. To remove the title, provide an empty string.
|
33
56
|
cmap_name:
|
34
57
|
The name of the colormap to use. Default is "viridis".
|
35
58
|
xlim:
|
@@ -37,21 +60,45 @@ def plot_gantt_chart(
|
|
37
60
|
the schedule is used.
|
38
61
|
number_of_x_ticks:
|
39
62
|
The number of ticks to use in the x-axis.
|
63
|
+
job_labels:
|
64
|
+
A list of labels for each job. If ``None``, the labels are
|
65
|
+
automatically generated as "Job 0", "Job 1", etc.
|
66
|
+
machine_labels:
|
67
|
+
A list of labels for each machine. If ``None``, the labels are
|
68
|
+
automatically generated as "0", "1", etc.
|
69
|
+
legend_title:
|
70
|
+
The title of the legend. If not provided, the legend will not have
|
71
|
+
a title.
|
72
|
+
x_label:
|
73
|
+
The label for the x-axis. Default is "Time units". To remove the
|
74
|
+
label, provide an empty string.
|
75
|
+
y_label:
|
76
|
+
The label for the y-axis. Default is "Machines". To remove the
|
77
|
+
label, provide an empty string.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
- A ``matplotlib.figure.Figure`` object.
|
81
|
+
- A ``matplotlib.axes.Axes`` object where the Gantt chart is plotted.
|
40
82
|
"""
|
41
|
-
fig, ax = _initialize_plot(schedule, title)
|
42
|
-
legend_handles = _plot_machine_schedules(
|
43
|
-
|
44
|
-
|
83
|
+
fig, ax = _initialize_plot(schedule, title, x_label, y_label)
|
84
|
+
legend_handles = _plot_machine_schedules(
|
85
|
+
schedule, ax, cmap_name, job_labels
|
86
|
+
)
|
87
|
+
_configure_legend(ax, legend_handles, legend_title)
|
88
|
+
_configure_axes(schedule, ax, xlim, number_of_x_ticks, machine_labels)
|
45
89
|
return fig, ax
|
46
90
|
|
47
91
|
|
48
92
|
def _initialize_plot(
|
49
|
-
schedule: Schedule,
|
93
|
+
schedule: Schedule,
|
94
|
+
title: str | None,
|
95
|
+
x_label: str = "Time units",
|
96
|
+
y_label: str = "Machines",
|
50
97
|
) -> tuple[Figure, plt.Axes]:
|
51
98
|
"""Initializes the plot."""
|
52
99
|
fig, ax = plt.subplots()
|
53
|
-
ax.set_xlabel(
|
54
|
-
ax.set_ylabel(
|
100
|
+
ax.set_xlabel(x_label)
|
101
|
+
ax.set_ylabel(y_label)
|
55
102
|
ax.grid(True, which="both", axis="x", linestyle="--", linewidth=0.5)
|
56
103
|
ax.yaxis.grid(False)
|
57
104
|
if title is None:
|
@@ -61,7 +108,10 @@ def _initialize_plot(
|
|
61
108
|
|
62
109
|
|
63
110
|
def _plot_machine_schedules(
|
64
|
-
schedule: Schedule,
|
111
|
+
schedule: Schedule,
|
112
|
+
ax: plt.Axes,
|
113
|
+
cmap_name: str,
|
114
|
+
job_labels: list[str] | None,
|
65
115
|
) -> dict[int, Patch]:
|
66
116
|
"""Plots the schedules for each machine."""
|
67
117
|
max_job_id = schedule.instance.num_jobs - 1
|
@@ -81,12 +131,20 @@ def _plot_machine_schedules(
|
|
81
131
|
)
|
82
132
|
if scheduled_op.job_id not in legend_handles:
|
83
133
|
legend_handles[scheduled_op.job_id] = Patch(
|
84
|
-
facecolor=color,
|
134
|
+
facecolor=color,
|
135
|
+
label=_get_job_label(job_labels, scheduled_op.job_id),
|
85
136
|
)
|
86
137
|
|
87
138
|
return legend_handles
|
88
139
|
|
89
140
|
|
141
|
+
def _get_job_label(job_labels: list[str] | None, job_id: int) -> str:
|
142
|
+
"""Returns the label for the job."""
|
143
|
+
if job_labels is None:
|
144
|
+
return f"Job {job_id}"
|
145
|
+
return job_labels[job_id]
|
146
|
+
|
147
|
+
|
90
148
|
def _plot_scheduled_operation(
|
91
149
|
ax: plt.Axes,
|
92
150
|
scheduled_op: ScheduledOperation,
|
@@ -103,7 +161,9 @@ def _plot_scheduled_operation(
|
|
103
161
|
)
|
104
162
|
|
105
163
|
|
106
|
-
def _configure_legend(
|
164
|
+
def _configure_legend(
|
165
|
+
ax: plt.Axes, legend_handles: dict[int, Patch], legend_title: str
|
166
|
+
):
|
107
167
|
"""Configures the legend for the plot."""
|
108
168
|
sorted_legend_handles = [
|
109
169
|
legend_handles[job_id] for job_id in sorted(legend_handles)
|
@@ -111,7 +171,8 @@ def _configure_legend(ax: plt.Axes, legend_handles: dict[int, Patch]):
|
|
111
171
|
ax.legend(
|
112
172
|
handles=sorted_legend_handles,
|
113
173
|
loc="upper left",
|
114
|
-
bbox_to_anchor=(1
|
174
|
+
bbox_to_anchor=(1, 1),
|
175
|
+
title=legend_title,
|
115
176
|
)
|
116
177
|
|
117
178
|
|
@@ -120,6 +181,7 @@ def _configure_axes(
|
|
120
181
|
ax: plt.Axes,
|
121
182
|
xlim: Optional[int],
|
122
183
|
number_of_x_ticks: int,
|
184
|
+
machine_labels: list[str] | None,
|
123
185
|
):
|
124
186
|
"""Sets the limits and labels for the axes."""
|
125
187
|
num_machines = len(schedule.schedule)
|
@@ -132,7 +194,9 @@ def _configure_axes(
|
|
132
194
|
for i in range(num_machines)
|
133
195
|
]
|
134
196
|
)
|
135
|
-
|
197
|
+
if machine_labels is None:
|
198
|
+
machine_labels = [str(i) for i in range(num_machines)]
|
199
|
+
ax.set_yticklabels(machine_labels)
|
136
200
|
makespan = schedule.makespan()
|
137
201
|
xlim = xlim if xlim is not None else makespan
|
138
202
|
ax.set_xlim(0, xlim)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.0a4
|
4
4
|
Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
|
5
5
|
License: MIT
|
6
6
|
Author: Pabloo22
|
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
|
|
29
29
|
<h1>JobShopLib</h1>
|
30
30
|
|
31
31
|
[](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml)
|
32
|
+
[](https://job-shop-lib.readthedocs.io/en/latest/?badge=latest)
|
32
33
|

|
33
34
|
[](https://github.com/psf/black)
|
34
35
|
[](https://opensource.org/licenses/MIT)
|
@@ -39,7 +40,7 @@ JobShopLib is a Python package for creating, solving, and visualizing Job Shop S
|
|
39
40
|
|
40
41
|
It follows a modular design, allowing users to easily extend the library with new solvers, dispatching rules, visualization functions, etc.
|
41
42
|
|
42
|
-
See [
|
43
|
+
See the [documentation](https://job-shop-lib.readthedocs.io/en/latest/) for more details about the latest version.
|
43
44
|
|
44
45
|
## Installation :package:
|
45
46
|
|
@@ -47,12 +48,23 @@ See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcoww
|
|
47
48
|
|
48
49
|
JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/) and it supports Python 3.10+.
|
49
50
|
|
50
|
-
You can install the latest version using `pip`:
|
51
|
+
You can install the latest stable version (version 0.5.1) using `pip`:
|
51
52
|
|
52
53
|
```bash
|
53
54
|
pip install job-shop-lib
|
54
55
|
```
|
55
56
|
|
57
|
+
See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcowwTZ4H?usp=sharing) Google Colab notebook for a quick start guide!
|
58
|
+
|
59
|
+
|
60
|
+
Version 1.0.0 is currently in alpha stage and can be installed with:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
pip install job-shop-lib==1.0.0a4
|
64
|
+
```
|
65
|
+
|
66
|
+
Although this version is not stable and may contain breaking changes in subsequent releases, it is recommended to install it to access the new reinforcement learning environments and familiarize yourself with new changes (see the [latest pull requests](https://github.com/Pabloo22/job_shop_lib/pulls?q=is%3Apr+is%3Aclosed)). This version is the first one with a [documentation page](https://job-shop-lib.readthedocs.io/en/latest/).
|
67
|
+
|
56
68
|
<!-- end installation -->
|
57
69
|
|
58
70
|
<!-- key features -->
|