Source code for quchip.viz.network

"""Static schematic of a chip's PortNetwork: Markov core, reference sections, planes."""

from __future__ import annotations

from collections import deque
from typing import Any

from matplotlib.figure import Figure
from matplotlib.patches import FancyBboxPatch

from quchip.chip.port_network import PortNetwork
from quchip.utils.jax_utils import maybe_concrete_scalar
from quchip.utils.labeling import resolve_label
from quchip.viz._style import _quchip_style, _resolve_single_axes

_CORE_COLOR = "#dbe9f6"
_REFERENCE_COLOR = "#f6e7c1"
_PLANE_COLOR = "#e2f0d9"
_PLANE_EDGE = "#2f7d32"
_HIDDEN_COLOR = "#7f7f7f"
_COLUMN_WIDTH = 3.0
_ROW_HEIGHT = 2.8


def _value_text(value: Any, precision: int = 3) -> str:
    """Format one tracked value; traced values render as ``<traced>``."""
    concrete = maybe_concrete_scalar(value)
    return "<traced>" if concrete is None else f"{concrete:.{precision}g}"


def _network_of(source: Any) -> PortNetwork:
    network = source if isinstance(source, PortNetwork) else getattr(source, "port_network", None)
    if network is None:
        raise ValueError("plot_port_network requires a PortNetwork or a chip with one attached.")
    return network


def _annotation(component: Any) -> list[str]:
    """Return the value lines drawn under a component label."""
    kind = component._kind
    parameters = component._parameters
    ports = [port for port in component._local_ports if port is not None]
    if ports:
        port = ports[0]
        targets = ", ".join(resolve_label(target) for target in port._targets)
        if port.rate is not None:
            return [f"port -> {targets}", f"rate {_value_text(port.rate)} /ns"]
        return [f"port -> {targets}", f"Q_ext {_value_text(port.external_quality_factor)}"]
    if kind == "circulator":
        return ["->".join((*component.sides, component.sides[0]))]
    if kind == "isolator":
        return ["1 -> 2", "2 -> load"]
    if kind == "filter":
        return [f"filter({', '.join(parameters)})"]
    if kind == "delay":
        return [f"{_value_text(parameters['duration'])} ns"]
    if kind == "amplifier":
        return [f"G {_value_text(parameters['gain'])}", f"n_add {_value_text(parameters['added_noise'])}"]
    if kind in {"attenuator", "beam_splitter"}:
        return [f"eta {_value_text(parameters['eta'])}"]
    if kind == "phase_shift":
        return [f"phase {_value_text(parameters['phase'])} rad"]
    return [kind]


def _columns(network: PortNetwork) -> dict[str, int]:
    """Assign each component a column by undirected graph distance from the chip ports."""
    neighbours: dict[str, set[str]] = {component.label: set() for component in network.components}
    for input_key, output_key in network._connections.items():
        neighbours[input_key[0]].add(output_key[0])
        neighbours[output_key[0]].add(input_key[0])
    columns: dict[str, int] = {}
    queue: deque[str] = deque()
    for component in network.components:
        if any(port is not None for port in component._local_ports):
            columns[component.label] = 0
            queue.append(component.label)
    while queue:
        label = queue.popleft()
        for other in sorted(neighbours[label]):
            if other not in columns:
                columns[other] = columns[label] + 1
                queue.append(other)
    for component in network.components:
        columns.setdefault(component.label, 0)
    return columns


def _box(axis: Any, x: float, y: float, width: float, height: float, **style: Any) -> None:
    axis.add_patch(
        FancyBboxPatch((x - width / 2, y - height / 2), width, height, boxstyle="round,pad=0.05", **style)
    )


