Source code for labscheduler.dev_tools.visualization

import graphviz

from labscheduler.structures import MoveOperation, Operation


[docs] def visualize_workflow(operations: list[Operation]) -> None: """ Visualizes a directed graph of operations using Graphviz. Node properties: - Color: • Red if both start and finish are None. • Yellow if start is not None but finish is None. • Green if neither start nor finish are None. - Label: • The first line is taken from operation.main_machine.preferred if it exists; otherwise, operation.main_machine.type is used. • The second line shows the duration. • If no main_machine is provided, the operation's name is used as a fallback. Edge properties: - For each edge from a preceding operation to an operation, the label is constructed from: • "Min:" from min_wait if the value exists and is non-zero. • "Max:" from max_wait if the value exists and is not infinity. • "Cost:" from wait_cost if the value exists. The three parts are concatenated together with commas. If none of the values are relevant, the edge is unlabeled. The graph is oriented top-to-bottom (TB) and is saved locally as "workflow_graph.png", which will be overwritten each time. The image is opened in the default viewer. """ dot = graphviz.Digraph(comment="Workflow Graph", format="png") dot.attr(rankdir="TB") # Create nodes with updated labels and coloring. for op in operations: # Determine node color. if op.start is None and op.finish is None: fillcolor = "red" elif op.finish is None: fillcolor = "yellow" else: fillcolor = "green" # Determine label from main_machine if available. main_machine = op.main_machine primary = main_machine.preferred if main_machine.preferred is not None else main_machine.type if isinstance(op, MoveOperation): primary = f"{op.origin_machine.preferred}--{primary}-->{op.target_machine.preferred}" # Add duration on the second line. label = f"{primary}\nDuration: {op.duration}" dot.node(op.name, label=label, style="filled", fillcolor=fillcolor) # Create edges from each operations preceding_operations. for op in operations: for predecessor in op.preceding_operations: label_parts = [] # Add min_wait label if relevant. if predecessor in op.min_wait and op.min_wait[predecessor] != 0: label_parts.append(f"Min={op.min_wait[predecessor]}") # Add max_wait label if relevant. if predecessor in op.max_wait: max_val = op.max_wait[predecessor] if max_val != float("inf") and max_val != "inf": label_parts.append(f"Max={max_val}") # Add wait_cost label if available. if predecessor in op.wait_cost: label_parts.append(f"C={round(op.wait_cost[predecessor])}") edge_label = ", ".join(label_parts) if label_parts else "" dot.edge(predecessor, op.name, label=edge_label) # Render to a local file "workflow_graph" (overwritten each time). dot.render("workflow_graph", view=True, cleanup=True)