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

Add noise amplification transformer #6665

Merged
merged 16 commits into from
Jul 12, 2024
114 changes: 114 additions & 0 deletions cirq-core/cirq/transformers/noise_adding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 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.

from collections.abc import Mapping

from typing import cast
from cirq import ops, circuits
from cirq.transformers import transformer_api
import numpy as np


NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
def _gate_in_moment(gate: ops.Gate, moment: circuits.Moment) -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit for performance:

return any(op.gate == gate for op in moment)

"""Check whether `gate` is in `moment`."""
return any(op.gate == gate for op in moment)


@transformer_api.transformer
class DepolerizingNoiseTransformer:
"""Add local depolarizing noise after two-qubit gates in a specified circuit. More specifically,
with probability p, append a random non-identity two-qubit Pauli operator after each specified
two-qubit gate.

Attrs:
p: The probability with which to add noise.
target_gate: Add depolarizing nose after this type of gate
"""

def __init__(
self, p: float | Mapping[tuple[ops.Qid, ops.Qid], float], target_gate: ops.Gate = ops.CZ
):
"""Initialize the depolarizing noise transformer with some depolarizing probability and
target gate.

Args:
p: The depolarizing probability, either a single float or a mapping from pairs of qubits
to floats.
target_gate: The gate after which to add depolarizing noise.

Raises:
TypeError: If `p` is not either be a float or a mapping from sorted qubit pairs to
floats.
"""

if not isinstance(p, (Mapping, float)):
raise TypeError( # pragma: no cover
"p must either be a float or a mapping from" # pragma: no cover
+ "sorted qubit pairs to floats" # pragma: no cover
) # pragma: no cover
self.p = p
Copy link
Collaborator

Choose a reason for hiding this comment

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

you can resolve p here and then use it in call as self.p_func(pair)

Suggested change
self.p = p
self.p_func = lambda _: p if isinstance(p, (int, float)) else lambda pair: p.get(pair, 0)

self.p_func = (
(lambda _: p)
if isinstance(p, (int, float))
else (lambda pair: cast(Mapping, p).get(pair, 0.0))
)
self.target_gate = target_gate

def __call__(
self,
circuit: circuits.AbstractCircuit,
rng: np.random.Generator | None = None,
*,
context: transformer_api.TransformerContext | None = None,
):
"""Apply the transformer to the given circuit.

Args:
circuit: The circuit to add noise to.
context: Not used; to satisfy transformer API.

Returns:
The transformed circuit.
"""
if rng is None:
rng = np.random.default_rng()
target_gate = self.target_gate

# add random Pauli gates with probability p after each of the specified gate
assert target_gate.num_qubits() == 2, "`target_gate` must be a two-qubit gate."
paulis = [ops.I, ops.X, ops.Y, ops.Z]
new_moments = []
for moment in circuit:
new_moments.append(moment)
if _gate_in_moment(target_gate, moment):
# add a new moment with the Paulis
target_pairs = {
tuple(sorted(op.qubits)) for op in moment.operations if op.gate == target_gate
}
added_moment_ops = []
for pair in target_pairs:
pair_sorted_tuple = (pair[0], pair[1])
p_i = self.p_func(pair_sorted_tuple)
apply = rng.choice([True, False], p=[p_i, 1 - p_i])
if apply:
choices = [
Copy link
Collaborator

Choose a reason for hiding this comment

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

[optional suggestion] this creates 17 objects for each pair, 16 pauli pairs + the numpy array. consider doing it this way

pauli_a_idx = np.choice(4)
if pauli_a_idx == 0:
   pauli_b_idx = np.choice(3) + 1
else:
   pauli_b_idx = np.choice(4)
paulit_to_apply = paulis[pauli_a_idx](pair[0]), paulis[pauli_b_idx](pair[1])

Copy link
Collaborator Author

@eliottrosenberg eliottrosenberg Jul 12, 2024

Choose a reason for hiding this comment

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

Thanks, Nour. I agree that this would be more efficient code, but I'm going to leave it as is for now so that I can move on. (It is already very fast as written.)

(pauli_a(pair[0]), pauli_b(pair[1]))
for pauli_a in paulis
for pauli_b in paulis
][1:]
pauli_to_apply = rng.choice(np.array(choices, dtype=object))
added_moment_ops.append(pauli_to_apply)
if len(added_moment_ops) > 0:
new_moments.append(circuits.Moment(*added_moment_ops))
return circuits.Circuit.from_moments(*new_moments)
53 changes: 53 additions & 0 deletions cirq-core/cirq/transformers/noise_adding_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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.

from cirq import ops, circuits, devices
import cirq.transformers.noise_adding as na
import numpy as np


def test_noise_adding():
qubits = devices.LineQubit.range(4)
one_layer = circuits.Circuit(ops.CZ(*qubits[:2]), ops.CZ(*qubits[2:]))
circuit = one_layer * 10

# test that p=0 does nothing
transformed_circuit_p0 = na.DepolerizingNoiseTransformer(0.0)(circuit)
assert transformed_circuit_p0 == circuit

# test that p=1 doubles the circuit depth
transformed_circuit_p1 = na.DepolerizingNoiseTransformer(1.0)(circuit)
assert len(transformed_circuit_p1) == 20

# test that we get a deterministic result when using a specific rng
rng = np.random.default_rng(0)
transformed_circuit_p0_03 = na.DepolerizingNoiseTransformer(0.03)(circuit, rng=rng)
expected_circuit = (
one_layer * 2
+ circuits.Circuit(ops.I(qubits[2]), ops.Z(qubits[3]))
+ one_layer * 4
+ circuits.Circuit(ops.Z(qubits[0]), ops.X(qubits[1]))
+ one_layer * 4
+ circuits.Circuit(ops.I(qubits[2]), ops.X(qubits[3]))
)
assert transformed_circuit_p0_03 == expected_circuit

# test that supplying a dictionary for p works
transformed_circuit_p_dict = na.DepolerizingNoiseTransformer(
{tuple(qubits[:2]): 1.0, tuple(qubits[2:]): 0.0}
)(circuit)
assert len(transformed_circuit_p_dict) == 20 # depth should be doubled
assert transformed_circuit_p_dict[1::2].all_qubits() == frozenset(
qubits[:2]
) # no single-qubit gates get added to qubits[2:]