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