Repository: rigetticomputing/pyquil
Pull request #144 (2017-10-10 19:16:18)

Clean up QPUConnection and implement its run_and_measure functionality
The focus of this PR is to improve and clean up `QPUConnection` and implement its `run_and_measure` method. It also contains various style and documentation fixes. This PR addresses some of the concerns in #141, but doesn't address the issues with `T1`, `Ramsey`, and `Rabi`. These will be fixed in a future PR.


Patch:
diff --git a/docs/source/qpu.rst b/docs/source/qpu.rst
index 5d332e8..19a2e28 100644
--- a/docs/source/qpu.rst
+++ b/docs/source/qpu.rst
@@ -1,6 +1,6 @@
 
-Quantum Processor Unit
-======================
+Quantum Processing Unit
+=======================
 pyQuil allows some basic single-qubit experiments to be run on a multi-qubit superconducting quantum
 processor.  These three types of experiments are some of the basic building blocks for calibrating
 qubits.  This documentation will cover the basics of each experiment, as well as show you how to run
@@ -264,6 +264,22 @@ Note: if you are using a python terminal instead of a notebook, then plot using
 
 From this we can extract that the T2 decoherence for this qubit is about 14.4 microseconds.
 
+Running PyQuil Programs
+-----------------------
+
+Run a pyQuil program on the QPU. Only certain operations are currently supported; please
+contact support@rigetti.com for more information.
+
+::
+
+    from pyquil.quil import Program
+    from pyquil.gates import I, X, Y, Z, H, RX, RY, RZ, CZ
+    pq = Program(H(0), H(1), CZ(0, 1), H(1))
+    qubits = [0, 1]
+    num_shots = 1000
+    quil_res =  qpu.run_and_measure(pq, qubits, num_shots)
+    wait_for_job(quil_res)
+    print quil_res.result['result']
 
 pyquil.qpu
 ----------
diff --git a/pyquil/api.py b/pyquil/api.py
index 9323657..8753771 100644
--- a/pyquil/api.py
+++ b/pyquil/api.py
@@ -18,16 +18,16 @@ Module for facilitating connections to the QVM / QPU.
 """
 
 from __future__ import print_function
-from urllib3.util.retry import Retry
 from requests.adapters import HTTPAdapter
-import requests
+from six import integer_types
+from urllib3.util.retry import Retry
 import json
+import requests
 import sys
-from six import integer_types
 
-import pyquil.quil as pq
 from pyquil.config import PyquilConfig
 from pyquil.job_results import JobResult, WavefunctionResult, recover_complexes
+from pyquil.quil import Program
 
 
 def add_noise_to_payload(payload, gate_noise, measurement_noise):
@@ -75,7 +75,7 @@ def _validate_noise_probabilities(noise_parameter):
         raise ValueError("noise_parameter values should all be non-negative")
 
 
-def _validate_run_items(run_items):
+def validate_run_items(run_items):
     """
     Check the validity of classical addresses / qubits for the payload.
 
@@ -162,7 +162,7 @@ class JobConnection(object):
         else:
             raise TypeError("random_seed should be None or a non-negative int or long.")
 
-        self.machine = 'QVM' # default to a QVM Machine Connection
+        self.machine = 'QVM'  # default to a QVM Machine Connection
 
     def post_json(self, jd, headers=None, route=""):
         """
@@ -170,7 +170,7 @@ class JobConnection(object):
 
         :param dict jd: JSON.
         :param dict headers: The headers for the post request.
-        :param: str route: The route to append to the endpoint, e.g. "/job"
+        :param str route: The route to append to the endpoint, e.g. "/job"
         :return: A non-error response.
         """
         if headers is None:
@@ -256,8 +256,7 @@ class JobConnection(object):
         """
         Query the QVM version.
 
-        :return: The current version of the QVM.
-        :rtype: string
+        :return: None
         """
         raise DeprecationWarning("Version checks have been deprecated.")
 
