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.
- job_shop_lib/__init__.py +19 -8
- job_shop_lib/{base_solver.py → _base_solver.py} +1 -1
- job_shop_lib/{job_shop_instance.py → _job_shop_instance.py} +155 -81
- job_shop_lib/_operation.py +118 -0
- job_shop_lib/{schedule.py → _schedule.py} +102 -84
- job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +25 -49
- job_shop_lib/benchmarking/__init__.py +66 -43
- job_shop_lib/benchmarking/_load_benchmark.py +88 -0
- job_shop_lib/constraint_programming/__init__.py +13 -0
- job_shop_lib/{cp_sat/ortools_solver.py → constraint_programming/_ortools_solver.py} +77 -22
- job_shop_lib/dispatching/__init__.py +51 -42
- job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +223 -130
- job_shop_lib/dispatching/_dispatcher_observer_config.py +67 -0
- job_shop_lib/dispatching/_factories.py +135 -0
- job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +6 -7
- job_shop_lib/dispatching/_optimal_operations_observer.py +113 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +168 -0
- job_shop_lib/dispatching/_unscheduled_operations_observer.py +70 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +51 -13
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +212 -0
- job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +20 -18
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +289 -0
- job_shop_lib/dispatching/feature_observers/_factory.py +95 -0
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +228 -0
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +35 -0
- job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +9 -5
- job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +8 -10
- job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
- job_shop_lib/dispatching/rules/__init__.py +87 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +84 -0
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +201 -0
- job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +70 -16
- job_shop_lib/dispatching/rules/_machine_chooser_factory.py +71 -0
- job_shop_lib/dispatching/rules/_utils.py +128 -0
- job_shop_lib/exceptions.py +18 -0
- job_shop_lib/generation/__init__.py +19 -0
- job_shop_lib/generation/_general_instance_generator.py +165 -0
- job_shop_lib/generation/_instance_generator.py +133 -0
- job_shop_lib/{generators/transformations.py → generation/_transformations.py} +16 -12
- job_shop_lib/generation/_utils.py +124 -0
- job_shop_lib/graphs/__init__.py +30 -12
- job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +41 -3
- job_shop_lib/graphs/{build_agent_task_graph.py → _build_resource_task_graphs.py} +28 -26
- job_shop_lib/graphs/_constants.py +38 -0
- job_shop_lib/graphs/_job_shop_graph.py +320 -0
- job_shop_lib/graphs/_node.py +182 -0
- job_shop_lib/graphs/graph_updaters/__init__.py +26 -0
- job_shop_lib/graphs/graph_updaters/_disjunctive_graph_updater.py +108 -0
- job_shop_lib/graphs/graph_updaters/_graph_updater.py +57 -0
- job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +155 -0
- job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
- job_shop_lib/py.typed +0 -0
- job_shop_lib/reinforcement_learning/__init__.py +68 -0
- job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +398 -0
- job_shop_lib/reinforcement_learning/_resource_task_graph_observation.py +329 -0
- job_shop_lib/reinforcement_learning/_reward_observers.py +87 -0
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +443 -0
- job_shop_lib/reinforcement_learning/_types_and_constants.py +62 -0
- job_shop_lib/reinforcement_learning/_utils.py +199 -0
- job_shop_lib/visualization/__init__.py +0 -25
- job_shop_lib/visualization/gantt/__init__.py +48 -0
- job_shop_lib/visualization/gantt/_gantt_chart_creator.py +257 -0
- job_shop_lib/visualization/gantt/_gantt_chart_video_and_gif_creation.py +422 -0
- job_shop_lib/visualization/{gantt_chart.py → gantt/_plot_gantt_chart.py} +84 -21
- job_shop_lib/visualization/graphs/__init__.py +29 -0
- job_shop_lib/visualization/graphs/_plot_disjunctive_graph.py +418 -0
- job_shop_lib/visualization/graphs/_plot_resource_task_graph.py +389 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/METADATA +87 -55
- job_shop_lib-1.0.0.dist-info/RECORD +73 -0
- {job_shop_lib-0.5.0.dist-info → job_shop_lib-1.0.0.dist-info}/WHEEL +1 -1
- job_shop_lib/benchmarking/load_benchmark.py +0 -142
- job_shop_lib/cp_sat/__init__.py +0 -5
- job_shop_lib/dispatching/dispatching_rule_solver.py +0 -119
- job_shop_lib/dispatching/factories.py +0 -206
- job_shop_lib/dispatching/feature_observers/composite_feature_observer.py +0 -87
- job_shop_lib/dispatching/feature_observers/earliest_start_time_observer.py +0 -156
- job_shop_lib/dispatching/feature_observers/factory.py +0 -58
- job_shop_lib/dispatching/feature_observers/feature_observer.py +0 -113
- job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
- job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
- job_shop_lib/dispatching/pruning_functions.py +0 -116
- job_shop_lib/generators/__init__.py +0 -7
- job_shop_lib/generators/basic_generator.py +0 -197
- job_shop_lib/graphs/constants.py +0 -21
- job_shop_lib/graphs/job_shop_graph.py +0 -202
- job_shop_lib/graphs/node.py +0 -166
- job_shop_lib/operation.py +0 -122
- job_shop_lib/visualization/agent_task_graph.py +0 -257
- job_shop_lib/visualization/create_gif.py +0 -209
- job_shop_lib/visualization/disjunctive_graph.py +0 -210
- job_shop_lib-0.5.0.dist-info/RECORD +0 -48
- {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
|
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
|
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
|
121
|
-
suffix: str
|
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)
|
job_shop_lib/graphs/__init__.py
CHANGED
@@ -1,19 +1,34 @@
|
|
1
|
-
"""Package for graph related classes and functions.
|
1
|
+
"""Package for graph related classes and functions.
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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.
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
"
|
41
|
-
"
|
42
|
-
"
|
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
|
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
|
-
|
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
|
|