Repository: rigetticomputing/pyquil
Pull request #524 (2018-08-06 20:41:50)

Added general pauli kraus model and its insertion into a list of programs.
This allows easy addition of the depolarizing channel to a program or list of programs, which is useful, for example, in testing correctness of RB code. 

merge_programs was modified so that merging programs with the same repeated definition of the same noisy gate does not throw a warning; however, the associated ADD-KRAUS pragmas are still redundantly added. This change is not required for the motivating functional changes but rather provides a more general alternative to merge_with_pauli_noise; this latter, more specific method was added to circumvent the problem of redundant ADD-KRAUS pragmas. 

These changes are meant to fix issue #493 @ampolloreno 

kylegulshen: Is it a problem that merge_with_pauli_noise could be called with programs that include measurements? I'm not sure what happens when a gate is inserted after a measurement, or if we should add safeguards against this.
ampolloreno: @kylegulshen I don't think we currently support this (gates after measurement), but I think it would be fine to leave a note in the docstring, without guarding against it.
ampolloreno: @ntezak @stevenheidel @mpharrigan I need someone else to approve to merge.

Patch:
diff --git a/docs/source/qpu.rst b/docs/source/qpu.rst
index c3642be..d3cd26d 100644
--- a/docs/source/qpu.rst
+++ b/docs/source/qpu.rst
@@ -266,6 +266,13 @@ new jobs (but pending jobs will remain queued).  When the QPU resumes activity,
 characteristics may be slightly different (in that different gates may enjoy different process
 fidelities).
 
