Skip to content

emic.output

Visualization and export utilities.

output

Output and visualization for epsilon-machines.

This module provides functions for visualizing, exporting, and serializing epsilon-machines and analysis results.

Functions:

Name Description
render_state_diagram

Render machine as Graphviz diagram

display_state_diagram

Display diagram in Jupyter

to_tikz

Export machine as TikZ code

to_mermaid

Export machine as Mermaid diagram

to_dot

Export machine as DOT format

to_json

Serialize machine to JSON

from_json

Deserialize machine from JSON

to_latex_table

Format analysis results as LaTeX table

Example

from emic.sources import GoldenMeanSource from emic.output import render_state_diagram, to_tikz machine = GoldenMeanSource(p=0.5).true_machine diagram = render_state_diagram(machine) _ = diagram.render('golden_mean', format='pdf') # doctest: +SKIP

DiagramStyle dataclass

DiagramStyle(
    layout: Literal["dot", "neato", "circo", "fdp"] = "dot",
    rankdir: Literal["LR", "TB", "RL", "BT"] = "LR",
    node_shape: str = "circle",
    node_color: str = "#4a90d9",
    node_fontsize: int = 12,
    node_width: float = 0.5,
    edge_fontsize: int = 10,
    edge_color: str = "#333333",
    show_probabilities: bool = True,
    probability_format: str = ".2f",
    state_labels: dict[str, str] = dict(),
    symbol_labels: dict[str, str] = dict(),
    size: tuple[float, float] | None = None,
    dpi: int = 150,
)

Configuration for state diagram rendering.

Attributes:

Name Type Description
layout Literal['dot', 'neato', 'circo', 'fdp']

Graphviz layout engine (dot, neato, circo, fdp).

rankdir Literal['LR', 'TB', 'RL', 'BT']

Graph direction (LR=left-to-right, TB=top-to-bottom).

node_shape str

Shape of state nodes (circle, doublecircle, box).

node_color str

Fill color for nodes (hex or name).

node_fontsize int

Font size for node labels.

node_width float

Minimum width of nodes in inches.

edge_fontsize int

Font size for edge labels.

edge_color str

Color of edges.

show_probabilities bool

Whether to show transition probabilities.

probability_format str

Format string for probabilities.

state_labels dict[str, str]

Custom labels for states (state_id -> label).

symbol_labels dict[str, str]

Custom labels for symbols (str(symbol) -> label).

size tuple[float, float] | None

Figure size in inches (width, height).

dpi int

Resolution in dots per inch.

render_state_diagram

render_state_diagram(
    machine: EpsilonMachine[A],
    style: DiagramStyle | None = None,
) -> Digraph

Render an epsilon-machine as a state diagram.

Creates a Graphviz directed graph representing the epsilon-machine. States are shown as nodes and transitions as labeled edges.

Parameters:

Name Type Description Default
machine EpsilonMachine[A]

The epsilon-machine to visualize.

required
style DiagramStyle | None

Rendering configuration. Uses defaults if not provided.

None

Returns:

Type Description
Digraph

A graphviz.Digraph object that can be rendered to various formats.

Raises:

Type Description
ImportError

If graphviz is not installed.

Example

from emic.sources import GoldenMeanSource from emic.output import render_state_diagram machine = GoldenMeanSource(p=0.5).true_machine diagram = render_state_diagram(machine) diagram.render('golden_mean', format='pdf') 'golden_mean.pdf'

