Skip to content

Commit

Permalink
Check that QVM/quilc servers are running and compatible (#913)
Browse files Browse the repository at this point in the history
  • Loading branch information
karalekas authored May 20, 2019
1 parent eb0eb60 commit 7aa16d9
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 22 deletions.
10 changes: 8 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
image: python:3.6

variables:
QVM_URL: "http://qvm:5000"
QUILC_URL: "tcp://quilc:5555"

before_script:
- pip install tox

Expand All @@ -15,8 +19,10 @@ test:
tags:
- github
script:
- export QVM_URL='http://qvm:5000'
- export QUILC_URL='tcp://quilc:5555'
- echo "[Rigetti Forest]" > ~/.forest_config
- echo "qvm_address = $QVM_URL" >> ~/.forest_config
- echo "quilc_address = $QUILC_URL" >> ~/.forest_config
- cat ~/.forest_config
- tox -e py36

style:
Expand Down
39 changes: 38 additions & 1 deletion pyquil/api/_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
PyQuilExecutableResponse, ParameterSpec,
RewriteArithmeticRequest)

from pyquil.api._base_connection import ForestConnection
from pyquil import __version__
from pyquil.api._qac import AbstractCompiler
from pyquil.api._error_reporting import _record_call
from pyquil.device import AbstractDevice
Expand All @@ -38,6 +38,27 @@
PYQUIL_PROGRAM_PROPERTIES = ["native_quil_metadata", "num_shots"]


class QuilcVersionMismatch(Exception):
pass


class QuilcNotRunning(Exception):
pass


def check_quilc_version(version_dict: Dict[str, str]):
"""
Verify that there is no mismatch between pyquil and quilc versions.
:param version_dict: Dictionary containing version information about quilc.
"""
quilc_version = version_dict['quilc']
major, minor, patch = map(int, quilc_version.split('.'))
if major == 1 and minor < 8:
raise QuilcVersionMismatch('Must use quilc >= 1.8.0 with pyquil >= 2.8.0, but you '
f'have quilc {quilc_version} and pyquil {__version__}')


