job-shop-lib 0.1.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 (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
+ ]