diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5e20c1..b18dadaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ### New features since last release +* Added support for converting Qiskit noise models to + PennyLane ``NoiseModels`` using ``load_noise_model``. + [(#577)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/577) + * Qiskit Sessions can now be used for the ``qiskit.remote`` device with the ``qiskit_session`` context manager. [(#551)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/551) @@ -42,6 +46,10 @@ capabilities and Qiskit. This release contains contributions from (in alphabetical order): +Utkarsh Azad +Lillian M. A. Frederiksen +Austin Huang + --- # Release 0.37.0 diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index 2a886b80..8ac3f871 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -17,4 +17,4 @@ from .aer import AerDevice from .basic_sim import BasicSimulatorDevice from .remote import RemoteDevice -from .converter import load, load_pauli_op, load_qasm, load_qasm_from_file +from .converter import load, load_pauli_op, load_qasm, load_qasm_from_file, load_noise_model diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 0df4e492..adc753b5 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -35,9 +35,14 @@ from sympy import lambdify import pennylane as qml +from pennylane.noise.conditionals import WiresIn, _rename +from pennylane.operation import AnyWires import pennylane.ops as pennylane_ops from pennylane.tape.tape import rotations_and_diagonal_measurements +from .noise_models import _build_noise_model_map + +# pylint: disable=too-many-instance-attributes QISKIT_OPERATION_MAP = { # native PennyLane operations also native to qiskit "PauliX": lib.XGate, @@ -1218,3 +1223,74 @@ def _expr_eval_clvals(clbits, clvals, expr_func, bitwise=False): condition_res = expr_func(meas1, clreg2) return condition_res + + +def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: + """Loads a PennyLane `NoiseModel `_ + from a Qiskit `noise model `_. + + Args: + noise_model (qiskit_aer.noise.NoiseModel): a Qiskit noise model object + kwargs: optional keyword arguments for the conversion of the noise model + + Keyword Arguments: + verbose (bool): show a complete list of Kraus matrices for ``qml.QubitChannel`` instead of + number of Kraus matrices and the number of qubits they act on. The default is ``False`` + decimal_places (int): number of decimal places to round the Kraus matrices when they are + being displayed for each ``qml.QubitChannel`` with ``verbose=False``. + + Returns: + pennylane.NoiseModel: An equivalent noise model constructed in PennyLane + + Raises: + ValueError: When an encountered quantum error cannot be converted. + + .. note:: + + Currently, PennyLane noise models do not support readout errors, so those will be skipped during conversion. + + **Example** + + Consider the following noise model constructed in Qiskit: + + >>> import qiskit_aer.noise as noise + >>> error_1 = noise.depolarizing_error(0.001, 1) # 1-qubit noise + >>> error_2 = noise.depolarizing_error(0.01, 2) # 2-qubit noise + >>> noise_model = noise.NoiseModel() + >>> noise_model.add_all_qubit_quantum_error(error_1, ['rz', 'ry']) + >>> noise_model.add_all_qubit_quantum_error(error_2, ['cx']) + + This noise model can be converted into PennyLane using: + + >>> load_noise_model(noise_model) + NoiseModel({ + OpIn(['RZ', 'RY']): QubitChannel(num_kraus=4, num_wires=1) + OpIn(['CNOT']): QubitChannel(num_kraus=16, num_wires=2) + }) + """ + # Build model maps for quantum error and readout errors in the noise model + qerror_dmap, _ = _build_noise_model_map(noise_model) + model_map = {} + for error, wires_map in qerror_dmap.items(): + conditions = [] + for wires, operations in wires_map.items(): + cond = qml.noise.op_in(operations) + if wires != AnyWires: + cond &= WiresIn(wires) + conditions.append(cond) + fcond = reduce(lambda cond1, cond2: cond1 | cond2, conditions) + + noise = qml.noise.partial_wires(error) + if isinstance(error, qml.QubitChannel) and not kwargs.get("verbose", False): + kraus_shape = qml.math.shape(error.data) + num_kraus, num_wires = kraus_shape[0], int(np.log2(kraus_shape[1])) + noise = _rename(f"QubitChannel(num_kraus={num_kraus}, num_wires={num_wires})")(noise) + + if isinstance(error, qml.QubitChannel) and kwargs.get("verbose", False): + if (decimals := kwargs.get("decimal_places", None)) is not None: + kraus_matrices = list(np.round(error.data, decimals=decimals)) + noise = _rename(f"QubitChannel(Klist={kraus_matrices})")(noise) + + model_map[fcond] = noise + + return qml.NoiseModel(model_map) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py new file mode 100644 index 00000000..517d0853 --- /dev/null +++ b/pennylane_qiskit/noise_models.py @@ -0,0 +1,133 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +r""" +This module contains functions for converting Qiskit NoiseModel objects +into PennyLane NoiseModels. +""" +from collections import defaultdict +from typing import Tuple +from warnings import warn + +import pennylane as qml +from pennylane.operation import AnyWires +from qiskit.quantum_info.operators.channel import Kraus + +# pylint:disable = protected-access +qiskit_op_map = { + "x": "X", + "y": "Y", + "z": "Z", + "h": "Hadamard", + "cx": "CNOT", + "cz": "CZ", + "swap": "SWAP", + "iswap": "ISWAP", + "rx": "RX", + "ry": "RY", + "rz": "RZ", + "id": "Identity", + "cswap": "CSWAP", + "crx": "CRX", + "cry": "CRY", + "crz": "CRZ", + "p": "PhaseShift", + "ccx": "Toffoli", + "qubitunitary": "QubitUnitary", + "u1": "U1", + "u2": "U2", + "u3": "U3", + "rzz": "IsingZZ", + "ryy": "IsingYY", + "rxx": "IsingXX", + "s": "S", + "t": "T", + "sx": "SX", + "cy": "CY", + "ch": "CH", + "cp": "CPhase", + "ccz": "CCZ", + "ecr": "ECR", + "sdg": qml.adjoint(qml.S), + "tdg": qml.adjoint(qml.T), + "sxdg": qml.adjoint(qml.SX), + "reset": qml.measure(AnyWires, reset=True), # TODO: Improve reset support +} + + +def _build_qerror_op(error) -> qml.QubitChannel: + """Builds a PennyLane error channel from a Qiksit ``QuantumError`` object. + + Args: + error (QuantumError): Quantum error object + + Returns: + qml.QubitChannel: an equivalent PennyLane error channel + """ + try: + kraus_matrices = Kraus(error).data + except Exception as exc: # pragma: no cover + raise ValueError(f"Error {error} could not be converted.") from exc + + return qml.QubitChannel(K_list=kraus_matrices, wires=AnyWires) + + +def _build_noise_model_map(noise_model) -> Tuple[dict, dict]: + """Builds a noise model map from a Qiskit noise model. This noise model map can be used + to efficiently construct a PennyLane noise model. + + Args: + noise_model (qiskit_aer.noise.NoiseModel): Qiskit's noise model + + Returns: + (dict, dict): returns mappings for the given quantum errors and readout errors in the ``noise_model``. + + For plugin developers: noise model map tuple consists of following two (nested) mappings for + quantum errors (qerror_dmap) and readout errors (rerror_dmap): + * qerror_dmap: noise_operation -> wires -> target_gate + + .. code-block:: python + + qerror_dmap = { + noise_op1: { + AnyWires: [qiskit_op1, qiskit_op2], + (0, 1): [qiskit_op3], + (2,): [qiskit_op4] + }, + noise_op2: { + AnyWires: [qiskit_op5], + (1, 2): [qiskit_op6, qiskit_op7] + } + } + + * rerror_dmap: noise_operation -> wires -> target_measurement + """ + qerror_dmap = defaultdict(lambda: defaultdict(list)) + + # Add default quantum errors + for gate_name, error in noise_model._default_quantum_errors.items(): + noise_op = _build_qerror_op(error) + qerror_dmap[noise_op][AnyWires].append(qiskit_op_map[gate_name]) + + # Add specific qubit errors + for gate_name, qubit_dict in noise_model._local_quantum_errors.items(): + for qubits, error in qubit_dict.items(): + noise_op = _build_qerror_op(error) + qerror_dmap[noise_op][qubits].append(qiskit_op_map[gate_name]) + + # TODO: Add support for the readout error + rerror_dmap = defaultdict(lambda: defaultdict(list)) + if noise_model._default_readout_error or noise_model._local_readout_errors: + warn("Readout errors are not supported currently and will be skipped.") + + return qerror_dmap, rerror_dmap diff --git a/setup.py b/setup.py index 9eea781c..b8087b84 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ 'pennylane.io': [ 'qiskit = pennylane_qiskit:load', 'qiskit_op = pennylane_qiskit:load_pauli_op', + 'qiskit_noise = pennylane_qiskit:load_noise_model', 'qasm = pennylane_qiskit:load_qasm', 'qasm_file = pennylane_qiskit:load_qasm_from_file', ], diff --git a/tests/test_converter.py b/tests/test_converter.py index ba61ab33..a268e897 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -17,6 +17,9 @@ import sys from typing import cast +import itertools as it +import functools as ft + import pytest from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.circuit import library as lib @@ -25,16 +28,20 @@ from qiskit.circuit.library import DraperQFTAdder from qiskit.circuit.parametervector import ParameterVectorElement from qiskit.quantum_info import SparsePauliOp +from qiskit.quantum_info.operators.channel import Kraus import pennylane as qml from pennylane import I, X, Y, Z from pennylane import numpy as np -from pennylane.tape.qscript import QuantumScript from pennylane.measurements import MidMeasureMP +from pennylane.noise import op_in, wires_in, partial_wires +from pennylane.operation import AnyWires +from pennylane.tape.qscript import QuantumScript from pennylane.wires import Wires from pennylane_qiskit.converter import ( load, load_pauli_op, + load_noise_model, load_qasm, load_qasm_from_file, map_wires, @@ -2580,3 +2587,120 @@ def test_convert_with_invalid_operator(self): match = "The operator 123 is not a valid Qiskit SparsePauliOp." with pytest.raises(ValueError, match=match): load_pauli_op(123) + + +# pylint:disable = import-outside-toplevel, too-few-public-methods +class TestLoadNoiseModel: + """Tests for :func:`load_noise_models()` function.""" + + @staticmethod + def _kraus_to_choi(krau_mats, optimize=False) -> np.ndarray: + r"""Transforms Kraus representation of a channel to its Choi representation.""" + kraus_vecs = np.array([kraus.ravel(order="F") for kraus in krau_mats]) + return np.einsum("ij,ik->jk", kraus_vecs, kraus_vecs.conj(), optimize=optimize) + + def test_build_noise_model(self): + """Tests that ``load_quantum_noise`` constructs a correct PennyLane NoiseModel from a given Qiskit noise model""" + from qiskit_aer import noise + from qiskit.providers.fake_provider import FakeOpenPulse2Q + + noise_model = noise.NoiseModel.from_backend(FakeOpenPulse2Q()) + loaded_noise_model = load_noise_model(noise_model) + + pl_model_map = { + op_in("Identity") + & wires_in(0): qml.ThermalRelaxationError( + pe=0.0, t1=26981.9403362283, t2=26034.6676428009, tg=1.0, wires=AnyWires + ), + op_in("Identity") + & wires_in(1): qml.ThermalRelaxationError( + pe=0.0, t1=30732.034088541, t2=28335.6514829973, tg=1.0, wires=AnyWires + ), + (op_in("U1") & wires_in(0)) + | (op_in("U1") & wires_in(1)): qml.DepolarizingChannel( + p=0.08999999999999997, wires=AnyWires + ), + op_in("U2") + & wires_in(0): qml.ThermalRelaxationError( + pe=0.4998455776, t1=7.8227384666, t2=7.8226559459, tg=1.0, wires=AnyWires + ), + op_in("U2") + & wires_in(1): qml.ThermalRelaxationError( + pe=0.4998644198, t1=7.8227957211, t2=7.8226273195, tg=1.0, wires=AnyWires + ), + op_in("U3") + & wires_in(0): qml.ThermalRelaxationError( + pe=0.4996911588, t1=7.8227934813, t2=7.8226284393, tg=1.0, wires=AnyWires + ), + op_in("U3") + & wires_in(1): qml.ThermalRelaxationError( + pe=0.4997288404, t1=7.8229079927, t2=7.8225711871, tg=1.0, wires=AnyWires + ), + op_in("CNOT") + & wires_in([0, 1]): qml.QubitChannel( + Kraus(noise_model._local_quantum_errors["cx"][(0, 1)]).data, + wires=AnyWires, + ), + } + + pl_noise_model = qml.NoiseModel( + {fcond: partial_wires(noise) for fcond, noise in pl_model_map.items()} + ) + + for (pl_k, pl_v), (qk_k, qk_v) in zip( + pl_noise_model.model_map.items(), loaded_noise_model.model_map.items() + ): + pl_op, qk_op = pl_v(AnyWires), qk_v(AnyWires) + assert repr(pl_k) == repr(qk_k) + assert isinstance(qk_op, qml.QubitChannel) + + choi_mat1 = self._kraus_to_choi(qk_op.data) + choi_mat2 = self._kraus_to_choi(pl_op.compute_kraus_matrices(*pl_op.data)) + assert np.allclose(choi_mat1, choi_mat2) + + @pytest.mark.parametrize( + "verbose, decimal", + [(True, 8), (False, None)], + ) + def test_build_noise_model_with_kwargs(self, verbose, decimal): + """Tests that ``load_quantum_noise`` constructs a correct PennyLane NoiseModel with kwargs""" + from qiskit_aer import noise + + error_1 = noise.depolarizing_error(0.001, 1) + error_2 = noise.depolarizing_error(0.01, 2) + + noise_model = noise.NoiseModel() + noise_model.add_all_qubit_quantum_error(error_1, ["rz", "ry"]) + noise_model.add_all_qubit_quantum_error(error_2, ["cx"]) + loaded_noise_model = load_noise_model(noise_model, verbose=verbose, decimal_places=decimal) + + pauli_mats1 = list(map(qml.matrix, [qml.I(0), qml.X(0), qml.Y(0), qml.Z(0)])) + pauli_mats2 = list( + ft.reduce(np.kron, prod, 1.0) for prod in it.product(pauli_mats1, repeat=2) + ) + pauli_prob1 = np.sqrt(error_1.probabilities) + pauli_prob2 = np.sqrt(error_2.probabilities) + kraus_ops1 = [prob * kraus_op for prob, kraus_op in zip(pauli_prob1, pauli_mats1)] + kraus_ops2 = [prob * kraus_op for prob, kraus_op in zip(pauli_prob2, pauli_mats2)] + + c0 = qml.noise.op_in([qml.RZ, qml.RY]) + c1 = qml.noise.op_in(qml.CNOT) + n0 = qml.noise.partial_wires(qml.QubitChannel(kraus_ops1, wires=[0])) + n1 = qml.noise.partial_wires(qml.QubitChannel(kraus_ops2, wires=[0, 1])) + pl_noise_model = qml.NoiseModel({c0: n0, c1: n1}) + + for (pl_k, pl_v), (qk_k, qk_v) in zip( + pl_noise_model.model_map.items(), loaded_noise_model.model_map.items() + ): + assert repr(pl_k) == repr(qk_k) + + pl_data = np.array(pl_v(AnyWires).data) + if verbose: + choi_mat1 = self._kraus_to_choi(qk_v(AnyWires).data) + choi_mat2 = self._kraus_to_choi(pl_data) + assert np.allclose(choi_mat1, choi_mat2) + else: + num_kraus, num_wires = pl_data.shape[0], int(np.log2(pl_data.shape[1])) + assert ( + qk_v.__name__ == f"QubitChannel(num_kraus={num_kraus}, num_wires={num_wires})" + ) diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py new file mode 100644 index 00000000..9e91f9ed --- /dev/null +++ b/tests/test_noise_models.py @@ -0,0 +1,230 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. +r""" +This module contains tests for converting Qiskit NoiseModels to PennyLane NoiseModels. +""" +from functools import reduce + +import itertools as it +import pytest +import numpy as np +import pennylane as qml +from pennylane.operation import AnyWires + +# pylint:disable = wrong-import-position, unnecessary-lambda +qiksit = pytest.importorskip("qiskit", "1.0.0") +from qiskit_aer import noise +from qiskit.quantum_info.operators.channel import Kraus + +from pennylane_qiskit.noise_models import ( + _build_noise_model_map, + _build_qerror_op, +) + + +class TestLoadNoiseChannels: + """Tests for the helper methods of :func:`load_noise_models()` function.""" + + @staticmethod + def _kraus_to_choi(krau_mats, optimize=False) -> np.ndarray: + r"""Transforms Kraus representation of a channel to its Choi representation.""" + kraus_vecs = np.array([kraus.ravel(order="F") for kraus in krau_mats]) + return np.einsum("ij,ik->jk", kraus_vecs, kraus_vecs.conj(), optimize=optimize) + + @pytest.mark.parametrize( + "qiskit_error, pl_channel", + [ + ( + noise.amplitude_damping_error(0.123, 0.0), + qml.AmplitudeDamping(0.123, wires=AnyWires), + ), + (noise.phase_damping_error(0.123), qml.PhaseDamping(0.123, wires=AnyWires)), + ( + noise.phase_amplitude_damping_error(0.0345, 0.0), + qml.AmplitudeDamping(0.0345, wires=AnyWires), + ), + ( + noise.phase_amplitude_damping_error(0.0, 0.0345), + qml.PhaseDamping(0.0345, wires=AnyWires), + ), + (noise.reset_error(0.02789), qml.ResetError(0.02789, 0.0, wires=AnyWires)), + (noise.reset_error(0.01364, 0.02789), qml.ResetError(0.01364, 0.02789, wires=AnyWires)), + ( + noise.thermal_relaxation_error(0.25, 0.45, 1.0, 0.01), + qml.ThermalRelaxationError(0.01, 0.25, 0.45, 1.0, wires=AnyWires), + ), + ( + noise.thermal_relaxation_error(0.45, 0.25, 1.0, 0.01), + qml.ThermalRelaxationError(0.01, 0.45, 0.25, 1.0, wires=AnyWires), + ), + ( + noise.depolarizing_error(0.3264, 1), + qml.DepolarizingChannel(0.3264 * 3 / 4, wires=AnyWires), + ), + ( + noise.pauli_error([("X", 0.1), ("I", 0.9)]), + qml.BitFlip(0.1, wires=AnyWires), + ), + ( + noise.pauli_error([("Y", 0.178), ("I", 0.822)]), + qml.PauliError("Y", 0.178, wires=AnyWires), + ), + ( + noise.coherent_unitary_error(qml.X(0).matrix()), + qml.QubitChannel([qml.X(0).matrix()], wires=AnyWires), + ), + ( + noise.mixed_unitary_error( + [(qml.I(0).matrix(), 0.9), (qml.X(0).matrix(), 0.03), (qml.Y(0).matrix(), 0.07)] + ), + qml.QubitChannel( + [ + np.sqrt(prob) * kraus_op(0).matrix() + for kraus_op, prob in [ + (qml.X, 0.03), + (qml.Y, 0.07), + (qml.I, 0.9), + ] + ], + wires=AnyWires, + ), + ), + ( + noise.depolarizing_error(0.2174, 2), + qml.QubitChannel( + Kraus(noise.depolarizing_error(0.2174, 2)).data, + wires=[0, 1], + ), + ), + ( + noise.phase_amplitude_damping_error(0.3451, 0.2356), + qml.QubitChannel( + np.array( + [ + [[-0.97035755 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, -0.74564448 + 0.0j]], + [[-0.2416738 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 0.31450645 + 0.0j]], + [[0.0 + 0.0j, 0.58745213 + 0.0j], [0.0 + 0.0j, 0.0 + 0.0j]], + ] + ), + wires=AnyWires, + ), + ), + ( + noise.kraus_error( + [ + [[-0.97035755 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, -0.74564448 + 0.0j]], + [[-0.2416738 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 0.31450645 + 0.0j]], + [[0.0 + 0.0j, 0.58745213 + 0.0j], [0.0 + 0.0j, 0.0 + 0.0j]], + ], + ), + qml.QubitChannel( + np.array( + [ + [[-0.97035755 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, -0.74564448 + 0.0j]], + [[-0.2416738 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, 0.31450645 + 0.0j]], + [[0.0 + 0.0j, 0.58745213 + 0.0j], [0.0 + 0.0j, 0.0 + 0.0j]], + ] + ), + wires=AnyWires, + ), + ), + ], + ) + def test_build_kraus_error_ops(self, qiskit_error, pl_channel): + """Tests that a quantum error can be correctly converted into a PennyLane QubitChannel.""" + pl_op_from_qiskit = _build_qerror_op(qiskit_error) + choi_mat1 = self._kraus_to_choi( + pl_op_from_qiskit.compute_kraus_matrices(*pl_op_from_qiskit.data) + ) + choi_mat2 = self._kraus_to_choi(pl_channel.compute_kraus_matrices(*pl_channel.data)) + assert np.allclose(choi_mat1, choi_mat2) + + @pytest.mark.parametrize( + "depol1, depol2, exc_pop", + [ + (0.123, 0.456, 0.414), + (0.631, 0.729, 0.128), + (0.384, 0.657, 0.902), + ], + ) + def test_build_model_map(self, depol1, depol2, exc_pop): + """Tests that _build_noise_model_map constructs correct model map for a Qiskit noise model""" + error_1 = noise.depolarizing_error(depol1, 1) + error_2 = noise.depolarizing_error(depol2, 2) + error_3 = noise.phase_amplitude_damping_error(0.14, 0.24, excited_state_population=exc_pop) + + noise_model = noise.NoiseModel() + noise_model.add_all_qubit_quantum_error(error_1, ["rz", "sx", "x"]) + noise_model.add_all_qubit_quantum_error(error_2, ["cx"]) + noise_model.add_all_qubit_quantum_error(error_3, ["ry", "rx"]) + + qerror_dmap, _ = _build_noise_model_map(noise_model) + + pl_channels = [ + qml.QubitChannel( + qml.DepolarizingChannel.compute_kraus_matrices(3 * depol1 / 4), + wires=AnyWires, + ), + qml.QubitChannel( + [ + np.sqrt(prob) * reduce(np.kron, prod, 1.0) + for prob, prod in zip( + [1 - 15 * depol2 / 16, *([depol2 / 16] * 15)], + it.product( + map(qml.matrix, [qml.I(0), qml.X(0), qml.Y(0), qml.Z(0)]), repeat=2 + ), + ) + ], + wires=AnyWires, + ), + qml.QubitChannel( + qml.ThermalRelaxationError.compute_kraus_matrices( + exc_pop, 6.6302933312, 4.1837870638, 1.0 + ), + wires=AnyWires, + ), + ] + for key, channel in zip(list(qerror_dmap.keys()), pl_channels): + choi_mat1 = self._kraus_to_choi(key.data) + choi_mat2 = self._kraus_to_choi(channel.data) + assert np.allclose(choi_mat1, choi_mat2) + + assert list(qerror_dmap.values()) == [ + {AnyWires: ["RZ", "SX", "X"]}, + {AnyWires: ["CNOT"]}, + {AnyWires: ["RY", "RX"]}, + ] + + @pytest.mark.parametrize( + "combination, p_error", + [ + (lambda err1, err2: err1.compose(err2), 0.052), + (lambda err1, err2: err1.tensor(err2), 0.037), + (lambda err1, err2: err1.expand(err2), 0.094), + ], + ) + def test_composition_error_ops(self, combination, p_error): + """Tests that a combination of quantum errors can be correctly converted into a PennyLane QubitChannel.""" + + bit_flip = noise.pauli_error([("X", p_error), ("I", 1 - p_error)]) + phase_flip = noise.pauli_error([("Z", p_error), ("I", 1 - p_error)]) + + combined_error = combination(bit_flip, phase_flip) + pl_op_from_qiskit = _build_qerror_op(combined_error) + + choi_mat1 = self._kraus_to_choi(Kraus(combined_error).data) + choi_mat2 = self._kraus_to_choi( + pl_op_from_qiskit.compute_kraus_matrices(*pl_op_from_qiskit.data) + ) + assert np.allclose(choi_mat1, choi_mat2)