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,135 @@
|
|
1
|
+
"""Contains factory functions for creating ready operations filters.
|
2
|
+
|
3
|
+
The factory functions create and return the appropriate functions based on the
|
4
|
+
specified names or enums.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Union
|
8
|
+
from enum import Enum
|
9
|
+
from collections.abc import Iterable
|
10
|
+
|
11
|
+
from job_shop_lib import Operation
|
12
|
+
from job_shop_lib.exceptions import ValidationError
|
13
|
+
|
14
|
+
from job_shop_lib.dispatching import (
|
15
|
+
Dispatcher,
|
16
|
+
filter_dominated_operations,
|
17
|
+
filter_non_immediate_machines,
|
18
|
+
filter_non_idle_machines,
|
19
|
+
filter_non_immediate_operations,
|
20
|
+
ReadyOperationsFilter,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
class ReadyOperationsFilterType(str, Enum):
|
25
|
+
"""Enumeration of ready operations filter types.
|
26
|
+
|
27
|
+
A filter function is used by the
|
28
|
+
:class:`~job_shop_lib.dispatching.Dispatcher` class to reduce the
|
29
|
+
amount of available operations to choose from.
|
30
|
+
"""
|
31
|
+
|
32
|
+
DOMINATED_OPERATIONS = "dominated_operations"
|
33
|
+
NON_IMMEDIATE_MACHINES = "non_immediate_machines"
|
34
|
+
NON_IDLE_MACHINES = "non_idle_machines"
|
35
|
+
NON_IMMEDIATE_OPERATIONS = "non_immediate_operations"
|
36
|
+
|
37
|
+
|
38
|
+
def create_composite_operation_filter(
|
39
|
+
ready_operations_filters: Iterable[
|
40
|
+
Union[ReadyOperationsFilter, str, ReadyOperationsFilterType]
|
41
|
+
],
|
42
|
+
) -> ReadyOperationsFilter:
|
43
|
+
"""Creates and returns a :class:`ReadyOperationsFilter` function by
|
44
|
+
combining multiple filter strategies.
|
45
|
+
|
46
|
+
The composite filter function applies multiple filter strategies
|
47
|
+
iteratively in the order they are specified in the list.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
ready_operations_filters:
|
51
|
+
A list of filter strategies to be used.
|
52
|
+
Supported values are 'dominated_operations' and
|
53
|
+
'non_immediate_machines' or any Callable that takes a
|
54
|
+
:class:`~job_shop_lib.dispatching.Dispatcher` instance and a list
|
55
|
+
of :class:`~job_shop_lib.Operation` instances as input
|
56
|
+
and returns a list of :class:`~job_shop_lib.Operation` instances.
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
|
60
|
+
instance and a list of :class:`~job_shop_lib.Operation`
|
61
|
+
instances as input and returns a list of
|
62
|
+
:class:`~job_shop_lib.Operation` instances based on
|
63
|
+
the specified list of filter strategies.
|
64
|
+
|
65
|
+
Raises:
|
66
|
+
ValidationError: If any of the filter strategies in the list are not
|
67
|
+
recognized or are not supported.
|
68
|
+
"""
|
69
|
+
|
70
|
+
filter_functions = [
|
71
|
+
ready_operations_filter_factory(name)
|
72
|
+
for name in ready_operations_filters
|
73
|
+
]
|
74
|
+
|
75
|
+
def composite_pruning_function(
|
76
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
77
|
+
) -> list[Operation]:
|
78
|
+
pruned_operations = operations
|
79
|
+
for pruning_function in filter_functions:
|
80
|
+
pruned_operations = pruning_function(dispatcher, pruned_operations)
|
81
|
+
|
82
|
+
return pruned_operations
|
83
|
+
|
84
|
+
return composite_pruning_function
|
85
|
+
|
86
|
+
|
87
|
+
def ready_operations_filter_factory(
|
88
|
+
filter_name: Union[str, ReadyOperationsFilterType, ReadyOperationsFilter]
|
89
|
+
) -> ReadyOperationsFilter:
|
90
|
+
"""Creates and returns a filter function based on the specified
|
91
|
+
filter strategy name.
|
92
|
+
|
93
|
+
The filter function filters operations based on certain criteria such as
|
94
|
+
dominated operations, immediate machine operations, etc.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
filter_name:
|
98
|
+
The name of the filter function to be used. See
|
99
|
+
:class:`ReadyOperationsFilterType` for supported values.
|
100
|
+
Alternatively, a custom filter function can be passed directly.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
A function that takes a :class:`~job_shop_lib.dispatching.Dispatcher`
|
104
|
+
instance and a list of :class:`~job_shop_lib.Operation`
|
105
|
+
instances as input and returns a list of
|
106
|
+
:class:`~job_shop_lib.Operation` instances based on
|
107
|
+
the specified filter function.
|
108
|
+
|
109
|
+
Raises:
|
110
|
+
ValidationError: If the ``filter_name`` argument is not recognized or
|
111
|
+
is not supported.
|
112
|
+
"""
|
113
|
+
if callable(filter_name):
|
114
|
+
return filter_name
|
115
|
+
|
116
|
+
filtering_strategies = {
|
117
|
+
ReadyOperationsFilterType.DOMINATED_OPERATIONS: (
|
118
|
+
filter_dominated_operations
|
119
|
+
),
|
120
|
+
ReadyOperationsFilterType.NON_IMMEDIATE_MACHINES: (
|
121
|
+
filter_non_immediate_machines
|
122
|
+
),
|
123
|
+
ReadyOperationsFilterType.NON_IDLE_MACHINES: filter_non_idle_machines,
|
124
|
+
ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS: (
|
125
|
+
filter_non_immediate_operations
|
126
|
+
),
|
127
|
+
}
|
128
|
+
|
129
|
+
if filter_name not in filtering_strategies:
|
130
|
+
raise ValidationError(
|
131
|
+
f"Unsupported filter function '{filter_name}'. "
|
132
|
+
f"Supported values are {', '.join(filtering_strategies.keys())}."
|
133
|
+
)
|
134
|
+
|
135
|
+
return filtering_strategies[filter_name] # type: ignore[index]
|
@@ -1,17 +1,16 @@
|
|
1
|
-
"""Home of the `
|
1
|
+
"""Home of the `HistoryObserver` class."""
|
2
2
|
|
3
|
+
from typing import List
|
3
4
|
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
4
5
|
from job_shop_lib import ScheduledOperation
|
5
6
|
|
6
7
|
|
7
|
-
class
|
8
|
+
class HistoryObserver(DispatcherObserver):
|
8
9
|
"""Observer that stores the history of the dispatcher."""
|
9
10
|
|
10
|
-
def __init__(self, dispatcher: Dispatcher):
|
11
|
-
|
12
|
-
|
13
|
-
super().__init__(dispatcher)
|
14
|
-
self.history: list[ScheduledOperation] = []
|
11
|
+
def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
|
12
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
13
|
+
self.history: List[ScheduledOperation] = []
|
15
14
|
|
16
15
|
def update(self, scheduled_operation: ScheduledOperation):
|
17
16
|
self.history.append(scheduled_operation)
|
@@ -0,0 +1,113 @@
|
|
1
|
+
"""Home of the `OptimalOperationsObserver` class."""
|
2
|
+
|
3
|
+
from typing import List, Set, Dict
|
4
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
5
|
+
from job_shop_lib import Schedule, Operation, ScheduledOperation
|
6
|
+
from job_shop_lib.exceptions import ValidationError
|
7
|
+
|
8
|
+
|
9
|
+
class OptimalOperationsObserver(DispatcherObserver):
|
10
|
+
"""Observer that identifies which available operations are optimal based on
|
11
|
+
a reference schedule.
|
12
|
+
|
13
|
+
This observer compares the available operations at each step with a
|
14
|
+
reference schedule to determine which operations would lead to the optimal
|
15
|
+
solution. It can be used for training purposes or to analyze decision
|
16
|
+
making in dispatching algorithms.
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
optimal_operations: Set of operations that are considered optimal
|
20
|
+
based on the reference schedule.
|
21
|
+
reference_schedule: The reference schedule used to determine optimal
|
22
|
+
operations.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
dispatcher: The dispatcher instance to observe.
|
26
|
+
reference_schedule: A complete schedule that represents the optimal
|
27
|
+
or reference solution.
|
28
|
+
subscribe: If True, automatically subscribes to the dispatcher.
|
29
|
+
|
30
|
+
Raises:
|
31
|
+
ValidationError: If the reference schedule is incomplete or if it
|
32
|
+
doesn't match the dispatcher's instance.
|
33
|
+
"""
|
34
|
+
|
35
|
+
_is_singleton = False
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
dispatcher: Dispatcher,
|
40
|
+
reference_schedule: Schedule,
|
41
|
+
*,
|
42
|
+
subscribe: bool = True,
|
43
|
+
):
|
44
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
45
|
+
|
46
|
+
if not reference_schedule.is_complete():
|
47
|
+
raise ValidationError("Reference schedule must be complete.")
|
48
|
+
|
49
|
+
if reference_schedule.instance != dispatcher.instance:
|
50
|
+
raise ValidationError(
|
51
|
+
"Reference schedule instance does not match dispatcher "
|
52
|
+
"instance."
|
53
|
+
)
|
54
|
+
|
55
|
+
self.reference_schedule = reference_schedule
|
56
|
+
self.optimal_available: Set[Operation] = set()
|
57
|
+
self._operation_to_scheduled: Dict[Operation, ScheduledOperation] = {}
|
58
|
+
self._machine_next_operation_index: List[int] = [0] * len(
|
59
|
+
reference_schedule.schedule
|
60
|
+
)
|
61
|
+
|
62
|
+
self._build_operation_mapping()
|
63
|
+
self._update_optimal_operations()
|
64
|
+
|
65
|
+
def _build_operation_mapping(self) -> None:
|
66
|
+
"""Builds a mapping from operations to their scheduled versions in
|
67
|
+
the reference schedule."""
|
68
|
+
for machine_schedule in self.reference_schedule.schedule:
|
69
|
+
for scheduled_op in machine_schedule:
|
70
|
+
self._operation_to_scheduled[scheduled_op.operation] = (
|
71
|
+
scheduled_op
|
72
|
+
)
|
73
|
+
|
74
|
+
def _update_optimal_operations(self) -> None:
|
75
|
+
"""Updates the set of optimal operations based on current state.
|
76
|
+
|
77
|
+
An operation is considered optimal if it is the next unscheduled
|
78
|
+
operation in its machine's sequence according to the reference
|
79
|
+
schedule.
|
80
|
+
"""
|
81
|
+
self.optimal_available.clear()
|
82
|
+
available_operations = self.dispatcher.available_operations()
|
83
|
+
|
84
|
+
if not available_operations:
|
85
|
+
return
|
86
|
+
|
87
|
+
for operation in available_operations:
|
88
|
+
scheduled_op = self._operation_to_scheduled[operation]
|
89
|
+
machine_index = scheduled_op.machine_id
|
90
|
+
next_index = self._machine_next_operation_index[machine_index]
|
91
|
+
|
92
|
+
if (
|
93
|
+
scheduled_op
|
94
|
+
== self.reference_schedule.schedule[machine_index][next_index]
|
95
|
+
):
|
96
|
+
self.optimal_available.add(operation)
|
97
|
+
|
98
|
+
def update(self, scheduled_operation: ScheduledOperation) -> None:
|
99
|
+
"""Updates the optimal operations after an operation is scheduled.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
scheduled_operation: The operation that was just scheduled.
|
103
|
+
"""
|
104
|
+
self._machine_next_operation_index[scheduled_operation.machine_id] += 1
|
105
|
+
self._update_optimal_operations()
|
106
|
+
|
107
|
+
def reset(self) -> None:
|
108
|
+
"""Resets the observer to its initial state."""
|
109
|
+
self._machine_next_operation_index = [0] * len(
|
110
|
+
self.dispatcher.schedule.schedule
|
111
|
+
)
|
112
|
+
self.optimal_available.clear()
|
113
|
+
self._update_optimal_operations()
|
@@ -0,0 +1,168 @@
|
|
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 typing import List, Set
|
8
|
+
from collections.abc import Callable
|
9
|
+
|
10
|
+
from job_shop_lib import Operation
|
11
|
+
from job_shop_lib.dispatching import Dispatcher
|
12
|
+
|
13
|
+
|
14
|
+
ReadyOperationsFilter = Callable[
|
15
|
+
[Dispatcher, List[Operation]], List[Operation]
|
16
|
+
]
|
17
|
+
|
18
|
+
|
19
|
+
def filter_non_idle_machines(
|
20
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
21
|
+
) -> List[Operation]:
|
22
|
+
"""Filters out all the operations associated with non-idle machines.
|
23
|
+
|
24
|
+
A machine is considered idle if there are no ongoing operations
|
25
|
+
currently scheduled on it. This filter removes operations that are
|
26
|
+
associated with machines that are busy (i.e., have at least one
|
27
|
+
uncompleted operation).
|
28
|
+
|
29
|
+
Utilizes :meth:``Dispatcher.ongoing_operations()`` to determine machine
|
30
|
+
statuses.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
dispatcher: The dispatcher object.
|
34
|
+
operations: The list of operations to filter.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
The list of operations that are associated with idle machines.
|
38
|
+
"""
|
39
|
+
current_time = dispatcher.min_start_time(operations)
|
40
|
+
non_idle_machines = _get_non_idle_machines(dispatcher, current_time)
|
41
|
+
|
42
|
+
# Filter operations to keep those that are associated with at least one
|
43
|
+
# idle machine
|
44
|
+
filtered_operations: List[Operation] = []
|
45
|
+
for operation in operations:
|
46
|
+
if all(
|
47
|
+
machine_id in non_idle_machines
|
48
|
+
for machine_id in operation.machines
|
49
|
+
):
|
50
|
+
continue
|
51
|
+
filtered_operations.append(operation)
|
52
|
+
|
53
|
+
return filtered_operations
|
54
|
+
|
55
|
+
|
56
|
+
def _get_non_idle_machines(
|
57
|
+
dispatcher: Dispatcher, current_time: int
|
58
|
+
) -> Set[int]:
|
59
|
+
"""Returns the set of machine ids that are currently busy (i.e., have at
|
60
|
+
least one uncompleted operation)."""
|
61
|
+
|
62
|
+
non_idle_machines = set()
|
63
|
+
for machine_schedule in dispatcher.schedule.schedule:
|
64
|
+
for scheduled_operation in reversed(machine_schedule):
|
65
|
+
is_completed = scheduled_operation.end_time <= current_time
|
66
|
+
if is_completed:
|
67
|
+
break
|
68
|
+
non_idle_machines.add(scheduled_operation.machine_id)
|
69
|
+
|
70
|
+
return non_idle_machines
|
71
|
+
|
72
|
+
|
73
|
+
def filter_non_immediate_operations(
|
74
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
75
|
+
) -> List[Operation]:
|
76
|
+
"""Filters out all the operations that can't start immediately.
|
77
|
+
|
78
|
+
An operation can start immediately if its earliest start time is the
|
79
|
+
current time.
|
80
|
+
|
81
|
+
The current time is determined by the minimum start time of the
|
82
|
+
operations.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
dispatcher: The dispatcher object.
|
86
|
+
operations: The list of operations to filter.
|
87
|
+
"""
|
88
|
+
|
89
|
+
min_start_time = dispatcher.min_start_time(operations)
|
90
|
+
immediate_operations: List[Operation] = []
|
91
|
+
for operation in operations:
|
92
|
+
start_time = dispatcher.earliest_start_time(operation)
|
93
|
+
if start_time == min_start_time:
|
94
|
+
immediate_operations.append(operation)
|
95
|
+
|
96
|
+
return immediate_operations
|
97
|
+
|
98
|
+
|
99
|
+
def filter_dominated_operations(
|
100
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
101
|
+
) -> List[Operation]:
|
102
|
+
"""Filters out all the operations that are dominated.
|
103
|
+
An operation is dominated if there is another operation that ends before
|
104
|
+
it starts on the same machine.
|
105
|
+
"""
|
106
|
+
|
107
|
+
min_machine_end_times = _get_min_machine_end_times(dispatcher, operations)
|
108
|
+
|
109
|
+
non_dominated_operations: List[Operation] = []
|
110
|
+
for operation in operations:
|
111
|
+
# One benchmark instance has an operation with duration 0
|
112
|
+
if operation.duration == 0:
|
113
|
+
return [operation]
|
114
|
+
for machine_id in operation.machines:
|
115
|
+
start_time = dispatcher.start_time(operation, machine_id)
|
116
|
+
is_dominated = start_time >= min_machine_end_times[machine_id]
|
117
|
+
if not is_dominated:
|
118
|
+
non_dominated_operations.append(operation)
|
119
|
+
break
|
120
|
+
|
121
|
+
return non_dominated_operations
|
122
|
+
|
123
|
+
|
124
|
+
def filter_non_immediate_machines(
|
125
|
+
dispatcher: Dispatcher, operations: List[Operation]
|
126
|
+
) -> List[Operation]:
|
127
|
+
"""Filters out all the operations associated with machines which earliest
|
128
|
+
operation is not the current time."""
|
129
|
+
|
130
|
+
is_immediate_machine = _get_immediate_machines(dispatcher, operations)
|
131
|
+
non_dominated_operations: List[Operation] = []
|
132
|
+
for operation in operations:
|
133
|
+
if any(
|
134
|
+
is_immediate_machine[machine_id]
|
135
|
+
for machine_id in operation.machines
|
136
|
+
):
|
137
|
+
non_dominated_operations.append(operation)
|
138
|
+
|
139
|
+
return non_dominated_operations
|
140
|
+
|
141
|
+
|
142
|
+
def _get_min_machine_end_times(
|
143
|
+
dispatcher: Dispatcher, available_operations: List[Operation]
|
144
|
+
) -> List[float]:
|
145
|
+
end_times_per_machine = [float("inf")] * dispatcher.instance.num_machines
|
146
|
+
for op in available_operations:
|
147
|
+
for machine_id in op.machines:
|
148
|
+
start_time = dispatcher.start_time(op, machine_id)
|
149
|
+
end_times_per_machine[machine_id] = min(
|
150
|
+
end_times_per_machine[machine_id], start_time + op.duration
|
151
|
+
)
|
152
|
+
return end_times_per_machine
|
153
|
+
|
154
|
+
|
155
|
+
def _get_immediate_machines(
|
156
|
+
dispatcher: Dispatcher, available_operations: List[Operation]
|
157
|
+
) -> List[bool]:
|
158
|
+
"""Returns the machine ids of the machines that have at least one
|
159
|
+
operation with the lowest start time (i.e. the start time)."""
|
160
|
+
working_machines = [False] * dispatcher.instance.num_machines
|
161
|
+
# We can't use the current_time directly because it will cause
|
162
|
+
# an infinite loop.
|
163
|
+
current_time = dispatcher.min_start_time(available_operations)
|
164
|
+
for op in available_operations:
|
165
|
+
for machine_id in op.machines:
|
166
|
+
if dispatcher.start_time(op, machine_id) == current_time:
|
167
|
+
working_machines[machine_id] = True
|
168
|
+
return working_machines
|
@@ -0,0 +1,70 @@
|
|
1
|
+
"""Home of the `UnscheduledOperationsObserver` class."""
|
2
|
+
|
3
|
+
import collections
|
4
|
+
from collections.abc import Iterable
|
5
|
+
import itertools
|
6
|
+
from typing import Deque, List
|
7
|
+
|
8
|
+
from job_shop_lib import Operation, ScheduledOperation
|
9
|
+
from job_shop_lib.dispatching import DispatcherObserver, Dispatcher
|
10
|
+
|
11
|
+
|
12
|
+
class UnscheduledOperationsObserver(DispatcherObserver):
|
13
|
+
"""Stores the operations that have not been dispatched yet.
|
14
|
+
|
15
|
+
This observer maintains a list of deques, each containing unscheduled
|
16
|
+
operations for a specific job. It provides methods to access and
|
17
|
+
manipulate unscheduled operations efficiently.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, dispatcher: Dispatcher, *, subscribe: bool = True):
|
21
|
+
super().__init__(dispatcher, subscribe=subscribe)
|
22
|
+
self.unscheduled_operations_per_job: List[Deque[Operation]] = []
|
23
|
+
self.reset()
|
24
|
+
# In case the dispatcher has already scheduled some operations,
|
25
|
+
# we need to remove them.
|
26
|
+
# Note that we don't need to remove the operations in order.
|
27
|
+
for scheduled_operation in itertools.chain(
|
28
|
+
*self.dispatcher.schedule.schedule
|
29
|
+
):
|
30
|
+
self.update(scheduled_operation)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def unscheduled_operations(self) -> Iterable[Operation]:
|
34
|
+
"""An iterable of all unscheduled operations across all jobs."""
|
35
|
+
return itertools.chain(*self.unscheduled_operations_per_job)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def num_unscheduled_operations(self) -> int:
|
39
|
+
"""The total number of unscheduled operations."""
|
40
|
+
total_operations = self.dispatcher.instance.num_operations
|
41
|
+
num_scheduled_operations = (
|
42
|
+
self.dispatcher.schedule.num_scheduled_operations
|
43
|
+
)
|
44
|
+
return total_operations - num_scheduled_operations
|
45
|
+
|
46
|
+
def update(self, scheduled_operation: ScheduledOperation) -> None:
|
47
|
+
"""Removes a scheduled operation from the unscheduled operations.
|
48
|
+
|
49
|
+
This method is called by the dispatcher when an operation is
|
50
|
+
scheduled. It removes the operation from its job's deque of
|
51
|
+
unscheduled operations.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
scheduled_operation:
|
55
|
+
The operation that has been scheduled.
|
56
|
+
"""
|
57
|
+
job_id = scheduled_operation.operation.job_id
|
58
|
+
job_deque = self.unscheduled_operations_per_job[job_id]
|
59
|
+
if job_deque:
|
60
|
+
job_deque.popleft()
|
61
|
+
|
62
|
+
def reset(self) -> None:
|
63
|
+
"""Resets unscheduled operations to include all operations.
|
64
|
+
|
65
|
+
This method reinitializes the list of deques with all operations
|
66
|
+
from all jobs in the instance.
|
67
|
+
"""
|
68
|
+
self.unscheduled_operations_per_job = [
|
69
|
+
collections.deque(job) for job in self.dispatcher.instance.jobs
|
70
|
+
]
|
@@ -1,16 +1,53 @@
|
|
1
|
-
"""Contains FeatureObserver classes for observing features of the
|
2
|
-
dispatcher.
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
1
|
+
"""Contains :class:`FeatureObserver` classes for observing features of the
|
2
|
+
dispatcher.
|
3
|
+
|
4
|
+
.. autosummary::
|
5
|
+
:nosignatures:
|
6
|
+
|
7
|
+
FeatureObserver
|
8
|
+
FeatureType
|
9
|
+
CompositeFeatureObserver
|
10
|
+
EarliestStartTimeObserver
|
11
|
+
IsReadyObserver
|
12
|
+
DurationObserver
|
13
|
+
IsScheduledObserver
|
14
|
+
PositionInJobObserver
|
15
|
+
RemainingOperationsObserver
|
16
|
+
IsCompletedObserver
|
17
|
+
FeatureObserverType
|
18
|
+
feature_observer_factory
|
19
|
+
FeatureObserverConfig
|
20
|
+
|
21
|
+
A :class:`~job_shop_lib.dispatching.feature_observers.FeatureObserver` is a
|
22
|
+
a subclass of :class:`~job_shop_lib.dispatching.DispatcherObserver` that
|
23
|
+
observes features related to operations, machines, or jobs in the dispatcher.
|
24
|
+
|
25
|
+
Attributes are stored in numpy arrays with a shape of (``num_entities``,
|
26
|
+
``feature_size``), where ``num_entities`` is the number of entities being
|
27
|
+
observed (e.g., operations, machines, or jobs) and ``feature_size`` is the
|
28
|
+
number of values being observed for each entity.
|
29
|
+
|
30
|
+
The advantage of using arrays is that they can be easily updated in a
|
31
|
+
vectorized manner, which is more efficient than updating each attribute
|
32
|
+
individually. Furthermore, machine learning models can be trained on these
|
33
|
+
arrays to predict the best dispatching decisions.
|
34
|
+
"""
|
35
|
+
|
36
|
+
from ._feature_observer import FeatureObserver, FeatureType
|
37
|
+
from ._earliest_start_time_observer import EarliestStartTimeObserver
|
38
|
+
from ._is_ready_observer import IsReadyObserver
|
39
|
+
from ._duration_observer import DurationObserver
|
40
|
+
from ._is_scheduled_observer import IsScheduledObserver
|
41
|
+
from ._position_in_job_observer import PositionInJobObserver
|
42
|
+
from ._remaining_operations_observer import RemainingOperationsObserver
|
43
|
+
from ._is_completed_observer import IsCompletedObserver
|
44
|
+
from ._factory import (
|
45
|
+
FeatureObserverType,
|
46
|
+
feature_observer_factory,
|
47
|
+
FeatureObserverConfig,
|
48
|
+
)
|
49
|
+
from ._composite_feature_observer import CompositeFeatureObserver
|
50
|
+
|
14
51
|
|
15
52
|
__all__ = [
|
16
53
|
"FeatureObserver",
|
@@ -25,4 +62,5 @@ __all__ = [
|
|
25
62
|
"IsCompletedObserver",
|
26
63
|
"FeatureObserverType",
|
27
64
|
"feature_observer_factory",
|
65
|
+
"FeatureObserverConfig",
|
28
66
|
]
|