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
job_shop_lib/operation.py
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
"""Home of the `Operation` class."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
from job_shop_lib import JobShopLibError
|
6
|
-
|
7
|
-
|
8
|
-
class Operation:
|
9
|
-
"""Stores machine and duration information for a job operation.
|
10
|
-
|
11
|
-
Note:
|
12
|
-
To increase performance, some solvers such as the CP-SAT solver use
|
13
|
-
only integers to represent the operation's attributes. Should a
|
14
|
-
problem involve operations with non-integer durations, it would be
|
15
|
-
necessary to multiply all durations by a sufficiently large integer so
|
16
|
-
that every duration is an integer.
|
17
|
-
|
18
|
-
Attributes:
|
19
|
-
machines: A list of machine ids that can perform the operation.
|
20
|
-
duration: The time it takes to perform the operation.
|
21
|
-
"""
|
22
|
-
|
23
|
-
__slots__ = (
|
24
|
-
"machines",
|
25
|
-
"duration",
|
26
|
-
"_job_id",
|
27
|
-
"_position_in_job",
|
28
|
-
"_operation_id",
|
29
|
-
)
|
30
|
-
|
31
|
-
def __init__(self, machines: int | list[int], duration: int):
|
32
|
-
"""Initializes the object with the given machines and duration.
|
33
|
-
|
34
|
-
Args:
|
35
|
-
machines: A list of machine ids that can perform the operation. If
|
36
|
-
only one machine can perform the operation, it can be passed as
|
37
|
-
an integer.
|
38
|
-
duration: The time it takes to perform the operation.
|
39
|
-
"""
|
40
|
-
self.machines = [machines] if isinstance(machines, int) else machines
|
41
|
-
self.duration = duration
|
42
|
-
|
43
|
-
# Defined outside the class by the JobShopInstance class:
|
44
|
-
self._job_id: int | None = None
|
45
|
-
self._position_in_job: int | None = None
|
46
|
-
self._operation_id: int | None = None
|
47
|
-
|
48
|
-
@property
|
49
|
-
def machine_id(self) -> int:
|
50
|
-
"""Returns the id of the machine associated with the operation.
|
51
|
-
|
52
|
-
Raises:
|
53
|
-
ValueError: If the operation has multiple machines in its list.
|
54
|
-
"""
|
55
|
-
if len(self.machines) > 1:
|
56
|
-
raise JobShopLibError("Operation has multiple machines.")
|
57
|
-
return self.machines[0]
|
58
|
-
|
59
|
-
@property
|
60
|
-
def job_id(self) -> int:
|
61
|
-
"""Returns the id of the job that the operation belongs to."""
|
62
|
-
if self._job_id is None:
|
63
|
-
raise JobShopLibError("Operation has no job_id.")
|
64
|
-
return self._job_id
|
65
|
-
|
66
|
-
@job_id.setter
|
67
|
-
def job_id(self, value: int) -> None:
|
68
|
-
self._job_id = value
|
69
|
-
|
70
|
-
@property
|
71
|
-
def position_in_job(self) -> int:
|
72
|
-
"""Returns the position (starting at zero) of the operation in the
|
73
|
-
job.
|
74
|
-
|
75
|
-
Raises:
|
76
|
-
ValueError: If the operation has no position_in_job.
|
77
|
-
"""
|
78
|
-
if self._position_in_job is None:
|
79
|
-
raise JobShopLibError("Operation has no position_in_job.")
|
80
|
-
return self._position_in_job
|
81
|
-
|
82
|
-
@position_in_job.setter
|
83
|
-
def position_in_job(self, value: int) -> None:
|
84
|
-
self._position_in_job = value
|
85
|
-
|
86
|
-
@property
|
87
|
-
def operation_id(self) -> int:
|
88
|
-
"""Returns the id of the operation.
|
89
|
-
|
90
|
-
The operation id is unique within a job shop instance and should
|
91
|
-
be set by the JobShopInstance class.
|
92
|
-
|
93
|
-
It starts at 0 and is incremented by 1 for each operation in the
|
94
|
-
instance.
|
95
|
-
|
96
|
-
Raises:
|
97
|
-
ValueError: If the operation has no id.
|
98
|
-
"""
|
99
|
-
if self._operation_id is None:
|
100
|
-
raise JobShopLibError("Operation has no id.")
|
101
|
-
return self._operation_id
|
102
|
-
|
103
|
-
@operation_id.setter
|
104
|
-
def operation_id(self, value: int) -> None:
|
105
|
-
self._operation_id = value
|
106
|
-
|
107
|
-
def __hash__(self) -> int:
|
108
|
-
return hash(self.operation_id)
|
109
|
-
|
110
|
-
def __eq__(self, value: object) -> bool:
|
111
|
-
if not isinstance(value, Operation):
|
112
|
-
return False
|
113
|
-
return self.__slots__ == value.__slots__
|
114
|
-
|
115
|
-
def __repr__(self) -> str:
|
116
|
-
machines = (
|
117
|
-
self.machines[0] if len(self.machines) == 1 else self.machines
|
118
|
-
)
|
119
|
-
return (
|
120
|
-
f"O(m={machines}, d={self.duration}, "
|
121
|
-
f"j={self.job_id}, p={self.position_in_job})"
|
122
|
-
)
|
@@ -1,257 +0,0 @@
|
|
1
|
-
"""Contains functions to plot the agent-task graph of a job shop instance.
|
2
|
-
|
3
|
-
The agent-task graph was introduced by Junyoung Park et al. (2021).
|
4
|
-
In contrast to the disjunctive graph, instead of connecting operations that
|
5
|
-
share the same resources directly by disjunctive edges, operation nodes are
|
6
|
-
connected with machine ones. All machine nodes are connected between them, and
|
7
|
-
all operation nodes from the same job are connected by non-directed edges too.
|
8
|
-
|
9
|
-
See: job_shop_lib.graphs.build_agent_task_graph module for more information.
|
10
|
-
"""
|
11
|
-
|
12
|
-
from typing import Optional
|
13
|
-
|
14
|
-
import matplotlib.pyplot as plt
|
15
|
-
import networkx as nx
|
16
|
-
|
17
|
-
from job_shop_lib.graphs import NodeType, JobShopGraph, Node
|
18
|
-
|
19
|
-
|
20
|
-
def plot_agent_task_graph(
|
21
|
-
job_shop_graph: JobShopGraph,
|
22
|
-
title: Optional[str] = None,
|
23
|
-
figsize: tuple[int, int] = (10, 10),
|
24
|
-
layout: Optional[dict[Node, tuple[float, float]]] = None,
|
25
|
-
color_map_name: str = "tab10",
|
26
|
-
node_size: int = 1000,
|
27
|
-
alpha: float = 0.95,
|
28
|
-
add_legend: bool = False,
|
29
|
-
) -> plt.Figure:
|
30
|
-
"""Returns a plot of the agent-task graph of the instance.
|
31
|
-
|
32
|
-
Machine and job nodes are represented by squares, and the operation nodes
|
33
|
-
are represented by circles.
|
34
|
-
|
35
|
-
Args:
|
36
|
-
job_shop_graph:
|
37
|
-
The job shop graph instance. It should be already initialized with
|
38
|
-
the instance with a valid agent-task graph representation.
|
39
|
-
|
40
|
-
Returns:
|
41
|
-
The figure of the plot. This figure can be used to save the plot to a
|
42
|
-
file or to show it in a Jupyter notebook.
|
43
|
-
"""
|
44
|
-
if title is None:
|
45
|
-
title = (
|
46
|
-
f"Agent-Task Graph Visualization: {job_shop_graph.instance.name}"
|
47
|
-
)
|
48
|
-
# Create a new figure and axis
|
49
|
-
fig, ax = plt.subplots(figsize=figsize)
|
50
|
-
fig.suptitle(title)
|
51
|
-
|
52
|
-
# Create the networkx graph
|
53
|
-
graph = job_shop_graph.graph
|
54
|
-
|
55
|
-
# Create the layout if it was not provided
|
56
|
-
if layout is None:
|
57
|
-
layout = three_columns_layout(job_shop_graph)
|
58
|
-
|
59
|
-
# Define colors and shapes
|
60
|
-
color_map = plt.get_cmap(color_map_name)
|
61
|
-
machine_colors = {
|
62
|
-
machine.machine_id: color_map(i)
|
63
|
-
for i, machine in enumerate(
|
64
|
-
job_shop_graph.nodes_by_type[NodeType.MACHINE]
|
65
|
-
)
|
66
|
-
}
|
67
|
-
node_colors = [
|
68
|
-
_get_node_color(node, machine_colors) for node in job_shop_graph.nodes
|
69
|
-
]
|
70
|
-
node_shapes = {"machine": "s", "job": "d", "operation": "o", "global": "o"}
|
71
|
-
|
72
|
-
# Draw nodes with different shapes based on their type
|
73
|
-
for node_type, shape in node_shapes.items():
|
74
|
-
current_nodes = [
|
75
|
-
node.node_id
|
76
|
-
for node in job_shop_graph.nodes
|
77
|
-
if node.node_type.name.lower() == node_type
|
78
|
-
]
|
79
|
-
nx.draw_networkx_nodes(
|
80
|
-
graph,
|
81
|
-
layout,
|
82
|
-
nodelist=current_nodes,
|
83
|
-
node_color=[node_colors[i] for i in current_nodes],
|
84
|
-
node_shape=shape,
|
85
|
-
ax=ax,
|
86
|
-
node_size=node_size,
|
87
|
-
alpha=alpha,
|
88
|
-
)
|
89
|
-
|
90
|
-
# Draw edges
|
91
|
-
nx.draw_networkx_edges(graph, layout, ax=ax)
|
92
|
-
|
93
|
-
node_labels = {
|
94
|
-
node.node_id: _get_node_label(node) for node in job_shop_graph.nodes
|
95
|
-
}
|
96
|
-
nx.draw_networkx_labels(graph, layout, node_labels, ax=ax)
|
97
|
-
|
98
|
-
ax.set_axis_off()
|
99
|
-
|
100
|
-
plt.tight_layout()
|
101
|
-
|
102
|
-
# Add to the legend the meaning of m and d
|
103
|
-
if add_legend:
|
104
|
-
plt.figtext(0, 0.95, "d = duration", wrap=True, fontsize=12)
|
105
|
-
return fig
|
106
|
-
|
107
|
-
|
108
|
-
def _get_node_color(
|
109
|
-
node: Node, machine_colors: dict[int, tuple[float, float, float, float]]
|
110
|
-
) -> tuple[float, float, float, float] | str:
|
111
|
-
if node.node_type == NodeType.OPERATION:
|
112
|
-
return machine_colors[node.operation.machine_id]
|
113
|
-
if node.node_type == NodeType.MACHINE:
|
114
|
-
return machine_colors[node.machine_id]
|
115
|
-
|
116
|
-
return "lightblue"
|
117
|
-
|
118
|
-
|
119
|
-
def _get_node_label(node: Node) -> str:
|
120
|
-
if node.node_type == NodeType.OPERATION:
|
121
|
-
return f"d={node.operation.duration}"
|
122
|
-
if node.node_type == NodeType.MACHINE:
|
123
|
-
return f"M{node.machine_id}"
|
124
|
-
if node.node_type == NodeType.JOB:
|
125
|
-
return f"J{node.job_id}"
|
126
|
-
if node.node_type == NodeType.GLOBAL:
|
127
|
-
return "G"
|
128
|
-
|
129
|
-
raise ValueError(f"Invalid node type: {node.node_type}")
|
130
|
-
|
131
|
-
|
132
|
-
def three_columns_layout(
|
133
|
-
job_shop_graph: JobShopGraph,
|
134
|
-
*,
|
135
|
-
leftmost_position: float = 0.1,
|
136
|
-
rightmost_position: float = 0.9,
|
137
|
-
topmost_position: float = 1.0,
|
138
|
-
bottommost_position: float = 0.0,
|
139
|
-
) -> dict[Node, tuple[float, float]]:
|
140
|
-
"""Returns the layout of the agent-task graph.
|
141
|
-
|
142
|
-
The layout is organized in a grid manner. For example, for a JobShopGraph
|
143
|
-
representing a job shop instance with 2 machines and 3 jobs, the layout
|
144
|
-
would be:
|
145
|
-
|
146
|
-
0: - O_11 -
|
147
|
-
1: - O_12 J1
|
148
|
-
2: - O_13 -
|
149
|
-
3: M1 O_21 -
|
150
|
-
4: - O_22 J2
|
151
|
-
5: - O_23 -
|
152
|
-
6: M2 O_31 -
|
153
|
-
7: - O_32 J3
|
154
|
-
8: - O_33 -
|
155
|
-
9: - - -
|
156
|
-
10: - G -
|
157
|
-
Where M1 and M2 are the machine nodes, J1, J2, and J3 are the job
|
158
|
-
nodes, O_ij are the operation nodes, and G is the global node.
|
159
|
-
|
160
|
-
Args:
|
161
|
-
job_shop_graph:
|
162
|
-
The job shop graph instance. It should be already initialized with
|
163
|
-
the instance with a valid agent-task graph representation.
|
164
|
-
leftmost_position:
|
165
|
-
The center position of the leftmost column of the layout. It should
|
166
|
-
be a float between 0 and 1. The default is 0.1.
|
167
|
-
rightmost_position:
|
168
|
-
The center position of the rightmost column of the layout. It
|
169
|
-
should be a float between 0 and 1. The default is 0.9.
|
170
|
-
topmost_position:
|
171
|
-
The center position of the topmost node of the layout. It should be
|
172
|
-
a float between 0 and 1. The default is 0.9.
|
173
|
-
bottommost_position:
|
174
|
-
The center position of the bottommost node of the layout. It should
|
175
|
-
be a float between 0 and 1. The default is 0.1.
|
176
|
-
|
177
|
-
Returns:
|
178
|
-
A dictionary with the position of each node in the graph. The keys are
|
179
|
-
the node ids, and the values are tuples with the x and y coordinates.
|
180
|
-
"""
|
181
|
-
|
182
|
-
x_positions = _get_x_positions(leftmost_position, rightmost_position)
|
183
|
-
|
184
|
-
operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
|
185
|
-
machine_nodes = job_shop_graph.nodes_by_type[NodeType.MACHINE]
|
186
|
-
job_nodes = job_shop_graph.nodes_by_type[NodeType.JOB]
|
187
|
-
global_nodes = job_shop_graph.nodes_by_type[NodeType.GLOBAL]
|
188
|
-
|
189
|
-
total_positions = len(operation_nodes) + len(global_nodes) * 2
|
190
|
-
y_spacing = (topmost_position - bottommost_position) / total_positions
|
191
|
-
|
192
|
-
layout: dict[Node, tuple[float, float]] = {}
|
193
|
-
|
194
|
-
machines_spacing_multiplier = len(operation_nodes) // len(machine_nodes)
|
195
|
-
layout.update(
|
196
|
-
_assign_positions_from_top(
|
197
|
-
machine_nodes,
|
198
|
-
x_positions["machine"],
|
199
|
-
topmost_position,
|
200
|
-
y_spacing * machines_spacing_multiplier,
|
201
|
-
)
|
202
|
-
)
|
203
|
-
layout.update(
|
204
|
-
(
|
205
|
-
_assign_positions_from_top(
|
206
|
-
operation_nodes,
|
207
|
-
x_positions["operation"],
|
208
|
-
topmost_position,
|
209
|
-
y_spacing,
|
210
|
-
)
|
211
|
-
)
|
212
|
-
)
|
213
|
-
|
214
|
-
if global_nodes:
|
215
|
-
layout[global_nodes[0]] = (
|
216
|
-
x_positions["operation"],
|
217
|
-
bottommost_position,
|
218
|
-
)
|
219
|
-
|
220
|
-
if job_nodes:
|
221
|
-
job_multiplier = len(operation_nodes) // len(job_nodes)
|
222
|
-
layout.update(
|
223
|
-
_assign_positions_from_top(
|
224
|
-
job_nodes,
|
225
|
-
x_positions["job"],
|
226
|
-
topmost_position,
|
227
|
-
y_spacing * job_multiplier,
|
228
|
-
)
|
229
|
-
)
|
230
|
-
return layout
|
231
|
-
|
232
|
-
|
233
|
-
def _get_x_positions(
|
234
|
-
leftmost_position: float, rightmost_position: float
|
235
|
-
) -> dict[str, float]:
|
236
|
-
center_position = (
|
237
|
-
leftmost_position + (rightmost_position - leftmost_position) / 2
|
238
|
-
)
|
239
|
-
return {
|
240
|
-
"machine": leftmost_position,
|
241
|
-
"operation": center_position,
|
242
|
-
"job": rightmost_position,
|
243
|
-
}
|
244
|
-
|
245
|
-
|
246
|
-
def _assign_positions_from_top(
|
247
|
-
nodes: list[Node],
|
248
|
-
x: float,
|
249
|
-
top: float,
|
250
|
-
y_spacing: float,
|
251
|
-
) -> dict[Node, tuple[float, float]]:
|
252
|
-
layout: dict[Node, tuple[float, float]] = {}
|
253
|
-
for i, node in enumerate(nodes):
|
254
|
-
y = top - (i + 1) * y_spacing
|
255
|
-
layout[node] = (x, y)
|
256
|
-
|
257
|
-
return layout
|
@@ -1,209 +0,0 @@
|
|
1
|
-
"""Module for creating a GIF of the schedule being built by a
|
2
|
-
dispatching rule solver."""
|
3
|
-
|
4
|
-
import os
|
5
|
-
import pathlib
|
6
|
-
import shutil
|
7
|
-
from collections.abc import Callable
|
8
|
-
|
9
|
-
import imageio
|
10
|
-
import matplotlib.pyplot as plt
|
11
|
-
from matplotlib.figure import Figure
|
12
|
-
|
13
|
-
from job_shop_lib import JobShopInstance, Schedule, Operation
|
14
|
-
from job_shop_lib.dispatching import (
|
15
|
-
DispatchingRuleSolver,
|
16
|
-
Dispatcher,
|
17
|
-
HistoryTracker,
|
18
|
-
)
|
19
|
-
from job_shop_lib.visualization.gantt_chart import plot_gantt_chart
|
20
|
-
|
21
|
-
|
22
|
-
# Most of the arguments are optional with default values. There is no way to
|
23
|
-
# reduce the number of arguments without losing functionality.
|
24
|
-
# pylint: disable=too-many-arguments
|
25
|
-
def create_gif(
|
26
|
-
gif_path: str,
|
27
|
-
instance: JobShopInstance,
|
28
|
-
solver: DispatchingRuleSolver,
|
29
|
-
plot_function: (
|
30
|
-
Callable[[Schedule, int, list[Operation] | None, int | None], Figure]
|
31
|
-
| None
|
32
|
-
) = None,
|
33
|
-
fps: int = 1,
|
34
|
-
remove_frames: bool = True,
|
35
|
-
frames_dir: str | None = None,
|
36
|
-
plot_current_time: bool = True,
|
37
|
-
) -> None:
|
38
|
-
"""Creates a GIF of the schedule being built by the given solver.
|
39
|
-
|
40
|
-
Args:
|
41
|
-
gif_path:
|
42
|
-
The path to save the GIF file. It should end with ".gif".
|
43
|
-
instance:
|
44
|
-
The instance of the job shop problem to be scheduled.
|
45
|
-
solver:
|
46
|
-
The dispatching rule solver to use.
|
47
|
-
plot_function:
|
48
|
-
A function that plots a Gantt chart for a schedule. It
|
49
|
-
should take a `Schedule` object and the makespan of the schedule as
|
50
|
-
input and return a `Figure` object. If not provided, a default
|
51
|
-
function is used.
|
52
|
-
fps:
|
53
|
-
The number of frames per second in the GIF.
|
54
|
-
remove_frames:
|
55
|
-
Whether to remove the frames after creating the GIF.
|
56
|
-
frames_dir:
|
57
|
-
The directory to save the frames in. If not provided,
|
58
|
-
`gif_path.replace(".gif", "") + "_frames"` is used.
|
59
|
-
plot_current_time:
|
60
|
-
Whether to plot a vertical line at the current time.
|
61
|
-
"""
|
62
|
-
if plot_function is None:
|
63
|
-
plot_function = plot_gantt_chart_wrapper()
|
64
|
-
|
65
|
-
if frames_dir is None:
|
66
|
-
# Use the name of the GIF file as the directory name
|
67
|
-
frames_dir = gif_path.replace(".gif", "") + "_frames"
|
68
|
-
path = pathlib.Path(frames_dir)
|
69
|
-
path.mkdir(exist_ok=True)
|
70
|
-
frames_dir = str(path)
|
71
|
-
create_gantt_chart_frames(
|
72
|
-
frames_dir, instance, solver, plot_function, plot_current_time
|
73
|
-
)
|
74
|
-
create_gif_from_frames(frames_dir, gif_path, fps)
|
75
|
-
|
76
|
-
if remove_frames:
|
77
|
-
shutil.rmtree(frames_dir)
|
78
|
-
|
79
|
-
|
80
|
-
def plot_gantt_chart_wrapper(
|
81
|
-
title: str | None = None,
|
82
|
-
cmap: str = "viridis",
|
83
|
-
show_available_operations: bool = False,
|
84
|
-
) -> Callable[[Schedule, int, list[Operation] | None, int | None], Figure]:
|
85
|
-
"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
86
|
-
|
87
|
-
Args:
|
88
|
-
title: The title of the Gantt chart.
|
89
|
-
cmap: The name of the colormap to use.
|
90
|
-
show_available_operations:
|
91
|
-
Whether to show the available operations in the Gantt chart.
|
92
|
-
|
93
|
-
Returns:
|
94
|
-
A function that plots a Gantt chart for a schedule. The function takes
|
95
|
-
the following arguments:
|
96
|
-
- schedule: The schedule to plot.
|
97
|
-
- makespan: The makespan of the schedule.
|
98
|
-
- available_operations: A list of available operations. If None,
|
99
|
-
the available operations are not shown.
|
100
|
-
- current_time: The current time in the schedule. If provided, a
|
101
|
-
red vertical line is plotted at this time.
|
102
|
-
"""
|
103
|
-
|
104
|
-
def plot_function(
|
105
|
-
schedule: Schedule,
|
106
|
-
makespan: int,
|
107
|
-
available_operations: list | None = None,
|
108
|
-
current_time: int | None = None,
|
109
|
-
) -> Figure:
|
110
|
-
fig, ax = plot_gantt_chart(
|
111
|
-
schedule, title=title, cmap_name=cmap, xlim=makespan
|
112
|
-
)
|
113
|
-
|
114
|
-
if show_available_operations and available_operations is not None:
|
115
|
-
|
116
|
-
operations_text = "\n".join(
|
117
|
-
str(operation) for operation in available_operations
|
118
|
-
)
|
119
|
-
text = f"Available operations:\n{operations_text}"
|
120
|
-
# Print the available operations at the bottom right corner
|
121
|
-
# of the Gantt chart
|
122
|
-
fig.text(
|
123
|
-
1.25,
|
124
|
-
0.05,
|
125
|
-
text,
|
126
|
-
ha="right",
|
127
|
-
va="bottom",
|
128
|
-
transform=ax.transAxes,
|
129
|
-
bbox={
|
130
|
-
"facecolor": "white",
|
131
|
-
"alpha": 0.5,
|
132
|
-
"boxstyle": "round,pad=0.5",
|
133
|
-
},
|
134
|
-
)
|
135
|
-
if current_time is not None:
|
136
|
-
ax.axvline(current_time, color="red", linestyle="--")
|
137
|
-
return fig
|
138
|
-
|
139
|
-
return plot_function
|
140
|
-
|
141
|
-
|
142
|
-
def create_gantt_chart_frames(
|
143
|
-
frames_dir: str,
|
144
|
-
instance: JobShopInstance,
|
145
|
-
solver: DispatchingRuleSolver,
|
146
|
-
plot_function: Callable[
|
147
|
-
[Schedule, int, list[Operation] | None, int | None], Figure
|
148
|
-
],
|
149
|
-
plot_current_time: bool = True,
|
150
|
-
) -> None:
|
151
|
-
"""Creates frames of the Gantt chart for the schedule being built.
|
152
|
-
|
153
|
-
Args:
|
154
|
-
frames_dir:
|
155
|
-
The directory to save the frames in.
|
156
|
-
instance:
|
157
|
-
The instance of the job shop problem to be scheduled.
|
158
|
-
solver:
|
159
|
-
The dispatching rule solver to use.
|
160
|
-
plot_function:
|
161
|
-
A function that plots a Gantt chart for a schedule. It
|
162
|
-
should take a `Schedule` object and the makespan of the schedule as
|
163
|
-
input and return a `Figure` object.
|
164
|
-
plot_current_time:
|
165
|
-
Whether to plot a vertical line at the current time.
|
166
|
-
"""
|
167
|
-
dispatcher = Dispatcher(instance, pruning_function=solver.pruning_function)
|
168
|
-
history_tracker = HistoryTracker(dispatcher)
|
169
|
-
makespan = solver.solve(instance, dispatcher).makespan()
|
170
|
-
dispatcher.unsubscribe(history_tracker)
|
171
|
-
dispatcher.reset()
|
172
|
-
for i, scheduled_operation in enumerate(history_tracker.history, start=1):
|
173
|
-
dispatcher.dispatch(
|
174
|
-
scheduled_operation.operation, scheduled_operation.machine_id
|
175
|
-
)
|
176
|
-
current_time = (
|
177
|
-
None if not plot_current_time else dispatcher.current_time()
|
178
|
-
)
|
179
|
-
fig = plot_function(
|
180
|
-
dispatcher.schedule,
|
181
|
-
makespan,
|
182
|
-
dispatcher.available_operations(),
|
183
|
-
current_time,
|
184
|
-
)
|
185
|
-
_save_frame(fig, frames_dir, i)
|
186
|
-
|
187
|
-
|
188
|
-
def _save_frame(figure: Figure, frames_dir: str, number: int) -> None:
|
189
|
-
figure.savefig(f"{frames_dir}/frame_{number:02d}.png", bbox_inches="tight")
|
190
|
-
plt.close(figure)
|
191
|
-
|
192
|
-
|
193
|
-
def create_gif_from_frames(frames_dir: str, gif_path: str, fps: int) -> None:
|
194
|
-
"""Creates a GIF from the frames in the given directory.
|
195
|
-
|
196
|
-
Args:
|
197
|
-
frames_dir:
|
198
|
-
The directory containing the frames to be used in the GIF.
|
199
|
-
gif_path:
|
200
|
-
The path to save the GIF file. It should end with ".gif".
|
201
|
-
fps:
|
202
|
-
The number of frames per second in the GIF.
|
203
|
-
"""
|
204
|
-
frames = [
|
205
|
-
os.path.join(frames_dir, frame)
|
206
|
-
for frame in sorted(os.listdir(frames_dir))
|
207
|
-
]
|
208
|
-
images = [imageio.imread(frame) for frame in frames]
|
209
|
-
imageio.mimsave(gif_path, images, fps=fps, loop=0)
|