job-shop-lib 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|