job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0a1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. job_shop_lib/__init__.py +16 -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} +9 -4
  4. job_shop_lib/_operation.py +95 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +73 -54
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
  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} +57 -18
  11. job_shop_lib/dispatching/__init__.py +45 -41
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
  14. job_shop_lib/dispatching/_factories.py +125 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
  16. job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
  17. job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
  18. job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
  19. job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
  20. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
  21. job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
  22. job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
  23. job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
  24. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  25. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
  26. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
  27. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  28. job_shop_lib/dispatching/rules/__init__.py +51 -0
  29. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
  30. job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
  31. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
  32. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
  33. job_shop_lib/dispatching/rules/_utils.py +127 -0
  34. job_shop_lib/exceptions.py +18 -0
  35. job_shop_lib/generation/__init__.py +2 -2
  36. job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
  37. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
  38. job_shop_lib/graphs/__init__.py +17 -6
  39. job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
  40. job_shop_lib/graphs/{node.py → _node.py} +18 -12
  41. job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
  42. job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
  43. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
  44. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  45. job_shop_lib/reinforcement_learning/__init__.py +41 -0
  46. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
  47. job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
  48. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
  49. job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
  50. job_shop_lib/reinforcement_learning/_utils.py +96 -0
  51. job_shop_lib/visualization/__init__.py +20 -4
  52. job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
  53. job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
  54. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
  55. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
  56. job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
  57. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  58. job_shop_lib/cp_sat/__init__.py +0 -5
  59. job_shop_lib/dispatching/factories.py +0 -206
  60. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  61. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  62. job_shop_lib/generators/__init__.py +0 -8
  63. job_shop_lib/generators/basic_generator.py +0 -200
  64. job_shop_lib/generators/transformations.py +0 -164
  65. job_shop_lib/operation.py +0 -122
  66. job_shop_lib/visualization/create_gif.py +0 -209
  67. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  68. /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
  69. /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
  70. /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
  71. /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
  72. /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
  73. /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
  74. /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
  75. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
  76. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,69 @@
1
+ """Home of the `UnscheduledOperationsObserver` class."""
2
+
3
+ import collections
4
+ from collections.abc import Iterable
5
+ import itertools
6
+ from typing import Deque
7
+
8
+ from job_shop_lib import Operation, ScheduledOperation
9
+ from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
10
+
11
+
12
+ class UnscheduledOperationsObserver(DispatcherObserver):
13
+ """Stores the operations that have not been dispatched yet.
14
+
15
+ This observer maintains a list of deques, each containing unscheduled
16
+ operations for a specific job. It provides methods to access and
17
+ manipulate unscheduled operations efficiently.
18
+ """
19
+
20
+ def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
21
+ super().__init__(dispatcher, subscribe=subscribe)
22
+ self.unscheduled_operations_per_job: list[Deque[Operation]] = []
23
+ self.reset()
24
+ # In case the dispatcher has already scheduled some operations,
25
+ # we need to remove them.
26
+ # Note that we don't need to remove the operations in order.
27
+ for scheduled_operation in itertools.chain(
28
+ *self.dispatcher.schedule.schedule
29
+ ):
30
+ self.update(scheduled_operation)
31
+
32
+ @property
33
+ def unscheduled_operations(self) -> Iterable[Operation]:
34
+ """An iterable of all unscheduled operations across all jobs."""
35
+ return itertools.chain(*self.unscheduled_operations_per_job)
36
+
37
+ @property
38
+ def num_unscheduled_operations(self) -> int:
39
+ """The total number of unscheduled operations."""
40
+ total_operations = self.dispatcher.instance.num_operations
41
+ num_scheduled_operations = (
42
+ self.dispatcher.schedule.num_scheduled_operations
43
+ )
44
+ return total_operations - num_scheduled_operations
45
+
46
+ def update(self, scheduled_operation: ScheduledOperation) -> None:
47
+ """Removes a scheduled operation from the unscheduled operations.
48
+
49
+ This method is called by the dispatcher when an operation is
50
+ scheduled. It removes the operation from its job's deque of
51
+ unscheduled operations.
52
+
53
+ Args:
54
+ scheduled_operation: The operation that has been scheduled.
55
+ """
56
+ job_id = scheduled_operation.operation.job_id
57
+ job_deque = self.unscheduled_operations_per_job[job_id]
58
+ if job_deque:
59
+ job_deque.popleft()
60
+
61
+ def reset(self) -> None:
62
+ """Resets unscheduled operations to include all operations.
63
+
64
+ This method reinitializes the list of deques with all operations
65
+ from all jobs in the instance.
66
+ """
67
+ self.unscheduled_operations_per_job = [
68
+ collections.deque(job) for job in self.dispatcher.instance.jobs
69
+ ]
@@ -1,16 +1,21 @@
1
1
  """Contains FeatureObserver classes for observing features of the
2
2
  dispatcher."""
