Repository: quantumlib/Cirq

Issue:
MergeInteractions optimization is creating bad circuits
Not sure how this made it past the tests yet.

```python
def main():
    circuit = make_inefficient_circuit()
    print("BEFORE:")
    print(circuit.to_unitary_matrix().round(3))
    print()
    cirq.google.MergeInteractions().optimize_circuit(circuit)
    print("AFTER:")
    print(circuit.to_unitary_matrix().round(3))

def make_inefficient_circuit(t=0.1, v=0.11):
    from cirq import H, CNOT, Z
    a = cirq.NamedQubit('a')
    b = cirq.NamedQubit('b')

    return cirq.Circuit.from_ops(
        H(b),
        CNOT(a, b),
        H(b),
        CNOT(a, b),
        CNOT(b, a),
        H(a),
        CNOT(a, b),
        Z(a)**t, Z(b)**-t,
        CNOT(a, b),
        H(a), Z(b)**v,
        CNOT(a, b),
        Z(a)**-v, Z(b)**-v)
```

```
BEFORE:
[[ 1.   -0.j     0.   -0.j     0.   +0.j     0.   -0.j   ]
 [-0.   -0.j    -0.   -0.309j  0.951-0.j    -0.   +0.j   ]
 [ 0.   -0.j     0.951-0.j    -0.   -0.309j  0.   +0.j   ]
 [-0.   +0.j     0.   +0.j     0.   -0.j    -0.771+0.637j]]

AFTER:
[[-0.354-0.354j -0.354-0.354j -0.227-0.446j  0.227+0.446j]
 [-0.452-0.213j -0.452-0.213j  0.364+0.342j -0.364-0.342j]
 [-0.354-0.354j  0.354+0.354j -0.446-0.227j -0.446-0.227j]
 [-0.452-0.213j  0.452+0.213j  0.496+0.063j  0.496+0.063j]]
```


Discussion:

