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 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,,