[docs] def plot_port_network( source: Any, *, show_hidden: bool = True, ax: Any = None, ) -> Figure: """Draw a port network as a left-to-right schematic. Chip ports and Markovian components show in the shaded ``Markov core``. Reference sections (delays, filters, and amplifiers) and external planes show to its right. Cables made with ``link`` use double-headed edges, and directional connections use single arrows. A plane with separate ``input=`` and ``output=`` terminals has one arrow to each terminal. When *show_hidden* is true, hidden vacuum and load channels show as dashed stubs. Annotations show component values, including rates, ``eta``, durations, gain, and added noise. Traced values show as ``<traced>``. Parameters ---------- source : Chip or PortNetwork A chip with an attached port network, or the network itself. show_hidden : bool Draw hidden vacuum and load stubs. ax : matplotlib.axes.Axes, optional Existing axes to draw on. When ``None``, create a figure and axes. Returns ------- Figure The figure that contains the schematic (``ax.figure`` when *ax* is given). Raises ------ ValueError *source* is not a PortNetwork and has no attached port network. """ network = _network_of(source) columns = _columns(network) rows: dict[int, list[str]] = {} for label, column in columns.items(): rows.setdefault(column, []).append(label) positions: dict[str, tuple[float, float]] = {} for column, labels in rows.items(): for index, label in enumerate(labels): positions[label] = (_COLUMN_WIDTH * column, -_ROW_HEIGHT * (index - (len(labels) - 1) / 2.0)) plane_column = max(columns.values(), default=0) + 1 exposures = network.external_ports plane_positions: dict[str, tuple[float, float]] = {} for exposure in exposures: y = (positions[exposure._input_key[0]][1] + positions[exposure._output_key[0]][1]) / 2.0 while any(abs(y - taken[1]) < 0.7 for taken in plane_positions.values()): y -= 0.8 plane_positions[exposure.label] = (_COLUMN_WIDTH * plane_column, y) tallest = max(len(labels) for labels in rows.values()) with _quchip_style(): fig, axis = _resolve_single_axes(ax, figsize=(2.4 * (plane_column + 1.5), 1.2 + 2.2 * tallest)) axis.set_axis_off() axis.set_aspect("equal") core = [label for label in columns if not network._is_reference(label)] xs = [positions[label][0] for label in core] ys = [positions[label][1] for label in core] _box( axis, (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2, max(xs) - min(xs) + 2.2, max(ys) - min(ys) + 2.4, facecolor=_CORE_COLOR, edgecolor="none", zorder=0, ) axis.text(min(xs) - 1.1, max(ys) + 1.3, "Markov core", fontsize=9, color="#355c7d") for component in network.components: x, y = positions[component.label] reference = network._is_reference(component.label) fill = _REFERENCE_COLOR if reference else "white" _box(axis, x, y, 1.6, 1.4, facecolor=fill, edgecolor="#404040", zorder=2) axis.text(x, y + 0.4, component.label, ha="center", va="center", fontsize=9, weight="bold", zorder=3) notes = "\n".join(_annotation(component)) axis.text(x, y + 0.08, notes, ha="center", va="top", fontsize=7, zorder=3) if not show_hidden: continue count = len(component._hidden_pairs) for stub, (_, _, hidden_label) in enumerate(component._hidden_pairs): stub_x = x + 1.0 * (stub - (count - 1) / 2.0) stub_style: dict[str, Any] = {"linestyle": "--", "color": _HIDDEN_COLOR, "linewidth": 1, "zorder": 1} axis.plot([stub_x, stub_x], [y - 0.7, y - 1.2], **stub_style) stub_name = hidden_label.split(".")[-1] axis.text(stub_x, y - 1.3, stub_name, ha="center", va="top", fontsize=6, color=_HIDDEN_COLOR) drawn: set[frozenset[tuple[str, str]]] = set() for input_key, output_key in network._connections.items(): pair = frozenset((input_key, output_key)) if pair in drawn: continue drawn.add(pair) reciprocal = network._connections.get(output_key) == input_key axis.annotate( "", xy=positions[input_key[0]], xytext=positions[output_key[0]], arrowprops={ "arrowstyle": "<->" if reciprocal else "->", "color": "#303030", "lw": 1.2, "shrinkA": 30, "shrinkB": 30, "mutation_scale": 14, }, zorder=1, ) for exposure in exposures: x, y = plane_positions[exposure.label] plane_edge = (x - 0.5, y) source_x, source_y = positions[exposure._input_key[0]] sink_x, sink_y = positions[exposure._output_key[0]] source_edge, sink_edge = (source_x + 0.8, source_y), (sink_x + 0.8, sink_y) symmetric = exposure._input_key == exposure._output_key legs = ( [(plane_edge, source_edge, "<->")] if symmetric else [(source_edge, plane_edge, "->"), (plane_edge, sink_edge, "->")] ) for head, tail, style in legs: arrow: dict[str, Any] = {"arrowstyle": style, "color": _PLANE_EDGE, "lw": 1.2, "mutation_scale": 14} if not symmetric: arrow["connectionstyle"] = "arc3,rad=0.25" axis.annotate("", xy=head, xytext=tail, arrowprops=arrow, zorder=1) _box(axis, x, y, 1.0, 0.6, facecolor=_PLANE_COLOR, edgecolor=_PLANE_EDGE, zorder=2) axis.text(x, y, exposure.label, ha="center", va="center", fontsize=8, zorder=3) axis.relim() axis.autoscale_view() axis.margins(0.15) return fig