Repository: quantumlib/Cirq

Issue:
optimized_for_sycamore fails when you measure more than 16 qubits
```python
def thing():
    q = cirq.LineQubit.range(17)
    c = cirq.Circuit(cirq.H.on_each(*q), cirq.measure(*q, key='z'))
    c = cg.optimized_for_sycamore(c)
    return c
thing()    
```

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-44-accaace1183a> in <module>
      4     c = cg.optimized_for_sycamore(c)
      5     return c
----> 6 thing()

<ipython-input-44-accaace1183a> in thing()
      2     q = cirq.LineQubit.range(17)
      3     c = cirq.Circuit(cirq.H.on_each(*q), cirq.measure(*q, key='z'))
----> 4     c = cg.optimized_for_sycamore(c)
      5     return c
      6 thing()

~/ecc/cirq/cirq/google/optimizers/optimize_for_sycamore.py in optimized_for_sycamore(circuit, new_device, qubit_map, optimizer_type, tolerance, tabulation_resolution)
    159     opts = _OPTIMIZER_TYPES[optimizer_type](tolerance=tolerance, tabulation=tabulation)
    160     for optimizer in opts:
--> 161         optimizer(copy)
    162 
    163     return circuits.Circuit(

~/ecc/cirq/cirq/optimizers/drop_negligible.py in optimize_circuit(self, circuit)
     37         for moment_index, moment in enumerate(circuit):
     38             for op in moment.operations:
---> 39                 if op is not None and protocols.trace_distance_bound(op) <= self.tolerance:
     40                     deletions.append((moment_index, op))
     41         circuit.batch_remove(deletions)

~/ecc/cirq/cirq/protocols/trace_distance_bound.py in trace_distance_bound(val)
     71 
     72     for strat in strats:
---> 73         result = strat(val)
     74         if result is None:
     75             break

~/ecc/cirq/cirq/protocols/trace_distance_bound.py in _strat_distance_from_unitary(val)
     96 def _strat_distance_from_unitary(val: Any) -> Optional[float]:
     97     """Attempts to compute a value's trace_distance_bound from its unitary."""
---> 98     u = unitary_protocol.unitary(val, default=None)
     99 
    100     if u is None:

~/ecc/cirq/cirq/protocols/unitary_protocol.py in unitary(val, default)
    127     ]
    128     for strat in strats:
--> 129         result = strat(val)
    130         if result is None:
    131             break

~/ecc/cirq/cirq/protocols/unitary_protocol.py in _strat_unitary_from_apply_unitary(val)
    173 
    174     # Apply unitary effect to an identity matrix.
--> 175     state = qis.eye_tensor(val_qid_shape, dtype=np.complex128)
    176     buffer = np.empty_like(state)
    177     result = method(ApplyUnitaryArgs(state, buffer, range(len(val_qid_shape))))

