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,159 @@
|
|
1
|
+
"""Home of the `JobShopGraph` class."""
|
2
|
+
|
3
|
+
import collections
|
4
|
+
import networkx as nx
|
5
|
+
|
6
|
+
from job_shop_lib import JobShopInstance, Operation
|
7
|
+
from job_shop_lib.graphs import Node, NodeType
|
8
|
+
|
9
|
+
|
10
|
+
NODE_ATTR = "node"
|
11
|
+
|
12
|
+
|
13
|
+
class JobShopGraph:
|
14
|
+
"""Data structure to represent a `JobShopInstance` as a graph.
|
15
|
+
|
16
|
+
Provides a comprehensive graph-based representation of a job shop
|
17
|
+
scheduling problem, utilizing the `networkx` library to model the complex
|
18
|
+
relationships between jobs, operations, and machines. This class transforms
|
19
|
+
the abstract scheduling problem into a directed graph, where various
|
20
|
+
entities (jobs, machines, and operations) are nodes, and the dependencies
|
21
|
+
(such as operation order within a job or machine assignment) are edges.
|
22
|
+
|
23
|
+
This transformation allows for the application of graph algorithms
|
24
|
+
to analyze and solve scheduling problems.
|
25
|
+
|
26
|
+
Attributes:
|
27
|
+
instance:
|
28
|
+
The job shop instance encapsulated by this graph.
|
29
|
+
graph:
|
30
|
+
The directed graph representing the job shop, where nodes are
|
31
|
+
operations, machines, jobs, or abstract concepts like global,
|
32
|
+
source, and sink, with edges indicating dependencies.
|
33
|
+
nodes:
|
34
|
+
A list of all nodes, encapsulated by the `Node` class, in the
|
35
|
+
graph.
|
36
|
+
nodes_by_type:
|
37
|
+
A dictionary categorizing nodes by their `NodeType`,
|
38
|
+
facilitating access to nodes of a particular type.
|
39
|
+
nodes_by_machine:
|
40
|
+
A nested list mapping each machine to its associated
|
41
|
+
operation nodes, aiding in machine-specific analysis.
|
42
|
+
nodes_by_job:
|
43
|
+
Similar to `nodes_by_machine`, but maps jobs to their
|
44
|
+
operation nodes, useful for job-specific traversal.
|
45
|
+
"""
|
46
|
+
|
47
|
+
__slots__ = (
|
48
|
+
"instance",
|
49
|
+
"graph",
|
50
|
+
"nodes",
|
51
|
+
"nodes_by_type",
|
52
|
+
"nodes_by_machine",
|
53
|
+
"nodes_by_job",
|
54
|
+
"_next_node_id",
|
55
|
+
)
|
56
|
+
|
57
|
+
def __init__(self, instance: JobShopInstance):
|
58
|
+
"""Initializes the graph with the given instance.
|
59
|
+
|
60
|
+
Nodes of type `OPERATION` are added to the graph based on the
|
61
|
+
operations of the instance.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
instance:
|
65
|
+
The job shop instance that the graph represents.
|
66
|
+
"""
|
67
|
+
self.graph = nx.DiGraph()
|
68
|
+
self.instance = instance
|
69
|
+
|
70
|
+
self.nodes: list[Node] = []
|
71
|
+
|
72
|
+
self.nodes_by_type: dict[NodeType, list[Node]] = (
|
73
|
+
collections.defaultdict(list)
|
74
|
+
)
|
75
|
+
|
76
|
+
self.nodes_by_machine: list[list[Node]] = [
|
77
|
+
[] for _ in range(instance.num_machines)
|
78
|
+
]
|
79
|
+
|
80
|
+
self.nodes_by_job: list[list[Node]] = [
|
81
|
+
[] for _ in range(instance.num_jobs)
|
82
|
+
]
|
83
|
+
|
84
|
+
self._next_node_id = 0
|
85
|
+
|
86
|
+
self._add_operation_nodes()
|
87
|
+
|
88
|
+
def _add_operation_nodes(self) -> None:
|
89
|
+
"""Adds operation nodes to the graph."""
|
90
|
+
for job in self.instance.jobs:
|
91
|
+
for operation in job:
|
92
|
+
node = Node(node_type=NodeType.OPERATION, operation=operation)
|
93
|
+
self.add_node(node)
|
94
|
+
|
95
|
+
def get_operation_from_id(self, operation_id: int) -> Operation:
|
96
|
+
"""Returns the operation with the given id."""
|
97
|
+
return self.nodes[operation_id].operation
|
98
|
+
|
99
|
+
def add_node(self, node_for_adding: Node) -> None:
|
100
|
+
"""Adds a node to the graph and updates relevant class attributes.
|
101
|
+
|
102
|
+
This method assigns a unique identifier to the node, adds it to the
|
103
|
+
graph, and updates the nodes list and the nodes_by_type dictionary. If
|
104
|
+
the node is of type `OPERATION`, it also updates `nodes_by_job` and
|
105
|
+
`nodes_by_machine` based on the operation's job_id and machine_ids.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
node_for_adding (Node): The node to be added to the graph.
|
109
|
+
|
110
|
+
Raises:
|
111
|
+
ValueError: If the node type is unsupported or if required
|
112
|
+
attributes for the node type are missing.
|
113
|
+
|
114
|
+
Note:
|
115
|
+
This method directly modifies the graph attribute as well as
|
116
|
+
several other class attributes. Thus, adding nodes to the graph
|
117
|
+
should be done exclusively through this method to avoid
|
118
|
+
inconsistencies.
|
119
|
+
"""
|
120
|
+
node_for_adding.node_id = self._next_node_id
|
121
|
+
self.graph.add_node(
|
122
|
+
node_for_adding.node_id, **{NODE_ATTR: node_for_adding}
|
123
|
+
)
|
124
|
+
self.nodes_by_type[node_for_adding.node_type].append(node_for_adding)
|
125
|
+
self.nodes.append(node_for_adding)
|
126
|
+
self._next_node_id += 1
|
127
|
+
|
128
|
+
if node_for_adding.node_type == NodeType.OPERATION:
|
129
|
+
operation = node_for_adding.operation
|
130
|
+
self.nodes_by_job[operation.job_id].append(node_for_adding)
|
131
|
+
for machine_id in operation.machines:
|
132
|
+
self.nodes_by_machine[machine_id].append(node_for_adding)
|
133
|
+
|
134
|
+
def add_edge(
|
135
|
+
self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
|
136
|
+
) -> None:
|
137
|
+
"""Adds an edge to the graph.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
u_of_edge: The source node of the edge. If it is a `Node`, its
|
141
|
+
`node_id` is used as the source. Otherwise, it is assumed to be
|
142
|
+
the node_id of the source.
|
143
|
+
v_of_edge: The destination node of the edge. If it is a `Node`, its
|
144
|
+
`node_id` is used as the destination. Otherwise, it is assumed
|
145
|
+
to be the node_id of the destination.
|
146
|
+
**attr: Additional attributes to be added to the edge.
|
147
|
+
|
148
|
+
Raises:
|
149
|
+
ValueError: If `u_of_edge` or `v_of_edge` are not in the graph.
|
150
|
+
"""
|
151
|
+
if isinstance(u_of_edge, Node):
|
152
|
+
u_of_edge = u_of_edge.node_id
|
153
|
+
if isinstance(v_of_edge, Node):
|
154
|
+
v_of_edge = v_of_edge.node_id
|
155
|
+
if u_of_edge not in self.graph or v_of_edge not in self.graph:
|
156
|
+
raise ValueError(
|
157
|
+
"`u_of_edge` and `v_of_edge` must be in the graph."
|
158
|
+
)
|
159
|
+
self.graph.add_edge(u_of_edge, v_of_edge, **attr)
|
@@ -0,0 +1,147 @@
|
|
1
|
+
"""Home of the `Node` class."""
|
2
|
+
|
3
|
+
from job_shop_lib import Operation
|
4
|
+
from job_shop_lib.graphs.constants import NodeType
|
5
|
+
|
6
|
+
|
7
|
+
class Node:
|
8
|
+
"""Data structure to represent a node in the `JobShopGraph`.
|
9
|
+
|
10
|
+
A node is hashable by its id. The id is assigned when the node is added to
|
11
|
+
the graph. The id must be unique for each node in the graph, and should be
|
12
|
+
used to identify the node in the networkx graph.
|
13
|
+
|
14
|
+
Depending on the type of the node, it can have different attributes. The
|
15
|
+
following table shows the attributes of each type of node:
|
16
|
+
|
17
|
+
Node Type | Required Attribute
|
18
|
+
----------------|---------------------
|
19
|
+
OPERATION | `operation`
|
20
|
+
MACHINE | `machine_id`
|
21
|
+
JOB | `job_id`
|
22
|
+
|
23
|
+
In terms of equality, two nodes are equal if they have the same id.
|
24
|
+
Additionally, one node is equal to an integer if the integer is equal to
|
25
|
+
its id. It is also hashable by its id.
|
26
|
+
|
27
|
+
This allows for using the node as a key in a dictionary, at the same time
|
28
|
+
we can use its id to index that dictionary. Example:
|
29
|
+
|
30
|
+
```python
|
31
|
+
node = Node(NodeType.SOURCE)
|
32
|
+
node.node_id = 1
|
33
|
+
graph = {node: "some value"}
|
34
|
+
print(graph[node]) # "some value"
|
35
|
+
print(graph[1]) # "some value"
|
36
|
+
```
|
37
|
+
|
38
|
+
Args:
|
39
|
+
node_type:
|
40
|
+
The type of the node. It can be one of the following:
|
41
|
+
- NodeType.OPERATION
|
42
|
+
- NodeType.MACHINE
|
43
|
+
- NodeType.JOB
|
44
|
+
- NodeType.GLOBAL
|
45
|
+
...
|
46
|
+
operation:
|
47
|
+
The operation of the node. It should be provided if the node_type
|
48
|
+
is NodeType.OPERATION.
|
49
|
+
machine_id:
|
50
|
+
The id of the machine of the node. It should be provided if the
|
51
|
+
node_type is NodeType.MACHINE.
|
52
|
+
job_id:
|
53
|
+
The id of the job of the node. It should be provided if the
|
54
|
+
node_type is NodeType.JOB.
|
55
|
+
"""
|
56
|
+
|
57
|
+
__slots__ = "node_type", "_node_id", "_operation", "_machine_id", "_job_id"
|
58
|
+
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
node_type: NodeType,
|
62
|
+
operation: Operation | None = None,
|
63
|
+
machine_id: int | None = None,
|
64
|
+
job_id: int | None = None,
|
65
|
+
):
|
66
|
+
if node_type == NodeType.OPERATION and operation is None:
|
67
|
+
raise ValueError("Operation node must have an operation.")
|
68
|
+
|
69
|
+
if node_type == NodeType.MACHINE and machine_id is None:
|
70
|
+
raise ValueError("Machine node must have a machine_id.")
|
71
|
+
|
72
|
+
if node_type == NodeType.JOB and job_id is None:
|
73
|
+
raise ValueError("Job node must have a job_id.")
|
74
|
+
|
75
|
+
self.node_type = node_type
|
76
|
+
self._node_id: int | None = None
|
77
|
+
|
78
|
+
self._operation = operation
|
79
|
+
self._machine_id = machine_id
|
80
|
+
self._job_id = job_id
|
81
|
+
|
82
|
+
@property
|
83
|
+
def node_id(self) -> int:
|
84
|
+
"""Returns a unique identifier for the node."""
|
85
|
+
if self._node_id is None:
|
86
|
+
raise ValueError("Node has not been assigned an id.")
|
87
|
+
return self._node_id
|
88
|
+
|
89
|
+
@node_id.setter
|
90
|
+
def node_id(self, value: int) -> None:
|
91
|
+
self._node_id = value
|
92
|
+
|
93
|
+
@property
|
94
|
+
def operation(self) -> Operation:
|
95
|
+
"""Returns the operation of the node.
|
96
|
+
|
97
|
+
This property is mandatory for nodes of type `OPERATION`.
|
98
|
+
"""
|
99
|
+
if self._operation is None:
|
100
|
+
raise ValueError("Node has no operation.")
|
101
|
+
return self._operation
|
102
|
+
|
103
|
+
@property
|
104
|
+
def machine_id(self) -> int:
|
105
|
+
"""Returns the `machine_id` of the node.
|
106
|
+
|
107
|
+
This property is mandatory for nodes of type `MACHINE`.
|
108
|
+
"""
|
109
|
+
if self._machine_id is None:
|
110
|
+
raise ValueError("Node has no `machine_id`.")
|
111
|
+
return self._machine_id
|
112
|
+
|
113
|
+
@property
|
114
|
+
def job_id(self) -> int:
|
115
|
+
"""Returns the `job_id` of the node.
|
116
|
+
|
117
|
+
This property is mandatory for nodes of type `JOB`.
|
118
|
+
"""
|
119
|
+
if self._job_id is None:
|
120
|
+
raise ValueError("Node has no `job_id`.")
|
121
|
+
return self._job_id
|
122
|
+
|
123
|
+
def __hash__(self) -> int:
|
124
|
+
return self.node_id
|
125
|
+
|
126
|
+
def __eq__(self, __value: object) -> bool:
|
127
|
+
if isinstance(__value, Node):
|
128
|
+
__value = __value.node_id
|
129
|
+
return self.node_id == __value
|
130
|
+
|
131
|
+
def __repr__(self) -> str:
|
132
|
+
if self.node_type == NodeType.OPERATION:
|
133
|
+
return (
|
134
|
+
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
135
|
+
f"operation={self.operation})"
|
136
|
+
)
|
137
|
+
if self.node_type == NodeType.MACHINE:
|
138
|
+
return (
|
139
|
+
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
140
|
+
f"machine_id={self._machine_id})"
|
141
|
+
)
|
142
|
+
if self.node_type == NodeType.JOB:
|
143
|
+
return (
|
144
|
+
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
145
|
+
f"job_id={self._job_id})"
|
146
|
+
)
|
147
|
+
return f"Node(node_type={self.node_type.name}, id={self._node_id})"
|
@@ -0,0 +1,355 @@
|
|
1
|
+
"""Contains the `JobShopInstance` class."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import os
|
6
|
+
import functools
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from job_shop_lib import Operation
|
10
|
+
|
11
|
+
|
12
|
+
class JobShopInstance:
|
13
|
+
"""Data structure to store a Job Shop Scheduling Problem instance.
|
14
|
+
|
15
|
+
Additional attributes such as `num_jobs` or `num_machines` can be computed
|
16
|
+
from the instance and are cached for performance if they require expensive
|
17
|
+
computations.
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
jobs:
|
21
|
+
A list of lists of operations. Each list of operations represents
|
22
|
+
a job, and the operations are ordered by their position in the job.
|
23
|
+
The `job_id`, `position_in_job`, and `operation_id` attributes of
|
24
|
+
the operations are set when the instance is created.
|
25
|
+
name:
|
26
|
+
A string with the name of the instance.
|
27
|
+
metadata:
|
28
|
+
A dictionary with additional information about the instance.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
jobs: list[list[Operation]],
|
34
|
+
name: str = "JobShopInstance",
|
35
|
+
**metadata: Any,
|
36
|
+
):
|
37
|
+
"""Initializes the instance based on a list of lists of operations.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
jobs:
|
41
|
+
A list of lists of operations. Each list of operations
|
42
|
+
represents a job, and the operations are ordered by their
|
43
|
+
position in the job. The `job_id`, `position_in_job`, and
|
44
|
+
`operation_id` attributes of the operations are set when the
|
45
|
+
instance is created.
|
46
|
+
name:
|
47
|
+
A string with the name of the instance.
|
48
|
+
**metadata:
|
49
|
+
Additional information about the instance.
|
50
|
+
"""
|
51
|
+
self.jobs = jobs
|
52
|
+
self.set_operation_attributes()
|
53
|
+
self.name = name
|
54
|
+
self.metadata = metadata
|
55
|
+
|
56
|
+
def set_operation_attributes(self):
|
57
|
+
"""Sets the job_id and position of each operation."""
|
58
|
+
operation_id = 0
|
59
|
+
for job_id, job in enumerate(self.jobs):
|
60
|
+
for position, operation in enumerate(job):
|
61
|
+
operation.job_id = job_id
|
62
|
+
operation.position_in_job = position
|
63
|
+
operation.operation_id = operation_id
|
64
|
+
operation_id += 1
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def from_taillard_file(
|
68
|
+
cls,
|
69
|
+
file_path: os.PathLike | str | bytes,
|
70
|
+
encoding: str = "utf-8",
|
71
|
+
comment_symbol: str = "#",
|
72
|
+
name: str | None = None,
|
73
|
+
**metadata: Any,
|
74
|
+
) -> JobShopInstance:
|
75
|
+
"""Creates a JobShopInstance from a file following Taillard's format.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
file_path:
|
79
|
+
A path-like object or string representing the path to the file.
|
80
|
+
encoding:
|
81
|
+
The encoding of the file.
|
82
|
+
comment_symbol:
|
83
|
+
A string representing the comment symbol used in the file.
|
84
|
+
Lines starting with this symbol are ignored.
|
85
|
+
name:
|
86
|
+
A string with the name of the instance. If not provided, the
|
87
|
+
name of the instance is set to the name of the file.
|
88
|
+
**metadata:
|
89
|
+
Additional information about the instance.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
A JobShopInstance object with the operations read from the file,
|
93
|
+
and the name and metadata provided.
|
94
|
+
"""
|
95
|
+
with open(file_path, "r", encoding=encoding) as file:
|
96
|
+
lines = file.readlines()
|
97
|
+
|
98
|
+
first_non_comment_line_reached = False
|
99
|
+
jobs = []
|
100
|
+
for line in lines:
|
101
|
+
line = line.strip()
|
102
|
+
if line.startswith(comment_symbol):
|
103
|
+
continue
|
104
|
+
if not first_non_comment_line_reached:
|
105
|
+
first_non_comment_line_reached = True
|
106
|
+
continue
|
107
|
+
|
108
|
+
row = list(map(int, line.split()))
|
109
|
+
pairs = zip(row[::2], row[1::2])
|
110
|
+
operations = [
|
111
|
+
Operation(machines=machine_id, duration=duration)
|
112
|
+
for machine_id, duration in pairs
|
113
|
+
]
|
114
|
+
jobs.append(operations)
|
115
|
+
|
116
|
+
if name is None:
|
117
|
+
name = os.path.basename(str(file_path))
|
118
|
+
if "." in name:
|
119
|
+
name = name.split(".")[0]
|
120
|
+
return cls(jobs=jobs, name=name, **metadata)
|
121
|
+
|
122
|
+
def to_dict(self) -> dict[str, Any]:
|
123
|
+
"""Returns a dictionary representation of the instance.
|
124
|
+
|
125
|
+
This representation is useful for saving the instance to a JSON file,
|
126
|
+
which is a more computer-friendly format than more traditional ones
|
127
|
+
like Taillard's.
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
The returned dictionary has the following structure:
|
131
|
+
{
|
132
|
+
"name": self.name,
|
133
|
+
"duration_matrix": self.durations_matrix,
|
134
|
+
"machines_matrix": self.machines_matrix,
|
135
|
+
"metadata": self.metadata,
|
136
|
+
}
|
137
|
+
"""
|
138
|
+
return {
|
139
|
+
"name": self.name,
|
140
|
+
"duration_matrix": self.durations_matrix,
|
141
|
+
"machines_matrix": self.machines_matrix,
|
142
|
+
"metadata": self.metadata,
|
143
|
+
}
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
def from_matrices(
|
147
|
+
cls,
|
148
|
+
duration_matrix: list[list[int]],
|
149
|
+
machines_matrix: list[list[list[int]]] | list[list[int]],
|
150
|
+
name: str = "JobShopInstance",
|
151
|
+
metadata: dict[str, Any] | None = None,
|
152
|
+
) -> JobShopInstance:
|
153
|
+
"""Creates a JobShopInstance from duration and machines matrices.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
duration_matrix:
|
157
|
+
A list of lists of integers. The i-th list contains the
|
158
|
+
durations of the operations of the job with id i.
|
159
|
+
machines_matrix:
|
160
|
+
A list of lists of lists of integers if the
|
161
|
+
instance is flexible, or a list of lists of integers if the
|
162
|
+
instance is not flexible. The i-th list contains the machines
|
163
|
+
in which the operations of the job with id i can be processed.
|
164
|
+
name:
|
165
|
+
A string with the name of the instance.
|
166
|
+
metadata:
|
167
|
+
A dictionary with additional information about the instance.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
A JobShopInstance object.
|
171
|
+
"""
|
172
|
+
jobs: list[list[Operation]] = [[] for _ in range(len(duration_matrix))]
|
173
|
+
|
174
|
+
num_jobs = len(duration_matrix)
|
175
|
+
for job_id in range(num_jobs):
|
176
|
+
num_operations = len(duration_matrix[job_id])
|
177
|
+
for position_in_job in range(num_operations):
|
178
|
+
duration = duration_matrix[job_id][position_in_job]
|
179
|
+
machines = machines_matrix[job_id][position_in_job]
|
180
|
+
jobs[job_id].append(
|
181
|
+
Operation(duration=duration, machines=machines)
|
182
|
+
)
|
183
|
+
|
184
|
+
metadata = {} if metadata is None else metadata
|
185
|
+
return cls(jobs=jobs, name=name, **metadata)
|
186
|
+
|
187
|
+
def __repr__(self) -> str:
|
188
|
+
return (
|
189
|
+
f"JobShopInstance(name={self.name}, "
|
190
|
+
f"num_jobs={self.num_jobs}, num_machines={self.num_machines})"
|
191
|
+
)
|
192
|
+
|
193
|
+
def __eq__(self, other: Any) -> bool:
|
194
|
+
if not isinstance(other, JobShopInstance):
|
195
|
+
return False
|
196
|
+
return self.jobs == other.jobs
|
197
|
+
|
198
|
+
@property
|
199
|
+
def num_jobs(self) -> int:
|
200
|
+
"""Returns the number of jobs in the instance."""
|
201
|
+
return len(self.jobs)
|
202
|
+
|
203
|
+
@functools.cached_property
|
204
|
+
def num_machines(self) -> int:
|
205
|
+
"""Returns the number of machines in the instance.
|
206
|
+
|
207
|
+
Computed as the maximum machine id present in the instance plus one.
|
208
|
+
"""
|
209
|
+
max_machine_id = -1
|
210
|
+
for job in self.jobs:
|
211
|
+
for operation in job:
|
212
|
+
max_machine_id = max(max_machine_id, *operation.machines)
|
213
|
+
return max_machine_id + 1
|
214
|
+
|
215
|
+
@functools.cached_property
|
216
|
+
def num_operations(self) -> int:
|
217
|
+
"""Returns the number of operations in the instance."""
|
218
|
+
return sum(len(job) for job in self.jobs)
|
219
|
+
|
220
|
+
@functools.cached_property
|
221
|
+
def is_flexible(self) -> bool:
|
222
|
+
"""Returns True if any operation has more than one machine."""
|
223
|
+
return any(
|
224
|
+
any(len(operation.machines) > 1 for operation in job)
|
225
|
+
for job in self.jobs
|
226
|
+
)
|
227
|
+
|
228
|
+
@functools.cached_property
|
229
|
+
def durations_matrix(self) -> list[list[int]]:
|
230
|
+
"""Returns the duration matrix of the instance.
|
231
|
+
|
232
|
+
The duration of the operation with `job_id` i and `position_in_job` j
|
233
|
+
is stored in the i-th position of the j-th list of the returned matrix:
|
234
|
+
|
235
|
+
```python
|
236
|
+
duration = instance.durations_matrix[i][j]
|
237
|
+
```
|
238
|
+
"""
|
239
|
+
return [[operation.duration for operation in job] for job in self.jobs]
|
240
|
+
|
241
|
+
@functools.cached_property
|
242
|
+
def machines_matrix(self) -> list[list[list[int]]] | list[list[int]]:
|
243
|
+
"""Returns the machines matrix of the instance.
|
244
|
+
|
245
|
+
If the instance is flexible (i.e., if any operation has more than one
|
246
|
+
machine in which it can be processed), the returned matrix is a list of
|
247
|
+
lists of lists of integers.
|
248
|
+
|
249
|
+
Otherwise, the returned matrix is a list of lists of integers.
|
250
|
+
|
251
|
+
To access the machines of the operation with position i in the job
|
252
|
+
with id j, the following code must be used:
|
253
|
+
|
254
|
+
```python
|
255
|
+
machines = instance.machines_matrix[j][i]
|
256
|
+
```
|
257
|
+
|
258
|
+
"""
|
259
|
+
if self.is_flexible:
|
260
|
+
return [
|
261
|
+
[operation.machines for operation in job] for job in self.jobs
|
262
|
+
]
|
263
|
+
return [
|
264
|
+
[operation.machine_id for operation in job] for job in self.jobs
|
265
|
+
]
|
266
|
+
|
267
|
+
@functools.cached_property
|
268
|
+
def operations_by_machine(self) -> list[list[Operation]]:
|
269
|
+
"""Returns a list of lists of operations.
|
270
|
+
|
271
|
+
The i-th list contains the operations that can be processed in the
|
272
|
+
machine with id i.
|
273
|
+
"""
|
274
|
+
operations_by_machine: list[list[Operation]] = [
|
275
|
+
[] for _ in range(self.num_machines)
|
276
|
+
]
|
277
|
+
for job in self.jobs:
|
278
|
+
for operation in job:
|
279
|
+
for machine_id in operation.machines:
|
280
|
+
operations_by_machine[machine_id].append(operation)
|
281
|
+
|
282
|
+
return operations_by_machine
|
283
|
+
|
284
|
+
@functools.cached_property
|
285
|
+
def max_duration(self) -> float:
|
286
|
+
"""Returns the maximum duration of the instance.
|
287
|
+
|
288
|
+
Useful for normalizing the durations of the operations."""
|
289
|
+
return max(
|
290
|
+
max(operation.duration for operation in job) for job in self.jobs
|
291
|
+
)
|
292
|
+
|
293
|
+
@functools.cached_property
|
294
|
+
def max_duration_per_job(self) -> list[float]:
|
295
|
+
"""Returns the maximum duration of each job in the instance.
|
296
|
+
|
297
|
+
The maximum duration of the job with id i is stored in the i-th
|
298
|
+
position of the returned list.
|
299
|
+
|
300
|
+
Useful for normalizing the durations of the operations.
|
301
|
+
"""
|
302
|
+
return [max(op.duration for op in job) for job in self.jobs]
|
303
|
+
|
304
|
+
@functools.cached_property
|
305
|
+
def max_duration_per_machine(self) -> list[int]:
|
306
|
+
"""Returns the maximum duration of each machine in the instance.
|
307
|
+
|
308
|
+
The maximum duration of the machine with id i is stored in the i-th
|
309
|
+
position of the returned list.
|
310
|
+
|
311
|
+
Useful for normalizing the durations of the operations.
|
312
|
+
"""
|
313
|
+
max_duration_per_machine = [0] * self.num_machines
|
314
|
+
for job in self.jobs:
|
315
|
+
for operation in job:
|
316
|
+
for machine_id in operation.machines:
|
317
|
+
max_duration_per_machine[machine_id] = max(
|
318
|
+
max_duration_per_machine[machine_id],
|
319
|
+
operation.duration,
|
320
|
+
)
|
321
|
+
return max_duration_per_machine
|
322
|
+
|
323
|
+
@functools.cached_property
|
324
|
+
def job_durations(self) -> list[int]:
|
325
|
+
"""Returns a list with the duration of each job in the instance.
|
326
|
+
|
327
|
+
The duration of a job is the sum of the durations of its operations.
|
328
|
+
|
329
|
+
The duration of the job with id i is stored in the i-th position of the
|
330
|
+
returned list.
|
331
|
+
"""
|
332
|
+
return [sum(op.duration for op in job) for job in self.jobs]
|
333
|
+
|
334
|
+
@functools.cached_property
|
335
|
+
def machine_loads(self) -> list[int]:
|
336
|
+
"""Returns the total machine load of each machine in the instance.
|
337
|
+
|
338
|
+
The total machine load of a machine is the sum of the durations of the
|
339
|
+
operations that can be processed in that machine.
|
340
|
+
|
341
|
+
The total machine load of the machine with id i is stored in the i-th
|
342
|
+
position of the returned list.
|
343
|
+
"""
|
344
|
+
machine_times = [0] * self.num_machines
|
345
|
+
for job in self.jobs:
|
346
|
+
for operation in job:
|
347
|
+
for machine_id in operation.machines:
|
348
|
+
machine_times[machine_id] += operation.duration
|
349
|
+
|
350
|
+
return machine_times
|
351
|
+
|
352
|
+
@functools.cached_property
|
353
|
+
def total_duration(self) -> int:
|
354
|
+
"""Returns the sum of the durations of all operations in all jobs."""
|
355
|
+
return sum(self.job_durations)
|