def _extract_attribute_dictionary_from_program(program: Program) -> Dict[str, Any]:
"""
Collects the attributes from PYQUIL_PROGRAM_PROPERTIES on the Program object program
Expand Down Expand Up @@ -165,6 +186,14 @@ def __init__(self,
self.target_device = TargetDevice(isa=device.get_isa().to_dict(),
specs=device.get_specs().to_dict())
self.name = name
self.connect()

def connect(self):
try:
quilc_version_dict = self.get_version_info()['quilc']
check_quilc_version(quilc_version_dict)
except TimeoutError:
raise QuilcNotRunning(f'No quilc server running at {self.quilc_client.endpoint}')

def get_version_info(self) -> dict:
quilc_version_info = self.quilc_client.call('get_version_info')
Expand Down Expand Up @@ -238,6 +267,14 @@ def __init__(self, endpoint: str, device: AbstractDevice, timeout: float = 10) -
self.client = Client(endpoint, timeout=timeout)
self.target_device = TargetDevice(isa=device.get_isa().to_dict(),
specs=device.get_specs().to_dict())
self.connect()

def connect(self):
try:
version_dict = self.get_version_info()
check_quilc_version(version_dict)
except TimeoutError:
raise QuilcNotRunning(f'No quilc server running at {self.client.endpoint}')

def get_version_info(self) -> dict:
return self.client.call('get_version_info')
Expand Down
38 changes: 38 additions & 0 deletions pyquil/api/_qvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import numpy as np
from typing import List

from requests.exceptions import ConnectionError
from rpcq.messages import PyQuilExecutableResponse
from six import integer_types

from pyquil import __version__
from pyquil.api._base_connection import (validate_qubit_list, validate_noise_probabilities,
TYPE_MULTISHOT_MEASURE, TYPE_WAVEFUNCTION,
TYPE_EXPECTATION, post_json, ForestConnection)
Expand All @@ -36,6 +38,26 @@
from pyquil.wavefunction import Wavefunction


class QVMVersionMismatch(Exception):
pass


class QVMNotRunning(Exception):
pass


def check_qvm_version(version: str):
"""
Verify that there is no mismatch between pyquil and QVM versions.
:param version: The version of the QVM
"""
major, minor, patch = map(int, version.split('.'))
if major == 1 and minor < 8:
raise QVMVersionMismatch('Must use QVM >= 1.8.0 with pyquil >= 2.8.0, but you '
f'have QVM {version} and pyquil {__version__}')


class QVMConnection(object):
"""
Represents a connection to the QVM.
Expand Down Expand Up @@ -105,6 +127,14 @@ def __init__(self, device=None, endpoint=None,

self._connection = ForestConnection(sync_endpoint=endpoint)
self.session = self._connection.session # backwards compatibility
self.connect()

def connect(self):
try:
version_dict = self.get_version_info()
check_qvm_version(version_dict)
except ConnectionError:
raise QVMNotRunning(f'No QVM server running at {self._connection.sync_endpoint}')

@_record_call
def get_version_info(self):
Expand Down Expand Up @@ -405,6 +435,14 @@ def __init__(self,
raise TypeError("random_seed should be None or a non-negative int")

self.requires_executable = requires_executable
self.connect()

def connect(self):
try:
version_dict = self.get_version_info()
check_qvm_version(version_dict)
except ConnectionError:
raise QVMNotRunning(f'No QVM server running at {self.connection.sync_endpoint}')

@_record_call
def get_version_info(self):
Expand Down
10 changes: 8 additions & 2 deletions pyquil/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
get_benchmarker, local_qvm)
from pyquil.api._config import PyquilConfig
from pyquil.api._errors import UnknownApiError
from pyquil.api._compiler import QuilcNotRunning, QuilcVersionMismatch
from pyquil.api._qvm import QVMNotRunning, QVMVersionMismatch
from pyquil.device import Device
from pyquil.gates import I
from pyquil.paulis import sX
Expand Down Expand Up @@ -138,8 +140,10 @@ def qvm():
qvm = QVMConnection(random_seed=52)
qvm.run(Program(I(0)), [])
return qvm
except (RequestException, UnknownApiError) as e:
except (RequestException, QVMNotRunning, UnknownApiError) as e:
return pytest.skip("This test requires QVM connection: {}".format(e))
except QVMVersionMismatch as e:
return pytest.skip("This test requires a different version of the QVM: {}".format(e))


@pytest.fixture()
Expand All @@ -149,8 +153,10 @@ def compiler(test_device):
compiler = QVMCompiler(endpoint=config.quilc_url, device=test_device, timeout=1)
compiler.quil_to_native_quil(Program(I(0)))
return compiler
except (RequestException, UnknownApiError, TimeoutError) as e:
except (RequestException, QuilcNotRunning, UnknownApiError, TimeoutError) as e:
return pytest.skip("This test requires compiler connection: {}".format(e))
except QuilcVersionMismatch as e:
return pytest.skip("This test requires a different version of quilc: {}".format(e))


@pytest.fixture(scope='session')
Expand Down
46 changes: 32 additions & 14 deletions pyquil/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@
from pyquil.api import QVMConnection, QPUCompiler, get_qc, QVMCompiler
from pyquil.api._base_connection import (validate_noise_probabilities, validate_qubit_list,
prepare_register_list)
from pyquil.api._config import PyquilConfig
from pyquil.device import ISA, NxDevice
from pyquil.gates import CNOT, H, MEASURE, PHASE, Z, RZ, RX, CZ
from pyquil.paulis import PauliTerm
from pyquil.quil import Program
from pyquil.quilbase import Pragma, Halt
from pyquil.quilbase import Halt

EMPTY_PROGRAM = Program()
BELL_STATE = Program(H(0), CNOT(0, 1))
Expand All @@ -61,17 +60,18 @@
RB_ENCODED_REPLY = [[0, 0], [1, 1]]
RB_REPLY = [Program("H 0\nH 0\n"), Program("PHASE(pi/2) 0\nPHASE(pi/2) 0\n")]

mock_qvm = QVMConnection()
mock_endpoint = mock_qvm.sync_endpoint

def test_sync_run_mock(qvm: QVMConnection):
mock_qvm = qvm
mock_endpoint = mock_qvm.sync_endpoint

def test_sync_run_mock():
def mock_response(request, context):
assert json.loads(request.text) == {
"type": "multishot",
"addresses": {'ro': [0, 1]},
"trials": 2,
"compiled-quil": "DECLARE ro BIT[2]\nH 0\nCNOT 0 1\nMEASURE 0 ro[0]\nMEASURE 1 ro[1]\n"
"compiled-quil": "DECLARE ro BIT[2]\nH 0\nCNOT 0 1\nMEASURE 0 ro[0]\nMEASURE 1 ro[1]\n",
'rng-seed': 52
}
return '{"ro": [[0,0],[1,1]]}'

Expand Down Expand Up @@ -105,13 +105,17 @@ def test_sync_run(qvm: QVMConnection):
qvm.run(EMPTY_PROGRAM)


def test_sync_run_and_measure_mock():
def test_sync_run_and_measure_mock(qvm: QVMConnection):
mock_qvm = qvm
mock_endpoint = mock_qvm.sync_endpoint

def mock_response(request, context):
assert json.loads(request.text) == {
"type": "multishot-measure",
"qubits": [0, 1],
"trials": 2,
"compiled-quil": "H 0\nCNOT 0 1\n"
"compiled-quil": "H 0\nCNOT 0 1\n",
'rng-seed': 52
}
return '[[0,0],[1,1]]'

Expand All @@ -138,18 +142,23 @@ def test_sync_run_and_measure(qvm):
WAVEFUNCTION_PROGRAM = Program(H(0), CNOT(0, 1), MEASURE(0, 0), H(0))


def test_sync_expectation_mock():
def test_sync_expectation_mock(qvm: QVMConnection):
mock_qvm = qvm
mock_endpoint = mock_qvm.sync_endpoint

def mock_response(request, context):
assert json.loads(request.text) == {
"type": "expectation",
"state-preparation": BELL_STATE.out(),
"operators": ["Z 0\n", "Z 1\n", "Z 0\nZ 1\n"]
"operators": ["Z 0\n", "Z 1\n", "Z 0\nZ 1\n"],
'rng-seed': 52
}
return b'[0.0, 0.0, 1.0]'

with requests_mock.Mocker() as m:
m.post(mock_endpoint + '/qvm', content=mock_response)
result = mock_qvm.expectation(BELL_STATE, [Program(Z(0)), Program(Z(1)), Program(Z(0), Z(1))])
result = mock_qvm.expectation(BELL_STATE, [Program(Z(0)), Program(Z(1)),
Program(Z(0), Z(1))])
exp_expected = [0.0, 0.0, 1.0]
np.testing.assert_allclose(exp_expected, result)

Expand Down Expand Up @@ -178,12 +187,16 @@ def test_sync_expectation_2(qvm):
np.testing.assert_allclose(exp_expected, result)


def test_sync_paulisum_expectation():
def test_sync_paulisum_expectation(qvm: QVMConnection):
mock_qvm = qvm
mock_endpoint = mock_qvm.sync_endpoint

def mock_response(request, context):
assert json.loads(request.text) == {
"type": "expectation",
"state-preparation": BELL_STATE.out(),
"operators": ["Z 0\nZ 1\n", "Z 0\n", "Z 1\n"]
"operators": ["Z 0\nZ 1\n", "Z 0\n", "Z 1\n"],
'rng-seed': 52
}
return b'[1.0, 0.0, 0.0]'

Expand Down Expand Up @@ -234,7 +247,7 @@ def test_prepare_register_list():
# ---------------------


def test_get_qc_returns_remote_qvm_compiler():
def test_get_qc_returns_remote_qvm_compiler(qvm: QVMConnection, compiler: QVMCompiler):
with patch.dict('os.environ', {"COMPILER_URL": "tcp://192.168.0.0:5550"}):
qc = get_qc("9q-generic-qvm")
assert isinstance(qc.compiler, QVMCompiler)
Expand All @@ -250,6 +263,11 @@ def native_quil_to_binary(payload: BinaryExecutableRequest) -> BinaryExecutableR
return BinaryExecutableResponse(program=COMPILED_BYTES_ARRAY)


@mock_qpu_compiler_server.rpc_handler
def get_version_info() -> str:
return '1.8.1'


@pytest.fixture
def m_endpoints():
return "tcp://127.0.0.1:5550", "tcp://*:5550"
Expand Down
6 changes: 3 additions & 3 deletions pyquil/tests/test_qpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

def test_qpu_run():
config = PyquilConfig()
if config.qpu_url and config.compiler_url:
if config.qpu_url and config.qpu_compiler_url:
g = nx.Graph()
g.add_node(0)
device = NxDevice(g)
Expand Down Expand Up @@ -139,7 +139,7 @@ def mock_qpu():
def qpu_compiler(test_device):
try:
config = PyquilConfig()
compiler = QPUCompiler(endpoint=config.compiler_url, device=test_device, timeout=0.5)
compiler = QPUCompiler(endpoint=config.qpu_compiler_url, device=test_device, timeout=0.5)
compiler.quil_to_native_quil(Program(I(0)))
return compiler
except Exception as e:
Expand Down Expand Up @@ -233,7 +233,7 @@ def parse_expression(expression):
return parse(f"RZ({expression}) 0")[0].params[0]


def test_run_expects_executable():
def test_run_expects_executable(qvm, qpu_compiler):
# https://github.com/rigetti/pyquil/issues/740

# This test might need some more knowledgeable eyes. Not sure how
Expand Down

0 comments on commit 7aa16d9

Please sign in to comment.