Repository: quantumlib/Cirq

Issue:
GridDeviceMetadata: qubit ordering within a pair
**Description of the issue**
Today `GridDeviceMetadata` always stores `qubit_pairs` such that the left <= right (lexicographically) in a pair, so the pair `(right, left)` is not in `GridDeviceMetadata.qubit_pairs`.

Is this intended behavior? AFAIU the `DeviceSpecification` proto doesn't guarantee an ordering within a qubit target. The caller could sort the pair before checking against `qubit_pairs`, in which case the fix is a docstring change for `qubit_pairs`. Or alternatively `qubit_pairs` can be implemented with a special set that accepts pairs in both directions.

**How to reproduce the issue**
Flip one of the qubits [here](https://github.com/quantumlib/Cirq/blob/f5d8cfb9443907ba47ce7665f418d3231bbe6f1b/cirq-google/cirq_google/devices/serializable_device_test.py#L55) and rerun the test.

**Cirq version**
0.14

@MichaelBroughton 




Discussion:
We assume that GridDevice will always be symmetrical, so if there's a device where a 2-qubit gate can only be done in one direction, then we should create a new gate type and corresponding new device metadata type. 

For device specification proto: Calibration metrics are not in the device proto but assume bidirectionality. Device specification proto can specify one-way edges. So, we should introduce a new asymmetric type or raise an error. 
To clarify, this bug is indeed about the bidirectional case. The bug (or feature?) is that a pair `(l, r)` is only considered to be in `qubit_pairs` if `l < r`, even though `(r, l)` is a valid pair. Today the DeviceSpecification is allowed to specify either `(l, r)` or `(r, l)`.

We could either make GridDeviceMetadata keep the requirement that `l < r`, in which case the docstring needs to be updated and the caller can sort the pair, or remove that requirement.
I think this should be automatic.  GridDeviceMetaData should sort the pairs automatically for you, removing the burden from the caller, and making the behavior consistent.
That's my preference as well. Will send out a fix.

Resolving patch:
diff --git a/cirq-core/cirq/devices/grid_device_metadata.py b/cirq-core/cirq/devices/grid_device_metadata.py
--- a/cirq-core/cirq/devices/grid_device_metadata.py
+++ b/cirq-core/cirq/devices/grid_device_metadata.py
@@ -92,7 +92,7 @@ def __init__(
         connectivity.add_edges_from(sorted(edge_set), directed=False)
         super().__init__(all_qubits, connectivity)
 
-        self._qubit_pairs = frozenset(edge_set)
+        self._qubit_pairs = frozenset({frozenset(pair) for pair in edge_set})
         self._gateset = gateset
         self._isolated_qubits = all_qubits.difference(node_set)
 
@@ -110,8 +110,12 @@ def __init__(
         self._gate_durations = gate_durations
 
     @property
-    def qubit_pairs(self) -> FrozenSet[Tuple['cirq.Qid', 'cirq.Qid']]:
-        """Returns the set of all couple-able qubits on the device."""
+    def qubit_pairs(self) -> FrozenSet[FrozenSet['cirq.Qid']]:
+        """Returns the set of all couple-able qubits on the device.
+
+        Each element in the outer frozenset is a 2-element frozenset representing a bidirectional
+        pair.
+        """
         return self._qubit_pairs
 
     @property
@@ -135,15 +139,16 @@ def _value_equality_values_(self):
             duration_equality = sorted(self._gate_durations.items(), key=lambda x: repr(x[0]))
 
         return (
-            tuple(sorted(self._qubit_pairs)),
+            self._qubit_pairs,
             self._gateset,
             tuple(duration_equality),
             tuple(sorted(self.qubit_set)),
         )
 
     def __repr__(self) -> str:
+        qubit_pair_tuples = frozenset({tuple(sorted(p)) for p in self._qubit_pairs})
         return (
-            f'cirq.GridDeviceMetadata({repr(self._qubit_pairs)},'
+            f'cirq.GridDeviceMetadata({repr(qubit_pair_tuples)},'
             f' {repr(self._gateset)}, {repr(self._gate_durations)},'
             f' {repr(self.qubit_set)})'
         )
@@ -154,7 +159,7 @@ def _json_dict_(self):
             duration_payload = sorted(self._gate_durations.items(), key=lambda x: repr(x[0]))
 
         return {
-            'qubit_pairs': sorted(list(self._qubit_pairs)),
+            'qubit_pairs': sorted([sorted(pair) for pair in self._qubit_pairs]),
             'gateset': self._gateset,
             'gate_durations': duration_payload,
             'all_qubits': sorted(list(self.qubit_set)),


Test patch:
diff --git a/cirq-core/cirq/devices/grid_device_metadata_test.py b/cirq-core/cirq/devices/grid_device_metadata_test.py
--- a/cirq-core/cirq/devices/grid_device_metadata_test.py
+++ b/cirq-core/cirq/devices/grid_device_metadata_test.py
@@ -26,13 +26,13 @@ def test_griddevice_metadata():
     metadata = cirq.GridDeviceMetadata(qubit_pairs, gateset, all_qubits=qubits + isolated_qubits)
     expected_pairings = frozenset(
         {
-            (cirq.GridQubit(0, 0), cirq.GridQubit(0, 1)),
-            (cirq.GridQubit(0, 1), cirq.GridQubit(0, 2)),
-            (cirq.GridQubit(0, 1), cirq.GridQubit(1, 1)),
-            (cirq.GridQubit(0, 2), cirq.GridQubit(1, 2)),
-            (cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)),
-            (cirq.GridQubit(1, 1), cirq.GridQubit(1, 2)),
-            (cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)),
+            frozenset((cirq.GridQubit(0, 0), cirq.GridQubit(0, 1))),
+            frozenset((cirq.GridQubit(0, 1), cirq.GridQubit(0, 2))),
+            frozenset((cirq.GridQubit(0, 1), cirq.GridQubit(1, 1))),
+            frozenset((cirq.GridQubit(0, 2), cirq.GridQubit(1, 2))),
+            frozenset((cirq.GridQubit(1, 0), cirq.GridQubit(1, 1))),
+            frozenset((cirq.GridQubit(1, 1), cirq.GridQubit(1, 2))),
+            frozenset((cirq.GridQubit(0, 0), cirq.GridQubit(1, 0))),
         }
     )
     assert metadata.qubit_set == frozenset(qubits + isolated_qubits)
diff --git a/cirq-core/cirq/protocols/json_test_data/GridDeviceMetadata.repr b/cirq-core/cirq/protocols/json_test_data/GridDeviceMetadata.repr
--- a/cirq-core/cirq/protocols/json_test_data/GridDeviceMetadata.repr
+++ b/cirq-core/cirq/protocols/json_test_data/GridDeviceMetadata.repr
@@ -1 +1 @@
-cirq.GridDeviceMetadata(frozenset({(cirq.GridQubit(0, 2), cirq.GridQubit(1, 2)), (cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)), (cirq.GridQubit(0, 1), cirq.GridQubit(1, 1)), (cirq.GridQubit(0, 0), cirq.GridQubit(0, 1)), (cirq.GridQubit(0, 1), cirq.GridQubit(0, 2)), (cirq.GridQubit(1, 1), cirq.GridQubit(1, 2)), (cirq.GridQubit(1, 0), cirq.GridQubit(1, 1))}), cirq.Gateset(cirq.ops.common_gates.XPowGate, cirq.ops.common_gates.YPowGate, cirq.ops.common_gates.ZPowGate), {cirq.GateFamily(cirq.ops.common_gates.XPowGate): cirq.Duration(nanos=1), cirq.GateFamily(cirq.ops.common_gates.YPowGate): cirq.Duration(picos=1), cirq.GateFamily(cirq.ops.common_gates.ZPowGate): cirq.Duration(picos=1)}, cirq.GridQubit.rect(2, 3) + [cirq.GridQubit(9, 9), cirq.GridQubit(10, 10)])
\ No newline at end of file
+cirq.GridDeviceMetadata(frozenset({(cirq.GridQubit(0, 1), cirq.GridQubit(0, 2)), (cirq.GridQubit(1, 1), cirq.GridQubit(1, 2)), (cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)), (cirq.GridQubit(0, 0), cirq.GridQubit(0, 1)), (cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)), (cirq.GridQubit(0, 2), cirq.GridQubit(1, 2)), (cirq.GridQubit(0, 1), cirq.GridQubit(1, 1))}), cirq.Gateset(cirq.ops.common_gates.XPowGate, cirq.ops.common_gates.YPowGate, cirq.ops.common_gates.ZPowGate, unroll_circuit_op = True), {cirq.GateFamily(gate=cirq.ops.common_gates.XPowGate, ignore_global_phase=True): cirq.Duration(nanos=1), cirq.GateFamily(gate=cirq.ops.common_gates.YPowGate, ignore_global_phase=True): cirq.Duration(picos=1), cirq.GateFamily(gate=cirq.ops.common_gates.ZPowGate, ignore_global_phase=True): cirq.Duration(picos=1)}, frozenset({cirq.GridQubit(0, 0), cirq.GridQubit(1, 0), cirq.GridQubit(1, 2), cirq.GridQubit(10, 10), cirq.GridQubit(0, 2), cirq.GridQubit(0, 1), cirq.GridQubit(1, 1), cirq.GridQubit(9, 9)}))
\ No newline at end of file
diff --git a/cirq-google/cirq_google/devices/serializable_device_test.py b/cirq-google/cirq_google/devices/serializable_device_test.py
--- a/cirq-google/cirq_google/devices/serializable_device_test.py
+++ b/cirq-google/cirq_google/devices/serializable_device_test.py
@@ -89,7 +89,7 @@ def test_metadata_correct():
         qubits=qubits, pairs=pairs, gate_sets=[cg.FSIM_GATESET]
     )
     device = cgdk.SerializableDevice.from_proto(device_proto, gate_sets=[cg.FSIM_GATESET])
