job-shop-lib 1.0.0a3__py3-none-any.whl → 1.0.0a4__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. job_shop_lib/_job_shop_instance.py +104 -38
  2. job_shop_lib/_operation.py +12 -3
  3. job_shop_lib/_schedule.py +10 -12
  4. job_shop_lib/_scheduled_operation.py +15 -16
  5. job_shop_lib/dispatching/_dispatcher.py +12 -15
  6. job_shop_lib/dispatching/_dispatcher_observer_config.py +15 -2
  7. job_shop_lib/dispatching/_factories.py +2 -2
  8. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +0 -1
  9. job_shop_lib/dispatching/feature_observers/_factory.py +21 -18
  10. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +1 -0
  11. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +1 -1
  12. job_shop_lib/generation/_general_instance_generator.py +33 -34
  13. job_shop_lib/generation/_instance_generator.py +14 -17
  14. job_shop_lib/generation/_transformations.py +11 -8
  15. job_shop_lib/graphs/__init__.py +3 -0
  16. job_shop_lib/graphs/_build_disjunctive_graph.py +41 -3
  17. job_shop_lib/graphs/graph_updaters/_graph_updater.py +11 -13
  18. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +17 -20
  19. job_shop_lib/reinforcement_learning/__init__.py +16 -7
  20. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +69 -57
  21. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +42 -31
  22. job_shop_lib/reinforcement_learning/_types_and_constants.py +2 -2
  23. job_shop_lib/visualization/__init__.py +24 -5
  24. job_shop_lib/visualization/_gantt_chart_creator.py +118 -80
  25. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +15 -11
  26. job_shop_lib/visualization/_plot_disjunctive_graph.py +382 -0
  27. {job_shop_lib-1.0.0a3.dist-info → job_shop_lib-1.0.0a4.dist-info}/METADATA +5 -5
  28. {job_shop_lib-1.0.0a3.dist-info → job_shop_lib-1.0.0a4.dist-info}/RECORD +31 -31
  29. job_shop_lib/visualization/_disjunctive_graph.py +0 -210
  30. /job_shop_lib/visualization/{_agent_task_graph.py → _plot_agent_task_graph.py} +0 -0
  31. {job_shop_lib-1.0.0a3.dist-info → job_shop_lib-1.0.0a4.dist-info}/LICENSE +0 -0
  32. {job_shop_lib-1.0.0a3.dist-info → job_shop_lib-1.0.0a4.dist-info}/WHEEL +0 -0
@@ -15,16 +15,47 @@ from job_shop_lib import Operation
15
15
  class JobShopInstance:
16
16
  """Data structure to store a Job Shop Scheduling Problem instance.
17
17
 
18
- Additional attributes such as `num_jobs` or `num_machines` can be computed
19
- from the instance and are cached for performance if they require expensive
20
- computations.
18
+ Additional attributes such as ``num_machines`` or ``durations_matrix`` can
19
+ be computed from the instance and are cached for performance if they
20
+ require expensive computations.
21
+
22
+ Methods:
23
+
24
+ .. autosummary::
25
+ :nosignatures:
26
+
27
+ from_taillard_file
28
+ to_dict
29
+ from_matrices
30
+ set_operation_attributes
31
+
32
+ Properties:
33
+
34
+ .. autosummary::
35
+ :nosignatures:
36
+
37
+ num_jobs
38
+ num_machines
39
+ num_operations
40
+ is_flexible
41
+ durations_matrix
42
+ machines_matrix
43
+ durations_matrix_array
44
+ machines_matrix_array
45
+ operations_by_machine
46
+ max_duration
47
+ max_duration_per_job
48
+ max_duration_per_machine
49
+ job_durations
50
+ machine_loads
51
+ total_duration
21
52
 
22
53
  Attributes:
23
54
  jobs (list[list[Operation]]):
24
55
  A list of lists of operations. Each list of operations represents
25
56
  a job, and the operations are ordered by their position in the job.
26
- The `job_id`, `position_in_job`, and `operation_id` attributes of
27
- the operations are set when the instance is created.
57
+ The ``job_id``, ``position_in_job``, and `operation_id` attributes
58
+ of the operations are set when the instance is created.
28
59
  name (str):
29
60
  A string with the name of the instance.
30
61
  metadata (dict[str, Any]):
@@ -34,11 +65,16 @@ class JobShopInstance:
34
65
  jobs:
35
66
  A list of lists of operations. Each list of operations
36
67
  represents a job, and the operations are ordered by their
37
- position in the job. The `job_id`, `position_in_job`, and
38
- `operation_id` attributes of the operations are set when the
68
+ position in the job. The ``job_id``, ``position_in_job``, and
69
+ ``operation_id`` attributes of the operations are set when the
39
70
  instance is created.
40
71
  name:
41
72
  A string with the name of the instance.
73
+ set_operation_attributes:
74
+ If True, the ``job_id``, ``position_in_job``, and ``operation_id``
75
+ attributes of the operations are set when the instance is created.
76
+ See :meth:`set_operation_attributes` for more information. Defaults
77
+ to True.
42
78
  **metadata:
43
79
  Additional information about the instance.
44
80
  """
