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,135 @@
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 typing import Union
8
+ from enum import Enum
9
+ from collections.abc import Iterable
10
+
11
+ from job_shop_lib import Operation
12
+ from job_shop_lib.exceptions import ValidationError
13
+
14
+ from job_shop_lib.dispatching import (
15
+ Dispatcher,
16
+ filter_dominated_operations,
17
+ filter_non_immediate_machines,
18
+ filter_non_idle_machines,
19
+ filter_non_immediate_operations,
20
+ ReadyOperationsFilter,
21
+ )
22
+
23
+
24
+ class ReadyOperationsFilterType(str, Enum):
25
+ """Enumeration of ready operations filter types.
26
+
27
+ A filter function is used by the
28
+ :class:`~job_shop_lib.dispatching.Dispatcher` class to reduce the
29
+ amount of available operations to choose from.
30
+ """
31
+
32
+ DOMINATED_OPERATIONS = "dominated_operations"
33
+ NON_IMMEDIATE_MACHINES = "non_immediate_machines"
34
+ NON_IDLE_MACHINES = "non_idle_machines"
35
+ NON_IMMEDIATE_OPERATIONS = "non_immediate_operations"
36
+
37
+
38
+ def create_composite_operation_filter(
39
+ ready_operations_filters: Iterable[
40
+ Union[ReadyOperationsFilter, str, ReadyOperationsFilterType]
41
+ ],
42
+ ) -> ReadyOperationsFilter:
43
+ """Creates and returns a :class:`ReadyOperationsFilter` function by
44
+ combining multiple filter strategies.
45
+
46
+ The composite filter function applies multiple filter strategies
47
+ iteratively in the order they are specified in the list.
48
+
49
+ Args:
50
+ ready_operations_filters:
51
+ A list of filter strategies to be used.
52
+ Supported values are 'dominated_operations' and
53
+ 'non_immediate_machines' or any Callable that takes a
54
+ :class:`~job_shop_lib.dispatching.Dispatcher` instance and a list
55
+ of :class:`~job_shop_lib.Operation` instances as input
56
+ and returns a list of :class:`~job_shop_lib.Operation` instances.
57
+
58
+ Returns:
59
+ A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
60
+ instance and a list of :class:`~job_shop_lib.Operation`
61
+ instances as input and returns a list of
62
+ :class:`~job_shop_lib.Operation` instances based on
63
+ the specified list of filter strategies.
64
+
65
+ Raises:
66
+ ValidationError: If any of the filter strategies in the list are not
67
+ recognized or are not supported.
68
+ """
69
+
70
+ filter_functions = [
71
+ ready_operations_filter_factory(name)
72
+ for name in ready_operations_filters
73
+ ]
74
+
75
+ def composite_pruning_function(
76
+ dispatcher: Dispatcher, operations: list[Operation]
77
+ ) -> list[Operation]:
78
+ pruned_operations = operations
79
+ for pruning_function in filter_functions:
80
+ pruned_operations = pruning_function(dispatcher, pruned_operations)
81
+
82
+ return pruned_operations
83
+
84
+ return composite_pruning_function
85
+
86
+
87
+ def ready_operations_filter_factory(
88
+ filter_name: Union[str, ReadyOperationsFilterType, ReadyOperationsFilter]
89
+ ) -> ReadyOperationsFilter:
90
+ """Creates and returns a filter function based on the specified
91
+ filter strategy name.
92
+
93
+ The filter function filters operations based on certain criteria such as
94
+ dominated operations, immediate machine operations, etc.
95
+
96
+ Args:
97
+ filter_name:
98
+ The name of the filter function to be used. See
99
+ :class:`ReadyOperationsFilterType` for supported values.
100
+ Alternatively, a custom filter function can be passed directly.
101
+
102
+ Returns:
103
+ A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
104
+ instance and a list of :class:`~job_shop_lib.Operation`
105
+ instances as input and returns a list of
106
+ :class:`~job_shop_lib.Operation` instances based on
107
+ the specified filter function.
108
+
109
+ Raises:
110
+ ValidationError: If the ``filter_name`` argument is not recognized or
111
+ is not supported.
112
+ """
113
+ if callable(filter_name):
114
+ return filter_name
115
+
116
+ filtering_strategies = {
117
+ ReadyOperationsFilterType.DOMINATED_OPERATIONS: (
118
+ filter_dominated_operations
119
+ ),
120
+ ReadyOperationsFilterType.NON_IMMEDIATE_MACHINES: (
121
+ filter_non_immediate_machines
122
+ ),
123
+ ReadyOperationsFilterType.NON_IDLE_MACHINES: filter_non_idle_machines,
124
+ ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS: (
125
+ filter_non_immediate_operations
126
+ ),
127
+ }
128
+
129
+ if filter_name not in filtering_strategies:
130
+ raise ValidationError(
131
+ f"Unsupported filter function '{filter_name}'. "
132
+ f"Supported values are {', '.join(filtering_strategies.keys())}."
133
+ )
134
+
135
+ return filtering_strategies[filter_name] # type: ignore[index]
@@ -1,17 +1,16 @@
1
- """Home of the `HistoryTracker` class."""
1
+ """Home of the `HistoryObserver` class."""
2
2
 
