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,122 @@
|
|
1
|
+
import abc
|
2
|
+
|
3
|
+
import random
|
4
|
+
from typing import Iterator
|
5
|
+
|
6
|
+
from job_shop_lib import JobShopInstance
|
7
|
+
|
8
|
+
|
9
|
+
class InstanceGenerator(abc.ABC):
|
10
|
+
"""Common interface for all generators.
|
11
|
+
|
12
|
+
The class supports both single instance generation and iteration over
|
13
|
+
multiple instances, controlled by the `iteration_limit` parameter. It
|
14
|
+
implements the iterator protocol, allowing it to be used in a `for` loop.
|
15
|
+
|
16
|
+
Note:
|
17
|
+
When used as an iterator, the generator will produce instances until it
|
18
|
+
reaches the specified `iteration_limit`. If `iteration_limit` is None,
|
19
|
+
it will continue indefinitely.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
num_jobs_range:
|
23
|
+
The range of the number of jobs to generate. If a single
|
24
|
+
int is provided, it is used as both the minimum and maximum.
|
25
|
+
duration_range:
|
26
|
+
The range of durations for each operation.
|
27
|
+
num_machines_range:
|
28
|
+
The range of the number of machines available. If a
|
29
|
+
single int is provided, it is used as both the minimum and maximum.
|
30
|
+
name_suffix:
|
31
|
+
A suffix to append to each instance's name for identification.
|
32
|
+
seed:
|
33
|
+
Seed for the random number generator to ensure reproducibility.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__( # pylint: disable=too-many-arguments
|
37
|
+
self,
|
38
|
+
num_jobs: int | tuple[int, int] = (10, 20),
|
39
|
+
num_machines: int | tuple[int, int] = (5, 10),
|
40
|
+
duration_range: tuple[int, int] = (1, 99),
|
41
|
+
name_suffix: str = "generated_instance",
|
42
|
+
seed: int | None = None,
|
43
|
+
iteration_limit: int | None = None,
|
44
|
+
):
|
45
|
+
"""Initializes the instance generator with the given parameters.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
num_jobs:
|
49
|
+
The range of the number of jobs to generate.
|
50
|
+
num_machines:
|
51
|
+
The range of the number of machines available.
|
52
|
+
duration_range:
|
53
|
+
The range of durations for each operation.
|
54
|
+
name_suffix:
|
55
|
+
Suffix for instance names.
|
56
|
+
seed:
|
57
|
+
Seed for the random number generator.
|
58
|
+
iteration_limit:
|
59
|
+
Maximum number of instances to generate in iteration mode.
|
60
|
+
"""
|
61
|
+
|
62
|
+
if isinstance(num_jobs, int):
|
63
|
+
num_jobs = (num_jobs, num_jobs)
|
64
|
+
if isinstance(num_machines, int):
|
65
|
+
num_machines = (num_machines, num_machines)
|
66
|
+
if seed is not None:
|
67
|
+
random.seed(seed)
|
68
|
+
|
69
|
+
self.num_jobs_range = num_jobs
|
70
|
+
self.num_machines_range = num_machines
|
71
|
+
self.duration_range = duration_range
|
72
|
+
self.name_suffix = name_suffix
|
73
|
+
|
74
|
+
self._counter = 0
|
75
|
+
self._current_iteration = 0
|
76
|
+
self._iteration_limit = iteration_limit
|
77
|
+
|
78
|
+
@abc.abstractmethod
|
79
|
+
def generate(self) -> JobShopInstance:
|
80
|
+
"""Generates a single job shop instance"""
|
81
|
+
|
82
|
+
def _next_name(self) -> str:
|
83
|
+
self._counter += 1
|
84
|
+
return f"{self.name_suffix}_{self._counter}"
|
85
|
+
|
86
|
+
def __iter__(self) -> Iterator[JobShopInstance]:
|
87
|
+
self._current_iteration = 0
|
88
|
+
return self
|
89
|
+
|
90
|
+
def __next__(self) -> JobShopInstance:
|
91
|
+
if (
|
92
|
+
self._iteration_limit is not None
|
93
|
+
and self._current_iteration >= self._iteration_limit
|
94
|
+
):
|
95
|
+
raise StopIteration
|
96
|
+
self._current_iteration += 1
|
97
|
+
return self.generate()
|
98
|
+
|
99
|
+
def __len__(self) -> int:
|
100
|
+
if self._iteration_limit is None:
|
101
|
+
raise ValueError("Iteration limit is not set.")
|
102
|
+
return self._iteration_limit
|
103
|
+
|
104
|
+
@property
|
105
|
+
def max_num_jobs(self) -> int:
|
106
|
+
"""Returns the maximum number of jobs that can be generated."""
|
107
|
+
return self.num_jobs_range[1]
|
108
|
+
|
109
|
+
@property
|
110
|
+
def min_num_jobs(self) -> int:
|
111
|
+
"""Returns the minimum number of jobs that can be generated."""
|
112
|
+
return self.num_jobs_range[0]
|
113
|
+
|
114
|
+
@property
|
115
|
+
def max_num_machines(self) -> int:
|
116
|
+
"""Returns the maximum number of machines that can be generated."""
|
117
|
+
return self.num_machines_range[1]
|
118
|
+
|
119
|
+
@property
|
120
|
+
def min_num_machines(self) -> int:
|
121
|
+
"""Returns the minimum number of machines that can be generated."""
|
122
|
+
return self.num_machines_range[0]
|
@@ -0,0 +1,164 @@
|
|
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)
|
@@ -9,6 +9,9 @@ from job_shop_lib import JobShopInstance, Operation
|
|
9
9
|
class BasicGenerator: # pylint: disable=too-many-instance-attributes
|
10
10
|
"""Generates instances for job shop problems.
|
11
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
|
+
|
12
15
|
This class is designed to be versatile, enabling the creation of various
|
13
16
|
job shop instances without the need for multiple dedicated classes.
|
14
17
|
|
@@ -23,6 +23,26 @@ from job_shop_lib.graphs import JobShopGraph, EdgeType, NodeType, Node
|
|
23
23
|
|
24
24
|
|
25
25
|
def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
|
26
|
+
"""Builds and returns a disjunctive graph for the given job shop instance.
|
27
|
+
|
28
|
+
This function creates a complete disjunctive graph from a JobShopInstance.
|
29
|
+
It starts by initializing a JobShopGraph object and proceeds by adding
|
30
|
+
disjunctive edges between operations using the same machine, conjunctive
|
31
|
+
edges between successive operations in the same job, and finally, special
|
32
|
+
source and sink nodes with their respective edges to and from all other
|
33
|
+
operations.
|
34
|
+
|
35
|
+
Edges have a "type" attribute indicating whether they are disjunctive or
|
36
|
+
conjunctive.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
instance (JobShopInstance): The job shop instance for which to build
|
40
|
+
the graph.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
JobShopGraph: A JobShopGraph object representing the disjunctive graph
|
44
|
+
of the job shop scheduling problem.
|
45
|
+
"""
|
26
46
|
graph = JobShopGraph(instance)
|
27
47
|
add_disjunctive_edges(graph)
|
28
48
|
add_conjunctive_edges(graph)
|
@@ -6,6 +6,8 @@ import os
|
|
6
6
|
import functools
|
7
7
|
from typing import Any
|
8
8
|
|
9
|
+
import numpy as np
|
10
|
+
|
9
11
|
from job_shop_lib import Operation
|
10
12
|
|
11
13
|
|
@@ -264,6 +266,58 @@ class JobShopInstance:
|
|
264
266
|
[operation.machine_id for operation in job] for job in self.jobs
|
265
267
|
]
|
266
268
|
|
269
|
+
@functools.cached_property
|
270
|
+
def durations_matrix_array(self) -> np.ndarray:
|
271
|
+
"""Returns the duration matrix of the instance as a numpy array.
|
272
|
+
|
273
|
+
The returned array has shape (num_jobs, max_num_operations_per_job).
|
274
|
+
Non-existing operations are filled with np.nan.
|
275
|
+
|
276
|
+
Example:
|
277
|
+
>>> jobs = [[Operation(0, 2), Operation(1, 3)], [Operation(0, 4)]]
|
278
|
+
>>> instance = JobShopInstance(jobs)
|
279
|
+
>>> instance.durations_matrix_array
|
280
|
+
array([[ 2., 2.],
|
281
|
+
[ 4., nan]], dtype=float32)
|
282
|
+
"""
|
283
|
+
duration_matrix = self.durations_matrix
|
284
|
+
return self._fill_matrix_with_nans_2d(duration_matrix)
|
285
|
+
|
286
|
+
@functools.cached_property
|
287
|
+
def machines_matrix_array(self) -> np.ndarray:
|
288
|
+
"""Returns the machines matrix of the instance as a numpy array.
|
289
|
+
|
290
|
+
The returned array has shape (num_jobs, max_num_operations_per_job,
|
291
|
+
max_num_machines_per_operation). Non-existing machines are filled with
|
292
|
+
np.nan.
|
293
|
+
|
294
|
+
Example:
|
295
|
+
>>> jobs = [
|
296
|
+
... [Operation(machines=[0, 1], 2), Operation(machines=1, 3)],
|
297
|
+
... [Operation(machines=0, 6)],
|
298
|
+
... ]
|
299
|
+
>>> instance = JobShopInstance(jobs)
|
300
|
+
>>> instance.machines_matrix_array
|
301
|
+
array([[[ 0., 1.],
|
302
|
+
[ 1., nan]],
|
303
|
+
[[ 0., nan],
|
304
|
+
[nan, nan]]], dtype=float32)
|
305
|
+
"""
|
306
|
+
|
307
|
+
machines_matrix = self.machines_matrix
|
308
|
+
if self.is_flexible:
|
309
|
+
# False positive from mypy, the type of machines_matrix is
|
310
|
+
# list[list[list[int]]] here
|
311
|
+
return self._fill_matrix_with_nans_3d(
|
312
|
+
machines_matrix # type: ignore[arg-type]
|
313
|
+
)
|
314
|
+
|
315
|
+
# False positive from mypy, the type of machines_matrix is
|
316
|
+
# list[list[int]] here
|
317
|
+
return self._fill_matrix_with_nans_2d(
|
318
|
+
machines_matrix # type: ignore[arg-type]
|
319
|
+
)
|
320
|
+
|
267
321
|
@functools.cached_property
|
268
322
|
def operations_by_machine(self) -> list[list[Operation]]:
|
269
323
|
"""Returns a list of lists of operations.
|
@@ -353,3 +407,50 @@ class JobShopInstance:
|
|
353
407
|
def total_duration(self) -> int:
|
354
408
|
"""Returns the sum of the durations of all operations in all jobs."""
|
355
409
|
return sum(self.job_durations)
|
410
|
+
|
411
|
+
@staticmethod
|
412
|
+
def _fill_matrix_with_nans_2d(matrix: list[list[int]]) -> np.ndarray:
|
413
|
+
"""Fills a matrix with np.nan values.
|
414
|
+
|
415
|
+
Args:
|
416
|
+
matrix:
|
417
|
+
A list of lists of integers.
|
418
|
+
|
419
|
+
Returns:
|
420
|
+
A numpy array with the same shape as the input matrix, filled with
|
421
|
+
np.nan values.
|
422
|
+
"""
|
423
|
+
max_length = max(len(row) for row in matrix)
|
424
|
+
squared_matrix = np.full(
|
425
|
+
(len(matrix), max_length), np.nan, dtype=np.float32
|
426
|
+
)
|
427
|
+
for i, row in enumerate(matrix):
|
428
|
+
squared_matrix[i, : len(row)] = row
|
429
|
+
return squared_matrix
|
430
|
+
|
431
|
+
@staticmethod
|
432
|
+
def _fill_matrix_with_nans_3d(matrix: list[list[list[int]]]) -> np.ndarray:
|
433
|
+
"""Fills a 3D matrix with np.nan values.
|
434
|
+
|
435
|
+
Args:
|
436
|
+
matrix:
|
437
|
+
A list of lists of lists of integers.
|
438
|
+
|
439
|
+
Returns:
|
440
|
+
A numpy array with the same shape as the input matrix, filled with
|
441
|
+
np.nan values.
|
442
|
+
"""
|
443
|
+
max_length = max(len(row) for row in matrix)
|
444
|
+
max_inner_length = len(matrix[0][0])
|
445
|
+
for row in matrix:
|
446
|
+
for inner_row in row:
|
447
|
+
max_inner_length = max(max_inner_length, len(inner_row))
|
448
|
+
squared_matrix = np.full(
|
449
|
+
(len(matrix), max_length, max_inner_length),
|
450
|
+
np.nan,
|
451
|
+
dtype=np.float32,
|
452
|
+
)
|
453
|
+
for i, row in enumerate(matrix):
|
454
|
+
for j, inner_row in enumerate(row):
|
455
|
+
squared_matrix[i, j, : len(inner_row)] = inner_row
|
456
|
+
return squared_matrix
|
@@ -27,7 +27,8 @@ def create_gif(
|
|
27
27
|
instance: JobShopInstance,
|
28
28
|
solver: DispatchingRuleSolver,
|
29
29
|
plot_function: (
|
30
|
-
Callable[[Schedule, int, list[Operation] | None
|
30
|
+
Callable[[Schedule, int, list[Operation] | None, int | None], Figure]
|
31
|
+
| None
|
31
32
|
) = None,
|
32
33
|
fps: int = 1,
|
33
34
|
remove_frames: bool = True,
|
@@ -80,50 +81,59 @@ def plot_gantt_chart_wrapper(
|
|
80
81
|
title: str | None = None,
|
81
82
|
cmap: str = "viridis",
|
82
83
|
show_available_operations: bool = False,
|
83
|
-
) -> Callable[[Schedule, int, list[Operation] | None], Figure]:
|
84
|
+
) -> Callable[[Schedule, int, list[Operation] | None, int | None], Figure]:
|
84
85
|
"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
85
86
|
|
86
87
|
Args:
|
87
88
|
title: The title of the Gantt chart.
|
88
89
|
cmap: The name of the colormap to use.
|
90
|
+
show_available_operations:
|
91
|
+
Whether to show the available operations in the Gantt chart.
|
89
92
|
|
90
93
|
Returns:
|
91
94
|
A function that plots a Gantt chart for a schedule. The function takes
|
92
|
-
|
93
|
-
|
95
|
+
the following arguments:
|
96
|
+
- schedule: The schedule to plot.
|
97
|
+
- makespan: The makespan of the schedule.
|
98
|
+
- available_operations: A list of available operations. If None,
|
99
|
+
the available operations are not shown.
|
100
|
+
- current_time: The current time in the schedule. If provided, a
|
101
|
+
red vertical line is plotted at this time.
|
94
102
|
"""
|
95
103
|
|
96
104
|
def plot_function(
|
97
105
|
schedule: Schedule,
|
98
106
|
makespan: int,
|
99
107
|
available_operations: list | None = None,
|
108
|
+
current_time: int | None = None,
|
100
109
|
) -> Figure:
|
101
110
|
fig, ax = plot_gantt_chart(
|
102
111
|
schedule, title=title, cmap_name=cmap, xlim=makespan
|
103
112
|
)
|
104
113
|
|
105
|
-
if
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
114
|
+
if show_available_operations and available_operations is not None:
|
115
|
+
|
116
|
+
operations_text = "\n".join(
|
117
|
+
str(operation) for operation in available_operations
|
118
|
+
)
|
119
|
+
text = f"Available operations:\n{operations_text}"
|
120
|
+
# Print the available operations at the bottom right corner
|
121
|
+
# of the Gantt chart
|
122
|
+
fig.text(
|
123
|
+
1.25,
|
124
|
+
0.05,
|
125
|
+
text,
|
126
|
+
ha="right",
|
127
|
+
va="bottom",
|
128
|
+
transform=ax.transAxes,
|
129
|
+
bbox={
|
130
|
+
"facecolor": "white",
|
131
|
+
"alpha": 0.5,
|
132
|
+
"boxstyle": "round,pad=0.5",
|
133
|
+
},
|
134
|
+
)
|
135
|
+
if current_time is not None:
|
136
|
+
ax.axvline(current_time, color="red", linestyle="--")
|
127
137
|
return fig
|
128
138
|
|
129
139
|
return plot_function
|
@@ -133,7 +143,9 @@ def create_gantt_chart_frames(
|
|
133
143
|
frames_dir: str,
|
134
144
|
instance: JobShopInstance,
|
135
145
|
solver: DispatchingRuleSolver,
|
136
|
-
plot_function: Callable[
|
146
|
+
plot_function: Callable[
|
147
|
+
[Schedule, int, list[Operation] | None, int | None], Figure
|
148
|
+
],
|
137
149
|
plot_current_time: bool = True,
|
138
150
|
) -> None:
|
139
151
|
"""Creates frames of the Gantt chart for the schedule being built.
|
@@ -150,7 +162,8 @@ def create_gantt_chart_frames(
|
|
150
162
|
should take a `Schedule` object and the makespan of the schedule as
|
151
163
|
input and return a `Figure` object.
|
152
164
|
plot_current_time:
|
153
|
-
Whether to plot a vertical line at the current time.
|
165
|
+
Whether to plot a vertical line at the current time.
|
166
|
+
"""
|
154
167
|
dispatcher = Dispatcher(instance, pruning_function=solver.pruning_function)
|
155
168
|
history_tracker = HistoryTracker(dispatcher)
|
156
169
|
makespan = solver.solve(instance, dispatcher).makespan()
|
@@ -160,23 +173,19 @@ def create_gantt_chart_frames(
|
|
160
173
|
dispatcher.dispatch(
|
161
174
|
scheduled_operation.operation, scheduled_operation.machine_id
|
162
175
|
)
|
176
|
+
current_time = (
|
177
|
+
None if not plot_current_time else dispatcher.current_time()
|
178
|
+
)
|
163
179
|
fig = plot_function(
|
164
180
|
dispatcher.schedule,
|
165
181
|
makespan,
|
166
182
|
dispatcher.available_operations(),
|
183
|
+
current_time,
|
167
184
|
)
|
168
|
-
|
169
|
-
None if not plot_current_time else dispatcher.current_time()
|
170
|
-
)
|
171
|
-
_save_frame(fig, frames_dir, i, current_time)
|
172
|
-
|
185
|
+
_save_frame(fig, frames_dir, i)
|
173
186
|
|
174
|
-
def _save_frame(
|
175
|
-
figure: Figure, frames_dir: str, number: int, current_time: int | None
|
176
|
-
) -> None:
|
177
|
-
if current_time is not None:
|
178
|
-
figure.gca().axvline(current_time, color="red", linestyle="--")
|
179
187
|
|
188
|
+
def _save_frame(figure: Figure, frames_dir: str, number: int) -> None:
|
180
189
|
figure.savefig(f"{frames_dir}/frame_{number:02d}.png", bbox_inches="tight")
|
181
190
|
plt.close(figure)
|
182
191
|
|
@@ -65,7 +65,7 @@ def _plot_machine_schedules(
|
|
65
65
|
) -> dict[int, Patch]:
|
66
66
|
"""Plots the schedules for each machine."""
|
67
67
|
max_job_id = schedule.instance.num_jobs - 1
|
68
|
-
cmap = plt.
|
68
|
+
cmap = plt.get_cmap(cmap_name, max_job_id + 1)
|
69
69
|
norm = Normalize(vmin=0, vmax=max_job_id)
|
70
70
|
legend_handles = {}
|
71
71
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.1
|
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
|
@@ -15,7 +15,8 @@ Provides-Extra: pygraphviz
|
|
15
15
|
Requires-Dist: imageio (>=2,<3)
|
16
16
|
Requires-Dist: matplotlib (>=3,<4)
|
17
17
|
Requires-Dist: networkx (>=3,<4)
|
18
|
-
Requires-Dist:
|
18
|
+
Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
19
|
+
Requires-Dist: ortools (>=9.9,<9.10)
|
19
20
|
Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
|
20
21
|
Requires-Dist: pygraphviz (>=1.12,<2.0) ; extra == "pygraphviz"
|
21
22
|
Description-Content-Type: text/markdown
|
@@ -83,7 +84,7 @@ ft06 = load_benchmark_instance("ft06")
|
|
83
84
|
```
|
84
85
|
|
85
86
|
The module `benchmarks` contains functions to load the instances from the file and return them as `JobShopInstance` objects without having to download them
|
86
|
-
manually.
|
87
|
+
manually.
|
87
88
|
|
88
89
|
The contributions to this benchmark dataset are as follows:
|
89
90
|
|
@@ -171,13 +172,15 @@ class DispatchingRule(str, Enum):
|
|
171
172
|
We can visualize the solution with a `DispatchingRuleSolver` as a gif:
|
172
173
|
|
173
174
|
```python
|
174
|
-
from job_shop_lib.visualization import create_gif,
|
175
|
+
from job_shop_lib.visualization import create_gif, plot_gantt_chart_wrapper
|
175
176
|
from job_shop_lib.dispatching import DispatchingRuleSolver, DispatchingRule
|
176
177
|
|
177
178
|
plt.style.use("ggplot")
|
178
179
|
|
179
180
|
mwkr_solver = DispatchingRuleSolver("most_work_remaining")
|
180
|
-
plot_function =
|
181
|
+
plot_function = plot_gantt_chart_wrapper(
|
182
|
+
title="Solution with Most Work Remaining Rule"
|
183
|
+
)
|
181
184
|
create_gif(
|
182
185
|
gif_path="ft06_optimized.gif",
|
183
186
|
instance=ft06,
|
@@ -350,3 +353,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
350
353
|
Journal of Operational Research, vol. 64, no. 2, pp. 278–285, 1993.
|
351
354
|
|
352
355
|
- Park, Junyoung, Sanjar Bakhtiyar, and Jinkyoo Park. "ScheduleNet: Learn to solve multi-agent scheduling problems with reinforcement learning." arXiv preprint arXiv:2106.03051, 2021.
|
356
|
+
|