job-shop-lib 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +20 -0
- job_shop_lib/base_solver.py +37 -0
- job_shop_lib/benchmarking/__init__.py +78 -0
- job_shop_lib/benchmarking/benchmark_instances.json +1 -0
- job_shop_lib/benchmarking/load_benchmark.py +142 -0
- job_shop_lib/cp_sat/__init__.py +5 -0
- job_shop_lib/cp_sat/ortools_solver.py +201 -0
- job_shop_lib/dispatching/__init__.py +49 -0
- job_shop_lib/dispatching/dispatcher.py +269 -0
- job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
- job_shop_lib/dispatching/dispatching_rules.py +160 -0
- job_shop_lib/dispatching/factories.py +206 -0
- job_shop_lib/dispatching/pruning_functions.py +116 -0
- job_shop_lib/exceptions.py +26 -0
- job_shop_lib/generators/__init__.py +7 -0
- job_shop_lib/generators/basic_generator.py +197 -0
- job_shop_lib/graphs/__init__.py +52 -0
- job_shop_lib/graphs/build_agent_task_graph.py +209 -0
- job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
- job_shop_lib/graphs/constants.py +21 -0
- job_shop_lib/graphs/job_shop_graph.py +159 -0
- job_shop_lib/graphs/node.py +147 -0
- job_shop_lib/job_shop_instance.py +355 -0
- job_shop_lib/operation.py +120 -0
- job_shop_lib/schedule.py +180 -0
- job_shop_lib/scheduled_operation.py +97 -0
- job_shop_lib/visualization/__init__.py +25 -0
- job_shop_lib/visualization/agent_task_graph.py +257 -0
- job_shop_lib/visualization/create_gif.py +191 -0
- job_shop_lib/visualization/disjunctive_graph.py +206 -0
- job_shop_lib/visualization/gantt_chart.py +147 -0
- job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
- job_shop_lib-0.1.0.dist-info/METADATA +363 -0
- job_shop_lib-0.1.0.dist-info/RECORD +35 -0
- job_shop_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
"""Home of the `Operation` class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
|
6
|
+
class Operation:
|
7
|
+
"""Stores machine and duration information for a job operation.
|
8
|
+
|
9
|
+
Note:
|
10
|
+
To increase performance, some solvers such as the CP-SAT solver use
|
11
|
+
only integers to represent the operation's attributes. Should a
|
12
|
+
problem involve operations with non-integer durations, it would be
|
13
|
+
necessary to multiply all durations by a sufficiently large integer so
|
14
|
+
that every duration is an integer.
|
15
|
+
|
16
|
+
Attributes:
|
17
|
+
machines: A list of machine ids that can perform the operation.
|
18
|
+
duration: The time it takes to perform the operation.
|
19
|
+
"""
|
20
|
+
|
21
|
+
__slots__ = (
|
22
|
+
"machines",
|
23
|
+
"duration",
|
24
|
+
"_job_id",
|
25
|
+
"_position_in_job",
|
26
|
+
"_operation_id",
|
27
|
+
)
|
28
|
+
|
29
|
+
def __init__(self, machines: int | list[int], duration: int):
|
30
|
+
"""Initializes the object with the given machines and duration.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
machines: A list of machine ids that can perform the operation. If
|
34
|
+
only one machine can perform the operation, it can be passed as
|
35
|
+
an integer.
|
36
|
+
duration: The time it takes to perform the operation.
|
37
|
+
"""
|
38
|
+
self.machines = [machines] if isinstance(machines, int) else machines
|
39
|
+
self.duration = duration
|
40
|
+
|
41
|
+
# Defined outside the class by the JobShopInstance class:
|
42
|
+
self._job_id: int | None = None
|
43
|
+
self._position_in_job: int | None = None
|
44
|
+
self._operation_id: int | None = None
|
45
|
+
|
46
|
+
@property
|
47
|
+
def machine_id(self) -> int:
|
48
|
+
"""Returns the id of the machine associated with the operation.
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
ValueError: If the operation has multiple machines in its list.
|
52
|
+
"""
|
53
|
+
if len(self.machines) > 1:
|
54
|
+
raise ValueError("Operation has multiple machines.")
|
55
|
+
return self.machines[0]
|
56
|
+
|
57
|
+
@property
|
58
|
+
def job_id(self) -> int:
|
59
|
+
"""Returns the id of the job that the operation belongs to."""
|
60
|
+
if self._job_id is None:
|
61
|
+
raise ValueError("Operation has no job_id.")
|
62
|
+
return self._job_id
|
63
|
+
|
64
|
+
@job_id.setter
|
65
|
+
def job_id(self, value: int) -> None:
|
66
|
+
self._job_id = value
|
67
|
+
|
68
|
+
@property
|
69
|
+
def position_in_job(self) -> int:
|
70
|
+
"""Returns the position (starting at zero) of the operation in the
|
71
|
+
job.
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
ValueError: If the operation has no position_in_job.
|
75
|
+
"""
|
76
|
+
if self._position_in_job is None:
|
77
|
+
raise ValueError("Operation has no position_in_job.")
|
78
|
+
return self._position_in_job
|
79
|
+
|
80
|
+
@position_in_job.setter
|
81
|
+
def position_in_job(self, value: int) -> None:
|
82
|
+
self._position_in_job = value
|
83
|
+
|
84
|
+
@property
|
85
|
+
def operation_id(self) -> int:
|
86
|
+
"""Returns the id of the operation.
|
87
|
+
|
88
|
+
The operation id is unique within a job shop instance and should
|
89
|
+
be set by the JobShopInstance class.
|
90
|
+
|
91
|
+
It starts at 0 and is incremented by 1 for each operation in the
|
92
|
+
instance.
|
93
|
+
|
94
|
+
Raises:
|
95
|
+
ValueError: If the operation has no id.
|
96
|
+
"""
|
97
|
+
if self._operation_id is None:
|
98
|
+
raise ValueError("Operation has no id.")
|
99
|
+
return self._operation_id
|
100
|
+
|
101
|
+
@operation_id.setter
|
102
|
+
def operation_id(self, value: int) -> None:
|
103
|
+
self._operation_id = value
|
104
|
+
|
105
|
+
def __hash__(self) -> int:
|
106
|
+
return hash(self.operation_id)
|
107
|
+
|
108
|
+
def __eq__(self, __value: object) -> bool:
|
109
|
+
if isinstance(__value, Operation):
|
110
|
+
return self.operation_id == __value.operation_id
|
111
|
+
return False
|
112
|
+
|
113
|
+
def __repr__(self) -> str:
|
114
|
+
machines = (
|
115
|
+
self.machines[0] if len(self.machines) == 1 else self.machines
|
116
|
+
)
|
117
|
+
return (
|
118
|
+
f"O(m={machines}, d={self.duration}, "
|
119
|
+
f"j={self.job_id}, p={self.position_in_job})"
|
120
|
+
)
|
job_shop_lib/schedule.py
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
"""Home of the `Schedule` class."""
|
2
|
+
|
3
|
+
from job_shop_lib import ScheduledOperation, JobShopInstance
|
4
|
+
|
5
|
+
|
6
|
+
class Schedule:
|
7
|
+
"""Data structure to store a schedule for a `JobShopInstance` object.
|
8
|
+
|
9
|
+
Attributes:
|
10
|
+
instance:
|
11
|
+
The `JobShopInstance` object that the schedule is for.
|
12
|
+
schedule:
|
13
|
+
A list of lists of `ScheduledOperation` objects. Each list of
|
14
|
+
`ScheduledOperation` objects represents the order of operations
|
15
|
+
on a machine.
|
16
|
+
metadata:
|
17
|
+
A dictionary with additional information about the schedule. It
|
18
|
+
can be used to store information about the algorithm that generated
|
19
|
+
the schedule, for example.
|
20
|
+
"""
|
21
|
+
|
22
|
+
__slots__ = (
|
23
|
+
"instance",
|
24
|
+
"_schedule",
|
25
|
+
"metadata",
|
26
|
+
)
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
instance: JobShopInstance,
|
31
|
+
schedule: list[list[ScheduledOperation]] | None = None,
|
32
|
+
**metadata,
|
33
|
+
):
|
34
|
+
"""Initializes the object with the given instance and schedule.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
instance:
|
38
|
+
The `JobShopInstance` object that the schedule is for.
|
39
|
+
schedule:
|
40
|
+
A list of lists of `ScheduledOperation` objects. Each list of
|
41
|
+
`ScheduledOperation` objects represents the order of operations
|
42
|
+
on a machine. If not provided, the schedule is initialized as
|
43
|
+
an empty schedule.
|
44
|
+
**metadata:
|
45
|
+
Additional information about the schedule.
|
46
|
+
"""
|
47
|
+
if schedule is None:
|
48
|
+
schedule = [[] for _ in range(instance.num_machines)]
|
49
|
+
|
50
|
+
Schedule.check_schedule(schedule)
|
51
|
+
|
52
|
+
self.instance = instance
|
53
|
+
self._schedule = schedule
|
54
|
+
self.metadata = metadata
|
55
|
+
|
56
|
+
def __repr__(self) -> str:
|
57
|
+
return str(self.schedule)
|
58
|
+
|
59
|
+
@property
|
60
|
+
def schedule(self) -> list[list[ScheduledOperation]]:
|
61
|
+
"""Returns the schedule attribute."""
|
62
|
+
return self._schedule
|
63
|
+
|
64
|
+
@schedule.setter
|
65
|
+
def schedule(self, new_schedule: list[list[ScheduledOperation]]):
|
66
|
+
Schedule.check_schedule(new_schedule)
|
67
|
+
self._schedule = new_schedule
|
68
|
+
|
69
|
+
@property
|
70
|
+
def num_scheduled_operations(self) -> int:
|
71
|
+
"""Returns the number of operations that have been scheduled."""
|
72
|
+
return sum(len(machine_schedule) for machine_schedule in self.schedule)
|
73
|
+
|
74
|
+
def reset(self):
|
75
|
+
"""Resets the schedule to an empty state."""
|
76
|
+
self.schedule = [[] for _ in range(self.instance.num_machines)]
|
77
|
+
|
78
|
+
def makespan(self) -> int:
|
79
|
+
"""Returns the makespan of the schedule.
|
80
|
+
|
81
|
+
The makespan is the time at which all operations are completed.
|
82
|
+
"""
|
83
|
+
max_end_time = 0
|
84
|
+
for machine_schedule in self.schedule:
|
85
|
+
if machine_schedule:
|
86
|
+
max_end_time = max(max_end_time, machine_schedule[-1].end_time)
|
87
|
+
return max_end_time
|
88
|
+
|
89
|
+
def is_complete(self) -> bool:
|
90
|
+
"""Returns True if all operations have been scheduled."""
|
91
|
+
return self.num_scheduled_operations == self.instance.num_operations
|
92
|
+
|
93
|
+
def add(self, scheduled_operation: ScheduledOperation):
|
94
|
+
"""Adds a new `ScheduledOperation` to the schedule.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
scheduled_operation:
|
98
|
+
The `ScheduledOperation` to add to the schedule.
|
99
|
+
|
100
|
+
Raises:
|
101
|
+
ValueError: If the start time of the new operation is before the
|
102
|
+
end time of the last operation on the same machine. In favor of
|
103
|
+
performance, this method does not checks precedence
|
104
|
+
constraints.
|
105
|
+
"""
|
106
|
+
self._check_start_time_of_new_operation(scheduled_operation)
|
107
|
+
self.schedule[scheduled_operation.machine_id].append(
|
108
|
+
scheduled_operation
|
109
|
+
)
|
110
|
+
|
111
|
+
def _check_start_time_of_new_operation(
|
112
|
+
self,
|
113
|
+
new_operation: ScheduledOperation,
|
114
|
+
):
|
115
|
+
is_first_operation = not self.schedule[new_operation.machine_id]
|
116
|
+
if is_first_operation:
|
117
|
+
return
|
118
|
+
|
119
|
+
last_operation = self.schedule[new_operation.machine_id][-1]
|
120
|
+
self._check_start_time(new_operation, last_operation)
|
121
|
+
|
122
|
+
@staticmethod
|
123
|
+
def _check_start_time(
|
124
|
+
scheduled_operation: ScheduledOperation,
|
125
|
+
previous_operation: ScheduledOperation,
|
126
|
+
):
|
127
|
+
"""Raises a ValueError if the start time of the new operation is before
|
128
|
+
the end time of the last operation on the same machine."""
|
129
|
+
|
130
|
+
if previous_operation.end_time <= scheduled_operation.start_time:
|
131
|
+
return
|
132
|
+
|
133
|
+
raise ValueError(
|
134
|
+
"Operation cannot be scheduled before the last operation on "
|
135
|
+
"the same machine: end time of last operation "
|
136
|
+
f"({previous_operation.end_time}) > start time of new operation "
|
137
|
+
f"({scheduled_operation.start_time})."
|
138
|
+
)
|
139
|
+
|
140
|
+
@staticmethod
|
141
|
+
def check_schedule(schedule: list[list[ScheduledOperation]]):
|
142
|
+
"""Checks if a schedule is valid and raises a ValueError if it is not.
|
143
|
+
|
144
|
+
A schedule is considered invalid if:
|
145
|
+
- A `ScheduledOperation` has a machine id that does not match the
|
146
|
+
machine id of the machine schedule (the list of
|
147
|
+
`ScheduledOperation` objects) that it belongs to.
|
148
|
+
- The start time of a `ScheduledOperation` is before the end time
|
149
|
+
of the last operation on the same machine.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
schedule:
|
153
|
+
The schedule (a list of lists of `ScheduledOperation` objects)
|
154
|
+
to check.
|
155
|
+
|
156
|
+
Raises:
|
157
|
+
ValueError: If the schedule is invalid.
|
158
|
+
"""
|
159
|
+
for machine_id, scheduled_operations in enumerate(schedule):
|
160
|
+
for i, scheduled_operation in enumerate(scheduled_operations):
|
161
|
+
if scheduled_operation.machine_id != machine_id:
|
162
|
+
raise ValueError(
|
163
|
+
"The machine id of the scheduled operation "
|
164
|
+
f"({ScheduledOperation.machine_id}) does not match "
|
165
|
+
f"the machine id of the machine schedule ({machine_id}"
|
166
|
+
f"). Index of the operation: [{machine_id}][{i}]."
|
167
|
+
)
|
168
|
+
|
169
|
+
if i == 0:
|
170
|
+
continue
|
171
|
+
|
172
|
+
Schedule._check_start_time(
|
173
|
+
scheduled_operation, scheduled_operations[i - 1]
|
174
|
+
)
|
175
|
+
|
176
|
+
def __eq__(self, value: object) -> bool:
|
177
|
+
if not isinstance(value, Schedule):
|
178
|
+
return False
|
179
|
+
|
180
|
+
return self.schedule == value.schedule
|
@@ -0,0 +1,97 @@
|
|
1
|
+
"""Home of the `ScheduledOperation` class."""
|
2
|
+
|
3
|
+
from job_shop_lib import Operation
|
4
|
+
|
5
|
+
|
6
|
+
class ScheduledOperation:
|
7
|
+
"""Data structure to store a scheduled operation.
|
8
|
+
|
9
|
+
Attributes:
|
10
|
+
operation:
|
11
|
+
The `Operation` object that is scheduled.
|
12
|
+
start_time:
|
13
|
+
The time at which the operation is scheduled to start.
|
14
|
+
machine_id:
|
15
|
+
The id of the machine on which the operation is scheduled.
|
16
|
+
"""
|
17
|
+
|
18
|
+
__slots__ = ("operation", "start_time", "_machine_id")
|
19
|
+
|
20
|
+
def __init__(self, operation: Operation, start_time: int, machine_id: int):
|
21
|
+
"""Initializes the object with the given operation, start time, and
|
22
|
+
machine id.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
operation:
|
26
|
+
The `Operation` object that is scheduled.
|
27
|
+
start_time:
|
28
|
+
The time at which the operation is scheduled to start.
|
29
|
+
machine_id:
|
30
|
+
The id of the machine on which the operation is scheduled.
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
ValueError:
|
34
|
+
If the machine_id is not valid for the operation.
|
35
|
+
"""
|
36
|
+
self.operation = operation
|
37
|
+
self.start_time = start_time
|
38
|
+
self._machine_id = machine_id
|
39
|
+
self.machine_id = machine_id # Validate machine_id
|
40
|
+
|
41
|
+
@property
|
42
|
+
def machine_id(self) -> int:
|
43
|
+
"""Returns the id of the machine on which the operation has been
|
44
|
+
scheduled."""
|
45
|
+
return self._machine_id
|
46
|
+
|
47
|
+
@machine_id.setter
|
48
|
+
def machine_id(self, value: int):
|
49
|
+
if value not in self.operation.machines:
|
50
|
+
raise ValueError(
|
51
|
+
f"Operation cannot be scheduled on machine {value}. "
|
52
|
+
f"Valid machines are {self.operation.machines}."
|
53
|
+
)
|
54
|
+
self._machine_id = value
|
55
|
+
|
56
|
+
@property
|
57
|
+
def job_id(self) -> int:
|
58
|
+
"""Returns the id of the job that the operation belongs to.
|
59
|
+
|
60
|
+
Raises:
|
61
|
+
ValueError: If the operation has no job_id.
|
62
|
+
"""
|
63
|
+
|
64
|
+
if self.operation.job_id is None:
|
65
|
+
raise ValueError("Operation has no job_id.")
|
66
|
+
return self.operation.job_id
|
67
|
+
|
68
|
+
@property
|
69
|
+
def position(self) -> int:
|
70
|
+
"""Returns the position (starting at zero) of the operation in the job.
|
71
|
+
|
72
|
+
Raises:
|
73
|
+
ValueError: If the operation has no position_in_job.
|
74
|
+
"""
|
75
|
+
if self.operation.position_in_job is None:
|
76
|
+
raise ValueError("Operation has no position.")
|
77
|
+
return self.operation.position_in_job
|
78
|
+
|
79
|
+
@property
|
80
|
+
def end_time(self) -> int:
|
81
|
+
"""Returns the time at which the operation is scheduled to end."""
|
82
|
+
return self.start_time + self.operation.duration
|
83
|
+
|
84
|
+
def __repr__(self) -> str:
|
85
|
+
return (
|
86
|
+
f"S-Op(operation={self.operation}, "
|
87
|
+
f"start_time={self.start_time}, machine_id={self.machine_id})"
|
88
|
+
)
|
89
|
+
|
90
|
+
def __eq__(self, value: object) -> bool:
|
91
|
+
if not isinstance(value, ScheduledOperation):
|
92
|
+
return False
|
93
|
+
return (
|
94
|
+
self.operation is value.operation
|
95
|
+
and self.start_time == value.start_time
|
96
|
+
and self.machine_id == value.machine_id
|
97
|
+
)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
"""Package for visualization."""
|
2
|
+
|
3
|
+
from job_shop_lib.visualization.gantt_chart import plot_gantt_chart
|
4
|
+
from job_shop_lib.visualization.create_gif import (
|
5
|
+
create_gif,
|
6
|
+
create_gantt_chart_frames,
|
7
|
+
plot_gantt_chart_wrapper,
|
8
|
+
create_gif_from_frames,
|
9
|
+
)
|
10
|
+
from job_shop_lib.visualization.disjunctive_graph import plot_disjunctive_graph
|
11
|
+
from job_shop_lib.visualization.agent_task_graph import (
|
12
|
+
plot_agent_task_graph,
|
13
|
+
three_columns_layout,
|
14
|
+
)
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"plot_gantt_chart",
|
18
|
+
"create_gif",
|
19
|
+
"create_gantt_chart_frames",
|
20
|
+
"plot_gantt_chart_wrapper",
|
21
|
+
"create_gif_from_frames",
|
22
|
+
"plot_disjunctive_graph",
|
23
|
+
"plot_agent_task_graph",
|
24
|
+
"three_columns_layout",
|
25
|
+
]
|
@@ -0,0 +1,257 @@
|
|
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
|