Peano
Loading...
Searching...
No Matches
Graph.py
Go to the documentation of this file.
1# This file is part of the Peano project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3
4from typing import List, Union
5
6from .Task import Task
7
8
9class Graph:
10 """!
11
12 Holds a sequence of Task instances
13
14 """
15
16 def __init__(self):
17 self.tasks: List[Task] = []
18
19 def add_task(self, task: Task):
20 self.tasks.append(task)
21
22 def find_tasks(self, *tags) -> List[Task]:
23 """!
24
25 Return all tasks of this graph which carry at least one of the tags,
26 in the order in which they have been added to the graph
27
28 A task which carries several of the tags is returned only once.
29
30 @param tags: str
31 One or several tags, i.e. strings which the tasks hold in their
32 set tags (@ref peano4.api.taskgraph.Task.Task).
33
34 """
35 return [task for task in self.tasks if any(tag in task.tags for tag in tags)]
36
37 def insert_after(self, anchors: Union[Task, List[Task]], task: Task):
38 """!
39
40 Insert a task directly after one or several anchors
41
42 The routine adds task to the graph and makes it depend on all
43 anchors. All tasks which have depended on any of the anchors so far
44 depend on task instead. Consequently, task runs after the anchors
45 but before anything that used to follow them.
46
47 Use this routine to add a step to a task graph which someone else
48 has built, e.g. to postprocess the outcome of a task. task should
49 not depend on any other task yet, unless these dependencies run
50 before the anchors anyway.
51
52 Pass several anchors if several tasks produce one outcome together.
53 The optimistic Finite Volumes, for example, write the new solution
54 either in one task or in another, depending on the cell. A single
55 task inserted after both runs once per cell, after whichever of the
56 two has updated this cell. The anchors must not depend on each
57 other, neither directly nor indirectly, as the redirection would
58 create a cycle otherwise.
59
60 @param anchors: peano4.api.taskgraph.Task or List[peano4.api.taskgraph.Task]
61 One task or a list of tasks of this graph.
62
63 @param task: peano4.api.taskgraph.Task
64 The new task. It must not be part of the graph yet.
65
66 """
67 anchors = [anchors] if isinstance(anchors, Task) else list(anchors)
68 assert len(anchors) > 0, f"Task {task.name} needs at least one anchor"
69 for anchor in anchors:
70 assert anchor in self.tasks, f"Task {anchor.name} is not part of the graph"
71 assert task not in self.tasks, f"Task {task.name} is already part of the graph"
72 for anchor in anchors:
73 for other in anchors:
74 assert other is anchor or not self._depends_on(anchor, other), (
75 f"Anchor {anchor.name} depends on anchor {other.name}, so "
76 f"{task.name} cannot be inserted after both"
77 )
78 for successor in self.tasks:
79 if any(anchor in successor.dependencies for anchor in anchors):
80 redirected = []
81 for dependency in successor.dependencies:
82 replacement = (
83 task
84 if any(dependency is anchor for anchor in anchors)
85 else dependency
86 )
87 if replacement not in redirected:
88 redirected.append(replacement)
89 successor.dependencies = redirected
90 for anchor in anchors:
91 task.add_dependency(anchor)
92 self.add_task(task)
93
94 @staticmethod
95 def _depends_on(task: Task, other: Task) -> bool:
96 """! True if task depends on other, directly or indirectly"""
97 pending = list(task.dependencies)
98 visited = []
99 while pending:
100 dependency = pending.pop()
101 if dependency is other:
102 return True
103 if dependency not in visited:
104 visited.append(dependency)
105 pending += dependency.dependencies
106 return False
107
108 def ordered_tasks(self) -> List[Task]:
109 """!
110
111 Linearise this graph's tasks into the order
112 peano4.api.taskgraph.compiler.Sequential.translate() compiles them
113 into: a straightforward recursive traversal of the dependency DAG
114 (visit a task's dependencies before the task itself), which - since
115 every dependency is visited before whatever depends on it - yields
116 a valid topological order.
117
118 This is the single source of truth for "the task after this one"
119 that both translate() and
120 peano4.api.taskgraph.compiler.PropagateUseStatements.propagate_use_statements()
121 rely on; they must agree on the same order, since the latter's
122 whole job is to reason about which task's Observer immediately
123 follows which other one's once compiled.
124
125 Repeated calls recompute the order from scratch (cheap - task
126 graphs here are small) rather than caching it, so adding further
127 tasks/dependencies after an earlier call is always reflected.
128
129 """
130 order: List[Task] = []
131 visited = set()
132
133 def visit(task: Task):
134 if task not in visited:
135 visited.add(task)
136 for dependency in task.dependencies:
137 visit(dependency)
138 order.append(task)
139
140 for task in self.tasks:
141 visit(task)
142 return order
143
144 def log_as_text(self, title: str = "Task graph"):
145 """!
146 Textual counterpart to visualise(): logs a plain-text rendering of
147 this task graph through peano4.api.Logger(), for LoggerVerbosity.TEXT
148 runs, where a matplotlib window is neither wanted nor - in a batch/
149 non-interactive run - even possible.
150
151 Renders the same information visualise() draws as a UML state
152 diagram - every task's grid entity, its dependencies, its action
153 sets (as "do / <name>" lines), the cell/face/vertex submodels it
154 declares via use_cell()/use_face()/use_vertex() (each annotated
155 with its Task.DataAccess qualifier) and whether it repeats - as one
156 block of text per task, ordered root to sink by dependency depth
157 exactly like visualise() lays its layers out top to bottom.
158 """
159 import peano4.api
160
161 if not self.tasks:
162 peano4.api.Logger().log(f"Task graph '{title}' is empty.")
163 return
164
165 # Depth of a task = 0 if it has no dependencies, else 1 + the
166 # deepest of its dependencies. Recursive but memoised - task graphs
167 # here are small and acyclic by construction. @see visualise(),
168 # which computes the very same notion of depth to lay its layers
169 # out top to bottom.
170 depth_of_task = {}
171
172 def depth(task: Task) -> int:
173 if task not in depth_of_task:
174 depth_of_task[task] = (
175 0 if not task.dependencies else 1 + max(depth(dependency) for dependency in task.dependencies)
176 )
177 return depth_of_task[task]
178
179 ordered_tasks = sorted(self.tasks, key=depth)
180
181 lines = [f"Task graph '{title}':"]
182 for task in ordered_tasks:
183 lines.append(f"- {task.name} «{task.entity.name.lower()}»")
184 if task.dependencies:
185 lines.append(
186 " depends on: " + ", ".join(dependency.name for dependency in task.dependencies)
187 )
188 for action_set in task.action_sets:
189 lines.append(f" do / {action_set.get_action_set_name()}")
190 for kind, submodels, access in (
191 ("cell", task.cell_data, task.cell_data_access),
192 ("face", task.face_data, task.face_data_access),
193 ("vertex", task.vertex_data, task.vertex_data_access),
194 ):
195 if submodels:
196 lines.append(
197 f" {kind} / "
198 + ", ".join(f"{submodel.name} [{access[submodel].name}]" for submodel in submodels)
199 )
200 if task.has_to_repeat:
201 lines.append(f" repeats while: {task.has_to_repeat}")
202
203 peano4.api.Logger().log("\n".join(lines))
204
205 def visualise(self, title: str = "Task graph"):
206 """!
207 Render this task graph with matplotlib as a UML state diagram: one
208 rounded-rectangle state per task, a solid transition arrow from
209 every dependency to the task that depends on it. Tasks are laid
210 out top to bottom by dependency depth (a task without dependencies
211 sits at the top; everything that - directly or transitively -
212 depends on it sits below it), so following the transitions always
213 moves downwards, exactly like reading a state diagram's flow of
214 control. The tasks of one layer sit side by side: every task sits
215 straight below its leftmost dependency and is moved to the right
216 only where it would otherwise overlap another task.
217
218 A filled black circle - the UML initial pseudostate - transitions
219 into every task that has no dependencies; every task nothing else
220 depends on transitions into a bullseye - the UML final pseudostate.
221 A task with has_to_repeat set (i.e. not None) is additionally drawn
222 with a small self-transition loop right of its state, the standard UML
223 notation for "this state can re-trigger itself", labelled with the
224 task's has_to_repeat string itself.
225
226 Every action set attached to a task (@see Task.add_action_set) is
227 listed inside that task's own state box as a "do / <name>" line,
228 below a separator - the same internal-activity compartment a UML
229 state uses to list its entry/exit/do behaviours, as opposed to the
230 transition arrows between states, which describe order, not
231 ownership. The same compartment also lists the cell/face/vertex
232 submodels the task declares via use_cell()/use_face()/use_vertex()
233 as "cell / <name> [<Task.DataAccess>]" (and face/vertex
234 counterparts) lines, one per kind that is actually used.
235
236 Every state also carries a small coloured badge in its top-left
237 corner indicating which kind of grid entity (vertex/face/cell/
238 global) the task works on; a legend below the title spells out
239 what each colour means, restricted to the entity kinds actually
240 used in this particular graph.
241
242 Opens a blocking matplotlib window; close it to continue.
243 """
244 import matplotlib.pyplot as plt
245 from matplotlib.lines import Line2D
246 from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch
247 from matplotlib.path import Path
248
249 if not self.tasks:
250 print(f"Task graph '{title}' is empty - nothing to visualise.")
251 return
252
253 # Depth of a task = 0 if it has no dependencies, else 1 + the
254 # deepest of its dependencies. Recursive but memoised - task graphs
255 # here are small and acyclic by construction.
256 depth_of_task = {}
257
258 def depth(task: Task) -> int:
259 if task not in depth_of_task:
260 depth_of_task[task] = (
261 0 if not task.dependencies else 1 + max(depth(dependency) for dependency in task.dependencies)
262 )
263 return depth_of_task[task]
264
265 layers = {}
266 for task in self.tasks:
267 layers.setdefault(depth(task), []).append(task)
268
269 entity_colour = {
270 Task.GridEntity.VERTEX: "tab:blue",
271 Task.GridEntity.FACE: "tab:orange",
272 Task.GridEntity.CELL: "tab:green",
273 Task.GridEntity.GLOBAL: "tab:purple",
274 }
275
276 # A state's box grows with its content: a fixed-height header
277 # (task name + «entity» stereotype) plus, if the task owns action
278 # sets, one "do / <name>" line per action set in a body
279 # compartment below a separator - mirroring how a UML state lists
280 # its internal entry/exit/do behaviours.
281 HEADER_HEIGHT = 0.52
282 ACTIVITY_LINE_HEIGHT = 0.22
283 BODY_PADDING = 0.14
284 MIN_WIDTH = 1.7
285 CHAR_WIDTH = 0.10
286 HORIZONTAL_GAP = 0.6
287 VERTICAL_GAP = 0.9
288
289 def activity_lines(task: Task):
290 lines = [f"do / {action_set.get_action_set_name()}" for action_set in task.action_sets]
291 for kind, submodels, access in (
292 ("cell", task.cell_data, task.cell_data_access),
293 ("face", task.face_data, task.face_data_access),
294 ("vertex", task.vertex_data, task.vertex_data_access),
295 ):
296 if submodels:
297 lines.append(
298 f"{kind} / "
299 + ", ".join(f"{submodel.name} [{access[submodel].name}]" for submodel in submodels)
300 )
301 return lines
302
303 width_of_task, height_of_task = {}, {}
304 for task in self.tasks:
305 lines = activity_lines(task)
306 longest_line = max([len(task.name)] + [len(line) for line in lines])
307 width_of_task[task] = max(MIN_WIDTH, CHAR_WIDTH * longest_line + 0.6)
308 height_of_task[task] = HEADER_HEIGHT + (
309 BODY_PADDING + len(lines) * ACTIVITY_LINE_HEIGHT + 0.5 * BODY_PADDING if lines else 0
310 )
311
312 SELF_LOOP_WIDTH = 0.4
313 SELF_LOOP_LABEL_CHAR_WIDTH = 0.065
314 PSEUDOSTATE_OFFSET = 0.6
315
316 # Horizontal space a task's self-transition loop and its label take
317 # right of the task's state
318 def self_loop_extent(task: Task) -> float:
319 if not task.has_to_repeat:
320 return 0.0
321 return SELF_LOOP_WIDTH + 0.06 + SELF_LOOP_LABEL_CHAR_WIDTH * len(task.has_to_repeat)
322
323 # Every layer forms one row. The rows are stacked top to bottom,
324 # each as tall as its tallest state. Every state is placed straight
325 # below its leftmost dependency, i.e. left-aligned with it, and
326 # moved to the right only if it would otherwise overlap the state
327 # left of it in the same row. States without dependencies start at
328 # x=0 and extend to the right.
329 position_of_task = {}
330 row_y = 0.0
331 for layer in sorted(layers):
332 def preferred_left_edge(task: Task) -> float:
333 if not task.dependencies:
334 return 0.0
335 return min(
336 position_of_task[dependency][0] - width_of_task[dependency] / 2
337 for dependency in task.dependencies
338 )
339
340 tasks_in_layer = sorted(layers[layer], key=preferred_left_edge)
341 row_height = max(height_of_task[task] for task in tasks_in_layer)
342 next_free_x = float("-inf")
343 for task in tasks_in_layer:
344 left_edge = max(preferred_left_edge(task), next_free_x)
345 position_of_task[task] = (left_edge + width_of_task[task] / 2, row_y - row_height / 2)
346 next_free_x = left_edge + width_of_task[task] + self_loop_extent(task) + HORIZONTAL_GAP
347 row_y -= row_height + VERTICAL_GAP
348 bottom_y = row_y + VERTICAL_GAP
349
350 root_tasks = [task for task in self.tasks if not task.dependencies]
351 sink_tasks = [
352 task for task in self.tasks if not any(task in other.dependencies for other in self.tasks)
353 ]
354
355 # Text is sized in points, so - to guarantee it actually fits the
356 # (content-dependent) box widths/heights computed above in data
357 # units - the figure has to be built at a fixed, known number of
358 # inches per data unit, rather than left to matplotlib's default
359 # autoscaling, which would pick whatever scale happens to fit the
360 # default figure size.
361 INCH_PER_UNIT = 0.62
362 PADDING = 0.5
363
364 min_x = min(position_of_task[task][0] - width_of_task[task] / 2 for task in self.tasks)
365 max_x = max(
366 position_of_task[task][0] + width_of_task[task] / 2 + self_loop_extent(task) for task in self.tasks
367 )
368 min_y = bottom_y
369 max_y = 0.0
370 if root_tasks:
371 max_y = PSEUDOSTATE_OFFSET + 0.09
372 if sink_tasks:
373 min_y = bottom_y - PSEUDOSTATE_OFFSET - 0.11
374 min_x, max_x, min_y, max_y = min_x - PADDING, max_x + PADDING, min_y - PADDING, max_y + PADDING
375
376 # Reserve a fixed number of inches around the axes for title/
377 # legend/x-label chrome - fixed in inches, not a fraction of the
378 # figure, so it does not eat into the data area disproportionately
379 # on a small graph - and size the figure so the axes themselves
380 # come out to exactly INCH_PER_UNIT inches per data unit in both
381 # directions.
382 LEGEND_HEIGHT = 0.26
383 LEFT_CHROME, RIGHT_CHROME, TOP_CHROME, BOTTOM_CHROME = 0.35, 0.1, 0.4 + LEGEND_HEIGHT, 0.15
384 data_width_inches = (max_x - min_x) * INCH_PER_UNIT
385 data_height_inches = (max_y - min_y) * INCH_PER_UNIT
386 figure_width = data_width_inches + LEFT_CHROME + RIGHT_CHROME
387 figure_height = data_height_inches + TOP_CHROME + BOTTOM_CHROME
388
389 figure = plt.figure(figsize=(figure_width, figure_height))
390 axes = figure.add_axes(
391 (
392 LEFT_CHROME / figure_width,
393 BOTTOM_CHROME / figure_height,
394 data_width_inches / figure_width,
395 data_height_inches / figure_height,
396 )
397 )
398 axes.set_xlim(min_x, max_x)
399 axes.set_ylim(min_y, max_y)
400
401 # One rounded-rectangle "state" per task, with a small coloured
402 # badge in the top-left corner indicating the grid entity it
403 # works on, and - if it owns action sets - an internal-activity
404 # compartment listing them as "do / <name>".
405 state_patch = {}
406 for task in self.tasks:
407 x, y = position_of_task[task]
408 w, h = width_of_task[task], height_of_task[task]
409 lines = activity_lines(task)
410
411 box = FancyBboxPatch(
412 (x - w / 2, y - h / 2),
413 w,
414 h,
415 boxstyle="round,pad=0,rounding_size=0.09",
416 linewidth=1.3,
417 edgecolor="black",
418 facecolor="white",
419 zorder=2,
420 )
421 axes.add_patch(box)
422 state_patch[task] = box
423
424 axes.add_patch(
425 Circle(
426 (x - w / 2 + 0.10, y + h / 2 - 0.10),
427 0.045,
428 facecolor=entity_colour.get(task.entity, "tab:gray"),
429 edgecolor="none",
430 zorder=3,
431 )
432 )
433
434 header_center_y = y + h / 2 - HEADER_HEIGHT / 2 if lines else y
435 axes.annotate(
436 task.name,
437 (x, header_center_y + 0.08),
438 ha="center", va="center", zorder=3, fontsize=7.5, fontweight="bold",
439 )
440 axes.annotate(
441 f"«{task.entity.name.lower()}»",
442 (x, header_center_y - 0.13),
443 ha="center", va="center", zorder=3, fontsize=6, style="italic", color="dimgray",
444 )
445
446 if lines:
447 separator_y = y + h / 2 - HEADER_HEIGHT
448 axes.plot(
449 [x - w / 2, x + w / 2], [separator_y, separator_y],
450 color="black", linewidth=0.8, zorder=3,
451 )
452 for index, line in enumerate(lines):
453 line_y = separator_y - BODY_PADDING - index * ACTIVITY_LINE_HEIGHT - 0.08
454 axes.annotate(
455 line,
456 (x - w / 2 + 0.12, line_y),
457 ha="left", va="center", zorder=3, fontsize=6.5, color="dimgray",
458 )
459
460 # Transitions between states. patchA/patchB let matplotlib clip
461 # the arrow to the actual (content-dependent) state boundary, so
462 # this works regardless of how tall a particular box grew.
463 for task in self.tasks:
464 for dependency in task.dependencies:
465 axes.add_patch(
466 FancyArrowPatch(
467 position_of_task[dependency],
468 position_of_task[task],
469 patchA=state_patch[dependency],
470 patchB=state_patch[task],
471 arrowstyle="-|>",
472 mutation_scale=13,
473 connectionstyle="arc3,rad=0.08",
474 color="black",
475 linewidth=1.1,
476 zorder=1,
477 )
478 )
479
480 if task.has_to_repeat:
481 x, y = position_of_task[task]
482 w, h = width_of_task[task], height_of_task[task]
483 right_x = x + w / 2
484 upper_y, lower_y = y + 0.18 * h, y - 0.18 * h
485 path = Path(
486 [
487 (right_x, upper_y),
488 (right_x + SELF_LOOP_WIDTH, upper_y),
489 (right_x + SELF_LOOP_WIDTH, lower_y),
490 (right_x, lower_y),
491 ],
492 [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4],
493 )
494 axes.add_patch(
495 FancyArrowPatch(
496 path=path,
497 arrowstyle="-|>",
498 mutation_scale=12,
499 color="crimson",
500 linewidth=1.1,
501 zorder=2.5,
502 )
503 )
504 axes.annotate(
505 task.has_to_repeat,
506 (right_x + SELF_LOOP_WIDTH + 0.06, y),
507 ha="left", va="center", zorder=3, fontsize=6, style="italic", color="crimson",
508 )
509
510 # UML initial pseudostate (filled circle) transitioning into every
511 # task without dependencies, and UML final pseudostate (bullseye)
512 # collecting every task nothing else depends on.
513 if root_tasks:
514 x = sum(position_of_task[task][0] for task in root_tasks) / len(root_tasks)
515 initial_pos = (x, PSEUDOSTATE_OFFSET)
516 initial_patch = Circle(initial_pos, 0.09, facecolor="black", edgecolor="black", zorder=3)
517 axes.add_patch(initial_patch)
518 for task in root_tasks:
519 axes.add_patch(
520 FancyArrowPatch(
521 initial_pos,
522 position_of_task[task],
523 patchA=initial_patch,
524 patchB=state_patch[task],
525 arrowstyle="-|>",
526 mutation_scale=13,
527 color="black",
528 linewidth=1.1,
529 zorder=1,
530 )
531 )
532
533 if sink_tasks:
534 x = sum(position_of_task[task][0] for task in sink_tasks) / len(sink_tasks)
535 final_pos = (x, bottom_y - PSEUDOSTATE_OFFSET)
536 final_outer = Circle(final_pos, 0.11, facecolor="white", edgecolor="black", linewidth=1.3, zorder=3)
537 axes.add_patch(final_outer)
538 axes.add_patch(Circle(final_pos, 0.05, facecolor="black", edgecolor="none", zorder=4))
539 for task in sink_tasks:
540 axes.add_patch(
541 FancyArrowPatch(
542 position_of_task[task],
543 final_pos,
544 patchA=state_patch[task],
545 patchB=final_outer,
546 arrowstyle="-|>",
547 mutation_scale=13,
548 color="black",
549 linewidth=1.1,
550 zorder=1,
551 )
552 )
553
554 # Title and, right below it, a legend spelling out what the
555 # coloured badge on every state stands for - both placed as
556 # figure-level (not axes-level) elements in the TOP_CHROME rows
557 # reserved for them above, so they stay put regardless of the
558 # axes' own data-driven size.
559 title_height = TOP_CHROME - LEGEND_HEIGHT
560 figure.suptitle(title, y=1.0 - 0.5 * title_height / figure_height, fontsize=10)
561
562 used_entities = [entity for entity in Task.GridEntity if any(task.entity == entity for task in self.tasks)]
563 legend_handles = [
564 Line2D(
565 [0], [0],
566 marker="o", linestyle="none", markersize=6,
567 markerfacecolor=entity_colour.get(entity, "tab:gray"), markeredgecolor="none",
568 label=f"«{entity.name.lower()}»",
569 )
570 for entity in used_entities
571 ]
572 figure.legend(
573 handles=legend_handles,
574 loc="center",
575 bbox_to_anchor=(0.5, 1.0 - (title_height + 0.5 * LEGEND_HEIGHT) / figure_height),
576 ncol=len(legend_handles),
577 frameon=False,
578 fontsize=6.5,
579 handletextpad=0.3,
580 columnspacing=1.2,
581 )
582
583 axes.set_ylabel("Dependency depth")
584 axes.set_xticks([])
585 axes.set_yticks([])
586 axes.set_aspect("equal")
587 # Non-blocking: several related graphs (e.g. a project's Setup/
588 # TimeStep/Plot task graphs) are typically visualised back to back,
589 # and should all appear together rather than forcing the user to
590 # close each one before the next is even drawn. @see
591 # wait_for_visualisations(), which the caller invokes once after
592 # such a group to actually pause for the user to look at all of
593 # them together.
594 plt.show(block=False)
595
596
598 """!
599
600 Block until the user closes every currently open matplotlib figure -
601 the counterpart to Graph.visualise() and
602 peano4.api.taskgraph.compiler.ReportCompiledStepSequence.report_compiled_step_sequence()'s
603 own visualisation, both of which render with plt.show(block=False) so
604 that a whole group of related figures (e.g. a project's Setup/
605 TimeStep/Plot task graphs, or their compiled counterparts) can all
606 appear together rather than one at a time. Call this once after such
607 a group has been produced, to actually pause and let the user look at
608 all of them side by side before execution continues.
609
610 A no-op under any verbosity other than LoggerVerbosity.VISUALISATION,
611 and also a no-op (does not even import matplotlib) if nothing is
612 currently open - e.g. because the group in question turned out to be
613 empty.
614
615 """
616 import peano4.api
617
618 if not peano4.api.Logger().is_visualisation:
619 return
620
621 import matplotlib.pyplot as plt
622
623 if plt.get_fignums():
624 plt.show()
Singleton logger for the Peano 4 Python API.
Definition Logger.py:37
Holds a sequence of Task instances.
Definition Graph.py:9
bool _depends_on(Task task, Task other)
True if task depends on other, directly or indirectly.
Definition Graph.py:95
log_as_text(self, str title="Task graph")
Textual counterpart to visualise(): logs a plain-text rendering of this task graph through peano4....
Definition Graph.py:144
insert_after(self, Union[Task, List[Task]] anchors, Task task)
Insert a task directly after one or several anchors.
Definition Graph.py:37
List[Task] ordered_tasks(self)
Linearise this graph's tasks into the order peano4.api.taskgraph.compiler.Sequential....
Definition Graph.py:108
add_task(self, Task task)
Definition Graph.py:19
visualise(self, str title="Task graph")
Render this task graph with matplotlib as a UML state diagram: one rounded-rectangle state per task,...
Definition Graph.py:205
List[Task] find_tasks(self, *tags)
Return all tasks of this graph which carry at least one of the tags, in the order in which they have ...
Definition Graph.py:22
Which kind of mesh entity a Task works on.
Definition Task.py:114
wait_for_visualisations()
Block until the user closes every currently open matplotlib figure - the counterpart to Graph....
Definition Graph.py:597