job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. job_shop_lib/__init__.py +1 -1
  2. job_shop_lib/_job_shop_instance.py +34 -29
  3. job_shop_lib/_operation.py +4 -2
  4. job_shop_lib/_schedule.py +11 -11
  5. job_shop_lib/benchmarking/_load_benchmark.py +3 -3
  6. job_shop_lib/constraint_programming/_ortools_solver.py +6 -6
  7. job_shop_lib/dispatching/__init__.py +4 -3
  8. job_shop_lib/dispatching/_dispatcher.py +19 -19
  9. job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
  10. job_shop_lib/dispatching/_factories.py +4 -2
  11. job_shop_lib/dispatching/_history_observer.py +2 -1
  12. job_shop_lib/dispatching/_optimal_operations_observer.py +115 -0
  13. job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
  14. job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
  15. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
  16. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
  17. job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
  18. job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
  19. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
  20. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
  21. job_shop_lib/dispatching/rules/__init__.py +37 -1
  22. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
  23. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +50 -20
  24. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
  25. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
  26. job_shop_lib/dispatching/rules/_utils.py +9 -8
  27. job_shop_lib/generation/__init__.py +8 -0
  28. job_shop_lib/generation/_general_instance_generator.py +42 -64
  29. job_shop_lib/generation/_instance_generator.py +11 -7
  30. job_shop_lib/generation/_transformations.py +5 -4
  31. job_shop_lib/generation/_utils.py +124 -0
  32. job_shop_lib/graphs/__init__.py +7 -7
  33. job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
  34. job_shop_lib/graphs/_job_shop_graph.py +17 -13
  35. job_shop_lib/graphs/_node.py +6 -4
  36. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
  37. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
  38. job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
  39. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
  40. job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
  41. job_shop_lib/reinforcement_learning/_utils.py +3 -3
  42. job_shop_lib/visualization/__init__.py +0 -60
  43. job_shop_lib/visualization/gantt/__init__.py +48 -0
  44. job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
  45. job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
  46. job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
  47. job_shop_lib/visualization/graphs/__init__.py +29 -0
  48. job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
  49. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  50. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/METADATA +21 -15
  51. job_shop_lib-1.0.0b2.dist-info/RECORD +70 -0
  52. job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
  53. job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
  54. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/LICENSE +0 -0
  55. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,115 @@
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
+ _operation_to_scheduled: Dictionary mapping operations to their
24
+ scheduled versions in the reference schedule.
25
+
26
+ Args:
27
+ dispatcher: The dispatcher instance to observe.
28
+ reference_schedule: A complete schedule that represents the optimal
29
+ or reference solution.
30
+ subscribe: If True, automatically subscribes to the dispatcher.
31
+
32
+ Raises:
33
+ ValidationError: If the reference schedule is incomplete or if it
34
+ doesn't match the dispatcher's instance.
35
+ """
36
+
37
+ _is_singleton = False
38
+
39
+ def __init__(
40
+ self,
41
+ dispatcher: Dispatcher,
42
+ reference_schedule: Schedule,
43
+ *,
44
+ subscribe: bool = True,
45
+ ):
46
+ super().__init__(dispatcher, subscribe=subscribe)
47
+
48
+ if not reference_schedule.is_complete():
49
+ raise ValidationError("Reference schedule must be complete.")
50
+
51
+ if reference_schedule.instance != dispatcher.instance:
52
+ raise ValidationError(
53
+ "Reference schedule instance does not match dispatcher "
54
+ "instance."
55
+ )
56
+
57
+ self.reference_schedule = reference_schedule
58
+ self.optimal_available: Set[Operation] = set()
59
+ self._operation_to_scheduled: Dict[Operation, ScheduledOperation] = {}
60
+ self._machine_next_operation_index: List[int] = [0] * len(
61
+ reference_schedule.schedule
62
+ )
63
+
64
+ self._build_operation_mapping()
65
+ self._update_optimal_operations()
66
+
67
+ def _build_operation_mapping(self) -> None:
68
+ """Builds a mapping from operations to their scheduled versions in
69
+ the reference schedule."""
70
+ for machine_schedule in self.reference_schedule.schedule:
71
+ for scheduled_op in machine_schedule:
72
+ self._operation_to_scheduled[scheduled_op.operation] = (
73
+ scheduled_op
74
+ )
75
+
76
+ def _update_optimal_operations(self) -> None:
77
+ """Updates the set of optimal operations based on current state.
78
+
79
+ An operation is considered optimal if it is the next unscheduled
80
+ operation in its machine's sequence according to the reference
81
+ schedule.
82
+ """
83
+ self.optimal_available.clear()
84
+ available_operations = self.dispatcher.available_operations()
85
+
86
+ if not available_operations:
87
+ return
88
+
89
+ for operation in available_operations:
90
+ scheduled_op = self._operation_to_scheduled[operation]
91
+ machine_index = scheduled_op.machine_id
92
+ next_index = self._machine_next_operation_index[machine_index]
93
+
94
+ if (
95
+ scheduled_op
96
+ == self.reference_schedule.schedule[machine_index][next_index]
97
+ ):
98
+ self.optimal_available.add(operation)
99
+
100
+ def update(self, scheduled_operation: ScheduledOperation) -> None:
101
+ """Updates the optimal operations after an operation is scheduled.
102
+
103
+ Args:
104
+ scheduled_operation: The operation that was just scheduled.
105
+ """
106
+ self._machine_next_operation_index[scheduled_operation.machine_id] += 1
107
+ self._update_optimal_operations()
108
+
109
+ def reset(self) -> None:
110
+ """Resets the observer to its initial state."""
111
+ self._machine_next_operation_index = [0] * len(
112
+ self.dispatcher.schedule.schedule
113
+ )
114
+ self.optimal_available.clear()
115
+ self._update_optimal_operations()
@@ -4,6 +4,7 @@ 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 typing import List, Set
7
8
  from collections.abc import Callable
8
9
 
9
10
  from job_shop_lib import Operation
@@ -11,13 +12,13 @@ from job_shop_lib.dispatching import Dispatcher
11
12
 
12
13
 
13
14
  ReadyOperationsFilter = Callable[
14
- [Dispatcher, list[Operation]], list[Operation]
15
+ [Dispatcher, List[Operation]], List[Operation]
15
16
  ]
16
17
 
17
18
 
18
19
  def filter_non_idle_machines(
19
- dispatcher: Dispatcher, operations: list[Operation]
20
- ) -> list[Operation]:
20
+ dispatcher: Dispatcher, operations: List[Operation]
21
+ ) -> List[Operation]:
21
22
  """Filters out all the operations associated with non-idle machines.
