job-shop-lib 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/dispatching/dispatcher.py +219 -51
- job_shop_lib/dispatching/feature_observers/__init__.py +28 -0
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +87 -0
- job_shop_lib/dispatching/feature_observers/duration_observer.py +95 -0
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +156 -0
- job_shop_lib/dispatching/feature_observers/factory.py +58 -0
- job_shop_lib/dispatching/feature_observers/feature_observer.py +113 -0
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +98 -0
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +40 -0
- job_shop_lib/dispatching/feature_observers/is_scheduled_observer.py +34 -0
- job_shop_lib/dispatching/feature_observers/position_in_job_observer.py +39 -0
- job_shop_lib/dispatching/feature_observers/remaining_operations_observer.py +54 -0
- job_shop_lib/generation/__init__.py +11 -0
- job_shop_lib/generation/general_instance_generator.py +169 -0
- job_shop_lib/generation/instance_generator.py +122 -0
- job_shop_lib/generation/transformations.py +164 -0
- job_shop_lib/generators/__init__.py +2 -1
- job_shop_lib/generators/basic_generator.py +3 -0
- job_shop_lib/graphs/build_disjunctive_graph.py +20 -0
- job_shop_lib/job_shop_instance.py +101 -0
- job_shop_lib/visualization/create_gif.py +47 -38
- job_shop_lib/visualization/gantt_chart.py +1 -1
- {job_shop_lib-0.4.0.dist-info → job_shop_lib-0.5.1.dist-info}/METADATA +9 -5
- job_shop_lib-0.5.1.dist-info/RECORD +52 -0
- job_shop_lib-0.4.0.dist-info/RECORD +0 -37
- {job_shop_lib-0.4.0.dist-info → job_shop_lib-0.5.1.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.4.0.dist-info → job_shop_lib-0.5.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
"""Contains factory functions for creating node feature encoders."""
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
from job_shop_lib.dispatching.feature_observers import (
|
6
|
+
IsReadyObserver,
|
7
|
+
EarliestStartTimeObserver,
|
8
|
+
FeatureObserver,
|
9
|
+
DurationObserver,
|
10
|
+
IsScheduledObserver,
|
11
|
+
PositionInJobObserver,
|
12
|
+
RemainingOperationsObserver,
|
13
|
+
IsCompletedObserver,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class FeatureObserverType(str, Enum):
|
18
|
+
"""Enumeration of node feature creator types for the job shop scheduling
|
19
|
+
problem."""
|
20
|
+
|
21
|
+
IS_READY = "is_ready"
|
22
|
+
EARLIEST_START_TIME = "earliest_start_time"
|
23
|
+
DURATION = "duration"
|
24
|
+
IS_SCHEDULED = "is_scheduled"
|
25
|
+
POSITION_IN_JOB = "position_in_job"
|
26
|
+
REMAINING_OPERATIONS = "remaining_operations"
|
27
|
+
IS_COMPLETED = "is_completed"
|
28
|
+
COMPOSITE = "composite"
|
29
|
+
|
30
|
+
|
31
|
+
def feature_observer_factory(
|
32
|
+
node_feature_creator_type: str | FeatureObserverType,
|
33
|
+
**kwargs,
|
34
|
+
) -> FeatureObserver:
|
35
|
+
"""Creates and returns a node feature creator based on the specified
|
36
|
+
node feature creator type.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
node_feature_creator_type:
|
40
|
+
The type of node feature creator to create.
|
41
|
+
**kwargs:
|
42
|
+
Additional keyword arguments to pass to the node
|
43
|
+
feature creator constructor.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
A node feature creator instance.
|
47
|
+
"""
|
48
|
+
mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
|
49
|
+
FeatureObserverType.IS_READY: IsReadyObserver,
|
50
|
+
FeatureObserverType.EARLIEST_START_TIME: EarliestStartTimeObserver,
|
51
|
+
FeatureObserverType.DURATION: DurationObserver,
|
52
|
+
FeatureObserverType.IS_SCHEDULED: IsScheduledObserver,
|
53
|
+
FeatureObserverType.POSITION_IN_JOB: PositionInJobObserver,
|
54
|
+
FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
|
55
|
+
FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
|
56
|
+
}
|
57
|
+
feature_creator = mapping[node_feature_creator_type] # type: ignore[index]
|
58
|
+
return feature_creator(**kwargs)
|
@@ -0,0 +1,113 @@
|
|
1
|
+
"""Home of the `FeatureObserver` class and `FeatureType` enum."""
|
2
|
+
|
3
|
+
import enum
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
from job_shop_lib import ScheduledOperation
|
7
|
+
from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
|
8
|
+
|
9
|
+
|
10
|
+
class FeatureType(str, enum.Enum):
|
11
|
+
"""Types of features that can be extracted."""
|
12
|
+
|
13
|
+
OPERATIONS = "operations"
|
14
|
+
MACHINES = "machines"
|
15
|
+
JOBS = "jobs"
|
16
|
+
|
17
|
+
|
18
|
+
class FeatureObserver(DispatcherObserver):
|
19
|
+
"""Base class for feature observers."""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
dispatcher: Dispatcher,
|
24
|
+
feature_types: list[FeatureType] | FeatureType | None = None,
|
25
|
+
feature_size: dict[FeatureType, int] | int = 1,
|
26
|
+
is_singleton: bool = True,
|
27
|
+
subscribe: bool = True,
|
28
|
+
):
|
29
|
+
feature_types = self.get_feature_types_list(feature_types)
|
30
|
+
if isinstance(feature_size, int):
|
31
|
+
feature_size = {
|
32
|
+
feature_type: feature_size for feature_type in feature_types
|
33
|
+
}
|
34
|
+
super().__init__(dispatcher, is_singleton, subscribe)
|
35
|
+
|
36
|
+
number_of_entities = {
|
37
|
+
FeatureType.OPERATIONS: dispatcher.instance.num_operations,
|
38
|
+
FeatureType.MACHINES: dispatcher.instance.num_machines,
|
39
|
+
FeatureType.JOBS: dispatcher.instance.num_jobs,
|
40
|
+
}
|
41
|
+
self.feature_dimensions = {
|
42
|
+
feature_type: (
|
43
|
+
number_of_entities[feature_type],
|
44
|
+
feature_size[feature_type],
|
45
|
+
)
|
46
|
+
for feature_type in feature_types
|
47
|
+
}
|
48
|
+
self.features = {
|
49
|
+
feature_type: np.zeros(
|
50
|
+
self.feature_dimensions[feature_type],
|
51
|
+
dtype=np.float32,
|
52
|
+
)
|
53
|
+
for feature_type in feature_types
|
54
|
+
}
|
55
|
+
self.initialize_features()
|
56
|
+
|
57
|
+
def initialize_features(self):
|
58
|
+
"""Initializes the features based on the current state of the
|
59
|
+
dispatcher."""
|
60
|
+
|
61
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
62
|
+
"""Updates the features based on the scheduled operation.
|
63
|
+
|
64
|
+
By default, this method just calls `initialize_features`.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
scheduled_operation:
|
68
|
+
The operation that has been scheduled.
|
69
|
+
"""
|
70
|
+
self.initialize_features()
|
71
|
+
|
72
|
+
def reset(self):
|
73
|
+
"""Sets features to zero and calls to `initialize_features`."""
|
74
|
+
self.set_features_to_zero()
|
75
|
+
self.initialize_features()
|
76
|
+
|
77
|
+
def set_features_to_zero(
|
78
|
+
self, exclude: FeatureType | list[FeatureType] | None = None
|
79
|
+
):
|
80
|
+
"""Sets features to zero."""
|
81
|
+
if exclude is None:
|
82
|
+
exclude = []
|
83
|
+
if isinstance(exclude, FeatureType):
|
84
|
+
exclude = [exclude]
|
85
|
+
|
86
|
+
for feature_type in self.features:
|
87
|
+
if feature_type in exclude:
|
88
|
+
continue
|
89
|
+
self.features[feature_type][:] = 0.0
|
90
|
+
|
91
|
+
@staticmethod
|
92
|
+
def get_feature_types_list(
|
93
|
+
feature_types: list[FeatureType] | FeatureType | None,
|
94
|
+
) -> list[FeatureType]:
|
95
|
+
"""Returns a list of feature types.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
feature_types:
|
99
|
+
A list of feature types or a single feature type. If `None`,
|
100
|
+
all feature types are returned.
|
101
|
+
"""
|
102
|
+
if isinstance(feature_types, FeatureType):
|
103
|
+
feature_types = [feature_types]
|
104
|
+
if feature_types is None:
|
105
|
+
feature_types = list(FeatureType)
|
106
|
+
return feature_types
|
107
|
+
|
108
|
+
def __str__(self):
|
109
|
+
out = [self.__class__.__name__, ":\n"]
|
110
|
+
out.append("-" * len(out[0]))
|
111
|
+
for feature_type, feature in self.features.items():
|
112
|
+
out.append(f"\n{feature_type.value}:\n{feature}")
|
113
|
+
return "".join(out)
|
@@ -0,0 +1,98 @@
|
|
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
|
@@ -0,0 +1,40 @@
|
|
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]
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"""Home of the `IsScheduledObserver` class."""
|
2
|
+
|
3
|
+
from job_shop_lib import ScheduledOperation
|
4
|
+
from job_shop_lib.dispatching.feature_observers import (
|
5
|
+
FeatureObserver,
|
6
|
+
FeatureType,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
class IsScheduledObserver(FeatureObserver):
|
11
|
+
"""Observer that updates features based on scheduling operations.
|
12
|
+
|
13
|
+
This observer tracks which operations have been scheduled and updates
|
14
|
+
feature matrices accordingly. It updates a feature in the
|
15
|
+
`FeatureType.OPERATIONS` matrix to indicate that an operation has been
|
16
|
+
scheduled. Additionally, it counts the number of uncompleted but
|
17
|
+
scheduled operations for each machine and job, updating the respective
|
18
|
+
`FeatureType.MACHINES` and `FeatureType.JOBS` feature matrices.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
22
|
+
if FeatureType.OPERATIONS in self.features:
|
23
|
+
self.features[FeatureType.OPERATIONS][
|
24
|
+
scheduled_operation.operation.operation_id, 0
|
25
|
+
] = 1.0
|
26
|
+
|
27
|
+
ongoing_operations = self.dispatcher.ongoing_operations()
|
28
|
+
self.set_features_to_zero(exclude=FeatureType.OPERATIONS)
|
29
|
+
for scheduled_op in ongoing_operations:
|
30
|
+
if FeatureType.MACHINES in self.features:
|
31
|
+
machine_id = scheduled_op.machine_id
|
32
|
+
self.features[FeatureType.MACHINES][machine_id, 0] += 1.0
|
33
|
+
if FeatureType.JOBS in self.features:
|
34
|
+
self.features[FeatureType.JOBS][scheduled_op.job_id, 0] += 1.0
|
@@ -0,0 +1,39 @@
|
|
1
|
+
"""Home of the `PositionInJobObserver` class."""
|
2
|
+
|
3
|
+
from job_shop_lib.dispatching import Dispatcher
|
4
|
+
from job_shop_lib import ScheduledOperation
|
5
|
+
from job_shop_lib.dispatching.feature_observers import (
|
6
|
+
FeatureObserver,
|
7
|
+
FeatureType,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class PositionInJobObserver(FeatureObserver):
|
12
|
+
"""Observer that adds a feature indicating the position of
|
13
|
+
operations in their respective jobs.
|
14
|
+
|
15
|
+
Positions are adjusted dynamically as operations are scheduled.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, dispatcher: Dispatcher, subscribe: bool = True):
|
19
|
+
super().__init__(
|
20
|
+
dispatcher,
|
21
|
+
feature_types=[FeatureType.OPERATIONS],
|
22
|
+
feature_size=1,
|
23
|
+
subscribe=subscribe,
|
24
|
+
)
|
25
|
+
|
26
|
+
def initialize_features(self):
|
27
|
+
for operation in self.dispatcher.unscheduled_operations():
|
28
|
+
self.features[FeatureType.OPERATIONS][
|
29
|
+
operation.operation_id, 0
|
30
|
+
] = operation.position_in_job
|
31
|
+
|
32
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
33
|
+
job = self.dispatcher.instance.jobs[scheduled_operation.job_id]
|
34
|
+
for new_position_in_job, operation in enumerate(
|
35
|
+
job[scheduled_operation.position_in_job + 1 :]
|
36
|
+
):
|
37
|
+
self.features[FeatureType.OPERATIONS][
|
38
|
+
operation.operation_id, 0
|
39
|
+
] = new_position_in_job
|
@@ -0,0 +1,54 @@
|
|
1
|
+
"""Home of the `RemainingOperationsObserver` class."""
|
2
|
+
|
3
|
+
from job_shop_lib import ScheduledOperation
|
4
|
+
from job_shop_lib.dispatching import Dispatcher
|
5
|
+
from job_shop_lib.dispatching.feature_observers import (
|
6
|
+
FeatureObserver,
|
7
|
+
FeatureType,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class RemainingOperationsObserver(FeatureObserver):
|
12
|
+
"""Adds a feature indicating the number of remaining operations for each
|
13
|
+
job and machine.
|
14
|
+
|
15
|
+
It does not support FeatureType.OPERATIONS.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
dispatcher: Dispatcher,
|
21
|
+
feature_types: list[FeatureType] | FeatureType | None = None,
|
22
|
+
subscribe: bool = True,
|
23
|
+
):
|
24
|
+
if feature_types is None:
|
25
|
+
feature_types = [FeatureType.MACHINES, FeatureType.JOBS]
|
26
|
+
|
27
|
+
if (
|
28
|
+
feature_types == FeatureType.OPERATIONS
|
29
|
+
or FeatureType.OPERATIONS in feature_types
|
30
|
+
):
|
31
|
+
raise ValueError("FeatureType.OPERATIONS is not supported.")
|
32
|
+
super().__init__(
|
33
|
+
dispatcher,
|
34
|
+
feature_types=feature_types,
|
35
|
+
feature_size=1,
|
36
|
+
subscribe=subscribe,
|
37
|
+
)
|
38
|
+
|
39
|
+
def initialize_features(self):
|
40
|
+
for operation in self.dispatcher.unscheduled_operations():
|
41
|
+
if FeatureType.JOBS in self.features:
|
42
|
+
self.features[FeatureType.JOBS][operation.job_id, 0] += 1
|
43
|
+
if FeatureType.MACHINES in self.features:
|
44
|
+
self.features[FeatureType.MACHINES][
|
45
|
+
operation.machine_id, 0
|
46
|
+
] += 1
|
47
|
+
|
48
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
49
|
+
if FeatureType.JOBS in self.features:
|
50
|
+
job_id = scheduled_operation.job_id
|
51
|
+
self.features[FeatureType.JOBS][job_id, 0] -= 1
|
52
|
+
if FeatureType.MACHINES in self.features:
|
53
|
+
machine_id = scheduled_operation.machine_id
|
54
|
+
self.features[FeatureType.MACHINES][machine_id, 0] -= 1
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""Package for generating job shop instances."""
|
2
|
+
|
3
|
+
from job_shop_lib.generation.instance_generator import InstanceGenerator
|
4
|
+
from job_shop_lib.generation.general_instance_generator import (
|
5
|
+
GeneralInstanceGenerator,
|
6
|
+
)
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"InstanceGenerator",
|
10
|
+
"GeneralInstanceGenerator",
|
11
|
+
]
|
@@ -0,0 +1,169 @@
|
|
1
|
+
"""Home of the `BasicGenerator` class."""
|
2
|
+
|
3
|
+
import random
|
4
|
+
|
5
|
+
from job_shop_lib import JobShopInstance, Operation
|
6
|
+
from job_shop_lib.generation import InstanceGenerator
|
7
|
+
|
8
|
+
|
9
|
+
class GeneralInstanceGenerator(InstanceGenerator):
|
10
|
+
"""Generates instances for job shop problems.
|
11
|
+
|
12
|
+
This class is designed to be versatile, enabling the creation of various
|
13
|
+
job shop instances without the need for multiple dedicated classes.
|
14
|
+
|
15
|
+
It supports customization of the number of jobs, machines, operation
|
16
|
+
durations, and more.
|
17
|
+
|
18
|
+
The class supports both single instance generation and iteration over
|
19
|
+
multiple instances, controlled by the `iteration_limit` parameter. It
|
20
|
+
implements the iterator protocol, allowing it to be used in a `for` loop.
|
21
|
+
|
22
|
+
Note:
|
23
|
+
When used as an iterator, the generator will produce instances until it
|
24
|
+
reaches the specified `iteration_limit`. If `iteration_limit` is None,
|
25
|
+
it will continue indefinitely.
|
26
|
+
|
27
|
+
Attributes:
|
28
|
+
num_jobs_range:
|
29
|
+
The range of the number of jobs to generate. If a single
|
30
|
+
int is provided, it is used as both the minimum and maximum.
|
31
|
+
duration_range:
|
32
|
+
The range of durations for each operation.
|
33
|
+
num_machines_range:
|
34
|
+
The range of the number of machines available. If a
|
35
|
+
single int is provided, it is used as both the minimum and maximum.
|
36
|
+
machines_per_operation:
|
37
|
+
Specifies how many machines each operation
|
38
|
+
can be assigned to. If a single int is provided, it is used for
|
39
|
+
all operations.
|
40
|
+
allow_less_jobs_than_machines:
|
41
|
+
If True, allows generating instances where the number of jobs is
|
42
|
+
less than the number of machines.
|
43
|
+
allow_recirculation:
|
44
|
+
If True, a job can visit the same machine more than once.
|
45
|
+
name_suffix:
|
46
|
+
A suffix to append to each instance's name for identification.
|
47
|
+
seed:
|
48
|
+
Seed for the random number generator to ensure reproducibility.
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__( # pylint: disable=too-many-arguments
|
52
|
+
self,
|
53
|
+
num_jobs: int | tuple[int, int] = (10, 20),
|
54
|
+
num_machines: int | tuple[int, int] = (5, 10),
|
55
|
+
duration_range: tuple[int, int] = (1, 99),
|
56
|
+
allow_less_jobs_than_machines: bool = True,
|
57
|
+
allow_recirculation: bool = False,
|
58
|
+
machines_per_operation: int | tuple[int, int] = 1,
|
59
|
+
name_suffix: str = "classic_generated_instance",
|
60
|
+
seed: int | None = None,
|
61
|
+
iteration_limit: int | None = None,
|
62
|
+
):
|
63
|
+
"""Initializes the instance generator with the given parameters.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
num_jobs:
|
67
|
+
The range of the number of jobs to generate.
|
68
|
+
num_machines:
|
69
|
+
The range of the number of machines available.
|
70
|
+
duration_range:
|
71
|
+
The range of durations for each operation.
|
72
|
+
allow_less_jobs_than_machines:
|
73
|
+
Allows instances with fewer jobs than machines.
|
74
|
+
allow_recirculation:
|
75
|
+
Allows jobs to visit the same machine multiple times.
|
76
|
+
machines_per_operation:
|
77
|
+
Specifies how many machines each operation can be assigned to.
|
78
|
+
If a single int is provided, it is used for all operations.
|
79
|
+
name_suffix:
|
80
|
+
Suffix for instance names.
|
81
|
+
seed:
|
82
|
+
Seed for the random number generator.
|
83
|
+
iteration_limit:
|
84
|
+
Maximum number of instances to generate in iteration mode.
|
85
|
+
"""
|
86
|
+
super().__init__(
|
87
|
+
num_jobs=num_jobs,
|
88
|
+
num_machines=num_machines,
|
89
|
+
duration_range=duration_range,
|
90
|
+
name_suffix=name_suffix,
|
91
|
+
seed=seed,
|
92
|
+
iteration_limit=iteration_limit,
|
93
|
+
)
|
94
|
+
if isinstance(machines_per_operation, int):
|
95
|
+
machines_per_operation = (
|
96
|
+
machines_per_operation,
|
97
|
+
machines_per_operation,
|
98
|
+
)
|
99
|
+
self.machines_per_operation = machines_per_operation
|
100
|
+
|
101
|
+
self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
|
102
|
+
self.allow_recirculation = allow_recirculation
|
103
|
+
self.name_suffix = name_suffix
|
104
|
+
|
105
|
+
if seed is not None:
|
106
|
+
random.seed(seed)
|
107
|
+
|
108
|
+
def generate(self) -> JobShopInstance:
|
109
|
+
"""Generates a single job shop instance"""
|
110
|
+
num_jobs = random.randint(*self.num_jobs_range)
|
111
|
+
|
112
|
+
min_num_machines, max_num_machines = self.num_machines_range
|
113
|
+
if not self.allow_less_jobs_than_machines:
|
114
|
+
min_num_machines = min(num_jobs, max_num_machines)
|
115
|
+
num_machines = random.randint(min_num_machines, max_num_machines)
|
116
|
+
|
117
|
+
jobs = []
|
118
|
+
available_machines = list(range(num_machines))
|
119
|
+
for _ in range(num_jobs):
|
120
|
+
job = []
|
121
|
+
for _ in range(num_machines):
|
122
|
+
operation = self.create_random_operation(available_machines)
|
123
|
+
job.append(operation)
|
124
|
+
jobs.append(job)
|
125
|
+
available_machines = list(range(num_machines))
|
126
|
+
|
127
|
+
return JobShopInstance(jobs=jobs, name=self._next_name())
|
128
|
+
|
129
|
+
def create_random_operation(
|
130
|
+
self, available_machines: list[int] | None = None
|
131
|
+
) -> Operation:
|
132
|
+
"""Creates a random operation with the given available machines.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
available_machines:
|
136
|
+
A list of available machine_ids to choose from.
|
137
|
+
If None, all machines are available.
|
138
|
+
"""
|
139
|
+
duration = random.randint(*self.duration_range)
|
140
|
+
|
141
|
+
if self.machines_per_operation[1] > 1:
|
142
|
+
machines = self._choose_multiple_machines()
|
143
|
+
return Operation(machines=machines, duration=duration)
|
144
|
+
|
145
|
+
machine_id = self._choose_one_machine(available_machines)
|
146
|
+
return Operation(machines=machine_id, duration=duration)
|
147
|
+
|
148
|
+
def _choose_multiple_machines(self) -> list[int]:
|
149
|
+
num_machines = random.randint(*self.machines_per_operation)
|
150
|
+
available_machines = list(range(num_machines))
|
151
|
+
machines = []
|
152
|
+
for _ in range(num_machines):
|
153
|
+
machine = random.choice(available_machines)
|
154
|
+
machines.append(machine)
|
155
|
+
available_machines.remove(machine)
|
156
|
+
return machines
|
157
|
+
|
158
|
+
def _choose_one_machine(
|
159
|
+
self, available_machines: list[int] | None = None
|
160
|
+
) -> int:
|
161
|
+
if available_machines is None:
|
162
|
+
_, max_num_machines = self.num_machines_range
|
163
|
+
available_machines = list(range(max_num_machines))
|
164
|
+
|
165
|
+
machine_id = random.choice(available_machines)
|
166
|
+
if not self.allow_recirculation:
|
167
|
+
available_machines.remove(machine_id)
|
168
|
+
|
169
|
+
return machine_id
|