job-shop-lib 0.1.0__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 +20 -0
- job_shop_lib/base_solver.py +37 -0
- job_shop_lib/benchmarking/__init__.py +78 -0
- job_shop_lib/benchmarking/benchmark_instances.json +1 -0
- job_shop_lib/benchmarking/load_benchmark.py +142 -0
- job_shop_lib/cp_sat/__init__.py +5 -0
- job_shop_lib/cp_sat/ortools_solver.py +201 -0
- job_shop_lib/dispatching/__init__.py +49 -0
- job_shop_lib/dispatching/dispatcher.py +269 -0
- job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
- job_shop_lib/dispatching/dispatching_rules.py +160 -0
- job_shop_lib/dispatching/factories.py +206 -0
- job_shop_lib/dispatching/pruning_functions.py +116 -0
- job_shop_lib/exceptions.py +26 -0
- job_shop_lib/generators/__init__.py +7 -0
- job_shop_lib/generators/basic_generator.py +197 -0
- job_shop_lib/graphs/__init__.py +52 -0
- job_shop_lib/graphs/build_agent_task_graph.py +209 -0
- job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
- job_shop_lib/graphs/constants.py +21 -0
- job_shop_lib/graphs/job_shop_graph.py +159 -0
- job_shop_lib/graphs/node.py +147 -0
- job_shop_lib/job_shop_instance.py +355 -0
- job_shop_lib/operation.py +120 -0
- job_shop_lib/schedule.py +180 -0
- job_shop_lib/scheduled_operation.py +97 -0
- job_shop_lib/visualization/__init__.py +25 -0
- job_shop_lib/visualization/agent_task_graph.py +257 -0
- job_shop_lib/visualization/create_gif.py +191 -0
- job_shop_lib/visualization/disjunctive_graph.py +206 -0
- job_shop_lib/visualization/gantt_chart.py +147 -0
- job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
- job_shop_lib-0.1.0.dist-info/METADATA +363 -0
- job_shop_lib-0.1.0.dist-info/RECORD +35 -0
- job_shop_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
"""Home of the `DispatchingRuleSolver` class."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
|
5
|
+
from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
|
6
|
+
from job_shop_lib.dispatching import (
|
7
|
+
dispatching_rule_factory,
|
8
|
+
machine_chooser_factory,
|
9
|
+
pruning_function_factory,
|
10
|
+
DispatchingRule,
|
11
|
+
MachineChooser,
|
12
|
+
Dispatcher,
|
13
|
+
PruningFunction,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class DispatchingRuleSolver(BaseSolver):
|
18
|
+
"""Solves a job shop instance using a dispatching rule.
|
19
|
+
|
20
|
+
Attributes:
|
21
|
+
dispatching_rule:
|
22
|
+
The dispatching rule to use. It is a callable that takes a
|
23
|
+
dispatcher and returns the operation to be dispatched next.
|
24
|
+
machine_chooser:
|
25
|
+
Used to choose the machine where the operation will be dispatched
|
26
|
+
to. It is only used if the operation can be dispatched to multiple
|
27
|
+
machines.
|
28
|
+
pruning_function:
|
29
|
+
The pruning function to use. It is used to initialize the
|
30
|
+
dispatcher object internally when calling the solve method.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
dispatching_rule: (
|
36
|
+
str | Callable[[Dispatcher], Operation]
|
37
|
+
) = DispatchingRule.MOST_WORK_REMAINING,
|
38
|
+
machine_chooser: (
|
39
|
+
str | Callable[[Dispatcher, Operation], int]
|
40
|
+
) = MachineChooser.FIRST,
|
41
|
+
pruning_function: (
|
42
|
+
str
|
43
|
+
| Callable[[Dispatcher, list[Operation]], list[Operation]]
|
44
|
+
| None
|
45
|
+
) = PruningFunction.DOMINATED_OPERATIONS,
|
46
|
+
):
|
47
|
+
"""Initializes the solver with the given dispatching rule, machine
|
48
|
+
chooser and pruning function.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
dispatching_rule:
|
52
|
+
The dispatching rule to use. It can be a string with the name
|
53
|
+
of the dispatching rule, a DispatchingRule enum member, or a
|
54
|
+
callable that takes a dispatcher and returns the operation to
|
55
|
+
be dispatched next.
|
56
|
+
machine_chooser:
|
57
|
+
The machine chooser to use. It can be a string with the name
|
58
|
+
of the machine chooser, a MachineChooser enum member, or a
|
59
|
+
callable that takes a dispatcher and an operation and returns
|
60
|
+
the machine id where the operation will be dispatched.
|
61
|
+
pruning_function:
|
62
|
+
The pruning function to use. It can be a string with the name
|
63
|
+
of the pruning function, a PruningFunction enum member, or a
|
64
|
+
callable that takes a dispatcher and a list of operations and
|
65
|
+
returns a list of operations that should be considered for
|
66
|
+
dispatching.
|
67
|
+
"""
|
68
|
+
if isinstance(dispatching_rule, str):
|
69
|
+
dispatching_rule = dispatching_rule_factory(dispatching_rule)
|
70
|
+
if isinstance(machine_chooser, str):
|
71
|
+
machine_chooser = machine_chooser_factory(machine_chooser)
|
72
|
+
if isinstance(pruning_function, str):
|
73
|
+
pruning_function = pruning_function_factory(pruning_function)
|
74
|
+
|
75
|
+
self.dispatching_rule = dispatching_rule
|
76
|
+
self.machine_chooser = machine_chooser
|
77
|
+
self.pruning_function = pruning_function
|
78
|
+
|
79
|
+
def solve(self, instance: JobShopInstance) -> Schedule:
|
80
|
+
"""Returns a schedule for the given job shop instance using the
|
81
|
+
dispatching rule algorithm."""
|
82
|
+
dispatcher = Dispatcher(
|
83
|
+
instance, pruning_function=self.pruning_function
|
84
|
+
)
|
85
|
+
while not dispatcher.schedule.is_complete():
|
86
|
+
self.step(dispatcher)
|
87
|
+
|
88
|
+
return dispatcher.schedule
|
89
|
+
|
90
|
+
def step(self, dispatcher: Dispatcher) -> None:
|
91
|
+
"""Executes one step of the dispatching rule algorithm.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
dispatcher:
|
95
|
+
The dispatcher object that will be used to dispatch the
|
96
|
+
operations.
|
97
|
+
"""
|
98
|
+
selected_operation = self.dispatching_rule(dispatcher)
|
99
|
+
machine_id = self.machine_chooser(dispatcher, selected_operation)
|
100
|
+
dispatcher.dispatch(selected_operation, machine_id)
|
101
|
+
|
102
|
+
|
103
|
+
if __name__ == "__main__":
|
104
|
+
import cProfile
|
105
|
+
from job_shop_lib.benchmarking import load_benchmark_instance
|
106
|
+
|
107
|
+
ta_instances = []
|
108
|
+
for i in range(1, 81):
|
109
|
+
ta_instances.append(load_benchmark_instance(f"ta{i:02d}"))
|
110
|
+
solver = DispatchingRuleSolver(dispatching_rule="most_work_remaining")
|
111
|
+
cProfile.run("for instance in ta_instances: solver.solve(instance)")
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""Dispatching rules for the job shop scheduling problem.
|
2
|
+
|
3
|
+
This module contains functions that implement different dispatching rules for
|
4
|
+
the job shop scheduling problem. A dispatching rule determines the order in
|
5
|
+
which operations are selected for execution based on certain criteria such as
|
6
|
+
shortest processing time, first come first served, etc.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Callable
|
10
|
+
import random
|
11
|
+
|
12
|
+
from job_shop_lib import Operation
|
13
|
+
from job_shop_lib.dispatching import Dispatcher
|
14
|
+
|
15
|
+
|
16
|
+
def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
17
|
+
"""Dispatches the operation with the shortest duration."""
|
18
|
+
return min(
|
19
|
+
dispatcher.available_operations(),
|
20
|
+
key=lambda operation: operation.duration,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
|
25
|
+
"""Dispatches the operation with the lowest position in job."""
|
26
|
+
return min(
|
27
|
+
dispatcher.available_operations(),
|
28
|
+
key=lambda operation: operation.position_in_job,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
33
|
+
"""Dispatches the operation which job has the most remaining work."""
|
34
|
+
job_remaining_work = [0] * dispatcher.instance.num_jobs
|
35
|
+
for operation in dispatcher.uncompleted_operations():
|
36
|
+
job_remaining_work[operation.job_id] += operation.duration
|
37
|
+
|
38
|
+
return max(
|
39
|
+
dispatcher.available_operations(),
|
40
|
+
key=lambda operation: job_remaining_work[operation.job_id],
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
45
|
+
"""Dispatches the operation which job has the most remaining operations."""
|
46
|
+
job_remaining_operations = [0] * dispatcher.instance.num_jobs
|
47
|
+
for operation in dispatcher.uncompleted_operations():
|
48
|
+
job_remaining_operations[operation.job_id] += 1
|
49
|
+
|
50
|
+
return max(
|
51
|
+
dispatcher.available_operations(),
|
52
|
+
key=lambda operation: job_remaining_operations[operation.job_id],
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
def random_operation_rule(dispatcher: Dispatcher) -> Operation:
|
57
|
+
"""Dispatches a random operation."""
|
58
|
+
return random.choice(dispatcher.available_operations())
|
59
|
+
|
60
|
+
|
61
|
+
def score_based_rule(
|
62
|
+
score_function: Callable[[Dispatcher], list[int]]
|
63
|
+
) -> Callable[[Dispatcher], Operation]:
|
64
|
+
"""Creates a dispatching rule based on a scoring function.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
score_function: A function that takes a Dispatcher instance as input
|
68
|
+
and returns a list of scores for each job.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
A dispatching rule function that selects the operation with the highest
|
72
|
+
score based on the specified scoring function.
|
73
|
+
"""
|
74
|
+
|
75
|
+
def rule(dispatcher: Dispatcher) -> Operation:
|
76
|
+
scores = score_function(dispatcher)
|
77
|
+
return max(
|
78
|
+
dispatcher.available_operations(),
|
79
|
+
key=lambda operation: scores[operation.job_id],
|
80
|
+
)
|
81
|
+
|
82
|
+
return rule
|
83
|
+
|
84
|
+
|
85
|
+
def score_based_rule_with_tie_breaker(
|
86
|
+
score_functions: list[Callable[[Dispatcher], list[int]]],
|
87
|
+
) -> Callable[[Dispatcher], Operation]:
|
88
|
+
"""Creates a dispatching rule based on multiple scoring functions.
|
89
|
+
|
90
|
+
If there is a tie between two operations based on the first scoring
|
91
|
+
function, the second scoring function is used as a tie breaker. If there is
|
92
|
+
still a tie, the third scoring function is used, and so on.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
score_functions: A list of scoring functions that take a Dispatcher
|
96
|
+
instance as input and return a list of scores for each job.
|
97
|
+
"""
|
98
|
+
|
99
|
+
def rule(dispatcher: Dispatcher) -> Operation:
|
100
|
+
candidates = dispatcher.available_operations()
|
101
|
+
for scoring_function in score_functions:
|
102
|
+
scores = scoring_function(dispatcher)
|
103
|
+
best_score = max(scores)
|
104
|
+
candidates = [
|
105
|
+
operation
|
106
|
+
for operation in candidates
|
107
|
+
if scores[operation.job_id] == best_score
|
108
|
+
]
|
109
|
+
if len(candidates) == 1:
|
110
|
+
return candidates[0]
|
111
|
+
return candidates[0]
|
112
|
+
|
113
|
+
return rule
|
114
|
+
|
115
|
+
|
116
|
+
# SCORING FUNCTIONS
|
117
|
+
# -----------------
|
118
|
+
|
119
|
+
|
120
|
+
def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
121
|
+
"""Scores each job based on the duration of the next operation."""
|
122
|
+
num_jobs = dispatcher.instance.num_jobs
|
123
|
+
scores = [0] * num_jobs
|
124
|
+
for operation in dispatcher.available_operations():
|
125
|
+
scores[operation.job_id] = -operation.duration
|
126
|
+
return scores
|
127
|
+
|
128
|
+
|
129
|
+
def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
|
130
|
+
"""Scores each job based on the position of the next operation."""
|
131
|
+
num_jobs = dispatcher.instance.num_jobs
|
132
|
+
scores = [0] * num_jobs
|
133
|
+
for operation in dispatcher.available_operations():
|
134
|
+
scores[operation.job_id] = operation.operation_id
|
135
|
+
return scores
|
136
|
+
|
137
|
+
|
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
|
145
|
+
|
146
|
+
|
147
|
+
def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
|
148
|
+
"""Scores each job based on the remaining operations in the job."""
|
149
|
+
num_jobs = dispatcher.instance.num_jobs
|
150
|
+
scores = [0] * num_jobs
|
151
|
+
for operation in dispatcher.uncompleted_operations():
|
152
|
+
scores[operation.job_id] += 1
|
153
|
+
return scores
|
154
|
+
|
155
|
+
|
156
|
+
def random_score(dispatcher: Dispatcher) -> list[int]:
|
157
|
+
"""Scores each job randomly."""
|
158
|
+
return [
|
159
|
+
random.randint(0, 100) for _ in range(dispatcher.instance.num_jobs)
|
160
|
+
]
|
@@ -0,0 +1,206 @@
|
|
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
|
+
|
10
|
+
from collections.abc import Callable, Sequence
|
11
|
+
import random
|
12
|
+
|
13
|
+
from job_shop_lib import Operation
|
14
|
+
from job_shop_lib.dispatching import (
|
15
|
+
shortest_processing_time_rule,
|
16
|
+
first_come_first_served_rule,
|
17
|
+
most_work_remaining_rule,
|
18
|
+
most_operations_remaining_rule,
|
19
|
+
random_operation_rule,
|
20
|
+
Dispatcher,
|
21
|
+
prune_dominated_operations,
|
22
|
+
prune_non_immediate_machines,
|
23
|
+
create_composite_pruning_function,
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
class DispatchingRule(str, Enum):
|
28
|
+
"""Enumeration of dispatching rules for the job shop scheduling problem."""
|
29
|
+
|
30
|
+
SHORTEST_PROCESSING_TIME = "shortest_processing_time"
|
31
|
+
FIRST_COME_FIRST_SERVED = "first_come_first_served"
|
32
|
+
MOST_WORK_REMAINING = "most_work_remaining"
|
33
|
+
MOST_OPERATIONS_REMAINING = "most_operations_remaining"
|
34
|
+
RANDOM = "random"
|
35
|
+
|
36
|
+
|
37
|
+
class MachineChooser(str, Enum):
|
38
|
+
"""Enumeration of machine chooser strategies for the job shop scheduling"""
|
39
|
+
|
40
|
+
FIRST = "first"
|
41
|
+
RANDOM = "random"
|
42
|
+
|
43
|
+
|
44
|
+
class PruningFunction(str, Enum):
|
45
|
+
"""Enumeration of pruning functions.
|
46
|
+
|
47
|
+
A pruning function is used by the `Dispatcher` class to reduce the
|
48
|
+
amount of available operations to choose from.
|
49
|
+
"""
|
50
|
+
|
51
|
+
DOMINATED_OPERATIONS = "dominated_operations"
|
52
|
+
NON_IMMEDIATE_MACHINES = "non_immediate_machines"
|
53
|
+
|
54
|
+
|
55
|
+
def dispatching_rule_factory(
|
56
|
+
dispatching_rule: str | DispatchingRule,
|
57
|
+
) -> Callable[[Dispatcher], Operation]:
|
58
|
+
"""Creates and returns a dispatching rule function based on the specified
|
59
|
+
dispatching rule name.
|
60
|
+
|
61
|
+
The dispatching rule function determines the order in which operations are
|
62
|
+
selected for execution based on certain criteria such as shortest
|
63
|
+
processing time, first come first served, etc.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
dispatching_rule: The name of the dispatching rule to be used.
|
67
|
+
Supported values are 'shortest_processing_time',
|
68
|
+
'first_come_first_served', 'most_work_remaining',
|
69
|
+
and 'random'.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
A function that takes a Dispatcher instance as input and returns an
|
73
|
+
Operation based on the specified dispatching rule.
|
74
|
+
|
75
|
+
Raises:
|
76
|
+
ValueError: If the dispatching_rule argument is not recognized or is
|
77
|
+
not supported.
|
78
|
+
"""
|
79
|
+
dispatching_rules = {
|
80
|
+
DispatchingRule.SHORTEST_PROCESSING_TIME: (
|
81
|
+
shortest_processing_time_rule
|
82
|
+
),
|
83
|
+
DispatchingRule.FIRST_COME_FIRST_SERVED: first_come_first_served_rule,
|
84
|
+
DispatchingRule.MOST_WORK_REMAINING: most_work_remaining_rule,
|
85
|
+
DispatchingRule.MOST_OPERATIONS_REMAINING: (
|
86
|
+
most_operations_remaining_rule
|
87
|
+
),
|
88
|
+
DispatchingRule.RANDOM: random_operation_rule,
|
89
|
+
}
|
90
|
+
|
91
|
+
dispatching_rule = dispatching_rule.lower()
|
92
|
+
if dispatching_rule not in dispatching_rules:
|
93
|
+
raise ValueError(
|
94
|
+
f"Dispatching rule {dispatching_rule} not recognized. Available "
|
95
|
+
f"dispatching rules: {', '.join(dispatching_rules)}."
|
96
|
+
)
|
97
|
+
|
98
|
+
return dispatching_rules[dispatching_rule] # type: ignore[index]
|
99
|
+
|
100
|
+
|
101
|
+
def machine_chooser_factory(
|
102
|
+
machine_chooser: str,
|
103
|
+
) -> Callable[[Dispatcher, Operation], int]:
|
104
|
+
"""Creates and returns a machine chooser function based on the specified
|
105
|
+
machine chooser strategy name.
|
106
|
+
|
107
|
+
The machine chooser function determines which machine an operation should
|
108
|
+
be assigned to for execution. The selection can be based on different
|
109
|
+
strategies such as choosing the first available machine or selecting a
|
110
|
+
machine randomly.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
machine_chooser (str): The name of the machine chooser strategy to be
|
114
|
+
used. Supported values are 'first' and 'random'.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
A function that takes a Dispatcher instance and an Operation as input
|
118
|
+
and returns the index of the selected machine based on the specified
|
119
|
+
machine chooser strategy.
|
120
|
+
|
121
|
+
Raises:
|
122
|
+
ValueError: If the machine_chooser argument is not recognized or is
|
123
|
+
not supported.
|
124
|
+
"""
|
125
|
+
machine_choosers: dict[str, Callable[[Dispatcher, Operation], int]] = {
|
126
|
+
MachineChooser.FIRST: lambda _, operation: operation.machines[0],
|
127
|
+
MachineChooser.RANDOM: lambda _, operation: random.choice(
|
128
|
+
operation.machines
|
129
|
+
),
|
130
|
+
}
|
131
|
+
|
132
|
+
machine_chooser = machine_chooser.lower()
|
133
|
+
if machine_chooser not in machine_choosers:
|
134
|
+
raise ValueError(
|
135
|
+
f"Machine chooser {machine_chooser} not recognized. Available "
|
136
|
+
f"machine choosers: {', '.join(machine_choosers)}."
|
137
|
+
)
|
138
|
+
|
139
|
+
return machine_choosers[machine_chooser]
|
140
|
+
|
141
|
+
|
142
|
+
def composite_pruning_function_factory(
|
143
|
+
pruning_function_names: Sequence[str | PruningFunction],
|
144
|
+
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
145
|
+
"""Creates and returns a composite pruning function based on the
|
146
|
+
specified list of pruning strategies.
|
147
|
+
|
148
|
+
The composite pruning function filters out operations based on
|
149
|
+
the specified list of pruning strategies.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
pruning_functions:
|
153
|
+
A list of pruning strategies to be used. Supported values are
|
154
|
+
'dominated_operations' and 'non_immediate_machines'.
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
A function that takes a Dispatcher instance and a list of Operation
|
158
|
+
instances as input and returns a list of Operation instances based on
|
159
|
+
the specified list of pruning strategies.
|
160
|
+
|
161
|
+
Raises:
|
162
|
+
ValueError: If any of the pruning strategies in the list are not
|
163
|
+
recognized or are not supported.
|
164
|
+
"""
|
165
|
+
|
166
|
+
pruning_functions = [
|
167
|
+
pruning_function_factory(name) for name in pruning_function_names
|
168
|
+
]
|
169
|
+
return create_composite_pruning_function(pruning_functions)
|
170
|
+
|
171
|
+
|
172
|
+
def pruning_function_factory(
|
173
|
+
pruning_function_name: str | PruningFunction,
|
174
|
+
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
175
|
+
"""Creates and returns a pruning function based on the specified
|
176
|
+
pruning strategy name.
|
177
|
+
|
178
|
+
The pruning function filters out operations based on certain
|
179
|
+
criteria such as dominated operations, non-immediate machines, etc.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
pruning_function:
|
183
|
+
The name of the pruning function to be used. Supported values are
|
184
|
+
'dominated_operations' and 'non_immediate_machines'.
|
185
|
+
|
186
|
+
Returns:
|
187
|
+
A function that takes a Dispatcher instance and a list of Operation
|
188
|
+
instances as input and returns a list of Operation instances based on
|
189
|
+
the specified pruning function.
|
190
|
+
|
191
|
+
Raises:
|
192
|
+
ValueError: If the pruning_function argument is not recognized or is
|
193
|
+
not supported.
|
194
|
+
"""
|
195
|
+
pruning_strategies = {
|
196
|
+
PruningFunction.DOMINATED_OPERATIONS: prune_dominated_operations,
|
197
|
+
PruningFunction.NON_IMMEDIATE_MACHINES: prune_non_immediate_machines,
|
198
|
+
}
|
199
|
+
|
200
|
+
if pruning_function_name not in pruning_strategies:
|
201
|
+
raise ValueError(
|
202
|
+
f"Unsupported pruning function '{pruning_function_name}'. "
|
203
|
+
f"Supported values are {', '.join(pruning_strategies.keys())}."
|
204
|
+
)
|
205
|
+
|
206
|
+
return pruning_strategies[pruning_function_name] # type: ignore[index]
|
@@ -0,0 +1,116 @@
|
|
1
|
+
"""Contains functions to prune (filter) operations.
|
2
|
+
|
3
|
+
This functions are used by the `Dispatcher` class to reduce the
|
4
|
+
amount of available operations to choose from.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from collections.abc import Callable, Iterable
|
8
|
+
|
9
|
+
from job_shop_lib import Operation
|
10
|
+
from job_shop_lib.dispatching import Dispatcher
|
11
|
+
|
12
|
+
|
13
|
+
def create_composite_pruning_function(
|
14
|
+
pruning_functions: Iterable[
|
15
|
+
Callable[[Dispatcher, list[Operation]], list[Operation]]
|
16
|
+
],
|
17
|
+
) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
|
18
|
+
"""Creates and returns a composite pruning strategy function based on the
|
19
|
+
specified list of pruning strategies.
|
20
|
+
The composite pruning strategy function filters out operations based on
|
21
|
+
the specified list of pruning strategies.
|
22
|
+
Args:
|
23
|
+
pruning_strategies:
|
24
|
+
A list of pruning strategies to be used. Supported values are
|
25
|
+
'dominated_operations' and 'non_immediate_machines'.
|
26
|
+
Returns:
|
27
|
+
A function that takes a Dispatcher instance and a list of Operation
|
28
|
+
instances as input and returns a list of Operation instances based on
|
29
|
+
the specified list of pruning strategies.
|
30
|
+
Raises:
|
31
|
+
ValueError: If any of the pruning strategies in the list are not
|
32
|
+
recognized or are not supported.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def composite_pruning_function(
|
36
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
37
|
+
) -> list[Operation]:
|
38
|
+
pruned_operations = operations
|
39
|
+
for pruning_function in pruning_functions:
|
40
|
+
pruned_operations = pruning_function(dispatcher, pruned_operations)
|
41
|
+
|
42
|
+
return pruned_operations
|
43
|
+
|
44
|
+
return composite_pruning_function
|
45
|
+
|
46
|
+
|
47
|
+
def prune_dominated_operations(
|
48
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
49
|
+
) -> list[Operation]:
|
50
|
+
"""Filters out all the operations that are dominated.
|
51
|
+
An operation is dominated if there is another operation that ends before
|
52
|
+
it starts on the same machine.
|
53
|
+
"""
|
54
|
+
|
55
|
+
min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
|
56
|
+
|
57
|
+
non_dominated_operations: list[Operation] = []
|
58
|
+
for operation in operations:
|
59
|
+
# One benchmark instance has an operation with duration 0
|
60
|
+
if operation.duration == 0:
|
61
|
+
return [operation]
|
62
|
+
for machine_id in operation.machines:
|
63
|
+
start_time = dispatcher.start_time(operation, machine_id)
|
64
|
+
is_dominated = start_time >= min_machine_end_times[machine_id]
|
65
|
+
if not is_dominated:
|
66
|
+
non_dominated_operations.append(operation)
|
67
|
+
break
|
68
|
+
|
69
|
+
return non_dominated_operations
|
70
|
+
|
71
|
+
|
72
|
+
def prune_non_immediate_machines(
|
73
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
74
|
+
) -> list[Operation]:
|
75
|
+
"""Filters out all the operations associated with machines which earliest
|
76
|
+
operation is not the current time."""
|
77
|
+
|
78
|
+
is_immediate_machine = _get_immediate_machines(dispatcher, operations)
|
79
|
+
non_dominated_operations: list[Operation] = []
|
80
|
+
for operation in operations:
|
81
|
+
if any(
|
82
|
+
is_immediate_machine[machine_id]
|
83
|
+
for machine_id in operation.machines
|
84
|
+
):
|
85
|
+
non_dominated_operations.append(operation)
|
86
|
+
|
87
|
+
return non_dominated_operations
|
88
|
+
|
89
|
+
|
90
|
+
def _get_min_machine_end_times(
|
91
|
+
dispatcher: Dispatcher, available_operations: list[Operation]
|
92
|
+
) -> list[int | float]:
|
93
|
+
end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
|
94
|
+
for op in available_operations:
|
95
|
+
for machine_id in op.machines:
|
96
|
+
start_time = dispatcher.start_time(op, machine_id)
|
97
|
+
end_times_per_machine[machine_id] = min(
|
98
|
+
end_times_per_machine[machine_id], start_time + op.duration
|
99
|
+
)
|
100
|
+
return end_times_per_machine
|
101
|
+
|
102
|
+
|
103
|
+
def _get_immediate_machines(
|
104
|
+
self: Dispatcher, available_operations: list[Operation]
|
105
|
+
) -> list[bool]:
|
106
|
+
"""Returns the machine ids of the machines that have at least one
|
107
|
+
operation with the lowest start time (i.e. the start time)."""
|
108
|
+
working_machines = [False] * self.instance.num_machines
|
109
|
+
# We can't use the current_time directly because it will cause
|
110
|
+
# an infinite loop.
|
111
|
+
current_time = self.min_start_time(available_operations)
|
112
|
+
for op in available_operations:
|
113
|
+
for machine_id in op.machines:
|
114
|
+
if self.start_time(op, machine_id) == current_time:
|
115
|
+
working_machines[machine_id] = True
|
116
|
+
return working_machines
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""Exceptions for the job shop scheduling library."""
|
2
|
+
|
3
|
+
|
4
|
+
class JobShopLibError(Exception):
|
5
|
+
"""Base class for exceptions in the job shop scheduling library.
|
6
|
+
|
7
|
+
This class is the base class for all exceptions raised by the job
|
8
|
+
shop scheduling library.
|
9
|
+
|
10
|
+
It is useful for catching any exception that is raised by the
|
11
|
+
library, without having to catch each specific exception
|
12
|
+
separately.
|
13
|
+
"""
|
14
|
+
|
15
|
+
|
16
|
+
class NoSolutionFoundError(JobShopLibError):
|
17
|
+
"""Exception raised when no solution is found by a solver.
|
18
|
+
|
19
|
+
This exception is raised by a solver when it is unable to find a
|
20
|
+
feasible solution within a given time limit.
|
21
|
+
|
22
|
+
It is useful to distinguish this exception from other exceptions
|
23
|
+
that may be raised by a solver, such as a ValueError or a
|
24
|
+
TypeError, which may indicate a bug in the code or an invalid
|
25
|
+
input, rather than a failure to find a solution.
|
26
|
+
"""
|