job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b1__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 +1 -1
- job_shop_lib/_job_shop_instance.py +34 -29
- job_shop_lib/_operation.py +4 -2
- job_shop_lib/_schedule.py +11 -11
- job_shop_lib/benchmarking/_load_benchmark.py +3 -3
- job_shop_lib/constraint_programming/_ortools_solver.py +6 -6
- job_shop_lib/dispatching/_dispatcher.py +19 -19
- job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
- job_shop_lib/dispatching/_factories.py +4 -2
- job_shop_lib/dispatching/_history_observer.py +2 -1
- job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
- job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +23 -15
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
- job_shop_lib/dispatching/rules/_utils.py +9 -8
- job_shop_lib/generation/__init__.py +8 -0
- job_shop_lib/generation/_general_instance_generator.py +42 -64
- job_shop_lib/generation/_instance_generator.py +11 -7
- job_shop_lib/generation/_transformations.py +5 -4
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +7 -7
- job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
- job_shop_lib/graphs/_job_shop_graph.py +17 -13
- job_shop_lib/graphs/_node.py +6 -4
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
- job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
- job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
- job_shop_lib/reinforcement_learning/_utils.py +3 -3
- job_shop_lib/visualization/__init__.py +0 -60
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
- job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
- job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/METADATA +17 -15
- job_shop_lib-1.0.0b1.dist-info/RECORD +69 -0
- job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
- job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Module for plotting static Gantt charts for job shop schedules."""
|
2
2
|
|
3
|
-
from typing import Optional
|
3
|
+
from typing import Optional, List, Tuple, Dict
|
4
4
|
|
5
5
|
from matplotlib.figure import Figure
|
6
6
|
import matplotlib.pyplot as plt
|
@@ -9,23 +9,22 @@ from matplotlib.patches import Patch
|
|
9
9
|
|
10
10
|
from job_shop_lib import Schedule, ScheduledOperation
|
11
11
|
|
12
|
-
|
13
12
|
_BASE_Y_POSITION = 1
|
14
13
|
_Y_POSITION_INCREMENT = 10
|
15
14
|
|
16
15
|
|
17
16
|
def plot_gantt_chart(
|
18
17
|
schedule: Schedule,
|
19
|
-
title: str
|
18
|
+
title: Optional[str] = None,
|
20
19
|
cmap_name: str = "viridis",
|
21
|
-
xlim: int
|
20
|
+
xlim: Optional[int] = None,
|
22
21
|
number_of_x_ticks: int = 15,
|
23
|
-
job_labels:
|
24
|
-
machine_labels:
|
22
|
+
job_labels: Optional[List[str]] = None,
|
23
|
+
machine_labels: Optional[List[str]] = None,
|
25
24
|
legend_title: str = "",
|
26
25
|
x_label: str = "Time units",
|
27
26
|
y_label: str = "Machines",
|
28
|
-
) ->
|
27
|
+
) -> Tuple[Figure, plt.Axes]:
|
29
28
|
"""Plots a Gantt chart for the schedule.
|
30
29
|
|
31
30
|
This function generates a Gantt chart that visualizes the schedule of jobs
|
@@ -91,10 +90,10 @@ def plot_gantt_chart(
|
|
91
90
|
|
92
91
|
def _initialize_plot(
|
93
92
|
schedule: Schedule,
|
94
|
-
title: str
|
93
|
+
title: Optional[str],
|
95
94
|
x_label: str = "Time units",
|
96
95
|
y_label: str = "Machines",
|
97
|
-
) ->
|
96
|
+
) -> Tuple[Figure, plt.Axes]:
|
98
97
|
"""Initializes the plot."""
|
99
98
|
fig, ax = plt.subplots()
|
100
99
|
ax.set_xlabel(x_label)
|
@@ -111,8 +110,8 @@ def _plot_machine_schedules(
|
|
111
110
|
schedule: Schedule,
|
112
111
|
ax: plt.Axes,
|
113
112
|
cmap_name: str,
|
114
|
-
job_labels:
|
115
|
-
) ->
|
113
|
+
job_labels: Optional[List[str]],
|
114
|
+
) -> Dict[int, Patch]:
|
116
115
|
"""Plots the schedules for each machine."""
|
117
116
|
max_job_id = schedule.instance.num_jobs - 1
|
118
117
|
cmap = plt.get_cmap(cmap_name, max_job_id + 1)
|
@@ -138,7 +137,7 @@ def _plot_machine_schedules(
|
|
138
137
|
return legend_handles
|
139
138
|
|
140
139
|
|
141
|
-
def _get_job_label(job_labels:
|
140
|
+
def _get_job_label(job_labels: Optional[List[str]], job_id: int) -> str:
|
142
141
|
"""Returns the label for the job."""
|
143
142
|
if job_labels is None:
|
144
143
|
return f"Job {job_id}"
|
@@ -162,7 +161,7 @@ def _plot_scheduled_operation(
|
|
162
161
|
|
163
162
|
|
164
163
|
def _configure_legend(
|
165
|
-
ax: plt.Axes, legend_handles:
|
164
|
+
ax: plt.Axes, legend_handles: Dict[int, Patch], legend_title: str
|
166
165
|
):
|
167
166
|
"""Configures the legend for the plot."""
|
168
167
|
sorted_legend_handles = [
|
@@ -0,0 +1,29 @@
|
|
1
|
+
"""Contains functions and classes for visualizing job shop scheduling problems.
|
2
|
+
|
3
|
+
.. autosummary::
|
4
|
+
|
5
|
+
plot_disjunctive_graph
|
6
|
+
plot_heterogeneous_graph
|
7
|
+
three_columns_layout
|
8
|
+
duration_labeler
|
9
|
+
color_nodes_by_machine
|
10
|
+
|
11
|
+
"""
|
12
|
+
|
13
|
+
from ._plot_disjunctive_graph import (
|
14
|
+
plot_disjunctive_graph,
|
15
|
+
duration_labeler,
|
16
|
+
)
|
17
|
+
from ._plot_resource_task_graph import (
|
18
|
+
plot_resource_task_graph,
|
19
|
+
three_columns_layout,
|
20
|
+
color_nodes_by_machine,
|
21
|
+
)
|
22
|
+
|
23
|
+
__all__ = [
|
24
|
+
"plot_disjunctive_graph",
|
25
|
+
"plot_resource_task_graph",
|
26
|
+
"three_columns_layout",
|
27
|
+
"duration_labeler",
|
28
|
+
"color_nodes_by_machine",
|
29
|
+
]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Module for visualizing the disjunctive graph of a job shop instance."""
|
2
2
|
|
3
3
|
import functools
|
4
|
-
from typing import Any
|
4
|
+
from typing import Any, Optional, Tuple, Dict, Union
|
5
5
|
from collections.abc import Callable, Sequence, Iterable
|
6
6
|
import warnings
|
7
7
|
import copy
|
@@ -22,7 +22,7 @@ from job_shop_lib.graphs import (
|
|
22
22
|
from job_shop_lib.exceptions import ValidationError
|
23
23
|
|
24
24
|
|
25
|
-
Layout = Callable[[nx.Graph],
|
25
|
+
Layout = Callable[[nx.Graph], Dict[str, Tuple[float, float]]]
|
26
26
|
|
27
27
|
|
28
28
|
def duration_labeler(node: Node) -> str:
|
@@ -50,15 +50,15 @@ def duration_labeler(node: Node) -> str:
|
|
50
50
|
# For the "too many arguments" warning no satisfactory solution was
|
51
51
|
# found. I believe is still better than using `**kwargs` and losing the
|
52
52
|
# function signature or adding a dataclass for configuration (it would add
|
53
|
-
#
|
53
|
+
# more complexity). A TypedDict could be used too, but the default
|
54
54
|
# values would not be explicit.
|
55
55
|
# pylint: disable=too-many-arguments, too-many-locals, too-many-statements
|
56
56
|
# pylint: disable=too-many-branches, line-too-long
|
57
57
|
def plot_disjunctive_graph(
|
58
|
-
job_shop: JobShopGraph
|
58
|
+
job_shop: Union[JobShopGraph, JobShopInstance],
|
59
59
|
*,
|
60
|
-
title: str
|
61
|
-
figsize:
|
60
|
+
title: Optional[str] = None,
|
61
|
+
figsize: Tuple[float, float] = (6, 4),
|
62
62
|
node_size: int = 1600,
|
63
63
|
edge_width: int = 2,
|
64
64
|
font_size: int = 10,
|
@@ -69,21 +69,21 @@ def plot_disjunctive_graph(
|
|
69
69
|
color_map: str = "Dark2_r",
|
70
70
|
disjunctive_edge_color: str = "red",
|
71
71
|
conjunctive_edge_color: str = "black",
|
72
|
-
layout: Layout
|
73
|
-
draw_disjunctive_edges: bool
|
74
|
-
conjunctive_edges_additional_params:
|
75
|
-
disjunctive_edges_additional_params:
|
72
|
+
layout: Optional[Layout] = None,
|
73
|
+
draw_disjunctive_edges: Union[bool, str] = True,
|
74
|
+
conjunctive_edges_additional_params: Optional[Dict[str, Any]] = None,
|
75
|
+
disjunctive_edges_additional_params: Optional[Dict[str, Any]] = None,
|
76
76
|
conjunctive_patch_label: str = "Conjunctive edges",
|
77
77
|
disjunctive_patch_label: str = "Disjunctive edges",
|
78
78
|
legend_text: str = "$p_{ij}=$duration of $O_{ij}$",
|
79
79
|
show_machine_colors_in_legend: bool = True,
|
80
|
-
machine_labels: Sequence[str]
|
80
|
+
machine_labels: Optional[Sequence[str]] = None,
|
81
81
|
legend_location: str = "upper left",
|
82
|
-
legend_bbox_to_anchor:
|
82
|
+
legend_bbox_to_anchor: Tuple[float, float] = (1.01, 1),
|
83
83
|
start_node_label: str = "$S$",
|
84
84
|
end_node_label: str = "$T$",
|
85
85
|
font_family: str = "sans-serif",
|
86
|
-
) ->
|
86
|
+
) -> Tuple[plt.Figure, plt.Axes]:
|
87
87
|
r"""Plots the disjunctive graph of the given job shop instance or graph.
|
88
88
|
|
89
89
|
Args:
|
@@ -251,7 +251,7 @@ def plot_disjunctive_graph(
|
|
251
251
|
for u, v, d in job_shop_graph.graph.edges(data=True)
|
252
252
|
if d["type"] == EdgeType.CONJUNCTIVE
|
253
253
|
]
|
254
|
-
disjunctive_edges: Iterable[
|
254
|
+
disjunctive_edges: Iterable[Tuple[int, int]] = [
|
255
255
|
(u, v)
|
256
256
|
for u, v, d in job_shop_graph.graph.edges(data=True)
|
257
257
|
if d["type"] == EdgeType.DISJUNCTIVE
|
@@ -299,7 +299,7 @@ def plot_disjunctive_graph(
|
|
299
299
|
|
300
300
|
sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
|
301
301
|
labels[sink_node] = end_node_label
|
302
|
-
machine_colors: dict[int,
|
302
|
+
machine_colors: dict[int, Tuple[float, float, float, float]] = {}
|
303
303
|
for operation_node in operation_nodes:
|
304
304
|
if job_shop_graph.is_removed(operation_node.node_id):
|
305
305
|
continue
|
@@ -343,7 +343,9 @@ def plot_disjunctive_graph(
|
|
343
343
|
else f"Machine {machine_id}"
|
344
344
|
),
|
345
345
|
)
|
346
|
-
for machine_id, color in
|
346
|
+
for machine_id, color in sorted(
|
347
|
+
machine_colors.items(), key=lambda x: x[0]
|
348
|
+
)
|
347
349
|
]
|
348
350
|
handles.extend(machine_patches)
|
349
351
|
|
@@ -0,0 +1,389 @@
|
|
1
|
+
"""Contains functions to plot a resource-task graph representation of a job
|
2
|
+
shop instance.
|
3
|
+
|
4
|
+
It was introduced by Junyoung Park et al. (2021).
|
5
|
+
In contrast to the disjunctive graph, instead of connecting operations that
|
6
|
+
share the same resources directly by disjunctive edges, operation nodes are
|
7
|
+
connected with machine ones. All machine nodes are connected between them, and
|
8
|
+
all operation nodes from the same job are connected by non-directed edges too.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from collections.abc import Callable
|
12
|
+
from copy import deepcopy
|
13
|
+
from typing import Optional, Any, Tuple, Dict, Union, List
|
14
|
+
|
15
|
+
import matplotlib.pyplot as plt
|
16
|
+
import matplotlib.colors as mcolors
|
17
|
+
import networkx as nx
|
18
|
+
|
19
|
+
from job_shop_lib.graphs import NodeType, JobShopGraph, Node
|
20
|
+
|
21
|
+
|
22
|
+
def plot_resource_task_graph(
|
23
|
+
job_shop_graph: JobShopGraph,
|
24
|
+
*,
|
25
|
+
title: Optional[str] = None,
|
26
|
+
figsize: Tuple[int, int] = (10, 10),
|
27
|
+
layout: Optional[Dict[Node, Tuple[float, float]]] = None,
|
28
|
+
node_size: int = 1200,
|
29
|
+
node_font_color: str = "k",
|
30
|
+
font_size: int = 10,
|
31
|
+
alpha: float = 0.95,
|
32
|
+
add_legend: bool = False,
|
33
|
+
node_shapes: Optional[Dict[str, str]] = None,
|
34
|
+
node_color_map: Optional[
|
35
|
+
Callable[[Node], Tuple[float, float, float, float]]
|
36
|
+
] = None,
|
37
|
+
default_node_color: Union[
|
38
|
+
str, Tuple[float, float, float, float]
|
39
|
+
] = "lightblue",
|
40
|
+
machine_color_map_name: str = "tab10",
|
41
|
+
legend_text: str = "$p_{ij}$ = duration of $O_{ij}$",
|
42
|
+
edge_additional_params: Optional[Dict[str, Any]] = None,
|
43
|
+
draw_only_one_edge: bool = False,
|
44
|
+
) -> plt.Figure:
|
45
|
+
"""Returns a plot of the hetereogeneous graph of the instance.
|
46
|
+
|
47
|
+
Machine and job nodes are represented by squares, and the operation nodes
|
48
|
+
are represented by circles.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
job_shop_graph:
|
52
|
+
The job shop graph instance.
|
53
|
+
title:
|
54
|
+
The title of the plot. If ``None``, the title "Heterogeneous Graph
|
55
|
+
Visualization: {instance_name}" is used. The default is ``None``.
|
56
|
+
figsize:
|
57
|
+
The size of the figure. It should be a tuple with the width and
|
58
|
+
height in inches. The default is ``(10, 10)``.
|
59
|
+
layout:
|
60
|
+
A dictionary with the position of each node in the graph. The keys
|
61
|
+
are the node ids, and the values are tuples with the x and y
|
62
|
+
coordinates. If ``None``, the :func:`three_columns_layout` function
|
63
|
+
is used. The default is ``None``.
|
64
|
+
node_size:
|
65
|
+
The size of the nodes. The default is 1000.
|
66
|
+
alpha:
|
67
|
+
The transparency of the nodes. It should be a float between 0 and
|
68
|
+
1. The default is 0.95.
|
69
|
+
add_legend:
|
70
|
+
Whether to add a legend with the meaning of the colors and shapes.
|
71
|
+
The default is ``False``.
|
72
|
+
node_shapes:
|
73
|
+
A dictionary with the shapes of the nodes. The keys are the node
|
74
|
+
types, and the values are the shapes. The default is
|
75
|
+
``{"machine": "s", "job": "d", "operation": "o", "global": "o"}``.
|
76
|
+
node_color_map:
|
77
|
+
A function that receives a node and returns a tuple with the RGBA
|
78
|
+
values of the color to use in the plot. If ``None``,
|
79
|
+
:func:`color_nodes_by_machine` is used.
|
80
|
+
machine_color_map_name:
|
81
|
+
The name of the colormap to use for the machines. This argument is
|
82
|
+
only used if ``node_color_map`` is ``None``. The default is
|
83
|
+
``"tab10"``.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
The figure of the plot. This figure can be used to save the plot to a
|
87
|
+
file or to show it in a Jupyter notebook.
|
88
|
+
"""
|
89
|
+
if title is None:
|
90
|
+
title = (
|
91
|
+
"Heterogeneous Graph Visualization:"
|
92
|
+
f"{job_shop_graph.instance.name}"
|
93
|
+
)
|
94
|
+
# Create a new figure and axis
|
95
|
+
fig, ax = plt.subplots(figsize=figsize)
|
96
|
+
fig.suptitle(title)
|
97
|
+
|
98
|
+
# Create the networkx graph
|
99
|
+
graph = job_shop_graph.graph
|
100
|
+
nodes = job_shop_graph.non_removed_nodes()
|
101
|
+
|
102
|
+
# Create the layout if it was not provided
|
103
|
+
if layout is None:
|
104
|
+
layout = three_columns_layout(job_shop_graph)
|
105
|
+
|
106
|
+
# Define colors and shapes
|
107
|
+
color_map = plt.get_cmap(machine_color_map_name)
|
108
|
+
machine_colors = {
|
109
|
+
machine.machine_id: color_map(i)
|
110
|
+
for i, machine in enumerate(
|
111
|
+
job_shop_graph.nodes_by_type[NodeType.MACHINE]
|
112
|
+
)
|
113
|
+
}
|
114
|
+
node_color_map = (
|
115
|
+
color_nodes_by_machine(machine_colors, default_node_color)
|
116
|
+
if node_color_map is None
|
117
|
+
else node_color_map
|
118
|
+
)
|
119
|
+
node_colors = [
|
120
|
+
node_color_map(node) for node in job_shop_graph.nodes
|
121
|
+
] # We need to get the color of all nodes to avoid an index error
|
122
|
+
if node_shapes is None:
|
123
|
+
node_shapes = {
|
124
|
+
"machine": "s",
|
125
|
+
"job": "d",
|
126
|
+
"operation": "o",
|
127
|
+
"global": "o",
|
128
|
+
}
|
129
|
+
|
130
|
+
# Draw nodes with different shapes based on their type
|
131
|
+
for node_type, shape in node_shapes.items():
|
132
|
+
current_nodes = [
|
133
|
+
node.node_id
|
134
|
+
for node in nodes
|
135
|
+
if node.node_type.name.lower() == node_type
|
136
|
+
]
|
137
|
+
nx.draw_networkx_nodes(
|
138
|
+
graph,
|
139
|
+
layout,
|
140
|
+
nodelist=current_nodes,
|
141
|
+
node_color=[node_colors[i] for i in current_nodes],
|
142
|
+
node_shape=shape,
|
143
|
+
ax=ax,
|
144
|
+
node_size=node_size,
|
145
|
+
alpha=alpha,
|
146
|
+
)
|
147
|
+
|
148
|
+
# Draw edges
|
149
|
+
if edge_additional_params is None:
|
150
|
+
edge_additional_params = {}
|
151
|
+
if draw_only_one_edge:
|
152
|
+
graph = deepcopy(graph)
|
153
|
+
edges = list(graph.edges)
|
154
|
+
present_edges = set()
|
155
|
+
for edge in edges:
|
156
|
+
unorder_edge = frozenset(edge)
|
157
|
+
if unorder_edge in present_edges:
|
158
|
+
graph.remove_edge(*edge)
|
159
|
+
else:
|
160
|
+
present_edges.add(unorder_edge)
|
161
|
+
|
162
|
+
nx.draw_networkx_edges(graph, layout, ax=ax, **edge_additional_params)
|
163
|
+
|
164
|
+
node_color_map = (
|
165
|
+
color_nodes_by_machine(machine_colors, "lightblue")
|
166
|
+
if node_color_map is None
|
167
|
+
else node_color_map
|
168
|
+
)
|
169
|
+
node_labels = {node.node_id: _get_node_label(node) for node in nodes}
|
170
|
+
nx.draw_networkx_labels(
|
171
|
+
graph,
|
172
|
+
layout,
|
173
|
+
node_labels,
|
174
|
+
ax=ax,
|
175
|
+
font_size=font_size,
|
176
|
+
font_color=node_font_color,
|
177
|
+
)
|
178
|
+
|
179
|
+
ax.set_axis_off()
|
180
|
+
|
181
|
+
plt.tight_layout()
|
182
|
+
|
183
|
+
# Add to the legend the meaning of m and d
|
184
|
+
if add_legend:
|
185
|
+
plt.figtext(0, 0.95, legend_text, wrap=True, fontsize=12)
|
186
|
+
return fig
|
187
|
+
|
188
|
+
|
189
|
+
def _get_node_label(node: Node) -> str:
|
190
|
+
if node.node_type == NodeType.OPERATION:
|
191
|
+
i = node.operation.job_id
|
192
|
+
j = node.operation.position_in_job
|
193
|
+
ij = str(i) + str(j)
|
194
|
+
return f"$p_{{{ij}}}={node.operation.duration}$"
|
195
|
+
if node.node_type == NodeType.MACHINE:
|
196
|
+
return f"$M_{node.machine_id}$"
|
197
|
+
if node.node_type == NodeType.JOB:
|
198
|
+
return f"$J_{node.job_id}$"
|
199
|
+
if node.node_type == NodeType.GLOBAL:
|
200
|
+
return "$G$"
|
201
|
+
|
202
|
+
raise ValueError(f"Invalid node type: {node.node_type}")
|
203
|
+
|
204
|
+
|
205
|
+
def _color_to_rgba(
|
206
|
+
color: Union[str, Tuple[float, float, float, float]]
|
207
|
+
) -> Tuple[float, float, float, float]:
|
208
|
+
if isinstance(color, str):
|
209
|
+
return mcolors.to_rgba(color)
|
210
|
+
return color
|
211
|
+
|
212
|
+
|
213
|
+
def color_nodes_by_machine(
|
214
|
+
machine_colors: Dict[int, Tuple[float, float, float, float]],
|
215
|
+
default_color: Union[str, Tuple[float, float, float, float]],
|
216
|
+
) -> Callable[[Node], Tuple[float, float, float, float]]:
|
217
|
+
"""Returns a function that assigns a color to a node based on its type.
|
218
|
+
|
219
|
+
The function returns a color based on the node type. If the node is an
|
220
|
+
operation, the color is based on the machine it is assigned to. If the node
|
221
|
+
is a machine, the color is based on the machine id. If the node is a job or
|
222
|
+
global node, the color is the default color.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
machine_colors:
|
226
|
+
A dictionary with the colors of each machine. The keys are the
|
227
|
+
machine ids, and the values are tuples with the RGBA values.
|
228
|
+
default_color:
|
229
|
+
The default color to use for job and global nodes. It can be a
|
230
|
+
string with a color name or a tuple with the RGBA values.
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
A function that receives a node and returns a tuple with the RGBA
|
234
|
+
values of the color to use in the plot.
|
235
|
+
"""
|
236
|
+
|
237
|
+
def _get_node_color(node: Node) -> Tuple[float, float, float, float]:
|
238
|
+
if node.node_type == NodeType.OPERATION:
|
239
|
+
return machine_colors[node.operation.machine_id]
|
240
|
+
if node.node_type == NodeType.MACHINE:
|
241
|
+
return machine_colors[node.machine_id]
|
242
|
+
|
243
|
+
return _color_to_rgba(default_color)
|
244
|
+
|
245
|
+
return _get_node_color
|
246
|
+
|
247
|
+
|
248
|
+
def three_columns_layout(
|
249
|
+
job_shop_graph: JobShopGraph,
|
250
|
+
*,
|
251
|
+
leftmost_position: float = 0.1,
|
252
|
+
rightmost_position: float = 0.9,
|
253
|
+
topmost_position: float = 1.0,
|
254
|
+
bottommost_position: float = 0.0,
|
255
|
+
) -> Dict[Node, Tuple[float, float]]:
|
256
|
+
"""Generates coordinates for a three-column grid layout.
|
257
|
+
|
258
|
+
1. Left column: Machine nodes (M1, M2, etc.)
|
259
|
+
2. Middle column: Operation nodes (O_ij where i=job, j=operation)
|
260
|
+
3. Right column: Job nodes (J1, J2, etc.)
|
261
|
+
|
262
|
+
The operations are arranged vertically in groups by job, with a global
|
263
|
+
node (G) at the bottom.
|
264
|
+
|
265
|
+
For example, in a 2-machine, 3-job problem:
|
266
|
+
|
267
|
+
- Machine nodes (M1, M2) appear in the left column where needed
|
268
|
+
- Operation nodes (O_11 through O_33) form the central column
|
269
|
+
- Job nodes (J1, J2, J3) appear in the right column at the middle of their
|
270
|
+
respective operations
|
271
|
+
- The global node (G) appears at the bottom of the middle column
|
272
|
+
|
273
|
+
Args:
|
274
|
+
job_shop_graph:
|
275
|
+
The job shop graph instance. It should be already initialized with
|
276
|
+
the instance with a valid agent-task graph representation.
|
277
|
+
leftmost_position:
|
278
|
+
The center position of the leftmost column of the layout. It should
|
279
|
+
be a float between 0 and 1. The default is 0.1.
|
280
|
+
rightmost_position:
|
281
|
+
The center position of the rightmost column of the layout. It
|
282
|
+
should be a float between 0 and 1. The default is 0.9.
|
283
|
+
topmost_position:
|
284
|
+
The center position of the topmost node of the layout. It should be
|
285
|
+
a float between 0 and 1. The default is 0.9.
|
286
|
+
bottommost_position:
|
287
|
+
The center position of the bottommost node of the layout. It should
|
288
|
+
be a float between 0 and 1. The default is 0.1.
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
A dictionary with the position of each node in the graph. The keys are
|
292
|
+
the node ids, and the values are tuples with the x and y coordinates.
|
293
|
+
"""
|
294
|
+
|
295
|
+
x_positions = _get_x_positions(leftmost_position, rightmost_position)
|
296
|
+
|
297
|
+
operation_nodes = [
|
298
|
+
node
|
299
|
+
for node in job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
300
|
+
if not job_shop_graph.is_removed(node)
|
301
|
+
]
|
302
|
+
machine_nodes = [
|
303
|
+
node
|
304
|
+
for node in job_shop_graph.nodes_by_type[NodeType.MACHINE]
|
305
|
+
if not job_shop_graph.is_removed(node)
|
306
|
+
]
|
307
|
+
job_nodes = [
|
308
|
+
node
|
309
|
+
for node in job_shop_graph.nodes_by_type[NodeType.JOB]
|
310
|
+
if not job_shop_graph.is_removed(node)
|
311
|
+
]
|
312
|
+
global_nodes = [
|
313
|
+
node
|
314
|
+
for node in job_shop_graph.nodes_by_type[NodeType.GLOBAL]
|
315
|
+
if not job_shop_graph.is_removed(node)
|
316
|
+
]
|
317
|
+
|
318
|
+
# job_nodes = job_shop_graph.nodes_by_type[NodeType.JOB]
|
319
|
+
# global_nodes = job_shop_graph.nodes_by_type[NodeType.GLOBAL]
|
320
|
+
|
321
|
+
total_positions = len(operation_nodes) + len(global_nodes) * 2
|
322
|
+
y_spacing = (topmost_position - bottommost_position) / total_positions
|
323
|
+
|
324
|
+
layout: Dict[Node, Tuple[float, float]] = {}
|
325
|
+
|
326
|
+
machines_spacing_multiplier = len(operation_nodes) // len(machine_nodes)
|
327
|
+
layout.update(
|
328
|
+
_assign_positions_from_top(
|
329
|
+
machine_nodes,
|
330
|
+
x_positions["machine"],
|
331
|
+
topmost_position,
|
332
|
+
y_spacing * machines_spacing_multiplier,
|
333
|
+
)
|
334
|
+
)
|
335
|
+
layout.update(
|
336
|
+
(
|
337
|
+
_assign_positions_from_top(
|
338
|
+
operation_nodes,
|
339
|
+
x_positions["operation"],
|
340
|
+
topmost_position,
|
341
|
+
y_spacing,
|
342
|
+
)
|
343
|
+
)
|
344
|
+
)
|
345
|
+
|
346
|
+
if global_nodes:
|
347
|
+
layout[global_nodes[0]] = (
|
348
|
+
x_positions["operation"],
|
349
|
+
bottommost_position,
|
350
|
+
)
|
351
|
+
|
352
|
+
if job_nodes:
|
353
|
+
job_multiplier = len(operation_nodes) // len(job_nodes)
|
354
|
+
layout.update(
|
355
|
+
_assign_positions_from_top(
|
356
|
+
job_nodes,
|
357
|
+
x_positions["job"],
|
358
|
+
topmost_position,
|
359
|
+
y_spacing * job_multiplier,
|
360
|
+
)
|
361
|
+
)
|
362
|
+
return layout
|
363
|
+
|
364
|
+
|
365
|
+
def _get_x_positions(
|
366
|
+
leftmost_position: float, rightmost_position: float
|
367
|
+
) -> Dict[str, float]:
|
368
|
+
center_position = (
|
369
|
+
leftmost_position + (rightmost_position - leftmost_position) / 2
|
370
|
+
)
|
371
|
+
return {
|
372
|
+
"machine": leftmost_position,
|
373
|
+
"operation": center_position,
|
374
|
+
"job": rightmost_position,
|
375
|
+
}
|
376
|
+
|
377
|
+
|
378
|
+
def _assign_positions_from_top(
|
379
|
+
nodes: List[Node],
|
380
|
+
x: float,
|
381
|
+
top: float,
|
382
|
+
y_spacing: float,
|
383
|
+
) -> Dict[Node, Tuple[float, float]]:
|
384
|
+
layout: Dict[Node, Tuple[float, float]] = {}
|
385
|
+
for i, node in enumerate(nodes):
|
386
|
+
y = top - (i + 1) * y_spacing
|
387
|
+
layout[node] = (x, y)
|
388
|
+
|
389
|
+
return layout
|