job-shop-lib 0.5.0__py3-none-any.whl → 1.0.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.
Files changed (93) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +19 -0
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/_instance_generator.py +133 -0
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generators/__init__.py +0 -7
  84. job_shop_lib/generators/basic_generator.py +0 -197
  85. job_shop_lib/graphs/constants.py +0 -21
  86. job_shop_lib/graphs/job_shop_graph.py +0 -202
  87. job_shop_lib/graphs/node.py +0 -166
  88. job_shop_lib/operation.py +0 -122
  89. job_shop_lib/visualization/agent_task_graph.py +0 -257
  90. job_shop_lib/visualization/create_gif.py +0 -209
  91. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  92. job_shop_lib-0.5.0.dist-info/RECORD +0 -48
  93. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,418 @@
1
+ """Module for visualizing the disjunctive graph of a job shop instance."""
2
+
3
+ import functools
4
+ from typing import Any, Optional, Tuple, Dict, Union
5
+ from collections.abc import Callable, Sequence, Iterable
6
+ import warnings
7
+ import copy
8
+
9
+ import matplotlib
10
+ import matplotlib.colors
11
+ import matplotlib.pyplot as plt
12
+ import networkx as nx
13
+ from networkx.drawing.nx_agraph import graphviz_layout
14
+
15
+ from job_shop_lib import JobShopInstance
16
+ from job_shop_lib.graphs import (
17
+ JobShopGraph,
18
+ EdgeType,
19
+ NodeType,
20
+ Node,
21
+ build_disjunctive_graph,
22
+ )
23
+ from job_shop_lib.exceptions import ValidationError
24
+
25
+
26
+ Layout = Callable[[nx.Graph], Dict[str, Tuple[float, float]]]
27
+
28
+
29
+ def duration_labeler(node: Node) -> str:
30
+ """Returns a label for the node with the processing time.
31
+
32
+ In the form ``"$p_{ij}=duration$"``, where $i$ is the job id and $j$ is
33
+ the position in the job.
34
+
35
+ Args:
36
+ node:
37
+ The operation node to label. See
38
+ :class:`~job_shop_lib.graphs.Node`.
39
+ """
40
+ return (
41
+ f"$p_{{{node.operation.job_id + 1}"
42
+ f"{node.operation.position_in_job + 1}}}={node.operation.duration}$"
43
+ )
44
+
45
+
46
+ # This function could be improved by a function extraction refactoring
47
+ # (see `plot_gantt_chart`
48
+ # function as a reference in how to do it). That would solve the
49
+ # "too many locals" warning. However, this refactoring is not a priority at
50
+ # the moment. To compensate, sections are separated by comments.
51
+ # For the "too many arguments" warning no satisfactory solution was
52
+ # found. I believe is still better than using `**kwargs` and losing the
53
+ # function signature or adding a dataclass for configuration (it would add
54
+ # more complexity). A TypedDict could be used too, but the default
55
+ # values would not be explicit.
56
+ # pylint: disable=too-many-arguments, too-many-locals, too-many-statements
57
+ # pylint: disable=too-many-branches, line-too-long
58
+ def plot_disjunctive_graph(
59
+ job_shop: Union[JobShopGraph, JobShopInstance],
60
+ *,
61
+ title: Optional[str] = None,
62
+ figsize: Tuple[float, float] = (6, 4),
63
+ node_size: int = 1600,
64
+ edge_width: int = 2,
65
+ font_size: int = 10,
66
+ arrow_size: int = 35,
67
+ alpha: float = 0.95,
68
+ operation_node_labeler: Callable[[Node], str] = duration_labeler,
69
+ node_font_color: str = "white",
70
+ machine_colors: Optional[
71
+ Dict[int, Tuple[float, float, float, float]]
72
+ ] = None,
73
+ color_map: str = "Dark2_r",
74
+ disjunctive_edge_color: str = "red",
75
+ conjunctive_edge_color: str = "black",
76
+ layout: Optional[Layout] = None,
77
+ draw_disjunctive_edges: Union[bool, str] = True,
78
+ conjunctive_edges_additional_params: Optional[Dict[str, Any]] = None,
79
+ disjunctive_edges_additional_params: Optional[Dict[str, Any]] = None,
80
+ conjunctive_patch_label: str = "Conjunctive edges",
81
+ disjunctive_patch_label: str = "Disjunctive edges",
82
+ legend_text: str = "$p_{ij}=$duration of $O_{ij}$",
83
+ show_machine_colors_in_legend: bool = True,
84
+ machine_labels: Optional[Sequence[str]] = None,
85
+ legend_location: str = "upper left",
86
+ legend_bbox_to_anchor: Tuple[float, float] = (1.01, 1),
87
+ start_node_label: str = "$S$",
88
+ end_node_label: str = "$T$",
89
+ font_family: str = "sans-serif",
90
+ ) -> Tuple[plt.Figure, plt.Axes]:
91
+ r"""Plots the disjunctive graph of the given job shop instance or graph.
92
+
93
+ Args:
94
+ job_shop:
95
+ The job shop instance or graph to plot. Can be either a
96
+ :class:`JobShopGraph` or a :class:`JobShopInstance`. If a job shop
97
+ instance is given, the disjunctive graph is built before plotting
98
+ using the :func:`~job_shop_lib.graphs.build_disjunctive_graph`.
99
+ title:
100
+ The title of the graph (default is ``"Disjunctive Graph
101
+ Visualization: {job_shop.instance.name}"``).
102
+ figsize:
103
+ The size of the figure (default is (6, 4)).
104
+ node_size:
105
+ The size of the nodes (default is 1600).
106
+ edge_width:
107
+ The width of the edges (default is 2).
108
+ font_size:
109
+ The font size of the node labels (default is 10).
110
+ arrow_size:
111
+ The size of the arrows (default is 35).
112
+ alpha:
113
+ The transparency level of the nodes and edges (default is 0.95).
114
+ operation_node_labeler:
115
+ A function that formats labels for operation nodes. Receives a
116
+ :class:`~job_shop_lib.graphs.Node` and returns a string.
117
+ The default is :func:`duration_labeler`, which labels the nodes
118
+ with their duration.
119
+ node_font_color:
120
+ The color of the node labels (default is ``"white"``).
121
+ machine_colors:
122
+ A dictionary that maps machine ids to colors. If not provided,
123
+ the colors are generated using the ``color_map``. If provided,
124
+ the colors are used as the base for the node colors. The
125
+ dictionary should have the form ``{machine_id: (r, g, b, a)}``.
126
+ For source and sink nodes use ``-1`` as the machine id.
127
+ color_map:
128
+ The color map to use for the nodes (default is ``"Dark2_r"``).
129
+ disjunctive_edge_color:
130
+ The color of the disjunctive edges (default is ``"red"``).
131
+ conjunctive_edge_color:
132
+ The color of the conjunctive edges (default is ``"black"``).
133
+ layout:
134
+ The layout of the graph (default is ``graphviz_layout`` with
135
+ ``prog="dot"`` and ``args="-Grankdir=LR"``). If not available,
136
+ the spring layout is used. To install pygraphviz, check
137
+ `pygraphviz documentation
138
+ <https://pygraphviz.github.io/documentation/stable/install.html>`_.
139
+ draw_disjunctive_edges:
140
+ Whether to draw disjunctive edges (default is ``True``). If
141
+ ``False``, only conjunctive edges are drawn. If ``"single_edge",``
142
+ the disjunctive edges are drawn as undirected edges by removing one
143
+ of the directions. If using this last option is recommended to set
144
+ the "arrowstyle" parameter to ``"-"`` or ``"<->"`` in the
145
+ ``disjunctive_edges_additional_params`` to make the edges look
146
+ better. See `matplotlib documentation on arrow styles <https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.ArrowStyle.html#matplotlib.patches.ArrowStyle>`_
147
+ and `nx.draw_networkx_edges <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.nx_pylab.draw_networkx_edges.html>`_
148
+ for more information.
149
+ conjunctive_edges_additional_params:
150
+ Additional parameters to pass to the conjunctive edges when
151
+ drawing them (default is ``None``). See the documentation of
152
+ `nx.draw_networkx_edges <https://networkx.org/documentation/stable/reference/generated/networkx.drawing.nx_pylab.draw_networkx_edges.html>`_
153
+ for more information. The parameters that are explicitly set by
154
+ this function and should not be part of this dictionary are
155
+ ``edgelist``, ``pos``, ``width``, ``edge_color``, and
156
+ ``arrowsize``.
157
+ disjunctive_edges_additional_params:
158
+ Same as ``conjunctive_edges_additional_params``, but for
159
+ disjunctive edges (default is ``None``).
160
+ conjunctive_patch_label:
161
+ The label for the conjunctive edges in the legend (default is
162
+ ``"Conjunctive edges"``).
163
+ disjunctive_patch_label:
164
+ The label for the disjunctive edges in the legend (default is
165
+ ``"Disjunctive edges"``).
166
+ legend_text:
167
+ Text to display in the legend after the conjunctive and
168
+ disjunctive edges labels (default is
169
+ ``"$p_{ij}=$duration of $O_{ij}$"``).
170
+ show_machine_colors_in_legend:
171
+ Whether to show the colors of the machines in the legend
172
+ (default is ``True``).
173
+ machine_labels:
174
+ The labels for the machines (default is
175
+ ``[f"Machine {i}" for i in range(num_machines)]``). Not used if
176
+ ``show_machine_colors_in_legend`` is ``False``.
177
+ legend_location:
178
+ The location of the legend (default is "upper left").
179
+ legend_bbox_to_anchor:
180
+ The anchor of the legend box (default is ``(1.01, 1)``).
181
+ start_node_label:
182
+ The label for the start node (default is ``"$S$"``).
183
+ end_node_label:
184
+ The label for the end node (default is ``"$T$"``).
185
+ font_family:
186
+ The font family of the node labels (default is ``"sans-serif"``).
187
+
188
+ Returns:
189
+ A matplotlib Figure object representing the disjunctive graph.
190
+
191
+ Example:
192
+
193
+ .. code-block:: python
194
+
195
+ job_shop_instance = JobShopInstance(...) # or a JobShopGraph
196
+ fig = plot_disjunctive_graph(job_shop_instance)
197
+
198
+ """ # noqa: E501
199
+
200
+ if isinstance(job_shop, JobShopInstance):
201
+ job_shop_graph = build_disjunctive_graph(job_shop)
202
+ else:
203
+ job_shop_graph = job_shop
204
+
205
+ # Set up the plot
206
+ # ----------------
207
+ plt.figure(figsize=figsize)
208
+ if title is None:
209
+ title = (
210
+ f"Disjunctive Graph Visualization: {job_shop_graph.instance.name}"
211
+ )
212
+ plt.title(title)
213
+
214
+ # Set up the layout
215
+ # -----------------
216
+ if layout is None:
217
+ layout = functools.partial(
218
+ graphviz_layout, prog="dot", args="-Grankdir=LR"
219
+ )
220
+
221
+ temp_graph = copy.deepcopy(job_shop_graph.graph)
222
+ # Remove disjunctive edges to get a better layout
223
+ temp_graph.remove_edges_from(
224
+ [
225
+ (u, v)
226
+ for u, v, d in job_shop_graph.graph.edges(data=True)
227
+ if d["type"] == EdgeType.DISJUNCTIVE
228
+ ]
229
+ )
230
+
231
+ try:
232
+ pos = layout(temp_graph)
233
+ except ImportError:
234
+ warnings.warn(
235
+ "Default layout requires pygraphviz http://pygraphviz.github.io/. "
236
+ "Using spring layout instead.",
237
+ )
238
+ pos = nx.spring_layout(temp_graph)
239
+
240
+ # Draw nodes
241
+ # ----------
242
+ operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
243
+ cmap_func: Optional[matplotlib.colors.Colormap] = None
244
+ if machine_colors is None:
245
+ machine_colors = {}
246
+ cmap_func = matplotlib.colormaps.get_cmap(color_map)
247
+ remaining_machines = job_shop_graph.instance.num_machines
248
+ for operation_node in operation_nodes:
249
+ if job_shop_graph.is_removed(operation_node.node_id):
250
+ continue
251
+ machine_id = operation_node.operation.machine_id
252
+ if machine_id not in machine_colors:
253
+ machine_colors[machine_id] = cmap_func(
254
+ (_get_node_color(operation_node) + 1)
255
+ / job_shop_graph.instance.num_machines
256
+ )
257
+ remaining_machines -= 1
258
+ if remaining_machines == 0:
259
+ break
260
+ node_colors: list[Any] = [
261
+ _get_node_color(node)
262
+ for node in job_shop_graph.nodes
263
+ if not job_shop_graph.is_removed(node.node_id)
264
+ ]
265
+ else:
266
+ node_colors = []
267
+ for node in job_shop_graph.nodes:
268
+ if job_shop_graph.is_removed(node.node_id):
269
+ continue
270
+ if node.node_type == NodeType.OPERATION:
271
+ machine_id = node.operation.machine_id
272
+ else:
273
+ machine_id = -1
274
+ node_colors.append(machine_colors[machine_id])
275
+
276
+ nx.draw_networkx_nodes(
277
+ job_shop_graph.graph,
278
+ pos,
279
+ node_size=node_size,
280
+ node_color=node_colors,
281
+ alpha=alpha,
282
+ cmap=cmap_func,
283
+ )
284
+
285
+ # Draw edges
286
+ # ----------
287
+ conjunctive_edges = [
288
+ (u, v)
289
+ for u, v, d in job_shop_graph.graph.edges(data=True)
290
+ if d["type"] == EdgeType.CONJUNCTIVE
291
+ ]
292
+ disjunctive_edges: Iterable[Tuple[int, int]] = [
293
+ (u, v)
294
+ for u, v, d in job_shop_graph.graph.edges(data=True)
295
+ if d["type"] == EdgeType.DISJUNCTIVE
296
+ ]
297
+ if conjunctive_edges_additional_params is None:
298
+ conjunctive_edges_additional_params = {}
299
+ if disjunctive_edges_additional_params is None:
300
+ disjunctive_edges_additional_params = {}
301
+
302
+ nx.draw_networkx_edges(
303
+ job_shop_graph.graph,
304
+ pos,
305
+ edgelist=conjunctive_edges,
306
+ width=edge_width,
307
+ edge_color=conjunctive_edge_color,
308
+ arrowsize=arrow_size,
309
+ **conjunctive_edges_additional_params,
310
+ )
311
+
312
+ if draw_disjunctive_edges:
313
+ if draw_disjunctive_edges == "single_edge":
314
+ # Filter the disjunctive edges to remove one of the directions
315
+ disjunctive_edges_filtered = set()
316
+ for u, v in disjunctive_edges:
317
+ if u > v:
318
+ u, v = v, u
319
+ disjunctive_edges_filtered.add((u, v))
320
+ disjunctive_edges = disjunctive_edges_filtered
321
+ nx.draw_networkx_edges(
322
+ job_shop_graph.graph,
323
+ pos,
324
+ edgelist=disjunctive_edges,
325
+ width=edge_width,
326
+ edge_color=disjunctive_edge_color,
327
+ arrowsize=arrow_size,
328
+ **disjunctive_edges_additional_params,
329
+ )
330
+
331
+ # Draw node labels
332
+ # ----------------
333
+ labels = {}
334
+ if job_shop_graph.nodes_by_type[NodeType.SOURCE]:
335
+ source_node = job_shop_graph.nodes_by_type[NodeType.SOURCE][0]
336
+ if not job_shop_graph.is_removed(source_node.node_id):
337
+ labels[source_node] = start_node_label
338
+ if job_shop_graph.nodes_by_type[NodeType.SINK]:
339
+ sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
340
+ # check if the sink node is removed
341
+ if not job_shop_graph.is_removed(sink_node.node_id):
342
+ labels[sink_node] = end_node_label
343
+ for operation_node in operation_nodes:
344
+ if job_shop_graph.is_removed(operation_node.node_id):
345
+ continue
346
+ labels[operation_node] = operation_node_labeler(operation_node)
347
+
348
+ nx.draw_networkx_labels(
349
+ job_shop_graph.graph,
350
+ pos,
351
+ labels=labels,
352
+ font_color=node_font_color,
353
+ font_size=font_size,
354
+ font_family=font_family,
355
+ )
356
+ # Final touches
357
+ # -------------
358
+ plt.axis("off")
359
+ plt.tight_layout()
360
+ # Create a legend to indicate the meaning of the edge colors
361
+ conjunctive_patch = matplotlib.patches.Patch(
362
+ color=conjunctive_edge_color, label=conjunctive_patch_label
363
+ )
364
+ disjunctive_patch = matplotlib.patches.Patch(
365
+ color=disjunctive_edge_color, label=disjunctive_patch_label
366
+ )
367
+ handles = [conjunctive_patch, disjunctive_patch]
368
+
369
+ # Add machine colors to the legend
370
+ if show_machine_colors_in_legend:
371
+ machine_patches = [
372
+ matplotlib.patches.Patch(
373
+ color=color,
374
+ label=(
375
+ machine_labels[machine_id]
376
+ if machine_labels is not None
377
+ else f"Machine {machine_id}"
378
+ ),
379
+ )
380
+ for machine_id, color in sorted(
381
+ machine_colors.items(), key=lambda x: x[0]
382
+ )
383
+ ]
384
+ handles.extend(machine_patches)
385
+
386
+ # Add to the legend the meaning of m and d
387
+ if legend_text:
388
+ extra = matplotlib.patches.Rectangle(
389
+ (0, 0),
390
+ 1,
391
+ 1,
392
+ fc="w",
393
+ fill=False,
394
+ edgecolor="none",
395
+ linewidth=0,
396
+ label=legend_text,
397
+ )
398
+ handles.append(extra)
399
+
400
+ plt.legend(
401
+ handles=handles,
402
+ loc=legend_location,
403
+ bbox_to_anchor=legend_bbox_to_anchor,
404
+ borderaxespad=0.0,
405
+ )
406
+ return plt.gcf(), plt.gca()
407
+
408
+
409
+ def _get_node_color(node: Node) -> int:
410
+ """Returns the color of the node."""
411
+ if node.node_type == NodeType.SOURCE:
412
+ return -1
413
+ if node.node_type == NodeType.SINK:
414
+ return -1
415
+ if node.node_type == NodeType.OPERATION:
416
+ return node.operation.machine_id
417
+
418
+ raise ValidationError("Invalid node type.")