job-shop-lib 1.0.0a5__py3-none-any.whl → 1.0.0b2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. job_shop_lib/__init__.py +1 -1
  2. job_shop_lib/_job_shop_instance.py +34 -29
  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 -6
  7. job_shop_lib/dispatching/__init__.py +4 -3
  8. job_shop_lib/dispatching/_dispatcher.py +19 -19
  9. job_shop_lib/dispatching/_dispatcher_observer_config.py +4 -4
  10. job_shop_lib/dispatching/_factories.py +4 -2
  11. job_shop_lib/dispatching/_history_observer.py +2 -1
  12. job_shop_lib/dispatching/_optimal_operations_observer.py +115 -0
  13. job_shop_lib/dispatching/_ready_operation_filters.py +19 -18
  14. job_shop_lib/dispatching/_unscheduled_operations_observer.py +4 -3
  15. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +7 -8
  16. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +3 -1
  17. job_shop_lib/dispatching/feature_observers/_factory.py +13 -14
  18. job_shop_lib/dispatching/feature_observers/_feature_observer.py +9 -8
  19. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +2 -1
  20. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +4 -2
  21. job_shop_lib/dispatching/rules/__init__.py +37 -1
  22. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +4 -2
  23. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +50 -20
  24. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -8
  25. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +4 -3
  26. job_shop_lib/dispatching/rules/_utils.py +9 -8
  27. job_shop_lib/generation/__init__.py +8 -0
  28. job_shop_lib/generation/_general_instance_generator.py +42 -64
  29. job_shop_lib/generation/_instance_generator.py +11 -7
  30. job_shop_lib/generation/_transformations.py +5 -4
  31. job_shop_lib/generation/_utils.py +124 -0
  32. job_shop_lib/graphs/__init__.py +7 -7
  33. job_shop_lib/graphs/{_build_agent_task_graph.py → _build_resource_task_graphs.py} +26 -24
  34. job_shop_lib/graphs/_job_shop_graph.py +17 -13
  35. job_shop_lib/graphs/_node.py +6 -4
  36. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +4 -2
  37. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +40 -20
  38. job_shop_lib/reinforcement_learning/_reward_observers.py +3 -1
  39. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +89 -22
  40. job_shop_lib/reinforcement_learning/_types_and_constants.py +1 -1
  41. job_shop_lib/reinforcement_learning/_utils.py +3 -3
  42. job_shop_lib/visualization/__init__.py +0 -60
  43. job_shop_lib/visualization/gantt/__init__.py +48 -0
  44. job_shop_lib/visualization/{_gantt_chart_creator.py → gantt/_gantt_chart_creator.py} +12 -12
  45. job_shop_lib/visualization/{_gantt_chart_video_and_gif_creation.py → gantt/_gantt_chart_video_and_gif_creation.py} +22 -22
  46. job_shop_lib/visualization/{_plot_gantt_chart.py → gantt/_plot_gantt_chart.py} +12 -13
  47. job_shop_lib/visualization/graphs/__init__.py +29 -0
  48. job_shop_lib/visualization/{_plot_disjunctive_graph.py → graphs/_plot_disjunctive_graph.py} +18 -16
  49. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  50. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/METADATA +21 -15
  51. job_shop_lib-1.0.0b2.dist-info/RECORD +70 -0
  52. job_shop_lib/visualization/_plot_agent_task_graph.py +0 -276
  53. job_shop_lib-1.0.0a5.dist-info/RECORD +0 -66
  54. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/LICENSE +0 -0
  55. {job_shop_lib-1.0.0a5.dist-info → job_shop_lib-1.0.0b2.dist-info}/WHEEL +0 -0
@@ -6,6 +6,7 @@ which operations are selected for execution based on certain criteria such as
6
6
  shortest processing time, first come first served, etc.
7
7
  """
8
8
 
9
+ from typing import List, Optional
9
10
  from collections.abc import Callable, Sequence
10
11
  import random
11
12
 
@@ -88,7 +89,7 @@ def score_based_rule(
88
89
 
89
90
 
90
91
  def score_based_rule_with_tie_breaker(
91
- score_functions: list[Callable[[Dispatcher], Sequence[int]]],
92
+ score_functions: List[Callable[[Dispatcher], Sequence[int]]],
92
93
  ) -> Callable[[Dispatcher], Operation]:
93
94
  """Creates a dispatching rule based on multiple scoring functions.
94
95
 
