Repository: quantumlib/Cirq

Issue:
Gateset containership, subcircuits with negative reps
**Description of the issue**

Subcircuits allow negative repetitions, which cause each gate to be inverted. This inversion is not performed when gatesets iterate the subcircuit.

**How to reproduce the issue**

```python
def test_gateset_contains():
    gate = cirq.XPowGate(exponent=0.5)
    gateset = cirq.Gateset(gate)
    op = cirq.CircuitOperation(cirq.FrozenCircuit(gate.on(cirq.LineQubit(0))), repetitions=-1)

    # This should not pass, because the gates are XPow(-0.5), but it does
    assert op in gateset

    # This (correctly) fails.
    for inner_op in op.mapped_circuit().all_operations():
        assert inner_op in gateset
```
**Cirq version**
0.14.0.dev


Discussion:
Kind of annoyingly, one could even populate the gateset with the specific measurement gates (with measurement key repetition id prefixes) and it will fail unless the subcircuit is fully unrolled (including repetitions). Or maybe unrolling it completely is just the right thing to do.
Personally, I think negative reps is a bit of a misfeature and could be removed. It seems to me that "inverting a circuit" is a big operation that should not be hidden behind a sign in the number of repetitions.
Discussion on original PR regarding this: https://github.com/quantumlib/Cirq/pull/4491#discussion_r706509404

Worth revisiting it again now. cc @95-martin-orion 
From discussion on the Cirq sync: the issue exists in the interaction of Gateset and CircuitOperation. As @daxfohl suggests, the correct behavior would be to fully unroll the CircuitOperation to correctly identify the operations it contains.

> Personally, I think negative reps is a bit of a misfeature and could be removed. It seems to me that "inverting a circuit" is a big operation that should not be hidden behind a sign in the number of repetitions.

Supporting negative repetitions is convenient for recursively-defined circuits, where a given `CircuitOperation` may need to be inverted multiple times before finally being executed. That said, I don't have an example of such a use case offhand (and such cases are likely at the edge of NISQ anyways), so I'm not deeply opposed to removing the behavior in favor of readability. That said, I think that change is separate from the issue here.

Resolving patch:
diff --git a/cirq-core/cirq/ops/gateset.py b/cirq-core/cirq/ops/gateset.py
--- a/cirq-core/cirq/ops/gateset.py
+++ b/cirq-core/cirq/ops/gateset.py
@@ -392,13 +392,7 @@ def _validate_operation(self, op: raw_types.Operation) -> bool:
         if isinstance(op, raw_types.TaggedOperation):
             return self._validate_operation(op.sub_operation)
         elif isinstance(op, circuit_operation.CircuitOperation) and self._unroll_circuit_op:
-            op_circuit = protocols.resolve_parameters(
-                op.circuit.unfreeze(), op.param_resolver, recursive=False
-            )
-            op_circuit = op_circuit.transform_qubits(
-                lambda q: cast(circuit_operation.CircuitOperation, op).qubit_map.get(q, q)
-            )
-            return self.validate(op_circuit)
+            return self.validate(op.mapped_circuit(deep=True))
         else:
             return False
 


Test patch:
diff --git a/cirq-core/cirq/ops/gateset_test.py b/cirq-core/cirq/ops/gateset_test.py
--- a/cirq-core/cirq/ops/gateset_test.py
+++ b/cirq-core/cirq/ops/gateset_test.py
@@ -282,6 +282,13 @@ def assert_validate_and_contains_consistent(gateset, op_tree, result):
         )
 
 
+def test_gateset_validate_circuit_op_negative_reps():
+    gate = CustomXPowGate(exponent=0.5)
+    op = cirq.CircuitOperation(cirq.FrozenCircuit(gate.on(cirq.LineQubit(0))), repetitions=-1)
+    assert op not in cirq.Gateset(gate)
+    assert op ** -1 in cirq.Gateset(gate)
+
+
 def test_with_params():
     assert gateset.with_params() is gateset
     assert (