job-shop-lib 1.0.0a2__py3-none-any.whl → 1.0.0a4__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 +119 -55
  2. job_shop_lib/_operation.py +18 -7
  3. job_shop_lib/_schedule.py +13 -15
  4. job_shop_lib/_scheduled_operation.py +17 -18
  5. job_shop_lib/dispatching/__init__.py +4 -0
  6. job_shop_lib/dispatching/_dispatcher.py +36 -47
  7. job_shop_lib/dispatching/_dispatcher_observer_config.py +15 -2
  8. job_shop_lib/dispatching/_factories.py +10 -2
  9. job_shop_lib/dispatching/_ready_operation_filters.py +80 -0
  10. job_shop_lib/dispatching/feature_observers/_composite_feature_observer.py +0 -1
  11. job_shop_lib/dispatching/feature_observers/_factory.py +21 -18
  12. job_shop_lib/dispatching/feature_observers/_is_completed_observer.py +1 -0
  13. job_shop_lib/dispatching/feature_observers/_is_ready_observer.py +1 -1
  14. job_shop_lib/dispatching/rules/_dispatching_rule_solver.py +44 -25
  15. job_shop_lib/dispatching/rules/_dispatching_rules_functions.py +9 -9
  16. job_shop_lib/generation/_general_instance_generator.py +33 -34
  17. job_shop_lib/generation/_instance_generator.py +14 -17
  18. job_shop_lib/generation/_transformations.py +11 -8
  19. job_shop_lib/graphs/__init__.py +3 -0
  20. job_shop_lib/graphs/_build_disjunctive_graph.py +41 -3
  21. job_shop_lib/graphs/graph_updaters/_graph_updater.py +11 -13
  22. job_shop_lib/graphs/graph_updaters/_residual_graph_updater.py +17 -20
  23. job_shop_lib/reinforcement_learning/__init__.py +16 -7
  24. job_shop_lib/reinforcement_learning/_multi_job_shop_graph_env.py +69 -57
  25. job_shop_lib/reinforcement_learning/_single_job_shop_graph_env.py +43 -32
  26. job_shop_lib/reinforcement_learning/_types_and_constants.py +2 -2
  27. job_shop_lib/visualization/__init__.py +29 -10
  28. job_shop_lib/visualization/_gantt_chart_creator.py +122 -84
  29. job_shop_lib/visualization/_gantt_chart_video_and_gif_creation.py +68 -37
  30. job_shop_lib/visualization/_plot_disjunctive_graph.py +382 -0
  31. job_shop_lib/visualization/{_gantt_chart.py → _plot_gantt_chart.py} +78 -14
  32. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/METADATA +15 -3
  33. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/RECORD +36 -36
  34. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/WHEEL +1 -1
  35. job_shop_lib/visualization/_disjunctive_graph.py +0 -210
  36. /job_shop_lib/visualization/{_agent_task_graph.py → _plot_agent_task_graph.py} +0 -0
  37. {job_shop_lib-1.0.0a2.dist-info → job_shop_lib-1.0.0a4.dist-info}/LICENSE +0 -0
@@ -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]:
@@ -193,7 +193,6 @@ if __name__ == "__main__":
193
193
  dispatcher=dispatcher_,
194
194
  )
195
195
  for observer_type in feature_observer_types_
196
- if not observer_type == FeatureObserverType.COMPOSITE
197
196
  # and not FeatureObserverType.EARLIEST_START_TIME
198
197
  ]
