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

feature: adding TwoQubitPauliChannel #300

Merged
merged 7 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 75 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,87 @@ 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("Keys must be a string type")
mbeach-aws marked this conversation as resolved.
Show resolved Hide resolved
if len(pauli_string) != self.qubit_count:
raise ValueError("Length of each Pauli strings must be equal to number of qubits.")
mbeach-aws marked this conversation as resolved.
Show resolved Hide resolved
if not isinstance(prob, float):
raise TypeError("Keys must be a float type")
mbeach-aws marked this conversation as resolved.
Show resolved Hide resolved
if not set(pauli_string) <= self._allowed_substrings:
raise ValueError("Strings must be Pauli strings consisting of only [I, X, Y, Z]")
if prob < 0.0 or prob > 1.0:
raise ValueError("Individual values must be a real number in the interval [0,1]")

total_prob = sum(probabilities.values())
if total_prob > 1.0 or total_prob < 0.0:
raise ValueError(
f"Total probability must be a real number in the interval [0, 1]. Total probability was {total_prob}." # noqa: E501
)

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