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.
Files changed (27) hide show
  1. job_shop_lib/dispatching/dispatcher.py +219 -51
  2. job_shop_lib/dispatching/feature_observers/__init__.py +28 -0
  3. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +87 -0
  4. job_shop_lib/dispatching/feature_observers/duration_observer.py +95 -0
  5. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +156 -0
  6. job_shop_lib/dispatching/feature_observers/factory.py +58 -0
  7. job_shop_lib/dispatching/feature_observers/feature_observer.py +113 -0
  8. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +98 -0
  9. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +40 -0
  10. job_shop_lib/dispatching/feature_observers/is_scheduled_observer.py +34 -0
  11. job_shop_lib/dispatching/feature_observers/position_in_job_observer.py +39 -0
  12. job_shop_lib/dispatching/feature_observers/remaining_operations_observer.py +54 -0
  13. job_shop_lib/generation/__init__.py +11 -0
  14. job_shop_lib/generation/general_instance_generator.py +169 -0
  15. job_shop_lib/generation/instance_generator.py +122 -0
  16. job_shop_lib/generation/transformations.py +164 -0
  17. job_shop_lib/generators/__init__.py +2 -1
  18. job_shop_lib/generators/basic_generator.py +3 -0
  19. job_shop_lib/graphs/build_disjunctive_graph.py +20 -0
  20. job_shop_lib/job_shop_instance.py +101 -0
  21. job_shop_lib/visualization/create_gif.py +47 -38
  22. job_shop_lib/visualization/gantt_chart.py +1 -1
  23. {job_shop_lib-0.4.0.dist-info → job_shop_lib-0.5.1.dist-info}/METADATA +9 -5
  24. job_shop_lib-0.5.1.dist-info/RECORD +52 -0
  25. job_shop_lib-0.4.0.dist-info/RECORD +0 -37
  26. {job_shop_lib-0.4.0.dist-info → job_shop_lib-0.5.1.dist-info}/LICENSE +0 -0
  27. {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