job-shop-lib 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. job_shop_lib/__init__.py +20 -0
  2. job_shop_lib/base_solver.py +37 -0
  3. job_shop_lib/benchmarking/__init__.py +78 -0
  4. job_shop_lib/benchmarking/benchmark_instances.json +1 -0
  5. job_shop_lib/benchmarking/load_benchmark.py +142 -0
  6. job_shop_lib/cp_sat/__init__.py +5 -0
  7. job_shop_lib/cp_sat/ortools_solver.py +201 -0
  8. job_shop_lib/dispatching/__init__.py +49 -0
  9. job_shop_lib/dispatching/dispatcher.py +269 -0
  10. job_shop_lib/dispatching/dispatching_rule_solver.py +111 -0
  11. job_shop_lib/dispatching/dispatching_rules.py +160 -0
  12. job_shop_lib/dispatching/factories.py +206 -0
  13. job_shop_lib/dispatching/pruning_functions.py +116 -0
  14. job_shop_lib/exceptions.py +26 -0
  15. job_shop_lib/generators/__init__.py +7 -0
  16. job_shop_lib/generators/basic_generator.py +197 -0
  17. job_shop_lib/graphs/__init__.py +52 -0
  18. job_shop_lib/graphs/build_agent_task_graph.py +209 -0
  19. job_shop_lib/graphs/build_disjunctive_graph.py +78 -0
  20. job_shop_lib/graphs/constants.py +21 -0
  21. job_shop_lib/graphs/job_shop_graph.py +159 -0
  22. job_shop_lib/graphs/node.py +147 -0
  23. job_shop_lib/job_shop_instance.py +355 -0
  24. job_shop_lib/operation.py +120 -0
  25. job_shop_lib/schedule.py +180 -0
  26. job_shop_lib/scheduled_operation.py +97 -0
  27. job_shop_lib/visualization/__init__.py +25 -0
  28. job_shop_lib/visualization/agent_task_graph.py +257 -0
  29. job_shop_lib/visualization/create_gif.py +191 -0
  30. job_shop_lib/visualization/disjunctive_graph.py +206 -0
  31. job_shop_lib/visualization/gantt_chart.py +147 -0
  32. job_shop_lib-0.1.0.dist-info/LICENSE +21 -0
  33. job_shop_lib-0.1.0.dist-info/METADATA +363 -0
  34. job_shop_lib-0.1.0.dist-info/RECORD +35 -0
  35. job_shop_lib-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,111 @@
