Skip to content
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

Implement dynamical decoupling. #6515

Merged
merged 7 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@

from cirq.transformers import (
AbstractInitialMapper,
add_dynamical_decoupling,
DynamicalDecouplingModel,
align_left,
align_right,
CompilationTargetGateset,
Expand Down
1 change: 1 addition & 0 deletions cirq-core/cirq/json_resolver_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def _symmetricalqidpair(qids):
import sympy

return {
'DynamicalDecouplingModel': cirq.DynamicalDecouplingModel,
'AmplitudeDampingChannel': cirq.AmplitudeDampingChannel,
'AnyIntegerPowerGateFamily': cirq.AnyIntegerPowerGateFamily,
'AnyUnitaryGateFamily': cirq.AnyUnitaryGateFamily,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
{
"cirq_type": "DynamicalDecouplingModel",
"schema": "XX_PAIR"
},
{
"cirq_type": "DynamicalDecouplingModel",
"base_dd_sequence": [
{
"cirq_type": "_PauliX",
"exponent": 1.0,
"global_shift": 0.0
},
{
"cirq_type": "_PauliX",
"exponent": 1.0,
"global_shift": 0.0
}
]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[cirq.DynamicalDecouplingModel.from_schema("XX_PAIR"),
cirq.DynamicalDecouplingModel.from_base_dd_sequence(base_dd_sequence=[cirq.X, cirq.X])]
5 changes: 5 additions & 0 deletions cirq-core/cirq/transformers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@

from cirq.transformers.drop_negligible_operations import drop_negligible_operations

from cirq.transformers.dynamical_decoupling import (
add_dynamical_decoupling,
DynamicalDecouplingModel,
)

from cirq.transformers.eject_z import eject_z

from cirq.transformers.measurement_transformers import (
Expand Down
161 changes: 161 additions & 0 deletions cirq-core/cirq/transformers/dynamical_decoupling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Copyright 2024 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.

"""Transformer pass that adds dynamical decoupling operations to a circuit."""

import enum
from functools import reduce
from typing import Any, Dict, Optional, Tuple

from cirq.transformers import transformer_api
import cirq
from cirq import value
import numpy as np


@enum.unique
class _DynamicalDecouplingSchema(enum.Enum):
"""Supported schemes of dynamical decoupling."""

XX_PAIR = 'XX_PAIR'
Copy link
Collaborator

Choose a reason for hiding this comment

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

We may also want one that is does X X**-1. X and X**-1 have the same unitary but are implemented differently on hardware. Similarly for Y.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

X_XINV = 'X_XINV'
YY_PAIR = 'YY_PAIR'
Y_YINV = 'Y_YINV'


def _repeat_sequence(base_sequence: list['cirq.Gate'], num_idle_moments: int):
repeat_times = num_idle_moments // len(base_sequence)
return base_sequence * repeat_times


def _generate_dd_sequence_from_schema(
schema: _DynamicalDecouplingSchema, num_idle_moments: int = 2
) -> list['cirq.Gate']:
match schema:
case _DynamicalDecouplingSchema.XX_PAIR:
return _repeat_sequence([cirq.X, cirq.X], num_idle_moments)
case _DynamicalDecouplingSchema.X_XINV:
return _repeat_sequence([cirq.X, cirq.X**-1], num_idle_moments)
case _DynamicalDecouplingSchema.YY_PAIR:
return _repeat_sequence([cirq.Y, cirq.Y], num_idle_moments)
case _DynamicalDecouplingSchema.Y_YINV:
return _repeat_sequence([cirq.Y, cirq.Y**-1], num_idle_moments)


def _validate_dd_sequence(dd_sequence: list['cirq.Gate']) -> None:
if len(dd_sequence) < 2:
raise ValueError('Invalid dynamical decoupling sequence. Expect more than one gates.')
matrices = [cirq.unitary(gate) for gate in dd_sequence]
product = reduce(np.matmul, matrices)

if not cirq.equal_up_to_global_phase(product, np.eye(2)):
raise ValueError(
"Invalid dynamical decoupling sequence. Expect sequence production equals identity"
f" up to a global phase, got {product}.".replace('\n', ' ')
)


@value.value_equality
Copy link
Collaborator

Choose a reason for hiding this comment

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

it doesn't look like we mutate the properties of this class, consider making this a dataclass/attrs.frozen e.g.

@attrs.frozen
class DynamicalDecouplingModel:
   schema: Optional[_DynamicalDecouplingSchema] = attrs.field(default=None, validator=...)
   base_dd_sequence: Optional[list['cirq.Gate']] = attrs.field(default=None, validator=...) 
   ...etc

actually I don't think this class is needed.. consider removing it, this will simplify your design and make your life easier as you won't need to worry about json

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Context: I was not so sure about whether to add a model for dynamical decoupling. I chose to add a model class as I expected the dynamical decoupling schema might get more and more complex in the future. While for now, we only need to handle the case of "Supported schemes" and "Customized sequence". A function is good enough, and we do not need to worry about json.

class DynamicalDecouplingModel:
"""Dynamical decoupling model that generates dynamical decoupling operation sequences."""

def __init__(
self,
schema: Optional[_DynamicalDecouplingSchema] = None,
base_dd_sequence: Optional[list['cirq.Gate']] = None,
):
if not schema and not base_dd_sequence:
raise ValueError(
'Specify either schema or base_dd_sequence to construct a valid'
' DynamicalDecouplingModel.'
)
self.schema = schema
self.base_dd_sequence = base_dd_sequence
if base_dd_sequence:
_validate_dd_sequence(base_dd_sequence)

def generate_dd_sequence(self, num_idle_moments: int = 2) -> list['cirq.Gate']:
"""Returns the longest possible dynamical decoupling sequence."""
if num_idle_moments <= 0:
return []
if self.schema:
dd_sequence = _generate_dd_sequence_from_schema(self.schema, num_idle_moments)
elif self.base_dd_sequence:
dd_sequence = _repeat_sequence(self.base_dd_sequence, num_idle_moments)
return dd_sequence

@classmethod
def from_schema(cls, schema: str):
"""Create dynamical decoupling model according to a given schema."""
if not schema in _DynamicalDecouplingSchema.__members__:
raise ValueError("Invalid schema name.")
return cls(schema=_DynamicalDecouplingSchema[schema])

@classmethod
def from_base_dd_sequence(cls, base_dd_sequence: list['cirq.Gate']):
"""Create dynamical decoupling model according to a base sequence."""
return cls(base_dd_sequence=base_dd_sequence)

def _json_dict_(self) -> Dict[str, Any]:
d: Dict[str, Any] = {}
if self.schema:
d['schema'] = self.schema.name
if self.base_dd_sequence:
d['base_dd_sequence'] = self.base_dd_sequence
return d

@classmethod
def _from_json_dict_(cls, schema=None, base_dd_sequence=None, **kwargs):
if schema:
return cls(schema=_DynamicalDecouplingSchema[schema])
if base_dd_sequence:
return cls(base_dd_sequence=base_dd_sequence)

def _value_equality_values_(self) -> Any:
return self.schema, self.base_dd_sequence


@transformer_api.transformer
def add_dynamical_decoupling(
Copy link

Choose a reason for hiding this comment

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

Might be nice to have an option only to insert DD gates on moments that only have single qubits gates. On the hardware, the calibrations for the gates do not include calibrating single qubits and two qubits gates concurrently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, tracking this here. Feel free to put more thoughts there, thanks!

circuit: 'cirq.AbstractCircuit',
*,
context: Optional['cirq.TransformerContext'] = None,
dd_model: DynamicalDecouplingModel = DynamicalDecouplingModel.from_schema("X_XINV"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

instead of dd_model try

spin_echo_sequence: Sequence[cirq.Gate] = (cirq.X, cirq.X),

Copy link
Contributor Author

@babacry babacry May 8, 2024

Choose a reason for hiding this comment

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

Updated with schema.

My understanding is "dynamical decoupling sequence" is more general than "spin echo sequence", correct me if I was wrong @eliottrosenberg .

) -> 'cirq.Circuit':
"""Add dynamical decoupling gate operations to a given circuit.

Copy link
Collaborator

Choose a reason for hiding this comment

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

does this preserve the moment structure? consider adding this to the docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It preserves the moment structure.

Updated docstring.

Args:
circuit: Input circuit to transform.
context: `cirq.TransformerContext` storing common configurable options for transformers.
dd_model: Dynamical decoupling model that defines the schema to generate dynamical
decoupling sequences.

Return:
A copy of the input circuit with dynamical decoupling operations.
"""
last_busy_moment_by_qubits: Dict['cirq.Qid', int] = {q: 0 for q in circuit.all_qubits()}
insert_into: list[Tuple[int, 'cirq.OP_TREE']] = []

for moment_id, moment in enumerate(circuit):
for q in moment.qubits:
insert_gates = dd_model.generate_dd_sequence(
num_idle_moments=moment_id - last_busy_moment_by_qubits[q] - 1
)
for idx, gate in enumerate(insert_gates):
insert_into.append((last_busy_moment_by_qubits[q] + idx + 1, gate.on(q)))
Copy link

Choose a reason for hiding this comment

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

It seems like this is only inserting X gates on consecutive idle moments. I think the next step would be to divide the circuit into moments that are Clifford, then insert X gates on all idle single qubit moments (and perhaps also idle 2 qubit moments, but the caveat is my comment above). Then use the fact that Clifford circuits can be quickly and efficiently simulated to put the correct Clifford gate at the end of the group of moments, to inverse all the previous inserted gates. For CZ gates, this should just be a X, Y or Z gate, i believe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, tracking this here. Feel free to put more thoughts there, thanks!

last_busy_moment_by_qubits[q] = moment_id

updated_circuit = circuit.unfreeze(copy=True)
updated_circuit.batch_insert_into(insert_into)
return updated_circuit
121 changes: 121 additions & 0 deletions cirq-core/cirq/transformers/dynamical_decoupling_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Copyright 2024 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 cirq
from cirq import DynamicalDecouplingModel, add_dynamical_decoupling
import pytest


def assert_dd(
input_circuit: cirq.Circuit, expected_circuit: cirq.Circuit, dd_model: DynamicalDecouplingModel
):
updated_circuit = add_dynamical_decoupling(input_circuit, dd_model=dd_model)
cirq.testing.assert_same_circuits(updated_circuit, expected_circuit)


def test_no_insert_due_to_no_consecutive_moments():
a = cirq.NamedQubit("a")
b = cirq.NamedQubit("b")

# No insertion as there is no room for a dd sequence.
assert_dd(
input_circuit=cirq.Circuit(
cirq.Moment(cirq.H(a)), cirq.Moment(cirq.CNOT(a, b)), cirq.Moment(cirq.H(b))
),
expected_circuit=cirq.Circuit(
cirq.Moment(cirq.H(a)), cirq.Moment(cirq.CNOT(a, b)), cirq.Moment(cirq.H(b))
),
dd_model=DynamicalDecouplingModel.from_schema("XX_PAIR"),
)


@pytest.mark.parametrize(
'schema,inserted_gates',
[
("XX_PAIR", [cirq.X, cirq.X]),
("X_XINV", [cirq.X, cirq.X**-1]),
("YY_PAIR", [cirq.Y, cirq.Y]),
("Y_YINV", [cirq.Y, cirq.Y**-1]),
],
)
def test_insert_provided_schema(schema: str, inserted_gates: list['cirq.Gate']):
a = cirq.NamedQubit("a")
b = cirq.NamedQubit("b")
c = cirq.NamedQubit("c")

input_circuit = cirq.Circuit(
cirq.Moment(cirq.H(a)),
cirq.Moment(cirq.CNOT(a, b)),
cirq.Moment(cirq.CNOT(b, c)),
cirq.Moment(cirq.CNOT(b, c)),
cirq.Moment(cirq.measure_each(a, b, c)),
)
expected_circuit = cirq.Circuit(
cirq.Moment(cirq.H(a)),
cirq.Moment(cirq.CNOT(a, b)),
cirq.Moment(cirq.CNOT(b, c), inserted_gates[0](a)),
cirq.Moment(cirq.CNOT(b, c), inserted_gates[1](a)),
cirq.Moment(cirq.measure_each(a, b, c)),
)

# Insert one dynamical decoupling sequence in idle moments.
assert_dd(
input_circuit, expected_circuit, dd_model=DynamicalDecouplingModel.from_schema(schema)
)


def test_insert_by_customized_dd_sequence():
a = cirq.NamedQubit("a")
b = cirq.NamedQubit("b")
c = cirq.NamedQubit("c")

assert_dd(
input_circuit=cirq.Circuit(
cirq.Moment(cirq.H(a)),
cirq.Moment(cirq.CNOT(a, b)),
cirq.Moment(cirq.CNOT(b, c)),
cirq.Moment(cirq.CNOT(b, c)),
cirq.Moment(cirq.measure_each(a, b, c)),
),
expected_circuit=cirq.Circuit(
cirq.Moment(cirq.H(a)),
cirq.Moment(cirq.CNOT(a, b)),
cirq.Moment(cirq.CNOT(b, c), cirq.X(a)),
cirq.Moment(cirq.CNOT(b, c), cirq.X(a)),
cirq.Moment(cirq.measure_each(a, b, c)),
),
dd_model=DynamicalDecouplingModel.from_base_dd_sequence([cirq.X, cirq.X]),
)


def test_dd_model_constructor():
# Succeed
DynamicalDecouplingModel.from_schema("XX_PAIR")
DynamicalDecouplingModel.from_schema("YY_PAIR")
DynamicalDecouplingModel.from_base_dd_sequence([cirq.X, cirq.X, cirq.Y, cirq.Y])
# Fail
with pytest.raises(ValueError, match="Specify either schema or base_dd_sequence"):
DynamicalDecouplingModel()
with pytest.raises(ValueError, match="Invalid schema name."):
DynamicalDecouplingModel.from_schema("unimplemented_schema")
with pytest.raises(
ValueError, match="Invalid dynamical decoupling sequence. Expect more than one gates."
):
DynamicalDecouplingModel.from_base_dd_sequence([cirq.X])
with pytest.raises(
ValueError,
match="Invalid dynamical decoupling sequence. Expect sequence production equals identity"
" up to a global phase, got",
):
DynamicalDecouplingModel.from_base_dd_sequence([cirq.X, cirq.H])