Skip to content

graflo.plot.equivalence

Draw one equivalence as four small graphs, each about one thing.

An equivalence says several things at once, and one picture of all of them is a tangle of arrows. Each view here draws one of them, as a graph whose partitions read left to right:

  • members -- which types become the merged type: left members, the merged type, right members. A member a vocabulary joins, rather than one the equivalence names, has a dashed edge.
  • attributes -- where each member attribute lands on the merged type: a dark edge for a declared equivalence, a blue one for a vocabulary rename, a thin grey one for a name kept as it is, a dotted one for a mapping a later declaration overrode.
  • identity -- how a record is keyed: raw fields, the branches a record tries in order (#1, #2, ...), and the key they feed. An edge into a branch is labelled with the transform or the tag it reads with.
  • lookups -- each member's own key, kept on the merged type as a lookup; the edge is labelled with the key space.

Attributes and raw fields are small nodes inside a box named after their type or resource; boxes are filled by side. A figure carries no title, legend or note, so it can be embedded anywhere: :func:plot_equivalences writes what they would say -- the declaration, the identity in words, the findings, a caption per view -- to summary.md, and all of it to index.json.

Attributes

CAPTION = {'members': 'Members. The types that become the merged type; a dashed edge marks a member a vocabulary joins rather than one the equivalence names.', 'attributes': 'Attributes. Where each member attribute lands on the merged type: dark, a declared equivalence; blue, a vocabulary rename; thin grey, the name kept as it is; dotted, a mapping a later declaration overrode.', 'identity': 'Identity. The raw fields each branch reads, labelled with the transform or the tag; a record tries the branches in order, #1 first, and the first it completes keys it.', 'lookups': "Lookups. Each member's own key, kept on the merged type as a lookup in the key space the edge names."} module-attribute

INDEX_VERSION = 2 module-attribute

SIDE_FILL = {'left': '#E6EFF5', 'right': '#E7F3EA', 'merged': '#FFF1DB', '': '#F0F0F0'} module-attribute

STYLE = {'declared': {'color': _INK, 'penwidth': '1.3'}, 'vocabulary': {'color': '#1F5FBF', 'penwidth': '1.3'}, 'joined': {'color': '#1F5FBF', 'penwidth': '1.3', 'style': 'dashed'}, 'kept': {'color': '#A8A8A8', 'penwidth': '0.7'}, 'overridden': {'color': '#B0B0B0', 'penwidth': '0.9', 'style': 'dotted'}, 'source': {'color': '#B9770E', 'penwidth': '1.1'}, 'key': {'color': _INK, 'penwidth': '1.1'}, 'lookup': {'color': '#8E5A2B', 'penwidth': '1.1'}} module-attribute

VIEWS = ('members', 'attributes', 'identity', 'lookups') module-attribute

View = Literal['members', 'attributes', 'identity', 'lookups'] module-attribute

__all__ = ['CAPTION', 'INDEX_VERSION', 'SIDE_FILL', 'STYLE', 'VIEWS', 'View', 'build_equivalence_view', 'plot_equivalence', 'plot_equivalences', 'select_targets'] module-attribute

Classes

Functions:

build_equivalence_view(preview, target, view)

One view of one equivalence as a networkx graph, or None when it shows nothing.

Every node carries the preview id it stands for as preview_id, and every edge its style as kind. The boxes graph attribute maps a box id to (label, side, [node, ...]); ranks lists node groups drawn in one rank, in order.

Parameters:

Name Type Description Default
preview MergePreview

The whole preview, from preview_merge or declared_preview.

required
target str | int

A cluster id, or a position in the op's vertex_equivalences.

required
view View

One of :data:VIEWS.

required

Raises:

Type Description
KeyError

target names no equivalence.

Source code in graflo/plot/equivalence.py
def build_equivalence_view(
    preview: MergePreview, target: str | int, view: View
) -> nx.MultiDiGraph | None:
    """One view of one equivalence as a networkx graph, or ``None`` when it shows nothing.

    Every node carries the preview id it stands for as ``preview_id``, and
    every edge its style as ``kind``. The ``boxes`` graph attribute maps a
    box id to ``(label, side, [node, ...])``; ``ranks`` lists node groups
    drawn in one rank, in order.

    Args:
        preview: The whole preview, from ``preview_merge`` or ``declared_preview``.
        target: A cluster id, or a position in the op's ``vertex_equivalences``.
        view: One of :data:`VIEWS`.

    Raises:
        KeyError: *target* names no equivalence.
    """
    fig = _build(preview, target, view)
    if fig is None:
        return None
    fig.graph.graph["boxes"] = fig.boxes
    fig.graph.graph["ranks"] = fig.ranks
    return fig.graph

plot_equivalence(preview, target, directory, *, views=VIEWS, output_format='svg', stem=None, prog='dot', dpi=None)

Draw views of one equivalence into directory; a view with nothing to show is skipped.

Files are <stem>.<view>.<format>; stem defaults to <kind>-<merged name>, e.g. vertex-Machine.identity.svg.

Parameters:

Name Type Description Default
preview MergePreview

The whole preview, from preview_merge or declared_preview.

required
target str | int

A cluster id, or a position in the op's vertex_equivalences.

required
directory str | PathLike[str]

Where to write; created if missing.

required
views Sequence[View]

Which views to draw.

VIEWS
output_format str

svg, png, pdf or dot.

'svg'
stem str | None

File name stem.

None
prog str

Graphviz layout program.

'dot'
dpi int | None

Raster resolution, for png.

None

Returns:

Type Description
dict[View, Path]

The path written per view.

Raises:

Type Description
KeyError

target names no equivalence.

RuntimeError

Rendering is unavailable or failed; the message says why.

Source code in graflo/plot/equivalence.py
def plot_equivalence(
    preview: MergePreview,
    target: str | int,
    directory: str | os.PathLike[str],
    *,
    views: Sequence[View] = VIEWS,
    output_format: str = "svg",
    stem: str | None = None,
    prog: str = "dot",
    dpi: int | None = None,
) -> dict[View, Path]:
    """Draw *views* of one equivalence into *directory*; a view with nothing to show is skipped.

    Files are ``<stem>.<view>.<format>``; *stem* defaults to
    ``<kind>-<merged name>``, e.g. ``vertex-Machine.identity.svg``.

    Args:
        preview: The whole preview, from ``preview_merge`` or ``declared_preview``.
        target: A cluster id, or a position in the op's ``vertex_equivalences``.
        directory: Where to write; created if missing.
        views: Which views to draw.
        output_format: ``svg``, ``png``, ``pdf`` or ``dot``.
        stem: File name stem.
        prog: Graphviz layout program.
        dpi: Raster resolution, for ``png``.

    Returns:
        The path written per view.

    Raises:
        KeyError: *target* names no equivalence.
        RuntimeError: Rendering is unavailable or failed; the message says why.
    """
    out = Path(directory)
    focused = preview.focus(target)
    cluster = _cluster(focused)
    stem = stem or _slug(f"{cluster.kind}-{cluster.into or cluster.id}")
    written: dict[View, Path] = {}
    for view in views:
        graph = build_equivalence_view(preview, target, view)
        if graph is None:
            continue
        written[view] = _draw(
            graph,
            out / f"{stem}.{view}.{output_format}",
            output_format=output_format,
            prog=prog,
            dpi=dpi,
        )
    return written

plot_equivalences(preview, directory, *, select=(), views=VIEWS, output_format='svg', prog='dot', dpi=None)

Draw every selected equivalence into directory, with index.json and summary.md.

Parameters:

Name Type Description Default
preview MergePreview

The whole preview, after its outcome is folded in, so the finding positions in the index match preview.findings.

required
directory str | PathLike[str]

Where to write; created if missing.

required
select Sequence[str]

Patterns restricting the equivalences (see :func:select_targets).

()
views Sequence[View]

Which views to draw.

VIEWS
output_format str

svg, png, pdf or dot.

'svg'
prog str

Graphviz layout program.

'dot'
dpi int | None

Raster resolution, for png.

None

Returns:

Type Description
Path

The path of index.json.

Source code in graflo/plot/equivalence.py
def plot_equivalences(
    preview: MergePreview,
    directory: str | os.PathLike[str],
    *,
    select: Sequence[str] = (),
    views: Sequence[View] = VIEWS,
    output_format: str = "svg",
    prog: str = "dot",
    dpi: int | None = None,
) -> Path:
    """Draw every selected equivalence into *directory*, with ``index.json`` and ``summary.md``.

    Args:
        preview: The whole preview, after its outcome is folded in, so the
            finding positions in the index match ``preview.findings``.
        directory: Where to write; created if missing.
        select: Patterns restricting the equivalences (see :func:`select_targets`).
        views: Which views to draw.
        output_format: ``svg``, ``png``, ``pdf`` or ``dot``.
        prog: Graphviz layout program.
        dpi: Raster resolution, for ``png``.

    Returns:
        The path of ``index.json``.
    """
    from graflo.architecture.evolution.preview_focus import finding_positions

    out = Path(directory)
    out.mkdir(parents=True, exist_ok=True)
    taken: set[str] = set()
    entries: list[dict[str, Any]] = []
    held: set[int] = set()
    for cluster in _clusters(preview):
        held.update(finding_positions(preview, cluster.id))
    for target in select_targets(preview, select):
        cluster = next(c for c in preview.clusters if c.id == target)
        stem, n = _slug(f"{cluster.kind}-{cluster.into or cluster.id}"), 2
        unique = stem
        while unique.lower() in taken:
            unique, n = f"{stem}-{n}", n + 1
        taken.add(unique.lower())
        files = plot_equivalence(
            preview,
            target,
            out,
            views=views,
            output_format=output_format,
            stem=unique,
            prog=prog,
            dpi=dpi,
        )
        entries.append(
            {
                **_describe(preview, cluster),
                "views": {view: path.name for view, path in files.items()},
                "findings": [
                    _finding(preview, i) for i in finding_positions(preview, target)
                ],
            }
        )
    rest = [_finding(preview, i) for i in range(len(preview.findings)) if i not in held]
    index = out / "index.json"
    index.write_text(
        json.dumps(
            {
                "version": INDEX_VERSION,
                "left": preview.left_name,
                "right": preview.right_name,
                "outcome": preview.outcome.model_dump(mode="json"),
                "format": output_format,
                "summary": "summary.md",
                "equivalences": entries,
                "other_findings": rest,
            },
            indent=2,
        )
        + "\n"
    )
    (out / "summary.md").write_text(_summary(preview, entries, rest))
    return index

select_targets(preview, patterns)

The cluster ids patterns select, in reading order; all of them when none.

A pattern is a merged name, a position in vertex_equivalences, or an fnmatch pattern on the cluster id.

Source code in graflo/plot/equivalence.py
def select_targets(preview: MergePreview, patterns: Sequence[str]) -> list[str]:
    """The cluster ids *patterns* select, in reading order; all of them when none.

    A pattern is a merged name, a position in ``vertex_equivalences``, or an
    fnmatch pattern on the cluster id.
    """
    clusters = _clusters(preview)
    if not patterns:
        return [c.id for c in clusters]
    chosen: set[str] = set()
    for pattern in patterns:
        resolved = resolve_target(preview, pattern) if pattern.isdigit() else None
        for cluster in clusters:
            if (
                cluster.into == pattern
                or cluster.id == resolved
                or fnmatch(cluster.id, pattern)
            ):
                chosen.add(cluster.id)
    return [c.id for c in clusters if c.id in chosen]