Skip to content

Commit

Permalink
feature: adding TwoQubitPauliChannel (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbeach-aws authored Feb 1, 2022
1 parent 0967f97 commit 0ed70c7
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 17 deletions.
104 changes: 98 additions & 6 deletions src/braket/circuits/noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from typing import Any, Optional, Sequence
from typing import Any, Dict, Optional, Sequence

from braket.circuits.quantum_operator import QuantumOperator
from braket.circuits.qubit_set import QubitSet
Expand Down Expand Up @@ -121,7 +121,7 @@ def __init__(
def probability(self) -> float:
"""
Returns:
probability (float): The probability that parameterizes the noise channel.
probability (float): The probability that parametrizes the noise channel.
"""
return self._probability

Expand Down Expand Up @@ -163,7 +163,7 @@ def __init__(
def probability(self) -> float:
"""
Returns:
probability (float): The probability that parameterizes the noise channel.
probability (float): The probability that parametrizes the noise channel.
"""
return self._probability

Expand Down Expand Up @@ -205,18 +205,110 @@ def __init__(
def probability(self) -> float:
"""
Returns:
probability (float): The probability that parameterizes the noise channel.
probability (float): The probability that parametrizes the noise channel.
"""
return self._probability

def __repr__(self):
return f"{self.name}('probability': {self.probability}, 'qubit_count': {self.qubit_count})"


class MultiQubitPauliNoise(Noise):
"""
Class `MultiQubitPauliNoise` represents a general multi-qubit Pauli channel,
parameterized by up to 4**N - 1 probabilities.
"""

_allowed_substrings = {"I", "X", "Y", "Z"}

def __init__(
self,
probabilities: Dict[str, float],
qubit_count: Optional[int],
ascii_symbols: Sequence[str],
):
"""[summary]
Args:
probabilities (Dict[str, float]): A dictionary with Pauli string as the keys,
and the probabilities as values, i.e. {"XX": 0.1. "IZ": 0.2}.
qubit_count (Optional[int]): The number of qubits the Pauli noise acts on.
ascii_symbols (Sequence[str]): ASCII string symbols for the noise. These are used when
printing a diagram of a circuit. The length must be the same as `qubit_count`, and
index ordering is expected to correlate with the target ordering on the instruction.
Raises:
ValueError: If the `qubit_count` is less than 1, `ascii_symbols` are `None`, or
`ascii_symbols` length != `qubit_count`. Also if `probabilities` are not `float`s,
any `probabilities` > 1, or `probabilities` < 0, or if the sum of all
probabilities is > 1,
or if "II" is specified as a Pauli string.
Also if any Pauli string contains invalid strings.
Also if the length of probabilities is greater than 4**qubit_count.
TypeError: If the type of the dictionary keys are not strings.
If the probabilities are not floats.
"""

super().__init__(qubit_count=qubit_count, ascii_symbols=ascii_symbols)
self.probabilities = probabilities

if not probabilities:
raise ValueError("Pauli dictionary must not be empty.")

identity = self.qubit_count * "I"
if identity in probabilities:
raise ValueError(
f"{identity} is not allowed as a key. Please enter only non-identity Pauli strings."
)

for pauli_string, prob in probabilities.items():
if not isinstance(pauli_string, str):
raise TypeError(f"Type of {pauli_string} was not a string.")
if len(pauli_string) != self.qubit_count:
raise ValueError(
(
"Length of each Pauli string must be equal to number of qubits. "
f"{pauli_string} had length {len(pauli_string)} instead of length {self.qubit_count}." # noqa
)
)
if not isinstance(prob, float):
raise TypeError(
(
"Probabilities must be a float type. "
f"The probability for {pauli_string} was of type {type(prob)}."
)
)
if not set(pauli_string) <= self._allowed_substrings:
raise ValueError(
(
"Strings must be Pauli strings consisting of only [I, X, Y, Z]. "
f"Received {pauli_string}."
)
)
if prob < 0.0 or prob > 1.0:
raise ValueError(
(
"Individual probabilities must be real numbers in the interval [0, 1]. "
f"Probability for {pauli_string} was {prob}."
)
)
total_prob = sum(probabilities.values())
if total_prob > 1.0 or total_prob < 0.0:
raise ValueError(
(
"Total probability must be a real number in the interval [0, 1]. "
f"Total probability was {total_prob}."
)
)

def __repr__(self):
return f"{self.name}('probabilities' : {self.probabilities}, 'qubit_count': {self.qubit_count})" # noqa


class PauliNoise(Noise):
"""
Class `PauliNoise` represents the general Pauli noise channel on N qubits
parameterized by three probabilities.
Class `PauliNoise` represents the a single-qubit Pauli noise channel
acting on one qubit. It is parameterized by three probabilities.
"""

def __init__(
Expand Down
137 changes: 134 additions & 3 deletions src/braket/circuits/noises.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from typing import Iterable
import itertools
from typing import Dict, Iterable

import numpy as np

Expand All @@ -21,6 +22,7 @@
from braket.circuits.noise import (
DampingNoise,
GeneralizedAmplitudeDampingNoise,
MultiQubitPauliNoise,
Noise,
PauliNoise,
SingleProbabilisticNoise,
Expand Down Expand Up @@ -552,6 +554,135 @@ def two_qubit_dephasing(
Noise.register_noise(TwoQubitDephasing)


class TwoQubitPauliChannel(MultiQubitPauliNoise):
"""Two-Qubit Pauli noise channel which transforms a
density matrix :math:`\\rho` according to:
.. math::
\\rho \\Rightarrow (1-p) \\rho +
p_{IX} IX \\rho IX^{\\dagger} +
p_{IY} IY \\rho IY^{\\dagger} +
p_{IZ} IZ \\rho IZ^{\\dagger} +
p_{XI} XI \\rho XI^{\\dagger} +
p_{XX} XX \\rho XX^{\\dagger} +
p_{XY} XY \\rho XY^{\\dagger} +
p_{XZ} XZ \\rho XZ^{\\dagger} +
p_{YI} YI \\rho YI^{\\dagger} +
p_{YX} YX \\rho YX^{\\dagger} +
p_{YY} YY \\rho YY^{\\dagger} +
p_{YZ} YZ \\rho YZ^{\\dagger} +
p_{ZI} ZI \\rho ZI^{\\dagger} +
p_{ZX} ZX \\rho ZX^{\\dagger} +
p_{ZY} ZY \\rho ZY^{\\dagger} +
p_{ZZ} ZZ \\rho ZZ^{\\dagger})
where
.. math::
I = \\left(
\\begin{matrix}
1 & 0 \\\\
0 & 1
\\end{matrix}
\\right)
X = \\left(
\\begin{matrix}
0 & 1 \\\\
1 & 0
\\end{matrix}
\\right)
Y = \\left(
\\begin{matrix}
0 & -i \\\\
i & 0
\\end{matrix}
\\right)
Z = \\left(
\\begin{matrix}
1 & 0 \\\\
0 & -1
\\end{matrix}
\\right)
p = \\text{sum of all probabilities}
This noise channel is shown as `PC_2({"pauli_string": probability})` in circuit diagrams.
"""

_paulis = {
"I": np.array([[1.0, 0.0], [0.0, 1.0]], dtype=complex),
"X": np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex),
"Y": np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex),
"Z": np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex),
}
_tensor_products_strings = itertools.product(_paulis.keys(), repeat=2)
_names_list = ["".join(x) for x in _tensor_products_strings]

def __init__(self, probabilities: Dict[str, float]):
super().__init__(
probabilities=probabilities,
qubit_count=None,
ascii_symbols=[
f"PC2({probabilities})",
f"PC2({probabilities})",
],
)

total_prob = sum(self.probabilities.values())

K_list = [np.sqrt(1 - total_prob) * np.identity(4)] # "II" element
for pstring in self._names_list[1:]: # ignore "II"
if pstring in self.probabilities:
mat = np.sqrt(self.probabilities[pstring]) * np.kron(
self._paulis[pstring[0]], self._paulis[pstring[1]]
)
K_list.append(mat)
else:
K_list.append(np.zeros((4, 4)))
self._matrix = K_list

def to_ir(self, target: QubitSet):
return ir.MultiQubitPauliChannel.construct(
targets=[target[0], target[1]], probabilities=self.probabilities
)

def to_matrix(self) -> Iterable[np.ndarray]:
return self._matrix

@staticmethod
def fixed_qubit_count() -> int:
return 2

@staticmethod
@circuit.subroutine(register=True)
def two_qubit_pauli_channel(
target1: QubitInput, target2: QubitInput, probabilities: Dict[str, float]
) -> Iterable[Instruction]:
"""Registers this function into the circuit class.
Args:
target (Qubit, int, or iterable of Qubit / int): Target qubits
probability (float): Probability of two-qubit Pauli channel.
Returns:
Iterable[Instruction]: `Iterable` of Depolarizing instructions.
Examples:
>>> circ = Circuit().two_qubit_pauli_channel(0, 1, {"XX": 0.1})
"""
return [
Instruction(
Noise.TwoQubitPauliChannel(probabilities=probabilities),
target=[target1, target2],
)
]


Noise.register_noise(TwoQubitPauliChannel)


class AmplitudeDamping(DampingNoise):
"""AmplitudeDamping noise channel which transforms a density matrix :math:`\\rho` according to:
Expand Down Expand Up @@ -789,15 +920,15 @@ class Kraus(Noise):
Args:
matrices (Iterable[np.array]): A list of matrices that define a noise
channel. These matrices need to satisify the requirement of CPTP map.
channel. These matrices need to satisfy the requirement of CPTP map.
display_name (str): Name to be used for an instance of this general noise
channel for circuit diagrams. Defaults to `KR`.
Raises:
ValueError: If any matrix in `matrices` is not a two-dimensional square
matrix,
or has a dimension length which is not a positive exponent of 2,
or the `matrices` do not satisify CPTP condition.
or the `matrices` do not satisfy CPTP condition.
"""

def __init__(self, matrices: Iterable[np.ndarray], display_name: str = "KR"):
Expand Down
2 changes: 1 addition & 1 deletion src/braket/circuits/observables.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def eigenvalues(self):
def eigenvalue(self, index: int) -> float:
if index in self._eigenvalue_indices:
return self._eigenvalue_indices[index]
dimension = 2 ** self.qubit_count
dimension = 2**self.qubit_count
if index >= dimension:
raise ValueError(
f"Index {index} requested but observable has at most {dimension} eigenvalues"
Expand Down
2 changes: 1 addition & 1 deletion src/braket/circuits/quantum_operator_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def verify_quantum_operator_matrix_dimensions(matrix: np.array) -> None:
matrix = np.array(matrix, dtype=complex)
qubit_count = int(np.log2(matrix.shape[0]))

if 2 ** qubit_count != matrix.shape[0] or qubit_count < 1:
if 2**qubit_count != matrix.shape[0] or qubit_count < 1:
raise ValueError(f"`matrix` dimension {matrix.shape[0]} is not a positive power of 2")


Expand Down
4 changes: 2 additions & 2 deletions src/braket/circuits/unitary_calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def calculate_unitary(qubit_count: int, instructions: Iterable[Instruction]) ->
TypeError: If `instructions` is not composed only of `Gate` instances,
i.e. a circuit with `Noise` operators will raise this error.
"""
unitary = np.eye(2 ** qubit_count, dtype=complex)
unitary = np.eye(2**qubit_count, dtype=complex)
un_tensor = np.reshape(unitary, qubit_count * [2, 2])

for instr in instructions:
Expand All @@ -83,4 +83,4 @@ def calculate_unitary(qubit_count: int, instructions: Iterable[Instruction]) ->
casting="no",
)

return np.reshape(un_tensor, 2 * [2 ** qubit_count])
return np.reshape(un_tensor, 2 * [2**qubit_count])
2 changes: 1 addition & 1 deletion src/braket/tasks/gate_model_quantum_task_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ def _probability_from_measurements(

# count the basis state occurrences, and construct the probability vector
basis_states, counts = np.unique(indices, return_counts=True)
probabilities = np.zeros([2 ** num_measured_qubits], dtype=np.float64)
probabilities = np.zeros([2**num_measured_qubits], dtype=np.float64)
probabilities[basis_states] = counts / shots
return probabilities

Expand Down
4 changes: 2 additions & 2 deletions test/integ_tests/test_tensor_network_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _qft(circuit, num_qubits):
for i in range(num_qubits):
circuit.h(i)
for j in range(1, num_qubits - i):
circuit.cphaseshift(i + j, i, math.pi / (2 ** j))
circuit.cphaseshift(i + j, i, math.pi / (2**j))

for qubit in range(math.floor(num_qubits / 2)):
circuit.swap(qubit, num_qubits - qubit - 1)
Expand All @@ -78,7 +78,7 @@ def _inverse_qft(circuit, num_qubits):

for i in reversed(range(num_qubits)):
for j in reversed(range(1, num_qubits - i)):
circuit.cphaseshift(i + j, i, -math.pi / (2 ** j))
circuit.cphaseshift(i + j, i, -math.pi / (2**j))
circuit.h(i)

return circuit
Loading

0 comments on commit 0ed70c7

Please sign in to comment.