Skip to content

Commit 099fc74

Browse files
authored
Draw zero-target operations below circuit text diagrams as annotations (#4234)
Also fix a couple testing assert methods not hiding themselves from the traceback. This can change the diagrams of existing circuits, which will potentially break some downstream tests that assert the diagrams are something specific. However, I think having the information missing from the diagrams was more problematic. Fixes #4232 Fixes #4233
1 parent ef5ae03 commit 099fc74

File tree

4 files changed

+206
-13
lines changed

4 files changed

+206
-13
lines changed

cirq-core/cirq/circuits/circuit.py

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,21 +1147,24 @@ def to_text_diagram_drawer(
11471147
diagram.write(0, 0, '')
11481148
for q, i in qubit_map.items():
11491149
diagram.write(0, i, qubit_namer(q))
1150+
first_annotation_row = max(qubit_map.values(), default=0) + 1
11501151

11511152
if any(isinstance(op.untagged, cirq.GlobalPhaseOperation) for op in self.all_operations()):
11521153
diagram.write(0, max(qubit_map.values(), default=0) + 1, 'global phase:')
1154+
first_annotation_row += 1
11531155

11541156
moment_groups = [] # type: List[Tuple[int, int]]
11551157
for moment in self.moments:
11561158
_draw_moment_in_diagram(
1157-
moment,
1158-
use_unicode_characters,
1159-
qubit_map,
1160-
diagram,
1161-
precision,
1162-
moment_groups,
1163-
get_circuit_diagram_info,
1164-
include_tags,
1159+
moment=moment,
1160+
use_unicode_characters=use_unicode_characters,
1161+
qubit_map=qubit_map,
1162+
out_diagram=diagram,
1163+
precision=precision,
1164+
moment_groups=moment_groups,
1165+
get_circuit_diagram_info=get_circuit_diagram_info,
1166+
include_tags=include_tags,
1167+
first_annotation_row=first_annotation_row,
11651168
)
11661169

11671170
w = diagram.width()
@@ -2306,7 +2309,55 @@ def _resolve_operations(
23062309
return resolved_operations
23072310

23082311

2312+
def _get_moment_annotations(
2313+
moment: 'cirq.Moment',
2314+
) -> Iterator['cirq.Operation']:
2315+
for op in moment.operations:
2316+
if op.qubits:
2317+
continue
2318+
op = op.untagged
2319+
if isinstance(op, ops.GlobalPhaseOperation):
2320+
continue
2321+
if isinstance(op, CircuitOperation):
2322+
for m in op.circuit:
2323+
yield from _get_moment_annotations(m)
2324+
else:
2325+
yield op
2326+
2327+
2328+
def _draw_moment_annotations(
2329+
*,
2330+
moment: 'cirq.Moment',
2331+
col: int,
2332+
use_unicode_characters: bool,
2333+
qubit_map: Dict['cirq.Qid', int],
2334+
out_diagram: TextDiagramDrawer,
2335+
precision: Optional[int],
2336+
get_circuit_diagram_info: Callable[
2337+
['cirq.Operation', 'cirq.CircuitDiagramInfoArgs'], 'cirq.CircuitDiagramInfo'
2338+
],
2339+
include_tags: bool,
2340+
first_annotation_row: int,
2341+
):
2342+
2343+
for k, annotation in enumerate(_get_moment_annotations(moment)):
2344+
args = protocols.CircuitDiagramInfoArgs(
2345+
known_qubits=(),
2346+
known_qubit_count=0,
2347+
use_unicode_characters=use_unicode_characters,
2348+
qubit_map=qubit_map,
2349+
precision=precision,
2350+
include_tags=include_tags,
2351+
)
2352+
info = get_circuit_diagram_info(annotation, args)
2353+
symbols = info._wire_symbols_including_formatted_exponent(args)
2354+
text = symbols[0] if symbols else str(annotation)
2355+
out_diagram.force_vertical_padding_after(first_annotation_row + k - 1, 0)
2356+
out_diagram.write(col, first_annotation_row + k, text)
2357+
2358+
23092359
def _draw_moment_in_diagram(
2360+
*,
23102361
moment: 'cirq.Moment',
23112362
use_unicode_characters: bool,
23122363
qubit_map: Dict['cirq.Qid', int],
@@ -2315,8 +2366,9 @@ def _draw_moment_in_diagram(
23152366
moment_groups: List[Tuple[int, int]],
23162367
get_circuit_diagram_info: Optional[
23172368
Callable[['cirq.Operation', 'cirq.CircuitDiagramInfoArgs'], 'cirq.CircuitDiagramInfo']
2318-
] = None,
2319-
include_tags: bool = True,
2369+
],
2370+
include_tags: bool,
2371+
first_annotation_row: int,
23202372
):
23212373
if get_circuit_diagram_info is None:
23222374
get_circuit_diagram_info = protocols.CircuitDiagramInfo._op_info_with_fallback
@@ -2363,6 +2415,18 @@ def _draw_moment_in_diagram(
23632415
if x > max_x:
23642416
max_x = x
23652417

2418+
_draw_moment_annotations(
2419+
moment=moment,
2420+
use_unicode_characters=use_unicode_characters,
2421+
col=x0,
2422+
qubit_map=qubit_map,
2423+
out_diagram=out_diagram,
2424+
precision=precision,
2425+
get_circuit_diagram_info=get_circuit_diagram_info,
2426+
include_tags=include_tags,
2427+
first_annotation_row=first_annotation_row,
2428+
)
2429+
23662430
global_phase, tags = _get_global_phase_and_tags_for_ops(moment)
23672431

23682432
# Print out global phase, unless it's 1 (phase of 0pi) or it's the only op.

cirq-core/cirq/circuits/circuit_test.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4726,3 +4726,126 @@ def test_factorize_large_circuit():
47264726
assert len(factors) == 5
47274727
for f, d in zip(factors, desired):
47284728
cirq.testing.assert_has_diagram(f, d)
4729+
4730+
4731+
def test_zero_target_operations_go_below_diagram():
4732+
class CustomOperationAnnotation(cirq.Operation):
4733+
def __init__(self, text: str):
4734+
self.text = text
4735+
4736+
def with_qubits(self, *new_qubits):
4737+
raise NotImplementedError()
4738+
4739+
@property
4740+
def qubits(self):
4741+
return ()
4742+
4743+
def _circuit_diagram_info_(self, args) -> str:
4744+
return self.text
4745+
4746+
class CustomOperationAnnotationNoInfo(cirq.Operation):
4747+
def with_qubits(self, *new_qubits):
4748+
raise NotImplementedError()
4749+
4750+
@property
4751+
def qubits(self):
4752+
return ()
4753+
4754+
def __str__(self):
4755+
return "custom!"
4756+
4757+
class CustomGateAnnotation(cirq.Gate):
4758+
def __init__(self, text: str):
4759+
self.text = text
4760+
4761+
def _num_qubits_(self):
4762+
return 0
4763+
4764+
def _circuit_diagram_info_(self, args) -> str:
4765+
return self.text
4766+
4767+
cirq.testing.assert_has_diagram(
4768+
cirq.Circuit(
4769+
cirq.Moment(
4770+
CustomOperationAnnotation("a"),
4771+
CustomGateAnnotation("b").on(),
4772+
CustomOperationAnnotation("c"),
4773+
),
4774+
cirq.Moment(
4775+
CustomOperationAnnotation("e"),
4776+
CustomOperationAnnotation("d"),
4777+
),
4778+
),
4779+
"""
4780+
a e
4781+
b d
4782+
c
4783+
""",
4784+
)
4785+
4786+
cirq.testing.assert_has_diagram(
4787+
cirq.Circuit(
4788+
cirq.Moment(
4789+
cirq.H(cirq.LineQubit(0)),
4790+
CustomOperationAnnotation("a"),
4791+
cirq.GlobalPhaseOperation(1j),
4792+
),
4793+
),
4794+
"""
4795+
0: ─────────────H──────
4796+
4797+
global phase: 0.5π
4798+
a
4799+
""",
4800+
)
4801+
4802+
cirq.testing.assert_has_diagram(
4803+
cirq.Circuit(
4804+
cirq.Moment(
4805+
cirq.H(cirq.LineQubit(0)),
4806+
cirq.CircuitOperation(cirq.FrozenCircuit(CustomOperationAnnotation("a"))),
4807+
),
4808+
),
4809+
"""
4810+
0: ───H───
4811+
a
4812+
""",
4813+
)
4814+
4815+
cirq.testing.assert_has_diagram(
4816+
cirq.Circuit(
4817+
cirq.Moment(
4818+
cirq.X(cirq.LineQubit(0)),
4819+
CustomOperationAnnotation("a"),
4820+
CustomGateAnnotation("b").on(),
4821+
CustomOperationAnnotation("c"),
4822+
),
4823+
cirq.Moment(
4824+
CustomOperationAnnotation("eee"),
4825+
CustomOperationAnnotation("d"),
4826+
),
4827+
cirq.Moment(
4828+
cirq.CNOT(cirq.LineQubit(0), cirq.LineQubit(2)),
4829+
cirq.CNOT(cirq.LineQubit(1), cirq.LineQubit(3)),
4830+
CustomOperationAnnotationNoInfo(),
4831+
CustomOperationAnnotation("zzz"),
4832+
),
4833+
cirq.Moment(
4834+
cirq.H(cirq.LineQubit(2)),
4835+
),
4836+
),
4837+
"""
4838+
┌────────┐
4839+
0: ───X──────────@───────────────
4840+
4841+
1: ──────────────┼──────@────────
4842+
│ │
4843+
2: ──────────────X──────┼────H───
4844+
4845+
3: ─────────────────────X────────
4846+
a eee custom!
4847+
b d zzz
4848+
c
4849+
└────────┘
4850+
""",
4851+
)

cirq-core/cirq/protocols/circuit_diagram_info_protocol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ def _op_info_with_fallback(
157157
) -> 'cirq.CircuitDiagramInfo':
158158
info = protocols.circuit_diagram_info(op, args, None)
159159
if info is not None:
160-
if len(op.qubits) != len(info.wire_symbols):
160+
if max(1, len(op.qubits)) != len(info.wire_symbols):
161161
raise ValueError(
162-
'Wanted diagram info from {!r} for {} '
163-
'qubits but got {!r}'.format(op, len(op.qubits), info)
162+
f'Wanted diagram info from {op!r} for {len(op.qubits)} '
163+
f'qubits but got {info!r}'
164164
)
165165
return info
166166

cirq-core/cirq/testing/circuit_compare.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ def assert_has_diagram(
232232
beginning and whitespace at the end are ignored.
233233
**kwargs: Keyword arguments to be passed to actual.to_text_diagram().
234234
"""
235+
# pylint: disable=unused-variable
236+
__tracebackhide__ = True
237+
# pylint: enable=unused-variable
235238
actual_diagram = actual.to_text_diagram(**kwargs).lstrip("\n").rstrip()
236239
desired_diagram = desired.lstrip("\n").rstrip()
237240
assert actual_diagram == desired_diagram, (
@@ -402,6 +405,9 @@ def assert_has_consistent_qid_shape(val: Any) -> None:
402405
val: The value under test. Should have `_qid_shape_` and/or
403406
`num_qubits_` methods. Can optionally have a `qubits` property.
404407
"""
408+
# pylint: disable=unused-variable
409+
__tracebackhide__ = True
410+
# pylint: enable=unused-variable
405411
default = (-1,)
406412
qid_shape = protocols.qid_shape(val, default)
407413
num_qubits = protocols.num_qubits(val, default)

0 commit comments

Comments
 (0)