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,228 @@
1
+ """Home of the `FeatureObserver` class and `FeatureType` enum."""
2
+
3
+ import enum
4
+ from typing import Optional, Union, Dict, List, Tuple
5
+
6
+ import numpy as np
7
+
8
+ from job_shop_lib import ScheduledOperation
9
+ from job_shop_lib.exceptions import ValidationError
10
+ from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
11
+
12
+
13
+ class FeatureType(str, enum.Enum):
14
+ """Types of features that can be extracted."""
15
+
16
+ OPERATIONS = "operations"
17
+ MACHINES = "machines"
18
+ JOBS = "jobs"
19
+
20
+
21
+ class FeatureObserver(DispatcherObserver):
22
+ """Base class for feature observers.
23
+
24
+ A :class:`FeatureObserver` is a
25
+ a subclass of :class:`~job_shop_lib.dispatching.DispatcherObserver` that
26
+ observes features related to operations, machines, or jobs in the
27
+ :class:`~job_shop_lib.dispatching.Dispatcher`.
28
+
29
+ Attributes are stored in numpy arrays with a shape of (``num_entities``,
30
+ ``feature_size``), where ``num_entities`` is the number of entities being
31
+ observed (e.g., operations, machines, or jobs) and ``feature_size`` is the
32
+ number of values being observed for each entity.
33
+
34
+ The advantage of using arrays is that they can be easily updated in a
35
+ vectorized manner, which is more efficient than updating each attribute
36
+ individually. Furthermore, machine learning models can be trained on these
37
+ arrays to predict the best dispatching decisions.
38
+
39
+ Arrays use the data type ``np.float32``.
40
+
41
+ New :class:`FeatureObservers` must inherit from this class, and re-define
42
+ the class attributes ``_singleton`` (defualt ), ``_feature_size``
43
+ (default 1) and ``_supported_feature_types`` (default all feature types).
44
+
45
+ Feature observers are not singleton by default. This means that more than
46
+ one instance of the same feature observer type can be subscribed to the
47
+ dispatcher. This is useful when the first subscriber only observes a subset
48
+ of the features, and the second subscriber observes a different subset of
49
+ them. For example, the first subscriber could observe only the
50
+ operation-related features, while the second subscriber could observe the
51
+ jobs.
52
+
53
+ Args:
54
+ dispatcher:
55
+ The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
56
+ subscribe:
57
+ If ``True``, the observer is subscribed to the dispatcher upon
58
+ initialization. Otherwise, the observer must be subscribed later
59
+ or manually updated.
60
+ feature_types:
61
+ A list of :class:`FeatureType` or a single :class:`FeatureType`
62
+ that specifies the types of features to observe. They must be a
63
+ subset of the class attribute :attr:`supported_feature_types`.
64
+ If ``None``, all supported feature types are tracked.
65
+ """
66
+
67
+ _is_singleton = False
68
+ _feature_sizes: Union[Dict[FeatureType, int], int] = 1
69
+ _supported_feature_types = list(FeatureType)
70
+
71
+ __slots__ = {
72
+ "features": (
73
+ "A dictionary of numpy arrays with the features. "
74
+ "Each key is a :class:`FeatureType` and each value is a numpy "
75
+ "array with the features. The array has shape (``num_entities``, "
76
+ "``feature_size``), where ``num_entities`` is the number of "
77
+ "entities being observed (e.g., operations, machines, or jobs) and"
78
+ " ``feature_size`` is the number of values being observed for each"
79
+ " entity."
80
+ )
81
+ }
82
+
83
+ def __init__(
84
+ self,
85
+ dispatcher: Dispatcher,
86
+ *,
87
+ subscribe: bool = True,
88
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
89
+ ):
90
+ feature_types = self._get_feature_types_list(feature_types)
91
+ if isinstance(self._feature_sizes, int):
92
+ feature_size = {
93
+ feature_type: self._feature_sizes
94
+ for feature_type in feature_types
95
+ }
96
+ super().__init__(dispatcher, subscribe=subscribe)
97
+
98
+ number_of_entities = {
99
+ FeatureType.OPERATIONS: dispatcher.instance.num_operations,
100
+ FeatureType.MACHINES: dispatcher.instance.num_machines,
101
+ FeatureType.JOBS: dispatcher.instance.num_jobs,
102
+ }
103
+ feature_dimensions = {
104
+ feature_type: (
105
+ number_of_entities[feature_type],
106
+ feature_size[feature_type],
107
+ )
108
+ for feature_type in feature_types
109
+ }
110
+ self.features = {
111
+ feature_type: np.zeros(
112
+ feature_dimensions[feature_type],
113
+ dtype=np.float32,
114
+ )
115
+ for feature_type in feature_types
116
+ }
117
+ self.initialize_features()
118
+
119
+ @property
120
+ def feature_sizes(self) -> Dict[FeatureType, int]:
121
+ """Returns the size of the features.
122
+
123
+ The size of the features is the number of values being observed for
124
+ each entity. This corresponds to the second dimension of each array.
125
+
126
+ This number is typically one (e.g. measuring the duration
127
+ of each operation), but some feature observers like the
128
+ :class:`CompositeFeatureObserver` may track more than one value.
129
+ """
130
+ if isinstance(self._feature_sizes, int):
131
+ return {
132
+ feature_type: self._feature_sizes
133
+ for feature_type in self.features
134
+ }
135
+ return self._feature_sizes
136
+
137
+ @property
138
+ def supported_feature_types(self) -> List[FeatureType]:
139
+ """Returns the supported feature types."""
140
+ return self._supported_feature_types
141
+
142
+ @property
143
+ def feature_dimensions(self) -> Dict[FeatureType, Tuple[int, int]]:
144
+ """A dictionary containing the shape of each :class:`FeatureType`."""
145
+ feature_dimensions = {}
146
+ for feature_type, array in self.features.items():
147
+ feature_dimensions[feature_type] = array.shape
148
+ return feature_dimensions # type: ignore[return-value]
149
+
150
+ def initialize_features(self):
151
+ """Initializes the features based on the current state of the
152
+ dispatcher.
153
+
154
+ This method is automatically called after initializing the observer.
155
+ """
156
+
157
+ def update(self, scheduled_operation: ScheduledOperation):
158
+ """Updates the features based on the scheduled operation.
159
+
160
+ By default, this method just calls :meth:`initialize_features`.
161
+
162
+ Args:
163
+ ScheduledOperation:
164
+ The operation that has been scheduled.
165
+ """
166
+ self.initialize_features()
167
+
168
+ def reset(self):
169
+ """Sets features to zero and calls to :meth:``initialize_features``."""
170
+ self.set_features_to_zero()
171
+ self.initialize_features()
172
+
173
+ def set_features_to_zero(
174
+ self, exclude: Optional[Union[FeatureType, List[FeatureType]]] = None
175
+ ):
176
+ """Sets all features to zero except for the ones specified in
177
+ ``exclude``.
178
+
179
+ Setting a feature to zero means that all values in the feature array
180
+ are set to this value.
181
+
182
+ Args:
183
+ exclude:
184
+ A single :class:`FeatureType` or a list of :class:`FeatureType`
185
+ that specifies the features that should not be set to zero. If
186
+ ``None``, all currently used features are set to zero.
187
+ """
188
+ if exclude is None:
189
+ exclude = []
190
+ if isinstance(exclude, FeatureType):
191
+ exclude = [exclude]
192
+
193
+ for feature_type in self.features:
194
+ if feature_type in exclude:
195
+ continue
196
+ self.features[feature_type][:] = 0.0
197
+
198
+ def _get_feature_types_list(
199
+ self,
200
+ feature_types: Optional[Union[List[FeatureType], FeatureType]],
201
+ ) -> List[FeatureType]:
202
+ """Returns a list of feature types.
203
+
204
+ Args:
205
+ feature_types:
206
+ A list of feature types or a single feature type. If ``None``,
207
+ all feature types are returned.
208
+ """
209
+ if isinstance(feature_types, FeatureType):
210
+ return [feature_types]
211
+ if feature_types is None:
212
+ return self._supported_feature_types
213
+
214
+ for feature_type in feature_types:
215
+ if feature_type not in self._supported_feature_types:
216
+ raise ValidationError(
217
+ f"Feature type {feature_type} is not supported."
218
+ " Supported feature types are: "
219
+ f"{self._supported_feature_types}"
220
+ )
221
+ return feature_types
222
+
223
+ def __str__(self):
224
+ out = [self.__class__.__name__, ":\n"]
225
+ out.append("-" * len(out[0]))
226
+ for feature_type, feature in self.features.items():
227
+ out.append(f"\n{feature_type.value}:\n{feature}")
228
+ return "".join(out)
@@ -0,0 +1,97 @@
1
+ """Home of the `IsCompletedObserver` class."""
2
+
3
+ from typing import Optional, Union, List
4
+ import numpy as np
5
+
6
+ from job_shop_lib import ScheduledOperation
7
+ from job_shop_lib.dispatching import Dispatcher
8
+ from job_shop_lib.dispatching.feature_observers import (
9
+ FeatureObserver,
10
+ FeatureType,
11
+ )
12
+
13
+
14
+ class IsCompletedObserver(FeatureObserver):
15
+ """Adds a binary feature indicating whether each operation,
16
+ machine, or job has been completed.
17
+
18
+ An operation is considered completed if it has been scheduled and the
19
+ current time is greater than or equal to the sum of the operation's start
20
+ time and duration.
21
+
22
+ A machine or job is considered completed if all of its operations have been
23
+ completed.
24
+
25
+ Args:
26
+ dispatcher:
27
+ The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
28
+ feature_types:
29
+ A list of :class:`FeatureType` or a single :class:`FeatureType`
30
+ that specifies the types of features to observe. They must be a
31
+ subset of the class attribute :attr:`supported_feature_types`.
32
+ If ``None``, all supported feature types are tracked.
33
+ subscribe:
34
+ If ``True``, the observer is subscribed to the dispatcher upon
35
+ initialization. Otherwise, the observer must be subscribed later
36
+ or manually updated.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ dispatcher: Dispatcher,
42
+ *,
43
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
44
+ subscribe: bool = True,
45
+ ):
46
+ feature_types = self._get_feature_types_list(feature_types)
47
+ self._num_of_operations_per_machine = np.array(
48
+ [
49
+ len(operations_by_machine)
50
+ for operations_by_machine in (
51
+ dispatcher.instance.operations_by_machine
52
+ )
53
+ ]
54
+ )
55
+ self._num_of_operations_per_job = np.array(
56
+ [len(job) for job in dispatcher.instance.jobs]
57
+ )
58
+ super().__init__(
59
+ dispatcher,
60
+ feature_types=feature_types,
61
+ subscribe=subscribe,
62
+ )
63
+
64
+ def initialize_features(self):
65
+ if FeatureType.OPERATIONS in self.features:
66
+ completed_operations = [
67
+ op.operation.operation_id
68
+ for op in self.dispatcher.completed_operations()
69
+ ]
70
+ self.features[FeatureType.OPERATIONS][completed_operations, 0] = 1
71
+ if FeatureType.MACHINES in self.features:
72
+ num_completed_ops_per_machine = np.zeros(
73
+ len(self._num_of_operations_per_machine)
74
+ )
75
+ for op in self.dispatcher.completed_operations():
76
+ for machine_id in op.operation.machines:
77
+ num_completed_ops_per_machine[machine_id] += 1
78
+ self.features[FeatureType.MACHINES][:, 0] = (
79
+ num_completed_ops_per_machine
80
+ == self._num_of_operations_per_machine
81
+ ).astype(np.float32)
82
+ if FeatureType.JOBS in self.features:
83
+ num_completed_ops_per_job = np.zeros(
84
+ len(self._num_of_operations_per_job)
85
+ )
86
+ for op in self.dispatcher.completed_operations():
87
+ num_completed_ops_per_job[op.operation.job_id] += 1
88
+ self.features[FeatureType.JOBS][:, 0] = (
89
+ num_completed_ops_per_job
90
+ == self._num_of_operations_per_job
91
+ ).astype(np.float32)
92
+
93
+ def reset(self):
94
+ self.set_features_to_zero()
95
+
96
+ def update(self, scheduled_operation: ScheduledOperation):
97
+ self.initialize_features()
@@ -0,0 +1,35 @@
1
+ """Home of the `IsReadyObserver` class."""
2
+
3
+ from typing import List
4
+
5
+ from job_shop_lib.dispatching.feature_observers import (
6
+ FeatureObserver,
7
+ FeatureType,
8
+ )
9
+
10
+
11
+ class IsReadyObserver(FeatureObserver):
12
+ """Feature creator that adds a binary feature indicating if the operation,
13
+ machine or job is ready to be dispatched."""
14
+
15
+ def initialize_features(self):
16
+ self.set_features_to_zero()
17
+ for feature_type, feature in self.features.items():
18
+ feature_ids = self._get_ready_feature_ids(feature_type)
19
+ feature[feature_ids, 0] = 1.0
20
+
21
+ def _get_ready_feature_ids(self, feature_type: FeatureType) -> List[int]:
22
+ if feature_type == FeatureType.OPERATIONS:
23
+ return self._get_ready_operations()
24
+ if feature_type == FeatureType.MACHINES:
25
+ return self.dispatcher.available_machines()
26
+ if feature_type == FeatureType.JOBS:
27
+ return self.dispatcher.available_jobs()
28
+ raise ValueError(f"Feature type {feature_type} is not supported.")
29
+
30
+ def reset(self):
31
+ self.initialize_features()
32
+
33
+ def _get_ready_operations(self) -> List[int]:
34
+ available_operations = self.dispatcher.available_operations()
35
+ return [operation.operation_id for operation in available_operations]
@@ -8,14 +8,18 @@ from job_shop_lib.dispatching.feature_observers import (
8
8
 
9
9
 
10
10
  class IsScheduledObserver(FeatureObserver):
11
- """Observer that updates features based on scheduling operations.
11
+ """Updates features based on scheduling operations.
12
12
 
13
13
  This observer tracks which operations have been scheduled and updates
14
- feature matrices accordingly. It updates a feature in the
15
- `FeatureType.OPERATIONS` matrix to indicate that an operation has been
16
- scheduled. Additionally, it counts the number of uncompleted but
14
+ feature matrices accordingly.
15
+
16
+ It updates a feature in the
17
+ :meth:`FeatureType.OPERATIONS` matrix to indicate that an operation has
18
+ been scheduled.
19
+
20
+ Additionally, it counts the number of uncompleted but
17
21
  scheduled operations for each machine and job, updating the respective
18
- `FeatureType.MACHINES` and `FeatureType.JOBS` feature matrices.
22
+ :meth:`FeatureType.MACHINES` and :meth:`FeatureType.JOBS` feature matrices.
19
23
  """