~/ecc/cirq/cirq/qis/states.py in eye_tensor(half_shape, dtype)
    669     """
    670     identity = np.eye(np.prod(half_shape, dtype=int), dtype=dtype)
--> 671     identity.shape = half_shape * 2
    672     return identity

ValueError: maximum supported dimension for an ndarray is 32, found 34
```


Discussion:
Ran this just now and got

```
MemoryError: Unable to allocate 256. GiB for an array with shape (131072, 131072) and data type complex128
```

Might be the same problem, just different numpy version etc. Mine is 

```
In [9]: np.version.full_version
Out[9]: '1.19.4'
```
It fails during "drop empty moments" by trying to figure out if the measurement gate is an identity operation.
It tries to figure out the unitary(?) matrix of the measurement gate which involves tensoring up a 2^32-sized identity matrix.
Should `DropNegligible` call `cirq.is_measurement` and never drop measurement gates? Or should measure gate implement `_trace_distance_bound_`?
(Edit: I wrote this before seeing the other replies.  I agree with @maffoo.)

Looking at the stack trace, I see two issues that combine to cause your bug:
- The drop negligible optimizer tries to get the trace_distance_bound on a measurement.  It should either not do that or MeasurementGate should implement the protocol return a default value (maybe 1 in this case).  Does trace_distance_bound make sense for non-unitary gates?  If not, it should check and return a default value.
- When trace_distance_bound falls back to using the gate's unitary, it should give up for gates larger than 3-5 qubits (or have a flag to give up).  Here it tries to generate a 2^17 x 2^17 unitary matrix.
> When trace_distance_bound falls back to using the gate's unitary, it should give up for gates larger than 3-5 qubits (or have a flag to give up). Here it tries to generate a 2^17 x 2^17 unitary matrix.

Or we need a way for gates to declare that they do not have a unitary representation. We should be able to declare this for a measurement gate regardless of number of qubits.
Does defining `_is_unitary_(self): False` work?
This is still embarrassingly an issue. 

Resolving patch:
diff --git a/cirq/optimizers/drop_negligible.py b/cirq/optimizers/drop_negligible.py
--- a/cirq/optimizers/drop_negligible.py
+++ b/cirq/optimizers/drop_negligible.py
@@ -36,6 +36,8 @@ def optimize_circuit(self, circuit: _circuit.Circuit) -> None:
         deletions: List[Tuple[int, ops.Operation]] = []
         for moment_index, moment in enumerate(circuit):
             for op in moment.operations:
-                if op is not None and protocols.trace_distance_bound(op) <= self.tolerance:
+                if protocols.is_measurement(op):
+                    continue
+                if protocols.trace_distance_bound(op) <= self.tolerance:
                     deletions.append((moment_index, op))
         circuit.batch_remove(deletions)


Test patch:
diff --git a/cirq/google/optimizers/optimize_for_sycamore_test.py b/cirq/google/optimizers/optimize_for_sycamore_test.py
--- a/cirq/google/optimizers/optimize_for_sycamore_test.py
+++ b/cirq/google/optimizers/optimize_for_sycamore_test.py
@@ -19,15 +19,15 @@
 import cirq.google as cg
 
 
-@pytest.mark.parametrize(
-    'optimizer_type, gateset',
-    [
-        ('sqrt_iswap', cg.SQRT_ISWAP_GATESET),
-        ('sycamore', cg.SYC_GATESET),
-        ('xmon', cg.XMON),
-        ('xmon_partial_cz', cg.XMON),
-    ],
-)
+_OPTIMIZERS_AND_GATESETS = [
+    ('sqrt_iswap', cg.SQRT_ISWAP_GATESET),
+    ('sycamore', cg.SYC_GATESET),
+    ('xmon', cg.XMON),
+    ('xmon_partial_cz', cg.XMON),
+]
+
+
+@pytest.mark.parametrize('optimizer_type, gateset', _OPTIMIZERS_AND_GATESETS)
 def test_optimizer_output_gates_are_supported(optimizer_type, gateset):
     q0, q1 = cirq.LineQubit.range(2)
     circuit = cirq.Circuit(
@@ -39,6 +39,21 @@ def test_optimizer_output_gates_are_supported(optimizer_type, gateset):
             assert gateset.is_supported_operation(op)
 
 
+@pytest.mark.parametrize('optimizer_type, gateset', _OPTIMIZERS_AND_GATESETS)
+def test_optimize_large_measurement_gates(optimizer_type, gateset):
+    qubits = cirq.LineQubit.range(53)
+    circuit = cirq.Circuit(
+        cirq.X.on_each(qubits),
+        [cirq.CZ(qubits[i], qubits[i + 1]) for i in range(0, len(qubits) - 1, 2)],
+        [cirq.CZ(qubits[i], qubits[i + 1]) for i in range(1, len(qubits) - 1, 2)],
+        cirq.measure(*qubits, key='m'),
+    )
+    new_circuit = cg.optimized_for_sycamore(circuit, optimizer_type=optimizer_type)
+    for moment in new_circuit:
+        for op in moment:
+            assert gateset.is_supported_operation(op)
+
+
 def test_invalid_input():
     with pytest.raises(ValueError):
         q0, q1 = cirq.LineQubit.range(2)