Repository: rigetticomputing/pyquil (python)

Issue:
Should Pragmas accept QubitPlaceholders?
I was experimenting with adding a fencing Pragma, and found it useful for pragmas to allow qubitplaceholders in their args. For most use cases this is probably moot, since probably 99% of pragmas written in pyquil end up being parsed by our compiler, but if people are writing their own backend for interpreting quil programs, having pragmas accept qubitplaceholders seems like a good idea. e.g. something like the following:

```
p = Program()
q = QubitPlacholder()
q2 = QubitPlacholder()
p.inst(X(q))
p.inst(Pragma("FENCE", [q, q2]))
p.inst(X(q2))
```
I found this useful in prototyping two qubit randomized benchmarking, where I might want better timing control over when gates and measurements happen, coordinated across two qubits.

Pull request description:
adding support for qubitplaceholders to pragmas
Fixes #439.

Patch:
diff --git a/pyquil/quil.py b/pyquil/quil.py
index ff26d21..91fda55 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -204,13 +204,13 @@ class Program(object):
         _check_kraus_ops(len(qubit_indices), kraus_ops)
         return self.inst(_create_kraus_pragmas(name, tuple(qubit_indices), kraus_ops))
 
-    def define_noisy_readout(self, qubit_index, p00, p11):
+    def define_noisy_readout(self, qubit, p00, p11):
         """
         For this program define a classical bit flip readout error channel parametrized by
         ``p00`` and ``p11``. This models the effect of thermal noise that corrupts the readout
         signal **after** it has interrogated the qubit.
 
-        :param int qubit_index: The qubit with noisy readout.
+        :param int|QubitPlaceholder qubit: The qubit with noisy readout.
         :param float p00: The probability of obtaining the measurement result 0 given that the qubit
           is in state 0.
         :param float p11: The probability of obtaining the measurement result 1 given that the qubit
@@ -222,15 +222,15 @@ class Program(object):
             raise ValueError("p00 must be in the interval [0,1].")
         if not 0. <= p11 <= 1.:
             raise ValueError("p11 must be in the interval [0,1].")
-        if not isinstance(qubit_index, int):
-            raise TypeError("qubit_index must be a non-negative integer.")
-        if not qubit_index >= 0:
-            raise ValueError("qubit_index must be a non-negative integer.")
+        if not (isinstance(qubit, int) or isinstance(qubit, QubitPlaceholder)):
+            raise TypeError("qubit must be a non-negative integer, or QubitPlaceholder.")
+        if isinstance(qubit, int) and qubit < 0:
+            raise ValueError("qubit cannot be negative.")
         p00 = float(p00)
         p11 = float(p11)
         aprobs = [p00, 1. - p11, 1. - p00, p11]
         aprobs_str = "({})".format(" ".join(format_parameter(p) for p in aprobs))
-        pragma = Pragma("READOUT-POVM", [qubit_index], aprobs_str)
+        pragma = Pragma("READOUT-POVM", [qubit], aprobs_str)
         return self.inst(pragma)
 
     def no_noise(self):
@@ -575,7 +575,14 @@ def _what_type_of_qubit_does_it_use(program):
                 has_real_qubits = True
             else:
                 raise ValueError("Unknown qubit type {}".format(instr.qubit))
-
+        elif isinstance(instr, Pragma):
+            for arg in instr.args:
+                if isinstance(arg, QubitPlaceholder):
+                    qubits[arg] = 1
+                    has_placeholders = True
+                elif isinstance(arg, Qubit):
+                    qubits[arg] = 1
+                    has_real_qubits = True
     if not (has_placeholders or has_real_qubits):
         warnings.warn("Your program doesn't use any qubits")
 
@@ -643,7 +650,16 @@ def address_qubits(program, qubit_mapping=None):
             result.append(Gate(instr.name, instr.params, remapped_qubits))
         elif isinstance(instr, Measurement):
             result.append(Measurement(qubit_mapping[instr.qubit], instr.classical_reg))
-
+        elif isinstance(instr, Pragma):
+            new_args = []
+            for arg in instr.args:
+                # Pragmas can have arguments that represent things besides qubits, so here we
+                # make sure to just look up the QubitPlaceholders.
+                if isinstance(arg, QubitPlaceholder):
+                    new_args.append(qubit_mapping[arg])
+                else:
+                    new_args.append(arg)
+            result.append(Pragma(instr.command, new_args, instr.freeform_string))
         # Otherwise simply add it to the result
         else:
             result.append(instr)
diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py
index fc9f4e5..3e9272e 100644
--- a/pyquil/quilbase.py
+++ b/pyquil/quilbase.py
@@ -429,9 +429,9 @@ class Pragma(AbstractInstruction):
         if not isinstance(args, (tuple, list)):
             raise TypeError("Pragma arguments must be a list: {}".format(args))
         for a in args:
-            if not (isinstance(a, string_types) or isinstance(a, integer_types)):
+            if not (isinstance(a, string_types) or isinstance(a, integer_types) or
+                    isinstance(a, QubitPlaceholder) or isinstance(a, Qubit)):
                 raise TypeError("Pragma arguments must be strings or integers: {}".format(a))
-
         if not isinstance(freeform_string, string_types):
             raise TypeError("The freeform string argument must be a string: {}".format(
                 freeform_string))


Test patch:
diff --git a/pyquil/tests/__init__.py b/pyquil/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyquil/tests/test_parser.py b/pyquil/tests/test_parser.py
index 66ad72d..0068c61 100644
--- a/pyquil/tests/test_parser.py
+++ b/pyquil/tests/test_parser.py
@@ -21,17 +21,18 @@ from pyquil.parameters import Parameter, quil_sin, quil_cos
 from pyquil.parser import parse
 from pyquil.quilatom import Addr
 from pyquil.quilbase import Label, JumpTarget, Jump, JumpWhen, JumpUnless, DefGate, Qubit, Pragma
+from pyquil.tests.utils import parse_equals
 
 
 def test_simple_gate():
-    _test("A 0", Gate("A", [], [Qubit(0)]))
-    _test("A 1 10 100", Gate("A", [], [Qubit(1), Qubit(10), Qubit(100)]))
+    parse_equals("A 0", Gate("A", [], [Qubit(0)]))
+    parse_equals("A 1 10 100", Gate("A", [], [Qubit(1), Qubit(10), Qubit(100)]))
 
 
 def test_standard_gates():
-    _test("H 0", H(0))
-    _test("CNOT 0 1", CNOT(0, 1))
-    _test("SWAP 0 1", SWAP(0, 1))
+    parse_equals("H 0", H(0))
+    parse_equals("CNOT 0 1", CNOT(0, 1))
+    parse_equals("SWAP 0 1", SWAP(0, 1))
 
 
 def test_def_gate():
@@ -49,7 +50,7 @@ DEFGATE HADAMARD:
     1/sqrt(2), -1/sqrt(2)
     """.strip()
 
