Repository: quantumlib/Cirq

Issue:
Implement JSON serialization for PauliSum



Discussion:
@mpharrigan this is going to require serializing frozenset.  Do you have any objections to this?  I'm going to do it in a way that it preserves roundtrip (there is a crazy cool hack you could do which is to pass in the values of the set as keys in a dict with all values None, but figure that's not a good practice :) ) 
Do you mean adding generic support for frozensets in the cirq encoder? Or custom logic in `PauliSum` for turning the keys from `frozenset` to lists and then back to frozenset in `PauliSum`'s deserializer?

If the latter, that's expected although the plumbing is a little intricate in this particular case.

If the former, how do you intend to tag the resulting JSON so it's known to be a frozenset during deserialization?
Oh I was thinking the former, but you are correct that could be a way to handle it.   Was just going to re-use the "cirq_type" with frozenset.  An alternative would be to create a different key "builtin-type".  I think there was some discussion of this problem over in the numpy array serialization discussion. 

Now that I think about it maybe your first suggestion is the better approach.
Let me know how it goes. I'm not opposed to adding support for more basic containers in principle. I think since `_UnitPauliString` is an implementation detail it's not strictly necessary for this use case
I wanted to see what it looked like.  One issue you run into is tuples, which are default serialized to lists.  You can fix this https://stackoverflow.com/questions/15721363/preserve-python-tuples-with-json but not really a great hack.  

Resolving patch:
diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py
--- a/cirq-core/cirq/json_resolver_cache.py
+++ b/cirq-core/cirq/json_resolver_cache.py
@@ -146,6 +146,7 @@ def _symmetricalqidpair(qids):
         'PauliString': cirq.PauliString,
         'PauliStringPhasor': cirq.PauliStringPhasor,
         'PauliStringPhasorGate': cirq.PauliStringPhasorGate,
+        'PauliSum': cirq.PauliSum,
         '_PauliX': cirq.ops.pauli_gates._PauliX,
         '_PauliY': cirq.ops.pauli_gates._PauliY,
         '_PauliZ': cirq.ops.pauli_gates._PauliZ,
diff --git a/cirq-core/cirq/ops/linear_combinations.py b/cirq-core/cirq/ops/linear_combinations.py
--- a/cirq-core/cirq/ops/linear_combinations.py
+++ b/cirq-core/cirq/ops/linear_combinations.py
@@ -498,6 +498,20 @@ def _unitary_(self) -> np.ndarray:
             return m
         raise ValueError(f'{self} is not unitary')
 
+    def _json_dict_(self):
+        def key_json(k: UnitPauliStringT):
+            return [list(e) for e in sorted(k)]
+
+        return {'items': list((key_json(k), v) for k, v in self._linear_dict.items())}
+
+    @classmethod
+    def _from_json_dict_(cls, items, **kwargs):
+        mapping = {
+            frozenset(tuple(qid_pauli) for qid_pauli in unit_pauli_string): val
+            for unit_pauli_string, val in items
+        }
+        return cls(linear_dict=value.LinearDict(mapping))
+
     def expectation_from_state_vector(
         self,
         state_vector: np.ndarray,


Test patch:
diff --git a/cirq-core/cirq/protocols/json_test_data/PauliSum.json b/cirq-core/cirq/protocols/json_test_data/PauliSum.json
new file mode 100644
--- /dev/null
+++ b/cirq-core/cirq/protocols/json_test_data/PauliSum.json
@@ -0,0 +1,67 @@
+{
+  "cirq_type": "PauliSum",
+  "items": [
+    [
+      [
+        [
+          {
+            "cirq_type": "LineQubit",
+            "x": 0
+          },
+          {
+            "cirq_type": "_PauliX",
+            "exponent": 1.0,
+            "global_shift": 0.0
+          }
+        ],
+        [
+          {
+            "cirq_type": "LineQubit",
+            "x": 1
+          },
+          {
+            "cirq_type": "_PauliX",
+            "exponent": 1.0,
+            "global_shift": 0.0
+          }
+        ]
+      ],
+      {
+        "cirq_type": "complex",
+        "real": 1.0,
+        "imag": 0.0
+      }
+    ],
+    [
+      [
+        [
+          {
+            "cirq_type": "LineQubit",
+            "x": 0
+          },
+          {
+            "cirq_type": "_PauliY",
+            "exponent": 1.0,
+            "global_shift": 0.0
+          }
+        ],
+        [
+          {
+            "cirq_type": "LineQubit",
+            "x": 1
+          },
+          {
+            "cirq_type": "_PauliY",
+            "exponent": 1.0,
+            "global_shift": 0.0
+          }
+        ]
+      ],
+      {
+        "cirq_type": "complex",
+        "real": 0.0,
+        "imag": 1.0
+      }
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/cirq-core/cirq/protocols/json_test_data/PauliSum.repr b/cirq-core/cirq/protocols/json_test_data/PauliSum.repr
new file mode 100644
--- /dev/null
+++ b/cirq-core/cirq/protocols/json_test_data/PauliSum.repr
@@ -0,0 +1 @@
+cirq.PauliSum(cirq.LinearDict({frozenset({(cirq.LineQubit(1), cirq.X), (cirq.LineQubit(0), cirq.X)}): (1+0j), frozenset({(cirq.LineQubit(1), cirq.Y), (cirq.LineQubit(0), cirq.Y)}): (1j)}))
\ No newline at end of file
diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py
--- a/cirq-core/cirq/protocols/json_test_data/spec.py
+++ b/cirq-core/cirq/protocols/json_test_data/spec.py
@@ -47,7 +47,6 @@
         'DiagonalGate',
         'NeutralAtomDevice',
         'PauliInteractionGate',
-        'PauliSum',
         'PauliSumCollector',
         'PauliSumExponential',
         'PauliTransform',