job-shop-lib 0.2.0__tar.gz → 0.3.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/PKG-INFO +1 -1
  2. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/__init__.py +2 -1
  3. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/dispatching/__init__.py +4 -1
  4. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/dispatching/dispatcher.py +117 -31
  5. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/dispatching/dispatching_rule_solver.py +14 -6
  6. job_shop_lib-0.3.0/job_shop_lib/dispatching/history_tracker.py +20 -0
  7. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/graphs/job_shop_graph.py +85 -42
  8. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/graphs/node.py +37 -18
  9. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/operation.py +6 -4
  10. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/visualization/create_gif.py +21 -12
  11. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/visualization/disjunctive_graph.py +20 -16
  12. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/pyproject.toml +1 -1
  13. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/LICENSE +0 -0
  14. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/README.md +0 -0
  15. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/base_solver.py +0 -0
  16. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/benchmarking/__init__.py +0 -0
  17. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/benchmarking/benchmark_instances.json +0 -0
  18. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/benchmarking/load_benchmark.py +0 -0
  19. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/cp_sat/__init__.py +0 -0
  20. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/cp_sat/ortools_solver.py +0 -0
  21. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/dispatching/dispatching_rules.py +0 -0
  22. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/dispatching/factories.py +0 -0
  23. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/dispatching/pruning_functions.py +0 -0
  24. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/exceptions.py +0 -0
  25. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/generators/__init__.py +0 -0
  26. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/generators/basic_generator.py +0 -0
  27. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/generators/transformations.py +0 -0
  28. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/graphs/__init__.py +0 -0
  29. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/graphs/build_agent_task_graph.py +0 -0
  30. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/graphs/build_disjunctive_graph.py +0 -0
  31. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/graphs/constants.py +0 -0
  32. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/job_shop_instance.py +0 -0
  33. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/schedule.py +0 -0
  34. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/scheduled_operation.py +0 -0
  35. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/visualization/__init__.py +0 -0
  36. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/visualization/agent_task_graph.py +0 -0
  37. {job_shop_lib-0.2.0 → job_shop_lib-0.3.0}/job_shop_lib/visualization/gantt_chart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: job-shop-lib
3
- Version: 0.2.0
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,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,24 +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
 
63
+ @property
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
94
+
95
+ @property
96
+ def num_edges(self) -> int:
97
+ """Number of edges in the graph."""
98
+ return self.graph.number_of_edges()
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
+
88
105
  def _add_operation_nodes(self) -> None:
89
106
  """Adds operation nodes to the graph."""
90
107
  for job in self.instance.jobs:
@@ -92,10 +109,6 @@ class JobShopGraph:
92
109
  node = Node(node_type=NodeType.OPERATION, operation=operation)
93
110
  self.add_node(node)
94
111
 
95
- def get_operation_from_id(self, operation_id: int) -> Operation:
96
- """Returns the operation with the given id."""
97
- return self.nodes[operation_id].operation
98
-
99
112
  def add_node(self, node_for_adding: Node) -> None:
100
113
  """Adds a node to the graph and updates relevant class attributes.
101
114
 
@@ -121,15 +134,16 @@ class JobShopGraph:
121
134
  self.graph.add_node(
122
135
  node_for_adding.node_id, **{NODE_ATTR: node_for_adding}
123
136
  )
124
- self.nodes_by_type[node_for_adding.node_type].append(node_for_adding)
125
- 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)
126
139
  self._next_node_id += 1
140
+ self.removed_nodes.append(False)
127
141
 
128
142
  if node_for_adding.node_type == NodeType.OPERATION:
129
143
  operation = node_for_adding.operation
130
- self.nodes_by_job[operation.job_id].append(node_for_adding)
144
+ self._nodes_by_job[operation.job_id].append(node_for_adding)
131
145
  for machine_id in operation.machines:
132
- self.nodes_by_machine[machine_id].append(node_for_adding)
146
+ self._nodes_by_machine[machine_id].append(node_for_adding)
133
147
 
134
148
  def add_edge(
135
149
  self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
@@ -146,14 +160,43 @@ class JobShopGraph:
146
160
  **attr: Additional attributes to be added to the edge.
147
161
 
148
162
  Raises:
149
- 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.
150
165
  """
151
166
  if isinstance(u_of_edge, Node):
152
167
  u_of_edge = u_of_edge.node_id
153
168
  if isinstance(v_of_edge, Node):
154
169
  v_of_edge = v_of_edge.node_id
155
170
  if u_of_edge not in self.graph or v_of_edge not in self.graph:
156
- raise ValueError(
171
+ raise JobShopLibError(
157
172
  "`u_of_edge` and `v_of_edge` must be in the graph."
158
173
  )
159
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:
@@ -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
  [tool.poetry]
2
2
  name = "job-shop-lib"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "An easy-to-use and modular Python library for the Job Shop Scheduling Problem (JSSP)"
5
5
  authors = ["Pabloo22 <pablete.arino@gmail.com>"]
6
6
  license = "MIT"
File without changes
File without changes