job-shop-lib 0.5.1__py3-none-any.whl → 1.0.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 +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +10 -2
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +37 -26
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generation/general_instance_generator.py +0 -169
- job_shop_lib/generation/transformations.py +0 -164
- job_shop_lib/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -1,202 +0,0 @@
|
|
1
|
-
"""Home of the `JobShopGraph` class."""
|
2
|
-
|
3
|
-
import collections
|
4
|
-
import networkx as nx
|
5
|
-
|
6
|
-
from job_shop_lib import JobShopInstance, JobShopLibError
|
7
|
-
from job_shop_lib.graphs import Node, NodeType
|
8
|
-
|
9
|
-
|
10
|
-
NODE_ATTR = "node"
|
11
|
-
|
12
|
-
|
13
|
-
# pylint: disable=too-many-instance-attributes
|
14
|
-
class JobShopGraph:
|
15
|
-
"""Data structure to represent a `JobShopInstance` as a graph.
|
16
|
-
|
17
|
-
Provides a comprehensive graph-based representation of a job shop
|
18
|
-
scheduling problem, utilizing the `networkx` library to model the complex
|
19
|
-
relationships between jobs, operations, and machines. This class transforms
|
20
|
-
the abstract scheduling problem into a directed graph, where various
|
21
|
-
entities (jobs, machines, and operations) are nodes, and the dependencies
|
22
|
-
(such as operation order within a job or machine assignment) are edges.
|
23
|
-
|
24
|
-
This transformation allows for the application of graph algorithms
|
25
|
-
to analyze and solve scheduling problems.
|
26
|
-
|
27
|
-
Attributes:
|
28
|
-
instance:
|
29
|
-
The job shop instance encapsulated by this graph.
|
30
|
-
graph:
|
31
|
-
The directed graph representing the job shop, where nodes are
|
32
|
-
operations, machines, jobs, or abstract concepts like global,
|
33
|
-
source, and sink, with edges indicating dependencies.
|
34
|
-
"""
|
35
|
-
|
36
|
-
def __init__(self, instance: JobShopInstance):
|
37
|
-
"""Initializes the graph with the given instance.
|
38
|
-
|
39
|
-
Nodes of type `OPERATION` are added to the graph based on the
|
40
|
-
operations of the instance.
|
41
|
-
|
42
|
-
Args:
|
43
|
-
instance:
|
44
|
-
The job shop instance that the graph represents.
|
45
|
-
"""
|
46
|
-
self.graph = nx.DiGraph()
|
47
|
-
self.instance = instance
|
48
|
-
|
49
|
-
self._nodes: list[Node] = []
|
50
|
-
self._nodes_by_type: dict[NodeType, list[Node]] = (
|
51
|
-
collections.defaultdict(list)
|
52
|
-
)
|
53
|
-
self._nodes_by_machine: list[list[Node]] = [
|
54
|
-
[] for _ in range(instance.num_machines)
|
55
|
-
]
|
56
|
-
self._nodes_by_job: list[list[Node]] = [
|
57
|
-
[] for _ in range(instance.num_jobs)
|
58
|
-
]
|
59
|
-
self._next_node_id = 0
|
60
|
-
self.removed_nodes: list[bool] = []
|
61
|
-
self._add_operation_nodes()
|
62
|
-
|
63
|
-
@property
|
64
|
-
def nodes(self) -> list[Node]:
|
65
|
-
"""List of all nodes added to the graph.
|
66
|
-
|
67
|
-
It may contain nodes that have been removed from the graph.
|
68
|
-
"""
|
69
|
-
return self._nodes
|
70
|
-
|
71
|
-
@property
|
72
|
-
def nodes_by_type(self) -> dict[NodeType, list[Node]]:
|
73
|
-
"""Dictionary mapping node types to lists of nodes.
|
74
|
-
|
75
|
-
It may contain nodes that have been removed from the graph.
|
76
|
-
"""
|
77
|
-
return self._nodes_by_type
|
78
|
-
|
79
|
-
@property
|
80
|
-
def nodes_by_machine(self) -> list[list[Node]]:
|
81
|
-
"""List of lists mapping machine ids to operation nodes.
|
82
|
-
|
83
|
-
It may contain nodes that have been removed from the graph.
|
84
|
-
"""
|
85
|
-
return self._nodes_by_machine
|
86
|
-
|
87
|
-
@property
|
88
|
-
def nodes_by_job(self) -> list[list[Node]]:
|
89
|
-
"""List of lists mapping job ids to operation nodes.
|
90
|
-
|
91
|
-
It may contain nodes that have been removed from the graph.
|
92
|
-
"""
|
93
|
-
return self._nodes_by_job
|
94
|
-
|
95
|
-
@property
|
96
|
-
def num_edges(self) -> int:
|
97
|
-
"""Number of edges in the graph."""
|
98
|
-
return self.graph.number_of_edges()
|
99
|
-
|
100
|
-
@property
|
101
|
-
def num_job_nodes(self) -> int:
|
102
|
-
"""Number of job nodes in the graph."""
|
103
|
-
return len(self._nodes_by_type[NodeType.JOB])
|
104
|
-
|
105
|
-
def _add_operation_nodes(self) -> None:
|
106
|
-
"""Adds operation nodes to the graph."""
|
107
|
-
for job in self.instance.jobs:
|
108
|
-
for operation in job:
|
109
|
-
node = Node(node_type=NodeType.OPERATION, operation=operation)
|
110
|
-
self.add_node(node)
|
111
|
-
|
112
|
-
def add_node(self, node_for_adding: Node) -> None:
|
113
|
-
"""Adds a node to the graph and updates relevant class attributes.
|
114
|
-
|
115
|
-
This method assigns a unique identifier to the node, adds it to the
|
116
|
-
graph, and updates the nodes list and the nodes_by_type dictionary. If
|
117
|
-
the node is of type `OPERATION`, it also updates `nodes_by_job` and
|
118
|
-
`nodes_by_machine` based on the operation's job_id and machine_ids.
|
119
|
-
|
120
|
-
Args:
|
121
|
-
node_for_adding (Node): The node to be added to the graph.
|
122
|
-
|
123
|
-
Raises:
|
124
|
-
ValueError: If the node type is unsupported or if required
|
125
|
-
attributes for the node type are missing.
|
126
|
-
|
127
|
-
Note:
|
128
|
-
This method directly modifies the graph attribute as well as
|
129
|
-
several other class attributes. Thus, adding nodes to the graph
|
130
|
-
should be done exclusively through this method to avoid
|
131
|
-
inconsistencies.
|
132
|
-
"""
|
133
|
-
node_for_adding.node_id = self._next_node_id
|
134
|
-
self.graph.add_node(
|
135
|
-
node_for_adding.node_id, **{NODE_ATTR: node_for_adding}
|
136
|
-
)
|
137
|
-
self._nodes_by_type[node_for_adding.node_type].append(node_for_adding)
|
138
|
-
self._nodes.append(node_for_adding)
|
139
|
-
self._next_node_id += 1
|
140
|
-
self.removed_nodes.append(False)
|
141
|
-
|
142
|
-
if node_for_adding.node_type == NodeType.OPERATION:
|
143
|
-
operation = node_for_adding.operation
|
144
|
-
self._nodes_by_job[operation.job_id].append(node_for_adding)
|
145
|
-
for machine_id in operation.machines:
|
146
|
-
self._nodes_by_machine[machine_id].append(node_for_adding)
|
147
|
-
|
148
|
-
def add_edge(
|
149
|
-
self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
|
150
|
-
) -> None:
|
151
|
-
"""Adds an edge to the graph.
|
152
|
-
|
153
|
-
Args:
|
154
|
-
u_of_edge: The source node of the edge. If it is a `Node`, its
|
155
|
-
`node_id` is used as the source. Otherwise, it is assumed to be
|
156
|
-
the node_id of the source.
|
157
|
-
v_of_edge: The destination node of the edge. If it is a `Node`, its
|
158
|
-
`node_id` is used as the destination. Otherwise, it is assumed
|
159
|
-
to be the node_id of the destination.
|
160
|
-
**attr: Additional attributes to be added to the edge.
|
161
|
-
|
162
|
-
Raises:
|
163
|
-
JobShopLibError: If `u_of_edge` or `v_of_edge` are not in the
|
164
|
-
graph.
|
165
|
-
"""
|
166
|
-
if isinstance(u_of_edge, Node):
|
167
|
-
u_of_edge = u_of_edge.node_id
|
168
|
-
if isinstance(v_of_edge, Node):
|
169
|
-
v_of_edge = v_of_edge.node_id
|
170
|
-
if u_of_edge not in self.graph or v_of_edge not in self.graph:
|
171
|
-
raise JobShopLibError(
|
172
|
-
"`u_of_edge` and `v_of_edge` must be in the graph."
|
173
|
-
)
|
174
|
-
self.graph.add_edge(u_of_edge, v_of_edge, **attr)
|
175
|
-
|
176
|
-
def remove_node(self, node_id: int) -> None:
|
177
|
-
"""Removes a node from the graph and the isolated nodes that result
|
178
|
-
from the removal.
|
179
|
-
|
180
|
-
Args:
|
181
|
-
node_id: The id of the node to remove.
|
182
|
-
"""
|
183
|
-
self.graph.remove_node(node_id)
|
184
|
-
self.removed_nodes[node_id] = True
|
185
|
-
|
186
|
-
isolated_nodes = list(nx.isolates(self.graph))
|
187
|
-
for isolated_node in isolated_nodes:
|
188
|
-
self.removed_nodes[isolated_node] = True
|
189
|
-
|
190
|
-
self.graph.remove_nodes_from(isolated_nodes)
|
191
|
-
|
192
|
-
def is_removed(self, node: int | Node) -> bool:
|
193
|
-
"""Returns whether the node is removed from the graph.
|
194
|
-
|
195
|
-
Args:
|
196
|
-
node: The node to check. If it is a `Node`, its `node_id` is used
|
197
|
-
as the node to check. Otherwise, it is assumed to be the
|
198
|
-
`node_id` of the node to check.
|
199
|
-
"""
|
200
|
-
if isinstance(node, Node):
|
201
|
-
node = node.node_id
|
202
|
-
return self.removed_nodes[node]
|
job_shop_lib/graphs/node.py
DELETED
@@ -1,166 +0,0 @@
|
|
1
|
-
"""Home of the `Node` class."""
|
2
|
-
|
3
|
-
from job_shop_lib import Operation, JobShopLibError
|
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
|
-
Attributes:
|
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
|
-
"""
|
47
|
-
|
48
|
-
__slots__ = "node_type", "_node_id", "_operation", "_machine_id", "_job_id"
|
49
|
-
|
50
|
-
def __init__(
|
51
|
-
self,
|
52
|
-
node_type: NodeType,
|
53
|
-
operation: Operation | None = None,
|
54
|
-
machine_id: int | None = None,
|
55
|
-
job_id: int | None = None,
|
56
|
-
):
|
57
|
-
"""Initializes the node with the given attributes.
|
58
|
-
|
59
|
-
Args:
|
60
|
-
node_type:
|
61
|
-
The type of the node. It can be one of the following:
|
62
|
-
- NodeType.OPERATION
|
63
|
-
- NodeType.MACHINE
|
64
|
-
- NodeType.JOB
|
65
|
-
- NodeType.GLOBAL
|
66
|
-
...
|
67
|
-
operation:
|
68
|
-
The operation of the node. It should be provided if the
|
69
|
-
`node_type` is NodeType.OPERATION.
|
70
|
-
machine_id:
|
71
|
-
The id of the machine of the node. It should be provided if the
|
72
|
-
node_type is NodeType.MACHINE.
|
73
|
-
job_id:
|
74
|
-
The id of the job of the node. It should be provided if the
|
75
|
-
node_type is NodeType.JOB.
|
76
|
-
|
77
|
-
Raises:
|
78
|
-
JobShopLibError:
|
79
|
-
If the node_type is OPERATION and operation is None.
|
80
|
-
JobShopLibError:
|
81
|
-
If the node_type is MACHINE and machine_id is None.
|
82
|
-
JobShopLibError:
|
83
|
-
If the node_type is JOB and job_id is None.
|
84
|
-
"""
|
85
|
-
if node_type == NodeType.OPERATION and operation is None:
|
86
|
-
raise JobShopLibError("Operation node must have an operation.")
|
87
|
-
|
88
|
-
if node_type == NodeType.MACHINE and machine_id is None:
|
89
|
-
raise JobShopLibError("Machine node must have a machine_id.")
|
90
|
-
|
91
|
-
if node_type == NodeType.JOB and job_id is None:
|
92
|
-
raise JobShopLibError("Job node must have a job_id.")
|
93
|
-
|
94
|
-
self.node_type = node_type
|
95
|
-
self._node_id: int | None = None
|
96
|
-
|
97
|
-
self._operation = operation
|
98
|
-
self._machine_id = machine_id
|
99
|
-
self._job_id = job_id
|
100
|
-
|
101
|
-
@property
|
102
|
-
def node_id(self) -> int:
|
103
|
-
"""Returns a unique identifier for the node."""
|
104
|
-
if self._node_id is None:
|
105
|
-
raise JobShopLibError("Node has not been assigned an id.")
|
106
|
-
return self._node_id
|
107
|
-
|
108
|
-
@node_id.setter
|
109
|
-
def node_id(self, value: int) -> None:
|
110
|
-
self._node_id = value
|
111
|
-
|
112
|
-
@property
|
113
|
-
def operation(self) -> Operation:
|
114
|
-
"""Returns the operation of the node.
|
115
|
-
|
116
|
-
This property is mandatory for nodes of type `OPERATION`.
|
117
|
-
"""
|
118
|
-
if self._operation is None:
|
119
|
-
raise JobShopLibError("Node has no operation.")
|
120
|
-
return self._operation
|
121
|
-
|
122
|
-
@property
|
123
|
-
def machine_id(self) -> int:
|
124
|
-
"""Returns the `machine_id` of the node.
|
125
|
-
|
126
|
-
This property is mandatory for nodes of type `MACHINE`.
|
127
|
-
"""
|
128
|
-
if self._machine_id is None:
|
129
|
-
raise JobShopLibError("Node has no `machine_id`.")
|
130
|
-
return self._machine_id
|
131
|
-
|
132
|
-
@property
|
133
|
-
def job_id(self) -> int:
|
134
|
-
"""Returns the `job_id` of the node.
|
135
|
-
|
136
|
-
This property is mandatory for nodes of type `JOB`.
|
137
|
-
"""
|
138
|
-
if self._job_id is None:
|
139
|
-
raise JobShopLibError("Node has no `job_id`.")
|
140
|
-
return self._job_id
|
141
|
-
|
142
|
-
def __hash__(self) -> int:
|
143
|
-
return self.node_id
|
144
|
-
|
145
|
-
def __eq__(self, __value: object) -> bool:
|
146
|
-
if isinstance(__value, Node):
|
147
|
-
__value = __value.node_id
|
148
|
-
return self.node_id == __value
|
149
|
-
|
150
|
-
def __repr__(self) -> str:
|
151
|
-
if self.node_type == NodeType.OPERATION:
|
152
|
-
return (
|
153
|
-
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
154
|
-
f"operation={self.operation})"
|
155
|
-
)
|
156
|
-
if self.node_type == NodeType.MACHINE:
|
157
|
-
return (
|
158
|
-
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
159
|
-
f"machine_id={self._machine_id})"
|
160
|
-
)
|
161
|
-
if self.node_type == NodeType.JOB:
|
162
|
-
return (
|
163
|
-
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
164
|
-
f"job_id={self._job_id})"
|
165
|
-
)
|
166
|
-
return f"Node(node_type={self.node_type.name}, id={self._node_id})"
|
job_shop_lib/operation.py
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
"""Home of the `Operation` class."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
from job_shop_lib import JobShopLibError
|
6
|
-
|
7
|
-
|
8
|
-
class Operation:
|
9
|
-
"""Stores machine and duration information for a job operation.
|
10
|
-
|
11
|
-
Note:
|
12
|
-
To increase performance, some solvers such as the CP-SAT solver use
|
13
|
-
only integers to represent the operation's attributes. Should a
|
14
|
-
problem involve operations with non-integer durations, it would be
|
15
|
-
necessary to multiply all durations by a sufficiently large integer so
|
16
|
-
that every duration is an integer.
|
17
|
-
|
18
|
-
Attributes:
|
19
|
-
machines: A list of machine ids that can perform the operation.
|
20
|
-
duration: The time it takes to perform the operation.
|
21
|
-
"""
|
22
|
-
|
23
|
-
__slots__ = (
|
24
|
-
"machines",
|
25
|
-
"duration",
|
26
|
-
"_job_id",
|
27
|
-
"_position_in_job",
|
28
|
-
"_operation_id",
|
29
|
-
)
|
30
|
-
|
31
|
-
def __init__(self, machines: int | list[int], duration: int):
|
32
|
-
"""Initializes the object with the given machines and duration.
|
33
|
-
|
34
|
-
Args:
|
35
|
-
machines: A list of machine ids that can perform the operation. If
|
36
|
-
only one machine can perform the operation, it can be passed as
|
37
|
-
an integer.
|
38
|
-
duration: The time it takes to perform the operation.
|
39
|
-
"""
|
40
|
-
self.machines = [machines] if isinstance(machines, int) else machines
|
41
|
-
self.duration = duration
|
42
|
-
|
43
|
-
# Defined outside the class by the JobShopInstance class:
|
44
|
-
self._job_id: int | None = None
|
45
|
-
self._position_in_job: int | None = None
|
46
|
-
self._operation_id: int | None = None
|
47
|
-
|
48
|
-
@property
|
49
|
-
def machine_id(self) -> int:
|
50
|
-
"""Returns the id of the machine associated with the operation.
|
51
|
-
|
52
|
-
Raises:
|
53
|
-
ValueError: If the operation has multiple machines in its list.
|
54
|
-
"""
|
55
|
-
if len(self.machines) > 1:
|
56
|
-
raise JobShopLibError("Operation has multiple machines.")
|
57
|
-
return self.machines[0]
|
58
|
-
|
59
|
-
@property
|
60
|
-
def job_id(self) -> int:
|
61
|
-
"""Returns the id of the job that the operation belongs to."""
|
62
|
-
if self._job_id is None:
|
63
|
-
raise JobShopLibError("Operation has no job_id.")
|
64
|
-
return self._job_id
|
65
|
-
|
66
|
-
@job_id.setter
|
67
|
-
def job_id(self, value: int) -> None:
|
68
|
-
self._job_id = value
|
69
|
-
|
70
|
-
@property
|
71
|
-
def position_in_job(self) -> int:
|
72
|
-
"""Returns the position (starting at zero) of the operation in the
|
73
|
-
job.
|
74
|
-
|
75
|
-
Raises:
|
76
|
-
ValueError: If the operation has no position_in_job.
|
77
|
-
"""
|
78
|
-
if self._position_in_job is None:
|
79
|
-
raise JobShopLibError("Operation has no position_in_job.")
|
80
|
-
return self._position_in_job
|
81
|
-
|
82
|
-
@position_in_job.setter
|
83
|
-
def position_in_job(self, value: int) -> None:
|
84
|
-
self._position_in_job = value
|
85
|
-
|
86
|
-
@property
|
87
|
-
def operation_id(self) -> int:
|
88
|
-
"""Returns the id of the operation.
|
89
|
-
|
90
|
-
The operation id is unique within a job shop instance and should
|
91
|
-
be set by the JobShopInstance class.
|
92
|
-
|
93
|
-
It starts at 0 and is incremented by 1 for each operation in the
|
94
|
-
instance.
|
95
|
-
|
96
|
-
Raises:
|
97
|
-
ValueError: If the operation has no id.
|
98
|
-
"""
|
99
|
-
if self._operation_id is None:
|
100
|
-
raise JobShopLibError("Operation has no id.")
|
101
|
-
return self._operation_id
|
102
|
-
|
103
|
-
@operation_id.setter
|
104
|
-
def operation_id(self, value: int) -> None:
|
105
|
-
self._operation_id = value
|
106
|
-
|
107
|
-
def __hash__(self) -> int:
|
108
|
-
return hash(self.operation_id)
|
109
|
-
|
110
|
-
def __eq__(self, value: object) -> bool:
|
111
|
-
if not isinstance(value, Operation):
|
112
|
-
return False
|
113
|
-
return self.__slots__ == value.__slots__
|
114
|
-
|
115
|
-
def __repr__(self) -> str:
|
116
|
-
machines = (
|
117
|
-
self.machines[0] if len(self.machines) == 1 else self.machines
|
118
|
-
)
|
119
|
-
return (
|
120
|
-
f"O(m={machines}, d={self.duration}, "
|
121
|
-
f"j={self.job_id}, p={self.position_in_job})"
|
122
|
-
)
|