job-shop-lib 0.5.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. job_shop_lib/__init__.py +19 -8
  2. job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
  3. job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
  4. job_shop_lib/_operation.py +118 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +102 -84
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
  7. job_shop_lib/benchmarking/__init__.py +66 -43
  8. job_shop_lib/benchmarking/_load_benchmark.py +88 -0
  9. job_shop_lib/constraint_programming/__init__.py +13 -0
  10. job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
  11. job_shop_lib/dispatching/__init__.py +51 -42
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
  14. job_shop_lib/dispatching/_factories.py +135 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
  16. job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
  17. job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
  18. job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
  19. job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
  20. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
  21. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
  22. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
  23. job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
  24. job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
  25. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  26. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
  27. job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
  28. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
  29. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  30. job_shop_lib/dispatching/rules/__init__.py +87 -0
  31. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
  32. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
  33. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
  34. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
  35. job_shop_lib/dispatching/rules/_utils.py +128 -0
  36. job_shop_lib/exceptions.py +18 -0
  37. job_shop_lib/generation/__init__.py +19 -0
  38. job_shop_lib/generation/_general_instance_generator.py +165 -0
  39. job_shop_lib/generation/_instance_generator.py +133 -0
  40. job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
  41. job_shop_lib/generation/_utils.py +124 -0
  42. job_shop_lib/graphs/__init__.py +30 -12
  43. job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
  44. job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
  45. job_shop_lib/graphs/_constants.py +38 -0
  46. job_shop_lib/graphs/_job_shop_graph.py +320 -0
  47. job_shop_lib/graphs/_node.py +182 -0
  48. job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
  49. job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
  50. job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
  51. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
  52. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  53. job_shop_lib/py.typed +0 -0
  54. job_shop_lib/reinforcement_learning/__init__.py +68 -0
  55. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
  56. job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
  57. job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
  58. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
  59. job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
  60. job_shop_lib/reinforcement_learning/_utils.py +199 -0
  61. job_shop_lib/visualization/__init__.py +0 -25
  62. job_shop_lib/visualization/gantt/__init__.py +48 -0
  63. job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
  64. job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
  65. job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
  66. job_shop_lib/visualization/graphs/__init__.py +29 -0
  67. job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
  68. job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
  69. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
  70. job_shop_lib-1.0.0.dist-info/RECORD +73 -0
  71. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
  72. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  73. job_shop_lib/cp_sat/__init__.py +0 -5
  74. job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
  75. job_shop_lib/dispatching/factories.py +0 -206
  76. job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
  77. job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
  78. job_shop_lib/dispatching/feature_observers/factory.py +0 -58
  79. job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
  80. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  81. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  82. job_shop_lib/dispatching/pruning_functions.py +0 -116
  83. job_shop_lib/generators/__init__.py +0 -7
  84. job_shop_lib/generators/basic_generator.py +0 -197
  85. job_shop_lib/graphs/constants.py +0 -21
  86. job_shop_lib/graphs/job_shop_graph.py +0 -202
  87. job_shop_lib/graphs/node.py +0 -166
  88. job_shop_lib/operation.py +0 -122
  89. job_shop_lib/visualization/agent_task_graph.py +0 -257
  90. job_shop_lib/visualization/create_gif.py +0 -209
  91. job_shop_lib/visualization/disjunctive_graph.py +0 -210
  92. job_shop_lib-0.5.0.dist-info/RECORD +0 -48
  93. {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,165 @@
1
+ """Home of the `BasicGenerator` class."""
2
+
3
+ from typing import Optional, Tuple, Union
4
+ import random
5
+
6
+ from job_shop_lib import JobShopInstance
7
+ from job_shop_lib.exceptions import ValidationError
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
+ )
14
+
15
+
16
+ class GeneralInstanceGenerator(InstanceGenerator):
17
+ """Generates instances for job shop problems.
18
+
19
+ This class is designed to be versatile, enabling the creation of various
20
+ job shop instances without the need for multiple dedicated classes.
21
+
22
+ It supports customization of the number of jobs, machines, operation
23
+ durations, and more.
24
+
25
+ The class supports both single instance generation and iteration over
26
+ multiple instances, controlled by the ``iteration_limit`` parameter. It
27
+ implements the iterator protocol, allowing it to be used in a ``for`` loop.
28
+
29
+ The number of operations per machine is equal to the number of machines
30
+
31
+ Note:
32
+ When used as an iterator, the generator will produce instances until it
33
+ reaches the specified ``iteration_limit``. If ``iteration_limit`` is
34
+ ``None``, it will continue indefinitely.
35
+
36
+ Attributes:
37
+ num_jobs_range:
38
+ The range of the number of jobs to generate. If a single
39
+ ``int`` is provided, it is used as both the minimum and maximum.
40
+ duration_range:
41
+ The range of durations for each operation.
42
+ num_machines_range:
43
+ The range of the number of machines available. If a
44
+ single ``int`` is provided, it is used as both the minimum and
45
+ maximum.
46
+ machines_per_operation:
47
+ Specifies how many machines each operation
48
+ can be assigned to. If a single ``int`` is provided, it is used for
49
+ all operations.
50
+ allow_less_jobs_than_machines:
51
+ If ``True``, allows generating instances where the number of jobs
52
+ is less than the number of machines.
53
+ allow_recirculation:
54
+ If ``True``, a job can visit the same machine more than once.
55
+ name_suffix:
56
+ A suffix to append to each instance's name for identification.
57
+ seed:
58
+ Seed for the random number generator to ensure reproducibility.
59
+
60
+ Args:
61
+ num_jobs:
62
+ The range of the number of jobs to generate.
63
+ num_machines:
64
+ The range of the number of machines available.
65
+ duration_range:
66
+ The range of durations for each operation.
67
+ allow_less_jobs_than_machines:
68
+ Allows instances with fewer jobs than machines.
69
+ allow_recirculation:
70
+ Allows jobs to visit the same machine multiple times.
71
+ machines_per_operation:
72
+ Specifies how many machines each operation can be assigned to.
73
+ If a single ``int`` is provided, it is used for all operations.
74
+ name_suffix:
75
+ Suffix for instance names.
76
+ seed:
77
+ Seed for the random number generator.
78
+ iteration_limit:
79
+ Maximum number of instances to generate in iteration mode.
80
+ """
81
+
82
+ def __init__( # pylint: disable=too-many-arguments
83
+ self,
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),
87
+ allow_less_jobs_than_machines: bool = True,
88
+ allow_recirculation: bool = False,
89
+ machines_per_operation: Union[int, Tuple[int, int]] = 1,
90
+ name_suffix: str = "classic_generated_instance",
91
+ seed: Optional[int] = None,
92
+ iteration_limit: Optional[int] = None,
93
+ ):
94
+ super().__init__(
95
+ num_jobs=num_jobs,
96
+ num_machines=num_machines,
97
+ duration_range=duration_range,
98
+ name_suffix=name_suffix,
99
+ seed=seed,
100
+ iteration_limit=iteration_limit,
101
+ )
102
+ if isinstance(machines_per_operation, int):
103
+ machines_per_operation = (
104
+ machines_per_operation,
105
+ machines_per_operation,
106
+ )
107
+ if machines_per_operation != (1, 1):
108
+ raise NotImplementedError(
109
+ "The number of machines per operation must be 1 for now."
110
+ )
111
+ self.machines_per_operation = machines_per_operation
112
+
113
+ self.allow_less_jobs_than_machines = allow_less_jobs_than_machines
114
+ self.allow_recirculation = allow_recirculation
115
+ self.name_suffix = name_suffix
116
+
117
+ if seed is not None:
118
+ random.seed(seed)
119
+
120
+ def __repr__(self) -> str:
121
+ return (
122
+ f"GeneralInstanceGenerator("
123
+ f"num_jobs_range={self.num_jobs_range}, "
124
+ f"num_machines_range={self.num_machines_range}, "
125
+ f"duration_range={self.duration_range})"
126
+ )
127
+
128
+ def generate(
129
+ self,
130
+ num_jobs: Optional[int] = None,
131
+ num_machines: Optional[int] = None,
132
+ ) -> JobShopInstance:
133
+ if num_jobs is None:
134
+ num_jobs = random.randint(*self.num_jobs_range)
135
+
136
+ if num_machines is None:
137
+ min_num_machines, max_num_machines = self.num_machines_range
138
+ if not self.allow_less_jobs_than_machines:
139
+ min_num_machines = min(num_jobs, max_num_machines)
140
+ num_machines = random.randint(min_num_machines, max_num_machines)
141
+ elif (
142
+ not self.allow_less_jobs_than_machines and num_jobs < num_machines
143
+ ):
144
+ raise ValidationError(
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
159
+ )
160
+
161
+ return JobShopInstance.from_matrices(
162
+ duration_matrix.tolist(),
163
+ machine_matrix.tolist(),
164
+ name=self._next_name(),
165
+ )
@@ -0,0 +1,133 @@
1
+ """Home of the `InstanceGenerator` class."""
2
+
3
+ import abc
4
+
5
+ import random
6
+ from typing import Iterator, Optional, Tuple, Union
7
+
8
+ from job_shop_lib import JobShopInstance
9
+ from job_shop_lib.exceptions import UninitializedAttributeError
10
+
11
+
12
+ class InstanceGenerator(abc.ABC):
13
+ """Common interface for all generators.
14
+
15
+ The class supports both single instance generation and iteration over
16
+ multiple instances, controlled by the `iteration_limit` parameter. It
17
+ implements the iterator protocol, allowing it to be used in a `for` loop.
18
+
19
+ Note:
20
+ When used as an iterator, the generator will produce instances until it
21
+ reaches the specified `iteration_limit`. If `iteration_limit` is None,
22
+ it will continue indefinitely.
23
+
24
+ Attributes:
25
+ num_jobs_range:
26
+ The range of the number of jobs to generate. If a single
27
+ int is provided, it is used as both the minimum and maximum.
28
+ duration_range:
29
+ The range of durations for each operation.
30
+ num_machines_range:
31
+ The range of the number of machines available. If a
32
+ single int is provided, it is used as both the minimum and maximum.
33
+ name_suffix:
34
+ A suffix to append to each instance's name for identification.
35
+ seed:
36
+ Seed for the random number generator to ensure reproducibility.
37
+
38
+ Args:
39
+ num_jobs:
40
+ The range of the number of jobs to generate.
41
+ num_machines:
42
+ The range of the number of machines available.
43
+ duration_range:
44
+ The range of durations for each operation.
45
+ name_suffix:
46
+ Suffix for instance names.
47
+ seed:
48
+ Seed for the random number generator.
49
+ iteration_limit:
50
+ Maximum number of instances to generate in iteration mode.
51
+ """
52
+
53
+ def __init__( # pylint: disable=too-many-arguments
54
+ self,
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),
58
+ name_suffix: str = "generated_instance",
59
+ seed: Optional[int] = None,
60
+ iteration_limit: Optional[int] = None,
61
+ ):
62
+ if isinstance(num_jobs, int):
63
+ num_jobs = (num_jobs, num_jobs)
64
+ if isinstance(num_machines, int):
65
+ num_machines = (num_machines, num_machines)
66
+ if seed is not None:
67
+ random.seed(seed)
68
+
69
+ self.num_jobs_range = num_jobs
70
+ self.num_machines_range = num_machines
71
+ self.duration_range = duration_range
72
+ self.name_suffix = name_suffix
73
+
74
+ self._counter = 0
75
+ self._current_iteration = 0
76
+ self._iteration_limit = iteration_limit
77
+
78
+ @abc.abstractmethod
79
+ def generate(
80
+ self,
81
+ num_jobs: Optional[int] = None,
82
+ num_machines: Optional[int] = None,
83
+ ) -> JobShopInstance:
84
+ """Generates a single job shop instance
85
+
86
+ Args:
87
+ num_jobs: The number of jobs to generate. If None, a random value
88
+ within the specified range will be used.
89
+ num_machines: The number of machines to generate. If None, a random
90
+ value within the specified range will be used.
91
+ """
92
+
93
+ def _next_name(self) -> str:
94
+ self._counter += 1
95
+ return f"{self.name_suffix}_{self._counter}"
96
+
97
+ def __iter__(self) -> Iterator[JobShopInstance]:
98
+ self._current_iteration = 0
99
+ return self
100
+
101
+ def __next__(self) -> JobShopInstance:
102
+ if (
103
+ self._iteration_limit is not None
104
+ and self._current_iteration >= self._iteration_limit
105
+ ):
106
+ raise StopIteration
107
+ self._current_iteration += 1
108
+ return self.generate()
109
+
110
+ def __len__(self) -> int:
111
+ if self._iteration_limit is None:
112
+ raise UninitializedAttributeError("Iteration limit is not set.")
113
+ return self._iteration_limit
114
+
115
+ @property
116
+ def max_num_jobs(self) -> int:
117
+ """Returns the maximum number of jobs that can be generated."""
118
+ return self.num_jobs_range[1]
119
+
120
+ @property
121
+ def min_num_jobs(self) -> int:
122
+ """Returns the minimum number of jobs that can be generated."""
123
+ return self.num_jobs_range[0]
124
+
125
+ @property
126
+ def max_num_machines(self) -> int:
127
+ """Returns the maximum number of machines that can be generated."""
128
+ return self.num_machines_range[1]
129
+
130
+ @property
131
+ def min_num_machines(self) -> int:
132
+ """Returns the minimum number of machines that can be generated."""
133
+ return self.num_machines_range[0]
@@ -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}"
@@ -111,22 +112,25 @@ class AddDurationNoise(Transformation):
111
112
 
