job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +10 -2
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generation/general_instance_generator.py +0 -169
  84. job_shop_lib/generation/transformations.py +0 -164
  85. job_shop_lib/generators/__init__.py +0 -8
  86. job_shop_lib/generators/basic_generator.py +0 -200
  87. job_shop_lib/graphs/constants.py +0 -21
  88. job_shop_lib/graphs/job_shop_graph.py +0 -202
  89. job_shop_lib/graphs/node.py +0 -166
  90. job_shop_lib/operation.py +0 -122
  91. job_shop_lib/visualization/agent_task_graph.py +0 -257
  92. job_shop_lib/visualization/create_gif.py +0 -209
  93. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  94. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  95. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -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)
@@ -1,210 +0,0 @@
1
- """Module for visualizing the disjunctive graph of a job shop instance."""
2
-
3
- import functools
4
- from typing import Optional, Callable
5
- import warnings
6
- import copy
7
-
8
- import matplotlib
9
- import matplotlib.pyplot as plt
10
- import networkx as nx
11
- from networkx.drawing.nx_agraph import graphviz_layout
12
-
13
- from job_shop_lib import JobShopInstance
14
- from job_shop_lib.graphs import (
15
- JobShopGraph,
16
- EdgeType,
17
- NodeType,
18
- Node,
19
- build_disjunctive_graph,
20
- )
21
-
22
-
23
- Layout = Callable[[nx.Graph], dict[str, tuple[float, float]]]
24
-
25
-
26
- # This function could be improved by a function extraction refactoring
27
- # (see `plot_gantt_chart`
28
- # function as a reference in how to do it). That would solve the
29
- # "too many locals" warning. However, this refactoring is not a priority at
30
- # the moment. To compensate, sections are separated by comments.
31
- # For the "too many arguments" warning no satisfactory solution was
32
- # found. I believe is still better than using `**kwargs` and losing the
33
- # function signature or adding a dataclass for configuration (it would add
34
- # unnecessary complexity).
35
- # pylint: disable=too-many-arguments, too-many-locals
36
- def plot_disjunctive_graph(
37
- job_shop: JobShopGraph | JobShopInstance,
38
- figsize: tuple[float, float] = (6, 4),
39
- node_size: int = 1600,
40
- title: Optional[str] = None,
41
- layout: Optional[Layout] = None,
42
- edge_width: int = 2,
43
- font_size: int = 10,
44
- arrow_size: int = 35,
45
- alpha=0.95,
46
- node_font_color: str = "white",
47
- color_map: str = "Dark2_r",
48
- draw_disjunctive_edges: bool = True,
49
- ) -> plt.Figure:
50
- """Returns a plot of the disjunctive graph of the instance."""
51
-
52
- if isinstance(job_shop, JobShopInstance):
53
- job_shop_graph = build_disjunctive_graph(job_shop)
54
- else:
55
- job_shop_graph = job_shop
56
-
57
- # Set up the plot
58
- # ----------------
59
- plt.figure(figsize=figsize)
60
- if title is None:
61
- title = (
62
- f"Disjunctive Graph Visualization: {job_shop_graph.instance.name}"
63
- )
64
- plt.title(title)
65
-
66
- # Set up the layout
67
- # -----------------
68
- if layout is None:
69
- layout = functools.partial(
70
- graphviz_layout, prog="dot", args="-Grankdir=LR"
71
- )
72
-
73
- temp_graph = copy.deepcopy(job_shop_graph.graph)
74
- # Remove disjunctive edges to get a better layout
75
- temp_graph.remove_edges_from(
76
- [
77
- (u, v)
78
- for u, v, d in job_shop_graph.graph.edges(data=True)
79
- if d["type"] == EdgeType.DISJUNCTIVE
80
- ]
81
- )
82
-
83
- try:
84
- pos = layout(temp_graph)
85
- except ImportError:
86
- warnings.warn(
87
- "Default layout requires pygraphviz http://pygraphviz.github.io/. "
88
- "Using spring layout instead.",
89
- )
90
- pos = nx.spring_layout(temp_graph)
91
-
92
- # Draw nodes
93
- # ----------
94
- node_colors = [
95
- _get_node_color(node)
96
- for node in job_shop_graph.nodes
97
- if not job_shop_graph.is_removed(node.node_id)
98
- ]
99
-
100
- nx.draw_networkx_nodes(
101
- job_shop_graph.graph,
102
- pos,
103
- node_size=node_size,
104
- node_color=node_colors,
105
- alpha=alpha,
106
- cmap=matplotlib.colormaps.get_cmap(color_map),
107
- )
108
-
109
- # Draw edges
110
- # ----------
111
- conjunctive_edges = [
112
- (u, v)
113
- for u, v, d in job_shop_graph.graph.edges(data=True)
114
- if d["type"] == EdgeType.CONJUNCTIVE
115
- ]
116
- disjunctive_edges = [
117
- (u, v)
118
- for u, v, d in job_shop_graph.graph.edges(data=True)
119
- if d["type"] == EdgeType.DISJUNCTIVE
120
- ]
121
-
122
- nx.draw_networkx_edges(
123
- job_shop_graph.graph,
124
- pos,
125
- edgelist=conjunctive_edges,
126
- width=edge_width,
127
- edge_color="black",
128
- arrowsize=arrow_size,
129
- )
130
-
131
- if draw_disjunctive_edges:
132
- nx.draw_networkx_edges(
133
- job_shop_graph.graph,
134
- pos,
135
- edgelist=disjunctive_edges,
136
- width=edge_width,
137
- edge_color="red",
138
- arrowsize=arrow_size,
139
- )
140
-
141
- # Draw node labels
142
- # ----------------
143
- operation_nodes = job_shop_graph.nodes_by_type[NodeType.OPERATION]
144
-
145
- labels = {}
146
- source_node = job_shop_graph.nodes_by_type[NodeType.SOURCE][0]
147
- labels[source_node] = "S"
148
-
149
- sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
150
- labels[sink_node] = "T"
151
- for operation_node in operation_nodes:
152
- if job_shop_graph.is_removed(operation_node.node_id):
153
- continue
154
- labels[operation_node] = (
155
- f"m={operation_node.operation.machine_id}\n"
156
- f"d={operation_node.operation.duration}"
157
- )
158
-
159
- nx.draw_networkx_labels(
160
- job_shop_graph.graph,
161
- pos,
162
- labels=labels,
163
- font_color=node_font_color,
164
- font_size=font_size,
165
- font_family="sans-serif",
166
- )
167
-
168
- # Final touches
169
- # -------------
170
- plt.axis("off")
171
- plt.tight_layout()
172
- # Create a legend to indicate the meaning of the edge colors
173
- conjunctive_patch = matplotlib.patches.Patch(
174
- color="black", label="conjunctive edges"
175
- )
176
- disjunctive_patch = matplotlib.patches.Patch(
177
- color="red", label="disjunctive edges"
178
- )
179
-
180
- # Add to the legend the meaning of m and d
181
- text = "m = machine_id\nd = duration"
182
- extra = matplotlib.patches.Rectangle(
183
- (0, 0),
184
- 1,
185
- 1,
186
- fc="w",
187
- fill=False,
188
- edgecolor="none",
189
- linewidth=0,
190
- label=text,
191
- )
192
- plt.legend(
193
- handles=[conjunctive_patch, disjunctive_patch, extra],
194
- loc="upper left",
195
- bbox_to_anchor=(1.05, 1),
196
- borderaxespad=0.0,
197
- )
198
- return plt.gcf()
199
-
200
-
201
- def _get_node_color(node: Node) -> int:
202
- """Returns the color of the node."""
203
- if node.node_type == NodeType.SOURCE:
204
- return -1
205
- if node.node_type == NodeType.SINK:
206
- return -1
207
- if node.node_type == NodeType.OPERATION:
208
- return node.operation.machine_id
209
-
210
- raise ValueError("Invalid node type.")