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.
Files changed (35) hide show
  1. job_shop_lib/__init__.py +20 -0
  2. job_shop_lib/base_solver.py +37 -0
  3. job_shop_lib/benchmarking/__init__.py +78 -0
  4. job_shop_lib/benchmarking/benchmark_instances.json +1 -0
  5. job_shop_lib/benchmarking/load_benchmark.py +142 -0
  6. job_shop_lib/cp_sat/__init__.py +5 -0
  7. job_shop_lib/cp_sat/ortools_solver.py +201 -0
  8. job_shop_lib/dispatching/__init__.py +49 -0
  9. job_shop_lib/dispatching/dispatcher.py +269 -0
  10. job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
  11. job_shop_lib/dispatching/dispatching_rules.py +160 -0
  12. job_shop_lib/dispatching/factories.py +206 -0
  13. job_shop_lib/dispatching/pruning_functions.py +116 -0
  14. job_shop_lib/exceptions.py +26 -0
  15. job_shop_lib/generators/__init__.py +7 -0
  16. job_shop_lib/generators/basic_generator.py +197 -0
  17. job_shop_lib/graphs/__init__.py +52 -0
  18. job_shop_lib/graphs/build_agent_task_graph.py +209 -0
  19. job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
  20. job_shop_lib/graphs/constants.py +21 -0
  21. job_shop_lib/graphs/job_shop_graph.py +159 -0
  22. job_shop_lib/graphs/node.py +147 -0
  23. job_shop_lib/job_shop_instance.py +355 -0
  24. job_shop_lib/operation.py +120 -0
  25. job_shop_lib/schedule.py +180 -0
  26. job_shop_lib/scheduled_operation.py +97 -0
  27. job_shop_lib/visualization/__init__.py +25 -0
  28. job_shop_lib/visualization/agent_task_graph.py +257 -0
  29. job_shop_lib/visualization/create_gif.py +191 -0
  30. job_shop_lib/visualization/disjunctive_graph.py +206 -0
  31. job_shop_lib/visualization/gantt_chart.py +147 -0
  32. job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
  33. job_shop_lib-0.1.0.dist-info/METADATA +363 -0
  34. job_shop_lib-0.1.0.dist-info/RECORD +35 -0
  35. 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()