Skip to content

Commit b3af2ee

Browse files
daxfohlpavoljuhas
andauthored
Fix nondeterminacy in Circuit.insert (simplified) (#7043)
* Fix nondeterminacy and a couple other issues in Circuit.insert * code comments * code comments * code comments * test1 and test7 reverted * remove no longer relevant new tests * Update cirq-core/cirq/circuits/circuit.py Co-authored-by: Pavol Juhas <[email protected]> * Update circuit.py * Put back the last line of docstring --------- Co-authored-by: Pavol Juhas <[email protected]> Co-authored-by: Pavol Juhas <[email protected]>
1 parent 8073d4c commit b3af2ee

File tree

2 files changed

+133
-64
lines changed

2 files changed

+133
-64
lines changed

cirq-core/cirq/circuits/circuit.py

Lines changed: 87 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969

7070

7171
_TGate = TypeVar('_TGate', bound='cirq.Gate')
72+
_MOMENT_OR_OP = Union['cirq.Moment', 'cirq.Operation']
7273

7374
CIRCUIT_TYPE = TypeVar('CIRCUIT_TYPE', bound='AbstractCircuit')
7475
document(
@@ -2095,49 +2096,6 @@ def earliest_available_moment(
20952096
last_available = k
20962097
return last_available
20972098

2098-
def _pick_or_create_inserted_op_moment_index(
2099-
self, splitter_index: int, op: 'cirq.Operation', strategy: 'cirq.InsertStrategy'
2100-
) -> int:
2101-
"""Determines and prepares where an insertion will occur.
2102-
2103-
Args:
2104-
splitter_index: The index to insert at.
2105-
op: The operation that will be inserted.
2106-
strategy: The insertion strategy.
2107-
2108-
Returns:
2109-
The index of the (possibly new) moment where the insertion should
2110-
occur.
2111-
2112-
Raises:
2113-
ValueError: Unrecognized append strategy.
2114-
"""
2115-
2116-
if strategy is InsertStrategy.NEW or strategy is InsertStrategy.NEW_THEN_INLINE:
2117-
self._moments.insert(splitter_index, Moment())
2118-
self._mutated()
2119-
return splitter_index
2120-
2121-
if strategy is InsertStrategy.INLINE:
2122-
if 0 <= splitter_index - 1 < len(self._moments) and self._can_add_op_at(
2123-
splitter_index - 1, op
2124-
):
2125-
return splitter_index - 1
2126-
2127-
return self._pick_or_create_inserted_op_moment_index(
2128-
splitter_index, op, InsertStrategy.NEW
2129-
)
2130-
2131-
if strategy is InsertStrategy.EARLIEST:
2132-
if self._can_add_op_at(splitter_index, op):
2133-
return self.earliest_available_moment(op, end_moment_index=splitter_index)
2134-
2135-
return self._pick_or_create_inserted_op_moment_index(
2136-
splitter_index, op, InsertStrategy.INLINE
2137-
)
2138-
2139-
raise ValueError(f'Unrecognized append strategy: {strategy}')
2140-
21412099
def _can_add_op_at(self, moment_index: int, operation: 'cirq.Operation') -> bool:
21422100
if not 0 <= moment_index < len(self._moments):
21432101
return True
@@ -2147,7 +2105,7 @@ def _can_add_op_at(self, moment_index: int, operation: 'cirq.Operation') -> bool
21472105
def insert(
21482106
self,
21492107
index: int,
2150-
moment_or_operation_tree: Union['cirq.Operation', 'cirq.OP_TREE'],
2108+
moment_or_operation_tree: 'cirq.OP_TREE',
21512109
strategy: 'cirq.InsertStrategy' = InsertStrategy.EARLIEST,
21522110
) -> int:
21532111
"""Inserts operations into the circuit.
@@ -2170,24 +2128,57 @@ def insert(
21702128
"""
21712129
# limit index to 0..len(self._moments), also deal with indices smaller 0
21722130
k = max(min(index if index >= 0 else len(self._moments) + index, len(self._moments)), 0)
2173-
if strategy != InsertStrategy.EARLIEST or index != len(self._moments):
2131+
if strategy != InsertStrategy.EARLIEST or k != len(self._moments):
21742132
self._placement_cache = None
2175-
for moment_or_op in list(ops.flatten_to_ops_or_moments(moment_or_operation_tree)):
2176-
if self._placement_cache:
2177-
p = self._placement_cache.append(moment_or_op)
2178-
elif isinstance(moment_or_op, Moment):
2179-
p = k
2180-
else:
2181-
p = self._pick_or_create_inserted_op_moment_index(k, moment_or_op, strategy)
2182-
if isinstance(moment_or_op, Moment):
2183-
self._moments.insert(p, moment_or_op)
2184-
elif p == len(self._moments):
2185-
self._moments.append(Moment(moment_or_op))
2186-
else:
2187-
self._moments[p] = self._moments[p].with_operation(moment_or_op)
2188-
k = max(k, p + 1)
2189-
if strategy is InsertStrategy.NEW_THEN_INLINE:
2190-
strategy = InsertStrategy.INLINE
2133+
mops = list(ops.flatten_to_ops_or_moments(moment_or_operation_tree))
2134+
if self._placement_cache:
2135+
batches = [mops] # Any grouping would work here; this just happens to be the fastest.
2136+
elif strategy is InsertStrategy.NEW:
2137+
batches = [[mop] for mop in mops] # Each op goes into its own moment.
2138+
else:
2139+
batches = list(_group_into_moment_compatible(mops))
2140+
for batch in batches:
2141+
# Insert a moment if inline/earliest and _any_ op in the batch requires it.
2142+
if (
2143+
not self._placement_cache
2144+
and not isinstance(batch[0], Moment)
2145+
and strategy in (InsertStrategy.INLINE, InsertStrategy.EARLIEST)
2146+
and not all(
2147+
(strategy is InsertStrategy.EARLIEST and self._can_add_op_at(k, op))
2148+
or (k > 0 and self._can_add_op_at(k - 1, op))
2149+
for op in cast(List['cirq.Operation'], batch)
2150+
)
2151+
):
2152+
self._moments.insert(k, Moment())
2153+
if strategy is InsertStrategy.INLINE:
2154+
k += 1
2155+
max_p = 0
2156+
for moment_or_op in batch:
2157+
# Determine Placement
2158+
if self._placement_cache:
2159+
p = self._placement_cache.append(moment_or_op)
2160+
elif isinstance(moment_or_op, Moment):
2161+
p = k
2162+
elif strategy in (InsertStrategy.NEW, InsertStrategy.NEW_THEN_INLINE):
2163+
self._moments.insert(k, Moment())
2164+
p = k
2165+
elif strategy is InsertStrategy.INLINE:
2166+
p = k - 1
2167+
else: # InsertStrategy.EARLIEST:
2168+
p = self.earliest_available_moment(moment_or_op, end_moment_index=k)
2169+
# Place
2170+
if isinstance(moment_or_op, Moment):
2171+
self._moments.insert(p, moment_or_op)
2172+
elif p == len(self._moments):
2173+
self._moments.append(Moment(moment_or_op))
2174+
else:
2175+
self._moments[p] = self._moments[p].with_operation(moment_or_op)
2176+
# Iterate
2177+
max_p = max(p, max_p)
2178+
if strategy is InsertStrategy.NEW_THEN_INLINE:
2179+
strategy = InsertStrategy.INLINE
2180+
k += 1
2181+
k = max(k, max_p + 1)
21912182
self._mutated(preserve_placement_cache=True)
21922183
return k
21932184

@@ -2450,7 +2441,7 @@ def batch_insert(self, insertions: Iterable[Tuple[int, 'cirq.OP_TREE']]) -> None
24502441

24512442
def append(
24522443
self,
2453-
moment_or_operation_tree: Union['cirq.Moment', 'cirq.OP_TREE'],
2444+
moment_or_operation_tree: 'cirq.OP_TREE',
24542445
strategy: 'cirq.InsertStrategy' = InsertStrategy.EARLIEST,
24552446
) -> None:
24562447
"""Appends operations onto the end of the circuit.
@@ -2841,8 +2832,40 @@ def _group_until_different(items: Iterable[_TIn], key: Callable[[_TIn], _TKey],
28412832
return ((k, [val(i) for i in v]) for (k, v) in itertools.groupby(items, key))
28422833

28432834

2835+
def _group_into_moment_compatible(inputs: Sequence[_MOMENT_OR_OP]) -> Iterator[List[_MOMENT_OR_OP]]:
2836+
"""Groups sequential ops into those that can coexist in a single moment.
2837+
2838+
This function will go through the input sequence in order, emitting lists of sequential
2839+
operations that can go into a single moment. It does not try to rearrange the elements or try
2840+
to move them to open slots in earlier moments; it simply processes them in order and outputs
2841+
them. i.e. the output, if flattened, will equal the input.
2842+
2843+
Actual Moments in the input will always be emitted by themselves as a single-element list.
2844+
2845+
Examples:
2846+
[X(a), X(b), X(a)] -> [[X(a), X(b)], [X(a)]]
2847+
[X(a), X(a), X(b)] -> [[X(a)], [X(a), X(b)]]
2848+
[X(a), Moment(X(b)), X(c)] -> [[X(a)], [Moment(X(b))], [X(c)]]
2849+
"""
2850+
batch: List[_MOMENT_OR_OP] = []
2851+
batch_qubits: Set['cirq.Qid'] = set()
2852+
for mop in inputs:
2853+
is_moment = isinstance(mop, cirq.Moment)
2854+
if (is_moment and batch) or not batch_qubits.isdisjoint(mop.qubits):
2855+
yield batch
2856+
batch = []
2857+
batch_qubits.clear()
2858+
if is_moment:
2859+
yield [mop]
2860+
continue
2861+
batch.append(mop)
2862+
batch_qubits.update(mop.qubits)
2863+
if batch:
2864+
yield batch
2865+
2866+
28442867
def get_earliest_accommodating_moment_index(
2845-
moment_or_operation: Union['cirq.Moment', 'cirq.Operation'],
2868+
moment_or_operation: _MOMENT_OR_OP,
28462869
qubit_indices: Dict['cirq.Qid', int],
28472870
mkey_indices: Dict['cirq.MeasurementKey', int],
28482871
ckey_indices: Dict['cirq.MeasurementKey', int],
@@ -2938,7 +2961,7 @@ def __init__(self) -> None:
29382961
# For keeping track of length of the circuit thus far.
29392962
self._length = 0
29402963

2941-
def append(self, moment_or_operation: Union['cirq.Moment', 'cirq.Operation']) -> int:
2964+
def append(self, moment_or_operation: _MOMENT_OR_OP) -> int:
29422965
"""Find placement for moment/operation and update cache.
29432966
29442967
Determines the placement index of the provided operation, assuming

cirq-core/cirq/circuits/circuit_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3555,6 +3555,52 @@ def test_insert_operations_random_circuits(circuit):
35553555
assert circuit == other_circuit
35563556

35573557

3558+
def test_insert_zero_index():
3559+
# Should always go to moment[0], independent of qubit order or earliest/inline strategy.
3560+
q0, q1 = cirq.LineQubit.range(2)
3561+
c0 = cirq.Circuit(cirq.X(q0))
3562+
c0.insert(0, cirq.Y.on_each(q0, q1), strategy=cirq.InsertStrategy.EARLIEST)
3563+
c1 = cirq.Circuit(cirq.X(q0))
3564+
c1.insert(0, cirq.Y.on_each(q1, q0), strategy=cirq.InsertStrategy.EARLIEST)
3565+
c2 = cirq.Circuit(cirq.X(q0))
3566+
c2.insert(0, cirq.Y.on_each(q0, q1), strategy=cirq.InsertStrategy.INLINE)
3567+
c3 = cirq.Circuit(cirq.X(q0))
3568+
c3.insert(0, cirq.Y.on_each(q1, q0), strategy=cirq.InsertStrategy.INLINE)
3569+
expected = cirq.Circuit(cirq.Moment(cirq.Y(q0), cirq.Y(q1)), cirq.Moment(cirq.X(q0)))
3570+
assert c0 == expected
3571+
assert c1 == expected
3572+
assert c2 == expected
3573+
assert c3 == expected
3574+
3575+
3576+
def test_insert_earliest_on_previous_moment():
3577+
q = cirq.LineQubit(0)
3578+
c = cirq.Circuit(cirq.Moment(cirq.X(q)), cirq.Moment(), cirq.Moment(), cirq.Moment(cirq.Z(q)))
3579+
c.insert(3, cirq.Y(q), strategy=cirq.InsertStrategy.EARLIEST)
3580+
# Should fall back to moment[1] since EARLIEST
3581+
assert c == cirq.Circuit(
3582+
cirq.Moment(cirq.X(q)), cirq.Moment(cirq.Y(q)), cirq.Moment(), cirq.Moment(cirq.Z(q))
3583+
)
3584+
3585+
3586+
def test_insert_inline_end_of_circuit():
3587+
# If end index is specified, INLINE should place all ops there independent of qubit order.
3588+
q0, q1 = cirq.LineQubit.range(2)
3589+
c0 = cirq.Circuit(cirq.X(q0))
3590+
c0.insert(1, cirq.Y.on_each(q0, q1), strategy=cirq.InsertStrategy.INLINE)
3591+
c1 = cirq.Circuit(cirq.X(q0))
3592+
c1.insert(1, cirq.Y.on_each(q1, q0), strategy=cirq.InsertStrategy.INLINE)
3593+
c2 = cirq.Circuit(cirq.X(q0))
3594+
c2.insert(5, cirq.Y.on_each(q0, q1), strategy=cirq.InsertStrategy.INLINE)
3595+
c3 = cirq.Circuit(cirq.X(q0))
3596+
c3.insert(5, cirq.Y.on_each(q1, q0), strategy=cirq.InsertStrategy.INLINE)
3597+
expected = cirq.Circuit(cirq.Moment(cirq.X(q0)), cirq.Moment(cirq.Y(q0), cirq.Y(q1)))
3598+
assert c0 == expected
3599+
assert c1 == expected
3600+
assert c2 == expected
3601+
assert c3 == expected
3602+
3603+
35583604
def test_insert_operations_errors():
35593605
a, b, c = (cirq.NamedQubit(s) for s in 'abc')
35603606
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)