job-shop-lib 0.5.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -1,156 +0,0 @@
|
|
1
|
-
"""Home of the `EarliestStartTimeObserver` class."""
|
2
|
-
|
3
|
-
import numpy as np
|
4
|
-
|
5
|
-
from job_shop_lib.dispatching import Dispatcher
|
6
|
-
from job_shop_lib.dispatching.feature_observers import (
|
7
|
-
FeatureObserver,
|
8
|
-
FeatureType,
|
9
|
-
)
|
10
|
-
from job_shop_lib.scheduled_operation import ScheduledOperation
|
11
|
-
|
12
|
-
|
13
|
-
class EarliestStartTimeObserver(FeatureObserver):
|
14
|
-
"""Observer that adds a feature indicating the earliest start time of
|
15
|
-
each operation, machine, and job in the graph."""
|
16
|
-
|
17
|
-
def __init__(
|
18
|
-
self,
|
19
|
-
dispatcher: Dispatcher,
|
20
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
21
|
-
subscribe: bool = True,
|
22
|
-
):
|
23
|
-
|
24
|
-
# Earliest start times initialization
|
25
|
-
# -------------------------------
|
26
|
-
squared_duration_matrix = dispatcher.instance.durations_matrix_array
|
27
|
-
self.earliest_start_times = np.hstack(
|
28
|
-
(
|
29
|
-
np.zeros((squared_duration_matrix.shape[0], 1)),
|
30
|
-
np.cumsum(squared_duration_matrix[:, :-1], axis=1),
|
31
|
-
)
|
32
|
-
)
|
33
|
-
self.earliest_start_times[np.isnan(squared_duration_matrix)] = np.nan
|
34
|
-
# -------------------------------
|
35
|
-
super().__init__(
|
36
|
-
dispatcher, feature_types, feature_size=1, subscribe=subscribe
|
37
|
-
)
|
38
|
-
|
39
|
-
def update(self, scheduled_operation: ScheduledOperation):
|
40
|
-
"""Recomputes the earliest start times and calls the
|
41
|
-
`initialize_features` method.
|
42
|
-
|
43
|
-
The earliest start times is computed as the cumulative sum of the
|
44
|
-
previous unscheduled operations in the job plus the maximum of the
|
45
|
-
completion time of the last scheduled operation and the next available
|
46
|
-
time of the machine(s) the operation is assigned.
|
47
|
-
|
48
|
-
After that, we substract the current time.
|
49
|
-
"""
|
50
|
-
# We compute the gap that the current scheduled operation could be
|
51
|
-
# adding to each job.
|
52
|
-
job_id = scheduled_operation.job_id
|
53
|
-
next_operation_idx = self.dispatcher.job_next_operation_index[job_id]
|
54
|
-
if next_operation_idx < len(self.dispatcher.instance.jobs[job_id]):
|
55
|
-
old_start_time = self.earliest_start_times[
|
56
|
-
job_id, next_operation_idx
|
57
|
-
]
|
58
|
-
next_operation = self.dispatcher.instance.jobs[job_id][
|
59
|
-
next_operation_idx
|
60
|
-
]
|
61
|
-
new_start_time = max(
|
62
|
-
scheduled_operation.end_time,
|
63
|
-
old_start_time,
|
64
|
-
self.dispatcher.earliest_start_time(next_operation),
|
65
|
-
)
|
66
|
-
gap = new_start_time - old_start_time
|
67
|
-
self.earliest_start_times[job_id, next_operation_idx:] += gap
|
68
|
-
|
69
|
-
# Now, we compute the gap that could be introduced by the new
|
70
|
-
# next_available_time of the machine.
|
71
|
-
operations_by_machine = self.dispatcher.instance.operations_by_machine
|
72
|
-
for operation in operations_by_machine[scheduled_operation.machine_id]:
|
73
|
-
if self.dispatcher.is_scheduled(operation):
|
74
|
-
continue
|
75
|
-
old_start_time = self.earliest_start_times[
|
76
|
-
operation.job_id, operation.position_in_job
|
77
|
-
]
|
78
|
-
new_start_time = max(old_start_time, scheduled_operation.end_time)
|
79
|
-
gap = new_start_time - old_start_time
|
80
|
-
self.earliest_start_times[
|
81
|
-
operation.job_id, operation.position_in_job :
|
82
|
-
] += gap
|
83
|
-
|
84
|
-
self.initialize_features()
|
85
|
-
|
86
|
-
def initialize_features(self):
|
87
|
-
"""Initializes the features based on the current state of the
|
88
|
-
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
|
-
for feature_type in self.features:
|
95
|
-
mapping[feature_type]()
|
96
|
-
|
97
|
-
def _update_operation_features(self):
|
98
|
-
"""Ravels the 2D array into a 1D array"""
|
99
|
-
current_time = self.dispatcher.current_time()
|
100
|
-
next_index = 0
|
101
|
-
for job_id, operations in enumerate(self.dispatcher.instance.jobs):
|
102
|
-
self.features[FeatureType.OPERATIONS][
|
103
|
-
next_index : next_index + len(operations), 0
|
104
|
-
] = (
|
105
|
-
self.earliest_start_times[job_id, : len(operations)]
|
106
|
-
- current_time
|
107
|
-
)
|
108
|
-
next_index += len(operations)
|
109
|
-
|
110
|
-
def _update_machine_features(self):
|
111
|
-
"""Picks the minimum start time of all operations that can be scheduled
|
112
|
-
on that machine"""
|
113
|
-
current_time = self.dispatcher.current_time()
|
114
|
-
operations_by_machine = self.dispatcher.instance.operations_by_machine
|
115
|
-
for machine_id, operations in enumerate(operations_by_machine):
|
116
|
-
min_earliest_start_time = min(
|
117
|
-
(
|
118
|
-
self.earliest_start_times[
|
119
|
-
operation.job_id, operation.position_in_job
|
120
|
-
]
|
121
|
-
for operation in operations
|
122
|
-
if not self.dispatcher.is_scheduled(operation)
|
123
|
-
),
|
124
|
-
default=0,
|
125
|
-
)
|
126
|
-
self.features[FeatureType.MACHINES][machine_id, 0] = (
|
127
|
-
min_earliest_start_time - current_time
|
128
|
-
)
|
129
|
-
|
130
|
-
def _update_job_features(self):
|
131
|
-
"""Picks the earliest start time of the next operation in the job"""
|
132
|
-
current_time = self.dispatcher.current_time()
|
133
|
-
for job_id, next_operation_idx in enumerate(
|
134
|
-
self.dispatcher.job_next_operation_index
|
135
|
-
):
|
136
|
-
job_length = len(self.dispatcher.instance.jobs[job_id])
|
137
|
-
if next_operation_idx == job_length:
|
138
|
-
continue
|
139
|
-
self.features[FeatureType.JOBS][job_id, 0] = (
|
140
|
-
self.earliest_start_times[job_id, next_operation_idx]
|
141
|
-
- current_time
|
142
|
-
)
|
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)
|
@@ -1,58 +0,0 @@
|
|
1
|
-
"""Contains factory functions for creating node feature encoders."""
|
2
|
-
|
3
|
-
from enum import Enum
|
4
|
-
|
5
|
-
from job_shop_lib.dispatching.feature_observers import (
|
6
|
-
IsReadyObserver,
|
7
|
-
EarliestStartTimeObserver,
|
8
|
-
FeatureObserver,
|
9
|
-
DurationObserver,
|
10
|
-
IsScheduledObserver,
|
11
|
-
PositionInJobObserver,
|
12
|
-
RemainingOperationsObserver,
|
13
|
-
IsCompletedObserver,
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class FeatureObserverType(str, Enum):
|
18
|
-
"""Enumeration of node feature creator types for the job shop scheduling
|
19
|
-
problem."""
|
20
|
-
|
21
|
-
IS_READY = "is_ready"
|
22
|
-
EARLIEST_START_TIME = "earliest_start_time"
|
23
|
-
DURATION = "duration"
|
24
|
-
IS_SCHEDULED = "is_scheduled"
|
25
|
-
POSITION_IN_JOB = "position_in_job"
|
26
|
-
REMAINING_OPERATIONS = "remaining_operations"
|
27
|
-
IS_COMPLETED = "is_completed"
|
28
|
-
COMPOSITE = "composite"
|
29
|
-
|
30
|
-
|
31
|
-
def feature_observer_factory(
|
32
|
-
node_feature_creator_type: str | FeatureObserverType,
|
33
|
-
**kwargs,
|
34
|
-
) -> FeatureObserver:
|
35
|
-
"""Creates and returns a node feature creator based on the specified
|
36
|
-
node feature creator type.
|
37
|
-
|
38
|
-
Args:
|
39
|
-
node_feature_creator_type:
|
40
|
-
The type of node feature creator to create.
|
41
|
-
**kwargs:
|
42
|
-
Additional keyword arguments to pass to the node
|
43
|
-
feature creator constructor.
|
44
|
-
|
45
|
-
Returns:
|
46
|
-
A node feature creator instance.
|
47
|
-
"""
|
48
|
-
mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
|
49
|
-
FeatureObserverType.IS_READY: IsReadyObserver,
|
50
|
-
FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
|
51
|
-
FeatureObserverType.DURATION: DurationObserver,
|
52
|
-
FeatureObserverType.IS_SCHEDULED: IsScheduledObserver,
|
53
|
-
FeatureObserverType.POSITION_IN_JOB: PositionInJobObserver,
|
54
|
-
FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
|
55
|
-
FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
|
56
|
-
}
|
57
|
-
feature_creator = mapping[node_feature_creator_type] # type: ignore[index]
|
58
|
-
return feature_creator(**kwargs)
|
@@ -1,113 +0,0 @@
|
|
1
|
-
"""Home of the `FeatureObserver` class and `FeatureType` enum."""
|
2
|
-
|
3
|
-
import enum
|
4
|
-
|
5
|
-
import numpy as np
|
6
|
-
from job_shop_lib import ScheduledOperation
|
7
|
-
from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
|
8
|
-
|
9
|
-
|
10
|
-
class FeatureType(str, enum.Enum):
|
11
|
-
"""Types of features that can be extracted."""
|
12
|
-
|
13
|
-
OPERATIONS = "operations"
|
14
|
-
MACHINES = "machines"
|
15
|
-
JOBS = "jobs"
|
16
|
-
|
17
|
-
|
18
|
-
class FeatureObserver(DispatcherObserver):
|
19
|
-
"""Base class for feature observers."""
|
20
|
-
|
21
|
-
def __init__(
|
22
|
-
self,
|
23
|
-
dispatcher: Dispatcher,
|
24
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
25
|
-
feature_size: dict[FeatureType, int] | int = 1,
|
26
|
-
is_singleton: bool = True,
|
27
|
-
subscribe: bool = True,
|
28
|
-
):
|
29
|
-
feature_types = self.get_feature_types_list(feature_types)
|
30
|
-
if isinstance(feature_size, int):
|
31
|
-
feature_size = {
|
32
|
-
feature_type: feature_size for feature_type in feature_types
|
33
|
-
}
|
34
|
-
super().__init__(dispatcher, is_singleton, subscribe)
|
35
|
-
|
36
|
-
number_of_entities = {
|
37
|
-
FeatureType.OPERATIONS: dispatcher.instance.num_operations,
|
38
|
-
FeatureType.MACHINES: dispatcher.instance.num_machines,
|
39
|
-
FeatureType.JOBS: dispatcher.instance.num_jobs,
|
40
|
-
}
|
41
|
-
self.feature_dimensions = {
|
42
|
-
feature_type: (
|
43
|
-
number_of_entities[feature_type],
|
44
|
-
feature_size[feature_type],
|
45
|
-
)
|
46
|
-
for feature_type in feature_types
|
47
|
-
}
|
48
|
-
self.features = {
|
49
|
-
feature_type: np.zeros(
|
50
|
-
self.feature_dimensions[feature_type],
|
51
|
-
dtype=np.float32,
|
52
|
-
)
|
53
|
-
for feature_type in feature_types
|
54
|
-
}
|
55
|
-
self.initialize_features()
|
56
|
-
|
57
|
-
def initialize_features(self):
|
58
|
-
"""Initializes the features based on the current state of the
|
59
|
-
dispatcher."""
|
60
|
-
|
61
|
-
def update(self, scheduled_operation: ScheduledOperation):
|
62
|
-
"""Updates the features based on the scheduled operation.
|
63
|
-
|
64
|
-
By default, this method just calls `initialize_features`.
|
65
|
-
|
66
|
-
Args:
|
67
|
-
scheduled_operation:
|
68
|
-
The operation that has been scheduled.
|
69
|
-
"""
|
70
|
-
self.initialize_features()
|
71
|
-
|
72
|
-
def reset(self):
|
73
|
-
"""Sets features to zero and calls to `initialize_features`."""
|
74
|
-
self.set_features_to_zero()
|
75
|
-
self.initialize_features()
|
76
|
-
|
77
|
-
def set_features_to_zero(
|
78
|
-
self, exclude: FeatureType | list[FeatureType] | None = None
|
79
|
-
):
|
80
|
-
"""Sets features to zero."""
|
81
|
-
if exclude is None:
|
82
|
-
exclude = []
|
83
|
-
if isinstance(exclude, FeatureType):
|
84
|
-
exclude = [exclude]
|
85
|
-
|
86
|
-
for feature_type in self.features:
|
87
|
-
if feature_type in exclude:
|
88
|
-
continue
|
89
|
-
self.features[feature_type][:] = 0.0
|
90
|
-
|
91
|
-
@staticmethod
|
92
|
-
def get_feature_types_list(
|
93
|
-
feature_types: list[FeatureType] | FeatureType | None,
|
94
|
-
) -> list[FeatureType]:
|
95
|
-
"""Returns a list of feature types.
|
96
|
-
|
97
|
-
Args:
|
98
|
-
feature_types:
|
99
|
-
A list of feature types or a single feature type. If `None`,
|
100
|
-
all feature types are returned.
|
101
|
-
"""
|
102
|
-
if isinstance(feature_types, FeatureType):
|
103
|
-
feature_types = [feature_types]
|
104
|
-
if feature_types is None:
|
105
|
-
feature_types = list(FeatureType)
|
106
|
-
return feature_types
|
107
|
-
|
108
|
-
def __str__(self):
|
109
|
-
out = [self.__class__.__name__, ":\n"]
|
110
|
-
out.append("-" * len(out[0]))
|
111
|
-
for feature_type, feature in self.features.items():
|
112
|
-
out.append(f"\n{feature_type.value}:\n{feature}")
|
113
|
-
return "".join(out)
|
@@ -1,98 +0,0 @@
|
|
1
|
-
"""Home of the `IsCompletedObserver` class."""
|
2
|
-
|
3
|
-
from typing import Iterable
|
4
|
-
|
5
|
-
import numpy as np
|
6
|
-
|
7
|
-
from job_shop_lib import ScheduledOperation
|
8
|
-
from job_shop_lib.dispatching import Dispatcher
|
9
|
-
from job_shop_lib.dispatching.feature_observers import (
|
10
|
-
FeatureObserver,
|
11
|
-
FeatureType,
|
12
|
-
RemainingOperationsObserver,
|
13
|
-
)
|
14
|
-
|
15
|
-
|
16
|
-
class IsCompletedObserver(FeatureObserver):
|
17
|
-
"""Observer that adds a binary feature indicating whether each operation,
|
18
|
-
machine, or job has been completed."""
|
19
|
-
|
20
|
-
def __init__(
|
21
|
-
self,
|
22
|
-
dispatcher: Dispatcher,
|
23
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
24
|
-
):
|
25
|
-
feature_types = self.get_feature_types_list(feature_types)
|
26
|
-
self.remaining_ops_per_machine = np.zeros(
|
27
|
-
(dispatcher.instance.num_machines, 1), dtype=int
|
28
|
-
)
|
29
|
-
self.remaining_ops_per_job = np.zeros(
|
30
|
-
(dispatcher.instance.num_jobs, 1), dtype=int
|
31
|
-
)
|
32
|
-
super().__init__(dispatcher, feature_types, feature_size=1)
|
33
|
-
|
34
|
-
def initialize_features(self):
|
35
|
-
self._initialize_remaining_operations()
|
36
|
-
|
37
|
-
def update(self, scheduled_operation: ScheduledOperation):
|
38
|
-
if FeatureType.OPERATIONS in self.features:
|
39
|
-
# operation_id = scheduled_operation.operation.operation_id
|
40
|
-
# self.features[FeatureType.OPERATIONS][operation_id, 0] = 1
|
41
|
-
completed_operations = [
|
42
|
-
op.operation_id
|
43
|
-
for op in self.dispatcher.completed_operations()
|
44
|
-
]
|
45
|
-
self.features[FeatureType.OPERATIONS][completed_operations, 0] = 1
|
46
|
-
if FeatureType.MACHINES in self.features:
|
47
|
-
machine_id = scheduled_operation.machine_id
|
48
|
-
self.remaining_ops_per_machine[machine_id, 0] -= 1
|
49
|
-
is_completed = self.remaining_ops_per_machine[machine_id, 0] == 0
|
50
|
-
self.features[FeatureType.MACHINES][machine_id, 0] = is_completed
|
51
|
-
if FeatureType.JOBS in self.features:
|
52
|
-
job_id = scheduled_operation.job_id
|
53
|
-
self.remaining_ops_per_job[job_id, 0] -= 1
|
54
|
-
is_completed = self.remaining_ops_per_job[job_id, 0] == 0
|
55
|
-
self.features[FeatureType.JOBS][job_id, 0] = is_completed
|
56
|
-
|
57
|
-
def _initialize_remaining_operations(self):
|
58
|
-
remaining_ops_observer = self._get_remaining_operations_observer(
|
59
|
-
self.dispatcher, self.features
|
60
|
-
)
|
61
|
-
if remaining_ops_observer is not None:
|
62
|
-
if FeatureType.JOBS in self.features:
|
63
|
-
self.remaining_ops_per_job = remaining_ops_observer.features[
|
64
|
-
FeatureType.JOBS
|
65
|
-
].copy()
|
66
|
-
if FeatureType.MACHINES in self.features:
|
67
|
-
self.remaining_ops_per_machine = (
|
68
|
-
remaining_ops_observer.features[
|
69
|
-
FeatureType.MACHINES
|
70
|
-
].copy()
|
71
|
-
)
|
72
|
-
return
|
73
|
-
|
74
|
-
# If there is no remaining operations observer, we need to
|
75
|
-
# compute the remaining operations ourselves.
|
76
|
-
# We iterate over all operations using scheduled_operations
|
77
|
-
# instead of uncompleted_operations, because in this case
|
78
|
-
# they will output the same operations, and the former is slightly
|
79
|
-
# more efficient.
|
80
|
-
for operation in self.dispatcher.unscheduled_operations():
|
81
|
-
if FeatureType.JOBS in self.features:
|
82
|
-
self.remaining_ops_per_job[operation.job_id, 0] += 1
|
83
|
-
if FeatureType.MACHINES in self.features:
|
84
|
-
self.remaining_ops_per_machine[operation.machine_id, 0] += 1
|
85
|
-
|
86
|
-
def _get_remaining_operations_observer(
|
87
|
-
self, dispatcher: Dispatcher, feature_types: Iterable[FeatureType]
|
88
|
-
) -> RemainingOperationsObserver | None:
|
89
|
-
for observer in dispatcher.subscribers:
|
90
|
-
if not isinstance(observer, RemainingOperationsObserver):
|
91
|
-
continue
|
92
|
-
has_same_features = all(
|
93
|
-
feature_type in observer.features
|
94
|
-
for feature_type in feature_types
|
95
|
-
)
|
96
|
-
if has_same_features:
|
97
|
-
return observer
|
98
|
-
return None
|
@@ -1,40 +0,0 @@
|
|
1
|
-
"""Home of the `IsReadyObserver` class."""
|
2
|
-
|
3
|
-
from job_shop_lib.dispatching import Dispatcher
|
4
|
-
from job_shop_lib.dispatching.feature_observers import (
|
5
|
-
FeatureObserver,
|
6
|
-
FeatureType,
|
7
|
-
)
|
8
|
-
|
9
|
-
|
10
|
-
class IsReadyObserver(FeatureObserver):
|
11
|
-
"""Feature creator that adds a binary feature indicating if the operation
|
12
|
-
is ready to be dispatched."""
|
13
|
-
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
dispatcher: Dispatcher,
|
17
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
18
|
-
subscribe: bool = True,
|
19
|
-
):
|
20
|
-
super().__init__(
|
21
|
-
dispatcher, feature_types, feature_size=1, subscribe=subscribe
|
22
|
-
)
|
23
|
-
|
24
|
-
def initialize_features(self):
|
25
|
-
self.set_features_to_zero()
|
26
|
-
for feature_type, feature in self.features.items():
|
27
|
-
node_ids = self._get_ready_nodes(feature_type)
|
28
|
-
feature[node_ids, 0] = 1.0
|
29
|
-
|
30
|
-
def _get_ready_nodes(self, feature_type: FeatureType) -> list[int]:
|
31
|
-
mapping = {
|
32
|
-
FeatureType.OPERATIONS: self._get_ready_operation_nodes,
|
33
|
-
FeatureType.MACHINES: self.dispatcher.available_machines,
|
34
|
-
FeatureType.JOBS: self.dispatcher.available_jobs,
|
35
|
-
}
|
36
|
-
return mapping[feature_type]()
|
37
|
-
|
38
|
-
def _get_ready_operation_nodes(self) -> list[int]:
|
39
|
-
available_operations = self.dispatcher.available_operations()
|
40
|
-
return [operation.operation_id for operation in available_operations]
|
@@ -1,116 +0,0 @@
|
|
1
|
-
"""Contains functions to prune (filter) operations.
|
2
|
-
|
3
|
-
This functions are used by the `Dispatcher` class to reduce the
|
4
|
-
amount of available operations to choose from.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from collections.abc import Callable, Iterable
|
8
|
-
|
9
|
-
from job_shop_lib import Operation
|
10
|
-
from job_shop_lib.dispatching import Dispatcher
|
11
|
-
|
12
|
-
|
13
|
-
def create_composite_pruning_function(
|
14
|
-
pruning_functions: Iterable[
|
15
|
-
Callable[[Dispatcher, list[Operation]], list[Operation]]
|
16
|
-
],
|
17
|
-
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
18
|
-
"""Creates and returns a composite pruning strategy function based on the
|
19
|
-
specified list of pruning strategies.
|
20
|
-
The composite pruning strategy function filters out operations based on
|
21
|
-
the specified list of pruning strategies.
|
22
|
-
Args:
|
23
|
-
pruning_strategies:
|
24
|
-
A list of pruning strategies to be used. Supported values are
|
25
|
-
'dominated_operations' and 'non_immediate_machines'.
|
26
|
-
Returns:
|
27
|
-
A function that takes a Dispatcher instance and a list of Operation
|
28
|
-
instances as input and returns a list of Operation instances based on
|
29
|
-
the specified list of pruning strategies.
|
30
|
-
Raises:
|
31
|
-
ValueError: If any of the pruning strategies in the list are not
|
32
|
-
recognized or are not supported.
|
33
|
-
"""
|
34
|
-
|
35
|
-
def composite_pruning_function(
|
36
|
-
dispatcher: Dispatcher, operations: list[Operation]
|
37
|
-
) -> list[Operation]:
|
38
|
-
pruned_operations = operations
|
39
|
-
for pruning_function in pruning_functions:
|
40
|
-
pruned_operations = pruning_function(dispatcher, pruned_operations)
|
41
|
-
|
42
|
-
return pruned_operations
|
43
|
-
|
44
|
-
return composite_pruning_function
|
45
|
-
|
46
|
-
|
47
|
-
def prune_dominated_operations(
|
48
|
-
dispatcher: Dispatcher, operations: list[Operation]
|
49
|
-
) -> list[Operation]:
|
50
|
-
"""Filters out all the operations that are dominated.
|
51
|
-
An operation is dominated if there is another operation that ends before
|
52
|
-
it starts on the same machine.
|
53
|
-
"""
|
54
|
-
|
55
|
-
min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
|
56
|
-
|
57
|
-
non_dominated_operations: list[Operation] = []
|
58
|
-
for operation in operations:
|
59
|
-
# One benchmark instance has an operation with duration 0
|
60
|
-
if operation.duration == 0:
|
61
|
-
return [operation]
|
62
|
-
for machine_id in operation.machines:
|
63
|
-
start_time = dispatcher.start_time(operation, machine_id)
|
64
|
-
is_dominated = start_time >= min_machine_end_times[machine_id]
|
65
|
-
if not is_dominated:
|
66
|
-
non_dominated_operations.append(operation)
|
67
|
-
break
|
68
|
-
|
69
|
-
return non_dominated_operations
|
70
|
-
|
71
|
-
|
72
|
-
def prune_non_immediate_machines(
|
73
|
-
dispatcher: Dispatcher, operations: list[Operation]
|
74
|
-
) -> list[Operation]:
|
75
|
-
"""Filters out all the operations associated with machines which earliest
|
76
|
-
operation is not the current time."""
|
77
|
-
|
78
|
-
is_immediate_machine = _get_immediate_machines(dispatcher, operations)
|
79
|
-
non_dominated_operations: list[Operation] = []
|
80
|
-
for operation in operations:
|
81
|
-
if any(
|
82
|
-
is_immediate_machine[machine_id]
|
83
|
-
for machine_id in operation.machines
|
84
|
-
):
|
85
|
-
non_dominated_operations.append(operation)
|
86
|
-
|
87
|
-
return non_dominated_operations
|
88
|
-
|
89
|
-
|
90
|
-
def _get_min_machine_end_times(
|
91
|
-
dispatcher: Dispatcher, available_operations: list[Operation]
|
92
|
-
) -> list[int | float]:
|
93
|
-
end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
|
94
|
-
for op in available_operations:
|
95
|
-
for machine_id in op.machines:
|
96
|
-
start_time = dispatcher.start_time(op, machine_id)
|
97
|
-
end_times_per_machine[machine_id] = min(
|
98
|
-
end_times_per_machine[machine_id], start_time + op.duration
|
99
|
-
)
|
100
|
-
return end_times_per_machine
|
101
|
-
|
102
|
-
|
103
|
-
def _get_immediate_machines(
|
104
|
-
self: Dispatcher, available_operations: list[Operation]
|
105
|
-
) -> list[bool]:
|
106
|
-
"""Returns the machine ids of the machines that have at least one
|
107
|
-
operation with the lowest start time (i.e. the start time)."""
|
108
|
-
working_machines = [False] * self.instance.num_machines
|
109
|
-
# We can't use the current_time directly because it will cause
|
110
|
-
# an infinite loop.
|
111
|
-
current_time = self.min_start_time(available_operations)
|
112
|
-
for op in available_operations:
|
113
|
-
for machine_id in op.machines:
|
114
|
-
if self.start_time(op, machine_id) == current_time:
|
115
|
-
working_machines[machine_id] = True
|
116
|
-
return working_machines
|