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.
Files changed (37) hide show
  1. job_shop_lib/_job_shop_instance.py +119 -55
  2. job_shop_lib/_operation.py +18 -7
  3. job_shop_lib/_schedule.py +13 -15
  4. job_shop_lib/_scheduled_operation.py +17 -18
  5. job_shop_lib/dispatching/__init__.py +4 -0
  6. job_shop_lib/dispatching/_dispatcher.py +36 -47
  7. job_shop_lib/dispatching/_dispatcher_observer_config.py +15 -2
  8. job_shop_lib/dispatching/_factories.py +10 -2
  9. job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
  10. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +0 -1
  11. job_shop_lib/dispatching/feature_observers/_factory.py +21 -18
  12. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +1 -0
  13. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +1 -1
  14. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +44 -25
  15. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
  16. job_shop_lib/generation/_general_instance_generator.py +33 -34
  17. job_shop_lib/generation/_instance_generator.py +14 -17
  18. job_shop_lib/generation/_transformations.py +11 -8
  19. job_shop_lib/graphs/__init__.py +3 -0
  20. job_shop_lib/graphs/_build_disjunctive_graph.py +41 -3
  21. job_shop_lib/graphs/graph_updaters/_graph_updater.py +11 -13
  22. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +17 -20
  23. job_shop_lib/reinforcement_learning/__init__.py +16 -7
  24. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +69 -57
  25. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +43 -32
  26. job_shop_lib/reinforcement_learning/_types_and_constants.py +2 -2
  27. job_shop_lib/visualization/__init__.py +29 -10
  28. job_shop_lib/visualization/_gantt_chart_creator.py +122 -84
  29. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +68 -37
  30. job_shop_lib/visualization/_plot_disjunctive_graph.py +382 -0
  31. job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
  32. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/METADATA +15 -3
  33. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/RECORD +36 -36
  34. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/WHEEL +1 -1
  35. job_shop_lib/visualization/_disjunctive_graph.py +0 -210
  36. /job_shop_lib/visualization/{_agent_task_graph.py → _plot_agent_task_graph.py} +0 -0
  37. {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
- `f"Gantt Chart for {schedule.instance.name} instance"`
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(schedule, ax, cmap_name)
43
- _configure_legend(ax, legend_handles)
44
- _configure_axes(schedule, ax, xlim, number_of_x_ticks)
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, title: str | None
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("Time units")
54
- ax.set_ylabel("Machines")
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, ax: plt.Axes, cmap_name: str
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, label=f"Job {scheduled_op.job_id}"
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(ax: plt.Axes, legend_handles: dict[int, Patch]):
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.01, 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
- ax.set_yticklabels([str(i) for i in range(num_machines)])
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.0a2
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
  [![Tests](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml/badge.svg)](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml)
32
+ [![Documentation Status](https://readthedocs.org/projects/job-shop-lib/badge/?version=latest)](https://job-shop-lib.readthedocs.io/en/latest/?badge=latest)
32
33
  ![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)
33
34
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
34
35
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcowwTZ4H?usp=sharing) Google Colab notebook for a quick start guide!
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 -->