Skip to content

Add JSON serialisations #7396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from

Conversation

WingCode
Copy link
Contributor

closes #3846

@WingCode WingCode requested review from wcourtney, vtomole, verult and a team as code owners May 30, 2025 17:34
@github-actions github-actions bot added the size: M 50< lines changed <250 label May 30, 2025
@github-actions github-actions bot added size: L 250< lines changed <1000 and removed size: M 50< lines changed <250 labels Jun 1, 2025
@pavoljuhas
Copy link
Collaborator

@WingCode - thank you for working on this. The failing CI checks should give some pointers on what is amiss. Feel free to ask here if you need clarification.

Copy link

codecov bot commented Jun 7, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.69%. Comparing base (9d511e9) to head (4bcbb8b).
Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #7396    +/-   ##
========================================
  Coverage   98.69%   98.69%            
========================================
  Files        1112     1114     +2     
  Lines       97937    98097   +160     
========================================
+ Hits        96659    96819   +160     
  Misses       1278     1278            

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@WingCode
Copy link
Contributor Author

WingCode commented Jun 7, 2025

@pavoljuhas I have fixed the CI checks.

@@ -128,7 +178,18 @@ def noisy_moment(self, moment: cirq.Moment, system_qubits: Sequence[cirq.Qid]):
return output if self._prepend else output[::-1]
return moment

def _json_dict_(self) -> dict[str, object]:
return {'readout_decay_gate': self.readout_decay_gate, 'prepend': self._prepend}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead serialize the decay probability instead of the gate, similar to above? The gate is somewhat of an internal detail.

@@ -54,7 +65,18 @@ def noisy_moment(self, moment: cirq.Moment, system_qubits: Sequence[cirq.Qid]):
]
return output[::-1] if self._prepend else output

