Cirq icon indicating copy to clipboard operation
Cirq copied to clipboard

Generalize TextDiagramDrawer to allow drawing outline boxes

Open Strilanc opened this issue 6 years ago • 9 comments

This issue is harder than it sounds. But I do think it's important for us to start being able to draw multi-qubit boxes in text diagrams and in the latex output.

  1. Box interiors should cover lines but not text entries. The layer order is lines then boxes then text.

  2. When a Box border intersects a line, it should produce the appropriate unicode character.

    ┌─┬┐
    │ ││
    ├─┼┤
    └─┴┘
    ┏━┳┓
    ┃ ┃┃
    ┣━╋┫
    ┗━┻┛
    
    • Including if the line is emphasized and the box is not
      ┍┯┑
      ┝┿┥
      ┕┷┙
      ┎┰┒
      ┠╂┨
      ┖┸┚
      
  3. (Opinion) boxes may have their corners at half-coordinates, so that they can be used to surround a text cell without inserting an extra column or row.

  4. A useful intermediate concept is a grid made up of a bunch of common cells. Each cell specifies a min width, min height, text, left/up/down/right line character, and a center line character. The more general render method can produce a bunch of cells, and then the cell rendered can handle picky layout details like sizing.

Untested code for 4:

TextDiagramCell = NamedTuple('TextDiagramCell', [
    ('center', str),
    ('left_line', str),
    ('right_line', str),
    ('up_line', str),
    ('down_line', str),
    ('text', str),
    ('min_width', int),
    ('min_height', int),
])


def render_cell_lines(cell: TextDiagramCell,
                      width: int,
                      height: int) -> List[str]:
    out = collections.defaultdict(lambda: ' ')
    cy = height // 2
    cx = width // 2

    if cell.left_line:
        for x in range(cx):
            out[(x, cy)] = cell.left_line
    if cell.right_line:
        for x in range(cx + 1, width):
            out[(x, cy)] = cell.right_line
    if cell.up_line:
        for y in range(cy):
            out[(cx, y)] = cell.up_line
    if cell.down_line:
        for y in range(cy + 1, height):
            out[(cx, y)] = cell.down_line
    if cell.center:
        out[cx, cy] = cell.center

    lines = cell.text.split('\n')
    lines_height = len(lines)
    lines_width = max(len(line) for line in lines)
    off_x = (width - lines_width) // 2
    off_y = (height - lines_height) // 2
    for dy, line in enumerate(lines):
        for dx, c in enumerate(line):
            if 0 <= dx < width and 0 <= dy < height:
                out[(off_x + dx, off_y + dy)] = c

    return [
        ''.join(
            out[(x, y)]
            for x in range(width)
        )
        for y in range(height)
    ]


def render_cell_row(cells: List[TextDiagramCell],
                    widths: List[int]) -> str:
    height = max(
        max(cell.min_height, len(cell.text.split('\n')))
        for cell in cells
    )

    cell_lines = [
        render_cell_lines(cell, width, height)
        for cell, width in zip(cells, widths)
        for y in range(height)
    ]

    if not cell_lines:
        return ''

    transposed = zip(*cell_lines)
    return '\n'.join(
        ''.join(line_parts)
        for line_parts in transposed
    )


def render_cell_grid(width: int,
                     height: int,
                     cell_at: Callable[[int, int], TextDiagramCell]) -> str:
    widths = []
    for x in range(width):
        col_width = 0
        for y in range(height):
            c = cell_at(x, y)
            lines = c.text.split('\n')
            min_text_width = max(len(line) for line in lines)
            col_width = max(col_width, c.min_width, min_text_width)
        widths.append(col_width)

    rendered_rows = []
    for y in range(height):
        cells = [cell_at(x, y) for x in range(width)]
        rendered_rows.append(render_cell_row(cells, widths))
    return '\n'.join(rendered_rows)

Strilanc avatar Dec 06 '18 02:12 Strilanc

How would the boxes be specified?

bryano avatar Dec 06 '18 06:12 bryano

diagram.box(x1, y1, x2, y2)

If we're rooting the box at half-integer coordinates, then the coordinates (1, 1, 2, 2) would actually refer to (0.5, 0.5, 1.5, 1.5) and make a 1x1 box surrounding the text entry at (1, 1).

Strilanc avatar Dec 06 '18 08:12 Strilanc

I meant at a higher level. I imagibe you'd want to box a set of operations or something like that. Relatedly, what use case(s) do you have in mind?

bryano avatar Dec 06 '18 17:12 bryano

Ah. I try to keep the functionality of TextDiagramDrawer generic enough that it's not about circuits, but the reason for adding this feature is to do a better job of printing circuits with gates that act on adjacent qubits. For example, if you were printing a linear swap network with swaps between adjacent qubits then you could have a box that says "XX^0.5" instead of an XX^0.5 linked by a line to an XX on the other qubit. Gates could opt into this functionality by specifying additional details in their diagram info.

Strilanc avatar Dec 06 '18 18:12 Strilanc

I see. Something similar is done in qcircuit, but the difference there is that it's just a matter of changing the wire symbols to use the "multigate" macro.

bryano avatar Dec 07 '18 07:12 bryano

If boxes are "in front" of lines (1), under what circumstances would a line cross a box (2)? Would we need to allow intersecting boxes?

bryano avatar Dec 08 '18 18:12 bryano

Whenever a box is placed over a line, the line intersects the side of the box as it crosses behind the box. This requires a character to indicate the line apparently terminating into the side of the box.

For intersecting boxes... I guess we could go with last-added-wins and treat previous box boundaries as lines.

Strilanc avatar Dec 10 '18 00:12 Strilanc

The problem I see with the gridlines-at-half-integers approach is that gates on adjacent qubits should look like

    ┌─┐
    │G| 
    └─┘
    ┌─┐
    │G│
    └─┘

rather than

    ┌─┐
    │G| 
    ├─┤
    │G│
    └─┘

See #1240 for a first step in an alternative direction:

  1. Implement TextDiagramDrawer.vertical_cut a la TextDiagramDrawer.horizontal_cut.
  2. Implement TextDiagramDrawer.add_boxes that a) Checks that boxes don't intersect. b) Inserts columns and rows around boxes with zero padding, taking into account that a set of boxes may have the same column or row as a boundary and only adding one there. c) Writes corner elements as text entries. d) Cuts existing lines through boxes e) Writes new lines for box boundaries.

bryano avatar Dec 10 '18 22:12 bryano

@Strilanc I think this is maybe stale with current BlockDiagramDrawer. Do you think there is more that needs to be done?

dabacon avatar May 03 '20 13:05 dabacon

Close this as stale with the current BlockDiagramDrawer. There are also other issues tracking the need to have better visualizations support in Cirq.

tanujkhattar avatar Feb 02 '24 07:02 tanujkhattar