diff --git a/cirq-google/cirq_google/__init__.py b/cirq-google/cirq_google/__init__.py index 295dcfdd874..a18134f7f14 100644 --- a/cirq-google/cirq_google/__init__.py +++ b/cirq-google/cirq_google/__init__.py @@ -66,6 +66,7 @@ LZSResetViaResonator as LZSResetViaResonator, LeakageISWAP as LeakageISWAP, MultilevelResetViaResonator as MultilevelResetViaResonator, + MultiStepMultiLevelReset as MultiStepMultiLevelReset, NoSyncTag as NoSyncTag, PhysicalZTag as PhysicalZTag, SYC as SYC, diff --git a/cirq-google/cirq_google/json_resolver_cache.py b/cirq-google/cirq_google/json_resolver_cache.py index e3f2e36704d..6e48ea01b9d 100644 --- a/cirq-google/cirq_google/json_resolver_cache.py +++ b/cirq-google/cirq_google/json_resolver_cache.py @@ -62,6 +62,7 @@ def _old_xmon(*args, **kwargs): 'LZSResetViaResonator': cirq_google.LZSResetViaResonator, 'LeakageISWAP': cirq_google.LeakageISWAP, 'MultilevelResetViaResonator': cirq_google.MultilevelResetViaResonator, + 'MultiStepMultiLevelReset': cirq_google.MultiStepMultiLevelReset, # cirq_google.GateTabulation has been removed and replaced by cirq.TwoQubitGateTabulation. 'GateTabulation': TwoQubitGateTabulation, 'PhysicalZTag': cirq_google.PhysicalZTag, diff --git a/cirq-google/cirq_google/json_test_data/MultiStepMultiLevelReset.json b/cirq-google/cirq_google/json_test_data/MultiStepMultiLevelReset.json new file mode 100644 index 00000000000..ee6cbbbe740 --- /dev/null +++ b/cirq-google/cirq_google/json_test_data/MultiStepMultiLevelReset.json @@ -0,0 +1,16 @@ +{ + "cirq_type": "MultiStepMultiLevelReset", + "f_start": null, + "already_at_readout_detuning": null, + "f_end": null, + "end_at_idle": null, + "lengths": null, + "f_swaps_delta": null, + "gs": null, + "padding_before": null, + "padding_after": null, + "detune_to_start_freq": null, + "start_at_readout_detuning": null, + "coupler_amplitudes": null, + "compensate_coupled_qubit": null +} diff --git a/cirq-google/cirq_google/json_test_data/MultiStepMultiLevelReset.repr b/cirq-google/cirq_google/json_test_data/MultiStepMultiLevelReset.repr new file mode 100644 index 00000000000..06e19edd9c3 --- /dev/null +++ b/cirq-google/cirq_google/json_test_data/MultiStepMultiLevelReset.repr @@ -0,0 +1 @@ +cirq_google.MultiStepMultiLevelReset() diff --git a/cirq-google/cirq_google/ops/__init__.py b/cirq-google/cirq_google/ops/__init__.py index e8686ae39ed..6f301ceb057 100644 --- a/cirq-google/cirq_google/ops/__init__.py +++ b/cirq-google/cirq_google/ops/__init__.py @@ -54,4 +54,8 @@ MultilevelResetViaResonator as MultilevelResetViaResonator, ) +from cirq_google.ops.multi_step_multi_level_reset import ( + MultiStepMultiLevelReset as MultiStepMultiLevelReset, +) + from cirq_google.ops.leakage_iswap import LeakageISWAP as LeakageISWAP diff --git a/cirq-google/cirq_google/ops/multi_step_multi_level_reset.py b/cirq-google/cirq_google/ops/multi_step_multi_level_reset.py new file mode 100644 index 00000000000..d26788124de --- /dev/null +++ b/cirq-google/cirq_google/ops/multi_step_multi_level_reset.py @@ -0,0 +1,173 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Multi-step multi-level reset gate. + +This module defines `MultiStepMultiLevelReset`, an active reset gate for +superconducting qubits that resets the qubit to its ground state by iteratively +swapping excited-state populations into a fast-decaying reset resonator across +multiple frequency steps. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +import attrs +import cirq +from cirq_google.study import symbol_util as su + + +@attrs.frozen(eq=False, hash=False) +class MultiStepMultiLevelReset(cirq.Gate): + """Multi-step multi-level reset gate. + + This gate actively resets a superconducting qubit to the ground state by + iteratively swapping excited populations (including higher levels like |2> + and |1>) into an attached reset resonator across multiple frequency steps. + Each step specifies a duration (`lengths`), a swap frequency detuning relative + to the reset resonator frequency (`f_swaps_delta`), and a coupling strength + (`gs`). + + Parameters that are not specified (set to `None`) are populated from calibration + or device defaults at scheduling/runtime. + + Attributes: + f_start: Starting frequency of the reset trajectory. + already_at_readout_detuning: If True, indicates that the qubit is already + at the readout detuning frequency prior to this gate (e.g. immediately + following readout), rather than at idle. This is used to compute the + step pulse amplitude needed to transition into `f_start`. + f_end: Ending frequency of the reset trajectory. + end_at_idle: If True, the qubit returns to idle frequency at the end of + the gate, ignoring `f_end`. If False, the trajectory ends at `f_end` + and an additional step pulse is applied to return to idle. + lengths: Sequence of durations for each reset step. + f_swaps_delta: Sequence of swap detuning frequencies for each step, + measured relative to the reset resonator frequency. + gs: Sequence of coupling strengths for each reset step. + padding_before: Padding duration before the reset trajectory begins. + padding_after: Padding duration after the reset trajectory ends. + detune_to_start_freq: If True, include a detune step to bring the qubit + to `f_start`. If False, assume the qubit is already at `f_start`. + start_at_readout_detuning: If True, begins the reset trajectory at the + qubit's readout detuning frequency, overriding `f_start`. This differs + from `already_at_readout_detuning`, which specifies the qubit's initial + state before the gate starts. + coupler_amplitudes: Optional map of coupler names to detune amplitudes + for couplers connected to this qubit during reset. + compensate_coupled_qubit: If True, compensates for coupler detuning by + applying a compensation detune to the other qubit connected to each + coupler. + """ + + f_start: su.ValueOrSymbol | None = None + already_at_readout_detuning: bool | None = None + f_end: su.ValueOrSymbol | None = None + end_at_idle: bool | None = None + lengths: Sequence[su.ValueOrSymbol] | None = None + f_swaps_delta: Sequence[su.ValueOrSymbol] | None = None + gs: Sequence[su.ValueOrSymbol] | None = None + padding_before: su.ValueOrSymbol | None = None + padding_after: su.ValueOrSymbol | None = None + detune_to_start_freq: bool | None = None + start_at_readout_detuning: bool | None = None + coupler_amplitudes: dict[str, cirq.TParamVal] | None = None + compensate_coupled_qubit: bool | None = None + + def _num_qubits_(self) -> int: + return 1 + + def is_reset_gate(self) -> bool: + return True + + def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> list[str]: + return ["[R (MSML)]"] + + def _decompose_(self, qubits: Sequence[cirq.Qid]) -> list[cirq.Operation]: + return list(cirq.reset_each(*qubits)) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, MultiStepMultiLevelReset): + return NotImplemented + return ( + self.f_start == other.f_start + and self.already_at_readout_detuning == other.already_at_readout_detuning + and self.f_end == other.f_end + and self.end_at_idle == other.end_at_idle + and (tuple(self.lengths) if self.lengths is not None else None) + == (tuple(other.lengths) if other.lengths is not None else None) + and (tuple(self.f_swaps_delta) if self.f_swaps_delta is not None else None) + == (tuple(other.f_swaps_delta) if other.f_swaps_delta is not None else None) + and (tuple(self.gs) if self.gs is not None else None) + == (tuple(other.gs) if other.gs is not None else None) + and self.padding_before == other.padding_before + and self.padding_after == other.padding_after + and self.detune_to_start_freq == other.detune_to_start_freq + and self.start_at_readout_detuning == other.start_at_readout_detuning + and self.coupler_amplitudes == other.coupler_amplitudes + and self.compensate_coupled_qubit == other.compensate_coupled_qubit + ) + + def __hash__(self) -> int: + return hash( + ( + self.f_start, + self.already_at_readout_detuning, + self.f_end, + self.end_at_idle, + tuple(self.lengths) if self.lengths is not None else None, + tuple(self.f_swaps_delta) if self.f_swaps_delta is not None else None, + tuple(self.gs) if self.gs is not None else None, + self.padding_before, + self.padding_after, + self.detune_to_start_freq, + self.start_at_readout_detuning, + ( + tuple(sorted(self.coupler_amplitudes.items())) + if self.coupler_amplitudes is not None + else None + ), + self.compensate_coupled_qubit, + ) + ) + + def __repr__(self) -> str: + args = [] + for field in attrs.fields(type(self)): + val = getattr(self, field.name) + if val is not None: + args.append(f'{field.name}={val!r}') + return f"cirq_google.MultiStepMultiLevelReset({', '.join(args)})" + + def _json_dict_(self) -> dict[str, Any]: + return cirq.obj_to_dict_helper( + self, + [ + 'f_start', + 'already_at_readout_detuning', + 'f_end', + 'end_at_idle', + 'lengths', + 'f_swaps_delta', + 'gs', + 'padding_before', + 'padding_after', + 'detune_to_start_freq', + 'start_at_readout_detuning', + 'coupler_amplitudes', + 'compensate_coupled_qubit', + ], + ) diff --git a/cirq-google/cirq_google/ops/multi_step_multi_level_reset_test.py b/cirq-google/cirq_google/ops/multi_step_multi_level_reset_test.py new file mode 100644 index 00000000000..cb7268441ed --- /dev/null +++ b/cirq-google/cirq_google/ops/multi_step_multi_level_reset_test.py @@ -0,0 +1,254 @@ +# Copyright 2026 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import tunits as tu + +import cirq +import cirq_google as cg +import sympy +from cirq_google.api import v2 +from cirq_google.ops.multi_step_multi_level_reset import MultiStepMultiLevelReset + + +def test_multi_step_multi_level_reset_properties(): + gate = MultiStepMultiLevelReset() + assert cirq.num_qubits(gate) == 1 + assert gate.is_reset_gate() + assert not cirq.has_unitary(gate) + + +def test_circuit_diagram(): + q = cirq.GridQubit(0, 0) + op = MultiStepMultiLevelReset()(q) + circuit = cirq.Circuit(op) + cirq.testing.assert_has_diagram( + circuit, + """ +(0, 0): ───[R (MSML)]─── +""", + ) + + +def test_decomposition(): + q = cirq.GridQubit(0, 0) + op = MultiStepMultiLevelReset()(q) + decomp = cirq.decompose(op) + assert decomp == [cirq.ResetChannel()(q)] + + +def test_equality_and_hashing(): + gate1 = MultiStepMultiLevelReset( + f_start=2.5 * tu.GHz, + coupler_amplitudes={"coupler_A": 0.5}, + lengths=(10 * tu.ns, 12 * tu.ns), + ) + gate2 = MultiStepMultiLevelReset( + f_start=2.5 * tu.GHz, + coupler_amplitudes={"coupler_A": 0.5}, + lengths=[10 * tu.ns, 12 * tu.ns], + ) + gate3 = MultiStepMultiLevelReset( + f_start=3.0 * tu.GHz, + coupler_amplitudes={"coupler_A": 0.5}, + lengths=(10 * tu.ns, 12 * tu.ns), + ) + + assert gate1 == gate2 + assert hash(gate1) == hash(gate2) + assert gate1 != gate3 + assert gate1 != "other" + + # Verify gate can be used in set and in cirq.Moment + q = cirq.GridQubit(0, 0) + moment = cirq.Moment(gate1(q)) + assert moment == cirq.Moment(gate2(q)) + assert {gate1, gate2} == {gate1} + + +def test_repr(): + gate = MultiStepMultiLevelReset() + assert repr(gate) == 'cirq_google.MultiStepMultiLevelReset()' + + gate_with_args = MultiStepMultiLevelReset( + f_start=2.5 * tu.GHz, already_at_readout_detuning=False, coupler_amplitudes={"c": 0.5} + ) + assert repr(gate_with_args) == ( + f"cirq_google.MultiStepMultiLevelReset(f_start={2.5 * tu.GHz!r}, " + "already_at_readout_detuning=False, coupler_amplitudes={'c': 0.5})" + ) + + +def test_empty_serialization_round_trip(): + q = cirq.GridQubit(0, 0) + gate = MultiStepMultiLevelReset() + op = gate(q) + circuit = cirq.Circuit(op) + + # Serialize + proto = cg.CIRCUIT_SERIALIZER.serialize(circuit) + + # Verify proto structure + op_protos = [c.operation_value for c in proto.constants if c.HasField('operation_value')] + assert len(op_protos) == 1 + op_proto = op_protos[0] + + assert op_proto.WhichOneof('gate_value') == 'resetgate' + gate_proto = op_proto.resetgate + assert gate_proto.reset_type == "MultiStepMultiLevelReset" + assert len(gate_proto.arguments) == 0 + + # Deserialize + deserialized_circuit = cg.CIRCUIT_SERIALIZER.deserialize(proto) + assert deserialized_circuit == circuit + + deserialized_op = next(iter(deserialized_circuit.all_operations())) + assert isinstance(deserialized_op.gate, MultiStepMultiLevelReset) + assert deserialized_op.gate == gate + + +def _proto_for_unit(value: float, unit: tu.Value) -> v2.program_pb2.Arg: + proto = v2.program_pb2.Arg() + proto.arg_value.value_with_unit.MergeFrom((value * unit).to_proto()) + return proto + + +def test_full_serialization_round_trip(): + q = cirq.GridQubit(0, 0) + gate = MultiStepMultiLevelReset( + f_start=2.5 * tu.GHz, + already_at_readout_detuning=False, + f_end=3.0 * tu.GHz, + end_at_idle=True, + lengths=(10 * tu.ns, 12 * tu.ns), + f_swaps_delta=(0.1 * tu.GHz, -0.1 * tu.GHz), + gs=(10 * tu.MHz, 20 * tu.MHz), + padding_before=5 * tu.ns, + padding_after=10 * tu.ns, + detune_to_start_freq=True, + start_at_readout_detuning=False, + coupler_amplitudes={"coupler_A": 0.5}, + compensate_coupled_qubit=True, + ) + op = gate(q) + circuit = cirq.Circuit(op) + + # Serialize + proto = cg.CIRCUIT_SERIALIZER.serialize(circuit) + + # Verify proto structure matches the pyle format exactly + op_protos = [c.operation_value for c in proto.constants if c.HasField('operation_value')] + assert len(op_protos) == 1 + op_proto = op_protos[0] + + assert op_proto.WhichOneof('gate_value') == 'resetgate' + gate_proto = op_proto.resetgate + assert gate_proto.reset_type == "MultiStepMultiLevelReset" + + expected_args = { + "f_start": _proto_for_unit(2.5, tu.GHz), + "already_at_readout_detuning": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue(bool_value=False) + ), + "f_end": _proto_for_unit(3.0, tu.GHz), + "end_at_idle": v2.program_pb2.Arg(arg_value=v2.program_pb2.ArgValue(bool_value=True)), + "lengths": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue( + tuple_value=v2.program_pb2.Tuple( + sequence_type=v2.program_pb2.Tuple.SequenceType.TUPLE, + values=[_proto_for_unit(10, tu.ns), _proto_for_unit(12, tu.ns)], + ) + ) + ), + "f_swaps_delta": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue( + tuple_value=v2.program_pb2.Tuple( + sequence_type=v2.program_pb2.Tuple.SequenceType.TUPLE, + values=[_proto_for_unit(0.1, tu.GHz), _proto_for_unit(-0.1, tu.GHz)], + ) + ) + ), + "gs": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue( + tuple_value=v2.program_pb2.Tuple( + sequence_type=v2.program_pb2.Tuple.SequenceType.TUPLE, + values=[_proto_for_unit(10, tu.MHz), _proto_for_unit(20, tu.MHz)], + ) + ) + ), + "padding_before": _proto_for_unit(5, tu.ns), + "padding_after": _proto_for_unit(10, tu.ns), + "detune_to_start_freq": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue(bool_value=True) + ), + "start_at_readout_detuning": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue(bool_value=False) + ), + "coupler_amplitudes": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue(string_value='__JSON_DICT__:{"coupler_A": 0.5}') + ), + "compensate_coupled_qubit": v2.program_pb2.Arg( + arg_value=v2.program_pb2.ArgValue(bool_value=True) + ), + } + + assert gate_proto.arguments == expected_args + + # Deserialize + deserialized_circuit = cg.CIRCUIT_SERIALIZER.deserialize(proto) + assert deserialized_circuit == circuit + + deserialized_op = next(iter(deserialized_circuit.all_operations())) + assert isinstance(deserialized_op.gate, MultiStepMultiLevelReset) + assert deserialized_op.gate == gate + + +def test_internal_gate_deserialization_fallback(): + # Verify backward-compatibility deserialization if serialized as internalgate + op_proto = v2.program_pb2.Operation() + op_proto.qubit_constant_index.append(0) + op_proto.internalgate.name = "MultiStepMultiLevelReset" + op_proto.internalgate.gate_args["already_at_readout_detuning"].arg_value.bool_value = True + op_proto.internalgate.gate_args["coupler_amplitudes"].arg_value.string_value = ( + '__JSON_DICT__:{"c1": 0.25}' + ) + + program_proto = v2.program_pb2.Program() + program_proto.constants.add(qubit=v2.program_pb2.Qubit(id="0_0")) + program_proto.constants.add(operation_value=op_proto) + moment = program_proto.circuit.moments.add() + moment.operation_indices.append(1) + + circuit = cg.CIRCUIT_SERIALIZER.deserialize(program_proto) + deserialized_op = next(iter(circuit.all_operations())) + assert isinstance(deserialized_op.gate, MultiStepMultiLevelReset) + assert deserialized_op.gate.already_at_readout_detuning is True + assert deserialized_op.gate.coupler_amplitudes == {"c1": 0.25} + + +def test_json_serialization(): + # Empty gate + gate = MultiStepMultiLevelReset() + json_text = cirq.to_json(gate) + deserialized = cirq.read_json(json_text=json_text) + assert deserialized == gate + + # Gate with symbols/primitives + gate2 = MultiStepMultiLevelReset( + f_start=sympy.Symbol("f_start"), + already_at_readout_detuning=False, + coupler_amplitudes={"coupler_A": 0.5}, + ) + json_text2 = cirq.to_json(gate2) + deserialized2 = cirq.read_json(json_text=json_text2) + assert deserialized2 == gate2 diff --git a/cirq-google/cirq_google/serialization/circuit_serializer.py b/cirq-google/cirq_google/serialization/circuit_serializer.py index 45982ef4882..fe293ef2788 100644 --- a/cirq-google/cirq_google/serialization/circuit_serializer.py +++ b/cirq-google/cirq_google/serialization/circuit_serializer.py @@ -18,13 +18,14 @@ import functools import inspect +import json import warnings from collections.abc import Callable, Hashable, Mapping, Sequence from typing import Any -import sympy - +import attrs import cirq +import sympy from cirq_google.api import v2 from cirq_google.experimental.ops import CouplerPulse from cirq_google.ops import ( @@ -38,6 +39,7 @@ LeakageISWAP, LZSResetViaResonator, MultilevelResetViaResonator, + MultiStepMultiLevelReset, NoSyncTag, PhysicalZTag, SycamoreGate, @@ -63,6 +65,27 @@ _SERIALIZER_NAME = 'v2_5' +def _serialize_value(value: Any, out_msg: v2.program_pb2.Arg) -> None: + # Note: `v2.program_pb2.Arg` (and `ArgValue`) does not have a map/dict field in + # the protobuf schema, only primitive types, tunits.Value, and tuples/ndarrays. + # To represent dictionary arguments such as `coupler_amplitudes` without + # protobuf schema changes and to stay wire-compatible with pyle + # (see pyle.cirqtools.proto_serialization._serialize_value), + # dictionaries are serialized as JSON strings with the '__JSON_DICT__:' prefix. + if isinstance(value, dict): + val = "__JSON_DICT__:" + json.dumps(value) + else: + val = value + arg_func_langs.arg_to_proto(val, out=out_msg) + + +def _deserialize_value(arg_msg: v2.program_pb2.Arg) -> Any: + value = arg_func_langs.arg_from_proto(arg_msg) + if isinstance(value, str) and value.startswith("__JSON_DICT__:"): + return json.loads(value[len("__JSON_DICT__:") :]) + return value + + class CircuitSerializer(serializer.Serializer): """A class for serializing and deserializing programs and operations. @@ -398,6 +421,12 @@ def _serialize_gate_op( arg_func_langs.arg_to_proto(gate.dimension, out=msg.resetgate.arguments['dimension']) elif isinstance(gate, (MultilevelResetViaResonator, LZSResetViaResonator)): msg.resetgate.reset_type = type(gate).__name__ + elif isinstance(gate, MultiStepMultiLevelReset): + msg.resetgate.reset_type = type(gate).__name__ + gate_args = msg.resetgate.arguments + for arg in attrs.fields(type(gate)): + if (value := getattr(gate, arg.name, None)) is not None: + _serialize_value(value, gate_args[arg.name]) elif isinstance(gate, CouplerPulse): arg_func_langs.float_arg_to_proto( gate.hold_time.total_picos(), out=msg.couplerpulsegate.hold_time_ps @@ -927,6 +956,14 @@ def _deserialize_gate_op( op = LZSResetViaResonator()(*qubits) case "MultilevelResetViaResonator": op = MultilevelResetViaResonator()(*qubits) + case "MultiStepMultiLevelReset": + gate_args = operation_proto.resetgate.arguments + kwargs = {} + for arg in attrs.fields(MultiStepMultiLevelReset): + if arg.name not in gate_args: + continue + kwargs[arg.name] = _deserialize_value(gate_args[arg.name]) + op = MultiStepMultiLevelReset(**kwargs)(*qubits) case _: op = cirq.ResetChannel(dimension=dimensions)(*qubits) elif which_gate_type == 'internalgate': @@ -939,6 +976,14 @@ def _deserialize_gate_op( case "MultilevelResetViaResonator": # Can be removed once resetgate deployed (about 9/2026) gate = MultilevelResetViaResonator() + case "MultiStepMultiLevelReset": + gate_args = msg.gate_args + kwargs = {} + for arg in attrs.fields(MultiStepMultiLevelReset): + if arg.name not in gate_args: + continue + kwargs[arg.name] = _deserialize_value(gate_args[arg.name]) + gate = MultiStepMultiLevelReset(**kwargs) case "LeakageISWAPPhaseMatched": gate = LeakageISWAP(phase_matched=True) case "LeakageISWAPUnmatched":