def _json_dict_(self) -> dict[str, object]:
return {'qubit_noise_gate': self.qubit_noise_gate, 'prepend': self._prepend}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should serialize the probability rather than the gate.
(Ok to add that as a member variable if it makes it easier.

Same with other gates below.

Copy link
Collaborator

@pavoljuhas pavoljuhas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for serialization of contrib classes should be moved under cirq.contrib to limit any dependency of non-contrib cirq on cirq.contrib. Please see inline comments for other suggestions.

Comment on lines 155 to 157
# Avoid importing the optional contrib module at import time which would
# require optional dependencies. Instead, import noise models lazily
# when the resolver dictionary is created.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The classes below should be moved to contrib_class_resolver. They can use standard imports there without triggering the CI check that prohibits use of cirq.contrib from non-contrib cirq.

Also, the cirq.contrib module should register its resolver on import in a similar way as cirq_google below:

# Register cirq_google's public classes for JSON serialization.
from cirq.protocols.json_serialization import _register_resolver
from cirq_google.json_resolver_cache import _class_resolver_dictionary
_register_resolver(_class_resolver_dictionary)

@@ -0,0 +1,8 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the contrib classes we should add cirq-core/cirq/contrib/json_test_data/ directory in a similar fashion as for cirq-google/cirq_google/json_test_data. The json and repr files for contrib classes should be located there.

Also the TESTED_MODULES dictionary below should be updated with 'cirq.contrib' entry so that the test code would look for files in .../contrib/json_test_data.
(the key can be entered as f'cirq{"."}contrib' to skip the check for cirq.contrib)

TESTED_MODULES: dict[str, _ModuleDeprecation | None] = {
'cirq_aqt': None,
'cirq_ionq': None,
'cirq_google': None,
'cirq_pasqal': None,
'cirq_rigetti': None,
'cirq.protocols': None,
'non_existent_should_be_fine': None,
}

'Simulator',
'StabilizerSampler',
'DEFAULT_RESOLVERS',
'NoiseModelFromNoiseProperties',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'NoiseModelFromNoiseProperties' should be removed from the should_not_be_serialized list if this PR can support serialization of NoiseModelFromNoiseProperties. If that is too complicated, it is OK to limit the scope here to other NoiseModel classes only and leave NoiseModelFromNoiseProperties for later.

Comment on lines 72 to 74
tuple(sorted((q, p) for q, p in (self.depol_probs or {}).items())),
tuple(sorted((q, p) for q, p in (self.bitflip_probs or {}).items())),
tuple(sorted((q, p) for q, p in (self.decay_probs or {}).items())),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those dictionaries have simple float values. Would it work with just using them as are?

Comment on lines 126 to 129
obj = cls.__new__(cls)
obj.depol_probs = dict(depol_probs)
obj.bitflip_probs = dict(bitflip_probs)
obj.decay_probs = dict(decay_probs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace cls.__new__ with standard object construction cls(...) here and in other files. If _json_dict_ above returns all __init__ arguments, this method should be unnecessary and removed.

return {
'depol_probs': tuple((q, p) for q, p in (self.depol_probs or {}).items()),
'bitflip_probs': tuple((q, p) for q, p in (self.bitflip_probs or {}).items()),
'decay_probs': tuple((q, p) for q, p in (self.decay_probs or {}).items()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will it work with returning the dictionaries as they are?
Also, consider using obj_to_dict_helper here.

Comment on lines 325 to 332
def test_per_qubit_noise_model_repr_and_json():
q0 = cirq.GridQubit(0, 0)
model = cirq_google.experimental.noise_models.PerQubitDepolarizingWithDampedReadoutNoiseModel(
depol_probs={q0: 0.1}, bitflip_probs={q0: 0.2}, decay_probs={q0: 0.3}
)
cirq.testing.assert_equivalent_repr(model, setup_code='import cirq\nimport cirq_google')
restored = type(model)._from_json_dict_(**model._json_dict_())
assert restored == model
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be unnecessary after adding PerQubitDepolarizingWithDampedReadoutNoiseModel.json and PerQubitDepolarizingWithDampedReadoutNoiseModel.repr to cirq_google/json_test_data.

@@ -24,3 +24,4 @@
from cirq.contrib.qcircuit import circuit_to_latex_using_qcircuit as circuit_to_latex_using_qcircuit
from cirq.contrib import json # noqa: F401
from cirq.contrib.circuitdag import CircuitDag as CircuitDag, Unique as Unique
from cirq.contrib import noise_models as noise_models
Copy link
Collaborator

@pavoljuhas pavoljuhas Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to avoid adding new import here. If cirq.contrib registers its class resolver here, the import of noise_models can be deferred to the resolver execution.

"cirq.devices.ThermalNoiseModel("
f"qubits={set(self.rate_matrix_GHz.keys())!r}, "
f"gate_durations_ns={self.gate_durations_ns!r}, "
f"heat_rate_GHz=None, cool_rate_GHz=None, dephase_rate_GHz=None, "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops the attributes' values, please fix.

@@ -342,3 +342,19 @@ def test_noisy_moment_two_qubit():
[9.87330937e-01, 0, 0, 9.95013725e-01],
],
)


def test_thermal_noise_model_repr_and_json():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test (and other similar ones) should be replaced by adding ThermalNoiseModel.json and ThermalNoiseModel.repr files to json_test_data. Please check instructions in https://github.com/quantumlib/Cirq/blob/main/docs/dev/serialization.md.

@mhucka
Copy link
Contributor

mhucka commented Jun 11, 2025

FYI that @pavoljuhas is out on jury duty (a mandatory civic obligation in the USA) starting today and possibly more days (depending on unpredictable factors). If he had commented on the PR or this issue before and hasn't responded, that's why.

@pavoljuhas
Copy link
Collaborator

pavoljuhas commented Jun 16, 2025

Please consider setting up a local python environment for development per https://github.com/quantumlib/Cirq/blob/main/docs/dev/development.md#setting-up-an-environment and testing the code on your side first. A full test suite takes a while to run, but you can get fast results using either check/pytest-changed-files or, since you work on serialization, by running

check/pytest -n0 cirq-core/cirq/protocols/json_serialization_test.py

You can also consult the CI logs to find what tests are failing and specifically running those.

@WingCode
Copy link
Contributor Author

Please consider setting up a local python environment for development per https://github.com/quantumlib/Cirq/blob/main/docs/dev/development.md#setting-up-an-environment and testing the code on your side first. A full test suite takes a while to run, but you can get fast results using either check/pytest-changed-files or, since you work on serialization, by running

check/pytest -n0 cirq-core/cirq/protocols/json_serialization_test.py

You can also consult the CI logs to find what tests are failing and specifically running those.

Thank you for the helpful tip, and apologies for the oversight. I just realized that I was mistakenly pushing fixes to the wrong branch, which ended up triggering the CI unnecessarily. I’ll make sure to be more careful going forward.

@WingCode WingCode requested a review from pavoljuhas June 16, 2025 21:08
@mhucka mhucka assigned mhucka and WingCode and unassigned mhucka Jun 17, 2025
Copy link
Collaborator

@pavoljuhas pavoljuhas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not got through all files, but I think we can address the new comments before next iteration.

@@ -0,0 +1,35 @@
# pylint: disable=wrong-or-nonexistent-copyright-notice
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace with the actual copyright statement as in __init__.py.

Comment on lines +16 to +18
"QuantumVolumeResult",
"SwapPermutationGate",
"BayesianNetworkGate",
Copy link
Collaborator

@pavoljuhas pavoljuhas Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first 3 classes are serializable and should NOT be listed here.

Please add the corresponding .repr and .json files instead.

After that you can replace the dictionary filtering below with

resolver_cache=_class_resolver_dictionary(),

Comment on lines +72 to +74
@classmethod
def _from_json_dict_(cls, depol_prob, prepend, **kwargs):
return cls(depol_prob, prepend=prepend)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When _json_dict_ provides all __init__ arguments, the _from_json_dict_ method is redundant. Please remove here and in other similar instances in this PR.

Suggested change
@classmethod
def _from_json_dict_(cls, depol_prob, prepend, **kwargs):
return cls(depol_prob, prepend=prepend)

self.require_physical_tag: bool = require_physical_tag
self.skip_measurements: bool = skip_measurements
self._prepend = prepend

def _value_equality_values_(self):
gate_times = tuple(sorted((k, v) for k, v in self.gate_durations_ns.items()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gate_durations_ns can be used as is, no need for conversion here.

gate_times = {cirq.json_cirq_type(k): v for k, v in self.gate_durations_ns.items()}
return {
'gate_durations_ns': tuple(gate_times.items()),
'rate_matrix_GHz': tuple((q, m.tolist()) for q, m in self.rate_matrix_GHz.items()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rate_matrix_GHz has derived values which should not be stored. Please use it only to recover the qubits for class init, which do not seem to be stored anywhere else. Otherwise, just store here only the init arguments with a minimum conversions needed.

def _json_dict_(self) -> dict[str, object]:
gate_times = {cirq.json_cirq_type(k): v for k, v in self.gate_durations_ns.items()}
return {
'gate_durations_ns': tuple(gate_times.items()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to convert to tuple, please store as a dictionary (which are native to json).

prepend,
**kwargs,
):
obj = cls.__new__(cls)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace with standard cls(args) call. Introduction of cls.__new__ should have a very strong justification, otherwise it is often a sign the code needs another look.

Comment on lines +360 to +361
json_text = cirq.to_json(model)
assert cirq.read_json(json_text=json_text) == model
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates implicit testing that happens with addition of ThermalNoiseModel.json and ThermalNoiseModel.repr files. Please delete here and also for other instances where .json and .repr files exist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size: L 250< lines changed <1000 unitaryHACK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NoiseModels are not serializable
4 participants