From 7ea085aebd852b05e027d044fe602ee6ff1ad702 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 5 Jul 2024 12:21:41 -0400 Subject: [PATCH 01/36] initial helper functions --- pennylane_qiskit/noise_models.py | 264 +++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 pennylane_qiskit/noise_models.py diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py new file mode 100644 index 00000000..292d62f3 --- /dev/null +++ b/pennylane_qiskit/noise_models.py @@ -0,0 +1,264 @@ +# 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. +""" +import itertools as it + +from qiskit.quantum_info.operators.channel import Choi, Kraus +import pennylane as qml +import numpy as np + +# pylint:disable = protected-access + +kraus_indice_map = { + "PhaseDamping": ((0, 0, 3, 3), (0, 3, 0, 3)), + "AmplitudeDamping": ((0, 0, 2, 3, 3), (0, 3, 2, 0, 3)), + "ThermalRelaxationError": ((0, 0, 1, 2, 3, 3), (0, 3, 1, 2, 0, 3)), +} +pauli_error_map = {"X": "BitFlip", "Z": "PhaseFlip", "Y": "PauliError"} + + +def _kraus_to_choi(krau_op): + """Transform Kraus representation of a channel to its Choi representation.""" + kraus_l, kraus_r = krau_op._data + kraus_vecs1 = np.array([kraus.ravel(order="F") for kraus in kraus_l]) + kraus_vecs2 = kraus_vecs1 + if kraus_r is not None: + kraus_vecs2 = np.array([kraus.ravel(order="F") for kraus in kraus_r]) + return np.einsum("ij,ik->jk", kraus_vecs1, kraus_vecs2.conj()) + + +def _check_kraus_ops(kraus_mats): + """Checks parity for Qiskit's Kraus operations to existing PL channels.""" + choi_matrix = _kraus_to_choi(Kraus(kraus_mats)) # Kraus-independent Choi matrix + + if qml.math.shape(choi_matrix) == (4, 4): # PennyLane channels are single-qubit + + non_zero_indices = np.nonzero(choi_matrix.round(8)) + nz_values = choi_matrix[non_zero_indices] + + if len(nz_values) == 4 and np.allclose(non_zero_indices, kraus_indice_map["PhaseDamping"]): + if np.allclose(nz_values[[0, 3]], np.ones(2)) and np.isclose(*nz_values[[1, 2]]): + return (True, "PhaseDamping", np.round(1 - nz_values[1] ** 2, 10).real) + + if len(nz_values) == 5 and np.allclose( + non_zero_indices, kraus_indice_map["AmplitudeDamping"] + ): + if np.allclose([nz_values[0], np.sum(nz_values[[2, 4]])], np.ones(2)) and np.isclose( + *nz_values[[1, 3]] + ): + return (True, "AmplitudeDamping", np.round(nz_values[2], 10).real) + + if len(nz_values) == 6 and np.allclose( + non_zero_indices, kraus_indice_map["ThermalRelaxationError"] + ): + if np.allclose( + [np.sum(nz_values[[0, 2]]), np.sum(nz_values[[3, 5]])], np.ones(2) + ) and np.isclose(*nz_values[[1, 4]]): + pe = nz_values[2] / (nz_values[2] + nz_values[3]) + t1 = -1 / np.log(1 - nz_values[2] / pe).real + t2 = -1 / np.log(nz_values[1]) + return (True, "ThermalRelaxationError", np.round([pe, t1, t2, 1.0], 10).real) + + return (False, "QubitChannel", Kraus(Choi(choi_matrix)).data) + + +def _check_depolarization(error_dict): + """Checks parity for Qiskit's depolarization channel to that of PennyLane.""" + error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) + + if ( + list(it.chain(*error_dict["data"])) + == ["".join(tup) for tup in it.product(["I", "X", "Y", "Z"], repeat=num_wires)] + and len(set(error_dict["probs"])) == 2 + and set(error_wires) == {error_wires[0]} + ): + num_terms = 4**num_wires + id_factor = num_terms / (num_terms - 1) + prob_iden = error_dict["probs"][error_dict["data"].index(["I" * num_wires])] + param = id_factor * (1 - prob_iden) + return (True, param) + + return (False, 0.0) + + +def _build_error(error): + """Builds an error tuple from a Qiksit's QuantumError object""" + + error_repr = error.to_dict() + error_insts, error_probs = error_repr["instructions"], error_repr["probabilities"] + if len(error_insts) != len(error_probs): + raise ValueError( + f"Mismatch between instructions and the provided probabilities, got {error_insts} and {error_probs}" + ) + + error_dict = {"name": [], "wires": [], "data": [None] * len(error_insts), "probs": error_probs} + + for idx, einst in enumerate(error_insts): + e_name, e_wire, e_data = [], [], [] + for inst in einst: + inst_name = inst["name"] + e_name.append(inst_name[0].upper() if inst_name in ["id", "x", "y", "z"] else inst_name) + e_wire.append(tuple(inst["qubits"])) + e_data.append(error_probs[idx]) + + if inst_name in ["pauli", "kraus", "unitary"]: + e_data[-1] = inst["params"] + + if len(e_name) > 1 and e_name != ["reset", "X"]: + error_dict["name"] = ["kraus"] + error_dict["wires"] = [tuple(einst[0]["qubits"])] + error_dict["data"] = [Kraus(error).data] + break + + error_dict["name"].extend(e_name) + error_dict["wires"].extend(e_wire) + error_dict["data"][idx] = e_data[0] + + error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) + sorted_name = sorted(error_dict["name"]) + + if sorted_name[0] == "I" and sorted_name[1:] in ["X", "Y", "Z"] and len(sorted_name) == 2: + prob_pauli = error_dict["probs"][error_dict["name"].index(sorted_name[1])] + error_dict["name"] = pauli_error_map[sorted_name[1]] + error_dict["data"] = prob_pauli + error_dict["probs"] = prob_pauli + error_dict["wires"] = error_wires[0] + + elif sorted_name == ["I", "X", "Y", "Z"]: + error_dict["data"] = [["I"], ["X"], ["Y"], ["Z"]] + depol_flag, depol_param = _check_depolarization(error_dict) + if depol_flag: + error_dict["name"] = "DepolarizingChannel" + error_dict["data"] = depol_param * 3 / 4 + error_dict["probs"] = depol_param + else: + error_dict["name"] = "QubitChannel" + kraus_ops = ( + tup[0] @ tup[1] + for tup in it.product( + list(map(qml.matrix, [qml.I(0), qml.X(0), qml.Y(0), qml.Z(0)])), + repeat=num_wires, + ) + ) + error_dict["data"] = [ + np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) + ] + error_dict["wires"] = error_wires[0] + + elif set(sorted_name) == {"pauli"}: + depol_flag, depol_param = _check_depolarization(error_dict) + if depol_flag and len(error_wires[0]) == 1: + error_dict["name"] = "DepolarizingChannel" + error_dict["probs"] = depol_param + else: + error_dict["name"] = "QubitChannel" # "PauliError" + kraus_ops = ( + tup[0] @ tup[1] + for tup in it.product( + list(map(qml.matrix, [qml.I(0), qml.X(0), qml.Y(0), qml.Z(0)])), + repeat=num_wires, + ) + ) + error_dict["data"] = [ + np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) + ] + error_dict["wires"] = error_wires[0] + + elif sorted_name[0] == "I" and sorted_name[-1] == "reset": + if "Z" not in error_dict["name"]: + error_dict["name"] = "ResetError" + error_dict["data"] = error_probs[1:] + else: + error_dict["name"] = "ThermalRelaxationError" + pe = ( + 0.0 if len(error_probs) == 3 else error_probs[3] / (error_probs[2] + error_probs[3]) + ) + t1 = -1 / np.log(1 - error_probs[2] / (1 - pe)) + t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / (1 - pe)))) ** -1 + error_dict["data"] = list(np.round([pe, t1, t2, 1.0], 10)) + error_dict["wires"] = error_wires[0] + + elif sorted_name[0] == "kraus" and len(sorted_name) == 1: + kflag, kname, kdata = _check_kraus_ops(error_dict["data"][0]) + error_dict["name"] = kname if kflag else "QubitChannel" + error_dict["data"] = kdata + error_dict["wires"] = error_wires[0] + + elif "unitary" in sorted_name and ( + len(set(sorted_name)) == 1 or set(sorted_name) == {"unitary", "I"} + ): + error_dict["name"] = "QubitChannel" + kraus_ops = [ + op[0] if isinstance(op, list) else qml.I(0).matrix() * op for op in error_dict["data"] + ] + error_dict["data"] = [ + np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_probs, kraus_ops) + ] + error_dict["wires"] = error_wires[0] + + else: + error_dict = {"name": [], "wires": [], "data": [], "probs": []} + raise Warning(f"Error {error} could not be converted and will be skipped.") + + if error_dict["name"] == "PauliError": + error_dict["data"] = [error_dict["data"], error_dict["probs"]] + + if error_dict["name"] == "QubitChannel": + error_dict["data"] = [error_dict["data"]] + + if not hasattr(error_dict["data"], "__iter__"): + error_dict["data"] = [error_dict["data"]] + + error_dict.pop("probs", None) + + return error_dict + + +def _build_noise_model(noise_model): + error_list = [] + + # Add default quantum errors + for name, error in noise_model._default_quantum_errors.items(): + error_dict = _build_error(error) + error_list.append( + { + "operations": [name], + "noise_op": getattr(qml.ops, error_dict["name"])( + *error_dict["data"], wires=error_dict["wires"] + ), + } + ) + + # Add specific qubit errors + for name, qubit_dict in noise_model._local_quantum_errors.items(): + for qubits, error in qubit_dict.items(): + error_dict = _build_error(error) + error_list.append( + { + "operations": [name], + "gate_qubits": [qubits], + "noise_op": getattr(qml.ops, error_dict["name"])( + *error_dict["data"], wires=error_dict["wires"] + ), + } + ) + + # TODO: Add support for the readout error + if noise_model._default_readout_error or noise_model._local_readout_errors: + raise Warning(f"Readout errors {error} are not supported currently and will be skipped.") + + return error_list From aaa3ae9d2f308c79445826a59ec620e864a86eaa Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 5 Jul 2024 17:34:32 -0400 Subject: [PATCH 02/36] add conversion and coellection --- pennylane_qiskit/converter.py | 30 +++++++++++++++++++++ pennylane_qiskit/noise_models.py | 45 +++++++++++++++++--------------- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 7f7172e0..3a5d494d 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -34,8 +34,12 @@ import pennylane as qml import pennylane.ops as pennylane_ops +from pennylane.noise.conditionals import WiresIn, _rename +from pennylane.operation import AnyWires from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP +from .noise_models import _build_noise_model + # pylint: disable=too-many-instance-attributes inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} @@ -1042,3 +1046,29 @@ 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): + """Loads a PennyLane ``NoiseModel`` from a Qiskit `NoiseModel + `_.""" + + qerror_dmap, _ = _build_noise_model(noise_model) + model_map = {} + for error, wires_map in qerror_dmap.items(): + conditions = [] + cwires = [] + for wires, operations in wires_map.items(): + cond = qml.noise.op_in(operations) + if wires != AnyWires: + cond &= WiresIn(wires) + cwires.append(wires) + conditions.append(cond) + fcond = reduce(lambda cond1, cond2: cond1 | cond2, conditions) + + noise = qml.noise.partial_wires(error) + if isinstance(error, qml.QubitChannel): + noise = _rename(f"QubitChannel(Klist=Tensor{qml.math.shape(error.data)})")(noise) + + model_map[fcond] = noise + + return qml.NoiseModel(model_map) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 292d62f3..0a62d17b 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -15,11 +15,14 @@ This module contains functions for converting Qiskit NoiseModel objects into PennyLane NoiseModels. """ +from collections import defaultdict import itertools as it -from qiskit.quantum_info.operators.channel import Choi, Kraus -import pennylane as qml import numpy as np +import pennylane as qml +from pennylane.operation import AnyWires +from qiskit.quantum_info.operators.channel import Choi, Kraus + # pylint:disable = protected-access @@ -29,6 +32,18 @@ "ThermalRelaxationError": ((0, 0, 1, 2, 3, 3), (0, 3, 1, 2, 0, 3)), } pauli_error_map = {"X": "BitFlip", "Z": "PhaseFlip", "Y": "PauliError"} +qiskit_op_map = { + "cx": "CNOT", + "sx": "SX", + "id": "I", + "rx": "RX", + "ry": "RY", + "rz": "RZ", + "x": "X", + "y": "Y", + "z": "Z", + "reset": qml.measure(AnyWires, reset=True), +} def _kraus_to_choi(krau_op): @@ -229,36 +244,24 @@ def _build_error(error): def _build_noise_model(noise_model): - error_list = [] + qerror_dmap = defaultdict(lambda: defaultdict(list)) # Add default quantum errors for name, error in noise_model._default_quantum_errors.items(): error_dict = _build_error(error) - error_list.append( - { - "operations": [name], - "noise_op": getattr(qml.ops, error_dict["name"])( - *error_dict["data"], wires=error_dict["wires"] - ), - } - ) + noise_op = getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) + qerror_dmap[noise_op][AnyWires].append(qiskit_op_map[name]) # Add specific qubit errors for name, qubit_dict in noise_model._local_quantum_errors.items(): for qubits, error in qubit_dict.items(): error_dict = _build_error(error) - error_list.append( - { - "operations": [name], - "gate_qubits": [qubits], - "noise_op": getattr(qml.ops, error_dict["name"])( - *error_dict["data"], wires=error_dict["wires"] - ), - } - ) + noise_op = getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) + qerror_dmap[noise_op][qubits].append(qiskit_op_map[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: raise Warning(f"Readout errors {error} are not supported currently and will be skipped.") - return error_list + return qerror_dmap, rerror_dmap From a31c8e46af39671ac673ea729f6d9495912df7af Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 7 Jul 2024 23:05:18 -0400 Subject: [PATCH 03/36] add options --- pennylane_qiskit/noise_models.py | 140 +++++++++++++++++++------------ 1 file changed, 88 insertions(+), 52 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 0a62d17b..d1fba07c 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -16,6 +16,7 @@ into PennyLane NoiseModels. """ from collections import defaultdict +from functools import lru_cache, reduce import itertools as it import numpy as np @@ -42,8 +43,9 @@ "x": "X", "y": "Y", "z": "Z", - "reset": qml.measure(AnyWires, reset=True), + "reset": qml.measure(AnyWires, reset=True), # TODO: Improve reset support } +default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5)] def _kraus_to_choi(krau_op): @@ -56,50 +58,82 @@ def _kraus_to_choi(krau_op): return np.einsum("ij,ik->jk", kraus_vecs1, kraus_vecs2.conj()) -def _check_kraus_ops(kraus_mats): +def _check_kraus_ops(kraus_mats, **kwargs): """Checks parity for Qiskit's Kraus operations to existing PL channels.""" choi_matrix = _kraus_to_choi(Kraus(kraus_mats)) # Kraus-independent Choi matrix if qml.math.shape(choi_matrix) == (4, 4): # PennyLane channels are single-qubit + decimals, atol, rtol = tuple(kwargs.get(opt, dflt) for (opt, dflt) in default_option_map) - non_zero_indices = np.nonzero(choi_matrix.round(8)) + non_zero_indices = np.nonzero(choi_matrix.round(decimals)) nz_values = choi_matrix[non_zero_indices] - if len(nz_values) == 4 and np.allclose(non_zero_indices, kraus_indice_map["PhaseDamping"]): + if len(nz_values) == 4 and np.allclose( + non_zero_indices, + kraus_indice_map["PhaseDamping"], + rtol=rtol, + atol=atol, + ): if np.allclose(nz_values[[0, 3]], np.ones(2)) and np.isclose(*nz_values[[1, 2]]): - return (True, "PhaseDamping", np.round(1 - nz_values[1] ** 2, 10).real) + return (True, "PhaseDamping", np.round(1 - nz_values[1] ** 2, decimals).real) if len(nz_values) == 5 and np.allclose( - non_zero_indices, kraus_indice_map["AmplitudeDamping"] + non_zero_indices, + kraus_indice_map["AmplitudeDamping"], + rtol=rtol, + atol=atol, ): - if np.allclose([nz_values[0], np.sum(nz_values[[2, 4]])], np.ones(2)) and np.isclose( - *nz_values[[1, 3]] - ): - return (True, "AmplitudeDamping", np.round(nz_values[2], 10).real) + if np.allclose( + [nz_values[0], sum(nz_values[[2, 4]])], np.ones(2), rtol=rtol, atol=atol + ) and np.isclose(*nz_values[[1, 3]], rtol=rtol, atol=atol): + return (True, "AmplitudeDamping", np.round(nz_values[2], decimals).real) if len(nz_values) == 6 and np.allclose( - non_zero_indices, kraus_indice_map["ThermalRelaxationError"] + non_zero_indices, + kraus_indice_map["ThermalRelaxationError"], + rtol=rtol, + atol=atol, ): if np.allclose( - [np.sum(nz_values[[0, 2]]), np.sum(nz_values[[3, 5]])], np.ones(2) - ) and np.isclose(*nz_values[[1, 4]]): + [sum(nz_values[[0, 2]]), sum(nz_values[[3, 5]])], + np.ones(2), + rtol=rtol, + atol=atol, + ) and np.isclose( + *nz_values[[1, 4]], rtol=rtol, atol=atol + ): # uses t2 > t1 + tg = kwargs.get("gate_times", {}).get(kwargs["gate_name"], 1.0) pe = nz_values[2] / (nz_values[2] + nz_values[3]) - t1 = -1 / np.log(1 - nz_values[2] / pe).real - t2 = -1 / np.log(nz_values[1]) - return (True, "ThermalRelaxationError", np.round([pe, t1, t2, 1.0], 10).real) + t1 = -tg / np.log(1 - nz_values[2] / pe) + t2 = -tg / np.log(nz_values[1]) + return (True, "ThermalRelaxationError", np.round([pe, t1, t2, tg], decimals).real) return (False, "QubitChannel", Kraus(Choi(choi_matrix)).data) +@lru_cache +def _generate_product(items, repeat=1, matrix=False): + return tuple( + it.product( + ( + items + if not matrix + else tuple(map(qml.matrix, tuple(getattr(qml, i)(0) for i in items))) + ), + repeat=repeat, + ) + ) # TODO: Analyze speed gains with sparse matrices + + def _check_depolarization(error_dict): """Checks parity for Qiskit's depolarization channel to that of PennyLane.""" error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) if ( - list(it.chain(*error_dict["data"])) - == ["".join(tup) for tup in it.product(["I", "X", "Y", "Z"], repeat=num_wires)] - and len(set(error_dict["probs"])) == 2 + len(set(error_dict["probs"])) == 2 and set(error_wires) == {error_wires[0]} + and list(it.chain(*error_dict["data"])) + == ["".join(pauli) for pauli in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires)] ): num_terms = 4**num_wires id_factor = num_terms / (num_terms - 1) @@ -110,14 +144,13 @@ def _check_depolarization(error_dict): return (False, 0.0) -def _build_error(error): - """Builds an error tuple from a Qiksit's QuantumError object""" - +def _error_dict(error): + """Builds error dictionary from error""" error_repr = error.to_dict() error_insts, error_probs = error_repr["instructions"], error_repr["probabilities"] if len(error_insts) != len(error_probs): raise ValueError( - f"Mismatch between instructions and the provided probabilities, got {error_insts} and {error_probs}" + f"Mismatch between instructions and provided probabilities, got {error_insts} and {error_probs}" ) error_dict = {"name": [], "wires": [], "data": [None] * len(error_insts), "probs": error_probs} @@ -143,7 +176,15 @@ def _build_error(error): error_dict["wires"].extend(e_wire) error_dict["data"][idx] = e_data[0] + return error_dict + + +def _build_error(error, **kwargs): + """Builds an error tuple from a Qiksit's QuantumError object""" + error_dict = _error_dict(error) + error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) + error_probs = error_dict["probs"] sorted_name = sorted(error_dict["name"]) if sorted_name[0] == "I" and sorted_name[1:] in ["X", "Y", "Z"] and len(sorted_name) == 2: @@ -162,13 +203,10 @@ def _build_error(error): error_dict["probs"] = depol_param else: error_dict["name"] = "QubitChannel" - kraus_ops = ( - tup[0] @ tup[1] - for tup in it.product( - list(map(qml.matrix, [qml.I(0), qml.X(0), qml.Y(0), qml.Z(0)])), - repeat=num_wires, - ) - ) + kraus_ops = [ + reduce(lambda mat1, mat2: mat1 @ mat2, prod, np.eye(int(2**num_wires))) + for prod in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires, matrix=True) + ] error_dict["data"] = [ np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) ] @@ -180,14 +218,11 @@ def _build_error(error): error_dict["name"] = "DepolarizingChannel" error_dict["probs"] = depol_param else: - error_dict["name"] = "QubitChannel" # "PauliError" - kraus_ops = ( - tup[0] @ tup[1] - for tup in it.product( - list(map(qml.matrix, [qml.I(0), qml.X(0), qml.Y(0), qml.Z(0)])), - repeat=num_wires, - ) - ) + error_dict["name"] = "QubitChannel" # TODO: Make PauliError multi-qubit channel + kraus_ops = [ + reduce(lambda mat1, mat2: mat1 @ mat2, prod, np.eye(int(2**num_wires))) + for prod in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires, matrix=True) + ] error_dict["data"] = [ np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) ] @@ -197,18 +232,19 @@ def _build_error(error): if "Z" not in error_dict["name"]: error_dict["name"] = "ResetError" error_dict["data"] = error_probs[1:] - else: + else: # uses t1 > t2 error_dict["name"] = "ThermalRelaxationError" - pe = ( - 0.0 if len(error_probs) == 3 else error_probs[3] / (error_probs[2] + error_probs[3]) + tg = kwargs.get("gate_times", {}).get(kwargs["gate_name"], 1.0) + p0 = ( + 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) ) - t1 = -1 / np.log(1 - error_probs[2] / (1 - pe)) - t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / (1 - pe)))) ** -1 - error_dict["data"] = list(np.round([pe, t1, t2, 1.0], 10)) + t1 = -tg / np.log(1 - error_probs[2] / p0) + t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 + error_dict["data"] = list(np.round([1 - p0, t1, t2, tg], kwargs.get("decimals", 10))) error_dict["wires"] = error_wires[0] elif sorted_name[0] == "kraus" and len(sorted_name) == 1: - kflag, kname, kdata = _check_kraus_ops(error_dict["data"][0]) + kflag, kname, kdata = _check_kraus_ops(error_dict["data"][0], **kwargs) error_dict["name"] = kname if kflag else "QubitChannel" error_dict["data"] = kdata error_dict["wires"] = error_wires[0] @@ -243,21 +279,21 @@ def _build_error(error): return error_dict -def _build_noise_model(noise_model): +def _build_noise_model(noise_model, **kwargs): qerror_dmap = defaultdict(lambda: defaultdict(list)) # Add default quantum errors - for name, error in noise_model._default_quantum_errors.items(): - error_dict = _build_error(error) + for gate_name, error in noise_model._default_quantum_errors.items(): + error_dict = _build_error(error, gate_name=gate_name, **kwargs) noise_op = getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) - qerror_dmap[noise_op][AnyWires].append(qiskit_op_map[name]) + qerror_dmap[noise_op][AnyWires].append(qiskit_op_map[gate_name]) # Add specific qubit errors - for name, qubit_dict in noise_model._local_quantum_errors.items(): + for gate_name, qubit_dict in noise_model._local_quantum_errors.items(): for qubits, error in qubit_dict.items(): - error_dict = _build_error(error) + error_dict = _build_error(error, gate_name=gate_name, **kwargs) noise_op = getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) - qerror_dmap[noise_op][qubits].append(qiskit_op_map[name]) + qerror_dmap[noise_op][qubits].append(qiskit_op_map[gate_name]) # TODO: Add support for the readout error rerror_dmap = defaultdict(lambda: defaultdict(list)) From a23a8e3394230c9bafb12538f10b3b0c858f5453 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 8 Jul 2024 10:20:22 -0400 Subject: [PATCH 04/36] add documentation --- pennylane_qiskit/converter.py | 45 +++++++--- pennylane_qiskit/noise_models.py | 149 ++++++++++++++++++++++++------- 2 files changed, 150 insertions(+), 44 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 3a5d494d..c5b15af2 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -15,15 +15,19 @@ This module contains functions for converting Qiskit QuantumCircuit objects into PennyLane circuit templates. """ -from typing import Dict, Any, Iterable, Sequence, Union import warnings from functools import partial, reduce +from typing import Any, Dict, Iterable, Sequence, Union import numpy as np +import pennylane as qml +import pennylane.ops as pennylane_ops import qiskit.qasm2 +from pennylane.noise.conditionals import WiresIn, _rename +from pennylane.operation import AnyWires from qiskit import QuantumCircuit +from qiskit.circuit import Barrier, Clbit, ControlFlowOp, Measure from qiskit.circuit import Parameter, ParameterExpression, ParameterVector -from qiskit.circuit import Measure, Barrier, ControlFlowOp, Clbit from qiskit.circuit.classical import expr from qiskit.circuit.controlflow.switch_case import _DefaultCaseType from qiskit.circuit.library import GlobalPhaseGate @@ -32,13 +36,9 @@ from qiskit.quantum_info import SparsePauliOp from sympy import lambdify -import pennylane as qml -import pennylane.ops as pennylane_ops -from pennylane.noise.conditionals import WiresIn, _rename -from pennylane.operation import AnyWires from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP -from .noise_models import _build_noise_model +from .noise_models import _build_noise_model_map # pylint: disable=too-many-instance-attributes @@ -1048,11 +1048,36 @@ def _expr_eval_clvals(clbits, clvals, expr_func, bitwise=False): return condition_res -def load_noise_model(noise_model): +def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: """Loads a PennyLane ``NoiseModel`` from a Qiskit `NoiseModel - `_.""" + `_. + + Args: + noise_model (qiskit_aer.noise.NoiseModel): A Qiskit noise model object + kwargs: Optional keyword arguments for conversion of the noise model. + + Keyword Arguments: + gate_times (Dict[str, float]): gate times for building thermal relaxation error. + If not provided, the default value of ``1.0`` will be used for construction. + decimals: number of decimal places to round the Kraus matrices for errors to. + If not provided, the default value of ``10`` is used. + atol: the relative tolerance parameter. Default value is ``1e-05``. + rtol: the absolute tolernace parameters. Defualt value is ``1e-08``. + optimize: controls if intermediate optimization is used while transforming Kraus + operators to a Choi matrix, wherever required. Default is ``False``. + + Returns: + qml.NoiseModel: An equivalent noise model constructed in PennyLane + + Raises: + ValueError: When an encountered quantum error cannoted be converted. + + .. note:: + + Currently, PennyLane noise models does not support readout errors, so those will be skipped during conversion. + """ - qerror_dmap, _ = _build_noise_model(noise_model) + qerror_dmap, _ = _build_noise_model_map(noise_model, **kwargs) model_map = {} for error, wires_map in qerror_dmap.items(): conditions = [] diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index d1fba07c..5b52d93c 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -15,15 +15,17 @@ This module contains functions for converting Qiskit NoiseModel objects into PennyLane NoiseModels. """ +import itertools as it from collections import defaultdict from functools import lru_cache, reduce -import itertools as it +from typing import List, Tuple, Union +from warnings import warn import numpy as np import pennylane as qml from pennylane.operation import AnyWires from qiskit.quantum_info.operators.channel import Choi, Kraus - +from qiskit_aer.noise import NoiseModel, QuantumError # pylint:disable = protected-access @@ -45,22 +47,52 @@ "z": "Z", "reset": qml.measure(AnyWires, reset=True), # TODO: Improve reset support } -default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5)] +default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5), ("optimize", False)] + + +def _kraus_to_choi(krau_op: Kraus, optimize=False) -> np.ndarray: + r"""Transforms Kraus representation of a channel to its Choi representation. + + Quantum channels are generally defined by Kraus operators :math:`{K_i}`, which + unfortunately do not provide a unique description of the channel. In contrast, + the Choi matrix (\Lambda) computed from any such Kraus representation will + always be unique and can be used unambiguosly to obtain represent a channel. + + .. math:: + + \Lambda = \sum_{i, j} \vert i \rangle \langle j \vert \otimes \sum_k K_k \vert i \rangle \langle j K_k^\dagger + Args: + krau_op (Kraus): A Qiskit's Kraus operator that defines a channel. + optimize (bool): Use intermediate ``einsum`` optimization. -def _kraus_to_choi(krau_op): - """Transform Kraus representation of a channel to its Choi representation.""" + Returns: + Choi matrix of the quantum channel defined by given Kraus operators. + + For plugin developers: This has a runtime cost of :math:`O(\#K * D^4)`, where :math:`\#K` are the + number of Kraus operators, and :math:`D` is the dimensions of the transformed Hilbert space. + """ kraus_l, kraus_r = krau_op._data kraus_vecs1 = np.array([kraus.ravel(order="F") for kraus in kraus_l]) kraus_vecs2 = kraus_vecs1 if kraus_r is not None: kraus_vecs2 = np.array([kraus.ravel(order="F") for kraus in kraus_r]) - return np.einsum("ij,ik->jk", kraus_vecs1, kraus_vecs2.conj()) + return np.einsum("ij,ik->jk", kraus_vecs1, kraus_vecs2.conj(), optimize=optimize) + +def _check_kraus_ops( + kraus_mats: List[np.ndarray], **kwargs +) -> Tuple[bool, str, Union[float, np.ndarray, Kraus]]: + """Checks parity for Qiskit's Kraus operations to the existing PennyLane channels. -def _check_kraus_ops(kraus_mats, **kwargs): - """Checks parity for Qiskit's Kraus operations to existing PL channels.""" - choi_matrix = _kraus_to_choi(Kraus(kraus_mats)) # Kraus-independent Choi matrix + This helper method constructs an unique representation of the quantum channel via its Choi matrix and + then uses it to map them to the following existing PennyLane Channels such as ``PhaseDamping``, + ``AmplitudeDamping``, ``ThermalRelaxation`` and ``QubitChannel``. + + Args: + kraus_mats (List(tensor)): list of Kraus operators defining a quantum channel + """ + choi_matrix = _kraus_to_choi(Kraus(kraus_mats), optimize=kwargs.get("optimize", False)) if qml.math.shape(choi_matrix) == (4, 4): # PennyLane channels are single-qubit decimals, atol, rtol = tuple(kwargs.get(opt, dflt) for (opt, dflt) in default_option_map) @@ -112,7 +144,8 @@ def _check_kraus_ops(kraus_mats, **kwargs): @lru_cache -def _generate_product(items, repeat=1, matrix=False): +def _generate_product(items: Tuple, repeat: int = 1, matrix: bool = False) -> Tuple: + """Helper method to generate product for Pauli terms and matrices efficiently.""" return tuple( it.product( ( @@ -125,8 +158,16 @@ def _generate_product(items, repeat=1, matrix=False): ) # TODO: Analyze speed gains with sparse matrices -def _check_depolarization(error_dict): - """Checks parity for Qiskit's depolarization channel to that of PennyLane.""" +def _check_depolarization(error_dict: dict) -> Tuple[bool, float]: + """Checks parity for Qiskit's depolarization channel to that of PennyLane. + + Args: + error_dict (dict): error dictionary for the quantum error + + Returns: + (bool, float): A tuple representing whether the encountered quantum error + is a depolarization error and the related probability. + """ error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) if ( @@ -144,11 +185,25 @@ def _check_depolarization(error_dict): return (False, 0.0) -def _error_dict(error): - """Builds error dictionary from error""" +def _build_qerror_dict(error: QuantumError) -> dict[str, Union[float, int]]: + """Builds error dictionary for post-processing from Qiskit's error object. + + Args: + error (dict): error dictionary for the quantum error + + Returns: + Tuple[bool, float]: A tuple representing whether the encountered quantum error + is a depolarization error and the related probability. + + For plugin developers: the build dictionary representation help stores the following: + * name - Qiskit's standard name for the encountered quantum error. + * wires - Wires on which the error acts. + * data - Data from the quantum error required by PennyLane for reconstruction. + * probs - Probabilities for the instructions in a quantum error. + """ error_repr = error.to_dict() error_insts, error_probs = error_repr["instructions"], error_repr["probabilities"] - if len(error_insts) != len(error_probs): + if len(error_insts) != len(error_probs): # pragma: no cover raise ValueError( f"Mismatch between instructions and provided probabilities, got {error_insts} and {error_probs}" ) @@ -179,9 +234,18 @@ def _error_dict(error): return error_dict -def _build_error(error, **kwargs): - """Builds an error tuple from a Qiksit's QuantumError object""" - error_dict = _error_dict(error) +def _build_qerror_op(error: QuantumError, **kwargs) -> qml.operation.Operation: + """Builds an PennyLane error operation from a Qiksit's QuantumError object. + + Args: + error (QuantumError): Quantum error object + kwargs: Optional keyword arguments used during conversion + + Returns: + qml.operation.Channel: converted PennyLane quantum channel which is + theoretically equivalent to the given Qiksit's QuantumError object + """ + error_dict = _build_qerror_dict(error) error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) error_probs = error_dict["probs"] @@ -192,7 +256,6 @@ def _build_error(error, **kwargs): error_dict["name"] = pauli_error_map[sorted_name[1]] error_dict["data"] = prob_pauli error_dict["probs"] = prob_pauli - error_dict["wires"] = error_wires[0] elif sorted_name == ["I", "X", "Y", "Z"]: error_dict["data"] = [["I"], ["X"], ["Y"], ["Z"]] @@ -210,7 +273,6 @@ def _build_error(error, **kwargs): error_dict["data"] = [ np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) ] - error_dict["wires"] = error_wires[0] elif set(sorted_name) == {"pauli"}: depol_flag, depol_param = _check_depolarization(error_dict) @@ -226,7 +288,6 @@ def _build_error(error, **kwargs): error_dict["data"] = [ np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) ] - error_dict["wires"] = error_wires[0] elif sorted_name[0] == "I" and sorted_name[-1] == "reset": if "Z" not in error_dict["name"]: @@ -241,13 +302,11 @@ def _build_error(error, **kwargs): t1 = -tg / np.log(1 - error_probs[2] / p0) t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 error_dict["data"] = list(np.round([1 - p0, t1, t2, tg], kwargs.get("decimals", 10))) - error_dict["wires"] = error_wires[0] elif sorted_name[0] == "kraus" and len(sorted_name) == 1: kflag, kname, kdata = _check_kraus_ops(error_dict["data"][0], **kwargs) error_dict["name"] = kname if kflag else "QubitChannel" error_dict["data"] = kdata - error_dict["wires"] = error_wires[0] elif "unitary" in sorted_name and ( len(set(sorted_name)) == 1 or set(sorted_name) == {"unitary", "I"} @@ -259,11 +318,9 @@ def _build_error(error, **kwargs): error_dict["data"] = [ np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_probs, kraus_ops) ] - error_dict["wires"] = error_wires[0] - else: - error_dict = {"name": [], "wires": [], "data": [], "probs": []} - raise Warning(f"Error {error} could not be converted and will be skipped.") + else: # pragma: no cover + raise ValueError(f"Error {error} could not be converted.") if error_dict["name"] == "PauliError": error_dict["data"] = [error_dict["data"], error_dict["probs"]] @@ -274,30 +331,54 @@ def _build_error(error, **kwargs): if not hasattr(error_dict["data"], "__iter__"): error_dict["data"] = [error_dict["data"]] + if error_dict["wires"]: + error_dict["wires"] = error_wires[0] + error_dict.pop("probs", None) - return error_dict + return getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) + + +def _build_noise_model_map(noise_model: NoiseModel, **kwargs) -> Tuple(dict, dict): + """Builds noise model maps from which noise model can be constructed efficiently. + + Args: + noise_model (NoiseModel): Qiskit's noise model + kwargs: Optional keyword arguments for providing extra information + + Keyword Arguments: + gate_times (Dict[str, float]): gate times for building thermal relaxation error. + If not provided, the default value of ``1.0`` will be used for construction. + decimals: number of decimal places to round the Kraus matrices for errors to. + If not provided, the default value of ``10`` is used. + atol: the relative tolerance parameter. Default value is ``1e-05``. + rtol: the absolute tolernace parameters. Defualt value is ``1e-08``. + optimize: controls if intermediate optimization is used while transforming Kraus + operators to a Choi matrix, wherever required. Default is ``False``. + Returns: + (dict, dict): returns mappings for ecountered quantum errors and readout errors. -def _build_noise_model(noise_model, **kwargs): + For plugin developers: noise model map tuple consists of following two mappings: + * qerror_dmap: noise_operation -> wires -> target_gate + * 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(): - error_dict = _build_error(error, gate_name=gate_name, **kwargs) - noise_op = getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) + noise_op = _build_qerror_op(error, gate_name=gate_name, **kwargs) 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(): - error_dict = _build_error(error, gate_name=gate_name, **kwargs) - noise_op = getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) + noise_op = _build_qerror_op(error, gate_name=gate_name, **kwargs) 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: - raise Warning(f"Readout errors {error} are not supported currently and will be skipped.") + warn(f"Readout errors {error} are not supported currently and will be skipped.") return qerror_dmap, rerror_dmap From 10768b973284a649cc90a23e9678d2398de5aa4e Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 8 Jul 2024 10:59:17 -0400 Subject: [PATCH 05/36] tweak docs --- pennylane_qiskit/noise_models.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 5b52d93c..c14350e3 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -25,7 +25,6 @@ import pennylane as qml from pennylane.operation import AnyWires from qiskit.quantum_info.operators.channel import Choi, Kraus -from qiskit_aer.noise import NoiseModel, QuantumError # pylint:disable = protected-access @@ -185,11 +184,11 @@ def _check_depolarization(error_dict: dict) -> Tuple[bool, float]: return (False, 0.0) -def _build_qerror_dict(error: QuantumError) -> dict[str, Union[float, int]]: +def _build_qerror_dict(error) -> dict[str, Union[float, int]]: """Builds error dictionary for post-processing from Qiskit's error object. Args: - error (dict): error dictionary for the quantum error + error (QuantumError): Quantum error object Returns: Tuple[bool, float]: A tuple representing whether the encountered quantum error @@ -234,7 +233,7 @@ def _build_qerror_dict(error: QuantumError) -> dict[str, Union[float, int]]: return error_dict -def _build_qerror_op(error: QuantumError, **kwargs) -> qml.operation.Operation: +def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: """Builds an PennyLane error operation from a Qiksit's QuantumError object. Args: @@ -339,11 +338,11 @@ def _build_qerror_op(error: QuantumError, **kwargs) -> qml.operation.Operation: return getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) -def _build_noise_model_map(noise_model: NoiseModel, **kwargs) -> Tuple(dict, dict): +def _build_noise_model_map(noise_model, **kwargs) -> Tuple(dict, dict): """Builds noise model maps from which noise model can be constructed efficiently. Args: - noise_model (NoiseModel): Qiskit's noise model + noise_model (qiskit_aer.noise.NoiseModel): Qiskit's noise model kwargs: Optional keyword arguments for providing extra information Keyword Arguments: From 0badd30c88b33927f6b290b819cea5597a01da93 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 8 Jul 2024 12:20:42 -0400 Subject: [PATCH 06/36] minor tweaks --- pennylane_qiskit/noise_models.py | 126 ++++++++++++++++--------------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index c14350e3..5085259f 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -79,7 +79,7 @@ def _kraus_to_choi(krau_op: Kraus, optimize=False) -> np.ndarray: return np.einsum("ij,ik->jk", kraus_vecs1, kraus_vecs2.conj(), optimize=optimize) -def _check_kraus_ops( +def _process_kraus_ops( kraus_mats: List[np.ndarray], **kwargs ) -> Tuple[bool, str, Union[float, np.ndarray, Kraus]]: """Checks parity for Qiskit's Kraus operations to the existing PennyLane channels. @@ -157,15 +157,14 @@ def _generate_product(items: Tuple, repeat: int = 1, matrix: bool = False) -> Tu ) # TODO: Analyze speed gains with sparse matrices -def _check_depolarization(error_dict: dict) -> Tuple[bool, float]: +def _process_depolarization(error_dict: dict) -> dict: """Checks parity for Qiskit's depolarization channel to that of PennyLane. Args: error_dict (dict): error dictionary for the quantum error Returns: - (bool, float): A tuple representing whether the encountered quantum error - is a depolarization error and the related probability. + dict: An updated error dictionary based on parity with depolarization channel. """ error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) @@ -179,9 +178,46 @@ def _check_depolarization(error_dict: dict) -> Tuple[bool, float]: id_factor = num_terms / (num_terms - 1) prob_iden = error_dict["probs"][error_dict["data"].index(["I" * num_wires])] param = id_factor * (1 - prob_iden) - return (True, param) + error_dict["name"] = "DepolarizingChannel" + error_dict["data"] = param * 3 / 4 + error_dict["probs"] = param + return error_dict + + error_dict["name"] = "QubitChannel" + kraus_ops = [ + reduce(lambda mat1, mat2: mat1 @ mat2, prod, np.eye(int(2**num_wires))) + for prod in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires, matrix=True) + ] + error_dict["data"] = [ + np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) + ] + return error_dict + - return (False, 0.0) +def _process_reset(error_dict: dict, **kwargs) -> dict: + """Checks parity of a qunatum error with ``Reset`` instruction to a PennyLane Channel. + + Args: + error_dict (dict): error dictionary for the quantum error + **kwargs: optional keyword arguments + + Returns: + dict: An updated error dictionary based on parity with existing PennyLane channel. + """ + error_probs = error_dict["probs"] + + if "Z" not in error_dict["name"]: + error_dict["name"] = "ResetError" + error_dict["data"] = error_probs[1:] + else: # uses t1 > t2 + error_dict["name"] = "ThermalRelaxationError" + tg = kwargs.get("gate_times", {}).get(kwargs["gate_name"], 1.0) + p0 = 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) + t1 = -tg / np.log(1 - error_probs[2] / p0) + t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 + error_dict["data"] = list(np.round([1 - p0, t1, t2, tg], kwargs.get("decimals", 10))) + + return error_dict def _build_qerror_dict(error) -> dict[str, Union[float, int]]: @@ -233,6 +269,25 @@ def _build_qerror_dict(error) -> dict[str, Union[float, int]]: return error_dict +def _process_qerror_dict(error_dict: dict) -> dict[str, Union[float, int]]: + """Helper method for post processing error dictionary for constructing PennyLane Channel.""" + if error_dict["name"] == "PauliError": + error_dict["data"] = [error_dict["data"], error_dict["probs"]] + + if error_dict["name"] == "QubitChannel": + error_dict["data"] = [error_dict["data"]] + + if not hasattr(error_dict["data"], "__iter__"): + error_dict["data"] = [error_dict["data"]] + + if error_dict["wires"]: + error_dict["wires"] = error_dict["wires"][0] + + error_dict.pop("probs", None) + + return error_dict + + def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: """Builds an PennyLane error operation from a Qiksit's QuantumError object. @@ -246,7 +301,6 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: """ error_dict = _build_qerror_dict(error) - error_wires, num_wires = error_dict["wires"], len(error_dict["wires"][0]) error_probs = error_dict["probs"] sorted_name = sorted(error_dict["name"]) @@ -258,52 +312,16 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: elif sorted_name == ["I", "X", "Y", "Z"]: error_dict["data"] = [["I"], ["X"], ["Y"], ["Z"]] - depol_flag, depol_param = _check_depolarization(error_dict) - if depol_flag: - error_dict["name"] = "DepolarizingChannel" - error_dict["data"] = depol_param * 3 / 4 - error_dict["probs"] = depol_param - else: - error_dict["name"] = "QubitChannel" - kraus_ops = [ - reduce(lambda mat1, mat2: mat1 @ mat2, prod, np.eye(int(2**num_wires))) - for prod in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires, matrix=True) - ] - error_dict["data"] = [ - np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) - ] + error_dict = _process_depolarization(error_dict) elif set(sorted_name) == {"pauli"}: - depol_flag, depol_param = _check_depolarization(error_dict) - if depol_flag and len(error_wires[0]) == 1: - error_dict["name"] = "DepolarizingChannel" - error_dict["probs"] = depol_param - else: - error_dict["name"] = "QubitChannel" # TODO: Make PauliError multi-qubit channel - kraus_ops = [ - reduce(lambda mat1, mat2: mat1 @ mat2, prod, np.eye(int(2**num_wires))) - for prod in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires, matrix=True) - ] - error_dict["data"] = [ - np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_dict["probs"], kraus_ops) - ] + error_dict = _process_depolarization(error_dict) elif sorted_name[0] == "I" and sorted_name[-1] == "reset": - if "Z" not in error_dict["name"]: - error_dict["name"] = "ResetError" - error_dict["data"] = error_probs[1:] - else: # uses t1 > t2 - error_dict["name"] = "ThermalRelaxationError" - tg = kwargs.get("gate_times", {}).get(kwargs["gate_name"], 1.0) - p0 = ( - 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) - ) - t1 = -tg / np.log(1 - error_probs[2] / p0) - t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 - error_dict["data"] = list(np.round([1 - p0, t1, t2, tg], kwargs.get("decimals", 10))) + error_dict = _process_reset(error_dict, **kwargs) elif sorted_name[0] == "kraus" and len(sorted_name) == 1: - kflag, kname, kdata = _check_kraus_ops(error_dict["data"][0], **kwargs) + kflag, kname, kdata = _process_kraus_ops(error_dict["data"][0], **kwargs) error_dict["name"] = kname if kflag else "QubitChannel" error_dict["data"] = kdata @@ -321,19 +339,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: else: # pragma: no cover raise ValueError(f"Error {error} could not be converted.") - if error_dict["name"] == "PauliError": - error_dict["data"] = [error_dict["data"], error_dict["probs"]] - - if error_dict["name"] == "QubitChannel": - error_dict["data"] = [error_dict["data"]] - - if not hasattr(error_dict["data"], "__iter__"): - error_dict["data"] = [error_dict["data"]] - - if error_dict["wires"]: - error_dict["wires"] = error_wires[0] - - error_dict.pop("probs", None) + error_dict = _process_qerror_dict(error_dict=error_dict) return getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) From 717f78fa5670f9e6c9e2193acff7397e93985c0f Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 8 Jul 2024 15:30:05 -0400 Subject: [PATCH 07/36] add tests --- pennylane_qiskit/noise_models.py | 28 ++--- tests/test_noise_models.py | 180 +++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 tests/test_noise_models.py diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 5085259f..0f1d9dd6 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -46,7 +46,7 @@ "z": "Z", "reset": qml.measure(AnyWires, reset=True), # TODO: Improve reset support } -default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5), ("optimize", False)] +default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5)] def _kraus_to_choi(krau_op: Kraus, optimize=False) -> np.ndarray: @@ -105,7 +105,9 @@ def _process_kraus_ops( rtol=rtol, atol=atol, ): - if np.allclose(nz_values[[0, 3]], np.ones(2)) and np.isclose(*nz_values[[1, 2]]): + if np.allclose(nz_values[[0, 3]], np.ones(2), rtol=rtol, atol=atol) and np.allclose( + *nz_values[[1, 2]], rtol=rtol, atol=atol + ): return (True, "PhaseDamping", np.round(1 - nz_values[1] ** 2, decimals).real) if len(nz_values) == 5 and np.allclose( @@ -116,7 +118,7 @@ def _process_kraus_ops( ): if np.allclose( [nz_values[0], sum(nz_values[[2, 4]])], np.ones(2), rtol=rtol, atol=atol - ) and np.isclose(*nz_values[[1, 3]], rtol=rtol, atol=atol): + ) and np.allclose(nz_values[[1, 3]], np.sqrt(nz_values[4]), rtol=rtol, atol=atol): return (True, "AmplitudeDamping", np.round(nz_values[2], decimals).real) if len(nz_values) == 6 and np.allclose( @@ -133,7 +135,7 @@ def _process_kraus_ops( ) and np.isclose( *nz_values[[1, 4]], rtol=rtol, atol=atol ): # uses t2 > t1 - tg = kwargs.get("gate_times", {}).get(kwargs["gate_name"], 1.0) + tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) pe = nz_values[2] / (nz_values[2] + nz_values[3]) t1 = -tg / np.log(1 - nz_values[2] / pe) t2 = -tg / np.log(nz_values[1]) @@ -157,11 +159,12 @@ def _generate_product(items: Tuple, repeat: int = 1, matrix: bool = False) -> Tu ) # TODO: Analyze speed gains with sparse matrices -def _process_depolarization(error_dict: dict) -> dict: +def _process_depolarization(error_dict: dict, multi_pauli: bool = False) -> dict: """Checks parity for Qiskit's depolarization channel to that of PennyLane. Args: error_dict (dict): error dictionary for the quantum error + multi_pauli (bool): accept multi-qubit Pauli errors Returns: dict: An updated error dictionary based on parity with depolarization channel. @@ -171,6 +174,7 @@ def _process_depolarization(error_dict: dict) -> dict: if ( len(set(error_dict["probs"])) == 2 and set(error_wires) == {error_wires[0]} + and (num_wires == 1 or multi_pauli) and list(it.chain(*error_dict["data"])) == ["".join(pauli) for pauli in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires)] ): @@ -179,13 +183,13 @@ def _process_depolarization(error_dict: dict) -> dict: prob_iden = error_dict["probs"][error_dict["data"].index(["I" * num_wires])] param = id_factor * (1 - prob_iden) error_dict["name"] = "DepolarizingChannel" - error_dict["data"] = param * 3 / 4 + error_dict["data"] = param / id_factor error_dict["probs"] = param return error_dict error_dict["name"] = "QubitChannel" kraus_ops = [ - reduce(lambda mat1, mat2: mat1 @ mat2, prod, np.eye(int(2**num_wires))) + reduce(np.kron, prod, 1.0) for prod in _generate_product(("I", "X", "Y", "Z"), repeat=num_wires, matrix=True) ] error_dict["data"] = [ @@ -208,10 +212,10 @@ def _process_reset(error_dict: dict, **kwargs) -> dict: if "Z" not in error_dict["name"]: error_dict["name"] = "ResetError" - error_dict["data"] = error_probs[1:] + error_dict["data"] = error_probs[1:] + ([0.0] if len(error_probs[1:]) == 1 else []) else: # uses t1 > t2 error_dict["name"] = "ThermalRelaxationError" - tg = kwargs.get("gate_times", {}).get(kwargs["gate_name"], 1.0) + tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) p0 = 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) t1 = -tg / np.log(1 - error_probs[2] / p0) t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 @@ -300,7 +304,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: theoretically equivalent to the given Qiksit's QuantumError object """ error_dict = _build_qerror_dict(error) - + print(error_dict) error_probs = error_dict["probs"] sorted_name = sorted(error_dict["name"]) @@ -330,7 +334,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: ): error_dict["name"] = "QubitChannel" kraus_ops = [ - op[0] if isinstance(op, list) else qml.I(0).matrix() * op for op in error_dict["data"] + op[0] if isinstance(op, list) else qml.I(0).matrix() for op in error_dict["data"] ] error_dict["data"] = [ np.sqrt(prob) * kraus_op for prob, kraus_op in zip(error_probs, kraus_ops) @@ -344,7 +348,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: return getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) -def _build_noise_model_map(noise_model, **kwargs) -> Tuple(dict, dict): +def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: """Builds noise model maps from which noise model can be constructed efficiently. Args: diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py new file mode 100644 index 00000000..36168020 --- /dev/null +++ b/tests/test_noise_models.py @@ -0,0 +1,180 @@ +# 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 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, + _generate_product, + _kraus_to_choi, +) + + +class TestLoadNoiseChannels: + """Tests for the helper methods of :func:`load_noise_models()` function.""" + + @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), + ), + ], + ) + def test_build_qerror_op(self, qiskit_error, pl_channel): + """Tests that a quantum error can be correctly converted into a PennyLane channel.""" + pl_op_from_qiskit = _build_qerror_op(qiskit_error) + assert qml.equal(pl_op_from_qiskit, pl_channel) + + @pytest.mark.parametrize( + "qiskit_error, pl_channel", + [ + ( + 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 = _kraus_to_choi(Kraus(list(pl_op_from_qiskit.data))) + choi_mat2 = _kraus_to_choi(Kraus(list(pl_channel.data))) + assert np.allclose(choi_mat1, choi_mat2) + + def test_build_model_map(self): + """Tests that _build_noise_model_map constructs correct model map for a noise model""" + error_1 = noise.depolarizing_error(0.123, 1) + error_2 = noise.depolarizing_error(0.456, 2) + error_3 = noise.phase_amplitude_damping_error(0.14, 0.24, excited_state_population=0.414) + + # Add errors to noise model + 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"]) + + model_map, _ = _build_noise_model_map(noise_model) + + assert list(model_map.keys()) == [ + qml.DepolarizingChannel(0.09225, wires=AnyWires), + qml.QubitChannel( + [ + np.sqrt(prob) * reduce(np.kron, prod, 1.0) + for prob, prod in zip( + [1 - 15 * 0.456 / 16, *([0.456 / 16] * 15)], + _generate_product(("I", "X", "Y", "Z"), repeat=2, matrix=True), + ) + ], + wires=AnyWires, + ), + qml.ThermalRelaxationError(0.414, 6.6302933312, 4.1837870638, 1.0, wires=AnyWires), + ] + assert list(model_map.values()) == [ + {AnyWires: ["RZ", "SX", "X"]}, + {AnyWires: ["CNOT"]}, + {AnyWires: ["RY", "RX"]}, + ] From 974d70ff65d850367a7b265e2275ea82470fcd0e Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 8 Jul 2024 22:51:08 -0400 Subject: [PATCH 08/36] add tests --- pennylane_qiskit/converter.py | 12 ++- pennylane_qiskit/noise_models.py | 53 +++++++--- tests/test_converter.py | 166 +++++++++++++++++++++++++++++++ tests/test_noise_models.py | 22 ++-- 4 files changed, 229 insertions(+), 24 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index c5b15af2..f034ecb5 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1059,12 +1059,14 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: Keyword Arguments: gate_times (Dict[str, float]): gate times for building thermal relaxation error. If not provided, the default value of ``1.0`` will be used for construction. - decimals: number of decimal places to round the Kraus matrices for errors to. + decimals (int): number of decimal places to round the Kraus matrices for errors to. If not provided, the default value of ``10`` is used. - atol: the relative tolerance parameter. Default value is ``1e-05``. - rtol: the absolute tolernace parameters. Defualt value is ``1e-08``. - optimize: controls if intermediate optimization is used while transforming Kraus + atol (float): the relative tolerance parameter. Default value is ``1e-05``. + rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. + optimize (bool): controls if intermediate optimization is used while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. + kraus_shape (bool): use shape of the Kraus operators to display ``qml.QubitChannel``. + Default is ``True``. Returns: qml.NoiseModel: An equivalent noise model constructed in PennyLane @@ -1091,7 +1093,7 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: fcond = reduce(lambda cond1, cond2: cond1 | cond2, conditions) noise = qml.noise.partial_wires(error) - if isinstance(error, qml.QubitChannel): + if isinstance(error, qml.QubitChannel) and kwargs.get("kraus_shape", True): noise = _rename(f"QubitChannel(Klist=Tensor{qml.math.shape(error.data)})")(noise) model_map[fcond] = noise diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 0f1d9dd6..90d95c4e 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -35,15 +35,42 @@ } pauli_error_map = {"X": "BitFlip", "Z": "PhaseFlip", "Y": "PauliError"} qiskit_op_map = { + "x": "X", + "y": "Y", + "z": "Z", + "h": "Hadamard", "cx": "CNOT", - "sx": "SX", - "id": "I", + "cz": "CZ", + "swap": "SWAP", + "iswap": "ISWAP", "rx": "RX", "ry": "RY", "rz": "RZ", - "x": "X", - "y": "Y", - "z": "Z", + "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 } default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5)] @@ -304,7 +331,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: theoretically equivalent to the given Qiksit's QuantumError object """ error_dict = _build_qerror_dict(error) - print(error_dict) + error_probs = error_dict["probs"] sorted_name = sorted(error_dict["name"]) @@ -319,7 +346,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: error_dict = _process_depolarization(error_dict) elif set(sorted_name) == {"pauli"}: - error_dict = _process_depolarization(error_dict) + error_dict = _process_depolarization(error_dict, kwargs.get("multi_pauli", False)) elif sorted_name[0] == "I" and sorted_name[-1] == "reset": error_dict = _process_reset(error_dict, **kwargs) @@ -358,12 +385,14 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: Keyword Arguments: gate_times (Dict[str, float]): gate times for building thermal relaxation error. If not provided, the default value of ``1.0`` will be used for construction. - decimals: number of decimal places to round the Kraus matrices for errors to. + decimals (int): number of decimal places to round the Kraus matrices for errors to. If not provided, the default value of ``10`` is used. - atol: the relative tolerance parameter. Default value is ``1e-05``. - rtol: the absolute tolernace parameters. Defualt value is ``1e-08``. - optimize: controls if intermediate optimization is used while transforming Kraus + atol (float): the relative tolerance parameter. Default value is ``1e-05``. + rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. + optimize (bool): controls if intermediate optimization is used while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. + multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not + supported with ``qml.DepolarizationChannel``, which is a single qubit channel. Returns: (dict, dict): returns mappings for ecountered quantum errors and readout errors. @@ -388,6 +417,6 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: # 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(f"Readout errors {error} are not supported currently and will be skipped.") + warn("Readout errors are not supported currently and will be skipped.") return qerror_dmap, rerror_dmap diff --git a/tests/test_converter.py b/tests/test_converter.py index 6d13f55f..352b2028 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -32,6 +32,7 @@ from pennylane.wires import Wires from pennylane_qiskit.converter import ( load, + load_noise_model, load_pauli_op, load_qasm, load_qasm_from_file, @@ -2206,3 +2207,168 @@ 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) + + +class TestLoadNoiseModel: + """Tests for :func:`load_noise_models()` function.""" + + qiksit = pytest.importorskip("qiskit", "1.0.0") + + def test_build_noise_model(self): + """Tests that _build_noise_model_map constructs correct model map for a noise model""" + + # pylint:disable = import-outside-toplevel + from qiskit.providers.fake_provider import FakeOpenPulse2Q + from qiskit_aer.noise import NoiseModel + from pennylane.noise import op_in, wires_in, partial_wires + from pennylane.operation import AnyWires + + loaded_noise_model = load_noise_model(NoiseModel.from_backend(FakeOpenPulse2Q())) + + pl_model_map = { + op_in("Identity") + & wires_in(0): qml.ThermalRelaxationError( + pe=0.0, t1=26981.9403362283, t2=26034.6676428009, tq=1.0, wires=AnyWires + ), + op_in("Identity") + & wires_in(1): qml.ThermalRelaxationError( + pe=0.0, t1=30732.034088541, t2=28335.6514829973, tq=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, tq=1.0, wires=AnyWires + ), + op_in("U2") + & wires_in(1): qml.ThermalRelaxationError( + pe=0.4998644198, t1=7.8227957211, t2=7.8226273195, tq=1.0, wires=AnyWires + ), + op_in("U3") + & wires_in(0): qml.ThermalRelaxationError( + pe=0.4996911588, t1=7.8227934813, t2=7.8226284393, tq=1.0, wires=AnyWires + ), + op_in("U3") + & wires_in(1): qml.ThermalRelaxationError( + pe=0.4997288404, t1=7.8229079927, t2=7.8225711871, tq=1.0, wires=AnyWires + ), + op_in("CNOT") + & wires_in([0, 1]): qml.QubitChannel( + np.array( + [ + [ + [-0.00630187 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.00630572 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.00630526 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.00630911 + 0.0j], + ], + [ + [-0.43615418 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.17440094 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, 0.20809143 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.05338186 + 0.0j], + ], + [ + [-0.09661797 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.18973192 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.15719968 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.44324055 + 0.0j], + ], + [ + [0.02447349 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.36545892 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, 0.36329539 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.02225592 + 0.0j], + ], + [ + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.5163849 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [-0.51620014 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.51659541 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, -0.0 + 0.0j, -0.51356736 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.05566075 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [-0.0 + 0.0j, -0.51161899 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.07136846 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + ], + [ + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [0.03615696 + 0.0j, -0.51514324 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.51514324 + 0.0j, -0.03615696 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, 0.07134191 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.51142865 + 0.0j], + [0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, 0.0 + 0.0j, -0.05563753 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.51335309 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.5163849 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, -0.14620844 + 0.0j, 0.49506128 + 0.0j, 0.0 + 0.0j], + ], + [ + [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], + [0.0 + 0.0j, -0.49506128 + 0.0j, -0.14620844 + 0.0j, 0.0 + 0.0j], + ], + ] + ), + 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() + ): + assert repr(pl_k) == repr(qk_k) + if "QubitChannel" not in pl_v.__name__: + assert pl_v.__name__ == qk_v.__name__ + else: + assert "QubitChannel(Klist=Tensor(16, 4, 4))" == qk_v.__name__ diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index 36168020..c12efca8 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -145,11 +145,19 @@ def test_build_kraus_error_ops(self, qiskit_error, pl_channel): choi_mat2 = _kraus_to_choi(Kraus(list(pl_channel.data))) assert np.allclose(choi_mat1, choi_mat2) - def test_build_model_map(self): + @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 noise model""" - error_1 = noise.depolarizing_error(0.123, 1) - error_2 = noise.depolarizing_error(0.456, 2) - error_3 = noise.phase_amplitude_damping_error(0.14, 0.24, excited_state_population=0.414) + 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) # Add errors to noise model noise_model = noise.NoiseModel() @@ -160,18 +168,18 @@ def test_build_model_map(self): model_map, _ = _build_noise_model_map(noise_model) assert list(model_map.keys()) == [ - qml.DepolarizingChannel(0.09225, wires=AnyWires), + qml.DepolarizingChannel(depol1 * 0.75, wires=AnyWires), qml.QubitChannel( [ np.sqrt(prob) * reduce(np.kron, prod, 1.0) for prob, prod in zip( - [1 - 15 * 0.456 / 16, *([0.456 / 16] * 15)], + [1 - 15 * depol2 / 16, *([depol2 / 16] * 15)], _generate_product(("I", "X", "Y", "Z"), repeat=2, matrix=True), ) ], wires=AnyWires, ), - qml.ThermalRelaxationError(0.414, 6.6302933312, 4.1837870638, 1.0, wires=AnyWires), + qml.ThermalRelaxationError(exc_pop, 6.6302933312, 4.1837870638, 1.0, wires=AnyWires), ] assert list(model_map.values()) == [ {AnyWires: ["RZ", "SX", "X"]}, From 7d97b9654983e879096ced9f56ab694de6eb674c Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Mon, 8 Jul 2024 23:51:50 -0400 Subject: [PATCH 09/36] add missing test --- pennylane_qiskit/noise_models.py | 4 ++-- tests/test_converter.py | 4 ++-- tests/test_noise_models.py | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 90d95c4e..35770526 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -101,7 +101,7 @@ def _kraus_to_choi(krau_op: Kraus, optimize=False) -> np.ndarray: kraus_l, kraus_r = krau_op._data kraus_vecs1 = np.array([kraus.ravel(order="F") for kraus in kraus_l]) kraus_vecs2 = kraus_vecs1 - if kraus_r is not None: + if kraus_r is not None: # pragma: no cover kraus_vecs2 = np.array([kraus.ravel(order="F") for kraus in kraus_r]) return np.einsum("ij,ik->jk", kraus_vecs1, kraus_vecs2.conj(), optimize=optimize) @@ -335,7 +335,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: error_probs = error_dict["probs"] sorted_name = sorted(error_dict["name"]) - if sorted_name[0] == "I" and sorted_name[1:] in ["X", "Y", "Z"] and len(sorted_name) == 2: + if sorted_name[0] == "I" and len(sorted_name) == 2 and sorted_name[1] in ["X", "Y", "Z"]: prob_pauli = error_dict["probs"][error_dict["name"].index(sorted_name[1])] error_dict["name"] = pauli_error_map[sorted_name[1]] error_dict["data"] = prob_pauli diff --git a/tests/test_converter.py b/tests/test_converter.py index 352b2028..4fafddbd 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -2209,6 +2209,7 @@ def test_convert_with_invalid_operator(self): load_pauli_op(123) +# pylint:disable = import-outside-toplevel, too-few-public-methods class TestLoadNoiseModel: """Tests for :func:`load_noise_models()` function.""" @@ -2217,7 +2218,6 @@ class TestLoadNoiseModel: def test_build_noise_model(self): """Tests that _build_noise_model_map constructs correct model map for a noise model""" - # pylint:disable = import-outside-toplevel from qiskit.providers.fake_provider import FakeOpenPulse2Q from qiskit_aer.noise import NoiseModel from pennylane.noise import op_in, wires_in, partial_wires @@ -2371,4 +2371,4 @@ def test_build_noise_model(self): if "QubitChannel" not in pl_v.__name__: assert pl_v.__name__ == qk_v.__name__ else: - assert "QubitChannel(Klist=Tensor(16, 4, 4))" == qk_v.__name__ + assert qk_v.__name__ == "QubitChannel(Klist=Tensor(16, 4, 4))" diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index c12efca8..1dbfebfd 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -67,6 +67,10 @@ class TestLoadNoiseChannels: 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), + ), ], ) def test_build_qerror_op(self, qiskit_error, pl_channel): From fc73f7a4771fcbc5c856ca23b60e87d9229c60b8 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 9 Jul 2024 08:19:18 -0400 Subject: [PATCH 10/36] minor tweak --- tests/test_converter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 4fafddbd..7405ab65 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -2368,7 +2368,4 @@ def test_build_noise_model(self): pl_noise_model.model_map.items(), loaded_noise_model.model_map.items() ): assert repr(pl_k) == repr(qk_k) - if "QubitChannel" not in pl_v.__name__: - assert pl_v.__name__ == qk_v.__name__ - else: - assert qk_v.__name__ == "QubitChannel(Klist=Tensor(16, 4, 4))" + assert qml.equal(pl_v(AnyWires), qk_v(AnyWires)) From caf5da24dbaeb070b53cc4363f42f0b9abb5639a Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 9 Jul 2024 09:14:22 -0400 Subject: [PATCH 11/36] update pl sphinx --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index e66880f3..812db6eb 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,7 +3,7 @@ docutils==0.20.1 ipykernel==6.29.2 jinja2==3.1.3 nbsphinx==0.9.3 -pennylane==0.34 +pennylane==0.37 pybind11==2.11.1 pygments==2.17.2 pygments-github-lexers==0.0.5 From d936023857f540ca2bde9d3892800c18f6741043 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 9 Jul 2024 09:43:41 -0400 Subject: [PATCH 12/36] minor test tweak --- tests/test_converter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 7405ab65..5577cbf2 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -2367,5 +2367,7 @@ def test_build_noise_model(self): 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 qml.equal(pl_v(AnyWires), qk_v(AnyWires)) + assert isinstance(pl_op, type(qk_op)) + assert np.allclose(pl_op.data, qk_op.data, atol=1e-5) From 46d60f1ed981e4c7a98710cf54ca8a53967ded7d Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 9 Jul 2024 10:30:17 -0400 Subject: [PATCH 13/36] missing test --- pennylane_qiskit/noise_models.py | 3 +-- tests/test_noise_models.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 35770526..1c29efa2 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -315,7 +315,6 @@ def _process_qerror_dict(error_dict: dict) -> dict[str, Union[float, int]]: error_dict["wires"] = error_dict["wires"][0] error_dict.pop("probs", None) - return error_dict @@ -338,7 +337,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: if sorted_name[0] == "I" and len(sorted_name) == 2 and sorted_name[1] in ["X", "Y", "Z"]: prob_pauli = error_dict["probs"][error_dict["name"].index(sorted_name[1])] error_dict["name"] = pauli_error_map[sorted_name[1]] - error_dict["data"] = prob_pauli + error_dict["data"] = prob_pauli if error_dict["name"] != "PauliError" else sorted_name[1] error_dict["probs"] = prob_pauli elif sorted_name == ["I", "X", "Y", "Z"]: diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index 1dbfebfd..b0f41ec4 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -71,12 +71,21 @@ class TestLoadNoiseChannels: 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), + ), ], ) def test_build_qerror_op(self, qiskit_error, pl_channel): """Tests that a quantum error can be correctly converted into a PennyLane channel.""" pl_op_from_qiskit = _build_qerror_op(qiskit_error) - assert qml.equal(pl_op_from_qiskit, pl_channel) + # TODO: Remove when qml.equal works with PauliError + if not isinstance(pl_op_from_qiskit, qml.PauliError): + assert qml.equal(pl_op_from_qiskit, pl_channel) + else: + assert isinstance(pl_op_from_qiskit, type(pl_channel)) + assert all([x1 == x2 for x1, x2 in zip(pl_op_from_qiskit.data, pl_channel.data)]) @pytest.mark.parametrize( "qiskit_error, pl_channel", From 3eb56e07d834de3ec51d889234e506e8941a78d5 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 9 Jul 2024 10:33:03 -0400 Subject: [PATCH 14/36] tweak --- tests/test_noise_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index b0f41ec4..b2012325 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -85,7 +85,7 @@ def test_build_qerror_op(self, qiskit_error, pl_channel): assert qml.equal(pl_op_from_qiskit, pl_channel) else: assert isinstance(pl_op_from_qiskit, type(pl_channel)) - assert all([x1 == x2 for x1, x2 in zip(pl_op_from_qiskit.data, pl_channel.data)]) + assert all(x1 == x2 for x1, x2 in zip(pl_op_from_qiskit.data, pl_channel.data)) @pytest.mark.parametrize( "qiskit_error, pl_channel", From ea7d6f7e0f32c44e0bc301f291f5821ab7f7c11d Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Tue, 9 Jul 2024 22:12:11 -0400 Subject: [PATCH 15/36] doc tweak --- pennylane_qiskit/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index f034ecb5..7351622c 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1065,8 +1065,8 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. optimize (bool): controls if intermediate optimization is used while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. - kraus_shape (bool): use shape of the Kraus operators to display ``qml.QubitChannel``. - Default is ``True``. + kraus_shape (bool): use shape of the Kraus operators to display ``qml.QubitChannel`` + instead of the complete list of matrices. Default is ``True``. Returns: qml.NoiseModel: An equivalent noise model constructed in PennyLane From 134b4f06b97de6a9c57594f890d3dea1f9e029a2 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 16:38:50 -0400 Subject: [PATCH 16/36] thermal error edge cases --- pennylane_qiskit/noise_models.py | 100 +++++++++++++++---------------- tests/test_noise_models.py | 4 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 1c29efa2..1dc4f50d 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -29,9 +29,9 @@ # pylint:disable = protected-access kraus_indice_map = { - "PhaseDamping": ((0, 0, 3, 3), (0, 3, 0, 3)), - "AmplitudeDamping": ((0, 0, 2, 3, 3), (0, 3, 2, 0, 3)), - "ThermalRelaxationError": ((0, 0, 1, 2, 3, 3), (0, 3, 1, 2, 0, 3)), + "PhaseDamping": {(0, 0), (0, 3), (3, 0), (3, 3)}, + "AmplitudeDamping": {(0, 0), (0, 3), (2, 2), (3, 0), (3, 3)}, + "ThermalRelaxation": {(0, 0), (0, 3), (1, 1), (2, 2), (3, 0), (3, 3)}, } pauli_error_map = {"X": "BitFlip", "Z": "PhaseFlip", "Y": "PauliError"} qiskit_op_map = { @@ -73,7 +73,7 @@ "sxdg": qml.adjoint(qml.SX), "reset": qml.measure(AnyWires, reset=True), # TODO: Improve reset support } -default_option_map = [("decimals", 10), ("atol", 1e-8), ("rtol", 1e-5)] +default_option_map = {"decimals":10, "atol":1e-8, "rtol":1e-5} def _kraus_to_choi(krau_op: Kraus, optimize=False) -> np.ndarray: @@ -120,55 +120,50 @@ def _process_kraus_ops( """ choi_matrix = _kraus_to_choi(Kraus(kraus_mats), optimize=kwargs.get("optimize", False)) + kdata = None if qml.math.shape(choi_matrix) == (4, 4): # PennyLane channels are single-qubit - decimals, atol, rtol = tuple(kwargs.get(opt, dflt) for (opt, dflt) in default_option_map) + decimals, atol, rtol = tuple( + kwargs.get("options", dict()).get(opt, dflt) for (opt, dflt) in default_option_map.items() + ) non_zero_indices = np.nonzero(choi_matrix.round(decimals)) + nz_indice = set(map(tuple, zip(*non_zero_indices))) nz_values = choi_matrix[non_zero_indices] - if len(nz_values) == 4 and np.allclose( - non_zero_indices, - kraus_indice_map["PhaseDamping"], - rtol=rtol, - atol=atol, - ): - if np.allclose(nz_values[[0, 3]], np.ones(2), rtol=rtol, atol=atol) and np.allclose( - *nz_values[[1, 2]], rtol=rtol, atol=atol + # Note: Inequality here is to priortize thermal-relaxation errors over damping errors. + if len(nz_values) <= 6 and nz_indice.issubset(kraus_indice_map["ThermalRelaxation"]): + nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] + if ( + np.allclose(nt_values[[(0, 2), (3, 5)]].sum(axis=1), 1.0, rtol=rtol, atol=atol) + and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol) ): - return (True, "PhaseDamping", np.round(1 - nz_values[1] ** 2, decimals).real) - - if len(nz_values) == 5 and np.allclose( - non_zero_indices, - kraus_indice_map["AmplitudeDamping"], - rtol=rtol, - atol=atol, - ): + tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) + if np.isclose(nt_values[[2, 3]].sum(), 0.0, rtol=rtol, atol=atol): + pe, t1 = 0.0, np.inf + else: + pe = nt_values[2] / (nt_values[2] + nt_values[3]) + t1 = -tg / np.log(1 - (nt_values[3] + nt_values[2])) + t2 = np.inf if np.isclose(nt_values[1], 1.0, rtol=rtol, atol=atol) else ( + -tg / np.log(nt_values[1]) + ) + kdata = (True, "ThermalRelaxationError", np.round([pe, t1, t2, tg], decimals).real) + + if kwargs.get("thermal_relaxation", True) and kdata is not None: + return kdata + + if len(nz_values) == 5 and nz_indice.issubset(kraus_indice_map["AmplitudeDamping"]): if np.allclose( [nz_values[0], sum(nz_values[[2, 4]])], np.ones(2), rtol=rtol, atol=atol ) and np.allclose(nz_values[[1, 3]], np.sqrt(nz_values[4]), rtol=rtol, atol=atol): return (True, "AmplitudeDamping", np.round(nz_values[2], decimals).real) - if len(nz_values) == 6 and np.allclose( - non_zero_indices, - kraus_indice_map["ThermalRelaxationError"], - rtol=rtol, - atol=atol, - ): - if np.allclose( - [sum(nz_values[[0, 2]]), sum(nz_values[[3, 5]])], - np.ones(2), - rtol=rtol, - atol=atol, - ) and np.isclose( - *nz_values[[1, 4]], rtol=rtol, atol=atol - ): # uses t2 > t1 - tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) - pe = nz_values[2] / (nz_values[2] + nz_values[3]) - t1 = -tg / np.log(1 - nz_values[2] / pe) - t2 = -tg / np.log(nz_values[1]) - return (True, "ThermalRelaxationError", np.round([pe, t1, t2, tg], decimals).real) + if len(nz_values) == 4 and nz_indice.issubset(kraus_indice_map["PhaseDamping"]): + if np.allclose(nz_values[[0, 3]], np.ones(2), rtol=rtol, atol=atol) and np.allclose( + *nz_values[[1, 2]], rtol=rtol, atol=atol + ): + return (True, "PhaseDamping", np.round(1 - nz_values[1] ** 2, decimals).real) - return (False, "QubitChannel", Kraus(Choi(choi_matrix)).data) + return (False, "QubitChannel", Kraus(Choi(choi_matrix)).data) if not kdata else kdata @lru_cache @@ -246,7 +241,8 @@ def _process_reset(error_dict: dict, **kwargs) -> dict: p0 = 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) t1 = -tg / np.log(1 - error_probs[2] / p0) t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 - error_dict["data"] = list(np.round([1 - p0, t1, t2, tg], kwargs.get("decimals", 10))) + decimals = kwargs.get("options", {}).get("decimals", default_option_map["decimals"]) + error_dict["data"] = list(np.round([1 - p0, t1, t2, tg], decimals=decimals)) return error_dict @@ -370,7 +366,6 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: raise ValueError(f"Error {error} could not be converted.") error_dict = _process_qerror_dict(error_dict=error_dict) - return getattr(qml.ops, error_dict["name"])(*error_dict["data"], wires=AnyWires) @@ -382,16 +377,21 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: kwargs: Optional keyword arguments for providing extra information Keyword Arguments: + thermal_relaxation (bool): prefer conversion of ``QiskitErrors`` to thermal relaxation errors + over damping errors. Default is ``False``. gate_times (Dict[str, float]): gate times for building thermal relaxation error. - If not provided, the default value of ``1.0`` will be used for construction. - decimals (int): number of decimal places to round the Kraus matrices for errors to. - If not provided, the default value of ``10`` is used. - atol (float): the relative tolerance parameter. Default value is ``1e-05``. - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. - optimize (bool): controls if intermediate optimization is used while transforming Kraus - operators to a Choi matrix, wherever required. Default is ``False``. + If not provided, the default value of ``1.0 s`` will be used for construction. multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not supported with ``qml.DepolarizationChannel``, which is a single qubit channel. + readout_error (bool): include readout error in the converted noise model. Default is ``True``. + optimize (bool): controls if a contraction order optimization is used for ``einsum`` while + transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. + options (dict[str, Union[int, float]]): optional parameters related to tolerance and rounding: + + - decimals (int): number of decimal places to round the Kraus matrices for errors to. + If not provided, the default value of ``10`` is used. + - atol (float): the relative tolerance parameter. Default value is ``1e-05``. + - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. Returns: (dict, dict): returns mappings for ecountered quantum errors and readout errors. diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index b2012325..684673d4 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -79,7 +79,7 @@ class TestLoadNoiseChannels: ) def test_build_qerror_op(self, qiskit_error, pl_channel): """Tests that a quantum error can be correctly converted into a PennyLane channel.""" - pl_op_from_qiskit = _build_qerror_op(qiskit_error) + pl_op_from_qiskit = _build_qerror_op(qiskit_error, thermal_relaxation=False) # TODO: Remove when qml.equal works with PauliError if not isinstance(pl_op_from_qiskit, qml.PauliError): assert qml.equal(pl_op_from_qiskit, pl_channel) @@ -154,7 +154,7 @@ def test_build_qerror_op(self, qiskit_error, pl_channel): 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 = _kraus_to_choi(Kraus(list(pl_op_from_qiskit.data))) + choi_mat1 = _kraus_to_choi(Kraus(list(pl_op_from_qiskit.compute_kraus_matrices(*pl_op_from_qiskit.data)))) choi_mat2 = _kraus_to_choi(Kraus(list(pl_channel.data))) assert np.allclose(choi_mat1, choi_mat2) From ab7a11ee9ec0662c448ec494acc805328545f76b Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 16:39:17 -0400 Subject: [PATCH 17/36] happy `black` --- pennylane_qiskit/noise_models.py | 20 +++++++++++--------- tests/test_noise_models.py | 4 +++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 1dc4f50d..a7f4f3aa 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -73,7 +73,7 @@ "sxdg": qml.adjoint(qml.SX), "reset": qml.measure(AnyWires, reset=True), # TODO: Improve reset support } -default_option_map = {"decimals":10, "atol":1e-8, "rtol":1e-5} +default_option_map = {"decimals": 10, "atol": 1e-8, "rtol": 1e-5} def _kraus_to_choi(krau_op: Kraus, optimize=False) -> np.ndarray: @@ -123,7 +123,8 @@ def _process_kraus_ops( kdata = None if qml.math.shape(choi_matrix) == (4, 4): # PennyLane channels are single-qubit decimals, atol, rtol = tuple( - kwargs.get("options", dict()).get(opt, dflt) for (opt, dflt) in default_option_map.items() + kwargs.get("options", dict()).get(opt, dflt) + for (opt, dflt) in default_option_map.items() ) non_zero_indices = np.nonzero(choi_matrix.round(decimals)) @@ -133,18 +134,19 @@ def _process_kraus_ops( # Note: Inequality here is to priortize thermal-relaxation errors over damping errors. if len(nz_values) <= 6 and nz_indice.issubset(kraus_indice_map["ThermalRelaxation"]): nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] - if ( - np.allclose(nt_values[[(0, 2), (3, 5)]].sum(axis=1), 1.0, rtol=rtol, atol=atol) - and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol) - ): + if np.allclose( + nt_values[[(0, 2), (3, 5)]].sum(axis=1), 1.0, rtol=rtol, atol=atol + ) and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol): tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) if np.isclose(nt_values[[2, 3]].sum(), 0.0, rtol=rtol, atol=atol): - pe, t1 = 0.0, np.inf + pe, t1 = 0.0, np.inf else: pe = nt_values[2] / (nt_values[2] + nt_values[3]) t1 = -tg / np.log(1 - (nt_values[3] + nt_values[2])) - t2 = np.inf if np.isclose(nt_values[1], 1.0, rtol=rtol, atol=atol) else ( - -tg / np.log(nt_values[1]) + t2 = ( + np.inf + if np.isclose(nt_values[1], 1.0, rtol=rtol, atol=atol) + else (-tg / np.log(nt_values[1])) ) kdata = (True, "ThermalRelaxationError", np.round([pe, t1, t2, tg], decimals).real) diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index 684673d4..bfb4bd98 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -154,7 +154,9 @@ def test_build_qerror_op(self, qiskit_error, pl_channel): 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 = _kraus_to_choi(Kraus(list(pl_op_from_qiskit.compute_kraus_matrices(*pl_op_from_qiskit.data)))) + choi_mat1 = _kraus_to_choi( + Kraus(list(pl_op_from_qiskit.compute_kraus_matrices(*pl_op_from_qiskit.data))) + ) choi_mat2 = _kraus_to_choi(Kraus(list(pl_channel.data))) assert np.allclose(choi_mat1, choi_mat2) From 7d6333a7793a12b5ff2e176143f453f7aee5b2c7 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 16:46:43 -0400 Subject: [PATCH 18/36] optional readout --- pennylane_qiskit/noise_models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index a7f4f3aa..9b5a2900 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -417,7 +417,8 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: # 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.") + if kwargs.get("readout_error", True): + 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 From b6f1a1f3d976e8857ea5ce57cfd28e7ef691997e Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 16:53:00 -0400 Subject: [PATCH 19/36] lil twix --- pennylane_qiskit/noise_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 9b5a2900..32f2ecf8 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -123,8 +123,7 @@ def _process_kraus_ops( kdata = None if qml.math.shape(choi_matrix) == (4, 4): # PennyLane channels are single-qubit decimals, atol, rtol = tuple( - kwargs.get("options", dict()).get(opt, dflt) - for (opt, dflt) in default_option_map.items() + kwargs.get("options", {}).get(opt, dflt) for (opt, dflt) in default_option_map.items() ) non_zero_indices = np.nonzero(choi_matrix.round(decimals)) From 9016f229cf42c5e8b0aebddfc035ff089eb97f83 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 16:59:37 -0400 Subject: [PATCH 20/36] doc tweak --- pennylane_qiskit/__init__.py | 2 +- pennylane_qiskit/converter.py | 19 ++++++++++++------- setup.py | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index 991b73d7..97808291 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -18,6 +18,6 @@ from .basic_aer import BasicAerDevice, BasicSimulatorDevice from .ibmq import IBMQDevice 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 from .runtime_devices import IBMQCircuitRunnerDevice from .runtime_devices import IBMQSamplerDevice diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 7351622c..2177490d 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1057,16 +1057,21 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: kwargs: Optional keyword arguments for conversion of the noise model. Keyword Arguments: + thermal_relaxation (bool): prefer conversion of ``QiskitErrors`` to thermal relaxation errors + over damping errors. Default is ``False``. gate_times (Dict[str, float]): gate times for building thermal relaxation error. - If not provided, the default value of ``1.0`` will be used for construction. - decimals (int): number of decimal places to round the Kraus matrices for errors to. - If not provided, the default value of ``10`` is used. - atol (float): the relative tolerance parameter. Default value is ``1e-05``. - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. - optimize (bool): controls if intermediate optimization is used while transforming Kraus - operators to a Choi matrix, wherever required. Default is ``False``. + If not provided, the default value of ``1.0 s`` will be used for construction. + readout_error (bool): include readout error in the converted noise model. Default is ``True``. + optimize (bool): controls if a contraction order optimization is used for ``einsum`` while + transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. kraus_shape (bool): use shape of the Kraus operators to display ``qml.QubitChannel`` instead of the complete list of matrices. Default is ``True``. + options (dict[str, Union[int, float]]): optional parameters related to tolerance and rounding: + + - decimals (int): number of decimal places to round the Kraus matrices for errors to. + If not provided, the default value of ``10`` is used. + - atol (float): the relative tolerance parameter. Default value is ``1e-05``. + - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. Returns: qml.NoiseModel: An equivalent noise model constructed in PennyLane diff --git a/setup.py b/setup.py index ad805ba6..3ce36e66 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ 'qiskit_op = pennylane_qiskit:load_pauli_op', 'qasm = pennylane_qiskit:load_qasm', 'qasm_file = pennylane_qiskit:load_qasm_from_file', + 'qiskit_noise = pennylane_qiskit:load_noise_model', ], }, 'description': 'PennyLane plugin for Qiskit', From ab64443e257c472a13550f6c6decfebc70be7b14 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 17:25:54 -0400 Subject: [PATCH 21/36] add example --- pennylane_qiskit/converter.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 2177490d..b5c0b6d3 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1082,6 +1082,50 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: .. note:: Currently, PennyLane noise models does not support readout errors, so those will be skipped during conversion. + + **Example** + + Consider the following noise model constructed in Qiskit: + + .. code-block:: python + + >>> import qiskit.providers.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']) # rz and ry gates get error_1 + >>> noise_model.add_all_qubit_quantum_error(error_2, ['cx']) # cx gates get error_2 + >>> load_noise_model(noise_model) + NoiseModel({ + OpIn(['RZ', 'RY']): DepolarizingChannel(p=0.0007499999999999174) + OpIn(['CNOT']): QubitChannel(Klist=Tensor(16, 4, 4)) + }) + + Equivalently, in PennyLane this will be: + + .. code-block:: python + + import numpy as np + import pennylane as qml + import itertools as it + import functools as ft + + pauli_mats = [ + ft.reduce(np.kron, prod, 1.0) + for prod in it.product( + (tuple(map(qml.matrix, tuple(getattr(qml, i)(0) for i in ["I", "X", "Y", "Z"])))), repeat=2, + ) + ] + pauli_prob = error_2.probabilities + kraus_dops = [np.sqrt(prob) * kraus_op for prob, kraus_op in zip(pauli_prob, kraus_ops)] + + c0 = qml.noise.op_eq(qml.RZ) | qml.noise.op_eq(qml.RY) + c1 = qml.noise.op_eq(qml.CNOT) + + n0 = qml.noise.partial_wires(qml.DepolarizingChannel, 0.001) + n1 = qml.noise.partial_wires(qml.QubitChanel(kraus_dops)) + + equivalent_pl_noise_model = qml.NoiseModel({c0: n0, c1: n1}) """ qerror_dmap, _ = _build_noise_model_map(noise_model, **kwargs) From 48ada9a99e73ef6bb2ed2dd75684084a61b0ec66 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 17:28:15 -0400 Subject: [PATCH 22/36] black fix --- pennylane_qiskit/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index b5c0b6d3..fa011829 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1119,7 +1119,7 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: pauli_prob = error_2.probabilities kraus_dops = [np.sqrt(prob) * kraus_op for prob, kraus_op in zip(pauli_prob, kraus_ops)] - c0 = qml.noise.op_eq(qml.RZ) | qml.noise.op_eq(qml.RY) + c0 = qml.noise.op_eq(qml.RZ) | qml.noise.op_eq(qml.RY) c1 = qml.noise.op_eq(qml.CNOT) n0 = qml.noise.partial_wires(qml.DepolarizingChannel, 0.001) From 5b0373643993cfee2bcd2f2d9894105f8c3757c1 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 17:49:30 -0400 Subject: [PATCH 23/36] tweak example --- pennylane_qiskit/converter.py | 12 ++++++------ pennylane_qiskit/noise_models.py | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index fa011829..c3458c06 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1068,8 +1068,7 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: instead of the complete list of matrices. Default is ``True``. options (dict[str, Union[int, float]]): optional parameters related to tolerance and rounding: - - decimals (int): number of decimal places to round the Kraus matrices for errors to. - If not provided, the default value of ``10`` is used. + - decimals (int): number of decimal places to round the Kraus matrices. Default is ``10``. - atol (float): the relative tolerance parameter. Default value is ``1e-05``. - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. @@ -1093,7 +1092,7 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: >>> 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']) # rz and ry gates get error_1 + >>> noise_model.add_all_qubit_quantum_error(error_1, ['rz', 'ry']) # rz/ry gates get error_1 >>> noise_model.add_all_qubit_quantum_error(error_2, ['cx']) # cx gates get error_2 >>> load_noise_model(noise_model) NoiseModel({ @@ -1113,17 +1112,18 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: pauli_mats = [ ft.reduce(np.kron, prod, 1.0) for prod in it.product( - (tuple(map(qml.matrix, tuple(getattr(qml, i)(0) for i in ["I", "X", "Y", "Z"])))), repeat=2, + map(qml.matrix, tuple(getattr(qml, i)(0) for i in ["I", "X", "Y", "Z"])) + repeat=2 ) ] pauli_prob = error_2.probabilities - kraus_dops = [np.sqrt(prob) * kraus_op for prob, kraus_op in zip(pauli_prob, kraus_ops)] + kraus_ops = [np.sqrt(prob) * kraus_op for prob, kraus_op in zip(pauli_prob, pauli_mats)] c0 = qml.noise.op_eq(qml.RZ) | qml.noise.op_eq(qml.RY) c1 = qml.noise.op_eq(qml.CNOT) n0 = qml.noise.partial_wires(qml.DepolarizingChannel, 0.001) - n1 = qml.noise.partial_wires(qml.QubitChanel(kraus_dops)) + n1 = qml.noise.partial_wires(qml.QubitChanel(kraus_ops)) equivalent_pl_noise_model = qml.NoiseModel({c0: n0, c1: n1}) """ diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 32f2ecf8..b7fe0c5b 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -389,8 +389,7 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. options (dict[str, Union[int, float]]): optional parameters related to tolerance and rounding: - - decimals (int): number of decimal places to round the Kraus matrices for errors to. - If not provided, the default value of ``10`` is used. + - decimals (int): number of decimal places to round the Kraus matrices. Default is ``10``. - atol (float): the relative tolerance parameter. Default value is ``1e-05``. - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. From 1a8b4ee63d355507baf623f21cbe5cf11863352c Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 20:32:03 -0400 Subject: [PATCH 24/36] update gate times --- pennylane_qiskit/converter.py | 10 +++-- pennylane_qiskit/noise_models.py | 74 ++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index c3458c06..c00f88be 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1049,8 +1049,8 @@ def _expr_eval_clvals(clbits, clvals, expr_func, bitwise=False): def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: - """Loads a PennyLane ``NoiseModel`` from a Qiskit `NoiseModel - `_. + """Loads a PennyLane `NoiseModel `_ + from a Qiskit `NoiseModel `_. Args: noise_model (qiskit_aer.noise.NoiseModel): A Qiskit noise model object @@ -1059,9 +1059,11 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: Keyword Arguments: thermal_relaxation (bool): prefer conversion of ``QiskitErrors`` to thermal relaxation errors over damping errors. Default is ``False``. - gate_times (Dict[str, float]): gate times for building thermal relaxation error. - If not provided, the default value of ``1.0 s`` will be used for construction. readout_error (bool): include readout error in the converted noise model. Default is ``True``. + gate_times (Dict[str, Tuple[Tuple[int], float]]): a dictionary to provide gate times for building + thermal relaxation error. Each key will be the instruction name and the corresponding values + will be tuple of qubit indices and the time in seconds. If it is not provided or a gate/qubit + is missing, then a default value of `1.0 s`` will be used for the specific constructions. optimize (bool): controls if a contraction order optimization is used for ``einsum`` while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. kraus_shape (bool): use shape of the Kraus operators to display ``qml.QubitChannel`` diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index b7fe0c5b..8532150f 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -132,25 +132,11 @@ def _process_kraus_ops( # Note: Inequality here is to priortize thermal-relaxation errors over damping errors. if len(nz_values) <= 6 and nz_indice.issubset(kraus_indice_map["ThermalRelaxation"]): - nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] - if np.allclose( - nt_values[[(0, 2), (3, 5)]].sum(axis=1), 1.0, rtol=rtol, atol=atol - ) and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol): - tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) - if np.isclose(nt_values[[2, 3]].sum(), 0.0, rtol=rtol, atol=atol): - pe, t1 = 0.0, np.inf - else: - pe = nt_values[2] / (nt_values[2] + nt_values[3]) - t1 = -tg / np.log(1 - (nt_values[3] + nt_values[2])) - t2 = ( - np.inf - if np.isclose(nt_values[1], 1.0, rtol=rtol, atol=atol) - else (-tg / np.log(nt_values[1])) - ) - kdata = (True, "ThermalRelaxationError", np.round([pe, t1, t2, tg], decimals).real) - - if kwargs.get("thermal_relaxation", True) and kdata is not None: - return kdata + kdata = _process_thermal_relaxation( + choi_matrix, decimals=decimals, atol=atol, rtol=rtol, **kwargs + ) + if kwargs.get("thermal_relaxation", True) and kdata is not None: + return kdata if len(nz_values) == 5 and nz_indice.issubset(kraus_indice_map["AmplitudeDamping"]): if np.allclose( @@ -167,6 +153,43 @@ def _process_kraus_ops( return (False, "QubitChannel", Kraus(Choi(choi_matrix)).data) if not kdata else kdata +def _extract_gate_time(gate_data: tuple[tuple[int], float], gate_wires: int) -> float: + """Helper method to extract gate time for a quantum error""" + tg = 1.0 + if gate_data is not None: + gate_data[0] = (gate_data[0],) if isinstance(int, gate_data[0]) else gate_data[0] + if gate_wires in gate_data[0]: + tg = gate_data[1] + return tg + +def _process_thermal_relaxation(choi_matrix, **kwargs): + """Computes parameters for thermal relaxation error from a Choi matrix of Kraus matrices""" + nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] + decimals, atol, rtol = tuple(map(kwargs.get, default_option_map)) + + kdata = None + if np.allclose( + nt_values[[(0, 2), (3, 5)]].sum(axis=1), 1.0, rtol=rtol, atol=atol + ) and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol): + tg = _extract_gate_time( + gate_data=kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), None), + gate_wires=kwargs.get("gate_wires", None) + ) + if np.isclose(nt_values[[2, 3]].sum(), 0.0, rtol=rtol, atol=atol): + pe, t1 = 0.0, np.inf + else: + pe = nt_values[2] / (nt_values[2] + nt_values[3]) + t1 = -tg / np.log(1 - (nt_values[3] + nt_values[2])) + t2 = ( + np.inf + if np.isclose(nt_values[1], 1.0, rtol=rtol, atol=atol) + else (-tg / np.log(nt_values[1])) + ) + kdata = (True, "ThermalRelaxationError", np.round([pe, t1, t2, tg], decimals).real) + + return kdata + + @lru_cache def _generate_product(items: Tuple, repeat: int = 1, matrix: bool = False) -> Tuple: """Helper method to generate product for Pauli terms and matrices efficiently.""" @@ -238,7 +261,10 @@ def _process_reset(error_dict: dict, **kwargs) -> dict: error_dict["data"] = error_probs[1:] + ([0.0] if len(error_probs[1:]) == 1 else []) else: # uses t1 > t2 error_dict["name"] = "ThermalRelaxationError" - tg = kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), 1.0) + tg = _extract_gate_time( + gate_data=kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), None), + gate_wires=kwargs.get("gate_wires", None) + ) p0 = 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) t1 = -tg / np.log(1 - error_probs[2] / p0) t2 = (1 / t1 - np.log(1 - 2 * error_probs[1] / (1 - error_probs[2] / p0)) / tg) ** -1 @@ -380,8 +406,10 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: Keyword Arguments: thermal_relaxation (bool): prefer conversion of ``QiskitErrors`` to thermal relaxation errors over damping errors. Default is ``False``. - gate_times (Dict[str, float]): gate times for building thermal relaxation error. - If not provided, the default value of ``1.0 s`` will be used for construction. + gate_times (Dict[Tuple(str, Tuple[int]), float]): a dictionary to provide gate times for building + thermal relaxation error. Each key will be a tuple of instruction name and qubit indices and + the corresponding value will be the time in seconds. If it is not provided or is incomplete, + a default value of `1.0 s`` will be used for the specific constructions. multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not supported with ``qml.DepolarizationChannel``, which is a single qubit channel. readout_error (bool): include readout error in the converted noise model. Default is ``True``. @@ -410,7 +438,7 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: # 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, gate_name=gate_name, **kwargs) + noise_op = _build_qerror_op(error, gate_name=gate_name, gate_wires=qubits, **kwargs) qerror_dmap[noise_op][qubits].append(qiskit_op_map[gate_name]) # TODO: Add support for the readout error From 061ca00d224f860993835111cf1be0211c16c329 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 20:32:17 -0400 Subject: [PATCH 25/36] happy black --- pennylane_qiskit/noise_models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 8532150f..c3409bd7 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -162,6 +162,7 @@ def _extract_gate_time(gate_data: tuple[tuple[int], float], gate_wires: int) -> tg = gate_data[1] return tg + def _process_thermal_relaxation(choi_matrix, **kwargs): """Computes parameters for thermal relaxation error from a Choi matrix of Kraus matrices""" nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] @@ -173,7 +174,7 @@ def _process_thermal_relaxation(choi_matrix, **kwargs): ) and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol): tg = _extract_gate_time( gate_data=kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), None), - gate_wires=kwargs.get("gate_wires", None) + gate_wires=kwargs.get("gate_wires", None), ) if np.isclose(nt_values[[2, 3]].sum(), 0.0, rtol=rtol, atol=atol): pe, t1 = 0.0, np.inf @@ -263,7 +264,7 @@ def _process_reset(error_dict: dict, **kwargs) -> dict: error_dict["name"] = "ThermalRelaxationError" tg = _extract_gate_time( gate_data=kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), None), - gate_wires=kwargs.get("gate_wires", None) + gate_wires=kwargs.get("gate_wires", None), ) p0 = 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) t1 = -tg / np.log(1 - error_probs[2] / p0) From d2cfc4a4ea2c682267266b345563c0424aae82d3 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Fri, 12 Jul 2024 21:00:11 -0400 Subject: [PATCH 26/36] improve gate times --- pennylane_qiskit/converter.py | 6 +++--- pennylane_qiskit/noise_models.py | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index c00f88be..e0119472 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1060,9 +1060,9 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: thermal_relaxation (bool): prefer conversion of ``QiskitErrors`` to thermal relaxation errors over damping errors. Default is ``False``. readout_error (bool): include readout error in the converted noise model. Default is ``True``. - gate_times (Dict[str, Tuple[Tuple[int], float]]): a dictionary to provide gate times for building - thermal relaxation error. Each key will be the instruction name and the corresponding values - will be tuple of qubit indices and the time in seconds. If it is not provided or a gate/qubit + gate_times (Dict[Tuple(str, Tuple[int]), float]): a dictionary to provide gate times for building + thermal relaxation error. Each key will be a tuple of instruction name and qubit indices and + the corresponding value will be the time in seconds. If it is not provided or a gate/qubit is missing, then a default value of `1.0 s`` will be used for the specific constructions. optimize (bool): controls if a contraction order optimization is used for ``einsum`` while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index c3409bd7..868dd436 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -153,13 +153,15 @@ def _process_kraus_ops( return (False, "QubitChannel", Kraus(Choi(choi_matrix)).data) if not kdata else kdata -def _extract_gate_time(gate_data: tuple[tuple[int], float], gate_wires: int) -> float: +def _extract_gate_time(gate_data: dict, gate_name: str, gate_wires: int) -> float: """Helper method to extract gate time for a quantum error""" tg = 1.0 - if gate_data is not None: - gate_data[0] = (gate_data[0],) if isinstance(int, gate_data[0]) else gate_data[0] - if gate_wires in gate_data[0]: - tg = gate_data[1] + if fgates := dict(filter(lambda item: item[0][0] == gate_name, gate_data)): + for g_d, g_t in fgates.items(): + g_d[1] = (g_d[1],) if isinstance(int, g_d[1]) else g_d[1] + if gate_wires in g_d: + tg = g_t + break return tg @@ -173,7 +175,8 @@ def _process_thermal_relaxation(choi_matrix, **kwargs): nt_values[[(0, 2), (3, 5)]].sum(axis=1), 1.0, rtol=rtol, atol=atol ) and np.isclose(*nt_values[[1, 4]], rtol=rtol, atol=atol): tg = _extract_gate_time( - gate_data=kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), None), + gate_data=kwargs.get("gate_times", {}), + gate_name=kwargs.get("gate_name", ""), gate_wires=kwargs.get("gate_wires", None), ) if np.isclose(nt_values[[2, 3]].sum(), 0.0, rtol=rtol, atol=atol): @@ -263,7 +266,8 @@ def _process_reset(error_dict: dict, **kwargs) -> dict: else: # uses t1 > t2 error_dict["name"] = "ThermalRelaxationError" tg = _extract_gate_time( - gate_data=kwargs.get("gate_times", {}).get(kwargs.get("gate_name", None), None), + gate_data=kwargs.get("gate_times", {}), + gate_name=kwargs.get("gate_name", ""), gate_wires=kwargs.get("gate_wires", None), ) p0 = 1.0 if len(error_probs) == 3 else error_probs[2] / (error_probs[2] + error_probs[3]) @@ -409,8 +413,8 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: over damping errors. Default is ``False``. gate_times (Dict[Tuple(str, Tuple[int]), float]): a dictionary to provide gate times for building thermal relaxation error. Each key will be a tuple of instruction name and qubit indices and - the corresponding value will be the time in seconds. If it is not provided or is incomplete, - a default value of `1.0 s`` will be used for the specific constructions. + the corresponding value will be the time in seconds. If it is not provided or a gate/qubit + is missing, then a default value of `1.0 s`` will be used for the specific constructions. multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not supported with ``qml.DepolarizationChannel``, which is a single qubit channel. readout_error (bool): include readout error in the converted noise model. Default is ``True``. From 8d9e9ff25683e336478f8b9ca3baa4064371f75b Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sat, 13 Jul 2024 00:57:51 -0400 Subject: [PATCH 27/36] gate times test --- pennylane_qiskit/noise_models.py | 43 ++++++++++++++++++++++++-------- tests/test_noise_models.py | 27 +++++++++++++++++++- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 868dd436..71ef2d4d 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -156,17 +156,35 @@ def _process_kraus_ops( def _extract_gate_time(gate_data: dict, gate_name: str, gate_wires: int) -> float: """Helper method to extract gate time for a quantum error""" tg = 1.0 - if fgates := dict(filter(lambda item: item[0][0] == gate_name, gate_data)): - for g_d, g_t in fgates.items(): - g_d[1] = (g_d[1],) if isinstance(int, g_d[1]) else g_d[1] - if gate_wires in g_d: + if fgates := dict(filter(lambda item: item[0][0] == gate_name, gate_data.items())): + for (_, w_r), g_t in fgates.items(): + if w_r in gate_wires if isinstance(w_r, int) else set(gate_wires).issubset(w_r): tg = g_t break return tg -def _process_thermal_relaxation(choi_matrix, **kwargs): - """Computes parameters for thermal relaxation error from a Choi matrix of Kraus matrices""" +def _process_thermal_relaxation(choi_matrix, **kwargs) -> Tuple[bool, str, np.ndarray] or None: + r"""Computes the parameters for thermal relaxation error from a Choi matrix of Kraus matrices. + + Args: + choi_matrix (ndarray): Choi matrix of the channel to be processed. + + For plugin developers: This assumes :math:`T_1 < T_2 \leq 2 T_1`, where the error is expressed + as a general non-unitary Kraus error channel. + + .. math:: + + \begin{bmatrix} + 1 - p_e p_{r} & 0 & 0 & \exp{-T_g/T_2} \\ + 0 & p_e p_{r} & 0 & 0 \\ + 0 & 0 & (1 - p_e) p_{r} & 0 \\ + \exp{-T_g/T_2} & 0 & 0 & 1 - (1 - p_e) p_{r} + \end{bmatrix} + + Parameters :math:`p_e` is the excited-state population and :math:`p_r = 1 - \exp{-T_g/T_1}` + with :math:`T_g` as gate time and :math:`T_1\ (T_2)` as the relaxation (dephasing) constants. + """ nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] decimals, atol, rtol = tuple(map(kwargs.get, default_option_map)) @@ -249,7 +267,7 @@ def _process_depolarization(error_dict: dict, multi_pauli: bool = False) -> dict def _process_reset(error_dict: dict, **kwargs) -> dict: - """Checks parity of a qunatum error with ``Reset`` instruction to a PennyLane Channel. + r"""Checks parity of a qunatum error with ``Reset`` instruction to a PennyLane Channel. Args: error_dict (dict): error dictionary for the quantum error @@ -257,6 +275,9 @@ def _process_reset(error_dict: dict, **kwargs) -> dict: Returns: dict: An updated error dictionary based on parity with existing PennyLane channel. + + For plugin developers: second branch of the condition assumes reset error expressed + from a thermal relaxation error with :math:`T_2 \leq T_1`. """ error_probs = error_dict["probs"] @@ -351,7 +372,7 @@ def _build_qerror_op(error, **kwargs) -> qml.operation.Operation: Args: error (QuantumError): Quantum error object - kwargs: Optional keyword arguments used during conversion + kwargs: Optional keyword arguments used during conversion. Returns: qml.operation.Channel: converted PennyLane quantum channel which is @@ -411,15 +432,15 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: Keyword Arguments: thermal_relaxation (bool): prefer conversion of ``QiskitErrors`` to thermal relaxation errors over damping errors. Default is ``False``. + readout_error (bool): include readout error in the converted noise model. Default is ``True``. gate_times (Dict[Tuple(str, Tuple[int]), float]): a dictionary to provide gate times for building thermal relaxation error. Each key will be a tuple of instruction name and qubit indices and the corresponding value will be the time in seconds. If it is not provided or a gate/qubit is missing, then a default value of `1.0 s`` will be used for the specific constructions. - multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not - supported with ``qml.DepolarizationChannel``, which is a single qubit channel. - readout_error (bool): include readout error in the converted noise model. Default is ``True``. optimize (bool): controls if a contraction order optimization is used for ``einsum`` while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. + multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not + supported with ``qml.DepolarizationChannel``, which is a single qubit channel. options (dict[str, Union[int, float]]): optional parameters related to tolerance and rounding: - decimals (int): number of decimal places to round the Kraus matrices. Default is ``10``. diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index bfb4bd98..c5fd287b 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -174,7 +174,6 @@ def test_build_model_map(self, depol1, depol2, exc_pop): error_2 = noise.depolarizing_error(depol2, 2) error_3 = noise.phase_amplitude_damping_error(0.14, 0.24, excited_state_population=exc_pop) - # Add errors to noise model 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"]) @@ -201,3 +200,29 @@ def test_build_model_map(self, depol1, depol2, exc_pop): {AnyWires: ["CNOT"]}, {AnyWires: ["RY", "RX"]}, ] + + @pytest.mark.parametrize( + "gate_times", + [ + {("sx", (0, 1)): 2.0, ("rx", (0,)): 2.5, ("rx", (1,)): 3.0}, + {("sx", (0,)): 2.0, ("sx", (1,)): 2.5, ("rx", (0, 1)): 3.0}, + ], + ) + def test_thermal_gate_times(self, gate_times): + """Tests that a quantum error can be correctly converted into a PennyLane QubitChannel.""" + + pl_channels, pl_vals = [], [] + noise_model = noise.NoiseModel() + for gate_wires, time in gate_times.items(): + gate, wires = gate_wires + for wire in wires: + noise_model.add_quantum_error( + noise.thermal_relaxation_error(0.14, 0.24, time, 0.02), gate, (wire,) + ) + pl_channels.append(qml.ThermalRelaxationError(0.02, 0.14, 0.24, time, wires=AnyWires)) + pl_vals.append({(wire,): [gate.upper()] for wire in wires}) + + model_map, _ = _build_noise_model_map(noise_model, gate_times=gate_times) + + assert list(model_map.keys()) == pl_channels + assert list(model_map.values()) == pl_vals From 9f5f96cabaa7326dd613203ffbcb344d6099e7f1 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sat, 13 Jul 2024 01:15:28 -0400 Subject: [PATCH 28/36] minor tweak --- tests/test_noise_models.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index c5fd287b..c59ac23a 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -202,13 +202,13 @@ def test_build_model_map(self, depol1, depol2, exc_pop): ] @pytest.mark.parametrize( - "gate_times", + "t_times, gate_times", [ - {("sx", (0, 1)): 2.0, ("rx", (0,)): 2.5, ("rx", (1,)): 3.0}, - {("sx", (0,)): 2.0, ("sx", (1,)): 2.5, ("rx", (0, 1)): 3.0}, + ((0.14, 0.24), {("sx", (0, 1)): 2.0, ("rx", (0,)): 2.5, ("rx", (1,)): 3.0}), + ((0.53, 0.22), {("sx", (0,)): 2.0, ("sx", (1,)): 2.5, ("rx", (0, 1)): 3.0}), ], ) - def test_thermal_gate_times(self, gate_times): + def test_thermal_gate_times(self, t_times, gate_times): """Tests that a quantum error can be correctly converted into a PennyLane QubitChannel.""" pl_channels, pl_vals = [], [] @@ -217,12 +217,14 @@ def test_thermal_gate_times(self, gate_times): gate, wires = gate_wires for wire in wires: noise_model.add_quantum_error( - noise.thermal_relaxation_error(0.14, 0.24, time, 0.02), gate, (wire,) + noise.thermal_relaxation_error(*t_times, time, 0.02), gate, (wire,) ) - pl_channels.append(qml.ThermalRelaxationError(0.02, 0.14, 0.24, time, wires=AnyWires)) + pl_channels.append(qml.ThermalRelaxationError(0.02, *t_times, time, wires=AnyWires)) pl_vals.append({(wire,): [gate.upper()] for wire in wires}) - model_map, _ = _build_noise_model_map(noise_model, gate_times=gate_times) + model_map, _ = _build_noise_model_map( + noise_model, gate_times=gate_times, options={"decimals": 8, "atol": 1e-8} + ) assert list(model_map.keys()) == pl_channels assert list(model_map.values()) == pl_vals From 123cf4312b22940415a03fc5f855a1373ac208d7 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 19:51:48 -0400 Subject: [PATCH 29/36] minor tweaks --- pennylane_qiskit/converter.py | 9 +++------ tests/test_noise_models.py | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index e0119472..2bbf107b 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1094,11 +1094,11 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: >>> 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']) # rz/ry gates get error_1 - >>> noise_model.add_all_qubit_quantum_error(error_2, ['cx']) # cx gates get error_2 + >>> noise_model.add_all_qubit_quantum_error(error_1, ['rz', 'ry']) + >>> noise_model.add_all_qubit_quantum_error(error_2, ['cx']) >>> load_noise_model(noise_model) NoiseModel({ - OpIn(['RZ', 'RY']): DepolarizingChannel(p=0.0007499999999999174) + OpIn(['RZ', 'RY']): DepolarizingChannel(p=0.00075) OpIn(['CNOT']): QubitChannel(Klist=Tensor(16, 4, 4)) }) @@ -1129,17 +1129,14 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: equivalent_pl_noise_model = qml.NoiseModel({c0: n0, c1: n1}) """ - qerror_dmap, _ = _build_noise_model_map(noise_model, **kwargs) model_map = {} for error, wires_map in qerror_dmap.items(): conditions = [] - cwires = [] for wires, operations in wires_map.items(): cond = qml.noise.op_in(operations) if wires != AnyWires: cond &= WiresIn(wires) - cwires.append(wires) conditions.append(cond) fcond = reduce(lambda cond1, cond2: cond1 | cond2, conditions) diff --git a/tests/test_noise_models.py b/tests/test_noise_models.py index c59ac23a..166a5c10 100644 --- a/tests/test_noise_models.py +++ b/tests/test_noise_models.py @@ -209,7 +209,7 @@ def test_build_model_map(self, depol1, depol2, exc_pop): ], ) def test_thermal_gate_times(self, t_times, gate_times): - """Tests that a quantum error can be correctly converted into a PennyLane QubitChannel.""" + """Tests that thermal relaxation computation uses provided gate times.""" pl_channels, pl_vals = [], [] noise_model = noise.NoiseModel() @@ -228,3 +228,26 @@ def test_thermal_gate_times(self, t_times, gate_times): assert list(model_map.keys()) == pl_channels assert list(model_map.values()) == pl_vals + + @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 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 = _kraus_to_choi(Kraus(combined_error)) + choi_mat2 = _kraus_to_choi( + Kraus(list(pl_op_from_qiskit.compute_kraus_matrices(*pl_op_from_qiskit.data))) + ) + assert np.allclose(choi_mat1, choi_mat2) From 787c85ec810cf9d2377a1b470f98c98a4cee59bc Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 19:52:58 -0400 Subject: [PATCH 30/36] docs tweak --- pennylane_qiskit/converter.py | 2 +- pennylane_qiskit/noise_models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 2bbf107b..f5551165 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1063,7 +1063,7 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: gate_times (Dict[Tuple(str, Tuple[int]), float]): a dictionary to provide gate times for building thermal relaxation error. Each key will be a tuple of instruction name and qubit indices and the corresponding value will be the time in seconds. If it is not provided or a gate/qubit - is missing, then a default value of `1.0 s`` will be used for the specific constructions. + is missing, then a default value of ``1.0 s`` will be used for the specific constructions. optimize (bool): controls if a contraction order optimization is used for ``einsum`` while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. kraus_shape (bool): use shape of the Kraus operators to display ``qml.QubitChannel`` diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 71ef2d4d..c6db2afe 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -436,7 +436,7 @@ def _build_noise_model_map(noise_model, **kwargs) -> Tuple[dict, dict]: gate_times (Dict[Tuple(str, Tuple[int]), float]): a dictionary to provide gate times for building thermal relaxation error. Each key will be a tuple of instruction name and qubit indices and the corresponding value will be the time in seconds. If it is not provided or a gate/qubit - is missing, then a default value of `1.0 s`` will be used for the specific constructions. + is missing, then a default value of ``1.0 s`` will be used for the specific constructions. optimize (bool): controls if a contraction order optimization is used for ``einsum`` while transforming Kraus operators to a Choi matrix, wherever required. Default is ``False``. multi_pauli (bool): assume depolarization channel to be multi-qubit. This is currently not From c2af70d353ff4773cc376a79d06ad58a287b81aa Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 19:59:33 -0400 Subject: [PATCH 31/36] tweaks --- pennylane_qiskit/converter.py | 6 +++--- pennylane_qiskit/noise_models.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index f5551165..77f86d74 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1070,9 +1070,9 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: instead of the complete list of matrices. Default is ``True``. options (dict[str, Union[int, float]]): optional parameters related to tolerance and rounding: - - decimals (int): number of decimal places to round the Kraus matrices. Default is ``10``. - - atol (float): the relative tolerance parameter. Default value is ``1e-05``. - - rtol (float): the absolute tolernace parameters. Defualt value is ``1e-08``. + - **decimals** (*int*): number of decimal places to round the Kraus matrices. Default is ``10``. + - **atol** (*float*): the relative tolerance parameter. Default value is ``1e-05``. + - **rtol** (*float*): the absolute tolernace parameters. Defualt value is ``1e-08``. Returns: qml.NoiseModel: An equivalent noise model constructed in PennyLane diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index c6db2afe..4933b20e 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -249,10 +249,9 @@ def _process_depolarization(error_dict: dict, multi_pauli: bool = False) -> dict num_terms = 4**num_wires id_factor = num_terms / (num_terms - 1) prob_iden = error_dict["probs"][error_dict["data"].index(["I" * num_wires])] - param = id_factor * (1 - prob_iden) error_dict["name"] = "DepolarizingChannel" - error_dict["data"] = param / id_factor - error_dict["probs"] = param + error_dict["data"] = 1 - prob_iden + error_dict["probs"] = id_factor * (1 - prob_iden) return error_dict error_dict["name"] = "QubitChannel" From ff7306685128b2501cf23cb38a32e3ab3faa35fb Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 20:02:06 -0400 Subject: [PATCH 32/36] tweak --- pennylane_qiskit/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 77f86d74..e313750a 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1118,8 +1118,8 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: repeat=2 ) ] - pauli_prob = error_2.probabilities - kraus_ops = [np.sqrt(prob) * kraus_op for prob, kraus_op in zip(pauli_prob, pauli_mats)] + pauli_prob = np.sqrt(error_2.probabilities) + kraus_ops = [prob * kraus_op for prob, kraus_op in zip(pauli_prob, pauli_mats)] c0 = qml.noise.op_eq(qml.RZ) | qml.noise.op_eq(qml.RY) c1 = qml.noise.op_eq(qml.CNOT) From 4b97f4dc73bd23c70d2e3c352997437e78f3bf96 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 20:03:57 -0400 Subject: [PATCH 33/36] whitespaces --- pennylane_qiskit/noise_models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane_qiskit/noise_models.py b/pennylane_qiskit/noise_models.py index 4933b20e..4ae3ec3a 100644 --- a/pennylane_qiskit/noise_models.py +++ b/pennylane_qiskit/noise_models.py @@ -166,11 +166,11 @@ def _extract_gate_time(gate_data: dict, gate_name: str, gate_wires: int) -> floa def _process_thermal_relaxation(choi_matrix, **kwargs) -> Tuple[bool, str, np.ndarray] or None: r"""Computes the parameters for thermal relaxation error from a Choi matrix of Kraus matrices. - + Args: choi_matrix (ndarray): Choi matrix of the channel to be processed. - For plugin developers: This assumes :math:`T_1 < T_2 \leq 2 T_1`, where the error is expressed + For plugin developers: This assumes :math:`T_1 < T_2 \leq 2 T_1`, where the error is expressed as a general non-unitary Kraus error channel. .. math:: @@ -183,7 +183,7 @@ def _process_thermal_relaxation(choi_matrix, **kwargs) -> Tuple[bool, str, np.nd \end{bmatrix} Parameters :math:`p_e` is the excited-state population and :math:`p_r = 1 - \exp{-T_g/T_1}` - with :math:`T_g` as gate time and :math:`T_1\ (T_2)` as the relaxation (dephasing) constants. + with :math:`T_g` as gate time and :math:`T_1\ (T_2)` as the relaxation (dephasing) constants. """ nt_values = choi_matrix[tuple(zip(*sorted(kraus_indice_map["ThermalRelaxation"])))] decimals, atol, rtol = tuple(map(kwargs.get, default_option_map)) From cfb6304fd3ea16362f37349916d3d3fb1b417512 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 20:30:39 -0400 Subject: [PATCH 34/36] doc fix? --- pennylane_qiskit/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index e313750a..aacebfe3 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1050,7 +1050,7 @@ def _expr_eval_clvals(clbits, clvals, expr_func, bitwise=False): def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: """Loads a PennyLane `NoiseModel `_ - from a Qiskit `NoiseModel `_. + from a Qiskit `noise model `_. Args: noise_model (qiskit_aer.noise.NoiseModel): A Qiskit noise model object @@ -1075,7 +1075,7 @@ def load_noise_model(noise_model, **kwargs) -> qml.NoiseModel: - **rtol** (*float*): the absolute tolernace parameters. Defualt value is ``1e-08``. Returns: - qml.NoiseModel: An equivalent noise model constructed in PennyLane + pennylane.NoiseModel: An equivalent noise model constructed in PennyLane Raises: ValueError: When an encountered quantum error cannoted be converted. From 1af5805c473feadd7112f81a63f0fca64169e7db Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 23:01:50 -0400 Subject: [PATCH 35/36] improve a test --- tests/test_converter.py | 113 ++++------------------------------------ 1 file changed, 11 insertions(+), 102 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 5577cbf2..66962cc2 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -2220,10 +2220,13 @@ def test_build_noise_model(self): from qiskit.providers.fake_provider import FakeOpenPulse2Q from qiskit_aer.noise import NoiseModel + from qiskit.quantum_info.operators.channel import Kraus from pennylane.noise import op_in, wires_in, partial_wires from pennylane.operation import AnyWires + from pennylane_qiskit.noise_models import _kraus_to_choi - loaded_noise_model = load_noise_model(NoiseModel.from_backend(FakeOpenPulse2Q())) + noise_model = NoiseModel.from_backend(FakeOpenPulse2Q()) + loaded_noise_model = load_noise_model(noise_model) pl_model_map = { op_in("Identity") @@ -2256,106 +2259,7 @@ def test_build_noise_model(self): ), op_in("CNOT") & wires_in([0, 1]): qml.QubitChannel( - np.array( - [ - [ - [-0.00630187 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.00630572 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.00630526 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.00630911 + 0.0j], - ], - [ - [-0.43615418 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.17440094 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, 0.20809143 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.05338186 + 0.0j], - ], - [ - [-0.09661797 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.18973192 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.15719968 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.44324055 + 0.0j], - ], - [ - [0.02447349 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.36545892 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, 0.36329539 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.02225592 + 0.0j], - ], - [ - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.5163849 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [-0.51620014 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.51659541 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, -0.0 + 0.0j, -0.51356736 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.05566075 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [-0.0 + 0.0j, -0.51161899 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.07136846 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - ], - [ - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [0.03615696 + 0.0j, -0.51514324 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.51514324 + 0.0j, -0.03615696 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, 0.07134191 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.51142865 + 0.0j], - [0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, 0.0 + 0.0j, -0.05563753 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.51335309 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.5163849 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, 0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, -0.14620844 + 0.0j, 0.49506128 + 0.0j, 0.0 + 0.0j], - ], - [ - [0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [-0.0 + 0.0j, 0.0 + 0.0j, -0.0 + 0.0j, -0.0 + 0.0j], - [0.0 + 0.0j, -0.49506128 + 0.0j, -0.14620844 + 0.0j, 0.0 + 0.0j], - ], - ] - ), + Kraus(noise_model._local_quantum_errors["cx"][(0, 1)]).data, wires=AnyWires, ), } @@ -2370,4 +2274,9 @@ def test_build_noise_model(self): pl_op, qk_op = pl_v(AnyWires), qk_v(AnyWires) assert repr(pl_k) == repr(qk_k) assert isinstance(pl_op, type(qk_op)) - assert np.allclose(pl_op.data, qk_op.data, atol=1e-5) + if not isinstance(pl_op, qml.QubitChannel): + assert np.allclose(pl_op.data, qk_op.data) + else: + choi_mat1 = _kraus_to_choi(Kraus(qk_op.data[0])) + choi_mat2 = _kraus_to_choi(Kraus(pl_op.data[0])) + assert np.allclose(choi_mat1, choi_mat2) From 53c26c11f0a1b2d320c6d21d4d906235ca1fb47f Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Sun, 14 Jul 2024 23:09:45 -0400 Subject: [PATCH 36/36] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c439fc..0bbebfb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ### New features since last release +* Added the support for converting Qiskit noise models to + PennyLane ``NoiseModels`` using ``load_noise_model``. + [(#569)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/569) + ### Improvements 🛠 ### Breaking changes 💔 @@ -16,6 +20,9 @@ This release contains contributions from (in alphabetical order): +Utkarsh Azad + + --- # Release 0.37.0