@@ -122,7 +123,7 @@ def score_based_rule_with_tie_breaker(
122
123
  # -----------------
123
124
 
124
125
 
125
- def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
126
+ def shortest_processing_time_score(dispatcher: Dispatcher) -> List[int]:
126
127
  """Scores each job based on the duration of the next operation."""
127
128
  num_jobs = dispatcher.instance.num_jobs
128
129
  scores = [0] * num_jobs
@@ -131,7 +132,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
131
132
  return scores
132
133
 
133
134
 
134
- def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
135
+ def first_come_first_served_score(dispatcher: Dispatcher) -> List[int]:
135
136
  """Scores each job based on the position of the next operation."""
136
137
  num_jobs = dispatcher.instance.num_jobs
137
138
  scores = [0] * num_jobs
@@ -153,9 +154,9 @@ class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
153
154
  """
154
155
 
155
156
  def __init__(self) -> None:
156
- self._duration_observer: DurationObserver | None = None
157
- self._is_ready_observer: IsReadyObserver | None = None
158
- self._current_dispatcher: Dispatcher | None = None
157
+ self._duration_observer: Optional[DurationObserver] = None
158
+ self._is_ready_observer: Optional[IsReadyObserver] = None
159
+ self._current_dispatcher: Optional[Dispatcher] = None
159
160
 
160
161
  def __call__(self, dispatcher: Dispatcher) -> Sequence[int]:
161
162
  """Scores each job based on the remaining work in the job."""
@@ -197,7 +198,7 @@ observer_based_most_work_remaining_rule = score_based_rule(
197
198
  )
198
199
 
199
200
 
200
- def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
201
+ def most_operations_remaining_score(dispatcher: Dispatcher) -> List[int]:
201
202
  """Scores each job based on the remaining operations in the job."""
202
203
  num_jobs = dispatcher.instance.num_jobs
203
204
  scores = [0] * num_jobs
@@ -206,7 +207,7 @@ def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
206
207
  return scores
207
208
 
208
209
 
209
- def random_score(dispatcher: Dispatcher) -> list[int]:
210
+ def random_score(dispatcher: Dispatcher) -> List[int]:
210
211
  """Scores each job randomly."""
211
212
  return [
212
213
  random.randint(0, 100) for _ in range(dispatcher.instance.num_jobs)
@@ -5,6 +5,7 @@ The factory functions create and return the appropriate functions based on the
5
5
  specified names or enums.
6
6
  """
7
7
 
8
+ from typing import Union, Dict
8
9
  from enum import Enum
9
10
  from collections.abc import Callable
10
11
  import random
@@ -25,7 +26,7 @@ MachineChooser = Callable[[Dispatcher, Operation], int]
25
26
 
26
27
 
27
28
  def machine_chooser_factory(
28
- machine_chooser: str | MachineChooser,
29
+ machine_chooser: Union[str, MachineChooser],
29
30
  ) -> MachineChooser:
30
31
  """Creates and returns a machine chooser function based on the specified
31
32
  machine chooser strategy name.
@@ -36,7 +37,7 @@ def machine_chooser_factory(
36
37
  machine randomly.
37
38
 
38
39
  Args:
39
- machine_chooser (str): The name of the machine chooser strategy to be
40
+ machine_chooser: The name of the machine chooser strategy to be
40
41
  used. Supported values are 'first' and 'random'.
41
42
 
42
43
  Returns:
@@ -49,7 +50,7 @@ def machine_chooser_factory(
49
50
  ValueError: If the machine_chooser argument is not recognized or is
50
51
  not supported.
51
52
  """
52
- machine_choosers: dict[str, Callable[[Dispatcher, Operation], int]] = {
53
+ machine_choosers: Dict[str, Callable[[Dispatcher, Operation], int]] = {
53
54
  MachineChooserType.FIRST: lambda _, operation: operation.machines[0],
54
55
  MachineChooserType.RANDOM: lambda _, operation: random.choice(
55
56
  operation.machines
@@ -1,5 +1,6 @@
1
1
  """Utility functions."""
2
2
 
3
+ from typing import Union, List
3
4
  import time
4
5
  from collections.abc import Callable
5
6
  import pandas as pd
@@ -10,12 +11,12 @@ from job_shop_lib.dispatching import Dispatcher
10
11
 
11
12
 
12
13
  def benchmark_dispatching_rules(
13
- dispatching_rules: (
14
- list[str | Callable[[Dispatcher], Operation]]
15
- | list[str]
16
- | list[Callable[[Dispatcher], Operation]]
17
- ),
18
- instances: list[JobShopInstance],
14
+ dispatching_rules: Union[
15
+ List[Union[str, Callable[[Dispatcher], Operation]]],
16
+ List[str],
17
+ List[Callable[[Dispatcher], Operation]]
18
+ ],
19
+ instances: List[JobShopInstance],
19
20
  ) -> pd.DataFrame:
20
21
  """Benchmark multiple dispatching rules on multiple JobShopInstances.
21
22
 
@@ -30,7 +31,7 @@ def benchmark_dispatching_rules(
30
31
  either a string (name of a built-in rule) or a callable
31
32
  (custom rule function).
32
33
  instances:
33
- iList of :class:`JobShopInstance` objects to be solved.
34
+ List of :class:`JobShopInstance` objects to be solved.
34
35
 
35
36
  Returns:
36
37
  A pandas DataFrame with columns:
@@ -111,7 +112,7 @@ if __name__ == "__main__":
111
112
  instances_ = [load_benchmark_instance(f"ta{i:02d}") for i in range(1, 3)]
112
113
 
113
114
  # Define rules
114
- rules_: list[str | Callable[[Dispatcher], Operation]] = [
115
+ rules_: List[str | Callable[[Dispatcher], Operation]] = [
115
116
  "most_work_remaining",
116
117
  "shortest_processing_time",
117
118
  most_work_remaining_rule,
@@ -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