job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0a1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +16 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +9 -4
- job_shop_lib/_operation.py +95 -0
- job_shop_lib/{schedule.py → _schedule.py} +73 -54
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
- 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} +57 -18
- job_shop_lib/dispatching/__init__.py +45 -41
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
- job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
- job_shop_lib/dispatching/_factories.py +125 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
- job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
- job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
- job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
- job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
- job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +51 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
- job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
- job_shop_lib/dispatching/rules/_utils.py +127 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +2 -2
- job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
- job_shop_lib/graphs/__init__.py +17 -6
- job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
- job_shop_lib/graphs/{node.py → _node.py} +18 -12
- job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/reinforcement_learning/__init__.py +41 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
- job_shop_lib/reinforcement_learning/_utils.py +96 -0
- job_shop_lib/visualization/__init__.py +20 -4
- job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
- job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
- job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/factories.py +0 -206
- 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/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/generators/transformations.py +0 -164
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
- /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
- /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
- /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
- /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
- /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
- /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Home of the `UnscheduledOperationsObserver` class."""
|
2
|
+
|
3
|
+
import collections
|
4
|
+
from collections.abc import Iterable
|
5
|
+
import itertools
|
6
|
+
from typing import Deque
|
7
|
+
|
8
|
+
from job_shop_lib import Operation, ScheduledOperation
|
9
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
10
|
+
|
11
|
+
|
12
|
+
class UnscheduledOperationsObserver(DispatcherObserver):
|
13
|
+
"""Stores the operations that have not been dispatched yet.
|
14
|
+
|
15
|
+
This observer maintains a list of deques, each containing unscheduled
|
16
|
+
operations for a specific job. It provides methods to access and
|
17
|
+
manipulate unscheduled operations efficiently.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
|
21
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
22
|
+
self.unscheduled_operations_per_job: list[Deque[Operation]] = []
|
23
|
+
self.reset()
|
24
|
+
# In case the dispatcher has already scheduled some operations,
|
25
|
+
# we need to remove them.
|
26
|
+
# Note that we don't need to remove the operations in order.
|
27
|
+
for scheduled_operation in itertools.chain(
|
28
|
+
*self.dispatcher.schedule.schedule
|
29
|
+
):
|
30
|
+
self.update(scheduled_operation)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def unscheduled_operations(self) -> Iterable[Operation]:
|
34
|
+
"""An iterable of all unscheduled operations across all jobs."""
|
35
|
+
return itertools.chain(*self.unscheduled_operations_per_job)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def num_unscheduled_operations(self) -> int:
|
39
|
+
"""The total number of unscheduled operations."""
|
40
|
+
total_operations = self.dispatcher.instance.num_operations
|
41
|
+
num_scheduled_operations = (
|
42
|
+
self.dispatcher.schedule.num_scheduled_operations
|
43
|
+
)
|
44
|
+
return total_operations - num_scheduled_operations
|
45
|
+
|
46
|
+
def update(self, scheduled_operation: ScheduledOperation) -> None:
|
47
|
+
"""Removes a scheduled operation from the unscheduled operations.
|
48
|
+
|
49
|
+
This method is called by the dispatcher when an operation is
|
50
|
+
scheduled. It removes the operation from its job's deque of
|
51
|
+
unscheduled operations.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
scheduled_operation: The operation that has been scheduled.
|
55
|
+
"""
|
56
|
+
job_id = scheduled_operation.operation.job_id
|
57
|
+
job_deque = self.unscheduled_operations_per_job[job_id]
|
58
|
+
if job_deque:
|
59
|
+
job_deque.popleft()
|
60
|
+
|
61
|
+
def reset(self) -> None:
|
62
|
+
"""Resets unscheduled operations to include all operations.
|
63
|
+
|
64
|
+
This method reinitializes the list of deques with all operations
|
65
|
+
from all jobs in the instance.
|
66
|
+
"""
|
67
|
+
self.unscheduled_operations_per_job = [
|
68
|
+
collections.deque(job) for job in self.dispatcher.instance.jobs
|
69
|
+
]
|
@@ -1,16 +1,21 @@
|
|
1
1
|
"""Contains FeatureObserver classes for observing features of the
|
2
2
|
dispatcher."""
|
3
3
|
|
4
|
-
from .
|
5
|
-
from .
|
6
|
-
from .
|
7
|
-
from .
|
8
|
-
from .
|
9
|
-
from .
|
10
|
-
from .
|
11
|
-
from .
|
12
|
-
from .
|
13
|
-
|
4
|
+
from ._feature_observer import FeatureObserver, FeatureType
|
5
|
+
from ._earliest_start_time_observer import EarliestStartTimeObserver
|
6
|
+
from ._is_ready_observer import IsReadyObserver
|
7
|
+
from ._duration_observer import DurationObserver
|
8
|
+
from ._is_scheduled_observer import IsScheduledObserver
|
9
|
+
from ._position_in_job_observer import PositionInJobObserver
|
10
|
+
from ._remaining_operations_observer import RemainingOperationsObserver
|
11
|
+
from ._is_completed_observer import IsCompletedObserver
|
12
|
+
from ._factory import (
|
13
|
+
FeatureObserverType,
|
14
|
+
feature_observer_factory,
|
15
|
+
FeatureObserverConfig,
|
16
|
+
)
|
17
|
+
from ._composite_feature_observer import CompositeFeatureObserver
|
18
|
+
|
14
19
|
|
15
20
|
__all__ = [
|
16
21
|
"FeatureObserver",
|
@@ -25,4 +30,5 @@ __all__ = [
|
|
25
30
|
"IsCompletedObserver",
|
26
31
|
"FeatureObserverType",
|
27
32
|
"feature_observer_factory",
|
33
|
+
"FeatureObserverConfig",
|
28
34
|
]
|
@@ -1,13 +1,24 @@
|
|
1
1
|
"""Home of the `CompositeFeatureObserver` class."""
|
2
2
|
|
3
3
|
from collections import defaultdict
|
4
|
+
from collections.abc import Sequence
|
5
|
+
|
6
|
+
# The Self type can be imported directly from Python’s typing module in
|
7
|
+
# version 3.11 and beyond. We use the typing_extensions module to support
|
8
|
+
# python 3.10.
|
9
|
+
from typing_extensions import Self
|
10
|
+
|
4
11
|
import numpy as np
|
12
|
+
from numpy.typing import NDArray
|
5
13
|
import pandas as pd
|
6
14
|
|
15
|
+
from job_shop_lib.exceptions import ValidationError
|
7
16
|
from job_shop_lib.dispatching import Dispatcher
|
8
17
|
from job_shop_lib.dispatching.feature_observers import (
|
9
18
|
FeatureObserver,
|
10
19
|
FeatureType,
|
20
|
+
FeatureObserverConfig,
|
21
|
+
feature_observer_factory,
|
11
22
|
)
|
12
23
|
|
13
24
|
|
@@ -29,8 +40,10 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
29
40
|
def __init__(
|
30
41
|
self,
|
31
42
|
dispatcher: Dispatcher,
|
32
|
-
|
43
|
+
*,
|
33
44
|
subscribe: bool = True,
|
45
|
+
feature_types: list[FeatureType] | FeatureType | None = None,
|
46
|
+
feature_observers: list[FeatureObserver] | None = None,
|
34
47
|
):
|
35
48
|
if feature_observers is None:
|
36
49
|
feature_observers = [
|
@@ -38,11 +51,48 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
38
51
|
for observer in dispatcher.subscribers
|
39
52
|
if isinstance(observer, FeatureObserver)
|
40
53
|
]
|
54
|
+
feature_types = self._get_feature_types_list(feature_types)
|
55
|
+
for observer in feature_observers:
|
56
|
+
if not set(observer.features.keys()).issubset(set(feature_types)):
|
57
|
+
raise ValidationError(
|
58
|
+
"The feature types observed by the feature observer "
|
59
|
+
f"{observer.__class__.__name__} are not a subset of the "
|
60
|
+
"feature types specified in the CompositeFeatureObserver."
|
61
|
+
f"Observer feature types: {observer.features.keys()}"
|
62
|
+
f"Composite feature types: {feature_types}"
|
63
|
+
)
|
41
64
|
self.feature_observers = feature_observers
|
42
65
|
self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
|
43
66
|
super().__init__(dispatcher, subscribe=subscribe)
|
44
67
|
self._set_column_names()
|
45
68
|
|
69
|
+
@classmethod
|
70
|
+
def from_feature_observer_configs(
|
71
|
+
cls,
|
72
|
+
dispatcher: Dispatcher,
|
73
|
+
feature_observer_configs: Sequence[FeatureObserverConfig],
|
74
|
+
subscribe: bool = True,
|
75
|
+
) -> Self:
|
76
|
+
"""Creates the composite feature observer.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
dispatcher:
|
80
|
+
The dispatcher used to create the feature observers.
|
81
|
+
feature_observer_configs:
|
82
|
+
The list of feature observer configuration objects.
|
83
|
+
subscribe:
|
84
|
+
Whether to subscribe the CompositeFeatureObserver to the
|
85
|
+
dispatcher.
|
86
|
+
"""
|
87
|
+
observers = [
|
88
|
+
feature_observer_factory(observer_config, dispatcher=dispatcher)
|
89
|
+
for observer_config in feature_observer_configs
|
90
|
+
]
|
91
|
+
composite_observer = cls(
|
92
|
+
dispatcher, feature_observers=observers, subscribe=subscribe
|
93
|
+
)
|
94
|
+
return composite_observer
|
95
|
+
|
46
96
|
@property
|
47
97
|
def features_as_dataframe(self) -> dict[FeatureType, pd.DataFrame]:
|
48
98
|
"""Returns the features as a dictionary of `pd.DataFrame` instances."""
|
@@ -54,7 +104,9 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
54
104
|
}
|
55
105
|
|
56
106
|
def initialize_features(self):
|
57
|
-
features: dict[FeatureType, list[np.
|
107
|
+
features: dict[FeatureType, list[NDArray[np.float32]]] = defaultdict(
|
108
|
+
list
|
109
|
+
)
|
58
110
|
for observer in self.feature_observers:
|
59
111
|
for feature_type, feature_matrix in observer.features.items():
|
60
112
|
features[feature_type].append(feature_matrix)
|
@@ -85,3 +137,33 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
85
137
|
out.append(f"{feature_type.value}:")
|
86
138
|
out.append(dataframe.to_string())
|
87
139
|
return "\n".join(out)
|
140
|
+
|
141
|
+
|
142
|
+
if __name__ == "__main__":
|
143
|
+
from cProfile import Profile
|
144
|
+
from job_shop_lib.benchmarking import load_benchmark_instance
|
145
|
+
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
146
|
+
from job_shop_lib.dispatching.feature_observers import (
|
147
|
+
FeatureObserverType,
|
148
|
+
)
|
149
|
+
|
150
|
+
ta80 = load_benchmark_instance("ta80")
|
151
|
+
|
152
|
+
dispatcher_ = Dispatcher(ta80)
|
153
|
+
feature_observer_types_ = list(FeatureObserverType)
|
154
|
+
feature_observers_ = [
|
155
|
+
feature_observer_factory(
|
156
|
+
observer_type,
|
157
|
+
dispatcher=dispatcher_,
|
158
|
+
)
|
159
|
+
for observer_type in feature_observer_types_
|
160
|
+
if not observer_type == FeatureObserverType.COMPOSITE
|
161
|
+
# and not FeatureObserverType.EARLIEST_START_TIME
|
162
|
+
]
|
163
|
+
composite_observer_ = CompositeFeatureObserver(
|
164
|
+
dispatcher_, feature_observers=feature_observers_
|
165
|
+
)
|
166
|
+
solver = DispatchingRuleSolver(dispatching_rule="random")
|
167
|
+
profiler = Profile()
|
168
|
+
profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
169
|
+
profiler.print_stats("cumtime")
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
import numpy as np
|
4
4
|
|
5
|
-
from job_shop_lib.dispatching import Dispatcher
|
6
5
|
from job_shop_lib import ScheduledOperation
|
7
6
|
from job_shop_lib.dispatching.feature_observers import (
|
8
7
|
FeatureObserver,
|
@@ -13,30 +12,20 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
13
12
|
class DurationObserver(FeatureObserver):
|
14
13
|
"""Measures the remaining duration of operations, machines, and jobs.
|
15
14
|
|
16
|
-
The duration of an Operation is:
|
15
|
+
The duration of an :class:`Operation` is:
|
17
16
|
- if the operation has not been scheduled, it is the duration of the
|
18
|
-
|
17
|
+
operation.
|
19
18
|
- if the operation has been scheduled, it is the remaining duration of
|
20
|
-
|
19
|
+
the operation.
|
21
20
|
- if the operation has been completed, it is the last duration of the
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
operation that has been computed. The duration must be set to 0
|
22
|
+
manually if needed. We do not update the duration of completed
|
23
|
+
operations to save computation time.
|
25
24
|
|
26
25
|
The duration of a Machine or Job is the sum of the durations of the
|
27
26
|
unscheduled operations that belong to the machine or job.
|
28
27
|
"""
|
29
28
|
|
30
|
-
def __init__(
|
31
|
-
self,
|
32
|
-
dispatcher: Dispatcher,
|
33
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
34
|
-
subscribe: bool = True,
|
35
|
-
):
|
36
|
-
super().__init__(
|
37
|
-
dispatcher, feature_types, feature_size=1, subscribe=subscribe
|
38
|
-
)
|
39
|
-
|
40
29
|
def initialize_features(self):
|
41
30
|
mapping = {
|
42
31
|
FeatureType.OPERATIONS: self._initialize_operation_durations,
|
@@ -7,18 +7,26 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
7
7
|
FeatureObserver,
|
8
8
|
FeatureType,
|
9
9
|
)
|
10
|
-
from job_shop_lib
|
10
|
+
from job_shop_lib import ScheduledOperation
|
11
11
|
|
12
12
|
|
13
13
|
class EarliestStartTimeObserver(FeatureObserver):
|
14
14
|
"""Observer that adds a feature indicating the earliest start time of
|
15
15
|
each operation, machine, and job in the graph."""
|
16
16
|
|
17
|
+
__slots__ = (
|
18
|
+
"earliest_start_times",
|
19
|
+
"_job_ids",
|
20
|
+
"_positions",
|
21
|
+
"machine_ids",
|
22
|
+
)
|
23
|
+
|
17
24
|
def __init__(
|
18
25
|
self,
|
19
26
|
dispatcher: Dispatcher,
|
20
|
-
|
27
|
+
*,
|
21
28
|
subscribe: bool = True,
|
29
|
+
feature_types: list[FeatureType] | FeatureType | None = None,
|
22
30
|
):
|
23
31
|
|
24
32
|
# Earliest start times initialization
|
@@ -32,8 +40,32 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
32
40
|
)
|
33
41
|
self.earliest_start_times[np.isnan(squared_duration_matrix)] = np.nan
|
34
42
|
# -------------------------------
|
43
|
+
|
44
|
+
# Cache:
|
45
|
+
operations_by_machine = dispatcher.instance.operations_by_machine
|
46
|
+
self._is_regular_instance = all(
|
47
|
+
len(job) == len(dispatcher.instance.jobs[0])
|
48
|
+
for job in dispatcher.instance.jobs
|
49
|
+
)
|
50
|
+
if self._is_regular_instance:
|
51
|
+
self._job_ids = np.array(
|
52
|
+
[
|
53
|
+
[op.job_id for op in machine_ops]
|
54
|
+
for machine_ops in operations_by_machine
|
55
|
+
]
|
56
|
+
)
|
57
|
+
self._positions = np.array(
|
58
|
+
[
|
59
|
+
[op.position_in_job for op in machine_ops]
|
60
|
+
for machine_ops in operations_by_machine
|
61
|
+
]
|
62
|
+
)
|
63
|
+
else:
|
64
|
+
self._job_ids = np.array([])
|
65
|
+
self._positions = np.array([])
|
66
|
+
|
35
67
|
super().__init__(
|
36
|
-
dispatcher, feature_types
|
68
|
+
dispatcher, feature_types=feature_types, subscribe=subscribe
|
37
69
|
)
|
38
70
|
|
39
71
|
def update(self, scheduled_operation: ScheduledOperation):
|
@@ -68,31 +100,56 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
68
100
|
|
69
101
|
# Now, we compute the gap that could be introduced by the new
|
70
102
|
# next_available_time of the machine.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
103
|
+
machine_ops = self.dispatcher.instance.operations_by_machine[
|
104
|
+
scheduled_operation.machine_id
|
105
|
+
]
|
106
|
+
unscheduled_mask = np.array(
|
107
|
+
[not self.dispatcher.is_scheduled(op) for op in machine_ops]
|
108
|
+
)
|
109
|
+
if np.any(unscheduled_mask):
|
110
|
+
if self._job_ids.size == 0:
|
111
|
+
job_ids = np.array([op.job_id for op in machine_ops])[
|
112
|
+
unscheduled_mask
|
113
|
+
]
|
114
|
+
else:
|
115
|
+
job_ids = self._job_ids[scheduled_operation.machine_id][
|
116
|
+
unscheduled_mask
|
117
|
+
]
|
118
|
+
|
119
|
+
if self._positions.size == 0:
|
120
|
+
positions = np.array(
|
121
|
+
[op.position_in_job for op in machine_ops]
|
122
|
+
)[unscheduled_mask]
|
123
|
+
else:
|
124
|
+
positions = self._positions[scheduled_operation.machine_id][
|
125
|
+
unscheduled_mask
|
126
|
+
]
|
127
|
+
old_start_times = self.earliest_start_times[job_ids, positions]
|
128
|
+
new_start_times = np.maximum(
|
129
|
+
scheduled_operation.end_time, old_start_times
|
130
|
+
)
|
131
|
+
gaps = new_start_times - old_start_times
|
132
|
+
|
133
|
+
for job_id, position, gap in zip(job_ids, positions, gaps):
|
134
|
+
self.earliest_start_times[job_id, position:] += gap
|
83
135
|
|
84
136
|
self.initialize_features()
|
85
137
|
|
86
138
|
def initialize_features(self):
|
87
139
|
"""Initializes the features based on the current state of the
|
88
140
|
dispatcher."""
|
89
|
-
mapping = {
|
90
|
-
FeatureType.OPERATIONS: self._update_operation_features,
|
91
|
-
FeatureType.MACHINES: self._update_machine_features,
|
92
|
-
FeatureType.JOBS: self._update_job_features,
|
93
|
-
}
|
94
141
|
for feature_type in self.features:
|
95
|
-
|
142
|
+
if feature_type == FeatureType.OPERATIONS:
|
143
|
+
self._update_operation_features()
|
144
|
+
elif (
|
145
|
+
feature_type == FeatureType.MACHINES
|
146
|
+
and self._is_regular_instance
|
147
|
+
):
|
148
|
+
self._update_machine_features_vectorized()
|
149
|
+
elif feature_type == FeatureType.MACHINES:
|
150
|
+
self._update_machine_features()
|
151
|
+
elif feature_type == FeatureType.JOBS:
|
152
|
+
self._update_job_features()
|
96
153
|
|
97
154
|
def _update_operation_features(self):
|
98
155
|
"""Ravels the 2D array into a 1D array"""
|
@@ -127,6 +184,42 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
127
184
|
min_earliest_start_time - current_time
|
128
185
|
)
|
129
186
|
|
187
|
+
def _update_machine_features_vectorized(self):
|
188
|
+
"""Picks the minimum start time of all operations that can be scheduled
|
189
|
+
on that machine"""
|
190
|
+
current_time = self.dispatcher.current_time()
|
191
|
+
operations_by_machine = self.dispatcher.instance.operations_by_machine
|
192
|
+
|
193
|
+
# Create a mask for unscheduled operations
|
194
|
+
is_unscheduled = np.array(
|
195
|
+
[
|
196
|
+
[not self.dispatcher.is_scheduled(op) for op in machine_ops]
|
197
|
+
for machine_ops in operations_by_machine
|
198
|
+
]
|
199
|
+
)
|
200
|
+
|
201
|
+
# Get earliest start times for all operations
|
202
|
+
earliest_start_times = self.earliest_start_times[
|
203
|
+
self._job_ids, self._positions
|
204
|
+
]
|
205
|
+
|
206
|
+
# Apply mask for unscheduled operations
|
207
|
+
masked_start_times = np.where(
|
208
|
+
is_unscheduled, earliest_start_times, np.inf
|
209
|
+
)
|
210
|
+
|
211
|
+
# Find minimum start time for each machine
|
212
|
+
min_start_times = np.min(masked_start_times, axis=1)
|
213
|
+
|
214
|
+
# Handle cases where all operations are scheduled
|
215
|
+
min_start_times = np.where(
|
216
|
+
np.isinf(min_start_times), 0, min_start_times
|
217
|
+
)
|
218
|
+
|
219
|
+
self.features[FeatureType.MACHINES][:, 0] = (
|
220
|
+
min_start_times - current_time
|
221
|
+
)
|
222
|
+
|
130
223
|
def _update_job_features(self):
|
131
224
|
"""Picks the earliest start time of the next operation in the job"""
|
132
225
|
current_time = self.dispatcher.current_time()
|
@@ -140,17 +233,3 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
140
233
|
self.earliest_start_times[job_id, next_operation_idx]
|
141
234
|
- current_time
|
142
235
|
)
|
143
|
-
|
144
|
-
|
145
|
-
if __name__ == "__main__":
|
146
|
-
squared_durations_matrix = np.array([[1, 1, 7], [5, 1, 1], [1, 3, 2]])
|
147
|
-
# Add a zeros column to the left of the matrix
|
148
|
-
cumulative_durations = np.hstack(
|
149
|
-
(
|
150
|
-
np.zeros((squared_durations_matrix.shape[0], 1)),
|
151
|
-
squared_durations_matrix[:, :-1],
|
152
|
-
)
|
153
|
-
)
|
154
|
-
# Set to nan the values that are not available
|
155
|
-
cumulative_durations[np.isnan(squared_durations_matrix)] = np.nan
|
156
|
-
print(cumulative_durations)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from enum import Enum
|
4
4
|
|
5
|
+
from job_shop_lib.dispatching import DispatcherObserverConfig
|
5
6
|
from job_shop_lib.dispatching.feature_observers import (
|
6
7
|
IsReadyObserver,
|
7
8
|
EarliestStartTimeObserver,
|
@@ -15,8 +16,7 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
15
16
|
|
16
17
|
|
17
18
|
class FeatureObserverType(str, Enum):
|
18
|
-
"""Enumeration of
|
19
|
-
problem."""
|
19
|
+
"""Enumeration of the different feature observers."""
|
20
20
|
|
21
21
|
IS_READY = "is_ready"
|
22
22
|
EARLIEST_START_TIME = "earliest_start_time"
|
@@ -28,15 +28,30 @@ class FeatureObserverType(str, Enum):
|
|
28
28
|
COMPOSITE = "composite"
|
29
29
|
|
30
30
|
|
31
|
+
# FeatureObserverConfig = DispatcherObserverConfig[
|
32
|
+
# type[FeatureObserver] | FeatureObserverType | str
|
33
|
+
# ]
|
34
|
+
FeatureObserverConfig = (
|
35
|
+
DispatcherObserverConfig[type[FeatureObserver]]
|
36
|
+
| DispatcherObserverConfig[FeatureObserverType]
|
37
|
+
| DispatcherObserverConfig[str]
|
38
|
+
)
|
39
|
+
|
40
|
+
|
31
41
|
def feature_observer_factory(
|
32
|
-
|
42
|
+
feature_creator_type: (
|
43
|
+
str
|
44
|
+
| FeatureObserverType
|
45
|
+
| type[FeatureObserver]
|
46
|
+
| FeatureObserverConfig
|
47
|
+
),
|
33
48
|
**kwargs,
|
34
49
|
) -> FeatureObserver:
|
35
50
|
"""Creates and returns a node feature creator based on the specified
|
36
51
|
node feature creator type.
|
37
52
|
|
38
53
|
Args:
|
39
|
-
|
54
|
+
feature_creator_type:
|
40
55
|
The type of node feature creator to create.
|
41
56
|
**kwargs:
|
42
57
|
Additional keyword arguments to pass to the node
|
@@ -45,6 +60,17 @@ def feature_observer_factory(
|
|
45
60
|
Returns:
|
46
61
|
A node feature creator instance.
|
47
62
|
"""
|
63
|
+
if isinstance(feature_creator_type, DispatcherObserverConfig):
|
64
|
+
return feature_observer_factory(
|
65
|
+
feature_creator_type.class_type,
|
66
|
+
**feature_creator_type.kwargs,
|
67
|
+
**kwargs,
|
68
|
+
)
|
69
|
+
# if the instance is of type type[FeatureObserver] we can just
|
70
|
+
# call the object constructor with the keyword arguments
|
71
|
+
if isinstance(feature_creator_type, type):
|
72
|
+
return feature_creator_type(**kwargs)
|
73
|
+
|
48
74
|
mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
|
49
75
|
FeatureObserverType.IS_READY: IsReadyObserver,
|
50
76
|
FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
|
@@ -54,5 +80,5 @@ def feature_observer_factory(
|
|
54
80
|
FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
|
55
81
|
FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
|
56
82
|
}
|
57
|
-
feature_creator = mapping[
|
83
|
+
feature_creator = mapping[feature_creator_type] # type: ignore[index]
|
58
84
|
return feature_creator(**kwargs)
|