@@ -47,15 +83,37 @@ class JobShopInstance:
47
83
  self,
48
84
  jobs: list[list[Operation]],
49
85
  name: str = "JobShopInstance",
86
+ set_operation_attributes: bool = True,
50
87
  **metadata: Any,
51
88
  ):
52
89
  self.jobs: list[list[Operation]] = jobs
53
- self.set_operation_attributes()
90
+ if set_operation_attributes:
91
+ self.set_operation_attributes()
54
92
  self.name: str = name
55
93
  self.metadata: dict[str, Any] = metadata
56
94
 
57
95
  def set_operation_attributes(self):
58
- """Sets the job_id and position of each operation."""
96
+ """Sets the ``job_id``, ``position_in_job``, and ``operation_id``
97
+ attributes for each operation in the instance.
98
+
99
+ The ``job_id`` attribute is set to the id of the job to which the
100
+ operation belongs.
101
+
102
+ The ``position_in_job`` attribute is set to the
103
+ position of the operation in the job (starts from 0).
104
+
105
+ The ``operation_id`` attribute is set to a unique identifier for the
106
+ operation (starting from 0).
107
+
108
+ The formula to compute the ``operation_id`` in a job shop instance with
109
+ a fixed number of operations per job is:
110
+
111
+ .. code-block:: python
112
+
113
+ operation_id = job_id * num_operations_per_job + position_in_job
114
+
115
+ """
116
+
59
117
  operation_id = 0
60
118
  for job_id, job in enumerate(self.jobs):
61
119
  for position, operation in enumerate(job):
@@ -90,8 +148,8 @@ class JobShopInstance:
90
148
  Additional information about the instance.
91
149
 
92
150
  Returns:
93
- A JobShopInstance object with the operations read from the file,
94
- and the name and metadata provided.
151
+ A :class:`JobShopInstance` object with the operations read from the
152
+ file, and the name and metadata provided.
95
153
  """
96
154
  with open(file_path, "r", encoding=encoding) as file:
97
155
  lines = file.readlines()
@@ -128,13 +186,17 @@ class JobShopInstance:
128
186
  like Taillard's.
129
187
 
130
188
  Returns:
131
- The returned dictionary has the following structure:
132
- {
133
- "name": self.name,
134
- "duration_matrix": self.durations_matrix,
135
- "machines_matrix": self.machines_matrix,
136
- "metadata": self.metadata,
137
- }
189
+ dict[str, Any]: The returned dictionary has the following
190
+ structure:
191
+
192
+ .. code-block:: python
193
+
194
+ {
195
+ "name": self.name,
196
+ "duration_matrix": self.durations_matrix,
197
+ "machines_matrix": self.machines_matrix,
198
+ "metadata": self.metadata,
199
+ }
138
200
  """
139
201
  return {
140
202
  "name": self.name,
@@ -151,7 +213,8 @@ class JobShopInstance:
151
213
  name: str = "JobShopInstance",
152
214
  metadata: dict[str, Any] | None = None,
153
215
  ) -> JobShopInstance:
154
- """Creates a JobShopInstance from duration and machines matrices.
216
+ """Creates a :class:`JobShopInstance` from duration and machines
217
+ matrices.
155
218
 
156
219
  Args:
157
220
  duration_matrix:
@@ -168,7 +231,7 @@ class JobShopInstance:
168
231
  A dictionary with additional information about the instance.
169
232
 
170
233
  Returns:
171
- A JobShopInstance object.
234
+ A :class:`JobShopInstance` object.
172
235
  """
173
236
  jobs: list[list[Operation]] = [[] for _ in range(len(duration_matrix))]
174
237
 
@@ -220,7 +283,7 @@ class JobShopInstance:
220
283
 
221
284
  @functools.cached_property
222
285
  def is_flexible(self) -> bool:
223
- """Returns True if any operation has more than one machine."""
286
+ """Returns ``True`` if any operation has more than one machine."""
224
287
  return any(
225
288
  any(len(operation.machines) > 1 for operation in job)
226
289
  for job in self.jobs
@@ -230,12 +293,14 @@ class JobShopInstance:
230
293
  def durations_matrix(self) -> list[list[int]]:
231
294
  """Returns the duration matrix of the instance.
232
295
 
233
- The duration of the operation with `job_id` i and `position_in_job` j
234
- is stored in the i-th position of the j-th list of the returned matrix:
296
+ The duration of the operation with ``job_id`` i and ``position_in_job``
297
+ j is stored in the i-th position of the j-th list of the returned
298
+ matrix:
299
+
300
+ .. code-block:: python
301
+
302
+ duration = instance.durations_matrix[i][j]
235
303
 
236
- ```python
237
- duration = instance.durations_matrix[i][j]
238
- ```
239
304
  """
240
305
  return [[operation.duration for operation in job] for job in self.jobs]
241
306
 
@@ -252,9 +317,9 @@ class JobShopInstance:
252
317
  To access the machines of the operation with position i in the job
253
318
  with id j, the following code must be used:
254
319
 
255
- ```python
256
- machines = instance.machines_matrix[j][i]
257
- ```
320
+ .. code-block:: python
321
+
322
+ machines = instance.machines_matrix[j][i]
258
323
 
259
324
  """
260
325
  if self.is_flexible:
@@ -269,8 +334,9 @@ class JobShopInstance:
269
334
  def durations_matrix_array(self) -> NDArray[np.float32]:
270
335
  """Returns the duration matrix of the instance as a numpy array.
271
336
 
272
- The returned array has shape (num_jobs, max_num_operations_per_job).
273
- Non-existing operations are filled with np.nan.
337
+ The returned array has shape (``num_jobs``,
338
+ ``max_num_operations_per_job``).
339
+ Non-existing operations are filled with ``np.nan``.
274
340
 
275
341
  Example:
276
342
  >>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
@@ -286,9 +352,9 @@ class JobShopInstance:
286
352
  def machines_matrix_array(self) -> NDArray[np.float32]:
287
353
  """Returns the machines matrix of the instance as a numpy array.
288
354
 
289
- The returned array has shape (num_jobs, max_num_operations_per_job,
290
- max_num_machines_per_operation). Non-existing machines are filled with
291
- np.nan.
355
+ The returned array has shape (``num_jobs``,
356
+ ``max_num_operations_per_job``, ``max_num_machines_per_operation``).
357
+ Non-existing machines are filled with ``np.nan``.
292
358
 
293
359
  Example:
294
360
  >>> jobs = [
@@ -411,7 +477,7 @@ class JobShopInstance:
411
477
  def _fill_matrix_with_nans_2d(
412
478
  matrix: list[list[int]],
413
479
  ) -> NDArray[np.float32]:
414
- """Fills a matrix with np.nan values.
480
+ """Fills a matrix with ``np.nan`` values.
415
481
 
416
482
  Args:
417
483
  matrix:
@@ -419,7 +485,7 @@ class JobShopInstance:
419
485
 
420
486
  Returns:
421
487
  A numpy array with the same shape as the input matrix, filled with
422
- np.nan values.
488
+ ``np.nan`` values.
423
489
  """
424
490
  max_length = max(len(row) for row in matrix)
425
491
  squared_matrix = np.full(
@@ -433,7 +499,7 @@ class JobShopInstance:
433
499
  def _fill_matrix_with_nans_3d(
434
500
  matrix: list[list[list[int]]],
435
501
  ) -> NDArray[np.float32]:
436
- """Fills a 3D matrix with np.nan values.
502
+ """Fills a 3D matrix with ``np.nan`` values.
437
503
 
438
504
  Args:
439
505
  matrix:
@@ -441,7 +507,7 @@ class JobShopInstance:
441
507
 
442
508
  Returns:
443
509
  A numpy array with the same shape as the input matrix, filled with
444
- np.nan values.
510
+ ``np.nan`` values.
445
511
  """
446
512
  max_length = max(len(row) for row in matrix)
447
513
  max_inner_length = len(matrix[0][0])
@@ -42,11 +42,20 @@ class Operation:
42
42
  "The time it takes to perform the operation. Often referred"
43
43
  " to as the processing time."
44
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.",
45
+ "job_id": (
46
+ "The id of the job the operation belongs to. Defaults to -1. "
47
+ "It is usually set by the :class:`JobShopInstance` class after "
48
+ "initialization."
49
+ ),
50
+ "position_in_job": (
51
+ "The index of the operation in the job. Defaults to -1. "
52
+ "It is usually set by the :class:`JobShopInstance` class after "
53
+ "initialization."
54
+ ),
47
55
  "operation_id": (
48
56
  "The id of the operation. This is unique within a "
49
- ":class:`JobShopInstance`."
57
+ ":class:`JobShopInstance`. Defaults to -1. It is usually set by "
58
+ "the :class:`JobShopInstance` class after initialization."
50
59
  ),
51
60
  }