Source code in src/emic/output/diagram.py
def render_state_diagram(
    machine: EpsilonMachine[A],
    style: DiagramStyle | None = None,
) -> graphviz.Digraph:
    """Render an epsilon-machine as a state diagram.

    Creates a Graphviz directed graph representing the epsilon-machine.
    States are shown as nodes and transitions as labeled edges.

    Args:
        machine: The epsilon-machine to visualize.
        style: Rendering configuration. Uses defaults if not provided.

    Returns:
        A graphviz.Digraph object that can be rendered to various formats.

    Raises:
        ImportError: If graphviz is not installed.

    Example:
        >>> from emic.sources import GoldenMeanSource
        >>> from emic.output import render_state_diagram
        >>> machine = GoldenMeanSource(p=0.5).true_machine
        >>> diagram = render_state_diagram(machine)
        >>> diagram.render('golden_mean', format='pdf')
        'golden_mean.pdf'
    """
    try:
        import graphviz
    except ImportError as e:
        msg = "graphviz is required for diagram rendering. Install with: pip install graphviz"
        raise ImportError(msg) from e

    style = style or DiagramStyle()

    graph_attr: dict[str, str] = {
        "rankdir": style.rankdir,
        "dpi": str(style.dpi),
    }
    if style.size:
        graph_attr["size"] = f"{style.size[0]},{style.size[1]}"

    dot = graphviz.Digraph(
        engine=style.layout,
        graph_attr=graph_attr,
        node_attr={
            "shape": style.node_shape,
            "style": "filled",
            "fillcolor": style.node_color,
            "fontsize": str(style.node_fontsize),
            "width": str(style.node_width),
        },
        edge_attr={
            "fontsize": str(style.edge_fontsize),
            "color": style.edge_color,
        },
    )

    # Add nodes
    for state in machine.states:
        label = style.state_labels.get(state.id, state.id)
        dot.node(state.id, label)

    # Add edges
    for state in machine.states:
        for trans in state.transitions:
            symbol_str = str(trans.symbol)
            symbol = style.symbol_labels.get(symbol_str, symbol_str)
            if style.show_probabilities:
                label = f"{symbol} ({trans.probability:{style.probability_format}})"
            else:
                label = symbol
            dot.edge(state.id, trans.target, label)

    return dot

display_state_diagram

display_state_diagram(
    machine: EpsilonMachine[A],
    style: DiagramStyle | None = None,
) -> None

Display state diagram in Jupyter notebook.

Renders the epsilon-machine as an inline SVG in Jupyter environments.

Parameters:

Name Type Description Default
machine EpsilonMachine[A]

The epsilon-machine to visualize.

required
style DiagramStyle | None

Rendering configuration. Uses defaults if not provided.

None

Raises:

Type Description
ImportError

If graphviz or IPython is not installed.

Example

from emic.sources import GoldenMeanSource from emic.output import display_state_diagram display_state_diagram(GoldenMeanSource().true_machine) # doctest: +SKIP

Source code in src/emic/output/diagram.py
def display_state_diagram(
    machine: EpsilonMachine[A],
    style: DiagramStyle | None = None,
) -> None:
    """Display state diagram in Jupyter notebook.

    Renders the epsilon-machine as an inline SVG in Jupyter environments.

    Args:
        machine: The epsilon-machine to visualize.
        style: Rendering configuration. Uses defaults if not provided.

    Raises:
        ImportError: If graphviz or IPython is not installed.

    Example:
        >>> from emic.sources import GoldenMeanSource
        >>> from emic.output import display_state_diagram
        >>> display_state_diagram(GoldenMeanSource().true_machine)  # doctest: +SKIP
    """
    diagram = render_state_diagram(machine, style)
    try:
        from IPython.display import display
    except ImportError as e:
        msg = "IPython is required for notebook display"
        raise ImportError(msg) from e

    display(diagram)

to_tikz

to_tikz(
    machine: EpsilonMachine[A],
    style: DiagramStyle | None = None,
) -> str

Generate TikZ code for a state diagram.

Creates LaTeX/TikZ code that can be included in documents to render a publication-quality state diagram.

Parameters:

Name Type Description Default
machine EpsilonMachine[A]

The epsilon-machine to export.

required
style DiagramStyle | None

Rendering configuration for labels and formatting.

None

Returns:

Type Description
str

LaTeX/TikZ code as a string.

Example

from emic.sources import GoldenMeanSource from emic.output import to_tikz machine = GoldenMeanSource(p=0.5).true_machine tikz = to_tikz(machine) print(tikz) # doctest: +ELLIPSIS \begin{tikzpicture}...

