Repository: quantumlib/Cirq

Issue:
CircuitOperation crashes simulation if parameters come from the main circuit
**Description of the issue**

If there's a parameterized operation in a subcircuit, it crashes when passing the param resolver into `simulate`.

**How to reproduce the issue**

```python
def test_subcircuit_with_params():
    circuit = cirq.Circuit(
        cirq.CircuitOperation(cirq.FrozenCircuit(cirq.X(cirq.LineQubit(0)) ** sympy.Symbol('a')))
    )
    cirq.Simulator().simulate(circuit, param_resolver={'a': 0})
```
raises
```python
ValueError: Recursive resolution of CircuitOperation parameters is prohibited. Use "recursive=False" to prevent this error
```
stack trace
```python
cirq-core/cirq/sim/simulator.py:503: in simulate
    return self.simulate_sweep(
cirq-core/cirq/sim/simulator.py:518: in simulate_sweep
    return list(self.simulate_sweep_iter(program, params, qubit_order, initial_state))
cirq-core/cirq/sim/simulator_base.py:351: in simulate_sweep_iter
    yield from super().simulate_sweep_iter(suffix, params, qubit_order, sim_state)
cirq-core/cirq/sim/simulator.py:615: in simulate_sweep_iter
    all_step_results = self.simulate_moment_steps(
cirq-core/cirq/sim/simulator.py:656: in simulate_moment_steps
    resolved_circuit = protocols.resolve_parameters(circuit, param_resolver)
cirq-core/cirq/protocols/resolve_parameters.py:184: in resolve_parameters
    result = getter(param_resolver, recursive)
cirq-core/cirq/circuits/circuit.py:2422: in _resolve_parameters_
    resolved_operations = _resolve_operations(moment.operations, resolver, recursive)
cirq-core/cirq/circuits/circuit.py:2490: in _resolve_operations
    resolved_operations.append(protocols.resolve_parameters(op, param_resolver, recursive))
cirq-core/cirq/protocols/resolve_parameters.py:184: in resolve_parameters
    result = getter(param_resolver, recursive)
```

**Cirq version**
0.14.0.dev


Handle recursive parameter resolution in CircuitOperation
**Is your feature request related to a use case or problem? Please describe.**

Child of #3235.

