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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
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
|
@@ -12,47 +12,86 @@ Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
14
14
|
Provides-Extra: pygraphviz
|
15
|
-
Requires-Dist:
|
15
|
+
Requires-Dist: gymnasium (>=0.29.1,<0.30.0)
|
16
|
+
Requires-Dist: imageio[ffmpeg] (>=2.34.1,<3.0.0)
|
16
17
|
Requires-Dist: matplotlib (>=3,<4)
|
17
18
|
Requires-Dist: networkx (>=3,<4)
|
18
19
|
Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
19
|
-
Requires-Dist: ortools (>=9.9,<
|
20
|
+
Requires-Dist: ortools (>=9.9,<10.0)
|
20
21
|
Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
|
21
22
|
Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
|
22
23
|
Description-Content-Type: text/markdown
|
23
24
|
|
24
25
|
<div align="center">
|
25
26
|
|
26
|
-
<img src="images/
|
27
|
+
<img src="docs/source/images/jslib_minimalist_logo_no_background_fixed.png" height="150px">
|
27
28
|
|
28
|
-
<h1>
|
29
|
+
<h1>JobShopLib</h1>
|
29
30
|
|
30
31
|
[](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml)
|
32
|
+
[](https://job-shop-lib.readthedocs.io/en/latest/?badge=latest)
|
31
33
|

|
32
34
|
[](https://github.com/psf/black)
|
33
35
|
[](https://opensource.org/licenses/MIT)
|
34
36
|
|
35
37
|
</div>
|
36
38
|
|
37
|
-
|
39
|
+
JobShopLib is a Python package for creating, solving, and visualizing job shop scheduling problems (JSSP).
|
38
40
|
|
39
|
-
It
|
41
|
+
It follows a modular design, allowing users to easily extend the library with new solvers, dispatching rules, visualization functions, etc.
|
40
42
|
|
41
|
-
See the [
|
43
|
+
See the [documentation](https://job-shop-lib.readthedocs.io/en/latest/) for more details about the latest version.
|
42
44
|
|
43
|
-
## Installation
|
45
|
+
## Installation :package:
|
44
46
|
|
45
|
-
|
47
|
+
<!-- start installation -->
|
48
|
+
|
49
|
+
JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/) and it supports Python 3.10+.
|
50
|
+
|
51
|
+
You can install the latest stable version (version 0.5.1) using `pip`:
|
46
52
|
|
47
53
|
```bash
|
48
54
|
pip install job-shop-lib
|
49
55
|
```
|
50
56
|
|
51
|
-
|
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
|
+
There is a [documentation page](https://job-shop-lib.readthedocs.io/en/latest/) for versions 1.0.0a3 and onward. See see the [latest pull requests](https://github.com/Pabloo22/job_shop_lib/pulls?q=is%3Apr+is%3Aclosed) for the latest changes.
|
61
|
+
|
62
|
+
<!-- end installation -->
|
63
|
+
|
64
|
+
<!-- key features -->
|
65
|
+
|
66
|
+
## Key Features :star:
|
67
|
+
|
68
|
+
- **Data Structures**: Easily create, manage, and manipulate job shop instances and solutions with user-friendly data structures. See [Getting Started](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/00-Getting-Started.ipynb) and [How Solutions are Represented](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/01-How-Solutions-are-Represented.ipynb).
|
69
|
+
|
70
|
+
- **Benchmark Instances**: Load well-known benchmark instances directly from the library without manual downloading. See [Load Benchmark Instances](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/05-Load-Benchmark-Instances.ipynb).
|
71
|
+
|
72
|
+
- **Random Instance Generation**: Create random instances with customizable sizes and properties or augment existing ones. See [`generation`](job_shop_lib/generation) package.
|
73
|
+
|
74
|
+
- **Multiple Solvers**:
|
75
|
+
- **Constraint Programming Solver**: OR-Tools' CP-SAT solver. See [Solving the Problem](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/tutorial/02-Solving-the-Problem.ipynb).
|
76
|
+
|
77
|
+
- **Dispatching Rule Solvers**: Use any of the available dispatching rules or create custom ones. See [Dispatching Rules](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/03-Dispatching-Rules.ipynb).
|
78
|
+
|
79
|
+
- **Gantt Charts**: Visualize final schedules and how are they created iteratively by dispatching rule solvers or sequences of scheduling decisions with GIFs or videos. See [Save Gif](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/06-Save-Gif.ipynb).
|
80
|
+
|
81
|
+
- **Graph Representations**:
|
82
|
+
- **Disjunctive Graphs**: Represent and visualize instances as disjunctive graphs. See [Disjunctive Graph](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/04-Disjunctive-Graph.ipynb).
|
83
|
+
- **Agent-Task Graphs**: Encode instances as agent-task graphs (introduced in [ScheduleNet paper](https://arxiv.org/abs/2106.03051)). See [Agent-Task Graph](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/07-Agent-Task-Graph.ipynb).
|
84
|
+
- Build your own custom graphs with the `JobShopGraph` class.
|
85
|
+
|
86
|
+
- **Gymnasium Environments**: Two environments for solving the problem with graph neural networks (GNNs) or any other method, and reinforcement learning (RL). See [SingleJobShopGraphEnv](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/09-SingleJobShopGraphEnv.ipynb) and [MultiJobShopGraphEnv](https://github.com/Pabloo22/job_shop_lib/blob/main/docs/source/examples/10-MultiJobShopGraphEnv.ipynb).
|
87
|
+
|
88
|
+
<!-- end key features -->
|
89
|
+
|
90
|
+
## Some Examples :rocket:
|
52
91
|
|
53
92
|
### Create a Job Shop Instance
|
54
93
|
|
55
|
-
You can create a
|
94
|
+
You can create a `JobShopInstance` by defining the jobs and operations. An operation is defined by the machine(s) it is processed on and the duration (processing time).
|
56
95
|
|
57
96
|
```python
|
58
97
|
from job_shop_lib import JobShopInstance, Operation
|
@@ -83,7 +122,7 @@ from job_shop_lib.benchmarking import load_benchmark_instance
|
|
83
122
|
ft06 = load_benchmark_instance("ft06")
|
84
123
|
```
|
85
124
|
|
86
|
-
The module `
|
125
|
+
The module `benchmarking` contains functions to load the instances from the file and return them as `JobShopInstance` objects without having to download them
|
87
126
|
manually.
|
88
127
|
|
89
128
|
The contributions to this benchmark dataset are as follows:
|
@@ -115,12 +154,12 @@ https://github.com/thomasWeise/jsspInstancesAndResults
|
|
115
154
|
|
116
155
|
### Generate a Random Instance
|
117
156
|
|
118
|
-
You can also generate a random instance with the `
|
157
|
+
You can also generate a random instance with the `GeneralInstanceGenerator` class.
|
119
158
|
|
120
159
|
```python
|
121
|
-
from job_shop_lib.
|
160
|
+
from job_shop_lib.generation import GeneralInstanceGenerator
|
122
161
|
|
123
|
-
generator =
|
162
|
+
generator = GeneralInstanceGenerator(
|
124
163
|
duration_range=(5, 10), seed=42, num_jobs=5, num_machines=5
|
125
164
|
)
|
126
165
|
random_instance = generator.generate()
|
@@ -129,7 +168,7 @@ random_instance = generator.generate()
|
|
129
168
|
This class can also work as an iterator to generate multiple instances:
|
130
169
|
|
131
170
|
```python
|
132
|
-
generator =
|
171
|
+
generator = GeneralInstanceGenerator(iteration_limit=100, seed=42)
|
133
172
|
instances = []
|
134
173
|
for instance in generator:
|
135
174
|
instances.append(instance)
|
@@ -145,7 +184,7 @@ Every solver is a `Callable` that receives a `JobShopInstance` and returns a `Sc
|
|
145
184
|
```python
|
146
185
|
import matplotlib.pyplot as plt
|
147
186
|
|
148
|
-
from job_shop_lib.
|
187
|
+
from job_shop_lib.constraint_programming import ORToolsSolver
|
149
188
|
from job_shop_lib.visualization import plot_gantt_chart
|
150
189
|
|
151
190
|
solver = ORToolsSolver(max_time_in_seconds=10)
|
@@ -154,7 +193,7 @@ ft06_schedule = solver(ft06)
|
|
154
193
|
fig, ax = plot_gantt_chart(ft06_schedule)
|
155
194
|
plt.show()
|
156
195
|
```
|
157
|
-

|
196
|
+

|
158
197
|
|
159
198
|
### Solve an Instance with a Dispatching Rule Solver
|
160
199
|
|
@@ -190,7 +229,7 @@ create_gif(
|
|
190
229
|
)
|
191
230
|
```
|
192
231
|
|
193
|
-

|
232
|
+

|
194
233
|
|
195
234
|
The dashed red line represents the current time step, which is computed as the earliest time when the next operation can start.
|
196
235
|
|
@@ -200,7 +239,7 @@ The dashed red line represents the current time step, which is computed as the e
|
|
200
239
|
|
201
240
|
### Representing Instances as Graphs
|
202
241
|
|
203
|
-
One of the main purposes of this library is to provide an easy way to encode instances as graphs. This can be very useful, not only for visualization purposes but also for developing
|
242
|
+
One of the main purposes of this library is to provide an easy way to encode instances as graphs. This can be very useful, not only for visualization purposes but also for developing graph neural network-based algorithms.
|
204
243
|
|
205
244
|
A graph is represented by the `JobShopGraph` class, which internally stores a `networkx.DiGraph` object.
|
206
245
|
|
@@ -208,7 +247,7 @@ A graph is represented by the `JobShopGraph` class, which internally stores a `n
|
|
208
247
|
|
209
248
|
The disjunctive graph is created by first adding nodes representing each operation in the jobs, along with two special nodes: a source $S$ and a sink $T$. Each operation node is linked to the next operation in its job sequence by **conjunctive edges**, forming a path from the source to the sink. These edges represent the order in which operations of a single job must be performed.
|
210
249
|
|
211
|
-
Additionally, the graph includes **disjunctive edges** between operations that use the same machine but belong to different jobs. These edges are bidirectional, indicating that either of the connected operations can be performed first. The disjunctive edges thus represent the scheduling choices available: the order in which operations sharing a machine can be processed. Solving the
|
250
|
+
Additionally, the graph includes **disjunctive edges** between operations that use the same machine but belong to different jobs. These edges are bidirectional, indicating that either of the connected operations can be performed first. The disjunctive edges thus represent the scheduling choices available: the order in which operations sharing a machine can be processed. Solving the job shop scheduling problem involves choosing a direction for each disjunctive edge such that the overall processing time is minimized.
|
212
251
|
|
213
252
|
```python
|
214
253
|
from job_shop_lib.visualization import plot_disjunctive_graph
|
@@ -217,11 +256,11 @@ fig = plot_disjunctive_graph(instance)
|
|
217
256
|
plt.show()
|
218
257
|
```
|
219
258
|
|
220
|
-

|
259
|
+

|
221
260
|
|
222
261
|
|
223
|
-
> [!
|
224
|
-
>
|
262
|
+
> [!TIP]
|
263
|
+
> Installing the optional dependency [PyGraphViz](https://pygraphviz.github.io/) is recommended.
|
225
264
|
|
226
265
|
The `JobShopGraph` class provides easy access to the nodes, for example, to get all the nodes of a specific type:
|
227
266
|
|
@@ -250,9 +289,9 @@ Other attributes include:
|
|
250
289
|
- `nodes_by_machine`: A nested list mapping each machine to its associated operation nodes, aiding in machine-specific analysis.
|
251
290
|
- `nodes_by_job`: Similar to `nodes_by_machine`, but maps jobs to their operation nodes, useful for job-specific traversal.
|
252
291
|
|
253
|
-
####
|
292
|
+
#### Resource-Task Graph
|
254
293
|
|
255
|
-
Introduced in the paper "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning" by [Park et al. (2021)](https://arxiv.org/abs/2106.03051), the
|
294
|
+
Introduced in the paper "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning" by [Park et al. (2021)](https://arxiv.org/abs/2106.03051), the resource-task graph (orginally named "agent-task graph") is a graph that represents the scheduling problem as a multi-agent reinforcement learning problem.
|
256
295
|
|
257
296
|
In contrast to the disjunctive graph, instead of connecting operations
|
258
297
|
that share the same resources directly by disjunctive edges, operation
|
@@ -263,66 +302,59 @@ from the same job are connected by non-directed edges too.
|
|
263
302
|
|
264
303
|
```python
|
265
304
|
from job_shop_lib.graphs import (
|
266
|
-
|
267
|
-
|
268
|
-
|
305
|
+
build_complete_resource_task_graph,
|
306
|
+
build_resource_task_graph_with_jobs,
|
307
|
+
build_resource_task_graph,
|
269
308
|
)
|
270
|
-
from job_shop_lib.visualization import
|
309
|
+
from job_shop_lib.visualization import plot_resource_task_graph
|
271
310
|
|
272
|
-
|
311
|
+
complete_resource_task_graph = build_complete_resource_task_graph(instance)
|
273
312
|
|
274
|
-
fig =
|
313
|
+
fig = plot_resource_task_graph(complete_agent_task_graph)
|
275
314
|
plt.show()
|
276
315
|
```
|
277
316
|
|
278
317
|
<div align="center">
|
279
|
-
<img src="examples/agent_task_graph.png" width="300">
|
318
|
+
<img src="docs/source/examples/output/agent_task_graph.png" width="300">
|
280
319
|
</div>
|
281
320
|
<br>
|
282
321
|
|
283
322
|
----
|
284
323
|
|
324
|
+
The library generalizes this graph by allowing the addition of job nodes and a global one (see `build_resource_task_graph_with_jobs` and `build_resource_task_graph`).
|
325
|
+
|
285
326
|
For more details, check the [examples](examples) folder.
|
286
327
|
|
287
328
|
## Installation for development
|
288
329
|
|
289
|
-
|
330
|
+
<!-- start installation development -->
|
290
331
|
|
291
332
|
1. Clone the repository.
|
292
333
|
|
293
|
-
2. Install [poetry](https://python-poetry.org/docs/) if you don't have it already:
|
294
|
-
```bash
|
295
|
-
pip install poetry==1.7
|
296
|
-
```
|
297
|
-
3. Create the virtual environment:
|
298
334
|
```bash
|
299
|
-
|
335
|
+
git clone https://github.com/Pabloo22/job_shop_lib.git
|
336
|
+
cd job_shop_lib
|
300
337
|
```
|
301
|
-
|
338
|
+
|
339
|
+
2. Install [poetry](https://python-poetry.org/docs/) if you don't have it already:
|
340
|
+
|
302
341
|
```bash
|
303
|
-
|
342
|
+
pip install poetry
|
304
343
|
```
|
305
|
-
|
344
|
+
|
345
|
+
3. Install dependencies:
|
306
346
|
```bash
|
307
347
|
make poetry_install_all
|
308
348
|
```
|
309
349
|
|
310
|
-
|
311
|
-
|
312
|
-
If you don't want to use Poetry, you can install the library directly from the source code:
|
313
|
-
|
314
|
-
```bash
|
315
|
-
git clone https://github.com/Pabloo22/job_shop_lib.git
|
316
|
-
cd job_shop_lib
|
317
|
-
pip install -e .
|
318
|
-
```
|
350
|
+
<!-- end installation development -->
|
319
351
|
|
320
|
-
## License
|
352
|
+
## License :scroll:
|
321
353
|
|
322
354
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
323
355
|
|
324
356
|
|
325
|
-
## References
|
357
|
+
## References :books:
|
326
358
|
|
327
359
|
- J. Adams, E. Balas, and D. Zawack, "The shifting bottleneck procedure
|
328
360
|
for job shop scheduling," Management Science, vol. 34, no. 3,
|