job-shop-lib 1.0.0a4__py3-none-any.whl → 1.0.0b1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. job_shop_lib/__init__.py +3 -0
  2. job_shop_lib/_job_shop_instance.py +36 -31
  3. job_shop_lib/_operation.py +4 -2
  4. job_shop_lib/_schedule.py +11 -11
  5. job_shop_lib/benchmarking/_load_benchmark.py +3 -3
  6. job_shop_lib/constraint_programming/_ortools_solver.py +6 -5
  7. job_shop_lib/dispatching/_dispatcher.py +58 -20
  8. job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
  9. job_shop_lib/dispatching/_factories.py +8 -6
  10. job_shop_lib/dispatching/_history_observer.py +2 -1
  11. job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
  12. job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
  13. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
  14. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
  15. job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
  16. job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
  17. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
  18. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
  19. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
  20. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +23 -15
  21. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
  22. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
  23. job_shop_lib/dispatching/rules/_utils.py +9 -8
  24. job_shop_lib/generation/__init__.py +8 -0
  25. job_shop_lib/generation/_general_instance_generator.py +42 -64
  26. job_shop_lib/generation/_instance_generator.py +11 -7
  27. job_shop_lib/generation/_transformations.py +5 -4
  28. job_shop_lib/generation/_utils.py +124 -0
  29. job_shop_lib/graphs/__init__.py +7 -7
  30. job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
  31. job_shop_lib/graphs/_job_shop_graph.py +17 -13
  32. job_shop_lib/graphs/_node.py +6 -4
  33. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
  34. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
  35. job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
  36. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
  37. job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
  38. job_shop_lib/reinforcement_learning/_utils.py +3 -3
  39. job_shop_lib/visualization/__init__.py +0 -60
  40. job_shop_lib/visualization/gantt/__init__.py +48 -0
  41. job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
  42. job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
  43. job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
  44. job_shop_lib/visualization/graphs/__init__.py +29 -0
  45. job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
  46. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  47. {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/METADATA +26 -24
  48. job_shop_lib-1.0.0b1.dist-info/RECORD +69 -0
  49. job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
  50. job_shop_lib-1.0.0a4.dist-info/RECORD +0 -66
  51. {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/LICENSE +0 -0
  52. {job_shop_lib-1.0.0a4.dist-info → job_shop_lib-1.0.0b1.dist-info}/WHEEL +0 -0
@@ -1,5 +1,10 @@
1
1
  """Package for generating job shop instances."""
2
2
 
3
+ from job_shop_lib.generation._utils import (
4
+ generate_duration_matrix,
5
+ generate_machine_matrix_with_recirculation,
6
+ generate_machine_matrix_without_recirculation,
7
+ )
3
8
  from job_shop_lib.generation._instance_generator import InstanceGenerator
4
9
  from job_shop_lib.generation._general_instance_generator import (
5
10
  GeneralInstanceGenerator,
@@ -8,4 +13,7 @@ from job_shop_lib.generation._general_instance_generator import (
8
13
  __all__ = [
9
14
  "InstanceGenerator",
10
15
  "GeneralInstanceGenerator",
16
+ "generate_duration_matrix",
17
+ "generate_machine_matrix_with_recirculation",
18
+ "generate_machine_matrix_without_recirculation",
11
19
  ]
@@ -1,10 +1,16 @@
1
1
  """Home of the `BasicGenerator` class."""
2
2
 
3
+ from typing import Optional, Tuple, Union
3
4
  import random
4
5
 
5
- from job_shop_lib import JobShopInstance, Operation
6
+ from job_shop_lib import JobShopInstance
6
7
  from job_shop_lib.exceptions import ValidationError
7
- from job_shop_lib.generation import InstanceGenerator
8
+ from job_shop_lib.generation import (
9
+ InstanceGenerator,
10
+ generate_duration_matrix,
11
+ generate_machine_matrix_with_recirculation,
12
+ generate_machine_matrix_without_recirculation,
13
+ )
8
14
 
9
15
 
10
16
  class GeneralInstanceGenerator(InstanceGenerator):
@@ -20,6 +26,8 @@ class GeneralInstanceGenerator(InstanceGenerator):
20
26
  multiple instances, controlled by the ``iteration_limit`` parameter. It
21
27
  implements the iterator protocol, allowing it to be used in a ``for`` loop.
22
28
 
29
+ The number of operations per machine is equal to the number of machines
30
+
23
31
  Note:
24
32
  When used as an iterator, the generator will produce instances until it
25
33
  reaches the specified ``iteration_limit``. If ``iteration_limit`` is
@@ -73,15 +81,15 @@ class GeneralInstanceGenerator(InstanceGenerator):
73
81
 
74
82
  def __init__( # pylint: disable=too-many-arguments
75
83
  self,
76
- num_jobs: int | tuple[int, int] = (10, 20),
77
- num_machines: int | tuple[int, int] = (5, 10),
78
- duration_range: tuple[int, int] = (1, 99),
84
+ num_jobs: Union[int, Tuple[int, int]] = (10, 20),
85
+ num_machines: Union[int, Tuple[int, int]] = (5, 10),
86
+ duration_range: Tuple[int, int] = (1, 99),
79
87
  allow_less_jobs_than_machines: bool = True,
80
88
  allow_recirculation: bool = False,
81
- machines_per_operation: int | tuple[int, int] = 1,
89
+ machines_per_operation: Union[int, Tuple[int, int]] = 1,
82
90
  name_suffix: str = "classic_generated_instance",
83
- seed: int | None = None,
84
- iteration_limit: int | None = None,
91
+ seed: Optional[int] = None,
92
+ iteration_limit: Optional[int] = None,
85
93
  ):
86
94
  super().__init__(
87
95
  num_jobs=num_jobs,
@@ -96,6 +104,10 @@ class GeneralInstanceGenerator(InstanceGenerator):
96
104
  machines_per_operation,
97
105
  machines_per_operation,
98
106
  )
107
+ if machines_per_operation != (1, 1):
108
+ raise NotImplementedError(
109
+ "The number of machines per operation must be 1 for now."
110
+ )
99
111
  self.machines_per_operation = machines_per_operation
100
112
 
101
113
  self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
@@ -114,7 +126,9 @@ class GeneralInstanceGenerator(InstanceGenerator):
114
126
  )
115
127
 
116
128
  def generate(
117
- self, num_jobs: int | None = None, num_machines: int | None = None
129
+ self,
130
+ num_jobs: Optional[int] = None,
131
+ num_machines: Optional[int] = None,
118
132
  ) -> JobShopInstance:
119
133
  if num_jobs is None:
120
134
  num_jobs = random.randint(*self.num_jobs_range)
@@ -128,60 +142,24 @@ class GeneralInstanceGenerator(InstanceGenerator):
128
142
  not self.allow_less_jobs_than_machines and num_jobs < num_machines
129
143
  ):
130
144
  raise ValidationError(
131
- "Theere are fewer jobs than machines, which is not allowed"
132
- "when `allow_less_jobs_than_machines` attribute is False."
145
+ "There are fewer jobs than machines, which is not allowed "
146
+ " when `allow_less_jobs_than_machines` attribute is False."
147
+ )
148
+
149
+ duration_matrix = generate_duration_matrix(
150
+ num_jobs, num_machines, self.duration_range
151
+ )
152
+ if self.allow_recirculation:
153
+ machine_matrix = generate_machine_matrix_with_recirculation(
154
+ num_jobs, num_machines
155
+ )
156
+ else:
157
+ machine_matrix = generate_machine_matrix_without_recirculation(
158
+ num_jobs, num_machines
133
159
  )
134
160
 
135
- jobs = []
136
- available_machines = list(range(num_machines))
137
- for _ in range(num_jobs):
138
- job = []
139
- for _ in range(num_machines):
140
- operation = self.create_random_operation(available_machines)
141
- job.append(operation)
142
- jobs.append(job)
143
- available_machines = list(range(num_machines))
144
-
145
- return JobShopInstance(jobs=jobs, name=self._next_name())
146
-
147
- def create_random_operation(
148
- self, available_machines: list[int] | None = None
149
- ) -> Operation:
150
- """Creates a random operation with the given available machines.
151
-
152
- Args:
153
- available_machines:
154
- A list of available machine_ids to choose from.
155
- If ``None``, all machines are available.
156
- """
157
- duration = random.randint(*self.duration_range)
158
-
159
- if self.machines_per_operation[1] > 1:
160
- machines = self._choose_multiple_machines()
161
- return Operation(machines=machines, duration=duration)
162
-
163
- machine_id = self._choose_one_machine(available_machines)
164
- return Operation(machines=machine_id, duration=duration)
165
-
166
- def _choose_multiple_machines(self) -> list[int]:
167
- num_machines = random.randint(*self.machines_per_operation)
168
- available_machines = list(range(num_machines))
169
- machines = []
170
- for _ in range(num_machines):
171
- machine = random.choice(available_machines)
172
- machines.append(machine)
173
- available_machines.remove(machine)
174
- return machines
175
-
176
- def _choose_one_machine(
177
- self, available_machines: list[int] | None = None
178
- ) -> int:
179
- if available_machines is None:
180
- _, max_num_machines = self.num_machines_range
181
- available_machines = list(range(max_num_machines))
182
-
183
- machine_id = random.choice(available_machines)
184
- if not self.allow_recirculation:
185
- available_machines.remove(machine_id)
186
-
187
- return machine_id
161
+ return JobShopInstance.from_matrices(
162
+ duration_matrix.tolist(),
163
+ machine_matrix.tolist(),
164
+ name=self._next_name(),
165
+ )
@@ -1,7 +1,9 @@
1
+ """Home of the `InstanceGenerator` class."""
2
+
1
3
  import abc
2
4
 
3
5
  import random
4
- from typing import Iterator
6
+ from typing import Iterator, Optional, Tuple, Union
5
7
 
6
8
  from job_shop_lib import JobShopInstance
7
9
  from job_shop_lib.exceptions import UninitializedAttributeError
@@ -50,12 +52,12 @@ class InstanceGenerator(abc.ABC):
50
52
 
51
53
  def __init__( # pylint: disable=too-many-arguments
52
54
  self,
53
- num_jobs: int | tuple[int, int] = (10, 20),
54
- num_machines: int | tuple[int, int] = (5, 10),
55
- duration_range: tuple[int, int] = (1, 99),
55
+ num_jobs: Union[int, Tuple[int, int]] = (10, 20),
56
+ num_machines: Union[int, Tuple[int, int]] = (5, 10),
57
+ duration_range: Tuple[int, int] = (1, 99),
56
58
  name_suffix: str = "generated_instance",
57
- seed: int | None = None,
58
- iteration_limit: int | None = None,
59
+ seed: Optional[int] = None,
60
+ iteration_limit: Optional[int] = None,
59
61
  ):
60
62
  if isinstance(num_jobs, int):
61
63
  num_jobs = (num_jobs, num_jobs)
@@ -75,7 +77,9 @@ class InstanceGenerator(abc.ABC):
75
77
 
76
78
  @abc.abstractmethod
77
79
  def generate(
78
- self, num_jobs: int | None = None, num_machines: int | None = None
80
+ self,
81
+ num_jobs: Optional[int] = None,
82
+ num_machines: Optional[int] = None,
79
83
  ) -> JobShopInstance:
80
84
  """Generates a single job shop instance
81
85
 
@@ -3,6 +3,7 @@
3
3
  import abc
4
4
  import copy
5
5
  import random
6
+ from typing import Optional
6
7
 
7
8
  from job_shop_lib import JobShopInstance, Operation
8
9
 
@@ -38,7 +39,7 @@ class RemoveMachines(Transformation):
38
39
  """Removes operations associated with randomly selected machines until
39
40
  there are exactly num_machines machines left."""
40
41
 
41
- def __init__(self, num_machines: int, suffix: str | None = None):
42
+ def __init__(self, num_machines: int, suffix: Optional[str] = None):
42
43
  if suffix is None:
43
44
  suffix = f"_machines={num_machines}"
44
45
  super().__init__(suffix=suffix)
@@ -83,7 +84,7 @@ class AddDurationNoise(Transformation):
83
84
  min_duration: int = 1,
84
85
  max_duration: int = 100,
85
86
  noise_level: int = 10,
86
- suffix: str | None = None,
87
+ suffix: Optional[str] = None,
87
88
  ):
88
89
  if suffix is None:
89
90
  suffix = f"_noise={noise_level}"
@@ -127,8 +128,8 @@ class RemoveJobs(Transformation):
127
128
  self,
128
129
  min_jobs: int,
129
130
  max_jobs: int,
130
- target_jobs: int | None = None,
131
- suffix: str | None = None,
131
+ target_jobs: Optional[int] = None,
132
+ suffix: Optional[str] = None,
132
133
  ):
133
134
  if suffix is None:
134
135
  suffix = f"_jobs={min_jobs}-{max_jobs}"
@@ -0,0 +1,124 @@
1
+ from typing import Tuple
2
+ import numpy as np
3
+ from numpy.typing import NDArray
4
+
5
+ from job_shop_lib.exceptions import ValidationError
6
+
7
+
8
+ def generate_duration_matrix(
9
+ num_jobs: int, num_machines: int, duration_range: Tuple[int, int]
10
+ ) -> NDArray[np.int32]:
11
+ """Generates a duration matrix.
12
+
13
+ Args:
14
+ num_jobs: The number of jobs.
15
+ num_machines: The number of machines.
16
+ duration_range: The range of the duration values.
17
+
18
+ Returns:
19
+ A duration matrix with shape (num_jobs, num_machines).
20
+ """
21
+ if duration_range[0] > duration_range[1]:
22
+ raise ValidationError(
23
+ "The lower bound of the duration range must be less than or equal "
24
+ "to the upper bound."
25
+ )
26
+ if num_jobs <= 0:
27
+ raise ValidationError("The number of jobs must be greater than 0.")
28
+ if num_machines <= 0:
29
+ raise ValidationError("The number of machines must be greater than 0.")
30
+
31
+ return np.random.randint(
32
+ duration_range[0],
33
+ duration_range[1] + 1,
34
+ size=(num_jobs, num_machines),
35
+ dtype=np.int32,
36
+ )
37
+
38
+
39
+ def generate_machine_matrix_with_recirculation(
40
+ num_jobs: int, num_machines: int
41
+ ) -> NDArray[np.int32]:
42
+ """Generate a machine matrix with recirculation.
43
+
44
+ Args:
45
+ num_jobs: The number of jobs.
46
+ num_machines: The number of machines.
47
+
48
+ Returns:
49
+ A machine matrix with recirculation with shape (num_machines,
50
+ num_jobs).
51
+ """
52
+ if num_jobs <= 0:
53
+ raise ValidationError("The number of jobs must be greater than 0.")
54
+ if num_machines <= 0:
55
+ raise ValidationError("The number of machines must be greater than 0.")
56
+ num_machines_is_correct = False
57
+ while not num_machines_is_correct:
58
+ machine_matrix: np.ndarray = np.random.randint(
59
+ 0, num_machines, size=(num_machines, num_jobs)
60
+ )
61
+ num_machines_is_correct = (
62
+ len(np.unique(machine_matrix)) == num_machines
63
+ )
64
+
65
+ return machine_matrix
66
+
67
+
68
+ def generate_machine_matrix_without_recirculation(
69
+ num_jobs: int, num_machines: int
70
+ ) -> NDArray[np.int32]:
71
+ """Generate a machine matrix without recirculation.
72
+
73
+ Args:
74
+ num_jobs: The number of jobs.
75
+ num_machines: The number of machines.
76
+
77
+ Returns:
78
+ A machine matrix without recirculation.
79
+ """
80
+ if num_jobs <= 0:
81
+ raise ValidationError("The number of jobs must be greater than 0.")
82
+ if num_machines <= 0:
83
+ raise ValidationError("The number of machines must be greater than 0.")
84
+ # Start with an arange repeated:
85
+ # m1: [0, 1, 2]
86
+ # m2: [0, 1, 2]
87
+ # m3: [0, 1, 2]
88
+ machine_matrix = np.tile(
89
+ np.arange(num_machines).reshape(1, num_machines),
90
+ (num_jobs, 1),
91
+ )
92
+ # Shuffle the columns:
93
+ machine_matrix = np.apply_along_axis(
94
+ np.random.permutation, 1, machine_matrix
95
+ )
96
+ return machine_matrix
97
+
98
+
99
+ if __name__ == "__main__":
100
+
101
+ NUM_JOBS = 3
102
+ NUM_MACHINES = 3
103
+ DURATION_RANGE = (1, 10)
104
+
105
+ duration_matrix = generate_duration_matrix(
106
+ num_jobs=NUM_JOBS,
107
+ num_machines=NUM_MACHINES,
108
+ duration_range=DURATION_RANGE,
109
+ )
110
+ print(duration_matrix)
111
+
112
+ machine_matrix_with_recirculation = (
113
+ generate_machine_matrix_with_recirculation(
114
+ num_jobs=NUM_JOBS, num_machines=NUM_MACHINES
115
+ )
116
+ )
117
+ print(machine_matrix_with_recirculation)
118
+
119
+ machine_matrix_without_recirculation = (
120
+ generate_machine_matrix_without_recirculation(
121
+ num_jobs=NUM_JOBS, num_machines=NUM_MACHINES
122
+ )
123
+ )
124
+ print(machine_matrix_without_recirculation)
@@ -25,10 +25,10 @@ from job_shop_lib.graphs._build_disjunctive_graph import (
25
25
  add_source_sink_nodes,
26
26
  add_source_sink_edges,
27
27
  )
28
- from job_shop_lib.graphs._build_agent_task_graph import (
29
- build_agent_task_graph,
30
- build_complete_agent_task_graph,
31
- build_agent_task_graph_with_jobs,
28
+ from job_shop_lib.graphs._build_resource_task_graphs import (
29
+ build_resource_task_graph,
30
+ build_complete_resource_task_graph,
31
+ build_resource_task_graph_with_jobs,
32
32
  add_same_job_operations_edges,
33
33
  add_machine_nodes,
34
34
  add_operation_machine_edges,
@@ -52,9 +52,9 @@ __all__ = [
52
52
  "add_conjunctive_edges",
53
53
  "add_source_sink_nodes",
54
54
  "add_source_sink_edges",
55
- "build_agent_task_graph",
56
- "build_complete_agent_task_graph",
57
- "build_agent_task_graph_with_jobs",
55
+ "build_resource_task_graph",
56
+ "build_complete_resource_task_graph",
57
+ "build_resource_task_graph_with_jobs",
58
58
  "add_same_job_operations_edges",
59
59
  "add_machine_nodes",
60
60
  "add_operation_machine_edges",
@@ -1,16 +1,13 @@
1
- """Contains helper functions to build the agent-task graph or one of
2
- its generalizations from a job shop instance.
1
+ """Contains helper functions to build the resource-task graphs from a job shop
2
+ instance.
3
3
 
4
- The agent-task graph was introduced by Junyoung Park et al. (2021).
4
+ The agent-task graph (renamed to resource-task graph) was introduced by
5
+ Junyoung Park et al. (2021).
5
6
  In contrast to the disjunctive graph, instead of connecting operations that
6
7
  share the same resources directly by disjunctive edges, operation nodes are
7
8
  connected with machine ones. All machine nodes are connected between them, and
8
9
  all operation nodes from the same job are connected by non-directed edges too.
9
10
 
10
- We also support a generalization of this approach by the addition of job nodes
11
- and a global node. Job nodes are connected to all operation nodes of the same
12
- job, and the global node is connected to all machine and job nodes.
13
-
14
11
  References:
15
12
  - Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
16
13
  solve multi-agent scheduling problems with reinforcement learning. ArXiv,
@@ -23,11 +20,13 @@ from job_shop_lib import JobShopInstance
23
20
  from job_shop_lib.graphs import JobShopGraph, NodeType, Node
24
21
 
25
22
 
26
- def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
27
- """Builds the agent-task graph of the instance with job and global nodes.
23
+ def build_complete_resource_task_graph(
24
+ instance: JobShopInstance,
25
+ ) -> JobShopGraph:
26
+ """Builds the resource-task graph of the instance with job and global
27
+ nodes.
28
28
 
29
- The complete agent-task graph is a generalization of the agent-task graph
30
- that includes job nodes and a global node.
29
+ The complete resource-task includes job nodes and a global node.
31
30
 
32
31
  Job nodes are connected to all operation nodes of the same job, and the
33
32
  global node is connected to all machine and job nodes.
@@ -38,10 +37,11 @@ def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
38
37
 
39
38
  Args:
40
39
  instance:
41
- The job shop instance in which the agent-task graph will be built.
40
+ The job shop instance in which the resource-task graph will be
41
+ built.
42
42
 
43
43
  Returns:
44
- The complete agent-task graph of the instance.
44
+ The complete resource-task graph of the instance.
45
45
  """
46
46
  graph = JobShopGraph(instance)
47
47
 
@@ -58,23 +58,23 @@ def build_complete_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
58
58
  return graph
59
59
 
60
60
 
61
- def build_agent_task_graph_with_jobs(
61
+ def build_resource_task_graph_with_jobs(
62
62
  instance: JobShopInstance,
63
63
  ) -> JobShopGraph:
64
- """Builds the agent-task graph of the instance with job nodes.
64
+ """Builds the resource-task graph of the instance with job nodes.
65
65
 
66
- The agent-task graph with job nodes is a generalization of the agent-task
67
- graph that includes job nodes.
66
+ The resource-task graph that includes job nodes.
68
67
 
69
68
  Job nodes are connected to all operation nodes of the same job, and their
70
69
  are connected between them.
71
70
 
72
71
  Args:
73
72
  instance:
74
- The job shop instance in which the agent-task graph will be built.
73
+ The job shop instance in which the resource-task graph will be
74
+ built.
75
75
 
76
76
  Returns:
77
- The agent-task graph of the instance with job nodes.
77
+ The resource-task graph of the instance with job nodes.
78
78
  """
79
79
  graph = JobShopGraph(instance)
80
80
 
@@ -89,10 +89,11 @@ def build_agent_task_graph_with_jobs(
89
89
  return graph
90
90
 
91
91
 
92
- def build_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
93
- """Builds the agent-task graph of the instance.
92
+ def build_resource_task_graph(instance: JobShopInstance) -> JobShopGraph:
93
+ """Builds the resource-task graph of the instance.
94
94
 
95
- The agent-task graph was introduced by Junyoung Park et al. (2021).
95
+ The JSSP resource-task graph representation was introduced by Junyoung
96
+ Park et al. (2021) (named agent-task graph in the original paper).
96
97
 
97
98
  In contrast to the disjunctive graph, instead of connecting operations
98
99
  that share the same resources directly by disjunctive edges, operation
@@ -103,10 +104,11 @@ def build_agent_task_graph(instance: JobShopInstance) -> JobShopGraph:
103
104
 
104
105
  Args:
105
106
  instance:
106
- The job shop instance in which the agent-task graph will be built.
107
+ The job shop instance in which the resource-task graph will be
108
+ built.
107
109
 
108
110
  Returns:
109
- The agent-task graph of the instance.
111
+ The resource-task graph of the instance.
110
112
  """
111
113
  graph = JobShopGraph(instance)
112
114
 
@@ -1,5 +1,6 @@
1
1
  """Home of the `JobShopGraph` class."""
2
2
 
3
+ from typing import List, Union, Dict
3
4
  import collections
4
5
  import networkx as nx
5
6
 
@@ -62,23 +63,23 @@ class JobShopGraph:
62
63
  self.graph = nx.DiGraph()
63
64
  self.instance = instance
64
65
 
65
- self._nodes: list[Node] = []
66
- self._nodes_by_type: dict[NodeType, list[Node]] = (
66
+ self._nodes: List[Node] = []
67
+ self._nodes_by_type: Dict[NodeType, List[Node]] = (
67
68
  collections.defaultdict(list)
68
69
  )
69
- self._nodes_by_machine: list[list[Node]] = [
70
+ self._nodes_by_machine: List[List[Node]] = [
70
71
  [] for _ in range(instance.num_machines)
71
72
  ]
72
- self._nodes_by_job: list[list[Node]] = [
73
+ self._nodes_by_job: List[List[Node]] = [
73
74
  [] for _ in range(instance.num_jobs)
74
75
  ]
75
76
  self._next_node_id = 0
76
- self.removed_nodes: list[bool] = []
77
+ self.removed_nodes: List[bool] = []
77
78
  if add_operation_nodes:
78
79
  self.add_operation_nodes()
79
80
 
80
81
  @property
81
- def nodes(self) -> list[Node]:
82
+ def nodes(self) -> List[Node]:
82
83
  """List of all nodes added to the graph.
83
84
 
84
85
  It may contain nodes that have been removed from the graph.
@@ -86,7 +87,7 @@ class JobShopGraph:
86
87
  return self._nodes
87
88
 
88
89
  @property
89
- def nodes_by_type(self) -> dict[NodeType, list[Node]]:
90
+ def nodes_by_type(self) -> Dict[NodeType, List[Node]]:
90
91
  """Dictionary mapping node types to lists of nodes.
91
92
 
92
93
  It may contain nodes that have been removed from the graph.
@@ -94,7 +95,7 @@ class JobShopGraph:
94
95
  return self._nodes_by_type
95
96
 
96
97
  @property
97
- def nodes_by_machine(self) -> list[list[Node]]:
98
+ def nodes_by_machine(self) -> List[List[Node]]:
98
99
  """List of lists mapping machine ids to operation nodes.
99
100
 
100
101
  It may contain nodes that have been removed from the graph.
@@ -102,7 +103,7 @@ class JobShopGraph:
102
103
  return self._nodes_by_machine
103
104
 
104
105
  @property
105
- def nodes_by_job(self) -> list[list[Node]]:
106
+ def nodes_by_job(self) -> List[List[Node]]:
106
107
  """List of lists mapping job ids to operation nodes.
107
108
 
108
109
  It may contain nodes that have been removed from the graph.
@@ -163,7 +164,10 @@ class JobShopGraph:
163
164
  self._nodes_by_machine[machine_id].append(node_for_adding)
164
165
 
165
166
  def add_edge(
166
- self, u_of_edge: Node | int, v_of_edge: Node | int, **attr
167
+ self,
168
+ u_of_edge: Union[Node, int],
169
+ v_of_edge: Union[Node, int],
170
+ **attr,
167
171
  ) -> None:
168
172
  """Adds an edge to the graph.
169
173
 
@@ -177,7 +181,7 @@ class JobShopGraph:
177
181
  **attr: Additional attributes to be added to the edge.
178
182
 
179
183
  Raises:
180
- JobShopLibError: If `u_of_edge` or `v_of_edge` are not in the
184
+ ValidationError: If `u_of_edge` or `v_of_edge` are not in the
181
185
  graph.
182
186
  """
183
187
  if isinstance(u_of_edge, Node):
@@ -206,7 +210,7 @@ class JobShopGraph:
206
210
 
207
211
  self.graph.remove_nodes_from(isolated_nodes)
208
212
 
209
- def is_removed(self, node: int | Node) -> bool:
213
+ def is_removed(self, node: Union[int, Node]) -> bool:
210
214
  """Returns whether the node is removed from the graph.
211
215
 
212
216
  Args:
@@ -218,7 +222,7 @@ class JobShopGraph:
218
222
  node = node.node_id
219
223
  return self.removed_nodes[node]
220
224
 
221
- def non_removed_nodes(self) -> list[Node]:
225
+ def non_removed_nodes(self) -> List[Node]:
222
226
  """Returns the nodes that are not removed from the graph."""
223
227
  return [node for node in self._nodes if not self.is_removed(node)]
224
228
 
@@ -1,5 +1,7 @@
1
1
  """Home of the `Node` class."""
2
2
 
3
+ from typing import Optional
4
+
3
5
  from job_shop_lib import Operation
4
6
  from job_shop_lib.exceptions import (
5
7
  UninitializedAttributeError,
@@ -80,9 +82,9 @@ class Node:
80
82
  def __init__(
81
83
  self,
82
84
  node_type: NodeType,
83
- operation: Operation | None = None,
84
- machine_id: int | None = None,
85
- job_id: int | None = None,
85
+ operation: Optional[Operation] = None,
86
+ machine_id: Optional[int] = None,
87
+ job_id: Optional[int] = None,
86
88
  ):
87
89
  if node_type == NodeType.OPERATION and operation is None:
88
90
  raise ValidationError("Operation node must have an operation.")
@@ -94,7 +96,7 @@ class Node:
94
96
  raise ValidationError("Job node must have a job_id.")
95
97
 
96
98
  self.node_type: NodeType = node_type
97
- self._node_id: int | None = None
99
+ self._node_id: Optional[int] = None
98
100
 
99
101
  self._operation = operation
100
102
  self._machine_id = machine_id
@@ -1,5 +1,7 @@
1
1
  """Home of the `ResidualGraphUpdater` class."""
2
2
 
3
+ from typing import Optional, List
4
+
3
5
  from job_shop_lib import ScheduledOperation
4
6
  from job_shop_lib.exceptions import UninitializedAttributeError
5
7
  from job_shop_lib.graphs import NodeType, JobShopGraph
@@ -54,7 +56,7 @@ class ResidualGraphUpdater(GraphUpdater):
54
56
  remove_completed_machine_nodes: bool = True,
55
57
  remove_completed_job_nodes: bool = True,
56
58
  ):
57
- self._is_completed_observer: None | IsCompletedObserver = None
59
+ self._is_completed_observer: Optional[IsCompletedObserver] = None
58
60
  self.remove_completed_job_nodes = remove_completed_job_nodes
59
61
  self.remove_completed_machine_nodes = remove_completed_machine_nodes
60
62
  self._initialize_is_completed_observer_attribute(dispatcher)
@@ -80,7 +82,7 @@ class ResidualGraphUpdater(GraphUpdater):
80
82
  return False
81
83
  return True
82
84
 
83
- feature_types: list[FeatureType] = []
85
+ feature_types: List[FeatureType] = []
84
86
  if self.remove_completed_machine_nodes:
85
87
  feature_types.append(FeatureType.MACHINES)
86
88
  if self.remove_completed_job_nodes: