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,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()
|