From 54bd21b57f0dbd42497e8ce31e7045c3b52f8ab2 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 12:51:29 +0300 Subject: [PATCH 01/28] Previous stable version of the noise transformation module --- .../aer/noise/noise_transformation.py | 405 ++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 qiskit/providers/aer/noise/noise_transformation.py diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py new file mode 100644 index 0000000000..faec53874f --- /dev/null +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -0,0 +1,405 @@ +# coding: utf-8 + +import numpy +import sympy +import itertools +import copy + +from functools import singledispatch + + +class NoiseTransformer: + """Transforms one quantum noise channel to another based on a specified criteria. + + A quantum 1-qubit noise channel is represented by Kraus operators: a sequence (E0, E1,...,En) of + 2x2 matrices that satisfy \sum_{i=0}^n E_i^\daggerE_i = I + + Given a quantum state's density function rho, the effect of the channel on this state is + rho -> \sum_{i=1}^n E_i * rho * E_i^\dagger + + The goal of this module is to transform one noise channel into another, where the goal channel + is constructed from a given set of matrices, using coefficients computed by the module in order + to satisfy some criteria (for now, we wish the output channel to be "close" to the input channel) + + The main public function of the module is transform(), and a conversion of qobj inputs is given + via the transform_qobj() function. + """ + + def __init__(self): + # the premade operators can be accessed by calling transform() with the given name string + self.premade_operators = { + 'pauli': {'X': numpy.array([[0, 1], [1, 0]]), + 'Y': numpy.array([[0, -1j], [1j, 0]]), + 'Z': numpy.array([[1, 0], [0, -1]]) + }, + 'relaxation': { + 'p': (numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])), + 'q': (numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])), + }, + 'clifford': self.single_qubit_full_clifford_group() + } + self.transform = singledispatch(self.transform) + self.transform.register(list, self.transform_by_operator_list) + self.transform.register(tuple, self.transform_by_operator_list) + self.transform.register(str, self.transform_by_operator_string) + self.transform.register(dict, self.transform_by_operator_dictionary) + self.fidelity_data = None + self.use_honesty_constraint = True + self.noise_kraus_operators = None + self.transform_channel_operators = None + + # transformation interface methods + + def transform(self, transform_channel_operators, noise_kraus_operators): + """Transforms the general noise given as a list of noise_kraus_operators using the operators + given via transform_channel_operators which can be list, dict or string""" + raise RuntimeError("{} is not an appropriate input to transform".format(transform_channel_operators)) + + def transform_by_operator_list(self, transform_channel_operators, noise_kraus_operators): + """Main transformation function; the other transformation functions use this one + + Args: + noise_kraus_operators: a list of matrices (Kraus operators) for the input channel + transform_channel_operators: a list of matrices or tuples of matrices + representing Kraus operators that can construct the output channel + e.g. [X,Y,Z] represent the Pauli channel + and [(|0><0|, |0><1|), |1><0|, |1><1|)] represents the relaxation channel + + + Output: + A list of amplitudes that define the output channel. + In the case the input is a list [A1, A2, ..., An] of transform matrices + and [E0, E1, ..., Em] of noise kraus operators, the output is + a list [p1, p2, ..., pn] of probabilities such that: + 1) p_i >= 0 + 2) p1 + ... + pn <= 1 + 3) [sqrt(p1)A1, sqrt(p2)A2, ..., sqrt(pn)An, sqrt(1-(p1 + ... + pn))I] is + a list of kraus operators that define the output channel + (which is "close" to the input chanel given by [E0, ..., Em]) + + This channel can be thought of as choosing the operator Ai in probability pi and applying + this operator to the quantum state. + + More generally, if the input is a list of tuples (not neccesarily of the same size): + [(A1, B1, ...), (A2, B2, ...), ... (An, Bn, ...)] then the output is + still a list [p1, p2, ..., pn] and now the output channel is defined by the operators + [sqrt(p1)A1, sqrt(p1)B1, ..., sqrt(pn)An, sqrt(pn)Bn, ..., sqrt(1-(p1 + ... + pn))I] + """ + self.noise_kraus_operators = noise_kraus_operators + self.transform_channel_operators = transform_channel_operators + full_transform_channel_operators = self.prepare_channel_operator_list(self.transform_channel_operators) + channel_matrices, const_channel_matrix = self.generate_channel_matrices(full_transform_channel_operators) + self.prepare_honesty_constraint(full_transform_channel_operators) + probabilities = self.transform_by_given_channel(channel_matrices, const_channel_matrix) + return probabilities + + # convert to sympy matrices and verify that each singleton is in a tuple; also add identity matrix + @staticmethod + def prepare_channel_operator_list(ops_list): + result = [[sympy.eye(2)]] + for ops in ops_list: + if not isinstance(ops, tuple) and not isinstance(ops, list): + ops = [ops] + result.append([sympy.Matrix(op) for op in ops]) + return result + + def prepare_honesty_constraint(self, transform_channel_operators_list): + if not self.use_honesty_constraint: + return + goal = self.fidelity(self.noise_kraus_operators) + coefficients = [self.fidelity(ops) for ops in transform_channel_operators_list] + self.fidelity_data = { + 'goal': goal, + 'coefficients': coefficients[1:] # coefficients[0] corresponds to I + } + + def transform_by_operator_string(self, transform_channel_operators_string, noise_kraus_operators): + if transform_channel_operators_string in self.premade_operators: + return self.transform_by_operator_dictionary( + self.premade_operators[transform_channel_operators_string], noise_kraus_operators) + raise RuntimeError("No information about noise type {}".format(transform_channel_operators_string)) + + def transform_by_operator_dictionary(self, transform_channel_operators_dictionary, noise_kraus_operators): + names, operators = zip(*transform_channel_operators_dictionary.items()) + probabilities = self.transform_by_operator_list(operators, noise_kraus_operators) + return dict(zip(names, probabilities)) + + def qobj_noise_to_kraus_operators(self, noise): + # Noises are given as a list of probabilities [p1, p2, ..., pn] and circuits [C1, C2, ..., Cn] + # Each circuit consists of a list of noise operators that are applied sqeuentially + # If (E_0, E_1, ..., E_n) and (F_0, F_1, ...., F_m) are applied sequentially, we can "merge" + # them into one noise with operators (E_iF_j) for 0 <= i <= n and 0 <= j <= m + # This is then multiplied by sqrt(pk) for the circuit k + probabilities = noise['probabilities'] + circuits = noise['instructions'] + operators_for_circuits = [self.noise_circuit_to_kraus_operators(c) for c in circuits] + final_operators_list = [] + for k, ops in enumerate(operators_for_circuits): + p = numpy.sqrt(probabilities[k]) + final_operators_list += [p * op for op in ops] + return final_operators_list + + def noise_circuit_to_kraus_operators(self, circuit): + # first, convert the list of dict-based noises to actual lists of matrices + operators_list = [self.qobj_instruction_to_matrices(instruction) for instruction in circuit] + Id = numpy.array([[1,0], [0,1]]) + final_operators = [Id] + for operators in operators_list: + final_operators = [op[0].dot(op[1]) for op in itertools.product(final_operators, operators)] + return final_operators + + def qobj_instruction_to_matrices(self, instruction): + if instruction['name'] == 'kraus': + kraus_operators = [self.qobj_matrix_to_numpy_matrix(m) for m in instruction['params']] + return kraus_operators + raise RuntimeError("Does not know how to handle noises of type {}".format(instruction['name'])) + + def transform_qobj(self, transform_channel, qobj): + result_qobj = copy.deepcopy(qobj) + try: + noise_list = result_qobj["config"]["noise_model"]["errors"] + except KeyError: + return result_qobj # if we can't find noises, we don't change anything + for noise in noise_list: + noise_in_kraus_form = self.qobj_noise_to_kraus_operators(noise) + probabilities = self.transform(transform_channel, noise_in_kraus_form) + transformed_channel_kraus_operators = self.probability_list_to_matrices(probabilities, transform_channel) + qobj_kraus_operators = [self.numpy_matrix_to_qobj_matrix(m) for m in transformed_channel_kraus_operators] + noise['probabilities'] = [1.0] + noise['instructions'] = [[{"name": "kraus", "qubits": [0], "params": qobj_kraus_operators}]] + return result_qobj + + @staticmethod + def qobj_matrix_to_numpy_matrix(m): + return numpy.array([[entry[0] + entry[1] * 1j for entry in row] for row in m]) + + @staticmethod + def numpy_matrix_to_qobj_matrix(m): + return [[[numpy.real(a), numpy.imag(a)] for a in row] for row in (numpy.array(m))] + + def probability_list_to_matrices(self, probs, transform_channel): + # probs are either a list or a dict of pairs of the form (probability, matrix_list) + if isinstance(probs, dict): + probs = probs.values() + transformation_data = zip(probs, self.transform_channel_operators) + # now assume probs is a list of pairs of the form (probability, matrix_list) + id_matrix = numpy.eye(2) * numpy.sqrt(1 - sum(probs)) + return [id_matrix] + [numpy.sqrt(prob) * matrix for (prob, matrix_list) in transformation_data for matrix in matrix_list] + + # methods relevant to the transformation to quadratic programming instance + + @staticmethod + def fidelity(channel): + return sum([numpy.abs(numpy.trace(E)) ** 2 for E in channel]) + + def generate_channel_matrices(self, transform_channel_operators_list): + """ + Generates a list of 4x4 symbolic matrices describing the channel defined from the given operators + + Args: + transform_channel_operators_list: a list of tuples of matrices which represent Kraus operators + The identity matrix is assumed to be the first element in the list + [(I, ), (A1, B1, ...), (A2, B2, ...), ..., (An, Bn, ...)] + e.g. for a Pauli channel, the matrices are + [(I,), (X,), (Y,), (Z,)] + for relaxation they are + [(I, ), (|0><0|, |0><1|), |1><0|, |1><1|)] + + We consider this input to symbolically represent a channel in the following manner: + define indeterminates x0, x1, ..., xn which are meant to represent probabilities + such that xi >=0 and x0 = 1-(x1 + ... + xn) + Now consider the quantum channel defined via the Kraus operators + {sqrt(x0)I, sqrt(x1)A1, sqrt(x1)B1, ..., sqrt(xn)An, sqrt(xn)Bn, ...} + This is the channel C symbolically represented by the operators + + + Output: + A list of 4x4 complex matrices ([D1, D2, ..., Dn], E) such that: + The matrix x1*D1 + ... + xn*Dn + E represents the operation of the channel C on the density operator + we find it easier to work with this representation of C when performing the combinatorial optimization + """ + + symbols_string = " ".join(["x{}".format(i) for i in range(len(transform_channel_operators_list))]) + symbols = sympy.symbols(symbols_string, real=True, positive=True) + exp = symbols[1] # exp will contain the symbolic expression "x1 +...+ xn" + for i in range(2, len(symbols)): + exp = symbols[i] + exp + # symbolic_operators_list is a list of lists; we flatten it the next line + symbolic_operators_list = [[sympy.sqrt(symbols[i]) * op for op in ops] + for (i, ops) in enumerate(transform_channel_operators_list)] + symbolic_operators = [op for ops in symbolic_operators_list for op in ops] + # channel_matrix_representation() peforms the required linear algebra to find the representing matrices. + operators_channel = self.channel_matrix_representation(symbolic_operators).subs(symbols[0], 1 - exp) + return self.generate_channel_quadratic_programming_matrices(operators_channel, symbols[1:]) + + @staticmethod + def compute_channel_operation(rho, operators): + """ + Given a quantum state's density function rho, the effect of the channel on this state is + rho -> \sum_{i=1}^n E_i * rho * E_i^\dagger + """ + + return sum([E * rho * E.H for E in operators], sympy.zeros(operators[0].rows)) + + @staticmethod + def flatten_matrix(m): + return [element for element in m] + + def channel_matrix_representation(self, operators): + """ + We convert the operators to a matrix by applying the channel to the four basis elements of + the 2x2 matrix space representing density operators; this is standard linear algebra + """ + standard_base = [ + sympy.Matrix([[1, 0], [0, 0]]), + sympy.Matrix([[0, 1], [0, 0]]), + sympy.Matrix([[0, 0], [1, 0]]), + sympy.Matrix([[0, 0], [0, 1]]) + ] + return (sympy.Matrix([self.flatten_matrix(self.compute_channel_operation(rho, operators)) + for rho in standard_base])) + + def generate_channel_quadratic_programming_matrices(self, channel, symbols): + """ + Args: + channel: a 4x4 symbolic matrix + symbols: the symbols x1, ..., xn which may occur in the matrix + + Output: + A list of 4x4 complex matrices ([D1, D2, ..., Dn], E) such that: + channel == x1*D1 + ... + xn*Dn + E + """ + return ( + [self.get_matrix_from_channel(channel, symbol) for symbol in symbols], + self.get_const_matrix_from_channel(channel, symbols) + ) + + @staticmethod + def get_matrix_from_channel(channel, symbol): + """ + Args: + channel: a 4x4 symbolic matrix. + Each entry is assumed to be a polynomial of the form a1x1 + ... + anxn + c + symbol: a symbol xi + + Output: + A 4x4 numerical matrix where for each entry, + if a1x1 + ... + anxn + c was the corresponding entry in the input channel + then the corresponding entry in the output matrix is ai. + """ + n = channel.rows + M = numpy.zeros((n, n), dtype=numpy.complex_) + for (i, j) in itertools.product(range(n), range(n)): + M[i, j] = numpy.complex(sympy.Poly(channel[i, j], symbol).coeff_monomial(symbol)) + return M + + @staticmethod + def get_const_matrix_from_channel(channel, symbols): + """ + Args: + channel: a 4x4 symbolic matrix. + Each entry is assumed to be a polynomial of the form a1x1 + ... + anxn + c + symbols: The full list [x1, ..., xn] of symbols used in the matrix + + Output: + A 4x4 numerical matrix where for each entry, + if a1x1 + ... + anxn + c was the corresponding entry in the input channel + then the corresponding entry in the output matrix is c. + """ + n = channel.rows + M = numpy.zeros((n, n), dtype=numpy.complex_) + for (i, j) in itertools.product(range(n), range(n)): + M[i, j] = numpy.complex(sympy.Poly(channel[i, j], symbols).coeff_monomial(1)) + return M + + def transform_by_given_channel(self, channel_matrices, const_channel_matrix): + """ + This method creates the quadratic programming instance for minimizing the Hilbert-Schmidt + norm of the matrix (A-B) obtained as the difference of the input noise channel and the + output channel we wish to determine. + """ + target_channel_matrix = self.numeric_channel_matrix_representation(self.noise_kraus_operators) + const_matrix = const_channel_matrix - target_channel_matrix + P = self.compute_P(channel_matrices) + q = self.compute_q(channel_matrices, const_matrix) + return self.solve_quadratic_program(P, q) + + @staticmethod + def numeric_compute_channel_operation(rho, operators): + return numpy.sum([E.dot(rho).dot(E.conj().T) for E in operators], 0) + + def numeric_channel_matrix_representation(self, operators): + standard_base = [ + numpy.array([[1, 0], [0, 0]]), + numpy.array([[0, 1], [0, 0]]), + numpy.array([[0, 0], [1, 0]]), + numpy.array([[0, 0], [0, 1]]) + ] + return (numpy.array([self.numeric_compute_channel_operation(rho, operators).flatten() + for rho in standard_base])) + + def compute_P(self, As): + vs = [numpy.array(A).flatten() for A in As] + n = len(vs) + P = sympy.zeros(n, n) + for (i, j) in itertools.product(range(n), range(n)): + P[i, j] = 2 * numpy.real(numpy.dot(vs[i], numpy.conj(vs[j]))) + return P + + def compute_q(self, As, C): + vs = [numpy.array(A).flatten() for A in As] + vC = numpy.array(C).flatten() + n = len(vs) + q = sympy.zeros(1, n) + for i in range(n): + q[i] = 2 * numpy.real(numpy.dot(numpy.conj(vC), vs[i])) + return q + + # the following method is the only place in the code where we rely on the cvxopt library + # should we consider another library, only this method needs to change + def solve_quadratic_program(self, P, q): + try: + import cvxopt + except ImportError: + raise ImportError("The CVXOPT library is required to use this module") + P = cvxopt.matrix(numpy.array(P).astype(float)) + q = cvxopt.matrix(numpy.array(q).astype(float)).T + n = len(q) + # G and h constrain: + # 1) sum of probs is less then 1 + # 2) All probs bigger than 0 + # 3) Honesty (measured using fidelity, if given) + G_data = [[1] * n] + [([-1 if i == k else 0 for i in range(n)]) for k in range(n)] + h_data = [1] + [0] * n + if self.fidelity_data is not None: + G_data.append(self.fidelity_data['coefficients']) + h_data.append(self.fidelity_data['goal']) + G = cvxopt.matrix(numpy.array(G_data).astype(float)) + h = cvxopt.matrix(numpy.array(h_data).astype(float)) + cvxopt.solvers.options['show_progress'] = False + return cvxopt.solvers.qp(P, q, G, h)['x'] + + def single_qubit_full_clifford_group(self): + # easy representation of all 23 non-identity 1-qubit Clifford group representation by using only S,H,X,Y,Z + clifford_ops_names = [ + "X", "Y", "Z", + "S", "SX", "SY", "SZ", + "SH", "SHX", "SHY", "SHZ", + "SHS", "SHSX", "SHSY", "SHSZ", + "SHSH", "SHSHX", "SHSHY", "SHSHZ", + "SHSHS", "SHSHSX", "SHSHSY", "SHSHSZ", + ] + base_ops = { + 'X': numpy.array([[0, 1], [1, 0]]), + 'Y': numpy.array([[0, -1j], [1j, 0]]), + 'Z': numpy.array([[1, 0], [0, -1]]), + 'S': numpy.array([[1, 0], [0, 1j]]), + 'H': numpy.sqrt(2) * numpy.array([[1, 1], [1, -1]]) + } + return dict([(name, self.op_name_to_matrix(name, base_ops)) for name in clifford_ops_names]) + + def op_name_to_matrix(self, name, base_ops): + m = numpy.array([[1, 0], [0, 1]]) + for op_name in name: + m = m * base_ops[op_name] + return m \ No newline at end of file From cb9581dece37739420206e4666ba7e4fde6399d8 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 12:56:10 +0300 Subject: [PATCH 02/28] Previous stable version of the noise transformation unit tests --- test/terra/test_noise_transformation.py | 184 ++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 test/terra/test_noise_transformation.py diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py new file mode 100644 index 0000000000..ced59af26b --- /dev/null +++ b/test/terra/test_noise_transformation.py @@ -0,0 +1,184 @@ +import unittest +from aer.noise import NoiseTransformer +import numpy + +#TODO: skip tests if CVXOPT is not present + +class TestNoiseTransformer(unittest.TestCase): + def setUp(self): + #TODO: replace with Qiskit based defs + X = numpy.array([[0, 1], [1, 0]]) + Y = numpy.array([[0, -1j], [1j, 0]]) + Z = numpy.array([[1, 0], [0, -1]]) + S = numpy.array([[1, 0], [0, 1j]]) + H = numpy.sqrt(2) * numpy.array([[1, 1], [1, -1]]) + self.ops = {'X': X, 'Y': Y, 'Z': Z, 'H': H, 'S': S} + self.n = NoiseTransformer() + + def assertDictAlmostEqual(self, lhs, rhs, places = None): + keys = set(lhs.keys()).union(set(rhs.keys())) + for key in keys: + self.assertAlmostEqual(lhs.get(key), rhs.get(key), msg = "Not almost equal for key {}: {} !~ {}".format(key, lhs.get(key), rhs.get(key)), places = places) + + def assertListAlmostEqual(self, lhs, rhs, places = None): + self.assertEqual(len(lhs), len(rhs), msg = "List lengths differ: {} != {}".format(len(lhs), len(rhs))) + for i in range(len(lhs)): + if isinstance(lhs[i], numpy.ndarray) and isinstance(rhs[i], numpy.ndarray): + self.assertMatricesAlmostEqual(lhs[i], rhs[i], places = places) + else: + self.assertAlmostEqual(lhs[i], rhs[i], places = places) + + def assertMatricesAlmostEqual(self, lhs, rhs, places = None): + self.assertEqual(lhs.shape, rhs.shape, "Marix shapes differ: {} vs {}".format(lhs, rhs)) + n, m = lhs.shape + for x in range(n): + for y in range(m): + self.assertAlmostEqual(lhs[x,y], rhs[x,y], places = places, msg="Matrices {} and {} differ on ({}, {})".format(lhs, rhs, x, y)) + + + def test_transformation_by_pauli(self): + n = NoiseTransformer() + #polarization + X = self.ops['X'] + Y = self.ops['Y'] + Z = self.ops['Z'] + p = 0.22 + theta = numpy.pi / 5 + E0 = numpy.sqrt(1 - p) * numpy.array(numpy.eye(2)) + E1 = numpy.sqrt(p) * (numpy.cos(theta) * X + numpy.sin(theta) * Y) + results = n.transform({"X": X, "Y": Y, "Z": Z}, (E0, E1)) + expected_results = {'X': p*numpy.cos(theta)*numpy.cos(theta), 'Y': p*numpy.sin(theta)*numpy.sin(theta), 'Z': 0} + for op in results.keys(): + self.assertAlmostEqual(expected_results[op], results[op], 3) + + #now try again without fidelity; should be the same + n.use_honesty_constraint = False + results = n.transform({"X": X, "Y": Y, "Z": Z}, (E0, E1)) + for op in results.keys(): + self.assertAlmostEqual(expected_results[op], results[op], 3) + + def test_relaxation(self): + # amplitude damping + gamma = 0.5 + E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) + E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) + results = self.n.transform("relaxation", (E0, E1)) + expected_results = {'p': (gamma - numpy.sqrt(1 - gamma) + 1) / 2, 'q': 0} + for op in results.keys(): + self.assertAlmostEqual(expected_results[op], results[op], 3) + + def test_transform(self): + X = self.ops['X'] + Y = self.ops['Y'] + Z = self.ops['Z'] + p = 0.34 + theta = numpy.pi / 7 + E0 = numpy.sqrt(1 - p) * numpy.array(numpy.eye(2)) + E1 = numpy.sqrt(p) * (numpy.cos(theta) * X + numpy.sin(theta) * Y) + + results_dict = self.n.transform({"X": X, "Y": Y, "Z": Z}, (E0, E1)) + results_string = self.n.transform('pauli', (E0, E1)) + results_list = list(self.n.transform([X, Y, Z], (E0, E1))) + results_tuple = list(self.n.transform((X, Y, Z), (E0, E1))) + results_list_as_dict = dict(zip(['X', 'Y', 'Z'],results_list)) + + self.assertDictAlmostEqual(results_dict, results_string) + self.assertListAlmostEqual(results_list, results_tuple) + self.assertDictAlmostEqual(results_list_as_dict, results_dict) + + def test_fidelity(self): + n = NoiseTransformer() + expected_fidelity = {'X': 0, 'Y': 0, 'Z': 0, 'H': 0, 'S': 2} + for key in expected_fidelity: + self.assertAlmostEqual(expected_fidelity[key], n.fidelity([self.ops[key]]), msg = "Wrong fidelity for {}".format(key)) + + def test_numeric_channel_matrix_representation(self): + n = NoiseTransformer() + gamma = 0.5 + E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) + E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) + numeric_matrix = n.numeric_channel_matrix_representation((E0, E1)) + expected_numeric_matrix = numpy.array([[1, 0, 0, 0], [0, 0.707106781186548, 0, 0],[0,0,0.707106781186548,0],[0.500000000000000, 0, 0, 0.500000000000000]]) + self.assertMatricesAlmostEqual(expected_numeric_matrix, numeric_matrix) + + def test_op_name_to_matrix(self): + X = self.ops['X'] + Y = self.ops['Y'] + Z = self.ops['Z'] + H = self.ops['H'] + S = self.ops['S'] + self.assertTrue((X*Y*Z == self.n.op_name_to_matrix('XYZ', self.ops)).all()) + self.assertTrue((S*X*S*Y*H*Z*H*S*X*Z == self.n.op_name_to_matrix('SXSYHZHSXZ', self.ops)).all()) + + def test_qobj_noise_to_kraus_operators(self): + amplitude_damping_kraus_noise = { + "type": "qerror", + "operations": ["h"], + "probabilities": [1.0], + "instructions": [ + [{"name": "kraus", "qubits": [0], "params": [ + [[[1, 0], [0, 0]], [[0, 0], [0.5, 0]]], + [[[0, 0], [0.86602540378, 0]], [[0, 0], [0, 0]]]]}] + ] + } + n = NoiseTransformer() + kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) + matrices = amplitude_damping_kraus_noise['instructions'][0][0]['params'] + expected_kraus_operators = [n.qobj_matrix_to_numpy_matrix(m) for m in matrices] + self.assertListAlmostEqual(expected_kraus_operators, kraus_operators, places=4) + + #now for malformed data + amplitude_damping_kraus_noise['instructions'][0][0] = {"name": "TTG", "qubits": [0]} + with self.assertRaises(RuntimeError): + kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) + + def test_qobj_conversion(self): + import json + with open("../data/qobj_noise_kraus.json") as f: + qobj = json.load(f) + n = NoiseTransformer() + result_qobj = n.transform_qobj('relaxation', qobj) + + gamma = 0.75 # can be seen from the json file + expected_p = (gamma - numpy.sqrt(1 - gamma) + 1) / 2 + expected_q = 0 + + expected_matrices = [ + numpy.sqrt(1-(expected_p + expected_q)) * numpy.array([[1, 0], [0, 1]]), + numpy.sqrt(expected_p) * numpy.array([[1, 0], [0, 0]]), + numpy.sqrt(expected_p) * numpy.array([[0, 1], [0, 0]]), + numpy.sqrt(expected_q) * numpy.array([[0, 0], [0, 1]]), + numpy.sqrt(expected_q) * numpy.array([[0, 0], [1, 0]]) + ] + matrices = result_qobj['config']['noise_model']['errors'][0]['instructions'][0][0]['params'] + matrices = [n.qobj_matrix_to_numpy_matrix(m) for m in matrices] + self.assertListAlmostEqual(expected_matrices, matrices, places = 3) + + #let's also run on something without noise and verify nothing changes + with open("../data/qobj_snapshot_expval_pauli.json") as f: + qobj = json.load(f) + result_qobj = n.transform_qobj('relaxation', qobj) + self.assertEqual(qobj, result_qobj) + + def test_errors(self): + n = NoiseTransformer() + gamma = 0.5 + E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) + E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) + # kraus error is legit, transform_channel_operators are not + with self.assertRaisesRegex(RuntimeError, "7 is not an appropriate input to transform"): + n.transform(7, (E0, E1)) + with self.assertRaisesRegex(RuntimeError, "No information about noise type seven"): + n.transform("seven", (E0, E1)) + + #let's pretend cvxopt does not exist; the script should raise ImportError with proper message + import unittest.mock + import sys + with unittest.mock.patch.dict(sys.modules, {'cvxopt': None}): + with self.assertRaisesRegex(ImportError, "The CVXOPT library is required to use this module"): + n.transform("relaxation", (E0, E1)) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From d78be7d16cedeb84de7ca378531f17850a8420d5 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 12:58:39 +0300 Subject: [PATCH 03/28] Adding noise transformation to the noise package --- qiskit/providers/aer/noise/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/providers/aer/noise/__init__.py b/qiskit/providers/aer/noise/__init__.py index fdad7d93ff..d8911cb9a8 100644 --- a/qiskit/providers/aer/noise/__init__.py +++ b/qiskit/providers/aer/noise/__init__.py @@ -65,5 +65,6 @@ """ from .noise_model import NoiseModel +from .noise_transformation import NoiseTransformer from . import errors -from . import device +from . import device \ No newline at end of file From 1f58bfd6c730f83e2d775fccc930b07373262335 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 16:21:56 +0300 Subject: [PATCH 04/28] Beginning noise transformation overhaul (tests are failing) --- .../aer/noise/noise_transformation.py | 213 ++++++++---------- 1 file changed, 92 insertions(+), 121 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index faec53874f..f2d8e04887 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -3,10 +3,72 @@ import numpy import sympy import itertools -import copy - -from functools import singledispatch +from qiskit.providers.aer.noise.errors import QuantumError +from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary + +#only for 1-qubit errors for now +def quantum_error_to_kraus_operators(error): + qubits_num = 1 + error_ops = [] + for noise_circuit, noise_prob in zip(error._noise_circuits, error._noise_probabilities): + noise_circuit_ops = [numpy.eye(2 ** qubits_num)] + for noise_circuit_element in noise_circuit: + std_op = standard_gate_unitary(noise_circuit_element['name']) + if std_op is not None: + kraus_matrices = std_op + if noise_circuit_element['name'] == 'kraus': + kraus_matrices = noise_circuit_element['params'] + noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] + error_ops += [np.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] + +def approximate_quantum_error(error, operator_dict = None): + """Return an approximate QuantumError. + + Args: + error (QuantumError): the error to be approximated. + + Returns: + QuantumError: the approximate quantum error. + """ + # This would accept as input anything that the QuantumError class can take as input + # Eg: list of circuits, list of kraus matrices, a QuantumChannel sublcass object, or a + # QuantumError object. This could be done as: + if not isinstance(error, QuantumError): + error = QuantumError(error) + error_kraus_operators = quantum_error_to_kraus_operators(error) #TODO: ad-hoc; QuantumError should support general conversion + #error_kraus_operators = error.kraus_operators() + transformer = NoiseTransformer() + if operator_dict is not None: + transformed_error_operators = transformer.transform_by_operator_dictionary(operator_dict, error_kraus_operators) + quantum_error_spec = [] + for (op_name, op_matrices) in operator_dict: + probability = transformed_error_operators[op_name] + quantum_error_spec.append([{'name': 'kraus', + 'qubits': [0], + 'params': op_matrices} + ], probability) + return QuantumError(quantum_error_spec) + return None + +def approximate_noise_model(model, **kwargs): + """Return an approximate noise model. + + Args: + model (NoiseModel): the noise model to be approximated. + + Returns: + NoiseModel: the approximate noise model. + + Raises: + NoiseError: if the QuantumError cannot be approximated. + """ + # This function would iterate over all QuantumErrors in the noise model + # and call the `approximate_quantum_errror` function on them to generate + # an approximate noise model + # TODO: Raise error about 2-qubit errors + approx_noise_model = None + return approx_noise_model class NoiseTransformer: """Transforms one quantum noise channel to another based on a specified criteria. @@ -27,22 +89,19 @@ class NoiseTransformer: def __init__(self): # the premade operators can be accessed by calling transform() with the given name string - self.premade_operators = { - 'pauli': {'X': numpy.array([[0, 1], [1, 0]]), - 'Y': numpy.array([[0, -1j], [1j, 0]]), - 'Z': numpy.array([[1, 0], [0, -1]]) + #TODO: replace with errorutils + self.named_operators = { + 'pauli': {'X': standard_gate_unitary('x'), + 'Y': standard_gate_unitary('y'), + 'Z': standard_gate_unitary('z') }, - 'relaxation': { + 'reset': { 'p': (numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])), 'q': (numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])), }, - 'clifford': self.single_qubit_full_clifford_group() + # 'clifford': self.single_qubit_full_clifford_group() } - self.transform = singledispatch(self.transform) - self.transform.register(list, self.transform_by_operator_list) - self.transform.register(tuple, self.transform_by_operator_list) - self.transform.register(str, self.transform_by_operator_string) - self.transform.register(dict, self.transform_by_operator_dictionary) + self.fidelity_data = None self.use_honesty_constraint = True self.noise_kraus_operators = None @@ -50,14 +109,8 @@ def __init__(self): # transformation interface methods - def transform(self, transform_channel_operators, noise_kraus_operators): - """Transforms the general noise given as a list of noise_kraus_operators using the operators - given via transform_channel_operators which can be list, dict or string""" - raise RuntimeError("{} is not an appropriate input to transform".format(transform_channel_operators)) - def transform_by_operator_list(self, transform_channel_operators, noise_kraus_operators): - """Main transformation function; the other transformation functions use this one - + """ Args: noise_kraus_operators: a list of matrices (Kraus operators) for the input channel transform_channel_operators: a list of matrices or tuples of matrices @@ -93,6 +146,19 @@ def transform_by_operator_list(self, transform_channel_operators, noise_kraus_op probabilities = self.transform_by_given_channel(channel_matrices, const_channel_matrix) return probabilities + # convenience wrapper method + def transform_by_operator_dictionary(self, transform_channel_operators_dictionary, noise_kraus_operators): + names, operators = zip(*transform_channel_operators_dictionary.items()) + probabilities = self.transform_by_operator_list(operators, noise_kraus_operators) + return dict(zip(names, probabilities)) + + # convenience wrapper method + def transform_by_operator_string(self, transform_channel_operators_string, noise_kraus_operators): + if transform_channel_operators_string in self.named_operators: + return self.transform_by_operator_dictionary( + self.named_operators[transform_channel_operators_string], noise_kraus_operators) + raise RuntimeError("No information about noise type {}".format(transform_channel_operators_string)) + # convert to sympy matrices and verify that each singleton is in a tuple; also add identity matrix @staticmethod def prepare_channel_operator_list(ops_list): @@ -113,83 +179,10 @@ def prepare_honesty_constraint(self, transform_channel_operators_list): 'coefficients': coefficients[1:] # coefficients[0] corresponds to I } - def transform_by_operator_string(self, transform_channel_operators_string, noise_kraus_operators): - if transform_channel_operators_string in self.premade_operators: - return self.transform_by_operator_dictionary( - self.premade_operators[transform_channel_operators_string], noise_kraus_operators) - raise RuntimeError("No information about noise type {}".format(transform_channel_operators_string)) - - def transform_by_operator_dictionary(self, transform_channel_operators_dictionary, noise_kraus_operators): - names, operators = zip(*transform_channel_operators_dictionary.items()) - probabilities = self.transform_by_operator_list(operators, noise_kraus_operators) - return dict(zip(names, probabilities)) - - def qobj_noise_to_kraus_operators(self, noise): - # Noises are given as a list of probabilities [p1, p2, ..., pn] and circuits [C1, C2, ..., Cn] - # Each circuit consists of a list of noise operators that are applied sqeuentially - # If (E_0, E_1, ..., E_n) and (F_0, F_1, ...., F_m) are applied sequentially, we can "merge" - # them into one noise with operators (E_iF_j) for 0 <= i <= n and 0 <= j <= m - # This is then multiplied by sqrt(pk) for the circuit k - probabilities = noise['probabilities'] - circuits = noise['instructions'] - operators_for_circuits = [self.noise_circuit_to_kraus_operators(c) for c in circuits] - final_operators_list = [] - for k, ops in enumerate(operators_for_circuits): - p = numpy.sqrt(probabilities[k]) - final_operators_list += [p * op for op in ops] - return final_operators_list - - def noise_circuit_to_kraus_operators(self, circuit): - # first, convert the list of dict-based noises to actual lists of matrices - operators_list = [self.qobj_instruction_to_matrices(instruction) for instruction in circuit] - Id = numpy.array([[1,0], [0,1]]) - final_operators = [Id] - for operators in operators_list: - final_operators = [op[0].dot(op[1]) for op in itertools.product(final_operators, operators)] - return final_operators - - def qobj_instruction_to_matrices(self, instruction): - if instruction['name'] == 'kraus': - kraus_operators = [self.qobj_matrix_to_numpy_matrix(m) for m in instruction['params']] - return kraus_operators - raise RuntimeError("Does not know how to handle noises of type {}".format(instruction['name'])) - - def transform_qobj(self, transform_channel, qobj): - result_qobj = copy.deepcopy(qobj) - try: - noise_list = result_qobj["config"]["noise_model"]["errors"] - except KeyError: - return result_qobj # if we can't find noises, we don't change anything - for noise in noise_list: - noise_in_kraus_form = self.qobj_noise_to_kraus_operators(noise) - probabilities = self.transform(transform_channel, noise_in_kraus_form) - transformed_channel_kraus_operators = self.probability_list_to_matrices(probabilities, transform_channel) - qobj_kraus_operators = [self.numpy_matrix_to_qobj_matrix(m) for m in transformed_channel_kraus_operators] - noise['probabilities'] = [1.0] - noise['instructions'] = [[{"name": "kraus", "qubits": [0], "params": qobj_kraus_operators}]] - return result_qobj - - @staticmethod - def qobj_matrix_to_numpy_matrix(m): - return numpy.array([[entry[0] + entry[1] * 1j for entry in row] for row in m]) - - @staticmethod - def numpy_matrix_to_qobj_matrix(m): - return [[[numpy.real(a), numpy.imag(a)] for a in row] for row in (numpy.array(m))] - - def probability_list_to_matrices(self, probs, transform_channel): - # probs are either a list or a dict of pairs of the form (probability, matrix_list) - if isinstance(probs, dict): - probs = probs.values() - transformation_data = zip(probs, self.transform_channel_operators) - # now assume probs is a list of pairs of the form (probability, matrix_list) - id_matrix = numpy.eye(2) * numpy.sqrt(1 - sum(probs)) - return [id_matrix] + [numpy.sqrt(prob) * matrix for (prob, matrix_list) in transformation_data for matrix in matrix_list] - # methods relevant to the transformation to quadratic programming instance @staticmethod - def fidelity(channel): + def fidelity(channel): #TODO replace with qiskit fidelity return sum([numpy.abs(numpy.trace(E)) ** 2 for E in channel]) def generate_channel_matrices(self, transform_channel_operators_list): @@ -324,6 +317,8 @@ def transform_by_given_channel(self, channel_matrices, const_channel_matrix): q = self.compute_q(channel_matrices, const_matrix) return self.solve_quadratic_program(P, q) + #TODO: replace with Kraus object from Terra ("evolve" method) + # qiskit-terra\qiskit\quantum_info\operators\channel @staticmethod def numeric_compute_channel_operation(rho, operators): return numpy.sum([E.dot(rho).dot(E.conj().T) for E in operators], 0) @@ -359,6 +354,7 @@ def compute_q(self, As, C): # should we consider another library, only this method needs to change def solve_quadratic_program(self, P, q): try: + #TODO: consider using QP with CVXPY import cvxopt except ImportError: raise ImportError("The CVXOPT library is required to use this module") @@ -377,29 +373,4 @@ def solve_quadratic_program(self, P, q): G = cvxopt.matrix(numpy.array(G_data).astype(float)) h = cvxopt.matrix(numpy.array(h_data).astype(float)) cvxopt.solvers.options['show_progress'] = False - return cvxopt.solvers.qp(P, q, G, h)['x'] - - def single_qubit_full_clifford_group(self): - # easy representation of all 23 non-identity 1-qubit Clifford group representation by using only S,H,X,Y,Z - clifford_ops_names = [ - "X", "Y", "Z", - "S", "SX", "SY", "SZ", - "SH", "SHX", "SHY", "SHZ", - "SHS", "SHSX", "SHSY", "SHSZ", - "SHSH", "SHSHX", "SHSHY", "SHSHZ", - "SHSHS", "SHSHSX", "SHSHSY", "SHSHSZ", - ] - base_ops = { - 'X': numpy.array([[0, 1], [1, 0]]), - 'Y': numpy.array([[0, -1j], [1j, 0]]), - 'Z': numpy.array([[1, 0], [0, -1]]), - 'S': numpy.array([[1, 0], [0, 1j]]), - 'H': numpy.sqrt(2) * numpy.array([[1, 1], [1, -1]]) - } - return dict([(name, self.op_name_to_matrix(name, base_ops)) for name in clifford_ops_names]) - - def op_name_to_matrix(self, name, base_ops): - m = numpy.array([[1, 0], [0, 1]]) - for op_name in name: - m = m * base_ops[op_name] - return m \ No newline at end of file + return cvxopt.solvers.qp(P, q, G, h)['x'] \ No newline at end of file From b83b328a576657f6f5892a842eda840bc3ea1f4c Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 16:50:37 +0300 Subject: [PATCH 05/28] Unified interface for dict/list/string approximating operator input --- .../aer/noise/noise_transformation.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index f2d8e04887..9d152b5db1 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -22,7 +22,10 @@ def quantum_error_to_kraus_operators(error): noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] error_ops += [np.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] -def approximate_quantum_error(error, operator_dict = None): +def approximate_quantum_error(error, + operator_string = None, + operator_dict = None, + operator_list = None): """Return an approximate QuantumError. Args: @@ -39,18 +42,23 @@ def approximate_quantum_error(error, operator_dict = None): error_kraus_operators = quantum_error_to_kraus_operators(error) #TODO: ad-hoc; QuantumError should support general conversion #error_kraus_operators = error.kraus_operators() transformer = NoiseTransformer() + if operator_string is not None: + operator_dict = transformer.named_operators[operator_string] if operator_dict is not None: - transformed_error_operators = transformer.transform_by_operator_dictionary(operator_dict, error_kraus_operators) + names, operators_list = zip(*operator_dict.items()) + if operator_list is not None: + probabilities = transformer.transform_by_operator_list(operator_list, error_kraus_operators) quantum_error_spec = [] - for (op_name, op_matrices) in operator_dict: - probability = transformed_error_operators[op_name] + for (op_matrices, probability) in zip(operator_list, probabilities): quantum_error_spec.append([{'name': 'kraus', 'qubits': [0], 'params': op_matrices} ], probability) return QuantumError(quantum_error_spec) + return None + def approximate_noise_model(model, **kwargs): """Return an approximate noise model. @@ -88,9 +96,7 @@ class NoiseTransformer: """ def __init__(self): - # the premade operators can be accessed by calling transform() with the given name string - #TODO: replace with errorutils - self.named_operators = { + named_operators = { 'pauli': {'X': standard_gate_unitary('x'), 'Y': standard_gate_unitary('y'), 'Z': standard_gate_unitary('z') @@ -152,13 +158,6 @@ def transform_by_operator_dictionary(self, transform_channel_operators_dictionar probabilities = self.transform_by_operator_list(operators, noise_kraus_operators) return dict(zip(names, probabilities)) - # convenience wrapper method - def transform_by_operator_string(self, transform_channel_operators_string, noise_kraus_operators): - if transform_channel_operators_string in self.named_operators: - return self.transform_by_operator_dictionary( - self.named_operators[transform_channel_operators_string], noise_kraus_operators) - raise RuntimeError("No information about noise type {}".format(transform_channel_operators_string)) - # convert to sympy matrices and verify that each singleton is in a tuple; also add identity matrix @staticmethod def prepare_channel_operator_list(ops_list): From b4e46422bf188d57d93a83b948e472a593cd56b0 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 16:59:30 +0300 Subject: [PATCH 06/28] Bugfixes in quantum_error_to_kraus_operators --- qiskit/providers/aer/noise/noise_transformation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 9d152b5db1..b20101720f 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -19,8 +19,13 @@ def quantum_error_to_kraus_operators(error): kraus_matrices = std_op if noise_circuit_element['name'] == 'kraus': kraus_matrices = noise_circuit_element['params'] + if noise_circuit_element['name'] == 'unitary': + #TODO: I expect only one matrix here. Is this assumption correct? + kraus_matrices = noise_circuit_element['params'] + if kraus_matrices is None: + raise "Could not understand the error {}".format(error) noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] - error_ops += [np.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] + error_ops += [numpy.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] def approximate_quantum_error(error, operator_string = None, From e35a958a1acb88627df4cfc16132398894354238 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 15 Apr 2019 17:22:58 +0300 Subject: [PATCH 07/28] More bugfixes --- qiskit/providers/aer/noise/noise_transformation.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index b20101720f..699293742e 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -16,16 +16,18 @@ def quantum_error_to_kraus_operators(error): for noise_circuit_element in noise_circuit: std_op = standard_gate_unitary(noise_circuit_element['name']) if std_op is not None: - kraus_matrices = std_op + kraus_matrices = [std_op] if noise_circuit_element['name'] == 'kraus': kraus_matrices = noise_circuit_element['params'] if noise_circuit_element['name'] == 'unitary': #TODO: I expect only one matrix here. Is this assumption correct? - kraus_matrices = noise_circuit_element['params'] + kraus_matrices = [noise_circuit_element['params']] if kraus_matrices is None: raise "Could not understand the error {}".format(error) + kraus_matrices = [numpy.array(matrix) for matrix in kraus_matrices] noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] error_ops += [numpy.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] + return error_ops def approximate_quantum_error(error, operator_string = None, @@ -50,7 +52,7 @@ def approximate_quantum_error(error, if operator_string is not None: operator_dict = transformer.named_operators[operator_string] if operator_dict is not None: - names, operators_list = zip(*operator_dict.items()) + names, operator_list = zip(*operator_dict.items()) if operator_list is not None: probabilities = transformer.transform_by_operator_list(operator_list, error_kraus_operators) quantum_error_spec = [] @@ -61,7 +63,7 @@ def approximate_quantum_error(error, ], probability) return QuantumError(quantum_error_spec) - return None + raise Exception("Quantum error approximation failed - no approximating operators detected") def approximate_noise_model(model, **kwargs): From f39350fd2453b9604ac61d02bfd957c1e8771f82 Mon Sep 17 00:00:00 2001 From: cjwood Date: Mon, 15 Apr 2019 14:14:46 -0400 Subject: [PATCH 08/28] add to_channel method to QuantumError * fix bug in QuantumError tensor --- .../providers/aer/noise/errors/errorutils.py | 105 +++++++----------- .../aer/noise/errors/quantum_error.py | 10 +- test/terra/noise/test_quantum_error.py | 55 ++++++++- 3 files changed, 98 insertions(+), 72 deletions(-) diff --git a/qiskit/providers/aer/noise/errors/errorutils.py b/qiskit/providers/aer/noise/errors/errorutils.py index a344c7b71f..abb12215dc 100644 --- a/qiskit/providers/aer/noise/errors/errorutils.py +++ b/qiskit/providers/aer/noise/errors/errorutils.py @@ -324,83 +324,49 @@ def standard_gate_unitary(name): return None -def reset_superop(num_qubits): - """Return a N-qubit reset SuperOp.""" - reset = SuperOp( - np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])) - if num_qubits == 1: - return reset - reset_n = reset - for _ in range(num_qubits - 1): - reset_n.tensor(reset) - return reset_n - - -def standard_instruction_operator(instr): - """Return the Operator for a standard gate instruction.""" - # Convert to dict (for QobjInstruction types) - if hasattr(instr, 'as_dict'): - instr = instr.as_dict() - # Get name and parameters - name = instr.get('name', "") - params = instr.get('params', []) - # Check if standard unitary gate name +def standard_instruction_operator(name, params=None): + """Return the Operator for a standard gate.""" mat = standard_gate_unitary(name) if isinstance(mat, np.ndarray): return Operator(mat) - # Check if standard parameterized waltz gates - if name == 'u1': + if name is 'u1': lam = params[0] mat = np.diag([1, np.exp(1j * lam)]) return Operator(mat) - if name == 'u2': + if name is 'u2': phi = params[0] lam = params[1] mat = np.array([[1, -np.exp(1j * lam)], [np.exp(1j * phi), np.exp(1j * (phi + lam))]]) / np.sqrt(2) return Operator(mat) - if name == 'u3': + if name is 'u3': theta = params[0] phi = params[1] lam = params[2] mat = np.array( [[np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)], - [np.exp(1j * phi) * np.sin(theta / 2), np.exp(1j * (phi + lam)) * np.cos(theta / 2)]]) + [ + np.exp(1j * phi) * np.sin(theta / 2), + np.exp(1j * (phi + lam)) * np.cos(theta / 2) + ]]) return Operator(mat) - - # Check if unitary instruction - if name == 'unitary': - return Operator(params[0]) - - # Otherwise return None if we cannot convert instruction - return None + if name is 'unitary': + return Operator(params) + raise NoiseError('Cannot convert instruction to Operator') -def standard_instruction_channel(instr): - """Return the SuperOp channel for a standard instruction.""" - # Check if standard operator - oper = standard_instruction_operator(instr) - if oper is not None: - return SuperOp(oper) - - # Convert to dict (for QobjInstruction types) - if hasattr(instr, 'as_dict'): - instr = instr.as_dict() - # Get name and parameters - name = instr.get('name', "") - - # Check if reset instruction - if name == 'reset': - # params should be the number of qubits being reset - num_qubits = len(instr['qubits']) - return reset_superop(num_qubits) - # Check if Kraus instruction - if name == 'kraus': - params = instr['params'] - return SuperOp(Kraus(params)) - return None +def reset_superop(num_qubits): + """Return a N-qubit reset SuperOp.""" + reset = SuperOp( + np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])) + if num_qubits == 1: + return reset + reset_n = reset + for _ in range(num_qubits - 1): + reset_n.tensor(reset) + return reset_n def circuit2superop(circuit, min_qubits=1): @@ -421,13 +387,28 @@ def circuit2superop(circuit, min_qubits=1): superop = SuperOp(np.eye(4**num_qubits)) # compose each circuit element with the superoperator for instr in circuit: - instr_op = standard_instruction_channel(instr) - if instr_op is None: - raise NoiseError('Cannot convert instruction {} to SuperOp'.format(instr)) - if hasattr(instr, 'qubits'): - qubits = instr.qubits + name = None + qubits = None + params = None + if isinstance(instr, dict): + # Parse from plain dictionary qobj instruction + name = instr['name'] + qubits = instr.get('qubits') + params = instr.get('params') + else: + # Parse from QasmQobjInstruction + if hasattr(instr, 'name'): + name = instr.name + if hasattr(instr, 'qubits'): + qubits = instr.qubits + if hasattr(instr, 'params'): + params = instr.params + if name is 'reset': + instr_op = reset_superop(len(qubits)) + elif name is 'kraus': + instr_op = Kraus(params) else: - qubits = instr['qubits'] + instr_op = SuperOp(standard_instruction_operator(name, params)) superop = superop.compose(instr_op, qubits=qubits) return superop diff --git a/qiskit/providers/aer/noise/errors/quantum_error.py b/qiskit/providers/aer/noise/errors/quantum_error.py index 0c655f6cfd..19c2831278 100644 --- a/qiskit/providers/aer/noise/errors/quantum_error.py +++ b/qiskit/providers/aer/noise/errors/quantum_error.py @@ -13,15 +13,13 @@ import numpy as np -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.quantum_info.operators import Kraus, SuperOp +from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel +from qiskit.quantum_info.operators.channel.kraus import Kraus +from qiskit.quantum_info.operators.channel.superop import SuperOp from qiskit.quantum_info.operators.predicates import ATOL_DEFAULT, RTOL_DEFAULT from ..noiseerror import NoiseError -from .errorutils import kraus2instructions -from .errorutils import circuit2superop -from .errorutils import standard_instruction_channel -from .errorutils import standard_instruction_operator +from .errorutils import kraus2instructions, circuit2superop logger = logging.getLogger(__name__) diff --git a/test/terra/noise/test_quantum_error.py b/test/terra/noise/test_quantum_error.py index 6e17a7a5ad..6019ea1aa4 100644 --- a/test/terra/noise/test_quantum_error.py +++ b/test/terra/noise/test_quantum_error.py @@ -194,6 +194,12 @@ def test_tensor_both_kraus(self): target = SuperOp(Kraus([A0, A1]).tensor(Kraus([B0, B1]))) error = QuantumError([A0, A1]).tensor(QuantumError([B0, B1])) kraus, p = error.error_term(0) + targets = [ + np.kron(B0, A0), + np.kron(B0, A1), + np.kron(B1, A0), + np.kron(B1, A1) + ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0, 1]) @@ -273,9 +279,15 @@ def test_tensor_both_unitary_standard_gates(self): ] # Target circuits target_circs = [[{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'x', 'qubits': [1] }], [{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'y', 'qubits': [1] }], [{ @@ -316,6 +328,12 @@ def test_expand_both_kraus(self): target = SuperOp(Kraus([A0, A1]).expand(Kraus([B0, B1]))) error = QuantumError([A0, A1]).expand(QuantumError([B0, B1])) kraus, p = error.error_term(0) + targets = [ + np.kron(B0, A0), + np.kron(B1, A0), + np.kron(B0, A1), + np.kron(B1, A1) + ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0, 1]) @@ -395,9 +413,15 @@ def test_expand_both_unitary_standard_gates(self): ] # Target circuits target_circs = [[{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'x', 'qubits': [1] }], [{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'y', 'qubits': [1] }], [{ @@ -446,6 +470,12 @@ def test_compose_both_kraus(self): target = SuperOp(Kraus([A0, A1]).compose(Kraus([B0, B1]))) error = QuantumError([A0, A1]).compose(QuantumError([B0, B1])) kraus, p = error.error_term(0) + targets = [ + np.dot(B0, A0), + np.dot(B0, A1), + np.dot(B1, A0), + np.dot(B1, A1) + ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0]) @@ -525,9 +555,15 @@ def test_compose_both_qobj(self): ] # Target circuits target_circs = [[{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'x', 'qubits': [0] }], [{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'y', 'qubits': [0] }], [{ @@ -564,10 +600,15 @@ def test_compose_front_both_kraus(self): A1 = np.array([[0, 0], [0, np.sqrt(0.3)]], dtype=complex) B0 = np.array([[1, 0], [0, np.sqrt(1 - 0.5)]], dtype=complex) B1 = np.array([[0, 0], [0, np.sqrt(0.5)]], dtype=complex) - # Use quantum channels for reference - target = SuperOp(Kraus([A0, A1]).compose(Kraus([B0, B1]), front=True)) - error = QuantumError([A0, A1]).compose(QuantumError([B0, B1]), front=True) + error = QuantumError([B0, B1]).compose( + QuantumError([A0, A1]), front=True) kraus, p = error.error_term(0) + targets = [ + np.dot(B0, A0), + np.dot(B0, A1), + np.dot(B1, A0), + np.dot(B1, A1) + ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0]) @@ -647,9 +688,15 @@ def test_compose_front_both_qobj(self): ] # Target circuits target_circs = [[{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'x', 'qubits': [0] - }], [ { + }], [{ + 'name': 'id', + 'qubits': [0] + }, { 'name': 'y', 'qubits': [0] }], [{ From 97aa8837483406376eb0ca04f9e26cf6f335f791 Mon Sep 17 00:00:00 2001 From: cjwood Date: Mon, 15 Apr 2019 17:55:52 -0400 Subject: [PATCH 09/28] remove inplace from QuantumError, ReadoutError --- qiskit/providers/aer/noise/errors/quantum_error.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit/providers/aer/noise/errors/quantum_error.py b/qiskit/providers/aer/noise/errors/quantum_error.py index 19c2831278..f848f8441c 100644 --- a/qiskit/providers/aer/noise/errors/quantum_error.py +++ b/qiskit/providers/aer/noise/errors/quantum_error.py @@ -336,7 +336,7 @@ def compose(self, other, front=False): combined_circuit.append({'name': 'id', 'qubits': [0]}) # Add circuit combined_noise_circuits.append(combined_circuit) - noise_ops = self._combine_kraus(zip(combined_noise_circuits, combined_noise_probabilities)) + noise_ops = zip(combined_noise_circuits, combined_noise_probabilities) return QuantumError(noise_ops) def power(self, n): @@ -407,7 +407,7 @@ def _tensor_product(self, other, reverse=False): """Return the tensor product error channel. Args: - other (QuantumError): a quantum channel subclass + other (QuantumChannel): a quantum channel subclass reverse (bool): If False return self ⊗ other, if True return if True return (other ⊗ self) [Default: False Returns: @@ -466,8 +466,7 @@ def _tensor_product(self, other, reverse=False): combined_circuit.append({'name': 'id', 'qubits': [0]}) # Add circuit combined_noise_circuits.append(combined_circuit) - # Now we combine any error circuits containing only Kraus operations - noise_ops = self._combine_kraus(zip(combined_noise_circuits, combined_noise_probabilities)) + noise_ops = zip(combined_noise_circuits, combined_noise_probabilities) return QuantumError(noise_ops) @staticmethod From 00f73aeb6a9a4311e3a30a85ba637f3037cc33f2 Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 10:52:32 +0300 Subject: [PATCH 10/28] Using the new to_channel method for errors --- .../aer/noise/noise_transformation.py | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 699293742e..610e6d6551 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -6,28 +6,29 @@ from qiskit.providers.aer.noise.errors import QuantumError from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary - -#only for 1-qubit errors for now -def quantum_error_to_kraus_operators(error): - qubits_num = 1 - error_ops = [] - for noise_circuit, noise_prob in zip(error._noise_circuits, error._noise_probabilities): - noise_circuit_ops = [numpy.eye(2 ** qubits_num)] - for noise_circuit_element in noise_circuit: - std_op = standard_gate_unitary(noise_circuit_element['name']) - if std_op is not None: - kraus_matrices = [std_op] - if noise_circuit_element['name'] == 'kraus': - kraus_matrices = noise_circuit_element['params'] - if noise_circuit_element['name'] == 'unitary': - #TODO: I expect only one matrix here. Is this assumption correct? - kraus_matrices = [noise_circuit_element['params']] - if kraus_matrices is None: - raise "Could not understand the error {}".format(error) - kraus_matrices = [numpy.array(matrix) for matrix in kraus_matrices] - noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] - error_ops += [numpy.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] - return error_ops +from qiskit.quantum_info.operators.channel import Kraus + +# #only for 1-qubit errors for now +# def quantum_error_to_kraus_operators(error): +# qubits_num = 1 +# error_ops = [] +# for noise_circuit, noise_prob in zip(error._noise_circuits, error._noise_probabilities): +# noise_circuit_ops = [numpy.eye(2 ** qubits_num)] +# for noise_circuit_element in noise_circuit: +# std_op = standard_gate_unitary(noise_circuit_element['name']) +# if std_op is not None: +# kraus_matrices = [std_op] +# if noise_circuit_element['name'] == 'kraus': +# kraus_matrices = noise_circuit_element['params'] +# if noise_circuit_element['name'] == 'unitary': +# #TODO: I expect only one matrix here. Is this assumption correct? +# kraus_matrices = [noise_circuit_element['params']] +# if kraus_matrices is None: +# raise "Could not understand the error {}".format(error) +# kraus_matrices = [numpy.array(matrix) for matrix in kraus_matrices] +# noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] +# error_ops += [numpy.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] +# return error_ops def approximate_quantum_error(error, operator_string = None, @@ -46,8 +47,7 @@ def approximate_quantum_error(error, # QuantumError object. This could be done as: if not isinstance(error, QuantumError): error = QuantumError(error) - error_kraus_operators = quantum_error_to_kraus_operators(error) #TODO: ad-hoc; QuantumError should support general conversion - #error_kraus_operators = error.kraus_operators() + error_kraus_operators = Kraus(error.to_channel()).data transformer = NoiseTransformer() if operator_string is not None: operator_dict = transformer.named_operators[operator_string] @@ -55,12 +55,15 @@ def approximate_quantum_error(error, names, operator_list = zip(*operator_dict.items()) if operator_list is not None: probabilities = transformer.transform_by_operator_list(operator_list, error_kraus_operators) - quantum_error_spec = [] + identity_prob = 1 - sum(probabilities) + if identity_prob < 0 or identity_prob > 1: + raise RuntimeError("Approximated channel operators probabilities sum to {}".format(1-identity_prob)) + quantum_error_spec = [([{'name': 'id', 'qubits':[0]}],identity_prob)] for (op_matrices, probability) in zip(operator_list, probabilities): - quantum_error_spec.append([{'name': 'kraus', + quantum_error_spec.append(([{'name': 'kraus', 'qubits': [0], 'params': op_matrices} - ], probability) + ], probability)) return QuantumError(quantum_error_spec) raise Exception("Quantum error approximation failed - no approximating operators detected") From 90b7872379501c853c9401d19809b88aaccf6543 Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 11:30:11 +0300 Subject: [PATCH 11/28] small adjustments --- qiskit/providers/aer/noise/noise_transformation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 610e6d6551..a8adf51dbb 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -60,9 +60,10 @@ def approximate_quantum_error(error, raise RuntimeError("Approximated channel operators probabilities sum to {}".format(1-identity_prob)) quantum_error_spec = [([{'name': 'id', 'qubits':[0]}],identity_prob)] for (op_matrices, probability) in zip(operator_list, probabilities): + # convert op_matrices to list since tuples of Kraus matrices are treated as generalized Kraus representation quantum_error_spec.append(([{'name': 'kraus', 'qubits': [0], - 'params': op_matrices} + 'params': list(op_matrices)} ], probability)) return QuantumError(quantum_error_spec) @@ -106,7 +107,7 @@ class NoiseTransformer: """ def __init__(self): - named_operators = { + self.named_operators = { 'pauli': {'X': standard_gate_unitary('x'), 'Y': standard_gate_unitary('y'), 'Z': standard_gate_unitary('z') From e3d10736c193f832dd86d427ec847e3a6b890fe0 Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 11:34:14 +0300 Subject: [PATCH 12/28] Beginning testing overhaul; 3 working tests with the new interface --- test/terra/test_noise_transformation.py | 231 +++++++++++------------- 1 file changed, 106 insertions(+), 125 deletions(-) diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index ced59af26b..247dbf4e8e 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -1,20 +1,28 @@ import unittest -from aer.noise import NoiseTransformer import numpy +from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary +from qiskit.providers.aer.noise import NoiseTransformer +from qiskit.providers.aer.noise import approximate_quantum_error +from qiskit.providers.aer.noise.errors.standard_errors import amplitude_damping_error +from qiskit.providers.aer.noise.errors.standard_errors import reset_error +from qiskit.providers.aer.noise.errors.standard_errors import pauli_error #TODO: skip tests if CVXOPT is not present class TestNoiseTransformer(unittest.TestCase): def setUp(self): - #TODO: replace with Qiskit based defs - X = numpy.array([[0, 1], [1, 0]]) - Y = numpy.array([[0, -1j], [1j, 0]]) - Z = numpy.array([[1, 0], [0, -1]]) - S = numpy.array([[1, 0], [0, 1j]]) - H = numpy.sqrt(2) * numpy.array([[1, 1], [1, -1]]) - self.ops = {'X': X, 'Y': Y, 'Z': Z, 'H': H, 'S': S} + self.ops = {'X': standard_gate_unitary('x'), + 'Y': standard_gate_unitary('y'), + 'Z': standard_gate_unitary('z'), + 'H': standard_gate_unitary('h'), + 'S': standard_gate_unitary('s') + } self.n = NoiseTransformer() + def assertErrorsAlmostEqual(self, lhs, rhs, places = 3): + self.assertMatricesAlmostEqual(lhs.to_channel()._data, rhs.to_channel()._data, places) + + def assertDictAlmostEqual(self, lhs, rhs, places = None): keys = set(lhs.keys()).union(set(rhs.keys())) for key in keys: @@ -38,7 +46,7 @@ def assertMatricesAlmostEqual(self, lhs, rhs, places = None): def test_transformation_by_pauli(self): n = NoiseTransformer() - #polarization + #polarization in the XY plane; we represent via Kraus operators X = self.ops['X'] Y = self.ops['Y'] Z = self.ops['Z'] @@ -46,26 +54,28 @@ def test_transformation_by_pauli(self): theta = numpy.pi / 5 E0 = numpy.sqrt(1 - p) * numpy.array(numpy.eye(2)) E1 = numpy.sqrt(p) * (numpy.cos(theta) * X + numpy.sin(theta) * Y) - results = n.transform({"X": X, "Y": Y, "Z": Z}, (E0, E1)) - expected_results = {'X': p*numpy.cos(theta)*numpy.cos(theta), 'Y': p*numpy.sin(theta)*numpy.sin(theta), 'Z': 0} - for op in results.keys(): - self.assertAlmostEqual(expected_results[op], results[op], 3) + results = approximate_quantum_error((E0, E1), operator_dict={"X": X, "Y": Y, "Z": Z}) + expected_results = pauli_error([('X', p*numpy.cos(theta)*numpy.cos(theta)), + ('Y', p*numpy.sin(theta)*numpy.sin(theta)), + ('Z', 0), + ('I', 1-p)]) + self.assertErrorsAlmostEqual(expected_results, results) + #now try again without fidelity; should be the same n.use_honesty_constraint = False - results = n.transform({"X": X, "Y": Y, "Z": Z}, (E0, E1)) - for op in results.keys(): - self.assertAlmostEqual(expected_results[op], results[op], 3) + results = approximate_quantum_error((E0, E1), operator_dict={"X": X, "Y": Y, "Z": Z}) + self.assertErrorsAlmostEqual(expected_results, results) - def test_relaxation(self): - # amplitude damping + def test_reset(self): + # approximating amplitude damping using relaxation operators gamma = 0.5 - E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) - E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) - results = self.n.transform("relaxation", (E0, E1)) - expected_results = {'p': (gamma - numpy.sqrt(1 - gamma) + 1) / 2, 'q': 0} - for op in results.keys(): - self.assertAlmostEqual(expected_results[op], results[op], 3) + error = amplitude_damping_error(gamma) + p = (gamma - numpy.sqrt(1 - gamma) + 1) / 2 + q = 0 + expected_results = reset_error(p,q) + results = approximate_quantum_error(error, "reset") + self.assertErrorsAlmostEqual(results, expected_results) def test_transform(self): X = self.ops['X'] @@ -76,107 +86,78 @@ def test_transform(self): E0 = numpy.sqrt(1 - p) * numpy.array(numpy.eye(2)) E1 = numpy.sqrt(p) * (numpy.cos(theta) * X + numpy.sin(theta) * Y) - results_dict = self.n.transform({"X": X, "Y": Y, "Z": Z}, (E0, E1)) - results_string = self.n.transform('pauli', (E0, E1)) - results_list = list(self.n.transform([X, Y, Z], (E0, E1))) - results_tuple = list(self.n.transform((X, Y, Z), (E0, E1))) - results_list_as_dict = dict(zip(['X', 'Y', 'Z'],results_list)) - - self.assertDictAlmostEqual(results_dict, results_string) - self.assertListAlmostEqual(results_list, results_tuple) - self.assertDictAlmostEqual(results_list_as_dict, results_dict) - - def test_fidelity(self): - n = NoiseTransformer() - expected_fidelity = {'X': 0, 'Y': 0, 'Z': 0, 'H': 0, 'S': 2} - for key in expected_fidelity: - self.assertAlmostEqual(expected_fidelity[key], n.fidelity([self.ops[key]]), msg = "Wrong fidelity for {}".format(key)) - - def test_numeric_channel_matrix_representation(self): - n = NoiseTransformer() - gamma = 0.5 - E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) - E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) - numeric_matrix = n.numeric_channel_matrix_representation((E0, E1)) - expected_numeric_matrix = numpy.array([[1, 0, 0, 0], [0, 0.707106781186548, 0, 0],[0,0,0.707106781186548,0],[0.500000000000000, 0, 0, 0.500000000000000]]) - self.assertMatricesAlmostEqual(expected_numeric_matrix, numeric_matrix) - - def test_op_name_to_matrix(self): - X = self.ops['X'] - Y = self.ops['Y'] - Z = self.ops['Z'] - H = self.ops['H'] - S = self.ops['S'] - self.assertTrue((X*Y*Z == self.n.op_name_to_matrix('XYZ', self.ops)).all()) - self.assertTrue((S*X*S*Y*H*Z*H*S*X*Z == self.n.op_name_to_matrix('SXSYHZHSXZ', self.ops)).all()) - - def test_qobj_noise_to_kraus_operators(self): - amplitude_damping_kraus_noise = { - "type": "qerror", - "operations": ["h"], - "probabilities": [1.0], - "instructions": [ - [{"name": "kraus", "qubits": [0], "params": [ - [[[1, 0], [0, 0]], [[0, 0], [0.5, 0]]], - [[[0, 0], [0.86602540378, 0]], [[0, 0], [0, 0]]]]}] - ] - } - n = NoiseTransformer() - kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) - matrices = amplitude_damping_kraus_noise['instructions'][0][0]['params'] - expected_kraus_operators = [n.qobj_matrix_to_numpy_matrix(m) for m in matrices] - self.assertListAlmostEqual(expected_kraus_operators, kraus_operators, places=4) - - #now for malformed data - amplitude_damping_kraus_noise['instructions'][0][0] = {"name": "TTG", "qubits": [0]} - with self.assertRaises(RuntimeError): - kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) - - def test_qobj_conversion(self): - import json - with open("../data/qobj_noise_kraus.json") as f: - qobj = json.load(f) - n = NoiseTransformer() - result_qobj = n.transform_qobj('relaxation', qobj) - - gamma = 0.75 # can be seen from the json file - expected_p = (gamma - numpy.sqrt(1 - gamma) + 1) / 2 - expected_q = 0 - - expected_matrices = [ - numpy.sqrt(1-(expected_p + expected_q)) * numpy.array([[1, 0], [0, 1]]), - numpy.sqrt(expected_p) * numpy.array([[1, 0], [0, 0]]), - numpy.sqrt(expected_p) * numpy.array([[0, 1], [0, 0]]), - numpy.sqrt(expected_q) * numpy.array([[0, 0], [0, 1]]), - numpy.sqrt(expected_q) * numpy.array([[0, 0], [1, 0]]) - ] - matrices = result_qobj['config']['noise_model']['errors'][0]['instructions'][0][0]['params'] - matrices = [n.qobj_matrix_to_numpy_matrix(m) for m in matrices] - self.assertListAlmostEqual(expected_matrices, matrices, places = 3) - - #let's also run on something without noise and verify nothing changes - with open("../data/qobj_snapshot_expval_pauli.json") as f: - qobj = json.load(f) - result_qobj = n.transform_qobj('relaxation', qobj) - self.assertEqual(qobj, result_qobj) - - def test_errors(self): - n = NoiseTransformer() - gamma = 0.5 - E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) - E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) - # kraus error is legit, transform_channel_operators are not - with self.assertRaisesRegex(RuntimeError, "7 is not an appropriate input to transform"): - n.transform(7, (E0, E1)) - with self.assertRaisesRegex(RuntimeError, "No information about noise type seven"): - n.transform("seven", (E0, E1)) - - #let's pretend cvxopt does not exist; the script should raise ImportError with proper message - import unittest.mock - import sys - with unittest.mock.patch.dict(sys.modules, {'cvxopt': None}): - with self.assertRaisesRegex(ImportError, "The CVXOPT library is required to use this module"): - n.transform("relaxation", (E0, E1)) + results_dict = approximate_quantum_error((E0, E1), operator_dict={"X": X, "Y": Y, "Z": Z}) + results_string = approximate_quantum_error((E0, E1), operator_string='pauli') + results_list = approximate_quantum_error((E0, E1), operator_list=[X, Y, Z]) + results_tuple = approximate_quantum_error((E0, E1), operator_list=(X, Y, Z)) + + self.assertErrorsAlmostEqual(results_dict, results_string) + self.assertErrorsAlmostEqual(results_string, results_list) + self.assertErrorsAlmostEqual(results_list, results_tuple) + + # def test_fidelity(self): + # n = NoiseTransformer() + # expected_fidelity = {'X': 0, 'Y': 0, 'Z': 0, 'H': 0, 'S': 2} + # for key in expected_fidelity: + # self.assertAlmostEqual(expected_fidelity[key], n.fidelity([self.ops[key]]), msg = "Wrong fidelity for {}".format(key)) + # + # def test_numeric_channel_matrix_representation(self): + # n = NoiseTransformer() + # gamma = 0.5 + # E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) + # E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) + # numeric_matrix = n.numeric_channel_matrix_representation((E0, E1)) + # expected_numeric_matrix = numpy.array([[1, 0, 0, 0], [0, 0.707106781186548, 0, 0],[0,0,0.707106781186548,0],[0.500000000000000, 0, 0, 0.500000000000000]]) + # self.assertMatricesAlmostEqual(expected_numeric_matrix, numeric_matrix) + # + # def test_op_name_to_matrix(self): + # X = self.ops['X'] + # Y = self.ops['Y'] + # Z = self.ops['Z'] + # H = self.ops['H'] + # S = self.ops['S'] + # self.assertTrue((X*Y*Z == self.n.op_name_to_matrix('XYZ', self.ops)).all()) + # self.assertTrue((S*X*S*Y*H*Z*H*S*X*Z == self.n.op_name_to_matrix('SXSYHZHSXZ', self.ops)).all()) + # + # def test_qobj_noise_to_kraus_operators(self): + # amplitude_damping_kraus_noise = { + # "type": "qerror", + # "operations": ["h"], + # "probabilities": [1.0], + # "instructions": [ + # [{"name": "kraus", "qubits": [0], "params": [ + # [[[1, 0], [0, 0]], [[0, 0], [0.5, 0]]], + # [[[0, 0], [0.86602540378, 0]], [[0, 0], [0, 0]]]]}] + # ] + # } + # n = NoiseTransformer() + # kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) + # matrices = amplitude_damping_kraus_noise['instructions'][0][0]['params'] + # expected_kraus_operators = [n.qobj_matrix_to_numpy_matrix(m) for m in matrices] + # self.assertListAlmostEqual(expected_kraus_operators, kraus_operators, places=4) + # + # #now for malformed data + # amplitude_damping_kraus_noise['instructions'][0][0] = {"name": "TTG", "qubits": [0]} + # with self.assertRaises(RuntimeError): + # kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) + # + # def test_errors(self): + # n = NoiseTransformer() + # gamma = 0.5 + # E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) + # E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) + # # kraus error is legit, transform_channel_operators are not + # with self.assertRaisesRegex(RuntimeError, "7 is not an appropriate input to transform"): + # n.transform(7, (E0, E1)) + # with self.assertRaisesRegex(RuntimeError, "No information about noise type seven"): + # n.transform("seven", (E0, E1)) + # + # #let's pretend cvxopt does not exist; the script should raise ImportError with proper message + # import unittest.mock + # import sys + # with unittest.mock.patch.dict(sys.modules, {'cvxopt': None}): + # with self.assertRaisesRegex(ImportError, "The CVXOPT library is required to use this module"): + # n.transform("relaxation", (E0, E1)) From 0e03ee642e2ea5cc9c88c2e13f0f81561ac27800 Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 13:37:52 +0300 Subject: [PATCH 13/28] Target channel matrix now computed via SuperOp --- .../aer/noise/noise_transformation.py | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index a8adf51dbb..9ef360c378 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -7,6 +7,8 @@ from qiskit.providers.aer.noise.errors import QuantumError from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary from qiskit.quantum_info.operators.channel import Kraus +from qiskit.quantum_info.operators.channel import SuperOp + # #only for 1-qubit errors for now # def quantum_error_to_kraus_operators(error): @@ -192,7 +194,7 @@ def prepare_honesty_constraint(self, transform_channel_operators_list): # methods relevant to the transformation to quadratic programming instance @staticmethod - def fidelity(channel): #TODO replace with qiskit fidelity + def fidelity(channel): return sum([numpy.abs(numpy.trace(E)) ** 2 for E in channel]) def generate_channel_matrices(self, transform_channel_operators_list): @@ -321,28 +323,14 @@ def transform_by_given_channel(self, channel_matrices, const_channel_matrix): norm of the matrix (A-B) obtained as the difference of the input noise channel and the output channel we wish to determine. """ - target_channel_matrix = self.numeric_channel_matrix_representation(self.noise_kraus_operators) + target_channel = SuperOp(Kraus(self.noise_kraus_operators)) + target_channel_matrix = target_channel._data.T + const_matrix = const_channel_matrix - target_channel_matrix P = self.compute_P(channel_matrices) q = self.compute_q(channel_matrices, const_matrix) return self.solve_quadratic_program(P, q) - #TODO: replace with Kraus object from Terra ("evolve" method) - # qiskit-terra\qiskit\quantum_info\operators\channel - @staticmethod - def numeric_compute_channel_operation(rho, operators): - return numpy.sum([E.dot(rho).dot(E.conj().T) for E in operators], 0) - - def numeric_channel_matrix_representation(self, operators): - standard_base = [ - numpy.array([[1, 0], [0, 0]]), - numpy.array([[0, 1], [0, 0]]), - numpy.array([[0, 0], [1, 0]]), - numpy.array([[0, 0], [0, 1]]) - ] - return (numpy.array([self.numeric_compute_channel_operation(rho, operators).flatten() - for rho in standard_base])) - def compute_P(self, As): vs = [numpy.array(A).flatten() for A in As] n = len(vs) From 593c5ac697280bbd941c560bb83944e48328c5b9 Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 13:52:26 +0300 Subject: [PATCH 14/28] Tests are now fully working --- qiskit/providers/aer/noise/__init__.py | 1 + .../aer/noise/noise_transformation.py | 27 +----- test/terra/test_noise_transformation.py | 88 +++++-------------- 3 files changed, 26 insertions(+), 90 deletions(-) diff --git a/qiskit/providers/aer/noise/__init__.py b/qiskit/providers/aer/noise/__init__.py index d8911cb9a8..cf1d79a042 100644 --- a/qiskit/providers/aer/noise/__init__.py +++ b/qiskit/providers/aer/noise/__init__.py @@ -66,5 +66,6 @@ from .noise_model import NoiseModel from .noise_transformation import NoiseTransformer +from .noise_transformation import approximate_quantum_error from . import errors from . import device \ No newline at end of file diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 9ef360c378..9be4b47a61 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -9,30 +9,7 @@ from qiskit.quantum_info.operators.channel import Kraus from qiskit.quantum_info.operators.channel import SuperOp - -# #only for 1-qubit errors for now -# def quantum_error_to_kraus_operators(error): -# qubits_num = 1 -# error_ops = [] -# for noise_circuit, noise_prob in zip(error._noise_circuits, error._noise_probabilities): -# noise_circuit_ops = [numpy.eye(2 ** qubits_num)] -# for noise_circuit_element in noise_circuit: -# std_op = standard_gate_unitary(noise_circuit_element['name']) -# if std_op is not None: -# kraus_matrices = [std_op] -# if noise_circuit_element['name'] == 'kraus': -# kraus_matrices = noise_circuit_element['params'] -# if noise_circuit_element['name'] == 'unitary': -# #TODO: I expect only one matrix here. Is this assumption correct? -# kraus_matrices = [noise_circuit_element['params']] -# if kraus_matrices is None: -# raise "Could not understand the error {}".format(error) -# kraus_matrices = [numpy.array(matrix) for matrix in kraus_matrices] -# noise_circuit_ops = [b @ a for (a, b) in itertools.product(noise_circuit_ops, kraus_matrices)] -# error_ops += [numpy.sqrt(noise_prob) * noise_op for noise_op in noise_circuit_ops] -# return error_ops - -def approximate_quantum_error(error, +def approximate_quantum_error(error, *, operator_string = None, operator_dict = None, operator_list = None): @@ -52,6 +29,8 @@ def approximate_quantum_error(error, error_kraus_operators = Kraus(error.to_channel()).data transformer = NoiseTransformer() if operator_string is not None: + if operator_string not in transformer.named_operators.keys(): + raise RuntimeError("No information about noise type {}".format(operator_string)) operator_dict = transformer.named_operators[operator_string] if operator_dict is not None: names, operator_list = zip(*operator_dict.items()) diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index 247dbf4e8e..9aa898b4ce 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -69,12 +69,12 @@ def test_transformation_by_pauli(self): def test_reset(self): # approximating amplitude damping using relaxation operators - gamma = 0.5 + gamma = 0.23 error = amplitude_damping_error(gamma) p = (gamma - numpy.sqrt(1 - gamma) + 1) / 2 q = 0 expected_results = reset_error(p,q) - results = approximate_quantum_error(error, "reset") + results = approximate_quantum_error(error, operator_string="reset") self.assertErrorsAlmostEqual(results, expected_results) def test_transform(self): @@ -95,71 +95,27 @@ def test_transform(self): self.assertErrorsAlmostEqual(results_string, results_list) self.assertErrorsAlmostEqual(results_list, results_tuple) - # def test_fidelity(self): - # n = NoiseTransformer() - # expected_fidelity = {'X': 0, 'Y': 0, 'Z': 0, 'H': 0, 'S': 2} - # for key in expected_fidelity: - # self.assertAlmostEqual(expected_fidelity[key], n.fidelity([self.ops[key]]), msg = "Wrong fidelity for {}".format(key)) - # - # def test_numeric_channel_matrix_representation(self): - # n = NoiseTransformer() - # gamma = 0.5 - # E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) - # E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) - # numeric_matrix = n.numeric_channel_matrix_representation((E0, E1)) - # expected_numeric_matrix = numpy.array([[1, 0, 0, 0], [0, 0.707106781186548, 0, 0],[0,0,0.707106781186548,0],[0.500000000000000, 0, 0, 0.500000000000000]]) - # self.assertMatricesAlmostEqual(expected_numeric_matrix, numeric_matrix) - # - # def test_op_name_to_matrix(self): - # X = self.ops['X'] - # Y = self.ops['Y'] - # Z = self.ops['Z'] - # H = self.ops['H'] - # S = self.ops['S'] - # self.assertTrue((X*Y*Z == self.n.op_name_to_matrix('XYZ', self.ops)).all()) - # self.assertTrue((S*X*S*Y*H*Z*H*S*X*Z == self.n.op_name_to_matrix('SXSYHZHSXZ', self.ops)).all()) - # - # def test_qobj_noise_to_kraus_operators(self): - # amplitude_damping_kraus_noise = { - # "type": "qerror", - # "operations": ["h"], - # "probabilities": [1.0], - # "instructions": [ - # [{"name": "kraus", "qubits": [0], "params": [ - # [[[1, 0], [0, 0]], [[0, 0], [0.5, 0]]], - # [[[0, 0], [0.86602540378, 0]], [[0, 0], [0, 0]]]]}] - # ] - # } - # n = NoiseTransformer() - # kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) - # matrices = amplitude_damping_kraus_noise['instructions'][0][0]['params'] - # expected_kraus_operators = [n.qobj_matrix_to_numpy_matrix(m) for m in matrices] - # self.assertListAlmostEqual(expected_kraus_operators, kraus_operators, places=4) - # - # #now for malformed data - # amplitude_damping_kraus_noise['instructions'][0][0] = {"name": "TTG", "qubits": [0]} - # with self.assertRaises(RuntimeError): - # kraus_operators = n.qobj_noise_to_kraus_operators(amplitude_damping_kraus_noise) - # - # def test_errors(self): - # n = NoiseTransformer() - # gamma = 0.5 - # E0 = numpy.array([[1, 0], [0, numpy.sqrt(1 - gamma)]]) - # E1 = numpy.array([[0, numpy.sqrt(gamma)], [0, 0]]) - # # kraus error is legit, transform_channel_operators are not - # with self.assertRaisesRegex(RuntimeError, "7 is not an appropriate input to transform"): - # n.transform(7, (E0, E1)) - # with self.assertRaisesRegex(RuntimeError, "No information about noise type seven"): - # n.transform("seven", (E0, E1)) - # - # #let's pretend cvxopt does not exist; the script should raise ImportError with proper message - # import unittest.mock - # import sys - # with unittest.mock.patch.dict(sys.modules, {'cvxopt': None}): - # with self.assertRaisesRegex(ImportError, "The CVXOPT library is required to use this module"): - # n.transform("relaxation", (E0, E1)) - + def test_fidelity(self): + n = NoiseTransformer() + expected_fidelity = {'X': 0, 'Y': 0, 'Z': 0, 'H': 0, 'S': 2} + for key in expected_fidelity: + self.assertAlmostEqual(expected_fidelity[key], n.fidelity([self.ops[key]]), msg = "Wrong fidelity for {}".format(key)) + def test_errors(self): + gamma = 0.23 + error = amplitude_damping_error(gamma) + # kraus error is legit, transform_channel_operators are not + with self.assertRaisesRegex(TypeError, "takes 1 positional argument but 2 were given"): + approximate_quantum_error(error, 7) + with self.assertRaisesRegex(RuntimeError, "No information about noise type seven"): + approximate_quantum_error(error, operator_string="seven") + + #let's pretend cvxopt does not exist; the script should raise ImportError with proper message + import unittest.mock + import sys + with unittest.mock.patch.dict(sys.modules, {'cvxopt': None}): + with self.assertRaisesRegex(ImportError, "The CVXOPT library is required to use this module"): + approximate_quantum_error(error, operator_string="reset") if __name__ == '__main__': unittest.main() \ No newline at end of file From 1b9d4e19b125fbb18da4573c2175a4e8ede5a0d5 Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 14:38:20 +0300 Subject: [PATCH 15/28] Implementation of approximate_noise_model --- .../aer/noise/noise_transformation.py | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 9be4b47a61..909d5f5c36 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -5,6 +5,8 @@ import itertools from qiskit.providers.aer.noise.errors import QuantumError +from qiskit.providers.aer.noise import NoiseModel +from qiskit.providers.aer.noise.noiseerror import NoiseError from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary from qiskit.quantum_info.operators.channel import Kraus from qiskit.quantum_info.operators.channel import SuperOp @@ -21,11 +23,13 @@ def approximate_quantum_error(error, *, Returns: QuantumError: the approximate quantum error. """ - # This would accept as input anything that the QuantumError class can take as input - # Eg: list of circuits, list of kraus matrices, a QuantumChannel sublcass object, or a - # QuantumError object. This could be done as: + if not isinstance(error, QuantumError): error = QuantumError(error) + if error.number_of_qubits > 1: + raise NoiseError("Only 1-qubit noises can be converted, {}-qubit noise found in model".format( + error.number_of_qubits)) + error_kraus_operators = Kraus(error.to_channel()).data transformer = NoiseTransformer() if operator_string is not None: @@ -51,7 +55,10 @@ def approximate_quantum_error(error, *, raise Exception("Quantum error approximation failed - no approximating operators detected") -def approximate_noise_model(model, **kwargs): +def approximate_noise_model(model, *, + operator_string = None, + operator_dict = None, + operator_list = None): """Return an approximate noise model. Args: @@ -63,11 +70,59 @@ def approximate_noise_model(model, **kwargs): Raises: NoiseError: if the QuantumError cannot be approximated. """ - # This function would iterate over all QuantumErrors in the noise model - # and call the `approximate_quantum_errror` function on them to generate - # an approximate noise model + + #We need to iterate over all the errors in the noise model. + #No nice interface for this now, easiest way is to mimic as_dict # TODO: Raise error about 2-qubit errors - approx_noise_model = None + + error_list = [] + # Add default quantum errors + for operation, errors in model._default_quantum_errors.items(): + for error in errors: + error = approximate_quantum_error(error, operator_string=operator_string, operator_dict=operator_dict, operator_list=operator_list) + error_dict = error.as_dict() + error_dict["operations"] = [operation] + error_list.append(error_dict) + + # Add specific qubit errors + for operation, qubit_dict in model._local_quantum_errors.items(): + for qubits_str, errors in qubit_dict.items(): + for error in errors: + error = approximate_quantum_error(error, operator_string=operator_string, operator_dict=operator_dict, + operator_list=operator_list) + error_dict = error.as_dict() + error_dict["operations"] = [operation] + error_dict["gate_qubits"] = [model._str2qubits(qubits_str)] + error_list.append(error_dict) + + # Add non-local errors + for operation, qubit_dict in model._nonlocal_quantum_errors.items(): + for qubits_str, errors in qubit_dict.items(): + for error, noise_qubits in errors: + error = approximate_quantum_error(error, operator_string=operator_string, operator_dict=operator_dict, + operator_list=operator_list) + error_dict = error.as_dict() + error_dict["operations"] = [operation] + error_dict["gate_qubits"] = [model._str2qubits(qubits_str)] + error_dict["noise_qubits"] = [list(noise_qubits)] + error_list.append(error_dict) + + # Add default readout error + if model._default_readout_error is not None: + error = approximate_quantum_error(model._default_readout_error, operator_string=operator_string, operator_dict=operator_dict, + operator_list=operator_list) + error_dict = error.as_dict() + error_list.append(error_dict) + + # Add local readout error + for qubits_str, error in model._local_readout_errors.items(): + error = approximate_quantum_error(error, operator_string=operator_string, operator_dict=operator_dict, + operator_list=operator_list) + error_dict = error.as_dict() + error_dict["gate_qubits"] = [model._str2qubits(qubits_str)] + error_list.append(error_dict) + + approx_noise_model = NoiseModel.from_dict({"errors": error_list, "x90_gates": model._x90_gates}) return approx_noise_model class NoiseTransformer: From e8e554d4049c6aff2efd2d64ea661becb15113cb Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 16:57:31 +0300 Subject: [PATCH 16/28] Now testing noise model transformation --- test/terra/test_noise_transformation.py | 45 ++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index 9aa898b4ce..379820a5e7 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -1,8 +1,10 @@ import unittest import numpy from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary +from qiskit.providers.aer.noise import NoiseModel from qiskit.providers.aer.noise import NoiseTransformer from qiskit.providers.aer.noise import approximate_quantum_error +from qiskit.providers.aer.noise import approximate_noise_model from qiskit.providers.aer.noise.errors.standard_errors import amplitude_damping_error from qiskit.providers.aer.noise.errors.standard_errors import reset_error from qiskit.providers.aer.noise.errors.standard_errors import pauli_error @@ -19,10 +21,29 @@ def setUp(self): } self.n = NoiseTransformer() + + def assertNoiseModelsAlmostEqual(self, lhs, rhs, places = 3): + self.assertNoiseDictsAlmostEqual(lhs._nonlocal_quantum_errors, rhs._nonlocal_quantum_errors, places=places) + self.assertNoiseDictsAlmostEqual(lhs._local_quantum_errors, rhs._local_quantum_errors, places=places) + self.assertNoiseDictsAlmostEqual(lhs._default_quantum_errors, rhs._default_quantum_errors, places=places) + self.assertNoiseDictsAlmostEqual(lhs._local_readout_errors, rhs._local_readout_errors, places=places) + if lhs._default_readout_error is not None: + self.assertTrue(rhs._default_readout_error is not None) + self.assertErrorsAlmostEqual(lhs._default_readout_error, rhs._default_readout_error, places=places) + else: + self.assertTrue(rhs._default_readout_error is None) + + def assertNoiseDictsAlmostEqual(self, lhs, rhs, places=3): + keys = set(lhs.keys()).union(set(rhs.keys())) + for key in keys: + self.assertTrue(key in lhs.keys(), msg="Key {} is missing from lhs".format(key)) + self.assertTrue(key in rhs.keys(), msg="Key {} is missing from rhs".format(key)) + for (lhs_error, rhs_error) in zip (lhs[key], rhs[key]): + self.assertErrorsAlmostEqual(lhs_error, rhs_error, places=places) + def assertErrorsAlmostEqual(self, lhs, rhs, places = 3): self.assertMatricesAlmostEqual(lhs.to_channel()._data, rhs.to_channel()._data, places) - def assertDictAlmostEqual(self, lhs, rhs, places = None): keys = set(lhs.keys()).union(set(rhs.keys())) for key in keys: @@ -101,6 +122,28 @@ def test_fidelity(self): for key in expected_fidelity: self.assertAlmostEqual(expected_fidelity[key], n.fidelity([self.ops[key]]), msg = "Wrong fidelity for {}".format(key)) + def test_approx_noise_model(self): + noise_model = NoiseModel() + gamma = 0.23 + p = 0.4 + q = 0.33 + ad_error = amplitude_damping_error(gamma) + r_error = reset_error(p,q) #should be approximated as-is + noise_model.add_all_qubit_quantum_error(ad_error, 'iden x y s') + noise_model.add_all_qubit_quantum_error(r_error, 'iden z h') + + result = approximate_noise_model(noise_model, operator_string="reset") + + expected_result = NoiseModel() + gamma_p = (gamma - numpy.sqrt(1 - gamma) + 1) / 2 + gamma_q = 0 + ad_error_approx = reset_error(gamma_p, gamma_q) + expected_result.add_all_qubit_quantum_error(ad_error_approx, 'iden x y s') + expected_result.add_all_qubit_quantum_error(r_error, 'iden z h') + + self.assertNoiseModelsAlmostEqual(expected_result, result) + + def test_errors(self): gamma = 0.23 error = amplitude_damping_error(gamma) From 45efe0dd65e0379cc0b6a7f2a21c5d91abfd7c3a Mon Sep 17 00:00:00 2001 From: Gadi Date: Tue, 16 Apr 2019 17:25:22 +0300 Subject: [PATCH 17/28] Noise transformer tests will be skipped unless cvxopt is installed; added cvxopt to requirements-dev --- requirements-dev.txt | 1 + test/terra/test_noise_transformation.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4abe2bf94c..59b22db41a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,4 @@ cmake scikit-build cython asv +cvxopt \ No newline at end of file diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index 379820a5e7..85eda9cea5 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -9,8 +9,13 @@ from qiskit.providers.aer.noise.errors.standard_errors import reset_error from qiskit.providers.aer.noise.errors.standard_errors import pauli_error -#TODO: skip tests if CVXOPT is not present +try: + import cvxopt + has_cvxopt = True +except ImportError: + has_cvxopt = False +@unittest.skipUnless(has_cvxopt, "Needs cvxopt to test") class TestNoiseTransformer(unittest.TestCase): def setUp(self): self.ops = {'X': standard_gate_unitary('x'), From 79e37e70ba925267db16d392dd7c982a4ca56dee Mon Sep 17 00:00:00 2001 From: Gadi Date: Wed, 17 Apr 2019 12:56:26 +0300 Subject: [PATCH 18/28] Added Clifford support --- qiskit/providers/aer/noise/noise_transformation.py | 8 ++------ test/terra/test_noise_transformation.py | 7 +++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 909d5f5c36..c3c28a67a7 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -8,6 +8,7 @@ from qiskit.providers.aer.noise import NoiseModel from qiskit.providers.aer.noise.noiseerror import NoiseError from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary +from qiskit.providers.aer.noise.errors.errorutils import single_qubit_clifford_matrix from qiskit.quantum_info.operators.channel import Kraus from qiskit.quantum_info.operators.channel import SuperOp @@ -73,7 +74,6 @@ def approximate_noise_model(model, *, #We need to iterate over all the errors in the noise model. #No nice interface for this now, easiest way is to mimic as_dict - # TODO: Raise error about 2-qubit errors error_list = [] # Add default quantum errors @@ -137,9 +137,6 @@ class NoiseTransformer: The goal of this module is to transform one noise channel into another, where the goal channel is constructed from a given set of matrices, using coefficients computed by the module in order to satisfy some criteria (for now, we wish the output channel to be "close" to the input channel) - - The main public function of the module is transform(), and a conversion of qobj inputs is given - via the transform_qobj() function. """ def __init__(self): @@ -152,7 +149,7 @@ def __init__(self): 'p': (numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])), 'q': (numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])), }, - # 'clifford': self.single_qubit_full_clifford_group() + 'clifford': dict([(j, single_qubit_clifford_matrix(j)) for j in range(24)]) } self.fidelity_data = None @@ -386,7 +383,6 @@ def compute_q(self, As, C): # should we consider another library, only this method needs to change def solve_quadratic_program(self, P, q): try: - #TODO: consider using QP with CVXPY import cvxopt except ImportError: raise ImportError("The CVXOPT library is required to use this module") diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index 85eda9cea5..6f01d41db0 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -148,6 +148,13 @@ def test_approx_noise_model(self): self.assertNoiseModelsAlmostEqual(expected_result, result) + def test_clifford(self): + x_p = 0.17 + y_p = 0.13 + z_p = 0.34 + error = pauli_error([('X', x_p), ('Y', y_p), ('Z', z_p), ('I', 1 - (x_p + y_p + z_p))]) + results = approximate_quantum_error(error, operator_string="clifford") + self.assertErrorsAlmostEqual(error, results) def test_errors(self): gamma = 0.23 From 210aa4c07ea11853964245ef1569e4c1b52d1b8c Mon Sep 17 00:00:00 2001 From: Gadi Date: Wed, 17 Apr 2019 12:58:20 +0300 Subject: [PATCH 19/28] Added support for approximate_noise_model --- qiskit/providers/aer/noise/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/providers/aer/noise/__init__.py b/qiskit/providers/aer/noise/__init__.py index cf1d79a042..53f18f9cd6 100644 --- a/qiskit/providers/aer/noise/__init__.py +++ b/qiskit/providers/aer/noise/__init__.py @@ -67,5 +67,6 @@ from .noise_model import NoiseModel from .noise_transformation import NoiseTransformer from .noise_transformation import approximate_quantum_error +from .noise_transformation import approximate_noise_model from . import errors from . import device \ No newline at end of file From 3c88bb2dc400cd292b5ff325052b9a612cb7e8a9 Mon Sep 17 00:00:00 2001 From: Gadi Date: Wed, 17 Apr 2019 14:40:06 +0300 Subject: [PATCH 20/28] Documentation --- .../aer/noise/noise_transformation.py | 49 ++++++++++++++----- test/terra/test_noise_transformation.py | 8 +++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index c3c28a67a7..179ed17053 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -1,4 +1,23 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- + +# Copyright 2019, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" +Noise transformation module + +The goal of this module is to transform one 1-qubit noise channel (given by the QuantumError class) +into another, built from specified "building blocks" (given as Kraus matrices) such that +the new channel is as close as possible to the original one in the Hilber-Schmidt metric. + +For a typical use case, consider a simulator for circuits built from the Clifford group. +Computations on such circuits can be simulated at polynomial time and space, but not all +noise channels can be used in such a simulation. To enable noisy Clifford simulation one can +transform the given noise channel into the closest one, Hilbert-Schmidt wise, that +can be used in a Clifford simulator. +""" import numpy import sympy @@ -16,10 +35,18 @@ def approximate_quantum_error(error, *, operator_string = None, operator_dict = None, operator_list = None): - """Return an approximate QuantumError. + """Return an approximate QuantumError bases on the Hilbert-Schmidt metric. Args: error (QuantumError): the error to be approximated. + operator_string (string): a name for a premade set of building blocks for the output channel + operator_dict (dict): a dictionary whose values are the building blocks for the output channel + operator_list (dict): list of building blocks for the output channel + + Additional Information: + The operator input precedence is as follows: list < dict < string + if a string is given, dict is overwritten; if a dict is given, list is overwritten + possible values for string are 'pauli', 'reset', 'clifford'; see NoiseTransformer.named_operators Returns: QuantumError: the approximate quantum error. @@ -64,6 +91,14 @@ def approximate_noise_model(model, *, Args: model (NoiseModel): the noise model to be approximated. + operator_string (string): a name for a premade set of building blocks for the output channel + operator_dict (dict): a dictionary whose values are the building blocks for the output channel + operator_list (dict): list of building blocks for the output channel + + Additional Information: + The operator input precedence is as follows: list < dict < string + if a string is given, dict is overwritten; if a dict is given, list is overwritten + possible values for string are 'pauli', 'reset', 'clifford'; see NoiseTransformer.named_operators Returns: NoiseModel: the approximate noise model. @@ -127,16 +162,6 @@ def approximate_noise_model(model, *, class NoiseTransformer: """Transforms one quantum noise channel to another based on a specified criteria. - - A quantum 1-qubit noise channel is represented by Kraus operators: a sequence (E0, E1,...,En) of - 2x2 matrices that satisfy \sum_{i=0}^n E_i^\daggerE_i = I - - Given a quantum state's density function rho, the effect of the channel on this state is - rho -> \sum_{i=1}^n E_i * rho * E_i^\dagger - - The goal of this module is to transform one noise channel into another, where the goal channel - is constructed from a given set of matrices, using coefficients computed by the module in order - to satisfy some criteria (for now, we wish the output channel to be "close" to the input channel) """ def __init__(self): diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index 6f01d41db0..c77d10cddf 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -1,3 +1,11 @@ +# Copyright 2019, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. +""" +NoiseTransformer class tests +""" + import unittest import numpy from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary From 3e23dcc0268b84a6797be132b07051bd002d6ee2 Mon Sep 17 00:00:00 2001 From: Gadi Date: Thu, 18 Apr 2019 12:47:51 +0300 Subject: [PATCH 21/28] Now generates noise approximation using unitaries if possible --- .../aer/noise/noise_transformation.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/noise_transformation.py index 179ed17053..2485f550a5 100644 --- a/qiskit/providers/aer/noise/noise_transformation.py +++ b/qiskit/providers/aer/noise/noise_transformation.py @@ -73,10 +73,16 @@ def approximate_quantum_error(error, *, raise RuntimeError("Approximated channel operators probabilities sum to {}".format(1-identity_prob)) quantum_error_spec = [([{'name': 'id', 'qubits':[0]}],identity_prob)] for (op_matrices, probability) in zip(operator_list, probabilities): + if len(op_matrices) == 1: #can be added as unitary + quantum_error_spec.append(([{'name': 'unitary', + 'qubits': [0], + 'params': op_matrices[0]} + ], probability)) + else: # convert op_matrices to list since tuples of Kraus matrices are treated as generalized Kraus representation - quantum_error_spec.append(([{'name': 'kraus', + quantum_error_spec.append(([{'name': 'kraus', 'qubits': [0], - 'params': list(op_matrices)} + 'params': op_matrices} ], probability)) return QuantumError(quantum_error_spec) @@ -166,15 +172,15 @@ class NoiseTransformer: def __init__(self): self.named_operators = { - 'pauli': {'X': standard_gate_unitary('x'), - 'Y': standard_gate_unitary('y'), - 'Z': standard_gate_unitary('z') + 'pauli': {'X': [standard_gate_unitary('x')], + 'Y': [standard_gate_unitary('y')], + 'Z': [standard_gate_unitary('z')] }, 'reset': { - 'p': (numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])), - 'q': (numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])), + 'p': [numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])], + 'q': [numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])], }, - 'clifford': dict([(j, single_qubit_clifford_matrix(j)) for j in range(24)]) + 'clifford': dict([(j, [single_qubit_clifford_matrix(j)]) for j in range(24)]) } self.fidelity_data = None From a843049f9e1ec6337aec6bff8e769fa896551285 Mon Sep 17 00:00:00 2001 From: Gadi Date: Thu, 18 Apr 2019 14:54:45 +0300 Subject: [PATCH 22/28] Restructure - add noise utils directory --- qiskit/providers/aer/noise/__init__.py | 14 ++++++++++---- qiskit/providers/aer/noise/utils/__init__.py | 12 ++++++++++++ .../aer/noise/{ => utils}/noise_transformation.py | 0 test/terra/test_noise_transformation.py | 6 +++--- 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 qiskit/providers/aer/noise/utils/__init__.py rename qiskit/providers/aer/noise/{ => utils}/noise_transformation.py (100%) diff --git a/qiskit/providers/aer/noise/__init__.py b/qiskit/providers/aer/noise/__init__.py index 53f18f9cd6..c5eb96032a 100644 --- a/qiskit/providers/aer/noise/__init__.py +++ b/qiskit/providers/aer/noise/__init__.py @@ -62,11 +62,17 @@ Amplitude damping error Phase damping error Combined phase and amplitude damping error + +Noise Utilities +-------------- +The `noise.utils` module contains utilities for noise models and errors including: + 'approximate_quantum_error' for approximating a general quantum error via + a provided set of errors (e.g. approximating amplitude damping via reset errors) + 'approximate_noise_model' for approximating all the errors in a nose model using + the same provided set of errors """ from .noise_model import NoiseModel -from .noise_transformation import NoiseTransformer -from .noise_transformation import approximate_quantum_error -from .noise_transformation import approximate_noise_model from . import errors -from . import device \ No newline at end of file +from . import device +from . import utils \ No newline at end of file diff --git a/qiskit/providers/aer/noise/utils/__init__.py b/qiskit/providers/aer/noise/utils/__init__.py new file mode 100644 index 0000000000..909ac057ca --- /dev/null +++ b/qiskit/providers/aer/noise/utils/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""Noise utils for Qiskit Aer. +""" +from .noise_transformation import NoiseTransformer +from .noise_transformation import approximate_quantum_error +from .noise_transformation import approximate_noise_model diff --git a/qiskit/providers/aer/noise/noise_transformation.py b/qiskit/providers/aer/noise/utils/noise_transformation.py similarity index 100% rename from qiskit/providers/aer/noise/noise_transformation.py rename to qiskit/providers/aer/noise/utils/noise_transformation.py diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index c77d10cddf..9576ebb2ab 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -10,9 +10,9 @@ import numpy from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary from qiskit.providers.aer.noise import NoiseModel -from qiskit.providers.aer.noise import NoiseTransformer -from qiskit.providers.aer.noise import approximate_quantum_error -from qiskit.providers.aer.noise import approximate_noise_model +from qiskit.providers.aer.noise.utils import NoiseTransformer +from qiskit.providers.aer.noise.utils import approximate_quantum_error +from qiskit.providers.aer.noise.utils import approximate_noise_model from qiskit.providers.aer.noise.errors.standard_errors import amplitude_damping_error from qiskit.providers.aer.noise.errors.standard_errors import reset_error from qiskit.providers.aer.noise.errors.standard_errors import pauli_error From bf30c5c5f2d6615d821ed9af47842318764f5bfe Mon Sep 17 00:00:00 2001 From: Gadi Date: Thu, 18 Apr 2019 15:08:40 +0300 Subject: [PATCH 23/28] Allow uppercase letters in named noises --- qiskit/providers/aer/noise/utils/noise_transformation.py | 1 + test/terra/test_noise_transformation.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/qiskit/providers/aer/noise/utils/noise_transformation.py b/qiskit/providers/aer/noise/utils/noise_transformation.py index 2485f550a5..fe2aa129d6 100644 --- a/qiskit/providers/aer/noise/utils/noise_transformation.py +++ b/qiskit/providers/aer/noise/utils/noise_transformation.py @@ -61,6 +61,7 @@ def approximate_quantum_error(error, *, error_kraus_operators = Kraus(error.to_channel()).data transformer = NoiseTransformer() if operator_string is not None: + operator_string = operator_string.lower() if operator_string not in transformer.named_operators.keys(): raise RuntimeError("No information about noise type {}".format(operator_string)) operator_dict = transformer.named_operators[operator_string] diff --git a/test/terra/test_noise_transformation.py b/test/terra/test_noise_transformation.py index 9576ebb2ab..6cd2bcd0e3 100644 --- a/test/terra/test_noise_transformation.py +++ b/test/terra/test_noise_transformation.py @@ -164,6 +164,13 @@ def test_clifford(self): results = approximate_quantum_error(error, operator_string="clifford") self.assertErrorsAlmostEqual(error, results) + def test_approx_names(self): + gamma = 0.23 + error = amplitude_damping_error(gamma) + results_1 = approximate_quantum_error(error, operator_string="pauli") + results_2 = approximate_quantum_error(error, operator_string="Pauli") + self.assertErrorsAlmostEqual(results_1, results_2) + def test_errors(self): gamma = 0.23 error = amplitude_damping_error(gamma) From 96850a0be7de0f38461905aa82587a0c8ce3b19e Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 22 Apr 2019 10:59:49 +0300 Subject: [PATCH 24/28] Now when possible outputs error circuits instead of Kraus/Unitary --- .../aer/noise/utils/noise_transformation.py | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/qiskit/providers/aer/noise/utils/noise_transformation.py b/qiskit/providers/aer/noise/utils/noise_transformation.py index fe2aa129d6..d806ed89d6 100644 --- a/qiskit/providers/aer/noise/utils/noise_transformation.py +++ b/qiskit/providers/aer/noise/utils/noise_transformation.py @@ -27,7 +27,7 @@ from qiskit.providers.aer.noise import NoiseModel from qiskit.providers.aer.noise.noiseerror import NoiseError from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary -from qiskit.providers.aer.noise.errors.errorutils import single_qubit_clifford_matrix +from qiskit.providers.aer.noise.errors.errorutils import single_qubit_clifford_instructions from qiskit.quantum_info.operators.channel import Kraus from qiskit.quantum_info.operators.channel import SuperOp @@ -68,23 +68,15 @@ def approximate_quantum_error(error, *, if operator_dict is not None: names, operator_list = zip(*operator_dict.items()) if operator_list is not None: - probabilities = transformer.transform_by_operator_list(operator_list, error_kraus_operators) + op_matrix_list = [transformer.operator_matrix(operator) for operator in operator_list] + probabilities = transformer.transform_by_operator_list(op_matrix_list, error_kraus_operators) identity_prob = 1 - sum(probabilities) if identity_prob < 0 or identity_prob > 1: raise RuntimeError("Approximated channel operators probabilities sum to {}".format(1-identity_prob)) quantum_error_spec = [([{'name': 'id', 'qubits':[0]}],identity_prob)] - for (op_matrices, probability) in zip(operator_list, probabilities): - if len(op_matrices) == 1: #can be added as unitary - quantum_error_spec.append(([{'name': 'unitary', - 'qubits': [0], - 'params': op_matrices[0]} - ], probability)) - else: - # convert op_matrices to list since tuples of Kraus matrices are treated as generalized Kraus representation - quantum_error_spec.append(([{'name': 'kraus', - 'qubits': [0], - 'params': op_matrices} - ], probability)) + op_circuit_list = [transformer.operator_circuit(operator) for operator in operator_list] + for (operator, probability) in zip(op_circuit_list, probabilities): + quantum_error_spec.append((operator, probability)) return QuantumError(quantum_error_spec) raise Exception("Quantum error approximation failed - no approximating operators detected") @@ -181,7 +173,7 @@ def __init__(self): 'p': [numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])], 'q': [numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])], }, - 'clifford': dict([(j, [single_qubit_clifford_matrix(j)]) for j in range(24)]) + 'clifford': dict([(j, single_qubit_clifford_instructions(j)) for j in range(1,24)]) } self.fidelity_data = None @@ -189,8 +181,42 @@ def __init__(self): self.noise_kraus_operators = None self.transform_channel_operators = None - # transformation interface methods + def operator_matrix(self, operator): + """ + Converts an operator representation to Kraus matrix representation + Args: + operator: operator representation. Can be a noise circuit or a matrix or a list of matrices + Output: + The operator, converted to Kraus representation + """ + if isinstance(operator, list) and isinstance(operator[0], dict): + operator_error = QuantumError([(operator, 1)]) + kraus_rep = Kraus(operator_error.to_channel()).data + return kraus_rep + return operator + + def operator_circuit(self, operator): + """ + Converts an operator representation to noise circuit + Args: + operator: operator representation. Can be a noise circuit or a matrix or a list of matrices + Output: + The operator, converted to noise circuit representation + """ + if isinstance(operator, numpy.ndarray): + return [{'name': 'unitary', 'qubits': [0], 'params': operator}] + + if isinstance(operator, list) and isinstance(operator[0], numpy.ndarray): + if len(operator) == 1: + return [{'name': 'unitary', 'qubits': [0], 'params': operator[0]}] + else: + return [{'name': 'kraus', 'qubits': [0], 'params': operator}] + + return operator + + + # transformation interface methods def transform_by_operator_list(self, transform_channel_operators, noise_kraus_operators): """ Args: @@ -228,12 +254,6 @@ def transform_by_operator_list(self, transform_channel_operators, noise_kraus_op probabilities = self.transform_by_given_channel(channel_matrices, const_channel_matrix) return probabilities - # convenience wrapper method - def transform_by_operator_dictionary(self, transform_channel_operators_dictionary, noise_kraus_operators): - names, operators = zip(*transform_channel_operators_dictionary.items()) - probabilities = self.transform_by_operator_list(operators, noise_kraus_operators) - return dict(zip(names, probabilities)) - # convert to sympy matrices and verify that each singleton is in a tuple; also add identity matrix @staticmethod def prepare_channel_operator_list(ops_list): From efdd10de2b374a6adefaec487e87c973ea183655 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 22 Apr 2019 11:07:52 +0300 Subject: [PATCH 25/28] Now represent all named operators using circuits --- .../providers/aer/noise/utils/noise_transformation.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qiskit/providers/aer/noise/utils/noise_transformation.py b/qiskit/providers/aer/noise/utils/noise_transformation.py index d806ed89d6..766a9f5a16 100644 --- a/qiskit/providers/aer/noise/utils/noise_transformation.py +++ b/qiskit/providers/aer/noise/utils/noise_transformation.py @@ -26,7 +26,6 @@ from qiskit.providers.aer.noise.errors import QuantumError from qiskit.providers.aer.noise import NoiseModel from qiskit.providers.aer.noise.noiseerror import NoiseError -from qiskit.providers.aer.noise.errors.errorutils import standard_gate_unitary from qiskit.providers.aer.noise.errors.errorutils import single_qubit_clifford_instructions from qiskit.quantum_info.operators.channel import Kraus from qiskit.quantum_info.operators.channel import SuperOp @@ -165,13 +164,13 @@ class NoiseTransformer: def __init__(self): self.named_operators = { - 'pauli': {'X': [standard_gate_unitary('x')], - 'Y': [standard_gate_unitary('y')], - 'Z': [standard_gate_unitary('z')] + 'pauli': {'X': [{'name': 'x', 'qubits': [0]}], + 'Y': [{'name': 'y', 'qubits': [0]}], + 'Z': [{'name': 'z', 'qubits': [0]}] }, 'reset': { - 'p': [numpy.array([[1, 0], [0, 0]]), numpy.array([[0, 1], [0, 0]])], - 'q': [numpy.array([[0, 0], [0, 1]]), numpy.array([[0, 0], [1, 0]])], + 'p': [{'name': 'reset','qubits': [0]}], #reset to |0> + 'q': [{'name': 'reset', 'qubits': [0]}, {'name': 'x','qubits': [0]}] #reset to |1> }, 'clifford': dict([(j, single_qubit_clifford_instructions(j)) for j in range(1,24)]) } From e87ec928fbe0b1f298b89c15d16b1d209d1deafa Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 22 Apr 2019 12:16:46 +0300 Subject: [PATCH 26/28] Return files to master version --- .../providers/aer/noise/errors/errorutils.py | 107 +++++++++++------- .../aer/noise/errors/quantum_error.py | 37 +++--- test/terra/noise/test_quantum_error.py | 57 +--------- 3 files changed, 88 insertions(+), 113 deletions(-) diff --git a/qiskit/providers/aer/noise/errors/errorutils.py b/qiskit/providers/aer/noise/errors/errorutils.py index abb12215dc..b0c035316b 100644 --- a/qiskit/providers/aer/noise/errors/errorutils.py +++ b/qiskit/providers/aer/noise/errors/errorutils.py @@ -324,49 +324,83 @@ def standard_gate_unitary(name): return None -def standard_instruction_operator(name, params=None): - """Return the Operator for a standard gate.""" +def reset_superop(num_qubits): + """Return a N-qubit reset SuperOp.""" + reset = SuperOp( + np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])) + if num_qubits == 1: + return reset + reset_n = reset + for _ in range(num_qubits - 1): + reset_n.tensor(reset) + return reset_n + + +def standard_instruction_operator(instr): + """Return the Operator for a standard gate instruction.""" + # Convert to dict (for QobjInstruction types) + if hasattr(instr, 'as_dict'): + instr = instr.as_dict() + # Get name and parameters + name = instr.get('name', "") + params = instr.get('params', []) + # Check if standard unitary gate name mat = standard_gate_unitary(name) if isinstance(mat, np.ndarray): return Operator(mat) + # Check if standard parameterized waltz gates - if name is 'u1': + if name == 'u1': lam = params[0] mat = np.diag([1, np.exp(1j * lam)]) return Operator(mat) - if name is 'u2': + if name == 'u2': phi = params[0] lam = params[1] mat = np.array([[1, -np.exp(1j * lam)], [np.exp(1j * phi), np.exp(1j * (phi + lam))]]) / np.sqrt(2) return Operator(mat) - if name is 'u3': + if name == 'u3': theta = params[0] phi = params[1] lam = params[2] mat = np.array( [[np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)], - [ - np.exp(1j * phi) * np.sin(theta / 2), - np.exp(1j * (phi + lam)) * np.cos(theta / 2) - ]]) + [np.exp(1j * phi) * np.sin(theta / 2), np.exp(1j * (phi + lam)) * np.cos(theta / 2)]]) return Operator(mat) - if name is 'unitary': - return Operator(params) - raise NoiseError('Cannot convert instruction to Operator') + # Check if unitary instruction + if name == 'unitary': + return Operator(params[0]) -def reset_superop(num_qubits): - """Return a N-qubit reset SuperOp.""" - reset = SuperOp( - np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])) - if num_qubits == 1: - return reset - reset_n = reset - for _ in range(num_qubits - 1): - reset_n.tensor(reset) - return reset_n + # Otherwise return None if we cannot convert instruction + return None + + +def standard_instruction_channel(instr): + """Return the SuperOp channel for a standard instruction.""" + # Check if standard operator + oper = standard_instruction_operator(instr) + if oper is not None: + return SuperOp(oper) + + # Convert to dict (for QobjInstruction types) + if hasattr(instr, 'as_dict'): + instr = instr.as_dict() + # Get name and parameters + name = instr.get('name', "") + + # Check if reset instruction + if name == 'reset': + # params should be the number of qubits being reset + num_qubits = len(instr['qubits']) + return reset_superop(num_qubits) + # Check if Kraus instruction + if name == 'kraus': + params = instr['params'] + return SuperOp(Kraus(params)) + return None def circuit2superop(circuit, min_qubits=1): @@ -387,28 +421,13 @@ def circuit2superop(circuit, min_qubits=1): superop = SuperOp(np.eye(4**num_qubits)) # compose each circuit element with the superoperator for instr in circuit: - name = None - qubits = None - params = None - if isinstance(instr, dict): - # Parse from plain dictionary qobj instruction - name = instr['name'] - qubits = instr.get('qubits') - params = instr.get('params') - else: - # Parse from QasmQobjInstruction - if hasattr(instr, 'name'): - name = instr.name - if hasattr(instr, 'qubits'): - qubits = instr.qubits - if hasattr(instr, 'params'): - params = instr.params - if name is 'reset': - instr_op = reset_superop(len(qubits)) - elif name is 'kraus': - instr_op = Kraus(params) + instr_op = standard_instruction_channel(instr) + if instr_op is None: + raise NoiseError('Cannot convert instruction {} to SuperOp'.format(instr)) + if hasattr(instr, 'qubits'): + qubits = instr.qubits else: - instr_op = SuperOp(standard_instruction_operator(name, params)) + qubits = instr['qubits'] superop = superop.compose(instr_op, qubits=qubits) return superop @@ -603,4 +622,4 @@ def kraus2instructions(kraus_ops, standard_gates, atol=ATOL_DEFAULT): ] instructions.append(make_kraus_instruction(non_unitaries, qubits)) probabilities.append(prob_kraus) - return zip(instructions, probabilities) + return zip(instructions, probabilities) \ No newline at end of file diff --git a/qiskit/providers/aer/noise/errors/quantum_error.py b/qiskit/providers/aer/noise/errors/quantum_error.py index f848f8441c..e006cd2632 100644 --- a/qiskit/providers/aer/noise/errors/quantum_error.py +++ b/qiskit/providers/aer/noise/errors/quantum_error.py @@ -13,13 +13,15 @@ import numpy as np -from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel -from qiskit.quantum_info.operators.channel.kraus import Kraus -from qiskit.quantum_info.operators.channel.superop import SuperOp +from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.quantum_info.operators import Kraus, SuperOp from qiskit.quantum_info.operators.predicates import ATOL_DEFAULT, RTOL_DEFAULT from ..noiseerror import NoiseError -from .errorutils import kraus2instructions, circuit2superop +from .errorutils import kraus2instructions +from .errorutils import circuit2superop +from .errorutils import standard_instruction_channel +from .errorutils import standard_instruction_operator logger = logging.getLogger(__name__) @@ -272,7 +274,7 @@ def as_dict(self): return error def compose(self, other, front=False): - """Return the composition error channel self∘other. + """Return the composition error channel self?other. Args: other (QuantumError): a quantum error channel. @@ -336,7 +338,7 @@ def compose(self, other, front=False): combined_circuit.append({'name': 'id', 'qubits': [0]}) # Add circuit combined_noise_circuits.append(combined_circuit) - noise_ops = zip(combined_noise_circuits, combined_noise_probabilities) + noise_ops = self._combine_kraus(zip(combined_noise_circuits, combined_noise_probabilities)) return QuantumError(noise_ops) def power(self, n): @@ -359,13 +361,13 @@ def power(self, n): return ret def tensor(self, other): - """Return the tensor product quantum error channel self ⊗ other. + """Return the tensor product quantum error channel self ? other. Args: other (QuantumError): a quantum error channel. Returns: - QuantumError: the tensor product error channel self ⊗ other. + QuantumError: the tensor product error channel self ? other. Raises: NoiseError: if other cannot be converted to a QuantumError. @@ -373,13 +375,13 @@ def tensor(self, other): return self._tensor_product(other, reverse=False) def expand(self, other): - """Return the tensor product quantum error channel self ⊗ other. + """Return the tensor product quantum error channel self ? other. Args: other (QuantumError): a quantum error channel. Returns: - QuantumError: the tensor product error channel other ⊗ self. + QuantumError: the tensor product error channel other ? self. Raises: NoiseError: if other cannot be converted to a QuantumError. @@ -387,7 +389,7 @@ def expand(self, other): return self._tensor_product(other, reverse=True) def kron(self, other): - """Return the tensor product quantum error channel self ⊗ other. + """Return the tensor product quantum error channel self ? other. DEPRECIATED: use QuantumError.tensor instead. @@ -395,7 +397,7 @@ def kron(self, other): other (QuantumError): a quantum error or channel. Returns: - QuantumError: the tensor product channel self ⊗ other. + QuantumError: the tensor product channel self ? other. """ warnings.warn( 'The kron() method is deprecated and will be removed ' @@ -407,9 +409,9 @@ def _tensor_product(self, other, reverse=False): """Return the tensor product error channel. Args: - other (QuantumChannel): a quantum channel subclass - reverse (bool): If False return self ⊗ other, if True return - if True return (other ⊗ self) [Default: False + other (QuantumError): a quantum channel subclass + reverse (bool): If False return self ? other, if True return + if True return (other ? self) [Default: False Returns: QuantumError: the tensor product error channel. @@ -466,7 +468,8 @@ def _tensor_product(self, other, reverse=False): combined_circuit.append({'name': 'id', 'qubits': [0]}) # Add circuit combined_noise_circuits.append(combined_circuit) - noise_ops = zip(combined_noise_circuits, combined_noise_probabilities) + # Now we combine any error circuits containing only Kraus operations + noise_ops = self._combine_kraus(zip(combined_noise_circuits, combined_noise_probabilities)) return QuantumError(noise_ops) @staticmethod @@ -572,4 +575,4 @@ def _compose_instr(instr0, instr1): else: name = 'unitary' params = [op0.compose(op1).data] - return {'name': name, 'qubits': qubits0, 'params': params} + return {'name': name, 'qubits': qubits0, 'params': params} \ No newline at end of file diff --git a/test/terra/noise/test_quantum_error.py b/test/terra/noise/test_quantum_error.py index 6019ea1aa4..2c91c1c7de 100644 --- a/test/terra/noise/test_quantum_error.py +++ b/test/terra/noise/test_quantum_error.py @@ -194,12 +194,6 @@ def test_tensor_both_kraus(self): target = SuperOp(Kraus([A0, A1]).tensor(Kraus([B0, B1]))) error = QuantumError([A0, A1]).tensor(QuantumError([B0, B1])) kraus, p = error.error_term(0) - targets = [ - np.kron(B0, A0), - np.kron(B0, A1), - np.kron(B1, A0), - np.kron(B1, A1) - ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0, 1]) @@ -279,15 +273,9 @@ def test_tensor_both_unitary_standard_gates(self): ] # Target circuits target_circs = [[{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'x', 'qubits': [1] }], [{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'y', 'qubits': [1] }], [{ @@ -328,12 +316,6 @@ def test_expand_both_kraus(self): target = SuperOp(Kraus([A0, A1]).expand(Kraus([B0, B1]))) error = QuantumError([A0, A1]).expand(QuantumError([B0, B1])) kraus, p = error.error_term(0) - targets = [ - np.kron(B0, A0), - np.kron(B1, A0), - np.kron(B0, A1), - np.kron(B1, A1) - ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0, 1]) @@ -413,15 +395,9 @@ def test_expand_both_unitary_standard_gates(self): ] # Target circuits target_circs = [[{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'x', 'qubits': [1] }], [{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'y', 'qubits': [1] }], [{ @@ -470,12 +446,6 @@ def test_compose_both_kraus(self): target = SuperOp(Kraus([A0, A1]).compose(Kraus([B0, B1]))) error = QuantumError([A0, A1]).compose(QuantumError([B0, B1])) kraus, p = error.error_term(0) - targets = [ - np.dot(B0, A0), - np.dot(B0, A1), - np.dot(B1, A0), - np.dot(B1, A1) - ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0]) @@ -555,15 +525,9 @@ def test_compose_both_qobj(self): ] # Target circuits target_circs = [[{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'x', 'qubits': [0] }], [{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'y', 'qubits': [0] }], [{ @@ -600,15 +564,10 @@ def test_compose_front_both_kraus(self): A1 = np.array([[0, 0], [0, np.sqrt(0.3)]], dtype=complex) B0 = np.array([[1, 0], [0, np.sqrt(1 - 0.5)]], dtype=complex) B1 = np.array([[0, 0], [0, np.sqrt(0.5)]], dtype=complex) - error = QuantumError([B0, B1]).compose( - QuantumError([A0, A1]), front=True) + # Use quantum channels for reference + target = SuperOp(Kraus([A0, A1]).compose(Kraus([B0, B1]), front=True)) + error = QuantumError([A0, A1]).compose(QuantumError([B0, B1]), front=True) kraus, p = error.error_term(0) - targets = [ - np.dot(B0, A0), - np.dot(B0, A1), - np.dot(B1, A0), - np.dot(B1, A1) - ] self.assertEqual(p, 1) self.assertEqual(kraus[0]['name'], 'kraus') self.assertEqual(kraus[0]['qubits'], [0]) @@ -688,15 +647,9 @@ def test_compose_front_both_qobj(self): ] # Target circuits target_circs = [[{ - 'name': 'id', - 'qubits': [0] - }, { 'name': 'x', 'qubits': [0] - }], [{ - 'name': 'id', - 'qubits': [0] - }, { + }], [ { 'name': 'y', 'qubits': [0] }], [{ @@ -759,4 +712,4 @@ def test_to_channel_circuit(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file From e7f3fa8d4a38f2da95169adb6d2312d22496e608 Mon Sep 17 00:00:00 2001 From: Gadi Date: Mon, 22 Apr 2019 12:52:02 +0300 Subject: [PATCH 27/28] Bugfix --- qiskit/providers/aer/noise/utils/noise_transformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/providers/aer/noise/utils/noise_transformation.py b/qiskit/providers/aer/noise/utils/noise_transformation.py index 766a9f5a16..2d81f3cafe 100644 --- a/qiskit/providers/aer/noise/utils/noise_transformation.py +++ b/qiskit/providers/aer/noise/utils/noise_transformation.py @@ -203,11 +203,11 @@ def operator_circuit(self, operator): The operator, converted to noise circuit representation """ if isinstance(operator, numpy.ndarray): - return [{'name': 'unitary', 'qubits': [0], 'params': operator}] + return [{'name': 'unitary', 'qubits': [0], 'params': [operator]}] if isinstance(operator, list) and isinstance(operator[0], numpy.ndarray): if len(operator) == 1: - return [{'name': 'unitary', 'qubits': [0], 'params': operator[0]}] + return [{'name': 'unitary', 'qubits': [0], 'params': operator}] else: return [{'name': 'kraus', 'qubits': [0], 'params': operator}] From 37fe2c33f421abe1e84fb44444c8a27343a1d7f4 Mon Sep 17 00:00:00 2001 From: Gadi Aleksandrowicz Date: Mon, 22 Apr 2019 21:38:07 +0300 Subject: [PATCH 28/28] Font fix --- .../aer/noise/errors/quantum_error.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qiskit/providers/aer/noise/errors/quantum_error.py b/qiskit/providers/aer/noise/errors/quantum_error.py index e006cd2632..0c655f6cfd 100644 --- a/qiskit/providers/aer/noise/errors/quantum_error.py +++ b/qiskit/providers/aer/noise/errors/quantum_error.py @@ -274,7 +274,7 @@ def as_dict(self): return error def compose(self, other, front=False): - """Return the composition error channel self?other. + """Return the composition error channel self∘other. Args: other (QuantumError): a quantum error channel. @@ -361,13 +361,13 @@ def power(self, n): return ret def tensor(self, other): - """Return the tensor product quantum error channel self ? other. + """Return the tensor product quantum error channel self ⊗ other. Args: other (QuantumError): a quantum error channel. Returns: - QuantumError: the tensor product error channel self ? other. + QuantumError: the tensor product error channel self ⊗ other. Raises: NoiseError: if other cannot be converted to a QuantumError. @@ -375,13 +375,13 @@ def tensor(self, other): return self._tensor_product(other, reverse=False) def expand(self, other): - """Return the tensor product quantum error channel self ? other. + """Return the tensor product quantum error channel self ⊗ other. Args: other (QuantumError): a quantum error channel. Returns: - QuantumError: the tensor product error channel other ? self. + QuantumError: the tensor product error channel other ⊗ self. Raises: NoiseError: if other cannot be converted to a QuantumError. @@ -389,7 +389,7 @@ def expand(self, other): return self._tensor_product(other, reverse=True) def kron(self, other): - """Return the tensor product quantum error channel self ? other. + """Return the tensor product quantum error channel self ⊗ other. DEPRECIATED: use QuantumError.tensor instead. @@ -397,7 +397,7 @@ def kron(self, other): other (QuantumError): a quantum error or channel. Returns: - QuantumError: the tensor product channel self ? other. + QuantumError: the tensor product channel self ⊗ other. """ warnings.warn( 'The kron() method is deprecated and will be removed ' @@ -410,8 +410,8 @@ def _tensor_product(self, other, reverse=False): Args: other (QuantumError): a quantum channel subclass - reverse (bool): If False return self ? other, if True return - if True return (other ? self) [Default: False + reverse (bool): If False return self ⊗ other, if True return + if True return (other ⊗ self) [Default: False Returns: QuantumError: the tensor product error channel. @@ -575,4 +575,4 @@ def _compose_instr(instr0, instr1): else: name = 'unitary' params = [op0.compose(op1).data] - return {'name': name, 'qubits': qubits0, 'params': params} \ No newline at end of file + return {'name': name, 'qubits': qubits0, 'params': params}