Repository: rigetticomputing/pyquil
Pull request #172 (2017-11-03 02:00:43)

Added helper function to define noisy gates as Kraus maps
This implements the new `PRAGMA ADD-KRAUS <GATENAME> <QUBITS+> "(...)"` statements for noisy qvm simulations.
@stevenheidel maybe take a quick look, but even if it looks good let's discuss before merging.

stevenheidel: I think the main thing I want to understand is whether or not we can just add the pragmas inline as soon as they're defined instead of waiting until a measure operation. Keeping track of them for the lifetime of the program could cause the same problem we have with resource managers and defined gates; there's a number of edge cases affecting combining programs, if_then, etc
ntezak: That's a good point, my primary concern with that would be having multiple Kraus op definitions for the exact same gate (and qubits). But we could add a check for that at the end. It seems like gate definitions are also treated somewhat separately and I would argue these pragmas are similar. There is another pragma that *must* be inline, however, and that is the "PRAGMA NO-NOISE" instruction that only affects the very next QUIL instruction.
stevenheidel: I'd rather have the validation logic in the QVM. PyQuil's main goal is simply the construction of a valid quil program, not necessarily a valid quil program that actually runs.
ntezak: Ok, sounds good. Should I make a general class for PRAGMAs off of
AbstractInstruction and a subclass for ADD-KRAUS?

On Fri, Nov 3, 2017 at 11:27 AM Steven Heidel <notifications@github.com>
wrote:

> I'd rather have the validation logic in the QVM. PyQuil's main goal is
> simply the construction of a valid quil program, not necessarily a valid
> quil program that actually runs.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/rigetticomputing/pyquil/pull/172#issuecomment-341788745>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABgESTHIRL8qX8cr2gV0D7bkQTC9CSqIks5sy1r4gaJpZM4QQkvo>
> .
>

stevenheidel: Can you just keep the method for add-kraus, construct and validate the pragma in there, and then add it to the program using p.inst? No need for further subclassing I think
ntezak: Ok, what type should the object be that is passed to p.inst?


On Fri, Nov 3, 2017 at 2:13 PM Steven Heidel <notifications@github.com>
wrote:

> Can you just keep the method for add-kraus, construct and validate the
> pragma in there, and then add it to the program using p.inst? No need for
> further subclassing I think
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/rigetticomputing/pyquil/pull/172#issuecomment-341828747>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABgESfLqN2nbPSBq_zrOGWqozJext3I9ks5sy4IHgaJpZM4QQkvo>
> .
>

stevenheidel: Oh interesting, there's no typed Pragma right now. Add a class for Pragma that subclasses QuilAtom I think, and then just construct your kraus pragma.
ntezak: +1

On Fri, Nov 3, 2017 at 2:59 PM Steven Heidel <notifications@github.com>
wrote:

> Oh interesting, there's no typed Pragma right now. Add a class for Pragma
> that subclasses QuilAtom I think, and then just construct your kraus pragma.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/rigetticomputing/pyquil/pull/172#issuecomment-341837719>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABgESYC8wirC61zcqZt1F9nectyhfhH6ks5sy4zNgaJpZM4QQkvo>
> .
>

ntezak: @stevenheidel I think this is ready for your (re-)review

