job-shop-lib 0.5.0__py3-none-any.whl → 1.0.0__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.
Files changed (93) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +19 -0
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/_instance_generator.py +133 -0
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generators/__init__.py +0 -7
  84. job_shop_lib/generators/basic_generator.py +0 -197
  85. job_shop_lib/graphs/constants.py +0 -21
  86. job_shop_lib/graphs/job_shop_graph.py +0 -202
  87. job_shop_lib/graphs/node.py +0 -166
  88. job_shop_lib/operation.py +0 -122
  89. job_shop_lib/visualization/agent_task_graph.py +0 -257
  90. job_shop_lib/visualization/create_gif.py +0 -209
  91. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  92. job_shop_lib-0.5.0.dist-info/RECORD +0 -48
  93. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,212 @@
1
+ """Home of the `CompositeFeatureObserver` class."""
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Sequence
5
+ from typing import List, Dict, Union, Optional, Type
6
+ # The Self type can be imported directly from Python’s typing module in
7
+ # version 3.11 and beyond. We use the typing_extensions module to support
8
+ # python >=3.8
9
+ from typing_extensions import Self
10
+ import numpy as np
11
+ from numpy.typing import NDArray
12
+ import pandas as pd
13
+
14
+ from job_shop_lib.exceptions import ValidationError
15
+ from job_shop_lib.dispatching import Dispatcher
16
+ from job_shop_lib.dispatching.feature_observers import (
17
+ FeatureObserver,
18
+ FeatureType,
19
+ FeatureObserverConfig,
20
+ feature_observer_factory,
21
+ FeatureObserverType,
22
+ )
23
+
24
+
25
+ class CompositeFeatureObserver(FeatureObserver):
26
+ """Aggregates features from other FeatureObserver instances subscribed to
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.
45
+ feature_observers:
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
+
60
+ """
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
+
75
+ def __init__(
76
+ self,
77
+ dispatcher: Dispatcher,
78
+ *,
79
+ subscribe: bool = True,
80
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
81
+ feature_observers: Optional[List[FeatureObserver]] = None,
82
+ ):
83
+ if feature_observers is None:
84
+ feature_observers = [
85
+ observer
86
+ for observer in dispatcher.subscribers
87
+ if isinstance(observer, FeatureObserver)
88
+ ]
89
+ feature_types = self._get_feature_types_list(feature_types)
90
+ for observer in feature_observers:
91
+ if not set(observer.features.keys()).issubset(set(feature_types)):
92
+ raise ValidationError(
93
+ "The feature types observed by the feature observer "
94
+ f"{observer.__class__.__name__} are not a subset of the "
95
+ "feature types specified in the CompositeFeatureObserver."
96
+ f"Observer feature types: {observer.features.keys()}"
97
+ f"Composite feature types: {feature_types}"
98
+ )
99
+ self.feature_observers = feature_observers
100
+ self.column_names: Dict[FeatureType, List[str]] = defaultdict(list)
101
+ super().__init__(dispatcher, subscribe=subscribe)
102
+ self._set_column_names()
103
+
104
+ @classmethod
105
+ def from_feature_observer_configs(
106
+ cls,
107
+ dispatcher: Dispatcher,
108
+ feature_observer_configs: Sequence[
109
+ Union[
110
+ str,
111
+ FeatureObserverType,
112
+ Type[FeatureObserver],
113
+ FeatureObserverConfig,
114
+ ],
115
+ ],
116
+ subscribe: bool = True,
117
+ ) -> Self:
118
+ """Creates the composite feature observer.
119
+
120
+ Args:
121
+ dispatcher:
122
+ The dispatcher used to create the feature observers.
123
+ feature_observer_configs:
124
+ The list of feature observer configuration objects.
125
+ subscribe:
126
+ Whether to subscribe the CompositeFeatureObserver to the
127
+ dispatcher.
128
+ """
129
+ observers = [
130
+ feature_observer_factory(observer_config, dispatcher=dispatcher)
131
+ for observer_config in feature_observer_configs
132
+ ]
133
+ composite_observer = cls(
134
+ dispatcher, feature_observers=observers, subscribe=subscribe
135
+ )
136
+ return composite_observer
137
+
138
+ @property
139
+ def features_as_dataframe(self) -> Dict[FeatureType, pd.DataFrame]:
140
+ """Returns the features as a dictionary of `pd.DataFrame` instances."""
141
+ return {
142
+ feature_type: pd.DataFrame(
143
+ feature_matrix, columns=self.column_names[feature_type]
144
+ )
145
+ for feature_type, feature_matrix in self.features.items()
146
+ }
147
+
148
+ def initialize_features(self):
149
+ features: Dict[FeatureType, List[NDArray[np.float32]]] = defaultdict(
150
+ list
151
+ )
152
+ for observer in self.feature_observers:
153
+ for feature_type, feature_matrix in observer.features.items():
154
+ features[feature_type].append(feature_matrix)
155
+
156
+ self.features = {
157
+ feature_type: np.concatenate(features, axis=1)
158
+ for feature_type, features in features.items()
159
+ }
160
+
161
+ def _set_column_names(self):
162
+ for observer in self.feature_observers:
163
+ for feature_type, feature_matrix in observer.features.items():
164
+ feature_name = observer.__class__.__name__.replace(
165
+ "Observer", ""
166
+ )
167
+ if feature_matrix.shape[1] > 1:
168
+ self.column_names[feature_type] += [
169
+ f"{feature_name}_{i}"
170
+ for i in range(feature_matrix.shape[1])
171
+ ]
172
+ else:
173
+ self.column_names[feature_type].append(feature_name)
174
+
175
+ def __str__(self):
176
+ out = [f"{self.__class__.__name__}:"]
177
+ out.append("-" * (len(out[0]) - 1))
178
+ for feature_type, dataframe in self.features_as_dataframe.items():
179
+ out.append(f"{feature_type.value}:")
180
+ out.append(dataframe.to_string())
181
+ return "\n".join(out)
182
+
183
+
184
+ if __name__ == "__main__":
185
+ # from cProfile import Profile
186
+ import time
187
+ from job_shop_lib.benchmarking import load_benchmark_instance
188
+ from job_shop_lib.dispatching.rules import DispatchingRuleSolver
189
+
190
+ ta80 = load_benchmark_instance("ta80")
191
+
192
+ dispatcher_ = Dispatcher(ta80)
193
+ feature_observer_types_ = list(FeatureObserverType)
194
+ feature_observers_ = [
195
+ feature_observer_factory(
196
+ observer_type,
197
+ dispatcher=dispatcher_,
198
+ )
199
+ for observer_type in feature_observer_types_
200
+ # and not FeatureObserverType.EARLIEST_START_TIME
201
+ ]
202
+ composite_observer_ = CompositeFeatureObserver(
203
+ dispatcher_, feature_observers=feature_observers_
204
+ )
205
+ solver = DispatchingRuleSolver(dispatching_rule="random")
206
+ # profiler = Profile()
207
+ # profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
208
+ # profiler.print_stats("cumtime")
209
+ start = time.perf_counter()
210
+ solver.solve(dispatcher_.instance, dispatcher_)
211
+ end = time.perf_counter()
212
+ print(f"Time: {end - start}")
@@ -2,7 +2,6 @@
2
2
 
