job-shop-lib 0.2.1__py3-none-any.whl → 0.4.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 +125 -46
- 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 +10 -8
- job_shop_lib/schedule.py +96 -1
- job_shop_lib/scheduled_operation.py +17 -5
- 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.4.0.dist-info}/METADATA +1 -1
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.4.0.dist-info}/RECORD +16 -15
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.4.0.dist-info}/LICENSE +0 -0
- {job_shop_lib-0.2.1.dist-info → job_shop_lib-0.4.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,12 @@
|
|
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
|
10
|
+
from warnings import warn
|
7
11
|
|
8
12
|
from job_shop_lib import (
|
9
13
|
JobShopInstance,
|
@@ -13,6 +17,59 @@ from job_shop_lib import (
|
|
13
17
|
)
|
14
18
|
|
15
19
|
|
20
|
+
# Added here to avoid circular imports
|
21
|
+
class DispatcherObserver(abc.ABC):
|
22
|
+
"""Interface for classes that observe the dispatcher."""
|
23
|
+
|
24
|
+
def __init__(self, dispatcher: Dispatcher):
|
25
|
+
"""Initializes the observer with the dispatcher and subscribes to
|
26
|
+
it."""
|
27
|
+
self.dispatcher = dispatcher
|
28
|
+
self.dispatcher.subscribe(self)
|
29
|
+
|
30
|
+
@abc.abstractmethod
|
31
|
+
def update(self, scheduled_operation: ScheduledOperation):
|
32
|
+
"""Called when an operation is scheduled on a machine."""
|
33
|
+
|
34
|
+
@abc.abstractmethod
|
35
|
+
def reset(self):
|
36
|
+
"""Called when the dispatcher is reset."""
|
37
|
+
|
38
|
+
def __str__(self) -> str:
|
39
|
+
return self.__class__.__name__
|
40
|
+
|
41
|
+
def __repr__(self) -> str:
|
42
|
+
return str(self)
|
43
|
+
|
44
|
+
|
45
|
+
def _dispatcher_cache(method):
|
46
|
+
"""Decorator to cache results of a method based on its name.
|
47
|
+
|
48
|
+
This decorator assumes that the class has an attribute called `_cache`
|
49
|
+
that is a dictionary. It caches the result of the method based on its
|
50
|
+
name. If the result is already cached, it returns the cached result
|
51
|
+
instead of recomputing it.
|
52
|
+
|
53
|
+
The decorator is useful since the dispatcher class can clear the cache
|
54
|
+
when the state of the dispatcher changes.
|
55
|
+
"""
|
56
|
+
|
57
|
+
@wraps(method)
|
58
|
+
def wrapper(self: Dispatcher, *args, **kwargs):
|
59
|
+
# pylint: disable=protected-access
|
60
|
+
cache_key = method.__name__
|
61
|
+
cached_result = self._cache.get(cache_key)
|
62
|
+
if cached_result is not None:
|
63
|
+
return cached_result
|
64
|
+
|
65
|
+
result = method(self, *args, **kwargs)
|
66
|
+
self._cache[cache_key] = result
|
67
|
+
return result
|
68
|
+
|
69
|
+
return wrapper
|
70
|
+
|
71
|
+
|
72
|
+
# pylint: disable=too-many-instance-attributes
|
16
73
|
class Dispatcher:
|
17
74
|
"""Handles the logic of scheduling operations on machines.
|
18
75
|
|
@@ -27,8 +84,10 @@ class Dispatcher:
|
|
27
84
|
schedule:
|
28
85
|
The schedule of operations on machines.
|
29
86
|
pruning_function:
|
30
|
-
|
31
|
-
|
87
|
+
A function that filters out operations that are not ready to be
|
88
|
+
scheduled.
|
89
|
+
subscribers:
|
90
|
+
A list of observers that are subscribed to the dispatcher.
|
32
91
|
"""
|
33
92
|
|
34
93
|
__slots__ = (
|
@@ -38,6 +97,8 @@ class Dispatcher:
|
|
38
97
|
"_job_next_operation_index",
|
39
98
|
"_job_next_available_time",
|
40
99
|
"pruning_function",
|
100
|
+
"subscribers",
|
101
|
+
"_cache",
|
41
102
|
)
|
42
103
|
|
43
104
|
def __init__(
|
@@ -52,20 +113,28 @@ class Dispatcher:
|
|
52
113
|
Args:
|
53
114
|
instance:
|
54
115
|
The instance of the job shop problem to be solved.
|
55
|
-
|
56
|
-
A
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
pruning, pass an empty list.
|
116
|
+
pruning_function:
|
117
|
+
A function that filters out operations that are not ready to
|
118
|
+
be scheduled. The function should take the dispatcher and a
|
119
|
+
list of operations as input and return a list of operations
|
120
|
+
that are ready to be scheduled. If `None`, no pruning is done.
|
61
121
|
"""
|
62
122
|
|
63
123
|
self.instance = instance
|
64
124
|
self.schedule = Schedule(self.instance)
|
125
|
+
self.pruning_function = pruning_function
|
126
|
+
|
65
127
|
self._machine_next_available_time = [0] * self.instance.num_machines
|
66
128
|
self._job_next_operation_index = [0] * self.instance.num_jobs
|
67
129
|
self._job_next_available_time = [0] * self.instance.num_jobs
|
68
|
-
self.
|
130
|
+
self.subscribers: list[DispatcherObserver] = []
|
131
|
+
self._cache: dict[str, Any] = {}
|
132
|
+
|
133
|
+
def __str__(self) -> str:
|
134
|
+
return f"{self.__class__.__name__}({self.instance})"
|
135
|
+
|
136
|
+
def __repr__(self) -> str:
|
137
|
+
return str(self)
|
69
138
|
|
70
139
|
@property
|
71
140
|
def machine_next_available_time(self) -> list[int]:
|
@@ -87,21 +156,13 @@ class Dispatcher:
|
|
87
156
|
def create_schedule_from_raw_solution(
|
88
157
|
cls, instance: JobShopInstance, raw_solution: list[list[Operation]]
|
89
158
|
) -> Schedule:
|
90
|
-
"""
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
The instance of the job shop problem to be solved.
|
98
|
-
raw_solution:
|
99
|
-
A list of lists of operations, where each list represents the
|
100
|
-
order of operations for a machine.
|
101
|
-
|
102
|
-
Returns:
|
103
|
-
A Schedule object representing the solution.
|
104
|
-
"""
|
159
|
+
"""Deprecated method, use `Schedule.from_job_sequences` instead."""
|
160
|
+
warn(
|
161
|
+
"Dispatcher.create_schedule_from_raw_solution is deprecated. "
|
162
|
+
"Use Schedule.from_job_sequences instead. It will be removed in "
|
163
|
+
"version 1.0.0.",
|
164
|
+
DeprecationWarning,
|
165
|
+
)
|
105
166
|
dispatcher = cls(instance)
|
106
167
|
dispatcher.reset()
|
107
168
|
raw_solution_deques = [
|
@@ -117,12 +178,23 @@ class Dispatcher:
|
|
117
178
|
operations.popleft()
|
118
179
|
return dispatcher.schedule
|
119
180
|
|
181
|
+
def subscribe(self, observer: DispatcherObserver):
|
182
|
+
"""Subscribes an observer to the dispatcher."""
|
183
|
+
self.subscribers.append(observer)
|
184
|
+
|
185
|
+
def unsubscribe(self, observer: DispatcherObserver):
|
186
|
+
"""Unsubscribes an observer from the dispatcher."""
|
187
|
+
self.subscribers.remove(observer)
|
188
|
+
|
120
189
|
def reset(self) -> None:
|
121
190
|
"""Resets the dispatcher to its initial state."""
|
122
191
|
self.schedule.reset()
|
123
192
|
self._machine_next_available_time = [0] * self.instance.num_machines
|
124
193
|
self._job_next_operation_index = [0] * self.instance.num_jobs
|
125
194
|
self._job_next_available_time = [0] * self.instance.num_jobs
|
195
|
+
self._cache = {}
|
196
|
+
for subscriber in self.subscribers:
|
197
|
+
subscriber.reset()
|
126
198
|
|
127
199
|
def dispatch(self, operation: Operation, machine_id: int) -> None:
|
128
200
|
"""Schedules the given operation on the given machine.
|
@@ -153,6 +225,10 @@ class Dispatcher:
|
|
153
225
|
self.schedule.add(scheduled_operation)
|
154
226
|
self._update_tracking_attributes(scheduled_operation)
|
155
227
|
|
228
|
+
# Notify subscribers
|
229
|
+
for subscriber in self.subscribers:
|
230
|
+
subscriber.update(scheduled_operation)
|
231
|
+
|
156
232
|
def is_operation_ready(self, operation: Operation) -> bool:
|
157
233
|
"""Returns True if the given operation is ready to be scheduled.
|
158
234
|
|
@@ -164,11 +240,15 @@ class Dispatcher:
|
|
164
240
|
The operation to be checked.
|
165
241
|
"""
|
166
242
|
return (
|
167
|
-
self.
|
243
|
+
self._job_next_operation_index[operation.job_id]
|
168
244
|
== operation.position_in_job
|
169
245
|
)
|
170
246
|
|
171
|
-
def start_time(
|
247
|
+
def start_time(
|
248
|
+
self,
|
249
|
+
operation: Operation,
|
250
|
+
machine_id: int,
|
251
|
+
) -> int:
|
172
252
|
"""Computes the start time for the given operation on the given
|
173
253
|
machine.
|
174
254
|
|
@@ -181,11 +261,12 @@ class Dispatcher:
|
|
181
261
|
The operation to be scheduled.
|
182
262
|
machine_id:
|
183
263
|
The id of the machine on which the operation is to be
|
184
|
-
scheduled.
|
264
|
+
scheduled. If None, the start time is computed based on the
|
265
|
+
next available time for the operation on any machine.
|
185
266
|
"""
|
186
267
|
return max(
|
187
|
-
self.
|
188
|
-
self.
|
268
|
+
self._machine_next_available_time[machine_id],
|
269
|
+
self._job_next_available_time[operation.job_id],
|
189
270
|
)
|
190
271
|
|
191
272
|
def _update_tracking_attributes(
|
@@ -196,10 +277,12 @@ class Dispatcher:
|
|
196
277
|
machine_id = scheduled_operation.machine_id
|
197
278
|
end_time = scheduled_operation.end_time
|
198
279
|
|
199
|
-
self.
|
200
|
-
self.
|
201
|
-
self.
|
280
|
+
self._machine_next_available_time[machine_id] = end_time
|
281
|
+
self._job_next_operation_index[job_id] += 1
|
282
|
+
self._job_next_available_time[job_id] = end_time
|
283
|
+
self._cache = {}
|
202
284
|
|
285
|
+
@_dispatcher_cache
|
203
286
|
def current_time(self) -> int:
|
204
287
|
"""Returns the current time of the schedule.
|
205
288
|
|
@@ -207,7 +290,8 @@ class Dispatcher:
|
|
207
290
|
operations.
|
208
291
|
"""
|
209
292
|
available_operations = self.available_operations()
|
210
|
-
|
293
|
+
current_time = self.min_start_time(available_operations)
|
294
|
+
return current_time
|
211
295
|
|
212
296
|
def min_start_time(self, operations: list[Operation]) -> int:
|
213
297
|
"""Returns the minimum start time of the available operations."""
|
@@ -220,6 +304,7 @@ class Dispatcher:
|
|
220
304
|
min_start_time = min(min_start_time, start_time)
|
221
305
|
return int(min_start_time)
|
222
306
|
|
307
|
+
@_dispatcher_cache
|
223
308
|
def uncompleted_operations(self) -> list[Operation]:
|
224
309
|
"""Returns the list of operations that have not been scheduled.
|
225
310
|
|
@@ -228,30 +313,24 @@ class Dispatcher:
|
|
228
313
|
It is more efficient than checking all operations in the instance.
|
229
314
|
"""
|
230
315
|
uncompleted_operations = []
|
231
|
-
for job_id, next_position in enumerate(self.
|
316
|
+
for job_id, next_position in enumerate(self._job_next_operation_index):
|
232
317
|
operations = self.instance.jobs[job_id][next_position:]
|
233
318
|
uncompleted_operations.extend(operations)
|
234
319
|
return uncompleted_operations
|
235
320
|
|
321
|
+
@_dispatcher_cache
|
236
322
|
def available_operations(self) -> list[Operation]:
|
237
323
|
"""Returns a list of available operations for processing, optionally
|
238
|
-
filtering out operations
|
324
|
+
filtering out operations using the pruning function.
|
239
325
|
|
240
326
|
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.
|
327
|
+
being processed. It then optionally filters these operations using the
|
328
|
+
pruning function.
|
247
329
|
|
248
330
|
Returns:
|
249
331
|
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
332
|
"""
|
333
|
+
|
255
334
|
available_operations = self._available_operations()
|
256
335
|
if self.pruning_function is not None:
|
257
336
|
available_operations = self.pruning_function(
|
@@ -261,7 +340,7 @@ class Dispatcher:
|
|
261
340
|
|
262
341
|
def _available_operations(self) -> list[Operation]:
|
263
342
|
available_operations = []
|
264
|
-
for job_id, next_position in enumerate(self.
|
343
|
+
for job_id, next_position in enumerate(self._job_next_operation_index):
|
265
344
|
if next_position == len(self.instance.jobs[job_id]):
|
266
345
|
continue
|
267
346
|
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
|
@@ -105,10 +107,10 @@ class Operation:
|
|
105
107
|
def __hash__(self) -> int:
|
106
108
|
return hash(self.operation_id)
|
107
109
|
|
108
|
-
def __eq__(self,
|
109
|
-
if isinstance(
|
110
|
-
return
|
111
|
-
return
|
110
|
+
def __eq__(self, value: object) -> bool:
|
111
|
+
if not isinstance(value, Operation):
|
112
|
+
return False
|
113
|
+
return self.__slots__ == value.__slots__
|
112
114
|
|
113
115
|
def __repr__(self) -> str:
|
114
116
|
machines = (
|
job_shop_lib/schedule.py
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
"""Home of the `Schedule` class."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
from collections import deque
|
7
|
+
|
8
|
+
from job_shop_lib import ScheduledOperation, JobShopInstance, JobShopLibError
|
4
9
|
|
5
10
|
|
6
11
|
class Schedule:
|
@@ -71,6 +76,96 @@ class Schedule:
|
|
71
76
|
"""Returns the number of operations that have been scheduled."""
|
72
77
|
return sum(len(machine_schedule) for machine_schedule in self.schedule)
|
73
78
|
|
79
|
+
def to_dict(self) -> dict:
|
80
|
+
"""Returns a dictionary representation of the schedule.
|
81
|
+
|
82
|
+
This representation is useful for saving the instance to a JSON file.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
A dictionary representation of the schedule with the following
|
86
|
+
keys:
|
87
|
+
- "instance": A dictionary representation of the instance.
|
88
|
+
- "job_sequences": A list of lists of job ids. Each list of job
|
89
|
+
ids represents the order of operations on the machine. The
|
90
|
+
machine that the list corresponds to is determined by the
|
91
|
+
index of the list.
|
92
|
+
- "metadata": A dictionary with additional information about
|
93
|
+
the schedule.
|
94
|
+
"""
|
95
|
+
job_sequences: list[list[int]] = []
|
96
|
+
for machine_schedule in self.schedule:
|
97
|
+
job_sequences.append(
|
98
|
+
[operation.job_id for operation in machine_schedule]
|
99
|
+
)
|
100
|
+
|
101
|
+
return {
|
102
|
+
"instance": self.instance.to_dict(),
|
103
|
+
"job_sequences": job_sequences,
|
104
|
+
"metadata": self.metadata,
|
105
|
+
}
|
106
|
+
|
107
|
+
@staticmethod
|
108
|
+
def from_dict(
|
109
|
+
instance: dict[str, Any] | JobShopInstance,
|
110
|
+
job_sequences: list[list[int]],
|
111
|
+
metadata: dict[str, Any] | None = None,
|
112
|
+
) -> Schedule:
|
113
|
+
"""Creates a schedule from a dictionary representation."""
|
114
|
+
if isinstance(instance, dict):
|
115
|
+
instance = JobShopInstance.from_matrices(**instance)
|
116
|
+
schedule = Schedule.from_job_sequences(instance, job_sequences)
|
117
|
+
schedule.metadata = metadata if metadata is not None else {}
|
118
|
+
return schedule
|
119
|
+
|
120
|
+
@staticmethod
|
121
|
+
def from_job_sequences(
|
122
|
+
instance: JobShopInstance,
|
123
|
+
job_sequences: list[list[int]],
|
124
|
+
) -> Schedule:
|
125
|
+
"""Creates an active schedule from a list of job sequences.
|
126
|
+
|
127
|
+
An active schedule is the optimal schedule for the given job sequences.
|
128
|
+
In other words, it is not possible to construct another schedule,
|
129
|
+
through changes in the order of processing on the machines, with at
|
130
|
+
least one operation finishing earlier and no operation finishing later.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
instance:
|
134
|
+
The `JobShopInstance` object that the schedule is for.
|
135
|
+
job_sequences:
|
136
|
+
A list of lists of job ids. Each list of job ids represents the
|
137
|
+
order of operations on the machine. The machine that the list
|
138
|
+
corresponds to is determined by the index of the list.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
A `Schedule` object with the given job sequences.
|
142
|
+
"""
|
143
|
+
from job_shop_lib.dispatching import Dispatcher
|
144
|
+
|
145
|
+
dispatcher = Dispatcher(instance)
|
146
|
+
dispatcher.reset()
|
147
|
+
raw_solution_deques = [deque(job_ids) for job_ids in job_sequences]
|
148
|
+
|
149
|
+
while not dispatcher.schedule.is_complete():
|
150
|
+
at_least_one_operation_scheduled = False
|
151
|
+
for machine_id, job_ids in enumerate(raw_solution_deques):
|
152
|
+
if not job_ids:
|
153
|
+
continue
|
154
|
+
job_id = job_ids[0]
|
155
|
+
operation_index = dispatcher.job_next_operation_index[job_id]
|
156
|
+
operation = instance.jobs[job_id][operation_index]
|
157
|
+
is_ready = dispatcher.is_operation_ready(operation)
|
158
|
+
if is_ready and machine_id in operation.machines:
|
159
|
+
dispatcher.dispatch(operation, machine_id)
|
160
|
+
job_ids.popleft()
|
161
|
+
at_least_one_operation_scheduled = True
|
162
|
+
|
163
|
+
if not at_least_one_operation_scheduled:
|
164
|
+
raise JobShopLibError(
|
165
|
+
"Invalid job sequences. No valid operation to schedule."
|
166
|
+
)
|
167
|
+
return dispatcher.schedule
|
168
|
+
|
74
169
|
def reset(self):
|
75
170
|
"""Resets the schedule to an empty state."""
|
76
171
|
self.schedule = [[] for _ in range(self.instance.num_machines)]
|
@@ -1,6 +1,8 @@
|
|
1
1
|
"""Home of the `ScheduledOperation` class."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from warnings import warn
|
4
|
+
|
5
|
+
from job_shop_lib import Operation, JobShopLibError
|
4
6
|
|
5
7
|
|
6
8
|
class ScheduledOperation:
|
@@ -47,7 +49,7 @@ class ScheduledOperation:
|
|
47
49
|
@machine_id.setter
|
48
50
|
def machine_id(self, value: int):
|
49
51
|
if value not in self.operation.machines:
|
50
|
-
raise
|
52
|
+
raise JobShopLibError(
|
51
53
|
f"Operation cannot be scheduled on machine {value}. "
|
52
54
|
f"Valid machines are {self.operation.machines}."
|
53
55
|
)
|
@@ -62,18 +64,28 @@ class ScheduledOperation:
|
|
62
64
|
"""
|
63
65
|
|
64
66
|
if self.operation.job_id is None:
|
65
|
-
raise
|
67
|
+
raise JobShopLibError("Operation has no job_id.")
|
66
68
|
return self.operation.job_id
|
67
69
|
|
68
70
|
@property
|
69
71
|
def position(self) -> int:
|
72
|
+
"""Deprecated. Use `position_in_job` instead."""
|
73
|
+
warn(
|
74
|
+
"The `position` attribute is deprecated. Use `position_in_job` "
|
75
|
+
"instead. It will be removed in version 1.0.0.",
|
76
|
+
DeprecationWarning,
|
77
|
+
)
|
78
|
+
return self.position_in_job
|
79
|
+
|
80
|
+
@property
|
81
|
+
def position_in_job(self) -> int:
|
70
82
|
"""Returns the position (starting at zero) of the operation in the job.
|
71
83
|
|
72
84
|
Raises:
|
73
85
|
ValueError: If the operation has no position_in_job.
|
74
86
|
"""
|
75
87
|
if self.operation.position_in_job is None:
|
76
|
-
raise
|
88
|
+
raise JobShopLibError("Operation has no position.")
|
77
89
|
return self.operation.position_in_job
|
78
90
|
|
79
91
|
@property
|
@@ -91,7 +103,7 @@ class ScheduledOperation:
|
|
91
103
|
if not isinstance(value, ScheduledOperation):
|
92
104
|
return False
|
93
105
|
return (
|
94
|
-
self.operation
|
106
|
+
self.operation == value.operation
|
95
107
|
and self.start_time == value.start_time
|
96
108
|
and self.machine_id == value.machine_id
|
97
109
|
)
|
@@ -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=2rzvmn6EQz2gIS8tP2tUPJ34uUq7SZzJubHmrjw_qV8,12394
|
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/schedule.py,sha256=
|
27
|
-
job_shop_lib/scheduled_operation.py,sha256=
|
26
|
+
job_shop_lib/operation.py,sha256=S61x0xgu09JLwrRp7syd1P2psbl0ByGuK_hHoHp4ng8,3916
|
27
|
+
job_shop_lib/schedule.py,sha256=aODGwMv9slFIqOTCz2hF_EIpXhddz8-iAH5gSzGO5G8,10393
|
28
|
+
job_shop_lib/scheduled_operation.py,sha256=qzXzat1dQBbQ-sLyoG1iXbF9eWbdFeZDFjhAFVavHPk,3526
|
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.4.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
|
35
|
+
job_shop_lib-0.4.0.dist-info/METADATA,sha256=nWD2fekWRXglydO3tihH7tLHvYVhILm_TASf-Yn82qA,12624
|
36
|
+
job_shop_lib-0.4.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
37
|
+
job_shop_lib-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|