job-shop-lib 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- job_shop_lib/__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
|