20
24
 
21
25
  def update(self, scheduled_operation: ScheduledOperation):
@@ -1,6 +1,5 @@
1
1
  """Home of the `PositionInJobObserver` class."""
2
2
 
3
- from job_shop_lib.dispatching import Dispatcher
4
3
  from job_shop_lib import ScheduledOperation
5
4
  from job_shop_lib.dispatching.feature_observers import (
6
5
  FeatureObserver,
@@ -9,19 +8,18 @@ from job_shop_lib.dispatching.feature_observers import (
9
8
 
10
9
 
11
10
  class PositionInJobObserver(FeatureObserver):
12
- """Observer that adds a feature indicating the position of
11
+ """Adds a feature indicating the position of
13
12
  operations in their respective jobs.
14
13
 
15
- Positions are adjusted dynamically as operations are scheduled.
14
+ Positions are adjusted dynamically as operations are scheduled. In other
15
+ words, the position of an operation is the number of unscheduled operations
16
+ that precede it in the job.
17
+
18
+ It only supports the :meth:`~job_shop_lib.FeatureType.OPERATIONS` feature
19
+ type.
16
20
  """
17
21
 
18
- def __init__(self, dispatcher: Dispatcher, subscribe: bool = True):
19
- super().__init__(
20
- dispatcher,
21
- feature_types=[FeatureType.OPERATIONS],
22
- feature_size=1,
23
- subscribe=subscribe,
24
- )
22
+ _supported_feature_types = [FeatureType.OPERATIONS]
25
23
 
26
24
  def initialize_features(self):
27
25
  for operation in self.dispatcher.unscheduled_operations():
@@ -1,7 +1,7 @@
1
1
  """Home of the `RemainingOperationsObserver` class."""
2
2
 
3
3
  from job_shop_lib import ScheduledOperation
4
- from job_shop_lib.dispatching import Dispatcher
4
+ from job_shop_lib.dispatching import UnscheduledOperationsObserver
5
5
  from job_shop_lib.dispatching.feature_observers import (
6
6
  FeatureObserver,
7
7
  FeatureType,
@@ -12,38 +12,20 @@ class RemainingOperationsObserver(FeatureObserver):
12
12
  """Adds a feature indicating the number of remaining operations for each
13
13
  job and machine.
14
14
 
15
- It does not support FeatureType.OPERATIONS.
15
+ It does not support :meth:`FeatureType.OPERATIONS`.
16
16
  """
17
17
 
18
- def __init__(
19
- self,
20
- dispatcher: Dispatcher,
21
- feature_types: list[FeatureType] | FeatureType | None = None,
22
- subscribe: bool = True,
23
- ):
24
- if feature_types is None:
25
- feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
26
-
27
- if (
28
- feature_types == FeatureType.OPERATIONS
29
- or FeatureType.OPERATIONS in feature_types
30
- ):
31
- raise ValueError("FeatureType.OPERATIONS is not supported.")
32
- super().__init__(
33
- dispatcher,
34
- feature_types=feature_types,
35
- feature_size=1,
36
- subscribe=subscribe,
37
- )
18
+ _supported_feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
38
19
 
39
20
  def initialize_features(self):
40
- for operation in self.dispatcher.unscheduled_operations():
21
+ unscheduled_ops_observer = self.dispatcher.create_or_get_observer(
22
+ UnscheduledOperationsObserver
23
+ )
24
+ for operation in unscheduled_ops_observer.unscheduled_operations:
41
25
  if FeatureType.JOBS in self.features:
42
26
  self.features[FeatureType.JOBS][operation.job_id, 0] += 1
43
27
  if FeatureType.MACHINES in self.features:
44
- self.features[FeatureType.MACHINES][
45
- operation.machine_id, 0
46
- ] += 1
28
+ self.features[FeatureType.MACHINES][operation.machines, 0] += 1
47
29
 
48
30
  def update(self, scheduled_operation: ScheduledOperation):
49
31
  if FeatureType.JOBS in self.features:
@@ -0,0 +1,87 @@
1
+ """Contains the dispatching rules for the job shop scheduling problem.
2
+
3
+ Main objects:
4
+
5
+ .. autosummary::
6
+
7
+ DispatchingRuleSolver
8
+ dispatching_rule_factory
9
+ DispatchingRuleType
10
+ MachineChooserType
11
+ dispatching_rule_factory
12
+ machine_chooser_factory
13
+
14
+ Dispatching rules:
15
+
16
+ .. autosummary::
17
+
18
+ shortest_processing_time_rule
19
+ first_come_first_served_rule
20
+ most_work_remaining_rule
21
+ most_operations_remaining_rule
22
+ random_operation_rule
23
+ score_based_rule
24
+ score_based_rule_with_tie_breaker
25
+ observer_based_most_work_remaining_rule
26
+
27
+ Dispatching rule scorers:
28
+
29
+ .. autosummary::
30
+
31
+ shortest_processing_time_score
32
+ first_come_first_served_score
33
+ MostWorkRemainingScorer
34
+ most_operations_remaining_score
35
+ random_score
36
+
37
+ """
38
+
39
+ from ._dispatching_rules_functions import (
40
+ shortest_processing_time_rule,
41
+ first_come_first_served_rule,
42
+ most_work_remaining_rule,
43
+ most_operations_remaining_rule,
44
+ random_operation_rule,
45
+ score_based_rule,
46
+ score_based_rule_with_tie_breaker,
47
+ shortest_processing_time_score,
48
+ first_come_first_served_score,
49
+ MostWorkRemainingScorer,
50
+ most_operations_remaining_score,
51
+ random_score,
52
+ observer_based_most_work_remaining_rule,
53
+ )
54
+ from ._machine_chooser_factory import (
55
+ MachineChooserType,
56
+ MachineChooser,
57
+ machine_chooser_factory,
58
+ )
59
+
60
+ from ._dispatching_rule_factory import (
61
+ dispatching_rule_factory,
62
+ DispatchingRuleType,
63
+ )
64
+ from ._dispatching_rule_solver import DispatchingRuleSolver
65
+
66
+
67
+ __all__ = [
68
+ "shortest_processing_time_rule",
69
+ "first_come_first_served_rule",
70
+ "most_work_remaining_rule",
71
+ "most_operations_remaining_rule",
72
+ "random_operation_rule",
73
+ "score_based_rule",
74
+ "score_based_rule_with_tie_breaker",
75
+ "shortest_processing_time_score",
76
+ "first_come_first_served_score",
77
+ "MostWorkRemainingScorer",
78
+ "most_operations_remaining_score",
79
+ "random_score",
80
+ "dispatching_rule_factory",
81
+ "DispatchingRuleType",
82
+ "MachineChooserType",
83
+ "machine_chooser_factory",
84
+ "MachineChooser",
85
+ "DispatchingRuleSolver",
86
+ "observer_based_most_work_remaining_rule",
87
+ ]
@@ -0,0 +1,84 @@
1
+ """Contains factory functions for creating dispatching rules, machine choosers,
2
+ and pruning functions for the job shop scheduling problem.
3
+
4
+ The factory functions create and return the appropriate functions based on the
5
+ specified names or enums.
6
+ """
7
+
8
+ from typing import Dict, Union
9
+
10
+ from enum import Enum
11
+ from collections.abc import Callable
12
+
13
+ from job_shop_lib import Operation
14
+ from job_shop_lib.exceptions import ValidationError
15
+ from job_shop_lib.dispatching import Dispatcher
16
+ from job_shop_lib.dispatching.rules import (
17
+ shortest_processing_time_rule,
18
+ first_come_first_served_rule,
19
+ most_operations_remaining_rule,
20
+ random_operation_rule,
21
+ most_work_remaining_rule,
22
+ )
23
+
24
+
25
+ class DispatchingRuleType(str, Enum):
26
+ """Enumeration of dispatching rules for the job shop scheduling problem."""
27
+
28
+ SHORTEST_PROCESSING_TIME = "shortest_processing_time"
29
+ FIRST_COME_FIRST_SERVED = "first_come_first_served"
30
+ MOST_WORK_REMAINING = "most_work_remaining"
31
+ MOST_OPERATIONS_REMAINING = "most_operations_remaining"
32
+ RANDOM = "random"
33
+
34
+
35
+ def dispatching_rule_factory(
36
+ dispatching_rule: Union[str, DispatchingRuleType],
37
+ ) -> Callable[[Dispatcher], Operation]:
38
+ """Creates and returns a dispatching rule function based on the specified
39
+ dispatching rule name.
40
+
41
+ The dispatching rule function determines the order in which operations are
42
+ selected for execution based on certain criteria such as shortest
43
+ processing time, first come first served, etc.
44
+
45
+ Args:
46
+ dispatching_rule: The name of the dispatching rule to be used.
47
+ Supported values are 'shortest_processing_time',
48
+ 'first_come_first_served', 'most_work_remaining',
49
+ and 'random'.
50
+
51
+ Returns:
52
+ A function that takes a Dispatcher instance as input and returns an
53
+ Operation based on the specified dispatching rule.
54
+
55
+ Raises:
56
+ ValidationError:
57
+ If the dispatching_rule argument is not recognized or it is
58
+ not supported.
59
+ """
60
+ dispatching_rules: Dict[
61
+ DispatchingRuleType,
62
+ Callable[[Dispatcher], Operation],
63
+ ] = {
64
+ DispatchingRuleType.SHORTEST_PROCESSING_TIME: (
65
+ shortest_processing_time_rule
66
+ ),
67
+ DispatchingRuleType.FIRST_COME_FIRST_SERVED: (
68
+ first_come_first_served_rule
69
+ ),
70
+ DispatchingRuleType.MOST_WORK_REMAINING: most_work_remaining_rule,
71
+ DispatchingRuleType.MOST_OPERATIONS_REMAINING: (
72
+ most_operations_remaining_rule
73
+ ),
74
+ DispatchingRuleType.RANDOM: random_operation_rule,
75
+ }
76
+
77
+ dispatching_rule = dispatching_rule.lower()
78
+ if dispatching_rule not in dispatching_rules:
79
+ raise ValidationError(
80
+ f"Dispatching rule {dispatching_rule} not recognized. Available "
81
+ f"dispatching rules: {', '.join(dispatching_rules)}."
82
+ )
83
+
84
+ return dispatching_rules[dispatching_rule] # type: ignore[index]