52
61
 
job_shop_lib/_schedule.py CHANGED
@@ -25,6 +25,16 @@ class Schedule:
25
25
  is_complete
26
26
  add
27
27
  reset
28
+
29
+ Args:
30
+ instance:
31
+ The :class:`JobShopInstance` object that the schedule is for.
32
+ schedule:
33
+ A list of lists of :class:`ScheduledOperation` objects. Each
34
+ list represents the order of operations on a machine. If
35
+ not provided, the schedule is initialized as an empty schedule.
36
+ **metadata:
37
+ Additional information about the schedule.
28
38
  """
29
39
 
30
40
  __slots__ = {
@@ -48,18 +58,6 @@ class Schedule:
48
58
  schedule: list[list[ScheduledOperation]] | None = None,
49
59
  **metadata: Any,
50
60
  ):
51
- """Initializes the object with the given instance and schedule.
52
-
53
- Args:
54
- instance:
55
- The :class:`JobShopInstance` object that the schedule is for.
56
- schedule:
57
- A list of lists of :class:`ScheduledOperation` objects. Each
58
- list represents the order of operations on a machine. If
59
- not provided, the schedule is initialized as an empty schedule.
60
- **metadata:
61
- Additional information about the schedule.
62
- """
63
61
  if schedule is None:
64
62
  schedule = [[] for _ in range(instance.num_machines)]
65
63
 
@@ -5,7 +5,21 @@ from job_shop_lib.exceptions import ValidationError
5
5
 
6
6
 
7
7
  class ScheduledOperation:
8
- """Data structure to store a scheduled operation."""
8
+ """Data structure to store a scheduled operation.
9
+
10
+ Args:
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
+ machine_id:
16
+ The id of the machine on which the operation is scheduled.
17
+
18
+ Raises:
19
+ ValidationError:
20
+ If the given machine_id is not in the list of valid machines
21
+ for the operation.
22
+ """
9
23
 
10
24
  __slots__ = {
11
25
  "operation": "The :class:`Operation` object that is scheduled.",
@@ -16,21 +30,6 @@ class ScheduledOperation:
16
30
  }
17
31
 
18
32
  def __init__(self, operation: Operation, start_time: int, machine_id: int):
19
- """Initializes a new instance of the :class:`ScheduledOperation` class.
20
-
21
- Args:
22
- operation:
23
- The :class:`Operation` object that is scheduled.
24
- start_time:
25
- The time at which the operation is scheduled to start.
26
- machine_id:
27
- The id of the machine on which the operation is scheduled.
28
-
29
- Raises:
30
- ValidationError:
31
- If the given machine_id is not in the list of valid machines
32
- for the operation.
33
- """
34
33
  self.operation: Operation = operation
35
34
  self.start_time: int = start_time
36
35
  self._machine_id = machine_id
@@ -29,6 +29,18 @@ class DispatcherObserver(abc.ABC):
29
29
  dispatcher:
30
30
  The :class:`Dispatcher` instance to observe.
31
31
 
32
+ Args:
33
+ dispatcher:
34
+ The :class:`Dispatcher` instance to observe.
35
+ subscribe:
36
+ If ``True``, automatically subscribes the observer to the
37
+ dispatcher when it is initialized. Defaults to ``True``.
38
+
39
+ Raises:
40
+ ValidationError: If ``is_singleton`` is ``True`` and an observer of the
41
+ same type already exists in the dispatcher's list of
42
+ subscribers.
43
+
32
44
  Example:
33
45
 
34
46
  .. code-block:: python
@@ -61,21 +73,6 @@ class DispatcherObserver(abc.ABC):
61
73
  *,
62
74
  subscribe: bool = True,
63
75
  ):
64
- """Initializes the observer with the :class:`Dispatcher` and subscribes
65
- to it.
66
-
67
- Args:
68
- dispatcher:
69
- The `Dispatcher` instance to observe.
70
- subscribe:
71
- If True, automatically subscribes the observer to the
72
- dispatcher.
73
-
74
- Raises:
75
- ValidationError: If ``is_singleton`` is True and an observer of the
76
- same type already exists in the dispatcher's list of
77
- subscribers.
78
- """
79
76
  if self._is_singleton and any(
80
77
  isinstance(observer, self.__class__)
81
78
  for observer in dispatcher.subscribers
@@ -26,14 +26,21 @@ class DispatcherObserverConfig(Generic[T]):
26
26
  keyword arguments to pass to the dispatcher observer constructor while
27
27
  not containing the ``dispatcher`` argument.
28
28
 
29
- Attributes:
29
+ Args:
30
30
  class_type:
31
31
  Type of the class to be initialized. It can be the class type, an
32
32
  enum value, or a string. This is useful for the creation of
33
- DispatcherObserver instances from the factory functions.
33
+ :class:`~job_shop_lib.dispatching.DispatcherObserver` instances
34
+ from the factory functions.
34
35
  kwargs:
35
36
  Keyword arguments needed to initialize the class. It must not
36
37
  contain the ``dispatcher`` argument.
38
+
39
+ .. seealso::
40
+
41
+ - :class:`~job_shop_lib.dispatching.DispatcherObserver`
42
+ - :func:`job_shop_lib.dispatching.feature_observers.\\
43
+ feature_observer_factory`
37
44
  """
