job-shop-lib 1.0.0b5__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.
- job_shop_lib/__init__.py +1 -1
- job_shop_lib/_operation.py +9 -3
- job_shop_lib/_scheduled_operation.py +3 -0
- job_shop_lib/dispatching/_dispatcher.py +6 -13
- job_shop_lib/dispatching/_factories.py +3 -3
- job_shop_lib/dispatching/_optimal_operations_observer.py +0 -2
- job_shop_lib/dispatching/_ready_operation_filters.py +4 -4
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +10 -5
- job_shop_lib/dispatching/feature_observers/_factory.py +8 -3
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +1 -1
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +35 -67
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +1 -1
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +3 -2
- job_shop_lib/graphs/__init__.py +2 -0
- job_shop_lib/graphs/_build_resource_task_graphs.py +1 -1
- job_shop_lib/graphs/_job_shop_graph.py +38 -19
- job_shop_lib/graphs/graph_updaters/__init__.py +3 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +3 -1
- job_shop_lib/graphs/graph_updaters/_utils.py +2 -2
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +4 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +1 -1
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +102 -24
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +11 -2
- job_shop_lib/reinforcement_learning/_types_and_constants.py +11 -10
- job_shop_lib/reinforcement_learning/_utils.py +29 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +5 -2
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +53 -19
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/METADATA +4 -10
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/RECORD +33 -31
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0b5.dist-info → job_shop_lib-1.0.1.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
job_shop_lib/_operation.py
CHANGED
@@ -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
|
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
|
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
|
|
@@ -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.
|
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[
|
461
|
+
def scheduled_operations(self) -> List[ScheduledOperation]:
|
463
462
|
"""Returns the list of operations that have been scheduled."""
|
464
463
|
scheduled_operations = []
|
465
|
-
for
|
466
|
-
|
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[
|
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.
|
99
|
-
|
100
|
-
|
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
|
-
|
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] *
|
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 =
|
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
|
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[
|
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
|
-
|
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``.
|
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.
|
61
|
-
|
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.
|
64
|
-
(dispatcher.instance.
|
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
|
-
|
114
|
-
|
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.
|
123
|
-
|
124
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
self.
|
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
|
-
|
51
|
-
not
|
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],
|
job_shop_lib/graphs/__init__.py
CHANGED
@@ -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
|
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
|
136
|
-
|
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
|
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:
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
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
|
-
|
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:
|
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:
|
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
|
-
|
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=
|
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
|
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
|
-
|
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
|