112
113
  class RemoveJobs(Transformation):
113
114
  """Removes jobs randomly until the number of jobs is within a specified
114
- range."""
115
+ range.
116
+
117
+ Args:
118
+ min_jobs:
119
+ The minimum number of jobs to remain in the instance.
120
+ max_jobs:
121
+ The maximum number of jobs to remain in the instance.
122
+ target_jobs:
123
+ If specified, the number of jobs to remain in the
124
+ instance. Overrides ``min_jobs`` and ``max_jobs``.
125
+ """
115
126
 
116
127
  def __init__(
117
128
  self,
118
129
  min_jobs: int,
119
130
  max_jobs: int,
120
- target_jobs: int | None = None,
121
- suffix: str | None = None,
131
+ target_jobs: Optional[int] = None,
132
+ suffix: Optional[str] = None,
122
133
  ):
123
- """
124
- Args:
125
- min_jobs: The minimum number of jobs to remain in the instance.
126
- max_jobs: The maximum number of jobs to remain in the instance.
127
- target_jobs: If specified, the number of jobs to remain in the
128
- instance. Overrides min_jobs and max_jobs.
129
- """
130
134
  if suffix is None:
131
135
  suffix = f"_jobs={min_jobs}-{max_jobs}"
132
136
  super().__init__(suffix=suffix)
@@ -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)
@@ -1,19 +1,34 @@
1
- """Package for graph related classes and functions."""
1
+ """Package for graph related classes and functions.
2
2
 
