Repository: rigetticomputing/pyquil
Pull request #581 (2018-10-22 21:24:35)

Update protoquil definition / checking
Add updated `validate_protoquil` function, use it for `is_protoquil` method of quil program. add tests.


Patch:
diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py
index e37bb31..ec32b63 100644
--- a/pyquil/api/__init__.py
+++ b/pyquil/api/__init__.py
@@ -31,7 +31,7 @@ from pyquil.api._error_reporting import pyquil_protect
 from pyquil.api._job import Job
 from pyquil.api._qam import QAM
 from pyquil.api._qpu import get_devices, QPU
-from pyquil.api._quantum_computer import QuantumComputer, list_quantum_computers, get_qc
+from pyquil.api._quantum_computer import QuantumComputer, list_quantum_computers, get_qc, local_qvm
 from pyquil.api._qvm import QVMConnection, QVM
 from pyquil.api._wavefunction_simulator import WavefunctionSimulator
 from pyquil.device import Device
diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py
index 6cd9246..41b7097 100644
--- a/pyquil/api/_compiler.py
+++ b/pyquil/api/_compiler.py
@@ -83,8 +83,8 @@ def _collect_classical_memory_write_locations(program: Program) -> List[Optional
             ro_size = instr.memory_size
             break
 
-    measures_by_qubit = Counter()
-    ro_sources: Dict[int, int] = {}
+    measures_by_qubit: Dict[int, int] = Counter()
+    ro_sources: Dict[int, Tuple[int, int]] = {}
 
     for instr in program:
         if isinstance(instr, Measurement):
diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py
index d8956f9..314f306 100644
--- a/pyquil/api/_quantum_computer.py
+++ b/pyquil/api/_quantum_computer.py
@@ -16,7 +16,9 @@
 import re
 import warnings
 from math import pi
-from typing import List, Dict, Tuple
+from typing import List, Dict, Tuple, Iterator
+import subprocess
+from contextlib import contextmanager
 
 import networkx as nx
 import numpy as np
@@ -474,3 +476,48 @@ def get_qc(name: str, *, as_qvm: bool = None, noisy: bool = None,
                            compiler=_get_qvm_compiler_based_on_endpoint(
                                device=device,
                                endpoint=connection.compiler_endpoint))
+
+
+@contextmanager
+def local_qvm() -> Iterator[Tuple[subprocess.Popen, subprocess.Popen]]:
+    """A context manager for the Rigetti local QVM and QUIL compiler.
+
+    You must first have installed the `qvm` and `quilc` executables from
+    the forest SDK. [https://www.rigetti.com/forest]
+
+    This context manager will start up external processes for both the
+    compiler and virtual machine, and then terminate them when the context
+    is exited.
+
+    If `qvm` (or `quilc`) is already running, then the existing process will
+    be used, and will not terminated at exit.
+
+    >>> from pyquil import get_qc, Program
+    >>> from pyquil.gates import CNOT, Z
+    >>> from pyquil.api import local_qvm
+    >>>
+    >>> qvm = get_qc('9q-generic-qvm')
+    >>> prog = Program(Z(0), CNOT(0, 1))
+    >>>
+    >>> with local_qvm():
+    >>>     results = qvm.run_and_measure(prog, trials=10)
+
+    :raises: FileNotFoundError: If either executable is not installed.
+    """
+    # Enter. Acquire resource
+    qvm = subprocess.Popen(['qvm', '-S'],
+                           stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+
+    quilc = subprocess.Popen(['quilc', '-S'],
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+
+    # Return context
+    try:
+        yield (qvm, quilc)
+
+    finally:
+        # Exit. Release resource
+        qvm.terminate()
+        quilc.terminate()
diff --git a/pyquil/device.py b/pyquil/device.py
index ce42488..fd80933 100644
--- a/pyquil/device.py
+++ b/pyquil/device.py
@@ -485,9 +485,6 @@ class NxDevice(AbstractDevice):
     def __init__(self, topology: nx.Graph) -> None:
         self.topology = topology
 
-    def qubits(self):
-        return sorted(self.topology.nodes)
-
     def qubit_topology(self):
         return self.topology
 
diff --git a/pyquil/quil.py b/pyquil/quil.py
index 043b3bf..2afe45f 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -36,7 +36,7 @@ from pyquil.quilatom import (LabelPlaceholder, QubitPlaceholder, unpack_qubit, A
 from pyquil.gates import MEASURE, QUANTUM_GATES, H, RESET
 from pyquil.quilbase import (DefGate, Gate, Measurement, Pragma, AbstractInstruction, Qubit,
                              Jump, Label, JumpConditional, JumpTarget, JumpUnless, JumpWhen,
-                             Declare)
+                             Declare, Halt, Reset, ResetQubit)
 
 
 class Program(object):
@@ -500,14 +500,16 @@ class Program(object):
 
     def is_protoquil(self):
         """
-        Protoquil programs may only contain gates, no classical instructions and no jumps.
+        Protoquil programs may only contain gates, Pragmas, and final global RESETs. It may not
+        contain classical instructions or jumps.
 
         :return: True if the Program is Protoquil, False otherwise
         """
-        for instr in self._instructions:
-            if not isinstance(instr, Gate):
-                return False
-        return True
+        try:
+            validate_protoquil(self)
+            return True
+        except ValueError:
+            return False
 
     def pop(self):
         """
@@ -981,3 +983,36 @@ def percolate_declares(program: Program) -> Program:
     p._defined_gates = program._defined_gates
 
     return p
+
+
+def validate_protoquil(program: Program) -> None:
+    """
+    Ensure that a program is valid ProtoQuil, otherwise raise a ValueError.
+    Protoquil allows a global RESET before any gates, and MEASUREs on each qubit after any gates
+    on that qubit. Pragmas are always allowed, and a final Halt instruction is allowed.
+
+    :param program: The Quil program to validate.
+    """
+    gates_seen = False
+    halted = False
+    measured_qubits = set()
+    for instr in program.instructions:
+        if isinstance(instr, Pragma) or isinstance(instr, Declare):
+            continue
+        elif isinstance(instr, Halt):
+            halted = True
+        elif halted:
+            raise ValueError(f"Cannot have instruction {instr} after HALT")
+        elif isinstance(instr, Gate):
+            gates_seen = True
+            if any(q.index in measured_qubits for q in instr.qubits):
+                raise ValueError("Cannot apply gates to qubits that were already measured.")
+        elif isinstance(instr, Reset):
+            if gates_seen:
+                raise ValueError("ProtoQuil disallows RESET after a gate application.")
+        elif isinstance(instr, ResetQubit):
+            raise ValueError("ProtoQuil only allows for global RESET.")
+        elif isinstance(instr, Measurement):
+            measured_qubits.add(instr.qubit.index)
+        else:
+            raise ValueError(f"Unhandled instruction type in ProtoQuil validation: {instr}")


Test patch:
diff --git a/pyquil/tests/conftest.py b/pyquil/tests/conftest.py
index 3060ae4..e3b7e53 100644
--- a/pyquil/tests/conftest.py
+++ b/pyquil/tests/conftest.py
@@ -1,8 +1,12 @@
+
+import shutil
+
 import numpy as np
 import pytest
 from requests import RequestException
 
-from pyquil.api import QVMConnection, LocalQVMCompiler, ForestConnection, get_benchmarker
+from pyquil.api import (QVMConnection, LocalQVMCompiler, ForestConnection,
+                        get_benchmarker, local_qvm)
 from pyquil.api._config import PyquilConfig
 from pyquil.api._errors import UnknownApiError
 from pyquil.device import Device
@@ -163,3 +167,14 @@ def benchmarker():
     except RequestException as e:
         return pytest.skip("This test requires a running local benchmarker endpoint (ie quilc): {}"
                            .format(e))
+
+
+@pytest.fixture(scope='session')
+def local_qvm_quilc():
+    """Execute test with local qvm and quilc running"""
+    if shutil.which('qvm') is None or shutil.which('quilc') is None:
+        return pytest.skip("This test requires 'qvm' and 'quilc' "
+                           "executables to be installed locally.")
+
+    with local_qvm() as context:
+        yield context
diff --git a/pyquil/tests/test_quantum_computer.py b/pyquil/tests/test_quantum_computer.py
index 484d32a..100f48a 100644
--- a/pyquil/tests/test_quantum_computer.py
+++ b/pyquil/tests/test_quantum_computer.py
@@ -5,7 +5,7 @@ import numpy as np
 import pytest
 
 from pyquil import Program, get_qc, list_quantum_computers
-from pyquil.api import QVM, QuantumComputer
+from pyquil.api import QVM, QuantumComputer, local_qvm
 from pyquil.api._qac import AbstractCompiler
 from pyquil.api._quantum_computer import _get_flipped_protoquil_program, _parse_name
 from pyquil.device import NxDevice, gates_in_isa
@@ -276,12 +276,21 @@ def test_qc_error():
         get_qc('5q', as_qvm=False)
 
 
-def test_run_and_measure_concat(qvm, compiler):
+def test_run_and_measure(local_qvm_quilc):
     qc = get_qc("9q-generic-qvm")
     prog = Program(I(8))
     trials = 11
-    # note to devs: this is included as an example in the run_and_measure docstrings
-    # so if you change it here ... change it there!
-    bitstrings = qc.run_and_measure(prog, trials)
+    with local_qvm():   # Redundant with test fixture.
+        bitstrings = qc.run_and_measure(prog, trials)
     bitstring_array = np.vstack(bitstrings[q] for q in sorted(qc.qubits())).T
     assert bitstring_array.shape == (trials, len(qc.qubits()))
+
+
+def test_run_symmetrized_readout_error(local_qvm_quilc):
+    qc = get_qc("9q-generic-qvm")
+    trials = 11
+    prog = Program(I(8))
+
+    # Trials not even
+    with pytest.raises(ValueError):
+        bitstrings = qc.run_symmetrized_readout(prog, trials)
diff --git a/pyquil/tests/test_quil.py b/pyquil/tests/test_quil.py
index d625920..dab32cb 100644
--- a/pyquil/tests/test_quil.py
+++ b/pyquil/tests/test_quil.py
@@ -27,7 +27,7 @@ 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, merge_with_pauli_noise, address_qubits, \
-    get_classical_addresses_from_program, Pragma
+    get_classical_addresses_from_program, Pragma, validate_protoquil
 from pyquil.quilatom import QubitPlaceholder, Addr, MemoryReference
 from pyquil.quilbase import DefGate, Gate, Qubit, JumpWhen, Declare
 from pyquil.tests.utils import parse_equals
@@ -1000,3 +1000,119 @@ def test_measure_all_noncontig():
         'MEASURE 10 ro[10]',
         '',
     ])
+
+
+def test_validate_protoquil_reset_first():
+    prog = Program(
+        H(0),
+        RESET(),
+    )
+    with pytest.raises(ValueError):
+        validate_protoquil(prog)
+    assert not prog.is_protoquil()
+
+
+def test_validate_protoquil_reset_qubit():
+    prog = Program(
+        RESET(2),
+    )
+    with pytest.raises(ValueError):
+        validate_protoquil(prog)
+    assert not prog.is_protoquil()
+
+
+def test_validate_protoquil_measure_last():
+    prog = Program(
+        MEASURE(0),
+        H(0),
+    )
+    with pytest.raises(ValueError):
+        validate_protoquil(prog)
+    assert not prog.is_protoquil()
+
+
+def test_validate_protoquil_with_pragma():
+    prog = Program(
+        RESET(),
+        H(1),
+        Pragma('DELAY'),
+        MEASURE(1)
+    )
+    assert prog.is_protoquil()
+
+
+def test_validate_protoquil_suite():
+    validate_protoquil(Program("""
+RESET
+DECLARE ro BIT[3]
+RX(-pi/4) 2
+RZ(4*pi) 3
+I 0
+CZ 2 3
+MEASURE 2 ro[2]
+MEASURE 3 ro[3]
+"""))
+
+    validate_protoquil(Program("""
+RESET
+DECLARE ro BIT[3]
+RX(-pi/4) 2
+RZ(4*pi) 3
+I 0
+CZ 2 3
+MEASURE 2 ro[2]
+MEASURE 3 ro[3]
+HALT
+"""))
+    validate_protoquil(Program("""
+RESET
+DECLARE ro BIT[3]
+RX(-pi/4) 2
+RZ(4*pi) 3
+I 0
+MEASURE 0
+CZ 2 3
+MEASURE 2 ro[2]
+X 3
+MEASURE 3 ro[3]
+HALT
+"""))
+
+    with pytest.raises(ValueError):
+        validate_protoquil(Program("""
+RESET
+DECLARE ro BIT[3]
+RX(-pi/4) 2
+RZ(4*pi) 3
+RESET
+I 0
+CZ 2 3
+MEASURE 2 ro[2]
+MEASURE 3 ro[3]
+"""))
+
+    with pytest.raises(ValueError):
+        validate_protoquil(Program("""
+RESET
+DECLARE ro BIT[3]
+RX(-pi/4) 2
+RZ(4*pi) 3
+MEASURE 2
+I 0
+CZ 2 3
+MEASURE 2 ro[2]
+MEASURE 3 ro[3]
+"""))
+
+    with pytest.raises(ValueError):
+        validate_protoquil(Program("""
+RESET
+DECLARE ro BIT[3]
+RX(-pi/4) 2
+RZ(4*pi) 3
+HALT
+I 0
+CZ 2 3
+MEASURE 2 ro[2]
+MEASURE 3 ro[3]
+"""))