job-shop-lib 0.2.1__py3-none-any.whl → 0.4.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 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
- from job_shop_lib.exceptions import JobShopLibError, NoSolutionFoundError
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
- The pipeline of pruning methods to be used to filter out
31
- operations from the list of available operations.
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
- pruning_strategies:
56
- A list of pruning strategies to be used to filter out
57
- operations from the list of available operations. Supported
58
- values are 'dominated_operations' and 'non_immediate_machines'.
59
- Defaults to [PruningStrategy.DOMINATED_OPERATIONS]. To disable
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.pruning_function = pruning_function
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
- """Creates a schedule from a raw solution.
91
-
92
- A raw solution is a list of lists of operations, where each list
93
- represents the order of operations for a machine.
94
-
95
- Args:
96
- instance:
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.job_next_operation_index[operation.job_id]
243
+ self._job_next_operation_index[operation.job_id]
168
244
  == operation.position_in_job
169
245
  )
170
246
 
171
- def start_time(self, operation: Operation, machine_id: int) -> int:
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.machine_next_available_time[machine_id],
188
- self.job_next_available_time[operation.job_id],
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.machine_next_available_time[machine_id] = end_time
200
- self.job_next_operation_index[job_id] += 1
201
- self.job_next_available_time[job_id] = end_time
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
- return self.min_start_time(available_operations)
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.job_next_operation_index):
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 known to be bad choices.
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 to exclude
242
- ones that are deemed inefficient or suboptimal choices.
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.job_next_operation_index):
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(self, instance: JobShopInstance) -> Schedule:
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 = Dispatcher(
83
- instance, pruning_function=self.pruning_function
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 cProfile
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, Operation
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.nodes: list[Node] = []
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 num_nodes(self) -> int:
90
- """Number of nodes in the graph."""
91
- return len(self.nodes)
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.nodes_by_type[node_for_adding.node_type].append(node_for_adding)
135
- self.nodes.append(node_for_adding)
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.nodes_by_job[operation.job_id].append(node_for_adding)
144
+ self._nodes_by_job[operation.job_id].append(node_for_adding)
141
145
  for machine_id in operation.machines:
142
- self.nodes_by_machine[machine_id].append(node_for_adding)
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
- ValueError: If `u_of_edge` or `v_of_edge` are not in the graph.
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 ValueError(
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]
@@ -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
- Args:
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 ValueError("Operation node must have an operation.")
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 ValueError("Machine node must have a machine_id.")
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 ValueError("Job node must have a job_id.")
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 ValueError("Node has not been assigned an id.")
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 ValueError("Node has no operation.")
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 ValueError("Node has no `machine_id`.")
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 ValueError("Node has no `job_id`.")
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 ValueError("Operation has multiple machines.")
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 ValueError("Operation has no job_id.")
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 ValueError("Operation has no position_in_job.")
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 ValueError("Operation has no id.")
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, __value: object) -> bool:
109
- if isinstance(__value, Operation):
110
- return self.operation_id == __value.operation_id
111
- return False
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 job_shop_lib import ScheduledOperation, JobShopInstance
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 job_shop_lib import Operation
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 ValueError(
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 ValueError("Operation has no job_id.")
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 ValueError("Operation has no position.")
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 is value.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 typing import Callable
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 DispatchingRuleSolver, Dispatcher
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=dict(facecolor="white", alpha=0.5, boxstyle="round,pad=0.5"),
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
- schedule = dispatcher.schedule
148
- makespan = solver(instance).makespan()
149
- iteration = 0
150
-
151
- while not schedule.is_complete():
152
- solver.step(dispatcher)
153
- iteration += 1
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, iteration, current_time)
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
- try:
69
- from networkx.drawing.nx_agraph import (
70
- graphviz_layout,
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
- pos = layout(temp_graph) # type: ignore
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 = [_get_node_color(node) for node in job_shop_graph.nodes]
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)
5
5
  License: MIT
6
6
  Author: Pabloo22
@@ -1,15 +1,16 @@
1
- job_shop_lib/__init__.py,sha256=fnRe5gLpfMWvC0cySvSlhmNmxX0gJzP6nHSx3I2_nh8,581
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=8lWYVqUOTM43k53YJ8HBFH8nyf5mlWk2CnpEDpyqrF0,1432
9
- job_shop_lib/dispatching/dispatcher.py,sha256=o7NcMS3cKOvtpJXZD1ftCYfdRQL1hLu4LbAZiksgNUo,10135
10
- job_shop_lib/dispatching/dispatching_rule_solver.py,sha256=a6SY8KviN9STalUE2ouAfKkQkhxFNUmM1lCabQtK3NM,4392
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=2LDtZR1s61Lb-XjVE6SSu1Foca_YeMoCDVgT3XuNZKk,6229
23
- job_shop_lib/graphs/node.py,sha256=wyKhSK6kUPWucPtbBv8E_BlFyFPg352u5fRBUnC6Mos,4880
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=dERsRpZLUwHMAPItd_KIHhbrKv0d1aS9GBabORktoEY,3862
26
- job_shop_lib/schedule.py,sha256=1xzue2ro927VZw9IWg_tlBLZ7kDbN091aOW6ZMEjOYQ,6509
27
- job_shop_lib/scheduled_operation.py,sha256=EfG5AcrspjO3erhM2ejlSOtYKNnaNTsLEu2gu2N3FxA,3127
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=3j339wjgGZKLOyMWGdVqVBQu4WFDUhyualHx8b3CJMQ,6382
31
- job_shop_lib/visualization/disjunctive_graph.py,sha256=feiRAMxuG5CG2naO7I3HtcrSQw99yWxWzIGgZC_pxIs,5803
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.2.1.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
34
- job_shop_lib-0.2.1.dist-info/METADATA,sha256=DFmlsg1-gGkYYsqmUwMrNw_T31Vmu8WI_myPX2JSY6I,12624
35
- job_shop_lib-0.2.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
36
- job_shop_lib-0.2.1.dist-info/RECORD,,
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,,