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
@@ -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,
|