@@ -294,9 +293,9 @@ class JobConnection(object):
         if classical_addresses is None:
             classical_addresses = []
 
-        if not isinstance(quil_program, pq.Program):
+        if not isinstance(quil_program, Program):
             raise TypeError("quil_program must be a Quil program object")
-        _validate_run_items(classical_addresses)
+        validate_run_items(classical_addresses)
 
         payload = {'type': TYPE_WAVEFUNCTION,
                    'quil-instructions': quil_program.out(),
@@ -313,14 +312,14 @@ class JobConnection(object):
         prep_program.
 
         :param Program prep_prog: Quil program for state preparation.
-        :param list operators: A list of PauliTerms. Default is Identity operator.
+        :param list operator_programs: A list of PauliTerms. Default is Identity operator.
         :returns: Expectation value of the operators.
         :rtype: float
         """
         if operator_programs is None:
-            operator_programs = [pq.Program()]
+            operator_programs = [Program()]
 
-        if not isinstance(prep_prog, pq.Program):
+        if not isinstance(prep_prog, Program):
             raise TypeError("prep_prog variable must be a Quil program object")
 
         payload = {'type': TYPE_EXPECTATION,
@@ -354,9 +353,9 @@ class JobConnection(object):
                  in `classical_addresses`.
         :rtype: list
         """
-        if not isinstance(quil_program, pq.Program):
+        if not isinstance(quil_program, Program):
             raise TypeError("quil_program must be a Quil program object")
-        _validate_run_items(classical_addresses)
+        validate_run_items(classical_addresses)
         if not isinstance(trials, integer_types):
             raise TypeError("trials must be an integer")
 
@@ -381,9 +380,9 @@ class JobConnection(object):
         :return: A list of a list of bits.
         :rtype: list
         """
-        if not isinstance(quil_program, pq.Program):
+        if not isinstance(quil_program, Program):
             raise TypeError("quil_program must be a Quil program object")
-        _validate_run_items(qubits)
+        validate_run_items(qubits)
         if not isinstance(trials, integer_types):
             raise TypeError("trials must be an integer")
 
diff --git a/pyquil/qpu.py b/pyquil/qpu.py
index 1daa283..58e5be5 100644
--- a/pyquil/qpu.py
+++ b/pyquil/qpu.py
@@ -1,9 +1,29 @@
+##############################################################################
+# 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 facilitating connections to the QPU.
+"""
+
 import requests
 
-from pyquil.api import JobConnection, _validate_run_items
-import pyquil.quil as pq
+from pyquil.api import JobConnection, TYPE_MULTISHOT, TYPE_MULTISHOT_MEASURE, validate_run_items
 from pyquil.job_results import RamseyResult, RabiResult, T1Result
-ENDPOINT = "https://job.rigetti.com/beta"
+from pyquil.quil import Program
+
+ENDPOINT = 'https://job.rigetti.com/beta'
 
 
 class NoParametersFoundException(Exception):
@@ -12,10 +32,13 @@ class NoParametersFoundException(Exception):
 
 class QPUConnection(JobConnection):
 
-    def __init__(self, device_name, **kwargs):
+    def __init__(self, device_name='Device', **kwargs):
+        """
+        :param str device_name: Unique identifier of the device in question
+        """
         super(QPUConnection, self).__init__(**kwargs)
         self.device_name = device_name
-        self.machine = "QPU"
+        self.machine = 'QPU'
         self.session.headers = self.headers
         self.endpoint = ENDPOINT
 
@@ -30,9 +53,9 @@ class QPUConnection(JobConnection):
 
     def rabi(self, qubit_id):
         """
-        Runs a Rabi experiment on the given qubit
+        Runs a Rabi experiment on the given qubit.
 
-        :param int qubit_id:
+        :param int qubit_id: Unique identifier of the qubit in question
         :return: A RabiResult object
         """
         payload = self.get_rabi_params(qubit_id)
@@ -47,11 +70,11 @@ class QPUConnection(JobConnection):
 
     def ramsey(self, qubit_id):
         """
-         Runs a Ramsey experiment on the given qubit
+        Runs a Ramsey experiment on the given qubit.
 
-         :param int qubit_id:
-         :return: A RamseyResult object
-         """
+        :param int qubit_id: Unique identifier of the qubit in question
+        :return: A RamseyResult object
+        """
         payload = self.get_ramsey_params(qubit_id)
         payload.update({
             'type': 'pyquillow',
@@ -64,11 +87,11 @@ class QPUConnection(JobConnection):
 
     def t1(self, qubit_id):
         """
-         Runs a T1 experiment on the given qubit
+        Runs a T1 experiment on the given qubit.
 
-         :param int qubit_id:
-         :return: A T1Result object
-         """
+        :param int qubit_id: Unique identifier of the qubit in question
+        :return: A T1Result object
+        """
         payload = self.get_t1_params(qubit_id)
         payload.update({
             'type': 'pyquillow',
@@ -84,35 +107,56 @@ class QPUConnection(JobConnection):
 
     def run(self, quil_program, classical_addresses, trials=1):
         """
-        Run a pyQuil program on the QPU.
+        Run a pyQuil program on the QPU. This functionality is in beta.
 
-        :param Program quil_program: Quil program
-        :param list classical_addresses: Unused
-        :param int trials: Unused
+        :param Program quil_program: Quil program to run on the QPU
+        :param list classical_addresses: Currently unused
+        :param int trials: Number of shots to take
         :return: A job result
         :rtype: JobResult
         """
-        if not isinstance(quil_program, pq.Program):
-            raise TypeError("quil_program must be a Quil program object")
-        _validate_run_items(classical_addresses)
+        if not isinstance(quil_program, Program):
+            raise TypeError('quil_program must be a Quil program object')
+        validate_run_items(classical_addresses)
         if not isinstance(trials, int):
-            raise TypeError("trials must be an integer")
+            raise TypeError('trials must be an integer')
 
-        payload = {"type": 'multishot',
-                   "addresses": classical_addresses,
-                   "trials": trials,
-                   "quil-instructions": quil_program.out(),
-                   'device_id': self.device_name,
-                   'qcid': 0,
-                   'experiment': 'quil'}
+        payload = {'type': TYPE_MULTISHOT,
+                   'addresses': classical_addresses,
+                   'trials': trials,
+                   'quil-instructions': quil_program.out(),
+                   'device_id': self.device_name}
 
         res = self.post_job(payload, headers=self.headers)
         return self.process_response(res)
 
     def run_and_measure(self, quil_program, qubits, trials=1):
-        return NotImplementedError
+        """
+        Run a pyQuil program on the QPU multiple times, measuring all the qubits in the QPU
+        simultaneously at the end of the program each time. This functionality is in beta.
+
+        :param Program quil_program: Quil program to run on the QPU
+        :param list qubits: The list of qubits to return results for
+        :param int trials: Number of shots to take
+        :return: A job result
+        :rtype: JobResult
+        """
+        if not isinstance(quil_program, Program):
+            raise TypeError('quil_program must be a Quil program object')
+        validate_run_items(qubits)
+        if not isinstance(trials, int):
+            raise TypeError('trials must be an integer')
+
+        payload = {'type': TYPE_MULTISHOT_MEASURE,
+                   'qubits': qubits,
+                   'trials': trials,
+                   'quil-instructions': quil_program.out(),
+                   'device_id': self.device_name}
+
+        res = self.post_job(payload, headers=self.json_headers)
+        return self.process_response(res)
 
-    def expectation(self, prep_prog, operator_programs=[pq.Program()]):
+    def expectation(self, prep_prog, operator_programs=[Program()]):
         return NotImplementedError
 
     def bit_string_probabilities(self, quil_program):
@@ -121,13 +165,15 @@ class QPUConnection(JobConnection):
     def get_info(self):
         """
         Gets information about what devices are currently available through the Forest API.
+
         :return: A JSON dictionary with configuration information.
+        :rtype: dict
         """
-        url = self.endpoint + "/config"
+        url = self.endpoint + '/config'
         headers = {
-                'Content-Type' : 'application/json; charset=utf-8',
-                'x-api-key' : self.api_key,
-                'x-user-id' : self.user_id,
+                'Content-Type': 'application/json; charset=utf-8',
+                'x-api-key': self.api_key,
+                'x-user-id': self.user_id,
         }
         res = requests.get(url, headers=headers)
         config_json = res.json()
@@ -136,16 +182,18 @@ class QPUConnection(JobConnection):
     def get_params(self, qcid, func):
         """
         Get and parse the configuration information from the Forest API.
-        :param int qcid: The qubit number to look at
+
+        :param int qcid: Unique identifier of the qubit in question
         :param func: A function to apply to the qubit specific JSON dictionary of config info
-        :return:
+        :return: A dictionary with the parameter info
+        :rtype: dict
         """
         config_dict = self.get_info()
         try:
-            device_config = [dd for dd in config_dict['devices'] if dd['name'] == self.device_name][0]
+            device_config = [d for d in config_dict['devices'] if d['name'] == self.device_name][0]
             device_config = device_config['qubits']
         except (KeyError, IndexError):
-            raise NoParametersFoundException("Device with name {} not found.".format(self.device_name))
+            raise NoParametersFoundException('Device {} not found.'.format(self.device_name))
         for qc in device_config:
             if qc['num'] == qcid:
                 return func(qc)
@@ -153,8 +201,9 @@ class QPUConnection(JobConnection):
 
     def get_rabi_params(self, qcid):
         """
-        Gets the current Rabi experiment parameters for a specific qubit on a specific device
-        :param int qcid:
+        Gets the current Rabi experiment parameters for a specific qubit on a specific device.
+
+        :param int qcid: Unique identifier of the qubit in question
         :return: A dictionary with the parameter info
         :rtype: dict
         """
@@ -170,8 +219,9 @@ class QPUConnection(JobConnection):
 
     def get_ramsey_params(self, qcid):
         """
-        Gets the current Ramsey experiment parameters for a specific qubit on a specific device
-        :param int qcid:
+        Gets the current Ramsey experiment parameters for a specific qubit on a specific device.
+
+        :param int qcid: Unique identifier of the qubit in question
         :return: A dictionary with the parameter info
         :rtype: dict
         """
@@ -187,8 +237,9 @@ class QPUConnection(JobConnection):
 
     def get_t1_params(self, qcid):
         """
-        Gets the current T1 experiment parameters for a specific qubit on a specific device
-        :param int qcid:
+        Gets the current T1 experiment parameters for a specific qubit on a specific device.
+
+        :param int qcid: Unique identifier of the qubit in question
         :return: A dictionary with the parameter info
         :rtype: dict
         """


Test patch:
diff --git a/pyquil/tests/test_api.py b/pyquil/tests/test_api.py
index 9a21ac2..dfe0dc4 100644
--- a/pyquil/tests/test_api.py
+++ b/pyquil/tests/test_api.py
@@ -16,14 +16,15 @@
 ##############################################################################
 
 
-import pytest
-import json
 from mock import Mock
+import json
 import numpy as np
+import pytest
 
-import pyquil.api as qvm
-import pyquil.quil as pq
-from pyquil.gates import *
+from pyquil.api import (add_noise_to_payload, add_rng_seed_to_payload, SyncConnection,
+                        _validate_noise_probabilities, validate_run_items)
+from pyquil.quil import Program
+from pyquil.gates import CNOT, H, MEASURE
 
 
 class MockPostJson(object):
@@ -37,9 +38,9 @@ class MockPostJson(object):
 
 @pytest.fixture
 def cxn():
-    c = qvm.SyncConnection()
+    c = SyncConnection()
     c.post_json = MockPostJson()
-    c.post_json.return_value.text = json.dumps("Success")
+    c.post_json.return_value.text = json.dumps('Success')
     c.measurement_noise = 1
     return c
 
@@ -58,14 +59,14 @@ def cxn_wf(cxn):
 
 @pytest.fixture
 def prog():
-    p = pq.Program()
+    p = Program()
     p.inst(H(0), CNOT(0, 1), MEASURE(0, 0), MEASURE(1, 1))
     return p
 
 
 @pytest.fixture
 def prog_wf():
-    p = pq.Program()
+    p = Program()
     p.inst(H(0)).inst(CNOT(0, 1)).measure(0, 0)
     p.inst(H(0))
     return p
@@ -73,57 +74,57 @@ def prog_wf():
 
 def test_add_rng_seed_to_payload():
     payload = {}
-    qvm.add_rng_seed_to_payload(payload, 1)
+    add_rng_seed_to_payload(payload, 1)
     assert payload['rng-seed'] == 1
 
 
 def test_dont_add_rng_seed_to_payload():
     payload = {}
-    qvm.add_rng_seed_to_payload(payload, None)
+    add_rng_seed_to_payload(payload, None)
     assert 'rng-seed' not in payload
 
 
 def test_add_noise_to_payload():
     payload = {}
-    qvm.add_noise_to_payload(payload, 1, None)
-    assert payload["gate-noise"] == 1
-    assert "measurement-noise" not in payload
+    add_noise_to_payload(payload, 1, None)
+    assert payload['gate-noise'] == 1
+    assert 'measurement-noise' not in payload
 
 
 def test_validate_noise_probabilities():
     with pytest.raises(TypeError):
-        qvm._validate_noise_probabilities(1)
+        _validate_noise_probabilities(1)
     with pytest.raises(TypeError):
-        qvm._validate_noise_probabilities(['a', 'b', 'c'])
+        _validate_noise_probabilities(['a', 'b', 'c'])
     with pytest.raises(ValueError):
-        qvm._validate_noise_probabilities([0.0, 0.0, 0.0, 0.0])
+        _validate_noise_probabilities([0.0, 0.0, 0.0, 0.0])
     with pytest.raises(ValueError):
-        qvm._validate_noise_probabilities([0.5, 0.5, 0.5])
+        _validate_noise_probabilities([0.5, 0.5, 0.5])
     with pytest.raises(ValueError):
-        qvm._validate_noise_probabilities([-0.5, -0.5, -0.5])
+        _validate_noise_probabilities([-0.5, -0.5, -0.5])
 
 
 def test_validate_run_items():
     with pytest.raises(TypeError):
-        qvm._validate_run_items(-1, 1)
+        validate_run_items(-1, 1)
     with pytest.raises(TypeError):
-        qvm._validate_run_items(["a", 0], 1)
+        validate_run_items(['a', 0], 1)
 
 
 def test_run(cxn, prog):
     with pytest.raises(TypeError):
-        cxn.run(prog, [0, 1], "a")
-    assert cxn.run(prog, [0, 1], 1) == "Success"
+        cxn.run(prog, [0, 1], 'a')
+    assert cxn.run(prog, [0, 1], 1) == 'Success'
 
 
 def test_run_and_measure(cxn, prog):
     with pytest.raises(TypeError):
-        cxn.run_and_measure(prog, [0, 1], "a")
-    assert cxn.run_and_measure(prog, [0, 1], 1) == "Success"
+        cxn.run_and_measure(prog, [0, 1], 'a')
+    assert cxn.run_and_measure(prog, [0, 1], 1) == 'Success'
 
 
 def test_expectation(cxn, prog):
-    assert cxn.expectation(prog) == "Success"
+    assert cxn.expectation(prog) == 'Success'
 
 
 def test_wavefunction(cxn_wf, prog_wf):