Patch:
diff --git a/pyquil/kraus.py b/pyquil/kraus.py
new file mode 100644
index 0000000..f849fbf
--- /dev/null
+++ b/pyquil/kraus.py
@@ -0,0 +1,58 @@
+##############################################################################
+# Copyright 2016-2017 Rigetti Computing
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+##############################################################################
+"""
+Module for creating and verifying noisy gate definitions in terms of Kraus maps.
+"""
+import numpy as np
+
+from pyquil.quilbase import format_parameter, Pragma
+
+
+def _check_kraus_ops(n, kraus_ops):
+    """
+    Verify that the Kraus operators are of the correct shape and satisfy the correct normalization.
+
+    :param int n: Number of qubits
+    :param list|tuple kraus_ops: The Kraus operators as numpy.ndarrays.
+    """
+    for k in kraus_ops:
+        if not np.shape(k) == (2 ** n, 2 ** n):
+            raise ValueError(
+                "Kraus operators for {0} qubits must have shape {1}x{1}: {2}".format(n, 2 ** n, k))
+
+    kdk_sum = sum(np.transpose(k).conjugate().dot(k) for k in kraus_ops)
+    if not np.allclose(kdk_sum, np.eye(2**n), atol=1e-5):
+        raise ValueError(
+            "Kraus operator not correctly normalized: sum_j K_j^*K_j == {}".format(kdk_sum))
+
+
+def _create_kraus_pragmas(name, qubit_indices, kraus_ops):
+    """
+    Generate the pragmas to define a Kraus map for a specific gate on some qubits.
+
+    :param str name: The name of the gate.
+    :param list|tuple qubit_indices: The qubits
+    :param list|tuple kraus_ops: The Kraus operators as matrices.
+    :return: A QUIL string with PRAGMA ADD-KRAUS ... statements.
+    :rtype: str
+    """
+
+    prefix = "PRAGMA ADD-KRAUS {} {}".format(name, " ".join(map(str, qubit_indices)))
+    pragmas = [Pragma("ADD-KRAUS",
+                      qubit_indices,
+                      "({})".format(" ".join(map(format_parameter, np.ravel(k)))))
+               for k in kraus_ops]
+    return pragmas
diff --git a/pyquil/quil.py b/pyquil/quil.py
index c1e42d6..ce0e606 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -16,8 +16,12 @@
 """
 Module for creating and defining Quil programs.
 """
+from math import pi
+
 from six import integer_types
 
+from pyquil.kraus import _check_kraus_ops, _create_kraus_pragmas
+from .gates import MEASURE, STANDARD_GATES
 from .quilbase import (InstructionGroup,
                        Instr,
                        Addr,
@@ -26,12 +30,9 @@ from .quilbase import (InstructionGroup,
                        DefGate,
                        Gate,
                        Measurement,
-                       issubinstance,
                        AbstractQubit,
-                       merge_resource_managers)
-
-from .gates import MEASURE, STANDARD_GATES
-from math import pi
+                       merge_resource_managers, Pragma)
+import numpy as np
 
 
 class Program(InstructionGroup):
@@ -130,6 +131,31 @@ class Program(InstructionGroup):
         self.defined_gates.append(DefGate(name, matrix))
         return self
 
+    def define_noisy_gate(self, name, qubit_indices, kraus_ops):
+        """
+        Overload a static ideal gate with a noisy one defined in terms of a Kraus map.
+
+        :param str name: The name of the gate.
+        :param tuple|list qubit_indices: The qubits it acts on.
+        :param tuple|list kraus_ops: The Kraus operators.
+        :return: The Program instance
+        :rtype: Program
+        """
+        kraus_ops = [np.asarray(k, dtype=np.complex128) for k in kraus_ops]
+        _check_kraus_ops(len(qubit_indices), kraus_ops)
+        self.inst(_create_kraus_pragmas(name, tuple(qubit_indices), kraus_ops))
+        return self
+
+    def no_noise(self):
+        """
+        Prevent a noisy gate definition from being applied to the immediately following Gate
+        instruction.
+
+        :return: Program
+        """
+        self.inst(Pragma("NO-NOISE"))
+        return self
+
     def measure(self, qubit_index, classical_reg):
         """
         Measures a qubit at qubit_index and puts the result in classical_reg
diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py
index ff40289..fd95543 100644
--- a/pyquil/quilbase.py
+++ b/pyquil/quilbase.py
@@ -591,6 +591,44 @@ class RawInstr(AbstractInstruction):
         return self.instr
 
 
+class Pragma(AbstractInstruction):
+    """
+    A PRAGMA instruction.
+
+    This is printed in QUIL as::
+
+        PRAGMA <command> <arg1> <arg2> ... <argn> "<freeform_string>"
+
+    """
+
+    def __init__(self, command, args=(), freeform_string=""):
+        if not isinstance(command, str):
+            raise TypeError("Pragma's require an identifier.")
+        for a in args:
+            if not isinstance(a, (str, int)):
+                raise TypeError("Pragma arguments must be strings or integers: {}".format(a))
+        if not isinstance(freeform_string, str):
+            raise TypeError("The freeform string argument must be a string: {}".format(
+                freeform_string))
+        self.command = command
+        self.args = args
+        self.freeform_string = freeform_string
+
+    def out(self):
+        ret = "PRAGMA {}".format(self.command)
+        if self.args:
+            ret += " {}".format(" ".join(str(a) for a in self.args))
+        if self.freeform_string:
+            ret += " \"{}\"".format(self.freeform_string)
+        return ret
+
+    def __repr__(self):
+        return '<PRAGMA {}>'.format(self.command)
+
+    def __str__(self):
+        return self.out
+
+
 class Instr(AbstractInstruction):
     """
     Representation of an instruction represented by an operator, parameters, and arguments.


