job-shop-lib 1.0.0a1__py3-none-any.whl → 1.0.0a3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/_job_shop_instance.py +18 -20
- job_shop_lib/_operation.py +30 -24
- job_shop_lib/_schedule.py +17 -16
- job_shop_lib/_scheduled_operation.py +10 -12
- job_shop_lib/constraint_programming/_ortools_solver.py +31 -16
- job_shop_lib/dispatching/__init__.py +4 -0
- job_shop_lib/dispatching/_dispatcher.py +24 -32
- job_shop_lib/dispatching/_factories.py +8 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +34 -2
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +54 -14
- job_shop_lib/dispatching/feature_observers/_duration_observer.py +15 -2
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +62 -10
- job_shop_lib/dispatching/feature_observers/_factory.py +5 -1
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +87 -16
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +32 -2
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +3 -3
- job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py +9 -5
- job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py +7 -2
- job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py +1 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +66 -43
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
- job_shop_lib/graphs/__init__.py +2 -0
- job_shop_lib/graphs/_build_agent_task_graph.py +2 -2
- job_shop_lib/graphs/_constants.py +18 -1
- job_shop_lib/graphs/_job_shop_graph.py +36 -20
- job_shop_lib/graphs/_node.py +60 -52
- job_shop_lib/graphs/graph_updaters/__init__.py +11 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +1 -1
- job_shop_lib/visualization/__init__.py +5 -5
- job_shop_lib/visualization/_gantt_chart_creator.py +5 -5
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +63 -36
- job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/METADATA +15 -3
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/RECORD +37 -37
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/WHEEL +1 -1
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/LICENSE +0 -0
@@ -12,7 +12,7 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
12
12
|
class DurationObserver(FeatureObserver):
|
13
13
|
"""Measures the remaining duration of operations, machines, and jobs.
|
14
14
|
|
15
|
-
The duration of an :class
|
15
|
+
The duration of an :class:`~job_shop_lib.Operation` is:
|
16
16
|
- if the operation has not been scheduled, it is the duration of the
|
17
17
|
operation.
|
18
18
|
- if the operation has been scheduled, it is the remaining duration of
|
@@ -22,8 +22,21 @@ class DurationObserver(FeatureObserver):
|
|
22
22
|
manually if needed. We do not update the duration of completed
|
23
23
|
operations to save computation time.
|
24
24
|
|
25
|
-
The duration of a
|
25
|
+
The duration of a machine or job is the sum of the durations of the
|
26
26
|
unscheduled operations that belong to the machine or job.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
dispatcher:
|
30
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
31
|
+
subscribe:
|
32
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
33
|
+
initialization. Otherwise, the observer must be subscribed later
|
34
|
+
or manually updated.
|
35
|
+
feature_types:
|
36
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
37
|
+
that specifies the types of features to observe. They must be a
|
38
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
39
|
+
If ``None``, all supported feature types are tracked.
|
27
40
|
"""
|
28
41
|
|
29
42
|
def initialize_features(self):
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Home of the `EarliestStartTimeObserver` class."""
|
2
2
|
|
3
3
|
import numpy as np
|
4
|
+
from numpy.typing import NDArray
|
4
5
|
|
5
6
|
from job_shop_lib.dispatching import Dispatcher
|
6
7
|
from job_shop_lib.dispatching.feature_observers import (
|
@@ -12,14 +13,62 @@ from job_shop_lib import ScheduledOperation
|
|
12
13
|
|
13
14
|
class EarliestStartTimeObserver(FeatureObserver):
|
14
15
|
"""Observer that adds a feature indicating the earliest start time of
|
15
|
-
each operation, machine, and job in the graph.
|
16
|
+
each operation, machine, and job in the graph.
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
The earliest start time of an operation refers to the earliest time at
|
19
|
+
which the operation could potentially start without violating any
|
20
|
+
constraints. This time is normalized by the current time (i.e., the
|
21
|
+
difference between the earliest start time and the current time).
|
22
|
+
|
23
|
+
The earliest start time of a machine is the earliest start time of the
|
24
|
+
next operation that can be scheduled on that machine.
|
25
|
+
|
26
|
+
Finally, the earliest start time of a job is the earliest start time of the
|
27
|
+
next operation in the job.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
dispatcher:
|
31
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
32
|
+
subscribe:
|
33
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
34
|
+
initialization. Otherwise, the observer must be subscribed later
|
35
|
+
or manually updated.
|
36
|
+
feature_types:
|
37
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
38
|
+
that specifies the types of features to observe. They must be a
|
39
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
40
|
+
If ``None``, all supported feature types are tracked.
|
41
|
+
"""
|
42
|
+
|
43
|
+
__slots__ = {
|
44
|
+
"earliest_start_times": (
|
45
|
+
"A 2D numpy array with the earliest start "
|
46
|
+
"times of each operation. The array has "
|
47
|
+
"shape (``num_jobs``, ``max_operations_per_job``). "
|
48
|
+
"The value at index (i, j) is the earliest start "
|
49
|
+
"time of the j-th operation in the i-th job. "
|
50
|
+
"If a job has fewer than the maximum number of "
|
51
|
+
"operations in a job, the remaining values are "
|
52
|
+
"set to ``np.nan``. Similarly to "
|
53
|
+
":class:`~job_shop_lib.JobShopInstance`'s "
|
54
|
+
":meth:`~job_shop_lib.JobShopInstance.durations_matrix_array` "
|
55
|
+
"method."
|
56
|
+
),
|
57
|
+
"_job_ids": (
|
58
|
+
"An array that stores the job IDs for each operation in the "
|
59
|
+
"dispatcher's instance. The array has shape "
|
60
|
+
"(``num_machines``, ``max_operations_per_machine``)."
|
61
|
+
),
|
62
|
+
"_positions": (
|
63
|
+
"An array that stores the positions of each operation in their "
|
64
|
+
"respective jobs. The array has shape "
|
65
|
+
"(``num_machines``, ``max_operations_per_machine``)."
|
66
|
+
),
|
67
|
+
"_is_regular_instance": (
|
68
|
+
"Whether the dispatcher's instance is a regular "
|
69
|
+
"instance, where each job has the same number of operations."
|
70
|
+
),
|
71
|
+
}
|
23
72
|
|
24
73
|
def __init__(
|
25
74
|
self,
|
@@ -32,9 +81,9 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
32
81
|
# Earliest start times initialization
|
33
82
|
# -------------------------------
|
34
83
|
squared_duration_matrix = dispatcher.instance.durations_matrix_array
|
35
|
-
self.earliest_start_times = np.hstack(
|
84
|
+
self.earliest_start_times: NDArray[np.float32] = np.hstack(
|
36
85
|
(
|
37
|
-
np.zeros((squared_duration_matrix.shape[0], 1)),
|
86
|
+
np.zeros((squared_duration_matrix.shape[0], 1), dtype=float),
|
38
87
|
np.cumsum(squared_duration_matrix[:, :-1], axis=1),
|
39
88
|
)
|
40
89
|
)
|
@@ -70,7 +119,7 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
70
119
|
|
71
120
|
def update(self, scheduled_operation: ScheduledOperation):
|
72
121
|
"""Recomputes the earliest start times and calls the
|
73
|
-
|
122
|
+
``initialize_features`` method.
|
74
123
|
|
75
124
|
The earliest start times is computed as the cumulative sum of the
|
76
125
|
previous unscheduled operations in the job plus the maximum of the
|
@@ -78,6 +127,9 @@ class EarliestStartTimeObserver(FeatureObserver):
|
|
78
127
|
time of the machine(s) the operation is assigned.
|
79
128
|
|
80
129
|
After that, we substract the current time.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
scheduled_operation: The operation that has been scheduled.
|
81
133
|
"""
|
82
134
|
# We compute the gap that the current scheduled operation could be
|
83
135
|
# adding to each job.
|
@@ -16,7 +16,11 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
16
16
|
|
17
17
|
|
18
18
|
class FeatureObserverType(str, Enum):
|
19
|
-
"""Enumeration of the different feature observers.
|
19
|
+
"""Enumeration of the different feature observers.
|
20
|
+
|
21
|
+
Each feature observer is associated with a string value that can be used
|
22
|
+
to create the feature observer using the factory function.
|
23
|
+
"""
|
20
24
|
|
21
25
|
IS_READY = "is_ready"
|
22
26
|
EARLIEST_START_TIME = "earliest_start_time"
|
@@ -20,6 +20,27 @@ class FeatureType(str, enum.Enum):
|
|
20
20
|
class FeatureObserver(DispatcherObserver):
|
21
21
|
"""Base class for feature observers.
|
22
22
|
|
23
|
+
A :class:`FeatureObserver` is a
|
24
|
+
a subclass of :class:`~job_shop_lib.dispatching.DispatcherObserver` that
|
25
|
+
observes features related to operations, machines, or jobs in the
|
26
|
+
:class:`~job_shop_lib.dispatching.Dispatcher`.
|
27
|
+
|
28
|
+
Attributes are stored in numpy arrays with a shape of (``num_entities``,
|
29
|
+
``feature_size``), where ``num_entities`` is the number of entities being
|
30
|
+
observed (e.g., operations, machines, or jobs) and ``feature_size`` is the
|
31
|
+
number of values being observed for each entity.
|
32
|
+
|
33
|
+
The advantage of using arrays is that they can be easily updated in a
|
34
|
+
vectorized manner, which is more efficient than updating each attribute
|
35
|
+
individually. Furthermore, machine learning models can be trained on these
|
36
|
+
arrays to predict the best dispatching decisions.
|
37
|
+
|
38
|
+
Arrays use the data type ``np.float32``. This is because most machine
|
39
|
+
|
40
|
+
New :class:`FeatureObservers` must inherit from this class, and re-define
|
41
|
+
the class attributes ``_singleton`` (defualt ), ``_feature_size``
|
42
|
+
(default 1) and ``_supported_feature_types`` (default all feature types).
|
43
|
+
|
23
44
|
Feature observers are not singleton by default. This means that more than
|
24
45
|
one instance of the same feature observer type can be subscribed to the
|
25
46
|
dispatcher. This is useful when the first subscriber only observes a subset
|
@@ -27,16 +48,36 @@ class FeatureObserver(DispatcherObserver):
|
|
27
48
|
them. For example, the first subscriber could observe only the
|
28
49
|
operation-related features, while the second subscriber could observe the
|
29
50
|
jobs.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
dispatcher:
|
54
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
55
|
+
subscribe:
|
56
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
57
|
+
initialization. Otherwise, the observer must be subscribed later
|
58
|
+
or manually updated.
|
59
|
+
feature_types:
|
60
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
61
|
+
that specifies the types of features to observe. They must be a
|
62
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
63
|
+
If ``None``, all supported feature types are tracked.
|
30
64
|
"""
|
31
65
|
|
32
66
|
_is_singleton = False
|
33
|
-
|
67
|
+
_feature_sizes: dict[FeatureType, int] | int = 1
|
34
68
|
_supported_feature_types = list(FeatureType)
|
35
69
|
|
36
|
-
__slots__ =
|
37
|
-
"features"
|
38
|
-
|
39
|
-
|
70
|
+
__slots__ = {
|
71
|
+
"features": (
|
72
|
+
"A dictionary of numpy arrays with the features. "
|
73
|
+
"Each key is a :class:`FeatureType` and each value is a numpy "
|
74
|
+
"array with the features. The array has shape (``num_entities``, "
|
75
|
+
"``feature_size``), where ``num_entities`` is the number of "
|
76
|
+
"entities being observed (e.g., operations, machines, or jobs) and"
|
77
|
+
" ``feature_size`` is the number of values being observed for each"
|
78
|
+
" entity."
|
79
|
+
)
|
80
|
+
}
|
40
81
|
|
41
82
|
def __init__(
|
42
83
|
self,
|
@@ -46,9 +87,9 @@ class FeatureObserver(DispatcherObserver):
|
|
46
87
|
feature_types: list[FeatureType] | FeatureType | None = None,
|
47
88
|
):
|
48
89
|
feature_types = self._get_feature_types_list(feature_types)
|
49
|
-
if isinstance(self.
|
90
|
+
if isinstance(self._feature_sizes, int):
|
50
91
|
feature_size = {
|
51
|
-
feature_type: self.
|
92
|
+
feature_type: self._feature_sizes
|
52
93
|
for feature_type in feature_types
|
53
94
|
}
|
54
95
|
super().__init__(dispatcher, subscribe=subscribe)
|
@@ -58,7 +99,7 @@ class FeatureObserver(DispatcherObserver):
|
|
58
99
|
FeatureType.MACHINES: dispatcher.instance.num_machines,
|
59
100
|
FeatureType.JOBS: dispatcher.instance.num_jobs,
|
60
101
|
}
|
61
|
-
|
102
|
+
feature_dimensions = {
|
62
103
|
feature_type: (
|
63
104
|
number_of_entities[feature_type],
|
64
105
|
feature_size[feature_type],
|
@@ -67,7 +108,7 @@ class FeatureObserver(DispatcherObserver):
|
|
67
108
|
}
|
68
109
|
self.features = {
|
69
110
|
feature_type: np.zeros(
|
70
|
-
|
111
|
+
feature_dimensions[feature_type],
|
71
112
|
dtype=np.float32,
|
72
113
|
)
|
73
114
|
for feature_type in feature_types
|
@@ -75,23 +116,42 @@ class FeatureObserver(DispatcherObserver):
|
|
75
116
|
self.initialize_features()
|
76
117
|
|
77
118
|
@property
|
78
|
-
def
|
79
|
-
"""Returns the size of the features.
|
80
|
-
|
119
|
+
def feature_sizes(self) -> dict[FeatureType, int]:
|
120
|
+
"""Returns the size of the features.
|
121
|
+
|
122
|
+
The size of the features is the number of values being observed for
|
123
|
+
each entity. This corresponds to the second dimension of each array.
|
124
|
+
|
125
|
+
This number is typically one (e.g. measuring the duration
|
126
|
+
of each operation), but some feature observers like the
|
127
|
+
:class:`CompositeFeatureObserver` may track more than one value.
|
128
|
+
"""
|
129
|
+
if isinstance(self._feature_sizes, int):
|
81
130
|
return {
|
82
|
-
feature_type: self.
|
131
|
+
feature_type: self._feature_sizes
|
83
132
|
for feature_type in self.features
|
84
133
|
}
|
85
|
-
return self.
|
134
|
+
return self._feature_sizes
|
86
135
|
|
87
136
|
@property
|
88
137
|
def supported_feature_types(self) -> list[FeatureType]:
|
89
138
|
"""Returns the supported feature types."""
|
90
139
|
return self._supported_feature_types
|
91
140
|
|
141
|
+
@property
|
142
|
+
def feature_dimensions(self) -> dict[FeatureType, tuple[int, int]]:
|
143
|
+
"""A dictionary containing the shape of each :class:`FeatureType`."""
|
144
|
+
feature_dimensions = {}
|
145
|
+
for feature_type, array in self.features.items():
|
146
|
+
feature_dimensions[feature_type] = array.shape
|
147
|
+
return feature_dimensions # type: ignore[return-value]
|
148
|
+
|
92
149
|
def initialize_features(self):
|
93
150
|
"""Initializes the features based on the current state of the
|
94
|
-
dispatcher.
|
151
|
+
dispatcher.
|
152
|
+
|
153
|
+
This method is automatically called after initializing the observer.
|
154
|
+
"""
|
95
155
|
|
96
156
|
def update(self, scheduled_operation: ScheduledOperation):
|
97
157
|
"""Updates the features based on the scheduled operation.
|
@@ -112,7 +172,18 @@ class FeatureObserver(DispatcherObserver):
|
|
112
172
|
def set_features_to_zero(
|
113
173
|
self, exclude: FeatureType | list[FeatureType] | None = None
|
114
174
|
):
|
115
|
-
"""Sets features to zero
|
175
|
+
"""Sets all features to zero except for the ones specified in
|
176
|
+
``exclude``.
|
177
|
+
|
178
|
+
Setting a feature to zero means that all values in the feature array
|
179
|
+
are set to this value.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
exclude:
|
183
|
+
A single :class:`FeatureType` or a list of :class:`FeatureType`
|
184
|
+
that specifies the features that should not be set to zero. If
|
185
|
+
``None``, all currently used features are set to zero.
|
186
|
+
"""
|
116
187
|
if exclude is None:
|
117
188
|
exclude = []
|
118
189
|
if isinstance(exclude, FeatureType):
|
@@ -15,8 +15,38 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
15
15
|
|
16
16
|
|
17
17
|
class IsCompletedObserver(FeatureObserver):
|
18
|
-
"""
|
19
|
-
machine, or job has been completed.
|
18
|
+
"""Adds a binary feature indicating whether each operation,
|
19
|
+
machine, or job has been completed.
|
20
|
+
|
21
|
+
An operation is considered completed if it has been scheduled and the
|
22
|
+
current time is greater than or equal to the sum of the operation's start
|
23
|
+
time and duration.
|
24
|
+
|
25
|
+
A machine or job is considered completed if all of its operations have been
|
26
|
+
completed.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
dispatcher:
|
30
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
31
|
+
feature_types:
|
32
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
33
|
+
that specifies the types of features to observe. They must be a
|
34
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
35
|
+
If ``None``, all supported feature types are tracked.
|
36
|
+
subscribe:
|
37
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
38
|
+
initialization. Otherwise, the observer must be subscribed later
|
39
|
+
or manually updated.
|
40
|
+
"""
|
41
|
+
|
42
|
+
__slots__ = {
|
43
|
+
"remaining_ops_per_machine": (
|
44
|
+
"The number of unscheduled operations per machine."
|
45
|
+
),
|
46
|
+
"remaining_ops_per_job": (
|
47
|
+
"The number of unscheduled operations per job."
|
48
|
+
),
|
49
|
+
}
|
20
50
|
|
21
51
|
def __init__(
|
22
52
|
self,
|
@@ -7,8 +7,8 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
7
7
|
|
8
8
|
|
9
9
|
class IsReadyObserver(FeatureObserver):
|
10
|
-
"""Feature creator that adds a binary feature indicating if the operation
|
11
|
-
is ready to be dispatched."""
|
10
|
+
"""Feature creator that adds a binary feature indicating if the operation,
|
11
|
+
machine or job is ready to be dispatched."""
|
12
12
|
|
13
13
|
def initialize_features(self):
|
14
14
|
self.set_features_to_zero()
|
@@ -29,5 +29,5 @@ class IsReadyObserver(FeatureObserver):
|
|
29
29
|
self.initialize_features()
|
30
30
|
|
31
31
|
def _get_ready_operations(self) -> list[int]:
|
32
|
-
available_operations = self.dispatcher.
|
32
|
+
available_operations = self.dispatcher.available_operations()
|
33
33
|
return [operation.operation_id for operation in available_operations]
|
@@ -8,14 +8,18 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
8
8
|
|
9
9
|
|
10
10
|
class IsScheduledObserver(FeatureObserver):
|
11
|
-
"""
|
11
|
+
"""Updates features based on scheduling operations.
|
12
12
|
|
13
13
|
This observer tracks which operations have been scheduled and updates
|
14
|
-
feature matrices accordingly.
|
15
|
-
|
16
|
-
|
14
|
+
feature matrices accordingly.
|
15
|
+
|
16
|
+
It updates a feature in the
|
17
|
+
:meth:`FeatureType.OPERATIONS` matrix to indicate that an operation has
|
18
|
+
been scheduled.
|
19
|
+
|
20
|
+
Additionally, it counts the number of uncompleted but
|
17
21
|
scheduled operations for each machine and job, updating the respective
|
18
|
-
|
22
|
+
:meth:`FeatureType.MACHINES` and :meth:`FeatureType.JOBS` feature matrices.
|
19
23
|
"""
|
20
24
|
|
21
25
|
def update(self, scheduled_operation: ScheduledOperation):
|
@@ -8,10 +8,15 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
8
8
|
|
9
9
|
|
10
10
|
class PositionInJobObserver(FeatureObserver):
|
11
|
-
"""
|
11
|
+
"""Adds a feature indicating the position of
|
12
12
|
operations in their respective jobs.
|
13
13
|
|
14
|
-
Positions are adjusted dynamically as operations are scheduled.
|
14
|
+
Positions are adjusted dynamically as operations are scheduled. In other
|
15
|
+
words, the position of an operation is the number of unscheduled operations
|
16
|
+
that precede it in the job.
|
17
|
+
|
18
|
+
It only supports the :meth:`~job_shop_lib.FeatureType.OPERATIONS` feature
|
19
|
+
type.
|
15
20
|
"""
|
16
21
|
|
17
22
|
_supported_feature_types = [FeatureType.OPERATIONS]
|
@@ -12,7 +12,7 @@ class RemainingOperationsObserver(FeatureObserver):
|
|
12
12
|
"""Adds a feature indicating the number of remaining operations for each
|
13
13
|
job and machine.
|
14
14
|
|
15
|
-
It does not support :
|
15
|
+
It does not support :meth:`FeatureType.OPERATIONS`.
|
16
16
|
"""
|
17
17
|
|
18
18
|
_supported_feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
|
@@ -1,12 +1,14 @@
|
|
1
1
|
"""Home of the `DispatchingRuleSolver` class."""
|
2
2
|
|
3
|
-
from collections.abc import Callable
|
3
|
+
from collections.abc import Callable, Iterable
|
4
4
|
|
5
5
|
from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
|
6
6
|
from job_shop_lib.dispatching import (
|
7
7
|
ready_operations_filter_factory,
|
8
8
|
Dispatcher,
|
9
9
|
ReadyOperationsFilterType,
|
10
|
+
ReadyOperationsFilter,
|
11
|
+
create_composite_operation_filter,
|
10
12
|
)
|
11
13
|
from job_shop_lib.dispatching.rules import (
|
12
14
|
dispatching_rule_factory,
|
@@ -30,6 +32,35 @@ class DispatchingRuleSolver(BaseSolver):
|
|
30
32
|
pruning_function:
|
31
33
|
The pruning function to use. It is used to initialize the
|
32
34
|
dispatcher object internally when calling the solve method.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
dispatching_rule:
|
38
|
+
The dispatching rule to use. It can be a string with the name
|
39
|
+
of the dispatching rule, a class`DispatchingRuleType` enum member,
|
40
|
+
or a callable that takes a dispatcher and returns the operation to
|
41
|
+
be dispatched next.
|
42
|
+
machine_chooser:
|
43
|
+
The machine chooser to use. It can be a string with the name
|
44
|
+
of the machine chooser, a :class:`MachineChooserType` member, or a
|
45
|
+
callable that takes a dispatcher and an operation and returns
|
46
|
+
the machine id where the operation will be dispatched.
|
47
|
+
ready_operations_filter:
|
48
|
+
The ready operations filter to use. It can be either:
|
49
|
+
|
50
|
+
- a string with the name of the pruning function
|
51
|
+
- a :class`ReadyOperationsFilterType` enum member.
|
52
|
+
- a callable that takes a dispatcher and a list of operations
|
53
|
+
and returns a list of operations that should be considered
|
54
|
+
for dispatching,
|
55
|
+
- a list with names or actual ready operations filters to be used.
|
56
|
+
If a list is provided, a composite filter will be created
|
57
|
+
using the specified filters.
|
58
|
+
|
59
|
+
.. seealso::
|
60
|
+
- :func:`job_shop_lib.dispatching.rules.dispatching_rule_factory`
|
61
|
+
- :func:`job_shop_lib.dispatching.rules.machine_chooser_factory`
|
62
|
+
- :func:`~job_shop_lib.dispatching.ready_operations_filter_factory`
|
63
|
+
- :func:`~job_shop_lib.dispatching.create_composite_operation_filter`
|
33
64
|
"""
|
34
65
|
|
35
66
|
def __init__(
|
@@ -40,45 +71,33 @@ class DispatchingRuleSolver(BaseSolver):
|
|
40
71
|
machine_chooser: (
|
41
72
|
str | Callable[[Dispatcher, Operation], int]
|
42
73
|
) = MachineChooserType.FIRST,
|
43
|
-
|
44
|
-
str
|
45
|
-
|
|
74
|
+
ready_operations_filter: (
|
75
|
+
Iterable[ReadyOperationsFilter | str | ReadyOperationsFilterType]
|
76
|
+
| str
|
77
|
+
| ReadyOperationsFilterType
|
78
|
+
| ReadyOperationsFilter
|
46
79
|
| None
|
47
|
-
) =
|
80
|
+
) = (
|
81
|
+
ReadyOperationsFilterType.DOMINATED_OPERATIONS,
|
82
|
+
ReadyOperationsFilterType.NON_IDLE_MACHINES,
|
83
|
+
),
|
48
84
|
):
|
49
|
-
"""Initializes the solver with the given dispatching rule, machine
|
50
|
-
chooser and pruning function.
|
51
|
-
|
52
|
-
Args:
|
53
|
-
dispatching_rule:
|
54
|
-
The dispatching rule to use. It can be a string with the name
|
55
|
-
of the dispatching rule, a DispatchingRule enum member, or a
|
56
|
-
callable that takes a dispatcher and returns the operation to
|
57
|
-
be dispatched next.
|
58
|
-
machine_chooser:
|
59
|
-
The machine chooser to use. It can be a string with the name
|
60
|
-
of the machine chooser, a MachineChooser enum member, or a
|
61
|
-
callable that takes a dispatcher and an operation and returns
|
62
|
-
the machine id where the operation will be dispatched.
|
63
|
-
pruning_function:
|
64
|
-
The pruning function to use. It can be a string with the name
|
65
|
-
of the pruning function, a PruningFunction enum member, or a
|
66
|
-
callable that takes a dispatcher and a list of operations and
|
67
|
-
returns a list of operations that should be considered for
|
68
|
-
dispatching.
|
69
|
-
"""
|
70
85
|
if isinstance(dispatching_rule, str):
|
71
86
|
dispatching_rule = dispatching_rule_factory(dispatching_rule)
|
72
87
|
if isinstance(machine_chooser, str):
|
73
88
|
machine_chooser = machine_chooser_factory(machine_chooser)
|
74
|
-
if isinstance(
|
75
|
-
|
76
|
-
|
89
|
+
if isinstance(ready_operations_filter, str):
|
90
|
+
ready_operations_filter = ready_operations_filter_factory(
|
91
|
+
ready_operations_filter
|
92
|
+
)
|
93
|
+
if isinstance(ready_operations_filter, Iterable):
|
94
|
+
ready_operations_filter = create_composite_operation_filter(
|
95
|
+
ready_operations_filter
|
77
96
|
)
|
78
97
|
|
79
98
|
self.dispatching_rule = dispatching_rule
|
80
99
|
self.machine_chooser = machine_chooser
|
81
|
-
self.
|
100
|
+
self.ready_operations_filter = ready_operations_filter
|
82
101
|
|
83
102
|
def solve(
|
84
103
|
self, instance: JobShopInstance, dispatcher: Dispatcher | None = None
|
@@ -87,7 +106,7 @@ class DispatchingRuleSolver(BaseSolver):
|
|
87
106
|
dispatching rule algorithm."""
|
88
107
|
if dispatcher is None:
|
89
108
|
dispatcher = Dispatcher(
|
90
|
-
instance, ready_operations_filter=self.
|
109
|
+
instance, ready_operations_filter=self.ready_operations_filter
|
91
110
|
)
|
92
111
|
while not dispatcher.schedule.is_complete():
|
93
112
|
self.step(dispatcher)
|
@@ -110,19 +129,23 @@ class DispatchingRuleSolver(BaseSolver):
|
|
110
129
|
if __name__ == "__main__":
|
111
130
|
import time
|
112
131
|
import cProfile
|
113
|
-
import pstats
|
114
|
-
from io import StringIO
|
115
|
-
from job_shop_lib.benchmarking import
|
132
|
+
# import pstats
|
133
|
+
# from io import StringIO
|
134
|
+
from job_shop_lib.benchmarking import (
|
135
|
+
# load_benchmark_instance,
|
136
|
+
load_all_benchmark_instances,
|
137
|
+
)
|
116
138
|
|
117
139
|
# from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
|
118
140
|
# most_work_remaining_rule_2,
|
119
141
|
# )
|
120
142
|
|
121
|
-
ta_instances = [
|
122
|
-
|
123
|
-
]
|
143
|
+
# ta_instances = [
|
144
|
+
# load_benchmark_instance(f"ta{i:02d}") for i in range(1, 81)
|
145
|
+
# ]
|
146
|
+
ta_instances = load_all_benchmark_instances().values()
|
124
147
|
solver = DispatchingRuleSolver(
|
125
|
-
dispatching_rule="most_work_remaining",
|
148
|
+
dispatching_rule="most_work_remaining", ready_operations_filter=None
|
126
149
|
)
|
127
150
|
|
128
151
|
start = time.perf_counter()
|
@@ -131,10 +154,10 @@ if __name__ == "__main__":
|
|
131
154
|
profiler = cProfile.Profile()
|
132
155
|
|
133
156
|
# Run the code under profiling
|
134
|
-
profiler.enable()
|
157
|
+
# profiler.enable()
|
135
158
|
for instance_ in ta_instances:
|
136
159
|
solver.solve(instance_)
|
137
|
-
profiler.disable()
|
160
|
+
# profiler.disable()
|
138
161
|
|
139
162
|
end = time.perf_counter()
|
140
163
|
|
@@ -142,7 +165,7 @@ if __name__ == "__main__":
|
|
142
165
|
print(f"Elapsed time: {end - start:.2f} seconds.")
|
143
166
|
|
144
167
|
# Print profiling results
|
145
|
-
s = StringIO()
|
146
|
-
ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
|
147
|
-
profiler.print_stats("cumtime") # Print top 20 time-consuming functions
|
168
|
+
# s = StringIO()
|
169
|
+
# ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
|
170
|
+
# profiler.print_stats("cumtime") # Print top 20 time-consuming functions
|
148
171
|
# print(s.getvalue())
|