job-shop-lib 1.0.0a1__py3-none-any.whl → 1.0.0a2__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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):
|