job-shop-lib 0.4.0__tar.gz → 0.5.1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/PKG-INFO +9 -5
  2. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/README.md +6 -4
  3. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/dispatcher.py +219 -51
  4. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/__init__.py +28 -0
  5. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +87 -0
  6. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/duration_observer.py +95 -0
  7. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +156 -0
  8. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/factory.py +58 -0
  9. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/feature_observer.py +113 -0
  10. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/is_completed_observer.py +98 -0
  11. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/is_ready_observer.py +40 -0
  12. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/is_scheduled_observer.py +34 -0
  13. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/position_in_job_observer.py +39 -0
  14. job_shop_lib-0.5.1/job_shop_lib/dispatching/feature_observers/remaining_operations_observer.py +54 -0
  15. job_shop_lib-0.5.1/job_shop_lib/generation/__init__.py +11 -0
  16. job_shop_lib-0.5.1/job_shop_lib/generation/general_instance_generator.py +169 -0
  17. job_shop_lib-0.5.1/job_shop_lib/generation/instance_generator.py +122 -0
  18. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/generators/__init__.py +2 -1
  19. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/generators/basic_generator.py +3 -0
  20. job_shop_lib-0.5.1/job_shop_lib/generators/transformations.py +164 -0
  21. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/build_disjunctive_graph.py +20 -0
  22. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/job_shop_instance.py +101 -0
  23. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/create_gif.py +47 -38
  24. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/gantt_chart.py +1 -1
  25. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/pyproject.toml +3 -3
  26. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/LICENSE +0 -0
  27. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/__init__.py +0 -0
  28. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/base_solver.py +0 -0
  29. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/benchmarking/__init__.py +0 -0
  30. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/benchmarking/benchmark_instances.json +0 -0
  31. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/benchmarking/load_benchmark.py +0 -0
  32. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/cp_sat/__init__.py +0 -0
  33. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/cp_sat/ortools_solver.py +0 -0
  34. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/__init__.py +0 -0
  35. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/dispatching_rule_solver.py +0 -0
  36. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/dispatching_rules.py +0 -0
  37. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/factories.py +0 -0
  38. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/history_tracker.py +0 -0
  39. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/dispatching/pruning_functions.py +0 -0
  40. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/exceptions.py +0 -0
  41. {job_shop_lib-0.4.0/job_shop_lib/generators → job_shop_lib-0.5.1/job_shop_lib/generation}/transformations.py +0 -0
  42. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/__init__.py +0 -0
  43. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/build_agent_task_graph.py +0 -0
  44. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/constants.py +0 -0
  45. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/job_shop_graph.py +0 -0
  46. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/graphs/node.py +0 -0
  47. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/operation.py +0 -0
  48. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/schedule.py +0 -0
  49. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/scheduled_operation.py +0 -0
  50. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/__init__.py +0 -0
  51. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/agent_task_graph.py +0 -0
  52. {job_shop_lib-0.4.0 → job_shop_lib-0.5.1}/job_shop_lib/visualization/disjunctive_graph.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
5
5
  License: MIT
6
6
  Author: Pabloo22
@@ -15,7 +15,8 @@ Provides-Extra: pygraphviz
15
15
  Requires-Dist: imageio (>=2,<3)
16
16
  Requires-Dist: matplotlib (>=3,<4)
17
17
  Requires-Dist: networkx (>=3,<4)
18
- Requires-Dist: ortools (>=9,<10)
18
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
19
+ Requires-Dist: ortools (>=9.9,<9.10)
19
20
  Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
20
21
  Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
21
22
  Description-Content-Type: text/markdown
@@ -83,7 +84,7 @@ ft06 = load_benchmark_instance("ft06")
83
84
  ```
84
85
 
85
86
  The module `benchmarks` contains functions to load the instances from the file and return them as `JobShopInstance` objects without having to download them
86
- manually. The instances are stored in [benchmark_instances.json](job_shop_lib/benchmarks/benchmark_instances.json).
87
+ manually.
87
88
 
88
89
  The contributions to this benchmark dataset are as follows:
89
90
 
@@ -171,13 +172,15 @@ class DispatchingRule(str, Enum):
171
172
  We can visualize the solution with a `DispatchingRuleSolver` as a gif:
172
173
 
173
174
  ```python