1
+ """Home of the `DispatchingRuleSolver` class."""
2
+
3
+ from collections.abc import Callable
4
+
5
+ from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
6
+ from job_shop_lib.dispatching import (
7
+ dispatching_rule_factory,
8
+ machine_chooser_factory,
9
+ pruning_function_factory,
10
+ DispatchingRule,
11
+ MachineChooser,
12
+ Dispatcher,
13
+ PruningFunction,
14
+ )
15
+
16
+
17
+ class DispatchingRuleSolver(BaseSolver):
18
+ """Solves a job shop instance using a dispatching rule.
19
+
20
+ Attributes:
21
+ dispatching_rule:
22
+ The dispatching rule to use. It is a callable that takes a
23
+ dispatcher and returns the operation to be dispatched next.
24
+ machine_chooser:
25
+ Used to choose the machine where the operation will be dispatched
26
+ to. It is only used if the operation can be dispatched to multiple
27
+ machines.
28
+ pruning_function:
29
+ The pruning function to use. It is used to initialize the
30
+ dispatcher object internally when calling the solve method.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ dispatching_rule: (
36
+ str | Callable[[Dispatcher], Operation]
37
+ ) = DispatchingRule.MOST_WORK_REMAINING,
38
+ machine_chooser: (
39
+ str | Callable[[Dispatcher, Operation], int]
40
+ ) = MachineChooser.FIRST,
41
+ pruning_function: (
42
+ str
43
+ | Callable[[Dispatcher, list[Operation]], list[Operation]]
44
+ | None
45
+ ) = PruningFunction.DOMINATED_OPERATIONS,
46
+ ):
47
+ """Initializes the solver with the given dispatching rule, machine
48
+ chooser and pruning function.
49
+
50
+ Args:
51
+ dispatching_rule:
52
+ The dispatching rule to use. It can be a string with the name
53
+ of the dispatching rule, a DispatchingRule enum member, or a
54
+ callable that takes a dispatcher and returns the operation to
55
+ be dispatched next.
56
+ machine_chooser:
57
+ The machine chooser to use. It can be a string with the name
58
+ of the machine chooser, a MachineChooser enum member, or a
59
+ callable that takes a dispatcher and an operation and returns
60
+ the machine id where the operation will be dispatched.
61
+ pruning_function:
62
+ The pruning function to use. It can be a string with the name
63
+ of the pruning function, a PruningFunction enum member, or a
64
+ callable that takes a dispatcher and a list of operations and
65
+ returns a list of operations that should be considered for
66
+ dispatching.
67
+ """
68
+ if isinstance(dispatching_rule, str):
69
+ dispatching_rule = dispatching_rule_factory(dispatching_rule)
70
+ if isinstance(machine_chooser, str):
71
+ machine_chooser = machine_chooser_factory(machine_chooser)
72
+ if isinstance(pruning_function, str):
73
+ pruning_function = pruning_function_factory(pruning_function)
74
+
75
+ self.dispatching_rule = dispatching_rule
76
+ self.machine_chooser = machine_chooser
77
+ self.pruning_function = pruning_function
78
+
79
+ def solve(self, instance: JobShopInstance) -> Schedule:
80
+ """Returns a schedule for the given job shop instance using the
81
+ dispatching rule algorithm."""
82
+ dispatcher = Dispatcher(
83
+ instance, pruning_function=self.pruning_function
84
+ )
85
+ while not dispatcher.schedule.is_complete():
86
+ self.step(dispatcher)
87
+
88
+ return dispatcher.schedule
89
+
90
+ def step(self, dispatcher: Dispatcher) -> None:
91
+ """Executes one step of the dispatching rule algorithm.
92
+
93
+ Args:
94
+ dispatcher:
95
+ The dispatcher object that will be used to dispatch the
96
+ operations.
97
+ """
98
+ selected_operation = self.dispatching_rule(dispatcher)
99
+ machine_id = self.machine_chooser(dispatcher, selected_operation)
100
+ dispatcher.dispatch(selected_operation, machine_id)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ import cProfile
105
+ from job_shop_lib.benchmarking import load_benchmark_instance
106
+
107
+ ta_instances = []
108
+ for i in range(1, 81):
109
+ ta_instances.append(load_benchmark_instance(f"ta{i:02d}"))
110
+ solver = DispatchingRuleSolver(dispatching_rule="most_work_remaining")
111
+ cProfile.run("for instance in ta_instances: solver.solve(instance)")
@@ -0,0 +1,160 @@
1
+ """Dispatching rules for the job shop scheduling problem.
2
+
3
+ This module contains functions that implement different dispatching rules for
4
+ the job shop scheduling problem. A dispatching rule determines the order in
5
+ which operations are selected for execution based on certain criteria such as
6
+ shortest processing time, first come first served, etc.
7
+ """
8
+
9
+ from typing import Callable
10
+ import random
11
+
12
+ from job_shop_lib import Operation
13
+ from job_shop_lib.dispatching import Dispatcher
14
+
15
+
16
+ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
17
+ """Dispatches the operation with the shortest duration."""
18
+ return min(
19
+ dispatcher.available_operations(),
20
+ key=lambda operation: operation.duration,
21
+ )
22
+
23
+
24
+ def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
25
+ """Dispatches the operation with the lowest position in job."""
26
+ return min(
27
+ dispatcher.available_operations(),
28
+ key=lambda operation: operation.position_in_job,
29
+ )
30
+
31
+
32
+ def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
33
+ """Dispatches the operation which job has the most remaining work."""
34
+ job_remaining_work = [0] * dispatcher.instance.num_jobs
35
+ for operation in dispatcher.uncompleted_operations():
36
+ job_remaining_work[operation.job_id] += operation.duration
37
+
38
+ return max(
39
+ dispatcher.available_operations(),
40
+ key=lambda operation: job_remaining_work[operation.job_id],
41
+ )
42
+
43
+
44
+ def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
45
+ """Dispatches the operation which job has the most remaining operations."""
46
+ job_remaining_operations = [0] * dispatcher.instance.num_jobs
47
+ for operation in dispatcher.uncompleted_operations():
48
+ job_remaining_operations[operation.job_id] += 1
49
+
50
+ return max(
51
+ dispatcher.available_operations(),
52
+ key=lambda operation: job_remaining_operations[operation.job_id],
53
+ )
54
+
55
+
56
+ def random_operation_rule(dispatcher: Dispatcher) -> Operation:
57
+ """Dispatches a random operation."""
58
+ return random.choice(dispatcher.available_operations())
59
+
60
+
61
+ def score_based_rule(
62
+ score_function: Callable[[Dispatcher], list[int]]
63
+ ) -> Callable[[Dispatcher], Operation]:
64
+ """Creates a dispatching rule based on a scoring function.
65
+
66
+ Args:
67
+ score_function: A function that takes a Dispatcher instance as input
68
+ and returns a list of scores for each job.
69
+
70
+ Returns:
71
+ A dispatching rule function that selects the operation with the highest
72
+ score based on the specified scoring function.
73
+ """
74
+
75
+ def rule(dispatcher: Dispatcher) -> Operation:
76
+ scores = score_function(dispatcher)
77
+ return max(
78
+ dispatcher.available_operations(),
79
+ key=lambda operation: scores[operation.job_id],
80
+ )
81
+
82
+ return rule
83
+
84
+
85
+ def score_based_rule_with_tie_breaker(
86
+ score_functions: list[Callable[[Dispatcher], list[int]]],
87
+ ) -> Callable[[Dispatcher], Operation]:
88
+ """Creates a dispatching rule based on multiple scoring functions.
89
+
90
+ If there is a tie between two operations based on the first scoring
91
+ function, the second scoring function is used as a tie breaker. If there is
92
+ still a tie, the third scoring function is used, and so on.
93
+
94
+ Args:
95
+ score_functions: A list of scoring functions that take a Dispatcher
96
+ instance as input and return a list of scores for each job.
97
+ """
98
+
99
+ def rule(dispatcher: Dispatcher) -> Operation:
100
+ candidates = dispatcher.available_operations()
101
+ for scoring_function in score_functions:
102
+ scores = scoring_function(dispatcher)
103
+ best_score = max(scores)
104
+ candidates = [
105
+ operation
106
+ for operation in candidates
107
+ if scores[operation.job_id] == best_score
108
+ ]
109
+ if len(candidates) == 1:
110
+ return candidates[0]
111
+ return candidates[0]
112
+
113
+ return rule
114
+
115
+
116
+ # SCORING FUNCTIONS
117
+ # -----------------
118
+
119
+
120
+ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
121
+ """Scores each job based on the duration of the next operation."""
122
+ num_jobs = dispatcher.instance.num_jobs
123
+ scores = [0] * num_jobs
124
+ for operation in dispatcher.available_operations():
125
+ scores[operation.job_id] = -operation.duration
126
+ return scores
127
+
128
+
129
+ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
130
+ """Scores each job based on the position of the next operation."""
131
+ num_jobs = dispatcher.instance.num_jobs
132
+ scores = [0] * num_jobs
133
+ for operation in dispatcher.available_operations():
134
+ scores[operation.job_id] = operation.operation_id
135
+ return scores
136
+
137
+
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
145
+
146
+
147
+ def most_operations_remaining_score(dispatcher: Dispatcher) -> list[int]:
148
+ """Scores each job based on the remaining operations in the job."""
149
+ num_jobs = dispatcher.instance.num_jobs
150
+ scores = [0] * num_jobs
151
+ for operation in dispatcher.uncompleted_operations():
152
+ scores[operation.job_id] += 1
153
+ return scores
154
+
155
+
156
+ def random_score(dispatcher: Dispatcher) -> list[int]:
157
+ """Scores each job randomly."""
158
+ return [
159
+ random.randint(0, 100) for _ in range(dispatcher.instance.num_jobs)
160
+ ]
@@ -0,0 +1,206 @@
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
+
10
+ from collections.abc import Callable, Sequence
11
+ import random
12
+
13
+ from job_shop_lib import Operation
14
+ from job_shop_lib.dispatching import (
15
+ shortest_processing_time_rule,
16
+ first_come_first_served_rule,
17
+ most_work_remaining_rule,
18
+ most_operations_remaining_rule,
19
+ random_operation_rule,
20
+ Dispatcher,
21
+ prune_dominated_operations,
22
+ prune_non_immediate_machines,
23
+ create_composite_pruning_function,
24
+ )
25
+
26
+
27
+ class DispatchingRule(str, Enum):
28
+ """Enumeration of dispatching rules for the job shop scheduling problem."""
29
+
30
+ SHORTEST_PROCESSING_TIME = "shortest_processing_time"
31
+ FIRST_COME_FIRST_SERVED = "first_come_first_served"
32
+ MOST_WORK_REMAINING = "most_work_remaining"
33
+ MOST_OPERATIONS_REMAINING = "most_operations_remaining"
34
+ RANDOM = "random"
35
+
36
+
37
+ class MachineChooser(str, Enum):
38
+ """Enumeration of machine chooser strategies for the job shop scheduling"""
39
+
40
+ FIRST = "first"
41
+ RANDOM = "random"
42
+
43
+
44
+ class PruningFunction(str, Enum):
45
+ """Enumeration of pruning functions.
46
+
47
+ A pruning function is used by the `Dispatcher` class to reduce the
48
+ amount of available operations to choose from.
49
+ """
50
+
51
+ DOMINATED_OPERATIONS = "dominated_operations"
52
+ NON_IMMEDIATE_MACHINES = "non_immediate_machines"
53
+
54
+
55
+ def dispatching_rule_factory(
56
+ dispatching_rule: str | DispatchingRule,
57
+ ) -> Callable[[Dispatcher], Operation]:
58
+ """Creates and returns a dispatching rule function based on the specified
59
+ dispatching rule name.
60
+
61
+ The dispatching rule function determines the order in which operations are
62
+ selected for execution based on certain criteria such as shortest
63
+ processing time, first come first served, etc.
64
+
65
+ Args:
66
+ dispatching_rule: The name of the dispatching rule to be used.
67
+ Supported values are 'shortest_processing_time',
68
+ 'first_come_first_served', 'most_work_remaining',
69
+ and 'random'.
70
+
71
+ Returns:
72
+ A function that takes a Dispatcher instance as input and returns an
73
+ Operation based on the specified dispatching rule.
74
+
75
+ Raises:
76
+ ValueError: If the dispatching_rule argument is not recognized or is
77
+ not supported.
78
+ """
79
+ dispatching_rules = {
80
+ DispatchingRule.SHORTEST_PROCESSING_TIME: (
81
+ shortest_processing_time_rule
82
+ ),
83
+ DispatchingRule.FIRST_COME_FIRST_SERVED: first_come_first_served_rule,
84
+ DispatchingRule.MOST_WORK_REMAINING: most_work_remaining_rule,
85
+ DispatchingRule.MOST_OPERATIONS_REMAINING: (
86
+ most_operations_remaining_rule
87
+ ),
88
+ DispatchingRule.RANDOM: random_operation_rule,
89
+ }
90
+
91
+ dispatching_rule = dispatching_rule.lower()
92
+ if dispatching_rule not in dispatching_rules:
93
+ raise ValueError(
94
+ f"Dispatching rule {dispatching_rule} not recognized. Available "
95
+ f"dispatching rules: {', '.join(dispatching_rules)}."
96
+ )
97
+
98
+ return dispatching_rules[dispatching_rule] # type: ignore[index]
99
+
100
+
101
+ def machine_chooser_factory(
102
+ machine_chooser: str,
103
+ ) -> Callable[[Dispatcher, Operation], int]:
104
+ """Creates and returns a machine chooser function based on the specified
105
+ machine chooser strategy name.
106
+
107
+ The machine chooser function determines which machine an operation should
108
+ be assigned to for execution. The selection can be based on different
109
+ strategies such as choosing the first available machine or selecting a
110
+ machine randomly.
111
+
112
+ Args:
113
+ machine_chooser (str): The name of the machine chooser strategy to be
114
+ used. Supported values are 'first' and 'random'.
115
+
116
+ Returns:
117
+ A function that takes a Dispatcher instance and an Operation as input
118
+ and returns the index of the selected machine based on the specified
119
+ machine chooser strategy.
120
+
121
+ Raises:
122
+ ValueError: If the machine_chooser argument is not recognized or is
123
+ not supported.
124
+ """
125
+ machine_choosers: dict[str, Callable[[Dispatcher, Operation], int]] = {
126
+ MachineChooser.FIRST: lambda _, operation: operation.machines[0],
127
+ MachineChooser.RANDOM: lambda _, operation: random.choice(
128
+ operation.machines
129
+ ),
130
+ }
131
+
132
+ machine_chooser = machine_chooser.lower()
133
+ if machine_chooser not in machine_choosers:
134
+ raise ValueError(
135
+ f"Machine chooser {machine_chooser} not recognized. Available "
136
+ f"machine choosers: {', '.join(machine_choosers)}."
137
+ )
138
+
139
+ return machine_choosers[machine_chooser]
140
+
141
+
142
+ def composite_pruning_function_factory(
143
+ pruning_function_names: Sequence[str | PruningFunction],
144
+ ) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
145
+ """Creates and returns a composite pruning function based on the
146
+ specified list of pruning strategies.
147
+
148
+ The composite pruning function filters out operations based on
149
+ the specified list of pruning strategies.
150
+
151
+ Args:
152
+ pruning_functions:
153
+ A list of pruning strategies to be used. Supported values are
154
+ 'dominated_operations' and 'non_immediate_machines'.
155
+
156
+ Returns:
157
+ A function that takes a Dispatcher instance and a list of Operation
158
+ instances as input and returns a list of Operation instances based on
159
+ the specified list of pruning strategies.
160
+
161
+ Raises:
162
+ ValueError: If any of the pruning strategies in the list are not
163
+ recognized or are not supported.
164
+ """
165
+
166
+ pruning_functions = [
167
+ pruning_function_factory(name) for name in pruning_function_names
168
+ ]
169
+ return create_composite_pruning_function(pruning_functions)
170
+
171
+
172
+ def pruning_function_factory(
173
+ pruning_function_name: str | PruningFunction,
174
+ ) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
175
+ """Creates and returns a pruning function based on the specified
176
+ pruning strategy name.
177
+
178
+ The pruning function filters out operations based on certain
179
+ criteria such as dominated operations, non-immediate machines, etc.
180
+
181
+ Args:
182
+ pruning_function:
183
+ The name of the pruning function to be used. Supported values are
184
+ 'dominated_operations' and 'non_immediate_machines'.
185
+
186
+ Returns:
187
+ A function that takes a Dispatcher instance and a list of Operation
188
+ instances as input and returns a list of Operation instances based on
189
+ the specified pruning function.
190
+
191
+ Raises:
192
+ ValueError: If the pruning_function argument is not recognized or is
193
+ not supported.
194
+ """
195
+ pruning_strategies = {
196
+ PruningFunction.DOMINATED_OPERATIONS: prune_dominated_operations,
197
+ PruningFunction.NON_IMMEDIATE_MACHINES: prune_non_immediate_machines,
198
+ }
199
+
200
+ if pruning_function_name not in pruning_strategies:
201
+ raise ValueError(
202
+ f"Unsupported pruning function '{pruning_function_name}'. "
203
+ f"Supported values are {', '.join(pruning_strategies.keys())}."
204
+ )
205
+
206
+ return pruning_strategies[pruning_function_name] # type: ignore[index]
@@ -0,0 +1,116 @@
1
+ """Contains functions to prune (filter) operations.
2
+
3
+ This functions are used by the `Dispatcher` class to reduce the
4
+ amount of available operations to choose from.
5
+ """
6
+
7
+ from collections.abc import Callable, Iterable
8
+
9
+ from job_shop_lib import Operation
10
+ from job_shop_lib.dispatching import Dispatcher
11
+
12
+
13
+ def create_composite_pruning_function(
14
+ pruning_functions: Iterable[
15
+ Callable[[Dispatcher, list[Operation]], list[Operation]]
16
+ ],
17
+ ) -> Callable[[Dispatcher, list[Operation]], list[Operation]]:
18
+ """Creates and returns a composite pruning strategy function based on the
19
+ specified list of pruning strategies.
20
+ The composite pruning strategy function filters out operations based on
21
+ the specified list of pruning strategies.
22
+ Args:
23
+ pruning_strategies:
24
+ A list of pruning strategies to be used. Supported values are
25
+ 'dominated_operations' and 'non_immediate_machines'.
26
+ Returns:
27
+ A function that takes a Dispatcher instance and a list of Operation
28
+ instances as input and returns a list of Operation instances based on
29
+ the specified list of pruning strategies.
30
+ Raises:
31
+ ValueError: If any of the pruning strategies in the list are not
32
+ recognized or are not supported.
33
+ """
34
+
35
+ def composite_pruning_function(
36
+ dispatcher: Dispatcher, operations: list[Operation]
37
+ ) -> list[Operation]:
38
+ pruned_operations = operations
39
+ for pruning_function in pruning_functions:
40
+ pruned_operations = pruning_function(dispatcher, pruned_operations)
41
+
42
+ return pruned_operations
43
+
44
+ return composite_pruning_function
45
+
46
+
47
+ def prune_dominated_operations(
48
+ dispatcher: Dispatcher, operations: list[Operation]
49
+ ) -> list[Operation]:
50
+ """Filters out all the operations that are dominated.
51
+ An operation is dominated if there is another operation that ends before
52
+ it starts on the same machine.
53
+ """
54
+
55
+ min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
56
+
57
+ non_dominated_operations: list[Operation] = []
58
+ for operation in operations:
59
+ # One benchmark instance has an operation with duration 0
60
+ if operation.duration == 0:
61
+ return [operation]
62
+ for machine_id in operation.machines:
63
+ start_time = dispatcher.start_time(operation, machine_id)
64
+ is_dominated = start_time >= min_machine_end_times[machine_id]
65
+ if not is_dominated:
66
+ non_dominated_operations.append(operation)
67
+ break
68
+
69
+ return non_dominated_operations
70
+
71
+
72
+ def prune_non_immediate_machines(
73
+ dispatcher: Dispatcher, operations: list[Operation]
74
+ ) -> list[Operation]:
75
+ """Filters out all the operations associated with machines which earliest
76
+ operation is not the current time."""
77
+
78
+ is_immediate_machine = _get_immediate_machines(dispatcher, operations)
79
+ non_dominated_operations: list[Operation] = []
80
+ for operation in operations:
81
+ if any(
82
+ is_immediate_machine[machine_id]
83
+ for machine_id in operation.machines
84
+ ):
85
+ non_dominated_operations.append(operation)
86
+
87
+ return non_dominated_operations
88
+
89
+
90
+ def _get_min_machine_end_times(
91
+ dispatcher: Dispatcher, available_operations: list[Operation]
92
+ ) -> list[int | float]:
93
+ end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
94
+ for op in available_operations:
95
+ for machine_id in op.machines:
96
+ start_time = dispatcher.start_time(op, machine_id)
97
+ end_times_per_machine[machine_id] = min(
98
+ end_times_per_machine[machine_id], start_time + op.duration
99
+ )
100
+ return end_times_per_machine
101
+
102
+
103
+ def _get_immediate_machines(
104
+ self: Dispatcher, available_operations: list[Operation]
105
+ ) -> list[bool]:
106
+ """Returns the machine ids of the machines that have at least one
107
+ operation with the lowest start time (i.e. the start time)."""
108
+ working_machines = [False] * self.instance.num_machines
109
+ # We can't use the current_time directly because it will cause
110
+ # an infinite loop.
111
+ current_time = self.min_start_time(available_operations)
112
+ for op in available_operations:
113
+ for machine_id in op.machines:
114
+ if self.start_time(op, machine_id) == current_time:
115
+ working_machines[machine_id] = True
116
+ return working_machines
@@ -0,0 +1,26 @@
1
+ """Exceptions for the job shop scheduling library."""
2
+
3
+
4
+ class JobShopLibError(Exception):
5
+ """Base class for exceptions in the job shop scheduling library.
6
+
7
+ This class is the base class for all exceptions raised by the job
8
+ shop scheduling library.
9
+
10
+ It is useful for catching any exception that is raised by the
11
+ library, without having to catch each specific exception
12
+ separately.
13
+ """
14
+
15
+
16
+ class NoSolutionFoundError(JobShopLibError):
17
+ """Exception raised when no solution is found by a solver.
18
+
19
+ This exception is raised by a solver when it is unable to find a
20
+ feasible solution within a given time limit.
21
+
22
+ It is useful to distinguish this exception from other exceptions
23
+ that may be raised by a solver, such as a ValueError or a
24
+ TypeError, which may indicate a bug in the code or an invalid
25
+ input, rather than a failure to find a solution.
26
+ """
@@ -0,0 +1,7 @@
1
+ """Package for generating job shop instances."""
2
+
3
+ from job_shop_lib.generators.basic_generator import BasicGenerator
4
+
5
+ __all__ = [
6
+ "BasicGenerator",
7
+ ]