-    _test(defgates, sqrt_x, hadamard)
+    parse_equals(defgates, sqrt_x, hadamard)
 
 
 def test_def_gate_with_variables():
@@ -63,20 +64,20 @@ def test_def_gate_with_variables():
               '    cos(%theta/2), i*sin(%theta/2)\n' \
               '    i*sin(%theta/2), cos(%theta/2)\n\n'
 
-    _test(defgate, DefGate('RX', rx, [theta]))
+    parse_equals(defgate, DefGate('RX', rx, [theta]))
 
 
 def test_parameters():
-    _test("RX(123) 0", RX(123)(0))
-    _test("CPHASE00(0) 0 1", CPHASE00(0)(0, 1))
-    _test("A(8,9) 0", Gate("A", [8, 9], [Qubit(0)]))
-    _test("A(8, 9) 0", Gate("A", [8, 9], [Qubit(0)]))
+    parse_equals("RX(123) 0", RX(123)(0))
+    parse_equals("CPHASE00(0) 0 1", CPHASE00(0)(0, 1))
+    parse_equals("A(8,9) 0", Gate("A", [8, 9], [Qubit(0)]))
+    parse_equals("A(8, 9) 0", Gate("A", [8, 9], [Qubit(0)]))
 
 
 def test_expressions():
     # Test expressions by wrapping them in an RX gate for convenience
     def _expr(expression, expected):
-        _test("RX(" + expression + ") 0", RX(expected)(0))
+        parse_equals("RX(" + expression + ") 0", RX(expected)(0))
 
     # Decimals
     _expr("+123", 123)
@@ -137,37 +138,37 @@ def test_expressions():
 
 
 def test_measure():
-    _test("MEASURE 0", MEASURE(0))
-    _test("MEASURE 0 [1]", MEASURE(0, 1))
+    parse_equals("MEASURE 0", MEASURE(0))
+    parse_equals("MEASURE 0 [1]", MEASURE(0, 1))
 
 
 def test_jumps():
-    _test("LABEL @test_1", JumpTarget(Label("test_1")))
-    _test("JUMP @test_1", Jump(Label("test_1")))
-    _test("JUMP-WHEN @test_1 [0]", JumpWhen(Label("test_1"), Addr(0)))
-    _test("JUMP-UNLESS @test_1 [1]", JumpUnless(Label("test_1"), Addr(1)))
+    parse_equals("LABEL @test_1", JumpTarget(Label("test_1")))
+    parse_equals("JUMP @test_1", Jump(Label("test_1")))
+    parse_equals("JUMP-WHEN @test_1 [0]", JumpWhen(Label("test_1"), Addr(0)))
+    parse_equals("JUMP-UNLESS @test_1 [1]", JumpUnless(Label("test_1"), Addr(1)))
 
 
 def test_others():
