job-shop-lib 0.5.1__py3-none-any.whl → 1.0.0a1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. job_shop_lib/__init__.py +16 -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} +9 -4
  4. job_shop_lib/_operation.py +95 -0
  5. job_shop_lib/{schedule.py → _schedule.py} +73 -54
  6. job_shop_lib/{scheduled_operation.py → _scheduled_operation.py} +13 -37
  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} +57 -18
  11. job_shop_lib/dispatching/__init__.py +45 -41
  12. job_shop_lib/dispatching/{dispatcher.py → _dispatcher.py} +153 -80
  13. job_shop_lib/dispatching/_dispatcher_observer_config.py +54 -0
  14. job_shop_lib/dispatching/_factories.py +125 -0
  15. job_shop_lib/dispatching/{history_tracker.py → _history_observer.py} +4 -6
  16. job_shop_lib/dispatching/{pruning_functions.py → _ready_operation_filters.py} +6 -35
  17. job_shop_lib/dispatching/_unscheduled_operations_observer.py +69 -0
  18. job_shop_lib/dispatching/feature_observers/__init__.py +16 -10
  19. job_shop_lib/dispatching/feature_observers/{composite_feature_observer.py → _composite_feature_observer.py} +84 -2
  20. job_shop_lib/dispatching/feature_observers/{duration_observer.py → _duration_observer.py} +6 -17
  21. job_shop_lib/dispatching/feature_observers/{earliest_start_time_observer.py → _earliest_start_time_observer.py} +114 -35
  22. job_shop_lib/dispatching/feature_observers/{factory.py → _factory.py} +31 -5
  23. job_shop_lib/dispatching/feature_observers/{feature_observer.py → _feature_observer.py} +59 -16
  24. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +97 -0
  25. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +33 -0
  26. job_shop_lib/dispatching/feature_observers/{position_in_job_observer.py → _position_in_job_observer.py} +1 -8
  27. job_shop_lib/dispatching/feature_observers/{remaining_operations_observer.py → _remaining_operations_observer.py} +8 -26
  28. job_shop_lib/dispatching/rules/__init__.py +51 -0
  29. job_shop_lib/dispatching/rules/_dispatching_rule_factory.py +82 -0
  30. job_shop_lib/dispatching/{dispatching_rule_solver.py → rules/_dispatching_rule_solver.py} +44 -15
  31. job_shop_lib/dispatching/{dispatching_rules.py → rules/_dispatching_rules_functions.py} +74 -21
  32. job_shop_lib/dispatching/rules/_machine_chooser_factory.py +69 -0
  33. job_shop_lib/dispatching/rules/_utils.py +127 -0
  34. job_shop_lib/exceptions.py +18 -0
  35. job_shop_lib/generation/__init__.py +2 -2
  36. job_shop_lib/generation/{general_instance_generator.py → _general_instance_generator.py} +26 -7
  37. job_shop_lib/generation/{instance_generator.py → _instance_generator.py} +13 -3
  38. job_shop_lib/graphs/__init__.py +17 -6
  39. job_shop_lib/graphs/{job_shop_graph.py → _job_shop_graph.py} +81 -2
  40. job_shop_lib/graphs/{node.py → _node.py} +18 -12
  41. job_shop_lib/graphs/graph_updaters/__init__.py +13 -0
  42. job_shop_lib/graphs/graph_updaters/_graph_updater.py +59 -0
  43. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +154 -0
  44. job_shop_lib/graphs/graph_updaters/_utils.py +25 -0
  45. job_shop_lib/reinforcement_learning/__init__.py +41 -0
  46. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +366 -0
  47. job_shop_lib/reinforcement_learning/_reward_observers.py +85 -0
  48. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +337 -0
  49. job_shop_lib/reinforcement_learning/_types_and_constants.py +61 -0
  50. job_shop_lib/reinforcement_learning/_utils.py +96 -0
  51. job_shop_lib/visualization/__init__.py +20 -4
  52. job_shop_lib/visualization/{agent_task_graph.py → _agent_task_graph.py} +28 -9
  53. job_shop_lib/visualization/_gantt_chart_creator.py +219 -0
  54. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +388 -0
  55. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/METADATA +68 -44
  56. job_shop_lib-1.0.0a1.dist-info/RECORD +66 -0
  57. job_shop_lib/benchmarking/load_benchmark.py +0 -142
  58. job_shop_lib/cp_sat/__init__.py +0 -5
  59. job_shop_lib/dispatching/factories.py +0 -206
  60. job_shop_lib/dispatching/feature_observers/is_completed_observer.py +0 -98
  61. job_shop_lib/dispatching/feature_observers/is_ready_observer.py +0 -40
  62. job_shop_lib/generators/__init__.py +0 -8
  63. job_shop_lib/generators/basic_generator.py +0 -200
  64. job_shop_lib/generators/transformations.py +0 -164
  65. job_shop_lib/operation.py +0 -122
  66. job_shop_lib/visualization/create_gif.py +0 -209
  67. job_shop_lib-0.5.1.dist-info/RECORD +0 -52
  68. /job_shop_lib/dispatching/feature_observers/{is_scheduled_observer.py → _is_scheduled_observer.py} +0 -0
  69. /job_shop_lib/generation/{transformations.py → _transformations.py} +0 -0
  70. /job_shop_lib/graphs/{build_agent_task_graph.py → _build_agent_task_graph.py} +0 -0
  71. /job_shop_lib/graphs/{build_disjunctive_graph.py → _build_disjunctive_graph.py} +0 -0
  72. /job_shop_lib/graphs/{constants.py → _constants.py} +0 -0
  73. /job_shop_lib/visualization/{disjunctive_graph.py → _disjunctive_graph.py} +0 -0
  74. /job_shop_lib/visualization/{gantt_chart.py → _gantt_chart.py} +0 -0
  75. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/LICENSE +0 -0
  76. {job_shop_lib-0.5.1.dist-info → job_shop_lib-1.0.0a1.dist-info}/WHEEL +0 -0