3
3
 
4
- from .feature_observer import FeatureObserver, FeatureType
5
- from .composite_feature_observer import CompositeFeatureObserver
6
- from .earliest_start_time_observer import EarliestStartTimeObserver
7
- from .is_ready_observer import IsReadyObserver
8
- from .duration_observer import DurationObserver
9
- from .is_scheduled_observer import IsScheduledObserver
10
- from .position_in_job_observer import PositionInJobObserver
11
- from .remaining_operations_observer import RemainingOperationsObserver
12
- from .is_completed_observer import IsCompletedObserver
13
- from .factory import FeatureObserverType, feature_observer_factory
4
+ from ._feature_observer import FeatureObserver, FeatureType
5
+ from ._earliest_start_time_observer import EarliestStartTimeObserver
6
+ from ._is_ready_observer import IsReadyObserver
7
+ from ._duration_observer import DurationObserver
8
+ from ._is_scheduled_observer import IsScheduledObserver
9
+ from ._position_in_job_observer import PositionInJobObserver
10
+ from ._remaining_operations_observer import RemainingOperationsObserver
11
+ from ._is_completed_observer import IsCompletedObserver
12
+ from ._factory import (
13
+ FeatureObserverType,
14
+ feature_observer_factory,
15
+ FeatureObserverConfig,
16
+ )
17
+ from ._composite_feature_observer import CompositeFeatureObserver
18
+
14
19
 