CircuitOperation implicitly relies on single-step parameter resolution for its internal parameter mappings. This is inconsistent with the default behavior of `ParamResolver`, which is to resolve parameters recursively (see PR #3538).

**Describe the solution you'd like**

`CircuitOperation` should accept `resolve_parameters(..., recursive=True)` and handle it in a manner consistent with `ParamResolver` (within reason).

**[optional] Describe alternatives/workarounds you've considered**

Disabling recursive resolution for CircuitOperation (current behavior)

Changing default resolution behavior to single-step

**What is the urgency from your perspective for this issue? Is it blocking important work?**

P2 - we should do it in the next couple of quarters


Discussion:
Looks like the root cause is already documented in #3619, however leaving this open for now since that is a feature request while this is a bug.
Discussed at cirq cync. Default to single step and keep the error message would be a minor breaking change but the behavior would be most predictable 
I was thinking this would be a quick fix, but I assume it's not or it would've been done already. Do you have any thoughts on the matter that you can note here?

By `recursive` does that mean it would have to recur through the subcircuit operations, or does it just mean recurring through the local symbols of the CircuitOperation, (or does it mean both)?

It is the root cause for #5016. I don't know whether that was a known effect or not. 
> I was thinking this would be a quick fix, but I assume it's not or it would've been done already. Do you have any thoughts on the matter that you can note here?

I haven't looked at this in a while, but discussion on #3538 demonstrates why this is potentially complicated. In summary:

- CircuitOperation currently expects single-step resolution (e.g. `{a: b, b: a}` maps a to b and b to a)
- This is in direct conflict with recursive resolution behavior (where `{a: b, b: a}` throws an infinite-loop error)
- Changing over in a backwards-compatible way potentially affects many `_resolve_parameters_` calls

A breaking change could probably resolve this; at the time, we opted to keep things stable. You're welcome to take a look, especially given #5016 (which I agree stems from this).

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
@@ -592,10 +592,14 @@ def _with_measurement_key_mapping_(self, key_map: Dict[str, str]) -> 'cirq.Circu
         return self.with_measurement_key_mapping(key_map)
 
     def with_params(
-        self, param_values: 'cirq.ParamResolverOrSimilarType'
+        self, param_values: 'cirq.ParamResolverOrSimilarType', recursive: bool = False
     ) -> 'cirq.CircuitOperation':
         """Returns a copy of this operation with an updated ParamResolver.
 
+        Any existing parameter mappings will have their values updated given
+        the provided mapping, and any new parameters will be added to the
+        ParamResolver.
+
         Note that any resulting parameter mappings with no corresponding
         parameter in the base circuit will be omitted.
 
@@ -603,6 +607,11 @@ def with_params(
             param_values: A map or ParamResolver able to convert old param
                 values to new param values. This map will be composed with any
                 existing ParamResolver via single-step resolution.
+            recursive: If True, resolves parameter values recursively over the
+                resolver; otherwise performs a single resolution step. This
+                behavior applies only to the passed-in mapping, for the current
+                application. Existing parameters are never resolved recursively
+                because a->b and b->a needs to be a valid mapping.
 
         Returns:
             A copy of this operation with its ParamResolver updated as specified
@@ -611,18 +620,12 @@ def with_params(
         new_params = {}
         for k in protocols.parameter_symbols(self.circuit):
             v = self.param_resolver.value_of(k, recursive=False)
-            v = protocols.resolve_parameters(v, param_values, recursive=False)
+            v = protocols.resolve_parameters(v, param_values, recursive=recursive)
             if v != k:
                 new_params[k] = v
         return self.replace(param_resolver=new_params)
 
-    # TODO: handle recursive parameter resolution gracefully
     def _resolve_parameters_(
         self, resolver: 'cirq.ParamResolver', recursive: bool
     ) -> 'cirq.CircuitOperation':
-        if recursive:
-            raise ValueError(
-                'Recursive resolution of CircuitOperation parameters is prohibited. '
-                'Use "recursive=False" to prevent this error.'
-            )
-        return self.with_params(resolver.param_dict)
+        return self.with_params(resolver.param_dict, recursive)


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
@@ -20,7 +20,6 @@
 import cirq
 from cirq.circuits.circuit_operation import _full_join_string_lists
 
-
 ALL_SIMULATORS = (
     cirq.Simulator(),
     cirq.DensityMatrixSimulator(),
@@ -248,9 +247,40 @@ def test_with_params():
         == op_with_params
     )
 
-    # Recursive parameter resolution is rejected.
-    with pytest.raises(ValueError, match='Use "recursive=False"'):
-        _ = cirq.resolve_parameters(op_base, cirq.ParamResolver(param_dict))
+
+def test_recursive_params():
+    q = cirq.LineQubit(0)
+    a, a2, b, b2 = sympy.symbols('a a2 b b2')
+    circuitop = cirq.CircuitOperation(
+        cirq.FrozenCircuit(
+            cirq.X(q) ** a,
+            cirq.Z(q) ** b,
+        ),
+        # Not recursive, a and b are swapped.
+        param_resolver=cirq.ParamResolver({a: b, b: a}),
+    )
+    # Recursive, so a->a2->0 and b->b2->1.
+    outer_params = {a: a2, a2: 0, b: b2, b2: 1}
+    resolved = cirq.resolve_parameters(circuitop, outer_params)
+    # Combined, a->b->b2->1, and b->a->a2->0.
+    assert resolved.param_resolver.param_dict == {a: 1, b: 0}
+
+    # Non-recursive, so a->a2 and b->b2.
+    resolved = cirq.resolve_parameters(circuitop, outer_params, recursive=False)
+    # Combined, a->b->b2, and b->a->a2.
+    assert resolved.param_resolver.param_dict == {a: b2, b: a2}
+
+    with pytest.raises(RecursionError):
+        cirq.resolve_parameters(circuitop, {a: a2, a2: a})
+
+    # Non-recursive, so a->b and b->a.
+    resolved = cirq.resolve_parameters(circuitop, {a: b, b: a}, recursive=False)
+    # Combined, a->b->a, and b->a->b.
+    assert resolved.param_resolver.param_dict == {}
+
+    # First example should behave like an X when simulated
+    result = cirq.Simulator().simulate(cirq.Circuit(circuitop), param_resolver=outer_params)
+    assert np.allclose(result.state_vector(), [0, 1])
 
 
 @pytest.mark.parametrize('add_measurements', [True, False])