job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|