15
20
  __all__ = [
16
21
  "FeatureObserver",
@@ -25,4 +30,5 @@ __all__ = [
25
30
  "IsCompletedObserver",
26
31
  "FeatureObserverType",
27
32
  "feature_observer_factory",
33
+ "FeatureObserverConfig",
28
34
  ]
@@ -1,13 +1,24 @@
1
1
  """Home of the `CompositeFeatureObserver` class."""
2
2
 
3
3
  from collections import defaultdict
4
+ from collections.abc import Sequence
5
+
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.10.
9
+ from typing_extensions import Self
10
+
4
11
  import numpy as np
12
+ from numpy.typing import NDArray
5
13
  import pandas as pd
6
14
 
15
+ from job_shop_lib.exceptions import ValidationError
7
16
  from job_shop_lib.dispatching import Dispatcher
8
17
  from job_shop_lib.dispatching.feature_observers import (
9
18
  FeatureObserver,
10
19
  FeatureType,
20
+ FeatureObserverConfig,
21
+ feature_observer_factory,
11
22
  )
12
23
 
13
24
 
@@ -29,8 +40,10 @@ class CompositeFeatureObserver(FeatureObserver):
29
40
  def __init__(
30
41
  self,
31
42
  dispatcher: Dispatcher,
32
- feature_observers: list[FeatureObserver] | None = None,
43
+ *,
33
44
  subscribe: bool = True,
45
+ feature_types: list[FeatureType] | FeatureType | None = None,
46
+ feature_observers: list[FeatureObserver] | None = None,
34
47
  ):
35
48
  if feature_observers is None:
36
49
  feature_observers = [
@@ -38,11 +51,48 @@ class CompositeFeatureObserver(FeatureObserver):
38
51
  for observer in dispatcher.subscribers
39
52
  if isinstance(observer, FeatureObserver)
40
53
  ]
54
+ feature_types = self._get_feature_types_list(feature_types)
55
+ for observer in feature_observers:
56
+ if not set(observer.features.keys()).issubset(set(feature_types)):
57
+ raise ValidationError(
58
+ "The feature types observed by the feature observer "
59
+ f"{observer.__class__.__name__} are not a subset of the "
60
+ "feature types specified in the CompositeFeatureObserver."
61
+ f"Observer feature types: {observer.features.keys()}"
62
+ f"Composite feature types: {feature_types}"
63
+ )
41
64
  self.feature_observers = feature_observers
42
65
  self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
43
66
  super().__init__(dispatcher, subscribe=subscribe)
44
67
  self._set_column_names()
45
68
 
69
+ @classmethod
70
+ def from_feature_observer_configs(
71
+ cls,
72
+ dispatcher: Dispatcher,
73
+ feature_observer_configs: Sequence[FeatureObserverConfig],
74
+ subscribe: bool = True,
75
+ ) -> Self:
76
+ """Creates the composite feature observer.
77
+
78
+ Args:
79
+ dispatcher:
80
+ The dispatcher used to create the feature observers.
81
+ feature_observer_configs:
82
+ The list of feature observer configuration objects.
83
+ subscribe:
84
+ Whether to subscribe the CompositeFeatureObserver to the
85
+ dispatcher.
86
+ """
87
+ observers = [
88
+ feature_observer_factory(observer_config, dispatcher=dispatcher)
89
+ for observer_config in feature_observer_configs
90
+ ]
91
+ composite_observer = cls(
92
+ dispatcher, feature_observers=observers, subscribe=subscribe
93
+ )
94
+ return composite_observer
95
+
46
96
  @property
47
97
  def features_as_dataframe(self) -> dict[FeatureType, pd.DataFrame]:
48
98
  """Returns the features as a dictionary of `pd.DataFrame` instances."""
@@ -54,7 +104,9 @@ class CompositeFeatureObserver(FeatureObserver):
54
104
  }
55
105
 
56
106
  def initialize_features(self):
57
- features: dict[FeatureType, list[np.ndarray]] = defaultdict(list)
107
+ features: dict[FeatureType, list[NDArray[np.float32]]] = defaultdict(
108
+ list
109
+ )
58
110
  for observer in self.feature_observers:
59
111
  for feature_type, feature_matrix in observer.features.items():
60
112
  features[feature_type].append(feature_matrix)
@@ -85,3 +137,33 @@ class CompositeFeatureObserver(FeatureObserver):
85
137
  out.append(f"{feature_type.value}:")
86
138
  out.append(dataframe.to_string())
87
139
  return "\n".join(out)
