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.
Files changed (76) hide show
  1. job_shop_lib/__init__.py +16 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +9 -4
  4. job_shop_lib/_operation.py +95 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +73 -54
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +57 -18
  11. job_shop_lib/dispatching/__init__.py +45 -41
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
  14. job_shop_lib/dispatching/_factories.py +125 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
  16. job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
  17. job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
  18. job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
  19. job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
  20. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
  21. job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
  22. job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
  23. job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
  24. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  25. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
  26. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
  27. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  28. job_shop_lib/dispatching/rules/__init__.py +51 -0
  29. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
  30. job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
  31. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
  32. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
  33. job_shop_lib/dispatching/rules/_utils.py +127 -0
  34. job_shop_lib/exceptions.py +18 -0
  35. job_shop_lib/generation/__init__.py +2 -2
  36. job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
  37. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
  38. job_shop_lib/graphs/__init__.py +17 -6
  39. job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
  40. job_shop_lib/graphs/{node.py → _node.py} +18 -12
  41. job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
  42. job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
  43. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
  44. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  45. job_shop_lib/reinforcement_learning/__init__.py +41 -0
  46. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
  47. job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
  48. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
  49. job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
  50. job_shop_lib/reinforcement_learning/_utils.py +96 -0
  51. job_shop_lib/visualization/__init__.py +20 -4
  52. job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
  53. job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
  54. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
  55. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
  56. job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
  57. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  58. job_shop_lib/cp_sat/__init__.py +0 -5
  59. job_shop_lib/dispatching/factories.py +0 -206
  60. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  61. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  62. job_shop_lib/generators/__init__.py +0 -8
  63. job_shop_lib/generators/basic_generator.py +0 -200
  64. job_shop_lib/generators/transformations.py +0 -164
  65. job_shop_lib/operation.py +0 -122
  66. job_shop_lib/visualization/create_gif.py +0 -209
  67. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  68. /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
  69. /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
  70. /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
  71. /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
  72. /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
  73. /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
  74. /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
  75. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
  76. {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,8 +0,0 @@
1
- """DEPRECATED: Use `job_shop_lib.generation` instead. It will be removed in
2
- version 1.0.0."""
3
-
4
- from job_shop_lib.generators.basic_generator import BasicGenerator
5
-
6
- __all__ = [
7
- "BasicGenerator",
8
- ]
@@ -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
- )