@@ -6,17 +6,22 @@ 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 Callable
9
+ from collections.abc import Callable, Sequence
10
10
  import random
11
11
 
12
12
  from job_shop_lib import Operation
13
- from job_shop_lib.dispatching import Dispatcher
13
+ from job_shop_lib.dispatching import Dispatcher, DispatcherObserver
14
+ from job_shop_lib.dispatching.feature_observers import (
15
+ DurationObserver,
16
+ FeatureType,
17
+ IsReadyObserver,
18
+ )
14
19
 
15
20
 
16
21
  def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
17
22
  """Dispatches the operation with the shortest duration."""
18
23
  return min(
19
- dispatcher.available_operations(),
24
+ dispatcher.ready_operations(),
20
25
  key=lambda operation: operation.duration,
21
26
  )
22
27
 
@@ -24,7 +29,7 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
24
29
  def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
25
30
  """Dispatches the operation with the lowest position in job."""
26
31
  return min(
27
- dispatcher.available_operations(),
32
+ dispatcher.ready_operations(),
28
33
  key=lambda operation: operation.position_in_job,
29
34
  )
30
35
 
@@ -32,11 +37,11 @@ def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
32
37
  def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
33
38
  """Dispatches the operation which job has the most remaining work."""
34
39
  job_remaining_work = [0] * dispatcher.instance.num_jobs
35
- for operation in dispatcher.uncompleted_operations():
40
+ for operation in dispatcher.unscheduled_operations():
36
41
  job_remaining_work[operation.job_id] += operation.duration
37
42
 
38
43
  return max(
39
- dispatcher.available_operations(),
44
+ dispatcher.ready_operations(),
40
45
  key=lambda operation: job_remaining_work[operation.job_id],
41
46
  )
42
47
 
@@ -48,18 +53,18 @@ def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
48
53
  job_remaining_operations[operation.job_id] += 1
49
54
 
50
55
  return max(
51
- dispatcher.available_operations(),
56
+ dispatcher.ready_operations(),
52
57
  key=lambda operation: job_remaining_operations[operation.job_id],
53
58
  )
54
59
 
55
60
 
56
61
  def random_operation_rule(dispatcher: Dispatcher) -> Operation:
57
62
  """Dispatches a random operation."""
58
- return random.choice(dispatcher.available_operations())
63
+ return random.choice(dispatcher.ready_operations())
59
64
 
60
65
 
61
66
  def score_based_rule(
62
- score_function: Callable[[Dispatcher], list[int]]
67
+ score_function: Callable[[Dispatcher], Sequence[float]]
63
68
  ) -> Callable[[Dispatcher], Operation]:
