Repository: quantumlib/Cirq

Issue:
contrib/test_route_circuit_via_unitaries flaked
Happened in this build:

https://github.com/quantumlib/Cirq/pull/2165/checks?check_run_id=235027498

This is the build log from travis:

```
_____ test_route_circuit_via_unitaries[circuit9-device_graph9-greedy-True] _____
circuit = cirq.Circuit([
    cirq.Moment(operations=[
        cirq.S.on(cirq.NamedQubit('1')),
        cirq.ISWAP.on(cirq.NamedQ...t('0'), cirq.NamedQubit('3')),
    ]),
    cirq.Moment(operations=[
        cirq.Y.on(cirq.NamedQubit('2')),
    ]),
])
device_graph = <networkx.classes.graph.Graph object at 0x7fd4cf8e76a0>
algo = 'greedy', make_bad = True
    @pytest.mark.parametrize(
        'circuit,device_graph,algo,make_bad', [(cirq.testing.random_circuit(
            4, 8, 0.5), ccr.get_grid_device_graph(3, 2), 'greedy', make_bad)
                                               for make_bad in (False, True)
                                               for _ in range(5)] +
        [(cirq.Circuit(), ccr.get_grid_device_graph(3, 2), 'greedy', False)])
    def test_route_circuit_via_unitaries(circuit, device_graph, algo, make_bad):
        swap_network = ccr.route_circuit(circuit, device_graph, algo_name=algo)
    
        logical_qubits = sorted(circuit.all_qubits())
        if len(logical_qubits) < 2:
            return
        reverse_mapping = {l: p for p, l in swap_network.initial_mapping.items()}
        physical_qubits = [reverse_mapping[l] for l in logical_qubits]
        physical_qubits += list(set(device_graph).difference(physical_qubits))
        n_unused_qubits = len(physical_qubits) - len(logical_qubits)
    
        if make_bad:
            swap_network.circuit += [cirq.CNOT(*physical_qubits[:2])]
        cca.return_to_initial_mapping(swap_network.circuit)
    
        logical_unitary = circuit.unitary(qubit_order=logical_qubits)
        logical_unitary = np.kron(logical_unitary, np.eye(1 << n_unused_qubits))
        physical_unitary = swap_network.circuit.unitary(qubit_order=physical_qubits)
    
>       assert ccr.is_valid_routing(circuit, swap_network) == (not make_bad)
cirq/contrib/routing/router_test.py:64: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cirq/contrib/routing/utils.py:76: in is_valid_routing
    return cca.is_topologically_sorted(circuit_dag, logical_operations, equals)
cirq/contrib/acquaintance/topological_sort.py:63: in is_topologically_sorted
    ops.flatten_op_tree(operations)):
cirq/ops/op_tree.py:89: in flatten_to_ops
    for subtree in root:
cirq/contrib/acquaintance/permutation.py:234: in get_logical_operations
    yield op.transform_qubits(mapping.__getitem__)
cirq/ops/raw_types.py:364: in transform_qubits
    return self.with_qubits(*(func(q) for q in self.qubits))
cirq/ops/gate_operation.py:54: in with_qubits
    return self.gate.on(*new_qubits)
cirq/ops/common_gates.py:1131: in on
    return super().on(*args)
cirq/ops/raw_types.py:200: in on
    return gate_operation.GateOperation(self, list(qubits))
cirq/ops/gate_operation.py:39: in __init__
    gate.validate_args(qubits)
cirq/ops/raw_types.py:190: in validate_args
    _validate_qid_shape(self, qubits)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
val = cirq.CNOT, qubits = [cirq.NamedQubit('0'), cirq.NamedQubit('0')]
    def _validate_qid_shape(val: Any, qubits: Sequence[Qid]) -> None:
        """Helper function to validate qubits for gates and operations.
    
        Raises:
            ValueError: The operation had qids that don't match it's qid shape.
        """
        qid_shape = protocols.qid_shape(val)
        if len(qubits) != len(qid_shape):
            raise ValueError('Wrong number of qubits for <{!r}>. '
                             'Expected {} qubits but got <{!r}>.'.format(
                                 val, len(qid_shape), qubits))
        if any(qid.dimension != dimension
               for qid, dimension in zip(qubits, qid_shape)):
            raise ValueError('Wrong shape of qids for <{!r}>. '
                             'Expected {} but got {} <{!r}>.'.format(
                                 val, qid_shape,
                                 tuple(qid.dimension for qid in qubits), qubits))
        if len(set(qubits)) != len(qubits):
            raise ValueError('Duplicate qids for <{!r}>. '
                             'Expected unique qids but got <{!r}>.'.format(
>                                val, qubits))
E           ValueError: Duplicate qids for <cirq.CNOT>. Expected unique qids but got <[cirq.NamedQubit('0'), cirq.NamedQubit('0')]>.
cirq/ops/raw_types.py:452: ValueError
----------- coverage: platform linux, python 3.6.7-final-0 -----------
Coverage annotated source written next to source
=============================== warnings summary ===============================
/home/travis/build/quantumlib/Cirq/cirq/ops/arithmetic_operation.py:224: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  dst[outputs] = src[inputs]
-- Docs: https://docs.pytest.org/en/latest/warnings.html
1 failed, 7902 passed, 26 skipped, 72 xfailed, 1 warnings in 274.27 seconds
Finished pytest
Running incremental-coverage
All touched lines covered
Finished incremental-coverage
Failed.
```