Source code in src/emic/output/latex.py
def to_tikz(
    machine: EpsilonMachine[A],
    style: DiagramStyle | None = None,
) -> str:
    """Generate TikZ code for a state diagram.

    Creates LaTeX/TikZ code that can be included in documents to render
    a publication-quality state diagram.

    Args:
        machine: The epsilon-machine to export.
        style: Rendering configuration for labels and formatting.

    Returns:
        LaTeX/TikZ code as a string.

    Example:
        >>> from emic.sources import GoldenMeanSource
        >>> from emic.output import to_tikz
        >>> machine = GoldenMeanSource(p=0.5).true_machine
        >>> tikz = to_tikz(machine)
        >>> print(tikz)  # doctest: +ELLIPSIS
        \\begin{tikzpicture}...
    """
    from emic.output.diagram import DiagramStyle

    style = style or DiagramStyle()

    lines = [
        r"\begin{tikzpicture}[->,>=stealth',shorten >=1pt,auto,node distance=2.5cm]",
        r"  \tikzstyle{state}=[circle,draw,minimum size=1cm,fill=blue!20]",
    ]

    # Add nodes with positions
    state_list = list(machine.states)
    for i, state in enumerate(state_list):
        label = style.state_labels.get(state.id, state.id)
        x = i * 3  # Simple horizontal layout
        lines.append(f"  \\node[state] ({state.id}) at ({x},0) {{${label}$}};")

    # Add edges
    for state in machine.states:
        for trans in state.transitions:
            symbol_str = str(trans.symbol)
            symbol = style.symbol_labels.get(symbol_str, symbol_str)
            if style.show_probabilities:
                label = f"{symbol}/{trans.probability:{style.probability_format}}"
            else:
                label = symbol

            if trans.target == state.id:
                # Self-loop
                lines.append(
                    f"  \\path ({state.id}) edge [loop above] node {{${label}$}} ({state.id});"
                )
            else:
                lines.append(f"  \\path ({state.id}) edge node {{${label}$}} ({trans.target});")

    lines.append(r"\end{tikzpicture}")

    return "\n".join(lines)

to_latex_table

to_latex_table(
    summaries: list[tuple[str, AnalysisSummary]],
    measures: list[str] | None = None,
    caption: str = "Epsilon-machine analysis",
    label: str = "tab:analysis",
) -> str

Generate LaTeX table of analysis results.

Creates a publication-ready LaTeX table comparing complexity measures across multiple processes or machines.

Parameters:

Name Type Description Default
summaries list[tuple[str, AnalysisSummary]]

List of (name, summary) pairs to include.

required
measures list[str] | None

Which measures to include. Defaults to common measures.

None
caption str

Table caption.

'Epsilon-machine analysis'
label str

LaTeX label for cross-referencing.

'tab:analysis'

Returns:

Type Description
str

LaTeX table code as a string.

Example

from emic.sources import GoldenMeanSource, BiasedCoinSource from emic.analysis import analyze from emic.output import to_latex_table results = [ ... ("Golden Mean", analyze(GoldenMeanSource(p=0.5).true_machine)), ... ("Biased Coin", analyze(BiasedCoinSource(p=0.7).true_machine)), ... ] latex = to_latex_table(results) print(latex) # doctest: +ELLIPSIS \begin{table}...

Source code in src/emic/output/latex.py
def to_latex_table(
    summaries: list[tuple[str, AnalysisSummary]],
    measures: list[str] | None = None,
    caption: str = "Epsilon-machine analysis",
    label: str = "tab:analysis",
) -> str:
    """Generate LaTeX table of analysis results.

    Creates a publication-ready LaTeX table comparing complexity measures
    across multiple processes or machines.

    Args:
        summaries: List of (name, summary) pairs to include.
        measures: Which measures to include. Defaults to common measures.
        caption: Table caption.
        label: LaTeX label for cross-referencing.

    Returns:
        LaTeX table code as a string.

    Example:
        >>> from emic.sources import GoldenMeanSource, BiasedCoinSource
        >>> from emic.analysis import analyze
        >>> from emic.output import to_latex_table
        >>> results = [
        ...     ("Golden Mean", analyze(GoldenMeanSource(p=0.5).true_machine)),
        ...     ("Biased Coin", analyze(BiasedCoinSource(p=0.7).true_machine)),
        ... ]
        >>> latex = to_latex_table(results)
        >>> print(latex)  # doctest: +ELLIPSIS
        \\begin{table}...
    """
    measures = measures or [
        "num_states",
        "statistical_complexity",
        "entropy_rate",
        "excess_entropy",
    ]

    # Header mapping to LaTeX math notation
    header_map = {
        "num_states": r"$|\mathcal{S}|$",
        "num_transitions": r"$|\mathcal{T}|$",
        "statistical_complexity": r"$C_\mu$",
        "entropy_rate": r"$h_\mu$",
        "excess_entropy": r"$E$",
        "topological_complexity": r"$C_0$",
    }

    headers = ["Process"] + [header_map.get(m, m) for m in measures]

    lines = [
        r"\begin{table}[htbp]",
        r"\centering",
        r"\caption{" + caption + "}",
        r"\label{" + label + "}",
        r"\begin{tabular}{l" + "c" * len(measures) + "}",
        r"\toprule",
        " & ".join(headers) + r" \\",
        r"\midrule",
    ]

    for name, summary in summaries:
        values = [name]
        for m in measures:
            v = getattr(summary, m, None)
            if v is None:
                values.append("--")
            elif isinstance(v, float):
                values.append(f"{v:.4f}")
            else:
                values.append(str(v))
        lines.append(" & ".join(values) + r" \\")

    lines.extend(
        [
            r"\bottomrule",
            r"\end{tabular}",
            r"\end{table}",
        ]
    )

    return "\n".join(lines)

