job-shop-lib 0.5.0__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 +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- 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.0.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.0.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/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- 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.0.dist-info/RECORD +0 -48
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -1,20 +1,17 @@
|
|
1
|
-
"""Contains helper functions to build the
|
2
|
-
|
1
|
+
"""Contains helper functions to build the resource-task graphs from a job shop
|
2
|
+
instance.
|
3
3
|
|
4
|
-
The agent-task graph was introduced by
|
4
|
+
The resource-task graph (originally called agent-task graph) was introduced by
|
5
|
+
Junyoung Park et al. (2021).
|
5
6
|
In contrast to the disjunctive graph, instead of connecting operations that
|
6
7
|
share the same resources directly by disjunctive edges, operation nodes are
|
7
8
|
connected with machine ones. All machine nodes are connected between them, and
|
8
9
|
all operation nodes from the same job are connected by non-directed edges too.
|
9
10
|
|
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
11
|
References:
|
15
12
|
- 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.
|
13
|
+
solve multi-agent scheduling problems with reinforcement learning. ArXiv,
|
14
|
+
abs/2106.03051, 2021.
|
18
15
|
"""
|
19
16
|
|
20
17
|
import itertools
|
@@ -23,11 +20,13 @@ from job_shop_lib import JobShopInstance
|
|
23
20
|
from job_shop_lib.graphs import JobShopGraph, NodeType, Node
|
24
21
|
|
25
22
|
|
26
|
-
def
|
27
|
-
|
23
|
+
def build_complete_resource_task_graph(
|
24
|
+
instance: JobShopInstance,
|
25
|
+
) -> JobShopGraph:
|
26
|
+
"""Builds the resource-task graph of the instance with job and global
|
27
|
+
nodes.
|
28
28
|
|
29
|
-
The complete
|
30
|
-
that includes job nodes and a global node.
|
29
|
+
The complete resource-task includes job nodes and a global node.
|
31
30
|
|
32
31
|
Job nodes are connected to all operation nodes of the same job, and the
|
33
32
|
global node is connected to all machine and job nodes.
|
@@ -38,10 +37,11 @@ def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
|
38
37
|
|
39
38
|
Args:
|
40
39
|
instance:
|
41
|
-
The job shop instance in which the
|
40
|
+
The job shop instance in which the resource-task graph will be
|
41
|
+
built.
|
42
42
|
|
43
43
|
Returns:
|
44
|
-
The complete
|
44
|
+
The complete resource-task graph of the instance.
|
45
45
|
"""
|
46
46
|
graph = JobShopGraph(instance)
|
47
47
|
|
@@ -58,23 +58,23 @@ def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
|
58
58
|
return graph
|
59
59
|
|
60
60
|
|
61
|
-
def
|
61
|
+
def build_resource_task_graph_with_jobs(
|
62
62
|
instance: JobShopInstance,
|
63
63
|
) -> JobShopGraph:
|
64
|
-
"""Builds the
|
64
|
+
"""Builds the resource-task graph of the instance with job nodes.
|
65
65
|
|
66
|
-
The
|
67
|
-
graph that includes job nodes.
|
66
|
+
The resource-task graph that includes job nodes.
|
68
67
|
|
69
68
|
Job nodes are connected to all operation nodes of the same job, and their
|
70
69
|
are connected between them.
|
71
70
|
|
72
71
|
Args:
|
73
72
|
instance:
|
74
|
-
The job shop instance in which the
|
73
|
+
The job shop instance in which the resource-task graph will be
|
74
|
+
built.
|
75
75
|
|
76
76
|
Returns:
|
77
|
-
The
|
77
|
+
The resource-task graph of the instance with job nodes.
|
78
78
|
"""
|
79
79
|
graph = JobShopGraph(instance)
|
80
80
|
|
@@ -89,10 +89,11 @@ def build_agent_task_graph_with_jobs(
|
|
89
89
|
return graph
|
90
90
|
|
91
91
|
|
92
|
-
def
|
93
|
-
"""Builds the
|
92
|
+
def build_resource_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
93
|
+
"""Builds the resource-task graph of the instance.
|
94
94
|
|
95
|
-
The
|
95
|
+
The JSSP resource-task graph representation was introduced by Junyoung
|
96
|
+
Park et al. (2021) (named agent-task graph in the original paper).
|
96
97
|
|
97
98
|
In contrast to the disjunctive graph, instead of connecting operations
|
98
99
|
that share the same resources directly by disjunctive edges, operation
|
@@ -103,10 +104,11 @@ def build_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
|
|
103
104
|
|
104
105
|
Args:
|
105
106
|
instance:
|
106
|
-
The job shop instance in which the
|
107
|
+
The job shop instance in which the resource-task graph will be
|
108
|
+
built.
|
107
109
|
|
108
110
|
Returns:
|
109
|
-
The
|
111
|
+
The resource-task graph of the instance.
|
110
112
|
"""
|
111
113
|
graph = JobShopGraph(instance)
|
112
114
|
|
@@ -0,0 +1,38 @@
|
|
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
|
+
Nodes of type :class:`NodeType.OPERATION`, :class:`NodeType.MACHINE`, and
|
17
|
+
:class:`NodeType.JOB` have specific attributes associated with them in the
|
18
|
+
:class:`Node` class.
|
19
|
+
|
20
|
+
On the other hand, Nodes of type :class:`NodeType.GLOBAL`,
|
21
|
+
:class:`NodeType.SOURCE`, and :class:`NodeType.SINK` are used to represent
|
22
|
+
different concepts in the graph and accesing them, but they do not have
|
23
|
+
specific attributes associated with them.
|
24
|
+
|
25
|
+
.. tip::
|
26
|
+
|
27
|
+
While uncommon, it can be useful to extend this enumeration with
|
28
|
+
additional node types. This can be achieved using
|
29
|
+
`aenum <https://github.com/ethanfurman/aenum>`_'s
|
30
|
+
``extend_enum`` (requires using Python 3.11+).
|
31
|
+
"""
|
32
|
+
|
33
|
+
OPERATION = enum.auto()
|
34
|
+
MACHINE = enum.auto()
|
35
|
+
JOB = enum.auto()
|
36
|
+
GLOBAL = enum.auto()
|
37
|
+
SOURCE = enum.auto()
|
38
|
+
SINK = enum.auto()
|
@@ -0,0 +1,320 @@
|
|
1
|
+
"""Home of the `JobShopGraph` class."""
|
2
|
+
|
3
|
+
from typing import List, Union, Dict
|
4
|
+
import collections
|
5
|
+
import networkx as nx
|
6
|
+
|
7
|
+
from job_shop_lib import JobShopInstance
|
8
|
+
from job_shop_lib.exceptions import ValidationError
|
9
|
+
from job_shop_lib.graphs import Node, NodeType
|
10
|
+
|
11
|
+
|
12
|
+
NODE_ATTR = "node"
|
13
|
+
|
14
|
+
|
15
|
+
# pylint: disable=too-many-instance-attributes
|
16
|
+
class JobShopGraph:
|
17
|
+
"""Represents a :class:`JobShopInstance` as a heterogeneous directed graph.
|
18
|
+
|
19
|
+
Provides a comprehensive graph-based representation of a job shop
|
20
|
+
scheduling problem, utilizing the ``networkx`` library to model the complex
|
21
|
+
relationships between jobs, operations, and machines. This class transforms
|
22
|
+
the abstract scheduling problem into a directed graph, where various
|
23
|
+
entities (jobs, machines, and operations) are nodes, and the dependencies
|
24
|
+
(such as operation order within a job or machine assignment) are edges.
|
25
|
+
|
26
|
+
This transformation allows for the application of graph algorithms
|
27
|
+
to analyze and solve scheduling problems.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
instance:
|
31
|
+
The job shop instance that the graph represents.
|
32
|
+
add_operation_nodes:
|
33
|
+
Whether to add nodes of type :class:`NodeType.OPERATION` to the
|
34
|
+
to the graph. If set to ``False``, the graph will be empty, and
|
35
|
+
operation nodes will need to be added manually.
|
36
|
+
"""
|
37
|
+
|
38
|
+
__slots__ = {
|
39
|
+
"instance": "The job shop instance that the graph represents.",
|
40
|
+
"graph": (
|
41
|
+
"The directed graph representing the job shop, where nodes are "
|
42
|
+
"operations, machines, jobs, or abstract concepts like global, "
|
43
|
+
"source, and sink, with edges indicating dependencies."
|
44
|
+
),
|
45
|
+
"_nodes": "List of all nodes added to the graph.",
|
46
|
+
"_nodes_by_type": "Dictionary mapping node types to lists of nodes.",
|
47
|
+
"_nodes_by_machine": (
|
48
|
+
"List of lists mapping machine ids to operation nodes."
|
49
|
+
),
|
50
|
+
"_nodes_by_job": "List of lists mapping job ids to operation nodes.",
|
51
|
+
"_next_node_id": (
|
52
|
+
"The id to assign to the next node added to thegraph."
|
53
|
+
),
|
54
|
+
"removed_nodes": (
|
55
|
+
"List of boolean values indicating whether a node has been "
|
56
|
+
"removed from the graph."
|
57
|
+
),
|
58
|
+
}
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self, instance: JobShopInstance, add_operation_nodes: bool = True
|
62
|
+
):
|
63
|
+
self.graph = nx.DiGraph()
|
64
|
+
self.instance = instance
|
65
|
+
|
66
|
+
self._nodes: List[Node] = []
|
67
|
+
self._nodes_by_type: Dict[NodeType, List[Node]] = (
|
68
|
+
collections.defaultdict(list)
|
69
|
+
)
|
70
|
+
self._nodes_by_machine: List[List[Node]] = [
|
71
|
+
[] for _ in range(instance.num_machines)
|
72
|
+
]
|
73
|
+
self._nodes_by_job: List[List[Node]] = [
|
74
|
+
[] for _ in range(instance.num_jobs)
|
75
|
+
]
|
76
|
+
self._next_node_id = 0
|
77
|
+
self.removed_nodes: List[bool] = []
|
78
|
+
if add_operation_nodes:
|
79
|
+
self.add_operation_nodes()
|
80
|
+
|
81
|
+
@property
|
82
|
+
def nodes(self) -> List[Node]:
|
83
|
+
"""List of all nodes added to the graph.
|
84
|
+
|
85
|
+
It may contain nodes that have been removed from the graph.
|
86
|
+
"""
|
87
|
+
return self._nodes
|
88
|
+
|
89
|
+
@property
|
90
|
+
def nodes_by_type(self) -> Dict[NodeType, List[Node]]:
|
91
|
+
"""Dictionary mapping node types to lists of nodes.
|
92
|
+
|
93
|
+
It may contain nodes that have been removed from the graph.
|
94
|
+
"""
|
95
|
+
return self._nodes_by_type
|
96
|
+
|
97
|
+
@property
|
98
|
+
def nodes_by_machine(self) -> List[List[Node]]:
|
99
|
+
"""List of lists mapping machine ids to operation nodes.
|
100
|
+
|
101
|
+
It may contain nodes that have been removed from the graph.
|
102
|
+
"""
|
103
|
+
return self._nodes_by_machine
|
104
|
+
|
105
|
+
@property
|
106
|
+
def nodes_by_job(self) -> List[List[Node]]:
|
107
|
+
"""List of lists mapping job ids to operation nodes.
|
108
|
+
|
109
|
+
It may contain nodes that have been removed from the graph.
|
110
|
+
"""
|
111
|
+
return self._nodes_by_job
|
112
|
+
|
113
|
+
@property
|
114
|
+
def num_edges(self) -> int:
|
115
|
+
"""Number of edges in the graph."""
|
116
|
+
return self.graph.number_of_edges()
|
117
|
+
|
118
|
+
@property
|
119
|
+
def num_job_nodes(self) -> int:
|
120
|
+
"""Number of job nodes in the graph."""
|
121
|
+
return len(self._nodes_by_type[NodeType.JOB])
|
122
|
+
|
123
|
+
def add_operation_nodes(self) -> None:
|
124
|
+
"""Adds operation nodes to the graph."""
|
125
|
+
for job in self.instance.jobs:
|
126
|
+
for operation in job:
|
127
|
+
node = Node(node_type=NodeType.OPERATION, operation=operation)
|
128
|
+
self.add_node(node)
|
129
|
+
|
130
|
+
def add_node(self, node_for_adding: Node) -> None:
|
131
|
+
"""Adds a node to the graph and updates relevant class attributes.
|
132
|
+
|
133
|
+
This method assigns a unique identifier to the node, adds it to the
|
134
|
+
graph, and updates the nodes list and the nodes_by_type dictionary. If
|
135
|
+
the node is of type :class:`NodeType.OPERATION`, it also updates
|
136
|
+
``nodes_by_job`` and ``nodes_by_machine`` based on the operation's
|
137
|
+
job id and machine ids.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
node_for_adding:
|
141
|
+
The node to be added to the graph.
|
142
|
+
|
143
|
+
Note:
|
144
|
+
This method directly modifies the graph attribute as well as
|
145
|
+
several other class attributes. Thus, adding nodes to the graph
|
146
|
+
should be done exclusively through this method to avoid
|
147
|
+
inconsistencies.
|
148
|
+
"""
|
149
|
+
node_for_adding.node_id = self._next_node_id
|
150
|
+
self.graph.add_node(
|
151
|
+
node_for_adding.node_id, **{NODE_ATTR: node_for_adding}
|
152
|
+
)
|
153
|
+
self._nodes_by_type[node_for_adding.node_type].append(node_for_adding)
|
154
|
+
self._nodes.append(node_for_adding)
|
155
|
+
self._next_node_id += 1
|
156
|
+
self.removed_nodes.append(False)
|
157
|
+
|
158
|
+
if node_for_adding.node_type == NodeType.OPERATION:
|
159
|
+
operation = node_for_adding.operation
|
160
|
+
self._nodes_by_job[operation.job_id].append(node_for_adding)
|
161
|
+
for machine_id in operation.machines:
|
162
|
+
self._nodes_by_machine[machine_id].append(node_for_adding)
|
163
|
+
|
164
|
+
def add_edge(
|
165
|
+
self,
|
166
|
+
u_of_edge: Union[Node, int],
|
167
|
+
v_of_edge: Union[Node, int],
|
168
|
+
**attr,
|
169
|
+
) -> None:
|
170
|
+
"""Adds an edge to the graph.
|
171
|
+
|
172
|
+
It automatically determines the edge type based on the source and
|
173
|
+
destination nodes unless explicitly provided in the ``attr`` argument
|
174
|
+
via the ``type`` key. The edge type is a tuple of strings:
|
175
|
+
``(source_node_type, "to", destination_node_type)``.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
u_of_edge:
|
179
|
+
The source node of the edge. If it is a :class:`Node`, its
|
180
|
+
``node_id`` is used as the source. Otherwise, it is assumed to
|
181
|
+
be the ``node_id`` of the source.
|
182
|
+
v_of_edge:
|
183
|
+
The destination node of the edge. If it is a :class:`Node`,
|
184
|
+
its ``node_id`` is used as the destination. Otherwise, it
|
185
|
+
is assumed to be the ``node_id`` of the destination.
|
186
|
+
**attr:
|
187
|
+
Additional attributes to be added to the edge.
|
188
|
+
|
189
|
+
Raises:
|
190
|
+
ValidationError: If ``u_of_edge`` or ``v_of_edge`` are not in the
|
191
|
+
graph.
|
192
|
+
"""
|
193
|
+
if isinstance(u_of_edge, Node):
|
194
|
+
u_of_edge = u_of_edge.node_id
|
195
|
+
if isinstance(v_of_edge, Node):
|
196
|
+
v_of_edge = v_of_edge.node_id
|
197
|
+
if u_of_edge not in self.graph or v_of_edge not in self.graph:
|
198
|
+
raise ValidationError(
|
199
|
+
"`u_of_edge` and `v_of_edge` must be in the graph."
|
200
|
+
)
|
201
|
+
edge_type = attr.pop("type", None)
|
202
|
+
if edge_type is None:
|
203
|
+
u_node = self.nodes[u_of_edge]
|
204
|
+
v_node = self.nodes[v_of_edge]
|
205
|
+
edge_type = (
|
206
|
+
u_node.node_type.name.lower(),
|
207
|
+
"to",
|
208
|
+
v_node.node_type.name.lower(),
|
209
|
+
)
|
210
|
+
self.graph.add_edge(u_of_edge, v_of_edge, type=edge_type, **attr)
|
211
|
+
|
212
|
+
def remove_node(self, node_id: int) -> None:
|
213
|
+
"""Removes a node from the graph and the isolated nodes that result
|
214
|
+
from the removal.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
node_id:
|
218
|
+
The id of the node to remove.
|
219
|
+
"""
|
220
|
+
self.graph.remove_node(node_id)
|
221
|
+
self.removed_nodes[node_id] = True
|
222
|
+
|
223
|
+
def remove_isolated_nodes(self) -> None:
|
224
|
+
"""Removes isolated nodes from the graph."""
|
225
|
+
isolated_nodes = list(nx.isolates(self.graph))
|
226
|
+
for isolated_node in isolated_nodes:
|
227
|
+
self.removed_nodes[isolated_node] = True
|
228
|
+
|
229
|
+
self.graph.remove_nodes_from(isolated_nodes)
|
230
|
+
|
231
|
+
def is_removed(self, node: Union[int, Node]) -> bool:
|
232
|
+
"""Returns whether the node is removed from the graph.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
node:
|
236
|
+
The node to check. If it is a ``Node``, its `node_id` is used
|
237
|
+
as the node to check. Otherwise, it is assumed to be the
|
238
|
+
``node_id`` of the node to check.
|
239
|
+
"""
|
240
|
+
if isinstance(node, Node):
|
241
|
+
node = node.node_id
|
242
|
+
return self.removed_nodes[node]
|
243
|
+
|
244
|
+
def non_removed_nodes(self) -> List[Node]:
|
245
|
+
"""Returns the nodes that are not removed from the graph."""
|
246
|
+
return [node for node in self._nodes if not self.is_removed(node)]
|
247
|
+
|
248
|
+
def get_machine_node(self, machine_id: int) -> Node:
|
249
|
+
"""Returns the node representing the machine with the given id.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
machine_id: The id of the machine.
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
The node representing the machine with the given id.
|
256
|
+
"""
|
257
|
+
return self.get_node_by_type_and_id(
|
258
|
+
NodeType.MACHINE, machine_id, "machine_id"
|
259
|
+
)
|
260
|
+
|
261
|
+
def get_job_node(self, job_id: int) -> Node:
|
262
|
+
"""Returns the node representing the job with the given id.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
job_id: The id of the job.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
The node representing the job with the given id.
|
269
|
+
"""
|
270
|
+
return self.get_node_by_type_and_id(NodeType.JOB, job_id, "job_id")
|
271
|
+
|
272
|
+
def get_operation_node(self, operation_id: int) -> Node:
|
273
|
+
"""Returns the node representing the operation with the given id.
|
274
|
+
|
275
|
+
Args:
|
276
|
+
operation_id: The id of the operation.
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
The node representing the operation with the given id.
|
280
|
+
"""
|
281
|
+
return self.get_node_by_type_and_id(
|
282
|
+
NodeType.OPERATION, operation_id, "operation.operation_id"
|
283
|
+
)
|
284
|
+
|
285
|
+
def get_node_by_type_and_id(
|
286
|
+
self, node_type: NodeType, node_id: int, id_attr: str
|
287
|
+
) -> Node:
|
288
|
+
"""Generic method to get a node by type and id.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
node_type:
|
292
|
+
The type of the node.
|
293
|
+
node_id:
|
294
|
+
The id of the node.
|
295
|
+
id_attr:
|
296
|
+
The attribute name to compare the id. Can be nested like
|
297
|
+
'operation.operation_id'.
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
The node with the given id.
|
301
|
+
"""
|
302
|
+
|
303
|
+
def get_nested_attr(obj, attr_path: str):
|
304
|
+
"""Helper function to get nested attribute."""
|
305
|
+
attrs = attr_path.split(".")
|
306
|
+
for attr in attrs:
|
307
|
+
obj = getattr(obj, attr)
|
308
|
+
return obj
|
309
|
+
|
310
|
+
nodes = self._nodes_by_type[node_type]
|
311
|
+
if node_id < len(nodes):
|
312
|
+
node = nodes[node_id]
|
313
|
+
if get_nested_attr(node, id_attr) == node_id:
|
314
|
+
return node
|
315
|
+
|
316
|
+
for node in nodes:
|
317
|
+
if get_nested_attr(node, id_attr) == node_id:
|
318
|
+
return node
|
319
|
+
|
320
|
+
raise ValidationError(f"No node found with node.{id_attr}={node_id}")
|
@@ -0,0 +1,182 @@
|
|
1
|
+
"""Home of the `Node` class."""
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from job_shop_lib import Operation
|
6
|
+
from job_shop_lib.exceptions import (
|
7
|
+
UninitializedAttributeError,
|
8
|
+
ValidationError,
|
9
|
+
)
|
10
|
+
from job_shop_lib.graphs._constants import NodeType
|
11
|
+
|
12
|
+
|
13
|
+
class Node:
|
14
|
+
"""Represents a node in the :class:`JobShopGraph`.
|
15
|
+
|
16
|
+
A node is hashable by its id. The id is assigned when the node is added to
|
17
|
+
the graph. The id must be unique for each node in the graph, and should be
|
18
|
+
used to identify the node in the networkx graph.
|
19
|
+
|
20
|
+
Depending on the type of the node, it can have different attributes. The
|
21
|
+
following table shows the attributes of each type of node:
|
22
|
+
|
23
|
+
+----------------+---------------------+
|
24
|
+
| Node Type | Required Attribute |
|
25
|
+
+================+=====================+
|
26
|
+
| OPERATION | ``operation`` |
|
27
|
+
+----------------+---------------------+
|
28
|
+
| MACHINE | ``machine_id`` |
|
29
|
+
+----------------+---------------------+
|
30
|
+
| JOB | ``job_id`` |
|
31
|
+
+----------------+---------------------+
|
32
|
+
|
33
|
+
In terms of equality, two nodes are equal if they have the same id.
|
34
|
+
Additionally, one node is equal to an integer if the integer is equal to
|
35
|
+
its id. It is also hashable by its id.
|
36
|
+
|
37
|
+
This allows for using the node as a key in a dictionary, at the same time
|
38
|
+
we can use its id to index that dictionary. Example:
|
39
|
+
|
40
|
+
.. code-block:: python
|
41
|
+
|
42
|
+
node = Node(NodeType.SOURCE)
|
43
|
+
node.node_id = 1
|
44
|
+
graph = {node: "some value"}
|
45
|
+
print(graph[node]) # "some value"
|
46
|
+
print(graph[1]) # "some value"
|
47
|
+
|
48
|
+
Args:
|
49
|
+
node_type:
|
50
|
+
The type of the node. See :class:`NodeType` for theavailable types.
|
51
|
+
operation:
|
52
|
+
The operation of the node. Required if ``node_type`` is
|
53
|
+
:attr:`NodeType.OPERATION`.
|
54
|
+
machine_id:
|
55
|
+
The id of the machine. Required if ``node_type`` is
|
56
|
+
:attr:`NodeType.MACHINE`.
|
57
|
+
job_id:
|
58
|
+
The id of the job. Required if ``node_type`` is
|
59
|
+
:attr:`NodeType.JOB`.
|
60
|
+
|
61
|
+
Raises:
|
62
|
+
ValidationError:
|
63
|
+
If the ``node_type`` is :attr:`NodeType.OPERATION`,
|
64
|
+
:attr:`NodeType.MACHINE`, or :attr:`NodeType.JOB` and the
|
65
|
+
corresponding ``operation``, ``machine_id``, or ``job_id`` is
|
66
|
+
``None``, respectively.
|
67
|
+
|
68
|
+
"""
|
69
|
+
|
70
|
+
__slots__ = {
|
71
|
+
"node_type": "The type of the node.",
|
72
|
+
"_node_id": "Unique identifier for the node.",
|
73
|
+
"_operation": (
|
74
|
+
"The operation associated with the node."
|
75
|
+
),
|
76
|
+
"_machine_id": (
|
77
|
+
"The machine ID associated with the node."
|
78
|
+
),
|
79
|
+
"_job_id": "The job ID associated with the node.",
|
80
|
+
}
|
81
|
+
|
82
|
+
def __init__(
|
83
|
+
self,
|
84
|
+
node_type: NodeType,
|
85
|
+
operation: Optional[Operation] = None,
|
86
|
+
machine_id: Optional[int] = None,
|
87
|
+
job_id: Optional[int] = None,
|
88
|
+
):
|
89
|
+
if node_type == NodeType.OPERATION and operation is None:
|
90
|
+
raise ValidationError("Operation node must have an operation.")
|
91
|
+
|
92
|
+
if node_type == NodeType.MACHINE and machine_id is None:
|
93
|
+
raise ValidationError("Machine node must have a machine_id.")
|
94
|
+
|
95
|
+
if node_type == NodeType.JOB and job_id is None:
|
96
|
+
raise ValidationError("Job node must have a job_id.")
|
97
|
+
|
98
|
+
self.node_type: NodeType = node_type
|
99
|
+
self._node_id: Optional[int] = None
|
100
|
+
|
101
|
+
self._operation = operation
|
102
|
+
self._machine_id = machine_id
|
103
|
+
self._job_id = job_id
|
104
|
+
|
105
|
+
@property
|
106
|
+
def node_id(self) -> int:
|
107
|
+
"""Returns a unique identifier for the node."""
|
108
|
+
if self._node_id is None:
|
109
|
+
raise UninitializedAttributeError(
|
110
|
+
"Node has not been assigned an id."
|
111
|
+
)
|
112
|
+
return self._node_id
|
113
|
+
|
114
|
+
@node_id.setter
|
115
|
+
def node_id(self, value: int) -> None:
|
116
|
+
self._node_id = value
|
117
|
+
|
118
|
+
@property
|
119
|
+
def operation(self) -> Operation:
|
120
|
+
"""Returns the operation of the node.
|
121
|
+
|
122
|
+
This property is mandatory for nodes of type
|
123
|
+
:class:`NodeType.OPERATION`.
|
124
|
+
|
125
|
+
Raises:
|
126
|
+
UninitializedAttributeError: If the node has no operation.
|
127
|
+
"""
|
128
|
+
if self._operation is None:
|
129
|
+
raise UninitializedAttributeError("Node has no operation.")
|
130
|
+
return self._operation
|
131
|
+
|
132
|
+
@property
|
133
|
+
def machine_id(self) -> int:
|
134
|
+
"""Returns the `machine_id` of the node.
|
135
|
+
|
136
|
+
This property is mandatory for nodes of type `MACHINE`.
|
137
|
+
|
138
|
+
Raises:
|
139
|
+
UninitializedAttributeError: If the node has no ``machine_id``.
|
140
|
+
"""
|
141
|
+
if self._machine_id is None:
|
142
|
+
raise UninitializedAttributeError("Node has no ``machine_id``.")
|
143
|
+
return self._machine_id
|
144
|
+
|
145
|
+
@property
|
146
|
+
def job_id(self) -> int:
|
147
|
+
"""Returns the `job_id` of the node.
|
148
|
+
|
149
|
+
This property is mandatory for nodes of type `JOB`.
|
150
|
+
|
151
|
+
Raises:
|
152
|
+
UninitializedAttributeError: If the node has no `job_id`.
|
153
|
+
"""
|
154
|
+
if self._job_id is None:
|
155
|
+
raise UninitializedAttributeError("Node has no `job_id`.")
|
156
|
+
return self._job_id
|
157
|
+
|
158
|
+
def __hash__(self) -> int:
|
159
|
+
return self.node_id
|
160
|
+
|
161
|
+
def __eq__(self, __value: object) -> bool:
|
162
|
+
if isinstance(__value, Node):
|
163
|
+
__value = __value.node_id
|
164
|
+
return self.node_id == __value
|
165
|
+
|
166
|
+
def __repr__(self) -> str:
|
167
|
+
if self.node_type == NodeType.OPERATION:
|
168
|
+
return (
|
169
|
+
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
170
|
+
f"operation={self.operation})"
|
171
|
+
)
|
172
|
+
if self.node_type == NodeType.MACHINE:
|
173
|
+
return (
|
174
|
+
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
175
|
+
f"machine_id={self._machine_id})"
|
176
|
+
)
|
177
|
+
if self.node_type == NodeType.JOB:
|
178
|
+
return (
|
179
|
+
f"Node(node_type={self.node_type.name}, id={self._node_id}, "
|
180
|
+
f"job_id={self._job_id})"
|
181
|
+
)
|
182
|
+
return f"Node(node_type={self.node_type.name}, id={self._node_id})"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""Contains classes and functions for updating the graph representation of the
|
2
|
+
job shop scheduling problem.
|
3
|
+
|
4
|
+
Currently, the following classes and utilities are available:
|
5
|
+
|
6
|
+
.. autosummary::
|
7
|
+
|
8
|
+
GraphUpdater
|
9
|
+
ResidualGraphUpdater
|
10
|
+
DisjunctiveGraphUpdater
|
11
|
+
remove_completed_operations
|
12
|
+
|
13
|
+
"""
|
14
|
+
|
15
|
+
from ._graph_updater import GraphUpdater
|
16
|
+
from ._utils import remove_completed_operations
|
17
|
+
from ._residual_graph_updater import ResidualGraphUpdater
|
18
|
+
from ._disjunctive_graph_updater import DisjunctiveGraphUpdater
|
19
|
+
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
"GraphUpdater",
|
23
|
+
"remove_completed_operations",
|
24
|
+
"ResidualGraphUpdater",
|
25
|
+
"DisjunctiveGraphUpdater",
|
26
|
+
]
|