job-shop-lib 1.0.0a1__py3-none-any.whl → 1.0.0a3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- job_shop_lib/_job_shop_instance.py +18 -20
- job_shop_lib/_operation.py +30 -24
- job_shop_lib/_schedule.py +17 -16
- job_shop_lib/_scheduled_operation.py +10 -12
- job_shop_lib/constraint_programming/_ortools_solver.py +31 -16
- job_shop_lib/dispatching/__init__.py +4 -0
- job_shop_lib/dispatching/_dispatcher.py +24 -32
- job_shop_lib/dispatching/_factories.py +8 -0
- job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
- job_shop_lib/dispatching/feature_observers/__init__.py +34 -2
- job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +54 -14
- job_shop_lib/dispatching/feature_observers/_duration_observer.py +15 -2
- job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +62 -10
- job_shop_lib/dispatching/feature_observers/_factory.py +5 -1
- job_shop_lib/dispatching/feature_observers/_feature_observer.py +87 -16
- job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +32 -2
- job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +3 -3
- job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py +9 -5
- job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py +7 -2
- job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py +1 -1
- job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +66 -43
- job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
- job_shop_lib/graphs/__init__.py +2 -0
- job_shop_lib/graphs/_build_agent_task_graph.py +2 -2
- job_shop_lib/graphs/_constants.py +18 -1
- job_shop_lib/graphs/_job_shop_graph.py +36 -20
- job_shop_lib/graphs/_node.py +60 -52
- job_shop_lib/graphs/graph_updaters/__init__.py +11 -1
- job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +1 -1
- job_shop_lib/visualization/__init__.py +5 -5
- job_shop_lib/visualization/_gantt_chart_creator.py +5 -5
- job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +63 -36
- job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/METADATA +15 -3
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/RECORD +37 -37
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/WHEEL +1 -1
- {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/LICENSE +0 -0
@@ -20,15 +20,27 @@ class JobShopInstance:
|
|
20
20
|
computations.
|
21
21
|
|
22
22
|
Attributes:
|
23
|
-
jobs:
|
23
|
+
jobs (list[list[Operation]]):
|
24
24
|
A list of lists of operations. Each list of operations represents
|
25
25
|
a job, and the operations are ordered by their position in the job.
|
26
26
|
The `job_id`, `position_in_job`, and `operation_id` attributes of
|
27
27
|
the operations are set when the instance is created.
|
28
|
-
name:
|
28
|
+
name (str):
|
29
29
|
A string with the name of the instance.
|
30
|
-
metadata:
|
30
|
+
metadata (dict[str, Any]):
|
31
31
|
A dictionary with additional information about the instance.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
jobs:
|
35
|
+
A list of lists of operations. Each list of operations
|
36
|
+
represents a job, and the operations are ordered by their
|
37
|
+
position in the job. The `job_id`, `position_in_job`, and
|
38
|
+
`operation_id` attributes of the operations are set when the
|
39
|
+
instance is created.
|
40
|
+
name:
|
41
|
+
A string with the name of the instance.
|
42
|
+
**metadata:
|
43
|
+
Additional information about the instance.
|
32
44
|
"""
|
33
45
|
|
34
46
|
def __init__(
|
@@ -37,24 +49,10 @@ class JobShopInstance:
|
|
37
49
|
name: str = "JobShopInstance",
|
38
50
|
**metadata: Any,
|
39
51
|
):
|
40
|
-
|
41
|
-
|
42
|
-
Args:
|
43
|
-
jobs:
|
44
|
-
A list of lists of operations. Each list of operations
|
45
|
-
represents a job, and the operations are ordered by their
|
46
|
-
position in the job. The `job_id`, `position_in_job`, and
|
47
|
-
`operation_id` attributes of the operations are set when the
|
48
|
-
instance is created.
|
49
|
-
name:
|
50
|
-
A string with the name of the instance.
|
51
|
-
**metadata:
|
52
|
-
Additional information about the instance.
|
53
|
-
"""
|
54
|
-
self.jobs = jobs
|
52
|
+
self.jobs: list[list[Operation]] = jobs
|
55
53
|
self.set_operation_attributes()
|
56
|
-
self.name = name
|
57
|
-
self.metadata = metadata
|
54
|
+
self.name: str = name
|
55
|
+
self.metadata: dict[str, Any] = metadata
|
58
56
|
|
59
57
|
def set_operation_attributes(self):
|
60
58
|
"""Sets the job_id and position of each operation."""
|
job_shop_lib/_operation.py
CHANGED
@@ -23,32 +23,38 @@ class Operation:
|
|
23
23
|
necessary to multiply all durations by a sufficiently large integer so
|
24
24
|
that every duration is an integer.
|
25
25
|
|
26
|
-
|
27
|
-
machines:
|
28
|
-
|
26
|
+
Args:
|
27
|
+
machines:
|
28
|
+
A list of machine ids that can perform the operation. If
|
29
|
+
only one machine can perform the operation, it can be passed as
|
30
|
+
an integer.
|
31
|
+
duration:
|
32
|
+
The time it takes to perform the operation.
|
29
33
|
"""
|
30
34
|
|
31
|
-
__slots__ =
|
32
|
-
"machines"
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
__slots__ = {
|
36
|
+
"machines": (
|
37
|
+
"A list of machine ids that can perform the operation. If "
|
38
|
+
"only one machine can perform the operation, it can be passed as "
|
39
|
+
"an integer."
|
40
|
+
),
|
41
|
+
"duration": (
|
42
|
+
"The time it takes to perform the operation. Often referred"
|
43
|
+
" to as the processing time."
|
44
|
+
),
|
45
|
+
"job_id": "The id of the job the operation belongs to.",
|
46
|
+
"position_in_job": "The index of the operation in the job.",
|
47
|
+
"operation_id": (
|
48
|
+
"The id of the operation. This is unique within a "
|
49
|
+
":class:`JobShopInstance`."
|
50
|
+
),
|
51
|
+
}
|
38
52
|
|
39
53
|
def __init__(self, machines: int | list[int], duration: int):
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
A list of machine ids that can perform the operation. If
|
45
|
-
only one machine can perform the operation, it can be passed as
|
46
|
-
an integer.
|
47
|
-
duration:
|
48
|
-
The time it takes to perform the operation.
|
49
|
-
"""
|
50
|
-
self.machines = [machines] if isinstance(machines, int) else machines
|
51
|
-
self.duration = duration
|
54
|
+
self.machines: list[int] = (
|
55
|
+
[machines] if isinstance(machines, int) else machines
|
56
|
+
)
|
57
|
+
self.duration: int = duration
|
52
58
|
|
53
59
|
# Defined outside the class by the JobShopInstance class:
|
54
60
|
self.job_id: int = -1
|
@@ -60,8 +66,8 @@ class Operation:
|
|
60
66
|
"""Returns the id of the machine associated with the operation.
|
61
67
|
|
62
68
|
Raises:
|
63
|
-
UninitializedAttributeError:
|
64
|
-
|
69
|
+
UninitializedAttributeError:
|
70
|
+
If the operation has multiple machines in its list.
|
65
71
|
"""
|
66
72
|
if len(self.machines) > 1:
|
67
73
|
raise UninitializedAttributeError(
|
job_shop_lib/_schedule.py
CHANGED
@@ -25,27 +25,28 @@ class Schedule:
|
|
25
25
|
is_complete
|
26
26
|
add
|
27
27
|
reset
|
28
|
-
|
29
|
-
Attributes:
|
30
|
-
instance:
|
31
|
-
The :class:`JobShopInstance` object that the schedule is for.
|
32
|
-
metadata:
|
33
|
-
A dictionary with additional information about the schedule. It
|
34
|
-
can be used to store information about the algorithm that generated
|
35
|
-
the schedule, for example.
|
36
28
|
"""
|
37
29
|
|
38
|
-
__slots__ =
|
39
|
-
"instance"
|
40
|
-
|
41
|
-
|
42
|
-
|
30
|
+
__slots__ = {
|
31
|
+
"instance": (
|
32
|
+
"The :class:`JobShopInstance` object that the schedule is for."
|
33
|
+
),
|
34
|
+
"_schedule": (
|
35
|
+
"A list of lists of :class:`ScheduledOperation` objects. "
|
36
|
+
"Each list represents the order of operations on a machine."
|
37
|
+
),
|
38
|
+
"metadata": (
|
39
|
+
"A dictionary with additional information about the "
|
40
|
+
"schedule. It can be used to store information about the "
|
41
|
+
"algorithm that generated the schedule, for example."
|
42
|
+
),
|
43
|
+
}
|
43
44
|
|
44
45
|
def __init__(
|
45
46
|
self,
|
46
47
|
instance: JobShopInstance,
|
47
48
|
schedule: list[list[ScheduledOperation]] | None = None,
|
48
|
-
**metadata,
|
49
|
+
**metadata: Any,
|
49
50
|
):
|
50
51
|
"""Initializes the object with the given instance and schedule.
|
51
52
|
|
@@ -64,9 +65,9 @@ class Schedule:
|
|
64
65
|
|
65
66
|
Schedule.check_schedule(schedule)
|
66
67
|
|
67
|
-
self.instance = instance
|
68
|
+
self.instance: JobShopInstance = instance
|
68
69
|
self._schedule = schedule
|
69
|
-
self.metadata = metadata
|
70
|
+
self.metadata: dict[str, Any] = metadata
|
70
71
|
|
71
72
|
def __repr__(self) -> str:
|
72
73
|
return str(self.schedule)
|
@@ -5,17 +5,15 @@ from job_shop_lib.exceptions import ValidationError
|
|
5
5
|
|
6
6
|
|
7
7
|
class ScheduledOperation:
|
8
|
-
"""Data structure to store a scheduled operation.
|
8
|
+
"""Data structure to store a scheduled operation."""
|
9
9
|
|
10
|
-
|
11
|
-
operation:
|
12
|
-
|
13
|
-
|
14
|
-
The
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
__slots__ = ("operation", "start_time", "_machine_id")
|
10
|
+
__slots__ = {
|
11
|
+
"operation": "The :class:`Operation` object that is scheduled.",
|
12
|
+
"start_time": "The time at which the operation is scheduled to start.",
|
13
|
+
"_machine_id": (
|
14
|
+
"The id of the machine on which the operation is scheduled."
|
15
|
+
),
|
16
|
+
}
|
19
17
|
|
20
18
|
def __init__(self, operation: Operation, start_time: int, machine_id: int):
|
21
19
|
"""Initializes a new instance of the :class:`ScheduledOperation` class.
|
@@ -33,8 +31,8 @@ class ScheduledOperation:
|
|
33
31
|
If the given machine_id is not in the list of valid machines
|
34
32
|
for the operation.
|
35
33
|
"""
|
36
|
-
self.operation = operation
|
37
|
-
self.start_time = start_time
|
34
|
+
self.operation: Operation = operation
|
35
|
+
self.start_time: int = start_time
|
38
36
|
self._machine_id = machine_id
|
39
37
|
self.machine_id = machine_id # Validate machine_id
|
40
38
|
|
@@ -22,22 +22,32 @@ class ORToolsSolver(BaseSolver):
|
|
22
22
|
<https://developers.google.com/optimization/cp/cp_solver>`_ solver.
|
23
23
|
|
24
24
|
Attributes:
|
25
|
-
log_search_progress (
|
25
|
+
log_search_progress (bool):
|
26
26
|
Whether to log the search progress to the console.
|
27
|
-
max_time_in_seconds (
|
27
|
+
max_time_in_seconds (float | None):
|
28
28
|
The maximum time in seconds to allow the solver to search for
|
29
29
|
a solution. If no solution is found within this time, a
|
30
30
|
:class:`~job_shop_lib.exceptions.NoSolutionFoundError` is
|
31
31
|
raised. If ``None``, the solver will run until an optimal
|
32
32
|
solution is found.
|
33
|
-
model (
|
33
|
+
model (cp_model.CpModel):
|
34
34
|
The `OR-Tools' CP-SAT model
|
35
35
|
<https://developers.google.com/optimization/reference/python/sat/
|
36
36
|
python/cp_model#cp_model.CpModel>`_.
|
37
|
-
solver (
|
37
|
+
solver (cp_model.CpSolver):
|
38
38
|
The `OR-Tools' CP-SAT solver
|
39
39
|
<https://developers.google.com/optimization/reference/python/sat/
|
40
40
|
python/cp_model#cp_model.CpSolver>`_.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
max_time_in_seconds:
|
44
|
+
The maximum time in seconds to allow the solver to search for
|
45
|
+
a solution. If no solution is found within this time, a
|
46
|
+
:class:`~job_shop_lib.exceptions.NoSolutionFoundError` is
|
47
|
+
raised. If ``None``, the solver will run until an optimal
|
48
|
+
solution is found.
|
49
|
+
log_search_progress:
|
50
|
+
Whether to log the search progress to the console.
|
41
51
|
"""
|
42
52
|
|
43
53
|
def __init__(
|
@@ -45,18 +55,6 @@ class ORToolsSolver(BaseSolver):
|
|
45
55
|
max_time_in_seconds: float | None = None,
|
46
56
|
log_search_progress: bool = False,
|
47
57
|
):
|
48
|
-
"""Initializes the solver.
|
49
|
-
|
50
|
-
Args:
|
51
|
-
max_time_in_seconds:
|
52
|
-
The maximum time in seconds to allow the solver to search for
|
53
|
-
a solution. If no solution is found within this time, a
|
54
|
-
:class:`~job_shop_lib.exceptions.NoSolutionFoundError` is
|
55
|
-
raised. If ``None``, the solver will run until an optimal
|
56
|
-
solution is found.
|
57
|
-
log_search_progress:
|
58
|
-
Whether to log the search progress to the console.
|
59
|
-
"""
|
60
58
|
self.log_search_progress = log_search_progress
|
61
59
|
self.max_time_in_seconds = max_time_in_seconds
|
62
60
|
|
@@ -66,6 +64,23 @@ class ORToolsSolver(BaseSolver):
|
|
66
64
|
self._operations_start: dict[Operation, tuple[IntVar, IntVar]] = {}
|
67
65
|
|
68
66
|
def __call__(self, instance: JobShopInstance) -> Schedule:
|
67
|
+
"""Equivalent to calling the :meth:`~ORToolsSolver.solve` method.
|
68
|
+
|
69
|
+
This method is necessary because, in JobShopLib, solvers are defined
|
70
|
+
as callables that receive an instance and return a schedule.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
instance: The job shop instance to be solved.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
The best schedule found by the solver.
|
77
|
+
Its metadata contains the following information:
|
78
|
+
|
79
|
+
* status (``str``): ``"optimal"`` or ``"feasible"``
|
80
|
+
* elapsed_time (``float``): The time taken to solve the problem
|
81
|
+
* makespan (``int``): The total duration of the schedule
|
82
|
+
* solved_by (``str``): ``"ORToolsSolver"``
|
83
|
+
"""
|
69
84
|
# Re-defined here since we already add metadata to the schedule in
|
70
85
|
# the solve method.
|
71
86
|
return self.solve(instance)
|
@@ -32,6 +32,8 @@ from ._ready_operation_filters import (
|
|
32
32
|
filter_dominated_operations,
|
33
33
|
filter_non_immediate_machines,
|
34
34
|
ReadyOperationsFilter,
|
35
|
+
filter_non_idle_machines,
|
36
|
+
filter_non_immediate_operations,
|
35
37
|
)
|
36
38
|
from ._dispatcher_observer_config import DispatcherObserverConfig
|
37
39
|
from ._factories import (
|
@@ -53,4 +55,6 @@ __all__ = [
|
|
53
55
|
"DispatcherObserverConfig",
|
54
56
|
"UnscheduledOperationsObserver",
|
55
57
|
"ReadyOperationsFilter",
|
58
|
+
"filter_non_idle_machines",
|
59
|
+
"filter_non_immediate_operations",
|
56
60
|
]
|
@@ -156,26 +156,30 @@ class Dispatcher:
|
|
156
156
|
responsible for scheduling the operations on the machines and keeping
|
157
157
|
track of the next available time for each machine and job.
|
158
158
|
|
159
|
-
|
159
|
+
Args:
|
160
160
|
instance:
|
161
|
-
The instance of the job shop problem to be
|
162
|
-
schedule:
|
163
|
-
The schedule of operations on machines.
|
161
|
+
The instance of the job shop problem to be solved.
|
164
162
|
ready_operations_filter:
|
165
|
-
A function that filters out operations that are not ready to
|
166
|
-
scheduled.
|
163
|
+
A function that filters out operations that are not ready to
|
164
|
+
be scheduled. The function should take the dispatcher and a
|
165
|
+
list of operations as input and return a list of operations
|
166
|
+
that are ready to be scheduled. If ``None``, no filtering is
|
167
|
+
done.
|
167
168
|
"""
|
168
169
|
|
169
|
-
__slots__ =
|
170
|
-
"instance",
|
171
|
-
"schedule",
|
172
|
-
"_machine_next_available_time",
|
173
|
-
"_job_next_operation_index",
|
174
|
-
"_job_next_available_time",
|
175
|
-
"ready_operations_filter"
|
176
|
-
|
177
|
-
|
178
|
-
|
170
|
+
__slots__ = {
|
171
|
+
"instance": "The instance of the job shop problem to be scheduled.",
|
172
|
+
"schedule": "The schedule of operations on machines.",
|
173
|
+
"_machine_next_available_time": "",
|
174
|
+
"_job_next_operation_index": "",
|
175
|
+
"_job_next_available_time": "",
|
176
|
+
"ready_operations_filter": (
|
177
|
+
"A function that filters out operations that are not ready to be "
|
178
|
+
"scheduled."
|
179
|
+
),
|
180
|
+
"subscribers": "A list of observers subscribed to the dispatcher.",
|
181
|
+
"_cache": "A dictionary to cache the results of the cached methods.",
|
182
|
+
}
|
179
183
|
|
180
184
|
def __init__(
|
181
185
|
self,
|
@@ -184,18 +188,6 @@ class Dispatcher:
|
|
184
188
|
Callable[[Dispatcher, list[Operation]], list[Operation]] | None
|
185
189
|
) = None,
|
186
190
|
) -> None:
|
187
|
-
"""Initializes the object with the given instance.
|
188
|
-
|
189
|
-
Args:
|
190
|
-
instance:
|
191
|
-
The instance of the job shop problem to be solved.
|
192
|
-
ready_operations_filter:
|
193
|
-
A function that filters out operations that are not ready to
|
194
|
-
be scheduled. The function should take the dispatcher and a
|
195
|
-
list of operations as input and return a list of operations
|
196
|
-
that are ready to be scheduled. If ``None``, no filtering is
|
197
|
-
done.
|
198
|
-
"""
|
199
191
|
|
200
192
|
self.instance = instance
|
201
193
|
self.schedule = Schedule(self.instance)
|
@@ -371,7 +363,7 @@ class Dispatcher:
|
|
371
363
|
The current time is the minimum start time of the available
|
372
364
|
operations.
|
373
365
|
"""
|
374
|
-
available_operations = self.
|
366
|
+
available_operations = self.available_operations()
|
375
367
|
current_time = self.min_start_time(available_operations)
|
376
368
|
return current_time
|
377
369
|
|
@@ -387,7 +379,7 @@ class Dispatcher:
|
|
387
379
|
return int(min_start_time)
|
388
380
|
|
389
381
|
@_dispatcher_cache
|
390
|
-
def
|
382
|
+
def available_operations(self) -> list[Operation]:
|
391
383
|
"""Returns a list of available operations for processing, optionally
|
392
384
|
filtering out operations using the filter function.
|
393
385
|
|
@@ -443,7 +435,7 @@ class Dispatcher:
|
|
443
435
|
@_dispatcher_cache
|
444
436
|
def available_machines(self) -> list[int]:
|
445
437
|
"""Returns the list of ready machines."""
|
446
|
-
available_operations = self.
|
438
|
+
available_operations = self.available_operations()
|
447
439
|
available_machines = set()
|
448
440
|
for operation in available_operations:
|
449
441
|
available_machines.update(operation.machines)
|
@@ -452,7 +444,7 @@ class Dispatcher:
|
|
452
444
|
@_dispatcher_cache
|
453
445
|
def available_jobs(self) -> list[int]:
|
454
446
|
"""Returns the list of ready jobs."""
|
455
|
-
available_operations = self.
|
447
|
+
available_operations = self.available_operations()
|
456
448
|
available_jobs = set(
|
457
449
|
operation.job_id for operation in available_operations
|
458
450
|
)
|
@@ -13,6 +13,8 @@ from job_shop_lib.dispatching import (
|
|
13
13
|
Dispatcher,
|
14
14
|
filter_dominated_operations,
|
15
15
|
filter_non_immediate_machines,
|
16
|
+
filter_non_idle_machines,
|
17
|
+
filter_non_immediate_operations,
|
16
18
|
ReadyOperationsFilter,
|
17
19
|
)
|
18
20
|
|
@@ -27,6 +29,8 @@ class ReadyOperationsFilterType(str, Enum):
|
|
27
29
|
|
28
30
|
DOMINATED_OPERATIONS = "dominated_operations"
|
29
31
|
NON_IMMEDIATE_MACHINES = "non_immediate_machines"
|
32
|
+
NON_IDLE_MACHINES = "non_idle_machines"
|
33
|
+
NON_IMMEDIATE_OPERATIONS = "non_immediate_operations"
|
30
34
|
|
31
35
|
|
32
36
|
def create_composite_operation_filter(
|
@@ -114,6 +118,10 @@ def ready_operations_filter_factory(
|
|
114
118
|
ReadyOperationsFilterType.NON_IMMEDIATE_MACHINES: (
|
115
119
|
filter_non_immediate_machines
|
116
120
|
),
|
121
|
+
ReadyOperationsFilterType.NON_IDLE_MACHINES: filter_non_idle_machines,
|
122
|
+
ReadyOperationsFilterType.NON_IMMEDIATE_OPERATIONS: (
|
123
|
+
filter_non_immediate_operations
|
124
|
+
),
|
117
125
|
}
|
118
126
|
|
119
127
|
if filter_name not in filtering_strategies:
|
@@ -15,6 +15,86 @@ ReadyOperationsFilter = Callable[
|
|
15
15
|
]
|
16
16
|
|
17
17
|
|
18
|
+
def filter_non_idle_machines(
|
19
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
20
|
+
) -> list[Operation]:
|
21
|
+
"""Filters out all the operations associated with non-idle machines.
|
22
|
+
|
23
|
+
A machine is considered idle if there are no ongoing operations
|
24
|
+
currently scheduled on it. This filter removes operations that are
|
25
|
+
associated with machines that are busy (i.e., have at least one
|
26
|
+
uncompleted operation).
|
27
|
+
|
28
|
+
Utilizes :meth:``Dispatcher.ongoing_operations()`` to determine machine
|
29
|
+
statuses.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
dispatcher: The dispatcher object.
|
33
|
+
operations: The list of operations to filter.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
The list of operations that are associated with idle machines.
|
37
|
+
"""
|
38
|
+
current_time = dispatcher.min_start_time(operations)
|
39
|
+
non_idle_machines = _get_non_idle_machines(dispatcher, current_time)
|
40
|
+
|
41
|
+
# Filter operations to keep those that are associated with at least one
|
42
|
+
# idle machine
|
43
|
+
filtered_operations: list[Operation] = []
|
44
|
+
for operation in operations:
|
45
|
+
if all(
|
46
|
+
machine_id in non_idle_machines
|
47
|
+
for machine_id in operation.machines
|
48
|
+
):
|
49
|
+
continue
|
50
|
+
filtered_operations.append(operation)
|
51
|
+
|
52
|
+
return filtered_operations
|
53
|
+
|
54
|
+
|
55
|
+
def _get_non_idle_machines(
|
56
|
+
dispatcher: Dispatcher, current_time: int
|
57
|
+
) -> set[int]:
|
58
|
+
"""Returns the set of machine ids that are currently busy (i.e., have at
|
59
|
+
least one uncompleted operation)."""
|
60
|
+
|
61
|
+
non_idle_machines = set()
|
62
|
+
for machine_schedule in dispatcher.schedule.schedule:
|
63
|
+
for scheduled_operation in reversed(machine_schedule):
|
64
|
+
is_completed = scheduled_operation.end_time <= current_time
|
65
|
+
if is_completed:
|
66
|
+
break
|
67
|
+
non_idle_machines.add(scheduled_operation.machine_id)
|
68
|
+
|
69
|
+
return non_idle_machines
|
70
|
+
|
71
|
+
|
72
|
+
def filter_non_immediate_operations(
|
73
|
+
dispatcher: Dispatcher, operations: list[Operation]
|
74
|
+
) -> list[Operation]:
|
75
|
+
"""Filters out all the operations that can't start immediately.
|
76
|
+
|
77
|
+
An operation can start immediately if its earliest start time is the
|
78
|
+
current time.
|
79
|
+
|
80
|
+
The current time is determined by the minimum start time of the
|
81
|
+
operations.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
dispatcher: The dispatcher object.
|
85
|
+
operations: The list of operations to filter.
|
86
|
+
"""
|
87
|
+
|
88
|
+
min_start_time = dispatcher.min_start_time(operations)
|
89
|
+
immediate_operations: list[Operation] = []
|
90
|
+
for operation in operations:
|
91
|
+
start_time = dispatcher.earliest_start_time(operation)
|
92
|
+
if start_time == min_start_time:
|
93
|
+
immediate_operations.append(operation)
|
94
|
+
|
95
|
+
return immediate_operations
|
96
|
+
|
97
|
+
|
18
98
|
def filter_dominated_operations(
|
19
99
|
dispatcher: Dispatcher, operations: list[Operation]
|
20
100
|
) -> list[Operation]:
|
@@ -1,5 +1,37 @@
|
|
1
|
-
"""Contains FeatureObserver classes for observing features of the
|
2
|
-
dispatcher.
|
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
|
+
"""
|
3
35
|
|
4
36
|
from ._feature_observer import FeatureObserver, FeatureType
|
5
37
|
from ._earliest_start_time_observer import EarliestStartTimeObserver
|
@@ -24,19 +24,54 @@ from job_shop_lib.dispatching.feature_observers import (
|
|
24
24
|
|
25
25
|
class CompositeFeatureObserver(FeatureObserver):
|
26
26
|
"""Aggregates features from other FeatureObserver instances subscribed to
|
27
|
-
the same
|
28
|
-
first axis (horizontal concatenation).
|
29
|
-
|
30
|
-
|
27
|
+
the same :class:`~job_shop_lib.dispatching.Dispatcher` by concatenating
|
28
|
+
their feature matrices along the first axis (horizontal concatenation).
|
29
|
+
|
30
|
+
It provides also a custom ``__str__`` method to display the features
|
31
|
+
in a more readable way.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
dispatcher:
|
35
|
+
The :class:`~job_shop_lib.dispatching.Dispatcher` to observe.
|
36
|
+
subscribe:
|
37
|
+
If ``True``, the observer is subscribed to the dispatcher upon
|
38
|
+
initialization. Otherwise, the observer must be subscribed later
|
39
|
+
or manually updated.
|
40
|
+
feature_types:
|
41
|
+
A list of :class:`FeatureType` or a single :class:`FeatureType`
|
42
|
+
that specifies the types of features to observe. They must be a
|
43
|
+
subset of the class attribute :attr:`supported_feature_types`.
|
44
|
+
If ``None``, all supported feature types are tracked.
|
31
45
|
feature_observers:
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
46
|
+
A list of `FeatureObserver` instances to aggregate features from.
|
47
|
+
If ``None``, all feature observers subscribed to the dispatcher are
|
48
|
+
used.
|
49
|
+
|
50
|
+
.. seealso::
|
51
|
+
|
52
|
+
An example using this class can be found in the
|
53
|
+
:doc:`../examples/08-Feature-Observers` example.
|
54
|
+
|
55
|
+
Additionally, the class
|
56
|
+
:class:`~job_shop_lib.reinforcement_learning.SingleJobShopGraphEnv`
|
57
|
+
uses this feature observer to aggregate features from multiple
|
58
|
+
ones.
|
59
|
+
|
38
60
|
"""
|
39
61
|
|
62
|
+
__slots__ = {
|
63
|
+
"feature_observers": (
|
64
|
+
"List of :class:`FeatureObserver` instances to aggregate features "
|
65
|
+
"from."
|
66
|
+
),
|
67
|
+
"column_names": (
|
68
|
+
"Dictionary mapping :class:`FeatureType` to a list of column "
|
69
|
+
"names for the corresponding feature matrix. They are generated "
|
70
|
+
"based on the class name of the :class:`FeatureObserver` instance "
|
71
|
+
"that produced the feature."
|
72
|
+
),
|
73
|
+
}
|
74
|
+
|
40
75
|
def __init__(
|
41
76
|
self,
|
42
77
|
dispatcher: Dispatcher,
|
@@ -140,7 +175,8 @@ class CompositeFeatureObserver(FeatureObserver):
|
|
140
175
|
|
141
176
|
|
142
177
|
if __name__ == "__main__":
|
143
|
-
from cProfile import Profile
|
178
|
+
# from cProfile import Profile
|
179
|
+
import time
|
144
180
|
from job_shop_lib.benchmarking import load_benchmark_instance
|
145
181
|
from job_shop_lib.dispatching.rules import DispatchingRuleSolver
|
146
182
|
from job_shop_lib.dispatching.feature_observers import (
|
@@ -164,6 +200,10 @@ if __name__ == "__main__":
|
|
164
200
|
dispatcher_, feature_observers=feature_observers_
|
165
201
|
)
|
166
202
|
solver = DispatchingRuleSolver(dispatching_rule="random")
|
167
|
-
profiler = Profile()
|
168
|
-
profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
169
|
-
profiler.print_stats("cumtime")
|
203
|
+
# profiler = Profile()
|
204
|
+
# profiler.runcall(solver.solve, dispatcher_.instance, dispatcher_)
|
205
|
+
# profiler.print_stats("cumtime")
|
206
|
+
start = time.perf_counter()
|
207
|
+
solver.solve(dispatcher_.instance, dispatcher_)
|
208
|
+
end = time.perf_counter()
|
209
|
+
print(f"Time: {end - start}")
|