job-shop-lib 1.0.0a1__py3-none-any.whl → 1.0.0a3__py3-none-any.whl

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/_job_shop_instance.py +18 -20
  2. job_shop_lib/_operation.py +30 -24
  3. job_shop_lib/_schedule.py +17 -16
  4. job_shop_lib/_scheduled_operation.py +10 -12
  5. job_shop_lib/constraint_programming/_ortools_solver.py +31 -16
  6. job_shop_lib/dispatching/__init__.py +4 -0
  7. job_shop_lib/dispatching/_dispatcher.py +24 -32
  8. job_shop_lib/dispatching/_factories.py +8 -0
  9. job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
  10. job_shop_lib/dispatching/feature_observers/__init__.py +34 -2
  11. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +54 -14
  12. job_shop_lib/dispatching/feature_observers/_duration_observer.py +15 -2
  13. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +62 -10
  14. job_shop_lib/dispatching/feature_observers/_factory.py +5 -1
  15. job_shop_lib/dispatching/feature_observers/_feature_observer.py +87 -16
  16. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +32 -2
  17. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +3 -3
  18. job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py +9 -5
  19. job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py +7 -2
  20. job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py +1 -1
  21. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +66 -43
  22. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
  23. job_shop_lib/graphs/__init__.py +2 -0
  24. job_shop_lib/graphs/_build_agent_task_graph.py +2 -2
  25. job_shop_lib/graphs/_constants.py +18 -1
  26. job_shop_lib/graphs/_job_shop_graph.py +36 -20
  27. job_shop_lib/graphs/_node.py +60 -52
  28. job_shop_lib/graphs/graph_updaters/__init__.py +11 -1
  29. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +1 -1
  30. job_shop_lib/visualization/__init__.py +5 -5
  31. job_shop_lib/visualization/_gantt_chart_creator.py +5 -5
  32. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +63 -36
  33. job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
  34. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/METADATA +15 -3
  35. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/RECORD +37 -37
  36. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/WHEEL +1 -1
  37. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/LICENSE +0 -0