Discussion:

Resolving patch:
diff --git a/cirq/contrib/acquaintance/permutation.py b/cirq/contrib/acquaintance/permutation.py
--- a/cirq/contrib/acquaintance/permutation.py
+++ b/cirq/contrib/acquaintance/permutation.py
@@ -13,8 +13,8 @@
 # limitations under the License.
 
 import abc
-from typing import (cast, Dict, Iterable, Sequence, Tuple, TypeVar, Union,
-                    TYPE_CHECKING)
+from typing import (cast, Dict, Iterable, Sequence, Tuple, TYPE_CHECKING,
+                    TypeVar, Union)
 
 from cirq import circuits, ops, optimizers, protocols, value
 
@@ -64,7 +64,10 @@ def update_mapping(self, mapping: Dict[ops.Qid, LogicalIndex],
         new_keys = [keys[permutation[i]] for i in indices]
         old_elements = [mapping.get(keys[i]) for i in indices]
         for new_key, old_element in zip(new_keys, old_elements):
-            if old_element is not None:
+            if old_element is None:
+                if new_key in mapping:
+                    del mapping[new_key]
+            else:
                 mapping[new_key] = old_element
 
     @staticmethod
@@ -225,12 +228,27 @@ def update_mapping(mapping: Dict[ops.Qid, LogicalIndex],
 def get_logical_operations(operations: 'cirq.OP_TREE',
                            initial_mapping: Dict[ops.Qid, ops.Qid]
                           ) -> Iterable['cirq.Operation']:
+    """Gets the logical operations specified by the physical operations and
+    initial mapping.
+
+    Args:
+        operations: The physical operations.
+        initial_mapping: The initial mapping of physical to logical qubits.
+
+    Raises:
+        ValueError: A non-permutation physical operation acts on an unmapped
+            qubit.
+    """
     mapping = initial_mapping.copy()
     for op in cast(Iterable['cirq.Operation'], ops.flatten_op_tree(operations)):
         if (isinstance(op, ops.GateOperation) and
                 isinstance(op.gate, PermutationGate)):
             op.gate.update_mapping(mapping, op.qubits)
         else:
+            for q in op.qubits:
+                if mapping.get(q) is None:
+                    raise ValueError(
+                        f'Operation {op} acts on unmapped qubit {q}.')
             yield op.transform_qubits(mapping.__getitem__)
 
 
diff --git a/cirq/contrib/routing/swap_network.py b/cirq/contrib/routing/swap_network.py
--- a/cirq/contrib/routing/swap_network.py
+++ b/cirq/contrib/routing/swap_network.py
@@ -63,3 +63,8 @@ def __eq__(self, other):
     @property
     def device(self):
         return self.circuit.device
+
+    def __str__(self):
+        circuit = self.circuit.copy()
+        cca.display_mapping(circuit, self.initial_mapping)
+        return str(circuit)
diff --git a/cirq/contrib/routing/utils.py b/cirq/contrib/routing/utils.py
--- a/cirq/contrib/routing/utils.py
+++ b/cirq/contrib/routing/utils.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 import operator
+import re
 from typing import Callable, Iterable, List
 
 import networkx as nx
@@ -73,4 +74,10 @@ def is_valid_routing(
     circuit_dag = circuits.CircuitDag.from_circuit(circuit,
                                                    can_reorder=can_reorder)
     logical_operations = swap_network.get_logical_operations()
-    return cca.is_topologically_sorted(circuit_dag, logical_operations, equals)
+    try:
+        return cca.is_topologically_sorted(circuit_dag, logical_operations,
+                                           equals)
+    except ValueError as err:
+        if re.match(r'Operation .* acts on unmapped qubit .*\.', str(err)):
+            return False
+        raise


Test patch:
diff --git a/cirq/contrib/routing/router_test.py b/cirq/contrib/routing/router_test.py
--- a/cirq/contrib/routing/router_test.py
+++ b/cirq/contrib/routing/router_test.py
@@ -38,7 +38,8 @@ def test_route_circuit(circuit, device_graph, algo):
 
 @pytest.mark.parametrize(
     'circuit,device_graph,algo,make_bad', [(cirq.testing.random_circuit(
-        4, 8, 0.5), ccr.get_grid_device_graph(3, 2), 'greedy', make_bad)
+        4, 8, 0.5), ccr.get_grid_device_graph(3, 2), algo, make_bad)
+                                           for algo in ccr.ROUTERS
                                            for make_bad in (False, True)
                                            for _ in range(5)] +
     [(cirq.Circuit(), ccr.get_grid_device_graph(3, 2), 'greedy', False)])
diff --git a/cirq/contrib/routing/swap_network_test.py b/cirq/contrib/routing/swap_network_test.py
--- a/cirq/contrib/routing/swap_network_test.py
+++ b/cirq/contrib/routing/swap_network_test.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import itertools
+
 import pytest
 
 import cirq
@@ -55,3 +57,31 @@ def test_swap_network_equality(circuits):
         for y in (0, 1):
             mapping = {cirq.GridQubit(x, y): q for x, q in enumerate(qubits)}
             et.add_equality_group(ccr.SwapNetwork(circuit, mapping))
+
+
+def test_swap_network_str():
+    n_qubits = 5
+    phys_qubits = cirq.GridQubit.rect(n_qubits, 1)
+    log_qubits = cirq.LineQubit.range(n_qubits)
+
+    gates = {
+        (l, ll): cirq.ZZ for l, ll in itertools.combinations(log_qubits, 2)
+    }
+    initial_mapping = {p: l for p, l in zip(phys_qubits, log_qubits)}
+    execution_strategy = cca.GreedyExecutionStrategy(gates, initial_mapping)
+    routed_circuit = cca.complete_acquaintance_strategy(phys_qubits, 2)
+    execution_strategy(routed_circuit)
+    swap_network = ccr.SwapNetwork(routed_circuit, initial_mapping)
+    actual_str = str(swap_network)
+    expected_str = """
+(0, 0): ───0───ZZ───0───╲0╱───1────────1─────────1───ZZ───1───╲0╱───3────────3─────────3───ZZ───3───╲0╱───4───
+               │        │                            │        │                            │        │
+(1, 0): ───1───ZZ───1───╱1╲───0───ZZ───0───╲0╱───3───ZZ───3───╱1╲───1───ZZ───1───╲0╱───4───ZZ───4───╱1╲───3───
+                                  │        │                            │        │
+(2, 0): ───2───ZZ───2───╲0╱───3───ZZ───3───╱1╲───0───ZZ───0───╲0╱───4───ZZ───4───╱1╲───1───ZZ───1───╲0╱───2───
+               │        │                            │        │                            │        │
+(3, 0): ───3───ZZ───3───╱1╲───2───ZZ───2───╲0╱───4───ZZ───4───╱1╲───0───ZZ───0───╲0╱───2───ZZ───2───╱1╲───1───
+                                  │        │                            │        │
+(4, 0): ───4────────4─────────4───ZZ───4───╱1╲───2────────2─────────2───ZZ───2───╱1╲───0────────0─────────0───
+    """.strip()
+    assert actual_str == expected_str
diff --git a/cirq/contrib/routing/utils_test.py b/cirq/contrib/routing/utils_test.py
--- a/cirq/contrib/routing/utils_test.py
+++ b/cirq/contrib/routing/utils_test.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
 import cirq
 import cirq.contrib.routing as ccr
 
@@ -24,3 +26,19 @@ def test_ops_are_consistent_with_device_graph():
         circuit.all_operations(), device_graph)
     assert not ccr.ops_are_consistent_with_device_graph(
         [cirq.X(cirq.GridQubit(0, 0))], device_graph)
+
+
+def test_is_valid_routing_with_bad_args():
+    p, q, r = cirq.LineQubit.range(3)
+    x, y = cirq.NamedQubit('x'), cirq.NamedQubit('y')
+    circuit = cirq.Circuit([cirq.CNOT(x, y), cirq.CZ(x, y)])
+    routed_circuit = cirq.Circuit([cirq.CNOT(p, q), cirq.CZ(q, r)])
+    initial_mapping = {p: x, q: y}
+    swap_network = ccr.SwapNetwork(routed_circuit, initial_mapping)
+    assert not ccr.is_valid_routing(circuit, swap_network)
+
+    def equals(*args):
+        raise ValueError
+
+    with pytest.raises(ValueError):
+        ccr.is_valid_routing(circuit, swap_network, equals=equals)