Repository: quantumlib/Cirq

Issue:
Don't construct huge repetition id lists for CircuitOperation when use_repetition_ids=False
Do not run this repro code unless you like thrashing your machine.

```
import cirq
op = cirq.CircuitOperation(
    cirq.FrozenCircuit(),
    repetitions=1_000_000_000_000,
    use_repetition_ids=False)
```

# Expected behavior

Because `use_repetition_ids` is set to `False`, no list of repetition ids is ever created and the code finishes in under a second. This remains true when using the operation for other actions that don't affect the repetition ids, like transforming its qubits or putting it into a diagram.

# Actual behavior

Internally, the code attempts to create a list of one trillion repetition ids. This list will never be used and also does not fit in memory. The code takes forever and eventually crashes.

---

For context, I know that Mike Newman has used stim to run circuits with hundreds of millions of rounds. If he ever attempted to use stimcirq to convert that circuit into a cirq circuit... `*KABOOM*`.


Discussion:

Resolving patch:
diff --git a/cirq-core/cirq/circuits/circuit_operation.py b/cirq-core/cirq/circuits/circuit_operation.py
--- a/cirq-core/cirq/circuits/circuit_operation.py
+++ b/cirq-core/cirq/circuits/circuit_operation.py
@@ -55,16 +55,16 @@ def default_repetition_ids(repetitions: IntParam) -> Optional[List[str]]:
     return None
 
 
-def _full_join_string_lists(list1: Optional[List[str]], list2: Optional[List[str]]):
+def _full_join_string_lists(
+    list1: Optional[List[str]], list2: Optional[List[str]]
+) -> Optional[List[str]]:
     if list1 is None and list2 is None:
         return None  # coverage: ignore
     if list1 is None:
         return list2  # coverage: ignore
     if list2 is None:
         return list1
-    return [
-        f'{REPETITION_ID_SEPARATOR.join([first, second])}' for first in list1 for second in list2
-    ]
+    return [f'{first}{REPETITION_ID_SEPARATOR}{second}' for first in list1 for second in list2]
 
 
 @dataclasses.dataclass(frozen=True)
@@ -224,7 +224,7 @@ def qubits(self) -> Tuple['cirq.Qid', ...]:
         return tuple(self.qubit_map.get(q, q) for q in ordered_qubits)
 
     def _default_repetition_ids(self) -> Optional[List[str]]:
-        return default_repetition_ids(self.repetitions)
+        return default_repetition_ids(self.repetitions) if self.use_repetition_ids else None
 
     def _qid_shape_(self) -> Tuple[int, ...]:
         return tuple(q.dimension for q in self.qubits)
@@ -524,7 +524,8 @@ def repeat(
             expected_repetition_id_length = abs(repetitions)
 
             if repetition_ids is None:
-                repetition_ids = default_repetition_ids(expected_repetition_id_length)
+                if self.use_repetition_ids:
+                    repetition_ids = default_repetition_ids(expected_repetition_id_length)
             elif len(repetition_ids) != expected_repetition_id_length:
                 raise ValueError(
                     f'Expected repetition_ids={repetition_ids} length to be '


Test patch:
diff --git a/cirq-core/cirq/circuits/circuit_operation_test.py b/cirq-core/cirq/circuits/circuit_operation_test.py
--- a/cirq-core/cirq/circuits/circuit_operation_test.py
+++ b/cirq-core/cirq/circuits/circuit_operation_test.py
@@ -11,6 +11,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import unittest.mock as mock
 from typing import Optional
 
 import numpy as np
@@ -18,6 +19,7 @@
 import sympy
 
 import cirq
+import cirq.circuits.circuit_operation as circuit_operation
 from cirq.circuits.circuit_operation import _full_join_string_lists
 
 ALL_SIMULATORS = (cirq.Simulator(), cirq.DensityMatrixSimulator(), cirq.CliffordSimulator())
@@ -346,6 +348,27 @@ def test_repeat_zero_times(add_measurements, use_repetition_ids, initial_reps):
     assert np.allclose(result.state_vector(), [1, 0])
 
 
+def test_no_repetition_ids():
+    def default_repetition_ids(self):
+        assert False, "Should not call default_repetition_ids"
+
+    with mock.patch.object(circuit_operation, 'default_repetition_ids', new=default_repetition_ids):
+        q = cirq.LineQubit(0)
+        op = cirq.CircuitOperation(
+            cirq.Circuit(cirq.X(q), cirq.measure(q)).freeze(),
+            repetitions=1_000_000,
+            use_repetition_ids=False,
+        )
+        assert op.repetitions == 1_000_000
+        assert op.repetition_ids is None
+        _ = repr(op)
+        _ = str(op)
+
+        op2 = op.repeat(10)
+        assert op2.repetitions == 10_000_000
+        assert op2.repetition_ids is None
+
+
 def test_parameterized_repeat():
     q = cirq.LineQubit(0)
     op = cirq.CircuitOperation(cirq.FrozenCircuit(cirq.X(q))) ** sympy.Symbol('a')