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
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
138
|
+
for operation in dispatcher.available_operations():
|
139
139
|
scores[operation.job_id] = operation.operation_id
|
140
140
|
return scores
|
141
141
|
|
job_shop_lib/graphs/__init__.py
CHANGED
@@ -13,8 +13,8 @@ job, and the global node is connected to all machine and job nodes.
|
|
13
13
|
|
14
14
|
References:
|
15
15
|
- Junyoung Park, Sanjar Bakhtiyar, and Jinkyoo Park. Schedulenet: Learn to
|
16
|
-
solve multi-agent scheduling problems with reinforcement learning. ArXiv,
|
17
|
-
abs/2106.03051, 2021.
|
16
|
+
solve multi-agent scheduling problems with reinforcement learning. ArXiv,
|
17
|
+
abs/2106.03051, 2021.
|
18
18
|
"""
|
19
19
|
|
20
20
|
import itertools
|
@@ -11,7 +11,24 @@ class EdgeType(enum.Enum):
|
|
11
11
|
|
12
12
|
|
13
13
|
class NodeType(enum.Enum):
|
14
|
-
"""Enumeration of node types.
|
14
|
+
"""Enumeration of node types.
|
15
|
+
|
16
|
+
Nodes of type :class:`NodeType.OPERATION`, :class:`NodeType.MACHINE`, and
|
17
|
+
:class:`NodeType.JOB` have specific attributes associated with them in the
|
18
|
+
:class:`Node` class.
|
19
|
+
|
20
|
+
On the other hand, Nodes of type :class:`NodeType.GLOBAL`,
|
21
|
+
:class:`NodeType.SOURCE`, and :class:`NodeType.SINK` are used to represent
|
22
|
+
different concepts in the graph and accesing them, but they do not have
|
23
|
+
specific attributes associated with them.
|
24
|
+
|
25
|
+
.. tip::
|
26
|
+
|
27
|
+
While uncommon, it can be useful to extend this enumeration with
|
28
|
+
additional node types. This can be achieved using
|
29
|
+
`aenum <https://github.com/ethanfurman/aenum>`_'s
|
30
|
+
``extend_enum`` (requires using Python 3.11+).
|
31
|
+
"""
|
15
32
|
|
16
33
|
OPERATION = enum.auto()
|
17
34
|
MACHINE = enum.auto()
|
@@ -13,10 +13,10 @@ NODE_ATTR = "node"
|
|
13
13
|
|
14
14
|
# pylint: disable=too-many-instance-attributes
|
15
15
|
class JobShopGraph:
|
16
|
-
"""
|
16
|
+
"""Represents a :class:`JobShopInstance` as a heterogeneous directed graph.
|
17
17
|
|
18
18
|
Provides a comprehensive graph-based representation of a job shop
|
19
|
-
scheduling problem, utilizing the
|
19
|
+
scheduling problem, utilizing the ``networkx`` library to model the complex
|
20
20
|
relationships between jobs, operations, and machines. This class transforms
|
21
21
|
the abstract scheduling problem into a directed graph, where various
|
22
22
|
entities (jobs, machines, and operations) are nodes, and the dependencies
|
@@ -25,25 +25,40 @@ class JobShopGraph:
|
|
25
25
|
This transformation allows for the application of graph algorithms
|
26
26
|
to analyze and solve scheduling problems.
|
27
27
|
|
28
|
-
|
28
|
+
Args:
|
29
29
|
instance:
|
30
|
-
The job shop instance
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
The job shop instance that the graph represents.
|
31
|
+
add_operation_nodes:
|
32
|
+
Whether to add nodes of type :class:`NodeType.OPERATION` to the
|
33
|
+
to the graph. If set to ``False``, the graph will be empty, and
|
34
|
+
operation nodes will need to be added manually.
|
35
35
|
"""
|
36
36
|
|
37
|
-
|
38
|
-
"""
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
""
|
37
|
+
__slots__ = {
|
38
|
+
"instance": "The job shop instance that the graph represents.",
|
39
|
+
"graph": (
|
40
|
+
"The directed graph representing the job shop, where nodes are "
|
41
|
+
"operations, machines, jobs, or abstract concepts like global, "
|
42
|
+
"source, and sink, with edges indicating dependencies."
|
43
|
+
),
|
44
|
+
"_nodes": "List of all nodes added to the graph.",
|
45
|
+
"_nodes_by_type": "Dictionary mapping node types to lists of nodes.",
|
46
|
+
"_nodes_by_machine": (
|
47
|
+
"List of lists mapping machine ids to operation nodes."
|
48
|
+
),
|
49
|
+
"_nodes_by_job": "List of lists mapping job ids to operation nodes.",
|
50
|
+
"_next_node_id": (
|
51
|
+
"The id to assign to the next node added to thegraph."
|
52
|
+
),
|
53
|
+
"removed_nodes": (
|
54
|
+
"List of boolean values indicating whether a node has been "
|
55
|
+
"removed from the graph."
|
56
|
+
),
|
57
|
+
}
|
58
|
+
|
59
|
+
def __init__(
|
60
|
+
self, instance: JobShopInstance, add_operation_nodes: bool = True
|
61
|
+
):
|
47
62
|
self.graph = nx.DiGraph()
|
48
63
|
self.instance = instance
|
49
64
|
|
@@ -59,7 +74,8 @@ class JobShopGraph:
|
|
59
74
|
]
|
60
75
|
self._next_node_id = 0
|
61
76
|
self.removed_nodes: list[bool] = []
|
62
|
-
|
77
|
+
if add_operation_nodes:
|
78
|
+
self.add_operation_nodes()
|
63
79
|
|
64
80
|
@property
|
65
81
|
def nodes(self) -> list[Node]:
|
@@ -103,7 +119,7 @@ class JobShopGraph:
|
|
103
119
|
"""Number of job nodes in the graph."""
|
104
120
|
return len(self._nodes_by_type[NodeType.JOB])
|
105
121
|
|
106
|
-
def
|
122
|
+
def add_operation_nodes(self) -> None:
|
107
123
|
"""Adds operation nodes to the graph."""
|
108
124
|
for job in self.instance.jobs:
|
109
125
|
for operation in job:
|
job_shop_lib/graphs/_node.py
CHANGED
@@ -9,7 +9,7 @@ from job_shop_lib.graphs._constants import NodeType
|
|
9
9
|
|
10
10
|
|
11
11
|
class Node:
|
12
|
-
"""
|
12
|
+
"""Represents a node in the :class:`JobShopGraph`.
|
13
13
|
|
14
14
|
A node is hashable by its id. The id is assigned when the node is added to
|
15
15
|
the graph. The id must be unique for each node in the graph, and should be
|
@@ -18,11 +18,15 @@ class Node:
|
|
18
18
|
Depending on the type of the node, it can have different attributes. The
|
19
19
|
following table shows the attributes of each type of node:
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
+----------------+---------------------+
|
22
|
+
| Node Type | Required Attribute |
|
23
|
+
+================+=====================+
|
24
|
+
| OPERATION | ``operation`` |
|
25
|
+
+----------------+---------------------+
|
26
|
+
| MACHINE | ``machine_id`` |
|
27
|
+
+----------------+---------------------+
|
28
|
+
| JOB | ``job_id`` |
|
29
|
+
+----------------+---------------------+
|
26
30
|
|
27
31
|
In terms of equality, two nodes are equal if they have the same id.
|
28
32
|
Additionally, one node is equal to an integer if the integer is equal to
|
@@ -31,25 +35,47 @@ class Node:
|
|
31
35
|
This allows for using the node as a key in a dictionary, at the same time
|
32
36
|
we can use its id to index that dictionary. Example:
|
33
37
|
|
34
|
-
|
35
|
-
node = Node(NodeType.SOURCE)
|
36
|
-
node.node_id = 1
|
37
|
-
graph = {node: "some value"}
|
38
|
-
print(graph[node]) # "some value"
|
39
|
-
print(graph[1]) # "some value"
|
40
|
-
```
|
38
|
+
.. code-block:: python
|
41
39
|
|
42
|
-
|
40
|
+
node = Node(NodeType.SOURCE)
|
41
|
+
node.node_id = 1
|
42
|
+
graph = {node: "some value"}
|
43
|
+
print(graph[node]) # "some value"
|
44
|
+
print(graph[1]) # "some value"
|
45
|
+
|
46
|
+
Args:
|
43
47
|
node_type:
|
44
|
-
The type of the node.
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
48
|
+
The type of the node. See :class:`NodeType` for theavailable types.
|
49
|
+
operation:
|
50
|
+
The operation of the node. Required if ``node_type`` is
|
51
|
+
:attr:`NodeType.OPERATION`.
|
52
|
+
machine_id:
|
53
|
+
The id of the machine. Required if ``node_type`` is
|
54
|
+
:attr:`NodeType.MACHINE`.
|
55
|
+
job_id:
|
56
|
+
The id of the job. Required if ``node_type`` is
|
57
|
+
:attr:`NodeType.JOB`.
|
58
|
+
|
59
|
+
Raises:
|
60
|
+
ValidationError:
|
61
|
+
If the ``node_type`` is :attr:`NodeType.OPERATION`,
|
62
|
+
:attr:`NodeType.MACHINE`, or :attr:`NodeType.JOB` and the
|
63
|
+
corresponding ``operation``, ``machine_id``, or ``job_id`` is
|
64
|
+
``None``, respectively.
|
65
|
+
|
50
66
|
"""
|
51
67
|
|
52
|
-
__slots__ =
|
68
|
+
__slots__ = {
|
69
|
+
"node_type": "The type of the node.",
|
70
|
+
"_node_id": "Unique identifier for the node.",
|
71
|
+
"_operation": (
|
72
|
+
"The operation associated with the node."
|
73
|
+
),
|
74
|
+
"_machine_id": (
|
75
|
+
"The machine ID associated with the node."
|
76
|
+
),
|
77
|
+
"_job_id": "The job ID associated with the node.",
|
78
|
+
}
|
53
79
|
|
54
80
|
def __init__(
|
55
81
|
self,
|
@@ -58,34 +84,6 @@ class Node:
|
|
58
84
|
machine_id: int | None = None,
|
59
85
|
job_id: int | None = None,
|
60
86
|
):
|
61
|
-
"""Initializes the node with the given attributes.
|
62
|
-
|
63
|
-
Args:
|
64
|
-
node_type:
|
65
|
-
The type of the node. It can be one of the following:
|
66
|
-
- NodeType.OPERATION
|
67
|
-
- NodeType.MACHINE
|
68
|
-
- NodeType.JOB
|
69
|
-
- NodeType.GLOBAL
|
70
|
-
...
|
71
|
-
operation:
|
72
|
-
The operation of the node. It should be provided if the
|
73
|
-
`node_type` is NodeType.OPERATION.
|
74
|
-
machine_id:
|
75
|
-
The id of the machine of the node. It should be provided if the
|
76
|
-
node_type is NodeType.MACHINE.
|
77
|
-
job_id:
|
78
|
-
The id of the job of the node. It should be provided if the
|
79
|
-
node_type is NodeType.JOB.
|
80
|
-
|
81
|
-
Raises:
|
82
|
-
ValidationError:
|
83
|
-
If the node_type is OPERATION and operation is None.
|
84
|
-
ValidationError:
|
85
|
-
If the node_type is MACHINE and machine_id is None.
|
86
|
-
ValidationError:
|
87
|
-
If the node_type is JOB and job_id is None.
|
88
|
-
"""
|
89
87
|
if node_type == NodeType.OPERATION and operation is None:
|
90
88
|
raise ValidationError("Operation node must have an operation.")
|
91
89
|
|
@@ -95,7 +93,7 @@ class Node:
|
|
95
93
|
if node_type == NodeType.JOB and job_id is None:
|
96
94
|
raise ValidationError("Job node must have a job_id.")
|
97
95
|
|
98
|
-
self.node_type = node_type
|
96
|
+
self.node_type: NodeType = node_type
|
99
97
|
self._node_id: int | None = None
|
100
98
|
|
101
99
|
self._operation = operation
|
@@ -119,7 +117,11 @@ class Node:
|
|
119
117
|
def operation(self) -> Operation:
|
120
118
|
"""Returns the operation of the node.
|
121
119
|
|
122
|
-
This property is mandatory for nodes of type
|
120
|
+
This property is mandatory for nodes of type
|
121
|
+
:class:`NodeType.OPERATION`.
|
122
|
+
|
123
|
+
Raises:
|
124
|
+
UninitializedAttributeError: If the node has no operation.
|
123
125
|
"""
|
124
126
|
if self._operation is None:
|
125
127
|
raise UninitializedAttributeError("Node has no operation.")
|
@@ -130,9 +132,12 @@ class Node:
|
|
130
132
|
"""Returns the `machine_id` of the node.
|
131
133
|
|
132
134
|
This property is mandatory for nodes of type `MACHINE`.
|
135
|
+
|
136
|
+
Raises:
|
137
|
+
UninitializedAttributeError: If the node has no ``machine_id``.
|
133
138
|
"""
|
134
139
|
if self._machine_id is None:
|
135
|
-
raise UninitializedAttributeError("Node has no
|
140
|
+
raise UninitializedAttributeError("Node has no ``machine_id``.")
|
136
141
|
return self._machine_id
|
137
142
|
|
138
143
|
@property
|
@@ -140,6 +145,9 @@ class Node:
|
|
140
145
|
"""Returns the `job_id` of the node.
|
141
146
|
|
142
147
|
This property is mandatory for nodes of type `JOB`.
|
148
|
+
|
149
|
+
Raises:
|
150
|
+
UninitializedAttributeError: If the node has no `job_id`.
|
143
151
|
"""
|
144
152
|
if self._job_id is None:
|
145
153
|
raise UninitializedAttributeError("Node has no `job_id`.")
|
@@ -1,5 +1,15 @@
|
|
1
1
|
"""Contains classes and functions for updating the graph representation of the
|
2
|
-
job shop scheduling problem.
|
2
|
+
job shop scheduling problem.
|
3
|
+
|
4
|
+
Currently, the following classes and utilities are available:
|
5
|
+
|
6
|
+
.. autosummary::
|
7
|
+
|
8
|
+
GraphUpdater
|
9
|
+
ResidualGraphUpdater
|
10
|
+
remove_completed_operations
|
11
|
+
|
12
|
+
"""
|
3
13
|
|
4
14
|
from ._graph_updater import GraphUpdater
|
5
15
|
from ._utils import remove_completed_operations
|
@@ -260,7 +260,7 @@ class SingleJobShopGraphEnv(gym.Env):
|
|
260
260
|
truncated = False
|
261
261
|
info: dict[str, Any] = {
|
262
262
|
"feature_names": self.composite_observer.column_names,
|
263
|
-
"available_operations": self.dispatcher.
|
263
|
+
"available_operations": self.dispatcher.available_operations(),
|
264
264
|
}
|
265
265
|
return obs, reward, done, truncated, info
|
266
266
|
|
@@ -1,11 +1,11 @@
|
|
1
1
|
"""Package for visualization."""
|
2
2
|
|
3
|
-
from job_shop_lib.visualization.
|
3
|
+
from job_shop_lib.visualization._plot_gantt_chart import plot_gantt_chart
|
4
4
|
from job_shop_lib.visualization._gantt_chart_video_and_gif_creation import (
|
5
|
-
|
5
|
+
create_gantt_chart_gif,
|
6
6
|
create_gantt_chart_video,
|
7
7
|
create_gantt_chart_frames,
|
8
|
-
|
8
|
+
get_partial_gantt_chart_plotter,
|
9
9
|
create_video_from_frames,
|
10
10
|
create_gif_from_frames,
|
11
11
|
)
|
@@ -26,9 +26,9 @@ from job_shop_lib.visualization._gantt_chart_creator import (
|
|
26
26
|
__all__ = [
|
27
27
|
"plot_gantt_chart",
|
28
28
|
"create_gantt_chart_video",
|
29
|
-
"
|
29
|
+
"create_gantt_chart_gif",
|
30
30
|
"create_gantt_chart_frames",
|
31
|
-
"
|
31
|
+
"get_partial_gantt_chart_plotter",
|
32
32
|
"create_gif_from_frames",
|
33
33
|
"create_video_from_frames",
|
34
34
|
"plot_disjunctive_graph",
|
@@ -10,8 +10,8 @@ from job_shop_lib.dispatching import (
|
|
10
10
|
)
|
11
11
|
from job_shop_lib.visualization import (
|
12
12
|
create_gantt_chart_video,
|
13
|
-
|
14
|
-
|
13
|
+
get_partial_gantt_chart_plotter,
|
14
|
+
create_gantt_chart_gif,
|
15
15
|
)
|
16
16
|
|
17
17
|
|
@@ -146,7 +146,7 @@ class GanttChartCreator:
|
|
146
146
|
self.history_observer: HistoryObserver = (
|
147
147
|
dispatcher.create_or_get_observer(HistoryObserver)
|
148
148
|
)
|
149
|
-
self.plot_function =
|
149
|
+
self.plot_function = get_partial_gantt_chart_plotter(
|
150
150
|
**self.gannt_chart_wrapper_config
|
151
151
|
)
|
152
152
|
|
@@ -175,7 +175,7 @@ class GanttChartCreator:
|
|
175
175
|
return self.plot_function(
|
176
176
|
self.schedule,
|
177
177
|
None,
|
178
|
-
self.dispatcher.
|
178
|
+
self.dispatcher.available_operations(),
|
179
179
|
self.dispatcher.current_time(),
|
180
180
|
)
|
181
181
|
|
@@ -196,7 +196,7 @@ class GanttChartCreator:
|
|
196
196
|
The configuration for the GIF creation can be customized through the
|
197
197
|
`gif_config` attribute.
|
198
198
|
"""
|
199
|
-
|
199
|
+
create_gantt_chart_gif(
|
200
200
|
instance=self.history_observer.dispatcher.instance,
|
201
201
|
schedule_history=self.history_observer.history,
|
202
202
|
plot_function=self.plot_function,
|