22
23
 
23
24
  A machine is considered idle if there are no ongoing operations
@@ -40,7 +41,7 @@ def filter_non_idle_machines(
40
41
 
41
42
  # Filter operations to keep those that are associated with at least one
42
43
  # idle machine
43
- filtered_operations: list[Operation] = []
44
+ filtered_operations: List[Operation] = []
44
45
  for operation in operations:
45
46
  if all(
46
47
  machine_id in non_idle_machines
@@ -54,7 +55,7 @@ def filter_non_idle_machines(
54
55
 
55
56
  def _get_non_idle_machines(
56
57
  dispatcher: Dispatcher, current_time: int
57
- ) -> set[int]:
58
+ ) -> Set[int]:
58
59
  """Returns the set of machine ids that are currently busy (i.e., have at
59
60
  least one uncompleted operation)."""
60
61
 
@@ -70,8 +71,8 @@ def _get_non_idle_machines(
70
71
 
71
72
 
72
73
  def filter_non_immediate_operations(
73
- dispatcher: Dispatcher, operations: list[Operation]
74
- ) -> list[Operation]:
74
+ dispatcher: Dispatcher, operations: List[Operation]
75
+ ) -> List[Operation]:
75
76
  """Filters out all the operations that can't start immediately.
76
77
 
77
78
  An operation can start immediately if its earliest start time is the
@@ -86,7 +87,7 @@ def filter_non_immediate_operations(
86
87
  """
87
88
 
88
89
  min_start_time = dispatcher.min_start_time(operations)
89
- immediate_operations: list[Operation] = []
90
+ immediate_operations: List[Operation] = []
90
91
  for operation in operations:
91
92
  start_time = dispatcher.earliest_start_time(operation)
92
93
  if start_time == min_start_time:
@@ -96,8 +97,8 @@ def filter_non_immediate_operations(
96
97
 
97
98
 
98
99
  def filter_dominated_operations(
99
- dispatcher: Dispatcher, operations: list[Operation]
100
- ) -> list[Operation]:
100
+ dispatcher: Dispatcher, operations: List[Operation]
101
+ ) -> List[Operation]:
101
102
  """Filters out all the operations that are dominated.
102
103
  An operation is dominated if there is another operation that ends before
103
104
  it starts on the same machine.
@@ -105,7 +106,7 @@ def filter_dominated_operations(
105
106
 
106
107
  min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
107
108
 
108
- non_dominated_operations: list[Operation] = []
109
+ non_dominated_operations: List[Operation] = []
109
110
  for operation in operations:
110
111
  # One benchmark instance has an operation with duration 0
111
112
  if operation.duration == 0:
@@ -121,13 +122,13 @@ def filter_dominated_operations(
121
122
 
122
123
 
123
124
  def filter_non_immediate_machines(
124
- dispatcher: Dispatcher, operations: list[Operation]
125
- ) -> list[Operation]:
125
+ dispatcher: Dispatcher, operations: List[Operation]
126
+ ) -> List[Operation]:
126
127
  """Filters out all the operations associated with machines which earliest
127
128
  operation is not the current time."""
128
129
 
129
130
  is_immediate_machine = _get_immediate_machines(dispatcher, operations)
130
- non_dominated_operations: list[Operation] = []
131
+ non_dominated_operations: List[Operation] = []
131
132
  for operation in operations:
132
133
  if any(
133
134
  is_immediate_machine[machine_id]
@@ -139,8 +140,8 @@ def filter_non_immediate_machines(
139
140
 
140
141
 
141
142
  def _get_min_machine_end_times(
142
- dispatcher: Dispatcher, available_operations: list[Operation]
143
- ) -> list[int | float]:
143
+ dispatcher: Dispatcher, available_operations: List[Operation]
144
+ ) -> List[float]:
144
145
  end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
145
146
  for op in available_operations:
146
147
  for machine_id in op.machines:
@@ -152,8 +153,8 @@ def _get_min_machine_end_times(
152
153
 
153
154
 
154
155
  def _get_immediate_machines(
155
- self: Dispatcher, available_operations: list[Operation]
156
- ) -> list[bool]:
156
+ self: Dispatcher, available_operations: List[Operation]
157
+ ) -> List[bool]:
157
158
  """Returns the machine ids of the machines that have at least one
158
159
  operation with the lowest start time (i.e. the start time)."""
159
160
  working_machines = [False] * self.instance.num_machines
@@ -3,7 +3,7 @@
3
3
  import collections
4
4
  from collections.abc import Iterable
5
5
  import itertools
6
- from typing import Deque
6
+ from typing import Deque, List
7
7
 
8
8
  from job_shop_lib import Operation, ScheduledOperation
9
9
  from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
@@ -19,7 +19,7 @@ class UnscheduledOperationsObserver(DispatcherObserver):
19
19
 
20
20
  def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
21
21
  super().__init__(dispatcher, subscribe=subscribe)
22
- self.unscheduled_operations_per_job: list[Deque[Operation]] = []
22
+ self.unscheduled_operations_per_job: List[Deque[Operation]] = []
23
23
  self.reset()
24
24
  # In case the dispatcher has already scheduled some operations,
25
25
  # we need to remove them.
@@ -51,7 +51,8 @@ class UnscheduledOperationsObserver(DispatcherObserver):
51
51
  unscheduled operations.
52
52
 
53
53
  Args:
54
- scheduled_operation: The operation that has been scheduled.
54
+ scheduled_operation:
55
+ The operation that has been scheduled.
55
56
  """
56
57
  job_id = scheduled_operation.operation.job_id
57
58
  job_deque = self.unscheduled_operations_per_job[job_id]
@@ -2,12 +2,11 @@
2
2
 
3
3
  from collections import defaultdict
4
4
  from collections.abc import Sequence
5
-
5
+ from typing import List, Dict, Union, Optional
6
6
  # The Self type can be imported directly from Python’s typing module in
7
7
  # version 3.11 and beyond. We use the typing_extensions module to support
8
- # python 3.10.
8
+ # python >=3.8
9
9
  from typing_extensions import Self
10
-
11
10
  import numpy as np
12
11
  from numpy.typing import NDArray
13
12
  import pandas as pd
@@ -77,8 +76,8 @@ class CompositeFeatureObserver(FeatureObserver):
77
76
  dispatcher: Dispatcher,
78
77
  *,
79
78
  subscribe: bool = True,
80
- feature_types: list[FeatureType] | FeatureType | None = None,
81
- feature_observers: list[FeatureObserver] | None = None,
79
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
80
+ feature_observers: Optional[List[FeatureObserver]] = None,
82
81
  ):
83
82
  if feature_observers is None:
84
83
  feature_observers = [
@@ -97,7 +96,7 @@ class CompositeFeatureObserver(FeatureObserver):
97
96
  f"Composite feature types: {feature_types}"
98
97
  )
99
98
  self.feature_observers = feature_observers
100
- self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
99
+ self.column_names: Dict[FeatureType, List[str]] = defaultdict(list)
101
100
  super().__init__(dispatcher, subscribe=subscribe)
102
101
  self._set_column_names()
103
102
 
@@ -129,7 +128,7 @@ class CompositeFeatureObserver(FeatureObserver):
129
128
  return composite_observer
130
129
 
131
130
  @property
132
- def features_as_dataframe(self) -> dict[FeatureType, pd.DataFrame]:
131
+ def features_as_dataframe(self) -> Dict[FeatureType, pd.DataFrame]:
133
132
  """Returns the features as a dictionary of `pd.DataFrame` instances."""
134
133
  return {
135
134
  feature_type: pd.DataFrame(
@@ -139,7 +138,7 @@ class CompositeFeatureObserver(FeatureObserver):
139
138
  }
140
139
 
141
140
  def initialize_features(self):
142
- features: dict[FeatureType, list[NDArray[np.float32]]] = defaultdict(
141
+ features: Dict[FeatureType, List[NDArray[np.float32]]] = defaultdict(
143
142
  list
144
143
  )
145
144
  for observer in self.feature_observers:
@@ -1,5 +1,7 @@
1
1
  """Home of the `EarliestStartTimeObserver` class."""
2
2
 
3
+ from typing import List, Optional, Union
4
+
3
5
  import numpy as np
4
6
  from numpy.typing import NDArray
5
7
 
@@ -75,7 +77,7 @@ class EarliestStartTimeObserver(FeatureObserver):
75
77
  dispatcher: Dispatcher,
76
78
  *,
77
79
  subscribe: bool = True,
78
- feature_types: list[FeatureType] | FeatureType | None = None,
80
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
79
81
  ):
80
82
 
81
83
  # Earliest start times initialization
@@ -1,6 +1,7 @@
1
1
  """Contains factory functions for creating :class:`FeatureObserver`s."""
2
2
 
3
3
  from enum import Enum
4
+ from typing import Union, Type
4
5
 
5
6
  from job_shop_lib.dispatching import DispatcherObserverConfig
6
7
  from job_shop_lib.dispatching.feature_observers import (
@@ -36,22 +37,20 @@ class FeatureObserverType(str, Enum):
36
37
 
37
38
 
38
39
  # FeatureObserverConfig = DispatcherObserverConfig[
39
- # type[FeatureObserver] | FeatureObserverType | str
40
+ # Type[FeatureObserver] | FeatureObserverType | str
40
41
  # ]
41
- FeatureObserverConfig = (
42
- DispatcherObserverConfig[type[FeatureObserver]]
43
- | DispatcherObserverConfig[FeatureObserverType]
44
- | DispatcherObserverConfig[str]
45
- )
42
+ FeatureObserverConfig = DispatcherObserverConfig[
43
+ Union[Type[FeatureObserver], FeatureObserverType, str]
44
+ ]
46
45
 
47
46
 
48
47
  def feature_observer_factory(
49
- feature_observer_type: (
50
- str
51
- | FeatureObserverType
52
- | type[FeatureObserver]
53
- | FeatureObserverConfig
54
- ),
48
+ feature_observer_type: Union[
49
+ str,
50
+ FeatureObserverType,
51
+ Type[FeatureObserver],
52
+ FeatureObserverConfig
53
+ ],
55
54
  **kwargs,
56
55
  ) -> FeatureObserver:
57
56
  """Creates and returns a :class:`FeatureObserver` based on the specified
@@ -73,12 +72,12 @@ def feature_observer_factory(
73
72
  **feature_observer_type.kwargs,
74
73
  **kwargs,
75
74
  )
76
- # if the instance is of type type[FeatureObserver] we can just
75
+ # if the instance is of type Type[FeatureObserver] we can just
77
76
  # call the object constructor with the keyword arguments
78
77
  if isinstance(feature_observer_type, type):
79
78
  return feature_observer_type(**kwargs)
80
79
 
81
- mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
80
+ mapping: dict[FeatureObserverType, Type[FeatureObserver]] = {
82
81
  FeatureObserverType.IS_READY: IsReadyObserver,
83
82
  FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
84
83
  FeatureObserverType.DURATION: DurationObserver,
@@ -1,6 +1,7 @@
1
1
  """Home of the `FeatureObserver` class and `FeatureType` enum."""
2
2
 
3
3
  import enum
4
+ from typing import Optional, Union, Dict, List, Tuple
4
5
 
5
6
  import numpy as np
6
7
 
@@ -64,7 +65,7 @@ class FeatureObserver(DispatcherObserver):
64
65
  """
65
66
 
66
67
  _is_singleton = False
67
- _feature_sizes: dict[FeatureType, int] | int = 1
68
+ _feature_sizes: Union[Dict[FeatureType, int], int] = 1
68
69
  _supported_feature_types = list(FeatureType)
69
70
 
70
71
  __slots__ = {
@@ -84,7 +85,7 @@ class FeatureObserver(DispatcherObserver):
84
85
  dispatcher: Dispatcher,
85
86
  *,
86
87
  subscribe: bool = True,
87
- feature_types: list[FeatureType] | FeatureType | None = None,
88
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
88
89
  ):
89
90
  feature_types = self._get_feature_types_list(feature_types)
90
91
  if isinstance(self._feature_sizes, int):
@@ -116,7 +117,7 @@ class FeatureObserver(DispatcherObserver):
116
117
  self.initialize_features()
117
118
 
118
119
  @property
119
- def feature_sizes(self) -> dict[FeatureType, int]:
120
+ def feature_sizes(self) -> Dict[FeatureType, int]:
120
121
  """Returns the size of the features.
121
122
 
122
123
  The size of the features is the number of values being observed for
@@ -134,12 +135,12 @@ class FeatureObserver(DispatcherObserver):
134
135
  return self._feature_sizes
135
136
 
136
137
  @property
137
- def supported_feature_types(self) -> list[FeatureType]:
138
+ def supported_feature_types(self) -> List[FeatureType]:
138
139
  """Returns the supported feature types."""
139
140
  return self._supported_feature_types
140
141
 
141
142
  @property
142
- def feature_dimensions(self) -> dict[FeatureType, tuple[int, int]]:
143
+ def feature_dimensions(self) -> Dict[FeatureType, Tuple[int, int]]:
143
144
  """A dictionary containing the shape of each :class:`FeatureType`."""
144
145
  feature_dimensions = {}
145
146
  for feature_type, array in self.features.items():
@@ -170,7 +171,7 @@ class FeatureObserver(DispatcherObserver):
170
171
  self.initialize_features()
171
172
 
172
173
  def set_features_to_zero(
173
- self, exclude: FeatureType | list[FeatureType] | None = None
174
+ self, exclude: Optional[Union[FeatureType, List[FeatureType]]] = None
174
175
  ):
175
176
  """Sets all features to zero except for the ones specified in
176
177
  ``exclude``.
@@ -196,8 +197,8 @@ class FeatureObserver(DispatcherObserver):
196
197
 
197
198
  def _get_feature_types_list(
198
199
  self,
199
- feature_types: list[FeatureType] | FeatureType | None,
200
- ) -> list[FeatureType]:
200
+ feature_types: Optional[Union[List[FeatureType], FeatureType]],
201
+ ) -> List[FeatureType]:
201
202
  """Returns a list of feature types.
202
203
 
203
204
  Args:
@@ -1,5 +1,6 @@
1
1
  """Home of the `IsCompletedObserver` class."""
2
2
 
3
+ from typing import Optional, Union, List
3
4
  import numpy as np
4
5
 
5
6
  from job_shop_lib import ScheduledOperation
@@ -52,7 +53,7 @@ class IsCompletedObserver(FeatureObserver):
52
53
  self,
53
54
  dispatcher: Dispatcher,
54
55
  *,
55
- feature_types: list[FeatureType] | FeatureType | None = None,
56
+ feature_types: Optional[Union[List[FeatureType], FeatureType]] = None,
56
57
  subscribe: bool = True,
57
58
  ):
58
59
  feature_types = self._get_feature_types_list(feature_types)
@@ -1,5 +1,7 @@
1
1
  """Home of the `IsReadyObserver` class."""
2
2
 
3
+ from typing import List
4
+
3
5
  from job_shop_lib.dispatching.feature_observers import (
4
6
  FeatureObserver,
5
7
  FeatureType,
@@ -16,7 +18,7 @@ class IsReadyObserver(FeatureObserver):
16
18
  feature_ids = self._get_ready_feature_ids(feature_type)
17
19
  feature[feature_ids, 0] = 1.0
18
20
 
19
- def _get_ready_feature_ids(self, feature_type: FeatureType) -> list[int]:
21
+ def _get_ready_feature_ids(self, feature_type: FeatureType) -> List[int]:
20
22
  if feature_type == FeatureType.OPERATIONS:
21
23
  return self._get_ready_operations()
22
24
  if feature_type == FeatureType.MACHINES:
@@ -28,6 +30,6 @@ class IsReadyObserver(FeatureObserver):
28
30
  def reset(self):
29
31
  self.initialize_features()
30
32
 
31
- def _get_ready_operations(self) -> list[int]:
33
+ def _get_ready_operations(self) -> List[int]:
32
34
  available_operations = self.dispatcher.available_operations()
33
35
  return [operation.operation_id for operation in available_operations]
@@ -1,4 +1,40 @@
1
- """Contains the dispatching rules for the job shop scheduling problem."""
1
+ """Contains the dispatching rules for the job shop scheduling problem.
2
+
3
+ Main objects:
4
+
5
+ .. autosummary::
6
+
7
+ DispatchingRuleSolver
8
+ dispatching_rule_factory
9
+ DispatchingRuleType
10
+ MachineChooserType
11
+ dispatching_rule_factory
12
+ machine_chooser_factory
13
+
14
+ Dispatching rules:
15
+
16
+ .. autosummary::
17
+
18
+ shortest_processing_time_rule
19
+ first_come_first_served_rule
20
+ most_work_remaining_rule
21
+ most_operations_remaining_rule
22
+ random_operation_rule
23
+ score_based_rule
24
+ score_based_rule_with_tie_breaker
25
+ observer_based_most_work_remaining_rule
26
+
27
+ Dispatching rule scorers:
28
+
29
+ .. autosummary::
30
+
31
+ shortest_processing_time_score
32
+ first_come_first_served_score
33
+ MostWorkRemainingScorer
34
+ most_operations_remaining_score
35
+ random_score
36
+
37
+ """
2
38
 
3
39
  from ._dispatching_rules_functions import (
4
40
  shortest_processing_time_rule,
@@ -5,6 +5,8 @@ The factory functions create and return the appropriate functions based on the
5
5
  specified names or enums.
6
6
  """
7
7
 
8
+ from typing import Dict, Union
9
+
8
10
  from enum import Enum
9
11
  from collections.abc import Callable
10
12
 
@@ -31,7 +33,7 @@ class DispatchingRuleType(str, Enum):
31
33
 
32
34
 
33
35
  def dispatching_rule_factory(
34
- dispatching_rule: str | DispatchingRuleType,
36
+ dispatching_rule: Union[str, DispatchingRuleType,]
35
37
  ) -> Callable[[Dispatcher], Operation]:
36
38
  """Creates and returns a dispatching rule function based on the specified
37
39
  dispatching rule name.
@@ -55,7 +57,7 @@ def dispatching_rule_factory(
55
57
  If the dispatching_rule argument is not recognized or it is
56
58
  not supported.
57
59
  """
58
- dispatching_rules: dict[
60
+ dispatching_rules: Dict[
59
61
  DispatchingRuleType,
60
62
  Callable[[Dispatcher], Operation],
61
63
  ] = {
@@ -1,5 +1,6 @@
1
1
  """Home of the `DispatchingRuleSolver` class."""
2
2
 
3
+ from typing import Optional, Union
3
4
  from collections.abc import Callable, Iterable
4
5
 
5
6
  from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
@@ -29,14 +30,14 @@ class DispatchingRuleSolver(BaseSolver):
29
30
  Used to choose the machine where the operation will be dispatched
30
31
  to. It is only used if the operation can be dispatched to multiple
31
32
  machines.
32
- pruning_function:
33
- The pruning function to use. It is used to initialize the
33
+ ready_operations_filter:
34
+ The ready operations filter to use. It is used to initialize the
34
35
  dispatcher object internally when calling the solve method.
35
36
 
36
37
  Args:
37
38
  dispatching_rule:
38
39
  The dispatching rule to use. It can be a string with the name
39
- of the dispatching rule, a class`DispatchingRuleType` enum member,
40
+ of the dispatching rule, a :class:`DispatchingRuleType` member,
40
41
  or a callable that takes a dispatcher and returns the operation to
41
42
  be dispatched next.
42
43
  machine_chooser:
@@ -65,21 +66,26 @@ class DispatchingRuleSolver(BaseSolver):
65
66
 
66
67
  def __init__(
67
68
  self,
68
- dispatching_rule: (
69
- str | Callable[[Dispatcher], Operation]
70
- ) = DispatchingRuleType.MOST_WORK_REMAINING,
71
- machine_chooser: (
72
- str | Callable[[Dispatcher, Operation], int]
73
- ) = MachineChooserType.FIRST,
74
- ready_operations_filter: (
75
- Iterable[ReadyOperationsFilter | str | ReadyOperationsFilterType]
76
- | str
77
- | ReadyOperationsFilterType
78
- | ReadyOperationsFilter
79
- | None
80
- ) = (
69
+ dispatching_rule: Union[
70
+ str, Callable[[Dispatcher], Operation]
71
+ ] = DispatchingRuleType.MOST_WORK_REMAINING,
72
+ machine_chooser: Union[
73
+ str, Callable[[Dispatcher, Operation], int]
74
+ ] = MachineChooserType.FIRST,
75
+ ready_operations_filter: Optional[
76
+ Union[
77
+ Iterable[
78
+ Union[
79
+ ReadyOperationsFilter, str, ReadyOperationsFilterType
80
+ ]
81
+ ],
82
+ str,
83
+ ReadyOperationsFilterType,
84
+ ReadyOperationsFilter,
85
+ ]
86
+ ] = (
81
87
  ReadyOperationsFilterType.DOMINATED_OPERATIONS,
82
- ReadyOperationsFilterType.NON_IDLE_MACHINES,
88
+ ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS,
83
89
  ),
84
90
  ):
85
91
  if isinstance(dispatching_rule, str):
@@ -100,10 +106,34 @@ class DispatchingRuleSolver(BaseSolver):
100
106
  self.ready_operations_filter = ready_operations_filter
101
107
 
102
108
  def solve(
103
- self, instance: JobShopInstance, dispatcher: Dispatcher | None = None
109
+ self,
110
+ instance: JobShopInstance,
111
+ dispatcher: Optional[Dispatcher] = None,
104
112
  ) -> Schedule:
105
- """Returns a schedule for the given job shop instance using the
106
- dispatching rule algorithm."""
113
+ """Solves the instance using the dispatching rule and machine chooser
114
+ algorithms.
115
+
116
+ Args:
117
+ instance:
118
+ The job shop instance to be solved.
119
+ dispatcher:
120
+ The dispatcher object that will be used to dispatch the
121
+ operations. If not provided, a new dispatcher will be created
122
+ using the ready operations filter provided in the constructor.
123
+
124
+ Returns:
125
+ The schedule obtained after solving the instance.
126
+
127
+ .. tip::
128
+ Another way to use the solver is by calling it as a function. This
129
+ will call the ``solve`` method internally and will add metadata to
130
+ the schedule. For example:
131
+
132
+ .. code-block:: python
133
+
134
+ solver = DispatchingRuleSolver()
135
+ schedule = solver(instance)
136
+ """
107
137
  if dispatcher is None:
108
138
  dispatcher = Dispatcher(
109
139
  instance, ready_operations_filter=self.ready_operations_filter