Cirq
Cirq copied to clipboard
Generalize TextDiagramDrawer to allow drawing outline boxes
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.
-
Box interiors should cover lines but not text entries. The layer order is lines then boxes then text.
-
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
┍┯┑ ┝┿┥ ┕┷┙ ┎┰┒ ┠╂┨ ┖┸┚
- Including if the line is emphasized and the box is not
-
(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.
-
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)
How would the boxes be specified?
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).
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?
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.
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.
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?
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.
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:
- Implement
TextDiagramDrawer.vertical_cut
a laTextDiagramDrawer.horizontal_cut
. - 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.
@Strilanc I think this is maybe stale with current BlockDiagramDrawer. Do you think there is more that needs to be done?
Close this as stale with the current BlockDiagramDrawer
. There are also other issues tracking the need to have better visualizations support in Cirq.