job-shop-lib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,197 @@
|
|
1
|
+
"""Home of the `BasicGenerator` class."""
|
2
|
+
|
3
|
+
import random
|
4
|
+
from typing import Iterator
|
5
|
+
|
6
|
+
from job_shop_lib import JobShopInstance, Operation
|
7
|
+
|
8
|
+
|
9
|
+
class BasicGenerator: # pylint: disable=too-many-instance-attributes
|
10
|
+
"""Generates instances for job shop problems.
|
11
|
+
|
12
|
+
This class is designed to be versatile, enabling the creation of various
|
13
|
+
job shop instances without the need for multiple dedicated classes.
|
14
|
+
|
15
|
+
It supports customization of the number of jobs, machines, operation
|
16
|
+
durations, and more.
|
17
|
+
|
18
|
+
The class supports both single instance generation and iteration over
|
19
|
+
multiple instances, controlled by the `iteration_limit` parameter. It
|
20
|
+
implements the iterator protocol, allowing it to be used in a `for` loop.
|
21
|
+
|
22
|
+
Note:
|
23
|
+
When used as an iterator, the generator will produce instances until it
|
24
|
+
reaches the specified `iteration_limit`. If `iteration_limit` is None,
|
25
|
+
it will continue indefinitely.
|
26
|
+
|
27
|
+
Attributes:
|
28
|
+
num_jobs_range:
|
29
|
+
The range of the number of jobs to generate. If a single
|
30
|
+
int is provided, it is used as both the minimum and maximum.
|
31
|
+
duration_range:
|
32
|
+
The range of durations for each operation.
|
33
|
+
num_machines_range:
|
34
|
+
The range of the number of machines available. If a
|
35
|
+
single int is provided, it is used as both the minimum and maximum.
|
36
|
+
machines_per_operation:
|
37
|
+
Specifies how many machines each operation
|
38
|
+
can be assigned to. If a single int is provided, it is used for
|
39
|
+
all operations.
|
40
|
+
allow_less_jobs_than_machines:
|
41
|
+
If True, allows generating instances where the number of jobs is
|
42
|
+
less than the number of machines.
|
43
|
+
allow_recirculation:
|
44
|
+
If True, a job can visit the same machine more than once.
|
45
|
+
name_suffix:
|
46
|
+
A suffix to append to each instance's name for identification.
|
47
|
+
seed:
|
48
|
+
Seed for the random number generator to ensure reproducibility.
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__( # pylint: disable=too-many-arguments
|
52
|
+
self,
|
53
|
+
num_jobs: int | tuple[int, int] = (10, 20),
|
54
|
+
num_machines: int | tuple[int, int] = (5, 10),
|
55
|
+
duration_range: tuple[int, int] = (1, 99),
|
56
|
+
allow_less_jobs_than_machines: bool = True,
|
57
|
+
allow_recirculation: bool = False,
|
58
|
+
machines_per_operation: int | tuple[int, int] = 1,
|
59
|
+
name_suffix: str = "classic_generated_instance",
|
60
|
+
seed: int | None = None,
|
61
|
+
iteration_limit: int | None = None,
|
62
|
+
):
|
63
|
+
"""Initializes the instance generator with the given parameters.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
num_jobs:
|
67
|
+
The range of the number of jobs to generate.
|
68
|
+
num_machines:
|
69
|
+
The range of the number of machines available.
|
70
|
+
duration_range:
|
71
|
+
The range of durations for each operation.
|
72
|
+
allow_less_jobs_than_machines:
|
73
|
+
Allows instances with fewer jobs than machines.
|
74
|
+
allow_recirculation:
|
75
|
+
Allows jobs to visit the same machine multiple times.
|
76
|
+
machines_per_operation:
|
77
|
+
Specifies how many machines each operation can be assigned to.
|
78
|
+
If a single int is provided, it is used for all operations.
|
79
|
+
name_suffix:
|
80
|
+
Suffix for instance names.
|
81
|
+
seed:
|
82
|
+
Seed for the random number generator.
|
83
|
+
iteration_limit:
|
84
|
+
Maximum number of instances to generate in iteration mode.
|
85
|
+
"""
|
86
|
+
if isinstance(num_jobs, int):
|
87
|
+
num_jobs = (num_jobs, num_jobs)
|
88
|
+
|
89
|
+
if isinstance(num_machines, int):
|
90
|
+
num_machines = (num_machines, num_machines)
|
91
|
+
|
92
|
+
if isinstance(machines_per_operation, int):
|
93
|
+
machines_per_operation = (
|
94
|
+
machines_per_operation,
|
95
|
+
machines_per_operation,
|
96
|
+
)
|
97
|
+
|
98
|
+
self.num_jobs_range = num_jobs
|
99
|
+
self.duration_range = duration_range
|
100
|
+
self.num_machines_range = num_machines
|
101
|
+
self.machines_per_operation = machines_per_operation
|
102
|
+
|
103
|
+
self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
|
104
|
+
self.allow_recirculation = allow_recirculation
|
105
|
+
self.name_suffix = name_suffix
|
106
|
+
|
107
|
+
self._counter = 0
|
108
|
+
self._current_iteration = 0
|
109
|
+
self._iteration_limit = iteration_limit
|
110
|
+
|
111
|
+
if seed is not None:
|
112
|
+
random.seed(seed)
|
113
|
+
|
114
|
+
def generate(self) -> JobShopInstance:
|
115
|
+
"""Generates a single job shop instance"""
|
116
|
+
num_jobs = random.randint(*self.num_jobs_range)
|
117
|
+
|
118
|
+
min_num_machines, max_num_machines = self.num_machines_range
|
119
|
+
if not self.allow_less_jobs_than_machines:
|
120
|
+
min_num_machines = min(num_jobs, max_num_machines)
|
121
|
+
num_machines = random.randint(min_num_machines, max_num_machines)
|
122
|
+
|
123
|
+
jobs = []
|
124
|
+
available_machines = list(range(num_machines))
|
125
|
+
for _ in range(num_jobs):
|
126
|
+
job = []
|
127
|
+
for _ in range(num_machines):
|
128
|
+
operation = self.create_random_operation(available_machines)
|
129
|
+
job.append(operation)
|
130
|
+
jobs.append(job)
|
131
|
+
available_machines = list(range(num_machines))
|
132
|
+
|
133
|
+
return JobShopInstance(jobs=jobs, name=self._get_name())
|
134
|
+
|
135
|
+
def __iter__(self) -> Iterator[JobShopInstance]:
|
136
|
+
self._current_iteration = 0
|
137
|
+
return self
|
138
|
+
|
139
|
+
def __next__(self) -> JobShopInstance:
|
140
|
+
if (
|
141
|
+
self._iteration_limit is not None
|
142
|
+
and self._current_iteration >= self._iteration_limit
|
143
|
+
):
|
144
|
+
raise StopIteration
|
145
|
+
self._current_iteration += 1
|
146
|
+
return self.generate()
|
147
|
+
|
148
|
+
def __len__(self) -> int:
|
149
|
+
if self._iteration_limit is None:
|
150
|
+
raise ValueError("Iteration limit is not set.")
|
151
|
+
return self._iteration_limit
|
152
|
+
|
153
|
+
def create_random_operation(
|
154
|
+
self, available_machines: list[int] | None = None
|
155
|
+
) -> Operation:
|
156
|
+
"""Creates a random operation with the given available machines.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
available_machines:
|
160
|
+
A list of available machine_ids to choose from.
|
161
|
+
If None, all machines are available.
|
162
|
+
"""
|
163
|
+
duration = random.randint(*self.duration_range)
|
164
|
+
|
165
|
+
if self.machines_per_operation[1] > 1:
|
166
|
+
machines = self._choose_multiple_machines()
|
167
|
+
return Operation(machines=machines, duration=duration)
|
168
|
+
|
169
|
+
machine_id = self._choose_one_machine(available_machines)
|
170
|
+
return Operation(machines=machine_id, duration=duration)
|
171
|
+
|
172
|
+
def _choose_multiple_machines(self) -> list[int]:
|
173
|
+
num_machines = random.randint(*self.machines_per_operation)
|
174
|
+
available_machines = list(range(num_machines))
|
175
|
+
machines = []
|
176
|
+
for _ in range(num_machines):
|
177
|
+
machine = random.choice(available_machines)
|
178
|
+
machines.append(machine)
|
179
|
+
available_machines.remove(machine)
|
180
|
+
return machines
|
181
|
+
|
182
|
+
def _choose_one_machine(
|
183
|
+
self, available_machines: list[int] | None = None
|
184
|
+
) -> int:
|
185
|
+
if available_machines is None:
|
186
|
+
_, max_num_machines = self.num_machines_range
|
187
|
+
available_machines = list(range(max_num_machines))
|
188
|
+
|
189
|
+
machine_id = random.choice(available_machines)
|
190
|
+
if not self.allow_recirculation:
|
191
|
+
available_machines.remove(machine_id)
|
192
|
+
|
193
|
+
return machine_id
|
194
|
+
|
195
|
+
def _get_name(self) -> str:
|
196
|
+
self._counter += 1
|
197
|
+
return f"{self.name_suffix}_{self._counter}"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"""Package for graph related classes and functions."""
|
2
|
+
|
3
|
+
from job_shop_lib.graphs.constants import EdgeType, NodeType
|
4
|
+
from job_shop_lib.graphs.node import Node
|
5
|
+
from job_shop_lib.graphs.job_shop_graph import JobShopGraph, NODE_ATTR
|
6
|
+
from job_shop_lib.graphs.build_disjunctive_graph import (
|
7
|
+
build_disjunctive_graph,
|
8
|
+
add_disjunctive_edges,
|
9
|
+
add_conjunctive_edges,
|
10
|
+
add_source_sink_nodes,
|
11
|
+
add_source_sink_edges,
|
12
|
+
)
|
13
|
+
from job_shop_lib.graphs.build_agent_task_graph import (
|
14
|
+
build_agent_task_graph,
|
15
|
+
build_complete_agent_task_graph,
|
16
|
+
build_agent_task_graph_with_jobs,
|
17
|
+
add_same_job_operations_edges,
|
18
|
+
add_machine_nodes,
|
19
|
+
add_operation_machine_edges,
|
20
|
+
add_machine_machine_edges,
|
21
|
+
add_job_nodes,
|
22
|
+
add_operation_job_edges,
|
23
|
+
add_global_node,
|
24
|
+
add_machine_global_edges,
|
25
|
+
add_job_global_edges,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
__all__ = [
|
30
|
+
"EdgeType",
|
31
|
+
"NodeType",
|
32
|
+
"Node",
|
33
|
+
"JobShopGraph",
|
34
|
+
"NODE_ATTR",
|
35
|
+
"build_disjunctive_graph",
|
36
|
+
"add_disjunctive_edges",
|
37
|
+
"add_conjunctive_edges",
|
38
|
+
"add_source_sink_nodes",
|
39
|
+
"add_source_sink_edges",
|
40
|
+
"build_agent_task_graph",
|
41
|
+
"build_complete_agent_task_graph",
|
42
|
+
"build_agent_task_graph_with_jobs",
|
43
|
+
"add_same_job_operations_edges",
|
44
|
+
"add_machine_nodes",
|
45
|
+
"add_operation_machine_edges",
|
46
|
+
"add_machine_machine_edges",
|
47
|
+
"add_job_nodes",
|
48
|
+
"add_operation_job_edges",
|
49
|
+
"add_global_node",
|
50
|
+
"add_machine_global_edges",
|
51
|
+
"add_job_global_edges",
|
52
|
+
]
|
@@ -0,0 +1,209 @@
|
|
1
|
+
"""Contains helper functions to build the agent-task graph or one of
|
2
|
+
its generalizations from a job shop instance.
|
3
|
+
|
4
|
+
The agent-task graph 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
|
+
We also support a generalization of this approach by the addition of job nodes
|
11
|
+
and a global node. Job nodes are connected to all operation nodes of the same
|
12
|
+
job, and the global node is connected to all machine and job nodes.
|
13
|
+
|
14
|
+
References:
|
15
|
+
- Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
|
16
|
+
solve multi-agent scheduling problems with reinforcement learning. ArXiv,
|
17
|
+
abs/2106.03051, 2021.
|
18
|
+
"""
|
19
|
+
|
20
|
+
import itertools
|
21
|
+
|
22
|
+
from job_shop_lib import JobShopInstance
|
23
|
+
from job_shop_lib.graphs import JobShopGraph, NodeType, Node
|
24
|
+
|
25
|
+
|
26
|
+
def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
27
|
+
"""Builds the agent-task graph of the instance with job and global nodes.
|
28
|
+
|
29
|
+
The complete agent-task graph is a generalization of the agent-task graph
|
30
|
+
that includes job nodes and a global node.
|
31
|
+
|
32
|
+
Job nodes are connected to all operation nodes of the same job, and the
|
33
|
+
global node is connected to all machine and job nodes.
|
34
|
+
|
35
|
+
This representation does not include edges between job or machine nodes
|
36
|
+
with the same type because they are connected indirectly by the global
|
37
|
+
node.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
instance:
|
41
|
+
The job shop instance in which the agent-task graph will be built.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
The complete agent-task graph of the instance.
|
45
|
+
"""
|
46
|
+
graph = JobShopGraph(instance)
|
47
|
+
|
48
|
+
add_machine_nodes(graph)
|
49
|
+
add_operation_machine_edges(graph)
|
50
|
+
|
51
|
+
add_job_nodes(graph)
|
52
|
+
add_operation_job_edges(graph)
|
53
|
+
|
54
|
+
add_global_node(graph)
|
55
|
+
add_machine_global_edges(graph)
|
56
|
+
add_job_global_edges(graph)
|
57
|
+
|
58
|
+
return graph
|
59
|
+
|
60
|
+
|
61
|
+
def build_agent_task_graph_with_jobs(
|
62
|
+
instance: JobShopInstance,
|
63
|
+
) -> JobShopGraph:
|
64
|
+
"""Builds the agent-task graph of the instance with job nodes.
|
65
|
+
|
66
|
+
The agent-task graph with job nodes is a generalization of the agent-task
|
67
|
+
graph that includes job nodes.
|
68
|
+
|
69
|
+
Job nodes are connected to all operation nodes of the same job, and their
|
70
|
+
are connected between them.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
instance:
|
74
|
+
The job shop instance in which the agent-task graph will be built.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
The agent-task graph of the instance with job nodes.
|
78
|
+
"""
|
79
|
+
graph = JobShopGraph(instance)
|
80
|
+
|
81
|
+
add_machine_nodes(graph)
|
82
|
+
add_operation_machine_edges(graph)
|
83
|
+
add_machine_machine_edges(graph)
|
84
|
+
|
85
|
+
add_job_nodes(graph)
|
86
|
+
add_operation_job_edges(graph)
|
87
|
+
add_job_job_edges(graph)
|
88
|
+
|
89
|
+
return graph
|
90
|
+
|
91
|
+
|
92
|
+
def build_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
93
|
+
"""Builds the agent-task graph of the instance.
|
94
|
+
|
95
|
+
The agent-task graph was introduced by Junyoung Park et al. (2021).
|
96
|
+
|
97
|
+
In contrast to the disjunctive graph, instead of connecting operations
|
98
|
+
that share the same resources directly by disjunctive edges, operation
|
99
|
+
nodes are connected with machine ones.
|
100
|
+
|
101
|
+
All machine nodes are connected between them, and all operation nodes
|
102
|
+
from the same job are connected by non-directed edges too.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
instance:
|
106
|
+
The job shop instance in which the agent-task graph will be built.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
The agent-task graph of the instance.
|
110
|
+
"""
|
111
|
+
graph = JobShopGraph(instance)
|
112
|
+
|
113
|
+
add_machine_nodes(graph)
|
114
|
+
add_operation_machine_edges(graph)
|
115
|
+
add_machine_machine_edges(graph)
|
116
|
+
|
117
|
+
add_same_job_operations_edges(graph)
|
118
|
+
|
119
|
+
return graph
|
120
|
+
|
121
|
+
|
122
|
+
# BUILDING BLOCKS
|
123
|
+
# -----------------------------------------------------------------------------
|
124
|
+
def add_same_job_operations_edges(graph: JobShopGraph) -> None:
|
125
|
+
"""Adds edges between all operations of the same job."""
|
126
|
+
for job in graph.nodes_by_job:
|
127
|
+
for operation1, operation2 in itertools.combinations(job, 2):
|
128
|
+
graph.add_edge(operation1, operation2)
|
129
|
+
graph.add_edge(operation2, operation1)
|
130
|
+
|
131
|
+
|
132
|
+
# MACHINE NODES
|
133
|
+
# -------------
|
134
|
+
def add_machine_nodes(graph: JobShopGraph) -> None:
|
135
|
+
"""Adds a machine node for each machine in the instance."""
|
136
|
+
for machine_id in range(graph.instance.num_machines):
|
137
|
+
machine_node = Node(node_type=NodeType.MACHINE, machine_id=machine_id)
|
138
|
+
graph.add_node(machine_node)
|
139
|
+
|
140
|
+
|
141
|
+
def add_operation_machine_edges(graph: JobShopGraph) -> None:
|
142
|
+
"""Adds edges between operation and machine nodes."""
|
143
|
+
for machine_node in graph.nodes_by_type[NodeType.MACHINE]:
|
144
|
+
operation_nodes_in_machine = graph.nodes_by_machine[
|
145
|
+
machine_node.machine_id
|
146
|
+
]
|
147
|
+
for operation_node in operation_nodes_in_machine:
|
148
|
+
graph.add_edge(machine_node, operation_node)
|
149
|
+
graph.add_edge(operation_node, machine_node)
|
150
|
+
|
151
|
+
|
152
|
+
def add_machine_machine_edges(graph: JobShopGraph) -> None:
|
153
|
+
"""Adds edges between all machine nodes."""
|
154
|
+
for machine1, machine2 in itertools.combinations(
|
155
|
+
graph.nodes_by_type[NodeType.MACHINE], 2
|
156
|
+
):
|
157
|
+
graph.add_edge(machine1, machine2)
|
158
|
+
graph.add_edge(machine2, machine1)
|
159
|
+
|
160
|
+
|
161
|
+
# JOB NODES
|
162
|
+
# ---------
|
163
|
+
def add_job_nodes(graph: JobShopGraph) -> None:
|
164
|
+
"""Adds a job node for each job in the instance."""
|
165
|
+
for job_id in range(graph.instance.num_jobs):
|
166
|
+
job_node = Node(node_type=NodeType.JOB, job_id=job_id)
|
167
|
+
graph.add_node(job_node)
|
168
|
+
|
169
|
+
|
170
|
+
def add_operation_job_edges(graph: JobShopGraph) -> None:
|
171
|
+
"""Adds edges between operation and job nodes."""
|
172
|
+
for job_node in graph.nodes_by_type[NodeType.JOB]:
|
173
|
+
operation_nodes_in_job = graph.nodes_by_job[job_node.job_id]
|
174
|
+
for operation_node in operation_nodes_in_job:
|
175
|
+
graph.add_edge(job_node, operation_node)
|
176
|
+
graph.add_edge(operation_node, job_node)
|
177
|
+
|
178
|
+
|
179
|
+
def add_job_job_edges(graph: JobShopGraph) -> None:
|
180
|
+
"""Adds edges between all job nodes."""
|
181
|
+
for job1, job2 in itertools.combinations(
|
182
|
+
graph.nodes_by_type[NodeType.JOB], 2
|
183
|
+
):
|
184
|
+
graph.add_edge(job1, job2)
|
185
|
+
graph.add_edge(job2, job1)
|
186
|
+
|
187
|
+
|
188
|
+
# GLOBAL NODE
|
189
|
+
# -----------
|
190
|
+
def add_global_node(graph: JobShopGraph) -> None:
|
191
|
+
"""Adds a global node to the graph."""
|
192
|
+
global_node = Node(node_type=NodeType.GLOBAL)
|
193
|
+
graph.add_node(global_node)
|
194
|
+
|
195
|
+
|
196
|
+
def add_machine_global_edges(graph: JobShopGraph) -> None:
|
197
|
+
"""Adds edges between machine and global nodes."""
|
198
|
+
global_node = graph.nodes_by_type[NodeType.GLOBAL][0]
|
199
|
+
for machine_node in graph.nodes_by_type[NodeType.MACHINE]:
|
200
|
+
graph.add_edge(global_node, machine_node)
|
201
|
+
graph.add_edge(machine_node, global_node)
|
202
|
+
|
203
|
+
|
204
|
+
def add_job_global_edges(graph: JobShopGraph) -> None:
|
205
|
+
"""Adds edges between job and global nodes."""
|
206
|
+
global_node = graph.nodes_by_type[NodeType.GLOBAL][0]
|
207
|
+
for job_node in graph.nodes_by_type[NodeType.JOB]:
|
208
|
+
graph.add_edge(global_node, job_node)
|
209
|
+
graph.add_edge(job_node, global_node)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"""Module for building the disjunctive graph of a job shop instance.
|
2
|
+
|
3
|
+
The disjunctive graph is created by first adding nodes representing each
|
4
|
+
operation in the jobs, along with two special nodes: a source $S$ and a sink
|
5
|
+
$T$. Each operation node is linked to the next operation in its job sequence
|
6
|
+
by **conjunctive edges**, forming a path from the source to the sink. These
|
7
|
+
edges represent the order in which operations of a single job must be
|
8
|
+
performed.
|
9
|
+
|
10
|
+
Additionally, the graph includes **disjunctive edges** between operations
|
11
|
+
that use the same machine but belong to different jobs. These edges are
|
12
|
+
bidirectional, indicating that either of the connected operations can be
|
13
|
+
performed first. The disjunctive edges thus represent the scheduling choices
|
14
|
+
available: the order in which operations sharing a machine can be processed.
|
15
|
+
Solving the Job Shop Scheduling problem involves choosing a direction for
|
16
|
+
each disjunctive edge such that the overall processing time is minimized.
|
17
|
+
"""
|
18
|
+
|
19
|
+
import itertools
|
20
|
+
|
21
|
+
from job_shop_lib import JobShopInstance
|
22
|
+
from job_shop_lib.graphs import JobShopGraph, EdgeType, NodeType, Node
|
23
|
+
|
24
|
+
|
25
|
+
def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
|
26
|
+
graph = JobShopGraph(instance)
|
27
|
+
add_disjunctive_edges(graph)
|
28
|
+
add_conjunctive_edges(graph)
|
29
|
+
add_source_sink_nodes(graph)
|
30
|
+
add_source_sink_edges(graph)
|
31
|
+
return graph
|
32
|
+
|
33
|
+
|
34
|
+
def add_disjunctive_edges(graph: JobShopGraph) -> None:
|
35
|
+
"""Adds disjunctive edges to the graph."""
|
36
|
+
|
37
|
+
for machine in graph.nodes_by_machine:
|
38
|
+
for node1, node2 in itertools.combinations(machine, 2):
|
39
|
+
graph.add_edge(
|
40
|
+
node1,
|
41
|
+
node2,
|
42
|
+
type=EdgeType.DISJUNCTIVE,
|
43
|
+
)
|
44
|
+
graph.add_edge(
|
45
|
+
node2,
|
46
|
+
node1,
|
47
|
+
type=EdgeType.DISJUNCTIVE,
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
def add_conjunctive_edges(graph: JobShopGraph) -> None:
|
52
|
+
"""Adds conjunctive edges to the graph."""
|
53
|
+
|
54
|
+
for job_operations in graph.nodes_by_job:
|
55
|
+
for i in range(1, len(job_operations)):
|
56
|
+
graph.add_edge(
|
57
|
+
job_operations[i - 1],
|
58
|
+
job_operations[i],
|
59
|
+
type=EdgeType.CONJUNCTIVE,
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
def add_source_sink_nodes(graph: JobShopGraph) -> None:
|
64
|
+
"""Adds source and sink nodes to the graph."""
|
65
|
+
source = Node(node_type=NodeType.SOURCE)
|
66
|
+
sink = Node(node_type=NodeType.SINK)
|
67
|
+
graph.add_node(source)
|
68
|
+
graph.add_node(sink)
|
69
|
+
|
70
|
+
|
71
|
+
def add_source_sink_edges(graph: JobShopGraph) -> None:
|
72
|
+
"""Adds edges between source and sink nodes and operations."""
|
73
|
+
source = graph.nodes_by_type[NodeType.SOURCE][0]
|
74
|
+
sink = graph.nodes_by_type[NodeType.SINK][0]
|
75
|
+
|
76
|
+
for job_operations in graph.nodes_by_job:
|
77
|
+
graph.add_edge(source, job_operations[0], type=EdgeType.CONJUNCTIVE)
|
78
|
+
graph.add_edge(job_operations[-1], sink, type=EdgeType.CONJUNCTIVE)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""Constants for the graph module."""
|
2
|
+
|
3
|
+
import enum
|
4
|
+
|
5
|
+
|
6
|
+
class EdgeType(enum.Enum):
|
7
|
+
"""Enumeration of edge types."""
|
8
|
+
|
9
|
+
CONJUNCTIVE = 0
|
10
|
+
DISJUNCTIVE = 1
|
11
|
+
|
12
|
+
|
13
|
+
class NodeType(enum.Enum):
|
14
|
+
"""Enumeration of node types."""
|
15
|
+
|
16
|
+
OPERATION = enum.auto()
|
17
|
+
MACHINE = enum.auto()
|
18
|
+
JOB = enum.auto()
|
19
|
+
GLOBAL = enum.auto()
|
20
|
+
SOURCE = enum.auto()
|
21
|
+
SINK = enum.auto()
|