38
45
 
39
46
  # We use the type hint T, instead of ObserverType, to allow for string or
@@ -44,7 +51,13 @@ class DispatcherObserverConfig(Generic[T]):
44
51
  # This allows for the creation of a FeatureObserver instance
45
52
  # from the factory function.
46
53
  class_type: T
54
+ """Type of the class to be initialized. It can be the class type, an
55
+ enum value, or a string. This is useful for the creation of
56
+ :class:`DispatcherObserver` instances from the factory functions."""
57
+
47
58
  kwargs: dict[str, Any] = field(default_factory=dict)
59
+ """Keyword arguments needed to initialize the class. It must not
60
+ contain the ``dispatcher`` argument."""
48
61
 
49
62
  def __post_init__(self):
50
63
  if "dispatcher" in self.kwargs:
@@ -51,13 +51,13 @@ def create_composite_operation_filter(
51
51
  'non_immediate_machines' or any Callable that takes a
52
52
  :class:`~job_shop_lib.dispatching.Dispatcher` instance and a list
53
53
  of :class:`~job_shop_lib.Operation` instances as input
54
- and returns a list of :class:`~job_shop_lib.Operation`instances.
54
+ and returns a list of :class:`~job_shop_lib.Operation` instances.
55
55
 
56
56
  Returns:
57
57
  A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
58
58
  instance and a list of :class:`~job_shop_lib.Operation`
59
59
  instances as input and returns a list of
60
- :class:`~job_shop_lib.Operation`instances based on
60
+ :class:`~job_shop_lib.Operation` instances based on
61
61
  the specified list of filter strategies.
62
62
 
63
63
  Raises:
@@ -193,7 +193,6 @@ if __name__ == "__main__":
193
193
  dispatcher=dispatcher_,
194
194
  )
195
195
  for observer_type in feature_observer_types_
196
- if not observer_type == FeatureObserverType.COMPOSITE
197
196
  # and not FeatureObserverType.EARLIEST_START_TIME
198
197
  ]
