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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {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
|
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.
|
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],
|
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:
|
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) ->
|
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) ->
|
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
|
-
|
139
|
-
"""Scores each job based on the remaining work in the job.
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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) ->
|
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) ->
|
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())
|
job_shop_lib/exceptions.py
CHANGED
@@ -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
|
+
]
|