140
+
141
+
142
+ if __name__ == "__main__":
143
+ from cProfile import Profile
144
+ from job_shop_lib.benchmarking import load_benchmark_instance
145
+ from job_shop_lib.dispatching.rules import DispatchingRuleSolver
146
+ from job_shop_lib.dispatching.feature_observers import (
147
+ FeatureObserverType,
148
+ )
149
+
150
+ ta80 = load_benchmark_instance("ta80")
151
+
152
+ dispatcher_ = Dispatcher(ta80)
153
+ feature_observer_types_ = list(FeatureObserverType)
154
+ feature_observers_ = [
155
+ feature_observer_factory(
156
+ observer_type,
157
+ dispatcher=dispatcher_,
158
+ )
159
+ for observer_type in feature_observer_types_
160
+ if not observer_type == FeatureObserverType.COMPOSITE
161
+ # and not FeatureObserverType.EARLIEST_START_TIME
162
+ ]
163
+ composite_observer_ = CompositeFeatureObserver(
164
+ dispatcher_, feature_observers=feature_observers_
165
+ )
166
+ solver = DispatchingRuleSolver(dispatching_rule="random")
167
+ profiler = Profile()
168
+ profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
169
+ profiler.print_stats("cumtime")
@@ -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,30 +12,20 @@ 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:`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
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
27
  """
29
28
 
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
- )
39
-
40
29
  def initialize_features(self):
41
30
  mapping = {
42
31
  FeatureType.OPERATIONS: self._initialize_operation_durations,
@@ -7,18 +7,26 @@ from job_shop_lib.dispatching.feature_observers import (
7
7
  FeatureObserver,
8
8
  FeatureType,
9
9
  )
10
- from job_shop_lib.scheduled_operation import ScheduledOperation
10
+ from job_shop_lib import ScheduledOperation
11
11
 
12
12
 
13
13
  class EarliestStartTimeObserver(FeatureObserver):
14
14
  """Observer that adds a feature indicating the earliest start time of
15
15
  each operation, machine, and job in the graph."""
16
16
 
17
+ __slots__ = (
18
+ "earliest_start_times",
19
+ "_job_ids",
20
+ "_positions",
21
+ "machine_ids",
22
+ )
23
+
17
24
  def __init__(
18
25
  self,
19
26
  dispatcher: Dispatcher,
20
- feature_types: list[FeatureType] | FeatureType | None = None,
27
+ *,
21
28
  subscribe: bool = True,
29
+ feature_types: list[FeatureType] | FeatureType | None = None,
22
30
  ):
23
31
 
24
32
  # Earliest start times initialization
@@ -32,8 +40,32 @@ class EarliestStartTimeObserver(FeatureObserver):
32
40
  )
33
41
  self.earliest_start_times[np.isnan(squared_duration_matrix)] = np.nan
34
42
  # -------------------------------
43
+
44
+ # Cache:
45
+ operations_by_machine = dispatcher.instance.operations_by_machine
46
+ self._is_regular_instance = all(
47
+ len(job) == len(dispatcher.instance.jobs[0])
48
+ for job in dispatcher.instance.jobs
49
+ )
50
+ if self._is_regular_instance:
51
+ self._job_ids = np.array(
52
+ [
53
+ [op.job_id for op in machine_ops]
54
+ for machine_ops in operations_by_machine
55
+ ]
56
+ )
57
+ self._positions = np.array(
58
+ [
59
+ [op.position_in_job for op in machine_ops]
60
+ for machine_ops in operations_by_machine
61
+ ]
62
+ )
63
+ else:
64
+ self._job_ids = np.array([])
65
+ self._positions = np.array([])
66
+
35
67
  super().__init__(
36
- dispatcher, feature_types, feature_size=1, subscribe=subscribe
68
+ dispatcher, feature_types=feature_types, subscribe=subscribe
37
69
  )
38
70
 
39
71
  def update(self, scheduled_operation: ScheduledOperation):
@@ -68,31 +100,56 @@ class EarliestStartTimeObserver(FeatureObserver):
68
100
 
69
101
  # Now, we compute the gap that could be introduced by the new
70
102
  # next_available_time of the machine.
71
- operations_by_machine = self.dispatcher.instance.operations_by_machine
72
- for operation in operations_by_machine[scheduled_operation.machine_id]:
73
- if self.dispatcher.is_scheduled(operation):
74
- continue
75
- old_start_time = self.earliest_start_times[
76
- operation.job_id, operation.position_in_job
77
- ]
78
- new_start_time = max(old_start_time, scheduled_operation.end_time)
79
- gap = new_start_time - old_start_time
80
- self.earliest_start_times[
81
- operation.job_id, operation.position_in_job :
82
- ] += gap
103
+ machine_ops = self.dispatcher.instance.operations_by_machine[
104
+ scheduled_operation.machine_id
105
+ ]
106
+ unscheduled_mask = np.array(
107
+ [not self.dispatcher.is_scheduled(op) for op in machine_ops]
108
+ )
109
+ if np.any(unscheduled_mask):
110
+ if self._job_ids.size == 0:
111
+ job_ids = np.array([op.job_id for op in machine_ops])[
112
+ unscheduled_mask
113
+ ]
114
+ else:
115
+ job_ids = self._job_ids[scheduled_operation.machine_id][
116
+ unscheduled_mask
117
+ ]
118
+
119
+ if self._positions.size == 0:
120
+ positions = np.array(
121
+ [op.position_in_job for op in machine_ops]
122
+ )[unscheduled_mask]
123
+ else:
124
+ positions = self._positions[scheduled_operation.machine_id][
125
+ unscheduled_mask
126
+ ]
127
+ old_start_times = self.earliest_start_times[job_ids, positions]
128
+ new_start_times = np.maximum(
129
+ scheduled_operation.end_time, old_start_times
130
+ )
131
+ gaps = new_start_times - old_start_times
132
+
133
+ for job_id, position, gap in zip(job_ids, positions, gaps):
134
+ self.earliest_start_times[job_id, position:] += gap
83
135
 
84
136
  self.initialize_features()
85
137
 
86
138
  def initialize_features(self):
87
139
  """Initializes the features based on the current state of the
88
140
  dispatcher."""
89
- mapping = {
90
- FeatureType.OPERATIONS: self._update_operation_features,
91
- FeatureType.MACHINES: self._update_machine_features,
92
- FeatureType.JOBS: self._update_job_features,
93
- }
94
141
  for feature_type in self.features:
95
- mapping[feature_type]()
142
+ if feature_type == FeatureType.OPERATIONS:
143
+ self._update_operation_features()
144
+ elif (
145
+ feature_type == FeatureType.MACHINES
146
+ and self._is_regular_instance
147
+ ):
148
+ self._update_machine_features_vectorized()
149
+ elif feature_type == FeatureType.MACHINES:
150
+ self._update_machine_features()
151
+ elif feature_type == FeatureType.JOBS:
152
+ self._update_job_features()
96
153
 
97
154
  def _update_operation_features(self):
98
155
  """Ravels the 2D array into a 1D array"""
@@ -127,6 +184,42 @@ class EarliestStartTimeObserver(FeatureObserver):
127
184
  min_earliest_start_time - current_time
128
185
  )
129
186
 
187
+ def _update_machine_features_vectorized(self):
188
+ """Picks the minimum start time of all operations that can be scheduled
189
+ on that machine"""
190
+ current_time = self.dispatcher.current_time()
191
+ operations_by_machine = self.dispatcher.instance.operations_by_machine
192
+
193
+ # Create a mask for unscheduled operations
194
+ is_unscheduled = np.array(
195
+ [
196
+ [not self.dispatcher.is_scheduled(op) for op in machine_ops]
197
+ for machine_ops in operations_by_machine
198
+ ]
199
+ )
200
+
201
+ # Get earliest start times for all operations
202
+ earliest_start_times = self.earliest_start_times[
203
+ self._job_ids, self._positions
204
+ ]
205
+
206
+ # Apply mask for unscheduled operations
207
+ masked_start_times = np.where(
208
+ is_unscheduled, earliest_start_times, np.inf
209
+ )
210
+
211
+ # Find minimum start time for each machine
212
+ min_start_times = np.min(masked_start_times, axis=1)
213
+
214
+ # Handle cases where all operations are scheduled
215
+ min_start_times = np.where(
216
+ np.isinf(min_start_times), 0, min_start_times
217
+ )
218
+
219
+ self.features[FeatureType.MACHINES][:, 0] = (
220
+ min_start_times - current_time
221
+ )
222
+
130
223
  def _update_job_features(self):
131
224
  """Picks the earliest start time of the next operation in the job"""
132
225
  current_time = self.dispatcher.current_time()
@@ -140,17 +233,3 @@ class EarliestStartTimeObserver(FeatureObserver):
140
233
  self.earliest_start_times[job_id, next_operation_idx]
141
234
  - current_time
142
235
  )
143
-
144
-
145
- if __name__ == "__main__":
146
- squared_durations_matrix = np.array([[1, 1, 7], [5, 1, 1], [1, 3, 2]])
147
- # Add a zeros column to the left of the matrix
148
- cumulative_durations = np.hstack(
149
- (
150
- np.zeros((squared_durations_matrix.shape[0], 1)),
151
- squared_durations_matrix[:, :-1],
152
- )
153
- )
154
- # Set to nan the values that are not available
155
- cumulative_durations[np.isnan(squared_durations_matrix)] = np.nan
156
- print(cumulative_durations)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from enum import Enum
4
4
 
5
+ from job_shop_lib.dispatching import DispatcherObserverConfig
5
6
  from job_shop_lib.dispatching.feature_observers import (
6
7
  IsReadyObserver,
7
8
  EarliestStartTimeObserver,
@@ -15,8 +16,7 @@ from job_shop_lib.dispatching.feature_observers import (
15
16
 
16
17
 
17
18
  class FeatureObserverType(str, Enum):
18
- """Enumeration of node feature creator types for the job shop scheduling
19
- problem."""
19
+ """Enumeration of the different feature observers."""
20
20
 
21
21
  IS_READY = "is_ready"
22
22
  EARLIEST_START_TIME = "earliest_start_time"
@@ -28,15 +28,30 @@ class FeatureObserverType(str, Enum):
28
28
  COMPOSITE = "composite"
29
29
 
30
30
 
31
+ # FeatureObserverConfig = DispatcherObserverConfig[
32
+ # type[FeatureObserver] | FeatureObserverType | str
33
+ # ]
34
+ FeatureObserverConfig = (
35
+ DispatcherObserverConfig[type[FeatureObserver]]
36
+ | DispatcherObserverConfig[FeatureObserverType]
37
+ | DispatcherObserverConfig[str]
38
+ )
39
+
40
+
31
41
  def feature_observer_factory(
32
- node_feature_creator_type: str | FeatureObserverType,
42
+ feature_creator_type: (
43
+ str
44
+ | FeatureObserverType
45
+ | type[FeatureObserver]
46
+ | FeatureObserverConfig
47
+ ),
33
48
  **kwargs,
34
49
  ) -> FeatureObserver:
35
50
  """Creates and returns a node feature creator based on the specified
36
51
  node feature creator type.
37
52
 
38
53
  Args:
39
- node_feature_creator_type:
54
+ feature_creator_type:
40
55
  The type of node feature creator to create.
41
56
  **kwargs:
42
57
  Additional keyword arguments to pass to the node
@@ -45,6 +60,17 @@ def feature_observer_factory(
45
60
  Returns:
46
61
  A node feature creator instance.
47
62
  """
63
+ if isinstance(feature_creator_type, DispatcherObserverConfig):
64
+ return feature_observer_factory(
65
+ feature_creator_type.class_type,
66
+ **feature_creator_type.kwargs,
67
+ **kwargs,
68
+ )
69
+ # if the instance is of type type[FeatureObserver] we can just
70
+ # call the object constructor with the keyword arguments
71
+ if isinstance(feature_creator_type, type):
72
+ return feature_creator_type(**kwargs)
73
+
48
74
  mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
49
75
  FeatureObserverType.IS_READY: IsReadyObserver,
50
76
  FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
@@ -54,5 +80,5 @@ def feature_observer_factory(
54
80
  FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
55
81
  FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
56
82
  }
57
- feature_creator = mapping[node_feature_creator_type] # type: ignore[index]
83
+ feature_creator = mapping[feature_creator_type] # type: ignore[index]
58
84
  return feature_creator(**kwargs)