job-shop-lib 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/__init__.py +2 -1
- job_shop_lib/dispatching/__init__.py +4 -1
- job_shop_lib/dispatching/dispatcher.py +117 -31
- job_shop_lib/dispatching/dispatching_rule_solver.py +14 -6
- job_shop_lib/dispatching/history_tracker.py +20 -0
- job_shop_lib/graphs/job_shop_graph.py +78 -45
- job_shop_lib/graphs/node.py +37 -18
- job_shop_lib/operation.py +6 -4
- job_shop_lib/visualization/create_gif.py +21 -12
- job_shop_lib/visualization/disjunctive_graph.py +20 -16
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.3.0.dist-info}/METADATA +1 -1
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.3.0.dist-info}/RECORD +14 -13
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.3.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.3.0.dist-info}/WHEEL +0 -0
job_shop_lib/__init__.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
"""Contains the main data structures and base classes.
|
2
2
|
"""
|
3
3
|
|
4
|
+
from job_shop_lib.exceptions import JobShopLibError, NoSolutionFoundError
|
4
5
|
from job_shop_lib.operation import Operation
|
5
6
|
from job_shop_lib.job_shop_instance import JobShopInstance
|
6
7
|
from job_shop_lib.scheduled_operation import ScheduledOperation
|
7
8
|
from job_shop_lib.schedule import Schedule
|
8
9
|
from job_shop_lib.base_solver import BaseSolver, Solver
|
9
|
-
|
10
|
+
|
10
11
|
|
11
12
|
__all__ = [
|
12
13
|
"Operation",
|
@@ -1,7 +1,8 @@
|
|
1
1
|
"""Package containing all the functionality to solve the Job Shop Scheduling
|
2
2
|
Problem step-by-step."""
|
3
3
|
|
4
|
-
from job_shop_lib.dispatching.dispatcher import Dispatcher
|
4
|
+
from job_shop_lib.dispatching.dispatcher import Dispatcher, DispatcherObserver
|
5
|
+
from job_shop_lib.dispatching.history_tracker import HistoryTracker
|
5
6
|
from job_shop_lib.dispatching.dispatching_rules import (
|
6
7
|
shortest_processing_time_rule,
|
7
8
|
first_come_first_served_rule,
|
@@ -46,4 +47,6 @@ __all__ = [
|
|
46
47
|
"PruningFunction",
|
47
48
|
"pruning_function_factory",
|
48
49
|
"composite_pruning_function_factory",
|
50
|
+
"DispatcherObserver",
|
51
|
+
"HistoryTracker",
|
49
52
|
]
|
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import abc
|
6
|
+
from typing import Any
|
5
7
|
from collections.abc import Callable
|
6
8
|
from collections import deque
|
9
|
+
from functools import wraps
|
7
10
|
|
8
11
|
from job_shop_lib import (
|
9
12
|
JobShopInstance,
|
@@ -13,6 +16,59 @@ from job_shop_lib import (
|
|
13
16
|
)
|
14
17
|
|
15
18
|
|
19
|
+
# Added here to avoid circular imports
|
20
|
+
class DispatcherObserver(abc.ABC):
|
21
|
+
"""Interface for classes that observe the dispatcher."""
|
22
|
+
|
23
|
+
def __init__(self, dispatcher: Dispatcher):
|
24
|
+
"""Initializes the observer with the dispatcher and subscribes to
|
25
|
+
it."""
|
26
|
+
self.dispatcher = dispatcher
|
27
|
+
self.dispatcher.subscribe(self)
|
28
|
+
|
29
|
+
@abc.abstractmethod
|
30
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
31
|
+
"""Called when an operation is scheduled on a machine."""
|
32
|
+
|
33
|
+
@abc.abstractmethod
|
34
|
+
def reset(self):
|
35
|
+
"""Called when the dispatcher is reset."""
|
36
|
+
|
37
|
+
def __str__(self) -> str:
|
38
|
+
return self.__class__.__name__
|
39
|
+
|
40
|
+
def __repr__(self) -> str:
|
41
|
+
return str(self)
|
42
|
+
|
43
|
+
|
44
|
+
def _dispatcher_cache(method):
|
45
|
+
"""Decorator to cache results of a method based on its name.
|
46
|
+
|
47
|
+
This decorator assumes that the class has an attribute called `_cache`
|
48
|
+
that is a dictionary. It caches the result of the method based on its
|
49
|
+
name. If the result is already cached, it returns the cached result
|
50
|
+
instead of recomputing it.
|
51
|
+
|
52
|
+
The decorator is useful since the dispatcher class can clear the cache
|
53
|
+
when the state of the dispatcher changes.
|
54
|
+
"""
|
55
|
+
|
56
|
+
@wraps(method)
|
57
|
+
def wrapper(self: Dispatcher, *args, **kwargs):
|
58
|
+
# pylint: disable=protected-access
|
59
|
+
cache_key = method.__name__
|
60
|
+
cached_result = self._cache.get(cache_key)
|
61
|
+
if cached_result is not None:
|
62
|
+
return cached_result
|
63
|
+
|
64
|
+
result = method(self, *args, **kwargs)
|
65
|
+
self._cache[cache_key] = result
|
66
|
+
return result
|
67
|
+
|
68
|
+
return wrapper
|
69
|
+
|
70
|
+
|
71
|
+
# pylint: disable=too-many-instance-attributes
|
16
72
|
class Dispatcher:
|
17
73
|
"""Handles the logic of scheduling operations on machines.
|
18
74
|
|
@@ -27,8 +83,10 @@ class Dispatcher:
|
|
27
83
|
schedule:
|
28
84
|
The schedule of operations on machines.
|
29
85
|
pruning_function:
|
30
|
-
|
31
|
-
|
86
|
+
A function that filters out operations that are not ready to be
|
87
|
+
scheduled.
|
88
|
+
subscribers:
|
89
|
+
A list of observers that are subscribed to the dispatcher.
|
32
90
|
"""
|
33
91
|
|
34
92
|
__slots__ = (
|
@@ -38,6 +96,8 @@ class Dispatcher:
|
|
38
96
|
"_job_next_operation_index",
|
39
97
|
"_job_next_available_time",
|
40
98
|
"pruning_function",
|
99
|
+
"subscribers",
|
100
|
+
"_cache",
|
41
101
|
)
|
42
102
|
|
43
103
|
def __init__(
|
@@ -52,20 +112,28 @@ class Dispatcher:
|
|
52
112
|
Args:
|
53
113
|
instance:
|
54
114
|
The instance of the job shop problem to be solved.
|
55
|
-
|
56
|
-
A
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
pruning, pass an empty list.
|
115
|
+
pruning_function:
|
116
|
+
A function that filters out operations that are not ready to
|
117
|
+
be scheduled. The function should take the dispatcher and a
|
118
|
+
list of operations as input and return a list of operations
|
119
|
+
that are ready to be scheduled. If `None`, no pruning is done.
|
61
120
|
"""
|
62
121
|
|
63
122
|
self.instance = instance
|
64
123
|
self.schedule = Schedule(self.instance)
|
124
|
+
self.pruning_function = pruning_function
|
125
|
+
|
65
126
|
self._machine_next_available_time = [0] * self.instance.num_machines
|
66
127
|
self._job_next_operation_index = [0] * self.instance.num_jobs
|
67
128
|
self._job_next_available_time = [0] * self.instance.num_jobs
|
68
|
-
self.
|
129
|
+
self.subscribers: list[DispatcherObserver] = []
|
130
|
+
self._cache: dict[str, Any] = {}
|
131
|
+
|
132
|
+
def __str__(self) -> str:
|
133
|
+
return f"{self.__class__.__name__}({self.instance})"
|
134
|
+
|
135
|
+
def __repr__(self) -> str:
|
136
|
+
return str(self)
|
69
137
|
|
70
138
|
@property
|
71
139
|
def machine_next_available_time(self) -> list[int]:
|
@@ -117,12 +185,23 @@ class Dispatcher:
|
|
117
185
|
operations.popleft()
|
118
186
|
return dispatcher.schedule
|
119
187
|
|
188
|
+
def subscribe(self, observer: DispatcherObserver):
|
189
|
+
"""Subscribes an observer to the dispatcher."""
|
190
|
+
self.subscribers.append(observer)
|
191
|
+
|
192
|
+
def unsubscribe(self, observer: DispatcherObserver):
|
193
|
+
"""Unsubscribes an observer from the dispatcher."""
|
194
|
+
self.subscribers.remove(observer)
|
195
|
+
|
120
196
|
def reset(self) -> None:
|
121
197
|
"""Resets the dispatcher to its initial state."""
|
122
198
|
self.schedule.reset()
|
123
199
|
self._machine_next_available_time = [0] * self.instance.num_machines
|
124
200
|
self._job_next_operation_index = [0] * self.instance.num_jobs
|
125
201
|
self._job_next_available_time = [0] * self.instance.num_jobs
|
202
|
+
self._cache = {}
|
203
|
+
for subscriber in self.subscribers:
|
204
|
+
subscriber.reset()
|
126
205
|
|
127
206
|
def dispatch(self, operation: Operation, machine_id: int) -> None:
|
128
207
|
"""Schedules the given operation on the given machine.
|
@@ -153,6 +232,10 @@ class Dispatcher:
|
|
153
232
|
self.schedule.add(scheduled_operation)
|
154
233
|
self._update_tracking_attributes(scheduled_operation)
|
155
234
|
|
235
|
+
# Notify subscribers
|
236
|
+
for subscriber in self.subscribers:
|
237
|
+
subscriber.update(scheduled_operation)
|
238
|
+
|
156
239
|
def is_operation_ready(self, operation: Operation) -> bool:
|
157
240
|
"""Returns True if the given operation is ready to be scheduled.
|
158
241
|
|
@@ -164,11 +247,15 @@ class Dispatcher:
|
|
164
247
|
The operation to be checked.
|
165
248
|
"""
|
166
249
|
return (
|
167
|
-
self.
|
250
|
+
self._job_next_operation_index[operation.job_id]
|
168
251
|
== operation.position_in_job
|
169
252
|
)
|
170
253
|
|
171
|
-
def start_time(
|
254
|
+
def start_time(
|
255
|
+
self,
|
256
|
+
operation: Operation,
|
257
|
+
machine_id: int,
|
258
|
+
) -> int:
|
172
259
|
"""Computes the start time for the given operation on the given
|
173
260
|
machine.
|
174
261
|
|
@@ -181,11 +268,12 @@ class Dispatcher:
|
|
181
268
|
The operation to be scheduled.
|
182
269
|
machine_id:
|
183
270
|
The id of the machine on which the operation is to be
|
184
|
-
scheduled.
|
271
|
+
scheduled. If None, the start time is computed based on the
|
272
|
+
next available time for the operation on any machine.
|
185
273
|
"""
|
186
274
|
return max(
|
187
|
-
self.
|
188
|
-
self.
|
275
|
+
self._machine_next_available_time[machine_id],
|
276
|
+
self._job_next_available_time[operation.job_id],
|
189
277
|
)
|
190
278
|
|
191
279
|
def _update_tracking_attributes(
|
@@ -196,10 +284,12 @@ class Dispatcher:
|
|
196
284
|
machine_id = scheduled_operation.machine_id
|
197
285
|
end_time = scheduled_operation.end_time
|
198
286
|
|
199
|
-
self.
|
200
|
-
self.
|
201
|
-
self.
|
287
|
+
self._machine_next_available_time[machine_id] = end_time
|
288
|
+
self._job_next_operation_index[job_id] += 1
|
289
|
+
self._job_next_available_time[job_id] = end_time
|
290
|
+
self._cache = {}
|
202
291
|
|
292
|
+
@_dispatcher_cache
|
203
293
|
def current_time(self) -> int:
|
204
294
|
"""Returns the current time of the schedule.
|
205
295
|
|
@@ -207,7 +297,8 @@ class Dispatcher:
|
|
207
297
|
operations.
|
208
298
|
"""
|
209
299
|
available_operations = self.available_operations()
|
210
|
-
|
300
|
+
current_time = self.min_start_time(available_operations)
|
301
|
+
return current_time
|
211
302
|
|
212
303
|
def min_start_time(self, operations: list[Operation]) -> int:
|
213
304
|
"""Returns the minimum start time of the available operations."""
|
@@ -220,6 +311,7 @@ class Dispatcher:
|
|
220
311
|
min_start_time = min(min_start_time, start_time)
|
221
312
|
return int(min_start_time)
|
222
313
|
|
314
|
+
@_dispatcher_cache
|
223
315
|
def uncompleted_operations(self) -> list[Operation]:
|
224
316
|
"""Returns the list of operations that have not been scheduled.
|
225
317
|
|
@@ -228,30 +320,24 @@ class Dispatcher:
|
|
228
320
|
It is more efficient than checking all operations in the instance.
|
229
321
|
"""
|
230
322
|
uncompleted_operations = []
|
231
|
-
for job_id, next_position in enumerate(self.
|
323
|
+
for job_id, next_position in enumerate(self._job_next_operation_index):
|
232
324
|
operations = self.instance.jobs[job_id][next_position:]
|
233
325
|
uncompleted_operations.extend(operations)
|
234
326
|
return uncompleted_operations
|
235
327
|
|
328
|
+
@_dispatcher_cache
|
236
329
|
def available_operations(self) -> list[Operation]:
|
237
330
|
"""Returns a list of available operations for processing, optionally
|
238
|
-
filtering out operations
|
331
|
+
filtering out operations using the pruning function.
|
239
332
|
|
240
333
|
This method first gathers all possible next operations from the jobs
|
241
|
-
being processed. It then optionally filters these operations
|
242
|
-
|
243
|
-
|
244
|
-
An operation is sub-optimal if there is another operation that could
|
245
|
-
be scheduled in the same machine that would finish before the start
|
246
|
-
time of the sub-optimal operation.
|
334
|
+
being processed. It then optionally filters these operations using the
|
335
|
+
pruning function.
|
247
336
|
|
248
337
|
Returns:
|
249
338
|
A list of Operation objects that are available for scheduling.
|
250
|
-
|
251
|
-
Raises:
|
252
|
-
ValueError: If using the filter_bad_choices option and one of the
|
253
|
-
available operations can be scheduled in more than one machine.
|
254
339
|
"""
|
340
|
+
|
255
341
|
available_operations = self._available_operations()
|
256
342
|
if self.pruning_function is not None:
|
257
343
|
available_operations = self.pruning_function(
|
@@ -261,7 +347,7 @@ class Dispatcher:
|
|
261
347
|
|
262
348
|
def _available_operations(self) -> list[Operation]:
|
263
349
|
available_operations = []
|
264
|
-
for job_id, next_position in enumerate(self.
|
350
|
+
for job_id, next_position in enumerate(self._job_next_operation_index):
|
265
351
|
if next_position == len(self.instance.jobs[job_id]):
|
266
352
|
continue
|
267
353
|
operation = self.instance.jobs[job_id][next_position]
|
@@ -76,12 +76,15 @@ class DispatchingRuleSolver(BaseSolver):
|
|
76
76
|
self.machine_chooser = machine_chooser
|
77
77
|
self.pruning_function = pruning_function
|
78
78
|
|
79
|
-
def solve(
|
79
|
+
def solve(
|
80
|
+
self, instance: JobShopInstance, dispatcher: Dispatcher | None = None
|
81
|
+
) -> Schedule:
|
80
82
|
"""Returns a schedule for the given job shop instance using the
|
81
83
|
dispatching rule algorithm."""
|
82
|
-
dispatcher
|
83
|
-
|
84
|
-
|
84
|
+
if dispatcher is None:
|
85
|
+
dispatcher = Dispatcher(
|
86
|
+
instance, pruning_function=self.pruning_function
|
87
|
+
)
|
85
88
|
while not dispatcher.schedule.is_complete():
|
86
89
|
self.step(dispatcher)
|
87
90
|
|
@@ -101,11 +104,16 @@ class DispatchingRuleSolver(BaseSolver):
|
|
101
104
|
|
102
105
|
|
103
106
|
if __name__ == "__main__":
|
104
|
-
import
|
107
|
+
import time
|
105
108
|
from job_shop_lib.benchmarking import load_benchmark_instance
|
106
109
|
|
107
110
|
ta_instances = []
|
108
111
|
for i in range(1, 81):
|
109
112
|
ta_instances.append(load_benchmark_instance(f"ta{i:02d}"))
|
110
113
|
solver = DispatchingRuleSolver(dispatching_rule="most_work_remaining")
|
111
|
-
cProfile.run("for instance in ta_instances: solver.solve(instance)")
|
114
|
+
# cProfile.run("for instance in ta_instances: solver.solve(instance)")
|
115
|
+
start = time.perf_counter()
|
116
|
+
for instance_ in ta_instances:
|
117
|
+
solver.solve(instance_)
|
118
|
+
end = time.perf_counter()
|
119
|
+
print(f"Elapsed time: {end - start:.2f} seconds.")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""Home of the `HistoryTracker` class."""
|
2
|
+
|
3
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
4
|
+
from job_shop_lib import ScheduledOperation
|
5
|
+
|
6
|
+
|
7
|
+
class HistoryTracker(DispatcherObserver):
|
8
|
+
"""Observer that stores the history of the dispatcher."""
|
9
|
+
|
10
|
+
def __init__(self, dispatcher: Dispatcher):
|
11
|
+
"""Initializes the observer with the current state of the
|
12
|
+
dispatcher."""
|
13
|
+
super().__init__(dispatcher)
|
14
|
+
self.history: list[ScheduledOperation] = []
|
15
|
+
|
16
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
17
|
+
self.history.append(scheduled_operation)
|
18
|
+
|
19
|
+
def reset(self):
|
20
|
+
self.history = []
|
@@ -3,13 +3,14 @@
|
|
3
3
|
import collections
|
4
4
|
import networkx as nx
|
5
5
|
|
6
|
-
from job_shop_lib import JobShopInstance,
|
6
|
+
from job_shop_lib import JobShopInstance, JobShopLibError
|
7
7
|
from job_shop_lib.graphs import Node, NodeType
|
8
8
|
|
9
9
|
|
10
10
|
NODE_ATTR = "node"
|
11
11
|
|
12
12
|
|
13
|
+
# pylint: disable=too-many-instance-attributes
|
13
14
|
class JobShopGraph:
|
14
15
|
"""Data structure to represent a `JobShopInstance` as a graph.
|
15
16
|
|
@@ -30,30 +31,8 @@ class JobShopGraph:
|
|
30
31
|
The directed graph representing the job shop, where nodes are
|
31
32
|
operations, machines, jobs, or abstract concepts like global,
|
32
33
|
source, and sink, with edges indicating dependencies.
|
33
|
-
nodes:
|
34
|
-
A list of all nodes, encapsulated by the `Node` class, in the
|
35
|
-
graph.
|
36
|
-
nodes_by_type:
|
37
|
-
A dictionary categorizing nodes by their `NodeType`,
|
38
|
-
facilitating access to nodes of a particular type.
|
39
|
-
nodes_by_machine:
|
40
|
-
A nested list mapping each machine to its associated
|
41
|
-
operation nodes, aiding in machine-specific analysis.
|
42
|
-
nodes_by_job:
|
43
|
-
Similar to `nodes_by_machine`, but maps jobs to their
|
44
|
-
operation nodes, useful for job-specific traversal.
|
45
34
|
"""
|
46
35
|
|
47
|
-
__slots__ = (
|
48
|
-
"instance",
|
49
|
-
"graph",
|
50
|
-
"nodes",
|
51
|
-
"nodes_by_type",
|
52
|
-
"nodes_by_machine",
|
53
|
-
"nodes_by_job",
|
54
|
-
"_next_node_id",
|
55
|
-
)
|
56
|
-
|
57
36
|
def __init__(self, instance: JobShopInstance):
|
58
37
|
"""Initializes the graph with the given instance.
|
59
38
|
|
@@ -67,34 +46,62 @@ class JobShopGraph:
|
|
67
46
|
self.graph = nx.DiGraph()
|
68
47
|
self.instance = instance
|
69
48
|
|
70
|
-
self.
|
71
|
-
|
72
|
-
self.nodes_by_type: dict[NodeType, list[Node]] = (
|
49
|
+
self._nodes: list[Node] = []
|
50
|
+
self._nodes_by_type: dict[NodeType, list[Node]] = (
|
73
51
|
collections.defaultdict(list)
|
74
52
|
)
|
75
|
-
|
76
|
-
self.nodes_by_machine: list[list[Node]] = [
|
53
|
+
self._nodes_by_machine: list[list[Node]] = [
|
77
54
|
[] for _ in range(instance.num_machines)
|
78
55
|
]
|
79
|
-
|
80
|
-
self.nodes_by_job: list[list[Node]] = [
|
56
|
+
self._nodes_by_job: list[list[Node]] = [
|
81
57
|
[] for _ in range(instance.num_jobs)
|
82
58
|
]
|
83
|
-
|
84
59
|
self._next_node_id = 0
|
85
|
-
|
60
|
+
self.removed_nodes: list[bool] = []
|
86
61
|
self._add_operation_nodes()
|
87
62
|
|
88
63
|
@property
|
89
|
-
def
|
90
|
-
"""
|
91
|
-
|
64
|
+
def nodes(self) -> list[Node]:
|
65
|
+
"""List of all nodes added to the graph.
|
66
|
+
|
67
|
+
It may contain nodes that have been removed from the graph.
|
68
|
+
"""
|
69
|
+
return self._nodes
|
70
|
+
|
71
|
+
@property
|
72
|
+
def nodes_by_type(self) -> dict[NodeType, list[Node]]:
|
73
|
+
"""Dictionary mapping node types to lists of nodes.
|
74
|
+
|
75
|
+
It may contain nodes that have been removed from the graph.
|
76
|
+
"""
|
77
|
+
return self._nodes_by_type
|
78
|
+
|
79
|
+
@property
|
80
|
+
def nodes_by_machine(self) -> list[list[Node]]:
|
81
|
+
"""List of lists mapping machine ids to operation nodes.
|
82
|
+
|
83
|
+
It may contain nodes that have been removed from the graph.
|
84
|
+
"""
|
85
|
+
return self._nodes_by_machine
|
86
|
+
|
87
|
+
@property
|
88
|
+
def nodes_by_job(self) -> list[list[Node]]:
|
89
|
+
"""List of lists mapping job ids to operation nodes.
|
90
|
+
|
91
|
+
It may contain nodes that have been removed from the graph.
|
92
|
+
"""
|
93
|
+
return self._nodes_by_job
|
92
94
|
|
93
95
|
@property
|
94
96
|
def num_edges(self) -> int:
|
95
97
|
"""Number of edges in the graph."""
|
96
98
|
return self.graph.number_of_edges()
|
97
99
|
|
100
|
+
@property
|
101
|
+
def num_job_nodes(self) -> int:
|
102
|
+
"""Number of job nodes in the graph."""
|
103
|
+
return len(self._nodes_by_type[NodeType.JOB])
|
104
|
+
|
98
105
|
def _add_operation_nodes(self) -> None:
|
99
106
|
"""Adds operation nodes to the graph."""
|
100
107
|
for job in self.instance.jobs:
|
@@ -102,10 +109,6 @@ class JobShopGraph:
|
|
102
109
|
node = Node(node_type=NodeType.OPERATION, operation=operation)
|
103
110
|
self.add_node(node)
|
104
111
|
|
105
|
-
def get_operation_from_id(self, operation_id: int) -> Operation:
|
106
|
-
"""Returns the operation with the given id."""
|
107
|
-
return self.nodes[operation_id].operation
|
108
|
-
|
109
112
|
def add_node(self, node_for_adding: Node) -> None:
|
110
113
|
"""Adds a node to the graph and updates relevant class attributes.
|
111
114
|
|
@@ -131,15 +134,16 @@ class JobShopGraph:
|
|
131
134
|
self.graph.add_node(
|
132
135
|
node_for_adding.node_id, **{NODE_ATTR: node_for_adding}
|
133
136
|
)
|
134
|
-
self.
|
135
|
-
self.
|
137
|
+
self._nodes_by_type[node_for_adding.node_type].append(node_for_adding)
|
138
|
+
self._nodes.append(node_for_adding)
|
136
139
|
self._next_node_id += 1
|
140
|
+
self.removed_nodes.append(False)
|
137
141
|
|
138
142
|
if node_for_adding.node_type == NodeType.OPERATION:
|
139
143
|
operation = node_for_adding.operation
|
140
|
-
self.
|
144
|
+
self._nodes_by_job[operation.job_id].append(node_for_adding)
|
141
145
|
for machine_id in operation.machines:
|
142
|
-
self.
|
146
|
+
self._nodes_by_machine[machine_id].append(node_for_adding)
|
143
147
|
|
144
148
|
def add_edge(
|
145
149
|
self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
|
@@ -156,14 +160,43 @@ class JobShopGraph:
|
|
156
160
|
**attr: Additional attributes to be added to the edge.
|
157
161
|
|
158
162
|
Raises:
|
159
|
-
|
163
|
+
JobShopLibError: If `u_of_edge` or `v_of_edge` are not in the
|
164
|
+
graph.
|
160
165
|
"""
|
161
166
|
if isinstance(u_of_edge, Node):
|
162
167
|
u_of_edge = u_of_edge.node_id
|
163
168
|
if isinstance(v_of_edge, Node):
|
164
169
|
v_of_edge = v_of_edge.node_id
|
165
170
|
if u_of_edge not in self.graph or v_of_edge not in self.graph:
|
166
|
-
raise
|
171
|
+
raise JobShopLibError(
|
167
172
|
"`u_of_edge` and `v_of_edge` must be in the graph."
|
168
173
|
)
|
169
174
|
self.graph.add_edge(u_of_edge, v_of_edge, **attr)
|
175
|
+
|
176
|
+
def remove_node(self, node_id: int) -> None:
|
177
|
+
"""Removes a node from the graph and the isolated nodes that result
|
178
|
+
from the removal.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
node_id: The id of the node to remove.
|
182
|
+
"""
|
183
|
+
self.graph.remove_node(node_id)
|
184
|
+
self.removed_nodes[node_id] = True
|
185
|
+
|
186
|
+
isolated_nodes = list(nx.isolates(self.graph))
|
187
|
+
for isolated_node in isolated_nodes:
|
188
|
+
self.removed_nodes[isolated_node] = True
|
189
|
+
|
190
|
+
self.graph.remove_nodes_from(isolated_nodes)
|
191
|
+
|
192
|
+
def is_removed(self, node: int | Node) -> bool:
|
193
|
+
"""Returns whether the node is removed from the graph.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
node: The node to check. If it is a `Node`, its `node_id` is used
|
197
|
+
as the node to check. Otherwise, it is assumed to be the
|
198
|
+
`node_id` of the node to check.
|
199
|
+
"""
|
200
|
+
if isinstance(node, Node):
|
201
|
+
node = node.node_id
|
202
|
+
return self.removed_nodes[node]
|
job_shop_lib/graphs/node.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Home of the `Node` class."""
|
2
2
|
|
3
|
-
from job_shop_lib import Operation
|
3
|
+
from job_shop_lib import Operation, JobShopLibError
|
4
4
|
from job_shop_lib.graphs.constants import NodeType
|
5
5
|
|
6
6
|
|
@@ -35,7 +35,7 @@ class Node:
|
|
35
35
|
print(graph[1]) # "some value"
|
36
36
|
```
|
37
37
|
|
38
|
-
|
38
|
+
Attributes:
|
39
39
|
node_type:
|
40
40
|
The type of the node. It can be one of the following:
|
41
41
|
- NodeType.OPERATION
|
@@ -43,15 +43,6 @@ class Node:
|
|
43
43
|
- NodeType.JOB
|
44
44
|
- NodeType.GLOBAL
|
45
45
|
...
|
46
|
-
operation:
|
47
|
-
The operation of the node. It should be provided if the node_type
|
48
|
-
is NodeType.OPERATION.
|
49
|
-
machine_id:
|
50
|
-
The id of the machine of the node. It should be provided if the
|
51
|
-
node_type is NodeType.MACHINE.
|
52
|
-
job_id:
|
53
|
-
The id of the job of the node. It should be provided if the
|
54
|
-
node_type is NodeType.JOB.
|
55
46
|
"""
|
56
47
|
|
57
48
|
__slots__ = "node_type", "_node_id", "_operation", "_machine_id", "_job_id"
|
@@ -63,14 +54,42 @@ class Node:
|
|
63
54
|
machine_id: int | None = None,
|
64
55
|
job_id: int | None = None,
|
65
56
|
):
|
57
|
+
"""Initializes the node with the given attributes.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
node_type:
|
61
|
+
The type of the node. It can be one of the following:
|
62
|
+
- NodeType.OPERATION
|
63
|
+
- NodeType.MACHINE
|
64
|
+
- NodeType.JOB
|
65
|
+
- NodeType.GLOBAL
|
66
|
+
...
|
67
|
+
operation:
|
68
|
+
The operation of the node. It should be provided if the
|
69
|
+
`node_type` is NodeType.OPERATION.
|
70
|
+
machine_id:
|
71
|
+
The id of the machine of the node. It should be provided if the
|
72
|
+
node_type is NodeType.MACHINE.
|
73
|
+
job_id:
|
74
|
+
The id of the job of the node. It should be provided if the
|
75
|
+
node_type is NodeType.JOB.
|
76
|
+
|
77
|
+
Raises:
|
78
|
+
JobShopLibError:
|
79
|
+
If the node_type is OPERATION and operation is None.
|
80
|
+
JobShopLibError:
|
81
|
+
If the node_type is MACHINE and machine_id is None.
|
82
|
+
JobShopLibError:
|
83
|
+
If the node_type is JOB and job_id is None.
|
84
|
+
"""
|
66
85
|
if node_type == NodeType.OPERATION and operation is None:
|
67
|
-
raise
|
86
|
+
raise JobShopLibError("Operation node must have an operation.")
|
68
87
|
|
69
88
|
if node_type == NodeType.MACHINE and machine_id is None:
|
70
|
-
raise
|
89
|
+
raise JobShopLibError("Machine node must have a machine_id.")
|
71
90
|
|
72
91
|
if node_type == NodeType.JOB and job_id is None:
|
73
|
-
raise
|
92
|
+
raise JobShopLibError("Job node must have a job_id.")
|
74
93
|
|
75
94
|
self.node_type = node_type
|
76
95
|
self._node_id: int | None = None
|
@@ -83,7 +102,7 @@ class Node:
|
|
83
102
|
def node_id(self) -> int:
|
84
103
|
"""Returns a unique identifier for the node."""
|
85
104
|
if self._node_id is None:
|
86
|
-
raise
|
105
|
+
raise JobShopLibError("Node has not been assigned an id.")
|
87
106
|
return self._node_id
|
88
107
|
|
89
108
|
@node_id.setter
|
@@ -97,7 +116,7 @@ class Node:
|
|
97
116
|
This property is mandatory for nodes of type `OPERATION`.
|
98
117
|
"""
|
99
118
|
if self._operation is None:
|
100
|
-
raise
|
119
|
+
raise JobShopLibError("Node has no operation.")
|
101
120
|
return self._operation
|
102
121
|
|
103
122
|
@property
|
@@ -107,7 +126,7 @@ class Node:
|
|
107
126
|
This property is mandatory for nodes of type `MACHINE`.
|
108
127
|
"""
|
109
128
|
if self._machine_id is None:
|
110
|
-
raise
|
129
|
+
raise JobShopLibError("Node has no `machine_id`.")
|
111
130
|
return self._machine_id
|
112
131
|
|
113
132
|
@property
|
@@ -117,7 +136,7 @@ class Node:
|
|
117
136
|
This property is mandatory for nodes of type `JOB`.
|
118
137
|
"""
|
119
138
|
if self._job_id is None:
|
120
|
-
raise
|
139
|
+
raise JobShopLibError("Node has no `job_id`.")
|
121
140
|
return self._job_id
|
122
141
|
|
123
142
|
def __hash__(self) -> int:
|
job_shop_lib/operation.py
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
from job_shop_lib import JobShopLibError
|
6
|
+
|
5
7
|
|
6
8
|
class Operation:
|
7
9
|
"""Stores machine and duration information for a job operation.
|
@@ -51,14 +53,14 @@ class Operation:
|
|
51
53
|
ValueError: If the operation has multiple machines in its list.
|
52
54
|
"""
|
53
55
|
if len(self.machines) > 1:
|
54
|
-
raise
|
56
|
+
raise JobShopLibError("Operation has multiple machines.")
|
55
57
|
return self.machines[0]
|
56
58
|
|
57
59
|
@property
|
58
60
|
def job_id(self) -> int:
|
59
61
|
"""Returns the id of the job that the operation belongs to."""
|
60
62
|
if self._job_id is None:
|
61
|
-
raise
|
63
|
+
raise JobShopLibError("Operation has no job_id.")
|
62
64
|
return self._job_id
|
63
65
|
|
64
66
|
@job_id.setter
|
@@ -74,7 +76,7 @@ class Operation:
|
|
74
76
|
ValueError: If the operation has no position_in_job.
|
75
77
|
"""
|
76
78
|
if self._position_in_job is None:
|
77
|
-
raise
|
79
|
+
raise JobShopLibError("Operation has no position_in_job.")
|
78
80
|
return self._position_in_job
|
79
81
|
|
80
82
|
@position_in_job.setter
|
@@ -95,7 +97,7 @@ class Operation:
|
|
95
97
|
ValueError: If the operation has no id.
|
96
98
|
"""
|
97
99
|
if self._operation_id is None:
|
98
|
-
raise
|
100
|
+
raise JobShopLibError("Operation has no id.")
|
99
101
|
return self._operation_id
|
100
102
|
|
101
103
|
@operation_id.setter
|
@@ -4,14 +4,18 @@ dispatching rule solver."""
|
|
4
4
|
import os
|
5
5
|
import pathlib
|
6
6
|
import shutil
|
7
|
-
from
|
7
|
+
from collections.abc import Callable
|
8
8
|
|
9
9
|
import imageio
|
10
10
|
import matplotlib.pyplot as plt
|
11
11
|
from matplotlib.figure import Figure
|
12
12
|
|
13
13
|
from job_shop_lib import JobShopInstance, Schedule, Operation
|
14
|
-
from job_shop_lib.dispatching import
|
14
|
+
from job_shop_lib.dispatching import (
|
15
|
+
DispatchingRuleSolver,
|
16
|
+
Dispatcher,
|
17
|
+
HistoryTracker,
|
18
|
+
)
|
15
19
|
from job_shop_lib.visualization.gantt_chart import plot_gantt_chart
|
16
20
|
|
17
21
|
|
@@ -114,7 +118,11 @@ def plot_gantt_chart_wrapper(
|
|
114
118
|
ha="right",
|
115
119
|
va="bottom",
|
116
120
|
transform=ax.transAxes,
|
117
|
-
bbox=
|
121
|
+
bbox={
|
122
|
+
"facecolor": "white",
|
123
|
+
"alpha": 0.5,
|
124
|
+
"boxstyle": "round,pad=0.5",
|
125
|
+
},
|
118
126
|
)
|
119
127
|
return fig
|
120
128
|
|
@@ -144,22 +152,23 @@ def create_gantt_chart_frames(
|
|
144
152
|
plot_current_time:
|
145
153
|
Whether to plot a vertical line at the current time."""
|
146
154
|
dispatcher = Dispatcher(instance, pruning_function=solver.pruning_function)
|
147
|
-
|
148
|
-
makespan = solver(instance).makespan()
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
155
|
+
history_tracker = HistoryTracker(dispatcher)
|
156
|
+
makespan = solver.solve(instance, dispatcher).makespan()
|
157
|
+
dispatcher.unsubscribe(history_tracker)
|
158
|
+
dispatcher.reset()
|
159
|
+
for i, scheduled_operation in enumerate(history_tracker.history, start=1):
|
160
|
+
dispatcher.dispatch(
|
161
|
+
scheduled_operation.operation, scheduled_operation.machine_id
|
162
|
+
)
|
154
163
|
fig = plot_function(
|
155
|
-
schedule,
|
164
|
+
dispatcher.schedule,
|
156
165
|
makespan,
|
157
166
|
dispatcher.available_operations(),
|
158
167
|
)
|
159
168
|
current_time = (
|
160
169
|
None if not plot_current_time else dispatcher.current_time()
|
161
170
|
)
|
162
|
-
_save_frame(fig, frames_dir,
|
171
|
+
_save_frame(fig, frames_dir, i, current_time)
|
163
172
|
|
164
173
|
|
165
174
|
def _save_frame(
|
@@ -8,6 +8,7 @@ import copy
|
|
8
8
|
import matplotlib
|
9
9
|
import matplotlib.pyplot as plt
|
10
10
|
import networkx as nx
|
11
|
+
from networkx.drawing.nx_agraph import graphviz_layout
|
11
12
|
|
12
13
|
from job_shop_lib import JobShopInstance
|
13
14
|
from job_shop_lib.graphs import (
|
@@ -65,20 +66,9 @@ def plot_disjunctive_graph(
|
|
65
66
|
# Set up the layout
|
66
67
|
# -----------------
|
67
68
|
if layout is None:
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
)
|
72
|
-
|
73
|
-
layout = functools.partial(
|
74
|
-
graphviz_layout, prog="dot", args="-Grankdir=LR"
|
75
|
-
)
|
76
|
-
except ImportError:
|
77
|
-
warnings.warn(
|
78
|
-
"Could not import graphviz_layout. "
|
79
|
-
+ "Using spring_layout instead."
|
80
|
-
)
|
81
|
-
layout = nx.spring_layout
|
69
|
+
layout = functools.partial(
|
70
|
+
graphviz_layout, prog="dot", args="-Grankdir=LR"
|
71
|
+
)
|
82
72
|
|
83
73
|
temp_graph = copy.deepcopy(job_shop_graph.graph)
|
84
74
|
# Remove disjunctive edges to get a better layout
|
@@ -89,11 +79,23 @@ def plot_disjunctive_graph(
|
|
89
79
|
if d["type"] == EdgeType.DISJUNCTIVE
|
90
80
|
]
|
91
81
|
)
|
92
|
-
|
82
|
+
|
83
|
+
try:
|
84
|
+
pos = layout(temp_graph)
|
85
|
+
except ImportError:
|
86
|
+
warnings.warn(
|
87
|
+
"Default layout requires pygraphviz http://pygraphviz.github.io/. "
|
88
|
+
"Using spring layout instead.",
|
89
|
+
)
|
90
|
+
pos = nx.spring_layout(temp_graph)
|
93
91
|
|
94
92
|
# Draw nodes
|
95
93
|
# ----------
|
96
|
-
node_colors = [
|
94
|
+
node_colors = [
|
95
|
+
_get_node_color(node)
|
96
|
+
for node in job_shop_graph.nodes
|
97
|
+
if not job_shop_graph.is_removed(node.node_id)
|
98
|
+
]
|
97
99
|
|
98
100
|
nx.draw_networkx_nodes(
|
99
101
|
job_shop_graph.graph,
|
@@ -147,6 +149,8 @@ def plot_disjunctive_graph(
|
|
147
149
|
sink_node = job_shop_graph.nodes_by_type[NodeType.SINK][0]
|
148
150
|
labels[sink_node] = "T"
|
149
151
|
for operation_node in operation_nodes:
|
152
|
+
if job_shop_graph.is_removed(operation_node.node_id):
|
153
|
+
continue
|
150
154
|
labels[operation_node] = (
|
151
155
|
f"m={operation_node.operation.machine_id}\n"
|
152
156
|
f"d={operation_node.operation.duration}"
|
@@ -1,15 +1,16 @@
|
|
1
|
-
job_shop_lib/__init__.py,sha256=
|
1
|
+
job_shop_lib/__init__.py,sha256=S1847nzbTzRPlh1bkZrsNi7ljGiDkVTGMqodFtPSgBo,582
|
2
2
|
job_shop_lib/base_solver.py,sha256=WCNWVcGGVIV7V2WlTZKcNFsq_BpY4UdhEbYVZ8X3LrE,1368
|
3
3
|
job_shop_lib/benchmarking/__init__.py,sha256=u1P5sP7GCXzPHMYwtqaOfyh9Pvpp9tLaPxyrLYtDk0s,3354
|
4
4
|
job_shop_lib/benchmarking/benchmark_instances.json,sha256=F9EvyzFwVxiKAN6rQTsrMhsKstmyUmroyWduM7a00KQ,464841
|
5
5
|
job_shop_lib/benchmarking/load_benchmark.py,sha256=CjiSALutgWcfD-SDU6w9WO3udvPVpl_-moab3vagbaw,5625
|
6
6
|
job_shop_lib/cp_sat/__init__.py,sha256=DqrF9IewFMkVB5BhFOHhlJvG6w6BW4ecxBXySunGLoU,97
|
7
7
|
job_shop_lib/cp_sat/ortools_solver.py,sha256=zsISUQy0dQvn7bmUsAQBCe-V92CFskJHkSfngSP4KSg,8130
|
8
|
-
job_shop_lib/dispatching/__init__.py,sha256=
|
9
|
-
job_shop_lib/dispatching/dispatcher.py,sha256=
|
10
|
-
job_shop_lib/dispatching/dispatching_rule_solver.py,sha256=
|
8
|
+
job_shop_lib/dispatching/__init__.py,sha256=xk6NjndZ4-EH5G_fGSEX4LQEXL53TRYn5dKEb5uFggI,1568
|
9
|
+
job_shop_lib/dispatching/dispatcher.py,sha256=O9iqytBveJT3WMTELkpNy8IeNiI2Yy8uxbP2ruLLT1A,12582
|
10
|
+
job_shop_lib/dispatching/dispatching_rule_solver.py,sha256=fbNfSclH6Jw1F-QGY1oxAj9wm2hHhJHGnsF2HateXX8,4669
|
11
11
|
job_shop_lib/dispatching/dispatching_rules.py,sha256=SIDkPx_1uTkM0loEqGMqotLBBSaGi1gH0WS85GXrT_I,5557
|
12
12
|
job_shop_lib/dispatching/factories.py,sha256=ldyIbz3QuLuDkrqbgJXV6YoM6AV6CKyHu8z4hXLG2Vo,7267
|
13
|
+
job_shop_lib/dispatching/history_tracker.py,sha256=3jSh7pKEGiOcEK6bXK8AQJK4NtASxTknRjmHRKenxt8,649
|
13
14
|
job_shop_lib/dispatching/pruning_functions.py,sha256=d94_uBHuESp4NSf_jBk1q8eBCfTPuU9meiL3StiqJiA,4378
|
14
15
|
job_shop_lib/exceptions.py,sha256=0Wla1lK6E2u1o3t2hJj9hUwyoJ-1ebkXd42GdXFAhV0,899
|
15
16
|
job_shop_lib/generators/__init__.py,sha256=CrMExfhRbw_0TnYgJ1HwFmq13LEFYFU9wSFANmlSTSQ,154
|
@@ -19,18 +20,18 @@ job_shop_lib/graphs/__init__.py,sha256=mWyF0MypyYfvFhy2F93BJkFIVsxS_0ZqvPuc29B7T
|
|
19
20
|
job_shop_lib/graphs/build_agent_task_graph.py,sha256=ktj-oNLUPmWHfL81EVyaoF4hXClWYfnN7oG2Nn4pOsg,7128
|
20
21
|
job_shop_lib/graphs/build_disjunctive_graph.py,sha256=IRMBtHw8aru5rYGz796-dc6QyaLJFh4LlPlN_BPSq5c,2877
|
21
22
|
job_shop_lib/graphs/constants.py,sha256=dqPF--okue5sF70Iv-YR14QKFx4pxPwT2dL1Rh5jylM,374
|
22
|
-
job_shop_lib/graphs/job_shop_graph.py,sha256=
|
23
|
-
job_shop_lib/graphs/node.py,sha256=
|
23
|
+
job_shop_lib/graphs/job_shop_graph.py,sha256=B0buqcg7US6UvIRWsoY8_FwqzPa_nVjnBu7hPIrygUo,7404
|
24
|
+
job_shop_lib/graphs/node.py,sha256=FrSndtvqgRbN69jIcU6q1TkBh-LOGg8sxxYjDZqCcf4,5613
|
24
25
|
job_shop_lib/job_shop_instance.py,sha256=ZB0NOcTvGSq0zmmxiDceaC0DH9ljpJXD0hfKOmP0jcE,12801
|
25
|
-
job_shop_lib/operation.py,sha256=
|
26
|
+
job_shop_lib/operation.py,sha256=bZclBeyge71Avm9ArwvGuBKZp5Idw5EUm6m35jav0C4,3924
|
26
27
|
job_shop_lib/schedule.py,sha256=1xzue2ro927VZw9IWg_tlBLZ7kDbN091aOW6ZMEjOYQ,6509
|
27
28
|
job_shop_lib/scheduled_operation.py,sha256=EfG5AcrspjO3erhM2ejlSOtYKNnaNTsLEu2gu2N3FxA,3127
|
28
29
|
job_shop_lib/visualization/__init__.py,sha256=Kxjk3ERYXPAHR72nkD92gFdJltSLA2kxLZrlZzZJS8o,693
|
29
30
|
job_shop_lib/visualization/agent_task_graph.py,sha256=G-c9eiawz6m9sdnDM1r-ZHz6K-gYDIAreHpb6pkYE7w,8284
|
30
|
-
job_shop_lib/visualization/create_gif.py,sha256=
|
31
|
-
job_shop_lib/visualization/disjunctive_graph.py,sha256=
|
31
|
+
job_shop_lib/visualization/create_gif.py,sha256=KrwMpSYvSCsL5Ld3taiNHSl_QDrODLpqM-MKQG_C2oU,6674
|
32
|
+
job_shop_lib/visualization/disjunctive_graph.py,sha256=pg4KG9BfQbnBPnXYgbyPGe0AuHSmhYqPeqWYAf_spWQ,5905
|
32
33
|
job_shop_lib/visualization/gantt_chart.py,sha256=OyBMBnjSsRC769qXimJ3IIQWlssgPfx-nlVeSeU5sWY,4415
|
33
|
-
job_shop_lib-0.
|
34
|
-
job_shop_lib-0.
|
35
|
-
job_shop_lib-0.
|
36
|
-
job_shop_lib-0.
|
34
|
+
job_shop_lib-0.3.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
35
|
+
job_shop_lib-0.3.0.dist-info/METADATA,sha256=DPjUe3anmq-s0y-_09o8uBjvFTmHqVt5N_AI3BPu1AM,12624
|
36
|
+
job_shop_lib-0.3.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
37
|
+
job_shop_lib-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|