+Agave QPU
+~~~~~~~~~
+
+8Q-AGAVE UPDATE
+---------------
+After a full thermal cycle, we have temporarily disabled qubit pairs 3_4, 4_5, 5_6, and 6_7 due to performance degradation.
+
 Agave QPU Properties
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -335,14 +342,14 @@ assignment fidelity is given by expression :math:`\mathsf{F}_\textrm{RO} = [p(0|
 
    ,:math:`\omega^{\textrm{max}}_{\textrm{r}}/2\pi`,:math:`\omega^{\textrm{max}}_{01}/2\pi`,:math:`T_1`,:math:`T^*_2`,:math:`\mathsf{F}_{\textrm{1q}}`,:math:`\mathsf{F}_{\textrm{RO}}`
    ,:math:`\textrm{MHz}`,:math:`\textrm{MHz}`,:math:`\mu\textrm{s}`,:math:`\mu\textrm{s}`,,
-   0 ,5863,4586,10.72,10.6,0.957,0.784
-   1 ,5293,3909,10.04,9.2,0.951,0.910
-   2 ,5713,4524,15.52,12.5,0.982,0.943
-   3 ,5411,4054,14.17,18.5,0.970,0.912
-   4 ,5620,4660,14.58,26.2,0.969,0.678
-   5 ,5171,4081,14.86,12.8,0.962,0.832
-   6 ,5751,4760,14.17,12.9,0.969,0.753
-   7 ,5454,4110,13.19,17.7,0.932,0.895
+   0 ,5863,4586,10.7,10.6,0.9740,0.7841
+   1 ,5293,3909,10.0,9.2,0.9764,0.8830
+   2 ,5713,4524,15.5,12.5,0.9820,0.9296
+   3 ,5411,4054,14.2,18.5,0.9676,0.9054
+   4
+   5
+   6 ,5751,4760,14.2,12.9,0.9715,0.9156
+   7
 
 
 Qubit-Qubit Coupling
@@ -396,14 +403,14 @@ tomography under complete positivity (cp) and trace preservation (tp) constraint
 
    ,:math:`f_\textrm{m}`,:math:`t_\textrm{CZ}`,:math:`\mathsf{F}^\textrm{cptp}_{\textrm{PT}}`
    ,:math:`\textrm{MHz}`,ns
-   0 - 1, 226,195,0.92
-   1 - 2, 153,198,0.91
-   2 - 3, 138,132,0.82
-   3 - 4, 163,160,0.87
-   4 - 5, 168,163,0.67
-   5 - 6, 107,186,0.93
-   6 - 7, 123,162,0.93
-   7 - 0, 298,118,0.91
+   0 - 1, 226,195,0.859
+   1 - 2, 153,198,0.8077
+   2 - 3, 138,132,0.9054
+   3 - 4, CURRENTLY UNAVAILABLE
+   4 - 5, CURRENTLY UNAVAILABLE
+   5 - 6, CURRENTLY UNAVAILABLE
+   6 - 7, CURRENTLY UNAVAILABLE
+   7 - 0, CURRENTLY UNAVAILABLE
 
 ..  [Nielsen2002] Nielsen, M. A. (2002) ‘A simple formula for the average gate fidelity of a quantum dynamical operation’, http://arxiv.org/abs/quant-ph/0205035
 ..  [Reagor2018] Reagor, M. et al. (2018) ‘Demonstration of universal parametric entangling gates on a multi-qubit lattice’, http://advances.sciencemag.org/lookup/doi/10.1126/sciadv.aao3603
diff --git a/pyquil/noise.py b/pyquil/noise.py
index 53a3685..81cec92 100644
--- a/pyquil/noise.py
+++ b/pyquil/noise.py
@@ -228,6 +228,40 @@ def append_kraus_to_gate(kraus_ops, gate_matrix):
     return [kj.dot(gate_matrix) for kj in kraus_ops]
 
 
+def pauli_kraus_map(probabilities):
+    """
+    Generate the Kraus operators corresponding to a pauli channel.
+
+    :params list|floats probabilities: The 4^num_qubits list of probabilities specifying the desired pauli channel.
+    There should be either 4 or 16 probabilities specified in the order I, X, Y, Z for 1 qubit
+    or II, IX, IY, IZ, XI, XX, XY, etc for 2 qubits.
+
+            For example::
+
+                The d-dimensional depolarazing channel \Delta parameterized as
+                \Delta(\rho) = p \rho + [(1-p)/d] I
+                is specfiied by the list of probabilities
+                [p + (1-p)/d, (1-p)/d,  (1-p)/d), ... , (1-p)/d)]
+
+    :return: A list of the 4^num_qubits Kraus operators that parametrize the map.
+    :rtype: list
+    """
+    if len(probabilities) not in [4, 16]:
+        raise ValueError("Currently we only support one or two qubits, "
+                         "so the provided list of probabilities must have length 4 or 16.")
+    if not np.allclose(sum(probabilities), 1.0, atol=1e-3):
+        raise ValueError("Probabilities must sum to one.")
+
+    paulis = [np.eye(2), np.array([[0, 1], [1, 0]]), np.array([[0, -1j], [1j, 0]]), np.array([[1, 0], [0, -1]])]
+
+    if len(probabilities) == 4:
+        operators = paulis
+    else:
+        operators = np.kron(paulis, paulis)
+
+    return [coeff * op for coeff, op in zip(np.sqrt(probabilities), operators)]
+
+
 def damping_kraus_map(p=0.10):
     """
     Generate the Kraus operators corresponding to an amplitude damping
diff --git a/pyquil/quil.py b/pyquil/quil.py
index 6a895c1..80f32f4 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -18,6 +18,7 @@ Module for creating and defining Quil programs.
 """
 import itertools
 import types
+from typing import Iterable, List
 import warnings
 from collections import OrderedDict
 from math import pi
@@ -26,7 +27,7 @@ import numpy as np
 from six import string_types
 
 from pyquil._parser.PyQuilListener import run_parser
-from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas
+from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas, pauli_kraus_map
 from pyquil.parameters import format_parameter
 from pyquil.quilatom import LabelPlaceholder, QubitPlaceholder, unpack_qubit
 from pyquil.gates import MEASURE, QUANTUM_GATES, H
@@ -719,15 +720,68 @@ def instantiate_labels(instructions):
     return result
 
 
+def merge_with_pauli_noise(prog_list: Iterable, probabilities: List, qubits: List):
+    """
+    Insert pauli noise channels between each item in the list of programs.
+    This noise channel is implemented as a single noisy identity gate acting on the provided qubits.
+    This method does not rely on merge_programs and so avoids the inclusion of redundant Kraus Pragmas
+    that would occur if merge_programs was called directly on programs with distinct noisy gate definitions.
+
+    :param prog_list: an iterable such as a program or a list of programs.
+        If a program is provided, a single noise gate will be applied after each gate in the program.
+        If a list of programs is provided, the noise gate will be applied after each program.
+    :param probabilities: The 4^num_qubits list of probabilities specifying the desired pauli channel.
+        There should be either 4 or 16 probabilities specified in the order
+        I, X, Y, Z or II, IX, IY, IZ, XI, XX, XY, etc respectively.
+    :param qubits: a list of the qubits that the noisy gate should act on.
+    :return: A single program with noisy gates inserted between each element of the program list.
+    :rtype: Program
+    """
+    p = Program()
+    p.defgate("pauli_noise", np.eye(2 ** len(qubits)))
+    p.define_noisy_gate("pauli_noise", qubits, pauli_kraus_map(probabilities))
+    for elem in prog_list:
+        p.inst(Program(elem))
+        if isinstance(elem, Measurement):
+            continue  # do not apply noise after measurement
+        p.inst(("pauli_noise", *qubits))
+    return p
+
+
 def merge_programs(prog_list):
     """
-    Merges a list of pyQuil programs into a single one by appending them in sequence
+    Merges a list of pyQuil programs into a single one by appending them in sequence.
+    If multiple programs in the list contain the same gate and/or noisy gate definition
+    with identical name, this definition will only be applied once. If different definitions
+    with the same name appear multiple times in the program list, each will be applied once
+    in the order of last occurrence.
 
     :param list prog_list: A list of pyquil programs
     :return: a single pyQuil program
     :rtype: Program
     """
-    return sum(prog_list, Program())
+    definitions = [gate for prog in prog_list for gate in Program(prog).defined_gates]
+    seen = {}
+    # Collect definitions in reverse order and reapply definitions in reverse
+    # collected order to ensure that the last occurrence of a definition is applied last.
+    for definition in reversed(definitions):
+        name = definition.name
+        if name in seen.keys():
+            # Do not add truly identical definitions with the same name
+            # If two different definitions share a name, we include each definition so as to provide
+            # a waring to the user when the contradictory defgate is called.
+            if definition not in seen[name]:
+                seen[name].append(definition)
+        else:
+            seen[name] = [definition]
+    new_definitions = [gate for key in seen.keys() for gate in reversed(seen[key])]
+
+    p = sum([Program(prog).instructions for prog in prog_list], Program())  # Combine programs without gate definitions
+
+    for definition in new_definitions:
+        p.defgate(definition.name, definition.matrix, definition.parameters)
+
+    return p
 
 
 def get_classical_addresses_from_program(program):


Test patch:
diff --git a/pyquil/tests/test_noise.py b/pyquil/tests/test_noise.py
index bb7458c..69cd608 100644
--- a/pyquil/tests/test_noise.py
+++ b/pyquil/tests/test_noise.py
@@ -5,7 +5,7 @@ from unittest.mock import Mock
 
 from pyquil.api import QPUConnection
 from pyquil.gates import CZ, RZ, RX, I, H
-from pyquil.noise import (damping_kraus_map, dephasing_kraus_map, tensor_kraus_maps,
+from pyquil.noise import (pauli_kraus_map, damping_kraus_map, dephasing_kraus_map, tensor_kraus_maps,
                           _get_program_gates, _decoherence_noise_model,
                           add_decoherence_noise, combine_kraus_maps, damping_after_dephasing,
                           INFINITY, apply_noise_model, _noise_model_program_header, KrausModel,
@@ -16,6 +16,21 @@ from pyquil.quil import Pragma, Program
 from pyquil.quilbase import DefGate, Gate
 
 
+def test_pauli_kraus_map():
+    probabilities = [.1, .2, .3, .4]
+    k1, k2, k3, k4 = pauli_kraus_map(probabilities)
+    assert np.allclose(k1, np.sqrt(.1) * np.eye(2), atol=1 * 10 ** -8)
+    assert np.allclose(k2, np.sqrt(.2) * np.array([[0, 1.], [1., 0]]), atol=1 * 10 ** -8)
+    assert np.allclose(k3, np.sqrt(.3) * np.array([[0, -1.j], [1.j, 0]]), atol=1 * 10 ** -8)
+    assert np.allclose(k4, np.sqrt(.4) * np.array([[1, 0], [0, -1]]), atol=1 * 10 ** -8)
+
+    two_q_pauli_kmaps = pauli_kraus_map(np.kron(probabilities, list(reversed(probabilities))))
+    q1_pauli_kmaps = [k1, k2, k3, k4]
+    q2_pauli_kmaps = pauli_kraus_map(list(reversed(probabilities)))
+    tensor_kmaps = tensor_kraus_maps(q1_pauli_kmaps, q2_pauli_kmaps)
+    assert np.allclose(two_q_pauli_kmaps, tensor_kmaps)
+
+
 def test_damping_kraus_map():
     p = 0.05
     k1, k2 = damping_kraus_map(p=p)
diff --git a/pyquil/tests/test_quil.py b/pyquil/tests/test_quil.py
index e4c6f05..3ca75a6 100644
--- a/pyquil/tests/test_quil.py
+++ b/pyquil/tests/test_quil.py
@@ -25,7 +25,7 @@ from pyquil.gates import I, X, Y, Z, H, T, S, RX, RY, RZ, CNOT, CCNOT, PHASE, CP
     TRUE, FALSE, NOT, AND, OR, MOVE, EXCHANGE
 from pyquil.parameters import Parameter, quil_sin, quil_cos
 from pyquil.paulis import exponential_map, sZ
-from pyquil.quil import Program, merge_programs, address_qubits, \
+from pyquil.quil import Program, merge_programs, merge_with_pauli_noise, address_qubits, \
     get_classical_addresses_from_program, Pragma
 from pyquil.quilatom import QubitPlaceholder
 from pyquil.quilbase import DefGate, Gate, Addr, Qubit, JumpWhen
@@ -551,6 +551,38 @@ def test_prog_merge():
     prog_0 = Program(X(0))
     prog_1 = Program(Y(0))
     assert merge_programs([prog_0, prog_1]).out() == (prog_0 + prog_1).out()
+    prog_0.defgate("test", np.eye(2))
+    prog_0.inst(("test", 0))
+    prog_1.defgate("test", np.eye(2))
+    prog_1.inst(("test", 0))
+    assert merge_programs([prog_0, prog_1]).out() == """DEFGATE test:
+    1.0, 0
+    0, 1.0
+
+X 0
+test 0
+Y 0
+test 0
+"""
+
+
+def test_merge_with_pauli_noise():
+    p = Program(X(0)).inst(Z(0))
+    probs = [0., 1., 0., 0.]
+    merged = merge_with_pauli_noise(p, probs, [0])
+    assert merged.out() == """DEFGATE pauli_noise:
+    1.0, 0
+    0, 1.0
+
+PRAGMA ADD-KRAUS pauli_noise 0 "(0.0 0.0 0.0 0.0)"
+PRAGMA ADD-KRAUS pauli_noise 0 "(0.0 1.0 1.0 0.0)"
+PRAGMA ADD-KRAUS pauli_noise 0 "(0.0 0.0 0.0 0.0)"
+PRAGMA ADD-KRAUS pauli_noise 0 "(0.0 0.0 0.0 -0.0)"
+X 0
+pauli_noise 0
+Z 0
+pauli_noise 0
+"""
 
 
 def test_get_qubits():