job-shop-lib 1.0.0a1__py3-none-any.whl → 1.0.0a3__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/_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}")
|