Skip to content

Commit 073ca32

Browse files
Add noise amplification transformer (quantumlib#6665)
* Add noise amplification transformer * add _gate_in_moment * add copyright notice * add raises documentation, remove import cirq, fix coverage * transformer api * * after `circuit` input Co-authored-by: Noureldin <[email protected]> * format * update test * Convert to a class * types and lint * suggestions from Nour * lint * suggestion from Nour * add () * types * lint --------- Co-authored-by: Noureldin <[email protected]>
1 parent b8af1d3 commit 073ca32

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

cirq/transformers/noise_adding.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright 2024 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections.abc import Mapping
16+
17+
from typing import cast
18+
from cirq import ops, circuits
19+
from cirq.transformers import transformer_api
20+
import numpy as np
21+
22+
23+
def _gate_in_moment(gate: ops.Gate, moment: circuits.Moment) -> bool:
24+
"""Check whether `gate` is in `moment`."""
25+
return any(op.gate == gate for op in moment)
26+
27+
28+
@transformer_api.transformer
29+
class DepolerizingNoiseTransformer:
30+
"""Add local depolarizing noise after two-qubit gates in a specified circuit. More specifically,
31+
with probability p, append a random non-identity two-qubit Pauli operator after each specified
32+
two-qubit gate.
33+
34+
Attrs:
35+
p: The probability with which to add noise.
36+
target_gate: Add depolarizing nose after this type of gate
37+
"""
38+
39+
def __init__(
40+
self, p: float | Mapping[tuple[ops.Qid, ops.Qid], float], target_gate: ops.Gate = ops.CZ
41+
):
42+
"""Initialize the depolarizing noise transformer with some depolarizing probability and
43+
target gate.
44+
45+
Args:
46+
p: The depolarizing probability, either a single float or a mapping from pairs of qubits
47+
to floats.
48+
target_gate: The gate after which to add depolarizing noise.
49+
50+
Raises:
51+
TypeError: If `p` is not either be a float or a mapping from sorted qubit pairs to
52+
floats.
53+
"""
54+
55+
if not isinstance(p, (Mapping, float)):
56+
raise TypeError( # pragma: no cover
57+
"p must either be a float or a mapping from" # pragma: no cover
58+
+ "sorted qubit pairs to floats" # pragma: no cover
59+
) # pragma: no cover
60+
self.p = p
61+
self.p_func = (
62+
(lambda _: p)
63+
if isinstance(p, (int, float))
64+
else (lambda pair: cast(Mapping, p).get(pair, 0.0))
65+
)
66+
self.target_gate = target_gate
67+
68+
def __call__(
69+
self,
70+
circuit: circuits.AbstractCircuit,
71+
rng: np.random.Generator | None = None,
72+
*,
73+
context: transformer_api.TransformerContext | None = None,
74+
):
75+
"""Apply the transformer to the given circuit.
76+
77+
Args:
78+
circuit: The circuit to add noise to.
79+
context: Not used; to satisfy transformer API.
80+
81+
Returns:
82+
The transformed circuit.
83+
"""
84+
if rng is None:
85+
rng = np.random.default_rng()
86+
target_gate = self.target_gate
87+
88+
# add random Pauli gates with probability p after each of the specified gate
89+
assert target_gate.num_qubits() == 2, "`target_gate` must be a two-qubit gate."
90+
paulis = [ops.I, ops.X, ops.Y, ops.Z]
91+
new_moments = []
92+
for moment in circuit:
93+
new_moments.append(moment)
94+
if _gate_in_moment(target_gate, moment):
95+
# add a new moment with the Paulis
96+
target_pairs = {
97+
tuple(sorted(op.qubits)) for op in moment.operations if op.gate == target_gate
98+
}
99+
added_moment_ops = []
100+
for pair in target_pairs:
101+
pair_sorted_tuple = (pair[0], pair[1])
102+
p_i = self.p_func(pair_sorted_tuple)
103+
apply = rng.choice([True, False], p=[p_i, 1 - p_i])
104+
if apply:
105+
choices = [
106+
(pauli_a(pair[0]), pauli_b(pair[1]))
107+
for pauli_a in paulis
108+
for pauli_b in paulis
109+
][1:]
110+
pauli_to_apply = rng.choice(np.array(choices, dtype=object))
111+
added_moment_ops.append(pauli_to_apply)
112+
if len(added_moment_ops) > 0:
113+
new_moments.append(circuits.Moment(*added_moment_ops))
114+
return circuits.Circuit.from_moments(*new_moments)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2024 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from cirq import ops, circuits, devices
16+
import cirq.transformers.noise_adding as na
17+
import numpy as np
18+
19+
20+
def test_noise_adding():
21+
qubits = devices.LineQubit.range(4)
22+
one_layer = circuits.Circuit(ops.CZ(*qubits[:2]), ops.CZ(*qubits[2:]))
23+
circuit = one_layer * 10
24+
25+
# test that p=0 does nothing
26+
transformed_circuit_p0 = na.DepolerizingNoiseTransformer(0.0)(circuit)
27+
assert transformed_circuit_p0 == circuit
28+
29+
# test that p=1 doubles the circuit depth
30+
transformed_circuit_p1 = na.DepolerizingNoiseTransformer(1.0)(circuit)
31+
assert len(transformed_circuit_p1) == 20
32+
33+
# test that we get a deterministic result when using a specific rng
34+
rng = np.random.default_rng(0)
35+
transformed_circuit_p0_03 = na.DepolerizingNoiseTransformer(0.03)(circuit, rng=rng)
36+
expected_circuit = (
37+
one_layer * 2
38+
+ circuits.Circuit(ops.I(qubits[2]), ops.Z(qubits[3]))
39+
+ one_layer * 4
40+
+ circuits.Circuit(ops.Z(qubits[0]), ops.X(qubits[1]))
41+
+ one_layer * 4
42+
+ circuits.Circuit(ops.I(qubits[2]), ops.X(qubits[3]))
43+
)
44+
assert transformed_circuit_p0_03 == expected_circuit
45+
46+
# test that supplying a dictionary for p works
47+
transformed_circuit_p_dict = na.DepolerizingNoiseTransformer(
48+
{tuple(qubits[:2]): 1.0, tuple(qubits[2:]): 0.0}
49+
)(circuit)
50+
assert len(transformed_circuit_p_dict) == 20 # depth should be doubled
51+
assert transformed_circuit_p_dict[1::2].all_qubits() == frozenset(
52+
qubits[:2]
53+
) # no single-qubit gates get added to qubits[2:]

0 commit comments

Comments
 (0)