diff --git a/setup.py b/setup.py index 29c165b2..6744e582 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ "flake8", "isort", "jsonschema==3.2.0", + "numpy", "pre-commit", "pylint", "pytest", diff --git a/src/braket/ir/jaqcd/__init__.py b/src/braket/ir/jaqcd/__init__.py index 072acef3..85f8ecb7 100644 --- a/src/braket/ir/jaqcd/__init__.py +++ b/src/braket/ir/jaqcd/__init__.py @@ -35,6 +35,7 @@ I, ISwap, Kraus, + MultiQubitPauliChannel, PauliChannel, PhaseDamping, PhaseFlip, diff --git a/src/braket/ir/jaqcd/instructions.py b/src/braket/ir/jaqcd/instructions.py index aec49fb2..f0514f8e 100644 --- a/src/braket/ir/jaqcd/instructions.py +++ b/src/braket/ir/jaqcd/instructions.py @@ -20,6 +20,7 @@ DampingSingleProbability, DoubleControl, DoubleTarget, + MultiProbability, MultiTarget, SingleControl, SingleProbability, @@ -766,7 +767,7 @@ class Type(str, Enum): class PauliChannel(SingleTarget, TripleProbability): """ - Genearal Pauli noise channel. + A single qubit Pauli noise channel. Attributes: type (str): The instruction type. default = "pauli_channel". (type) is @@ -783,6 +784,28 @@ class Type(str, Enum): type = Type.pauli_channel +class MultiQubitPauliChannel(DoubleTarget, MultiProbability): + """ + Multi-qubit Pauli noise channel. + + Attributes: + type (str): The instruction type. default = "multi_qubit_pauli_channel". (type) is + optional. This should be unique among all instruction types. + target (int): The target qubit(s). This is list of intergers >= 0. + The length of the list must match the length of the Pauli strings provided. + + Examples: + >>> MultiQubitPauliChannel(target=1, probabilities={"X": 0.1}) + >>> MultiQubitPauliChannel(target=[0,1], probabilities={"XY": 0.1}) + >>> MultiQubitPauliChannel(target=[0,1,2], probabilities={"XYZ": 0.1}) + """ + + class Type(str, Enum): + multi_qubit_pauli_channel = "multi_qubit_pauli_channel" + + type = Type.multi_qubit_pauli_channel + + class Depolarizing(SingleTarget, SingleProbability_34): """ Depolarizing noise channel. diff --git a/src/braket/ir/jaqcd/shared_models.py b/src/braket/ir/jaqcd/shared_models.py index df367d81..0de1d623 100644 --- a/src/braket/ir/jaqcd/shared_models.py +++ b/src/braket/ir/jaqcd/shared_models.py @@ -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 Optional, Union +from typing import Dict, Optional, Union from pydantic import BaseModel, confloat, conint, conlist, constr, root_validator @@ -233,6 +233,54 @@ def validate_probabilities(cls, values): return values +class MultiProbability(BaseModel): + """A multi-value-probability parameter set for the Pauli noise channel. + + Attributes: + probabilities [Dict[str, float]]: The coefficients of the Pauli channel + + Examples: + >>> MultiProbability(probabilities={"X": 0.1}) + >>> MultiProbability(probabilities={"XY": 0.1, "YX": 0.01}) + """ + + probabilities: Dict[ + constr(regex="^[IXYZ]+$", min_length=1), confloat(ge=float("0.0"), le=float("1.0")) + ] + + @root_validator + def validate_probabilities(cls, values): + """ + Pydantic uses the validation subsystem to create objects. + This custom validator has the purpose to ensure sum(probabilities) <= 1 + and that the lengths of each Pauli string are equal. + """ + + probabilities = values.get("probabilities") + if not probabilities: + raise ValueError("Pauli dictionary must not be empty.") + + qubit_count = len(list(probabilities)[0]) + + if qubit_count * "I" in probabilities.keys(): + i = qubit_count * "I" + raise ValueError( + f"{i} is not allowed as a key. Please enter only non-identity Pauli strings." + ) + + for pauli_string, prob in probabilities.items(): + if len(pauli_string) != qubit_count: + raise ValueError("Length of each Pauli string must be equal to number of qubits.") + + 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 + ) + + return values + + class TwoDimensionalMatrix(BaseModel): """ Two-dimensional non-empty matrix. diff --git a/test/unit_tests/braket/ir/jaqcd/test_multi_probability.py b/test/unit_tests/braket/ir/jaqcd/test_multi_probability.py new file mode 100644 index 00000000..57eb9979 --- /dev/null +++ b/test/unit_tests/braket/ir/jaqcd/test_multi_probability.py @@ -0,0 +1,87 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file 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 itertools import product + +import numpy as np +import pytest + +from braket.ir.jaqcd.shared_models import MultiProbability + +np.random.seed(1) + + +def pauli_strings(n_qubits: int): + pauli_strings = list(map(lambda x: "".join(x), product(["I", "X", "Y", "Z"], repeat=n_qubits))) + return pauli_strings[1:] # remove identity term + + +def random_noise(n_qubits: int): + return np.random.dirichlet(np.ones(4**n_qubits))[1:] # remove identity term + + +@pytest.mark.parametrize("n_qubits", [1, 2, 3]) +class TestMultiProbability: + """A class with common parameters, `param1` and `param2`.""" + + def test_multiprobability(self, n_qubits): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + MultiProbability(probabilities=paulis) + + def test_multiprobability_less_probabilities(self, n_qubits): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + del paulis["X" * n_qubits] + del paulis["Z" * n_qubits] + MultiProbability(probabilities=paulis) + + @pytest.mark.xfail(raises=ValueError) + @pytest.mark.parametrize("str", ["T", "s", "x"]) + def test_multiprobability_non_pauli(self, n_qubits, str): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + paulis[str * n_qubits] = 0.0 + MultiProbability(probabilities=paulis) + + @pytest.mark.xfail(raises=ValueError) + @pytest.mark.parametrize("value", [12, -0.1, 1.1, np.inf, None]) + def test_multiprobability_non_float(self, n_qubits, value): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + paulis["X" * n_qubits] = value + MultiProbability(probabilities=paulis) + + @pytest.mark.xfail(raises=ValueError) + def test_multiprobability_empty(self, n_qubits): + MultiProbability(probabilities={}) + + @pytest.mark.xfail(raises=ValueError) + def test_multiprobability_identity(self, n_qubits): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + paulis["I" * n_qubits] = 0.1 + MultiProbability(probabilities=paulis) + + @pytest.mark.xfail(raises=ValueError) + def test_multiprobability_pauli_equal_lengths(self, n_qubits): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + paulis["X"] = 0.0 + paulis["XY"] = 0.0 + MultiProbability(probabilities=paulis) + + @pytest.mark.xfail(raises=ValueError) + def test_multiprobability_sum_over_one(self, n_qubits): + paulis = dict(zip(pauli_strings(n_qubits), 10 * random_noise(n_qubits))) + MultiProbability(probabilities=paulis) + + @pytest.mark.xfail(raises=ValueError) + def test_multiprobability_sum_under_zero(self, n_qubits): + paulis = dict(zip(pauli_strings(n_qubits), random_noise(n_qubits))) + paulis["Z" * n_qubits] = -0.9 + MultiProbability(probabilities=paulis)