3
- from job_shop_lib.graphs.constants import EdgeType, NodeType
4
- from job_shop_lib.graphs.node import Node
5
- from job_shop_lib.graphs.job_shop_graph import JobShopGraph, NODE_ATTR
6
- from job_shop_lib.graphs.build_disjunctive_graph import (
3
+ The main classes and functions available in this package are:
4
+
5
+ .. autosummary::
6
+ JobShopGraph
7
+ Node
8
+ NodeType
9
+ build_disjunctive_graph
10
+ build_solved_disjunctive_graph
11
+ build_resource_task_graph
12
+ build_complete_resource_task_graph
13
+ build_resource_task_graph_with_jobs
14
+
15
+ """
16
+
17
+ from job_shop_lib.graphs._constants import EdgeType, NodeType
18
+ from job_shop_lib.graphs._node import Node
19
+ from job_shop_lib.graphs._job_shop_graph import JobShopGraph, NODE_ATTR
20
+ from job_shop_lib.graphs._build_disjunctive_graph import (
7
21
  build_disjunctive_graph,
22
+ build_solved_disjunctive_graph,
8
23
  add_disjunctive_edges,
9
24
  add_conjunctive_edges,
10
25
  add_source_sink_nodes,
11
26
  add_source_sink_edges,
12
27
  )
13
- from job_shop_lib.graphs.build_agent_task_graph import (
14
- build_agent_task_graph,
15
- build_complete_agent_task_graph,
16
- 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,
17
32
  add_same_job_operations_edges,
18
33
  add_machine_nodes,
19
34
  add_operation_machine_edges,
@@ -23,6 +38,7 @@ from job_shop_lib.graphs.build_agent_task_graph import (
23
38
  add_global_node,
24
39
  add_machine_global_edges,
25
40
  add_job_global_edges,
41
+ add_job_job_edges,
26
42
  )
27
43
 
28
44
 
@@ -37,9 +53,9 @@ __all__ = [
37
53
  "add_conjunctive_edges",
38
54
  "add_source_sink_nodes",
39
55
  "add_source_sink_edges",
40
- "build_agent_task_graph",
41
- "build_complete_agent_task_graph",
42
- "build_agent_task_graph_with_jobs",
56
+ "build_resource_task_graph",
57
+ "build_complete_resource_task_graph",
58
+ "build_resource_task_graph_with_jobs",
43
59
  "add_same_job_operations_edges",
44
60
  "add_machine_nodes",
45
61
  "add_operation_machine_edges",
@@ -49,4 +65,6 @@ __all__ = [
49
65
  "add_global_node",
50
66
  "add_machine_global_edges",
51
67
  "add_job_global_edges",
68
+ "build_solved_disjunctive_graph",
69
+ "add_job_job_edges",
52
70
  ]
@@ -18,14 +18,15 @@ each disjunctive edge such that the overall processing time is minimized.
18
18
 
19
19
  import itertools
20
20
 
21
- from job_shop_lib import JobShopInstance
21
+ from job_shop_lib import JobShopInstance, Schedule
22
22
  from job_shop_lib.graphs import JobShopGraph, EdgeType, NodeType, Node
23
23
 
24
24
 
25
25
  def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
26
26
  """Builds and returns a disjunctive graph for the given job shop instance.
27
27
 
28
- This function creates a complete disjunctive graph from a JobShopInstance.
28
+ This function creates a complete disjunctive graph from a
29
+ :JobShopInstance.
29
30
  It starts by initializing a JobShopGraph object and proceeds by adding
30
31
  disjunctive edges between operations using the same machine, conjunctive
31
32
  edges between successive operations in the same job, and finally, special
@@ -40,7 +41,7 @@ def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
40
41
  the graph.
41
42
 
42
43
  Returns:
43
- JobShopGraph: A JobShopGraph object representing the disjunctive graph
44
+ A :class:`JobShopGraph` object representing the disjunctive graph
44
45
  of the job shop scheduling problem.
45
46
  """
46
47
  graph = JobShopGraph(instance)
@@ -51,6 +52,43 @@ def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
51
52
  return graph
52
53
 
53
54
 
55
+ def build_solved_disjunctive_graph(schedule: Schedule) -> JobShopGraph:
56
+ """Builds and returns a disjunctive graph for the given solved schedule.
57
+
58
+ This function constructs a disjunctive graph from the given schedule,
59
+ keeping only the disjunctive edges that represent the chosen ordering
60
+ of operations on each machine as per the solution schedule.
61
+
62
+ Args:
63
+ schedule (Schedule): The solved schedule that contains the sequencing
64
+ of operations on each machine.
65
+
66
+ Returns:
67
+ A JobShopGraph object representing the disjunctive graph
68
+ of the solved job shop scheduling problem.
69
+ """
70
+ # Build the base disjunctive graph from the job shop instance
71
+ graph = JobShopGraph(schedule.instance)
72
+ add_conjunctive_edges(graph)
73
+ add_source_sink_nodes(graph)
74
+ add_source_sink_edges(graph)
75
+
76
+ # Iterate over each machine and add only the edges that match the solution
77
+ # order
78
+ for machine_schedule in schedule.schedule:
79
+ for i, scheduled_operation in enumerate(machine_schedule):
80
+ if i + 1 >= len(machine_schedule):
81
+ break
82
+ next_scheduled_operation = machine_schedule[i + 1]
83
+ graph.add_edge(
84
+ scheduled_operation.operation.operation_id,
85
+ next_scheduled_operation.operation.operation_id,
86
+ type=EdgeType.DISJUNCTIVE,
87
+ )
88
+
89
+ return graph
90
+
91
+
54
92
  def add_disjunctive_edges(graph: JobShopGraph) -> None:
55
93
  """Adds disjunctive edges to the graph."""
56
94