-    _test("RESET", RESET)
-    _test("WAIT", WAIT)
-    _test("NOP", NOP)
+    parse_equals("RESET", RESET)
+    parse_equals("WAIT", WAIT)
+    parse_equals("NOP", NOP)
 
 
 def test_classical():
-    _test("TRUE [0]", TRUE(0))
-    _test("FALSE [0]", FALSE(0))
-    _test("NOT [0]", NOT(0))
-    _test("AND [0] [1]", AND(0, 1))
-    _test("OR [0] [1]", OR(0, 1))
-    _test("MOVE [0] [1]", MOVE(0, 1))
-    _test("EXCHANGE [0] [1]", EXCHANGE(0, 1))
+    parse_equals("TRUE [0]", TRUE(0))
+    parse_equals("FALSE [0]", FALSE(0))
+    parse_equals("NOT [0]", NOT(0))
+    parse_equals("AND [0] [1]", AND(0, 1))
+    parse_equals("OR [0] [1]", OR(0, 1))
+    parse_equals("MOVE [0] [1]", MOVE(0, 1))
+    parse_equals("EXCHANGE [0] [1]", EXCHANGE(0, 1))
 
 
 def test_pragma():
-    _test('PRAGMA gate_time H "10 ns"', Pragma('gate_time', ['H'], '10 ns'))
-    _test('PRAGMA qubit 0', Pragma('qubit', [0]))
-    _test('PRAGMA NO-NOISE', Pragma('NO-NOISE'))
+    parse_equals('PRAGMA gate_time H "10 ns"', Pragma('gate_time', ['H'], '10 ns'))
+    parse_equals('PRAGMA qubit 0', Pragma('qubit', [0]))
+    parse_equals('PRAGMA NO-NOISE', Pragma('NO-NOISE'))
 
 
 def test_invalid():
@@ -176,8 +177,4 @@ def test_invalid():
 
 
 def test_empty_program():
-    _test("")
-
-
-def _test(quil_string, *instructions):
-    assert list(instructions) == parse(quil_string)
+    parse_equals("")
diff --git a/pyquil/tests/test_quil.py b/pyquil/tests/test_quil.py
index 1415cbf..3367058 100644
--- a/pyquil/tests/test_quil.py
+++ b/pyquil/tests/test_quil.py
@@ -26,9 +26,10 @@ from pyquil.gates import I, X, Y, Z, H, T, S, RX, RY, RZ, CNOT, CCNOT, PHASE, CP
 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, \
-    get_classical_addresses_from_program
+    get_classical_addresses_from_program, Pragma
 from pyquil.quilatom import QubitPlaceholder
 from pyquil.quilbase import DefGate, Gate, Addr, Qubit, JumpWhen
+from pyquil.tests.utils import parse_equals
 
 
 def test_gate():
@@ -779,3 +780,26 @@ def test_get_classical_addresses_from_program():
 
     p += [MEASURE(i, i) for i in [0, 3, 1]]
     assert get_classical_addresses_from_program(p) == [0, 1, 3]
+
+
+def test_pragma_with_placeholders():
+    q = QubitPlaceholder()
+    q2 = QubitPlaceholder()
+    p = Program()
+    p.inst(Pragma('FENCE', [q, q2]))
+    address_map = {q: 0, q2: 1}
+    addressed_pragma = address_qubits(p, address_map)[0]
+    parse_equals('PRAGMA FENCE 0 1\n', addressed_pragma)
+
+    pq = Program(X(q))
+    pq.define_noisy_readout(q, .8, .9)
+
+    pq.inst(X(q2))
+    pq.define_noisy_readout(q2, .9, .8)
+
+    ret = address_qubits(pq, address_map).out()
+    assert ret == """X 0
+PRAGMA READOUT-POVM 0 "(0.8 0.09999999999999998 0.19999999999999996 0.9)"
+X 1
+PRAGMA READOUT-POVM 1 "(0.9 0.19999999999999996 0.09999999999999998 0.8)"
+"""
diff --git a/pyquil/tests/utils.py b/pyquil/tests/utils.py
new file mode 100644
index 0000000..84c70ff
--- /dev/null
+++ b/pyquil/tests/utils.py
@@ -0,0 +1,5 @@
+from pyquil.parser import parse
+
+
+def parse_equals(quil_string, *instructions):
+    assert list(instructions) == parse(quil_string)