Skip to content

Commit 67044e5

Browse files
daniel-mills-cqcDaniel MillsDaniel Mills
authored
Noise Aware ZNE (#96)
* remove noise scale by 1 as default * Allow list of circuits when scaling and add noise scaled mitex * Add noise aware noise scaling * Add additional documentation * Add end to end noise aware zne test * Test scaling and merge * Repair haging test by removing plot * Review Corrections * Use ResultHandle * Move to using PTMs for scaling * Correct formatting and add docs * Additional inline docs --------- Co-authored-by: Daniel Mills <[email protected]> Co-authored-by: Daniel Mills <[email protected]>
1 parent 1b18cb2 commit 67044e5

File tree

9 files changed

+943
-104
lines changed

9 files changed

+943
-104
lines changed

docs_src/zero_noise_extrapolation.rst

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ qermit.zero_noise_extrapolation
55

66
.. automethod:: qermit.zero_noise_extrapolation.zne.gen_ZNE_MitEx
77

8+
.. automethod:: qermit.zero_noise_extrapolation.zne.gen_noise_scaled_mitex
9+
810
.. autoclass:: qermit.zero_noise_extrapolation.zne.Folding
911
:members:
1012

@@ -13,6 +15,8 @@ qermit.zero_noise_extrapolation
1315

1416
.. automethod:: qermit.zero_noise_extrapolation.zne.digital_folding_task_gen
1517

18+
.. automethod:: qermit.zero_noise_extrapolation.zne.merge_experiments_task_gen
19+
1620
.. automethod:: qermit.zero_noise_extrapolation.zne.extrapolation_task_gen
1721

1822
.. automethod:: qermit.zero_noise_extrapolation.zne.gen_initial_compilation_task

qermit/noise_model/noise_model.py

+186
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from pytket.pauli import QubitPauliString
1212
from numpy.random import Generator
1313
from enum import Enum
14+
from itertools import product
15+
from scipy.linalg import fractional_matrix_power # type: ignore
16+
from numpy.typing import NDArray
1417

1518

1619
Direction = Enum('Direction', ['forward', 'backward'])
@@ -52,9 +55,160 @@ def __init__(
5255
+ " but should be less than or equal to 1."
5356
)
5457

58+
# If the given distribution is empty then no
59+
# noise will be acted.
60+
if distribution == {}:
61+
pass
62+
# If it it not empty then we check that the number
63+
# of qubits in each of the errors match.
64+
else:
65+
n_qubits = len(list(distribution.keys())[0])
66+
if not all(len(error) == n_qubits for error in distribution.keys()):
67+
raise Exception("Errors must all act on the same number of qubits.")
68+
5569
self.distribution = distribution
5670
self.rng = rng
5771

72+
@property
73+
def identity_error_rate(self) -> float:
74+
"""The rate at which no error occurs.
75+
76+
:return: Rate at which no error occurs.
77+
Calculated as 1 minus the total error rate of
78+
error in this distribution.
79+
:rtype: float
80+
"""
81+
return 1 - sum(self.distribution.values())
82+
83+
def to_ptm(self) -> Tuple[NDArray, Dict[Tuple[Pauli, ...], int]]:
84+
"""Convert error distribution to Pauli Transfer Matrix (PTM) form.
85+
86+
:return: PTM of error distribution and Pauli index dictionary.
87+
The Pauli index dictionary maps Pauli errors to their
88+
index in the PTM
89+
:rtype: Tuple[NDArray, Dict[Tuple[Pauli, ...], int]]
90+
"""
91+
92+
# Initialise an empty PTM and index dictionary
93+
# of the appropriate size.
94+
ptm = np.zeros((4**self.n_qubits, 4**self.n_qubits))
95+
pauli_index = {
96+
pauli: index
97+
for index, pauli
98+
in enumerate(product({Pauli.I, Pauli.X, Pauli.Y, Pauli.Z}, repeat=self.n_qubits))
99+
}
100+
101+
# For each pauli, calculate the corresponding
102+
# PTM entry as a sum pf error weights multiplied by +/-1
103+
# Depending on commutation relations.
104+
for pauli_tuple, index in pauli_index.items():
105+
106+
pauli = QermitPauli.from_pauli_iterable(
107+
pauli_iterable=pauli_tuple,
108+
qubit_list=[Qubit(i) for i in range(self.n_qubits)]
109+
)
110+
111+
# Can add the identity error rate.
112+
# This will not come up in the following for loop
113+
# as the error distribution does not save
114+
# the rate at which no errors occur.
115+
ptm[index][index] += self.identity_error_rate
116+
117+
for error, error_rate in self.distribution.items():
118+
error_pauli = QermitPauli.from_pauli_iterable(
119+
pauli_iterable=error,
120+
qubit_list=[Qubit(i) for i in range(self.n_qubits)]
121+
)
122+
123+
ptm[index][index] += error_rate * QermitPauli.commute_coeff(pauli_one=pauli, pauli_two=error_pauli)
124+
125+
# Some checks that the form of the PTM is correct.
126+
identity = tuple(Pauli.I for _ in range(self.n_qubits))
127+
if not abs(ptm[pauli_index[identity]][pauli_index[identity]] - 1.0) < 10**(-6):
128+
raise Exception(
129+
"The identity entry of the PTM is incorrect. "
130+
+ "This is a fault in Qermit. "
131+
+ "Please report this as an issue."
132+
)
133+
134+
if not self == ErrorDistribution.from_ptm(ptm=ptm, pauli_index=pauli_index):
135+
raise Exception(
136+
"From PTM does not match to PTM. "
137+
+ "This is a fault in Qermit. "
138+
+ "Please report this as an issue."
139+
)
140+
141+
return ptm, pauli_index
142+
143+
@classmethod
144+
def from_ptm(cls, ptm: NDArray, pauli_index: Dict[Tuple[Pauli, ...], int]) -> ErrorDistribution:
145+
"""Convert a Pauli Transfer Matrix (PTM) to an error distribution.
146+
147+
:param ptm: Pauli Transfer Matrix to convert. Should be a 4^n by 4^n matrix
148+
where n is the number of qubits.
149+
:type ptm: NDArray
150+
:param pauli_index: A dictionary mapping Pauli errors to
151+
their index in the PTM.
152+
:type pauli_index: Dict[Tuple[Pauli, ...], int]
153+
:return: The converted error distribution.
154+
:rtype: ErrorDistribution
155+
"""
156+
157+
if ptm.ndim != 2:
158+
raise Exception(
159+
f"This given matrix is not has dimension {ptm.ndim} "
160+
+ "but should have dimension 2."
161+
)
162+
163+
if ptm.shape[0] != ptm.shape[1]:
164+
raise Exception(
165+
"The dimensions of the given PTM are "
166+
+ f"{ptm.shape[0]} and {ptm.shape[1]} "
167+
+ "but they should match."
168+
)
169+
170+
n_qubit = math.log(ptm.shape[0], 4)
171+
if n_qubit % 1 != 0.0:
172+
raise Exception(
173+
"The given PTM should have a dimension of the form 4^n "
174+
+ "where n is the number of qubits."
175+
)
176+
177+
if not np.array_equal(ptm, np.diag(np.diag(ptm))):
178+
raise Exception(
179+
"The given PTM is not diagonal as it should be."
180+
)
181+
182+
# calculate the error rates by solving simultaneous
183+
# linear equations. In particular the matrix to invert
184+
# is the matrix of commutation values.
185+
commutation_matrix = np.zeros(ptm.shape)
186+
for pauli_one_tuple, index_one in pauli_index.items():
187+
pauli_one = QermitPauli.from_pauli_iterable(
188+
pauli_iterable=pauli_one_tuple,
189+
qubit_list=[Qubit(i) for i in range(len(pauli_one_tuple))]
190+
)
191+
for pauli_two_tuple, index_two in pauli_index.items():
192+
pauli_two = QermitPauli.from_pauli_iterable(
193+
pauli_iterable=pauli_two_tuple,
194+
qubit_list=[Qubit(i) for i in range(len(pauli_two_tuple))]
195+
)
196+
commutation_matrix[index_one][index_two] = QermitPauli.commute_coeff(pauli_one=pauli_one, pauli_two=pauli_two)
197+
198+
error_rate_list = np.matmul(ptm.diagonal(), np.linalg.inv(commutation_matrix))
199+
distribution = {
200+
error: error_rate_list[index]
201+
for error, index in pauli_index.items()
202+
if (error_rate_list[index] > 10**(-6)) and error != tuple(Pauli.I for _ in range(int(n_qubit)))
203+
}
204+
return cls(distribution=distribution)
205+
206+
@property
207+
def n_qubits(self) -> int:
208+
"""The number of qubits this error distribution acts on.
209+
"""
210+
return len(list(self.distribution.keys())[0])
211+
58212
def __eq__(self, other: object) -> bool:
59213
"""Check equality of two instances of ErrorDistribution by ensuring
60214
that all keys in distribution match, and that the probabilities are
@@ -208,6 +362,22 @@ def plot(self):
208362

209363
return fig
210364

365+
def scale(self, scaling_factor: float) -> ErrorDistribution:
366+
"""Scale the error rates of this error distribution.
367+
This is done by converting the error distribution to a PTM,
368+
scaling that matrix appropriately, and converting back to a
369+
new error distribution.
370+
371+
:param scaling_factor: The factor by which the noise should be scaled.
372+
:type scaling_factor: float
373+
:return: A new error distribution with the noise scaled.
374+
:rtype: ErrorDistribution
375+
"""
376+
377+
ptm, pauli_index = self.to_ptm()
378+
scaled_ptm = fractional_matrix_power(ptm, scaling_factor)
379+
return ErrorDistribution.from_ptm(ptm=scaled_ptm, pauli_index=pauli_index)
380+
211381

212382
class LogicalErrorDistribution:
213383
"""
@@ -304,6 +474,22 @@ def __init__(self, noise_model: Dict[OpType, ErrorDistribution]):
304474

305475
self.noise_model = noise_model
306476

477+
def scale(self, scaling_factor: float) -> NoiseModel:
478+
"""Generate new error model where all error rates have been scaled by
479+
the given scaling factor.
480+
481+
:param scaling_factor: Factor by which to scale the error rates.
482+
:type scaling_factor: float
483+
:return: New noise model with scaled error rates.
484+
:rtype: NoiseModel
485+
"""
486+
return NoiseModel(
487+
noise_model={
488+
op_type: error_distribution.scale(scaling_factor=scaling_factor)
489+
for op_type, error_distribution in self.noise_model.items()
490+
}
491+
)
492+
307493
def reset_rng(self, rng: Generator):
308494
"""Reset randomness generator.
309495

qermit/noise_model/qermit_pauli.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
from numpy.random import Generator
77
from typing import List, Union, Tuple, Dict
8+
from collections.abc import Iterable
89

910

1011
class QermitPauli:
@@ -49,6 +50,31 @@ def __init__(
4950
self.phase = phase
5051
self.qubit_list = qubit_list
5152

53+
@staticmethod
54+
def commute_coeff(pauli_one: QermitPauli, pauli_two: QermitPauli) -> int:
55+
"""Calculate the coefficient which result from commuting pauli_one
56+
past pauli_two. That is to say P_2 P_1 = c P_1 P_2 where c is the
57+
coefficient returned by this function.
58+
59+
:param pauli_one: First Pauli
60+
:type pauli_one: QermitPauli
61+
:param pauli_two: Second Pauli
62+
:type pauli_two: QermitPauli
63+
:raises Exception: Raised if the Paulis do not act
64+
on matching qubits.
65+
:return: Coefficient resulting from commuting the two Paulis.
66+
:rtype: int
67+
"""
68+
if not pauli_one.qubit_list == pauli_two.qubit_list:
69+
raise Exception(
70+
"The given Paulis must act on the same qubits. "
71+
+ f"In this case the qubits acted on by pauli_one {pauli_one.qubit_list} "
72+
+ f"differ from those of pauli_two {pauli_two.qubit_list}."
73+
)
74+
power = sum(pauli_one.X_list[qubit] * pauli_two.Z_list[qubit] for qubit in pauli_one.qubit_list)
75+
power += sum(pauli_one.Z_list[qubit] * pauli_two.X_list[qubit] for qubit in pauli_one.qubit_list)
76+
return (-1) ** power
77+
5278
def is_measureable(self, qubit_list: List[Qubit]) -> bool:
5379
"""Checks if this Pauli would be measurable on the given qubits in the
5480
computational bases. That is to say if at least one Pauli on the given
@@ -592,7 +618,7 @@ def pauli_string(self) -> Tuple[List[Pauli], complex]:
592618

593619
@property
594620
def qubit_pauli_string(self) -> Tuple[QubitPauliString, complex]:
595-
"""Qubit pauli string corresponding to Paulii,
621+
"""Qubit pauli string corresponding to Pauli,
596622
along with the appropriate phase.
597623
598624
:return: Pauli string and phase corresponding to Pauli.
@@ -606,3 +632,21 @@ def qubit_pauli_string(self) -> Tuple[QubitPauliString, complex]:
606632
)
607633

608634
return qubit_pauli_string, operator_phase
635+
636+
@classmethod
637+
def from_pauli_iterable(cls, pauli_iterable: Iterable[Pauli], qubit_list: List[Qubit]) -> QermitPauli:
638+
"""Create a QermitPauli from a Pauli iterable.
639+
640+
:param pauli_iterable: The Pauli iterable to convert.
641+
:type pauli_iterable: Iterable[Pauli]
642+
:param qubit_list: The qubits on which the resulting pauli will act.
643+
:type qubit_list: List[Qubit]
644+
:return: The pauli corresponding to the given iterable.
645+
:rtype: QermitPauli
646+
"""
647+
return cls(
648+
Z_list=[int(pauli in (Pauli.Z, Pauli.Y)) for pauli in pauli_iterable],
649+
X_list=[int(pauli in (Pauli.X, Pauli.Y)) for pauli in pauli_iterable],
650+
qubit_list=qubit_list,
651+
phase=sum(int(pauli == Pauli.Y) for pauli in pauli_iterable) % 4,
652+
)

0 commit comments

Comments
 (0)