Source code for prefltlf2pdfa.viz

import base64
import pygraphviz
import os
import seaborn as sns
from loguru import logger
from prefltlf2pdfa.prefltlf import PrefAutomaton


def _color_palette(n):
    """
    Generate a color palette based on the number of colors requested.

    This function utilizes different seaborn color palettes depending on the value of `n`.

    Args:
        n (int): The number of colors to generate.

    Returns:
        list: A list of RGB tuples representing the color palette.
    """
    if n < 10:
        colors = sns.color_palette("pastel", n_colors=n)
    elif n < 20:
        colors = sns.color_palette("viridis", n_colors=n)
    else:
        colors = [(1, 1, 1) for _ in range(n)]

    return colors


def _create_dot_semi_automaton(paut, node2color, **kwargs):
    """
    Create a DOT representation of a semi-automaton.

    Args:
        paut (PrefAutomaton): The preference automaton to represent.
        node2color (dict): A mapping from node identifiers to colors.
        kwargs: Additional options for the representation.

    Returns:
        AGraph: A pygraphviz AGraph object representing the semi-automaton.
    """
    # Extract options
    sa_state = kwargs.get("show_sa_state", False)
    sa_class = kwargs.get("show_class", False)
    sa_color = kwargs.get("show_color", False)

    if sa_color:
        assert node2color is not None, "Coloring requested but no color map provided (it is None)."

    # Create graph to display semi-automaton
    dot_semi_aut = pygraphviz.AGraph(directed=True)

    # Add nodes to semi-automaton
    for sid, data in paut.get_states(data=True):
        # Determine state name
        st_label = data["name"] if sa_state else sid

        # Append state class if option enabled
        st_label = f"{st_label}\n[{data['partition']}]" if sa_class else st_label

        # Add node
        if sa_color:
            color = node2color[data['partition']]
            color = '#{:02x}{:02x}{:02x}'.format(int(color[0] * 255), int(color[1] * 255), int(color[2] * 255))
            dot_semi_aut.add_node(sid, **{"label": st_label, "fillcolor": color, "style": "filled"})
        else:
            dot_semi_aut.add_node(sid, **{"label": st_label})

    # Add initial state to semi-automaton
    dot_semi_aut.add_node("init", **{"label": "", "shape": "plaintext"})

    # Add edges to semi-automaton
    for u, d in paut.transitions.items():
        for label, v in d.items():
            dot_semi_aut.add_edge(u, v, **{"label": label})
    dot_semi_aut.add_edge("init", paut.init_state, **{"label": ""})

    # Return semi-automaton
    return dot_semi_aut


def _create_dot_pref_graph(paut, node2color, **kwargs):
    """
    Create a DOT representation of a preference graph.

    Args:
        paut (PrefAutomaton): The preference automaton to represent.
        node2color (dict): A mapping from node identifiers to colors.
        kwargs: Additional options for the representation.

    Returns:
        AGraph: A pygraphviz AGraph object representing the preference graph.
    """
    # Extract options
    sa_color = kwargs.get("show_color", False)
    pg_state = kwargs.get("show_pg_state", False)

    if sa_color:
        assert node2color is not None, "Coloring requested but no color map provided (it is None)."

    # Preference graph
    dot_pref = pygraphviz.AGraph(directed=True)

    # Add nodes to preference graph
    for n, data in paut.pref_graph.nodes(data=True):
        # n_label = set(phi[i] for i in range(len(phi)) if data['name'][i] == 1) if pg_state else n
        n_label = data['name'] if pg_state else n
        if sa_color:
            color = node2color[n]
            color = '#{:02x}{:02x}{:02x}'.format(int(color[0] * 255), int(color[1] * 255), int(color[2] * 255))
        else:
            color = "white"

        dot_pref.add_node(n, **{"label": n_label, "fillcolor": color, "style": "filled"})

    # Add edges to preference graph
    for u, v in paut.pref_graph.edges():
        dot_pref.add_edge(u, v)

    return dot_pref


[docs] def paut2dot(paut: PrefAutomaton, **kwargs): """ Generate images for semi-automaton and preference graph. Args: paut (PrefAutomaton): The preference automaton to convert. kwargs: Additional options for customizing the representation. - show_sa_state (bool): If True, display the state names in the semi-automaton; otherwise, use state IDs. - show_class (bool): If True, append the state class to the state names in the semi-automaton. - show_color (bool): If True, apply a color palette to the nodes based on their partitions. - show_pg_state (bool): If True, display the names of nodes in the preference graph; otherwise, use node IDs. - engine (str): The drawing engine to use for rendering (default is "dot"). Returns: tuple: A tuple containing two images as base64 encoded strings (semi-automaton and preference graph). """ # Extract options sa_state = kwargs.get("show_sa_state", False) sa_class = kwargs.get("show_class", False) sa_color = kwargs.get("show_color", False) pg_state = kwargs.get("show_pg_state", False) logger.debug(f"Options for paut2dot: {sa_state=}, {sa_class=}, {sa_color=}, {pg_state=}") # If partition coloring is requested, then construct a color palette node2color = None if sa_color: colors = _color_palette(n=len(paut.pref_graph.nodes())) node2color = {node: colors[i] for i, node in enumerate(paut.pref_graph.nodes())} # Construct DOT representation for semi-automaton dot_semi_aut = _create_dot_semi_automaton(paut=paut, node2color=node2color, **kwargs) dot_pref_graph = _create_dot_pref_graph(paut=paut, node2color=node2color, **kwargs) # Set drawing engine dot_semi_aut.layout(prog=kwargs.get("engine", "dot")) dot_pref_graph.layout(prog=kwargs.get("engine", "dot")) return dot_semi_aut, dot_pref_graph
[docs] def paut2png(dot_semi_aut, dot_pref_graph, fpath="", fname="out"): """ Save the semi-automaton and preference graph as PNG images. Args: dot_semi_aut (AGraph): The DOT representation of the semi-automaton. dot_pref_graph (AGraph): The DOT representation of the preference graph. fpath (str): The file path where images should be saved. fname (str): The base name for the output files (without extension). """ if ".png" in fname: fname = fname[:-4] # Generate images (as bytes) dot_semi_aut.draw(path=os.path.join(fpath, f"{fname}_sa.png"), format="png") dot_pref_graph.draw(path=os.path.join(fpath, f"{fname}_pg.png"), format="png")
[docs] def paut2svg(dot_semi_aut, dot_pref_graph, fpath="", fname="out"): """ Save the semi-automaton and preference graph as SVG images. Args: dot_semi_aut (AGraph): The DOT representation of the semi-automaton. dot_pref_graph (AGraph): The DOT representation of the preference graph. fpath (str): The file path where images should be saved. fname (str): The base name for the output files (without extension). """ if ".svg" in fname: fname = fname[:-4] # Generate images (as bytes) dot_semi_aut.draw(path=os.path.join(fpath, f"{fname}_sa.svg"), format="svg") dot_pref_graph.draw(path=os.path.join(fpath, f"{fname}_pg.svg"), format="svg")
[docs] def paut2base64(dot_semi_aut, dot_pref_graph): """ Convert the semi-automaton and preference graph images to base64 format. Useful for display on html pages. Args: dot_semi_aut (AGraph): The DOT representation of the semi-automaton. dot_pref_graph (AGraph): The DOT representation of the preference graph. Returns: tuple: A tuple containing two base64 encoded strings (semi-automaton and preference graph). """ sa = dot_semi_aut.draw(path=None, format="png") pg = dot_pref_graph.draw(path=None, format="png") return base64.b64encode(sa), base64.b64encode(pg)