Repository: quantumlib/Cirq

Issue:
Avoid calling optimizers in optimizers
In review of #2955, @mrwojtek pointed out that optimizers should avoid calling other optimizers and instead leave any further optimization (such as merging single-qubit gates) up to the caller.

I agree with this since it makes it more explicit in the client code what optimizations actually take place and hence what output is produced. It can also reduce the number of calls to optimizers in some cases (e.g. when multiple optimizers are used).

In fact we already follow this approach in all optimizers except `decompose_two_qubit_interaction_into_four_fsim_gates_via_b`.

This issue is a request for comments on whether we want to remove

```python
    MergeSingleQubitGates().optimize_circuit(circuit)
    DropEmptyMoments().optimize_circuit(circuit)
```

from the end of that optimizer and whether we should adopt the rule of leaving such additional optimization steps up to the user in future optimizers.


Discussion:
My intuition is that optimizers shouldn't call optimizers because it violates single use.  

For this specific case we should talk about this at Cirq Cync since it breaks an existing "api".  
Discussed at Cirq Cynq: 

- this is a good idea 
- warn for a deprecation period then deprecate 



Resolving patch:
diff --git a/cirq/__init__.py b/cirq/__init__.py
--- a/cirq/__init__.py
+++ b/cirq/__init__.py
@@ -294,6 +294,7 @@
     decompose_cphase_into_two_fsim,
     decompose_multi_controlled_x,
     decompose_multi_controlled_rotation,
+    decompose_two_qubit_interaction_into_four_fsim_gates,
     decompose_two_qubit_interaction_into_four_fsim_gates_via_b,
     DropEmptyMoments,
     DropNegligible,
diff --git a/cirq/optimizers/__init__.py b/cirq/optimizers/__init__.py
--- a/cirq/optimizers/__init__.py
+++ b/cirq/optimizers/__init__.py
@@ -67,4 +67,6 @@
     two_qubit_matrix_to_operations,)
 
 from cirq.optimizers.two_qubit_to_fsim import (
-    decompose_two_qubit_interaction_into_four_fsim_gates_via_b,)
+    decompose_two_qubit_interaction_into_four_fsim_gates,
+    decompose_two_qubit_interaction_into_four_fsim_gates_via_b,
+)
diff --git a/cirq/optimizers/two_qubit_to_fsim.py b/cirq/optimizers/two_qubit_to_fsim.py
--- a/cirq/optimizers/two_qubit_to_fsim.py
+++ b/cirq/optimizers/two_qubit_to_fsim.py
@@ -13,16 +13,32 @@
 
 from cirq import ops, linalg, circuits, devices
 from cirq.optimizers import merge_single_qubit_gates, drop_empty_moments
+from cirq._compat import deprecated
 
 if TYPE_CHECKING:
     import cirq
 
 
+@deprecated(deadline='v0.10',
+            fix='Use cirq.decompose_two_qubit_interaction_into_four_fsim_gates.'
+           )
 def decompose_two_qubit_interaction_into_four_fsim_gates_via_b(
         interaction: Union['cirq.Operation', 'cirq.Gate', np.ndarray, Any],
         *,
         fsim_gate: Union['cirq.FSimGate', 'cirq.ISwapPowGate'],
         qubits: Sequence['cirq.Qid'] = None) -> 'cirq.Circuit':
+    circuit = decompose_two_qubit_interaction_into_four_fsim_gates(
+        interaction, fsim_gate=fsim_gate, qubits=qubits)
+    merge_single_qubit_gates.MergeSingleQubitGates().optimize_circuit(circuit)
+    drop_empty_moments.DropEmptyMoments().optimize_circuit(circuit)
+    return circuit
+
+
+def decompose_two_qubit_interaction_into_four_fsim_gates(
+        interaction: Union['cirq.Operation', 'cirq.Gate', np.ndarray, Any],
+        *,
+        fsim_gate: Union['cirq.FSimGate', 'cirq.ISwapPowGate'],
+        qubits: Sequence['cirq.Qid'] = None) -> 'cirq.Circuit':
     """Decomposes operations into an FSimGate near theta=pi/2, phi=0.
 
     This decomposition is guaranteed to use exactly four of the given FSim
@@ -72,19 +88,18 @@ def decompose_two_qubit_interaction_into_four_fsim_gates_via_b(
 
     b_decomposition = _decompose_b_gate_into_two_fsims(fsim_gate=mapped_gate,
                                                        qubits=qubits)
-    result = []
+    b_decomposition = [
+        fsim_gate(*op.qubits) if op.gate == mapped_gate else op
+        for op in b_decomposition
+    ]
+
+    result = circuits.Circuit()
     for op in result_using_b_gates:
         if isinstance(op.gate, _BGate):
-            result.extend(b_decomposition)
+            result.append(b_decomposition)
         else:
             result.append(op)
-
-    circuit = circuits.Circuit(
-        fsim_gate(*op.qubits) if op.gate == mapped_gate else op
-        for op in result)
-    merge_single_qubit_gates.MergeSingleQubitGates().optimize_circuit(circuit)
-    drop_empty_moments.DropEmptyMoments().optimize_circuit(circuit)
-    return circuit
+    return result
 
 
 def _sticky_0_to_1(v: float, *, atol: float) -> Optional[float]:


Test patch:
diff --git a/cirq/optimizers/two_qubit_to_fsim_test.py b/cirq/optimizers/two_qubit_to_fsim_test.py
--- a/cirq/optimizers/two_qubit_to_fsim_test.py
+++ b/cirq/optimizers/two_qubit_to_fsim_test.py
@@ -76,16 +76,16 @@ def test_decompose_xx_yy_into_two_fsims_ignoring_single_qubit_ops_fail():
 
 @pytest.mark.parametrize('obj,fsim_gate',
                          itertools.product(UNITARY_OBJS, FEASIBLE_FSIM_GATES))
-def test_decompose_two_qubit_interaction_into_four_fsim_gates_via_b(
+def test_decompose_two_qubit_interaction_into_four_fsim_gates(
         obj: Any, fsim_gate: cirq.FSimGate):
     qubits = (obj.qubits
               if isinstance(obj, cirq.Operation) else cirq.LineQubit.range(2))
-    circuit = cirq.decompose_two_qubit_interaction_into_four_fsim_gates_via_b(
+    circuit = cirq.decompose_two_qubit_interaction_into_four_fsim_gates(
         obj, fsim_gate=fsim_gate)
     desired_unitary = obj if isinstance(obj, np.ndarray) else cirq.unitary(obj)
     for operation in circuit.all_operations():
         assert len(operation.qubits) < 2 or operation.gate == fsim_gate
-    assert len(circuit) <= 4 + 5
+    assert len(circuit) <= 4 * 3 + 5
     assert cirq.approx_eq(circuit.unitary(qubit_order=qubits),
                           desired_unitary,
                           atol=1e-6)
@@ -151,3 +151,12 @@ def test_sticky_0_to_1():
     assert _sticky_0_to_1(2, atol=1e-8) is None
 
     assert _sticky_0_to_1(-0.1, atol=0.5) == 0
+
+
+def test_deprecated():
+    iswap = cirq.FSimGate(theta=np.pi / 2, phi=0)
+    with cirq.testing.assert_logs(
+            'decompose_two_qubit_interaction_into_four_fsim_gates_via_b',
+            'deprecated'):
+        _ = cirq.decompose_two_qubit_interaction_into_four_fsim_gates_via_b(
+            np.eye(4), fsim_gate=iswap)