to_dot

to_dot(machine: EpsilonMachine[A]) -> str

Export epsilon-machine as DOT format.

Creates a DOT (Graphviz) representation that can be rendered by Graphviz tools or imported into other visualization software.

Parameters:

Name Type Description Default
machine EpsilonMachine[A]

The epsilon-machine to export.

required

Returns:

Type Description
str

DOT format string.

Example

from emic.sources import GoldenMeanSource from emic.output import to_dot machine = GoldenMeanSource(p=0.5).true_machine dot = to_dot(machine) print(dot) # doctest: +ELLIPSIS digraph { ...

Source code in src/emic/output/serialization.py
def to_dot(machine: EpsilonMachine[A]) -> str:
    """Export epsilon-machine as DOT format.

    Creates a DOT (Graphviz) representation that can be rendered
    by Graphviz tools or imported into other visualization software.

    Args:
        machine: The epsilon-machine to export.

    Returns:
        DOT format string.

    Example:
        >>> from emic.sources import GoldenMeanSource
        >>> from emic.output import to_dot
        >>> machine = GoldenMeanSource(p=0.5).true_machine
        >>> dot = to_dot(machine)
        >>> print(dot)  # doctest: +ELLIPSIS
        digraph {
        ...
    """
    lines = [
        "digraph {",
        "  rankdir=LR;",
        '  node [shape=circle, style=filled, fillcolor="#4a90d9"];',
    ]

    # Add nodes
    for state in machine.states:
        lines.append(f'  "{state.id}";')

    # Add edges
    for state in machine.states:
        for trans in state.transitions:
            label = f"{trans.symbol} ({trans.probability:.2f})"
            lines.append(f'  "{state.id}" -> "{trans.target}" [label="{label}"];')

    lines.append("}")
    return "\n".join(lines)

to_mermaid

to_mermaid(machine: EpsilonMachine[A]) -> str

Export epsilon-machine as Mermaid diagram.

Creates a Mermaid state diagram that can be rendered in Markdown documents on GitHub, GitLab, and other platforms.

Parameters:

Name Type Description Default
machine EpsilonMachine[A]

The epsilon-machine to export.

required

Returns:

Type Description
str

Mermaid diagram code.

Example

from emic.sources import GoldenMeanSource from emic.output import to_mermaid machine = GoldenMeanSource(p=0.5).true_machine mermaid = to_mermaid(machine) print(mermaid) # doctest: +ELLIPSIS stateDiagram-v2 ...

Source code in src/emic/output/serialization.py
def to_mermaid(machine: EpsilonMachine[A]) -> str:
    """Export epsilon-machine as Mermaid diagram.

    Creates a Mermaid state diagram that can be rendered in Markdown
    documents on GitHub, GitLab, and other platforms.

    Args:
        machine: The epsilon-machine to export.

    Returns:
        Mermaid diagram code.

    Example:
        >>> from emic.sources import GoldenMeanSource
        >>> from emic.output import to_mermaid
        >>> machine = GoldenMeanSource(p=0.5).true_machine
        >>> mermaid = to_mermaid(machine)
        >>> print(mermaid)  # doctest: +ELLIPSIS
        stateDiagram-v2
        ...
    """
    lines = ["stateDiagram-v2"]

    for state in machine.states:
        for trans in state.transitions:
            label = f"{trans.symbol} | {trans.probability:.2f}"
            lines.append(f"    {state.id} --> {trans.target} : {label}")

    return "\n".join(lines)