-    assert device.metadata.qubit_pairs == frozenset(pairs)
+    assert device.metadata.qubit_pairs == frozenset({frozenset(p) for p in pairs})
     assert device.metadata.gateset == cirq.Gateset(
         cirq.FSimGate,
         cirq.ISwapPowGate,
diff --git a/cirq-google/cirq_google/devices/xmon_device_test.py b/cirq-google/cirq_google/devices/xmon_device_test.py
--- a/cirq-google/cirq_google/devices/xmon_device_test.py
+++ b/cirq-google/cirq_google/devices/xmon_device_test.py
@@ -59,18 +59,18 @@ def test_device_metadata():
     )
     assert d.metadata.qubit_pairs == frozenset(
         {
-            (cirq.GridQubit(0, 0), cirq.GridQubit(0, 1)),
-            (cirq.GridQubit(0, 1), cirq.GridQubit(1, 1)),
-            (cirq.GridQubit(2, 0), cirq.GridQubit(2, 1)),
-            (cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)),
-            (cirq.GridQubit(0, 2), cirq.GridQubit(1, 2)),
-            (cirq.GridQubit(1, 0), cirq.GridQubit(2, 0)),
-            (cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)),
-            (cirq.GridQubit(1, 1), cirq.GridQubit(2, 1)),
-            (cirq.GridQubit(1, 1), cirq.GridQubit(1, 2)),
-            (cirq.GridQubit(0, 1), cirq.GridQubit(0, 2)),
-            (cirq.GridQubit(2, 1), cirq.GridQubit(2, 2)),
-            (cirq.GridQubit(1, 2), cirq.GridQubit(2, 2)),
+            frozenset((cirq.GridQubit(0, 0), cirq.GridQubit(0, 1))),
+            frozenset((cirq.GridQubit(0, 1), cirq.GridQubit(1, 1))),
+            frozenset((cirq.GridQubit(2, 0), cirq.GridQubit(2, 1))),
+            frozenset((cirq.GridQubit(0, 0), cirq.GridQubit(1, 0))),
+            frozenset((cirq.GridQubit(0, 2), cirq.GridQubit(1, 2))),
+            frozenset((cirq.GridQubit(1, 0), cirq.GridQubit(2, 0))),
+            frozenset((cirq.GridQubit(1, 0), cirq.GridQubit(1, 1))),
+            frozenset((cirq.GridQubit(1, 1), cirq.GridQubit(2, 1))),
+            frozenset((cirq.GridQubit(1, 1), cirq.GridQubit(1, 2))),
+            frozenset((cirq.GridQubit(0, 1), cirq.GridQubit(0, 2))),
+            frozenset((cirq.GridQubit(2, 1), cirq.GridQubit(2, 2))),
+            frozenset((cirq.GridQubit(1, 2), cirq.GridQubit(2, 2))),
         }
     )