job-shop-lib 1.0.0b4__py3-none-any.whl → 1.0.1__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 (33) hide show
  1. job_shop_lib/__init__.py +1 -1
  2. job_shop_lib/_operation.py +9 -3
  3. job_shop_lib/_scheduled_operation.py +3 -0
  4. job_shop_lib/dispatching/_dispatcher.py +6 -13
  5. job_shop_lib/dispatching/_factories.py +3 -3
  6. job_shop_lib/dispatching/_optimal_operations_observer.py +0 -2
  7. job_shop_lib/dispatching/_ready_operation_filters.py +4 -4
  8. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +10 -5
  9. job_shop_lib/dispatching/feature_observers/_factory.py +8 -3
  10. job_shop_lib/dispatching/feature_observers/_feature_observer.py +1 -1
  11. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +35 -67
  12. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +1 -1
  13. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +3 -2
  14. job_shop_lib/graphs/__init__.py +2 -0
  15. job_shop_lib/graphs/_build_resource_task_graphs.py +1 -1
  16. job_shop_lib/graphs/_job_shop_graph.py +38 -19
  17. job_shop_lib/graphs/graph_updaters/__init__.py +3 -0
  18. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  19. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +3 -1
  20. job_shop_lib/graphs/graph_updaters/_utils.py +2 -2
  21. job_shop_lib/py.typed +0 -0
  22. job_shop_lib/reinforcement_learning/__init__.py +4 -0
  23. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +1 -1
  24. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +117 -46
  25. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +11 -2
  26. job_shop_lib/reinforcement_learning/_types_and_constants.py +11 -10
  27. job_shop_lib/reinforcement_learning/_utils.py +29 -0
  28. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +5 -2
  29. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +53 -19
  30. {job_shop_lib-1.0.0b4.dist-info → job_shop_lib-1.0.1.dist-info}/METADATA +4 -10
  31. {job_shop_lib-1.0.0b4.dist-info → job_shop_lib-1.0.1.dist-info}/RECORD +33 -31
  32. {job_shop_lib-1.0.0b4.dist-info → job_shop_lib-1.0.1.dist-info}/LICENSE +0 -0
  33. {job_shop_lib-1.0.0b4.dist-info → job_shop_lib-1.0.1.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py CHANGED
@@ -19,7 +19,7 @@ from job_shop_lib._schedule import Schedule
19
19
  from job_shop_lib._base_solver import BaseSolver, Solver
20
20
 
21
21
 
22
- __version__ = "1.0.0-b.4"
22
+ __version__ = "1.0.1"
23
23
 
24
24
  __all__ = [
25
25
  "Operation",
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Union, List
6
6
 
7
- from job_shop_lib.exceptions import UninitializedAttributeError
7
+ from job_shop_lib.exceptions import ValidationError
8
8
 
9
9
 
10
10
  class Operation:
@@ -81,8 +81,14 @@ class Operation:
81
81
  If the operation has multiple machines in its list.
82
82
  """
83
83
  if len(self.machines) > 1:
84
- raise UninitializedAttributeError(
85
- "Operation has multiple machines."
84
+ raise ValidationError(
85
+ "Operation has multiple machines. The `machine_id` property "
86
+ "should only be used when working with a classic JSSP "
87
+ "instance. This error prevents silent bugs. To handle "
88
+ "operations with more machines you have to use the machines "
89
+ "attribute. If you get this error using `job_shop_lib` "
90
+ "objects, it means that that object does not support "
91
+ "operations with multiple machines yet."
86
92
  )
87
93
  return self.machines[0]
88
94
 
@@ -80,3 +80,6 @@ class ScheduledOperation:
80
80
  and self.start_time == value.start_time
81
81
  and self.machine_id == value.machine_id
82
82
  )
83
+
84
+ def __hash__(self) -> int:
85
+ return hash((self.operation, self.start_time, self.machine_id))
@@ -336,8 +336,7 @@ class Dispatcher:
336
336
  The operation to be scheduled.
337
337
  machine_id:
338
338
  The id of the machine on which the operation is to be
339
- scheduled. If ``None``, the start time is computed based on the
340
- next available time for the operation on any machine.
339
+ scheduled.
341
340
  """
342
341
  return max(
343
342
  self._machine_next_available_time[machine_id],
@@ -459,12 +458,11 @@ class Dispatcher:
459
458
  return unscheduled_operations
460
459
 
461
460
  @_dispatcher_cache
462
- def scheduled_operations(self) -> List[Operation]:
461
+ def scheduled_operations(self) -> List[ScheduledOperation]:
463
462
  """Returns the list of operations that have been scheduled."""
464
463
  scheduled_operations = []
465
- for job_id, next_position in enumerate(self._job_next_operation_index):
466
- operations = self.instance.jobs[job_id][:next_position]
467
- scheduled_operations.extend(operations)
464
+ for machine_schedule in self.schedule.schedule:
465
+ scheduled_operations.extend(machine_schedule)
468
466
  return scheduled_operations
469
467
 
470
468
  @_dispatcher_cache
@@ -532,19 +530,14 @@ class Dispatcher:
532
530
  return scheduled_operation.end_time - adjusted_start_time
533
531
 
534
532
  @_dispatcher_cache
535
- def completed_operations(self) -> Set[Operation]:
533
+ def completed_operations(self) -> Set[ScheduledOperation]:
536
534
  """Returns the set of operations that have been completed.
537
535
 
538
536
  This method returns the operations that have been scheduled and the
539
537
  current time is greater than or equal to the end time of the operation.
540
538
  """
541
539
  scheduled_operations = set(self.scheduled_operations())
542
- ongoing_operations = set(
543
- map(
544
- lambda scheduled_op: scheduled_op.operation,
545
- self.ongoing_operations(),
546
- )
547
- )
540
+ ongoing_operations = set(self.ongoing_operations())
548
541
  completed_operations = scheduled_operations - ongoing_operations
549
542
  return completed_operations
550
543
 
@@ -95,9 +95,9 @@ def ready_operations_filter_factory(
95
95
 
96
96
  Args:
97
97
  filter_name:
98
- The name of the filter function to be used. Supported
99
- values are 'dominated_operations' and
100
- 'immediate_machine_operations'.
98
+ The name of the filter function to be used. See
99
+ :class:`ReadyOperationsFilterType` for supported values.
100
+ Alternatively, a custom filter function can be passed directly.
101
101
 
102
102
  Returns:
103
103
  A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
@@ -20,8 +20,6 @@ class OptimalOperationsObserver(DispatcherObserver):
20
20
  based on the reference schedule.
21
21
  reference_schedule: The reference schedule used to determine optimal
22
22
  operations.
23
- _operation_to_scheduled: Dictionary mapping operations to their
24
- scheduled versions in the reference schedule.
25
23
 
26
24
  Args:
27
25
  dispatcher: The dispatcher instance to observe.
@@ -153,16 +153,16 @@ def _get_min_machine_end_times(
153
153
 
154
154
 
155
155
  def _get_immediate_machines(
156
- self: Dispatcher, available_operations: List[Operation]
156
+ dispatcher: Dispatcher, available_operations: List[Operation]
157
157
  ) -> List[bool]:
158
158
  """Returns the machine ids of the machines that have at least one
159
159
  operation with the lowest start time (i.e. the start time)."""
160
- working_machines = [False] * self.instance.num_machines
160
+ working_machines = [False] * dispatcher.instance.num_machines
161
161
  # We can't use the current_time directly because it will cause
162
162
  # an infinite loop.
163
- current_time = self.min_start_time(available_operations)
163
+ current_time = dispatcher.min_start_time(available_operations)
164
164
  for op in available_operations:
165
165
  for machine_id in op.machines:
166
- if self.start_time(op, machine_id) == current_time:
166
+ if dispatcher.start_time(op, machine_id) == current_time:
167
167
  working_machines[machine_id] = True
168
168
  return working_machines
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections import defaultdict
4
4
  from collections.abc import Sequence
5
- from typing import List, Dict, Union, Optional
5
+ from typing import List, Dict, Union, Optional, Type
6
6
  # The Self type can be imported directly from Python’s typing module in
7
7
  # version 3.11 and beyond. We use the typing_extensions module to support
8
8
  # python >=3.8
@@ -18,6 +18,7 @@ from job_shop_lib.dispatching.feature_observers import (
18
18
  FeatureType,
19
19
  FeatureObserverConfig,
20
20
  feature_observer_factory,
21
+ FeatureObserverType,
21
22
  )
22
23
 
23
24
 
@@ -104,7 +105,14 @@ class CompositeFeatureObserver(FeatureObserver):
104
105
  def from_feature_observer_configs(
105
106
  cls,
106
107
  dispatcher: Dispatcher,
107
- feature_observer_configs: Sequence[FeatureObserverConfig],
108
+ feature_observer_configs: Sequence[
109
+ Union[
110
+ str,
111
+ FeatureObserverType,
112
+ Type[FeatureObserver],
113
+ FeatureObserverConfig,
114
+ ],
115
+ ],
108
116
  subscribe: bool = True,
109
117
  ) -> Self:
110
118
  """Creates the composite feature observer.
@@ -178,9 +186,6 @@ if __name__ == "__main__":
178
186
  import time
179
187
  from job_shop_lib.benchmarking import load_benchmark_instance
180
188
  from job_shop_lib.dispatching.rules import DispatchingRuleSolver
181
- from job_shop_lib.dispatching.feature_observers import (
182
- FeatureObserverType,
183
- )
184
189
 
185
190
  ta80 = load_benchmark_instance("ta80")
186
191
 
@@ -39,9 +39,14 @@ class FeatureObserverType(str, Enum):
39
39
  # FeatureObserverConfig = DispatcherObserverConfig[
40
40
  # Type[FeatureObserver] | FeatureObserverType | str
41
41
  # ]
42
- FeatureObserverConfig = DispatcherObserverConfig[
43
- Union[Type[FeatureObserver], FeatureObserverType, str]
44
- ]
42
+ # FeatureObserverConfig = DispatcherObserverConfig[
43
+ # Union[Type[FeatureObserver], FeatureObserverType, str]
44
+ # ]
45
+ FeatureObserverConfig = (
46
+ DispatcherObserverConfig[Type[FeatureObserver]]
47
+ | DispatcherObserverConfig[FeatureObserverType]
48
+ | DispatcherObserverConfig[str]
49
+ )
45
50
 
46
51
 
47
52
  def feature_observer_factory(
@@ -36,7 +36,7 @@ class FeatureObserver(DispatcherObserver):
36
36
  individually. Furthermore, machine learning models can be trained on these
37
37
  arrays to predict the best dispatching decisions.
38
38
 
39
- Arrays use the data type ``np.float32``. This is because most machine
39
+ Arrays use the data type ``np.float32``.
40
40
 
41
41
  New :class:`FeatureObservers` must inherit from this class, and re-define
42
42
  the class attributes ``_singleton`` (defualt ), ``_feature_size``
@@ -4,14 +4,10 @@ from typing import Optional, Union, List
4
4
  import numpy as np
5
5
 
6
6
  from job_shop_lib import ScheduledOperation
7
- from job_shop_lib.dispatching import (
8
- Dispatcher,
9
- DispatcherObserver,
10
- )
7
+ from job_shop_lib.dispatching import Dispatcher
11
8
  from job_shop_lib.dispatching.feature_observers import (
12
9
  FeatureObserver,
13
10
  FeatureType,
14
- RemainingOperationsObserver,
15
11
  )
16
12
 
17
13
 
@@ -40,15 +36,6 @@ class IsCompletedObserver(FeatureObserver):
40
36
  or manually updated.
41
37
  """
42
38
 
43
- __slots__ = {
44
- "remaining_ops_per_machine": (
45
- "The number of unscheduled operations per machine."
46
- ),
47
- "remaining_ops_per_job": (
48
- "The number of unscheduled operations per job."
49
- ),
50
- }
51
-
52
39
  def __init__(
53
40
  self,
54
41
  dispatcher: Dispatcher,
@@ -57,11 +44,16 @@ class IsCompletedObserver(FeatureObserver):
57
44
  subscribe: bool = True,
58
45
  ):
59
46
  feature_types = self._get_feature_types_list(feature_types)
60
- self.remaining_ops_per_machine = np.zeros(
61
- (dispatcher.instance.num_machines, 1), dtype=int
47
+ self._num_of_operations_per_machine = np.array(
48
+ [
49
+ len(operations_by_machine)
50
+ for operations_by_machine in (
51
+ dispatcher.instance.operations_by_machine
52
+ )
53
+ ]
62
54
  )
63
- self.remaining_ops_per_job = np.zeros(
64
- (dispatcher.instance.num_jobs, 1), dtype=int
55
+ self._num_of_operations_per_job = np.array(
56
+ [len(job) for job in dispatcher.instance.jobs]
65
57
  )
66
58
  super().__init__(
67
59
  dispatcher,
@@ -70,60 +62,36 @@ class IsCompletedObserver(FeatureObserver):
70
62
  )
71
63
 
72
64
  def initialize_features(self):
73
- def _has_same_features(observer: DispatcherObserver) -> bool:
74
- if not isinstance(observer, RemainingOperationsObserver):
75
- return False
76
- return all(
77
- feature_type in observer.features
78
- for feature_type in remaining_ops_feature_types
79
- )
80
-
81
- self.set_features_to_zero()
82
-
83
- remaining_ops_feature_types = [
84
- feature_type
85
- for feature_type in self.features.keys()
86
- if feature_type != FeatureType.OPERATIONS
87
- ]
88
- remaining_ops_observer = self.dispatcher.create_or_get_observer(
89
- RemainingOperationsObserver,
90
- condition=_has_same_features,
91
- feature_types=remaining_ops_feature_types,
92
- )
93
- if FeatureType.JOBS in self.features:
94
- self.remaining_ops_per_job = remaining_ops_observer.features[
95
- FeatureType.JOBS
96
- ].copy()
97
- if FeatureType.MACHINES in self.features:
98
- self.remaining_ops_per_machine = remaining_ops_observer.features[
99
- FeatureType.MACHINES
100
- ].copy()
101
-
102
- def reset(self):
103
- self.initialize_features()
104
-
105
- def update(self, scheduled_operation: ScheduledOperation):
106
65
  if FeatureType.OPERATIONS in self.features:
107
66
  completed_operations = [
108
- op.operation_id
67
+ op.operation.operation_id
109
68
  for op in self.dispatcher.completed_operations()
110
69
  ]
111
70
  self.features[FeatureType.OPERATIONS][completed_operations, 0] = 1
112
71
  if FeatureType.MACHINES in self.features:
113
- self.remaining_ops_per_machine[
114
- scheduled_operation.operation.machines, 0
115
- ] -= 1
116
- is_completed = (
117
- self.remaining_ops_per_machine[
118
- scheduled_operation.operation.machines, 0
119
- ]
120
- == 0
72
+ num_completed_ops_per_machine = np.zeros(
73
+ len(self._num_of_operations_per_machine)
121
74
  )
122
- self.features[FeatureType.MACHINES][
123
- scheduled_operation.operation.machines, 0
124
- ] = is_completed
75
+ for op in self.dispatcher.completed_operations():
76
+ for machine_id in op.operation.machines:
77
+ num_completed_ops_per_machine[machine_id] += 1
78
+ self.features[FeatureType.MACHINES][:, 0] = (
79
+ num_completed_ops_per_machine
80
+ == self._num_of_operations_per_machine
81
+ ).astype(np.float32)
125
82
  if FeatureType.JOBS in self.features:
126
- job_id = scheduled_operation.job_id
127
- self.remaining_ops_per_job[job_id, 0] -= 1
128
- is_completed = self.remaining_ops_per_job[job_id, 0] == 0
129
- self.features[FeatureType.JOBS][job_id, 0] = is_completed
83
+ num_completed_ops_per_job = np.zeros(
84
+ len(self._num_of_operations_per_job)
85
+ )
86
+ for op in self.dispatcher.completed_operations():
87
+ num_completed_ops_per_job[op.operation.job_id] += 1
88
+ self.features[FeatureType.JOBS][:, 0] = (
89
+ num_completed_ops_per_job
90
+ == self._num_of_operations_per_job
91
+ ).astype(np.float32)
92
+
93
+ def reset(self):
94
+ self.set_features_to_zero()
95
+
96
+ def update(self, scheduled_operation: ScheduledOperation):
97
+ self.initialize_features()
@@ -33,7 +33,7 @@ class DispatchingRuleType(str, Enum):
33
33
 
34
34
 
35
35
  def dispatching_rule_factory(
36
- dispatching_rule: Union[str, DispatchingRuleType,]
36
+ dispatching_rule: Union[str, DispatchingRuleType],
37
37
  ) -> Callable[[Dispatcher], Operation]:
38
38
  """Creates and returns a dispatching rule function based on the specified
39
39
  dispatching rule name.
@@ -47,8 +47,9 @@ def machine_chooser_factory(
47
47
  machine chooser strategy.
48
48
 
49
49
  Raises:
50
- ValueError: If the machine_chooser argument is not recognized or is
51
- not supported.
50
+ ValidationError:
51
+ If the ``machine_chooser`` argument is not recognized or
52
+ is not supported.
52
53
  """
53
54
  machine_choosers: Dict[str, Callable[[Dispatcher, Operation], int]] = {
54
55
  MachineChooserType.FIRST: lambda _, operation: operation.machines[0],
@@ -38,6 +38,7 @@ from job_shop_lib.graphs._build_resource_task_graphs import (
38
38
  add_global_node,
39
39
  add_machine_global_edges,
40
40
  add_job_global_edges,
41
+ add_job_job_edges,
41
42
  )
42
43
 
43
44
 
@@ -65,4 +66,5 @@ __all__ = [
65
66
  "add_machine_global_edges",
66
67
  "add_job_global_edges",
67
68
  "build_solved_disjunctive_graph",
69
+ "add_job_job_edges",
68
70
  ]
@@ -1,7 +1,7 @@
1
1
  """Contains helper functions to build the resource-task graphs from a job shop
2
2
  instance.
3
3
 
4
- The agent-task graph (renamed to resource-task graph) was introduced by
4
+ The resource-task graph (originally called agent-task graph) was introduced by
5
5
  Junyoung Park et al. (2021).
6
6
  In contrast to the disjunctive graph, instead of connecting operations that
7
7
  share the same resources directly by disjunctive edges, operation nodes are
@@ -132,15 +132,13 @@ class JobShopGraph:
132
132
 
133
133
  This method assigns a unique identifier to the node, adds it to the
134
134
  graph, and updates the nodes list and the nodes_by_type dictionary. If
135
- the node is of type `OPERATION`, it also updates `nodes_by_job` and
136
- `nodes_by_machine` based on the operation's job_id and machine_ids.
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.
137
138
 
138
139
  Args:
139
- node_for_adding (Node): The node to be added to the graph.
140
-
141
- Raises:
142
- ValueError: If the node type is unsupported or if required
143
- attributes for the node type are missing.
140
+ node_for_adding:
141
+ The node to be added to the graph.
144
142
 
145
143
  Note:
146
144
  This method directly modifies the graph attribute as well as
@@ -171,17 +169,25 @@ class JobShopGraph:
171
169
  ) -> None:
172
170
  """Adds an edge to the graph.
173
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
+
174
177
  Args:
175
- u_of_edge: The source node of the edge. If it is a `Node`, its
176
- `node_id` is used as the source. Otherwise, it is assumed to be
177
- the node_id of the source.
178
- v_of_edge: The destination node of the edge. If it is a `Node`, its
179
- `node_id` is used as the destination. Otherwise, it is assumed
180
- to be the node_id of the destination.
181
- **attr: Additional attributes to be added to the edge.
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.
182
188
 
183
189
  Raises:
184
- ValidationError: If `u_of_edge` or `v_of_edge` are not in the
190
+ ValidationError: If ``u_of_edge`` or ``v_of_edge`` are not in the
185
191
  graph.
186
192
  """
187
193
  if isinstance(u_of_edge, Node):
@@ -192,18 +198,30 @@ class JobShopGraph:
192
198
  raise ValidationError(
193
199
  "`u_of_edge` and `v_of_edge` must be in the graph."
194
200
  )
195
- self.graph.add_edge(u_of_edge, v_of_edge, **attr)
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)
196
211
 
197
212
  def remove_node(self, node_id: int) -> None:
198
213
  """Removes a node from the graph and the isolated nodes that result
199
214
  from the removal.
200
215
 
201
216
  Args:
202
- node_id: The id of the node to remove.
217
+ node_id:
218
+ The id of the node to remove.
203
219
  """
204
220
  self.graph.remove_node(node_id)
205
221
  self.removed_nodes[node_id] = True
206
222
 
223
+ def remove_isolated_nodes(self) -> None:
224
+ """Removes isolated nodes from the graph."""
207
225
  isolated_nodes = list(nx.isolates(self.graph))
208
226
  for isolated_node in isolated_nodes:
209
227
  self.removed_nodes[isolated_node] = True
@@ -214,9 +232,10 @@ class JobShopGraph:
214
232
  """Returns whether the node is removed from the graph.
215
233
 
216
234
  Args:
217
- node: The node to check. If it is a `Node`, its `node_id` is used
235
+ node:
236
+ The node to check. If it is a ``Node``, its `node_id` is used
218
237
  as the node to check. Otherwise, it is assumed to be the
219
- `node_id` of the node to check.
238
+ ``node_id`` of the node to check.
220
239
  """
221
240
  if isinstance(node, Node):
222
241
  node = node.node_id
@@ -7,6 +7,7 @@ Currently, the following classes and utilities are available:
7
7
 
8
8
  GraphUpdater
9
9
  ResidualGraphUpdater
10
+ DisjunctiveGraphUpdater
10
11
  remove_completed_operations
11
12
 
12
13
  """
@@ -14,10 +15,12 @@ Currently, the following classes and utilities are available:
14
15
  from ._graph_updater import GraphUpdater
15
16
  from ._utils import remove_completed_operations
16
17
  from ._residual_graph_updater import ResidualGraphUpdater
18
+ from ._disjunctive_graph_updater import DisjunctiveGraphUpdater
17
19
 
18
20
 
19
21
  __all__ = [
20
22
  "GraphUpdater",
21
23
  "remove_completed_operations",
22
24
  "ResidualGraphUpdater",
25
+ "DisjunctiveGraphUpdater",
23
26
  ]
@@ -0,0 +1,108 @@
1
+ """Home of the `ResidualGraphUpdater` class."""
2
+
3
+ from job_shop_lib import ScheduledOperation
4
+ from job_shop_lib.graphs.graph_updaters import (
5
+ ResidualGraphUpdater,
6
+ )
7
+ from job_shop_lib.exceptions import ValidationError
8
+
9
+
10
+ class DisjunctiveGraphUpdater(ResidualGraphUpdater):
11
+ """Updates the graph based on the completed operations.
12
+
13
+ This observer updates the graph by removing the completed
14
+ operation, machine and job nodes from the graph. It subscribes to the
15
+ :class:`~job_shop_lib.dispatching.feature_observers.IsCompletedObserver`
16
+ to determine which operations, machines and jobs have been completed.
17
+
18
+ After an operation is dispatched, one of two disjunctive arcs that
19
+ connected it with the previous operation is dropped. Similarly, the
20
+ disjunctive arcs associated with the previous scheduled operation are
21
+ removed.
22
+
23
+ Attributes:
24
+ remove_completed_machine_nodes:
25
+ If ``True``, removes completed machine nodes from the graph.
26
+ remove_completed_job_nodes:
27
+ If ``True``, removes completed job nodes from the graph.
28
+
29
+ Args:
30
+ dispatcher:
31
+ The dispatcher instance to observe.
32
+ job_shop_graph:
33
+ The job shop graph to update.
34
+ subscribe:
35
+ If ``True``, automatically subscribes the observer to the
36
+ dispatcher. Defaults to ``True``.
37
+ remove_completed_machine_nodes:
38
+ If ``True``, removes completed machine nodes from the graph.
39
+ Defaults to ``True``.
40
+ remove_completed_job_nodes:
41
+ If ``True``, removes completed job nodes from the graph.
42
+ Defaults to ``True``.
43
+ """
44
+
45
+ def update(self, scheduled_operation: ScheduledOperation) -> None:
46
+ """Updates the disjunctive graph.
47
+
48
+ After an operation is dispatched, one of two arcs that connected it
49
+ with the previous operation is dropped. Similarly, the disjunctive
50
+ arcs associated with the previous scheduled operation are removed.
51
+
52
+ Args:
53
+ scheduled_operation:
54
+ The scheduled operation that was dispatched.
55
+ """
56
+ super().update(scheduled_operation)
57
+ machine_schedule = self.dispatcher.schedule.schedule[
58
+ scheduled_operation.machine_id
59
+ ]
60
+ if len(machine_schedule) <= 1:
61
+ return
62
+
63
+ previous_scheduled_operation = machine_schedule[-2]
64
+
65
+ # Remove the disjunctive arcs between the scheduled operation and the
66
+ # previous operation
67
+ scheduled_operation_node = self.job_shop_graph.nodes[
68
+ scheduled_operation.operation.operation_id
69
+ ]
70
+ if (
71
+ scheduled_operation_node.operation
72
+ is not scheduled_operation.operation
73
+ ):
74
+ raise ValidationError(
75
+ "Scheduled operation node does not match scheduled operation."
76
+ "Make sure that the operation nodes have been the first to be "
77
+ "added to the graph. This method assumes that the operation id"
78
+ " and node id are the same."
79
+ )
80
+ scheduled_id = scheduled_operation_node.node_id
81
+ assert scheduled_id == scheduled_operation.operation.operation_id
82
+ previous_id = previous_scheduled_operation.operation.operation_id
83
+ if self.job_shop_graph.is_removed(
84
+ previous_id
85
+ ) or self.job_shop_graph.is_removed(scheduled_id):
86
+ return
87
+ self.job_shop_graph.graph.remove_edge(scheduled_id, previous_id)
88
+
89
+ # Now, remove all the disjunctive edges between the previous scheduled
90
+ # operation and the other operations in the machine schedule
91
+ operations_with_same_machine = (
92
+ self.dispatcher.instance.operations_by_machine[
93
+ scheduled_operation.machine_id
94
+ ]
95
+ )
96
+ already_scheduled_operations = {
97
+ scheduled_op.operation.operation_id
98
+ for scheduled_op in machine_schedule
99
+ }
100
+ for operation in operations_with_same_machine:
101
+ if operation.operation_id in already_scheduled_operations:
102
+ continue
103
+ self.job_shop_graph.graph.remove_edge(
104
+ previous_id, operation.operation_id
105
+ )
106
+ self.job_shop_graph.graph.remove_edge(
107
+ operation.operation_id, previous_id
108
+ )
@@ -112,7 +112,9 @@ class ResidualGraphUpdater(GraphUpdater):
112
112
  """Updates the residual graph based on the completed operations."""
113
113
  remove_completed_operations(
114
114
  self.job_shop_graph,
115
- completed_operations=self.dispatcher.completed_operations(),
115
+ completed_operations=(
116
+ op.operation for op in self.dispatcher.completed_operations()
117
+ ),
116
118
  )
117
119
  graph_has_machine_nodes = bool(
118
120
  self.job_shop_graph.nodes_by_type[NodeType.MACHINE]
@@ -1,4 +1,4 @@
1
- """Contains grah updater functions to update """
1
+ """Contains utility functions for updating the job shop graph."""
2
2
 
3
3
  from collections.abc import Iterable
4
4
 
@@ -13,7 +13,7 @@ def remove_completed_operations(
13
13
  """Removes the operation node of the scheduled operation from the graph.
14
14
 
15
15
  Args:
16
- graph:
16
+ job_shop_graph:
17
17
  The job shop graph to update.
18
18
  dispatcher:
19
19
  The dispatcher instance.
job_shop_lib/py.typed ADDED
File without changes