job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b1__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 (52) hide show
  1. job_shop_lib/__init__.py +1 -1
  2. job_shop_lib/_job_shop_instance.py +34 -29
  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 -6
  7. job_shop_lib/dispatching/_dispatcher.py +19 -19
  8. job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
  9. job_shop_lib/dispatching/_factories.py +4 -2
  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.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/METADATA +17 -15
  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.0a5.dist-info/RECORD +0 -66
  51. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/LICENSE +0 -0
  52. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b1.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
1
1
  """Module for plotting static Gantt charts for job shop schedules."""
2
2
 
3
- from typing import Optional
3
+ from typing import Optional, List, Tuple, Dict
4
4
 
5
5
  from matplotlib.figure import Figure
6
6
  import matplotlib.pyplot as plt
@@ -9,23 +9,22 @@ from matplotlib.patches import Patch
9
9
 
10
10
  from job_shop_lib import Schedule, ScheduledOperation
11
11
 
12
-
13
12
  _BASE_Y_POSITION = 1
14
13
  _Y_POSITION_INCREMENT = 10
15
14
 
16
15
 
17
16
  def plot_gantt_chart(
18
17
  schedule: Schedule,
19
- title: str | 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