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.
Files changed (93) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +19 -0
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/_instance_generator.py +133 -0
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generators/__init__.py +0 -7
  84. job_shop_lib/generators/basic_generator.py +0 -197
  85. job_shop_lib/graphs/constants.py +0 -21
  86. job_shop_lib/graphs/job_shop_graph.py +0 -202
  87. job_shop_lib/graphs/node.py +0 -166
  88. job_shop_lib/operation.py +0 -122
  89. job_shop_lib/visualization/agent_task_graph.py +0 -257
  90. job_shop_lib/visualization/create_gif.py +0 -209
  91. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  92. job_shop_lib-0.5.0.dist-info/RECORD +0 -48
  93. {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 agent-task graph or one of
2
- its generalizations from a job shop instance.
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 Junyoung Park et al. (2021).
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 build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
27
- """Builds the agent-task graph of the instance with job and global nodes.
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 agent-task graph is a generalization of the agent-task graph
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 agent-task graph will be built.
40
+ The job shop instance in which the resource-task graph will be
41
+ built.
42
42
 
43
43
  Returns:
44
- The complete agent-task graph of the instance.
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 build_agent_task_graph_with_jobs(
61
+ def build_resource_task_graph_with_jobs(
62
62
  instance: JobShopInstance,
63
63
  ) -> JobShopGraph:
64
- """Builds the agent-task graph of the instance with job nodes.
64
+ """Builds the resource-task graph of the instance with job nodes.
65
65
 
66
- The agent-task graph with job nodes is a generalization of the agent-task
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 agent-task graph will be built.
73
+ The job shop instance in which the resource-task graph will be
74
+ built.
75
75
 
76
76
  Returns:
77
- The agent-task graph of the instance with job nodes.
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 build_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
93
- """Builds the agent-task graph of the instance.
92
+ def build_resource_task_graph(instance: JobShopInstance) -> JobShopGraph:
93
+ """Builds the resource-task graph of the instance.
94
94
 
95
- The agent-task graph was introduced by Junyoung Park et al. (2021).
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 agent-task graph will be built.
107
+ The job shop instance in which the resource-task graph will be
108
+ built.
107
109
 
108
110
  Returns:
109
- The agent-task graph of the instance.
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
+ ]