job-shop-lib 0.1.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 +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
|
+
"""
|