@@ -21,7 +21,7 @@ from job_shop_lib.dispatching.feature_observers import (
21
21
  def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
22
22
  """Dispatches the operation with the shortest duration."""
23
23
  return min(
24
- dispatcher.ready_operations(),
24
+ dispatcher.available_operations(),
25
25
  key=lambda operation: operation.duration,
26
26
  )
27
27
 
@@ -29,7 +29,7 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
29
29
  def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
30
30
  """Dispatches the operation with the lowest position in job."""
31
31
  return min(
32
- dispatcher.ready_operations(),
32
+ dispatcher.available_operations(),
33
33
  key=lambda operation: operation.position_in_job,
34
34
  )
35
35
 
@@ -41,7 +41,7 @@ def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
41
41
  job_remaining_work[operation.job_id] += operation.duration
42
42
 
43
43
  return max(
44
- dispatcher.ready_operations(),
44
+ dispatcher.available_operations(),
45
45
  key=lambda operation: job_remaining_work[operation.job_id],
46
46
  )
47
47
 
@@ -53,14 +53,14 @@ def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
53
53
  job_remaining_operations[operation.job_id] += 1
54
54
 
55
55
  return max(
56
- dispatcher.ready_operations(),
56
+ dispatcher.available_operations(),
57
57
  key=lambda operation: job_remaining_operations[operation.job_id],
58
58
  )
59
59
 
60
60
 
61
61
  def random_operation_rule(dispatcher: Dispatcher) -> Operation:
62
62
  """Dispatches a random operation."""
63
- return random.choice(dispatcher.ready_operations())
63
+ return random.choice(dispatcher.available_operations())
64
64
 
65
65
 
66
66
  def score_based_rule(
@@ -80,7 +80,7 @@ def score_based_rule(
80
80
  def rule(dispatcher: Dispatcher) -> Operation:
81
81
  scores = score_function(dispatcher)
82
82
  return max(
83
- dispatcher.ready_operations(),
83
+ dispatcher.available_operations(),
84
84
  key=lambda operation: scores[operation.job_id],
85
85
  )
86
86
 
@@ -102,7 +102,7 @@ def score_based_rule_with_tie_breaker(
102
102
  """
103
103
 
104
104
  def rule(dispatcher: Dispatcher) -> Operation:
105
- candidates = dispatcher.ready_operations()
105
+ candidates = dispatcher.available_operations()
106
106
  for scoring_function in score_functions:
107
107
  scores = scoring_function(dispatcher)
108
108
  best_score = max(scores)
@@ -126,7 +126,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
126
126
  """Scores each job based on the duration of the next operation."""
127
127
  num_jobs = dispatcher.instance.num_jobs
128
128
  scores = [0] * num_jobs
129
- for operation in dispatcher.ready_operations():
129
+ for operation in dispatcher.available_operations():
130
130
  scores[operation.job_id] = -operation.duration
131
131
  return scores
132
132
 
@@ -135,7 +135,7 @@ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
135
135
  """Scores each job based on the position of the next operation."""
136
136
  num_jobs = dispatcher.instance.num_jobs
137
137
  scores = [0] * num_jobs
138
- for operation in dispatcher.ready_operations():
138
+ for operation in dispatcher.available_operations():
139
139
  scores[operation.job_id] = operation.operation_id
140
140
  return scores
141
141
 
@@ -1,5 +1,7 @@
1
1
  """Package for graph related classes and functions.
2
2
 
3
+ The main classes and functions available in this package are:
4
+
3
5
  .. autosummary::
4
6
  JobShopGraph
5
7
  Node
@@ -13,8 +13,8 @@ job, and the global node is connected to all machine and job nodes.
13
13
 
14
14
  References:
15
15
  - Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
16
- solve multi-agent scheduling problems with reinforcement learning. ArXiv,
17
- abs/2106.03051, 2021.
16
+ solve multi-agent scheduling problems with reinforcement learning. ArXiv,
17
+ abs/2106.03051, 2021.
18
18
  """
19
19
 
20
20
  import itertools
@@ -11,7 +11,24 @@ class EdgeType(enum.Enum):
11
11
 
12
12
 
13
13
  class NodeType(enum.Enum):
14
- """Enumeration of node types."""
14
+ """Enumeration of node types.
15
+
16
+ Nodes of type :class:`NodeType.OPERATION`, :class:`NodeType.MACHINE`, and
17
+ :class:`NodeType.JOB` have specific attributes associated with them in the
18
+ :class:`Node` class.
19
+
20
+ On the other hand, Nodes of type :class:`NodeType.GLOBAL`,
21
+ :class:`NodeType.SOURCE`, and :class:`NodeType.SINK` are used to represent
22
+ different concepts in the graph and accesing them, but they do not have
23
+ specific attributes associated with them.
24
+
25
+ .. tip::
26
+
27
+ While uncommon, it can be useful to extend this enumeration with
28
+ additional node types. This can be achieved using
29
+ `aenum <https://github.com/ethanfurman/aenum>`_'s
30
+ ``extend_enum`` (requires using Python 3.11+).
31
+ """
15
32
 
16
33
  OPERATION = enum.auto()
17
34
  MACHINE = enum.auto()
@@ -13,10 +13,10 @@ NODE_ATTR = "node"
13
13
 
14
14
  # pylint: disable=too-many-instance-attributes
15
15
  class JobShopGraph:
16
- """Data structure to represent a `JobShopInstance` as a graph.
16
+ """Represents a :class:`JobShopInstance` as a heterogeneous directed graph.
17
17
 
18
18
  Provides a comprehensive graph-based representation of a job shop
19
- scheduling problem, utilizing the `networkx` library to model the complex
19
+ scheduling problem, utilizing the ``networkx`` library to model the complex
20
20
  relationships between jobs, operations, and machines. This class transforms
21
21
  the abstract scheduling problem into a directed graph, where various
22
22
  entities (jobs, machines, and operations) are nodes, and the dependencies
@@ -25,25 +25,40 @@ class JobShopGraph:
25
25
  This transformation allows for the application of graph algorithms
26
26
  to analyze and solve scheduling problems.
27
27
 
28
- Attributes:
28
+ Args:
29
29
  instance:
30
- The job shop instance encapsulated by this graph.
31
- graph:
32
- The directed graph representing the job shop, where nodes are
33
- operations, machines, jobs, or abstract concepts like global,
34
- source, and sink, with edges indicating dependencies.
30
+ The job shop instance that the graph represents.
31
+ add_operation_nodes:
32
+ Whether to add nodes of type :class:`NodeType.OPERATION` to the
33
+ to the graph. If set to ``False``, the graph will be empty, and
34
+ operation nodes will need to be added manually.
35
35
  """
36
36
 
37
- def __init__(self, instance: JobShopInstance):
38
- """Initializes the graph with the given instance.
39
-
40
- Nodes of type `OPERATION` are added to the graph based on the
41
- operations of the instance.
42
-
43
- Args:
44
- instance:
45
- The job shop instance that the graph represents.
46
- """
37
+ __slots__ = {
38
+ "instance": "The job shop instance that the graph represents.",
39
+ "graph": (
40
+ "The directed graph representing the job shop, where nodes are "
41
+ "operations, machines, jobs, or abstract concepts like global, "
42
+ "source, and sink, with edges indicating dependencies."
43
+ ),
44
+ "_nodes": "List of all nodes added to the graph.",
45
+ "_nodes_by_type": "Dictionary mapping node types to lists of nodes.",
46
+ "_nodes_by_machine": (
47
+ "List of lists mapping machine ids to operation nodes."
48
+ ),
49
+ "_nodes_by_job": "List of lists mapping job ids to operation nodes.",
50
+ "_next_node_id": (
51
+ "The id to assign to the next node added to thegraph."
52
+ ),
53
+ "removed_nodes": (
54
+ "List of boolean values indicating whether a node has been "
55
+ "removed from the graph."
56
+ ),
57
+ }
58
+
59
+ def __init__(
60
+ self, instance: JobShopInstance, add_operation_nodes: bool = True
61
+ ):
47
62
  self.graph = nx.DiGraph()
48
63
  self.instance = instance
49
64
 
@@ -59,7 +74,8 @@ class JobShopGraph:
59
74
  ]
60
75
  self._next_node_id = 0
61
76
  self.removed_nodes: list[bool] = []
62
- self._add_operation_nodes()
77
+ if add_operation_nodes:
78
+ self.add_operation_nodes()
63
79
 
64
80
  @property
65
81
  def nodes(self) -> list[Node]:
@@ -103,7 +119,7 @@ class JobShopGraph:
103
119
  """Number of job nodes in the graph."""
104
120
  return len(self._nodes_by_type[NodeType.JOB])
105
121
 
106
- def _add_operation_nodes(self) -> None:
122
+ def add_operation_nodes(self) -> None:
107
123
  """Adds operation nodes to the graph."""
108
124
  for job in self.instance.jobs:
109
125
  for operation in job:
@@ -9,7 +9,7 @@ from job_shop_lib.graphs._constants import NodeType
9
9
 
10
10
 
11
11
  class Node:
12
- """Data structure to represent a node in the `JobShopGraph`.
12
+ """Represents a node in the :class:`JobShopGraph`.
13
13
 
14
14
  A node is hashable by its id. The id is assigned when the node is added to
15
15
  the graph. The id must be unique for each node in the graph, and should be
@@ -18,11 +18,15 @@ class Node:
18
18
  Depending on the type of the node, it can have different attributes. The
19
19
  following table shows the attributes of each type of node:
20
20
 
21
- Node Type | Required Attribute
22
- ----------------|---------------------
23
- OPERATION | `operation`
24
- MACHINE | `machine_id`
25
- JOB | `job_id`
21
+ +----------------+---------------------+
22
+ | Node Type | Required Attribute |
23
+ +================+=====================+
24
+ | OPERATION | ``operation`` |
25
+ +----------------+---------------------+
26
+ | MACHINE | ``machine_id`` |
27
+ +----------------+---------------------+
28
+ | JOB | ``job_id`` |
29
+ +----------------+---------------------+
26
30
 
27
31
  In terms of equality, two nodes are equal if they have the same id.
28
32
  Additionally, one node is equal to an integer if the integer is equal to
@@ -31,25 +35,47 @@ class Node:
31
35
  This allows for using the node as a key in a dictionary, at the same time
32
36
  we can use its id to index that dictionary. Example:
33
37
 
34
- ```python
35
- node = Node(NodeType.SOURCE)
36
- node.node_id = 1
37
- graph = {node: "some value"}
38
- print(graph[node]) # "some value"
39
- print(graph[1]) # "some value"
40
- ```
38
+ .. code-block:: python
41
39
 
42
- Attributes:
40
+ node = Node(NodeType.SOURCE)
41
+ node.node_id = 1
42
+ graph = {node: "some value"}
43
+ print(graph[node]) # "some value"
44
+ print(graph[1]) # "some value"
45
+
46
+ Args:
43
47
  node_type:
44
- The type of the node. It can be one of the following:
45
- - NodeType.OPERATION
46
- - NodeType.MACHINE
47
- - NodeType.JOB
48
- - NodeType.GLOBAL
49
- ...
48
+ The type of the node. See :class:`NodeType` for theavailable types.
49
+ operation:
50
+ The operation of the node. Required if ``node_type`` is
51
+ :attr:`NodeType.OPERATION`.
52
+ machine_id:
53
+ The id of the machine. Required if ``node_type`` is
54
+ :attr:`NodeType.MACHINE`.
55
+ job_id:
56
+ The id of the job. Required if ``node_type`` is
57
+ :attr:`NodeType.JOB`.
58
+
59
+ Raises:
60
+ ValidationError:
61
+ If the ``node_type`` is :attr:`NodeType.OPERATION`,
62
+ :attr:`NodeType.MACHINE`, or :attr:`NodeType.JOB` and the
63
+ corresponding ``operation``, ``machine_id``, or ``job_id`` is
64
+ ``None``, respectively.
65
+
50
66
  """
51
67
 
52
- __slots__ = "node_type", "_node_id", "_operation", "_machine_id", "_job_id"
68
+ __slots__ = {
69
+ "node_type": "The type of the node.",
70
+ "_node_id": "Unique identifier for the node.",
71
+ "_operation": (
72
+ "The operation associated with the node."
73
+ ),
74
+ "_machine_id": (
75
+ "The machine ID associated with the node."
76
+ ),
77
+ "_job_id": "The job ID associated with the node.",
78
+ }
53
79
 
54
80
  def __init__(
55
81
  self,
@@ -58,34 +84,6 @@ class Node:
58
84
  machine_id: int | None = None,
59
85
  job_id: int | None = None,
60
86
  ):
61
- """Initializes the node with the given attributes.
62
-
63
- Args:
64
- node_type:
65
- The type of the node. It can be one of the following:
66
- - NodeType.OPERATION
67
- - NodeType.MACHINE
68
- - NodeType.JOB
69
- - NodeType.GLOBAL
70
- ...
71
- operation:
72
- The operation of the node. It should be provided if the
73
- `node_type` is NodeType.OPERATION.
74
- machine_id:
75
- The id of the machine of the node. It should be provided if the
76
- node_type is NodeType.MACHINE.
77
- job_id:
78
- The id of the job of the node. It should be provided if the
79
- node_type is NodeType.JOB.
80
-
81
- Raises:
82
- ValidationError:
83
- If the node_type is OPERATION and operation is None.
84
- ValidationError:
85
- If the node_type is MACHINE and machine_id is None.
86
- ValidationError:
87
- If the node_type is JOB and job_id is None.
88
- """
89
87
  if node_type == NodeType.OPERATION and operation is None:
90
88
  raise ValidationError("Operation node must have an operation.")
91
89
 
@@ -95,7 +93,7 @@ class Node:
95
93
  if node_type == NodeType.JOB and job_id is None:
96
94
  raise ValidationError("Job node must have a job_id.")
97
95
 
98
- self.node_type = node_type
96
+ self.node_type: NodeType = node_type
99
97
  self._node_id: int | None = None
100
98
 
101
99
  self._operation = operation
@@ -119,7 +117,11 @@ class Node:
119
117
  def operation(self) -> Operation:
120
118
  """Returns the operation of the node.
121
119
 
122
- This property is mandatory for nodes of type `OPERATION`.
120
+ This property is mandatory for nodes of type
121
+ :class:`NodeType.OPERATION`.
122
+
123
+ Raises:
124
+ UninitializedAttributeError: If the node has no operation.
123
125
  """
124
126
  if self._operation is None:
125
127
  raise UninitializedAttributeError("Node has no operation.")
@@ -130,9 +132,12 @@ class Node:
130
132
  """Returns the `machine_id` of the node.
131
133
 
132
134
  This property is mandatory for nodes of type `MACHINE`.
135
+
136
+ Raises:
137
+ UninitializedAttributeError: If the node has no ``machine_id``.
133
138
  """
134
139
  if self._machine_id is None:
135
- raise UninitializedAttributeError("Node has no `machine_id`.")
140
+ raise UninitializedAttributeError("Node has no ``machine_id``.")
136
141
  return self._machine_id
137
142
 
138
143
  @property
@@ -140,6 +145,9 @@ class Node:
140
145
  """Returns the `job_id` of the node.
141
146
 
142
147
  This property is mandatory for nodes of type `JOB`.
148
+
149
+ Raises:
150
+ UninitializedAttributeError: If the node has no `job_id`.
143
151
  """
144
152
  if self._job_id is None:
145
153
  raise UninitializedAttributeError("Node has no `job_id`.")
@@ -1,5 +1,15 @@
1
1
  """Contains classes and functions for updating the graph representation of the
2
- job shop scheduling problem."""
2
+ job shop scheduling problem.
3
+
4
+ Currently, the following classes and utilities are available:
5
+
6
+ .. autosummary::
7
+
8
+ GraphUpdater
9
+ ResidualGraphUpdater
10
+ remove_completed_operations
11
+
12
+ """
3
13
 
4
14
  from ._graph_updater import GraphUpdater
5
15
  from ._utils import remove_completed_operations
@@ -260,7 +260,7 @@ class SingleJobShopGraphEnv(gym.Env):
260
260
  truncated = False
261
261
  info: dict[str, Any] = {
262
262
  "feature_names": self.composite_observer.column_names,
263
- "available_operations": self.dispatcher.ready_operations(),
263
+ "available_operations": self.dispatcher.available_operations(),
264
264
  }
265
265
  return obs, reward, done, truncated, info
266
266
 
@@ -1,11 +1,11 @@
1
1
  """Package for visualization."""
2
2
 
3
- from job_shop_lib.visualization._gantt_chart import plot_gantt_chart
3
+ from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
4
4
  from job_shop_lib.visualization._gantt_chart_video_and_gif_creation import (
5
- create_gif,
5
+ create_gantt_chart_gif,
6
6
  create_gantt_chart_video,
7
7
  create_gantt_chart_frames,
8
- plot_gantt_chart_wrapper,
8
+ get_partial_gantt_chart_plotter,
9
9
  create_video_from_frames,
10
10
  create_gif_from_frames,
11
11
  )
@@ -26,9 +26,9 @@ from job_shop_lib.visualization._gantt_chart_creator import (
26
26
  __all__ = [
27
27
  "plot_gantt_chart",
28
28
  "create_gantt_chart_video",
29
- "create_gif",
29
+ "create_gantt_chart_gif",
30
30
  "create_gantt_chart_frames",
31
- "plot_gantt_chart_wrapper",
31
+ "get_partial_gantt_chart_plotter",
32
32
  "create_gif_from_frames",
33
33
  "create_video_from_frames",
34
34
  "plot_disjunctive_graph",
@@ -10,8 +10,8 @@ from job_shop_lib.dispatching import (
10
10
  )
11
11
  from job_shop_lib.visualization import (
12
12
  create_gantt_chart_video,
13
- plot_gantt_chart_wrapper,
14
- create_gif,
13
+ get_partial_gantt_chart_plotter,
14
+ create_gantt_chart_gif,
15
15
  )
16
16
 
17
17
 
@@ -146,7 +146,7 @@ class GanttChartCreator:
146
146
  self.history_observer: HistoryObserver = (
147
147
  dispatcher.create_or_get_observer(HistoryObserver)
148
148
  )
149
- self.plot_function = plot_gantt_chart_wrapper(
149
+ self.plot_function = get_partial_gantt_chart_plotter(
150
150
  **self.gannt_chart_wrapper_config
151
151
  )
152
152
 
@@ -175,7 +175,7 @@ class GanttChartCreator:
175
175
  return self.plot_function(
176
176
  self.schedule,
177
177
  None,
178
- self.dispatcher.ready_operations(),
178
+ self.dispatcher.available_operations(),
179
179
  self.dispatcher.current_time(),
180
180
  )
181
181
 
@@ -196,7 +196,7 @@ class GanttChartCreator:
196
196
  The configuration for the GIF creation can be customized through the
197
197
  `gif_config` attribute.
198
198
  """
199
- create_gif(
199
+ create_gantt_chart_gif(
200
200
  instance=self.history_observer.dispatcher.instance,
201
201
  schedule_history=self.history_observer.history,
202
202
  plot_function=self.plot_function,