job-shop-lib 1.1.0__py3-none-any.whl → 1.1.2__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.
Files changed (23) hide show
  1. job_shop_lib/__init__.py +1 -1
  2. job_shop_lib/_job_shop_instance.py +5 -5
  3. job_shop_lib/_schedule.py +2 -2
  4. job_shop_lib/dispatching/_dispatcher.py +12 -7
  5. job_shop_lib/dispatching/_optimal_operations_observer.py +6 -3
  6. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +11 -46
  7. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +2 -2
  8. job_shop_lib/dispatching/feature_observers/_factory.py +2 -4
  9. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +0 -46
  10. job_shop_lib/generation/_general_instance_generator.py +6 -4
  11. job_shop_lib/graphs/_build_resource_task_graphs.py +1 -0
  12. job_shop_lib/graphs/_job_shop_graph.py +2 -2
  13. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +4 -3
  14. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +5 -3
  15. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +23 -29
  16. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +6 -4
  17. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +18 -11
  18. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +1 -1
  19. {job_shop_lib-1.1.0.dist-info → job_shop_lib-1.1.2.dist-info}/METADATA +6 -4
  20. {job_shop_lib-1.1.0.dist-info → job_shop_lib-1.1.2.dist-info}/RECORD +22 -23
  21. job_shop_lib/generation/_transformations.py +0 -167
  22. {job_shop_lib-1.1.0.dist-info → job_shop_lib-1.1.2.dist-info}/LICENSE +0 -0
  23. {job_shop_lib-1.1.0.dist-info → job_shop_lib-1.1.2.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py CHANGED
@@ -19,7 +19,7 @@ from job_shop_lib._schedule import Schedule
19
19
  from job_shop_lib._base_solver import BaseSolver, Solver
20
20
 
21
21
 
22
- __version__ = "1.1.0"
22
+ __version__ = "1.1.2"
23
23
 
24
24
  __all__ = [
25
25
  "Operation",
@@ -13,7 +13,7 @@ from job_shop_lib import Operation
13
13
 
14
14
 
15
15
  class JobShopInstance:
16
- """Data structure to store a Job Shop Scheduling Problem instance.
16
+ r"""Data structure to store a Job Shop Scheduling Problem instance.
17
17
 
18
18
  Additional attributes such as ``num_machines`` or ``durations_matrix`` can
19
19
  be computed from the instance and are cached for performance since they
@@ -75,7 +75,7 @@ class JobShopInstance:
75
75
  attributes of the operations are set when the instance is created.
76
76
  See :meth:`set_operation_attributes` for more information. Defaults
77
77
  to True.
78
- **metadata:
78
+ \**metadata:
79
79
  Additional information about the instance.
80
80
  """
81
81
 
@@ -131,7 +131,7 @@ class JobShopInstance:
131
131
  name: str | None = None,
132
132
  **metadata: Any,
133
133
  ) -> JobShopInstance:
134
- """Creates a JobShopInstance from a file following Taillard's format.
134
+ r"""Creates a JobShopInstance from a file following Taillard's format.
135
135
 
136
136
  Args:
137
137
  file_path:
@@ -144,7 +144,7 @@ class JobShopInstance:
144
144
  name:
145
145
  A string with the name of the instance. If not provided, the
146
146
  name of the instance is set to the name of the file.
147
- **metadata:
147
+ \**metadata:
148
148
  Additional information about the instance.
149
149
 
150
150
  Returns:
@@ -221,7 +221,7 @@ class JobShopInstance:
221
221
  A list of lists of integers. The i-th list contains the
222
222
  durations of the operations of the job with id i.
223
223
  machines_matrix:
224
- A list of lists of lists of integers if the
224
+ A list of lists of lists of integers if the
225
225
  instance is flexible, or a list of lists of integers if the
226
226
  instance is not flexible. The i-th list contains the machines
227
227
  in which the operations of the job with id i can be processed.
job_shop_lib/_schedule.py CHANGED
@@ -10,7 +10,7 @@ from job_shop_lib.exceptions import ValidationError
10
10
 
11
11
 
12
12
  class Schedule:
13
- """Data structure to store a complete or partial solution for a particular
13
+ r"""Data structure to store a complete or partial solution for a particular
14
14
  :class:`JobShopInstance`.
15
15
 
16
16
  A schedule is a list of lists of :class:`ScheduledOperation` objects. Each
@@ -33,7 +33,7 @@ class Schedule:
33
33
  A list of lists of :class:`ScheduledOperation` objects. Each
34
34
  list represents the order of operations on a machine. If
35
35
  not provided, the schedule is initialized as an empty schedule.
36
- **metadata:
36
+ \**metadata:
37
37
  Additional information about the schedule.
38
38
  """
39
39
 
@@ -30,9 +30,12 @@ def no_setup_time_calculator(
30
30
  operation belongs.
31
31
 
32
32
  Args:
33
- dispatcher: The dispatcher instance.
34
- operation: The operation to be scheduled.
35
- machine_id: The id of the machine on which the operation is to be
33
+ dispatcher:
34
+ The dispatcher instance.
35
+ operation:
36
+ The operation to be scheduled.
37
+ machine_id:
38
+ The id of the machine on which the operation is to be
36
39
  scheduled.
37
40
 
38
41
  Returns:
@@ -329,8 +332,10 @@ class Dispatcher:
329
332
  :attr:`~job_shop_lib.Operation.machine_id` attribute is used.
330
333
 
331
334
  Raises:
332
- ValidationError: If the operation is not ready to be scheduled.
333
- UninitializedAttributeError: If the operation has multiple
335
+ ValidationError:
336
+ If the operation is not ready to be scheduled.
337
+ UninitializedAttributeError:
338
+ If the operation has multiple
334
339
  machines in its list and no ``machine_id`` is provided.
335
340
  """
336
341
 
@@ -408,7 +413,7 @@ class Dispatcher:
408
413
  condition: Callable[[DispatcherObserver], bool] = lambda _: True,
409
414
  **kwargs,
410
415
  ) -> ObserverType:
411
- """Creates a new observer of the specified type or returns an existing
416
+ r"""Creates a new observer of the specified type or returns an existing
412
417
  observer of the same type if it already exists in the dispatcher's list
413
418
  of observers.
414
419
 
@@ -419,7 +424,7 @@ class Dispatcher:
419
424
  A function that takes an observer and returns True if it is
420
425
  the observer to be retrieved. By default, it returns True for
421
426
  all observers.
422
- **kwargs:
427
+ \**kwargs:
423
428
  Additional keyword arguments to be passed to the observer's
424
429
  constructor.
425
430
  """
@@ -21,10 +21,13 @@ class OptimalOperationsObserver(DispatcherObserver):
21
21
  operations.
22
22
 
23
23
  Args:
24
- dispatcher: The dispatcher instance to observe.
25
- reference_schedule: A complete schedule that represents the optimal
24
+ dispatcher:
25
+ The dispatcher instance to observe.
26
+ reference_schedule:
27
+ A complete schedule that represents the optimal
26
28
  or reference solution.
27
- subscribe: If True, automatically subscribes to the dispatcher.
29
+ subscribe:
30
+ If True, automatically subscribes to the dispatcher.
28
31
 
29
32
  Raises:
30
33
  ValidationError: If the reference schedule is incomplete or if it
@@ -11,7 +11,6 @@ import numpy as np
11
11
  from numpy.typing import NDArray
12
12
  import pandas as pd
13
13
 
14
- from job_shop_lib.exceptions import ValidationError
15
14
  from job_shop_lib.dispatching import Dispatcher
16
15
  from job_shop_lib.dispatching.feature_observers import (
17
16
  FeatureObserver,
@@ -86,19 +85,11 @@ class CompositeFeatureObserver(FeatureObserver):
86
85
  for observer in dispatcher.subscribers
87
86
  if isinstance(observer, FeatureObserver)
88
87
  ]
89
- feature_types = self._get_feature_types_list(feature_types)
90
- for observer in feature_observers:
91
- if not set(observer.features.keys()).issubset(set(feature_types)):
92
- raise ValidationError(
93
- "The feature types observed by the feature observer "
94
- f"{observer.__class__.__name__} are not a subset of the "
95
- "feature types specified in the CompositeFeatureObserver."
96
- f"Observer feature types: {observer.features.keys()}"
97
- f"Composite feature types: {feature_types}"
98
- )
99
88
  self.feature_observers = feature_observers
100
89
  self.column_names: dict[FeatureType, list[str]] = defaultdict(list)
101
- super().__init__(dispatcher, subscribe=subscribe)
90
+ super().__init__(
91
+ dispatcher, subscribe=subscribe, feature_types=feature_types
92
+ )
102
93
  self._set_column_names()
103
94
 
104
95
  @classmethod
@@ -148,12 +139,17 @@ class CompositeFeatureObserver(FeatureObserver):
148
139
  list
149
140
  )
150
141
  for observer in self.feature_observers:
151
- for feature_type, feature_matrix in observer.features.items():
142
+ for feature_type in self.supported_feature_types:
143
+ feature_matrix = observer.features.get(feature_type)
144
+ if feature_matrix is None:
145
+ continue
152
146
  features[feature_type].append(feature_matrix)
153
147
 
154
148
  self.features = {
155
- feature_type: np.concatenate(features, axis=1)
156
- for feature_type, features in features.items()
149
+ feature_type: np.concatenate(
150
+ feature_matrices, axis=1 # type: ignore[misc]
151
+ )
152
+ for feature_type, feature_matrices in features.items()
157
153
  }
158
154
 
159
155
  def _set_column_names(self):
@@ -177,34 +173,3 @@ class CompositeFeatureObserver(FeatureObserver):
177
173
  out.append(f"{feature_type.value}:")
178
174
  out.append(dataframe.to_string())
179
175
  return "\n".join(out)
180
-
181
-
182
- if __name__ == "__main__":
183
- # from cProfile import Profile
184
- import time
185
- from job_shop_lib.benchmarking import load_benchmark_instance
186
- from job_shop_lib.dispatching.rules import DispatchingRuleSolver
187
-
188
- ta80 = load_benchmark_instance("ta80")
189
-
190
- dispatcher_ = Dispatcher(ta80)
191
- feature_observer_types_ = list(FeatureObserverType)
192
- feature_observers_ = [
193
- feature_observer_factory(
194
- observer_type,
195
- dispatcher=dispatcher_,
196
- )
197
- for observer_type in feature_observer_types_
198
- # and not FeatureObserverType.EARLIEST_START_TIME
199
- ]
200
- composite_observer_ = CompositeFeatureObserver(
201
- dispatcher_, feature_observers=feature_observers_
202
- )
203
- solver = DispatchingRuleSolver(dispatching_rule="random")
204
- # profiler = Profile()
205
- # profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
206
- # profiler.print_stats("cumtime")
207
- start = time.perf_counter()
208
- solver.solve(dispatcher_.instance, dispatcher_)
209
- end = time.perf_counter()
210
- print(f"Time: {end - start}")
@@ -93,8 +93,8 @@ class EarliestStartTimeObserver(FeatureObserver):
93
93
  # Cache:
94
94
  operations_by_machine = dispatcher.instance.operations_by_machine
95
95
  self._is_regular_instance = all(
96
- len(job) == len(dispatcher.instance.jobs[0])
97
- for job in dispatcher.instance.jobs
96
+ len(machine_ops) == len(operations_by_machine[0])
97
+ for machine_ops in operations_by_machine
98
98
  )
99
99
  if self._is_regular_instance:
100
100
  self._job_ids = np.array(
@@ -1,5 +1,3 @@
1
- """Contains factory functions for creating :class:`FeatureObserver`s."""
2
-
3
1
  from enum import Enum
4
2
 
5
3
  from job_shop_lib.dispatching import DispatcherObserverConfig
@@ -57,13 +55,13 @@ def feature_observer_factory(
57
55
  ),
58
56
  **kwargs,
59
57
  ) -> FeatureObserver:
60
- """Creates and returns a :class:`FeatureObserver` based on the specified
58
+ r"""Creates and returns a :class:`FeatureObserver` based on the specified
61
59
  :class:`FeatureObserver` type.
62
60
 
63
61
  Args:
64
62
  feature_creator_type:
65
63
  The type of :class:`FeatureObserver` to create.
66
- **kwargs:
64
+ \*\*kwargs:
67
65
  Additional keyword arguments to pass to the
68
66
  :class:`FeatureObserver` constructor.
69
67
 
@@ -148,49 +148,3 @@ class DispatchingRuleSolver(BaseSolver):
148
148
  selected_operation = self.dispatching_rule(dispatcher)
149
149
  machine_id = self.machine_chooser(dispatcher, selected_operation)
150
150
  dispatcher.dispatch(selected_operation, machine_id)
151
-
152
-
153
- if __name__ == "__main__":
154
- import time
155
- import cProfile
156
-
157
- # import pstats
158
- # from io import StringIO
159
- from job_shop_lib.benchmarking import (
160
- # load_benchmark_instance,
161
- load_all_benchmark_instances,
162
- )
163
-
164
- # from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
165
- # most_work_remaining_rule_2,
166
- # )
167
-
168
- # ta_instances = [
169
- # load_benchmark_instance(f"ta{i:02d}") for i in range(1, 81)
170
- # ]
171
- ta_instances = load_all_benchmark_instances().values()
172
- solver = DispatchingRuleSolver(
173
- dispatching_rule="most_work_remaining", ready_operations_filter=None
174
- )
175
-
176
- start = time.perf_counter()
177
-
178
- # Create a Profile object
179
- profiler = cProfile.Profile()
180
-
181
- # Run the code under profiling
182
- # profiler.enable()
183
- for instance_ in ta_instances:
184
- solver.solve(instance_)
185
- # profiler.disable()
186
-
187
- end = time.perf_counter()
188
-
189
- # Print elapsed time
190
- print(f"Elapsed time: {end - start:.2f} seconds.")
191
-
192
- # Print profiling results
193
- # s = StringIO()
194
- # ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
195
- # profiler.print_stats("cumtime") # Print top 20 time-consuming functions
196
- # print(s.getvalue())
@@ -129,16 +129,18 @@ class GeneralInstanceGenerator(InstanceGenerator):
129
129
  num_machines: int | None = None,
130
130
  ) -> JobShopInstance:
131
131
  if num_jobs is None:
132
- num_jobs = self.rng.integers(
133
- self.num_jobs_range[0], self.num_jobs_range[1] + 1
132
+ num_jobs = int(
133
+ self.rng.integers(
134
+ self.num_jobs_range[0], self.num_jobs_range[1] + 1
135
+ )
134
136
  )
135
137
 
136
138
  if num_machines is None:
137
139
  min_num_machines, max_num_machines = self.num_machines_range
138
140
  if not self.allow_less_jobs_than_machines:
139
141
  min_num_machines = min(num_jobs, max_num_machines)
140
- num_machines = self.rng.integers(
141
- min_num_machines, max_num_machines + 1
142
+ num_machines = int(
143
+ self.rng.integers(min_num_machines, max_num_machines + 1)
142
144
  )
143
145
  elif (
144
146
  not self.allow_less_jobs_than_machines and num_jobs < num_machines
@@ -9,6 +9,7 @@ connected with machine ones. All machine nodes are connected between them, and
9
9
  all operation nodes from the same job are connected by non-directed edges too.
10
10
 
11
11
  References:
12
+
12
13
  - Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
13
14
  solve multi-agent scheduling problems with reinforcement learning. ArXiv,
14
15
  abs/2106.03051, 2021.
@@ -166,7 +166,7 @@ class JobShopGraph:
166
166
  v_of_edge: Node | int,
167
167
  **attr,
168
168
  ) -> None:
169
- """Adds an edge to the graph.
169
+ r"""Adds an edge to the graph.
170
170
 
171
171
  It automatically determines the edge type based on the source and
172
172
  destination nodes unless explicitly provided in the ``attr`` argument
@@ -182,7 +182,7 @@ class JobShopGraph:
182
182
  The destination node of the edge. If it is a :class:`Node`,
183
183
  its ``node_id`` is used as the destination. Otherwise, it
184
184
  is assumed to be the ``node_id`` of the destination.
185
- **attr:
185
+ \**attr:
186
186
  Additional attributes to be added to the edge.
187
187
 
188
188
  Raises:
@@ -277,7 +277,8 @@ class MultiJobShopGraphEnv(gym.Env):
277
277
  options: Additional options for reset (currently unused).
278
278
 
279
279
  Returns:
280
- A tuple containing:
280
+ tuple[ObservationDict, dict[str, Any]]:
281
+
281
282
  - ObservationDict: The initial observation of the environment.
282
283
  - dict: An info dictionary containing additional information about
283
284
  the reset state. This may include details about the generated
@@ -305,7 +306,7 @@ class MultiJobShopGraphEnv(gym.Env):
305
306
  def step(
306
307
  self, action: tuple[int, int]
307
308
  ) -> tuple[ObservationDict, float, bool, bool, dict[str, Any]]:
308
- """Takes a step in the environment.
309
+ r"""Takes a step in the environment.
309
310
 
310
311
  Args:
311
312
  action:
@@ -315,7 +316,7 @@ class MultiJobShopGraphEnv(gym.Env):
315
316
  operation.
316
317
 
317
318
  Returns:
318
- A tuple containing the following elements:
319
+ tuple[ObservationDict, float, bool, bool, dict[str, Any]]:
319
320
 
320
321
  - The observation of the environment.
321
322
  - The reward obtained.
@@ -247,9 +247,11 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
247
247
  def _get_start_from_zero_mappings(
248
248
  original_indices_dict: dict[str, NDArray[np.int32]],
249
249
  ) -> dict[str, dict[int, int]]:
250
- mappings = {}
250
+ mappings: dict[str, dict[int, int]] = {}
251
251
  for key, indices in original_indices_dict.items():
252
- mappings[key] = {idx: i for i, idx in enumerate(indices)}
252
+ mappings[key] = {
253
+ idx: i for i, idx in enumerate(indices) # type: ignore[misc]
254
+ } # idx is an integer (false positive)
253
255
  return mappings
254
256
 
255
257
  def _create_node_features_dict(
@@ -318,7 +320,7 @@ class ResourceTaskGraphObservation(ObservationWrapper, Generic[EnvType]):
318
320
  ~removed_nodes_of_this_type
319
321
  ]
320
322
  original_ids_dict[node_type] = np.where(
321
- ~removed_nodes_of_this_type
323
+ ~removed_nodes_of_this_type # type: ignore[assignment]
322
324
  )[0]
323
325
 
324
326
  return removed_nodes_dict, original_ids_dict
@@ -3,10 +3,12 @@
3
3
  from copy import deepcopy
4
4
  from collections.abc import Callable, Sequence
5
5
  from typing import Any
6
+ import warnings
6
7
 
7
8
  import matplotlib.pyplot as plt
8
9
  import gymnasium as gym
9
10
  import numpy as np
11
+
10
12
  from numpy.typing import NDArray
11
13
 
12
14
  from job_shop_lib import JobShopInstance, Operation
@@ -207,17 +209,34 @@ class SingleJobShopGraphEnv(gym.Env):
207
209
  """Returns current makespan of partial schedule."""
208
210
  return self.dispatcher.schedule.makespan()
209
211
 
210
- def machine_utilization(self) -> NDArray[np.float32]:
211
- """Returns utilization percentage for each machine."""
212
+ def machine_utilization( # noqa: DOC201,DOC203
213
+ self,
214
+ ) -> NDArray[np.float32]:
215
+ """Returns utilization percentage for each machine.
216
+
217
+ Returns:
218
+ Utilization percentage for each machine as a numpy array.
219
+
220
+ .. deprecated:: 1.1.2
221
+ This method is deprecated and will be removed in version 2.0.0.
222
+ """
223
+ warnings.warn(
224
+ "machine_utilization is deprecated and will be removed in "
225
+ "version 2.0.0",
226
+ DeprecationWarning,
227
+ stacklevel=2,
228
+ )
212
229
  total_time = max(1, self.current_makespan()) # Avoid division by zero
213
- machine_busy_time = np.zeros(self.instance.num_machines)
230
+ machine_busy_time = np.zeros(
231
+ self.instance.num_machines, dtype=np.float32
232
+ )
214
233
 
215
234
  for m_id, m_schedule in enumerate(self.dispatcher.schedule.schedule):
216
235
  machine_busy_time[m_id] = sum(
217
236
  op.operation.duration for op in m_schedule
218
237
  )
219
238
 
220
- return machine_busy_time / total_time
239
+ return machine_busy_time / total_time # type: ignore[return-value]
221
240
 
222
241
  def _get_observation_space(self) -> gym.spaces.Dict:
223
242
  """Returns the observation space dictionary."""
@@ -414,28 +433,3 @@ class SingleJobShopGraphEnv(gym.Env):
414
433
  raise ValidationError(
415
434
  f"Operation {next_operation} requires a machine_id"
416
435
  )
417
-
418
-
419
- if __name__ == "__main__":
420
- from job_shop_lib.dispatching.feature_observers import (
421
- FeatureObserverType,
422
- FeatureType,
423
- )
424
- from job_shop_lib.graphs import build_disjunctive_graph
425
- from job_shop_lib.benchmarking import load_benchmark_instance
426
-
427
- instance = load_benchmark_instance("ft06")
428
- job_shop_graph_ = build_disjunctive_graph(instance)
429
- feature_observer_configs_: list[DispatcherObserverConfig] = [
430
- DispatcherObserverConfig(
431
- FeatureObserverType.IS_READY,
432
- kwargs={"feature_types": [FeatureType.JOBS]},
433
- )
434
- ]
435
-
436
- env = SingleJobShopGraphEnv(
437
- job_shop_graph=job_shop_graph_,
438
- feature_observer_configs=feature_observer_configs_,
439
- render_mode="save_video",
440
- render_config={"video_config": {"fps": 4}},
441
- )
@@ -70,14 +70,16 @@ def get_partial_gantt_chart_plotter(
70
70
  show_available_operations: bool = False,
71
71
  **kwargs: Any,
72
72
  ) -> PartialGanttChartPlotter:
73
- """Returns a function that plots a Gantt chart for an unfinished schedule.
73
+ r"""Returns a function that plots a Gantt chart for an unfinished schedule.
74
74
 
75
75
  Args:
76
- title: The title of the Gantt chart.
77
- cmap: The name of the colormap to use.
76
+ title:
77
+ The title of the Gantt chart.
78
+ cmap:
79
+ The name of the colormap to use.
78
80
  show_available_operations:
79
81
  Whether to show the available operations in the Gantt chart.
80
- **kwargs: Additional keyword arguments to pass to the
82
+ \*\*kwargs: Additional keyword arguments to pass to the
81
83
  :func:`plot_gantt_chart` function.
82
84
 
83
85
  Returns:
@@ -366,18 +366,25 @@ def plot_disjunctive_graph(
366
366
 
367
367
  # Add machine colors to the legend
368
368
  if show_machine_colors_in_legend:
369
+ label_color_pairs = []
370
+ sorted_machine_colors = sorted(
371
+ machine_colors.items(), key=lambda x: x[0]
372
+ )
373
+ for machine_id, color in sorted_machine_colors:
374
+ label = None
375
+ if machine_id == -1:
376
+ continue
377
+ if machine_labels is not None:
378
+ label = machine_labels[machine_id]
379
+ elif machine_id >= 0:
380
+ label = f"Machine {machine_id}"
381
+
382
+ if label: # Add patch if a label was determined
383
+ label_color_pairs.append((label, color))
384
+
369
385
  machine_patches = [
370
- matplotlib.patches.Patch(
371
- color=color,
372
- label=(
373
- machine_labels[machine_id]
374
- if machine_labels is not None
375
- else f"Machine {machine_id}"
376
- ),
377
- )
378
- for machine_id, color in sorted(
379
- machine_colors.items(), key=lambda x: x[0]
380
- )
386
+ matplotlib.patches.Patch(color=color, label=label)
387
+ for label, color in sorted(label_color_pairs)
381
388
  ]
382
389
  handles.extend(machine_patches)
383
390
 
@@ -86,7 +86,7 @@ def plot_resource_task_graph(
86
86
  """
87
87
  if title is None:
88
88
  title = (
89
- "Heterogeneous Graph Visualization:"
89
+ "Heterogeneous Graph Visualization: "
90
90
  f"{job_shop_graph.instance.name}"
91
91
  )
92
92
  # Create a new figure and axis
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
5
5
  License: MIT
6
6
  Author: Pabloo22
@@ -16,9 +16,10 @@ Requires-Dist: gymnasium (>=1.0.0,<2.0.0)
16
16
  Requires-Dist: imageio[ffmpeg] (>=2.34.1,<3.0.0)
17
17
  Requires-Dist: matplotlib (>=3,<4)
18
18
  Requires-Dist: networkx (>=3,<4)
19
- Requires-Dist: numpy (>=1.26.4,<2.0.0)
20
- Requires-Dist: ortools (>=9.9,<10.0)
21
- Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
19
+ Requires-Dist: numpy (>=1.26.4,<3.0.0)
20
+ Requires-Dist: ortools (>=9.9,<10.0) ; sys_platform != "darwin"
21
+ Requires-Dist: ortools (>=9.9,<9.13) ; sys_platform == "darwin"
22
+ Requires-Dist: pyarrow (>=15,<21)
22
23
  Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
23
24
  Description-Content-Type: text/markdown
24
25
 
@@ -30,6 +31,7 @@ Description-Content-Type: text/markdown
30
31
 
31
32
  [![Tests](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml/badge.svg)](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml)
32
33
  [![Documentation Status](https://readthedocs.org/projects/job-shop-lib/badge/?version=latest)](https://job-shop-lib.readthedocs.io/en/latest/?badge=latest)
34
+ [![codecov](https://codecov.io/gh/Pabloo22/job_shop_lib/graph/badge.svg?token=DWXLYJWAOZ)](https://codecov.io/gh/Pabloo22/job_shop_lib)
33
35
  ![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)
34
36
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
35
37
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -1,8 +1,8 @@
1
- job_shop_lib/__init__.py,sha256=GsvMMVRVHzH1Qy7eKfNKp87NNRO8TBE-asEgZF7TIaA,639
1
+ job_shop_lib/__init__.py,sha256=lVN0bvlkAFwi2T5vOu57gyXNOnwQftdNEwvgWJB-yQ0,639
2
2
  job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
3
- job_shop_lib/_job_shop_instance.py,sha256=RWibh5_lVTHj0DEkMw94O4jYZhwRReYYpVVIbT0cYtQ,18353
3
+ job_shop_lib/_job_shop_instance.py,sha256=FkMBy9Yb8cNEGswI9vlN3Wh4mhtEX-QuDbKvSYUOXcM,18361
4
4
  job_shop_lib/_operation.py,sha256=lwCjgXwWlgESFuV3Yh4SCVofPGCd3hJU4vnK7peREac,4235
5
- job_shop_lib/_schedule.py,sha256=pY02c-VF7cHxR29dFAktrn32mFi0cmRIcDAGsoekK8g,11941
5
+ job_shop_lib/_schedule.py,sha256=PX3wOv9Cw8NgjBLV3yDJW0mNl7a25nvoEV5Hdv7R_-g,11943
6
6
  job_shop_lib/_scheduled_operation.py,sha256=czrGr87EOTlO2NPolIN5CDigeiCzvQEyra5IZPwSFZc,2801
7
7
  job_shop_lib/benchmarking/__init__.py,sha256=JPnCw5mK7sADAW0HctVKHEDRw22afp9caNh2eUS36Ys,3290
8
8
  job_shop_lib/benchmarking/_load_benchmark.py,sha256=-cgyx0Kn6uAc3KdGFSQb6eUVQjQggmpVKOH9qusNkXI,2930
@@ -10,19 +10,19 @@ job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMh
10
10
  job_shop_lib/constraint_programming/__init__.py,sha256=kKQRUxxS_nVFUdXGnf4bQOD9mqrXxZZWElS753A4YiA,454
11
11
  job_shop_lib/constraint_programming/_ortools_solver.py,sha256=oMPeA2VHoYX1ZvmygQ8kYew40ITLAQATmM4OhgVFuXM,10482
12
12
  job_shop_lib/dispatching/__init__.py,sha256=eyiCpCWIsx3LDoSOtPImjYAkI6R8t93kz56hM03WODE,2558
13
- job_shop_lib/dispatching/_dispatcher.py,sha256=5zCTBIl96dcqAmdwp_1I_4V9ox2X4zMz5-ZHwpMvWGU,23765
13
+ job_shop_lib/dispatching/_dispatcher.py,sha256=KnV_Kry3Ie81WbKhdpRQtOMsuFDNCuh5Kp2ZnelM-R8,23835
14
14
  job_shop_lib/dispatching/_dispatcher_observer_config.py,sha256=QF2d3rJWwmvutQBAkKxzQ1toJs6eMelT404LGS2z9HQ,2467
15
15
  job_shop_lib/dispatching/_factories.py,sha256=j3MhIwVXiq-B8JMit72ObvXSa2sdgWNhUD86gghL6Gg,4689
16
16
  job_shop_lib/dispatching/_history_observer.py,sha256=Vl8rQaxekUeEB-AyNxyC3c76zQakeh-rdri2iDnZvXw,610
17
- job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=Tdclcvt_BK28eYxTzgF7Kiz1IBZAFpXRay0U62-VDHA,4211
17
+ job_shop_lib/dispatching/_optimal_operations_observer.py,sha256=2EYxevjpeGMP3do-m0ZmtmjIjmNcxrWOSKzN_bW37gQ,4247
18
18
  job_shop_lib/dispatching/_ready_operation_filters.py,sha256=brhmhoyyoZ98wAEEfneZC-CD-aw9SerZHGMB1DpK8HY,5749
19
19
  job_shop_lib/dispatching/_start_time_calculators.py,sha256=sEtInDnFW9gsKDUEDUGQBaIjDWgCkFYvBca46j8XYfE,6408
20
20
  job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=0he-j4OlvqtXAJZD5x1nuBnUKqZUfftVx9NT3CVxPyg,2708
21
21
  job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
22
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=FPVnrX9ySS9Bg7OpIP9FPdUeJ25Jtti3q3JvpkkHvqk,7915
22
+ job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=tpvqTLIcNmbYROSFT62LiUZ_tI4fHWL_qCULKK43BU4,6429
23
23
  job_shop_lib/dispatching/feature_observers/_duration_observer.py,sha256=fbkUIVScF1iNjdVCYr1ImQm53TfahvVnGXhsRAsgdzY,4129
24
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=SOdXs-uzTzcLqOsmpbKvf-OGlGXOMVVJL9zgVVVDvF8,11442
25
- job_shop_lib/dispatching/feature_observers/_factory.py,sha256=DV1IlyygtffpnIMZ2AUOUKRujshIhECV1ESL7sp0P68,3288
24
+ job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha256=AQIjVp7VRDnb5GuYZlLUwk-xiXSqbsxJW-Ji7NjLoAw,11452
25
+ job_shop_lib/dispatching/feature_observers/_factory.py,sha256=NyXYK5A1hXsYEeEqngwVRNAFkevY95DglheeqyfFv8s,3217
26
26
  job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=a7COlMWd9XC_cpEgHlsj8LEKl8Ty1g77cBm-2goSMHU,8606
27
27
  job_shop_lib/dispatching/feature_observers/_is_completed_observer.py,sha256=EYJOyWL8ApUElLucoHnFlt0g2Ior_1yO7Q8V3FU_Qog,3576
28
28
  job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=wy_pA-1wmnzVjhq92mdsT2JJHYbfsm79mcMgSgYUCOs,1264
@@ -31,21 +31,20 @@ job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py,sha256=W
31
31
  job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py,sha256=5V87lCrJUabEe8AkTGXPu5yS8OGxeN8L3-xNyHmdmLs,1441
32
32
  job_shop_lib/dispatching/rules/__init__.py,sha256=0Nn9FBVmxVYeDeLsd7g7WkmKFBYJqOIDzArbqsC7FAI,2187
33
33
  job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=5fNpv90fAoR6rcE6NeJOWiB7ir-FVnoONIhHtKJ9H0E,2904
34
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=y_H7d9u1BOXrqvRJYFgzSrYprQlg2kgcDkTeFzMuEdw,7151
34
+ job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=1_canC1lXZATrQCZaHOY3JOLmTuT6U0Z_QWzgTOLwqI,5917
35
35
  job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=OldJH2ZcLq4a36yXFRiSICkKAh3RLYlvMIqKyT1OmPE,7606
36
36
  job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=CJ74ujgWXgG8cuULWY6VJkD_b3arTcOjTNLZJTAf8xE,2346
37
37
  job_shop_lib/dispatching/rules/_utils.py,sha256=m5qw4qyfaIvVrkmv51nuhreizr98-cg8AJKt2VTd48w,4603
38
38
  job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
39
39
  job_shop_lib/generation/__init__.py,sha256=QaWwuBfBNnOiG0OPiP_CV_flBu9dX7r2o_HwL47tREM,822
40
- job_shop_lib/generation/_general_instance_generator.py,sha256=Njd2vxkmSmgY-1XK-3qm_V-EnjaW_em99A7k8yR6KmU,6496
40
+ job_shop_lib/generation/_general_instance_generator.py,sha256=b_tnyP4H_buoN7b6lKQRLvDkeZDdys0mpqS3thB5-SQ,6544
41
41
  job_shop_lib/generation/_instance_generator.py,sha256=rT7CAJuv6E0zbmRFE_MFY6iaeZB06BshsBHl2_GyPzU,4567
42
- job_shop_lib/generation/_transformations.py,sha256=ZigQTBsS3xgB2FhBu9MpsFs7A-_VY3840V_RtOIhCBk,5296
43
42
  job_shop_lib/generation/_utils.py,sha256=TYBGt4Zjw94l6ukIjXBVAK3lmrrZXdyzyq_r1DMlL-E,3986
44
43
  job_shop_lib/graphs/__init__.py,sha256=wlYIiXTuZRE6Kx3K0RpPUoZikzoegBuN2hcdqMODtGk,2433
45
44
  job_shop_lib/graphs/_build_disjunctive_graph.py,sha256=UbUYdeQaaeEqLchcKJGHEFGl4wElfGLb1o_R-u8wqnA,5120
46
- job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=mWg8C-62aqvAwIKsreAHLYIq-VOc0q7BEnOnlUrywb8,6961
45
+ job_shop_lib/graphs/_build_resource_task_graphs.py,sha256=vIy_EkQjgQAd5YyJxKAuGf7CLTjgCfhz-fYrObF4DTU,6962
47
46
  job_shop_lib/graphs/_constants.py,sha256=K-GeVvh_DTWpo1KOX1clmxWS_pkUJbq19yOBmrCVIxI,1086
48
- job_shop_lib/graphs/_job_shop_graph.py,sha256=4qgalP5HsVEhSxRi0UKDhBu3F_UGZV-W6G2b79tpCvA,11425
47
+ job_shop_lib/graphs/_job_shop_graph.py,sha256=--9sbPpCiqC71kzmsPWFvMfqpx_gq4TL2x0HI2d-TEM,11427
49
48
  job_shop_lib/graphs/_node.py,sha256=Ue3_BqVPU4w9S70kDChfsMJ09spnW7Dg83osSzi7fko,5990
50
49
  job_shop_lib/graphs/graph_updaters/__init__.py,sha256=YOwb0RYypO9cEG-Nl3Ooj1yvAoyWDMNE_NAaUTyjzIw,658
51
50
  job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py,sha256=-t0T8W-Jz9TJQR9-ljPkcDsDC4CwJAfs2nUF3zjEtuw,4369
@@ -54,21 +53,21 @@ job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py,sha256=9NG3pu7Z5h-
54
53
  job_shop_lib/graphs/graph_updaters/_utils.py,sha256=sdw2Vo75P9c6Fy-YBlfgpXb9gPwHUluTB1E-9WINm_g,730
55
54
  job_shop_lib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
55
  job_shop_lib/reinforcement_learning/__init__.py,sha256=sAVgxylKfBnn2rrz0BFcab1kjvQQ1h-hgldfbkPF--E,1537
57
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=nv9F94SF5f7Xe3WZwkVG-3YD1JwipTvdL88whOT1oBg,15702
58
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=Tvgf7a-W7h_RTJ7lmoiVjed7S05wo5BVg8CFUqV1l9s,12769
56
+ job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=6nXw67Tfmim3LqlSuQ9Cfg3mMY-VmbMHuXfyOL90jng,15740
57
+ job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py,sha256=ZqN6kzuXbO0BdA1UKrTEHeiHAKzRNIeuH-dBt90ttEc,12914
59
58
  job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=4Kdyn9Jlp_1sBtVr6raF-ZFtcnKxwyCLykfX53TmuhU,2959
60
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=bSnZJS4kH6YcALbkM_oVFk6paixKVKvmdSvJzTi54A0,16700
59
+ job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=MnQYCVpwX4WHiGhYguHziDUrPIrKmXsjOUDoTmuoCBc,16403
61
60
  job_shop_lib/reinforcement_learning/_types_and_constants.py,sha256=6FpuQkZLV2H8_dXmax49OTgAw7dWQcUEWVWWdMLR7bs,1752
62
61
  job_shop_lib/reinforcement_learning/_utils.py,sha256=aHgNdW7YvUH8QM3l7NGIfrgzfpzGoklyYm1jM2Isi6Q,6043
63
62
  job_shop_lib/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
63
  job_shop_lib/visualization/gantt/__init__.py,sha256=xMvuNph6bfwulHYqqklCj_6SUQgRzvC92Yul75F3Zlg,1250
65
64
  job_shop_lib/visualization/gantt/_gantt_chart_creator.py,sha256=FgE4SmKLYKnS7dfTFgnBklWhwGyIo0DKWVkmxusDmp8,8606
66
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py,sha256=-6O5pNFn8frFGhPGztAFDPUUxImR06F03NY6BAA8lj8,14544
65
+ job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py,sha256=jessyvLFgS60HEEmO7o6imo2f0uEpCLkyo-p_SBpE7Y,14571
67
66
  job_shop_lib/visualization/gantt/_plot_gantt_chart.py,sha256=_4UGUTRuIw0tLzsJD9Gcf10aIy2YkDzTGsNTzFb5r0Y,6809
68
67
  job_shop_lib/visualization/graphs/__init__.py,sha256=HUWzfgQLeklNROtjnxeJX_FIySo_baTXO6klx0zUVpQ,630
69
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=YyNEKgg1ol34JYu8Ej1kKonsuFu7zDav4uuUzU_FS4Q,15905
70
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=Sp1aokyM_8QZb39QCLrjQECcbDHkdcYw7wMZILWxN9o,13191
71
- job_shop_lib-1.1.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
72
- job_shop_lib-1.1.0.dist-info/METADATA,sha256=VOiEg8x4xozLpf04qjhX0NBEsnDrbzEUPtUi89GqE10,16405
73
- job_shop_lib-1.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
74
- job_shop_lib-1.1.0.dist-info/RECORD,,
68
+ job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py,sha256=L9_ZGgvCFpGc2rTOdZESdtydFQqShjqedimIOhqZx6Y,16209
69
+ job_shop_lib/visualization/graphs/_plot_resource_task_graph.py,sha256=nkkdZ-9_OBevw72Frecwzv1y3WyhGZ9r9lz0y9MXvZ8,13192
70
+ job_shop_lib-1.1.2.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
71
+ job_shop_lib-1.1.2.dist-info/METADATA,sha256=i8mPexr3bUMJz7bKPC0D0VYKsFzYdMksnK4eOJFCQ20,16624
72
+ job_shop_lib-1.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
73
+ job_shop_lib-1.1.2.dist-info/RECORD,,
@@ -1,167 +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
- Args:
117
- min_jobs:
118
- The minimum number of jobs to remain in the instance.
119
- max_jobs:
120
- The maximum number of jobs to remain in the instance.
121
- target_jobs:
122
- If specified, the number of jobs to remain in the
123
- instance. Overrides ``min_jobs`` and ``max_jobs``.
124
- """
125
-
126
- def __init__(
127
- self,
128
- min_jobs: int,
129
- max_jobs: int,
130
- target_jobs: int | None = None,
131
- suffix: str | None = None,
132
- ):
133
- if suffix is None:
134
- suffix = f"_jobs={min_jobs}-{max_jobs}"
135
- super().__init__(suffix=suffix)
136
- self.min_jobs = min_jobs
137
- self.max_jobs = max_jobs
138
- self.target_jobs = target_jobs
139
-
140
- def apply(self, instance: JobShopInstance) -> JobShopInstance:
141
- if self.target_jobs is None:
142
- target_jobs = random.randint(self.min_jobs, self.max_jobs)
143
- else:
144
- target_jobs = self.target_jobs
145
- new_jobs = copy.deepcopy(instance.jobs)
146
-
147
- while len(new_jobs) > target_jobs:
148
- new_jobs.pop(random.randint(0, len(new_jobs) - 1))
149
-
150
- return JobShopInstance(new_jobs, instance.name)
151
-
152
- @staticmethod
153
- def remove_job(
154
- instance: JobShopInstance, job_index: int
155
- ) -> JobShopInstance:
156
- """Removes a specific job from the instance.
157
-
158
- Args:
159
- instance: The JobShopInstance from which to remove the job.
160
- job_index: The index of the job to remove.
161
-
162
- Returns:
163
- A new JobShopInstance with the specified job removed.
164
- """
165
- new_jobs = copy.deepcopy(instance.jobs)
166
- new_jobs.pop(job_index)
167
- return JobShopInstance(new_jobs, instance.name)