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.
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.
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.
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.
242 Opens a blocking matplotlib window; close it to continue.
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
250 print(f
"Task graph '{title}' is empty - nothing to visualise.")
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)
263 return depth_of_task[task]
266 for task
in self.
tasks:
267 layers.setdefault(depth(task), []).append(task)
270 Task.GridEntity.VERTEX:
"tab:blue",
271 Task.GridEntity.FACE:
"tab:orange",
272 Task.GridEntity.CELL:
"tab:green",
273 Task.GridEntity.GLOBAL:
"tab:purple",
282 ACTIVITY_LINE_HEIGHT = 0.22
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),
299 +
", ".join(f
"{submodel.name} [{access[submodel].name}]" for submodel
in submodels)
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
312 SELF_LOOP_WIDTH = 0.4
313 SELF_LOOP_LABEL_CHAR_WIDTH = 0.065
314 PSEUDOSTATE_OFFSET = 0.6
318 def self_loop_extent(task: Task) -> float:
319 if not task.has_to_repeat:
321 return SELF_LOOP_WIDTH + 0.06 + SELF_LOOP_LABEL_CHAR_WIDTH * len(task.has_to_repeat)
329 position_of_task = {}
331 for layer
in sorted(layers):
332 def preferred_left_edge(task: Task) -> float:
333 if not task.dependencies:
336 position_of_task[dependency][0] - width_of_task[dependency] / 2
337 for dependency
in task.dependencies
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
350 root_tasks = [task
for task
in self.
tasks if not task.dependencies]
352 task
for task
in self.
tasks if not any(task
in other.dependencies
for other
in self.
tasks)
364 min_x = min(position_of_task[task][0] - width_of_task[task] / 2
for task
in self.
tasks)
366 position_of_task[task][0] + width_of_task[task] / 2 + self_loop_extent(task)
for task
in self.
tasks
371 max_y = PSEUDOSTATE_OFFSET + 0.09
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
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
389 figure = plt.figure(figsize=(figure_width, figure_height))
390 axes = figure.add_axes(
392 LEFT_CHROME / figure_width,
393 BOTTOM_CHROME / figure_height,
394 data_width_inches / figure_width,
395 data_height_inches / figure_height,
398 axes.set_xlim(min_x, max_x)
399 axes.set_ylim(min_y, max_y)
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)
411 box = FancyBboxPatch(
412 (x - w / 2, y - h / 2),
415 boxstyle=
"round,pad=0,rounding_size=0.09",
422 state_patch[task] = box
426 (x - w / 2 + 0.10, y + h / 2 - 0.10),
428 facecolor=entity_colour.get(task.entity,
"tab:gray"),
434 header_center_y = y + h / 2 - HEADER_HEIGHT / 2
if lines
else y
437 (x, header_center_y + 0.08),
438 ha=
"center", va=
"center", zorder=3, fontsize=7.5, fontweight=
"bold",
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",
447 separator_y = y + h / 2 - HEADER_HEIGHT
449 [x - w / 2, x + w / 2], [separator_y, separator_y],
450 color=
"black", linewidth=0.8, zorder=3,
452 for index, line
in enumerate(lines):
453 line_y = separator_y - BODY_PADDING - index * ACTIVITY_LINE_HEIGHT - 0.08
456 (x - w / 2 + 0.12, line_y),
457 ha=
"left", va=
"center", zorder=3, fontsize=6.5, color=
"dimgray",
463 for task
in self.
tasks:
464 for dependency
in task.dependencies:
467 position_of_task[dependency],
468 position_of_task[task],
469 patchA=state_patch[dependency],
470 patchB=state_patch[task],
473 connectionstyle=
"arc3,rad=0.08",
480 if task.has_to_repeat:
481 x, y = position_of_task[task]
482 w, h = width_of_task[task], height_of_task[task]
484 upper_y, lower_y = y + 0.18 * h, y - 0.18 * h
488 (right_x + SELF_LOOP_WIDTH, upper_y),
489 (right_x + SELF_LOOP_WIDTH, lower_y),
492 [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4],
506 (right_x + SELF_LOOP_WIDTH + 0.06, y),
507 ha=
"left", va=
"center", zorder=3, fontsize=6, style=
"italic", color=
"crimson",
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:
522 position_of_task[task],
523 patchA=initial_patch,
524 patchB=state_patch[task],
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:
542 position_of_task[task],
544 patchA=state_patch[task],
559 title_height = TOP_CHROME - LEGEND_HEIGHT
560 figure.suptitle(title, y=1.0 - 0.5 * title_height / figure_height, fontsize=10)
562 used_entities = [entity
for entity
in Task.GridEntity if any(task.entity == entity
for task
in self.
tasks)]
566 marker=
"o", linestyle=
"none", markersize=6,
567 markerfacecolor=entity_colour.get(entity,
"tab:gray"), markeredgecolor=
"none",
568 label=f
"«{entity.name.lower()}»",
570 for entity
in used_entities
573 handles=legend_handles,
575 bbox_to_anchor=(0.5, 1.0 - (title_height + 0.5 * LEGEND_HEIGHT) / figure_height),
576 ncol=len(legend_handles),
583 axes.set_ylabel(
"Dependency depth")
586 axes.set_aspect(
"equal")
594 plt.show(block=
False)