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.
Files changed (27) hide show
  1. job_shop_lib/_operation.py +24 -20
  2. job_shop_lib/_schedule.py +14 -13
  3. job_shop_lib/_scheduled_operation.py +9 -11
  4. job_shop_lib/constraint_programming/_ortools_solver.py +31 -16
  5. job_shop_lib/dispatching/feature_observers/__init__.py +34 -2
  6. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +54 -14
  7. job_shop_lib/dispatching/feature_observers/_duration_observer.py +15 -2
  8. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +62 -10
  9. job_shop_lib/dispatching/feature_observers/_factory.py +5 -1
  10. job_shop_lib/dispatching/feature_observers/_feature_observer.py +87 -16
  11. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +32 -2
  12. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +2 -2
  13. job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py +9 -5
  14. job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py +7 -2
  15. job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py +1 -1
  16. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +28 -24
  17. job_shop_lib/graphs/__init__.py +2 -0
  18. job_shop_lib/graphs/_build_agent_task_graph.py +2 -2
  19. job_shop_lib/graphs/_constants.py +18 -1
  20. job_shop_lib/graphs/_job_shop_graph.py +36 -20
  21. job_shop_lib/graphs/_node.py +60 -52
  22. job_shop_lib/graphs/graph_updaters/__init__.py +11 -1
  23. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +1 -1
  24. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/METADATA +1 -1
  25. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/RECORD +27 -27
  26. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/LICENSE +0 -0
  27. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a2.dist-info}/WHEEL +0 -0
@@ -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
- Attributes:
27
- machines: A list of machine ids that can perform the operation.
28
- duration: The time it takes to perform the operation.
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
- "duration",
34
- "job_id",
35
- "position_in_job",
36
- "operation_id",
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
- "_schedule",
41
- "metadata",
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
- Attributes:
11
- operation:
12
- The :class:`Operation` object that is scheduled.
13
- start_time:
14
- The time at which the operation is scheduled to start.
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 (``bool``):
25
+ log_search_progress (bool):
26
26
  Whether to log the search progress to the console.
27
- max_time_in_seconds (``float | None``):
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 (``cp_model.CpModel``):
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 (``cp_model.CpSolver``):
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 `Dispatcher` by concatenating their feature matrices along the
28
- first axis (horizontal concatenation).
29
-
30
- Attributes:
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
- List of `FeatureObserver` instances to aggregate features from.
33
- column_names:
34
- Dictionary mapping `FeatureType` to a list of column names for the
35
- corresponding feature matrix. Column names are generated based on
36
- the class name of the `FeatureObserver` instance that produced the
37
- feature.
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:`Operation` is:
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 Machine or Job is the sum of the durations of the
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
- __slots__ = (
18
- "earliest_start_times",
19
- "_job_ids",
20
- "_positions",
21
- "machine_ids",
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
- `initialize_features` method.
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
- _feature_size: dict[FeatureType, int] | int = 1
67
+ _feature_sizes: dict[FeatureType, int] | int = 1
34
68
  _supported_feature_types = list(FeatureType)
35
69
 
36
- __slots__ = (
37
- "features",
38
- "feature_dimensions",
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._feature_size, int):
90
+ if isinstance(self._feature_sizes, int):
50
91
  feature_size = {
51
- feature_type: self._feature_size
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
- self.feature_dimensions = {
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
- self.feature_dimensions[feature_type],
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 feature_size(self) -> dict[FeatureType, int]:
79
- """Returns the size of the features."""
80
- if isinstance(self._feature_size, int):
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._feature_size
131
+ feature_type: self._feature_sizes
83
132
  for feature_type in self.features
84
133
  }
85
- return self._feature_size
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):