job-shop-lib 1.0.0a4__py3-none-any.whl → 1.0.0b1__py3-none-any.whl

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