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
@@ -1,98 +0,0 @@
|
|
1
|
-
"""Home of the `IsCompletedObserver` class."""
|
2
|
-
|
3
|
-
from typing import Iterable
|
4
|
-
|
5
|
-
import numpy as np
|
6
|
-
|
7
|
-
from job_shop_lib import ScheduledOperation
|
8
|
-
from job_shop_lib.dispatching import Dispatcher
|
9
|
-
from job_shop_lib.dispatching.feature_observers import (
|
10
|
-
FeatureObserver,
|
11
|
-
FeatureType,
|
12
|
-
RemainingOperationsObserver,
|
13
|
-
)
|
14
|
-
|
15
|
-
|
16
|
-
class IsCompletedObserver(FeatureObserver):
|
17
|
-
"""Observer that adds a binary feature indicating whether each operation,
|
18
|
-
machine, or job has been completed."""
|
19
|
-
|
20
|
-
def __init__(
|
21
|
-
self,
|
22
|
-
dispatcher: Dispatcher,
|
23
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
24
|
-
):
|
25
|
-
feature_types = self.get_feature_types_list(feature_types)
|
26
|
-
self.remaining_ops_per_machine = np.zeros(
|
27
|
-
(dispatcher.instance.num_machines, 1), dtype=int
|
28
|
-
)
|
29
|
-
self.remaining_ops_per_job = np.zeros(
|
30
|
-
(dispatcher.instance.num_jobs, 1), dtype=int
|
31
|
-
)
|
32
|
-
super().__init__(dispatcher, feature_types, feature_size=1)
|
33
|
-
|
34
|
-
def initialize_features(self):
|
35
|
-
self._initialize_remaining_operations()
|
36
|
-
|
37
|
-
def update(self, scheduled_operation: ScheduledOperation):
|
38
|
-
if FeatureType.OPERATIONS in self.features:
|
39
|
-
# operation_id = scheduled_operation.operation.operation_id
|
40
|
-
# self.features[FeatureType.OPERATIONS][operation_id, 0] = 1
|
41
|
-
completed_operations = [
|
42
|
-
op.operation_id
|
43
|
-
for op in self.dispatcher.completed_operations()
|
44
|
-
]
|
45
|
-
self.features[FeatureType.OPERATIONS][completed_operations, 0] = 1
|
46
|
-
if FeatureType.MACHINES in self.features:
|
47
|
-
machine_id = scheduled_operation.machine_id
|
48
|
-
self.remaining_ops_per_machine[machine_id, 0] -= 1
|
49
|
-
is_completed = self.remaining_ops_per_machine[machine_id, 0] == 0
|
50
|
-
self.features[FeatureType.MACHINES][machine_id, 0] = is_completed
|
51
|
-
if FeatureType.JOBS in self.features:
|
52
|
-
job_id = scheduled_operation.job_id
|
53
|
-
self.remaining_ops_per_job[job_id, 0] -= 1
|
54
|
-
is_completed = self.remaining_ops_per_job[job_id, 0] == 0
|
55
|
-
self.features[FeatureType.JOBS][job_id, 0] = is_completed
|
56
|
-
|
57
|
-
def _initialize_remaining_operations(self):
|
58
|
-
remaining_ops_observer = self._get_remaining_operations_observer(
|
59
|
-
self.dispatcher, self.features
|
60
|
-
)
|
61
|
-
if remaining_ops_observer is not None:
|
62
|
-
if FeatureType.JOBS in self.features:
|
63
|
-
self.remaining_ops_per_job = remaining_ops_observer.features[
|
64
|
-
FeatureType.JOBS
|
65
|
-
].copy()
|
66
|
-
if FeatureType.MACHINES in self.features:
|
67
|
-
self.remaining_ops_per_machine = (
|
68
|
-
remaining_ops_observer.features[
|
69
|
-
FeatureType.MACHINES
|
70
|
-
].copy()
|
71
|
-
)
|
72
|
-
return
|
73
|
-
|
74
|
-
# If there is no remaining operations observer, we need to
|
75
|
-
# compute the remaining operations ourselves.
|
76
|
-
# We iterate over all operations using scheduled_operations
|
77
|
-
# instead of uncompleted_operations, because in this case
|
78
|
-
# they will output the same operations, and the former is slightly
|
79
|
-
# more efficient.
|
80
|
-
for operation in self.dispatcher.unscheduled_operations():
|
81
|
-
if FeatureType.JOBS in self.features:
|
82
|
-
self.remaining_ops_per_job[operation.job_id, 0] += 1
|
83
|
-
if FeatureType.MACHINES in self.features:
|
84
|
-
self.remaining_ops_per_machine[operation.machine_id, 0] += 1
|
85
|
-
|
86
|
-
def _get_remaining_operations_observer(
|
87
|
-
self, dispatcher: Dispatcher, feature_types: Iterable[FeatureType]
|
88
|
-
) -> RemainingOperationsObserver | None:
|
89
|
-
for observer in dispatcher.subscribers:
|
90
|
-
if not isinstance(observer, RemainingOperationsObserver):
|
91
|
-
continue
|
92
|
-
has_same_features = all(
|
93
|
-
feature_type in observer.features
|
94
|
-
for feature_type in feature_types
|
95
|
-
)
|
96
|
-
if has_same_features:
|
97
|
-
return observer
|
98
|
-
return None
|
@@ -1,40 +0,0 @@
|
|
1
|
-
"""Home of the `IsReadyObserver` class."""
|
2
|
-
|
3
|
-
from job_shop_lib.dispatching import Dispatcher
|
4
|
-
from job_shop_lib.dispatching.feature_observers import (
|
5
|
-
FeatureObserver,
|
6
|
-
FeatureType,
|
7
|
-
)
|
8
|
-
|
9
|
-
|
10
|
-
class IsReadyObserver(FeatureObserver):
|
11
|
-
"""Feature creator that adds a binary feature indicating if the operation
|
12
|
-
is ready to be dispatched."""
|
13
|
-
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
dispatcher: Dispatcher,
|
17
|
-
feature_types: list[FeatureType] | FeatureType | None = None,
|
18
|
-
subscribe: bool = True,
|
19
|
-
):
|
20
|
-
super().__init__(
|
21
|
-
dispatcher, feature_types, feature_size=1, subscribe=subscribe
|
22
|
-
)
|
23
|
-
|
24
|
-
def initialize_features(self):
|
25
|
-
self.set_features_to_zero()
|
26
|
-
for feature_type, feature in self.features.items():
|
27
|
-
node_ids = self._get_ready_nodes(feature_type)
|
28
|
-
feature[node_ids, 0] = 1.0
|
29
|
-
|
30
|
-
def _get_ready_nodes(self, feature_type: FeatureType) -> list[int]:
|
31
|
-
mapping = {
|
32
|
-
FeatureType.OPERATIONS: self._get_ready_operation_nodes,
|
33
|
-
FeatureType.MACHINES: self.dispatcher.available_machines,
|
34
|
-
FeatureType.JOBS: self.dispatcher.available_jobs,
|
35
|
-
}
|
36
|
-
return mapping[feature_type]()
|
37
|
-
|
38
|
-
def _get_ready_operation_nodes(self) -> list[int]:
|
39
|
-
available_operations = self.dispatcher.available_operations()
|
40
|
-
return [operation.operation_id for operation in available_operations]
|
@@ -1,200 +0,0 @@
|
|
1
|
-
"""Home of the `BasicGenerator` class."""
|
2
|
-
|
3
|
-
import random
|
4
|
-
from typing import Iterator
|
5
|
-
|
6
|
-
from job_shop_lib import JobShopInstance, Operation
|
7
|
-
|
8
|
-
|
9
|
-
class BasicGenerator: # pylint: disable=too-many-instance-attributes
|
10
|
-
"""Generates instances for job shop problems.
|
11
|
-
|
12
|
-
DEPRECATED: Class moved to `job_shop_lib.generation` and renamed to
|
13
|
-
`GeneralInstanceGenerator`. This class will be removed in version 1.0.0.
|
14
|
-
|
15
|
-
This class is designed to be versatile, enabling the creation of various
|
16
|
-
job shop instances without the need for multiple dedicated classes.
|
17
|
-
|
18
|
-
It supports customization of the number of jobs, machines, operation
|
19
|
-
durations, and more.
|
20
|
-
|
21
|
-
The class supports both single instance generation and iteration over
|
22
|
-
multiple instances, controlled by the `iteration_limit` parameter. It
|
23
|
-
implements the iterator protocol, allowing it to be used in a `for` loop.
|
24
|
-
|
25
|
-
Note:
|
26
|
-
When used as an iterator, the generator will produce instances until it
|
27
|
-
reaches the specified `iteration_limit`. If `iteration_limit` is None,
|
28
|
-
it will continue indefinitely.
|
29
|
-
|
30
|
-
Attributes:
|
31
|
-
num_jobs_range:
|
32
|
-
The range of the number of jobs to generate. If a single
|
33
|
-
int is provided, it is used as both the minimum and maximum.
|
34
|
-
duration_range:
|
35
|
-
The range of durations for each operation.
|
36
|
-
num_machines_range:
|
37
|
-
The range of the number of machines available. If a
|
38
|
-
single int is provided, it is used as both the minimum and maximum.
|
39
|
-
machines_per_operation:
|
40
|
-
Specifies how many machines each operation
|
41
|
-
can be assigned to. If a single int is provided, it is used for
|
42
|
-
all operations.
|
43
|
-
allow_less_jobs_than_machines:
|
44
|
-
If True, allows generating instances where the number of jobs is
|
45
|
-
less than the number of machines.
|
46
|
-
allow_recirculation:
|
47
|
-
If True, a job can visit the same machine more than once.
|
48
|
-
name_suffix:
|
49
|
-
A suffix to append to each instance's name for identification.
|
50
|
-
seed:
|
51
|
-
Seed for the random number generator to ensure reproducibility.
|
52
|
-
"""
|
53
|
-
|
54
|
-
def __init__( # pylint: disable=too-many-arguments
|
55
|
-
self,
|
56
|
-
num_jobs: int | tuple[int, int] = (10, 20),
|
57
|
-
num_machines: int | tuple[int, int] = (5, 10),
|
58
|
-
duration_range: tuple[int, int] = (1, 99),
|
59
|
-
allow_less_jobs_than_machines: bool = True,
|
60
|
-
allow_recirculation: bool = False,
|
61
|
-
machines_per_operation: int | tuple[int, int] = 1,
|
62
|
-
name_suffix: str = "classic_generated_instance",
|
63
|
-
seed: int | None = None,
|
64
|
-
iteration_limit: int | None = None,
|
65
|
-
):
|
66
|
-
"""Initializes the instance generator with the given parameters.
|
67
|
-
|
68
|
-
Args:
|
69
|
-
num_jobs:
|
70
|
-
The range of the number of jobs to generate.
|
71
|
-
num_machines:
|
72
|
-
The range of the number of machines available.
|
73
|
-
duration_range:
|
74
|
-
The range of durations for each operation.
|
75
|
-
allow_less_jobs_than_machines:
|
76
|
-
Allows instances with fewer jobs than machines.
|
77
|
-
allow_recirculation:
|
78
|
-
Allows jobs to visit the same machine multiple times.
|
79
|
-
machines_per_operation:
|
80
|
-
Specifies how many machines each operation can be assigned to.
|
81
|
-
If a single int is provided, it is used for all operations.
|
82
|
-
name_suffix:
|
83
|
-
Suffix for instance names.
|
84
|
-
seed:
|
85
|
-
Seed for the random number generator.
|
86
|
-
iteration_limit:
|
87
|
-
Maximum number of instances to generate in iteration mode.
|
88
|
-
"""
|
89
|
-
if isinstance(num_jobs, int):
|
90
|
-
num_jobs = (num_jobs, num_jobs)
|
91
|
-
|
92
|
-
if isinstance(num_machines, int):
|
93
|
-
num_machines = (num_machines, num_machines)
|
94
|
-
|
95
|
-
if isinstance(machines_per_operation, int):
|
96
|
-
machines_per_operation = (
|
97
|
-
machines_per_operation,
|
98
|
-
machines_per_operation,
|
99
|
-
)
|
100
|
-
|
101
|
-
self.num_jobs_range = num_jobs
|
102
|
-
self.duration_range = duration_range
|
103
|
-
self.num_machines_range = num_machines
|
104
|
-
self.machines_per_operation = machines_per_operation
|
105
|
-
|
106
|
-
self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
|
107
|
-
self.allow_recirculation = allow_recirculation
|
108
|
-
self.name_suffix = name_suffix
|
109
|
-
|
110
|
-
self._counter = 0
|
111
|
-
self._current_iteration = 0
|
112
|
-
self._iteration_limit = iteration_limit
|
113
|
-
|
114
|
-
if seed is not None:
|
115
|
-
random.seed(seed)
|
116
|
-
|
117
|
-
def generate(self) -> JobShopInstance:
|
118
|
-
"""Generates a single job shop instance"""
|
119
|
-
num_jobs = random.randint(*self.num_jobs_range)
|
120
|
-
|
121
|
-
min_num_machines, max_num_machines = self.num_machines_range
|
122
|
-
if not self.allow_less_jobs_than_machines:
|
123
|
-
min_num_machines = min(num_jobs, max_num_machines)
|
124
|
-
num_machines = random.randint(min_num_machines, max_num_machines)
|
125
|
-
|
126
|
-
jobs = []
|
127
|
-
available_machines = list(range(num_machines))
|
128
|
-
for _ in range(num_jobs):
|
129
|
-
job = []
|
130
|
-
for _ in range(num_machines):
|
131
|
-
operation = self.create_random_operation(available_machines)
|
132
|
-
job.append(operation)
|
133
|
-
jobs.append(job)
|
134
|
-
available_machines = list(range(num_machines))
|
135
|
-
|
136
|
-
return JobShopInstance(jobs=jobs, name=self._get_name())
|
137
|
-
|
138
|
-
def __iter__(self) -> Iterator[JobShopInstance]:
|
139
|
-
self._current_iteration = 0
|
140
|
-
return self
|
141
|
-
|
142
|
-
def __next__(self) -> JobShopInstance:
|
143
|
-
if (
|
144
|
-
self._iteration_limit is not None
|
145
|
-
and self._current_iteration >= self._iteration_limit
|
146
|
-
):
|
147
|
-
raise StopIteration
|
148
|
-
self._current_iteration += 1
|
149
|
-
return self.generate()
|
150
|
-
|
151
|
-
def __len__(self) -> int:
|
152
|
-
if self._iteration_limit is None:
|
153
|
-
raise ValueError("Iteration limit is not set.")
|
154
|
-
return self._iteration_limit
|
155
|
-
|
156
|
-
def create_random_operation(
|
157
|
-
self, available_machines: list[int] | None = None
|
158
|
-
) -> Operation:
|
159
|
-
"""Creates a random operation with the given available machines.
|
160
|
-
|
161
|
-
Args:
|
162
|
-
available_machines:
|
163
|
-
A list of available machine_ids to choose from.
|
164
|
-
If None, all machines are available.
|
165
|
-
"""
|
166
|
-
duration = random.randint(*self.duration_range)
|
167
|
-
|
168
|
-
if self.machines_per_operation[1] > 1:
|
169
|
-
machines = self._choose_multiple_machines()
|
170
|
-
return Operation(machines=machines, duration=duration)
|
171
|
-
|
172
|
-
machine_id = self._choose_one_machine(available_machines)
|
173
|
-
return Operation(machines=machine_id, duration=duration)
|
174
|
-
|
175
|
-
def _choose_multiple_machines(self) -> list[int]:
|
176
|
-
num_machines = random.randint(*self.machines_per_operation)
|
177
|
-
available_machines = list(range(num_machines))
|
178
|
-
machines = []
|
179
|
-
for _ in range(num_machines):
|
180
|
-
machine = random.choice(available_machines)
|
181
|
-
machines.append(machine)
|
182
|
-
available_machines.remove(machine)
|
183
|
-
return machines
|
184
|
-
|
185
|
-
def _choose_one_machine(
|
186
|
-
self, available_machines: list[int] | None = None
|
187
|
-
) -> int:
|
188
|
-
if available_machines is None:
|
189
|
-
_, max_num_machines = self.num_machines_range
|
190
|
-
available_machines = list(range(max_num_machines))
|
191
|
-
|
192
|
-
machine_id = random.choice(available_machines)
|
193
|
-
if not self.allow_recirculation:
|
194
|
-
available_machines.remove(machine_id)
|
195
|
-
|
196
|
-
return machine_id
|
197
|
-
|
198
|
-
def _get_name(self) -> str:
|
199
|
-
self._counter += 1
|
200
|
-
return f"{self.name_suffix}_{self._counter}"
|
@@ -1,164 +0,0 @@
|
|
1
|
-
"""Classes for generating transformed JobShopInstance objects."""
|
2
|
-
|
3
|
-
import abc
|
4
|
-
import copy
|
5
|
-
import random
|
6
|
-
|
7
|
-
from job_shop_lib import JobShopInstance, Operation
|
8
|
-
|
9
|
-
|
10
|
-
class Transformation(abc.ABC):
|
11
|
-
"""Base class for transformations applied to JobShopInstance objects."""
|
12
|
-
|
13
|
-
def __init__(self, suffix: str = ""):
|
14
|
-
self.suffix = suffix
|
15
|
-
self.counter = 0
|
16
|
-
|
17
|
-
@abc.abstractmethod
|
18
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
19
|
-
"""Applies the transformation to a given JobShopInstance.
|
20
|
-
|
21
|
-
Args:
|
22
|
-
instance: The JobShopInstance to transform.
|
23
|
-
|
24
|
-
Returns:
|
25
|
-
A new JobShopInstance with the transformation applied.
|
26
|
-
"""
|
27
|
-
|
28
|
-
def __call__(self, instance: JobShopInstance) -> JobShopInstance:
|
29
|
-
instance = self.apply(instance)
|
30
|
-
suffix = f"{self.suffix}_id={self.counter}"
|
31
|
-
instance.name += suffix
|
32
|
-
self.counter += 1
|
33
|
-
return instance
|
34
|
-
|
35
|
-
|
36
|
-
# pylint: disable=too-few-public-methods
|
37
|
-
class RemoveMachines(Transformation):
|
38
|
-
"""Removes operations associated with randomly selected machines until
|
39
|
-
there are exactly num_machines machines left."""
|
40
|
-
|
41
|
-
def __init__(self, num_machines: int, suffix: str | None = None):
|
42
|
-
if suffix is None:
|
43
|
-
suffix = f"_machines={num_machines}"
|
44
|
-
super().__init__(suffix=suffix)
|
45
|
-
self.num_machines = num_machines
|
46
|
-
|
47
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
48
|
-
if instance.num_machines <= self.num_machines:
|
49
|
-
return instance # No need to remove machines
|
50
|
-
|
51
|
-
# Select machine indices to keep
|
52
|
-
machines_to_keep = set(
|
53
|
-
random.sample(range(instance.num_machines), self.num_machines)
|
54
|
-
)
|
55
|
-
|
56
|
-
# Re-index machines
|
57
|
-
machine_reindex_map = {
|
58
|
-
old_id: new_id
|
59
|
-
for new_id, old_id in enumerate(sorted(machines_to_keep))
|
60
|
-
}
|
61
|
-
|
62
|
-
new_jobs = []
|
63
|
-
for job in instance.jobs:
|
64
|
-
# Keep operations whose machine_id is in machines_to_keep and
|
65
|
-
# re-index them
|
66
|
-
new_jobs.append(
|
67
|
-
[
|
68
|
-
Operation(machine_reindex_map[op.machine_id], op.duration)
|
69
|
-
for op in job
|
70
|
-
if op.machine_id in machines_to_keep
|
71
|
-
]
|
72
|
-
)
|
73
|
-
|
74
|
-
return JobShopInstance(new_jobs, instance.name)
|
75
|
-
|
76
|
-
|
77
|
-
# pylint: disable=too-few-public-methods
|
78
|
-
class AddDurationNoise(Transformation):
|
79
|
-
"""Adds uniform integer noise to operation durations."""
|
80
|
-
|
81
|
-
def __init__(
|
82
|
-
self,
|
83
|
-
min_duration: int = 1,
|
84
|
-
max_duration: int = 100,
|
85
|
-
noise_level: int = 10,
|
86
|
-
suffix: str | None = None,
|
87
|
-
):
|
88
|
-
if suffix is None:
|
89
|
-
suffix = f"_noise={noise_level}"
|
90
|
-
super().__init__(suffix=suffix)
|
91
|
-
self.min_duration = min_duration
|
92
|
-
self.max_duration = max_duration
|
93
|
-
self.noise_level = noise_level
|
94
|
-
|
95
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
96
|
-
new_jobs = []
|
97
|
-
for job in instance.jobs:
|
98
|
-
new_job = []
|
99
|
-
for op in job:
|
100
|
-
noise = random.randint(-self.noise_level, self.noise_level)
|
101
|
-
new_duration = max(
|
102
|
-
self.min_duration,
|
103
|
-
min(self.max_duration, op.duration + noise),
|
104
|
-
)
|
105
|
-
|
106
|
-
new_job.append(Operation(op.machine_id, new_duration))
|
107
|
-
new_jobs.append(new_job)
|
108
|
-
|
109
|
-
return JobShopInstance(new_jobs, instance.name)
|
110
|
-
|
111
|
-
|
112
|
-
class RemoveJobs(Transformation):
|
113
|
-
"""Removes jobs randomly until the number of jobs is within a specified
|
114
|
-
range."""
|
115
|
-
|
116
|
-
def __init__(
|
117
|
-
self,
|
118
|
-
min_jobs: int,
|
119
|
-
max_jobs: int,
|
120
|
-
target_jobs: int | None = None,
|
121
|
-
suffix: str | None = None,
|
122
|
-
):
|
123
|
-
"""
|
124
|
-
Args:
|
125
|
-
min_jobs: The minimum number of jobs to remain in the instance.
|
126
|
-
max_jobs: The maximum number of jobs to remain in the instance.
|
127
|
-
target_jobs: If specified, the number of jobs to remain in the
|
128
|
-
instance. Overrides min_jobs and max_jobs.
|
129
|
-
"""
|
130
|
-
if suffix is None:
|
131
|
-
suffix = f"_jobs={min_jobs}-{max_jobs}"
|
132
|
-
super().__init__(suffix=suffix)
|
133
|
-
self.min_jobs = min_jobs
|
134
|
-
self.max_jobs = max_jobs
|
135
|
-
self.target_jobs = target_jobs
|
136
|
-
|
137
|
-
def apply(self, instance: JobShopInstance) -> JobShopInstance:
|
138
|
-
if self.target_jobs is None:
|
139
|
-
target_jobs = random.randint(self.min_jobs, self.max_jobs)
|
140
|
-
else:
|
141
|
-
target_jobs = self.target_jobs
|
142
|
-
new_jobs = copy.deepcopy(instance.jobs)
|
143
|
-
|
144
|
-
while len(new_jobs) > target_jobs:
|
145
|
-
new_jobs.pop(random.randint(0, len(new_jobs) - 1))
|
146
|
-
|
147
|
-
return JobShopInstance(new_jobs, instance.name)
|
148
|
-
|
149
|
-
@staticmethod
|
150
|
-
def remove_job(
|
151
|
-
instance: JobShopInstance, job_index: int
|
152
|
-
) -> JobShopInstance:
|
153
|
-
"""Removes a specific job from the instance.
|
154
|
-
|
155
|
-
Args:
|
156
|
-
instance: The JobShopInstance from which to remove the job.
|
157
|
-
job_index: The index of the job to remove.
|
158
|
-
|
159
|
-
Returns:
|
160
|
-
A new JobShopInstance with the specified job removed.
|
161
|
-
"""
|
162
|
-
new_jobs = copy.deepcopy(instance.jobs)
|
163
|
-
new_jobs.pop(job_index)
|
164
|
-
return JobShopInstance(new_jobs, instance.name)
|
job_shop_lib/operation.py
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
"""Home of the `Operation` class."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
from job_shop_lib import JobShopLibError
|
6
|
-
|
7
|
-
|
8
|
-
class Operation:
|
9
|
-
"""Stores machine and duration information for a job operation.
|
10
|
-
|
11
|
-
Note:
|
12
|
-
To increase performance, some solvers such as the CP-SAT solver use
|
13
|
-
only integers to represent the operation's attributes. Should a
|
14
|
-
problem involve operations with non-integer durations, it would be
|
15
|
-
necessary to multiply all durations by a sufficiently large integer so
|
16
|
-
that every duration is an integer.
|
17
|
-
|
18
|
-
Attributes:
|
19
|
-
machines: A list of machine ids that can perform the operation.
|
20
|
-
duration: The time it takes to perform the operation.
|
21
|
-
"""
|
22
|
-
|
23
|
-
__slots__ = (
|
24
|
-
"machines",
|
25
|
-
"duration",
|
26
|
-
"_job_id",
|
27
|
-
"_position_in_job",
|
28
|
-
"_operation_id",
|
29
|
-
)
|
30
|
-
|
31
|
-
def __init__(self, machines: int | list[int], duration: int):
|
32
|
-
"""Initializes the object with the given machines and duration.
|
33
|
-
|
34
|
-
Args:
|
35
|
-
machines: A list of machine ids that can perform the operation. If
|
36
|
-
only one machine can perform the operation, it can be passed as
|
37
|
-
an integer.
|
38
|
-
duration: The time it takes to perform the operation.
|
39
|
-
"""
|
40
|
-
self.machines = [machines] if isinstance(machines, int) else machines
|
41
|
-
self.duration = duration
|
42
|
-
|
43
|
-
# Defined outside the class by the JobShopInstance class:
|
44
|
-
self._job_id: int | None = None
|
45
|
-
self._position_in_job: int | None = None
|
46
|
-
self._operation_id: int | None = None
|
47
|
-
|
48
|
-
@property
|
49
|
-
def machine_id(self) -> int:
|
50
|
-
"""Returns the id of the machine associated with the operation.
|
51
|
-
|
52
|
-
Raises:
|
53
|
-
ValueError: If the operation has multiple machines in its list.
|
54
|
-
"""
|
55
|
-
if len(self.machines) > 1:
|
56
|
-
raise JobShopLibError("Operation has multiple machines.")
|
57
|
-
return self.machines[0]
|
58
|
-
|
59
|
-
@property
|
60
|
-
def job_id(self) -> int:
|
61
|
-
"""Returns the id of the job that the operation belongs to."""
|
62
|
-
if self._job_id is None:
|
63
|
-
raise JobShopLibError("Operation has no job_id.")
|
64
|
-
return self._job_id
|
65
|
-
|
66
|
-
@job_id.setter
|
67
|
-
def job_id(self, value: int) -> None:
|
68
|
-
self._job_id = value
|
69
|
-
|
70
|
-
@property
|
71
|
-
def position_in_job(self) -> int:
|
72
|
-
"""Returns the position (starting at zero) of the operation in the
|
73
|
-
job.
|
74
|
-
|
75
|
-
Raises:
|
76
|
-
ValueError: If the operation has no position_in_job.
|
77
|
-
"""
|
78
|
-
if self._position_in_job is None:
|
79
|
-
raise JobShopLibError("Operation has no position_in_job.")
|
80
|
-
return self._position_in_job
|
81
|
-
|
82
|
-
@position_in_job.setter
|
83
|
-
def position_in_job(self, value: int) -> None:
|
84
|
-
self._position_in_job = value
|
85
|
-
|
86
|
-
@property
|
87
|
-
def operation_id(self) -> int:
|
88
|
-
"""Returns the id of the operation.
|
89
|
-
|
90
|
-
The operation id is unique within a job shop instance and should
|
91
|
-
be set by the JobShopInstance class.
|
92
|
-
|
93
|
-
It starts at 0 and is incremented by 1 for each operation in the
|
94
|
-
instance.
|
95
|
-
|
96
|
-
Raises:
|
97
|
-
ValueError: If the operation has no id.
|
98
|
-
"""
|
99
|
-
if self._operation_id is None:
|
100
|
-
raise JobShopLibError("Operation has no id.")
|
101
|
-
return self._operation_id
|
102
|
-
|
103
|
-
@operation_id.setter
|
104
|
-
def operation_id(self, value: int) -> None:
|
105
|
-
self._operation_id = value
|
106
|
-
|
107
|
-
def __hash__(self) -> int:
|
108
|
-
return hash(self.operation_id)
|
109
|
-
|
110
|
-
def __eq__(self, value: object) -> bool:
|
111
|
-
if not isinstance(value, Operation):
|
112
|
-
return False
|
113
|
-
return self.__slots__ == value.__slots__
|
114
|
-
|
115
|
-
def __repr__(self) -> str:
|
116
|
-
machines = (
|
117
|
-
self.machines[0] if len(self.machines) == 1 else self.machines
|
118
|
-
)
|
119
|
-
return (
|
120
|
-
f"O(m={machines}, d={self.duration}, "
|
121
|
-
f"j={self.job_id}, p={self.position_in_job})"
|
122
|
-
)
|