3
+ from typing import List
3
4
  from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
4
5
  from job_shop_lib import ScheduledOperation
5
6
 
6
7
 
7
- class HistoryTracker(DispatcherObserver):
8
+ class HistoryObserver(DispatcherObserver):
8
9
  """Observer that stores the history of the dispatcher."""
9
10
 
10
- def __init__(self, dispatcher: Dispatcher):
11
- """Initializes the observer with the current state of the
12
- dispatcher."""
13
- super().__init__(dispatcher)
14
- self.history: list[ScheduledOperation] = []
11
+ def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
12
+ super().__init__(dispatcher, subscribe=subscribe)
13
+ self.history: List[ScheduledOperation] = []
15
14
 
16
15
  def update(self, scheduled_operation: ScheduledOperation):
17
16
  self.history.append(scheduled_operation)
@@ -0,0 +1,113 @@
1
+ """Home of the `OptimalOperationsObserver` class."""
2
+
3
+ from typing import List, Set, Dict
4
+ from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
5
+ from job_shop_lib import Schedule, Operation, ScheduledOperation
6
+ from job_shop_lib.exceptions import ValidationError
7
+
8
+
9
+ class OptimalOperationsObserver(DispatcherObserver):
10
+ """Observer that identifies which available operations are optimal based on
11
+ a reference schedule.
12
+
13
+ This observer compares the available operations at each step with a
14
+ reference schedule to determine which operations would lead to the optimal
15
+ solution. It can be used for training purposes or to analyze decision
16
+ making in dispatching algorithms.
17
+
18
+ Attributes:
19
+ optimal_operations: Set of operations that are considered optimal
20
+ based on the reference schedule.
21
+ reference_schedule: The reference schedule used to determine optimal
22
+ operations.
23
+
24
+ Args:
25
+ dispatcher: The dispatcher instance to observe.
26
+ reference_schedule: A complete schedule that represents the optimal
27
+ or reference solution.
28
+ subscribe: If True, automatically subscribes to the dispatcher.
29
+
30
+ Raises:
31
+ ValidationError: If the reference schedule is incomplete or if it
32
+ doesn't match the dispatcher's instance.
33
+ """
34
+
35
+ _is_singleton = False
36
+
37
+ def __init__(
38
+ self,
39
+ dispatcher: Dispatcher,
40
+ reference_schedule: Schedule,
41
+ *,
42
+ subscribe: bool = True,
43
+ ):
44
+ super().__init__(dispatcher, subscribe=subscribe)
45
+
46
+ if not reference_schedule.is_complete():
47
+ raise ValidationError("Reference schedule must be complete.")
48
+
49
+ if reference_schedule.instance != dispatcher.instance:
50
+ raise ValidationError(
51
+ "Reference schedule instance does not match dispatcher "
52
+ "instance."
53
+ )
54
+
55
+ self.reference_schedule = reference_schedule
56
+ self.optimal_available: Set[Operation] = set()
57
+ self._operation_to_scheduled: Dict[Operation, ScheduledOperation] = {}
58
+ self._machine_next_operation_index: List[int] = [0] * len(
59
+ reference_schedule.schedule
60
+ )
61
+
62
+ self._build_operation_mapping()
63
+ self._update_optimal_operations()
64
+
65
+ def _build_operation_mapping(self) -> None:
66
+ """Builds a mapping from operations to their scheduled versions in
67
+ the reference schedule."""
68
+ for machine_schedule in self.reference_schedule.schedule:
69
+ for scheduled_op in machine_schedule:
70
+ self._operation_to_scheduled[scheduled_op.operation] = (
71
+ scheduled_op
72
+ )
73
+
74
+ def _update_optimal_operations(self) -> None:
75
+ """Updates the set of optimal operations based on current state.
76
+
77
+ An operation is considered optimal if it is the next unscheduled
78
+ operation in its machine's sequence according to the reference
79
+ schedule.
80
+ """
81
+ self.optimal_available.clear()
82
+ available_operations = self.dispatcher.available_operations()
83
+
84
+ if not available_operations:
85
+ return
86
+
87
+ for operation in available_operations:
88
+ scheduled_op = self._operation_to_scheduled[operation]
89
+ machine_index = scheduled_op.machine_id
90
+ next_index = self._machine_next_operation_index[machine_index]
91
+
92
+ if (
93
+ scheduled_op
94
+ == self.reference_schedule.schedule[machine_index][next_index]
95
+ ):
96
+ self.optimal_available.add(operation)
97
+
98
+ def update(self, scheduled_operation: ScheduledOperation) -> None:
99
+ """Updates the optimal operations after an operation is scheduled.
100
+
101
+ Args:
102
+ scheduled_operation: The operation that was just scheduled.
103
+ """
104
+ self._machine_next_operation_index[scheduled_operation.machine_id] += 1
105
+ self._update_optimal_operations()
106
+
107
+ def reset(self) -> None:
108
+ """Resets the observer to its initial state."""
109
+ self._machine_next_operation_index = [0] * len(
110
+ self.dispatcher.schedule.schedule
111
+ )
112
+ self.optimal_available.clear()
113
+ self._update_optimal_operations()
@@ -0,0 +1,168 @@
1
+ """Contains functions to prune (filter) operations.
2
+
3
+ This functions are used by the `Dispatcher` class to reduce the
4
+ amount of available operations to choose from.
5
+ """
6
+
7
+ from typing import List, Set
8
+ from collections.abc import Callable
9
+
10
+ from job_shop_lib import Operation
11
+ from job_shop_lib.dispatching import Dispatcher
12
+
13
+
14
+ ReadyOperationsFilter = Callable[
15
+ [Dispatcher, List[Operation]], List[Operation]
16
+ ]
17
+
18
+
19
+ def filter_non_idle_machines(
20
+ dispatcher: Dispatcher, operations: List[Operation]
21
+ ) -> List[Operation]:
22
+ """Filters out all the operations associated with non-idle machines.
23
+
24
+ A machine is considered idle if there are no ongoing operations
25
+ currently scheduled on it. This filter removes operations that are
26
+ associated with machines that are busy (i.e., have at least one
27
+ uncompleted operation).
28
+
29
+ Utilizes :meth:``Dispatcher.ongoing_operations()`` to determine machine
30
+ statuses.
31
+
32
+ Args:
33
+ dispatcher: The dispatcher object.
34
+ operations: The list of operations to filter.
35
+
36
+ Returns:
37
+ The list of operations that are associated with idle machines.
38
+ """
39
+ current_time = dispatcher.min_start_time(operations)
40
+ non_idle_machines = _get_non_idle_machines(dispatcher, current_time)
41
+
42
+ # Filter operations to keep those that are associated with at least one
43
+ # idle machine
44
+ filtered_operations: List[Operation] = []
45
+ for operation in operations:
46
+ if all(
47
+ machine_id in non_idle_machines
48
+ for machine_id in operation.machines
49
+ ):
50
+ continue
51
+ filtered_operations.append(operation)
52
+
53
+ return filtered_operations
54
+
55
+
56
+ def _get_non_idle_machines(
57
+ dispatcher: Dispatcher, current_time: int
58
+ ) -> Set[int]:
59
+ """Returns the set of machine ids that are currently busy (i.e., have at
60
+ least one uncompleted operation)."""
61
+
62
+ non_idle_machines = set()
63
+ for machine_schedule in dispatcher.schedule.schedule:
64
+ for scheduled_operation in reversed(machine_schedule):
65
+ is_completed = scheduled_operation.end_time <= current_time
66
+ if is_completed:
67
+ break
68
+ non_idle_machines.add(scheduled_operation.machine_id)
69
+
70
+ return non_idle_machines
71
+
72
+
73
+ def filter_non_immediate_operations(
74
+ dispatcher: Dispatcher, operations: List[Operation]
75
+ ) -> List[Operation]:
76
+ """Filters out all the operations that can't start immediately.
77
+
78
+ An operation can start immediately if its earliest start time is the
79
+ current time.
80
+
81
+ The current time is determined by the minimum start time of the
82
+ operations.
83
+
84
+ Args:
85
+ dispatcher: The dispatcher object.
86
+ operations: The list of operations to filter.
87
+ """
88
+
89
+ min_start_time = dispatcher.min_start_time(operations)
90
+ immediate_operations: List[Operation] = []
91
+ for operation in operations:
92
+ start_time = dispatcher.earliest_start_time(operation)
93
+ if start_time == min_start_time:
94
+ immediate_operations.append(operation)
95
+
96
+ return immediate_operations
97
+
98
+
99
+ def filter_dominated_operations(
100
+ dispatcher: Dispatcher, operations: List[Operation]
101
+ ) -> List[Operation]:
102
+ """Filters out all the operations that are dominated.
103
+ An operation is dominated if there is another operation that ends before
104
+ it starts on the same machine.
105
+ """
106
+
107
+ min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
108
+
109
+ non_dominated_operations: List[Operation] = []
110
+ for operation in operations:
111
+ # One benchmark instance has an operation with duration 0
112
+ if operation.duration == 0:
113
+ return [operation]
114
+ for machine_id in operation.machines:
115
+ start_time = dispatcher.start_time(operation, machine_id)
116
+ is_dominated = start_time >= min_machine_end_times[machine_id]
117
+ if not is_dominated:
118
+ non_dominated_operations.append(operation)
119
+ break
120
+
121
+ return non_dominated_operations
122
+
123
+
124
+ def filter_non_immediate_machines(
125
+ dispatcher: Dispatcher, operations: List[Operation]
126
+ ) -> List[Operation]:
127
+ """Filters out all the operations associated with machines which earliest
128
+ operation is not the current time."""
129
+
130
+ is_immediate_machine = _get_immediate_machines(dispatcher, operations)
131
+ non_dominated_operations: List[Operation] = []
132
+ for operation in operations:
133
+ if any(
134
+ is_immediate_machine[machine_id]
135
+ for machine_id in operation.machines
136
+ ):
137
+ non_dominated_operations.append(operation)
138
+
139
+ return non_dominated_operations
140
+
141
+
142
+ def _get_min_machine_end_times(
143
+ dispatcher: Dispatcher, available_operations: List[Operation]
144
+ ) -> List[float]:
145
+ end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
146
+ for op in available_operations:
147
+ for machine_id in op.machines:
148
+ start_time = dispatcher.start_time(op, machine_id)
149
+ end_times_per_machine[machine_id] = min(
150
+ end_times_per_machine[machine_id], start_time + op.duration
151
+ )
152
+ return end_times_per_machine
153
+
154
+
155
+ def _get_immediate_machines(
156
+ dispatcher: Dispatcher, available_operations: List[Operation]
157
+ ) -> List[bool]:
158
+ """Returns the machine ids of the machines that have at least one
159
+ operation with the lowest start time (i.e. the start time)."""
160
+ working_machines = [False] * dispatcher.instance.num_machines
161
+ # We can't use the current_time directly because it will cause
162
+ # an infinite loop.
163
+ current_time = dispatcher.min_start_time(available_operations)
164
+ for op in available_operations:
165
+ for machine_id in op.machines:
166
+ if dispatcher.start_time(op, machine_id) == current_time:
167
+ working_machines[machine_id] = True
168
+ return working_machines
@@ -0,0 +1,70 @@
1
+ """Home of the `UnscheduledOperationsObserver` class."""
2
+
3
+ import collections
4
+ from collections.abc import Iterable
5
+ import itertools
6
+ from typing import Deque, List
7
+
8
+ from job_shop_lib import Operation, ScheduledOperation
9
+ from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
10
+
11
+
12
+ class UnscheduledOperationsObserver(DispatcherObserver):
13
+ """Stores the operations that have not been dispatched yet.
14
+
15
+ This observer maintains a list of deques, each containing unscheduled
16
+ operations for a specific job. It provides methods to access and
17
+ manipulate unscheduled operations efficiently.
18
+ """
19
+
20
+ def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
21
+ super().__init__(dispatcher, subscribe=subscribe)
22
+ self.unscheduled_operations_per_job: List[Deque[Operation]] = []
23
+ self.reset()
24
+ # In case the dispatcher has already scheduled some operations,
25
+ # we need to remove them.
26
+ # Note that we don't need to remove the operations in order.
27
+ for scheduled_operation in itertools.chain(
28
+ *self.dispatcher.schedule.schedule
29
+ ):
30
+ self.update(scheduled_operation)
31
+
32
+ @property
33
+ def unscheduled_operations(self) -> Iterable[Operation]:
34
+ """An iterable of all unscheduled operations across all jobs."""
35
+ return itertools.chain(*self.unscheduled_operations_per_job)
36
+
37
+ @property
38
+ def num_unscheduled_operations(self) -> int:
39
+ """The total number of unscheduled operations."""
40
+ total_operations = self.dispatcher.instance.num_operations
41
+ num_scheduled_operations = (
42
+ self.dispatcher.schedule.num_scheduled_operations
43
+ )
44
+ return total_operations - num_scheduled_operations
45
+
46
+ def update(self, scheduled_operation: ScheduledOperation) -> None:
47
+ """Removes a scheduled operation from the unscheduled operations.
48
+
49
+ This method is called by the dispatcher when an operation is
50
+ scheduled. It removes the operation from its job's deque of
51
+ unscheduled operations.
52
+
53
+ Args:
54
+ scheduled_operation:
55
+ The operation that has been scheduled.
56
+ """
57
+ job_id = scheduled_operation.operation.job_id
58
+ job_deque = self.unscheduled_operations_per_job[job_id]
59
+ if job_deque:
60
+ job_deque.popleft()
61
+
62
+ def reset(self) -> None:
63
+ """Resets unscheduled operations to include all operations.
64
+
65
+ This method reinitializes the list of deques with all operations
66
+ from all jobs in the instance.
67
+ """
68
+ self.unscheduled_operations_per_job = [
69
+ collections.deque(job) for job in self.dispatcher.instance.jobs
70
+ ]
@@ -1,16 +1,53 @@
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
1
+ """Contains :class:`FeatureObserver` classes for observing features of the
2
+ dispatcher.
3
+
4
+ .. autosummary::
5
+ :nosignatures:
6
+
7
+ FeatureObserver
8
+ FeatureType
9
+ CompositeFeatureObserver
10
+ EarliestStartTimeObserver
11
+ IsReadyObserver
12
+ DurationObserver
13
+ IsScheduledObserver
14
+ PositionInJobObserver
15
+ RemainingOperationsObserver
16
+ IsCompletedObserver
17
+ FeatureObserverType
18
+ feature_observer_factory
19
+ FeatureObserverConfig
20
+
21
+ A :class:`~job_shop_lib.dispatching.feature_observers.FeatureObserver` is a
22
+ a subclass of :class:`~job_shop_lib.dispatching.DispatcherObserver` that
23
+ observes features related to operations, machines, or jobs in the dispatcher.
24
+
25
+ Attributes are stored in numpy arrays with a shape of (``num_entities``,
26
+ ``feature_size``), where ``num_entities`` is the number of entities being
27
+ observed (e.g., operations, machines, or jobs) and ``feature_size`` is the
28
+ number of values being observed for each entity.
29
+
30
+ The advantage of using arrays is that they can be easily updated in a
31
+ vectorized manner, which is more efficient than updating each attribute
32
+ individually. Furthermore, machine learning models can be trained on these
33
+ arrays to predict the best dispatching decisions.
34
+ """
35
+
36
+ from ._feature_observer import FeatureObserver, FeatureType
37
+ from ._earliest_start_time_observer import EarliestStartTimeObserver
38
+ from ._is_ready_observer import IsReadyObserver
39
+ from ._duration_observer import DurationObserver
40
+ from ._is_scheduled_observer import IsScheduledObserver
41
+ from ._position_in_job_observer import PositionInJobObserver
42
+ from ._remaining_operations_observer import RemainingOperationsObserver
43
+ from ._is_completed_observer import IsCompletedObserver
44
+ from ._factory import (
45
+ FeatureObserverType,
46
+ feature_observer_factory,
47
+ FeatureObserverConfig,
48
+ )
49
+ from ._composite_feature_observer import CompositeFeatureObserver
50
+
14
51
 
15
52
  __all__ = [
16
53
  "FeatureObserver",
@@ -25,4 +62,5 @@ __all__ = [
25
62
  "IsCompletedObserver",
26
63
  "FeatureObserverType",
27
64
  "feature_observer_factory",
65
+ "FeatureObserverConfig",
28
66
  ]