199
198
  composite_observer_ = CompositeFeatureObserver(
@@ -1,4 +1,4 @@
1
- """Contains factory functions for creating node feature encoders."""
1
+ """Contains factory functions for creating :class:`FeatureObserver`s."""
2
2
 
3
3
  from enum import Enum
4
4
 
@@ -18,8 +18,12 @@ from job_shop_lib.dispatching.feature_observers import (
18
18
  class FeatureObserverType(str, Enum):
19
19
  """Enumeration of the different feature observers.
20
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.
21
+ Each :class:`FeatureObserver` is associated with a string value that can be
22
+ used to create the :class:`FeatureObserver` using the factory function.
23
+
24
+ It does not include the :class:`CompositeFeatureObserver` class since this
25
+ observer is often managed separately from the others. For example, a
26
+ common use case is to create a list of feature observers and pass them to
23
27
  """
24
28
 
25
29
  IS_READY = "is_ready"
@@ -29,7 +33,6 @@ class FeatureObserverType(str, Enum):
29
33
  POSITION_IN_JOB = "position_in_job"
30
34
  REMAINING_OPERATIONS = "remaining_operations"
31
35
  IS_COMPLETED = "is_completed"
32
- COMPOSITE = "composite"
33
36
 
34
37
 
35
38
  # FeatureObserverConfig = DispatcherObserverConfig[
@@ -43,7 +46,7 @@ FeatureObserverConfig = (
43
46
 
44
47
 
45
48
  def feature_observer_factory(
46
- feature_creator_type: (
49
+ feature_observer_type: (
47
50
  str
48
51
  | FeatureObserverType
49
52
  | type[FeatureObserver]
@@ -51,29 +54,29 @@ def feature_observer_factory(
51
54
  ),
52
55
  **kwargs,
53
56
  ) -> FeatureObserver:
54
- """Creates and returns a node feature creator based on the specified
55
- node feature creator type.
57
+ """Creates and returns a :class:`FeatureObserver` based on the specified
58
+ :class:`FeatureObserver` type.
56
59
 
57
60
  Args:
58
61
  feature_creator_type:
59
- The type of node feature creator to create.
62
+ The type of :class:`FeatureObserver` to create.
60
63
  **kwargs:
61
- Additional keyword arguments to pass to the node
62
- feature creator constructor.
64
+ Additional keyword arguments to pass to the
65
+ :class:`FeatureObserver` constructor.
63
66
 
64
67
  Returns:
65
- A node feature creator instance.
68
+ A :class:`FeatureObserver` instance.
66
69
  """
67
- if isinstance(feature_creator_type, DispatcherObserverConfig):
70
+ if isinstance(feature_observer_type, DispatcherObserverConfig):
68
71
  return feature_observer_factory(
69
- feature_creator_type.class_type,
70
- **feature_creator_type.kwargs,
72
+ feature_observer_type.class_type,
73
+ **feature_observer_type.kwargs,
71
74
  **kwargs,
72
75
  )
73
76
  # if the instance is of type type[FeatureObserver] we can just
74
77
  # call the object constructor with the keyword arguments
75
- if isinstance(feature_creator_type, type):
76
- return feature_creator_type(**kwargs)
78
+ if isinstance(feature_observer_type, type):
79
+ return feature_observer_type(**kwargs)
77
80
 
78
81
  mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
79
82
  FeatureObserverType.IS_READY: IsReadyObserver,
@@ -84,5 +87,5 @@ def feature_observer_factory(
84
87
  FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
85
88
  FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
86
89
  }
87
- feature_creator = mapping[feature_creator_type] # type: ignore[index]
88
- return feature_creator(**kwargs)
90
+ feature_observer = mapping[feature_observer_type] # type: ignore[index]
91
+ return feature_observer(**kwargs)
@@ -51,6 +51,7 @@ class IsCompletedObserver(FeatureObserver):
51
51
  def __init__(
52
52
  self,
53
53
  dispatcher: Dispatcher,
54
+ *,
54
55
  feature_types: list[FeatureType] | FeatureType | None = None,
55
56
  subscribe: bool = True,
56
57
  ):
@@ -55,7 +55,7 @@ class DispatchingRuleSolver(BaseSolver):
55
55
  - a list with names or actual ready operations filters to be used.
56
56
  If a list is provided, a composite filter will be created
57
57
  using the specified filters.
58
-
58
+
59
59
  .. seealso::
60
60
  - :func:`job_shop_lib.dispatching.rules.dispatching_rule_factory`
61
61
  - :func:`job_shop_lib.dispatching.rules.machine_chooser_factory`