3
3
  import numpy as np
4
4
 
5
- from job_shop_lib.dispatching import Dispatcher
6
5
  from job_shop_lib import ScheduledOperation
7
6
  from job_shop_lib.dispatching.feature_observers import (
8
7
  FeatureObserver,
@@ -13,29 +12,32 @@ from job_shop_lib.dispatching.feature_observers import (
13
12
  class DurationObserver(FeatureObserver):
14
13
  """Measures the remaining duration of operations, machines, and jobs.
15
14
 
16
- The duration of an Operation is:
15
+ The duration of an :class:`~job_shop_lib.Operation` is:
17
16
  - if the operation has not been scheduled, it is the duration of the
18
- operation.
17
+ operation.
19
18
  - if the operation has been scheduled, it is the remaining duration of
20
- the operation.
19
+ the operation.
21
20
  - if the operation has been completed, it is the last duration of the
22
- operation that has been computed. The duration must be set to 0
23
- manually if needed. We do not update the duration of completed
24
- operations to save computation time.
21
+ operation that has been computed. The duration must be set to 0
22
+ manually if needed. We do not update the duration of completed
23
+ operations to save computation time.
25
24
 
26
- 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
27
26
  unscheduled operations that belong to the machine or job.
28
- """
29
27
 
30
- def __init__(
31
- self,
32
- dispatcher: Dispatcher,
33
- feature_types: list[FeatureType] | FeatureType | None = None,
34
- subscribe: bool = True,
35
- ):
36
- super().__init__(
37
- dispatcher, feature_types, feature_size=1, subscribe=subscribe
38
- )
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.
40
+ """
39
41
 
40
42
  def initialize_features(self):
41
43
  mapping = {
@@ -0,0 +1,289 @@
1
+ """Home of the `EarliestStartTimeObserver` class."""
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from job_shop_lib.dispatching import Dispatcher
9
+ from job_shop_lib.dispatching.feature_observers import (
10
+ FeatureObserver,
11
+ FeatureType,
12
+ )
13
+ from job_shop_lib import ScheduledOperation
14
+
15
+
16
+ class EarliestStartTimeObserver(FeatureObserver):
17
+ """Observer that adds a feature indicating the earliest start time of
18
+ each operation, machine, and job in the graph.
19
+
20
+ The earliest start time of an operation refers to the earliest time at
21
+ which the operation could potentially start without violating any
22
+ constraints. This time is normalized by the current time (i.e., the
23
+ difference between the earliest start time and the current time).
24
+
25
+ The earliest start time of a machine is the earliest start time of the
26
+ next operation that can be scheduled on that machine.
27
+
28
+ Finally, the earliest start time of a job is the earliest start time of the
29
+ next operation in the job.
30
+
31
+ Args:
32
+ dispatcher:
33
+ The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
34
+ subscribe:
35
+ If ``True``, the observer is subscribed to the dispatcher upon
36
+ initialization. Otherwise, the observer must be subscribed later
37
+ or manually updated.
38
+ feature_types:
39
+ A list of :class:`FeatureType` or a single :class:`FeatureType`
40
+ that specifies the types of features to observe. They must be a
41
+ subset of the class attribute :attr:`supported_feature_types`.
42
+ If ``None``, all supported feature types are tracked.
43
+ """
44
+
45
+ __slots__ = {
46
+ "earliest_start_times": (
47
+ "A 2D numpy array with the earliest start "
48
+ "times of each operation. The array has "
49
+ "shape (``num_jobs``, ``max_operations_per_job``). "
50
+ "The value at index (i, j) is the earliest start "
51
+ "time of the j-th operation in the i-th job. "
52
+ "If a job has fewer than the maximum number of "
53
+ "operations in a job, the remaining values are "
54
+ "set to ``np.nan``. Similarly to "
55
+ ":class:`~job_shop_lib.JobShopInstance`'s "
56
+ ":meth:`~job_shop_lib.JobShopInstance.durations_matrix_array` "
57
+ "method."
58
+ ),
59
+ "_job_ids": (
60
+ "An array that stores the job IDs for each operation in the "
61
+ "dispatcher's instance. The array has shape "
62
+ "(``num_machines``, ``max_operations_per_machine``)."
63
+ ),
64
+ "_positions": (
65
+ "An array that stores the positions of each operation in their "
66
+ "respective jobs. The array has shape "
67
+ "(``num_machines``, ``max_operations_per_machine``)."
68
+ ),
69
+ "_is_regular_instance": (
70
+ "Whether the dispatcher's instance is a regular "
71
+ "instance, where each job has the same number of operations."
72
+ ),
73
+ }
74
+
75
+ def __init__(
76
+ self,
77
+ dispatcher: Dispatcher,
78
+ *,
79
+ subscribe: bool = True,
80
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
81
+ ):
82
+
83
+ # Earliest start times initialization
84
+ # -------------------------------
85
+ squared_duration_matrix = dispatcher.instance.durations_matrix_array
86
+ self.earliest_start_times: NDArray[np.float32] = np.hstack(
87
+ (
88
+ np.zeros((squared_duration_matrix.shape[0], 1), dtype=float),
89
+ np.cumsum(squared_duration_matrix[:, :-1], axis=1),
90
+ )
91
+ )
92
+ self.earliest_start_times[np.isnan(squared_duration_matrix)] = np.nan
93
+ # -------------------------------
94
+
95
+ # Cache:
96
+ operations_by_machine = dispatcher.instance.operations_by_machine
97
+ self._is_regular_instance = all(
98
+ len(job) == len(dispatcher.instance.jobs[0])
99
+ for job in dispatcher.instance.jobs
100
+ )
101
+ if self._is_regular_instance:
102
+ self._job_ids = np.array(
103
+ [
104
+ [op.job_id for op in machine_ops]
105
+ for machine_ops in operations_by_machine
106
+ ]
107
+ )
108
+ self._positions = np.array(
109
+ [
110
+ [op.position_in_job for op in machine_ops]
111
+ for machine_ops in operations_by_machine
112
+ ]
113
+ )
114
+ else:
115
+ self._job_ids = np.array([])
116
+ self._positions = np.array([])
117
+
118
+ super().__init__(
119
+ dispatcher, feature_types=feature_types, subscribe=subscribe
120
+ )
121
+
122
+ def update(self, scheduled_operation: ScheduledOperation):
123
+ """Recomputes the earliest start times and calls the
124
+ ``initialize_features`` method.
125
+
126
+ The earliest start times is computed as the cumulative sum of the
127
+ previous unscheduled operations in the job plus the maximum of the
128
+ completion time of the last scheduled operation and the next available
129
+ time of the machine(s) the operation is assigned.
130
+
131
+ After that, we substract the current time.
132
+
133
+ Args:
134
+ scheduled_operation: The operation that has been scheduled.
135
+ """
136
+ # We compute the gap that the current scheduled operation could be
137
+ # adding to each job.
138
+ job_id = scheduled_operation.job_id
139
+ next_operation_idx = self.dispatcher.job_next_operation_index[job_id]
140
+ if next_operation_idx < len(self.dispatcher.instance.jobs[job_id]):
141
+ old_start_time = self.earliest_start_times[
142
+ job_id, next_operation_idx
143
+ ]
144
+ next_operation = self.dispatcher.instance.jobs[job_id][
145
+ next_operation_idx
146
+ ]
147
+ new_start_time = max(
148
+ scheduled_operation.end_time,
149
+ old_start_time,
150
+ self.dispatcher.earliest_start_time(next_operation),
151
+ )
152
+ gap = new_start_time - old_start_time
153
+ self.earliest_start_times[job_id, next_operation_idx:] += gap
154
+
155
+ # Now, we compute the gap that could be introduced by the new
156
+ # next_available_time of the machine.
157
+ machine_ops = self.dispatcher.instance.operations_by_machine[
158
+ scheduled_operation.machine_id
159
+ ]
160
+ unscheduled_mask = np.array(
161
+ [not self.dispatcher.is_scheduled(op) for op in machine_ops]
162
+ )
163
+ if np.any(unscheduled_mask):
164
+ if self._job_ids.size == 0:
165
+ job_ids = np.array([op.job_id for op in machine_ops])[
166
+ unscheduled_mask
167
+ ]
168
+ else:
169
+ job_ids = self._job_ids[scheduled_operation.machine_id][
170
+ unscheduled_mask
171
+ ]
172
+
173
+ if self._positions.size == 0:
174
+ positions = np.array(
175
+ [op.position_in_job for op in machine_ops]
176
+ )[unscheduled_mask]
177
+ else:
178
+ positions = self._positions[scheduled_operation.machine_id][
179
+ unscheduled_mask
180
+ ]
181
+ old_start_times = self.earliest_start_times[job_ids, positions]
182
+ new_start_times = np.maximum(
183
+ scheduled_operation.end_time, old_start_times
184
+ )
185
+ gaps = new_start_times - old_start_times
186
+
187
+ for job_id, position, gap in zip(job_ids, positions, gaps):
188
+ self.earliest_start_times[job_id, position:] += gap
189
+
190
+ self.initialize_features()
191
+
192
+ def initialize_features(self):
193
+ """Initializes the features based on the current state of the
194
+ dispatcher."""
195
+ for feature_type in self.features:
196
+ if feature_type == FeatureType.OPERATIONS:
197
+ self._update_operation_features()
198
+ elif (
199
+ feature_type == FeatureType.MACHINES
200
+ and self._is_regular_instance
201
+ ):
202
+ self._update_machine_features_vectorized()
203
+ elif feature_type == FeatureType.MACHINES:
204
+ self._update_machine_features()
205
+ elif feature_type == FeatureType.JOBS:
206
+ self._update_job_features()
207
+
208
+ def _update_operation_features(self):
209
+ """Ravels the 2D array into a 1D array"""
210
+ current_time = self.dispatcher.current_time()
211
+ next_index = 0
212
+ for job_id, operations in enumerate(self.dispatcher.instance.jobs):
213
+ self.features[FeatureType.OPERATIONS][
214
+ next_index : next_index + len(operations), 0
215
+ ] = (
216
+ self.earliest_start_times[job_id, : len(operations)]
217
+ - current_time
218
+ )
219
+ next_index += len(operations)
220
+
221
+ def _update_machine_features(self):
222
+ """Picks the minimum start time of all operations that can be scheduled
223
+ on that machine"""
224
+ current_time = self.dispatcher.current_time()
225
+ operations_by_machine = self.dispatcher.instance.operations_by_machine
226
+ for machine_id, operations in enumerate(operations_by_machine):
227
+ min_earliest_start_time = min(
228
+ (
229
+ self.earliest_start_times[
230
+ operation.job_id, operation.position_in_job
231
+ ]
232
+ for operation in operations
233
+ if not self.dispatcher.is_scheduled(operation)
234
+ ),
235
+ default=0,
236
+ )
237
+ self.features[FeatureType.MACHINES][machine_id, 0] = (
238
+ min_earliest_start_time - current_time
239
+ )
240
+
241
+ def _update_machine_features_vectorized(self):
242
+ """Picks the minimum start time of all operations that can be scheduled
243
+ on that machine"""
244
+ current_time = self.dispatcher.current_time()
245
+ operations_by_machine = self.dispatcher.instance.operations_by_machine
246
+
247
+ # Create a mask for unscheduled operations
248
+ is_unscheduled = np.array(
249
+ [
250
+ [not self.dispatcher.is_scheduled(op) for op in machine_ops]
251
+ for machine_ops in operations_by_machine
252
+ ]
253
+ )
254
+
255
+ # Get earliest start times for all operations
256
+ earliest_start_times = self.earliest_start_times[
257
+ self._job_ids, self._positions
258
+ ]
259
+
260
+ # Apply mask for unscheduled operations
261
+ masked_start_times = np.where(
262
+ is_unscheduled, earliest_start_times, np.inf
263
+ )
264
+
265
+ # Find minimum start time for each machine
266
+ min_start_times = np.min(masked_start_times, axis=1)
267
+
268
+ # Handle cases where all operations are scheduled
269
+ min_start_times = np.where(
270
+ np.isinf(min_start_times), 0, min_start_times
271
+ )
272
+
273
+ self.features[FeatureType.MACHINES][:, 0] = (
274
+ min_start_times - current_time
275
+ )
276
+
277
+ def _update_job_features(self):
278
+ """Picks the earliest start time of the next operation in the job"""
279
+ current_time = self.dispatcher.current_time()
280
+ for job_id, next_operation_idx in enumerate(
281
+ self.dispatcher.job_next_operation_index
282
+ ):
283
+ job_length = len(self.dispatcher.instance.jobs[job_id])
284
+ if next_operation_idx == job_length:
285
+ continue
286
+ self.features[FeatureType.JOBS][job_id, 0] = (
287
+ self.earliest_start_times[job_id, next_operation_idx]
288
+ - current_time
289
+ )
@@ -0,0 +1,95 @@
1
+ """Contains factory functions for creating :class:`FeatureObserver`s."""
2
+
3
+ from enum import Enum
4
+ from typing import Union, Type
5
+
6
+ from job_shop_lib.dispatching import DispatcherObserverConfig
7
+ from job_shop_lib.dispatching.feature_observers import (
8
+ IsReadyObserver,
9
+ EarliestStartTimeObserver,
10
+ FeatureObserver,
11
+ DurationObserver,
12
+ IsScheduledObserver,
13
+ PositionInJobObserver,
14
+ RemainingOperationsObserver,
15
+ IsCompletedObserver,
16
+ )
17
+
18
+
19
+ class FeatureObserverType(str, Enum):
20
+ """Enumeration of the different feature observers.
21
+
22
+ Each :class:`FeatureObserver` is associated with a string value that can be
23
+ used to create the :class:`FeatureObserver` using the factory function.
24
+
25
+ It does not include the :class:`CompositeFeatureObserver` class since this
26
+ observer is often managed separately from the others. For example, a
27
+ common use case is to create a list of feature observers and pass them to
28
+ """
29
+
30
+ IS_READY = "is_ready"
31
+ EARLIEST_START_TIME = "earliest_start_time"
32
+ DURATION = "duration"
33
+ IS_SCHEDULED = "is_scheduled"
34
+ POSITION_IN_JOB = "position_in_job"
35
+ REMAINING_OPERATIONS = "remaining_operations"
36
+ IS_COMPLETED = "is_completed"
37
+
38
+
39
+ # FeatureObserverConfig = DispatcherObserverConfig[
40
+ # Type[FeatureObserver] | FeatureObserverType | str
41
+ # ]
42
+ # FeatureObserverConfig = DispatcherObserverConfig[
43
+ # Union[Type[FeatureObserver], FeatureObserverType, str]
44
+ # ]
45
+ FeatureObserverConfig = (
46
+ DispatcherObserverConfig[Type[FeatureObserver]]
47
+ | DispatcherObserverConfig[FeatureObserverType]
48
+ | DispatcherObserverConfig[str]
49
+ )
50
+
51
+
52
+ def feature_observer_factory(
53
+ feature_observer_type: Union[
54
+ str,
55
+ FeatureObserverType,
56
+ Type[FeatureObserver],
57
+ FeatureObserverConfig
58
+ ],
59
+ **kwargs,
60
+ ) -> FeatureObserver:
61
+ """Creates and returns a :class:`FeatureObserver` based on the specified
62
+ :class:`FeatureObserver` type.
63
+
64
+ Args:
65
+ feature_creator_type:
66
+ The type of :class:`FeatureObserver` to create.
67
+ **kwargs:
68
+ Additional keyword arguments to pass to the
69
+ :class:`FeatureObserver` constructor.
70
+
71
+ Returns:
72
+ A :class:`FeatureObserver` instance.
73
+ """
74
+ if isinstance(feature_observer_type, DispatcherObserverConfig):
75
+ return feature_observer_factory(
76
+ feature_observer_type.class_type,
77
+ **feature_observer_type.kwargs,
78
+ **kwargs,
79
+ )
80
+ # if the instance is of type Type[FeatureObserver] we can just
81
+ # call the object constructor with the keyword arguments
82
+ if isinstance(feature_observer_type, type):
83
+ return feature_observer_type(**kwargs)
84
+
85
+ mapping: dict[FeatureObserverType, Type[FeatureObserver]] = {
86
+ FeatureObserverType.IS_READY: IsReadyObserver,
87
+ FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
88
+ FeatureObserverType.DURATION: DurationObserver,
89
+ FeatureObserverType.IS_SCHEDULED: IsScheduledObserver,
90
+ FeatureObserverType.POSITION_IN_JOB: PositionInJobObserver,
91
+ FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
92
+ FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
93
+ }
94
+ feature_observer = mapping[feature_observer_type] # type: ignore[index]
95
+ return feature_observer(**kwargs)