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.
Files changed (37) hide show
  1. job_shop_lib/_job_shop_instance.py +18 -20
  2. job_shop_lib/_operation.py +30 -24
  3. job_shop_lib/_schedule.py +17 -16
  4. job_shop_lib/_scheduled_operation.py +10 -12
  5. job_shop_lib/constraint_programming/_ortools_solver.py +31 -16
  6. job_shop_lib/dispatching/__init__.py +4 -0
  7. job_shop_lib/dispatching/_dispatcher.py +24 -32
  8. job_shop_lib/dispatching/_factories.py +8 -0
  9. job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
  10. job_shop_lib/dispatching/feature_observers/__init__.py +34 -2
  11. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +54 -14
  12. job_shop_lib/dispatching/feature_observers/_duration_observer.py +15 -2
  13. job_shop_lib/dispatching/feature_observers/_earliest_start_time_observer.py +62 -10
  14. job_shop_lib/dispatching/feature_observers/_factory.py +5 -1
  15. job_shop_lib/dispatching/feature_observers/_feature_observer.py +87 -16
  16. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +32 -2
  17. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +3 -3
  18. job_shop_lib/dispatching/feature_observers/_is_scheduled_observer.py +9 -5
  19. job_shop_lib/dispatching/feature_observers/_position_in_job_observer.py +7 -2
  20. job_shop_lib/dispatching/feature_observers/_remaining_operations_observer.py +1 -1
  21. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +66 -43
  22. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
  23. job_shop_lib/graphs/__init__.py +2 -0
  24. job_shop_lib/graphs/_build_agent_task_graph.py +2 -2
  25. job_shop_lib/graphs/_constants.py +18 -1
  26. job_shop_lib/graphs/_job_shop_graph.py +36 -20
  27. job_shop_lib/graphs/_node.py +60 -52
  28. job_shop_lib/graphs/graph_updaters/__init__.py +11 -1
  29. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +1 -1
  30. job_shop_lib/visualization/__init__.py +5 -5
  31. job_shop_lib/visualization/_gantt_chart_creator.py +5 -5
  32. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +63 -36
  33. job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
  34. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/METADATA +15 -3
  35. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/RECORD +37 -37
  36. {job_shop_lib-1.0.0a1.dist-info → job_shop_lib-1.0.0a3.dist-info}/WHEEL +1 -1
  37. {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
- """Initializes the instance based on a list of lists of operations.
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."""
@@ -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
- Attributes:
27
- machines: A list of machine ids that can perform the operation.
28
- duration: The time it takes to perform the operation.
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
- "duration",
34
- "job_id",
35
- "position_in_job",
36
- "operation_id",
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
- """Initializes the object with the given machines and duration.
41
-
42
- Args:
43
- machines:
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: If the operation has multiple machines
64
- in its list.
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
- "_schedule",
41
- "metadata",
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
- Attributes:
11
- operation:
12
- The :class:`Operation` object that is scheduled.
13
- start_time:
14
- The time at which the operation is scheduled to start.
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 (``bool``):
25
+ log_search_progress (bool):
26
26
  Whether to log the search progress to the console.
27
- max_time_in_seconds (``float | None``):
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 (``cp_model.CpModel``):
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 (``cp_model.CpSolver``):
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
- Attributes:
159
+ Args:
160
160
  instance:
161
- The instance of the job shop problem to be scheduled.
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 be
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
- "subscribers",
177
- "_cache",
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.ready_operations()
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 ready_operations(self) -> list[Operation]:
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.ready_operations()
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.ready_operations()
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 `Dispatcher` by concatenating their feature matrices along the
28
- first axis (horizontal concatenation).
29
-
30
- Attributes:
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
- List of `FeatureObserver` instances to aggregate features from.
33
- column_names:
34
- Dictionary mapping `FeatureType` to a list of column names for the
35
- corresponding feature matrix. Column names are generated based on
36
- the class name of the `FeatureObserver` instance that produced the
37
- feature.
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}")