Repository: rigetticomputing/pyquil
Pull request #51 (2017-06-14 21:44:14)

Feature/invert
Added a method to the Program object that returns a new Program which is the dagger of the original.  Only works if original program is a valid set of Protoquil (no measurements or control flow).


Patch:
diff --git a/pyquil/quil.py b/pyquil/quil.py
index b7b5677..b2bb5bb 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -27,7 +27,8 @@ from .quilbase import (InstructionGroup,
                        Measurement,
                        merge_resource_managers)
 
-from .gates import MEASURE
+from .gates import MEASURE, STANDARD_GATES
+from math import pi
 
 
 class Program(InstructionGroup):
@@ -164,6 +165,51 @@ class Program(InstructionGroup):
         branch.Else.inst(else_program)
         return self.inst(branch)
 
+    def dagger(self, inv_dict=None, suffix="-INV"):
+        """
+        Creates the conjugate transpose of the Quil program. The program must not
+        contain any irreversible actions (measurement, control flow, qubit allocation).
+
+        :return: The Quil program's inverse
+        :rtype: Program
+
+        """
+
+        for action in self.actions:
+            assert action[0] == 0, "Program must be valid Protoquil"
+            gate = action[1]
+            assert not isinstance(gate, Measurement), "Program cannot contain measurements"
+            assert not isinstance(gate, While) and not isinstance(gate, If), \
+                "Program cannot contain control flow"
+
+        daggered = Program()
+
+        for gate in self.defined_gates:
+            if inv_dict == None or gate.name not in inv_dict:
+                daggered.defgate(gate.name + suffix, gate.matrix.T.conj())
+
+        for action in self.actions[::-1]:
+            gate = action[1]
+            if gate.operator_name in STANDARD_GATES:
+                if gate.operator_name == "S":
+                    daggered.inst(STANDARD_GATES["PHASE"](-pi / 2, *gate.arguments))
+                elif gate.operator_name == "T":
+                    daggered.inst(STANDARD_GATES["RZ"](pi / 4, *gate.arguments))
+                elif gate.operator_name == "ISWAP":
+                    daggered.inst(STANDARD_GATES["PSWAP"](pi / 2, *gate.arguments))
+                else:
+                    negated_params = list(map(lambda x : -1*x, gate.parameters))
+                    daggered.inst(STANDARD_GATES[gate.operator_name](*(negated_params+gate.arguments)))
+            else:
+                if inv_dict == None or gate.operator_name not in inv_dict:
+                    gate_inv_name = gate.operator_name + suffix
+                else:
+                    gate_inv_name = inv_dict[gate.operator_name]
+
+                daggered.inst(tuple([gate_inv_name] + gate.arguments))
+
+        return daggered
+
     def out(self):
         """
         Converts the Quil program to a readable string.
@@ -178,7 +224,6 @@ class Program(InstructionGroup):
         s += super(Program, self).out()
         return s
 
-
 def merge_programs(prog_list):
     """
     Merges a list of pyQuil programs into a single one by appending them in sequence


Test patch:
diff --git a/pyquil/tests/test_quil.py b/pyquil/tests/test_quil.py
index 9bf145e..31ed734 100644
--- a/pyquil/tests/test_quil.py
+++ b/pyquil/tests/test_quil.py
@@ -209,6 +209,49 @@ def test_measure_all():
                       'MEASURE 1 [1]\n' \
                       'MEASURE 2 [3]\n'
 
+def test_dagger():
+    # these gates are their own inverses
+    p = Program().inst(I(0), X(0), Y(0), Z(0),
+                       H(0), CNOT(0,1), CCNOT(0,1,2),
+                       SWAP(0,1), CSWAP(0,1,2))
+    assert p.dagger().out() == 'CSWAP 0 1 2\nSWAP 0 1\n' \
+                      'CCNOT 0 1 2\nCNOT 0 1\nH 0\n' \
+                      'Z 0\nY 0\nX 0\nI 0\n'
+
+    # these gates require negating a parameter
+    p = Program().inst(PHASE(pi, 0), RX(pi, 0), RY(pi, 0),
+                       RZ(pi, 0), CPHASE(pi, 0, 1),
+                       CPHASE00(pi, 0, 1), CPHASE01(pi, 0, 1),
+                       CPHASE10(pi, 0, 1), PSWAP(pi, 0, 1))
+    assert p.dagger().out() == 'PSWAP(-3.141592653589793) 0 1\n' \
+                               'CPHASE10(-3.141592653589793) 0 1\n' \
+                               'CPHASE01(-3.141592653589793) 0 1\n' \
+                               'CPHASE00(-3.141592653589793) 0 1\n' \
+                               'CPHASE(-3.141592653589793) 0 1\n' \
+                               'RZ(-3.141592653589793) 0\n' \
+                               'RY(-3.141592653589793) 0\n' \
+                               'RX(-3.141592653589793) 0\n' \
+                               'PHASE(-3.141592653589793) 0\n'
+
+    # these gates are special cases
+    p = Program().inst(S(0), T(0), ISWAP(0, 1))
+    assert p.dagger().out() == 'PSWAP(1.5707963267948966) 0 1\n' \
+                               'RZ(0.7853981633974483) 0\n' \
+                               'PHASE(-1.5707963267948966) 0\n'
+
+    # must invert defined gates
+    G = np.array([[0, 1], [0+1j, 0]])
+    p = Program().defgate("G", G).inst(("G", 0))
+    assert p.dagger().out() == 'DEFGATE G-INV:\n' \
+                               '    0.0+-0.0i, 0.0-1.0i\n' \
+                               '    1.0+-0.0i, 0.0+-0.0i\n\n' \
+                               'G-INV 0\n'
+
+    # can also pass in a list of inverses
+    inv_dict = {"G":"J"}
+    p = Program().defgate("G", G).inst(("G", 0))
+    assert p.dagger(inv_dict=inv_dict).out() == 'J 0\n'
+
 
 def test_construction_syntax():
     p = Program().inst(X(0), Y(1), Z(0)).measure(0, 1)