Resolving patch:
diff --git a/cirq/google/decompositions.py b/cirq/google/decompositions.py
--- a/cirq/google/decompositions.py
+++ b/cirq/google/decompositions.py
@@ -299,58 +299,97 @@ def two_qubit_matrix_to_native_gates(q0: ops.QubitId,
     _, (a0, a1), (x, y, z), (b0, b1) = linalg.kak_decomposition(
         mat,
         linalg.Tolerance(atol=tolerance))
+    return _kak_decomposition_to_native_gates(q0, q1,
+                                              a0, a1, x, y, z, b0, b1,
+                                              allow_partial_czs, tolerance)
 
-    def is_trivial_angle(rad: float) -> bool:
-        return abs(rad) < tolerance or abs(rad - np.pi / 4) < tolerance
 
-    def parity_interaction(rads: float,
-                           op: Optional[ops.ReversibleGate] = None):
-        """Yields a ZZ interaction framed by the given operation."""
-        if abs(rads) < tolerance:
-            return
+def _kak_decomposition_to_native_gates(q0: ops.QubitId,
+                                       q1: ops.QubitId,
+                                       a0: np.ndarray,
+                                       a1: np.ndarray,
+                                       x: float,
+                                       y: float,
+                                       z: float,
+                                       b0: np.ndarray,
+                                       b1: np.ndarray,
+                                       allow_partial_czs: bool,
+                                       tolerance: float = 1e-8
+                                       ) -> List[ops.Operation]:
+    """Assumes that the decomposition is canonical."""
+    pre = [_do_single_on(b0, q0, tolerance), _do_single_on(b1, q1, tolerance)]
+    post = [_do_single_on(a0, q0, tolerance), _do_single_on(a1, q1, tolerance)]
 
-        h = rads * -2 / np.pi
-        if op is not None:
-            yield op.on(q0), op.on(q1)
+    return list(ops.flatten_op_tree([
+        pre,
+        _non_local_part(q0, q1, x, y, z, allow_partial_czs, tolerance),
+        post,
+    ]))
 
-        # If rads is pi/4 radians within tolerance, single full-CZ suffices.
-        if abs(rads - (np.pi / 4)) < tolerance:
-            yield ops.CZ.on(q0, q1)
-        else:
-            yield _easy_direction_partial_cz(q0, q1, -2 * h)
 
-        yield ops.Z(q0)**h
-        yield ops.Z(q1)**h
-        if op is not None:
-            yield op.inverse().on(q0), op.inverse().on(q1)
+def _is_trivial_angle(rad: float, tolerance: float) -> bool:
+    """Tests if a circuit for an operator exp(i*rad*XX) (or YY, or ZZ) can
+    be performed with a whole CZ.
 
-    def do_single_on(u, q):
-        for gate in single_qubit_matrix_to_native_gates(u, tolerance):
-            yield gate(q)
+    Args:
+        rad: The angle in radians, assumed to be in the range [-pi/4, pi/4]
+    """
+    return abs(rad) < tolerance or abs(abs(rad) - np.pi / 4) < tolerance
 
-    def non_local_part():
-        """Yields non-local operation of KAK decomposition."""
 
-        if allow_partial_czs or all(is_trivial_angle(e) for e in [x, y, z]):
-            return [
-                parity_interaction(x, ops.Y**-0.5),
-                parity_interaction(y, ops.X**0.5),
-                parity_interaction(z)
-            ]
+def _parity_interaction(q0: ops.QubitId,
+                        q1: ops.QubitId,
+                        rads: float,
+                        tolerance: float,
+                        op: Optional[ops.ReversibleGate] = None):
+    """Yields a ZZ interaction framed by the given operation."""
+    if abs(rads) < tolerance:
+        return
 
-        if abs(z) >= tolerance:
-            return _xx_yy_zz_interaction_via_full_czs(q0, q1, x, y, z)
+    h = rads * -2 / np.pi
+    if op is not None:
+        yield op.on(q0), op.on(q1)
 
-        if y >= tolerance:
-            return _xx_yy_interaction_via_full_czs(q0, q1, x, y)
+    # If rads is ±pi/4 radians within tolerance, single full-CZ suffices.
+    if _is_trivial_angle(rads, tolerance):
+        yield ops.CZ.on(q0, q1)
+    else:
+        yield _easy_direction_partial_cz(q0, q1, -2 * h)
 
-        return _xx_interaction_via_full_czs(q0, q1, x)
+    yield ops.Z(q0)**h
+    yield ops.Z(q1)**h
+    if op is not None:
+        yield op.inverse().on(q0), op.inverse().on(q1)
 
-    pre = [do_single_on(b0, q0), do_single_on(b1, q1)]
-    post = [do_single_on(a0, q0), do_single_on(a1, q1)]
 
-    return list(ops.flatten_op_tree([
-        pre,
-        non_local_part(),
-        post,
-    ]))
+def _do_single_on(u: np.ndarray, q: ops.QubitId, tolerance: float=1e-8):
+    for gate in single_qubit_matrix_to_native_gates(u, tolerance):
+        yield gate(q)
+
+
+def _non_local_part(q0: ops.QubitId,
+                    q1: ops.QubitId,
+                    x: float,
+                    y: float,
+                    z: float,
+                    allow_partial_czs: bool,
+                    tolerance: float = 1e-8):
+    """Yields non-local operation of KAK decomposition."""
+
+    if (allow_partial_czs or
+        all(_is_trivial_angle(e, tolerance) for e in [x, y, z])):
+        return [
+            _parity_interaction(q0, q1, x, tolerance,
+                                cast(ops.ReversibleGate, ops.Y**-0.5)),
+            _parity_interaction(q0, q1, y, tolerance,
+                                cast(ops.ReversibleGate, ops.X**0.5)),
+            _parity_interaction(q0, q1, z, tolerance)
+        ]
+
+    if abs(z) >= tolerance:
+        return _xx_yy_zz_interaction_via_full_czs(q0, q1, x, y, z)
+
+    if y >= tolerance:
+        return _xx_yy_interaction_via_full_czs(q0, q1, x, y)
+
+    return _xx_interaction_via_full_czs(q0, q1, x)
diff --git a/cirq/google/merge_interactions.py b/cirq/google/merge_interactions.py
--- a/cirq/google/merge_interactions.py
+++ b/cirq/google/merge_interactions.py
@@ -29,7 +29,8 @@
 
 
 class MergeInteractions(PointOptimizer):
-    """Combines adjacent constant single-qubit rotations."""
+    """Combines series of adjacent one and two-qubit gates operating on a pair
+    of qubits."""
 
     def __init__(self,
                  tolerance: float = 1e-8,


Test patch:
diff --git a/cirq/google/decompositions_test.py b/cirq/google/decompositions_test.py
--- a/cirq/google/decompositions_test.py
+++ b/cirq/google/decompositions_test.py
@@ -347,3 +347,38 @@ def test_two_to_native_equivalent_and_bounded_for_known_and_random(
 
     assert_cz_depth_below(operations_with_partial, max_partial_cz_depth, False)
     assert_cz_depth_below(operations_with_full, max_full_cz_depth, True)
+
+
+def test_trivial_parity_interaction_corner_case():
+    q0 = ops.QubitId()
+    q1 = ops.QubitId()
+    nearPi4 = np.pi/4 * 0.99
+    tolerance = 1e-2
+    circuit = circuits.Circuit.from_ops(
+                decompositions._parity_interaction(q0, q1, -nearPi4, tolerance))
+    assert len(circuit) == 2
+
+
+@pytest.mark.parametrize('rad,expected', (lambda err, largeErr: [
+    (np.pi/4, True),
+    (np.pi/4 + err, True),
+    (np.pi/4 + largeErr, False),
+    (np.pi/4 - err, True),
+    (np.pi/4 - largeErr, False),
+    (-np.pi/4, True),
+    (-np.pi/4 + err, True),
+    (-np.pi/4 + largeErr, False),
+    (-np.pi/4 - err, True),
+    (-np.pi/4 - largeErr, False),
+    (0, True),
+    (err, True),
+    (largeErr, False),
+    (-err, True),
+    (-largeErr, False),
+    (np.pi/8, False),
+    (-np.pi/8, False),
+])(1e-8*2/3, 1e-8*4/3))
+def test_is_trivial_angle(rad, expected):
+    tolerance = 1e-8
+    out = decompositions._is_trivial_angle(rad, tolerance)
+    assert out == expected, 'rad = {}'.format(rad)