job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0a1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +16 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +9 -4
- job_shop_lib/_operation.py +95 -0
- job_shop_lib/{schedule.py → _schedule.py} +73 -54
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
- 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} +57 -18
- job_shop_lib/dispatching/__init__.py +45 -41
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
- job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
- job_shop_lib/dispatching/_factories.py +125 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
- job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
- job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
- job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
- job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
- job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +51 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
- job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
- job_shop_lib/dispatching/rules/_utils.py +127 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +2 -2
- job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
- job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
- job_shop_lib/graphs/__init__.py +17 -6
- job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
- job_shop_lib/graphs/{node.py → _node.py} +18 -12
- job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/reinforcement_learning/__init__.py +41 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
- job_shop_lib/reinforcement_learning/_utils.py +96 -0
- job_shop_lib/visualization/__init__.py +20 -4
- job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
- job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
- job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/factories.py +0 -206
- 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/generators/__init__.py +0 -8
- job_shop_lib/generators/basic_generator.py +0 -200
- job_shop_lib/generators/transformations.py +0 -164
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib-0.5.1.dist-info/RECORD +0 -52
- /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
- /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
- /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
- /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
- /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
- /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
- /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/WHEEL +0 -0
@@ -6,17 +6,22 @@ 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
|
9
|
+
from collections.abc import Callable, Sequence
|
10
10
|
import random
|
11
11
|
|
12
12
|
from job_shop_lib import Operation
|
13
|
-
from job_shop_lib.dispatching import Dispatcher
|
13
|
+
from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
|
14
|
+
from job_shop_lib.dispatching.feature_observers import (
|
15
|
+
DurationObserver,
|
16
|
+
FeatureType,
|
17
|
+
IsReadyObserver,
|
18
|
+
)
|
14
19
|
|
15
20
|
|
16
21
|
def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
17
22
|
"""Dispatches the operation with the shortest duration."""
|
18
23
|
return min(
|
19
|
-
dispatcher.
|
24
|
+
dispatcher.ready_operations(),
|
20
25
|
key=lambda operation: operation.duration,
|
21
26
|
)
|
22
27
|
|
@@ -24,7 +29,7 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
|
24
29
|
def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
|
25
30
|
"""Dispatches the operation with the lowest position in job."""
|
26
31
|
return min(
|
27
|
-
dispatcher.
|
32
|
+
dispatcher.ready_operations(),
|
28
33
|
key=lambda operation: operation.position_in_job,
|
29
34
|
)
|
30
35
|
|
@@ -32,11 +37,11 @@ def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
|
|
32
37
|
def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
33
38
|
"""Dispatches the operation which job has the most remaining work."""
|
34
39
|
job_remaining_work = [0] * dispatcher.instance.num_jobs
|
35
|
-
for operation in dispatcher.
|
40
|
+
for operation in dispatcher.unscheduled_operations():
|
36
41
|
job_remaining_work[operation.job_id] += operation.duration
|
37
42
|
|
38
43
|
return max(
|
39
|
-
dispatcher.
|
44
|
+
dispatcher.ready_operations(),
|
40
45
|
key=lambda operation: job_remaining_work[operation.job_id],
|
41
46
|
)
|
42
47
|
|
@@ -48,18 +53,18 @@ def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
|
48
53
|
job_remaining_operations[operation.job_id] += 1
|
49
54
|
|
50
55
|
return max(
|
51
|
-
dispatcher.
|
56
|
+
dispatcher.ready_operations(),
|
52
57
|
key=lambda operation: job_remaining_operations[operation.job_id],
|
53
58
|
)
|
54
59
|
|
55
60
|
|
56
61
|
def random_operation_rule(dispatcher: Dispatcher) -> Operation:
|
57
62
|
"""Dispatches a random operation."""
|
58
|
-
return random.choice(dispatcher.
|
63
|
+
return random.choice(dispatcher.ready_operations())
|
59
64
|
|
60
65
|
|
61
66
|
def score_based_rule(
|
62
|
-
score_function: Callable[[Dispatcher],
|
67
|
+
score_function: Callable[[Dispatcher], Sequence[float]]
|
63
68
|
) -> Callable[[Dispatcher], Operation]:
|
64
69
|
"""Creates a dispatching rule based on a scoring function.
|
65
70
|
|
@@ -75,7 +80,7 @@ def score_based_rule(
|
|
75
80
|
def rule(dispatcher: Dispatcher) -> Operation:
|
76
81
|
scores = score_function(dispatcher)
|
77
82
|
return max(
|
78
|
-
dispatcher.
|
83
|
+
dispatcher.ready_operations(),
|
79
84
|
key=lambda operation: scores[operation.job_id],
|
80
85
|
)
|
81
86
|
|
@@ -83,7 +88,7 @@ def score_based_rule(
|
|
83
88
|
|
84
89
|
|
85
90
|
def score_based_rule_with_tie_breaker(
|
86
|
-
score_functions: list[Callable[[Dispatcher],
|
91
|
+
score_functions: list[Callable[[Dispatcher], Sequence[int]]],
|
87
92
|
) -> Callable[[Dispatcher], Operation]:
|
88
93
|
"""Creates a dispatching rule based on multiple scoring functions.
|
89
94
|
|
@@ -97,7 +102,7 @@ def score_based_rule_with_tie_breaker(
|
|
97
102
|
"""
|
98
103
|
|
99
104
|
def rule(dispatcher: Dispatcher) -> Operation:
|
100
|
-
candidates = dispatcher.
|
105
|
+
candidates = dispatcher.ready_operations()
|
101
106
|
for scoring_function in score_functions:
|
102
107
|
scores = scoring_function(dispatcher)
|
103
108
|
best_score = max(scores)
|
@@ -121,7 +126,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
|
121
126
|
"""Scores each job based on the duration of the next operation."""
|
122
127
|
num_jobs = dispatcher.instance.num_jobs
|
123
128
|
scores = [0] * num_jobs
|
124
|
-
for operation in dispatcher.
|
129
|
+
for operation in dispatcher.ready_operations():
|
125
130
|
scores[operation.job_id] = -operation.duration
|
126
131
|
return scores
|
127
132
|
|
@@ -130,18 +135,66 @@ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
|
|
130
135
|
"""Scores each job based on the position of the next operation."""
|
131
136
|
num_jobs = dispatcher.instance.num_jobs
|
132
137
|
scores = [0] * num_jobs
|
133
|
-
for operation in dispatcher.
|
138
|
+
for operation in dispatcher.ready_operations():
|
134
139
|
scores[operation.job_id] = operation.operation_id
|
135
140
|
return scores
|
136
141
|
|
137
142
|
|
138
|
-
|
139
|
-
"""Scores each job based on the remaining work in the job.
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
143
|
+
class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
|
144
|
+
"""Scores each job based on the remaining work in the job.
|
145
|
+
|
146
|
+
This class is conceptually a function: it can be called with a
|
147
|
+
:class:`~job_shop_lib.dispatching.Dispatcher` instance as input, and it
|
148
|
+
returns a list of scores for each job. The reason for using a class instead
|
149
|
+
of a function is to cache the observers that are created for each
|
150
|
+
dispatcher instance. This way, the observers do not have to be retrieved
|
151
|
+
every time the function is called.
|
152
|
+
|
153
|
+
"""
|
154
|
+
|
155
|
+
def __init__(self) -> None:
|
156
|
+
self._duration_observer: DurationObserver | None = None
|
157
|
+
self._is_ready_observer: IsReadyObserver | None = None
|
158
|
+
self._current_dispatcher: Dispatcher | None = None
|
159
|
+
|
160
|
+
def __call__(self, dispatcher: Dispatcher) -> Sequence[int]:
|
161
|
+
"""Scores each job based on the remaining work in the job."""
|
162
|
+
|
163
|
+
if self._current_dispatcher is not dispatcher:
|
164
|
+
self._duration_observer = None
|
165
|
+
self._is_ready_observer = None
|
166
|
+
self._current_dispatcher = dispatcher
|
167
|
+
|
168
|
+
def has_job_feature(observer: DispatcherObserver) -> bool:
|
169
|
+
if not isinstance(observer, DurationObserver):
|
170
|
+
return False
|
171
|
+
return FeatureType.JOBS in observer.features
|
172
|
+
|
173
|
+
if self._duration_observer is None:
|
174
|
+
self._duration_observer = dispatcher.create_or_get_observer(
|
175
|
+
DurationObserver,
|
176
|
+
condition=has_job_feature,
|
177
|
+
feature_types=FeatureType.JOBS,
|
178
|
+
)
|
179
|
+
if self._is_ready_observer is None:
|
180
|
+
self._is_ready_observer = dispatcher.create_or_get_observer(
|
181
|
+
IsReadyObserver,
|
182
|
+
condition=has_job_feature,
|
183
|
+
feature_types=FeatureType.JOBS,
|
184
|
+
)
|
185
|
+
|
186
|
+
work_remaining = self._duration_observer.features[
|
187
|
+
FeatureType.JOBS
|
188
|
+
].copy()
|
189
|
+
is_ready = self._is_ready_observer.features[FeatureType.JOBS]
|
190
|
+
work_remaining[~is_ready.astype(bool)] = 0
|
191
|
+
|
192
|
+
return work_remaining.ravel() # type: ignore[return-value]
|
193
|
+
|
194
|
+
|
195
|
+
observer_based_most_work_remaining_rule = score_based_rule(
|
196
|
+
MostWorkRemainingScorer()
|
197
|
+
)
|
145
198
|
|
146
199
|
|
147
200
|
def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
|
@@ -0,0 +1,69 @@
|
|
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 enum import Enum
|
9
|
+
from collections.abc import Callable
|
10
|
+
import random
|
11
|
+
|
12
|
+
from job_shop_lib import Operation
|
13
|
+
from job_shop_lib.exceptions import ValidationError
|
14
|
+
from job_shop_lib.dispatching import Dispatcher
|
15
|
+
|
16
|
+
|
17
|
+
class MachineChooserType(str, Enum):
|
18
|
+
"""Enumeration of machine chooser strategies for the job shop scheduling"""
|
19
|
+
|
20
|
+
FIRST = "first"
|
21
|
+
RANDOM = "random"
|
22
|
+
|
23
|
+
|
24
|
+
MachineChooser = Callable[[Dispatcher, Operation], int]
|
25
|
+
|
26
|
+
|
27
|
+
def machine_chooser_factory(
|
28
|
+
machine_chooser: str | MachineChooser,
|
29
|
+
) -> MachineChooser:
|
30
|
+
"""Creates and returns a machine chooser function based on the specified
|
31
|
+
machine chooser strategy name.
|
32
|
+
|
33
|
+
The machine chooser function determines which machine an operation should
|
34
|
+
be assigned to for execution. The selection can be based on different
|
35
|
+
strategies such as choosing the first available machine or selecting a
|
36
|
+
machine randomly.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
machine_chooser (str): The name of the machine chooser strategy to be
|
40
|
+
used. Supported values are 'first' and 'random'.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
|
44
|
+
and an :class:`~job_shop_lib.Operation` as input
|
45
|
+
and returns the index of the selected machine based on the specified
|
46
|
+
machine chooser strategy.
|
47
|
+
|
48
|
+
Raises:
|
49
|
+
ValueError: If the machine_chooser argument is not recognized or is
|
50
|
+
not supported.
|
51
|
+
"""
|
52
|
+
machine_choosers: dict[str, Callable[[Dispatcher, Operation], int]] = {
|
53
|
+
MachineChooserType.FIRST: lambda _, operation: operation.machines[0],
|
54
|
+
MachineChooserType.RANDOM: lambda _, operation: random.choice(
|
55
|
+
operation.machines
|
56
|
+
),
|
57
|
+
}
|
58
|
+
|
59
|
+
if callable(machine_chooser):
|
60
|
+
return machine_chooser
|
61
|
+
|
62
|
+
machine_chooser = machine_chooser.lower()
|
63
|
+
if machine_chooser not in machine_choosers:
|
64
|
+
raise ValidationError(
|
65
|
+
f"Machine chooser {machine_chooser} not recognized. Available "
|
66
|
+
f"machine choosers: {', '.join(machine_choosers)}."
|
67
|
+
)
|
68
|
+
|
69
|
+
return machine_choosers[machine_chooser]
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""Utility functions."""
|
2
|
+
|
3
|
+
import time
|
4
|
+
from collections.abc import Callable
|
5
|
+
import pandas as pd
|
6
|
+
from job_shop_lib import JobShopInstance, Operation
|
7
|
+
from job_shop_lib.exceptions import JobShopLibError
|
8
|
+
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
9
|
+
from job_shop_lib.dispatching import Dispatcher
|
10
|
+
|
11
|
+
|
12
|
+
def benchmark_dispatching_rules(
|
13
|
+
dispatching_rules: (
|
14
|
+
list[str | Callable[[Dispatcher], Operation]]
|
15
|
+
| list[str]
|
16
|
+
| list[Callable[[Dispatcher], Operation]]
|
17
|
+
),
|
18
|
+
instances: list[JobShopInstance],
|
19
|
+
) -> pd.DataFrame:
|
20
|
+
"""Benchmark multiple dispatching rules on multiple JobShopInstances.
|
21
|
+
|
22
|
+
This function applies each provided dispatching rule to each given
|
23
|
+
:class:`JobShopInstance`, measuring the time taken to solve and the
|
24
|
+
makespan of the resulting schedule. It returns a DataFrame summarizing
|
25
|
+
the results.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
dispatching_rules:
|
29
|
+
List of dispatching rules. Each rule can be
|
30
|
+
either a string (name of a built-in rule) or a callable
|
31
|
+
(custom rule function).
|
32
|
+
instances:
|
33
|
+
iList of :class:`JobShopInstance` objects to be solved.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
A pandas DataFrame with columns:
|
37
|
+
- instance: Name of the :class:`JobShopInstance`.
|
38
|
+
- rule: Name of the dispatching rule used.
|
39
|
+
- time: Time taken to solve the instance (in seconds).
|
40
|
+
- makespan: Makespan of the resulting schedule.
|
41
|
+
|
42
|
+
Raises:
|
43
|
+
Any exception that might occur during the solving process is caught
|
44
|
+
and logged, with None values recorded for time and makespan.
|
45
|
+
|
46
|
+
Example:
|
47
|
+
>>> from job_shop_lib.benchmarking import load_benchmark_instance
|
48
|
+
>>> instances = [load_benchmark_instance(f"ta{i:02d}")
|
49
|
+
... for i in range(1, 3)]
|
50
|
+
>>> rules = ["most_work_remaining", "shortest_processing_time"]
|
51
|
+
>>> df = benchmark_dispatching_rules(rules, instances)
|
52
|
+
>>> print(df)
|
53
|
+
instance rule time makespan
|
54
|
+
0 ta01 shortest_processing_time 0.006492 3439
|
55
|
+
1 ta01 most_work_remaining_rule 0.012608 1583
|
56
|
+
2 ta02 shortest_processing_time 0.006240 2568
|
57
|
+
3 ta02 most_work_remaining_rule 0.012315 1630
|
58
|
+
|
59
|
+
Note:
|
60
|
+
- The function handles errors gracefully, allowing the benchmarking
|
61
|
+
process to continue even if solving a particular instance fails.
|
62
|
+
- For custom rule functions, the function name is used in the
|
63
|
+
'rule' column of the output DataFrame.
|
64
|
+
"""
|
65
|
+
results = []
|
66
|
+
|
67
|
+
for instance in instances:
|
68
|
+
for rule in dispatching_rules:
|
69
|
+
solver = DispatchingRuleSolver(dispatching_rule=rule)
|
70
|
+
|
71
|
+
start_time = time.perf_counter()
|
72
|
+
try:
|
73
|
+
schedule = solver.solve(instance)
|
74
|
+
solve_time = time.perf_counter() - start_time
|
75
|
+
makespan = schedule.makespan()
|
76
|
+
|
77
|
+
results.append(
|
78
|
+
{
|
79
|
+
"instance": instance.name,
|
80
|
+
"rule": (
|
81
|
+
rule if isinstance(rule, str) else rule.__name__
|
82
|
+
),
|
83
|
+
"time": solve_time,
|
84
|
+
"makespan": makespan,
|
85
|
+
}
|
86
|
+
)
|
87
|
+
except JobShopLibError as e:
|
88
|
+
print(f"Error solving {instance.name} with {rule}: {str(e)}")
|
89
|
+
results.append(
|
90
|
+
{
|
91
|
+
"instance": instance.name,
|
92
|
+
"rule": (
|
93
|
+
rule if isinstance(rule, str) else rule.__name__
|
94
|
+
),
|
95
|
+
"time": None,
|
96
|
+
"makespan": None,
|
97
|
+
}
|
98
|
+
)
|
99
|
+
|
100
|
+
return pd.DataFrame(results)
|
101
|
+
|
102
|
+
|
103
|
+
# Example usage:
|
104
|
+
if __name__ == "__main__":
|
105
|
+
from job_shop_lib.benchmarking import load_benchmark_instance
|
106
|
+
from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
|
107
|
+
most_work_remaining_rule,
|
108
|
+
)
|
109
|
+
|
110
|
+
# Load instances
|
111
|
+
instances_ = [load_benchmark_instance(f"ta{i:02d}") for i in range(1, 3)]
|
112
|
+
|
113
|
+
# Define rules
|
114
|
+
rules_: list[str | Callable[[Dispatcher], Operation]] = [
|
115
|
+
"most_work_remaining",
|
116
|
+
"shortest_processing_time",
|
117
|
+
most_work_remaining_rule,
|
118
|
+
]
|
119
|
+
|
120
|
+
# Run benchmark
|
121
|
+
df = benchmark_dispatching_rules(rules_, instances_)
|
122
|
+
|
123
|
+
# Display results
|
124
|
+
print(df)
|
125
|
+
|
126
|
+
# Group results by rule and compute average makespan and time
|
127
|
+
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."""
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Package for generating job shop instances."""
|
2
2
|
|
3
|
-
from job_shop_lib.generation.
|
4
|
-
from job_shop_lib.generation.
|
3
|
+
from job_shop_lib.generation._instance_generator import InstanceGenerator
|
4
|
+
from job_shop_lib.generation._general_instance_generator import (
|
5
5
|
GeneralInstanceGenerator,
|
6
6
|
)
|
7
7
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import random
|
4
4
|
|
5
5
|
from job_shop_lib import JobShopInstance, Operation
|
6
|
+
from job_shop_lib.exceptions import ValidationError
|
6
7
|
from job_shop_lib.generation import InstanceGenerator
|
7
8
|
|
8
9
|
|
@@ -105,14 +106,32 @@ class GeneralInstanceGenerator(InstanceGenerator):
|
|
105
106
|
if seed is not None:
|
106
107
|
random.seed(seed)
|
107
108
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
109
|
+
def __repr__(self) -> str:
|
110
|
+
return (
|
111
|
+
f"GeneralInstanceGenerator("
|
112
|
+
f"num_jobs_range={self.num_jobs_range}, "
|
113
|
+
f"num_machines_range={self.num_machines_range}, "
|
114
|
+
f"duration_range={self.duration_range})"
|
115
|
+
)
|
111
116
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
117
|
+
def generate(
|
118
|
+
self, num_jobs: int | None = None, num_machines: int | None = None
|
119
|
+
) -> JobShopInstance:
|
120
|
+
if num_jobs is None:
|
121
|
+
num_jobs = random.randint(*self.num_jobs_range)
|
122
|
+
|
123
|
+
if num_machines is None:
|
124
|
+
min_num_machines, max_num_machines = self.num_machines_range
|
125
|
+
if not self.allow_less_jobs_than_machines:
|
126
|
+
min_num_machines = min(num_jobs, max_num_machines)
|
127
|
+
num_machines = random.randint(min_num_machines, max_num_machines)
|
128
|
+
elif (
|
129
|
+
not self.allow_less_jobs_than_machines and num_jobs < num_machines
|
130
|
+
):
|
131
|
+
raise ValidationError(
|
132
|
+
"Theere are fewer jobs than machines, which is not allowed"
|
133
|
+
"when `allow_less_jobs_than_machines` attribute is False."
|
134
|
+
)
|
116
135
|
|
117
136
|
jobs = []
|
118
137
|
available_machines = list(range(num_machines))
|
@@ -4,6 +4,7 @@ import random
|
|
4
4
|
from typing import Iterator
|
5
5
|
|
6
6
|
from job_shop_lib import JobShopInstance
|
7
|
+
from job_shop_lib.exceptions import UninitializedAttributeError
|
7
8
|
|
8
9
|
|
9
10
|
class InstanceGenerator(abc.ABC):
|
@@ -76,8 +77,17 @@ class InstanceGenerator(abc.ABC):
|
|
76
77
|
self._iteration_limit = iteration_limit
|
77
78
|
|
78
79
|
@abc.abstractmethod
|
79
|
-
def generate(
|
80
|
-
|
80
|
+
def generate(
|
81
|
+
self, num_jobs: int | None = None, num_machines: int | None = None
|
82
|
+
) -> JobShopInstance:
|
83
|
+
"""Generates a single job shop instance
|
84
|
+
|
85
|
+
Args:
|
86
|
+
num_jobs: The number of jobs to generate. If None, a random value
|
87
|
+
within the specified range will be used.
|
88
|
+
num_machines: The number of machines to generate. If None, a random
|
89
|
+
value within the specified range will be used.
|
90
|
+
"""
|
81
91
|
|
82
92
|
def _next_name(self) -> str:
|
83
93
|
self._counter += 1
|
@@ -98,7 +108,7 @@ class InstanceGenerator(abc.ABC):
|
|
98
108
|
|
99
109
|
def __len__(self) -> int:
|
100
110
|
if self._iteration_limit is None:
|
101
|
-
raise
|
111
|
+
raise UninitializedAttributeError("Iteration limit is not set.")
|
102
112
|
return self._iteration_limit
|
103
113
|
|
104
114
|
@property
|
job_shop_lib/graphs/__init__.py
CHANGED
@@ -1,16 +1,27 @@
|
|
1
|
-
"""Package for graph related classes and functions.
|
1
|
+
"""Package for graph related classes and functions.
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
.. autosummary::
|
4
|
+
JobShopGraph
|
5
|
+
Node
|
6
|
+
NodeType
|
7
|
+
build_disjunctive_graph
|
8
|
+
build_agent_task_graph
|
9
|
+
build_complete_agent_task_graph
|
10
|
+
build_agent_task_graph_with_jobs
|
11
|
+
|
12
|
+
"""
|
13
|
+
|
14
|
+
from job_shop_lib.graphs._constants import EdgeType, NodeType
|
15
|
+
from job_shop_lib.graphs._node import Node
|
16
|
+
from job_shop_lib.graphs._job_shop_graph import JobShopGraph, NODE_ATTR
|
17
|
+
from job_shop_lib.graphs._build_disjunctive_graph import (
|
7
18
|
build_disjunctive_graph,
|
8
19
|
add_disjunctive_edges,
|
9
20
|
add_conjunctive_edges,
|
10
21
|
add_source_sink_nodes,
|
11
22
|
add_source_sink_edges,
|
12
23
|
)
|
13
|
-
from job_shop_lib.graphs.
|
24
|
+
from job_shop_lib.graphs._build_agent_task_graph import (
|
14
25
|
build_agent_task_graph,
|
15
26
|
build_complete_agent_task_graph,
|
16
27
|
build_agent_task_graph_with_jobs,
|
@@ -3,7 +3,8 @@
|
|
3
3
|
import collections
|
4
4
|
import networkx as nx
|
5
5
|
|
6
|
-
from job_shop_lib import JobShopInstance
|
6
|
+
from job_shop_lib import JobShopInstance
|
7
|
+
from job_shop_lib.exceptions import ValidationError
|
7
8
|
from job_shop_lib.graphs import Node, NodeType
|
8
9
|
|
9
10
|
|
@@ -168,7 +169,7 @@ class JobShopGraph:
|
|
168
169
|
if isinstance(v_of_edge, Node):
|
169
170
|
v_of_edge = v_of_edge.node_id
|
170
171
|
if u_of_edge not in self.graph or v_of_edge not in self.graph:
|
171
|
-
raise
|
172
|
+
raise ValidationError(
|
172
173
|
"`u_of_edge` and `v_of_edge` must be in the graph."
|
173
174
|
)
|
174
175
|
self.graph.add_edge(u_of_edge, v_of_edge, **attr)
|
@@ -200,3 +201,81 @@ class JobShopGraph:
|
|
200
201
|
if isinstance(node, Node):
|
201
202
|
node = node.node_id
|
202
203
|
return self.removed_nodes[node]
|
204
|
+
|
205
|
+
def non_removed_nodes(self) -> list[Node]:
|
206
|
+
"""Returns the nodes that are not removed from the graph."""
|
207
|
+
return [node for node in self._nodes if not self.is_removed(node)]
|
208
|
+
|
209
|
+
def get_machine_node(self, machine_id: int) -> Node:
|
210
|
+
"""Returns the node representing the machine with the given id.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
machine_id: The id of the machine.
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
The node representing the machine with the given id.
|
217
|
+
"""
|
218
|
+
return self.get_node_by_type_and_id(
|
219
|
+
NodeType.MACHINE, machine_id, "machine_id"
|
220
|
+
)
|
221
|
+
|
222
|
+
def get_job_node(self, job_id: int) -> Node:
|
223
|
+
"""Returns the node representing the job with the given id.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
job_id: The id of the job.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
The node representing the job with the given id.
|
230
|
+
"""
|
231
|
+
return self.get_node_by_type_and_id(NodeType.JOB, job_id, "job_id")
|
232
|
+
|
233
|
+
def get_operation_node(self, operation_id: int) -> Node:
|
234
|
+
"""Returns the node representing the operation with the given id.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
operation_id: The id of the operation.
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
The node representing the operation with the given id.
|
241
|
+
"""
|
242
|
+
return self.get_node_by_type_and_id(
|
243
|
+
NodeType.OPERATION, operation_id, "operation.operation_id"
|
244
|
+
)
|
245
|
+
|
246
|
+
def get_node_by_type_and_id(
|
247
|
+
self, node_type: NodeType, node_id: int, id_attr: str
|
248
|
+
) -> Node:
|
249
|
+
"""Generic method to get a node by type and id.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
node_type:
|
253
|
+
The type of the node.
|
254
|
+
node_id:
|
255
|
+
The id of the node.
|
256
|
+
id_attr:
|
257
|
+
The attribute name to compare the id. Can be nested like
|
258
|
+
'operation.operation_id'.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
The node with the given id.
|
262
|
+
"""
|
263
|
+
|
264
|
+
def get_nested_attr(obj, attr_path: str):
|
265
|
+
"""Helper function to get nested attribute."""
|
266
|
+
attrs = attr_path.split(".")
|
267
|
+
for attr in attrs:
|
268
|
+
obj = getattr(obj, attr)
|
269
|
+
return obj
|
270
|
+
|
271
|
+
nodes = self._nodes_by_type[node_type]
|
272
|
+
if node_id < len(nodes):
|
273
|
+
node = nodes[node_id]
|
274
|
+
if get_nested_attr(node, id_attr) == node_id:
|
275
|
+
return node
|
276
|
+
|
277
|
+
for node in nodes:
|
278
|
+
if get_nested_attr(node, id_attr) == node_id:
|
279
|
+
return node
|
280
|
+
|
281
|
+
raise ValidationError(f"No node found with node.{id_attr}={node_id}")
|