job-shop-lib 1.0.0a1__py3-none-any.whl → 1.0.0a2__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/_operation.py +24 -20
- job_shop_lib/_schedule.py +14 -13
- job_shop_lib/_scheduled_operation.py +9 -11
- job_shop_lib/constraint_programming/_ortools_solver.py +31 -16
- 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 +2 -2
- 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 +28 -24
- 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/visualization/_gantt_chart_video_and_gif_creation.py +1 -1
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/METADATA +1 -1
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/RECORD +27 -27
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/LICENSE +0 -0
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/WHEEL +0 -0
job_shop_lib/_operation.py
CHANGED
@@ -23,30 +23,34 @@ class Operation:
|
|
23
23
|
necessary to multiply all durations by a sufficiently large integer so
|
24
24
|
that every duration is an integer.
|
25
25
|
|
26
|
-
|
27
|
-
machines:
|
28
|
-
|
26
|
+
Args:
|
27
|
+
machines:
|
28
|
+
A list of machine ids that can perform the operation. If
|
29
|
+
only one machine can perform the operation, it can be passed as
|
30
|
+
an integer.
|
31
|
+
duration:
|
32
|
+
The time it takes to perform the operation.
|
29
33
|
"""
|
30
34
|
|
31
|
-
__slots__ =
|
32
|
-
"machines"
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
__slots__ = {
|
36
|
+
"machines": (
|
37
|
+
"A list of machine ids that can perform the operation. If "
|
38
|
+
"only one machine can perform the operation, it can be passed as "
|
39
|
+
"an integer."
|
40
|
+
),
|
41
|
+
"duration": (
|
42
|
+
"The time it takes to perform the operation. Often referred"
|
43
|
+
" to as the processing time."
|
44
|
+
),
|
45
|
+
"job_id": "The id of the job the operation belongs to.",
|
46
|
+
"position_in_job": "The index of the operation in the job.",
|
47
|
+
"operation_id": (
|
48
|
+
"The id of the operation. This is unique within a "
|
49
|
+
":class:`JobShopInstance`."
|
50
|
+
),
|
51
|
+
}
|
38
52
|
|
39
53
|
def __init__(self, machines: int | list[int], duration: int):
|
40
|
-
"""Initializes the object with the given machines and duration.
|
41
|
-
|
42
|
-
Args:
|
43
|
-
machines:
|
44
|
-
A list of machine ids that can perform the operation. If
|
45
|
-
only one machine can perform the operation, it can be passed as
|
46
|
-
an integer.
|
47
|
-
duration:
|
48
|
-
The time it takes to perform the operation.
|
49
|
-
"""
|
50
54
|
self.machines = [machines] if isinstance(machines, int) else machines
|
51
55
|
self.duration = duration
|
52
56
|
|
job_shop_lib/_schedule.py
CHANGED
@@ -25,21 +25,22 @@ class Schedule:
|
|
25
25
|
is_complete
|
26
26
|
add
|
27
27
|
reset
|
28
|
-
|
29
|
-
Attributes:
|
30
|
-
instance:
|
31
|
-
The :class:`JobShopInstance` object that the schedule is for.
|
32
|
-
metadata:
|
33
|
-
A dictionary with additional information about the schedule. It
|
34
|
-
can be used to store information about the algorithm that generated
|
35
|
-
the schedule, for example.
|
36
28
|
"""
|
37
29
|
|
38
|
-
__slots__ =
|
39
|
-
"instance"
|
40
|
-
|
41
|
-
|
42
|
-
|
30
|
+
__slots__ = {
|
31
|
+
"instance": (
|
32
|
+
"The :class:`JobShopInstance` object that the schedule is for."
|
33
|
+
),
|
34
|
+
"_schedule": (
|
35
|
+
"A list of lists of :class:`ScheduledOperation` objects. "
|
36
|
+
"Each list represents the order of operations on a machine."
|
37
|
+
),
|
38
|
+
"metadata": (
|
39
|
+
"A dictionary with additional information about the "
|
40
|
+
"schedule. It can be used to store information about the "
|
41
|
+
"algorithm that generated the schedule, for example."
|
42
|
+
),
|
43
|
+
}
|
43
44
|
|
44
45
|
def __init__(
|
45
46
|
self,
|
@@ -5,17 +5,15 @@ from job_shop_lib.exceptions import ValidationError
|
|
5
5
|
|
6
6
|
|
7
7
|
class ScheduledOperation:
|
8
|
-
"""Data structure to store a scheduled operation.
|
9
|
-
|
10
|
-
|
11
|
-
operation:
|
12
|
-
|
13
|
-
|
14
|
-
The
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
__slots__ = ("operation", "start_time", "_machine_id")
|
8
|
+
"""Data structure to store a scheduled operation."""
|
9
|
+
|
10
|
+
__slots__ = {
|
11
|
+
"operation": "The :class:`Operation` object that is scheduled.",
|
12
|
+
"start_time": "The time at which the operation is scheduled to start.",
|
13
|
+
"_machine_id": (
|
14
|
+
"The id of the machine on which the operation is scheduled."
|
15
|
+
),
|
16
|
+
}
|
19
17
|
|
20
18
|
def __init__(self, operation: Operation, start_time: int, machine_id: int):
|
21
19
|
"""Initializes a new instance of the :class:`ScheduledOperation` class.
|
@@ -22,22 +22,32 @@ class ORToolsSolver(BaseSolver):
|
|
22
22
|
<https://developers.google.com/optimization/cp/cp_solver>`_ solver.
|
23
23
|
|
24
24
|
Attributes:
|
25
|
-
log_search_progress (
|
25
|
+
log_search_progress (bool):
|
26
26
|
Whether to log the search progress to the console.
|
27
|
-
max_time_in_seconds (
|
27
|
+
max_time_in_seconds (float | None):
|
28
28
|
The maximum time in seconds to allow the solver to search for
|
29
29
|
a solution. If no solution is found within this time, a
|
30
30
|
:class:`~job_shop_lib.exceptions.NoSolutionFoundError` is
|
31
31
|
raised. If ``None``, the solver will run until an optimal
|
32
32
|
solution is found.
|
33
|
-
model (
|
33
|
+
model (cp_model.CpModel):
|
34
34
|
The `OR-Tools' CP-SAT model
|
35
35
|
<https://developers.google.com/optimization/reference/python/sat/
|
36
36
|
python/cp_model#cp_model.CpModel>`_.
|
37
|
-
solver (
|
37
|
+
solver (cp_model.CpSolver):
|
38
38
|
The `OR-Tools' CP-SAT solver
|
39
39
|
<https://developers.google.com/optimization/reference/python/sat/
|
40
40
|
python/cp_model#cp_model.CpSolver>`_.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
max_time_in_seconds:
|
44
|
+
The maximum time in seconds to allow the solver to search for
|
45
|
+
a solution. If no solution is found within this time, a
|
46
|
+
:class:`~job_shop_lib.exceptions.NoSolutionFoundError` is
|
47
|
+
raised. If ``None``, the solver will run until an optimal
|
48
|
+
solution is found.
|
49
|
+
log_search_progress:
|
50
|
+
Whether to log the search progress to the console.
|
41
51
|
"""
|
42
52
|
|
43
53
|
def __init__(
|
@@ -45,18 +55,6 @@ class ORToolsSolver(BaseSolver):
|
|
45
55
|
max_time_in_seconds: float | None = None,
|
46
56
|
log_search_progress: bool = False,
|
47
57
|
):
|
48
|
-
"""Initializes the solver.
|
49
|
-
|
50
|
-
Args:
|
51
|
-
max_time_in_seconds:
|
52
|
-
The maximum time in seconds to allow the solver to search for
|
53
|
-
a solution. If no solution is found within this time, a
|
54
|
-
:class:`~job_shop_lib.exceptions.NoSolutionFoundError` is
|
55
|
-
raised. If ``None``, the solver will run until an optimal
|
56
|
-
solution is found.
|
57
|
-
log_search_progress:
|
58
|
-
Whether to log the search progress to the console.
|
59
|
-
"""
|
60
58
|
self.log_search_progress = log_search_progress
|
61
59
|
self.max_time_in_seconds = max_time_in_seconds
|
62
60
|
|
@@ -66,6 +64,23 @@ class ORToolsSolver(BaseSolver):
|
|
66
64
|
self._operations_start: dict[Operation, tuple[IntVar, IntVar]] = {}
|
67
65
|
|
68
66
|
def __call__(self, instance: JobShopInstance) -> Schedule:
|
67
|
+
"""Equivalent to calling the :meth:`~ORToolsSolver.solve` method.
|
68
|
+
|
69
|
+
This method is necessary because, in JobShopLib, solvers are defined
|
70
|
+
as callables that receive an instance and return a schedule.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
instance: The job shop instance to be solved.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
The best schedule found by the solver.
|
77
|
+
Its metadata contains the following information:
|
78
|
+
|
79
|
+
* status (``str``): ``"optimal"`` or ``"feasible"``
|
80
|
+
* elapsed_time (``float``): The time taken to solve the problem
|
81
|
+
* makespan (``int``): The total duration of the schedule
|
82
|
+
* solved_by (``str``): ``"ORToolsSolver"``
|
83
|
+
"""
|
69
84
|
# Re-defined here since we already add metadata to the schedule in
|
70
85
|
# the solve method.
|
71
86
|
return self.solve(instance)
|
@@ -1,5 +1,37 @@
|
|
1
|
-
"""Contains FeatureObserver classes for observing features of the
|
2
|
-
dispatcher.
|
1
|
+
"""Contains :class:`FeatureObserver` classes for observing features of the
|
2
|
+
dispatcher.
|
3
|
+
|
4
|
+
.. autosummary::
|
5
|
+
:nosignatures:
|
6
|
+
|
7
|
+
FeatureObserver
|
8
|
+
FeatureType
|
9
|
+
CompositeFeatureObserver
|
10
|
+
EarliestStartTimeObserver
|
11
|
+
IsReadyObserver
|
12
|
+
DurationObserver
|
13
|
+
IsScheduledObserver
|
14
|
+
PositionInJobObserver
|
15
|
+
RemainingOperationsObserver
|
16
|
+
IsCompletedObserver
|
17
|
+
FeatureObserverType
|
18
|
+
feature_observer_factory
|
19
|
+
FeatureObserverConfig
|
20
|
+
|
21
|
+
A :class:`~job_shop_lib.dispatching.feature_observers.FeatureObserver` is a
|
22
|
+
a subclass of :class:`~job_shop_lib.dispatching.DispatcherObserver` that
|
23
|
+
observes features related to operations, machines, or jobs in the dispatcher.
|
24
|
+
|
25
|
+
Attributes are stored in numpy arrays with a shape of (``num_entities``,
|
26
|
+
``feature_size``), where ``num_entities`` is the number of entities being
|
27
|
+
observed (e.g., operations, machines, or jobs) and ``feature_size`` is the
|
28
|
+
number of values being observed for each entity.
|
29
|
+
|
30
|
+
The advantage of using arrays is that they can be easily updated in a
|
31
|
+
vectorized manner, which is more efficient than updating each attribute
|
32
|
+
individually. Furthermore, machine learning models can be trained on these
|
33
|
+
arrays to predict the best dispatching decisions.
|
34
|
+
"""
|
3
35
|
|
4
36
|
from ._feature_observer import FeatureObserver, FeatureType
|
5
37
|
from ._earliest_start_time_observer import EarliestStartTimeObserver
|
@@ -24,19 +24,54 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
24
24
|
|
25
25
|
class CompositeFeatureObserver(FeatureObserver):
|
26
26
|
"""Aggregates features from other FeatureObserver instances subscribed to
|
27
|
-
the same
|
28
|
-
first axis (horizontal concatenation).
|
29
|
-
|
30
|
-
|
27
|
+
the same :class:`~job_shop_lib.dispatching.Dispatcher` by concatenating
|
28
|
+
their feature matrices along the first axis (horizontal concatenation).
|
29
|
+
|
30
|
+
It provides also a custom ``__str__`` method to display the features
|
31
|
+
in a more readable way.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
dispatcher:
|
35
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
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
|
+
feature_types:
|
41
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
42
|
+
that specifies the types of features to observe. They must be a
|
43
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
44
|
+
If ``None``, all supported feature types are tracked.
|
31
45
|
feature_observers:
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
46
|
+
A list of `FeatureObserver` instances to aggregate features from.
|
47
|
+
If ``None``, all feature observers subscribed to the dispatcher are
|
48
|
+
used.
|
49
|
+
|
50
|
+
.. seealso::
|
51
|
+
|
52
|
+
An example using this class can be found in the
|
53
|
+
:doc:`../examples/08-Feature-Observers` example.
|
54
|
+
|
55
|
+
Additionally, the class
|
56
|
+
:class:`~job_shop_lib.reinforcement_learning.SingleJobShopGraphEnv`
|
57
|
+
uses this feature observer to aggregate features from multiple
|
58
|
+
ones.
|
59
|
+
|
38
60
|
"""
|
39
61
|
|
62
|
+
__slots__ = {
|
63
|
+
"feature_observers": (
|
64
|
+
"List of :class:`FeatureObserver` instances to aggregate features "
|
65
|
+
"from."
|
66
|
+
),
|
67
|
+
"column_names": (
|
68
|
+
"Dictionary mapping :class:`FeatureType` to a list of column "
|
69
|
+
"names for the corresponding feature matrix. They are generated "
|
70
|
+
"based on the class name of the :class:`FeatureObserver` instance "
|
71
|
+
"that produced the feature."
|
72
|
+
),
|
73
|
+
}
|
74
|
+
|
40
75
|
def __init__(
|
41
76
|
self,
|
42
77
|
dispatcher: Dispatcher,
|
@@ -140,7 +175,8 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
140
175
|
|
141
176
|
|
142
177
|
if __name__ == "__main__":
|
143
|
-
from cProfile import Profile
|
178
|
+
# from cProfile import Profile
|
179
|
+
import time
|
144
180
|
from job_shop_lib.benchmarking import load_benchmark_instance
|
145
181
|
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
146
182
|
from job_shop_lib.dispatching.feature_observers import (
|
@@ -164,6 +200,10 @@ if __name__ == "__main__":
|
|
164
200
|
dispatcher_, feature_observers=feature_observers_
|
165
201
|
)
|
166
202
|
solver = DispatchingRuleSolver(dispatching_rule="random")
|
167
|
-
profiler = Profile()
|
168
|
-
profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
169
|
-
profiler.print_stats("cumtime")
|
203
|
+
# profiler = Profile()
|
204
|
+
# profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
205
|
+
# profiler.print_stats("cumtime")
|
206
|
+
start = time.perf_counter()
|
207
|
+
solver.solve(dispatcher_.instance, dispatcher_)
|
208
|
+
end = time.perf_counter()
|
209
|
+
print(f"Time: {end - start}")
|
@@ -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):
|