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
@@ -3,11 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
- from typing import Any
6
+ from typing import Any, TypeVar
7
7
  from collections.abc import Callable
8
- from collections import deque
9
8
  from functools import wraps
10
- from warnings import warn
11
9
 
12
10
  from job_shop_lib import (
13
11
  JobShopInstance,
@@ -15,40 +13,74 @@ from job_shop_lib import (
15
13
  ScheduledOperation,
16
14
  Operation,
17
15
  )
16
+ from job_shop_lib.exceptions import ValidationError
18
17
 
19
18
 
20
19
  # Added here to avoid circular imports
21
20
  class DispatcherObserver(abc.ABC):
22
- """Interface for classes that observe th"""
21
+ """Abstract class that allows objects to observe and respond to changes
22
+ within the :class:`Dispatcher`.
23
+
24
+ It follows the Observer design pattern, where observers subscribe to the
25
+ dispatcher and receive updates when certain events occur, such as when
26
+ an operation is scheduled or when the dispatcher is reset.
27
+
28
+ Attributes:
29
+ dispatcher:
30
+ The :class:`Dispatcher` instance to observe.
31
+
32
+ Example:
33
+
34
+ .. code-block:: python
35
+
36
+ from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
37
+ from job_shop_lib import ScheduledOperation
38
+
39
+
40
+ class HistoryObserver(DispatcherObserver):
41
+ def __init__(self, dispatcher: Dispatcher):
42
+ super().__init__(dispatcher)
43
+ self.history: list[ScheduledOperation] = []
44
+
45
+ def update(self, scheduled_operation: ScheduledOperation):
46
+ self.history.append(scheduled_operation)
47
+
48
+ def reset(self):
49
+ self.history = []
50
+
51
+ """
52
+
53
+ # Made read-only following Google Style Guide recommendation
54
+ _is_singleton = True
55
+ """If True, ensures only one instance of this observer type is subscribed
56
+ to the dispatcher."""
23
57
 
24
58
  def __init__(
25
59
  self,
26
60
  dispatcher: Dispatcher,
27
- is_singleton: bool = True,
61
+ *,
28
62
  subscribe: bool = True,
29
63
  ):
30
- """Initializes the observer with the `Dispatcher` and subscribes to
31
- it.
64
+ """Initializes the observer with the :class:`Dispatcher` and subscribes
65
+ to it.
32
66
 
33
67
  Args:
34
- subject:
35
- The subject to observe.
36
- is_singleton:
37
- Whether the observer should be a singleton. If True, the
38
- observer will be the only instance of its class in the
39
- subject's list of subscribers. If False, the observer will
40
- be added to the subject's list of subscribers every time
41
- it is initialized.
68
+ dispatcher:
69
+ The `Dispatcher` instance to observe.
42
70
  subscribe:
43
- Whether to subscribe the observer to the subject. If False,
44
- the observer will not be subscribed to the subject and will
45
- not receive automatic updates.
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.
46
78
  """
47
- if is_singleton and any(
79
+ if self._is_singleton and any(
48
80
  isinstance(observer, self.__class__)
49
81
  for observer in dispatcher.subscribers
50
82
  ):
51
- raise ValueError(
83
+ raise ValidationError(
52
84
  f"An observer of type {self.__class__.__name__} already "
53
85
  "exists in the dispatcher's list of subscribers. If you want "
54
86
  "to create multiple instances of this observer, set "
@@ -59,6 +91,15 @@ class DispatcherObserver(abc.ABC):
59
91
  if subscribe:
60
92
  self.dispatcher.subscribe(self)
61
93
 
94
+ @property
95
+ def is_singleton(self) -> bool:
96
+ """Returns whether this observer is a singleton.
97
+
98
+ This is a class attribute that determines whether only one
99
+ instance of this observer type can be subscribed to the dispatcher.
100
+ """
101
+ return self._is_singleton
102
+
62
103
  @abc.abstractmethod
63
104
  def update(self, scheduled_operation: ScheduledOperation):
64
105
  """Called when an operation is scheduled on a machine."""
@@ -74,6 +115,11 @@ class DispatcherObserver(abc.ABC):
74
115
  return self.__class__.__name__
75
116
 
76
117
 
118
+ # Disable pylint's false positive
119
+ # pylint: disable=invalid-name
120
+ ObserverType = TypeVar("ObserverType", bound=DispatcherObserver)
121
+
122
+
77
123
  def _dispatcher_cache(method):
78
124
  """Decorator to cache results of a method based on its name.
79
125
 
@@ -115,7 +161,7 @@ class Dispatcher:
115
161
  The instance of the job shop problem to be scheduled.
116
162
  schedule:
117
163
  The schedule of operations on machines.
118
- pruning_function:
164
+ ready_operations_filter:
119
165
  A function that filters out operations that are not ready to be
120
166
  scheduled.
121
167
  """
@@ -126,7 +172,7 @@ class Dispatcher:
126
172
  "_machine_next_available_time",
127
173
  "_job_next_operation_index",
128
174
  "_job_next_available_time",
129
- "pruning_function",
175
+ "ready_operations_filter",
130
176
  "subscribers",
131
177
  "_cache",
132
178
  )
@@ -134,7 +180,7 @@ class Dispatcher:
134
180
  def __init__(
135
181
  self,
136
182
  instance: JobShopInstance,
137
- pruning_function: (
183
+ ready_operations_filter: (
138
184
  Callable[[Dispatcher, list[Operation]], list[Operation]] | None
139
185
  ) = None,
140
186
  ) -> None:
@@ -143,16 +189,17 @@ class Dispatcher:
143
189
  Args:
144
190
  instance:
145
191
  The instance of the job shop problem to be solved.
146
- pruning_function:
192
+ ready_operations_filter:
147
193
  A function that filters out operations that are not ready to
148
194
  be scheduled. The function should take the dispatcher and a
149
195
  list of operations as input and return a list of operations
150
- that are ready to be scheduled. If `None`, no pruning is done.
196
+ that are ready to be scheduled. If ``None``, no filtering is
197
+ done.
151
198
  """
152
199
 
153
200
  self.instance = instance
154
201
  self.schedule = Schedule(self.instance)
155
- self.pruning_function = pruning_function
202
+ self.ready_operations_filter = ready_operations_filter
156
203
 
157
204
  self._machine_next_available_time = [0] * self.instance.num_machines
158
205
  self._job_next_operation_index = [0] * self.instance.num_jobs
@@ -207,6 +254,7 @@ class Dispatcher:
207
254
  available time for the machine and the next available time for the
208
255
  job to which the operation belongs. The operation is then scheduled
209
256
  on the machine and the tracking attributes are updated.
257
+
210
258
  Args:
211
259
  operation:
212
260
  The operation to be scheduled.
@@ -215,11 +263,11 @@ class Dispatcher:
215
263
  scheduled.
216
264
 
217
265
  Raises:
218
- ValueError: If the operation is not ready to be scheduled.
266
+ ValidationError: If the operation is not ready to be scheduled.
219
267
  """
220
268
 
221
269
  if not self.is_operation_ready(operation):
222
- raise ValueError("Operation is not ready to be scheduled.")
270
+ raise ValidationError("Operation is not ready to be scheduled.")
223
271
 
224
272
  start_time = self.start_time(operation, machine_id)
225
273
 
@@ -229,10 +277,6 @@ class Dispatcher:
229
277
  self.schedule.add(scheduled_operation)
230
278
  self._update_tracking_attributes(scheduled_operation)
231
279
 
232
- # Notify subscribers
233
- for subscriber in self.subscribers:
234
- subscriber.update(scheduled_operation)
235
-
236
280
  def is_operation_ready(self, operation: Operation) -> bool:
237
281
  """Returns True if the given operation is ready to be scheduled.
238
282
 
@@ -265,7 +309,7 @@ class Dispatcher:
265
309
  The operation to be scheduled.
266
310
  machine_id:
267
311
  The id of the machine on which the operation is to be
268
- scheduled. If None, the start time is computed based on the
312
+ scheduled. If ``None``, the start time is computed based on the
269
313
  next available time for the operation on any machine.
270
314
  """
271
315
  return max(
@@ -286,6 +330,40 @@ class Dispatcher:
286
330
  self._job_next_available_time[job_id] = end_time
287
331
  self._cache = {}
288
332
 
333
+ # Notify subscribers
334
+ for subscriber in self.subscribers:
335
+ subscriber.update(scheduled_operation)
336
+
337
+ def create_or_get_observer(
338
+ self,
339
+ observer: type[ObserverType],
340
+ condition: Callable[[DispatcherObserver], bool] = lambda _: True,
341
+ **kwargs,
342
+ ) -> ObserverType:
343
+ """Creates a new observer of the specified type or returns an existing
344
+ observer of the same type if it already exists in the dispatcher's list
345
+ of observers.
346
+
347
+ Args:
348
+ observer:
349
+ The type of observer to be created or retrieved.
350
+ condition:
351
+ A function that takes an observer and returns True if it is
352
+ the observer to be retrieved. By default, it returns True for
353
+ all observers.
354
+ **kwargs:
355
+ Additional keyword arguments to be passed to the observer's
356
+ constructor.
357
+ """
358
+ for existing_observer in self.subscribers:
359
+ if isinstance(existing_observer, observer) and condition(
360
+ existing_observer
361
+ ):
362
+ return existing_observer
363
+
364
+ new_observer = observer(self, **kwargs)
365
+ return new_observer
366
+
289
367
  @_dispatcher_cache
290
368
  def current_time(self) -> int:
291
369
  """Returns the current time of the schedule.
@@ -293,7 +371,7 @@ class Dispatcher:
293
371
  The current time is the minimum start time of the available
294
372
  operations.
295
373
  """
296
- available_operations = self.available_operations()
374
+ available_operations = self.ready_operations()
297
375
  current_time = self.min_start_time(available_operations)
298
376
  return current_time
299
377
 
@@ -309,39 +387,39 @@ class Dispatcher:
309
387
  return int(min_start_time)
310
388
 
311
389
  @_dispatcher_cache
312
- def available_operations(self) -> list[Operation]:
390
+ def ready_operations(self) -> list[Operation]:
313
391
  """Returns a list of available operations for processing, optionally
314
- filtering out operations using the pruning function.
392
+ filtering out operations using the filter function.
315
393
 
316
394
  This method first gathers all possible next operations from the jobs
317
395
  being processed. It then optionally filters these operations using the
318
- pruning function.
396
+ filter function.
319
397
 
320
398
  Returns:
321
- A list of Operation objects that are available for scheduling.
399
+ A list of :class:`Operation` objects that are available for
400
+ scheduling.
322
401
  """
323
- available_operations = self.available_operations_without_pruning()
324
- if self.pruning_function is not None:
325
- available_operations = self.pruning_function(
402
+ available_operations = self.raw_ready_operations()
403
+ if self.ready_operations_filter is not None:
404
+ available_operations = self.ready_operations_filter(
326
405
  self, available_operations
327
406
  )
328
407
  return available_operations
329
408
 
330
409
  @_dispatcher_cache
331
- def available_operations_without_pruning(self) -> list[Operation]:
410
+ def raw_ready_operations(self) -> list[Operation]:
332
411
  """Returns a list of available operations for processing without
333
- applying the pruning function.
412
+ applying the filter function.
334
413
 
335
414
  Returns:
336
- A list of Operation objects that are available for scheduling
337
- based on precedence and machine constraints only.
415
+ A list of :class:`Operation` objects that are available for
416
+ scheduling based on precedence and machine constraints only.
338
417
  """
339
- available_operations = []
340
- for job_id, next_position in enumerate(self._job_next_operation_index):
341
- if next_position == len(self.instance.jobs[job_id]):
342
- continue
343
- operation = self.instance.jobs[job_id][next_position]
344
- available_operations.append(operation)
418
+ available_operations = [
419
+ self.instance.jobs[job_id][position]
420
+ for job_id, position in enumerate(self._job_next_operation_index)
421
+ if position < len(self.instance.jobs[job_id])
422
+ ]
345
423
  return available_operations
346
424
 
347
425
  @_dispatcher_cache
@@ -364,8 +442,8 @@ class Dispatcher:
364
442
 
365
443
  @_dispatcher_cache
366
444
  def available_machines(self) -> list[int]:
367
- """Returns the list of available machines."""
368
- available_operations = self.available_operations()
445
+ """Returns the list of ready machines."""
446
+ available_operations = self.ready_operations()
369
447
  available_machines = set()
370
448
  for operation in available_operations:
371
449
  available_machines.update(operation.machines)
@@ -373,8 +451,8 @@ class Dispatcher:
373
451
 
374
452
  @_dispatcher_cache
375
453
  def available_jobs(self) -> list[int]:
376
- """Returns the list of available jobs."""
377
- available_operations = self.available_operations()
454
+ """Returns the list of ready jobs."""
455
+ available_operations = self.ready_operations()
378
456
  available_jobs = set(
379
457
  operation.job_id for operation in available_operations
380
458
  )
@@ -384,7 +462,7 @@ class Dispatcher:
384
462
  """Calculates the earliest start time for a given operation based on
385
463
  machine and job constraints.
386
464
 
387
- This method is different from the `start_time` method in that it
465
+ This method is different from the ``start_time`` method in that it
388
466
  takes into account every machine that can process the operation, not
389
467
  just the one that will process it. However, it also assumes that
390
468
  the operation is ready to be scheduled in the job in favor of
@@ -489,28 +567,23 @@ class Dispatcher:
489
567
  current_time = self.current_time()
490
568
  return scheduled_operation.start_time <= current_time
491
569
 
492
- @classmethod
493
- def create_schedule_from_raw_solution(
494
- cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
495
- ) -> Schedule:
496
- """Deprecated method, use `Schedule.from_job_sequences` instead."""
497
- warn(
498
- "Dispatcher.create_schedule_from_raw_solution is deprecated. "
499
- "Use Schedule.from_job_sequences instead. It will be removed in "
500
- "version 1.0.0.",
501
- DeprecationWarning,
502
- )
503
- dispatcher = cls(instance)
504
- dispatcher.reset()
505
- raw_solution_deques = [
506
- deque(operations) for operations in raw_solution
570
+ def next_operation(self, job_id: int) -> Operation:
571
+ """Returns the next operation to be scheduled for the given job.
572
+
573
+ Args:
574
+ job_id:
575
+ The id of the job for which to return the next operation.
576
+
577
+ Raises:
578
+ ValidationError: If there are no more operations left for the job.
579
+ """
580
+ if (
581
+ len(self.instance.jobs[job_id])
582
+ <= self._job_next_operation_index[job_id]
583
+ ):
584
+ raise ValidationError(
585
+ f"No more operations left for job {job_id} to schedule."
586
+ )
587
+ return self.instance.jobs[job_id][
588
+ self._job_next_operation_index[job_id]
507
589
  ]
508
- while not dispatcher.schedule.is_complete():
509
- for machine_id, operations in enumerate(raw_solution_deques):
510
- if not operations:
511
- continue
512
- operation = operations[0]
513
- if dispatcher.is_operation_ready(operation):
514
- dispatcher.dispatch(operation, machine_id)
515
- operations.popleft()
516
- return dispatcher.schedule
@@ -0,0 +1,54 @@
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 TypeVar, Generic, Any
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ from job_shop_lib.exceptions import ValidationError
13
+
14
+
15
+ # Disable pylint's false positive
16
+ # pylint: disable=invalid-name
17
+ T = TypeVar("T")
18
+
19
+
20
+ @dataclass(slots=True, frozen=True)
21
+ class DispatcherObserverConfig(Generic[T]):
22
+ """Configuration for initializing any type of class.
23
+
24
+ Useful for specifying the type of the
25
+ :class:`~job_shop_lib.dispatching.DispatcherObserver` and additional
26
+ keyword arguments to pass to the dispatcher observer constructor while
27
+ not containing the ``dispatcher`` argument.
28
+
29
+ Attributes:
30
+ class_type:
31
+ Type of the class to be initialized. It can be the class type, an
32
+ enum value, or a string. This is useful for the creation of
33
+ DispatcherObserver instances from the factory functions.
34
+ kwargs:
35
+ Keyword arguments needed to initialize the class. It must not
36
+ contain the ``dispatcher`` argument.
37
+ """
38
+
39
+ # We use the type hint T, instead of ObserverType, to allow for string or
40
+ # specific Enum values to be passed as the type argument. For example:
41
+ # FeatureObserverConfig = DispatcherObserverConfig[
42
+ # type[FeatureObserver] | FeatureObserverType | str
43
+ # ]
44
+ # This allows for the creation of a FeatureObserver instance
45
+ # from the factory function.
46
+ class_type: T
47
+ kwargs: dict[str, Any] = field(default_factory=dict)
48
+
49
+ def __post_init__(self):
50
+ if "dispatcher" in self.kwargs:
51
+ raise ValidationError(
52
+ "The 'dispatcher' argument should not be included in the "
53
+ "kwargs attribute."
54
+ )
@@ -0,0 +1,125 @@
1
+ """Contains factory functions for creating ready operations filters.
2
+
3
+ The factory functions create and return the appropriate functions based on the
4
+ specified names or enums.
5
+ """
6
+
7
+ from enum import Enum
8
+ from collections.abc import Iterable
9
+
10
+ from job_shop_lib import Operation
11
+ from job_shop_lib.exceptions import ValidationError
12
+ from job_shop_lib.dispatching import (
13
+ Dispatcher,
14
+ filter_dominated_operations,
15
+ filter_non_immediate_machines,
16
+ ReadyOperationsFilter,
17
+ )
18
+
19
+
20
+ class ReadyOperationsFilterType(str, Enum):
21
+ """Enumeration of ready operations filter types.
22
+
23
+ A filter function is used by the
24
+ :class:`~job_shop_lib.dispatching.Dispatcher` class to reduce the
25
+ amount of available operations to choose from.
26
+ """
27
+
28
+ DOMINATED_OPERATIONS = "dominated_operations"
29
+ NON_IMMEDIATE_MACHINES = "non_immediate_machines"
30
+
31
+
32
+ def create_composite_operation_filter(
33
+ ready_operations_filters: Iterable[
34
+ ReadyOperationsFilter | str | ReadyOperationsFilterType
35
+ ],
36
+ ) -> ReadyOperationsFilter:
37
+ """Creates and returns a composite filter based on the specified list of
38
+ filters.
39
+
40
+ The composite filter function filters operations based on the specified
41
+ list of filter strategies.
42
+
43
+ Args:
44
+ ready_operations_filters:
45
+ A list of filter strategies to be used.
46
+ Supported values are 'dominated_operations' and
47
+ 'non_immediate_machines' or any Callable that takes a
48
+ :class:`~job_shop_lib.dispatching.Dispatcher` instance and a list
49
+ of :class:`~job_shop_lib.Operation` instances as input
50
+ and returns a list of :class:`~job_shop_lib.Operation`instances.
51
+
52
+ Returns:
53
+ A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
54
+ instance and a list of :class:`~job_shop_lib.Operation`
55
+ instances as input and returns a list of
56
+ :class:`~job_shop_lib.Operation`instances based on
57
+ the specified list of filter strategies.
58
+
59
+ Raises:
60
+ ValidationError: If any of the filter strategies in the list are not
61
+ recognized or are not supported.
62
+ """
63
+
64
+ filter_functions = [
65
+ ready_operations_filter_factory(name)
66
+ for name in ready_operations_filters
67
+ ]
68
+
69
+ def composite_pruning_function(
70
+ dispatcher: Dispatcher, operations: list[Operation]
71
+ ) -> list[Operation]:
72
+ pruned_operations = operations
73
+ for pruning_function in filter_functions:
74
+ pruned_operations = pruning_function(dispatcher, pruned_operations)
75
+
76
+ return pruned_operations
77
+
78
+ return composite_pruning_function
79
+
80
+
81
+ def ready_operations_filter_factory(
82
+ filter_name: str | ReadyOperationsFilterType | ReadyOperationsFilter,
83
+ ) -> ReadyOperationsFilter:
84
+ """Creates and returns a filter function based on the specified
85
+ filter strategy name.
86
+
87
+ The filter function filters operations based on certain criteria such as
88
+ dominated operations, immediate machine operations, etc.
89
+
90
+ Args:
91
+ filter_name:
92
+ The name of the filter function to be used. Supported
93
+ values are 'dominated_operations' and
94
+ 'immediate_machine_operations'.
95
+
96
+ Returns:
97
+ A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
98
+ instance and a list of :class:`~job_shop_lib.Operation`
99
+ instances as input and returns a list of
100
+ :class:`~job_shop_lib.Operation` instances based on
101
+ the specified filter function.
102
+
103
+ Raises:
104
+ ValidationError: If the ``filter_name`` argument is not recognized or
105
+ is not supported.
106
+ """
107
+ if callable(filter_name):
108
+ return filter_name
109
+
110
+ filtering_strategies = {
111
+ ReadyOperationsFilterType.DOMINATED_OPERATIONS: (
112
+ filter_dominated_operations
113
+ ),
114
+ ReadyOperationsFilterType.NON_IMMEDIATE_MACHINES: (
115
+ filter_non_immediate_machines
116
+ ),
117
+ }
118
+
119
+ if filter_name not in filtering_strategies:
120
+ raise ValidationError(
121
+ f"Unsupported filter function '{filter_name}'. "
122
+ f"Supported values are {', '.join(filtering_strategies.keys())}."
123
+ )
124
+
125
+ return filtering_strategies[filter_name] # type: ignore[index]
@@ -1,16 +1,14 @@
1
- """Home of the `HistoryTracker` class."""
1
+ """Home of the `HistoryObserver` class."""
2
2
 
3
3
  from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
4
4
  from job_shop_lib import ScheduledOperation
5
5
 
6
6
 
7
- class HistoryTracker(DispatcherObserver):
7
+ class HistoryObserver(DispatcherObserver):
8
8
  """Observer that stores the history of the dispatcher."""
9
9
 
10
- def __init__(self, dispatcher: Dispatcher):
11
- """Initializes the observer with the current state of the
12
- dispatcher."""
13
- super().__init__(dispatcher)
10
+ def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
11
+ super().__init__(dispatcher, subscribe=subscribe)
14
12
  self.history: list[ScheduledOperation] = []
15
13
 
16
14
  def update(self, scheduled_operation: ScheduledOperation):
@@ -4,47 +4,18 @@ This functions are used by the `Dispatcher` class to reduce the
4
4
  amount of available operations to choose from.
5
5
  """
6
6
 
7
- from collections.abc import Callable, Iterable
7
+ from collections.abc import Callable
8
8
 
9
9
  from job_shop_lib import Operation
10
10
  from job_shop_lib.dispatching import Dispatcher
11
11
 
12
12
 
13
- def create_composite_pruning_function(
14
- pruning_functions: Iterable[
15
- Callable[[Dispatcher, list[Operation]], list[Operation]]
16
- ],
17
- ) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
18
- """Creates and returns a composite pruning strategy function based on the
19
- specified list of pruning strategies.
20
- The composite pruning strategy function filters out operations based on
21
- the specified list of pruning strategies.
22
- Args:
23
- pruning_strategies:
24
- A list of pruning strategies to be used. Supported values are
25
- 'dominated_operations' and 'non_immediate_machines'.
26
- Returns:
27
- A function that takes a Dispatcher instance and a list of Operation
28
- instances as input and returns a list of Operation instances based on
29
- the specified list of pruning strategies.
30
- Raises:
31
- ValueError: If any of the pruning strategies in the list are not
32
- recognized or are not supported.
33
- """
34
-
35
- def composite_pruning_function(
36
- dispatcher: Dispatcher, operations: list[Operation]
37
- ) -> list[Operation]:
38
- pruned_operations = operations
39
- for pruning_function in pruning_functions:
40
- pruned_operations = pruning_function(dispatcher, pruned_operations)
41
-
42
- return pruned_operations
43
-
44
- return composite_pruning_function
13
+ ReadyOperationsFilter = Callable[
14
+ [Dispatcher, list[Operation]], list[Operation]
15
+ ]
45
16
 
46
17
 
47
- def prune_dominated_operations(
18
+ def filter_dominated_operations(
48
19
  dispatcher: Dispatcher, operations: list[Operation]
49
20
  ) -> list[Operation]:
50
21
  """Filters out all the operations that are dominated.
@@ -69,7 +40,7 @@ def prune_dominated_operations(
69
40
  return non_dominated_operations
70
41
 
71
42
 
72
- def prune_non_immediate_machines(
43
+ def filter_non_immediate_machines(
73
44
  dispatcher: Dispatcher, operations: list[Operation]
74
45
  ) -> list[Operation]:
75
46
  """Filters out all the operations associated with machines which earliest