to_json

to_json(machine: EpsilonMachine[A]) -> str

Serialize an epsilon-machine to JSON.

Creates a JSON representation that can be saved to a file or transmitted over a network. Use from_json to deserialize.

Parameters:

Name Type Description Default
machine EpsilonMachine[A]

The epsilon-machine to serialize.

required

Returns:

Type Description
str

JSON string representation.

Example

from emic.sources import GoldenMeanSource from emic.output import to_json, from_json machine = GoldenMeanSource(p=0.5).true_machine json_str = to_json(machine) restored = from_json(json_str) len(restored) == len(machine) True

Source code in src/emic/output/serialization.py
def to_json(machine: EpsilonMachine[A]) -> str:
    """Serialize an epsilon-machine to JSON.

    Creates a JSON representation that can be saved to a file or
    transmitted over a network. Use `from_json` to deserialize.

    Args:
        machine: The epsilon-machine to serialize.

    Returns:
        JSON string representation.

    Example:
        >>> from emic.sources import GoldenMeanSource
        >>> from emic.output import to_json, from_json
        >>> machine = GoldenMeanSource(p=0.5).true_machine
        >>> json_str = to_json(machine)
        >>> restored = from_json(json_str)
        >>> len(restored) == len(machine)
        True
    """
    states_list: list[dict[str, Any]] = []
    for state in machine.states:
        state_data: dict[str, Any] = {
            "id": state.id,
            "transitions": [
                {
                    "symbol": t.symbol,
                    "target": t.target,
                    "probability": t.probability,
                }
                for t in state.transitions
            ],
        }
        states_list.append(state_data)

    data: dict[str, Any] = {
        "alphabet": list(machine.alphabet),
        "start_state": machine.start_state,
        "states": states_list,
    }

    # Include stationary distribution if available
    if machine.stationary_distribution:
        data["stationary_distribution"] = {
            state_id: float(prob)
            for state_id, prob in machine.stationary_distribution.probs.items()
        }

    return json.dumps(data, indent=2)

from_json

from_json(json_str: str) -> EpsilonMachine[Any]

Deserialize an epsilon-machine from JSON.

Reconstructs an epsilon-machine from its JSON representation created by to_json.

Parameters:

Name Type Description Default
json_str str

JSON string representation.

required

Returns:

Type Description
EpsilonMachine[Any]

The reconstructed epsilon-machine.

Raises:

Type Description
ValueError

If the JSON is malformed or missing required fields.

JSONDecodeError

If the string is not valid JSON.

Example

from emic.output import from_json json_str = '''{"alphabet": [0, 1], "start_state": "A", ... "states": [{"id": "A", "transitions": [ ... {"symbol": 0, "target": "A", "probability": 0.5}, ... {"symbol": 1, "target": "A", "probability": 0.5} ... ]}]}''' machine = from_json(json_str) len(machine) 1

Source code in src/emic/output/serialization.py
def from_json(json_str: str) -> EpsilonMachine[Any]:
    """Deserialize an epsilon-machine from JSON.

    Reconstructs an epsilon-machine from its JSON representation
    created by `to_json`.

    Args:
        json_str: JSON string representation.

    Returns:
        The reconstructed epsilon-machine.

    Raises:
        ValueError: If the JSON is malformed or missing required fields.
        json.JSONDecodeError: If the string is not valid JSON.

    Example:
        >>> from emic.output import from_json
        >>> json_str = '''{"alphabet": [0, 1], "start_state": "A",
        ...     "states": [{"id": "A", "transitions": [
        ...         {"symbol": 0, "target": "A", "probability": 0.5},
        ...         {"symbol": 1, "target": "A", "probability": 0.5}
        ...     ]}]}'''
        >>> machine = from_json(json_str)
        >>> len(machine)
        1
    """
    from emic.types import EpsilonMachineBuilder

    data = json.loads(json_str)

    builder: EpsilonMachineBuilder[Any] = EpsilonMachineBuilder()

    for state_data in data["states"]:
        state_id = state_data["id"]
        for trans in state_data["transitions"]:
            builder.add_transition(
                source=state_id,
                symbol=trans["symbol"],
                target=trans["target"],
                probability=trans["probability"],
            )

    if "start_state" in data:
        builder.with_start_state(data["start_state"])

    return builder.build()