199
198
  composite_observer_ = CompositeFeatureObserver(
@@ -1,4 +1,4 @@
1
- """Contains factory functions for creating node feature encoders."""
1
+ """Contains factory functions for creating :class:`FeatureObserver`s."""
2
2
 
3
3
  from enum import Enum
4
4
 
@@ -18,8 +18,12 @@ from job_shop_lib.dispatching.feature_observers import (
18
18
  class FeatureObserverType(str, Enum):
19
19
  """Enumeration of the different feature observers.
20
20
 
21
- Each feature observer is associated with a string value that can be used
22
- to create the feature observer using the factory function.
21
+ Each :class:`FeatureObserver` is associated with a string value that can be
22
+ used to create the :class:`FeatureObserver` using the factory function.
23
+
24
+ It does not include the :class:`CompositeFeatureObserver` class since this
25
+ observer is often managed separately from the others. For example, a
26
+ common use case is to create a list of feature observers and pass them to
23
27
  """
24
28
 
25
29
  IS_READY = "is_ready"
@@ -29,7 +33,6 @@ class FeatureObserverType(str, Enum):
29
33
  POSITION_IN_JOB = "position_in_job"
30
34
  REMAINING_OPERATIONS = "remaining_operations"
31
35
  IS_COMPLETED = "is_completed"
32
- COMPOSITE = "composite"
33
36
 
34
37
 
35
38
  # FeatureObserverConfig = DispatcherObserverConfig[
@@ -43,7 +46,7 @@ FeatureObserverConfig = (
43
46
 
44
47
 
45
48
  def feature_observer_factory(
46
- feature_creator_type: (
49
+ feature_observer_type: (
47
50
  str
48
51
  | FeatureObserverType
49
52
  | type[FeatureObserver]
@@ -51,29 +54,29 @@ def feature_observer_factory(
51
54
  ),
52
55
  **kwargs,
53
56
  ) -> FeatureObserver:
54
- """Creates and returns a node feature creator based on the specified
55
- node feature creator type.
57
+ """Creates and returns a :class:`FeatureObserver` based on the specified
58
+ :class:`FeatureObserver` type.
56
59
 
57
60
  Args:
58
61
  feature_creator_type:
59
- The type of node feature creator to create.
62
+ The type of :class:`FeatureObserver` to create.
60
63
  **kwargs:
61
- Additional keyword arguments to pass to the node
62
- feature creator constructor.
64
+ Additional keyword arguments to pass to the
65
+ :class:`FeatureObserver` constructor.
63
66
 
64
67
  Returns:
65
- A node feature creator instance.
68
+ A :class:`FeatureObserver` instance.
66
69
  """
67
- if isinstance(feature_creator_type, DispatcherObserverConfig):
70
+ if isinstance(feature_observer_type, DispatcherObserverConfig):
68
71
  return feature_observer_factory(
69
- feature_creator_type.class_type,
70
- **feature_creator_type.kwargs,
72
+ feature_observer_type.class_type,
73
+ **feature_observer_type.kwargs,
71
74
  **kwargs,
72
75
  )
73
76
  # if the instance is of type type[FeatureObserver] we can just
74
77
  # call the object constructor with the keyword arguments
75
- if isinstance(feature_creator_type, type):
76
- return feature_creator_type(**kwargs)
78
+ if isinstance(feature_observer_type, type):
79
+ return feature_observer_type(**kwargs)
77
80
 
78
81
  mapping: dict[FeatureObserverType, type[FeatureObserver]] = {
79
82
  FeatureObserverType.IS_READY: IsReadyObserver,
@@ -84,5 +87,5 @@ def feature_observer_factory(
84
87
  FeatureObserverType.REMAINING_OPERATIONS: RemainingOperationsObserver,
85
88
  FeatureObserverType.IS_COMPLETED: IsCompletedObserver,
86
89
  }
87
- feature_creator = mapping[feature_creator_type] # type: ignore[index]
88
- return feature_creator(**kwargs)
90
+ feature_observer = mapping[feature_observer_type] # type: ignore[index]
91
+ return feature_observer(**kwargs)
@@ -51,6 +51,7 @@ class IsCompletedObserver(FeatureObserver):
51
51
  def __init__(
52
52
  self,
53
53
  dispatcher: Dispatcher,
54
+ *,
54
55
  feature_types: list[FeatureType] | FeatureType | None = None,
55
56
  subscribe: bool = True,
56
57
  ):
@@ -29,5 +29,5 @@ class IsReadyObserver(FeatureObserver):
29
29
  self.initialize_features()
30
30
 
31
31
  def _get_ready_operations(self) -> list[int]:
32
- available_operations = self.dispatcher.ready_operations()
32
+ available_operations = self.dispatcher.available_operations()
33
33
  return [operation.operation_id for operation in available_operations]
@@ -1,12 +1,14 @@
1
1
  """Home of the `DispatchingRuleSolver` class."""
2
2
 
3
- from collections.abc import Callable
3
+ from collections.abc import Callable, Iterable
4
4
 
5
5
  from job_shop_lib import JobShopInstance, Schedule, Operation, BaseSolver
6
6
  from job_shop_lib.dispatching import (
7
7
  ready_operations_filter_factory,
8
8
  Dispatcher,
9
9
  ReadyOperationsFilterType,
10
+ ReadyOperationsFilter,
11
+ create_composite_operation_filter,
10
12
  )
11
13
  from job_shop_lib.dispatching.rules import (
12
14
  dispatching_rule_factory,
@@ -30,6 +32,35 @@ class DispatchingRuleSolver(BaseSolver):
30
32
  pruning_function:
31
33
  The pruning function to use. It is used to initialize the
32
34
  dispatcher object internally when calling the solve method.
35
+
36
+ Args:
37
+ dispatching_rule:
38
+ The dispatching rule to use. It can be a string with the name
39
+ of the dispatching rule, a class`DispatchingRuleType` enum member,
40
+ or a callable that takes a dispatcher and returns the operation to
41
+ be dispatched next.
42
+ machine_chooser:
43
+ The machine chooser to use. It can be a string with the name
44
+ of the machine chooser, a :class:`MachineChooserType` member, or a
45
+ callable that takes a dispatcher and an operation and returns
46
+ the machine id where the operation will be dispatched.
47
+ ready_operations_filter:
48
+ The ready operations filter to use. It can be either:
49
+
50
+ - a string with the name of the pruning function
51
+ - a :class`ReadyOperationsFilterType` enum member.
52
+ - a callable that takes a dispatcher and a list of operations
53
+ and returns a list of operations that should be considered
54
+ for dispatching,
55
+ - a list with names or actual ready operations filters to be used.
56
+ If a list is provided, a composite filter will be created
57
+ using the specified filters.
58
+
59
+ .. seealso::
60
+ - :func:`job_shop_lib.dispatching.rules.dispatching_rule_factory`
61
+ - :func:`job_shop_lib.dispatching.rules.machine_chooser_factory`
62
+ - :func:`~job_shop_lib.dispatching.ready_operations_filter_factory`
63
+ - :func:`~job_shop_lib.dispatching.create_composite_operation_filter`
33
64
  """
34
65
 
35
66
  def __init__(
@@ -41,32 +72,16 @@ class DispatchingRuleSolver(BaseSolver):
41
72
  str | Callable[[Dispatcher, Operation], int]
42
73
  ) = MachineChooserType.FIRST,
43
74
  ready_operations_filter: (
44
- str
45
- | Callable[[Dispatcher, list[Operation]], list[Operation]]
75
+ Iterable[ReadyOperationsFilter | str | ReadyOperationsFilterType]
76
+ | str
77
+ | ReadyOperationsFilterType
78
+ | ReadyOperationsFilter
46
79
  | None
47
- ) = ReadyOperationsFilterType.DOMINATED_OPERATIONS,
80
+ ) = (
81
+ ReadyOperationsFilterType.DOMINATED_OPERATIONS,
82
+ ReadyOperationsFilterType.NON_IDLE_MACHINES,
83
+ ),
48
84
  ):
49
- """Initializes the solver with the given dispatching rule, machine
50
- chooser and pruning function.
51
-
52
- Args:
53
- dispatching_rule:
54
- The dispatching rule to use. It can be a string with the name
55
- of the dispatching rule, a DispatchingRule enum member, or a
56
- callable that takes a dispatcher and returns the operation to
57
- be dispatched next.
58
- machine_chooser:
59
- The machine chooser to use. It can be a string with the name
60
- of the machine chooser, a MachineChooser enum member, or a
61
- callable that takes a dispatcher and an operation and returns
62
- the machine id where the operation will be dispatched.
63
- ready_operations_filter:
64
- The ready operations filter to use. It can be a string with
65
- the name of the pruning function, a PruningFunction enum
66
- member, or a callable that takes a dispatcher and a list of
67
- operations and returns a list of operations that should be
68
- considered for dispatching.
69
- """
70
85
  if isinstance(dispatching_rule, str):
71
86
  dispatching_rule = dispatching_rule_factory(dispatching_rule)
72
87
  if isinstance(machine_chooser, str):
@@ -75,6 +90,10 @@ class DispatchingRuleSolver(BaseSolver):
75
90
  ready_operations_filter = ready_operations_filter_factory(
76
91
  ready_operations_filter
77
92
  )
93
+ if isinstance(ready_operations_filter, Iterable):
94
+ ready_operations_filter = create_composite_operation_filter(
95
+ ready_operations_filter
96
+ )
78
97
 
79
98
  self.dispatching_rule = dispatching_rule
80
99
  self.machine_chooser = machine_chooser
@@ -21,7 +21,7 @@ from job_shop_lib.dispatching.feature_observers import (
21
21
  def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
22
22
  """Dispatches the operation with the shortest duration."""
23
23
  return min(
24
- dispatcher.ready_operations(),
24
+ dispatcher.available_operations(),
25
25
  key=lambda operation: operation.duration,
26
26
  )
27
27
 
@@ -29,7 +29,7 @@ def shortest_processing_time_rule(dispatcher: Dispatcher) -> Operation:
29
29
  def first_come_first_served_rule(dispatcher: Dispatcher) -> Operation:
30
30
  """Dispatches the operation with the lowest position in job."""
31
31
  return min(
32
- dispatcher.ready_operations(),
32
+ dispatcher.available_operations(),
33
33
  key=lambda operation: operation.position_in_job,
34
34
  )
35
35
 
@@ -41,7 +41,7 @@ def most_work_remaining_rule(dispatcher: Dispatcher) -> Operation:
41
41
  job_remaining_work[operation.job_id] += operation.duration
42
42
 
43
43
  return max(
44
- dispatcher.ready_operations(),
44
+ dispatcher.available_operations(),
45
45
  key=lambda operation: job_remaining_work[operation.job_id],
46
46
  )
47
47
 
@@ -53,14 +53,14 @@ def most_operations_remaining_rule(dispatcher: Dispatcher) -> Operation:
53
53
  job_remaining_operations[operation.job_id] += 1
54
54
 
55
55
  return max(
56
- dispatcher.ready_operations(),
56
+ dispatcher.available_operations(),
57
57
  key=lambda operation: job_remaining_operations[operation.job_id],
58
58
  )
59
59
 
60
60
 
61
61
  def random_operation_rule(dispatcher: Dispatcher) -> Operation:
62
62
  """Dispatches a random operation."""
63
- return random.choice(dispatcher.ready_operations())
63
+ return random.choice(dispatcher.available_operations())
64
64
 
65
65
 
66
66
  def score_based_rule(
@@ -80,7 +80,7 @@ def score_based_rule(
80
80
  def rule(dispatcher: Dispatcher) -> Operation:
81
81
  scores = score_function(dispatcher)
82
82
  return max(
83
- dispatcher.ready_operations(),
83
+ dispatcher.available_operations(),
84
84
  key=lambda operation: scores[operation.job_id],
85
85
  )
86
86
 
@@ -102,7 +102,7 @@ def score_based_rule_with_tie_breaker(
102
102
  """
103
103
 
104
104
  def rule(dispatcher: Dispatcher) -> Operation:
105
- candidates = dispatcher.ready_operations()
105
+ candidates = dispatcher.available_operations()
106
106
  for scoring_function in score_functions:
107
107
  scores = scoring_function(dispatcher)
108
108
  best_score = max(scores)
@@ -126,7 +126,7 @@ def shortest_processing_time_score(dispatcher: Dispatcher) -> list[int]:
126
126
  """Scores each job based on the duration of the next operation."""
127
127
  num_jobs = dispatcher.instance.num_jobs
128
128
  scores = [0] * num_jobs
129
- for operation in dispatcher.ready_operations():
129
+ for operation in dispatcher.available_operations():
130
130
  scores[operation.job_id] = -operation.duration
131
131
  return scores
132
132
 
@@ -135,7 +135,7 @@ def first_come_first_served_score(dispatcher: Dispatcher) -> list[int]:
135
135
  """Scores each job based on the position of the next operation."""
136
136
  num_jobs = dispatcher.instance.num_jobs
137
137
  scores = [0] * num_jobs
138
- for operation in dispatcher.ready_operations():
138
+ for operation in dispatcher.available_operations():
139
139
  scores[operation.job_id] = operation.operation_id
140
140
  return scores
141
141
 
@@ -17,36 +17,58 @@ class GeneralInstanceGenerator(InstanceGenerator):
17
17
  durations, and more.
18
18
 
19
19
  The class supports both single instance generation and iteration over
20
- multiple instances, controlled by the `iteration_limit` parameter. It
21
- implements the iterator protocol, allowing it to be used in a `for` loop.
20
+ multiple instances, controlled by the ``iteration_limit`` parameter. It
21
+ implements the iterator protocol, allowing it to be used in a ``for`` loop.
22
22
 
23
23
  Note:
24
24
  When used as an iterator, the generator will produce instances until it
25
- reaches the specified `iteration_limit`. If `iteration_limit` is None,
26
- it will continue indefinitely.
25
+ reaches the specified ``iteration_limit``. If ``iteration_limit`` is
26
+ ``None``, it will continue indefinitely.
27
27
 
28
28
  Attributes:
29
29
  num_jobs_range:
30
30
  The range of the number of jobs to generate. If a single
31
- int is provided, it is used as both the minimum and maximum.
31
+ ``int`` is provided, it is used as both the minimum and maximum.
32
32
  duration_range:
33
33
  The range of durations for each operation.
34
34
  num_machines_range:
35
35
  The range of the number of machines available. If a
36
- single int is provided, it is used as both the minimum and maximum.
36
+ single ``int`` is provided, it is used as both the minimum and
37
+ maximum.
37
38
  machines_per_operation:
38
39
  Specifies how many machines each operation
39
- can be assigned to. If a single int is provided, it is used for
40
+ can be assigned to. If a single ``int`` is provided, it is used for
40
41
  all operations.
41
42
  allow_less_jobs_than_machines:
42
- If True, allows generating instances where the number of jobs is
43
- less than the number of machines.
43
+ If ``True``, allows generating instances where the number of jobs
44
+ is less than the number of machines.
44
45
  allow_recirculation:
45
- If True, a job can visit the same machine more than once.
46
+ If ``True``, a job can visit the same machine more than once.
46
47
  name_suffix:
47
48
  A suffix to append to each instance's name for identification.
48
49
  seed:
49
50
  Seed for the random number generator to ensure reproducibility.
51
+
52
+ Args:
53
+ num_jobs:
54
+ The range of the number of jobs to generate.
55
+ num_machines:
56
+ The range of the number of machines available.
57
+ duration_range:
58
+ The range of durations for each operation.
59
+ allow_less_jobs_than_machines:
60
+ Allows instances with fewer jobs than machines.
61
+ allow_recirculation:
62
+ Allows jobs to visit the same machine multiple times.
63
+ machines_per_operation:
64
+ Specifies how many machines each operation can be assigned to.
65
+ If a single ``int`` is provided, it is used for all operations.
66
+ name_suffix:
67
+ Suffix for instance names.
68
+ seed:
69
+ Seed for the random number generator.
70
+ iteration_limit:
71
+ Maximum number of instances to generate in iteration mode.
50
72
  """
51
73
 
52
74
  def __init__( # pylint: disable=too-many-arguments
@@ -61,29 +83,6 @@ class GeneralInstanceGenerator(InstanceGenerator):
61
83
  seed: int | None = None,
62
84
  iteration_limit: int | None = None,
63
85
  ):
64
- """Initializes the instance generator with the given parameters.
65
-
66
- Args:
67
- num_jobs:
68
- The range of the number of jobs to generate.
69
- num_machines:
70
- The range of the number of machines available.
71
- duration_range:
72
- The range of durations for each operation.
73
- allow_less_jobs_than_machines:
74
- Allows instances with fewer jobs than machines.
75
- allow_recirculation:
76
- Allows jobs to visit the same machine multiple times.
77
- machines_per_operation:
78
- Specifies how many machines each operation can be assigned to.
79
- If a single int is provided, it is used for all operations.
80
- name_suffix:
81
- Suffix for instance names.
82
- seed:
83
- Seed for the random number generator.
84
- iteration_limit:
85
- Maximum number of instances to generate in iteration mode.
86
- """
87
86
  super().__init__(
88
87
  num_jobs=num_jobs,
89
88
  num_machines=num_machines,
@@ -153,7 +152,7 @@ class GeneralInstanceGenerator(InstanceGenerator):
153
152
  Args:
154
153
  available_machines:
155
154
  A list of available machine_ids to choose from.
156
- If None, all machines are available.
155
+ If ``None``, all machines are available.
157
156
  """
158
157
  duration = random.randint(*self.duration_range)
159
158
 
@@ -32,6 +32,20 @@ class InstanceGenerator(abc.ABC):
32
32
  A suffix to append to each instance's name for identification.
33
33
  seed:
34
34
  Seed for the random number generator to ensure reproducibility.
35
+
36
+ Args:
37
+ num_jobs:
38
+ The range of the number of jobs to generate.
39
+ num_machines:
40
+ The range of the number of machines available.
41
+ duration_range:
42
+ The range of durations for each operation.
43
+ name_suffix:
44
+ Suffix for instance names.
45
+ seed:
46
+ Seed for the random number generator.
47
+ iteration_limit:
48
+ Maximum number of instances to generate in iteration mode.
35
49
  """
36
50
 
37
51
  def __init__( # pylint: disable=too-many-arguments
@@ -43,23 +57,6 @@ class InstanceGenerator(abc.ABC):
43
57
  seed: int | None = None,
44
58
  iteration_limit: int | None = None,
45
59
  ):
46
- """Initializes the instance generator with the given parameters.
47
-
48
- Args:
49
- num_jobs:
50
- The range of the number of jobs to generate.
51
- num_machines:
52
- The range of the number of machines available.
53
- duration_range:
54
- The range of durations for each operation.
55
- name_suffix:
56
- Suffix for instance names.
57
- seed:
58
- Seed for the random number generator.
59
- iteration_limit:
60
- Maximum number of instances to generate in iteration mode.
61
- """
62
-
63
60
  if isinstance(num_jobs, int):
64
61
  num_jobs = (num_jobs, num_jobs)
65
62
  if isinstance(num_machines, int):
@@ -111,7 +111,17 @@ class AddDurationNoise(Transformation):
111
111
 
112
112
  class RemoveJobs(Transformation):
113
113
  """Removes jobs randomly until the number of jobs is within a specified
114
- range."""
114
+ range.
115
+
116
+ Args:
117
+ min_jobs:
118
+ The minimum number of jobs to remain in the instance.
119
+ max_jobs:
120
+ The maximum number of jobs to remain in the instance.
121
+ target_jobs:
122
+ If specified, the number of jobs to remain in the
123
+ instance. Overrides ``min_jobs`` and ``max_jobs``.
124
+ """
115
125
 
116
126
  def __init__(
117
127
  self,
@@ -120,13 +130,6 @@ class RemoveJobs(Transformation):
120
130
  target_jobs: int | None = None,
121
131
  suffix: str | None = None,
122
132
  ):
123
- """
124
- Args:
125
- min_jobs: The minimum number of jobs to remain in the instance.
126
- max_jobs: The maximum number of jobs to remain in the instance.
127
- target_jobs: If specified, the number of jobs to remain in the
128
- instance. Overrides min_jobs and max_jobs.
129
- """
130
133
  if suffix is None:
131
134
  suffix = f"_jobs={min_jobs}-{max_jobs}"
132
135
  super().__init__(suffix=suffix)
@@ -7,6 +7,7 @@ The main classes and functions available in this package are:
7
7
  Node
8
8
  NodeType
9
9
  build_disjunctive_graph
10
+ build_solved_disjunctive_graph
10
11
  build_agent_task_graph
11
12
  build_complete_agent_task_graph
12
13
  build_agent_task_graph_with_jobs
@@ -18,6 +19,7 @@ from job_shop_lib.graphs._node import Node
18
19
  from job_shop_lib.graphs._job_shop_graph import JobShopGraph, NODE_ATTR
19
20
  from job_shop_lib.graphs._build_disjunctive_graph import (
20
21
  build_disjunctive_graph,
22
+ build_solved_disjunctive_graph,
21
23
  add_disjunctive_edges,
22
24
  add_conjunctive_edges,
23
25
  add_source_sink_nodes,
@@ -62,4 +64,5 @@ __all__ = [
62
64
  "add_global_node",
63
65
  "add_machine_global_edges",
64
66
  "add_job_global_edges",
67
+ "build_solved_disjunctive_graph",
65
68
  ]
@@ -18,14 +18,15 @@ each disjunctive edge such that the overall processing time is minimized.
18
18
 
19
19
  import itertools
20
20
 
21
- from job_shop_lib import JobShopInstance
21
+ from job_shop_lib import JobShopInstance, Schedule
22
22
  from job_shop_lib.graphs import JobShopGraph, EdgeType, NodeType, Node
23
23
 
24
24
 
25
25
  def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
26
26
  """Builds and returns a disjunctive graph for the given job shop instance.
27
27
 
28
- This function creates a complete disjunctive graph from a JobShopInstance.
28
+ This function creates a complete disjunctive graph from a
29
+ :JobShopInstance.
29
30
  It starts by initializing a JobShopGraph object and proceeds by adding
30
31
  disjunctive edges between operations using the same machine, conjunctive
31
32
  edges between successive operations in the same job, and finally, special
@@ -40,7 +41,7 @@ def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
40
41
  the graph.
41
42
 
42
43
  Returns:
43
- JobShopGraph: A JobShopGraph object representing the disjunctive graph
44
+ A :class:`JobShopGraph` object representing the disjunctive graph
44
45
  of the job shop scheduling problem.
45
46
  """
46
47
  graph = JobShopGraph(instance)
@@ -51,6 +52,43 @@ def build_disjunctive_graph(instance: JobShopInstance) -> JobShopGraph:
51
52
  return graph
52
53
 
53
54
 
55
+ def build_solved_disjunctive_graph(schedule: Schedule) -> JobShopGraph:
56
+ """Builds and returns a disjunctive graph for the given solved schedule.
57
+
58
+ This function constructs a disjunctive graph from the given schedule,
59
+ keeping only the disjunctive edges that represent the chosen ordering
60
+ of operations on each machine as per the solution schedule.
61
+
62
+ Args:
63
+ schedule (Schedule): The solved schedule that contains the sequencing
64
+ of operations on each machine.
65
+
66
+ Returns:
67
+ A JobShopGraph object representing the disjunctive graph
68
+ of the solved job shop scheduling problem.
69
+ """
70
+ # Build the base disjunctive graph from the job shop instance
71
+ graph = JobShopGraph(schedule.instance)
72
+ add_conjunctive_edges(graph)
73
+ add_source_sink_nodes(graph)
74
+ add_source_sink_edges(graph)
75
+
76
+ # Iterate over each machine and add only the edges that match the solution
77
+ # order
78
+ for machine_schedule in schedule.schedule:
79
+ for i, scheduled_operation in enumerate(machine_schedule):
80
+ if i + 1 >= len(machine_schedule):
81
+ break
82
+ next_scheduled_operation = machine_schedule[i + 1]
83
+ graph.add_edge(
84
+ scheduled_operation.operation.operation_id,
85
+ next_scheduled_operation.operation.operation_id,
86
+ type=EdgeType.DISJUNCTIVE,
87
+ )
88
+
89
+ return graph
90
+
91
+
54
92
  def add_disjunctive_edges(graph: JobShopGraph) -> None:
55
93
  """Adds disjunctive edges to the graph."""
56
94