Test patch:
diff --git a/pyquil/tests/test_quil.py b/pyquil/tests/test_quil.py
index e4dfefb..5d9db22 100644
--- a/pyquil/tests/test_quil.py
+++ b/pyquil/tests/test_quil.py
@@ -500,3 +500,71 @@ def test_eq():
 
     assert p1 == p2
     assert not p1 != p2
+
+
+def test_kraus():
+    pq = Program(X(0))
+    pq.define_noisy_gate("X", (0,), [
+        [[0., 1.],
+         [1., 0.]],
+        [[0., 0.],
+         [0., 0.]]
+    ])
+    pq.inst(X(1))
+    pq.define_noisy_gate("X", (1,), [
+        [[0., 1.],
+         [1., 0.]],
+        [[0., 0.],
+         [0., 0.]]
+    ])
+
+    ret = pq.out()
+    assert ret == """X 0
+PRAGMA ADD-KRAUS 0 "(0.0+0.0i 1.0+0.0i 1.0+0.0i 0.0+0.0i)"
+PRAGMA ADD-KRAUS 0 "(0.0+0.0i 0.0+0.0i 0.0+0.0i 0.0+0.0i)"
+X 1
+PRAGMA ADD-KRAUS 1 "(0.0+0.0i 1.0+0.0i 1.0+0.0i 0.0+0.0i)"
+PRAGMA ADD-KRAUS 1 "(0.0+0.0i 0.0+0.0i 0.0+0.0i 0.0+0.0i)"
+"""
+    # test error due to bad normalization
+    with pytest.raises(ValueError):
+        pq.define_noisy_gate("X", (0,), [
+            [[0., 1.],
+             [1., 0.]],
+            [[0., 1.],
+             [1., 0.]]
+        ])
+    # test error due to bad shape of kraus op
+    with pytest.raises(ValueError):
+        pq.define_noisy_gate("X", (0,), [
+            [[0., 1., 0.],
+             [1., 0., 0.]],
+            [[0., 1.],
+             [1., 0.]]
+        ])
+
+    pq1 = Program(X(0))
+    pq1.define_noisy_gate("X", (0,), [
+        [[0., 1.],
+         [1., 0.]],
+        [[0., 0.],
+         [0., 0.]]
+    ])
+    pq2 = Program(X(1))
+    pq2.define_noisy_gate("X", (1,), [
+        [[0., 1.],
+         [1., 0.]],
+        [[0., 0.],
+         [0., 0.]]
+    ])
+
+    assert pq1 + pq2 == pq
+
+    pq_nn = Program(X(0))
+    pq_nn.no_noise()
+    pq_nn.inst(X(1))
+
+    assert pq_nn.out() == """X 0
+PRAGMA NO-NOISE
+X 1
+"""