job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__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 +1 -1
- job_shop_lib/_job_shop_instance.py +34 -29
- job_shop_lib/_operation.py +4 -2
- job_shop_lib/_schedule.py +11 -11
- job_shop_lib/benchmarking/_load_benchmark.py +3 -3
- job_shop_lib/constraint_programming/_ortools_solver.py +6 -6
- job_shop_lib/dispatching/__init__.py +4 -3
- job_shop_lib/dispatching/_dispatcher.py +19 -19
- job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
- job_shop_lib/dispatching/_factories.py +4 -2
- job_shop_lib/dispatching/_history_observer.py +2 -1
- job_shop_lib/dispatching/_optimal_operations_observer.py +115 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
- job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
- job_shop_lib/dispatching/rules/__init__.py +37 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +50 -20
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
- job_shop_lib/dispatching/rules/_utils.py +9 -8
- job_shop_lib/generation/__init__.py +8 -0
- job_shop_lib/generation/_general_instance_generator.py +42 -64
- job_shop_lib/generation/_instance_generator.py +11 -7
- job_shop_lib/generation/_transformations.py +5 -4
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +7 -7
- job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
- job_shop_lib/graphs/_job_shop_graph.py +17 -13
- job_shop_lib/graphs/_node.py +6 -4
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
- job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
- job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
- job_shop_lib/reinforcement_learning/_utils.py +3 -3
- job_shop_lib/visualization/__init__.py +0 -60
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
- job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
- job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/METADATA +21 -15
- job_shop_lib-1.0.0b2.dist-info/RECORD +70 -0
- job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
- job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/WHEEL +0 -0
@@ -1,13 +1,13 @@
|
|
1
1
|
"""Home of the `GanttChartCreator` class and its configuration types."""
|
2
2
|
|
3
|
-
from typing import TypedDict
|
3
|
+
from typing import TypedDict, Optional
|
4
4
|
import matplotlib.pyplot as plt
|
5
5
|
|
6
6
|
from job_shop_lib.dispatching import (
|
7
7
|
Dispatcher,
|
8
8
|
HistoryObserver,
|
9
9
|
)
|
10
|
-
from job_shop_lib.visualization import (
|
10
|
+
from job_shop_lib.visualization.gantt import (
|
11
11
|
create_gantt_chart_video,
|
12
12
|
get_partial_gantt_chart_plotter,
|
13
13
|
create_gantt_chart_gif,
|
@@ -24,7 +24,7 @@ class PartialGanttChartPlotterConfig(TypedDict, total=False):
|
|
24
24
|
- :func:`get_partial_gantt_chart_plotter`
|
25
25
|
"""
|
26
26
|
|
27
|
-
title: str
|
27
|
+
title: Optional[str]
|
28
28
|
"""The title of the Gantt chart."""
|
29
29
|
|
30
30
|
cmap: str
|
@@ -43,7 +43,7 @@ class GifConfig(TypedDict, total=False):
|
|
43
43
|
:func:`create_gantt_chart_gif`
|
44
44
|
"""
|
45
45
|
|
46
|
-
gif_path: str
|
46
|
+
gif_path: Optional[str]
|
47
47
|
"""The path to save the GIF. It must end with '.gif'."""
|
48
48
|
|
49
49
|
fps: int
|
@@ -52,7 +52,7 @@ class GifConfig(TypedDict, total=False):
|
|
52
52
|
remove_frames: bool
|
53
53
|
"""Whether to remove the frames after creating the GIF."""
|
54
54
|
|
55
|
-
frames_dir: str
|
55
|
+
frames_dir: Optional[str]
|
56
56
|
"""The directory to store the frames."""
|
57
57
|
|
58
58
|
plot_current_time: bool
|
@@ -68,7 +68,7 @@ class VideoConfig(TypedDict, total=False):
|
|
68
68
|
:func:`create_gantt_chart_video`
|
69
69
|
"""
|
70
70
|
|
71
|
-
video_path: str
|
71
|
+
video_path: Optional[str]
|
72
72
|
"""The path to save the video. It must end with a valid video extension
|
73
73
|
(e.g., '.mp4')."""
|
74
74
|
|
@@ -78,7 +78,7 @@ class VideoConfig(TypedDict, total=False):
|
|
78
78
|
remove_frames: bool
|
79
79
|
"""Whether to remove the frames after creating the video."""
|
80
80
|
|
81
|
-
frames_dir: str
|
81
|
+
frames_dir: Optional[str]
|
82
82
|
"""The directory to store the frames."""
|
83
83
|
|
84
84
|
plot_current_time: bool
|
@@ -164,11 +164,11 @@ class GanttChartCreator:
|
|
164
164
|
def __init__(
|
165
165
|
self,
|
166
166
|
dispatcher: Dispatcher,
|
167
|
-
partial_gantt_chart_plotter_config:
|
168
|
-
PartialGanttChartPlotterConfig
|
169
|
-
|
170
|
-
gif_config: GifConfig
|
171
|
-
video_config: VideoConfig
|
167
|
+
partial_gantt_chart_plotter_config: Optional[
|
168
|
+
PartialGanttChartPlotterConfig
|
169
|
+
] = None,
|
170
|
+
gif_config: Optional[GifConfig] = None,
|
171
|
+
video_config: Optional[VideoConfig] = None,
|
172
172
|
):
|
173
173
|
if gif_config is None:
|
174
174
|
gif_config = {}
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
5
|
import shutil
|
6
|
-
from typing import Sequence, Protocol
|
6
|
+
from typing import Sequence, Protocol, Optional, List
|
7
7
|
|
8
8
|
import imageio
|
9
9
|
import matplotlib.pyplot as plt
|
@@ -22,7 +22,7 @@ from job_shop_lib.dispatching import (
|
|
22
22
|
HistoryObserver,
|
23
23
|
)
|
24
24
|
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
25
|
-
from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
|
25
|
+
from job_shop_lib.visualization.gantt._plot_gantt_chart import plot_gantt_chart
|
26
26
|
|
27
27
|
|
28
28
|
# This class serves as a more meaningful type hint than simply:
|
@@ -43,9 +43,9 @@ class PartialGanttChartPlotter(Protocol):
|
|
43
43
|
def __call__(
|
44
44
|
self,
|
45
45
|
schedule: Schedule,
|
46
|
-
makespan: int
|
47
|
-
available_operations:
|
48
|
-
current_time: int
|
46
|
+
makespan: Optional[int] = None,
|
47
|
+
available_operations: Optional[List[Operation]] = None,
|
48
|
+
current_time: Optional[int] = None,
|
49
49
|
) -> Figure:
|
50
50
|
"""Plots a Gantt chart for an unfinished schedule.
|
51
51
|
|
@@ -65,7 +65,7 @@ class PartialGanttChartPlotter(Protocol):
|
|
65
65
|
|
66
66
|
|
67
67
|
def get_partial_gantt_chart_plotter(
|
68
|
-
title: str
|
68
|
+
title: Optional[str] = None,
|
69
69
|
cmap: str = "viridis",
|
70
70
|
show_available_operations: bool = False,
|
71
71
|
) -> PartialGanttChartPlotter:
|
@@ -92,9 +92,9 @@ def get_partial_gantt_chart_plotter(
|
|
92
92
|
|
93
93
|
def plot_function(
|
94
94
|
schedule: Schedule,
|
95
|
-
makespan: int
|
96
|
-
available_operations:
|
97
|
-
current_time: int
|
95
|
+
makespan: Optional[int] = None,
|
96
|
+
available_operations: Optional[List[Operation]] = None,
|
97
|
+
current_time: Optional[int] = None,
|
98
98
|
) -> Figure:
|
99
99
|
fig, ax = plot_gantt_chart(
|
100
100
|
schedule, title=title, cmap_name=cmap, xlim=makespan
|
@@ -133,14 +133,14 @@ def get_partial_gantt_chart_plotter(
|
|
133
133
|
# pylint: disable=too-many-arguments
|
134
134
|
def create_gantt_chart_gif(
|
135
135
|
instance: JobShopInstance,
|
136
|
-
gif_path: str
|
137
|
-
solver: DispatchingRuleSolver
|
138
|
-
plot_function: PartialGanttChartPlotter
|
136
|
+
gif_path: Optional[str] = None,
|
137
|
+
solver: Optional[DispatchingRuleSolver] = None,
|
138
|
+
plot_function: Optional[PartialGanttChartPlotter] = None,
|
139
139
|
fps: int = 1,
|
140
140
|
remove_frames: bool = True,
|
141
|
-
frames_dir: str
|
141
|
+
frames_dir: Optional[str] = None,
|
142
142
|
plot_current_time: bool = True,
|
143
|
-
schedule_history: Sequence[ScheduledOperation]
|
143
|
+
schedule_history: Optional[Sequence[ScheduledOperation]] = None,
|
144
144
|
) -> None:
|
145
145
|
"""Creates a GIF of the schedule being built.
|
146
146
|
|
@@ -202,14 +202,14 @@ def create_gantt_chart_gif(
|
|
202
202
|
# pylint: disable=too-many-arguments
|
203
203
|
def create_gantt_chart_video(
|
204
204
|
instance: JobShopInstance,
|
205
|
-
video_path: str
|
206
|
-
solver: DispatchingRuleSolver
|
207
|
-
plot_function: PartialGanttChartPlotter
|
205
|
+
video_path: Optional[str] = None,
|
206
|
+
solver: Optional[DispatchingRuleSolver] = None,
|
207
|
+
plot_function: Optional[PartialGanttChartPlotter] = None,
|
208
208
|
fps: int = 1,
|
209
209
|
remove_frames: bool = True,
|
210
|
-
frames_dir: str
|
210
|
+
frames_dir: Optional[str] = None,
|
211
211
|
plot_current_time: bool = True,
|
212
|
-
schedule_history: Sequence[ScheduledOperation]
|
212
|
+
schedule_history: Optional[Sequence[ScheduledOperation]] = None,
|
213
213
|
) -> None:
|
214
214
|
"""Creates a video of the schedule being built.
|
215
215
|
|
@@ -268,10 +268,10 @@ def create_gantt_chart_video(
|
|
268
268
|
def create_gantt_chart_frames(
|
269
269
|
frames_dir: str,
|
270
270
|
instance: JobShopInstance,
|
271
|
-
solver: DispatchingRuleSolver
|
271
|
+
solver: Optional[DispatchingRuleSolver],
|
272
272
|
plot_function: PartialGanttChartPlotter,
|
273
273
|
plot_current_time: bool = True,
|
274
|
-
schedule_history: Sequence[ScheduledOperation]
|
274
|
+
schedule_history: Optional[Sequence[ScheduledOperation]] = None,
|
275
275
|
) -> None:
|
276
276
|
"""Creates frames of the Gantt chart for the schedule being built.
|
277
277
|
|
@@ -411,7 +411,7 @@ def resize_image_to_macro_block(
|
|
411
411
|
return image
|
412
412
|
|
413
413
|
|
414
|
-
def _load_images(frames_dir: str) ->
|
414
|
+
def _load_images(frames_dir: str) -> List:
|
415
415
|
frames = [
|
416
416
|
os.path.join(frames_dir, frame)
|
417
417
|
for frame in sorted(os.listdir(frames_dir))
|
@@ -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
|
18
|
+
title: Optional[str] = None,
|
20
19
|
cmap_name: str = "viridis",
|
21
|
-
xlim: int
|
20
|
+
xlim: Optional[int] = None,
|
22
21
|
number_of_x_ticks: int = 15,
|
23
|
-
job_labels:
|
24
|
-
machine_labels:
|
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
|
-
) ->
|
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
|
93
|
+
title: Optional[str],
|
95
94
|
x_label: str = "Time units",
|
96
95
|
y_label: str = "Machines",
|
97
|
-
) ->
|
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:
|
115
|
-
) ->
|
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:
|
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:
|
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_resource_task_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],
|
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
|
-
#
|
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
|
58
|
+
job_shop: Union[JobShopGraph, JobShopInstance],
|
59
59
|
*,
|
60
|
-
title: str
|
61
|
-
figsize:
|
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
|
73
|
-
draw_disjunctive_edges: bool
|
74
|
-
conjunctive_edges_additional_params:
|
75
|
-
disjunctive_edges_additional_params:
|
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]
|
80
|
+
machine_labels: Optional[Sequence[str]] = None,
|
81
81
|
legend_location: str = "upper left",
|
82
|
-
legend_bbox_to_anchor:
|
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
|
-
) ->
|
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[
|
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,
|
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
|
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
|
|