Repository: quantumlib/Cirq

Issue:
Make `unitary`, `has_unitary`, and `apply_unitary` fallback to `decompose`
This will make them consistent with how the simulators currently work, and probably better represents the user's intent (i.e. "I have a thing I don't know the function of, please do your best to tell me what it does").

Note that this will naturally result in lists of unitary operations having a unitary matrix according to cirq.

We might need to add an optional `qubit_order` value to the `unitary` method to make this work cleanly. If not specified, the order should be inferred (e.g. from operations' qubits property).

As part of this issue, a lot of special case "decompose to get unitary" code strewn around the code base (e.g. in simulators) should be rewritten.


Discussion:
I'm down with this but think we need to make sure we have pinned down what decompose actually means first #930 
I can work on this. However, I will maybe need some guidance. 

If I understand correctly, `unitary`  (`has_unitary`) should first try to decompose the object instead of raising `NotImplemented` (returning `False`)? However, if decompose fail, it should still raise  NotImplemented (return False)? 

I can't find a `apply_unitary_to_tensor` method anywhere in the code base.

I will probably get back to you about suitable test cases and the final task of rewriting "decompose to get unitary" in e.g. simulators.
`apply_unitary_to_tensor` was renamed to `apply_unitary`.

Yes, these methods should try to obtain a unitary effect through decomposition before giving up.
You are going to run into the snag "what order are the qubits in?" when attempting to reassemble multiple little unitaries into one larger unitary. This might be hard enough to deal with that tagging the issue "good first issue" was a bad idea. But of course I'll guide you as needed.

An acceptable compromise for this issue is to condition the decomposition fallback on the object being a `cirq.Operation`, because operations have a `qubits` property that returns a list that corresponds to the qubit order.

For unapplied gates things are significantly trickier, because you need to know how many qubits but there's... not really a way to do that. We will open up a follow-up issue to resolve that after this one is done.

> unitary (has_unitary) should first try to decompose the object instead of raising NotImplemented (returning False)? However, if decompose fail, it should still raise NotImplemented (return False)?

Correct. Note that you should use `cirq.decompose_once` instead of `cirq.decompose`, because you will be calling `unitary` / `apply_unitary` on the items. These calls will handle further decompositions if needed.

> I can't find a apply_unitary_to_tensor method anywhere in the code base.

It was renamed to `apply_unitary`.

> I will probably get back to you about suitable test cases and the final task of rewriting "decompose to get unitary" in e.g. simulators.

Okay.
Thanks for the hints. I will give it a try!

Resolving patch:
diff --git a/cirq/ops/three_qubit_gates.py b/cirq/ops/three_qubit_gates.py
--- a/cirq/ops/three_qubit_gates.py
+++ b/cirq/ops/three_qubit_gates.py
@@ -55,6 +55,9 @@ def _decompose_(self, qubits):
 
         where p = T**self._exponent
         """
+        if protocols.is_parameterized(self):
+            return NotImplemented
+
         a, b, c = qubits
 
         # Hacky magic: avoid the non-adjacent edge.
@@ -68,14 +71,16 @@ def _decompose_(self, qubits):
         sweep_abc = [common_gates.CNOT(a, b),
                      common_gates.CNOT(b, c)]
 
-        yield p(a), p(b), p(c)
-        yield sweep_abc
-        yield p(b)**-1, p(c)
-        yield sweep_abc
-        yield p(c)**-1
-        yield sweep_abc
-        yield p(c)**-1
-        yield sweep_abc
+        return [
+            p(a), p(b), p(c),
+            sweep_abc,
+            p(b)**-1, p(c),
+            sweep_abc,
+            p(c)**-1,
+            sweep_abc,
+            p(c)**-1,
+            sweep_abc,
+        ]
 
     def _apply_unitary_(self, args: protocols.ApplyUnitaryArgs) -> np.ndarray:
         if protocols.is_parameterized(self):
diff --git a/cirq/protocols/unitary.py b/cirq/protocols/unitary.py
--- a/cirq/protocols/unitary.py
+++ b/cirq/protocols/unitary.py
@@ -12,13 +12,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from typing import Any, TypeVar, Union
+from typing import TYPE_CHECKING, Any, TypeVar, Union
 
 import numpy as np
 from typing_extensions import Protocol
 
 from cirq.type_workarounds import NotImplementedType
 
+if TYPE_CHECKING:
+    # pylint: disable=unused-import
+    import cirq
+
 # This is a special indicator value used by the unitary method to determine
 # whether or not the caller provided a 'default' argument. It must be of type
 # np.ndarray to ensure the method has the correct type signature in that case.
@@ -82,16 +86,26 @@ def unitary(val: Any,
 
     Returns:
         If `val` has a _unitary_ method and its result is not NotImplemented,
-        that result is returned. Otherwise, if a default value was specified,
+        that result is returned. Otherwise, if `val` is a cirq.Operation, a
+        decomposition will be attempted and the resulting unitary will be
+        returned if unitaries exist for all operations of the decompostion.
+        If the result is still NotImplemented and a default value was specified,
         the default value is returned.
 
     Raises:
         TypeError: `val` doesn't have a _unitary_ method (or that method
             returned NotImplemented) and also no default value was specified.
     """
+    from cirq import Operation  # HACK: Avoids circular dependencies.
     getter = getattr(val, '_unitary_', None)
     result = NotImplemented if getter is None else getter()
 
+    # Fallback to decomposition for operations
+    if result is NotImplemented and isinstance(val, Operation):
+        decomposed_unitary = _decompose_and_get_unitary(val)
+        if decomposed_unitary is not None:
+            result = decomposed_unitary
+
     if result is not NotImplemented:
         return result
     if default is not RaiseTypeErrorIfNotProvided:
@@ -109,15 +123,59 @@ def has_unitary(val: Any) -> bool:
 
     Returns:
         If `val` has a _has_unitary_ method and its result is not
-        NotImplemented, that result is returned. Otherwise, if the value
-        has a _unitary_ method return if that has a non-default value.
-        Returns False if neither function exists.
+        NotImplemented, that result is returned. Otherwise, if `val` is a
+        cirq.Operation, a decomposition will be attempted and the resulting
+        unitary will be returned if unitaries exist for all operations of the
+        decompostion. Otherwise, if the value has a _unitary_ method return if
+        that has a non-default value. Returns False if neither function exists.
     """
+    from cirq.protocols.decompose import decompose_once
+    from cirq import Operation  # HACK: Avoids circular dependencies.
     getter = getattr(val, '_has_unitary_', None)
     result = NotImplemented if getter is None else getter()
 
+    # Fallback to decomposition for operations
+    if result is NotImplemented and isinstance(val, Operation):
+        decomposed_val = decompose_once(val, None)
+        if decomposed_val is not None:
+            result = all(has_unitary(v) for v in decomposed_val)
+
     if result is not NotImplemented:
         return result
 
     # No _has_unitary_ function, use _unitary_ instead
     return unitary(val, None) is not None
+
+
+def _decompose_and_get_unitary(val: 'cirq.Operation') -> np.ndarray:
+    """Try to decompose an `Operation` and return its unitary.
+
+    Returns:
+        If `val` can be decomposed into unitaries, calculate the resulting
+        unitary and return it. If it doesn't exist, None is returned.
+    """
+    from cirq.protocols.apply_unitary import apply_unitary, ApplyUnitaryArgs
+    from cirq.protocols.decompose import decompose_once
+
+    decomposed_val = decompose_once(val, None)
+    if decomposed_val is not None:
+        # Calculate the resulting unitary (if it exists)
+        n = len(val.qubits)
+        state = np.eye(1 << n, dtype=np.complex128)
+        state.shape = (2,) * (2 * n)
+        buffer = np.zeros(state.shape, dtype=np.complex128)
+        qubit_map = {q: i for i, q in enumerate(val.qubits)}
+        result = state
+        for op in decomposed_val:
+            indices = [qubit_map[q] for q in op.qubits]
+            result = apply_unitary(
+                unitary_value=op,
+                args=ApplyUnitaryArgs(state, buffer, indices),
+                default=None)
+            if result is None:
+                return None
+            if result is buffer:
+                buffer = state
+            state = result
+        if result is not None:
+            return result.reshape((1 << n, 1 << n))


Test patch:
diff --git a/cirq/optimizers/convert_to_cz_and_single_gates_test.py b/cirq/optimizers/convert_to_cz_and_single_gates_test.py
--- a/cirq/optimizers/convert_to_cz_and_single_gates_test.py
+++ b/cirq/optimizers/convert_to_cz_and_single_gates_test.py
@@ -90,7 +90,10 @@ def _decompose_(self, qubits):
     c_orig = cirq.Circuit(circuit)
     cirq.ConvertToCzAndSingleGates().optimize_circuit(circuit)
 
-    assert circuit == expected
+    cirq.testing.assert_allclose_up_to_global_phase(
+        circuit.to_unitary_matrix(),
+        expected.to_unitary_matrix(),
+        atol=1e-7)
     cirq.testing.assert_allclose_up_to_global_phase(
         circuit.to_unitary_matrix(),
         c_orig.to_unitary_matrix(),
diff --git a/cirq/protocols/unitary_test.py b/cirq/protocols/unitary_test.py
--- a/cirq/protocols/unitary_test.py
+++ b/cirq/protocols/unitary_test.py
@@ -20,28 +20,82 @@
 
 def test_unitary():
     m = np.array([[0, 1], [1, 0]])
+    m2 = np.array([[0, 0, 0, 1],
+                   [0, 0, 1, 0],
+                   [0, 1, 0, 0],
+                   [1, 0, 0, 0]])  # corresponds to X on two qubits
+    m3 = [[0, 0, 0, 0, 0, 1, 0, 0],
+          [0, 0, 0, 0, 1, 0, 0, 0],
+          [0, 0, 0, 0, 0, 0, 0, 1],
+          [0, 0, 0, 0, 0, 0, 1, 0],
+          [0, 1, 0, 0, 0, 0, 0, 0],
+          [1, 0, 0, 0, 0, 0, 0, 0],
+          [0, 0, 0, 1, 0, 0, 0, 0],
+          [0, 0, 1, 0, 0, 0, 0, 0]]  # corresponds to X on qubit 1 and 3
     d = np.array([])
 
     class NoMethod:
         pass
 
-    class ReturnsNotImplemented:
+    class ReturnsNotImplemented(cirq.Gate):
         def _has_unitary_(self):
             return NotImplemented
         def _unitary_(self):
             return NotImplemented
+        def num_qubits(self):
+            return None # coverage: ignore
 
     class ReturnsMatrix:
         def _unitary_(self) -> np.ndarray:
             return m
+        def num_qubits(self):
+            return 1 # coverage: ignore
 
-    class FullyImplemented:
+    class FullyImplemented(cirq.Gate):
         def __init__(self, unitary_value):
             self.unitary_value = unitary_value
         def _has_unitary_(self) -> bool:
             return self.unitary_value
         def _unitary_(self) -> np.ndarray:
             return m
+        def num_qubits(self):
+            return 1 # coverage: ignore
+
+    class Decomposable(cirq.Operation):
+        qubits = ()
+        with_qubits = NotImplemented
+        def __init__(self, qubits, unitary_value):
+            self.qubits = qubits
+            self.unitary_value = unitary_value
+        def _decompose_(self):
+            for q in self.qubits:
+                yield FullyImplemented(self.unitary_value)(q)
+
+    class DecomposableOrder(cirq.Operation):
+        qubits = ()
+        with_qubits = NotImplemented
+        def __init__(self, qubits):
+            self.qubits = qubits
+        def _decompose_(self):
+            yield FullyImplemented(True)(self.qubits[2])
+            yield FullyImplemented(True)(self.qubits[0])
+
+    class DecomposableNoUnitary(cirq.Operation):
+        qubits = ()
+        with_qubits = NotImplemented
+        def __init__(self, qubits):
+            self.qubits = qubits
+        def _decompose_(self):
+            for q in self.qubits:
+                yield ReturnsNotImplemented()(q)
+
+    class DummyOperation(cirq.Operation):
+        qubits = ()
+        with_qubits = NotImplemented
+        def __init__(self, qubits):
+            self.qubits = qubits
+        def _decompose_(self):
+            return ()
 
     with pytest.raises(TypeError, match='no _unitary_ method'):
         _ = cirq.unitary(NoMethod())
@@ -74,3 +128,35 @@ def _unitary_(self) -> np.ndarray:
     # Explicit function should override
     assert cirq.has_unitary(FullyImplemented(True))
     assert not cirq.has_unitary(FullyImplemented(False))
+
+    # Test if decomposed operations _has_unitary_
+    a = cirq.NamedQubit('a')
+    b = cirq.NamedQubit('b')
+    c = cirq.NamedQubit('c')
+
+    assert cirq.has_unitary(Decomposable((a, b), True))
+    assert not cirq.has_unitary(Decomposable((a,), False))
+    assert cirq.has_unitary(DummyOperation((a,)))
+    assert cirq.has_unitary(DummyOperation((a, b)))
+
+    # Test _decompose_and_get_unitary
+    from cirq.protocols.unitary import _decompose_and_get_unitary
+    np.testing.assert_allclose(_decompose_and_get_unitary(
+        Decomposable((a,), True)), m)
+    np.testing.assert_allclose(_decompose_and_get_unitary(
+        Decomposable((a, b), True)), m2)
+    np.testing.assert_allclose(_decompose_and_get_unitary(
+        DecomposableOrder((a, b, c))), m3)
+    np.testing.assert_allclose(_decompose_and_get_unitary(
+        DummyOperation((a,))), np.eye(2))
+    np.testing.assert_allclose(_decompose_and_get_unitary(
+        DummyOperation((a, b))), np.eye(4))
+
+    # Test if decomposed operations has _unitary_
+    np.testing.assert_allclose(cirq.unitary(Decomposable((a,), True)), m)
+    np.testing.assert_allclose(cirq.unitary(Decomposable((a, b), True)), m2)
+    np.testing.assert_allclose(cirq.unitary(DecomposableOrder((a, b, c))), m3)
+    np.testing.assert_allclose(cirq.unitary(DummyOperation((a,))), np.eye(2))
+    np.testing.assert_allclose(cirq.unitary(DummyOperation((a, b))), np.eye(4))
+
+    assert cirq.unitary(DecomposableNoUnitary((a,)), None) is None