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,201 @@
1
+ """Home of the `DispatchingRuleSolver` class."""
2
+
3
+ from typing import Optional, Union
4
+ from collections.abc import Callable, Iterable
5
+
6
+ from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
7
+ from job_shop_lib.dispatching import (
8
+ ready_operations_filter_factory,
9
+ Dispatcher,
10
+ ReadyOperationsFilterType,
11
+ ReadyOperationsFilter,
12
+ create_composite_operation_filter,
13
+ )
14
+ from job_shop_lib.dispatching.rules import (
15
+ dispatching_rule_factory,
16
+ machine_chooser_factory,
17
+ DispatchingRuleType,
18
+ MachineChooserType,
19
+ )
20
+
21
+
22
+ class DispatchingRuleSolver(BaseSolver):
23
+ """Solves a job shop instance using a dispatching rule.
24
+
25
+ Attributes:
26
+ dispatching_rule:
27
+ The dispatching rule to use. It is a callable that takes a
28
+ dispatcher and returns the operation to be dispatched next.
29
+ machine_chooser:
30
+ Used to choose the machine where the operation will be dispatched
31
+ to. It is only used if the operation can be dispatched to multiple
32
+ machines.
33
+ ready_operations_filter:
34
+ The ready operations filter to use. It is used to initialize the
35
+ dispatcher object internally when calling the solve method.
36
+
37
+ Args:
38
+ dispatching_rule:
39
+ The dispatching rule to use. It can be a string with the name
40
+ of the dispatching rule, a :class:`DispatchingRuleType` member,
41
+ or a callable that takes a dispatcher and returns the operation to
42
+ be dispatched next.
43
+ machine_chooser:
44
+ The machine chooser to use. It can be a string with the name
45
+ of the machine chooser, a :class:`MachineChooserType` member, or a
46
+ callable that takes a dispatcher and an operation and returns
47
+ the machine id where the operation will be dispatched.
48
+ ready_operations_filter:
49
+ The ready operations filter to use. It can be either:
50
+
51
+ - a string with the name of the pruning function
52
+ - a :class`ReadyOperationsFilterType` enum member.
53
+ - a callable that takes a dispatcher and a list of operations
54
+ and returns a list of operations that should be considered
55
+ for dispatching,
56
+ - a list with names or actual ready operations filters to be used.
57
+ If a list is provided, a composite filter will be created
58
+ using the specified filters.
59
+
60
+ .. seealso::
61
+ - :func:`job_shop_lib.dispatching.rules.dispatching_rule_factory`
62
+ - :func:`job_shop_lib.dispatching.rules.machine_chooser_factory`
63
+ - :func:`~job_shop_lib.dispatching.ready_operations_filter_factory`
64
+ - :func:`~job_shop_lib.dispatching.create_composite_operation_filter`
65
+ """
66
+
67
+ def __init__(
68
+ self,
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
+ ] = (
87
+ ReadyOperationsFilterType.DOMINATED_OPERATIONS,
88
+ ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS,
89
+ ),
90
+ ):
91
+ if isinstance(dispatching_rule, str):
92
+ dispatching_rule = dispatching_rule_factory(dispatching_rule)
93
+ if isinstance(machine_chooser, str):
94
+ machine_chooser = machine_chooser_factory(machine_chooser)
95
+ if isinstance(ready_operations_filter, str):
96
+ ready_operations_filter = ready_operations_filter_factory(
97
+ ready_operations_filter
98
+ )
99
+ if isinstance(ready_operations_filter, Iterable):
100
+ ready_operations_filter = create_composite_operation_filter(
101
+ ready_operations_filter
102
+ )
103
+
104
+ self.dispatching_rule = dispatching_rule
105
+ self.machine_chooser = machine_chooser
106
+ self.ready_operations_filter = ready_operations_filter
107
+
108
+ def solve(
109
+ self,
110
+ instance: JobShopInstance,
111
+ dispatcher: Optional[Dispatcher] = None,
112
+ ) -> Schedule:
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
+ """
137
+ if dispatcher is None:
138
+ dispatcher = Dispatcher(
139
+ instance, ready_operations_filter=self.ready_operations_filter
140
+ )
141
+ while not dispatcher.schedule.is_complete():
142
+ self.step(dispatcher)
143
+
144
+ return dispatcher.schedule
145
+
146
+ def step(self, dispatcher: Dispatcher) -> None:
147
+ """Executes one step of the dispatching rule algorithm.
148
+
149
+ Args:
150
+ dispatcher:
151
+ The dispatcher object that will be used to dispatch the
152
+ operations.
153
+ """
154
+ selected_operation = self.dispatching_rule(dispatcher)
155
+ machine_id = self.machine_chooser(dispatcher, selected_operation)
156
+ dispatcher.dispatch(selected_operation, machine_id)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ import time
161
+ import cProfile
162
+ # import pstats
163
+ # from io import StringIO
164
+ from job_shop_lib.benchmarking import (
165
+ # load_benchmark_instance,
166
+ load_all_benchmark_instances,
167
+ )
168
+
169
+ # from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
170
+ # most_work_remaining_rule_2,
171
+ # )
172
+
173
+ # ta_instances = [
174
+ # load_benchmark_instance(f"ta{i:02d}") for i in range(1, 81)
175
+ # ]
176
+ ta_instances = load_all_benchmark_instances().values()
177
+ solver = DispatchingRuleSolver(
178
+ dispatching_rule="most_work_remaining", ready_operations_filter=None
179
+ )
180
+
181
+ start = time.perf_counter()
182
+
183
+ # Create a Profile object
184
+ profiler = cProfile.Profile()
185
+
186
+ # Run the code under profiling
187
+ # profiler.enable()
188
+ for instance_ in ta_instances:
189
+ solver.solve(instance_)
190
+ # profiler.disable()
191
+
192
+ end = time.perf_counter()
193
+
194
+ # Print elapsed time
195
+ print(f"Elapsed time: {end - start:.2f} seconds.")
196
+
197
+ # Print profiling results
198
+ # s = StringIO()
199
+ # ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
200
+ # profiler.print_stats("cumtime") # Print top 20 time-consuming functions
201
+ # print(s.getvalue())
@@ -6,11 +6,17 @@ which operations are selected for execution based on certain criteria such as
6
6
  shortest processing time, first come first served, etc.
7
7
  """
