job-shop-lib 1.0.0a2__py3-none-any.whl → 1.0.0a3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/_job_shop_instance.py +18 -20
- job_shop_lib/_operation.py +6 -4
- job_shop_lib/_schedule.py +3 -3
- job_shop_lib/_scheduled_operation.py +2 -2
- job_shop_lib/dispatching/__init__.py +4 -0
- job_shop_lib/dispatching/_dispatcher.py +24 -32
- job_shop_lib/dispatching/_factories.py +8 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +1 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +44 -25
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +1 -1
- job_shop_lib/visualization/__init__.py +5 -5
- job_shop_lib/visualization/_gantt_chart_creator.py +5 -5
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +62 -35
- job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a3.dist-info}/METADATA +15 -3
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a3.dist-info}/RECORD +20 -20
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a3.dist-info}/WHEEL +1 -1
- {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a3.dist-info}/LICENSE +0 -0
@@ -20,15 +20,27 @@ class JobShopInstance:
|
|
20
20
|
computations.
|
21
21
|
|
22
22
|
Attributes:
|
23
|
-
jobs:
|
23
|
+
jobs (list[list[Operation]]):
|
24
24
|
A list of lists of operations. Each list of operations represents
|
25
25
|
a job, and the operations are ordered by their position in the job.
|
26
26
|
The `job_id`, `position_in_job`, and `operation_id` attributes of
|
27
27
|
the operations are set when the instance is created.
|
28
|
-
name:
|
28
|
+
name (str):
|
29
29
|
A string with the name of the instance.
|
30
|
-
metadata:
|
30
|
+
metadata (dict[str, Any]):
|
31
31
|
A dictionary with additional information about the instance.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
jobs:
|
35
|
+
A list of lists of operations. Each list of operations
|
36
|
+
represents a job, and the operations are ordered by their
|
37
|
+
position in the job. The `job_id`, `position_in_job`, and
|
38
|
+
`operation_id` attributes of the operations are set when the
|
39
|
+
instance is created.
|
40
|
+
name:
|
41
|
+
A string with the name of the instance.
|
42
|
+
**metadata:
|
43
|
+
Additional information about the instance.
|
32
44
|
"""
|
33
45
|
|
34
46
|
def __init__(
|
@@ -37,24 +49,10 @@ class JobShopInstance:
|
|
37
49
|
name: str = "JobShopInstance",
|
38
50
|
**metadata: Any,
|
39
51
|
):
|
40
|
-
|
41
|
-
|
42
|
-
Args:
|
43
|
-
jobs:
|
44
|
-
A list of lists of operations. Each list of operations
|
45
|
-
represents a job, and the operations are ordered by their
|
46
|
-
position in the job. The `job_id`, `position_in_job`, and
|
47
|
-
`operation_id` attributes of the operations are set when the
|
48
|
-
instance is created.
|
49
|
-
name:
|
50
|
-
A string with the name of the instance.
|
51
|
-
**metadata:
|
52
|
-
Additional information about the instance.
|
53
|
-
"""
|
54
|
-
self.jobs = jobs
|
52
|
+
self.jobs: list[list[Operation]] = jobs
|
55
53
|
self.set_operation_attributes()
|
56
|
-
self.name = name
|
57
|
-
self.metadata = metadata
|
54
|
+
self.name: str = name
|
55
|
+
self.metadata: dict[str, Any] = metadata
|
58
56
|
|
59
57
|
def set_operation_attributes(self):
|
60
58
|
"""Sets the job_id and position of each operation."""
|
job_shop_lib/_operation.py
CHANGED
@@ -51,8 +51,10 @@ class Operation:
|
|
51
51
|
}
|
52
52
|
|
53
53
|
def __init__(self, machines: int | list[int], duration: int):
|
54
|
-
self.machines
|
55
|
-
|
54
|
+
self.machines: list[int] = (
|
55
|
+
[machines] if isinstance(machines, int) else machines
|
56
|
+
)
|
57
|
+
self.duration: int = duration
|
56
58
|
|
57
59
|
# Defined outside the class by the JobShopInstance class:
|
58
60
|
self.job_id: int = -1
|
@@ -64,8 +66,8 @@ class Operation:
|
|
64
66
|
"""Returns the id of the machine associated with the operation.
|
65
67
|
|
66
68
|
Raises:
|
67
|
-
UninitializedAttributeError:
|
68
|
-
|
69
|
+
UninitializedAttributeError:
|
70
|
+
If the operation has multiple machines in its list.
|
69
71
|
"""
|
70
72
|
if len(self.machines) > 1:
|
71
73
|
raise UninitializedAttributeError(
|
job_shop_lib/_schedule.py
CHANGED
@@ -46,7 +46,7 @@ class Schedule:
|
|
46
46
|
self,
|
47
47
|
instance: JobShopInstance,
|
48
48
|
schedule: list[list[ScheduledOperation]] | None = None,
|
49
|
-
**metadata,
|
49
|
+
**metadata: Any,
|
50
50
|
):
|
51
51
|
"""Initializes the object with the given instance and schedule.
|
52
52
|
|
@@ -65,9 +65,9 @@ class Schedule:
|
|
65
65
|
|
66
66
|
Schedule.check_schedule(schedule)
|
67
67
|
|
68
|
-
self.instance = instance
|
68
|
+
self.instance: JobShopInstance = instance
|
69
69
|
self._schedule = schedule
|
70
|
-
self.metadata = metadata
|
70
|
+
self.metadata: dict[str, Any] = metadata
|
71
71
|
|
72
72
|
def __repr__(self) -> str:
|
73
73
|
return str(self.schedule)
|
@@ -31,8 +31,8 @@ class ScheduledOperation:
|
|
31
31
|
If the given machine_id is not in the list of valid machines
|
32
32
|
for the operation.
|
33
33
|
"""
|
34
|
-
self.operation = operation
|
35
|
-
self.start_time = start_time
|
34
|
+
self.operation: Operation = operation
|
35
|
+
self.start_time: int = start_time
|
36
36
|
self._machine_id = machine_id
|
37
37
|
self.machine_id = machine_id # Validate machine_id
|
38
38
|
|
@@ -32,6 +32,8 @@ from ._ready_operation_filters import (
|
|
32
32
|
filter_dominated_operations,
|
33
33
|
filter_non_immediate_machines,
|
34
34
|
ReadyOperationsFilter,
|
35
|
+
filter_non_idle_machines,
|
36
|
+
filter_non_immediate_operations,
|
35
37
|
)
|
36
38
|
from ._dispatcher_observer_config import DispatcherObserverConfig
|
37
39
|
from ._factories import (
|
@@ -53,4 +55,6 @@ __all__ = [
|
|
53
55
|
"DispatcherObserverConfig",
|
54
56
|
"UnscheduledOperationsObserver",
|
55
57
|
"ReadyOperationsFilter",
|
58
|
+
"filter_non_idle_machines",
|
59
|
+
"filter_non_immediate_operations",
|
56
60
|
]
|
@@ -156,26 +156,30 @@ class Dispatcher:
|
|
156
156
|
responsible for scheduling the operations on the machines and keeping
|
157
157
|
track of the next available time for each machine and job.
|
158
158
|
|
159
|
-
|
159
|
+
Args:
|
160
160
|
instance:
|
161
|
-
The instance of the job shop problem to be
|
162
|
-
schedule:
|
163
|
-
The schedule of operations on machines.
|
161
|
+
The instance of the job shop problem to be solved.
|
164
162
|
ready_operations_filter:
|
165
|
-
A function that filters out operations that are not ready to
|
166
|
-
scheduled.
|
163
|
+
A function that filters out operations that are not ready to
|
164
|
+
be scheduled. The function should take the dispatcher and a
|
165
|
+
list of operations as input and return a list of operations
|
166
|
+
that are ready to be scheduled. If ``None``, no filtering is
|
167
|
+
done.
|
167
168
|
"""
|
168
169
|
|
169
|
-
__slots__ =
|
170
|
-
"instance",
|
171
|
-
"schedule",
|
172
|
-
"_machine_next_available_time",
|
173
|
-
"_job_next_operation_index",
|
174
|
-
"_job_next_available_time",
|
175
|
-
"ready_operations_filter"
|
176
|
-
|
177
|
-
|
178
|
-
|
170
|
+
__slots__ = {
|
171
|
+
"instance": "The instance of the job shop problem to be scheduled.",
|
172
|
+
"schedule": "The schedule of operations on machines.",
|
173
|
+
"_machine_next_available_time": "",
|
174
|
+
"_job_next_operation_index": "",
|
175
|
+
"_job_next_available_time": "",
|
176
|
+
"ready_operations_filter": (
|
177
|
+
"A function that filters out operations that are not ready to be "
|
178
|
+
"scheduled."
|
179
|
+
),
|
180
|
+
"subscribers": "A list of observers subscribed to the dispatcher.",
|
181
|
+
"_cache": "A dictionary to cache the results of the cached methods.",
|
182
|
+
}
|
179
183
|
|
180
184
|
def __init__(
|
181
185
|
self,
|
@@ -184,18 +188,6 @@ class Dispatcher:
|
|
184
188
|
Callable[[Dispatcher, list[Operation]], list[Operation]] | None
|
185
189
|
) = None,
|
186
190
|
) -> None:
|
187
|
-
"""Initializes the object with the given instance.
|
188
|
-
|
189
|
-
Args:
|
190
|
-
instance:
|
191
|
-
The instance of the job shop problem to be solved.
|
192
|
-
ready_operations_filter:
|
193
|
-
A function that filters out operations that are not ready to
|
194
|
-
be scheduled. The function should take the dispatcher and a
|
195
|
-
list of operations as input and return a list of operations
|
196
|
-
that are ready to be scheduled. If ``None``, no filtering is
|
197
|
-
done.
|
198
|
-
"""
|
199
191
|
|
200
192
|
self.instance = instance
|
201
193
|
self.schedule = Schedule(self.instance)
|
@@ -371,7 +363,7 @@ class Dispatcher:
|
|
371
363
|
The current time is the minimum start time of the available
|
372
364
|
operations.
|
373
365
|
"""
|
374
|
-
available_operations = self.
|
366
|
+
available_operations = self.available_operations()
|
375
367
|
current_time = self.min_start_time(available_operations)
|
376
368
|
return current_time
|
377
369
|
|
@@ -387,7 +379,7 @@ class Dispatcher:
|
|
387
379
|
return int(min_start_time)
|
388
380
|
|
389
381
|
@_dispatcher_cache
|
390
|
-
def
|
382
|
+
def available_operations(self) -> list[Operation]:
|
391
383
|
"""Returns a list of available operations for processing, optionally
|
392
384
|
filtering out operations using the filter function.
|
393
385
|
|
@@ -443,7 +435,7 @@ class Dispatcher:
|
|
443
435
|
@_dispatcher_cache
|
444
436
|
def available_machines(self) -> list[int]:
|
445
437
|
"""Returns the list of ready machines."""
|
446
|
-
available_operations = self.
|
438
|
+
available_operations = self.available_operations()
|
447
439
|
available_machines = set()
|
448
440
|
for operation in available_operations:
|
449
441
|
available_machines.update(operation.machines)
|
@@ -452,7 +444,7 @@ class Dispatcher:
|
|
452
444
|
@_dispatcher_cache
|
453
445
|
def available_jobs(self) -> list[int]:
|
454
446
|
"""Returns the list of ready jobs."""
|
455
|
-
available_operations = self.
|
447
|
+
available_operations = self.available_operations()
|
456
448
|
available_jobs = set(
|
457
449
|
operation.job_id for operation in available_operations
|
458
450
|
)
|
@@ -13,6 +13,8 @@ from job_shop_lib.dispatching import (
|
|
13
13
|
Dispatcher,
|
14
14
|
filter_dominated_operations,
|
15
15
|
filter_non_immediate_machines,
|
16
|
+
filter_non_idle_machines,
|
17
|
+
filter_non_immediate_operations,
|
16
18
|
ReadyOperationsFilter,
|
17
19
|
)
|
18
20
|
|
@@ -27,6 +29,8 @@ class ReadyOperationsFilterType(str, Enum):
|
|
27
29
|
|
28
30
|
DOMINATED_OPERATIONS = "dominated_operations"
|
29
31
|
NON_IMMEDIATE_MACHINES = "non_immediate_machines"
|
32
|
+
NON_IDLE_MACHINES = "non_idle_machines"
|
33
|
+
NON_IMMEDIATE_OPERATIONS = "non_immediate_operations"
|
30
34
|
|
31
35
|
|
32
36
|
def create_composite_operation_filter(
|
@@ -114,6 +118,10 @@ def ready_operations_filter_factory(
|
|
114
118
|
ReadyOperationsFilterType.NON_IMMEDIATE_MACHINES: (
|
115
119
|
filter_non_immediate_machines
|
116
120
|
),
|
121
|
+
ReadyOperationsFilterType.NON_IDLE_MACHINES: filter_non_idle_machines,
|
122
|
+
ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS: (
|
123
|
+
filter_non_immediate_operations
|
124
|
+
),
|
117
125
|
}
|
118
126
|
|
119
127
|
if filter_name not in filtering_strategies:
|
@@ -15,6 +15,86 @@ ReadyOperationsFilter = Callable[
|
|
15
15
|
]
|
16
16
|
|
17
17
|
|
18
|
+
def filter_non_idle_machines(
|
19
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
20
|
+
) -> list[Operation]:
|
21
|
+
"""Filters out all the operations associated with non-idle machines.
|
22
|
+
|
23
|
+
A machine is considered idle if there are no ongoing operations
|
24
|
+
currently scheduled on it. This filter removes operations that are
|
25
|
+
associated with machines that are busy (i.e., have at least one
|
26
|
+
uncompleted operation).
|
27
|
+
|
28
|
+
Utilizes :meth:``Dispatcher.ongoing_operations()`` to determine machine
|
29
|
+
statuses.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
dispatcher: The dispatcher object.
|
33
|
+
operations: The list of operations to filter.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
The list of operations that are associated with idle machines.
|
37
|
+
"""
|
38
|
+
current_time = dispatcher.min_start_time(operations)
|
39
|
+
non_idle_machines = _get_non_idle_machines(dispatcher, current_time)
|
40
|
+
|
41
|
+
# Filter operations to keep those that are associated with at least one
|
42
|
+
# idle machine
|
43
|
+
filtered_operations: list[Operation] = []
|
44
|
+
for operation in operations:
|
45
|
+
if all(
|
46
|
+
machine_id in non_idle_machines
|
47
|
+
for machine_id in operation.machines
|
48
|
+
):
|
49
|
+
continue
|
50
|
+
filtered_operations.append(operation)
|
51
|
+
|
52
|
+
return filtered_operations
|
53
|
+
|
54
|
+
|
55
|
+
def _get_non_idle_machines(
|
56
|
+
dispatcher: Dispatcher, current_time: int
|
57
|
+
) -> set[int]:
|
58
|
+
"""Returns the set of machine ids that are currently busy (i.e., have at
|
59
|
+
least one uncompleted operation)."""
|
60
|
+
|
61
|
+
non_idle_machines = set()
|
62
|
+
for machine_schedule in dispatcher.schedule.schedule:
|
63
|
+
for scheduled_operation in reversed(machine_schedule):
|
64
|
+
is_completed = scheduled_operation.end_time <= current_time
|
65
|
+
if is_completed:
|
66
|
+
break
|
67
|
+
non_idle_machines.add(scheduled_operation.machine_id)
|
68
|
+
|
69
|
+
return non_idle_machines
|
70
|
+
|
71
|
+
|
72
|
+
def filter_non_immediate_operations(
|
73
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
74
|
+
) -> list[Operation]:
|
75
|
+
"""Filters out all the operations that can't start immediately.
|
76
|
+
|
77
|
+
An operation can start immediately if its earliest start time is the
|
78
|
+
current time.
|
79
|
+
|
80
|
+
The current time is determined by the minimum start time of the
|
81
|
+
operations.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
dispatcher: The dispatcher object.
|
85
|
+
operations: The list of operations to filter.
|
86
|
+
"""
|
87
|
+
|
88
|
+
min_start_time = dispatcher.min_start_time(operations)
|
89
|
+
immediate_operations: list[Operation] = []
|
90
|
+
for operation in operations:
|
91
|
+
start_time = dispatcher.earliest_start_time(operation)
|
92
|
+
if start_time == min_start_time:
|
93
|
+
immediate_operations.append(operation)
|
94
|
+
|
95
|
+
return immediate_operations
|
96
|
+
|
97
|
+
|
18
98
|
def filter_dominated_operations(
|
19
99
|
dispatcher: Dispatcher, operations: list[Operation]
|
20
100
|
) -> list[Operation]:
|
@@ -29,5 +29,5 @@ class IsReadyObserver(FeatureObserver):
|
|
29
29
|
self.initialize_features()
|
30
30
|
|
31
31
|
def _get_ready_operations(self) -> list[int]:
|
32
|
-
available_operations = self.dispatcher.
|
32
|
+
available_operations = self.dispatcher.available_operations()
|
33
33
|
return [operation.operation_id for operation in available_operations]
|
@@ -1,12 +1,14 @@
|
|
1
1
|
"""Home of the `DispatchingRuleSolver` class."""
|
2
2
|
|
3
|
-
from collections.abc import Callable
|
3
|
+
from collections.abc import Callable, Iterable
|
4
4
|
|
5
5
|
from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
|
6
6
|
from job_shop_lib.dispatching import (
|
7
7
|
ready_operations_filter_factory,
|
8
8
|
Dispatcher,
|
9
9
|
ReadyOperationsFilterType,
|
10
|
+
ReadyOperationsFilter,
|
11
|
+
create_composite_operation_filter,
|
10
12
|
)
|
11
13
|
from job_shop_lib.dispatching.rules import (
|
12
14
|
dispatching_rule_factory,
|
@@ -30,6 +32,35 @@ class DispatchingRuleSolver(BaseSolver):
|
|
30
32
|
pruning_function:
|
31
33
|
The pruning function to use. It is used to initialize the
|
32
34
|
dispatcher object internally when calling the solve method.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
dispatching_rule:
|
38
|
+
The dispatching rule to use. It can be a string with the name
|
39
|
+
of the dispatching rule, a class`DispatchingRuleType` enum member,
|
40
|
+
or a callable that takes a dispatcher and returns the operation to
|
41
|
+
be dispatched next.
|
42
|
+
machine_chooser:
|
43
|
+
The machine chooser to use. It can be a string with the name
|
44
|
+
of the machine chooser, a :class:`MachineChooserType` member, or a
|
45
|
+
callable that takes a dispatcher and an operation and returns
|
46
|
+
the machine id where the operation will be dispatched.
|
47
|
+
ready_operations_filter:
|
48
|
+
The ready operations filter to use. It can be either:
|
49
|
+
|
50
|
+
- a string with the name of the pruning function
|
51
|
+
- a :class`ReadyOperationsFilterType` enum member.
|
52
|
+
- a callable that takes a dispatcher and a list of operations
|
53
|
+
and returns a list of operations that should be considered
|
54
|
+
for dispatching,
|
55
|
+
- a list with names or actual ready operations filters to be used.
|
56
|
+
If a list is provided, a composite filter will be created
|
57
|
+
using the specified filters.
|
58
|
+
|
59
|
+
.. seealso::
|
60
|
+
- :func:`job_shop_lib.dispatching.rules.dispatching_rule_factory`
|
61
|
+
- :func:`job_shop_lib.dispatching.rules.machine_chooser_factory`
|
62
|
+
- :func:`~job_shop_lib.dispatching.ready_operations_filter_factory`
|
63
|
+
- :func:`~job_shop_lib.dispatching.create_composite_operation_filter`
|
33
64
|
"""
|
34
65
|
|
35
66
|
def __init__(
|
@@ -41,32 +72,16 @@ class DispatchingRuleSolver(BaseSolver):
|
|
41
72
|
str | Callable[[Dispatcher, Operation], int]
|
42
73
|
) = MachineChooserType.FIRST,
|
43
74
|
ready_operations_filter: (
|
44
|
-
str
|
45
|
-
|
|
75
|
+
Iterable[ReadyOperationsFilter | str | ReadyOperationsFilterType]
|
76
|
+
| str
|
77
|
+
| ReadyOperationsFilterType
|
78
|
+
| ReadyOperationsFilter
|
46
79
|
| None
|
47
|
-
) =
|
80
|
+
) = (
|
81
|
+
ReadyOperationsFilterType.DOMINATED_OPERATIONS,
|
82
|
+
ReadyOperationsFilterType.NON_IDLE_MACHINES,
|
83
|
+
),
|
48
84
|
):
|
49
|
-
"""Initializes the solver with the given dispatching rule, machine
|
50
|
-
chooser and pruning function.
|
51
|
-
|
52
|
-
Args:
|
53
|
-
dispatching_rule:
|
54
|
-
The dispatching rule to use. It can be a string with the name
|
55
|
-
of the dispatching rule, a DispatchingRule enum member, or a
|
56
|
-
callable that takes a dispatcher and returns the operation to
|
57
|
-
be dispatched next.
|
58
|
-
machine_chooser:
|
59
|
-
The machine chooser to use. It can be a string with the name
|
60
|
-
of the machine chooser, a MachineChooser enum member, or a
|
61
|
-
callable that takes a dispatcher and an operation and returns
|
62
|
-
the machine id where the operation will be dispatched.
|
63
|
-
ready_operations_filter:
|
64
|
-
The ready operations filter to use. It can be a string with
|
65
|
-
the name of the pruning function, a PruningFunction enum
|
66
|
-
member, or a callable that takes a dispatcher and a list of
|
67
|
-
operations and returns a list of operations that should be
|
68
|
-
considered for dispatching.
|
69
|
-
"""
|
70
85
|
if isinstance(dispatching_rule, str):
|
71
86
|
dispatching_rule = dispatching_rule_factory(dispatching_rule)
|
72
87
|
if isinstance(machine_chooser, str):
|
@@ -75,6 +90,10 @@ class DispatchingRuleSolver(BaseSolver):
|
|
75
90
|
ready_operations_filter = ready_operations_filter_factory(
|
76
91
|
ready_operations_filter
|
77
92
|
)
|
93
|
+
if isinstance(ready_operations_filter, Iterable):
|
94
|
+
ready_operations_filter = create_composite_operation_filter(
|
95
|
+
ready_operations_filter
|
96
|
+
)
|
78
97
|
|
79
98
|
self.dispatching_rule = dispatching_rule
|
80
99
|
self.machine_chooser = machine_chooser
|
@@ -21,7 +21,7 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
21
21
|
def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
22
22
|
"""Dispatches the operation with the shortest duration."""
|
23
23
|
return min(
|
24
|
-
dispatcher.
|
24
|
+
dispatcher.available_operations(),
|
25
25
|
key=lambda operation: operation.duration,
|
26
26
|
)
|
27
27
|
|
@@ -29,7 +29,7 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
|
|
29
29
|
def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
|
30
30
|
"""Dispatches the operation with the lowest position in job."""
|
31
31
|
return min(
|
32
|
-
dispatcher.
|
32
|
+
dispatcher.available_operations(),
|
33
33
|
key=lambda operation: operation.position_in_job,
|
34
34
|
)
|
35
35
|
|
@@ -41,7 +41,7 @@ def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
|
41
41
|
job_remaining_work[operation.job_id] += operation.duration
|
42
42
|
|
43
43
|
return max(
|
44
|
-
dispatcher.
|
44
|
+
dispatcher.available_operations(),
|
45
45
|
key=lambda operation: job_remaining_work[operation.job_id],
|
46
46
|
)
|
47
47
|
|
@@ -53,14 +53,14 @@ def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
|
|
53
53
|
job_remaining_operations[operation.job_id] += 1
|
54
54
|
|
55
55
|
return max(
|
56
|
-
dispatcher.
|
56
|
+
dispatcher.available_operations(),
|
57
57
|
key=lambda operation: job_remaining_operations[operation.job_id],
|
58
58
|
)
|
59
59
|
|
60
60
|
|
61
61
|
def random_operation_rule(dispatcher: Dispatcher) -> Operation:
|
62
62
|
"""Dispatches a random operation."""
|
63
|
-
return random.choice(dispatcher.
|
63
|
+
return random.choice(dispatcher.available_operations())
|
64
64
|
|
65
65
|
|
66
66
|
def score_based_rule(
|
@@ -80,7 +80,7 @@ def score_based_rule(
|
|
80
80
|
def rule(dispatcher: Dispatcher) -> Operation:
|
81
81
|
scores = score_function(dispatcher)
|
82
82
|
return max(
|
83
|
-
dispatcher.
|
83
|
+
dispatcher.available_operations(),
|
84
84
|
key=lambda operation: scores[operation.job_id],
|
85
85
|
)
|
86
86
|
|
@@ -102,7 +102,7 @@ def score_based_rule_with_tie_breaker(
|
|
102
102
|
"""
|
103
103
|
|
104
104
|
def rule(dispatcher: Dispatcher) -> Operation:
|
105
|
-
candidates = dispatcher.
|
105
|
+
candidates = dispatcher.available_operations()
|
106
106
|
for scoring_function in score_functions:
|
107
107
|
scores = scoring_function(dispatcher)
|
108
108
|
best_score = max(scores)
|
@@ -126,7 +126,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
|
|
126
126
|
"""Scores each job based on the duration of the next operation."""
|
127
127
|
num_jobs = dispatcher.instance.num_jobs
|
128
128
|
scores = [0] * num_jobs
|
129
|
-
for operation in dispatcher.
|
129
|
+
for operation in dispatcher.available_operations():
|
130
130
|
scores[operation.job_id] = -operation.duration
|
131
131
|
return scores
|
132
132
|
|
@@ -135,7 +135,7 @@ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
|
|
135
135
|
"""Scores each job based on the position of the next operation."""
|
136
136
|
num_jobs = dispatcher.instance.num_jobs
|
137
137
|
scores = [0] * num_jobs
|
138
|
-
for operation in dispatcher.
|
138
|
+
for operation in dispatcher.available_operations():
|
139
139
|
scores[operation.job_id] = operation.operation_id
|
140
140
|
return scores
|
141
141
|
|
@@ -260,7 +260,7 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
260
260
|
truncated = False
|
261
261
|
info: dict[str, Any] = {
|
262
262
|
"feature_names": self.composite_observer.column_names,
|
263
|
-
"available_operations": self.dispatcher.
|
263
|
+
"available_operations": self.dispatcher.available_operations(),
|
264
264
|
}
|
265
265
|
return obs, reward, done, truncated, info
|
266
266
|
|
@@ -1,11 +1,11 @@
|
|
1
1
|
"""Package for visualization."""
|
2
2
|
|
3
|
-
from job_shop_lib.visualization.
|
3
|
+
from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
|
4
4
|
from job_shop_lib.visualization._gantt_chart_video_and_gif_creation import (
|
5
|
-
|
5
|
+
create_gantt_chart_gif,
|
6
6
|
create_gantt_chart_video,
|
7
7
|
create_gantt_chart_frames,
|
8
|
-
|
8
|
+
get_partial_gantt_chart_plotter,
|
9
9
|
create_video_from_frames,
|
10
10
|
create_gif_from_frames,
|
11
11
|
)
|
@@ -26,9 +26,9 @@ from job_shop_lib.visualization._gantt_chart_creator import (
|
|
26
26
|
__all__ = [
|
27
27
|
"plot_gantt_chart",
|
28
28
|
"create_gantt_chart_video",
|
29
|
-
"
|
29
|
+
"create_gantt_chart_gif",
|
30
30
|
"create_gantt_chart_frames",
|
31
|
-
"
|
31
|
+
"get_partial_gantt_chart_plotter",
|
32
32
|
"create_gif_from_frames",
|
33
33
|
"create_video_from_frames",
|
34
34
|
"plot_disjunctive_graph",
|
@@ -10,8 +10,8 @@ from job_shop_lib.dispatching import (
|
|
10
10
|
)
|
11
11
|
from job_shop_lib.visualization import (
|
12
12
|
create_gantt_chart_video,
|
13
|
-
|
14
|
-
|
13
|
+
get_partial_gantt_chart_plotter,
|
14
|
+
create_gantt_chart_gif,
|
15
15
|
)
|
16
16
|
|
17
17
|
|
@@ -146,7 +146,7 @@ class GanttChartCreator:
|
|
146
146
|
self.history_observer: HistoryObserver = (
|
147
147
|
dispatcher.create_or_get_observer(HistoryObserver)
|
148
148
|
)
|
149
|
-
self.plot_function =
|
149
|
+
self.plot_function = get_partial_gantt_chart_plotter(
|
150
150
|
**self.gannt_chart_wrapper_config
|
151
151
|
)
|
152
152
|
|
@@ -175,7 +175,7 @@ class GanttChartCreator:
|
|
175
175
|
return self.plot_function(
|
176
176
|
self.schedule,
|
177
177
|
None,
|
178
|
-
self.dispatcher.
|
178
|
+
self.dispatcher.available_operations(),
|
179
179
|
self.dispatcher.current_time(),
|
180
180
|
)
|
181
181
|
|
@@ -196,7 +196,7 @@ class GanttChartCreator:
|
|
196
196
|
The configuration for the GIF creation can be customized through the
|
197
197
|
`gif_config` attribute.
|
198
198
|
"""
|
199
|
-
|
199
|
+
create_gantt_chart_gif(
|
200
200
|
instance=self.history_observer.dispatcher.instance,
|
201
201
|
schedule_history=self.history_observer.history,
|
202
202
|
plot_function=self.plot_function,
|
@@ -3,8 +3,7 @@
|
|
3
3
|
import os
|
4
4
|
import pathlib
|
5
5
|
import shutil
|
6
|
-
from
|
7
|
-
from typing import Sequence
|
6
|
+
from typing import Sequence, Protocol
|
8
7
|
|
9
8
|
import imageio
|
10
9
|
import matplotlib.pyplot as plt
|
@@ -23,19 +22,49 @@ from job_shop_lib.dispatching import (
|
|
23
22
|
HistoryObserver,
|
24
23
|
)
|
25
24
|
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
26
|
-
from job_shop_lib.visualization.
|
27
|
-
|
25
|
+
from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
|
26
|
+
|
27
|
+
|
28
|
+
# This class serves as a more meaningful type hint than simply:
|
29
|
+
# PlotFunction = Callable[
|
30
|
+
# [Schedule, int | None, list[Operation] | None, int | None], Figure
|
31
|
+
# ]
|
32
|
+
# That's why it doesn't have more methods or attributes. It is a protocol
|
33
|
+
# for functions, not for classes.
|
34
|
+
# pylint: disable=too-few-public-methods
|
35
|
+
class PartialGanttChartPlotter(Protocol):
|
36
|
+
"""A protocol for a function that plots an uncompleted Gantt chart
|
37
|
+
for a schedule.
|
38
|
+
|
39
|
+
This kind of functions are created using the
|
40
|
+
:func:`plot_gantt_chart_wrapper` function.
|
41
|
+
|
42
|
+
The function should take the following arguments:
|
43
|
+
|
44
|
+
- schedule: The schedule to plot.
|
45
|
+
- makespan: The makespan of the schedule if known. Can be used to fix the
|
46
|
+
x-axis limits.
|
47
|
+
- available_operations: A list of available operations. If ``None``,
|
48
|
+
the available operations are not shown.
|
49
|
+
- current_time: The current time in the schedule. If provided, a red
|
50
|
+
vertical line is plotted at this time.
|
51
|
+
"""
|
28
52
|
|
29
|
-
|
30
|
-
|
31
|
-
|
53
|
+
def __call__(
|
54
|
+
self,
|
55
|
+
schedule: Schedule,
|
56
|
+
makespan: int | None = None,
|
57
|
+
available_operations: list[Operation] | None = None,
|
58
|
+
current_time: int | None = None,
|
59
|
+
) -> Figure:
|
60
|
+
pass
|
32
61
|
|
33
62
|
|
34
|
-
def
|
63
|
+
def get_partial_gantt_chart_plotter(
|
35
64
|
title: str | None = None,
|
36
65
|
cmap: str = "viridis",
|
37
66
|
show_available_operations: bool = False,
|
38
|
-
) ->
|
67
|
+
) -> PartialGanttChartPlotter:
|
39
68
|
"""Returns a function that plots a Gantt chart for an unfinished schedule.
|
40
69
|
|
41
70
|
Args:
|
@@ -47,12 +76,13 @@ def plot_gantt_chart_wrapper(
|
|
47
76
|
Returns:
|
48
77
|
A function that plots a Gantt chart for a schedule. The function takes
|
49
78
|
the following arguments:
|
79
|
+
|
50
80
|
- schedule: The schedule to plot.
|
51
81
|
- makespan: The makespan of the schedule.
|
52
82
|
- available_operations: A list of available operations. If None,
|
53
|
-
|
83
|
+
the available operations are not shown.
|
54
84
|
- current_time: The current time in the schedule. If provided, a
|
55
|
-
|
85
|
+
red vertical line is plotted at this time.
|
56
86
|
|
57
87
|
"""
|
58
88
|
|
@@ -97,33 +127,30 @@ def plot_gantt_chart_wrapper(
|
|
97
127
|
# Most of the arguments are optional with default values. There is no way to
|
98
128
|
# reduce the number of arguments without losing functionality.
|
99
129
|
# pylint: disable=too-many-arguments
|
100
|
-
def
|
101
|
-
gif_path: str | None,
|
130
|
+
def create_gantt_chart_gif(
|
102
131
|
instance: JobShopInstance,
|
132
|
+
gif_path: str | None = None,
|
103
133
|
solver: DispatchingRuleSolver | None = None,
|
104
|
-
plot_function:
|
134
|
+
plot_function: PartialGanttChartPlotter | None = None,
|
105
135
|
fps: int = 1,
|
106
136
|
remove_frames: bool = True,
|
107
137
|
frames_dir: str | None = None,
|
108
138
|
plot_current_time: bool = True,
|
109
139
|
schedule_history: Sequence[ScheduledOperation] | None = None,
|
110
140
|
) -> None:
|
111
|
-
"""Creates a GIF of the schedule being built
|
112
|
-
|
113
|
-
Deprecated since version 0.6.0: Use `create_gif_or_video` instead.
|
141
|
+
"""Creates a GIF of the schedule being built.
|
114
142
|
|
115
143
|
Args:
|
116
|
-
gif_path:
|
117
|
-
The path to save the GIF file. It should end with ".gif". If not
|
118
|
-
provided, the name of the instance is used. It will be made an
|
119
|
-
optional argument in version 1.0.0.
|
120
144
|
instance:
|
121
145
|
The instance of the job shop problem to be scheduled.
|
146
|
+
gif_path:
|
147
|
+
The path to save the GIF file. It should end with ".gif". If not
|
148
|
+
provided, the name of the instance is used.
|
122
149
|
solver:
|
123
150
|
The dispatching rule solver to use. If not provided, the history
|
124
151
|
of scheduled operations should be provided.
|
125
152
|
plot_function:
|
126
|
-
A
|
153
|
+
A :class:`PlotFunction` that plots a Gantt chart for a schedule. It
|
127
154
|
should take a `Schedule` object and the makespan of the schedule as
|
128
155
|
input and return a `Figure` object. If not provided, a default
|
129
156
|
function is used.
|
@@ -133,7 +160,7 @@ def create_gif(
|
|
133
160
|
Whether to remove the frames after creating the GIF.
|
134
161
|
frames_dir:
|
135
162
|
The directory to save the frames in. If not provided,
|
136
|
-
|
163
|
+
``gif_path.replace(".gif", "") + "_frames"`` is used.
|
137
164
|
plot_current_time:
|
138
165
|
Whether to plot a vertical line at the current time.
|
139
166
|
schedule_history:
|
@@ -144,7 +171,7 @@ def create_gif(
|
|
144
171
|
gif_path = f"{instance.name}_gantt_chart.gif"
|
145
172
|
|
146
173
|
if plot_function is None:
|
147
|
-
plot_function =
|
174
|
+
plot_function = get_partial_gantt_chart_plotter()
|
148
175
|
|
149
176
|
if frames_dir is None:
|
150
177
|
# Use the name of the GIF file as the directory name
|
@@ -173,14 +200,14 @@ def create_gantt_chart_video(
|
|
173
200
|
instance: JobShopInstance,
|
174
201
|
video_path: str | None = None,
|
175
202
|
solver: DispatchingRuleSolver | None = None,
|
176
|
-
plot_function:
|
203
|
+
plot_function: PartialGanttChartPlotter | None = None,
|
177
204
|
fps: int = 1,
|
178
205
|
remove_frames: bool = True,
|
179
206
|
frames_dir: str | None = None,
|
180
207
|
plot_current_time: bool = True,
|
181
208
|
schedule_history: Sequence[ScheduledOperation] | None = None,
|
182
209
|
) -> None:
|
183
|
-
"""Creates a
|
210
|
+
"""Creates a video of the schedule being built.
|
184
211
|
|
185
212
|
Args:
|
186
213
|
instance:
|
@@ -192,16 +219,16 @@ def create_gantt_chart_video(
|
|
192
219
|
of scheduled operations should be provided.
|
193
220
|
plot_function:
|
194
221
|
A function that plots a Gantt chart for a schedule. It
|
195
|
-
should take a
|
196
|
-
input and return a
|
197
|
-
function is used.
|
222
|
+
should take a :class:`Schedule` object and the makespan of the
|
223
|
+
schedule as input and return a ``Figure`` object. If not provided,
|
224
|
+
a default function is used.
|
198
225
|
fps:
|
199
|
-
The number of frames per second in the
|
226
|
+
The number of frames per second in the video.
|
200
227
|
remove_frames:
|
201
|
-
Whether to remove the frames after creating the
|
228
|
+
Whether to remove the frames after creating the video.
|
202
229
|
frames_dir:
|
203
230
|
The directory to save the frames in. If not provided,
|
204
|
-
|
231
|
+
``name_without_the_extension + "_frames"`` is used.
|
205
232
|
plot_current_time:
|
206
233
|
Whether to plot a vertical line at the current time.
|
207
234
|
schedule_history:
|
@@ -212,7 +239,7 @@ def create_gantt_chart_video(
|
|
212
239
|
video_path = f"{instance.name}_gantt_chart.mp4"
|
213
240
|
|
214
241
|
if plot_function is None:
|
215
|
-
plot_function =
|
242
|
+
plot_function = get_partial_gantt_chart_plotter()
|
216
243
|
|
217
244
|
if frames_dir is None:
|
218
245
|
extension = video_path.split(".")[-1]
|
@@ -238,7 +265,7 @@ def create_gantt_chart_frames(
|
|
238
265
|
frames_dir: str,
|
239
266
|
instance: JobShopInstance,
|
240
267
|
solver: DispatchingRuleSolver | None,
|
241
|
-
plot_function:
|
268
|
+
plot_function: PartialGanttChartPlotter,
|
242
269
|
plot_current_time: bool = True,
|
243
270
|
schedule_history: Sequence[ScheduledOperation] | None = None,
|
244
271
|
) -> None:
|
@@ -295,7 +322,7 @@ def create_gantt_chart_frames(
|
|
295
322
|
fig = plot_function(
|
296
323
|
dispatcher.schedule,
|
297
324
|
makespan,
|
298
|
-
dispatcher.
|
325
|
+
dispatcher.available_operations(),
|
299
326
|
current_time,
|
300
327
|
)
|
301
328
|
_save_frame(fig, frames_dir, i)
|
@@ -20,16 +20,39 @@ def plot_gantt_chart(
|
|
20
20
|
cmap_name: str = "viridis",
|
21
21
|
xlim: int | None = None,
|
22
22
|
number_of_x_ticks: int = 15,
|
23
|
+
job_labels: None | list[str] = None,
|
24
|
+
machine_labels: None | list[str] = None,
|
25
|
+
legend_title: str = "",
|
26
|
+
x_label: str = "Time units",
|
27
|
+
y_label: str = "Machines",
|
23
28
|
) -> tuple[Figure, plt.Axes]:
|
24
29
|
"""Plots a Gantt chart for the schedule.
|
25
30
|
|
31
|
+
This function generates a Gantt chart that visualizes the schedule of jobs
|
32
|
+
across multiple machines. Each job is represented with a unique color,
|
33
|
+
and operations are plotted as bars on the corresponding machines over time.
|
34
|
+
|
35
|
+
The Gantt chart helps to understand the flow of jobs on machines and
|
36
|
+
visualize the makespan of the schedule, i.e., the time it takes to
|
37
|
+
complete all jobs.
|
38
|
+
|
39
|
+
The Gantt chart includes:
|
40
|
+
|
41
|
+
- X-axis: Time units, representing the progression of the schedule.
|
42
|
+
- Y-axis: Machines, which are assigned jobs at various time slots.
|
43
|
+
- Legend: A list of jobs, labeled and color-coded for clarity.
|
44
|
+
|
45
|
+
.. note::
|
46
|
+
The last tick on the x-axis always represents the makespan for easy
|
47
|
+
identification of the completion time.
|
48
|
+
|
26
49
|
Args:
|
27
50
|
schedule:
|
28
51
|
The schedule to plot.
|
29
52
|
title:
|
30
53
|
The title of the plot. If not provided, the title:
|
31
|
-
|
32
|
-
is used.
|
54
|
+
``f"Gantt Chart for {schedule.instance.name} instance"``
|
55
|
+
is used. To remove the title, provide an empty string.
|
33
56
|
cmap_name:
|
34
57
|
The name of the colormap to use. Default is "viridis".
|
35
58
|
xlim:
|
@@ -37,21 +60,45 @@ def plot_gantt_chart(
|
|
37
60
|
the schedule is used.
|
38
61
|
number_of_x_ticks:
|
39
62
|
The number of ticks to use in the x-axis.
|
63
|
+
job_labels:
|
64
|
+
A list of labels for each job. If ``None``, the labels are
|
65
|
+
automatically generated as "Job 0", "Job 1", etc.
|
66
|
+
machine_labels:
|
67
|
+
A list of labels for each machine. If ``None``, the labels are
|
68
|
+
automatically generated as "0", "1", etc.
|
69
|
+
legend_title:
|
70
|
+
The title of the legend. If not provided, the legend will not have
|
71
|
+
a title.
|
72
|
+
x_label:
|
73
|
+
The label for the x-axis. Default is "Time units". To remove the
|
74
|
+
label, provide an empty string.
|
75
|
+
y_label:
|
76
|
+
The label for the y-axis. Default is "Machines". To remove the
|
77
|
+
label, provide an empty string.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
- A ``matplotlib.figure.Figure`` object.
|
81
|
+
- A ``matplotlib.axes.Axes`` object where the Gantt chart is plotted.
|
40
82
|
"""
|
41
|
-
fig, ax = _initialize_plot(schedule, title)
|
42
|
-
legend_handles = _plot_machine_schedules(
|
43
|
-
|
44
|
-
|
83
|
+
fig, ax = _initialize_plot(schedule, title, x_label, y_label)
|
84
|
+
legend_handles = _plot_machine_schedules(
|
85
|
+
schedule, ax, cmap_name, job_labels
|
86
|
+
)
|
87
|
+
_configure_legend(ax, legend_handles, legend_title)
|
88
|
+
_configure_axes(schedule, ax, xlim, number_of_x_ticks, machine_labels)
|
45
89
|
return fig, ax
|
46
90
|
|
47
91
|
|
48
92
|
def _initialize_plot(
|
49
|
-
schedule: Schedule,
|
93
|
+
schedule: Schedule,
|
94
|
+
title: str | None,
|
95
|
+
x_label: str = "Time units",
|
96
|
+
y_label: str = "Machines",
|
50
97
|
) -> tuple[Figure, plt.Axes]:
|
51
98
|
"""Initializes the plot."""
|
52
99
|
fig, ax = plt.subplots()
|
53
|
-
ax.set_xlabel(
|
54
|
-
ax.set_ylabel(
|
100
|
+
ax.set_xlabel(x_label)
|
101
|
+
ax.set_ylabel(y_label)
|
55
102
|
ax.grid(True, which="both", axis="x", linestyle="--", linewidth=0.5)
|
56
103
|
ax.yaxis.grid(False)
|
57
104
|
if title is None:
|
@@ -61,7 +108,10 @@ def _initialize_plot(
|
|
61
108
|
|
62
109
|
|
63
110
|
def _plot_machine_schedules(
|
64
|
-
schedule: Schedule,
|
111
|
+
schedule: Schedule,
|
112
|
+
ax: plt.Axes,
|
113
|
+
cmap_name: str,
|
114
|
+
job_labels: list[str] | None,
|
65
115
|
) -> dict[int, Patch]:
|
66
116
|
"""Plots the schedules for each machine."""
|
67
117
|
max_job_id = schedule.instance.num_jobs - 1
|
@@ -81,12 +131,20 @@ def _plot_machine_schedules(
|
|
81
131
|
)
|
82
132
|
if scheduled_op.job_id not in legend_handles:
|
83
133
|
legend_handles[scheduled_op.job_id] = Patch(
|
84
|
-
facecolor=color,
|
134
|
+
facecolor=color,
|
135
|
+
label=_get_job_label(job_labels, scheduled_op.job_id),
|
85
136
|
)
|
86
137
|
|
87
138
|
return legend_handles
|
88
139
|
|
89
140
|
|
141
|
+
def _get_job_label(job_labels: list[str] | None, job_id: int) -> str:
|
142
|
+
"""Returns the label for the job."""
|
143
|
+
if job_labels is None:
|
144
|
+
return f"Job {job_id}"
|
145
|
+
return job_labels[job_id]
|
146
|
+
|
147
|
+
|
90
148
|
def _plot_scheduled_operation(
|
91
149
|
ax: plt.Axes,
|
92
150
|
scheduled_op: ScheduledOperation,
|
@@ -103,7 +161,9 @@ def _plot_scheduled_operation(
|
|
103
161
|
)
|
104
162
|
|
105
163
|
|
106
|
-
def _configure_legend(
|
164
|
+
def _configure_legend(
|
165
|
+
ax: plt.Axes, legend_handles: dict[int, Patch], legend_title: str
|
166
|
+
):
|
107
167
|
"""Configures the legend for the plot."""
|
108
168
|
sorted_legend_handles = [
|
109
169
|
legend_handles[job_id] for job_id in sorted(legend_handles)
|
@@ -111,7 +171,8 @@ def _configure_legend(ax: plt.Axes, legend_handles: dict[int, Patch]):
|
|
111
171
|
ax.legend(
|
112
172
|
handles=sorted_legend_handles,
|
113
173
|
loc="upper left",
|
114
|
-
bbox_to_anchor=(1
|
174
|
+
bbox_to_anchor=(1, 1),
|
175
|
+
title=legend_title,
|
115
176
|
)
|
116
177
|
|
117
178
|
|
@@ -120,6 +181,7 @@ def _configure_axes(
|
|
120
181
|
ax: plt.Axes,
|
121
182
|
xlim: Optional[int],
|
122
183
|
number_of_x_ticks: int,
|
184
|
+
machine_labels: list[str] | None,
|
123
185
|
):
|
124
186
|
"""Sets the limits and labels for the axes."""
|
125
187
|
num_machines = len(schedule.schedule)
|
@@ -132,7 +194,9 @@ def _configure_axes(
|
|
132
194
|
for i in range(num_machines)
|
133
195
|
]
|
134
196
|
)
|
135
|
-
|
197
|
+
if machine_labels is None:
|
198
|
+
machine_labels = [str(i) for i in range(num_machines)]
|
199
|
+
ax.set_yticklabels(machine_labels)
|
136
200
|
makespan = schedule.makespan()
|
137
201
|
xlim = xlim if xlim is not None else makespan
|
138
202
|
ax.set_xlim(0, xlim)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: job-shop-lib
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.0a3
|
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
|
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
|
|
29
29
|
<h1>JobShopLib</h1>
|
30
30
|
|
31
31
|
[](https://github.com/Pabloo22/job_shop_lib/actions/workflows/tests.yaml)
|
32
|
+
[](https://job-shop-lib.readthedocs.io/en/latest/?badge=latest)
|
32
33
|

|
33
34
|
[](https://github.com/psf/black)
|
34
35
|
[](https://opensource.org/licenses/MIT)
|
@@ -39,7 +40,7 @@ JobShopLib is a Python package for creating, solving, and visualizing Job Shop S
|
|
39
40
|
|
40
41
|
It follows a modular design, allowing users to easily extend the library with new solvers, dispatching rules, visualization functions, etc.
|
41
42
|
|
42
|
-
See [
|
43
|
+
See the [documentation](https://job-shop-lib.readthedocs.io/en/latest/) for more details about the latest version (1.0.0a2).
|
43
44
|
|
44
45
|
## Installation :package:
|
45
46
|
|
@@ -47,12 +48,23 @@ See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcoww
|
|
47
48
|
|
48
49
|
JobShopLib is distributed on [PyPI](https://pypi.org/project/job-shop-lib/) and it supports Python 3.10+.
|
49
50
|
|
50
|
-
You can install the latest version using `pip`:
|
51
|
+
You can install the latest stable version (version 0.5.1) using `pip`:
|
51
52
|
|
52
53
|
```bash
|
53
54
|
pip install job-shop-lib
|
54
55
|
```
|
55
56
|
|
57
|
+
See [this](https://colab.research.google.com/drive/1XV_Rvq1F2ns6DFG8uNj66q_rcowwTZ4H?usp=sharing) Google Colab notebook for a quick start guide!
|
58
|
+
|
59
|
+
|
60
|
+
Version 1.0.0 is currently in beta stage and can be installed with:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
pip install job-shop-lib==1.0.0a3
|
64
|
+
```
|
65
|
+
|
66
|
+
Although this version is not stable and may contain breaking changes in subsequent releases, it is recommended to install it to access the new reinforcement learning environments and familiare yourself with new changes (see the [latest pull requests](https://github.com/Pabloo22/job_shop_lib/pulls?q=is%3Apr+is%3Aclosed)). This version is the first one with a [documentation page](https://job-shop-lib.readthedocs.io/en/latest/).
|
67
|
+
|
56
68
|
<!-- end installation -->
|
57
69
|
|
58
70
|
<!-- key features -->
|
@@ -1,20 +1,20 @@
|
|
1
1
|
job_shop_lib/__init__.py,sha256=Ci5ipn-zciO88C5aX5Wx-UN8iBTbpde3dSSg02ZcfwM,597
|
2
2
|
job_shop_lib/_base_solver.py,sha256=p17XmtufNc9Y481cqZUT45pEkUmmW1HWG53dfhIBJH8,1363
|
3
|
-
job_shop_lib/_job_shop_instance.py,sha256=
|
4
|
-
job_shop_lib/_operation.py,sha256=
|
5
|
-
job_shop_lib/_schedule.py,sha256=
|
6
|
-
job_shop_lib/_scheduled_operation.py,sha256=
|
3
|
+
job_shop_lib/_job_shop_instance.py,sha256=Ubmm10DWO9SmhCOFsvEqeww7XFn-60JWTPDA40FOWZE,16434
|
4
|
+
job_shop_lib/_operation.py,sha256=wAYijQtKPPPuXQo9648tYV1GFUU9XGiXIRQgPsCbYtM,3404
|
5
|
+
job_shop_lib/_schedule.py,sha256=McDpisMyygxhw349Hxe4N3mCnugr8B5dVNCGN6sSYKg,11371
|
6
|
+
job_shop_lib/_scheduled_operation.py,sha256=a1LW2LQvpH1WTxpE2x792_glp5M_gInCf70fuBiheqc,2828
|
7
7
|
job_shop_lib/benchmarking/__init__.py,sha256=BYCrJUNr_uk2c0xIbDt07OnUMhQx8Dudkukx3TFWxgw,3271
|
8
8
|
job_shop_lib/benchmarking/_load_benchmark.py,sha256=-cgyx0Kn6uAc3KdGFSQb6eUVQjQggmpVKOH9qusNkXI,2930
|
9
9
|
job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMhsKstmyUmroyWduM7a00KQ,464841
|
10
10
|
job_shop_lib/constraint_programming/__init__.py,sha256=kKQRUxxS_nVFUdXGnf4bQOD9mqrXxZZWElS753A4YiA,454
|
11
11
|
job_shop_lib/constraint_programming/_ortools_solver.py,sha256=gRoImEBUa8_io-TzbSS-3f0CJ7UwacU5Lrz0bsDqibo,10462
|
12
|
-
job_shop_lib/dispatching/__init__.py,sha256=
|
13
|
-
job_shop_lib/dispatching/_dispatcher.py,sha256=
|
12
|
+
job_shop_lib/dispatching/__init__.py,sha256=2VQYWNSyuDx3zzrDPCEzs5VJd2AIJvF7vA7LwK5z2V4,1648
|
13
|
+
job_shop_lib/dispatching/_dispatcher.py,sha256=o0BqZBub9flPAHX59qBHV2RETQg7n31xHe6owg9Ki7U,21305
|
14
14
|
job_shop_lib/dispatching/_dispatcher_observer_config.py,sha256=l_lbaw9JJ5icVOmDAzAL6G5t6wG25bQLpRedN1bys8c,1932
|
15
|
-
job_shop_lib/dispatching/_factories.py,sha256=
|
15
|
+
job_shop_lib/dispatching/_factories.py,sha256=MYnjuK9NOVU9n98mrNu5Z0svBIYBCTDbVGzUX9rDWsQ,4594
|
16
16
|
job_shop_lib/dispatching/_history_observer.py,sha256=Vl8rQaxekUeEB-AyNxyC3c76zQakeh-rdri2iDnZvXw,610
|
17
|
-
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=
|
17
|
+
job_shop_lib/dispatching/_ready_operation_filters.py,sha256=Mywt57h8Nlj6XrptWakVt9n1Tq4jsneZFQEgjLMxJgw,5731
|
18
18
|
job_shop_lib/dispatching/_unscheduled_operations_observer.py,sha256=LNEzqOWqEf6fvtkQrDmDWFEhCfA75OgEtzdomzbxYII,2683
|
19
19
|
job_shop_lib/dispatching/feature_observers/__init__.py,sha256=EuJLvSpJpoXUK8A4UuC2k6Mpa293ZR3oCnnvYivIBtU,2240
|
20
20
|
job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py,sha256=DOS961MtWaDk2gxjOLA_75SyT6Nmn3IKuNtYO8odk8s,7938
|
@@ -23,14 +23,14 @@ job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py,sha2
|
|
23
23
|
job_shop_lib/dispatching/feature_observers/_factory.py,sha256=b5YyzdnorijtWUNrYWs4sf-G17eDxw8oYrol1rzMN1Q,2919
|
24
24
|
job_shop_lib/dispatching/feature_observers/_feature_observer.py,sha256=crbqG1KrmUOfG4z7shHNzhUg7-uSP4_RWxyOi-RRWmE,8635
|
25
25
|
job_shop_lib/dispatching/feature_observers/_is_completed_observer.py,sha256=wKmlASLjodztAB2ypTsi0XFLZ3h1ltzvsa9BpPrbksU,4581
|
26
|
-
job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=
|
26
|
+
job_shop_lib/dispatching/feature_observers/_is_ready_observer.py,sha256=wy_pA-1wmnzVjhq92mdsT2JJHYbfsm79mcMgSgYUCOs,1264
|
27
27
|
job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py,sha256=OcuMUB9_By6ZMtX-1_3z-xaxGbP85a5Zv0ywAv7XxWQ,1491
|
28
28
|
job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py,sha256=WRknpQBKXs6h6cXLFJW7ZCvjtU8CPL-iXXNPw3g-mLE,1303
|
29
29
|
job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py,sha256=5V87lCrJUabEe8AkTGXPu5yS8OGxeN8L3-xNyHmdmLs,1441
|
30
30
|
job_shop_lib/dispatching/rules/__init__.py,sha256=p1rkqf66L62uvAOM1ZxNV8xHoh7SuYjHi_8ZNBvPIjg,1450
|
31
31
|
job_shop_lib/dispatching/rules/_dispatching_rule_factory.py,sha256=5fNpv90fAoR6rcE6NeJOWiB7ir-FVnoONIhHtKJ9H0E,2904
|
32
|
-
job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=
|
33
|
-
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=
|
32
|
+
job_shop_lib/dispatching/rules/_dispatching_rule_solver.py,sha256=YgcxDUx9RqwfTxWtoKesngYVFDsHiWtsm1BAUrAyerY,6353
|
33
|
+
job_shop_lib/dispatching/rules/_dispatching_rules_functions.py,sha256=yRJXYH6QSxDCIK8vqNUfMCMpzWmr3j_camAR72Z6D9Q,7605
|
34
34
|
job_shop_lib/dispatching/rules/_machine_chooser_factory.py,sha256=xsJ8nJwPDBi-sfLJRQF_BBQDbyXDfopD1U-efXffQAE,2331
|
35
35
|
job_shop_lib/dispatching/rules/_utils.py,sha256=X8vET2p1D3RyoB9mFfsfRgmilcTmxPssKYyJQ2zEt0Q,4605
|
36
36
|
job_shop_lib/exceptions.py,sha256=ARzpoZJCvRIvOesCiqqFSRxkv6w9WwEXx0aBP-l2IKA,1597
|
@@ -51,16 +51,16 @@ job_shop_lib/graphs/graph_updaters/_utils.py,sha256=X5YfwJA1CCgpm1r9C036Gal2CkDh
|
|
51
51
|
job_shop_lib/reinforcement_learning/__init__.py,sha256=QVFo9e1X-tpanZkGdcCPV_WobQ2EZ_y5uoYSJ36XrQI,957
|
52
52
|
job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py,sha256=jKMcWYBvz1kwlw00Xe1x9HqhFkMMqlh-Y95NmeBL3-0,15129
|
53
53
|
job_shop_lib/reinforcement_learning/_reward_observers.py,sha256=4Kdyn9Jlp_1sBtVr6raF-ZFtcnKxwyCLykfX53TmuhU,2959
|
54
|
-
job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=
|
54
|
+
job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py,sha256=ByQQ1fCCpji0VAjKTcbBOGST-3MF6oq2naAtdElJUh8,12953
|
55
55
|
job_shop_lib/reinforcement_learning/_types_and_constants.py,sha256=CY849lbv6UXy40KRcMJT3WxvGWrLqcfysu65LPkTfg8,1715
|
56
56
|
job_shop_lib/reinforcement_learning/_utils.py,sha256=ilI089Bs8CRlfRV_yH6XH8oypTDtRa7hS-H4iRCC5lU,2497
|
57
|
-
job_shop_lib/visualization/__init__.py,sha256=
|
57
|
+
job_shop_lib/visualization/__init__.py,sha256=SVxg7Dt3azETfI0IvUtLtCBsirXQWirdjEjOQ2d5eWU,1137
|
58
58
|
job_shop_lib/visualization/_agent_task_graph.py,sha256=AaBTD_S34WjrsZnL_iMAplR_f67RahZi7x58SOvp-q0,8834
|
59
59
|
job_shop_lib/visualization/_disjunctive_graph.py,sha256=pg4KG9BfQbnBPnXYgbyPGe0AuHSmhYqPeqWYAf_spWQ,5905
|
60
|
-
job_shop_lib/visualization/
|
61
|
-
job_shop_lib/visualization/
|
62
|
-
job_shop_lib/visualization/
|
63
|
-
job_shop_lib-1.0.
|
64
|
-
job_shop_lib-1.0.
|
65
|
-
job_shop_lib-1.0.
|
66
|
-
job_shop_lib-1.0.
|
60
|
+
job_shop_lib/visualization/_gantt_chart_creator.py,sha256=xw7lfd1HIm7r5j2kDxgBdrgSickCslpUcm7QFrNXmtg,7763
|
61
|
+
job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py,sha256=IpKe2oZbrW4fvsdnRtuFWfXN-FdfwkYVdTAq3av9mKE,14249
|
62
|
+
job_shop_lib/visualization/_plot_gantt_chart.py,sha256=1WCJ5Gjl3dwA-w4Jn9suIg-ZGR28yYUAy8Jp-IiyvfI,6842
|
63
|
+
job_shop_lib-1.0.0a3.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
64
|
+
job_shop_lib-1.0.0a3.dist-info/METADATA,sha256=fJ7RYm8yUkmZvG5moRO5QUVDw0_ACIcqb7gxiuqLrQo,15663
|
65
|
+
job_shop_lib-1.0.0a3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
66
|
+
job_shop_lib-1.0.0a3.dist-info/RECORD,,
|
File without changes
|