Repository: quantumlib/Cirq

Issue:
(re-)register deserializer for QuantumVolumeResult
following #2443

cc @KevinVillela 


Discussion:
Thanks for the heads up - @Strilanc was saying that we can probably put the Quantum Volume stuff into cirq/experiments now. Happy to do that if it makes sense.
Yes, that would resolve this issue (+ restoring the entries in the big json dictionary)
I tried moving the file, but then I realized that it actually depends on something else in contrib - cirq.contrib.routing :\ https://github.com/quantumlib/Cirq/blob/master/cirq/contrib/quantum_volume/quantum_volume.py#L11. That means we don't want to pull it in, right? Is routing something we want in regular Cirq, or do we need another solution for json-ifying things in contrib?
Couple of other options:
  - Re-write quantum_volume.py to take in an arbitrary routing function, which would be called with ccr.route_circuit. This isn't ideal because routing is kind of a central part of the algorithm.
 - Just move QuantumVolumeResult (which has no contrib dependencies) out of contrib temporarily.
If we do have an `experiments/` module, the code will likely need to depend on contrib-quality code. Any experiment worth its salt will identify functionality not *strictly* associated with the experiment itself but either (a) not worth it (for the experiment developer) to put in the (large) effort to move to cirq-proper, or (b) slightly too specific to alleviate concerns of cirq becoming bloated

Resolving patch:
diff --git a/cirq/contrib/__init__.py b/cirq/contrib/__init__.py
--- a/cirq/contrib/__init__.py
+++ b/cirq/contrib/__init__.py
@@ -22,3 +22,4 @@
 from cirq.contrib import graph_device
 from cirq.contrib import quirk
 from cirq.contrib.qcircuit import circuit_to_latex_using_qcircuit
+from cirq.contrib import json
diff --git a/cirq/contrib/json.py b/cirq/contrib/json.py
new file mode 100644
--- /dev/null
+++ b/cirq/contrib/json.py
@@ -0,0 +1,19 @@
+"""Contains functions for adding JSON serialization and de-serialization for
+classes in Contrib.
+
+"""
+
+from cirq.protocols.json import DEFAULT_RESOLVERS
+
+
+def contrib_class_resolver(cirq_type: str):
+    """Extend cirq's JSON API with resolvers for cirq contrib classes."""
+    from cirq.contrib.quantum_volume import QuantumVolumeResult
+    classes = [
+        QuantumVolumeResult,
+    ]
+    d = {cls.__name__: cls for cls in classes}
+    return d.get(cirq_type, None)
+
+
+DEFAULT_CONTRIB_RESOLVERS = [contrib_class_resolver] + DEFAULT_RESOLVERS


Test patch:
diff --git a/cirq/contrib/json_test.py b/cirq/contrib/json_test.py
new file mode 100644
--- /dev/null
+++ b/cirq/contrib/json_test.py
@@ -0,0 +1,14 @@
+import cirq
+from cirq.contrib.quantum_volume import QuantumVolumeResult
+from cirq.testing import assert_json_roundtrip_works
+from cirq.contrib.json import DEFAULT_CONTRIB_RESOLVERS
+
+
+def test_quantum_volume():
+    qubits = cirq.LineQubit.range(5)
+    qvr = QuantumVolumeResult(
+        model_circuit=cirq.Circuit(cirq.H.on_each(qubits)),
+        heavy_set=[1, 2, 3],
+        compiled_circuit=cirq.Circuit(cirq.H.on_each(qubits)),
+        sampler_result=.1)
+    assert_json_roundtrip_works(qvr, resolvers=DEFAULT_CONTRIB_RESOLVERS)
diff --git a/cirq/protocols/json_test.py b/cirq/protocols/json_test.py
--- a/cirq/protocols/json_test.py
+++ b/cirq/protocols/json_test.py
@@ -29,36 +29,16 @@
 
 import cirq
 from cirq._compat import proper_repr, proper_eq
+from cirq.testing import assert_json_roundtrip_works
 
 TEST_DATA_PATH = pathlib.Path(__file__).parent / 'json_test_data'
 TEST_DATA_REL = 'cirq/protocols/json_test_data'
 
 