8
8
 
9
- from typing import Callable
9
+ from typing import List, Optional
10
+ from collections.abc import Callable, Sequence
10
11
  import random
11
12
 
12
13
  from job_shop_lib import Operation
13
- from job_shop_lib.dispatching import Dispatcher
14
+ from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
15
+ from job_shop_lib.dispatching.feature_observers import (
16
+ DurationObserver,
17
+ FeatureType,
18
+ IsReadyObserver,
19
+ )
14
20
 
15
21
 
16
22
  def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
@@ -32,7 +38,7 @@ def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
32
38
  def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
33
39
  """Dispatches the operation which job has the most remaining work."""
34
40
  job_remaining_work = [0] * dispatcher.instance.num_jobs
35
- for operation in dispatcher.uncompleted_operations():
41
+ for operation in dispatcher.unscheduled_operations():
36
42
  job_remaining_work[operation.job_id] += operation.duration
37
43
 
38
44
  return max(
@@ -59,7 +65,7 @@ def random_operation_rule(dispatcher: Dispatcher) -> Operation:
59
65
 
60
66
 
61
67
  def score_based_rule(
62
- score_function: Callable[[Dispatcher], list[int]]
68
+ score_function: Callable[[Dispatcher], Sequence[float]]
63
69
  ) -> Callable[[Dispatcher], Operation]:
64
70
  """Creates a dispatching rule based on a scoring function.
65
71
 
@@ -83,7 +89,7 @@ def score_based_rule(
83
89
 
84
90
 
85
91
  def score_based_rule_with_tie_breaker(
86
- score_functions: list[Callable[[Dispatcher], list[int]]],
92
+ score_functions: List[Callable[[Dispatcher], Sequence[int]]],
87
93
  ) -> Callable[[Dispatcher], Operation]:
88
94
  """Creates a dispatching rule based on multiple scoring functions.
89
95
 
@@ -117,7 +123,7 @@ def score_based_rule_with_tie_breaker(
117
123
  # -----------------
118
124
 
119
125
 
120
- def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
126
+ def shortest_processing_time_score(dispatcher: Dispatcher) -> List[int]:
121
127
  """Scores each job based on the duration of the next operation."""
122
128
  num_jobs = dispatcher.instance.num_jobs
123
129
  scores = [0] * num_jobs
@@ -126,7 +132,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
126
132
  return scores
127
133
 
128
134
 
129
- def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
135
+ def first_come_first_served_score(dispatcher: Dispatcher) -> List[int]:
130
136
  """Scores each job based on the position of the next operation."""
131
137
  num_jobs = dispatcher.instance.num_jobs
132
138
  scores = [0] * num_jobs
@@ -135,16 +141,64 @@ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
135
141
  return scores
136
142
 
137
143
 
138
- def most_work_remaining_score(dispatcher: Dispatcher) -> list[int]:
139
- """Scores each job based on the remaining work in the job."""
140
- num_jobs = dispatcher.instance.num_jobs
141
- scores = [0] * num_jobs
142
- for operation in dispatcher.uncompleted_operations():
143
- scores[operation.job_id] += operation.duration
144
- return scores
144
+ class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
145
+ """Scores each job based on the remaining work in the job.
146
+
147
+ This class is conceptually a function: it can be called with a
148
+ :class:`~job_shop_lib.dispatching.Dispatcher` instance as input, and it
149
+ returns a list of scores for each job. The reason for using a class instead
150
+ of a function is to cache the observers that are created for each
151
+ dispatcher instance. This way, the observers do not have to be retrieved
152
+ every time the function is called.
153
+
154
+ """
155
+
156
+ def __init__(self) -> None:
157
+ self._duration_observer: Optional[DurationObserver] = None
158
+ self._is_ready_observer: Optional[IsReadyObserver] = None
159
+ self._current_dispatcher: Optional[Dispatcher] = None
160
+
161
+ def __call__(self, dispatcher: Dispatcher) -> Sequence[int]:
162
+ """Scores each job based on the remaining work in the job."""
163
+
164
+ if self._current_dispatcher is not dispatcher:
165
+ self._duration_observer = None
166
+ self._is_ready_observer = None
167
+ self._current_dispatcher = dispatcher
168
+
169
+ def has_job_feature(observer: DispatcherObserver) -> bool:
170
+ if not isinstance(observer, DurationObserver):
171
+ return False
172
+ return FeatureType.JOBS in observer.features
173
+
174
+ if self._duration_observer is None:
175
+ self._duration_observer = dispatcher.create_or_get_observer(
176
+ DurationObserver,
177
+ condition=has_job_feature,
178
+ feature_types=FeatureType.JOBS,
179
+ )
180
+ if self._is_ready_observer is None:
181
+ self._is_ready_observer = dispatcher.create_or_get_observer(
182
+ IsReadyObserver,
183
+ condition=has_job_feature,
184
+ feature_types=FeatureType.JOBS,
185
+ )
186
+
187
+ work_remaining = self._duration_observer.features[
188
+ FeatureType.JOBS
189
+ ].copy()
190
+ is_ready = self._is_ready_observer.features[FeatureType.JOBS]
191
+ work_remaining[~is_ready.astype(bool)] = 0
192
+
193
+ return work_remaining.ravel() # type: ignore[return-value]
194
+
195
+
196
+ observer_based_most_work_remaining_rule = score_based_rule(
197
+ MostWorkRemainingScorer()
198
+ )
145
199
 
146
200
 
147
- def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
201
+ def most_operations_remaining_score(dispatcher: Dispatcher) -> List[int]:
148
202
  """Scores each job based on the remaining operations in the job."""
149
203
  num_jobs = dispatcher.instance.num_jobs
150
204
  scores = [0] * num_jobs
@@ -153,7 +207,7 @@ def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
153
207
  return scores
154
208
 
155
209
 
156
- def random_score(dispatcher: Dispatcher) -> list[int]:
210
+ def random_score(dispatcher: Dispatcher) -> List[int]:
157
211
  """Scores each job randomly."""
158
212
  return [
159
213
  random.randint(0, 100) for _ in range(dispatcher.instance.num_jobs)
@@ -0,0 +1,71 @@
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 Union, Dict
9
+ from enum import Enum
10
+ from collections.abc import Callable
11
+ import random
12
+
13
+ from job_shop_lib import Operation
14
+ from job_shop_lib.exceptions import ValidationError
15
+ from job_shop_lib.dispatching import Dispatcher
16
+
17
+
18
+ class MachineChooserType(str, Enum):
19
+ """Enumeration of machine chooser strategies for the job shop scheduling"""
20
+
21
+ FIRST = "first"
22
+ RANDOM = "random"
23
+
24
+
25
+ MachineChooser = Callable[[Dispatcher, Operation], int]
26
+
27
+
28
+ def machine_chooser_factory(
29
+ machine_chooser: Union[str, MachineChooser],
30
+ ) -> MachineChooser:
31
+ """Creates and returns a machine chooser function based on the specified
32
+ machine chooser strategy name.
33
+
34
+ The machine chooser function determines which machine an operation should
35
+ be assigned to for execution. The selection can be based on different
36
+ strategies such as choosing the first available machine or selecting a
37
+ machine randomly.
38
+
39
+ Args:
40
+ machine_chooser: The name of the machine chooser strategy to be
41
+ used. Supported values are 'first' and 'random'.
42
+
43
+ Returns:
44
+ A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
45
+ and an :class:`~job_shop_lib.Operation` as input
46
+ and returns the index of the selected machine based on the specified
47
+ machine chooser strategy.
48
+
49
+ Raises:
50
+ ValidationError:
51
+ If the ``machine_chooser`` argument is not recognized or
52
+ is not supported.
53
+ """
54
+ machine_choosers: Dict[str, Callable[[Dispatcher, Operation], int]] = {
55
+ MachineChooserType.FIRST: lambda _, operation: operation.machines[0],
56
+ MachineChooserType.RANDOM: lambda _, operation: random.choice(
57
+ operation.machines
58
+ ),
59
+ }
60
+
61
+ if callable(machine_chooser):
62
+ return machine_chooser
63
+
64
+ machine_chooser = machine_chooser.lower()
65
+ if machine_chooser not in machine_choosers:
66
+ raise ValidationError(
67
+ f"Machine chooser {machine_chooser} not recognized. Available "
68
+ f"machine choosers: {', '.join(machine_choosers)}."
69
+ )
70
+
71
+ return machine_choosers[machine_chooser]
@@ -0,0 +1,128 @@
1
+ """Utility functions."""
2
+
3
+ from typing import Union, List
4
+ import time
5
+ from collections.abc import Callable
6
+ import pandas as pd
7
+ from job_shop_lib import JobShopInstance, Operation
8
+ from job_shop_lib.exceptions import JobShopLibError
9
+ from job_shop_lib.dispatching.rules import DispatchingRuleSolver
10
+ from job_shop_lib.dispatching import Dispatcher
11
+
12
+
13
+ def benchmark_dispatching_rules(
14
+ dispatching_rules: Union[
15
+ List[Union[str, Callable[[Dispatcher], Operation]]],
16
+ List[str],
17
+ List[Callable[[Dispatcher], Operation]]
18
+ ],
19
+ instances: List[JobShopInstance],
20
+ ) -> pd.DataFrame:
21
+ """Benchmark multiple dispatching rules on multiple JobShopInstances.
22
+
23
+ This function applies each provided dispatching rule to each given
24
+ :class:`JobShopInstance`, measuring the time taken to solve and the
25
+ makespan of the resulting schedule. It returns a DataFrame summarizing
26
+ the results.
27
+
28
+ Args:
29
+ dispatching_rules:
30
+ List of dispatching rules. Each rule can be
31
+ either a string (name of a built-in rule) or a callable
32
+ (custom rule function).
33
+ instances:
34
+ List of :class:`JobShopInstance` objects to be solved.
35
+
36
+ Returns:
37
+ A pandas DataFrame with columns:
38
+ - instance: Name of the :class:`JobShopInstance`.
39
+ - rule: Name of the dispatching rule used.
40
+ - time: Time taken to solve the instance (in seconds).
41
+ - makespan: Makespan of the resulting schedule.
42
+
43
+ Raises:
44
+ Any exception that might occur during the solving process is caught
45
+ and logged, with None values recorded for time and makespan.
46
+
47
+ Example:
48
+ >>> from job_shop_lib.benchmarking import load_benchmark_instance
49
+ >>> instances = [load_benchmark_instance(f"ta{i:02d}")
50
+ ... for i in range(1, 3)]
51
+ >>> rules = ["most_work_remaining", "shortest_processing_time"]
52
+ >>> df = benchmark_dispatching_rules(rules, instances)
53
+ >>> print(df)
54
+ instance rule time makespan
55
+ 0 ta01 shortest_processing_time 0.006492 3439
56
+ 1 ta01 most_work_remaining_rule 0.012608 1583
57
+ 2 ta02 shortest_processing_time 0.006240 2568
58
+ 3 ta02 most_work_remaining_rule 0.012315 1630
59
+
60
+ Note:
61
+ - The function handles errors gracefully, allowing the benchmarking
62
+ process to continue even if solving a particular instance fails.
63
+ - For custom rule functions, the function name is used in the
64
+ 'rule' column of the output DataFrame.
65
+ """
66
+ results = []
67
+
68
+ for instance in instances:
69
+ for rule in dispatching_rules:
70
+ solver = DispatchingRuleSolver(dispatching_rule=rule)
71
+
72
+ start_time = time.perf_counter()
73
+ try:
74
+ schedule = solver.solve(instance)
75
+ solve_time = time.perf_counter() - start_time
76
+ makespan = schedule.makespan()
77
+
78
+ results.append(
79
+ {
80
+ "instance": instance.name,
81
+ "rule": (
82
+ rule if isinstance(rule, str) else rule.__name__
83
+ ),
84
+ "time": solve_time,
85
+ "makespan": makespan,
86
+ }
87
+ )
88
+ except JobShopLibError as e:
89
+ print(f"Error solving {instance.name} with {rule}: {str(e)}")
90
+ results.append(
91
+ {
92
+ "instance": instance.name,
93
+ "rule": (
94
+ rule if isinstance(rule, str) else rule.__name__
95
+ ),
96
+ "time": None,
97
+ "makespan": None,
98
+ }
99
+ )
100
+
101
+ return pd.DataFrame(results)
102
+
103
+
104
+ # Example usage:
105
+ if __name__ == "__main__":
106
+ from job_shop_lib.benchmarking import load_benchmark_instance
107
+ from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
108
+ most_work_remaining_rule,
109
+ )
110
+
111
+ # Load instances
112
+ instances_ = [load_benchmark_instance(f"ta{i:02d}") for i in range(1, 3)]
113
+
114
+ # Define rules
115
+ rules_: List[str | Callable[[Dispatcher], Operation]] = [
116
+ "most_work_remaining",
117
+ "shortest_processing_time",
118
+ most_work_remaining_rule,
119
+ ]
120
+
121
+ # Run benchmark
122
+ df = benchmark_dispatching_rules(rules_, instances_)
123
+
124
+ # Display results
125
+ print(df)
126
+
127
+ # Group results by rule and compute average makespan and time
128
+ print(df.groupby("rule")[["time", "makespan"]].mean())
@@ -24,3 +24,21 @@ class NoSolutionFoundError(JobShopLibError):
24
24
  TypeError, which may indicate a bug in the code or an invalid
25
25
  input, rather than a failure to find a solution.
26
26
  """
27
+
28
+
29
+ class ValidationError(JobShopLibError):
30
+ """Exception raised when a validation check fails.
31
+
32
+ This exception is raised when a validation check fails, indicating
33
+ that the input data is invalid or does not meet the requirements of
34
+ the function or class that is performing the validation.
35
+
36
+ It is useful to distinguish this exception from other exceptions
37
+ that may be raised by a function or class, such as a ValueError or
38
+ a TypeError, which may indicate a bug in the code or an invalid
39
+ input, rather than a validation failure.
40
+ """
41
+
42
+
43
+ class UninitializedAttributeError(JobShopLibError):
44
+ """Exception raised when an attribute is accessed before initialization."""
@@ -0,0 +1,19 @@
1
+ """Package for generating job shop instances."""
2
+
3
+ from job_shop_lib.generation._utils import (
4
+ generate_duration_matrix,
5
+ generate_machine_matrix_with_recirculation,
6
+ generate_machine_matrix_without_recirculation,
7
+ )
8
+ from job_shop_lib.generation._instance_generator import InstanceGenerator
9
+ from job_shop_lib.generation._general_instance_generator import (
10
+ GeneralInstanceGenerator,
11
+ )
12
+
13
+ __all__ = [
14
+ "InstanceGenerator",
15
+ "GeneralInstanceGenerator",
16
+ "generate_duration_matrix",
17
+ "generate_machine_matrix_with_recirculation",
18
+ "generate_machine_matrix_without_recirculation",
19
+ ]