job-shop-lib 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
job_shop_lib/__init__.py 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,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import abc
6
+ from typing import Any
5
7
  from collections.abc import Callable
6
8
  from collections import deque
9
+ from functools import wraps
7
10
 
8
11
  from job_shop_lib import (
9
12
  JobShopInstance,
@@ -13,6 +16,59 @@ from job_shop_lib import (
13
16
  )
14
17
 
15
18
 
19
+ # Added here to avoid circular imports
20
+ class DispatcherObserver(abc.ABC):
21
+ """Interface for classes that observe the dispatcher."""
22
+
23
+ def __init__(self, dispatcher: Dispatcher):
24
+ """Initializes the observer with the dispatcher and subscribes to
25
+ it."""
26
+ self.dispatcher = dispatcher
27
+ self.dispatcher.subscribe(self)
28
+
29
+ @abc.abstractmethod
30
+ def update(self, scheduled_operation: ScheduledOperation):
31
+ """Called when an operation is scheduled on a machine."""
32
+
33
+ @abc.abstractmethod
34
+ def reset(self):
35
+ """Called when the dispatcher is reset."""
36
+
37
+ def __str__(self) -> str:
38
+ return self.__class__.__name__
39
+
40
+ def __repr__(self) -> str:
41
+ return str(self)
42
+
43
+
44
+ def _dispatcher_cache(method):
45
+ """Decorator to cache results of a method based on its name.
46
+
47
+ This decorator assumes that the class has an attribute called `_cache`
48
+ that is a dictionary. It caches the result of the method based on its
49
+ name. If the result is already cached, it returns the cached result
50
+ instead of recomputing it.
51
+
52
+ The decorator is useful since the dispatcher class can clear the cache
53
+ when the state of the dispatcher changes.
54
+ """
55
+
56
+ @wraps(method)
57
+ def wrapper(self: Dispatcher, *args, **kwargs):
58
+ # pylint: disable=protected-access
59
+ cache_key = method.__name__
60
+ cached_result = self._cache.get(cache_key)
61
+ if cached_result is not None:
62
+ return cached_result
63
+
64
+ result = method(self, *args, **kwargs)
65
+ self._cache[cache_key] = result
66
+ return result
67
+
68
+ return wrapper
69
+
70
+
71
+ # pylint: disable=too-many-instance-attributes
16
72
  class Dispatcher:
17
73
  """Handles the logic of scheduling operations on machines.
18
74
 
@@ -27,8 +83,10 @@ class Dispatcher:
27
83
  schedule:
28
84
  The schedule of operations on machines.
29
85
  pruning_function:
30
- The pipeline of pruning methods to be used to filter out
31
- operations from the list of available operations.
86
+ A function that filters out operations that are not ready to be
87
+ scheduled.
88
+ subscribers:
89
+ A list of observers that are subscribed to the dispatcher.
32
90
  """
33
91
 
34
92
  __slots__ = (
@@ -38,6 +96,8 @@ class Dispatcher:
38
96
  "_job_next_operation_index",
39
97
  "_job_next_available_time",
40
98
  "pruning_function",
99
+ "subscribers",
100
+ "_cache",
41
101
  )
42
102
 
43
103
  def __init__(
@@ -52,20 +112,28 @@ class Dispatcher:
52
112
  Args:
53
113
  instance:
54
114
  The instance of the job shop problem to be solved.
55
- 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.
115
+ pruning_function:
116
+ A function that filters out operations that are not ready to
117
+ be scheduled. The function should take the dispatcher and a
118
+ list of operations as input and return a list of operations
119
+ that are ready to be scheduled. If `None`, no pruning is done.
61
120
  """
62
121
 
63
122
  self.instance = instance
64
123
  self.schedule = Schedule(self.instance)
124
+ self.pruning_function = pruning_function
125
+
65
126
  self._machine_next_available_time = [0] * self.instance.num_machines
66
127
  self._job_next_operation_index = [0] * self.instance.num_jobs
67
128
  self._job_next_available_time = [0] * self.instance.num_jobs
68
- self.pruning_function = pruning_function
129
+ self.subscribers: list[DispatcherObserver] = []
130
+ self._cache: dict[str, Any] = {}
131
+
132
+ def __str__(self) -> str:
133
+ return f"{self.__class__.__name__}({self.instance})"
134
+
135
+ def __repr__(self) -> str:
136
+ return str(self)
69
137
 
70
138
  @property
71
139
  def machine_next_available_time(self) -> list[int]:
@@ -117,12 +185,23 @@ class Dispatcher:
117
185
  operations.popleft()
118
186
  return dispatcher.schedule
119
187
 
188
+ def subscribe(self, observer: DispatcherObserver):
189
+ """Subscribes an observer to the dispatcher."""
190
+ self.subscribers.append(observer)
191
+
192
+ def unsubscribe(self, observer: DispatcherObserver):
193
+ """Unsubscribes an observer from the dispatcher."""
194
+ self.subscribers.remove(observer)
195
+
120
196
  def reset(self) -> None:
121
197
  """Resets the dispatcher to its initial state."""
122
198
  self.schedule.reset()
123
199
  self._machine_next_available_time = [0] * self.instance.num_machines
124
200
  self._job_next_operation_index = [0] * self.instance.num_jobs
125
201
  self._job_next_available_time = [0] * self.instance.num_jobs
202
+ self._cache = {}
203
+ for subscriber in self.subscribers:
204
+ subscriber.reset()
126
205
 
127
206
  def dispatch(self, operation: Operation, machine_id: int) -> None:
128
207
  """Schedules the given operation on the given machine.
@@ -153,6 +232,10 @@ class Dispatcher:
153
232
  self.schedule.add(scheduled_operation)
154
233
  self._update_tracking_attributes(scheduled_operation)
155
234
 
235
+ # Notify subscribers
236
+ for subscriber in self.subscribers:
237
+ subscriber.update(scheduled_operation)
238
+
156
239
  def is_operation_ready(self, operation: Operation) -> bool:
157
240
  """Returns True if the given operation is ready to be scheduled.
158
241
 
@@ -164,11 +247,15 @@ class Dispatcher:
164
247
  The operation to be checked.
165
248
  """
166
249
  return (
167
- self.job_next_operation_index[operation.job_id]
250
+ self._job_next_operation_index[operation.job_id]
168
251
  == operation.position_in_job
169
252
  )
170
253
 
171
- def start_time(self, operation: Operation, machine_id: int) -> int:
254
+ def start_time(
255
+ self,
256
+ operation: Operation,
257
+ machine_id: int,
258
+ ) -> int:
172
259
  """Computes the start time for the given operation on the given
173
260
  machine.
174
261
 
@@ -181,11 +268,12 @@ class Dispatcher:
181
268
  The operation to be scheduled.
182
269
  machine_id:
183
270
  The id of the machine on which the operation is to be
184
- scheduled.
271
+ scheduled. If None, the start time is computed based on the
272
+ next available time for the operation on any machine.
185
273
  """
186
274
  return max(
187
- self.machine_next_available_time[machine_id],
188
- self.job_next_available_time[operation.job_id],
275
+ self._machine_next_available_time[machine_id],
276
+ self._job_next_available_time[operation.job_id],
189
277
  )
190
278
 
191
279
  def _update_tracking_attributes(
@@ -196,10 +284,12 @@ class Dispatcher:
196
284
  machine_id = scheduled_operation.machine_id
197
285
  end_time = scheduled_operation.end_time
198
286
 
199
- self.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
287
+ self._machine_next_available_time[machine_id] = end_time
288
+ self._job_next_operation_index[job_id] += 1
289
+ self._job_next_available_time[job_id] = end_time
290
+ self._cache = {}
202
291
 
292
+ @_dispatcher_cache
203
293
  def current_time(self) -> int:
204
294
  """Returns the current time of the schedule.
205
295
 
@@ -207,7 +297,8 @@ class Dispatcher:
207
297
  operations.
208
298
  """
209
299
  available_operations = self.available_operations()
210
- return self.min_start_time(available_operations)
300
+ current_time = self.min_start_time(available_operations)
301
+ return current_time
211
302
 
212
303
  def min_start_time(self, operations: list[Operation]) -> int:
213
304
  """Returns the minimum start time of the available operations."""
@@ -220,6 +311,7 @@ class Dispatcher:
220
311
  min_start_time = min(min_start_time, start_time)
221
312
  return int(min_start_time)
222
313
 
314
+ @_dispatcher_cache
223
315
  def uncompleted_operations(self) -> list[Operation]:
224
316
  """Returns the list of operations that have not been scheduled.
225
317
 
@@ -228,30 +320,24 @@ class Dispatcher:
228
320
  It is more efficient than checking all operations in the instance.
229
321
  """
230
322
  uncompleted_operations = []
231
- for job_id, next_position in enumerate(self.job_next_operation_index):
323
+ for job_id, next_position in enumerate(self._job_next_operation_index):
232
324
  operations = self.instance.jobs[job_id][next_position:]
233
325
  uncompleted_operations.extend(operations)
234
326
  return uncompleted_operations
235
327
 
328
+ @_dispatcher_cache
236
329
  def available_operations(self) -> list[Operation]:
237
330
  """Returns a list of available operations for processing, optionally
238
- filtering out operations known to be bad choices.
331
+ filtering out operations using the pruning function.
239
332
 
240
333
  This method first gathers all possible next operations from the jobs
241
- being processed. It then optionally filters these operations 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.
334
+ being processed. It then optionally filters these operations using the
335
+ pruning function.
247
336
 
248
337
  Returns:
249
338
  A list of Operation objects that are available for scheduling.
250
-
251
- Raises:
252
- ValueError: If using the filter_bad_choices option and one of the
253
- available operations can be scheduled in more than one machine.
254
339
  """
340
+
255
341
  available_operations = self._available_operations()
256
342
  if self.pruning_function is not None:
257
343
  available_operations = self.pruning_function(
@@ -261,7 +347,7 @@ class Dispatcher:
261
347
 
262
348
  def _available_operations(self) -> list[Operation]:
263
349
  available_operations = []
264
- for job_id, next_position in enumerate(self.job_next_operation_index):
350
+ for job_id, next_position in enumerate(self._job_next_operation_index):
265
351
  if next_position == len(self.instance.jobs[job_id]):
266
352
  continue
267
353
  operation = self.instance.jobs[job_id][next_position]
@@ -76,12 +76,15 @@ class DispatchingRuleSolver(BaseSolver):
76
76
  self.machine_chooser = machine_chooser
77
77
  self.pruning_function = pruning_function
78
78
 
79
- def solve(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
@@ -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.3.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=O9iqytBveJT3WMTELkpNy8IeNiI2Yy8uxbP2ruLLT1A,12582
10
+ job_shop_lib/dispatching/dispatching_rule_solver.py,sha256=fbNfSclH6Jw1F-QGY1oxAj9wm2hHhJHGnsF2HateXX8,4669
11
11
  job_shop_lib/dispatching/dispatching_rules.py,sha256=SIDkPx_1uTkM0loEqGMqotLBBSaGi1gH0WS85GXrT_I,5557
12
12
  job_shop_lib/dispatching/factories.py,sha256=ldyIbz3QuLuDkrqbgJXV6YoM6AV6CKyHu8z4hXLG2Vo,7267
13
+ job_shop_lib/dispatching/history_tracker.py,sha256=3jSh7pKEGiOcEK6bXK8AQJK4NtASxTknRjmHRKenxt8,649
13
14
  job_shop_lib/dispatching/pruning_functions.py,sha256=d94_uBHuESp4NSf_jBk1q8eBCfTPuU9meiL3StiqJiA,4378
14
15
  job_shop_lib/exceptions.py,sha256=0Wla1lK6E2u1o3t2hJj9hUwyoJ-1ebkXd42GdXFAhV0,899
15
16
  job_shop_lib/generators/__init__.py,sha256=CrMExfhRbw_0TnYgJ1HwFmq13LEFYFU9wSFANmlSTSQ,154
@@ -19,18 +20,18 @@ job_shop_lib/graphs/__init__.py,sha256=mWyF0MypyYfvFhy2F93BJkFIVsxS_0ZqvPuc29B7T
19
20
  job_shop_lib/graphs/build_agent_task_graph.py,sha256=ktj-oNLUPmWHfL81EVyaoF4hXClWYfnN7oG2Nn4pOsg,7128
20
21
  job_shop_lib/graphs/build_disjunctive_graph.py,sha256=IRMBtHw8aru5rYGz796-dc6QyaLJFh4LlPlN_BPSq5c,2877
21
22
  job_shop_lib/graphs/constants.py,sha256=dqPF--okue5sF70Iv-YR14QKFx4pxPwT2dL1Rh5jylM,374
22
- job_shop_lib/graphs/job_shop_graph.py,sha256=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/operation.py,sha256=bZclBeyge71Avm9ArwvGuBKZp5Idw5EUm6m35jav0C4,3924
26
27
  job_shop_lib/schedule.py,sha256=1xzue2ro927VZw9IWg_tlBLZ7kDbN091aOW6ZMEjOYQ,6509
27
28
  job_shop_lib/scheduled_operation.py,sha256=EfG5AcrspjO3erhM2ejlSOtYKNnaNTsLEu2gu2N3FxA,3127
28
29
  job_shop_lib/visualization/__init__.py,sha256=Kxjk3ERYXPAHR72nkD92gFdJltSLA2kxLZrlZzZJS8o,693
29
30
  job_shop_lib/visualization/agent_task_graph.py,sha256=G-c9eiawz6m9sdnDM1r-ZHz6K-gYDIAreHpb6pkYE7w,8284
30
- job_shop_lib/visualization/create_gif.py,sha256=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.3.0.dist-info/LICENSE,sha256=9mggivMGd5taAu3xbmBway-VQZMBzurBGHofFopvUsQ,1069
35
+ job_shop_lib-0.3.0.dist-info/METADATA,sha256=DPjUe3anmq-s0y-_09o8uBjvFTmHqVt5N_AI3BPu1AM,12624
36
+ job_shop_lib-0.3.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
37
+ job_shop_lib-0.3.0.dist-info/RECORD,,