64
69
  """Creates a dispatching rule based on a scoring function.
65
70
 
@@ -75,7 +80,7 @@ def score_based_rule(
75
80
  def rule(dispatcher: Dispatcher) -> Operation:
76
81
  scores = score_function(dispatcher)
77
82
  return max(
78
- dispatcher.available_operations(),
83
+ dispatcher.ready_operations(),
79
84
  key=lambda operation: scores[operation.job_id],
80
85
  )
81
86
 
@@ -83,7 +88,7 @@ def score_based_rule(
83
88
 
84
89
 
85
90
  def score_based_rule_with_tie_breaker(
86
- score_functions: list[Callable[[Dispatcher], list[int]]],
91
+ score_functions: list[Callable[[Dispatcher], Sequence[int]]],
87
92
  ) -> Callable[[Dispatcher], Operation]:
88
93
  """Creates a dispatching rule based on multiple scoring functions.
89
94
 
@@ -97,7 +102,7 @@ def score_based_rule_with_tie_breaker(
97
102
  """
98
103
 
99
104
  def rule(dispatcher: Dispatcher) -> Operation:
100
- candidates = dispatcher.available_operations()
105
+ candidates = dispatcher.ready_operations()
101
106
  for scoring_function in score_functions:
102
107
  scores = scoring_function(dispatcher)
103
108
  best_score = max(scores)
@@ -121,7 +126,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
121
126
  """Scores each job based on the duration of the next operation."""
122
127
  num_jobs = dispatcher.instance.num_jobs
123
128
  scores = [0] * num_jobs
124
- for operation in dispatcher.available_operations():
129
+ for operation in dispatcher.ready_operations():
125
130
  scores[operation.job_id] = -operation.duration
126
131
  return scores
127
132
 
@@ -130,18 +135,66 @@ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
130
135
  """Scores each job based on the position of the next operation."""
131
136
  num_jobs = dispatcher.instance.num_jobs
132
137
  scores = [0] * num_jobs
133
- for operation in dispatcher.available_operations():
138
+ for operation in dispatcher.ready_operations():
134
139
  scores[operation.job_id] = operation.operation_id
135
140
  return scores
136
141
 
137
142
 
138
- def most_work_remaining_score(dispatcher: Dispatcher) -> list[int]:
139
- """Scores each job based on the remaining work in the job."""
140
- num_jobs = dispatcher.instance.num_jobs
141
- scores = [0] * num_jobs
142
- for operation in dispatcher.uncompleted_operations():
143
- scores[operation.job_id] += operation.duration
144
- return scores
143
+ class MostWorkRemainingScorer: # pylint: disable=too-few-public-methods
144
+ """Scores each job based on the remaining work in the job.
145
+
146
+ This class is conceptually a function: it can be called with a
147
+ :class:`~job_shop_lib.dispatching.Dispatcher` instance as input, and it
148
+ returns a list of scores for each job. The reason for using a class instead
149
+ of a function is to cache the observers that are created for each
150
+ dispatcher instance. This way, the observers do not have to be retrieved
151
+ every time the function is called.
152
+
153
+ """
154
+
155
+ 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
159
+
160
+ def __call__(self, dispatcher: Dispatcher) -> Sequence[int]:
161
+ """Scores each job based on the remaining work in the job."""
162
+
163
+ if self._current_dispatcher is not dispatcher:
164
+ self._duration_observer = None
165
+ self._is_ready_observer = None
166
+ self._current_dispatcher = dispatcher
167
+
168
+ def has_job_feature(observer: DispatcherObserver) -> bool:
169
+ if not isinstance(observer, DurationObserver):
170
+ return False
171
+ return FeatureType.JOBS in observer.features
172
+
173
+ if self._duration_observer is None:
174
+ self._duration_observer = dispatcher.create_or_get_observer(
175
+ DurationObserver,
176
+ condition=has_job_feature,
177
+ feature_types=FeatureType.JOBS,
178
+ )
179
+ if self._is_ready_observer is None:
180
+ self._is_ready_observer = dispatcher.create_or_get_observer(
181
+ IsReadyObserver,
182
+ condition=has_job_feature,
183
+ feature_types=FeatureType.JOBS,
184
+ )
185
+
186
+ work_remaining = self._duration_observer.features[
187
+ FeatureType.JOBS
188
+ ].copy()
189
+ is_ready = self._is_ready_observer.features[FeatureType.JOBS]
190
+ work_remaining[~is_ready.astype(bool)] = 0
191
+
192
+ return work_remaining.ravel() # type: ignore[return-value]
193
+
194
+
195
+ observer_based_most_work_remaining_rule = score_based_rule(
196
+ MostWorkRemainingScorer()
197
+ )
145
198
 
146
199
 
147
200
  def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
@@ -0,0 +1,69 @@
1
+ """Contains factory functions for creating dispatching rules, machine choosers,
2
+ and pruning functions for the job shop scheduling problem.
3
+
4
+ The factory functions create and return the appropriate functions based on the
5
+ specified names or enums.
6
+ """
7
+
8
+ from enum import Enum
9
+ from collections.abc import Callable
10
+ import random
11
+
12
+ from job_shop_lib import Operation
13
+ from job_shop_lib.exceptions import ValidationError
14
+ from job_shop_lib.dispatching import Dispatcher
15
+
16
+
17
+ class MachineChooserType(str, Enum):
18
+ """Enumeration of machine chooser strategies for the job shop scheduling"""
19
+
20
+ FIRST = "first"
21
+ RANDOM = "random"
22
+
23
+
24
+ MachineChooser = Callable[[Dispatcher, Operation], int]
25
+
26
+
27
+ def machine_chooser_factory(
28
+ machine_chooser: str | MachineChooser,
29
+ ) -> MachineChooser:
30
+ """Creates and returns a machine chooser function based on the specified
31
+ machine chooser strategy name.
32
+
33
+ The machine chooser function determines which machine an operation should
34
+ be assigned to for execution. The selection can be based on different
35
+ strategies such as choosing the first available machine or selecting a
36
+ machine randomly.
37
+
38
+ Args:
39
+ machine_chooser (str): The name of the machine chooser strategy to be
40
+ used. Supported values are 'first' and 'random'.
41
+
42
+ Returns:
43
+ A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
44
+ and an :class:`~job_shop_lib.Operation` as input
45
+ and returns the index of the selected machine based on the specified
46
+ machine chooser strategy.
47
+
48
+ Raises:
49
+ ValueError: If the machine_chooser argument is not recognized or is
50
+ not supported.
51
+ """
52
+ machine_choosers: dict[str, Callable[[Dispatcher, Operation], int]] = {
53
+ MachineChooserType.FIRST: lambda _, operation: operation.machines[0],
54
+ MachineChooserType.RANDOM: lambda _, operation: random.choice(
55
+ operation.machines
56
+ ),
57
+ }
58
+
59
+ if callable(machine_chooser):
60
+ return machine_chooser
61
+
62
+ machine_chooser = machine_chooser.lower()
63
+ if machine_chooser not in machine_choosers:
64
+ raise ValidationError(
65
+ f"Machine chooser {machine_chooser} not recognized. Available "
66
+ f"machine choosers: {', '.join(machine_choosers)}."
67
+ )
68
+
69
+ return machine_choosers[machine_chooser]
@@ -0,0 +1,127 @@
1
+ """Utility functions."""
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+ import pandas as pd
6
+ from job_shop_lib import JobShopInstance, Operation
7
+ from job_shop_lib.exceptions import JobShopLibError
8
+ from job_shop_lib.dispatching.rules import DispatchingRuleSolver
9
+ from job_shop_lib.dispatching import Dispatcher
10
+
11
+
12
+ 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],
19
+ ) -> pd.DataFrame:
20
+ """Benchmark multiple dispatching rules on multiple JobShopInstances.
21
+
22
+ This function applies each provided dispatching rule to each given
23
+ :class:`JobShopInstance`, measuring the time taken to solve and the
24
+ makespan of the resulting schedule. It returns a DataFrame summarizing
25
+ the results.
26
+
27
+ Args:
28
+ dispatching_rules:
29
+ List of dispatching rules. Each rule can be
30
+ either a string (name of a built-in rule) or a callable
31
+ (custom rule function).
32
+ instances:
33
+ iList of :class:`JobShopInstance` objects to be solved.
34
+
35
+ Returns:
36
+ A pandas DataFrame with columns:
37
+ - instance: Name of the :class:`JobShopInstance`.
38
+ - rule: Name of the dispatching rule used.
39
+ - time: Time taken to solve the instance (in seconds).
40
+ - makespan: Makespan of the resulting schedule.
41
+
42
+ Raises:
43
+ Any exception that might occur during the solving process is caught
44
+ and logged, with None values recorded for time and makespan.
45
+
46
+ Example:
47
+ >>> from job_shop_lib.benchmarking import load_benchmark_instance
48
+ >>> instances = [load_benchmark_instance(f"ta{i:02d}")
49
+ ... for i in range(1, 3)]
50
+ >>> rules = ["most_work_remaining", "shortest_processing_time"]
51
+ >>> df = benchmark_dispatching_rules(rules, instances)
52
+ >>> print(df)
53
+ instance rule time makespan
54
+ 0 ta01 shortest_processing_time 0.006492 3439
55
+ 1 ta01 most_work_remaining_rule 0.012608 1583
56
+ 2 ta02 shortest_processing_time 0.006240 2568
57
+ 3 ta02 most_work_remaining_rule 0.012315 1630
58
+
59
+ Note:
60
+ - The function handles errors gracefully, allowing the benchmarking
61
+ process to continue even if solving a particular instance fails.
62
+ - For custom rule functions, the function name is used in the
63
+ 'rule' column of the output DataFrame.
64
+ """
65
+ results = []
66
+
67
+ for instance in instances:
68
+ for rule in dispatching_rules:
69
+ solver = DispatchingRuleSolver(dispatching_rule=rule)
70
+
71
+ start_time = time.perf_counter()
72
+ try:
73
+ schedule = solver.solve(instance)
74
+ solve_time = time.perf_counter() - start_time
75
+ makespan = schedule.makespan()
76
+
77
+ results.append(
78
+ {
79
+ "instance": instance.name,
80
+ "rule": (
81
+ rule if isinstance(rule, str) else rule.__name__
82
+ ),
83
+ "time": solve_time,
84
+ "makespan": makespan,
85
+ }
86
+ )
87
+ except JobShopLibError as e:
88
+ print(f"Error solving {instance.name} with {rule}: {str(e)}")
89
+ results.append(
90
+ {
91
+ "instance": instance.name,
92
+ "rule": (
93
+ rule if isinstance(rule, str) else rule.__name__
94
+ ),
95
+ "time": None,
96
+ "makespan": None,
97
+ }
98
+ )
99
+
100
+ return pd.DataFrame(results)
101
+
102
+
103
+ # Example usage:
104
+ if __name__ == "__main__":
105
+ from job_shop_lib.benchmarking import load_benchmark_instance
106
+ from job_shop_lib.dispatching.rules._dispatching_rules_functions import (
107
+ most_work_remaining_rule,
108
+ )
109
+
110
+ # Load instances
111
+ instances_ = [load_benchmark_instance(f"ta{i:02d}") for i in range(1, 3)]
112
+
113
+ # Define rules
114
+ rules_: list[str | Callable[[Dispatcher], Operation]] = [
115
+ "most_work_remaining",
116
+ "shortest_processing_time",
117
+ most_work_remaining_rule,
118
+ ]
119
+
120
+ # Run benchmark
121
+ df = benchmark_dispatching_rules(rules_, instances_)
122
+
123
+ # Display results
124
+ print(df)
125
+
126
+ # Group results by rule and compute average makespan and time
127
+ print(df.groupby("rule")[["time", "makespan"]].mean())
@@ -24,3 +24,21 @@ class NoSolutionFoundError(JobShopLibError):
24
24
  TypeError, which may indicate a bug in the code or an invalid
25
25
  input, rather than a failure to find a solution.
26
26
  """
27
+
28
+
29
+ class ValidationError(JobShopLibError):
30
+ """Exception raised when a validation check fails.
31
+
32
+ This exception is raised when a validation check fails, indicating
33
+ that the input data is invalid or does not meet the requirements of
34
+ the function or class that is performing the validation.
35
+
36
+ It is useful to distinguish this exception from other exceptions
37
+ that may be raised by a function or class, such as a ValueError or
38
+ a TypeError, which may indicate a bug in the code or an invalid
39
+ input, rather than a validation failure.
40
+ """
41
+
42
+
43
+ class UninitializedAttributeError(JobShopLibError):
44
+ """Exception raised when an attribute is accessed before initialization."""
@@ -1,7 +1,7 @@
1
1
  """Package for generating job shop instances."""
2
2
 
3
- from job_shop_lib.generation.instance_generator import InstanceGenerator
4
- from job_shop_lib.generation.general_instance_generator import (
3
+ from job_shop_lib.generation._instance_generator import InstanceGenerator
4
+ from job_shop_lib.generation._general_instance_generator import (
5
5
  GeneralInstanceGenerator,
6
6
  )
7
7
 
@@ -3,6 +3,7 @@
3
3
  import random
4
4
 
5
5
  from job_shop_lib import JobShopInstance, Operation
6
+ from job_shop_lib.exceptions import ValidationError
6
7
  from job_shop_lib.generation import InstanceGenerator
7
8
 
8
9
 
@@ -105,14 +106,32 @@ class GeneralInstanceGenerator(InstanceGenerator):
105
106
  if seed is not None:
106
107
  random.seed(seed)
107
108
 
108
- def generate(self) -> JobShopInstance:
109
- """Generates a single job shop instance"""
110
- num_jobs = random.randint(*self.num_jobs_range)
109
+ def __repr__(self) -> str:
110
+ return (
111
+ f"GeneralInstanceGenerator("
112
+ f"num_jobs_range={self.num_jobs_range}, "
113
+ f"num_machines_range={self.num_machines_range}, "
114
+ f"duration_range={self.duration_range})"
115
+ )
111
116
 
112
- min_num_machines, max_num_machines = self.num_machines_range
113
- if not self.allow_less_jobs_than_machines:
114
- min_num_machines = min(num_jobs, max_num_machines)
115
- num_machines = random.randint(min_num_machines, max_num_machines)
117
+ def generate(
118
+ self, num_jobs: int | None = None, num_machines: int | None = None
119
+ ) -> JobShopInstance:
120
+ if num_jobs is None:
121
+ num_jobs = random.randint(*self.num_jobs_range)
122
+
123
+ if num_machines is None:
124
+ min_num_machines, max_num_machines = self.num_machines_range
125
+ if not self.allow_less_jobs_than_machines:
126
+ min_num_machines = min(num_jobs, max_num_machines)
127
+ num_machines = random.randint(min_num_machines, max_num_machines)
128
+ elif (
129
+ not self.allow_less_jobs_than_machines and num_jobs < num_machines
130
+ ):
131
+ raise ValidationError(
132
+ "Theere are fewer jobs than machines, which is not allowed"
133
+ "when `allow_less_jobs_than_machines` attribute is False."
134
+ )
116
135
 
117
136
  jobs = []
118
137
  available_machines = list(range(num_machines))
@@ -4,6 +4,7 @@ import random
4
4
  from typing import Iterator
5
5
 
6
6
  from job_shop_lib import JobShopInstance
7
+ from job_shop_lib.exceptions import UninitializedAttributeError
7
8
 
8
9
 
9
10
  class InstanceGenerator(abc.ABC):
@@ -76,8 +77,17 @@ class InstanceGenerator(abc.ABC):
76
77
  self._iteration_limit = iteration_limit
77
78
 
78
79
  @abc.abstractmethod
79
- def generate(self) -> JobShopInstance:
80
- """Generates a single job shop instance"""
80
+ def generate(
81
+ self, num_jobs: int | None = None, num_machines: int | None = None
82
+ ) -> JobShopInstance:
83
+ """Generates a single job shop instance
84
+
85
+ Args:
86
+ num_jobs: The number of jobs to generate. If None, a random value
87
+ within the specified range will be used.
88
+ num_machines: The number of machines to generate. If None, a random
89
+ value within the specified range will be used.
90
+ """
81
91
 
82
92
  def _next_name(self) -> str:
83
93
  self._counter += 1
@@ -98,7 +108,7 @@ class InstanceGenerator(abc.ABC):
98
108
 
99
109
  def __len__(self) -> int:
100
110
  if self._iteration_limit is None:
101
- raise ValueError("Iteration limit is not set.")
111
+ raise UninitializedAttributeError("Iteration limit is not set.")
102
112
  return self._iteration_limit
103
113
 
104
114
  @property
@@ -1,16 +1,27 @@
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
+ .. autosummary::
4
+ JobShopGraph
5
+ Node
6
+ NodeType
7
+ build_disjunctive_graph
8
+ build_agent_task_graph
9
+ build_complete_agent_task_graph
10
+ build_agent_task_graph_with_jobs
11
+
12
+ """
13
+
14
+ from job_shop_lib.graphs._constants import EdgeType, NodeType
15
+ from job_shop_lib.graphs._node import Node
16
+ from job_shop_lib.graphs._job_shop_graph import JobShopGraph, NODE_ATTR
17
+ from job_shop_lib.graphs._build_disjunctive_graph import (
7
18
  build_disjunctive_graph,
8
19
  add_disjunctive_edges,
9
20
  add_conjunctive_edges,
10
21
  add_source_sink_nodes,
11
22
  add_source_sink_edges,
12
23
  )
13
- from job_shop_lib.graphs.build_agent_task_graph import (
24
+ from job_shop_lib.graphs._build_agent_task_graph import (
14
25
  build_agent_task_graph,
15
26
  build_complete_agent_task_graph,
16
27
  build_agent_task_graph_with_jobs,
@@ -3,7 +3,8 @@
3
3
  import collections
4
4
  import networkx as nx
5
5
 
6
- from job_shop_lib import JobShopInstance, JobShopLibError
6
+ from job_shop_lib import JobShopInstance
7
+ from job_shop_lib.exceptions import ValidationError
7
8
  from job_shop_lib.graphs import Node, NodeType
8
9
 
9
10
 
@@ -168,7 +169,7 @@ class JobShopGraph:
168
169
  if isinstance(v_of_edge, Node):
169
170
  v_of_edge = v_of_edge.node_id
170
171
  if u_of_edge not in self.graph or v_of_edge not in self.graph:
171
- raise JobShopLibError(
172
+ raise ValidationError(
172
173
  "`u_of_edge` and `v_of_edge` must be in the graph."
173
174
  )
174
175
  self.graph.add_edge(u_of_edge, v_of_edge, **attr)
@@ -200,3 +201,81 @@ class JobShopGraph:
200
201
  if isinstance(node, Node):
201
202
  node = node.node_id
202
203
  return self.removed_nodes[node]
204
+
205
+ def non_removed_nodes(self) -> list[Node]:
206
+ """Returns the nodes that are not removed from the graph."""
207
+ return [node for node in self._nodes if not self.is_removed(node)]
208
+
209
+ def get_machine_node(self, machine_id: int) -> Node:
210
+ """Returns the node representing the machine with the given id.
211
+
212
+ Args:
213
+ machine_id: The id of the machine.
214
+
215
+ Returns:
216
+ The node representing the machine with the given id.
217
+ """
218
+ return self.get_node_by_type_and_id(
219
+ NodeType.MACHINE, machine_id, "machine_id"
220
+ )
221
+
222
+ def get_job_node(self, job_id: int) -> Node:
223
+ """Returns the node representing the job with the given id.
224
+
225
+ Args:
226
+ job_id: The id of the job.
227
+
228
+ Returns:
229
+ The node representing the job with the given id.
230
+ """
231
+ return self.get_node_by_type_and_id(NodeType.JOB, job_id, "job_id")
232
+
233
+ def get_operation_node(self, operation_id: int) -> Node:
234
+ """Returns the node representing the operation with the given id.
235
+
236
+ Args:
237
+ operation_id: The id of the operation.
238
+
239
+ Returns:
240
+ The node representing the operation with the given id.
241
+ """
242
+ return self.get_node_by_type_and_id(
243
+ NodeType.OPERATION, operation_id, "operation.operation_id"
244
+ )
245
+
246
+ def get_node_by_type_and_id(
247
+ self, node_type: NodeType, node_id: int, id_attr: str
248
+ ) -> Node:
249
+ """Generic method to get a node by type and id.
250
+
251
+ Args:
252
+ node_type:
253
+ The type of the node.
254
+ node_id:
255
+ The id of the node.
256
+ id_attr:
257
+ The attribute name to compare the id. Can be nested like
258
+ 'operation.operation_id'.
259
+
260
+ Returns:
261
+ The node with the given id.
262
+ """
263
+
264
+ def get_nested_attr(obj, attr_path: str):
265
+ """Helper function to get nested attribute."""
266
+ attrs = attr_path.split(".")
267
+ for attr in attrs:
268
+ obj = getattr(obj, attr)
269
+ return obj
270
+
271
+ nodes = self._nodes_by_type[node_type]
272
+ if node_id < len(nodes):
273
+ node = nodes[node_id]
274
+ if get_nested_attr(node, id_attr) == node_id:
275
+ return node
276
+
277
+ for node in nodes:
278
+ if get_nested_attr(node, id_attr) == node_id:
279
+ return node
280
+
281
+ raise ValidationError(f"No node found with node.{id_attr}={node_id}")