job-shop-lib 0.4.0__py3-none-any.whl → 0.5.1__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/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
|