174
- from job_shop_lib.visualization import create_gif, get_plot_function
175
+ from job_shop_lib.visualization import create_gif, plot_gantt_chart_wrapper
175
176
  from job_shop_lib.dispatching import DispatchingRuleSolver, DispatchingRule
176
177
 
177
178
  plt.style.use("ggplot")
178
179
 
179
180
  mwkr_solver = DispatchingRuleSolver("most_work_remaining")
180
- plot_function = get_plot_function(title="Solution with Most Work Remaining Rule")
181
+ plot_function = plot_gantt_chart_wrapper(
182
+ title="Solution with Most Work Remaining Rule"
183
+ )
181
184
  create_gif(
182
185
  gif_path="ft06_optimized.gif",
183
186
  instance=ft06,
@@ -350,3 +353,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
350
353
  Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
351
354
 
352
355
  - Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
356
+
@@ -61,7 +61,7 @@ ft06 = load_benchmark_instance("ft06")
61
61
  ```
62
62
 
63
63
  The module `benchmarks` contains functions to load the instances from the file and return them as `JobShopInstance` objects without having to download them
64
- manually. The instances are stored in [benchmark_instances.json](job_shop_lib/benchmarks/benchmark_instances.json).
64
+ manually.
65
65
 
66
66
  The contributions to this benchmark dataset are as follows:
67
67
 
@@ -149,13 +149,15 @@ class DispatchingRule(str, Enum):
149
149
  We can visualize the solution with a `DispatchingRuleSolver` as a gif:
150
150
 
151
151
  ```python
152
- from job_shop_lib.visualization import create_gif, get_plot_function
152
+ from job_shop_lib.visualization import create_gif, plot_gantt_chart_wrapper
153
153
  from job_shop_lib.dispatching import DispatchingRuleSolver, DispatchingRule
154
154
 
155
155
  plt.style.use("ggplot")
156
156
 
157
157
  mwkr_solver = DispatchingRuleSolver("most_work_remaining")
158
- plot_function = get_plot_function(title="Solution with Most Work Remaining Rule")
158
+ plot_function = plot_gantt_chart_wrapper(
159
+ title="Solution with Most Work Remaining Rule"
160
+ )
159
161
  create_gif(
160
162
  gif_path="ft06_optimized.gif",
161
163
  instance=ft06,
@@ -327,4 +329,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
327
329
  - E. Taillard, "Benchmarks for basic scheduling problems," European
328
330
  Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
329
331
 
330
- - Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
332
+ - Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
@@ -19,13 +19,45 @@ from job_shop_lib import (
19
19
 
20
20
  # Added here to avoid circular imports
21
21
  class DispatcherObserver(abc.ABC):
22
- """Interface for classes that observe the dispatcher."""
22
+ """Interface for classes that observe th"""
23
+
24
+ def __init__(
25
+ self,
26
+ dispatcher: Dispatcher,
27
+ is_singleton: bool = True,
28
+ subscribe: bool = True,
29
+ ):
30
+ """Initializes the observer with the `Dispatcher` and subscribes to
31
+ it.
32
+
33
+ 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.
42
+ 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.
46
+ """
47
+ if is_singleton and any(
48
+ isinstance(observer, self.__class__)
49
+ for observer in dispatcher.subscribers
50
+ ):
51
+ raise ValueError(
52
+ f"An observer of type {self.__class__.__name__} already "
53
+ "exists in the dispatcher's list of subscribers. If you want "
54
+ "to create multiple instances of this observer, set "
55
+ "`is_singleton` to False."
56
+ )
23
57
 
24
- def __init__(self, dispatcher: Dispatcher):
25
- """Initializes the observer with the dispatcher and subscribes to
26
- it."""
27
58
  self.dispatcher = dispatcher
28
- self.dispatcher.subscribe(self)
59
+ if subscribe:
60
+ self.dispatcher.subscribe(self)
29
61
 
30
62
  @abc.abstractmethod
31
63
  def update(self, scheduled_operation: ScheduledOperation):
@@ -39,7 +71,7 @@ class DispatcherObserver(abc.ABC):
39
71
  return self.__class__.__name__
40
72
 
41
73
  def __repr__(self) -> str:
42
- return str(self)
74
+ return self.__class__.__name__
43
75
 
44
76
 
45
77
  def _dispatcher_cache(method):
@@ -86,8 +118,6 @@ class Dispatcher:
86
118
  pruning_function:
87
119
  A function that filters out operations that are not ready to be
88
120
  scheduled.
89
- subscribers:
90
- A list of observers that are subscribed to the dispatcher.
91
121
  """
92
122
 
93
123
  __slots__ = (
@@ -152,32 +182,6 @@ class Dispatcher:
152
182
  """Returns the next available time for each job."""
153
183
  return self._job_next_available_time
154
184
 
155
- @classmethod
156
- def create_schedule_from_raw_solution(
157
- cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
158
- ) -> Schedule:
159
- """Deprecated method, use `Schedule.from_job_sequences` instead."""
160
- warn(
161
- "Dispatcher.create_schedule_from_raw_solution is deprecated. "
162
- "Use Schedule.from_job_sequences instead. It will be removed in "
163
- "version 1.0.0.",
164
- DeprecationWarning,
165
- )
166
- dispatcher = cls(instance)
167
- dispatcher.reset()
168
- raw_solution_deques = [
169
- deque(operations) for operations in raw_solution
170
- ]
171
- while not dispatcher.schedule.is_complete():
172
- for machine_id, operations in enumerate(raw_solution_deques):
173
- if not operations:
174
- continue
175
- operation = operations[0]
176
- if dispatcher.is_operation_ready(operation):
177
- dispatcher.dispatch(operation, machine_id)
178
- operations.popleft()
179
- return dispatcher.schedule
180
-
181
185
  def subscribe(self, observer: DispatcherObserver):
182
186
  """Subscribes an observer to the dispatcher."""
183
187
  self.subscribers.append(observer)
@@ -304,20 +308,6 @@ class Dispatcher:
304
308
  min_start_time = min(min_start_time, start_time)
305
309
  return int(min_start_time)
306
310
 
307
- @_dispatcher_cache
308
- def uncompleted_operations(self) -> list[Operation]:
309
- """Returns the list of operations that have not been scheduled.
310
-
311
- An operation is uncompleted if it has not been scheduled yet.
312
-
313
- It is more efficient than checking all operations in the instance.
314
- """
315
- uncompleted_operations = []
316
- for job_id, next_position in enumerate(self._job_next_operation_index):
317
- operations = self.instance.jobs[job_id][next_position:]
318
- uncompleted_operations.extend(operations)
319
- return uncompleted_operations
320
-
321
311
  @_dispatcher_cache
322
312
  def available_operations(self) -> list[Operation]:
323
313
  """Returns a list of available operations for processing, optionally
@@ -330,15 +320,22 @@ class Dispatcher:
330
320
  Returns:
331
321
  A list of Operation objects that are available for scheduling.
332
322
  """
333
-
334
- available_operations = self._available_operations()
323
+ available_operations = self.available_operations_without_pruning()
335
324
  if self.pruning_function is not None:
336
325
  available_operations = self.pruning_function(
337
326
  self, available_operations
338
327
  )
339
328
  return available_operations
340
329
 
341
- def _available_operations(self) -> list[Operation]:
330
+ @_dispatcher_cache
331
+ def available_operations_without_pruning(self) -> list[Operation]:
332
+ """Returns a list of available operations for processing without
333
+ applying the pruning function.
334
+
335
+ Returns:
336
+ A list of Operation objects that are available for scheduling
337
+ based on precedence and machine constraints only.
338
+ """
342
339
  available_operations = []
343
340
  for job_id, next_position in enumerate(self._job_next_operation_index):
344
341
  if next_position == len(self.instance.jobs[job_id]):
@@ -346,3 +343,174 @@ class Dispatcher:
346
343
  operation = self.instance.jobs[job_id][next_position]
347
344
  available_operations.append(operation)
348
345
  return available_operations
346
+
347
+ @_dispatcher_cache
348
+ def unscheduled_operations(self) -> list[Operation]:
349
+ """Returns the list of operations that have not been scheduled."""
350
+ unscheduled_operations = []
351
+ for job_id, next_position in enumerate(self._job_next_operation_index):
352
+ operations = self.instance.jobs[job_id][next_position:]
353
+ unscheduled_operations.extend(operations)
354
+ return unscheduled_operations
355
+
356
+ @_dispatcher_cache
357
+ def scheduled_operations(self) -> list[Operation]:
358
+ """Returns the list of operations that have been scheduled."""
359
+ scheduled_operations = []
360
+ for job_id, next_position in enumerate(self._job_next_operation_index):
361
+ operations = self.instance.jobs[job_id][:next_position]
362
+ scheduled_operations.extend(operations)
363
+ return scheduled_operations
364
+
365
+ @_dispatcher_cache
366
+ def available_machines(self) -> list[int]:
367
+ """Returns the list of available machines."""
368
+ available_operations = self.available_operations()
369
+ available_machines = set()
370
+ for operation in available_operations:
371
+ available_machines.update(operation.machines)
372
+ return list(available_machines)
373
+
374
+ @_dispatcher_cache
375
+ def available_jobs(self) -> list[int]:
376
+ """Returns the list of available jobs."""
377
+ available_operations = self.available_operations()
378
+ available_jobs = set(
379
+ operation.job_id for operation in available_operations
380
+ )
381
+ return list(available_jobs)
382
+
383
+ def earliest_start_time(self, operation: Operation) -> int:
384
+ """Calculates the earliest start time for a given operation based on
385
+ machine and job constraints.
386
+
387
+ This method is different from the `start_time` method in that it
388
+ takes into account every machine that can process the operation, not
389
+ just the one that will process it. However, it also assumes that
390
+ the operation is ready to be scheduled in the job in favor of
391
+ performance.
392
+
393
+ Args:
394
+ operation:
395
+ The operation for which to calculate the earliest start time.
396
+
397
+ Returns:
398
+ The earliest start time for the operation.
399
+ """
400
+ machine_earliest_start_time = min(
401
+ self._machine_next_available_time[machine_id]
402
+ for machine_id in operation.machines
403
+ )
404
+ job_start_time = self._job_next_available_time[operation.job_id]
405
+ return max(machine_earliest_start_time, job_start_time)
406
+
407
+ def remaining_duration(
408
+ self, scheduled_operation: ScheduledOperation
409
+ ) -> int:
410
+ """Calculates the remaining duration of a scheduled operation.
411
+
412
+ The method computes the remaining time for an operation to finish,
413
+ based on the maximum of the operation's start time or the current time.
414
+ This helps in determining how much time is left from 'now' until the
415
+ operation is completed.
416
+
417
+ Args:
418
+ scheduled_operation:
419
+ The operation for which to calculate the remaining time.
420
+
421
+ Returns:
422
+ The remaining duration.
423
+ """
424
+ adjusted_start_time = max(
425
+ scheduled_operation.start_time, self.current_time()
426
+ )
427
+ return scheduled_operation.end_time - adjusted_start_time
428
+
429
+ @_dispatcher_cache
430
+ def completed_operations(self) -> set[Operation]:
431
+ """Returns the set of operations that have been completed.
432
+
433
+ This method returns the operations that have been scheduled and the
434
+ current time is greater than or equal to the end time of the operation.
435
+ """
436
+ scheduled_operations = set(self.scheduled_operations())
437
+ ongoing_operations = set(
438
+ map(
439
+ lambda scheduled_op: scheduled_op.operation,
440
+ self.ongoing_operations(),
441
+ )
442
+ )
443
+ completed_operations = scheduled_operations - ongoing_operations
444
+ return completed_operations
445
+
446
+ @_dispatcher_cache
447
+ def uncompleted_operations(self) -> list[Operation]:
448
+ """Returns the list of operations that have not been completed yet.
449
+
450
+ This method checks for operations that either haven't been scheduled
451
+ or have been scheduled but haven't reached their completion time.
452
+
453
+ Note:
454
+ The behavior of this method changed in version 0.5.0. Previously, it
455
+ only returned unscheduled operations. For the old behavior, use the
456
+ `unscheduled_operations` method.
457
+ """
458
+ uncompleted_operations = self.unscheduled_operations()
459
+ uncompleted_operations.extend(
460
+ scheduled_operation.operation
461
+ for scheduled_operation in self.ongoing_operations()
462
+ )
463
+ return uncompleted_operations
464
+
465
+ @_dispatcher_cache
466
+ def ongoing_operations(self) -> list[ScheduledOperation]:
467
+ """Returns the list of operations that are currently being processed.
468
+
469
+ This method returns the operations that have been scheduled and are
470
+ currently being processed by the machines.
471
+ """
472
+ current_time = self.current_time()
473
+ ongoing_operations = []
474
+ for machine_schedule in self.schedule.schedule:
475
+ for scheduled_operation in reversed(machine_schedule):
476
+ is_completed = scheduled_operation.end_time <= current_time
477
+ if is_completed:
478
+ break
479
+ ongoing_operations.append(scheduled_operation)
480
+ return ongoing_operations
481
+
482
+ def is_scheduled(self, operation: Operation) -> bool:
483
+ """Checks if the given operation has been scheduled."""
484
+ job_next_op_idx = self._job_next_operation_index[operation.job_id]
485
+ return operation.position_in_job < job_next_op_idx
486
+
487
+ def is_ongoing(self, scheduled_operation: ScheduledOperation) -> bool:
488
+ """Checks if the given operation is currently being processed."""
489
+ current_time = self.current_time()
490
+ return scheduled_operation.start_time <= current_time
491
+
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
507
+ ]
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,28 @@
1
+ """Contains FeatureObserver classes for observing features of the
2
+ dispatcher."""
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
14
+
15
+ __all__ = [
16
+ "FeatureObserver",
17
+ "FeatureType",
18
+ "CompositeFeatureObserver",
19
+ "EarliestStartTimeObserver",
20
+ "IsReadyObserver",
21
+ "DurationObserver",
22
+ "IsScheduledObserver",
23
+ "PositionInJobObserver",
24
+ "RemainingOperationsObserver",
25
+ "IsCompletedObserver",
26
+ "FeatureObserverType",
27
+ "feature_observer_factory",
28
+ ]
@@ -0,0 +1,87 @@
1
+ """Home of the `CompositeFeatureObserver` class."""
2
+
3
+ from collections import defaultdict
4
+ import numpy as np
5
+ import pandas as pd
6
+
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 CompositeFeatureObserver(FeatureObserver):
15
+ """Aggregates features from other FeatureObserver instances subscribed to
16
+ the same `Dispatcher` by concatenating their feature matrices along the
17
+ first axis (horizontal concatenation).
18
+
19
+ Attributes:
20
+ feature_observers:
21
+ List of `FeatureObserver` instances to aggregate features from.
22
+ column_names:
23
+ Dictionary mapping `FeatureType` to a list of column names for the
24
+ corresponding feature matrix. Column names are generated based on
25
+ the class name of the `FeatureObserver` instance that produced the
26
+ feature.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ dispatcher: Dispatcher,
32
+ feature_observers: list[FeatureObserver] | None = None,
33
+ subscribe: bool = True,
34
+ ):
35
+ if feature_observers is None:
36
+ feature_observers = [
37
+ observer
38
+ for observer in dispatcher.subscribers
39
+ if isinstance(observer, FeatureObserver)
40
+ ]
41
+ self.feature_observers = feature_observers
42
+ self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
43
+ super().__init__(dispatcher, subscribe=subscribe)
44
+ self._set_column_names()
45
+
46
+ @property
47
+ def features_as_dataframe(self) -> dict[FeatureType, pd.DataFrame]:
48
+ """Returns the features as a dictionary of `pd.DataFrame` instances."""
49
+ return {
50
+ feature_type: pd.DataFrame(
51
+ feature_matrix, columns=self.column_names[feature_type]
52
+ )
53
+ for feature_type, feature_matrix in self.features.items()
54
+ }
55
+
56
+ def initialize_features(self):
57
+ features: dict[FeatureType, list[np.ndarray]] = defaultdict(list)
58
+ for observer in self.feature_observers:
59
+ for feature_type, feature_matrix in observer.features.items():
60
+ features[feature_type].append(feature_matrix)
61
+
62
+ self.features = {
63
+ feature_type: np.concatenate(features, axis=1)
64
+ for feature_type, features in features.items()
65
+ }
66
+
67
+ def _set_column_names(self):
68
+ for observer in self.feature_observers:
69
+ for feature_type, feature_matrix in observer.features.items():
70
+ feature_name = observer.__class__.__name__.replace(
71
+ "Observer", ""
72
+ )
73
+ if feature_matrix.shape[1] > 1:
74
+ self.column_names[feature_type] += [
75
+ f"{feature_name}_{i}"
76
+ for i in range(feature_matrix.shape[1])
77
+ ]
78
+ else:
79
+ self.column_names[feature_type].append(feature_name)
80
+
81
+ def __str__(self):
82
+ out = [f"{self.__class__.__name__}:"]
83
+ out.append("-" * (len(out[0]) - 1))
84
+ for feature_type, dataframe in self.features_as_dataframe.items():
85
+ out.append(f"{feature_type.value}:")
86
+ out.append(dataframe.to_string())
87
+ return "\n".join(out)
@@ -0,0 +1,95 @@
1
+ """Home of the `DurationObserver` class."""
2
+
3
+ import numpy as np
4
+
5
+ from job_shop_lib.dispatching import Dispatcher
6
+ from job_shop_lib import ScheduledOperation
7
+ from job_shop_lib.dispatching.feature_observers import (
8
+ FeatureObserver,
9
+ FeatureType,
10
+ )
11
+
12
+
13
+ class DurationObserver(FeatureObserver):
14
+ """Measures the remaining duration of operations, machines, and jobs.
15
+
16
+ The duration of an Operation is:
17
+ - if the operation has not been scheduled, it is the duration of the
18
+ operation.
19
+ - if the operation has been scheduled, it is the remaining duration of
20
+ the operation.
21
+ - 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.
25
+
26
+ The duration of a Machine or Job is the sum of the durations of the
27
+ unscheduled operations that belong to the machine or job.
28
+ """
29
+
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
+ def initialize_features(self):
41
+ mapping = {
42
+ FeatureType.OPERATIONS: self._initialize_operation_durations,
43
+ FeatureType.MACHINES: self._initialize_machine_durations,
44
+ FeatureType.JOBS: self._initialize_job_durations,
45
+ }
46
+ for feature_type in self.features:
47
+ mapping[feature_type]()
48
+
49
+ def update(self, scheduled_operation: ScheduledOperation):
50
+ mapping = {
51
+ FeatureType.OPERATIONS: self._update_operation_durations,
52
+ FeatureType.MACHINES: self._update_machine_durations,
53
+ FeatureType.JOBS: self._update_job_durations,
54
+ }
55
+ for feature_type in self.features:
56
+ mapping[feature_type](scheduled_operation)
57
+
58
+ def _initialize_operation_durations(self):
59
+ duration_matrix = self.dispatcher.instance.durations_matrix_array
60
+ operation_durations = np.array(duration_matrix).reshape(-1, 1)
61
+ # Drop the NaN values
62
+ operation_durations = operation_durations[
63
+ ~np.isnan(operation_durations)
64
+ ].reshape(-1, 1)
65
+ self.features[FeatureType.OPERATIONS] = operation_durations
66
+
67
+ def _initialize_machine_durations(self):
68
+ machine_durations = self.dispatcher.instance.machine_loads
69
+ for machine_id, machine_load in enumerate(machine_durations):
70
+ self.features[FeatureType.MACHINES][machine_id, 0] = machine_load
71
+
72
+ def _initialize_job_durations(self):
73
+ job_durations = self.dispatcher.instance.job_durations
74
+ for job_id, job_duration in enumerate(job_durations):
75
+ self.features[FeatureType.JOBS][job_id, 0] = job_duration
76
+
77
+ def _update_operation_durations(
78
+ self, scheduled_operation: ScheduledOperation
79
+ ):
80
+ operation_id = scheduled_operation.operation.operation_id
81
+ self.features[FeatureType.OPERATIONS][operation_id, 0] = (
82
+ self.dispatcher.remaining_duration(scheduled_operation)
83
+ )
84
+
85
+ def _update_machine_durations(
86
+ self, scheduled_operation: ScheduledOperation
87
+ ):
88
+ machine_id = scheduled_operation.machine_id
89
+ op_duration = scheduled_operation.operation.duration
90
+ self.features[FeatureType.MACHINES][machine_id, 0] -= op_duration
91
+
92
+ def _update_job_durations(self, scheduled_operation: ScheduledOperation):
93
+ operation_duration = scheduled_operation.operation.duration
94
+ job_id = scheduled_operation.job_id
95
+ self.features[FeatureType.JOBS][job_id, 0] -= operation_duration