-def assert_roundtrip(obj, text_should_be=None):
-    buffer = io.StringIO()
-    cirq.to_json(obj, buffer)
-
-    if text_should_be is not None:
-        buffer.seek(0)
-        text = buffer.read()
-        assert text == text_should_be
-
-    buffer.seek(0)
-    restored_obj = cirq.read_json(buffer)
-    if isinstance(obj, np.ndarray):
-        np.testing.assert_equal(restored_obj, obj)
-    elif isinstance(obj, pd.DataFrame):
-        pd.testing.assert_frame_equal(restored_obj, obj)
-    elif isinstance(obj, pd.Index):
-        pd.testing.assert_index_equal(restored_obj, obj)
-    else:
-        assert restored_obj == obj
-
-
 def test_line_qubit_roundtrip():
     q1 = cirq.LineQubit(12)
-    assert_roundtrip(q1,
-                     text_should_be="""{
+    assert_json_roundtrip_works(q1,
+                                text_should_be="""{
   "cirq_type": "LineQubit",
   "x": 12
 }""")
@@ -66,8 +46,8 @@ def test_line_qubit_roundtrip():
 
 def test_gridqubit_roundtrip():
     q = cirq.GridQubit(15, 18)
-    assert_roundtrip(q,
-                     text_should_be="""{
+    assert_json_roundtrip_works(q,
+                                text_should_be="""{
   "cirq_type": "GridQubit",
   "row": 15,
   "col": 18
@@ -77,8 +57,8 @@ def test_gridqubit_roundtrip():
 def test_op_roundtrip():
     q = cirq.LineQubit(5)
     op1 = cirq.rx(.123).on(q)
-    assert_roundtrip(op1,
-                     text_should_be="""{
+    assert_json_roundtrip_works(op1,
+                                text_should_be="""{
   "cirq_type": "GateOperation",
   "gate": {
     "cirq_type": "XPowGate",
@@ -352,10 +332,10 @@ def _find_classes_that_should_serialize() -> Set[Tuple[str, Type]]:
 
 
 def test_builtins():
-    assert_roundtrip(True)
-    assert_roundtrip(1)
-    assert_roundtrip(1 + 2j)
-    assert_roundtrip({
+    assert_json_roundtrip_works(True)
+    assert_json_roundtrip_works(1)
+    assert_json_roundtrip_works(1 + 2j)
+    assert_json_roundtrip_works({
         'test': [123, 5.5],
         'key2': 'asdf',
         '3': None,
@@ -366,39 +346,39 @@ def test_builtins():
 def test_numpy():
     x = np.ones(1)[0]
 
-    assert_roundtrip(x.astype(np.bool))
-    assert_roundtrip(x.astype(np.int8))
-    assert_roundtrip(x.astype(np.int16))
-    assert_roundtrip(x.astype(np.int32))
-    assert_roundtrip(x.astype(np.int64))
-    assert_roundtrip(x.astype(np.uint8))
-    assert_roundtrip(x.astype(np.uint16))
-    assert_roundtrip(x.astype(np.uint32))
-    assert_roundtrip(x.astype(np.uint64))
-    assert_roundtrip(x.astype(np.float32))
-    assert_roundtrip(x.astype(np.float64))
-    assert_roundtrip(x.astype(np.complex64))
-    assert_roundtrip(x.astype(np.complex128))
+    assert_json_roundtrip_works(x.astype(np.bool))
+    assert_json_roundtrip_works(x.astype(np.int8))
+    assert_json_roundtrip_works(x.astype(np.int16))
+    assert_json_roundtrip_works(x.astype(np.int32))
+    assert_json_roundtrip_works(x.astype(np.int64))
+    assert_json_roundtrip_works(x.astype(np.uint8))
+    assert_json_roundtrip_works(x.astype(np.uint16))
+    assert_json_roundtrip_works(x.astype(np.uint32))
+    assert_json_roundtrip_works(x.astype(np.uint64))
+    assert_json_roundtrip_works(x.astype(np.float32))
+    assert_json_roundtrip_works(x.astype(np.float64))
+    assert_json_roundtrip_works(x.astype(np.complex64))
+    assert_json_roundtrip_works(x.astype(np.complex128))
 
-    assert_roundtrip(np.ones((11, 5)))
-    assert_roundtrip(np.arange(3))
+    assert_json_roundtrip_works(np.ones((11, 5)))
+    assert_json_roundtrip_works(np.arange(3))
 
 
 def test_pandas():
-    assert_roundtrip(
+    assert_json_roundtrip_works(
         pd.DataFrame(data=[[1, 2, 3], [4, 5, 6]],
                      columns=['x', 'y', 'z'],
                      index=[2, 5]))
-    assert_roundtrip(pd.Index([1, 2, 3], name='test'))
-    assert_roundtrip(
+    assert_json_roundtrip_works(pd.Index([1, 2, 3], name='test'))
+    assert_json_roundtrip_works(
         pd.MultiIndex.from_tuples([(1, 2), (3, 4), (5, 6)],
                                   names=['alice', 'bob']))
 
-    assert_roundtrip(
+    assert_json_roundtrip_works(
         pd.DataFrame(index=pd.Index([1, 2, 3], name='test'),
                      data=[[11, 21.0], [12, 22.0], [13, 23.0]],
                      columns=['a', 'b']))
-    assert_roundtrip(
+    assert_json_roundtrip_works(
         pd.DataFrame(index=pd.MultiIndex.from_tuples([(1, 2), (2, 3), (3, 4)],
                                                      names=['x', 'y']),
                      data=[[11, 21.0], [12, 22.0], [13, 23.0]],
@@ -407,23 +387,23 @@ def test_pandas():
 
 def test_sympy():
     # Raw values.
-    assert_roundtrip(sympy.Symbol('theta'))
-    assert_roundtrip(sympy.Integer(5))
-    assert_roundtrip(sympy.Rational(2, 3))
-    assert_roundtrip(sympy.Float(1.1))
+    assert_json_roundtrip_works(sympy.Symbol('theta'))
+    assert_json_roundtrip_works(sympy.Integer(5))
+    assert_json_roundtrip_works(sympy.Rational(2, 3))
+    assert_json_roundtrip_works(sympy.Float(1.1))
 
     # Basic operations.
     s = sympy.Symbol('s')
     t = sympy.Symbol('t')
-    assert_roundtrip(t + s)
-    assert_roundtrip(t * s)
-    assert_roundtrip(t / s)
-    assert_roundtrip(t - s)
-    assert_roundtrip(t**s)
+    assert_json_roundtrip_works(t + s)
+    assert_json_roundtrip_works(t * s)
+    assert_json_roundtrip_works(t / s)
+    assert_json_roundtrip_works(t - s)
+    assert_json_roundtrip_works(t**s)
 
     # Linear combinations.
-    assert_roundtrip(t * 2)
-    assert_roundtrip(4 * t + 3 * s + 2)
+    assert_json_roundtrip_works(t * 2)
+    assert_json_roundtrip_works(4 * t + 3 * s + 2)
 
 
 def _write_test_data(key: str, *test_instances: Any):
diff --git a/cirq/testing/__init__.py b/cirq/testing/__init__.py
--- a/cirq/testing/__init__.py
+++ b/cirq/testing/__init__.py
@@ -51,6 +51,9 @@
 from cirq.testing.equivalent_repr_eval import (
     assert_equivalent_repr,)
 
+from cirq.testing.json import (
+    assert_json_roundtrip_works,)
+
 from cirq.testing.lin_alg_utils import (
     assert_allclose_up_to_global_phase,
     random_orthogonal,
diff --git a/cirq/testing/json.py b/cirq/testing/json.py
new file mode 100644
--- /dev/null
+++ b/cirq/testing/json.py
@@ -0,0 +1,38 @@
+import io
+import numpy as np
+import pandas as pd
+
+import cirq
+
+
+def assert_json_roundtrip_works(obj, text_should_be=None, resolvers=None):
+    """Tests that the given object can serialized and de-serialized
+
+    Args:
+        obj: The object to test round-tripping for.
+        text_should_be: An optional argument to assert the JSON serialized
+            output.
+        resolvers: Any resolvers if testing those other than the default.
+
+    Raises:
+        AssertionError: The given object can not be round-tripped according to
+            the given arguments.
+    """
+    buffer = io.StringIO()
+    cirq.protocols.to_json(obj, buffer)
+
+    if text_should_be is not None:
+        buffer.seek(0)
+        text = buffer.read()
+        assert text == text_should_be
+
+    buffer.seek(0)
+    restored_obj = cirq.protocols.read_json(buffer, resolvers=resolvers)
+    if isinstance(obj, np.ndarray):
+        np.testing.assert_equal(restored_obj, obj)
+    elif isinstance(obj, pd.DataFrame):
+        pd.testing.assert_frame_equal(restored_obj, obj)
+    elif isinstance(obj, pd.Index):
+        pd.testing.assert_index_equal(restored_obj, obj)
+    else:
+        assert restored_obj == obj