job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__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/_job_shop_instance.py +34 -29
- job_shop_lib/_operation.py +4 -2
- job_shop_lib/_schedule.py +11 -11
- job_shop_lib/benchmarking/_load_benchmark.py +3 -3
- job_shop_lib/constraint_programming/_ortools_solver.py +6 -6
- job_shop_lib/dispatching/__init__.py +4 -3
- job_shop_lib/dispatching/_dispatcher.py +19 -19
- job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
- job_shop_lib/dispatching/_factories.py +4 -2
- job_shop_lib/dispatching/_history_observer.py +2 -1
- job_shop_lib/dispatching/_optimal_operations_observer.py +115 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
- job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
- job_shop_lib/dispatching/rules/__init__.py +37 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +50 -20
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
- job_shop_lib/dispatching/rules/_utils.py +9 -8
- job_shop_lib/generation/__init__.py +8 -0
- job_shop_lib/generation/_general_instance_generator.py +42 -64
- job_shop_lib/generation/_instance_generator.py +11 -7
- job_shop_lib/generation/_transformations.py +5 -4
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +7 -7
- job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
- job_shop_lib/graphs/_job_shop_graph.py +17 -13
- job_shop_lib/graphs/_node.py +6 -4
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
- job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
- job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
- job_shop_lib/reinforcement_learning/_utils.py +3 -3
- job_shop_lib/visualization/__init__.py +0 -60
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
- job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
- job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/METADATA +21 -15
- job_shop_lib-1.0.0b2.dist-info/RECORD +70 -0
- job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
- job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
"""Home of the `OptimalOperationsObserver` class."""
|
2
|
+
|
3
|
+
from typing import List, Set, Dict
|
4
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
5
|
+
from job_shop_lib import Schedule, Operation, ScheduledOperation
|
6
|
+
from job_shop_lib.exceptions import ValidationError
|
7
|
+
|
8
|
+
|
9
|
+
class OptimalOperationsObserver(DispatcherObserver):
|
10
|
+
"""Observer that identifies which available operations are optimal based on
|
11
|
+
a reference schedule.
|
12
|
+
|
13
|
+
This observer compares the available operations at each step with a
|
14
|
+
reference schedule to determine which operations would lead to the optimal
|
15
|
+
solution. It can be used for training purposes or to analyze decision
|
16
|
+
making in dispatching algorithms.
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
optimal_operations: Set of operations that are considered optimal
|
20
|
+
based on the reference schedule.
|
21
|
+
reference_schedule: The reference schedule used to determine optimal
|
22
|
+
operations.
|
23
|
+
_operation_to_scheduled: Dictionary mapping operations to their
|
24
|
+
scheduled versions in the reference schedule.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
dispatcher: The dispatcher instance to observe.
|
28
|
+
reference_schedule: A complete schedule that represents the optimal
|
29
|
+
or reference solution.
|
30
|
+
subscribe: If True, automatically subscribes to the dispatcher.
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
ValidationError: If the reference schedule is incomplete or if it
|
34
|
+
doesn't match the dispatcher's instance.
|
35
|
+
"""
|
36
|
+
|
37
|
+
_is_singleton = False
|
38
|
+
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
dispatcher: Dispatcher,
|
42
|
+
reference_schedule: Schedule,
|
43
|
+
*,
|
44
|
+
subscribe: bool = True,
|
45
|
+
):
|
46
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
47
|
+
|
48
|
+
if not reference_schedule.is_complete():
|
49
|
+
raise ValidationError("Reference schedule must be complete.")
|
50
|
+
|
51
|
+
if reference_schedule.instance != dispatcher.instance:
|
52
|
+
raise ValidationError(
|
53
|
+
"Reference schedule instance does not match dispatcher "
|
54
|
+
"instance."
|
55
|
+
)
|
56
|
+
|
57
|
+
self.reference_schedule = reference_schedule
|
58
|
+
self.optimal_available: Set[Operation] = set()
|
59
|
+
self._operation_to_scheduled: Dict[Operation, ScheduledOperation] = {}
|
60
|
+
self._machine_next_operation_index: List[int] = [0] * len(
|
61
|
+
reference_schedule.schedule
|
62
|
+
)
|
63
|
+
|
64
|
+
self._build_operation_mapping()
|
65
|
+
self._update_optimal_operations()
|
66
|
+
|
67
|
+
def _build_operation_mapping(self) -> None:
|
68
|
+
"""Builds a mapping from operations to their scheduled versions in
|
69
|
+
the reference schedule."""
|
70
|
+
for machine_schedule in self.reference_schedule.schedule:
|
71
|
+
for scheduled_op in machine_schedule:
|
72
|
+
self._operation_to_scheduled[scheduled_op.operation] = (
|
73
|
+
scheduled_op
|
74
|
+
)
|
75
|
+
|
76
|
+
def _update_optimal_operations(self) -> None:
|
77
|
+
"""Updates the set of optimal operations based on current state.
|
78
|
+
|
79
|
+
An operation is considered optimal if it is the next unscheduled
|
80
|
+
operation in its machine's sequence according to the reference
|
81
|
+
schedule.
|
82
|
+
"""
|
83
|
+
self.optimal_available.clear()
|
84
|
+
available_operations = self.dispatcher.available_operations()
|
85
|
+
|
86
|
+
if not available_operations:
|
87
|
+
return
|
88
|
+
|
89
|
+
for operation in available_operations:
|
90
|
+
scheduled_op = self._operation_to_scheduled[operation]
|
91
|
+
machine_index = scheduled_op.machine_id
|
92
|
+
next_index = self._machine_next_operation_index[machine_index]
|
93
|
+
|
94
|
+
if (
|
95
|
+
scheduled_op
|
96
|
+
== self.reference_schedule.schedule[machine_index][next_index]
|
97
|
+
):
|
98
|
+
self.optimal_available.add(operation)
|
99
|
+
|
100
|
+
def update(self, scheduled_operation: ScheduledOperation) -> None:
|
101
|
+
"""Updates the optimal operations after an operation is scheduled.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
scheduled_operation: The operation that was just scheduled.
|
105
|
+
"""
|
106
|
+
self._machine_next_operation_index[scheduled_operation.machine_id] += 1
|
107
|
+
self._update_optimal_operations()
|
108
|
+
|
109
|
+
def reset(self) -> None:
|
110
|
+
"""Resets the observer to its initial state."""
|
111
|
+
self._machine_next_operation_index = [0] * len(
|
112
|
+
self.dispatcher.schedule.schedule
|
113
|
+
)
|
114
|
+
self.optimal_available.clear()
|
115
|
+
self._update_optimal_operations()
|
@@ -4,6 +4,7 @@ This functions are used by the `Dispatcher` class to reduce the
|
|
4
4
|
amount of available operations to choose from.
|
5
5
|
"""
|
6
6
|
|
7
|
+
from typing import List, Set
|
7
8
|
from collections.abc import Callable
|
8
9
|
|
9
10
|
from job_shop_lib import Operation
|
@@ -11,13 +12,13 @@ from job_shop_lib.dispatching import Dispatcher
|
|
11
12
|
|
12
13
|
|
13
14
|
ReadyOperationsFilter = Callable[
|
14
|
-
[Dispatcher,
|
15
|
+
[Dispatcher, List[Operation]], List[Operation]
|
15
16
|
]
|
16
17
|
|
17
18
|
|
18
19
|
def filter_non_idle_machines(
|
19
|
-
dispatcher: Dispatcher, operations:
|
20
|
-
) ->
|
20
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
21
|
+
) -> List[Operation]:
|
21
22
|
"""Filters out all the operations associated with non-idle machines.
|
22
23
|
|
23
24
|
A machine is considered idle if there are no ongoing operations
|
@@ -40,7 +41,7 @@ def filter_non_idle_machines(
|
|
40
41
|
|
41
42
|
# Filter operations to keep those that are associated with at least one
|
42
43
|
# idle machine
|
43
|
-
filtered_operations:
|
44
|
+
filtered_operations: List[Operation] = []
|
44
45
|
for operation in operations:
|
45
46
|
if all(
|
46
47
|
machine_id in non_idle_machines
|
@@ -54,7 +55,7 @@ def filter_non_idle_machines(
|
|
54
55
|
|
55
56
|
def _get_non_idle_machines(
|
56
57
|
dispatcher: Dispatcher, current_time: int
|
57
|
-
) ->
|
58
|
+
) -> Set[int]:
|
58
59
|
"""Returns the set of machine ids that are currently busy (i.e., have at
|
59
60
|
least one uncompleted operation)."""
|
60
61
|
|
@@ -70,8 +71,8 @@ def _get_non_idle_machines(
|
|
70
71
|
|
71
72
|
|
72
73
|
def filter_non_immediate_operations(
|
73
|
-
dispatcher: Dispatcher, operations:
|
74
|
-
) ->
|
74
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
75
|
+
) -> List[Operation]:
|
75
76
|
"""Filters out all the operations that can't start immediately.
|
76
77
|
|
77
78
|
An operation can start immediately if its earliest start time is the
|
@@ -86,7 +87,7 @@ def filter_non_immediate_operations(
|
|
86
87
|
"""
|
87
88
|
|
88
89
|
min_start_time = dispatcher.min_start_time(operations)
|
89
|
-
immediate_operations:
|
90
|
+
immediate_operations: List[Operation] = []
|
90
91
|
for operation in operations:
|
91
92
|
start_time = dispatcher.earliest_start_time(operation)
|
92
93
|
if start_time == min_start_time:
|
@@ -96,8 +97,8 @@ def filter_non_immediate_operations(
|
|
96
97
|
|
97
98
|
|
98
99
|
def filter_dominated_operations(
|
99
|
-
dispatcher: Dispatcher, operations:
|
100
|
-
) ->
|
100
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
101
|
+
) -> List[Operation]:
|
101
102
|
"""Filters out all the operations that are dominated.
|
102
103
|
An operation is dominated if there is another operation that ends before
|
103
104
|
it starts on the same machine.
|
@@ -105,7 +106,7 @@ def filter_dominated_operations(
|
|
105
106
|
|
106
107
|
min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
|
107
108
|
|
108
|
-
non_dominated_operations:
|
109
|
+
non_dominated_operations: List[Operation] = []
|
109
110
|
for operation in operations:
|
110
111
|
# One benchmark instance has an operation with duration 0
|
111
112
|
if operation.duration == 0:
|
@@ -121,13 +122,13 @@ def filter_dominated_operations(
|
|
121
122
|
|
122
123
|
|
123
124
|
def filter_non_immediate_machines(
|
124
|
-
dispatcher: Dispatcher, operations:
|
125
|
-
) ->
|
125
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
126
|
+
) -> List[Operation]:
|
126
127
|
"""Filters out all the operations associated with machines which earliest
|
127
128
|
operation is not the current time."""
|
128
129
|
|
129
130
|
is_immediate_machine = _get_immediate_machines(dispatcher, operations)
|
130
|
-
non_dominated_operations:
|
131
|
+
non_dominated_operations: List[Operation] = []
|
131
132
|
for operation in operations:
|
132
133
|
if any(
|
133
134
|
is_immediate_machine[machine_id]
|
@@ -139,8 +140,8 @@ def filter_non_immediate_machines(
|
|
139
140
|
|
140
141
|
|
141
142
|
def _get_min_machine_end_times(
|
142
|
-
dispatcher: Dispatcher, available_operations:
|
143
|
-
) ->
|
143
|
+
dispatcher: Dispatcher, available_operations: List[Operation]
|
144
|
+
) -> List[float]:
|
144
145
|
end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
|
145
146
|
for op in available_operations:
|
146
147
|
for machine_id in op.machines:
|
@@ -152,8 +153,8 @@ def _get_min_machine_end_times(
|
|
152
153
|
|
153
154
|
|
154
155
|
def _get_immediate_machines(
|
155
|
-
self: Dispatcher, available_operations:
|
156
|
-
) ->
|
156
|
+
self: Dispatcher, available_operations: List[Operation]
|
157
|
+
) -> List[bool]:
|
157
158
|
"""Returns the machine ids of the machines that have at least one
|
158
159
|
operation with the lowest start time (i.e. the start time)."""
|
159
160
|
working_machines = [False] * self.instance.num_machines
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import collections
|
4
4
|
from collections.abc import Iterable
|
5
5
|
import itertools
|
6
|
-
from typing import Deque
|
6
|
+
from typing import Deque, List
|
7
7
|
|
8
8
|
from job_shop_lib import Operation, ScheduledOperation
|
9
9
|
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
@@ -19,7 +19,7 @@ class UnscheduledOperationsObserver(DispatcherObserver):
|
|
19
19
|
|
20
20
|
def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
|
21
21
|
super().__init__(dispatcher, subscribe=subscribe)
|
22
|
-
self.unscheduled_operations_per_job:
|
22
|
+
self.unscheduled_operations_per_job: List[Deque[Operation]] = []
|
23
23
|
self.reset()
|
24
24
|
# In case the dispatcher has already scheduled some operations,
|
25
25
|
# we need to remove them.
|
@@ -51,7 +51,8 @@ class UnscheduledOperationsObserver(DispatcherObserver):
|
|
51
51
|
unscheduled operations.
|
52
52
|
|
53
53
|
Args:
|
54
|
-
scheduled_operation:
|
54
|
+
scheduled_operation:
|
55
|
+
The operation that has been scheduled.
|
55
56
|
"""
|
56
57
|
job_id = scheduled_operation.operation.job_id
|
57
58
|
job_deque = self.unscheduled_operations_per_job[job_id]
|
@@ -2,12 +2,11 @@
|
|
2
2
|
|
3
3
|
from collections import defaultdict
|
4
4
|
from collections.abc import Sequence
|
5
|
-
|
5
|
+
from typing import List, Dict, Union, Optional
|
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
|
-
# python 3.
|
8
|
+
# python >=3.8
|
9
9
|
from typing_extensions import Self
|
10
|
-
|
11
10
|
import numpy as np
|
12
11
|
from numpy.typing import NDArray
|
13
12
|
import pandas as pd
|
@@ -77,8 +76,8 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
77
76
|
dispatcher: Dispatcher,
|
78
77
|
*,
|
79
78
|
subscribe: bool = True,
|
80
|
-
feature_types:
|
81
|
-
feature_observers:
|
79
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
80
|
+
feature_observers: Optional[List[FeatureObserver]] = None,
|
82
81
|
):
|
83
82
|
if feature_observers is None:
|
84
83
|
feature_observers = [
|
@@ -97,7 +96,7 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
97
96
|
f"Composite feature types: {feature_types}"
|
98
97
|
)
|
99
98
|
self.feature_observers = feature_observers
|
100
|
-
self.column_names:
|
99
|
+
self.column_names: Dict[FeatureType, List[str]] = defaultdict(list)
|
101
100
|
super().__init__(dispatcher, subscribe=subscribe)
|
102
101
|
self._set_column_names()
|
103
102
|
|
@@ -129,7 +128,7 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
129
128
|
return composite_observer
|
130
129
|
|
131
130
|
@property
|
132
|
-
def features_as_dataframe(self) ->
|
131
|
+
def features_as_dataframe(self) -> Dict[FeatureType, pd.DataFrame]:
|
133
132
|
"""Returns the features as a dictionary of `pd.DataFrame` instances."""
|
134
133
|
return {
|
135
134
|
feature_type: pd.DataFrame(
|
@@ -139,7 +138,7 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
139
138
|
}
|
140
139
|
|
141
140
|
def initialize_features(self):
|
142
|
-
features:
|
141
|
+
features: Dict[FeatureType, List[NDArray[np.float32]]] = defaultdict(
|
143
142
|
list
|
144
143
|
)
|
145
144
|
for observer in self.feature_observers:
|
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Home of the `EarliestStartTimeObserver` class."""
|
2
2
|
|
3
|
+
from typing import List, Optional, Union
|
4
|
+
|
3
5
|
import numpy as np
|
4
6
|
from numpy.typing import NDArray
|
5
7
|
|
@@ -75,7 +77,7 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
75
77
|
dispatcher: Dispatcher,
|
76
78
|
*,
|
77
79
|
subscribe: bool = True,
|
78
|
-
feature_types:
|
80
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
79
81
|
):
|
80
82
|
|
81
83
|
# Earliest start times initialization
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Contains factory functions for creating :class:`FeatureObserver`s."""
|
2
2
|
|
3
3
|
from enum import Enum
|
4
|
+
from typing import Union, Type
|
4
5
|
|
5
6
|
from job_shop_lib.dispatching import DispatcherObserverConfig
|
6
7
|
from job_shop_lib.dispatching.feature_observers import (
|
@@ -36,22 +37,20 @@ class FeatureObserverType(str, Enum):
|
|
36
37
|
|
37
38
|
|
38
39
|
# FeatureObserverConfig = DispatcherObserverConfig[
|
39
|
-
#
|
40
|
+
# Type[FeatureObserver] | FeatureObserverType | str
|
40
41
|
# ]
|
41
|
-
FeatureObserverConfig =
|
42
|
-
|
43
|
-
|
44
|
-
| DispatcherObserverConfig[str]
|
45
|
-
)
|
42
|
+
FeatureObserverConfig = DispatcherObserverConfig[
|
43
|
+
Union[Type[FeatureObserver], FeatureObserverType, str]
|
44
|
+
]
|
46
45
|
|
47
46
|
|
48
47
|
def feature_observer_factory(
|
49
|
-
feature_observer_type:
|
50
|
-
str
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
48
|
+
feature_observer_type: Union[
|
49
|
+
str,
|
50
|
+
FeatureObserverType,
|
51
|
+
Type[FeatureObserver],
|
52
|
+
FeatureObserverConfig
|
53
|
+
],
|
55
54
|
**kwargs,
|
56
55
|
) -> FeatureObserver:
|
57
56
|
"""Creates and returns a :class:`FeatureObserver` based on the specified
|
@@ -73,12 +72,12 @@ def feature_observer_factory(
|
|
73
72
|
**feature_observer_type.kwargs,
|
74
73
|
**kwargs,
|
75
74
|
)
|
76
|
-
# if the instance is of type
|
75
|
+
# if the instance is of type Type[FeatureObserver] we can just
|
77
76
|
# call the object constructor with the keyword arguments
|
78
77
|
if isinstance(feature_observer_type, type):
|
79
78
|
return feature_observer_type(**kwargs)
|
80
79
|
|
81
|
-
mapping: dict[FeatureObserverType,
|
80
|
+
mapping: dict[FeatureObserverType, Type[FeatureObserver]] = {
|
82
81
|
FeatureObserverType.IS_READY: IsReadyObserver,
|
83
82
|
FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
|
84
83
|
FeatureObserverType.DURATION: DurationObserver,
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Home of the `FeatureObserver` class and `FeatureType` enum."""
|
2
2
|
|
3
3
|
import enum
|
4
|
+
from typing import Optional, Union, Dict, List, Tuple
|
4
5
|
|
5
6
|
import numpy as np
|
6
7
|
|
@@ -64,7 +65,7 @@ class FeatureObserver(DispatcherObserver):
|
|
64
65
|
"""
|
65
66
|
|
66
67
|
_is_singleton = False
|
67
|
-
_feature_sizes:
|
68
|
+
_feature_sizes: Union[Dict[FeatureType, int], int] = 1
|
68
69
|
_supported_feature_types = list(FeatureType)
|
69
70
|
|
70
71
|
__slots__ = {
|
@@ -84,7 +85,7 @@ class FeatureObserver(DispatcherObserver):
|
|
84
85
|
dispatcher: Dispatcher,
|
85
86
|
*,
|
86
87
|
subscribe: bool = True,
|
87
|
-
feature_types:
|
88
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
88
89
|
):
|
89
90
|
feature_types = self._get_feature_types_list(feature_types)
|
90
91
|
if isinstance(self._feature_sizes, int):
|
@@ -116,7 +117,7 @@ class FeatureObserver(DispatcherObserver):
|
|
116
117
|
self.initialize_features()
|
117
118
|
|
118
119
|
@property
|
119
|
-
def feature_sizes(self) ->
|
120
|
+
def feature_sizes(self) -> Dict[FeatureType, int]:
|
120
121
|
"""Returns the size of the features.
|
121
122
|
|
122
123
|
The size of the features is the number of values being observed for
|
@@ -134,12 +135,12 @@ class FeatureObserver(DispatcherObserver):
|
|
134
135
|
return self._feature_sizes
|
135
136
|
|
136
137
|
@property
|
137
|
-
def supported_feature_types(self) ->
|
138
|
+
def supported_feature_types(self) -> List[FeatureType]:
|
138
139
|
"""Returns the supported feature types."""
|
139
140
|
return self._supported_feature_types
|
140
141
|
|
141
142
|
@property
|
142
|
-
def feature_dimensions(self) ->
|
143
|
+
def feature_dimensions(self) -> Dict[FeatureType, Tuple[int, int]]:
|
143
144
|
"""A dictionary containing the shape of each :class:`FeatureType`."""
|
144
145
|
feature_dimensions = {}
|
145
146
|
for feature_type, array in self.features.items():
|
@@ -170,7 +171,7 @@ class FeatureObserver(DispatcherObserver):
|
|
170
171
|
self.initialize_features()
|
171
172
|
|
172
173
|
def set_features_to_zero(
|
173
|
-
self, exclude: FeatureType
|
174
|
+
self, exclude: Optional[Union[FeatureType, List[FeatureType]]] = None
|
174
175
|
):
|
175
176
|
"""Sets all features to zero except for the ones specified in
|
176
177
|
``exclude``.
|
@@ -196,8 +197,8 @@ class FeatureObserver(DispatcherObserver):
|
|
196
197
|
|
197
198
|
def _get_feature_types_list(
|
198
199
|
self,
|
199
|
-
feature_types:
|
200
|
-
) ->
|
200
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]],
|
201
|
+
) -> List[FeatureType]:
|
201
202
|
"""Returns a list of feature types.
|
202
203
|
|
203
204
|
Args:
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Home of the `IsCompletedObserver` class."""
|
2
2
|
|
3
|
+
from typing import Optional, Union, List
|
3
4
|
import numpy as np
|
4
5
|
|
5
6
|
from job_shop_lib import ScheduledOperation
|
@@ -52,7 +53,7 @@ class IsCompletedObserver(FeatureObserver):
|
|
52
53
|
self,
|
53
54
|
dispatcher: Dispatcher,
|
54
55
|
*,
|
55
|
-
feature_types:
|
56
|
+
feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
|
56
57
|
subscribe: bool = True,
|
57
58
|
):
|
58
59
|
feature_types = self._get_feature_types_list(feature_types)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Home of the `IsReadyObserver` class."""
|
2
2
|
|
3
|
+
from typing import List
|
4
|
+
|
3
5
|
from job_shop_lib.dispatching.feature_observers import (
|
4
6
|
FeatureObserver,
|
5
7
|
FeatureType,
|
@@ -16,7 +18,7 @@ class IsReadyObserver(FeatureObserver):
|
|
16
18
|
feature_ids = self._get_ready_feature_ids(feature_type)
|
17
19
|
feature[feature_ids, 0] = 1.0
|
18
20
|
|
19
|
-
def _get_ready_feature_ids(self, feature_type: FeatureType) ->
|
21
|
+
def _get_ready_feature_ids(self, feature_type: FeatureType) -> List[int]:
|
20
22
|
if feature_type == FeatureType.OPERATIONS:
|
21
23
|
return self._get_ready_operations()
|
22
24
|
if feature_type == FeatureType.MACHINES:
|
@@ -28,6 +30,6 @@ class IsReadyObserver(FeatureObserver):
|
|
28
30
|
def reset(self):
|
29
31
|
self.initialize_features()
|
30
32
|
|
31
|
-
def _get_ready_operations(self) ->
|
33
|
+
def _get_ready_operations(self) -> List[int]:
|
32
34
|
available_operations = self.dispatcher.available_operations()
|
33
35
|
return [operation.operation_id for operation in available_operations]
|
@@ -1,4 +1,40 @@
|
|
1
|
-
"""Contains the dispatching rules for the job shop scheduling problem.
|
1
|
+
"""Contains the dispatching rules for the job shop scheduling problem.
|
2
|
+
|
3
|
+
Main objects:
|
4
|
+
|
5
|
+
.. autosummary::
|
6
|
+
|
7
|
+
DispatchingRuleSolver
|
8
|
+
dispatching_rule_factory
|
9
|
+
DispatchingRuleType
|
10
|
+
MachineChooserType
|
11
|
+
dispatching_rule_factory
|
12
|
+
machine_chooser_factory
|
13
|
+
|
14
|
+
Dispatching rules:
|
15
|
+
|
16
|
+
.. autosummary::
|
17
|
+
|
18
|
+
shortest_processing_time_rule
|
19
|
+
first_come_first_served_rule
|
20
|
+
most_work_remaining_rule
|
21
|
+
most_operations_remaining_rule
|
22
|
+
random_operation_rule
|
23
|
+
score_based_rule
|
24
|
+
score_based_rule_with_tie_breaker
|
25
|
+
observer_based_most_work_remaining_rule
|
26
|
+
|
27
|
+
Dispatching rule scorers:
|
28
|
+
|
29
|
+
.. autosummary::
|
30
|
+
|
31
|
+
shortest_processing_time_score
|
32
|
+
first_come_first_served_score
|
33
|
+
MostWorkRemainingScorer
|
34
|
+
most_operations_remaining_score
|
35
|
+
random_score
|
36
|
+
|
37
|
+
"""
|
2
38
|
|
3
39
|
from ._dispatching_rules_functions import (
|
4
40
|
shortest_processing_time_rule,
|
@@ -5,6 +5,8 @@ The factory functions create and return the appropriate functions based on the
|
|
5
5
|
specified names or enums.
|
6
6
|
"""
|
7
7
|
|
8
|
+
from typing import Dict, Union
|
9
|
+
|
8
10
|
from enum import Enum
|
9
11
|
from collections.abc import Callable
|
10
12
|
|
@@ -31,7 +33,7 @@ class DispatchingRuleType(str, Enum):
|
|
31
33
|
|
32
34
|
|
33
35
|
def dispatching_rule_factory(
|
34
|
-
dispatching_rule: str
|
36
|
+
dispatching_rule: Union[str, DispatchingRuleType,]
|
35
37
|
) -> Callable[[Dispatcher], Operation]:
|
36
38
|
"""Creates and returns a dispatching rule function based on the specified
|
37
39
|
dispatching rule name.
|
@@ -55,7 +57,7 @@ def dispatching_rule_factory(
|
|
55
57
|
If the dispatching_rule argument is not recognized or it is
|
56
58
|
not supported.
|
57
59
|
"""
|
58
|
-
dispatching_rules:
|
60
|
+
dispatching_rules: Dict[
|
59
61
|
DispatchingRuleType,
|
60
62
|
Callable[[Dispatcher], Operation],
|
61
63
|
] = {
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Home of the `DispatchingRuleSolver` class."""
|
2
2
|
|
3
|
+
from typing import Optional, Union
|
3
4
|
from collections.abc import Callable, Iterable
|
4
5
|
|
5
6
|
from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
|
@@ -29,14 +30,14 @@ class DispatchingRuleSolver(BaseSolver):
|
|
29
30
|
Used to choose the machine where the operation will be dispatched
|
30
31
|
to. It is only used if the operation can be dispatched to multiple
|
31
32
|
machines.
|
32
|
-
|
33
|
-
The
|
33
|
+
ready_operations_filter:
|
34
|
+
The ready operations filter to use. It is used to initialize the
|
34
35
|
dispatcher object internally when calling the solve method.
|
35
36
|
|
36
37
|
Args:
|
37
38
|
dispatching_rule:
|
38
39
|
The dispatching rule to use. It can be a string with the name
|
39
|
-
of the dispatching rule, a class
|
40
|
+
of the dispatching rule, a :class:`DispatchingRuleType` member,
|
40
41
|
or a callable that takes a dispatcher and returns the operation to
|
41
42
|
be dispatched next.
|
42
43
|
machine_chooser:
|
@@ -65,21 +66,26 @@ class DispatchingRuleSolver(BaseSolver):
|
|
65
66
|
|
66
67
|
def __init__(
|
67
68
|
self,
|
68
|
-
dispatching_rule:
|
69
|
-
str
|
70
|
-
|
71
|
-
machine_chooser:
|
72
|
-
str
|
73
|
-
|
74
|
-
ready_operations_filter:
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
69
|
+
dispatching_rule: Union[
|
70
|
+
str, Callable[[Dispatcher], Operation]
|
71
|
+
] = DispatchingRuleType.MOST_WORK_REMAINING,
|
72
|
+
machine_chooser: Union[
|
73
|
+
str, Callable[[Dispatcher, Operation], int]
|
74
|
+
] = MachineChooserType.FIRST,
|
75
|
+
ready_operations_filter: Optional[
|
76
|
+
Union[
|
77
|
+
Iterable[
|
78
|
+
Union[
|
79
|
+
ReadyOperationsFilter, str, ReadyOperationsFilterType
|
80
|
+
]
|
81
|
+
],
|
82
|
+
str,
|
83
|
+
ReadyOperationsFilterType,
|
84
|
+
ReadyOperationsFilter,
|
85
|
+
]
|
86
|
+
] = (
|
81
87
|
ReadyOperationsFilterType.DOMINATED_OPERATIONS,
|
82
|
-
ReadyOperationsFilterType.
|
88
|
+
ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS,
|
83
89
|
),
|
84
90
|
):
|
85
91
|
if isinstance(dispatching_rule, str):
|
@@ -100,10 +106,34 @@ class DispatchingRuleSolver(BaseSolver):
|
|
100
106
|
self.ready_operations_filter = ready_operations_filter
|
101
107
|
|
102
108
|
def solve(
|
103
|
-
self,
|
109
|
+
self,
|
110
|
+
instance: JobShopInstance,
|
111
|
+
dispatcher: Optional[Dispatcher] = None,
|
104
112
|
) -> Schedule:
|
105
|
-
"""
|
106
|
-
|
113
|
+
"""Solves the instance using the dispatching rule and machine chooser
|
114
|
+
algorithms.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
instance:
|
118
|
+
The job shop instance to be solved.
|
119
|
+
dispatcher:
|
120
|
+
The dispatcher object that will be used to dispatch the
|
121
|
+
operations. If not provided, a new dispatcher will be created
|
122
|
+
using the ready operations filter provided in the constructor.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
The schedule obtained after solving the instance.
|
126
|
+
|
127
|
+
.. tip::
|
128
|
+
Another way to use the solver is by calling it as a function. This
|
129
|
+
will call the ``solve`` method internally and will add metadata to
|
130
|
+
the schedule. For example:
|
131
|
+
|
132
|
+
.. code-block:: python
|
133
|
+
|
134
|
+
solver = DispatchingRuleSolver()
|
135
|
+
schedule = solver(instance)
|
136
|
+
"""
|
107
137
|
if dispatcher is None:
|
108
138
|
dispatcher = Dispatcher(
|
109
139
|
instance, ready_operations_filter=self.ready_operations_filter
|