From ea20069ece9dc97cd545610beef8a217e8e29140 Mon Sep 17 00:00:00 2001 From: Eric Hulburd <3526083+erichulburd@users.noreply.github.com> Date: Wed, 17 Mar 2021 10:59:08 -0700 Subject: [PATCH 01/61] Breaking: Update to use qcs-api-client (#1300) Authored-by: Andrew Meyer --- conftest.py | 208 ++++++++++++ pyquil/api/_abstract_compiler.py | 36 +++ pyquil/api/_client.py | 243 ++++++++++++++ pyquil/api/_qam.py | 2 + pyquil/api/_quantum_computer.py | 2 + pyquil/api/_quantum_processors.py | 30 ++ pyquil/api/_qvm.py | 2 +- pyquil/api/_wavefunction_simulator.py | 13 + pyquil/api/tests/test_compiler.py | 53 +++ pyquil/api/tests/test_quantum_computer.py | 231 ++++++++++++++ pyquil/device/_main.py | 336 ++++++++++++++++++++ pyquil/tests/test_api.py | 245 ++++++++++++++ pyquil/tests/test_qvm.py | 145 +++++++++ pyquil/tests/test_wavefunction_simulator.py | 59 ++++ pyquil/tests/utils.py | 32 ++ requirements.txt | 44 +++ 16 files changed, 1680 insertions(+), 1 deletion(-) create mode 100644 conftest.py create mode 100644 pyquil/api/_client.py create mode 100644 pyquil/api/_quantum_processors.py create mode 100644 pyquil/api/tests/test_compiler.py create mode 100644 pyquil/api/tests/test_quantum_computer.py create mode 100644 pyquil/device/_main.py create mode 100644 pyquil/tests/test_api.py create mode 100644 pyquil/tests/test_qvm.py create mode 100644 pyquil/tests/test_wavefunction_simulator.py create mode 100644 pyquil/tests/utils.py create mode 100644 requirements.txt diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..7c68eac71 --- /dev/null +++ b/conftest.py @@ -0,0 +1,208 @@ +import numpy as np +import pytest +from requests import RequestException + +from pyquil.api import ( + QVMConnection, + QVMCompiler, + Client, + BenchmarkConnection, +) +from pyquil.api._errors import UnknownApiError +from pyquil.api._abstract_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 +from pyquil.quil import Program +from pyquil.tests.utils import DummyCompiler + + +@pytest.fixture +def isa_dict(): + return { + "1Q": {"0": {"type": "Xhalves"}, "1": {}, "2": {}, "3": {"dead": True}}, + "2Q": { + "0-1": {}, + "1-2": {"type": "ISWAP"}, + "0-2": {"type": "CPHASE"}, + "0-3": {"dead": True}, + }, + } + + +@pytest.fixture +def specs_dict(): + return { + "1Q": { + "0": { + "f1QRB": 0.99, + "f1QRB_std_err": 0.01, + "f1Q_simultaneous_RB": 0.98, + "f1Q_simultaneous_RB_std_err": 0.02, + "fRO": 0.93, + "T1": 20e-6, + "T2": 15e-6, + }, + "1": { + "f1QRB": 0.989, + "f1QRB_std_err": 0.011, + "f1Q_simultaneous_RB": 0.979, + "f1Q_simultaneous_RB_std_err": 0.021, + "fRO": 0.92, + "T1": 19e-6, + "T2": 12e-6, + }, + "2": { + "f1QRB": 0.983, + "f1QRB_std_err": 0.017, + "f1Q_simultaneous_RB": 0.973, + "f1Q_simultaneous_RB_std_err": 0.027, + "fRO": 0.95, + "T1": 21e-6, + "T2": 16e-6, + }, + "3": { + "f1QRB": 0.988, + "f1QRB_std_err": 0.012, + "f1Q_simultaneous_RB": 0.978, + "f1Q_simultaneous_RB_std_err": 0.022, + "fRO": 0.94, + "T1": 18e-6, + "T2": 11e-6, + }, + }, + "2Q": { + "0-1": {"fBellState": 0.90, "fCZ": 0.89, "fCZ_std_err": 0.01, "fCPHASE": 0.88}, + "1-2": {"fBellState": 0.91, "fCZ": 0.90, "fCZ_std_err": 0.12, "fCPHASE": 0.89}, + "0-2": {"fBellState": 0.92, "fCZ": 0.91, "fCZ_std_err": 0.20, "fCPHASE": 0.90}, + "0-3": {"fBellState": 0.89, "fCZ": 0.88, "fCZ_std_err": 0.03, "fCPHASE": 0.87}, + }, + } + + +@pytest.fixture +def noise_model_dict(): + return { + "gates": [ + { + "gate": "I", + "params": (5.0,), + "targets": (0, 1), + "kraus_ops": [[[[1.0]], [[1.0]]]], + "fidelity": 1.0, + }, + { + "gate": "RX", + "params": (np.pi / 2.0,), + "targets": (0,), + "kraus_ops": [[[[1.0]], [[1.0]]]], + "fidelity": 1.0, + }, + ], + "assignment_probs": {"1": [[1.0, 0.0], [0.0, 1.0]], "0": [[1.0, 0.0], [0.0, 1.0]]}, + } + + +@pytest.fixture +def device_raw(isa_dict, noise_model_dict, specs_dict): + return { + "isa": isa_dict, + "noise_model": noise_model_dict, + "specs": specs_dict, + "is_online": True, + "is_retuning": False, + } + + +@pytest.fixture +def test_device(device_raw): + return Device("test_device", device_raw) + + +@pytest.fixture(scope="session") +def qvm(client: Client): + try: + qvm = QVMConnection(client=client, random_seed=52) + qvm.run(Program(I(0)), []) + return qvm + 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() +def compiler(test_device, client: Client): + try: + compiler = QVMCompiler(device=test_device, client=client, timeout=1) + compiler.quil_to_native_quil(Program(I(0))) + return compiler + 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() +def dummy_compiler(test_device: Device, client: Client): + return DummyCompiler(test_device, client) + + +@pytest.fixture(scope="session") +def client(): + return Client() + + +@pytest.fixture(scope="session") +def benchmarker(client: Client): + try: + bm = BenchmarkConnection(client=client, timeout=2) + bm.apply_clifford_to_pauli(Program(I(0)), sX(0)) + return bm + except (RequestException, TimeoutError) as e: + return pytest.skip( + "This test requires a running local benchmarker endpoint (ie quilc): {}".format(e) + ) + + +def _str_to_bool(s): + """Convert either of the strings 'True' or 'False' to their Boolean equivalent""" + if s == "True": + return True + elif s == "False": + return False + else: + raise ValueError("Please specify either True or False") + + +def pytest_addoption(parser): + parser.addoption( + "--use-seed", + action="store", + type=_str_to_bool, + default=True, + help="run operator estimation tests faster by using a fixed random seed", + ) + parser.addoption( + "--runslow", action="store_true", default=False, help="run tests marked as being 'slow'" + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "slow: mark test as slow to run") + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--runslow"): + # --runslow given in cli: do not skip slow tests + return + skip_slow = pytest.mark.skip(reason="need --runslow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) + + +@pytest.fixture() +def use_seed(pytestconfig): + return pytestconfig.getoption("use_seed") diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 4dd6578f4..472ea72bb 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -34,6 +34,42 @@ from pyquil.quil import Program from pyquil.quilatom import MemoryReference, ExpressionDesignator from pyquil.quilbase import Gate +from pyquil.version import __version__ + +if sys.version_info < (3, 7): + from rpcq.external.dataclasses import dataclass +else: + from dataclasses import dataclass + + +class QuilcVersionMismatch(Exception): + pass + + +class QuilcNotRunning(Exception): + pass + + +@dataclass() +class EncryptedProgram: + """ + Encrypted binary, executable on a QPU. + """ + + program: str + """String representation of an encrypted Quil program.""" + + memory_descriptors: Dict[str, ParameterSpec] + """Descriptors for memory executable's regions, mapped by name.""" + + ro_sources: Dict[MemoryReference, str] + """Readout sources, mapped by memory reference.""" + + recalculation_table: Dict[ParameterAref, ExpressionDesignator] + """A mapping from memory references to the original gate arithmetic.""" + + +QuantumExecutable = Union[EncryptedProgram, Program] if sys.version_info < (3, 7): from rpcq.external.dataclasses import dataclass diff --git a/pyquil/api/_client.py b/pyquil/api/_client.py new file mode 100644 index 000000000..aebbffee0 --- /dev/null +++ b/pyquil/api/_client.py @@ -0,0 +1,243 @@ +import re +from contextlib import contextmanager +from datetime import datetime +from json.decoder import JSONDecodeError +from typing import Optional, Any, Callable, Iterator, cast + +import httpx +import rpcq +from dateutil.parser import parse as parsedate +from dateutil.tz import tzutc +from qcs_api_client.client import QCSClientConfiguration, build_sync_client +from qcs_api_client.models import ( + EngagementWithCredentials, + CreateEngagementRequest, + EngagementCredentials, +) +from qcs_api_client.operations.sync import create_engagement +from qcs_api_client.types import Response +from rpcq import ClientAuthConfig + +from pyquil.api._errors import ApiError, UnknownApiError, TooManyQubitsError, error_mapping +from pyquil.api._logger import logger + + +class Client: + """ + Class for housing application configuration and interacting with network resources. + """ + + _http: Optional[httpx.Client] + _config: QCSClientConfiguration + _has_default_config: bool + _engagement: Optional[EngagementWithCredentials] = None + + def __init__( + self, + *, + http: Optional[httpx.Client] = None, + configuration: Optional[QCSClientConfiguration] = None, + ): + """ + Instantiate a client. + + :param http: optional underlying HTTP client. If none is provided, a default + client will be created using ``configuration``. + :param configuration: QCS configuration. + """ + self._config = configuration or QCSClientConfiguration.load() + self._has_default_config = configuration is None + self._http = http + + def post_json(self, url: str, json: Any, timeout: float = 5) -> httpx.Response: + """ + Post JSON to a URL. Will raise an exception for response statuses >= 400. + + :param url: URL to post to. + :param json: JSON body of request. + :param timeout: Time limit for request, in seconds. + :return: HTTP response corresponding to request. + """ + logger.debug("Sending POST request to %s. Body: %s", url, json) + with self._http_client() as http: # type: httpx.Client + res = http.post(url, json=json, timeout=timeout) + if res.status_code >= 400: + raise _parse_error(res) + return res + + def qcs_request(self, request_fn: Callable[..., Response[Any]], **kwargs: Any) -> Any: + """ + Execute a QCS request. + + :param request_fn: Request function (from ``qcs_api_client.operations.sync``). + :param kwargs: Arguments to pass to request function. + :return: HTTP response corresponding to request. + """ + with self._http_client() as http: # type: httpx.Client + return request_fn(client=http, **kwargs).parsed + + def compiler_rpcq_request( + self, method_name: str, *args: Any, timeout: Optional[float] = None, **kwargs: Any + ) -> Any: + """ + Execute a remote function against the Quil compiler. + + :param method_name: Method name. + :param args: Arguments that will be passed to the remote function. + :param timeout: Optional time limit for request, in seconds. + :param kwargs: Keyword arguments that will be passed to the remote function. + :return: Result from remote function. + """ + return self._rpcq_request(self.quilc_url, method_name, *args, timeout=timeout, **kwargs) + + def processor_rpcq_request( + self, + quantum_processor_id: str, + method_name: str, + *args: Any, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Any: + """ + Execute a remote function against a processor endpoint. If there is no current engagement, + or if it is invalid, a new engagement will be requested for the given processor. + + :param quantum_processor_id: Processor to engage. + :param method_name: Method name. + :param args: Arguments that will be passed to the remote function. + :param timeout: Optional time limit for request, in seconds. + :param kwargs: Keyword arguments that will be passed to the remote function. + :return: Result from remote function. + """ + # TODO(andrew): handle multiple engagements at once (per processor) + if not _engagement_valid(quantum_processor_id, self._engagement): + self._engagement = self._create_engagement(quantum_processor_id) + + assert self._engagement is not None + + return self._rpcq_request( + self._engagement.address, + method_name, + *args, + timeout=timeout, + auth_config=_to_auth_config(self._engagement.credentials), + **kwargs, + ) + + def _rpcq_request( + self, + endpoint: str, + method_name: str, + *args: Any, + timeout: Optional[float] = None, + auth_config: Optional[ClientAuthConfig] = None, + **kwargs: Any, + ) -> Any: + client = rpcq.Client(endpoint, auth_config=auth_config) + response = client.call(method_name, *args, rpc_timeout=timeout, **kwargs) # type: ignore + client.close() # type: ignore + return response + + def reset(self) -> None: + """ + Clears current engagement and reloads configuration (if not overridden in constructor). + """ + self._engagement = None + if self._has_default_config: + self._config = QCSClientConfiguration.load() + + @property + def qvm_url(self) -> str: + """ + QVM URL from client configuration. + """ + return self._config.profile.applications.pyquil.qvm_url + + @property + def quilc_url(self) -> str: + """ + Quil compiler URL from client configuration. + """ + return self._config.profile.applications.pyquil.quilc_url + + def qvm_version(self) -> str: + """ + Get QVM version string. + """ + response = self.post_json(self.qvm_url, {"type": "version"}) + split_version_string = response.text.split() + try: + qvm_version = split_version_string[0] + except ValueError: + raise TypeError(f"Malformed version string returned by the QVM: {response.text}") + return qvm_version + + @contextmanager + def _http_client(self) -> Iterator[httpx.Client]: + if self._http is None: + with build_sync_client(configuration=self._config) as client: # type: httpx.Client + yield client + else: + yield self._http + + def _create_engagement(self, quantum_processor_id: str) -> EngagementWithCredentials: + return cast( + EngagementWithCredentials, + self.qcs_request( + create_engagement, + json_body=CreateEngagementRequest(quantum_processor_id=quantum_processor_id), + ), + ) + + +def _engagement_valid( + quantum_processor_id: str, engagement: Optional[EngagementWithCredentials] +) -> bool: + if engagement is None: + return False + + return all( + [ + engagement.credentials.client_public != "", + engagement.credentials.client_secret != "", + engagement.credentials.server_public != "", + parsedate(engagement.expires_at) > datetime.now(tzutc()), + engagement.address != "", + engagement.quantum_processor_id == quantum_processor_id, + ] + ) + + +def _to_auth_config(credentials: EngagementCredentials) -> ClientAuthConfig: + return rpcq.ClientAuthConfig( + client_secret_key=credentials.client_secret.encode(), + client_public_key=credentials.client_public.encode(), + server_public_key=credentials.server_public.encode(), + ) + + +def _parse_error(res: httpx.Response) -> ApiError: + """ + Errors should contain a "status" field with a human readable explanation of + what went wrong as well as a "error_type" field indicating the kind of error that can be mapped + to a Python type. + + There's a fallback error UnknownApiError for other types of exceptions (network issues, api + gateway problems, etc.) + """ + try: + body = res.json() + except JSONDecodeError: + raise UnknownApiError(res.text) + + if "error_type" not in body: + raise UnknownApiError(str(body)) + + error_type = body["error_type"] + status = body["status"] + + if re.search(r"[0-9]+ qubits were requested, but the QVM is limited to [0-9]+ qubits.", status): + return TooManyQubitsError(status) + + error_cls = error_mapping.get(error_type, UnknownApiError) + return error_cls(status) diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index 5fe6e22a8..f13e631a1 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -23,6 +23,7 @@ from pyquil.api._abstract_compiler import QuantumExecutable from pyquil.api._error_reporting import _record_call +from pyquil.api._abstract_compiler import QuantumExecutable from pyquil.experiment._main import Experiment @@ -138,6 +139,7 @@ def reset(self) -> None: when it has gotten into an unwanted state. This can happen, for example, if the QAM is interrupted in the middle of a run. """ + self._client.reset() self._variables_shim = {} self.executable = None self._memory_results = defaultdict(lambda: None) diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 72e2a9ebe..0b50acb8f 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -46,6 +46,7 @@ from pyquil.api._qam import QAM from pyquil.api._qcs_client import qcs_client from pyquil.api._qpu import QPU +from pyquil.api._quantum_processors import get_device from pyquil.api._qvm import QVM from pyquil.experiment._main import Experiment from pyquil.experiment._memory import merge_memory_map_lists @@ -583,6 +584,7 @@ def _get_qvm_qc( compiler=QVMCompiler( quantum_processor=quantum_processor, timeout=compiler_timeout, client_configuration=client_configuration ), + compiler=QVMCompiler(device=device, client=client, timeout=compiler_timeout), ) diff --git a/pyquil/api/_quantum_processors.py b/pyquil/api/_quantum_processors.py new file mode 100644 index 000000000..13b8f574c --- /dev/null +++ b/pyquil/api/_quantum_processors.py @@ -0,0 +1,30 @@ +############################################################################## +# Copyright 2018 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. +############################################################################## + +from pyquil.api import Client +from pyquil.api._client import _parse_error +from pyquil.device import Device + + +# TODO(andrew): update to get new ISA and instantiate a device, +# using new endpoints (and client functions) +def get_device(client: Client, quantum_processor_id: str) -> Device: + # STUB FOR NOW ################ + with client._http_client() as client: + res = client.get(f"https://forest-server.qcs.rigetti.com/devices/{quantum_processor_id}") + if res.status_code >= 400: + raise _parse_error(res) + return Device(quantum_processor_id, res.json()["device"]) diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 6788caad2..2618a0c65 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -80,7 +80,7 @@ def __init__( :param timeout: Time limit for requests, in seconds. :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. """ - super().__init__() + super().__init__(client) if (noise_model is not None) and (gate_noise is not None or measurement_noise is not None): raise ValueError( diff --git a/pyquil/api/_wavefunction_simulator.py b/pyquil/api/_wavefunction_simulator.py index 1de2ecba0..b891cf91a 100644 --- a/pyquil/api/_wavefunction_simulator.py +++ b/pyquil/api/_wavefunction_simulator.py @@ -170,6 +170,19 @@ def _expectation(self, prep_prog: Program, operator_programs: Iterable[Program]) response = self._qvm_client.measure_expectation(request) return np.asarray(response.expectations) + def _expectation(self, prep_prog: Program, operator_programs: Iterable[Program]) -> np.ndarray: + if isinstance(operator_programs, Program): + warnings.warn( + "You have provided a Program rather than a list of Programs. The results " + "from expectation will be line-wise expectation values of the " + "operator_programs.", + SyntaxWarning, + ) + + payload = expectation_payload(prep_prog, operator_programs, self.random_seed) + response = self.client.post_json(self.client.qvm_url, payload) + return np.asarray(response.json()) + @_record_call def run_and_measure( self, diff --git a/pyquil/api/tests/test_compiler.py b/pyquil/api/tests/test_compiler.py new file mode 100644 index 000000000..5e5b50a39 --- /dev/null +++ b/pyquil/api/tests/test_compiler.py @@ -0,0 +1,53 @@ +import math + +import pytest +from _pytest.monkeypatch import MonkeyPatch + +from pyquil import Program +from pyquil.api import Client +from pyquil.api._compiler import QPUCompiler +from pyquil.device import Device +from pyquil.gates import RX, MEASURE, RZ +from pyquil.quilatom import FormalArgument +from pyquil.quilbase import DefCalibration + + +# TODO(andrew): tests +# QPUCompiler +# QVMCompiler + + +def simple_program(): + program = Program() + readout = program.declare("ro", "BIT", 3) + program += RX(math.pi / 2, 0) + program += MEASURE(0, readout[0]) + return program + + +def test_invalid_protocol(test_device: Device, monkeypatch: MonkeyPatch): + monkeypatch.setenv( + "QCS_SETTINGS_APPLICATIONS_PYQUIL_QUILC_URL", "not-http-or-tcp://example.com" + ) + client = Client() + + with pytest.raises( + ValueError, + match="Expected compiler URL 'not-http-or-tcp://example.com' to start with 'tcp://'", + ): + QPUCompiler(quantum_processor_id=test_device.name, device=test_device, client=client) + + +def test_compile_with_quilt_calibrations(compiler: QPUCompiler): + program = simple_program() + q = FormalArgument("q") + defn = DefCalibration( + "H", [], [q], [RZ(math.pi / 2, q), RX(math.pi / 2, q), RZ(math.pi / 2, q)] + ) + cals = [defn] + program._calibrations = cals + # this should more or less pass through + compilation_result = compiler.quil_to_native_quil(program, protoquil=True) + assert compilation_result.calibrations == cals + assert program.calibrations == cals + assert compilation_result == program diff --git a/pyquil/api/tests/test_quantum_computer.py b/pyquil/api/tests/test_quantum_computer.py new file mode 100644 index 000000000..829cbf964 --- /dev/null +++ b/pyquil/api/tests/test_quantum_computer.py @@ -0,0 +1,231 @@ +import numpy as np + +from pyquil import Program +from pyquil.api import QVM, QuantumComputer, get_qc, Client +from pyquil.experiment import ExperimentSetting, Experiment +from pyquil.gates import CNOT, H, RESET, RY, X +from pyquil.noise import NoiseModel +from pyquil.paulis import sX, sY, sZ +from pyquil.tests.utils import DummyCompiler + + +def test_qc_expectation(client: Client, dummy_compiler: DummyCompiler): + qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) + + # bell state program + p = Program() + p += RESET() + p += H(0) + p += CNOT(0, 1) + p.wrap_in_numshots_loop(10) + + # XX, YY, ZZ experiment + sx = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sX(0) * sX(1)) + sy = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sY(0) * sY(1)) + sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1)) + + e = Experiment(settings=[sx, sy, sz], program=p) + + results = qc.experiment(e) + + # XX expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[0].expectation, 1) + assert np.isclose(results[0].std_err, 0) + assert results[0].total_counts == 40 + + # YY expectation value for bell state |00> + |11> is -1 + assert np.isclose(results[1].expectation, -1) + assert np.isclose(results[1].std_err, 0) + assert results[1].total_counts == 40 + + # ZZ expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[2].expectation, 1) + assert np.isclose(results[2].std_err, 0) + assert results[2].total_counts == 40 + + +def test_qc_expectation_larger_lattice(client: Client, dummy_compiler: DummyCompiler): + qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) + + q0 = 2 + q1 = 3 + + # bell state program + p = Program() + p += RESET() + p += H(q0) + p += CNOT(q0, q1) + p.wrap_in_numshots_loop(10) + + # XX, YY, ZZ experiment + sx = ExperimentSetting(in_state=sZ(q0) * sZ(q1), out_operator=sX(q0) * sX(q1)) + sy = ExperimentSetting(in_state=sZ(q0) * sZ(q1), out_operator=sY(q0) * sY(q1)) + sz = ExperimentSetting(in_state=sZ(q0) * sZ(q1), out_operator=sZ(q0) * sZ(q1)) + + e = Experiment(settings=[sx, sy, sz], program=p) + + results = qc.experiment(e) + + # XX expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[0].expectation, 1) + assert np.isclose(results[0].std_err, 0) + assert results[0].total_counts == 40 + + # YY expectation value for bell state |00> + |11> is -1 + assert np.isclose(results[1].expectation, -1) + assert np.isclose(results[1].std_err, 0) + assert results[1].total_counts == 40 + + # ZZ expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[2].expectation, 1) + assert np.isclose(results[2].std_err, 0) + assert results[2].total_counts == 40 + + +def asymmetric_ro_model(qubits: list, p00: float = 0.95, p11: float = 0.90) -> NoiseModel: + aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) + aprobs = {q: aprobs for q in qubits} + return NoiseModel([], aprobs) + + +def test_qc_calibration_1q(client: Client): + # noise model with 95% symmetrized readout fidelity per qubit + noise_model = asymmetric_ro_model([0], 0.945, 0.955) + qc = get_qc("1q-qvm", client=client) + qc.qam.noise_model = noise_model + + # bell state program (doesn't matter) + p = Program() + p += RESET() + p += H(0) + p += CNOT(0, 1) + p.wrap_in_numshots_loop(10000) + + # Z experiment + sz = ExperimentSetting(in_state=sZ(0), out_operator=sZ(0)) + e = Experiment(settings=[sz], program=p) + + results = qc.calibrate(e) + + # Z expectation value should just be 1 - 2 * readout_error + np.isclose(results[0].expectation, 0.9, atol=0.01) + assert results[0].total_counts == 20000 + + +def test_qc_calibration_2q(client: Client): + # noise model with 95% symmetrized readout fidelity per qubit + noise_model = asymmetric_ro_model([0, 1], 0.945, 0.955) + qc = get_qc("2q-qvm", client=client) + qc.qam.noise_model = noise_model + + # bell state program (doesn't matter) + p = Program() + p += RESET() + p += H(0) + p += CNOT(0, 1) + p.wrap_in_numshots_loop(10000) + + # ZZ experiment + sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1)) + e = Experiment(settings=[sz], program=p) + + results = qc.calibrate(e) + + # ZZ expectation should just be (1 - 2 * readout_error_q0) * (1 - 2 * readout_error_q1) + np.isclose(results[0].expectation, 0.81, atol=0.01) + assert results[0].total_counts == 40000 + + +def test_qc_joint_expectation(client: Client, dummy_compiler: DummyCompiler): + qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) + + # |01> state program + p = Program() + p += RESET() + p += X(0) + p.wrap_in_numshots_loop(10) + + # ZZ experiment + sz = ExperimentSetting( + in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] + ) + e = Experiment(settings=[sz], program=p) + + results = qc.experiment(e) + + # ZZ expectation value for state |01> is -1 + assert np.isclose(results[0].expectation, -1) + assert np.isclose(results[0].std_err, 0) + assert results[0].total_counts == 40 + # Z0 expectation value for state |01> is -1 + assert np.isclose(results[0].additional_results[0].expectation, -1) + assert results[0].additional_results[1].total_counts == 40 + # Z1 expectation value for state |01> is 1 + assert np.isclose(results[0].additional_results[1].expectation, 1) + assert results[0].additional_results[1].total_counts == 40 + + +def test_qc_joint_calibration(client: Client): + # noise model with 95% symmetrized readout fidelity per qubit + noise_model = asymmetric_ro_model([0, 1], 0.945, 0.955) + qc = get_qc("2q-qvm", client=client) + qc.qam.noise_model = noise_model + + # |01> state program + p = Program() + p += RESET() + p += X(0) + p.wrap_in_numshots_loop(10000) + + # ZZ experiment + sz = ExperimentSetting( + in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] + ) + e = Experiment(settings=[sz], program=p) + + results = qc.experiment(e) + + # ZZ expectation value for state |01> with 95% RO fid on both qubits is about -0.81 + assert np.isclose(results[0].expectation, -0.81, atol=0.01) + assert results[0].total_counts == 40000 + # Z0 expectation value for state |01> with 95% RO fid on both qubits is about -0.9 + assert np.isclose(results[0].additional_results[0].expectation, -0.9, atol=0.01) + assert results[0].additional_results[1].total_counts == 40000 + # Z1 expectation value for state |01> with 95% RO fid on both qubits is about 0.9 + assert np.isclose(results[0].additional_results[1].expectation, 0.9, atol=0.01) + assert results[0].additional_results[1].total_counts == 40000 + + +def test_qc_expectation_on_qvm(client: Client, dummy_compiler: DummyCompiler): + # regression test for https://github.com/rigetti/forest-tutorials/issues/2 + qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) + + p = Program() + theta = p.declare("theta", "REAL") + p += RESET() + p += RY(theta, 0) + p.wrap_in_numshots_loop(10000) + + sx = ExperimentSetting(in_state=sZ(0), out_operator=sX(0)) + e = Experiment(settings=[sx], program=p) + + thetas = [-np.pi / 2, 0.0, np.pi / 2] + results = [] + + # Verify that multiple calls to qc.experiment with the same experiment backed by a QVM that + # requires_exectutable does not raise an exception. + for theta in thetas: + results.append(qc.experiment(e, memory_map={"theta": [theta]})) + + assert np.isclose(results[0][0].expectation, -1.0, atol=0.01) + assert np.isclose(results[0][0].std_err, 0) + assert results[0][0].total_counts == 20000 + + # bounds on atol and std_err here are a little loose to try and avoid test flakiness. + assert np.isclose(results[1][0].expectation, 0.0, atol=0.1) + assert results[1][0].std_err < 0.01 + assert results[1][0].total_counts == 20000 + + assert np.isclose(results[2][0].expectation, 1.0, atol=0.01) + assert np.isclose(results[2][0].std_err, 0) + assert results[2][0].total_counts == 20000 diff --git a/pyquil/device/_main.py b/pyquil/device/_main.py new file mode 100644 index 000000000..abab1a0f3 --- /dev/null +++ b/pyquil/device/_main.py @@ -0,0 +1,336 @@ +############################################################################## +# Copyright 2016-2019 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. +############################################################################## +import warnings +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple, Union + +import networkx as nx +import numpy as np + +from pyquil.device._isa import Edge, GateInfo, ISA, MeasureInfo, Qubit, isa_from_graph, isa_to_graph +from pyquil.device._specs import Specs, specs_from_graph +from pyquil.noise import NoiseModel + +PERFECT_FIDELITY = 1e0 +PERFECT_DURATION = 1 / 100 +DEFAULT_CZ_DURATION = 200 +DEFAULT_CZ_FIDELITY = 0.89 +DEFAULT_ISWAP_DURATION = 200 +DEFAULT_ISWAP_FIDELITY = 0.90 +DEFAULT_CPHASE_DURATION = 200 +DEFAULT_CPHASE_FIDELITY = 0.85 +DEFAULT_XY_DURATION = 200 +DEFAULT_XY_FIDELITY = 0.86 +DEFAULT_RX_DURATION = 50 +DEFAULT_RX_FIDELITY = 0.95 +DEFAULT_MEASURE_FIDELITY = 0.90 +DEFAULT_MEASURE_DURATION = 2000 + + +class AbstractDevice(ABC): + @abstractmethod + def qubits(self) -> List[int]: + """ + A sorted list of qubits in the device topology. + """ + + @abstractmethod + def qubit_topology(self) -> nx.Graph: + """ + The connectivity of qubits in this device given as a NetworkX graph. + """ + + @abstractmethod + def get_isa(self, oneq_type: str = "Xhalves", twoq_type: str = "CZ") -> ISA: + """ + Construct an ISA suitable for targeting by compilation. + + This will raise an exception if the requested ISA is not supported by the device. + + :param oneq_type: The family of one-qubit gates to target + :param twoq_type: The family of two-qubit gates to target + """ + + @abstractmethod + def get_specs(self) -> Optional[Specs]: + """ + Construct a Specs object required by compilation + """ + + +class Device(AbstractDevice): + """ + A device (quantum chip) that can accept programs. + + Only devices that are online will actively be + accepting new programs. In addition to the ``self._raw`` attribute, two other attributes are + optionally constructed from the entries in ``self._raw`` -- ``isa`` and ``noise_model`` -- which + should conform to the dictionary format required by the ``.from_dict()`` methods for ``ISA`` + and ``NoiseModel``, respectively. + + :ivar dict _raw: Raw JSON response from the server with additional information about the device. + :ivar ISA isa: The instruction set architecture (ISA) for the device. + :ivar NoiseModel noise_model: The noise model for the device. + """ + + # TODO(andrew): update this to take a new ISA object + def __init__(self, name: str, raw: Dict[str, Any]): + """ + :param name: name of the device + :param raw: raw JSON response from the server with additional information about this device. + """ + self.name = name + self._raw = raw + + # TODO: Introduce distinction between supported ISAs and target ISA + self._isa = ISA.from_dict(raw["isa"]) if "isa" in raw and raw["isa"] != {} else None + self.specs = Specs.from_dict(raw["specs"]) if raw.get("specs") else None + self.noise_model = ( + NoiseModel.from_dict(raw["noise_model"]) if raw.get("noise_model") else None + ) + + @property + def isa(self) -> Optional[ISA]: + warnings.warn("Accessing the static ISA is deprecated. Use `get_isa`", DeprecationWarning) + return self._isa + + def qubits(self) -> List[int]: + assert self._isa is not None + return sorted(q.id for q in self._isa.qubits if not q.dead) + + def qubit_topology(self) -> nx.Graph: + """ + The connectivity of qubits in this device given as a NetworkX graph. + """ + assert self._isa is not None + return isa_to_graph(self._isa) + + def get_specs(self) -> Optional[Specs]: + return self.specs + + def get_isa(self, oneq_type: Optional[str] = None, twoq_type: Optional[str] = None) -> ISA: + """ + Construct an ISA suitable for targeting by compilation. + + This will raise an exception if the requested ISA is not supported by the device. + """ + if oneq_type is not None or twoq_type is not None: + raise ValueError( + "oneq_type and twoq_type are both fatally deprecated. If you want to " + "make an ISA with custom gate types, you'll have to do it by hand." + ) + + def safely_get(attr: str, index: Union[int, Tuple[int, ...]], default: Any) -> Any: + if self.specs is None: + return default + + getter = getattr(self.specs, attr, None) + if getter is None: + return default + + array = getter() + if (isinstance(index, int) and index < len(array)) or index in array: + return array[index] + else: + return default + + def qubit_type_to_gates(q: Qubit) -> List[Union[GateInfo, MeasureInfo]]: + gates: List[Union[GateInfo, MeasureInfo]] = [ + MeasureInfo( + operator="MEASURE", + qubit=q.id, + target="_", + fidelity=safely_get("fROs", q.id, DEFAULT_MEASURE_FIDELITY), + duration=DEFAULT_MEASURE_DURATION, + ), + MeasureInfo( + operator="MEASURE", + qubit=q.id, + target=None, + fidelity=safely_get("fROs", q.id, DEFAULT_MEASURE_FIDELITY), + duration=DEFAULT_MEASURE_DURATION, + ), + ] + if q.type is None or "Xhalves" in q.type: + gates += [ + GateInfo( + operator="RZ", + parameters=["_"], + arguments=[q.id], + duration=PERFECT_DURATION, + fidelity=PERFECT_FIDELITY, + ), + GateInfo( + operator="RX", + parameters=[0.0], + arguments=[q.id], + duration=DEFAULT_RX_DURATION, + fidelity=PERFECT_FIDELITY, + ), + ] + gates += [ + GateInfo( + operator="RX", + parameters=[param], + arguments=[q.id], + duration=DEFAULT_RX_DURATION, + fidelity=safely_get("f1QRBs", q.id, DEFAULT_RX_FIDELITY), + ) + for param in [np.pi, -np.pi, np.pi / 2, -np.pi / 2] + ] + if q.type is not None and "WILDCARD" in q.type: + gates += [ + GateInfo( + operator="_", + parameters="_", + arguments=[q.id], + duration=PERFECT_DURATION, + fidelity=PERFECT_FIDELITY, + ) + ] + return gates + + def edge_type_to_gates(e: Edge) -> List[GateInfo]: + gates: List[GateInfo] = [] + if ( + e is None + or isinstance(e.type, str) + and "CZ" == e.type + or isinstance(e.type, list) + and "CZ" in e.type + ): + gates += [ + GateInfo( + operator="CZ", + parameters=[], + arguments=["_", "_"], + duration=DEFAULT_CZ_DURATION, + fidelity=safely_get("fCZs", tuple(e.targets), DEFAULT_CZ_FIDELITY), + ) + ] + if ( + e is None + or isinstance(e.type, str) + and "ISWAP" == e.type + or isinstance(e.type, list) + and "ISWAP" in e.type + ): + gates += [ + GateInfo( + operator="ISWAP", + parameters=[], + arguments=["_", "_"], + duration=DEFAULT_ISWAP_DURATION, + fidelity=safely_get("fISWAPs", tuple(e.targets), DEFAULT_ISWAP_FIDELITY), + ) + ] + if ( + e is None + or isinstance(e.type, str) + and "CPHASE" == e.type + or isinstance(e.type, list) + and "CPHASE" in e.type + ): + gates += [ + GateInfo( + operator="CPHASE", + parameters=["theta"], + arguments=["_", "_"], + duration=DEFAULT_CPHASE_DURATION, + fidelity=safely_get("fCPHASEs", tuple(e.targets), DEFAULT_CPHASE_FIDELITY), + ) + ] + if ( + e is None + or isinstance(e.type, str) + and "XY" == e.type + or isinstance(e.type, list) + and "XY" in e.type + ): + gates += [ + GateInfo( + operator="XY", + parameters=["theta"], + arguments=["_", "_"], + duration=DEFAULT_XY_DURATION, + fidelity=safely_get("fXYs", tuple(e.targets), DEFAULT_XY_FIDELITY), + ) + ] + if ( + e is None + or isinstance(e.type, str) + and "WILDCARD" == e.type + or isinstance(e.type, list) + and "WILDCARD" in e.type + ): + gates += [ + GateInfo( + operator="_", + parameters="_", + arguments=["_", "_"], + duration=PERFECT_DURATION, + fidelity=PERFECT_FIDELITY, + ) + ] + return gates + + assert self._isa is not None + qubits = [ + Qubit(id=q.id, type=None, dead=q.dead, gates=qubit_type_to_gates(q)) + for q in self._isa.qubits + ] + edges = [ + Edge(targets=e.targets, type=None, dead=e.dead, gates=edge_type_to_gates(e)) + for e in self._isa.edges + ] + return ISA(qubits, edges) + + def __str__(self) -> str: + return "".format(self.name) + + def __repr__(self) -> str: + return str(self) + + +class NxDevice(AbstractDevice): + """A shim over the AbstractDevice API backed by a NetworkX graph. + + A ``Device`` holds information about the physical device. + Specifically, you might want to know about connectivity, available gates, performance specs, + and more. This class implements the AbstractDevice API for devices not available via + ``get_devices()``. Instead, the user is responsible for constructing a NetworkX + graph which represents a chip topology. + """ + + def __init__(self, topology: nx.Graph) -> None: + self.topology = topology + + def qubit_topology(self) -> nx.Graph: + return self.topology + + def get_isa( + self, oneq_type: str = "Xhalves", twoq_type: Optional[Union[str, List[str]]] = None + ) -> ISA: + return isa_from_graph(self.topology, oneq_type=oneq_type, twoq_type=twoq_type) + + def get_specs(self) -> Specs: + return specs_from_graph(self.topology) + + def qubits(self) -> List[int]: + return sorted(self.topology.nodes) + + def edges(self) -> List[Tuple[Any, ...]]: + return sorted(tuple(sorted(pair)) for pair in self.topology.edges) diff --git a/pyquil/tests/test_api.py b/pyquil/tests/test_api.py new file mode 100644 index 000000000..bfa5fdd8d --- /dev/null +++ b/pyquil/tests/test_api.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +############################################################################## +# 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. +############################################################################## +import json +from math import pi + +import numpy as np +import pytest +from pytest_httpx import HTTPXMock + +from pyquil.api import QVMConnection +from pyquil.device import ISA +from pyquil.gates import CNOT, H, MEASURE, PHASE, Z, RZ, RX, CZ +from pyquil.paulis import PauliTerm +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference +from pyquil.quilbase import Halt, Declare +from pyquil.simulation.tools import program_unitary + +EMPTY_PROGRAM = Program() +BELL_STATE = Program(H(0), CNOT(0, 1)) +BELL_STATE_MEASURE = Program( + Declare("ro", "BIT", 2), + H(0), + CNOT(0, 1), + MEASURE(0, MemoryReference("ro", 0)), + MEASURE(1, MemoryReference("ro", 1)), +) +COMPILED_BELL_STATE = Program( + [ + RZ(pi / 2, 0), + RX(pi / 2, 0), + RZ(-pi / 2, 1), + RX(pi / 2, 1), + CZ(1, 0), + RZ(-pi / 2, 0), + RX(-pi / 2, 1), + RZ(pi / 2, 1), + Halt(), + ] +) +DUMMY_ISA_DICT = {"1Q": {"0": {}, "1": {}}, "2Q": {"0-1": {}}} +DUMMY_ISA = ISA.from_dict(DUMMY_ISA_DICT) + +COMPILED_BYTES_ARRAY = b"SUPER SECRET PACKAGE" +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")] + + +def test_sync_run_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): + mock_qvm = qvm + mock_endpoint = mock_qvm.client.qvm_url + httpx_mock.add_response( + method="POST", + url=mock_endpoint, + match_content=json.dumps( + { + "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", + "rng-seed": 52, + } + ).encode(), + json={"ro": [[0, 0], [1, 1]]}, + ) + + assert mock_qvm.run(BELL_STATE_MEASURE, [0, 1], trials=2) == [[0, 0], [1, 1]] + + # Test no classical addresses + assert mock_qvm.run(BELL_STATE_MEASURE, trials=2) == [[0, 0], [1, 1]] + + with pytest.raises(ValueError): + mock_qvm.run(EMPTY_PROGRAM) + + +def test_sync_run(qvm: QVMConnection): + assert qvm.run(BELL_STATE_MEASURE, [0, 1], trials=2) == [[0, 0], [1, 1]] + + # Test range as well + assert qvm.run(BELL_STATE_MEASURE, range(2), trials=2) == [[0, 0], [1, 1]] + + # Test numpy ints + assert qvm.run(BELL_STATE_MEASURE, np.arange(2), trials=2) == [[0, 0], [1, 1]] + + # Test no classical addresses + assert qvm.run(BELL_STATE_MEASURE, trials=2) == [[0, 0], [1, 1]] + + with pytest.raises(ValueError): + qvm.run(EMPTY_PROGRAM) + + +def test_sync_run_and_measure_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): + mock_qvm = qvm + mock_endpoint = mock_qvm.client.qvm_url + httpx_mock.add_response( + method="POST", + url=mock_endpoint, + match_content=json.dumps( + { + "type": "multishot-measure", + "qubits": [0, 1], + "trials": 2, + "compiled-quil": "H 0\nCNOT 0 1\n", + "rng-seed": 52, + } + ).encode(), + json=[[0, 0], [1, 1]], + ) + + assert mock_qvm.run_and_measure(BELL_STATE, [0, 1], trials=2) == [[0, 0], [1, 1]] + + with pytest.raises(ValueError): + mock_qvm.run_and_measure(EMPTY_PROGRAM, [0]) + + +def test_sync_run_and_measure(qvm): + assert qvm.run_and_measure(BELL_STATE, [0, 1], trials=2) == [[1, 1], [0, 0]] + assert qvm.run_and_measure(BELL_STATE, [0, 1]) == [[1, 1]] + + with pytest.raises(ValueError): + qvm.run_and_measure(EMPTY_PROGRAM, [0]) + + +WAVEFUNCTION_PROGRAM = Program( + Declare("ro", "BIT"), H(0), CNOT(0, 1), MEASURE(0, MemoryReference("ro")), H(0) +) + + +def test_sync_expectation_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): + mock_qvm = qvm + mock_endpoint = mock_qvm.client.qvm_url + httpx_mock.add_response( + method="POST", + url=mock_endpoint, + match_content=json.dumps( + { + "type": "expectation", + "state-preparation": BELL_STATE.out(), + "operators": ["Z 0\n", "Z 1\n", "Z 0\nZ 1\n"], + "rng-seed": 52, + } + ).encode(), + json=[0.0, 0.0, 1.0], + ) + + 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) + + z0 = PauliTerm("Z", 0) + z1 = PauliTerm("Z", 1) + z01 = z0 * z1 + result = mock_qvm.pauli_expectation(BELL_STATE, [z0, z1, z01]) + exp_expected = [0.0, 0.0, 1.0] + np.testing.assert_allclose(exp_expected, result) + + +def test_sync_expectation(qvm): + result = 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) + + +def test_sync_expectation_2(qvm): + z0 = PauliTerm("Z", 0) + z1 = PauliTerm("Z", 1) + z01 = z0 * z1 + result = qvm.pauli_expectation(BELL_STATE, [z0, z1, z01]) + exp_expected = [0.0, 0.0, 1.0] + np.testing.assert_allclose(exp_expected, result) + + +def test_sync_paulisum_expectation(qvm: QVMConnection, httpx_mock: HTTPXMock): + mock_qvm = qvm + mock_endpoint = mock_qvm.client.qvm_url + httpx_mock.add_response( + method="POST", + url=mock_endpoint, + match_content=json.dumps( + { + "type": "expectation", + "state-preparation": BELL_STATE.out(), + "operators": ["Z 0\nZ 1\n", "Z 0\n", "Z 1\n"], + "rng-seed": 52, + } + ).encode(), + json=[1.0, 0.0, 0.0], + ) + + z0 = PauliTerm("Z", 0) + z1 = PauliTerm("Z", 1) + z01 = z0 * z1 + result = mock_qvm.pauli_expectation(BELL_STATE, 1j * z01 + z0 + z1) + exp_expected = 1j + np.testing.assert_allclose(exp_expected, result) + + +def test_sync_wavefunction(qvm): + qvm.random_seed = 0 # this test uses a stochastic program and assumes we measure 0 + result = qvm.wavefunction(WAVEFUNCTION_PROGRAM) + wf_expected = np.array([0.0 + 0.0j, 0.0 + 0.0j, 0.70710678 + 0.0j, -0.70710678 + 0.0j]) + np.testing.assert_allclose(result.amplitudes, wf_expected) + + +def test_quil_to_native_quil(compiler): + response = compiler.quil_to_native_quil(BELL_STATE) + p_unitary = program_unitary(response, n_qubits=2) + compiled_p_unitary = program_unitary(COMPILED_BELL_STATE, n_qubits=2) + from pyquil.simulation.tools import scale_out_phase + + assert np.allclose(p_unitary, scale_out_phase(compiled_p_unitary, p_unitary)) + + +def test_local_rb_sequence(benchmarker): + response = benchmarker.generate_rb_sequence(2, [PHASE(np.pi / 2, 0), H(0)], seed=52) + assert [prog.out() for prog in response] == [ + "H 0\nPHASE(pi/2) 0\nH 0\nPHASE(pi/2) 0\nPHASE(pi/2) 0\n", + "H 0\nPHASE(pi/2) 0\nH 0\nPHASE(pi/2) 0\nPHASE(pi/2) 0\n", + ] + + +def test_local_conjugate_request(benchmarker): + response = benchmarker.apply_clifford_to_pauli(Program("H 0"), PauliTerm("X", 0, 1.0)) + assert isinstance(response, PauliTerm) + assert str(response) == "(1+0j)*Z0" + + +def test_apply_clifford_to_pauli(benchmarker): + response = benchmarker.apply_clifford_to_pauli(Program("H 0"), PauliTerm("I", 0, 0.34)) + assert response == PauliTerm("I", 0, 0.34) diff --git a/pyquil/tests/test_qvm.py b/pyquil/tests/test_qvm.py new file mode 100644 index 000000000..d24bff050 --- /dev/null +++ b/pyquil/tests/test_qvm.py @@ -0,0 +1,145 @@ +import numpy as np +import pytest + +from pyquil import Program +from pyquil.api import QVM, Client +from pyquil.api._errors import QVMError +from pyquil.api._qvm import validate_noise_probabilities, validate_qubit_list, prepare_register_list +from pyquil.gates import MEASURE, X +from pyquil.quilbase import Declare, MemoryReference + + +def test_qvm__default_client(): + qvm = QVM() + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + + +def test_qvm_run_pqer(client: Client): + qvm = QVM(client=client, gate_noise=(0.01, 0.01, 0.01)) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + assert np.mean(bitstrings) > 0.8 + + +def test_qvm_run_just_program(client: Client): + qvm = QVM(client=client, gate_noise=(0.01, 0.01, 0.01)) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + assert np.mean(bitstrings) > 0.8 + + +def test_qvm_run_only_pqer(client: Client): + qvm = QVM(client=client, gate_noise=(0.01, 0.01, 0.01)) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + assert np.mean(bitstrings) > 0.8 + + +def test_qvm_run_region_declared_and_measured(client: Client): + qvm = QVM(client=client) + p = Program(Declare("reg", "BIT"), X(0), MEASURE(0, MemoryReference("reg"))) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="reg") + assert bitstrings.shape == (100, 1) + + +def test_qvm_run_region_declared_not_measured(client: Client): + qvm = QVM(client=client) + p = Program(Declare("reg", "BIT"), X(0)) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="reg") + assert bitstrings.shape == (100, 0) + + +# For backwards compatibility, we support omitting the declaration for "ro" specifically +# TODO(andrew): we should remove this lenient behavior +def test_qvm_run_region_not_declared_is_measured_ro(client: Client): + qvm = QVM(client=client) + p = Program(X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (100, 1) + + +def test_qvm_run_region_not_declared_is_measured_non_ro(client: Client): + qvm = QVM(client=client) + p = Program(X(0), MEASURE(0, MemoryReference("reg"))) + + with pytest.raises(QVMError, match='Bad memory region name "reg" in MEASURE'): + qvm.load(p).run().wait() + + +def test_qvm_run_region_not_declared_not_measured_ro(client: Client): + qvm = QVM(client=client) + p = Program(X(0)) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (100, 0) + + +def test_qvm_run_region_not_declared_not_measured_non_ro(client: Client): + qvm = QVM(client=client) + p = Program(X(0)) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + assert qvm.read_memory(region_name="reg") is None + + +def test_qvm_version(client: Client): + qvm = QVM(client=client) + version = qvm.get_version_info() + + def is_a_version_string(version_string: str): + parts = version_string.split(".") + try: + map(int, parts) + except ValueError: + return False + return True + + assert is_a_version_string(version) + + +def test_validate_noise_probabilities(): + with pytest.raises(TypeError, match="noise_parameter must be a tuple"): + validate_noise_probabilities(1) + with pytest.raises(TypeError, match="noise_parameter values should all be floats"): + validate_noise_probabilities(("a", "b", "c")) + with pytest.raises(ValueError, match="noise_parameter tuple must be of length 3"): + validate_noise_probabilities((0.0, 0.0, 0.0, 0.0)) + with pytest.raises( + ValueError, + match="sum of entries in noise_parameter must be between 0 and 1 \\(inclusive\\)", + ): + validate_noise_probabilities((0.5, 0.5, 0.5)) + with pytest.raises(ValueError, match="noise_parameter values should all be non-negative"): + validate_noise_probabilities((-0.5, -0.5, 1.0)) + + +def test_validate_qubit_list(): + with pytest.raises(TypeError): + validate_qubit_list([-1, 1]) + with pytest.raises(TypeError): + validate_qubit_list(["a", 0], 1) + + +def test_prepare_register_list(): + with pytest.raises(TypeError): + prepare_register_list({"ro": [-1, 1]}) diff --git a/pyquil/tests/test_wavefunction_simulator.py b/pyquil/tests/test_wavefunction_simulator.py new file mode 100644 index 000000000..5b1d033a8 --- /dev/null +++ b/pyquil/tests/test_wavefunction_simulator.py @@ -0,0 +1,59 @@ +import numpy as np + +import pytest + +from pyquil import Program +from pyquil.gates import H, CNOT + +from pyquil.api import WavefunctionSimulator, Client +from pyquil.paulis import PauliSum, sZ, sX + + +def test_wavefunction(client: Client): + wfnsim = WavefunctionSimulator(client=client) + bell = Program(H(0), CNOT(0, 1)) + wfn = wfnsim.wavefunction(bell) + np.testing.assert_allclose(wfn.amplitudes, 1 / np.sqrt(2) * np.array([1, 0, 0, 1])) + np.testing.assert_allclose(wfn.probabilities(), [0.5, 0, 0, 0.5]) + assert wfn.pretty_print() == "(0.71+0j)|00> + (0.71+0j)|11>" + + bitstrings = wfn.sample_bitstrings(1000) + parity = np.sum(bitstrings, axis=1) % 2 + assert np.all(parity == 0) + + +def test_random_seed(client: Client): + wfnsim = WavefunctionSimulator(client=client, random_seed=100) + assert wfnsim.random_seed == 100 + + with pytest.raises(TypeError): + WavefunctionSimulator(random_seed="NOT AN INTEGER") + + +def test_expectation(client: Client): + wfnsim = WavefunctionSimulator(client=client) + bell = Program(H(0), CNOT(0, 1)) + expects = wfnsim.expectation(bell, [sZ(0) * sZ(1), sZ(0), sZ(1), sX(0) * sX(1)]) + assert expects.size == 4 + np.testing.assert_allclose(expects, [1, 0, 0, 1]) + + pauli_sum = PauliSum([sZ(0) * sZ(1)]) + expects = wfnsim.expectation(bell, pauli_sum) + assert expects.size == 1 + np.testing.assert_allclose(expects, [1]) + + +def test_run_and_measure(client: Client): + wfnsim = WavefunctionSimulator(client=client) + bell = Program(H(0), CNOT(0, 1)) + bitstrings = wfnsim.run_and_measure(bell, trials=1000) + parity = np.sum(bitstrings, axis=1) % 2 + assert np.all(parity == 0) + + +def test_run_and_measure_qubits(client: Client): + wfnsim = WavefunctionSimulator(client=client) + bell = Program(H(0), CNOT(0, 1)) + bitstrings = wfnsim.run_and_measure(bell, qubits=[0, 100], trials=1000) + assert np.all(bitstrings[:, 1] == 0) + assert 0.4 < np.mean(bitstrings[:, 0]) < 0.6 diff --git a/pyquil/tests/utils.py b/pyquil/tests/utils.py new file mode 100644 index 000000000..4618bc394 --- /dev/null +++ b/pyquil/tests/utils.py @@ -0,0 +1,32 @@ +import os + +from pyquil.api import Client +from pyquil.device import AbstractDevice +from pyquil.parser import parse +from pyquil.api._abstract_compiler import AbstractCompiler +from pyquil import Program + + +def api_fixture_path(path: str) -> str: + dir_path = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(dir_path, "../api/tests/data", path) + + +def parse_equals(quil_string, *instructions): + expected = list(instructions) + actual = parse(quil_string) + assert expected == actual + + +class DummyCompiler(AbstractCompiler): + def __init__(self, device: AbstractDevice, client: Client): + super().__init__(device=device, client=client, timeout=10) # type: ignore + + def get_version_info(self): + return {} + + def quil_to_native_quil(self, program: Program, *, protoquil=None): + return program + + def native_quil_to_executable(self, nq_program: Program): + return nq_program diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..3d65e1389 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,44 @@ +# runtime deps +numpy==1.19.* # TODO: update to ^1.20 before v3 +scipy +lark +qcs-api-client == 0.6.2.dev1088410470 # TODO: update before v3 +requests +networkx >= 2.0.0 + + +# rigetti packages +rpcq >= 3.6.0 + +# optional latex deps +ipython + +# test deps +black==19.10b0 +coveralls +flake8 +flake8-bugbear==20.1.4 +mypy==0.740 +pytest +pytest-xdist # parallel execution +pytest-cov +pytest-timeout +pytest-rerunfailures +pytest-httpx + +# docs +sphinx>=3.0.0 +sphinx_rtd_theme +sphinx_autodoc_typehints>=1.11.0 +ipykernel +nbsphinx +recommonmark + +# pypi +twine + +# depdendency of contextvars, which we vendor +immutables==0.6 + +# pinning for mypy incompatibility +ruamel.yaml <= 0.16.5 From 1dcb94f0942d3eda388c8fbdf0d118eccb449cce Mon Sep 17 00:00:00 2001 From: Andrew Meyer Date: Wed, 17 Mar 2021 15:10:31 -0700 Subject: [PATCH 02/61] Breaking: Migrate project to poetry --- conftest.py | 15 +- pyquil/api/_abstract_compiler.py | 2 +- pyquil/api/_client.py | 4 +- pyquil/api/tests/test_compiler.py | 4 +- pyquil/api/tests/test_quantum_computer.py | 8 +- pyquil/device/_isa.py | 0 pyquil/device/qcs.py | 60 ++++++ pyquil/magic.py | 239 ++++++++++++++++++++++ pyquil/numpy_simulator.py | 29 +++ pyquil/tests/test_api.py | 7 +- pyquil/tests/test_magic.py | 83 ++++++++ 11 files changed, 423 insertions(+), 28 deletions(-) create mode 100644 pyquil/device/_isa.py create mode 100644 pyquil/device/qcs.py create mode 100644 pyquil/magic.py create mode 100644 pyquil/numpy_simulator.py create mode 100644 pyquil/tests/test_magic.py diff --git a/conftest.py b/conftest.py index 7c68eac71..4e4564526 100644 --- a/conftest.py +++ b/conftest.py @@ -126,10 +126,10 @@ def qvm(client: Client): qvm = QVMConnection(client=client, random_seed=52) qvm.run(Program(I(0)), []) return qvm - 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)) + except Exception as e: + return pytest.skip("This test requires QVM connection: {}".format(e)) @pytest.fixture() @@ -156,14 +156,9 @@ def client(): @pytest.fixture(scope="session") def benchmarker(client: Client): - try: - bm = BenchmarkConnection(client=client, timeout=2) - bm.apply_clifford_to_pauli(Program(I(0)), sX(0)) - return bm - except (RequestException, TimeoutError) as e: - return pytest.skip( - "This test requires a running local benchmarker endpoint (ie quilc): {}".format(e) - ) + bm = BenchmarkConnection(client=client, timeout=2) + bm.apply_clifford_to_pauli(Program(I(0)), sX(0)) + return bm def _str_to_bool(s): diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 472ea72bb..d7ed2d005 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -34,7 +34,7 @@ from pyquil.quil import Program from pyquil.quilatom import MemoryReference, ExpressionDesignator from pyquil.quilbase import Gate -from pyquil.version import __version__ +from pyquil._version import pyquil_version if sys.version_info < (3, 7): from rpcq.external.dataclasses import dataclass diff --git a/pyquil/api/_client.py b/pyquil/api/_client.py index aebbffee0..4911e08b7 100644 --- a/pyquil/api/_client.py +++ b/pyquil/api/_client.py @@ -190,9 +190,7 @@ def _create_engagement(self, quantum_processor_id: str) -> EngagementWithCredent ) -def _engagement_valid( - quantum_processor_id: str, engagement: Optional[EngagementWithCredentials] -) -> bool: +def _engagement_valid(quantum_processor_id: str, engagement: Optional[EngagementWithCredentials]) -> bool: if engagement is None: return False diff --git a/pyquil/api/tests/test_compiler.py b/pyquil/api/tests/test_compiler.py index 5e5b50a39..01ee4c2c0 100644 --- a/pyquil/api/tests/test_compiler.py +++ b/pyquil/api/tests/test_compiler.py @@ -41,9 +41,7 @@ def test_invalid_protocol(test_device: Device, monkeypatch: MonkeyPatch): def test_compile_with_quilt_calibrations(compiler: QPUCompiler): program = simple_program() q = FormalArgument("q") - defn = DefCalibration( - "H", [], [q], [RZ(math.pi / 2, q), RX(math.pi / 2, q), RZ(math.pi / 2, q)] - ) + defn = DefCalibration("H", [], [q], [RZ(math.pi / 2, q), RX(math.pi / 2, q), RZ(math.pi / 2, q)]) cals = [defn] program._calibrations = cals # this should more or less pass through diff --git a/pyquil/api/tests/test_quantum_computer.py b/pyquil/api/tests/test_quantum_computer.py index 829cbf964..b1d388d40 100644 --- a/pyquil/api/tests/test_quantum_computer.py +++ b/pyquil/api/tests/test_quantum_computer.py @@ -146,9 +146,7 @@ def test_qc_joint_expectation(client: Client, dummy_compiler: DummyCompiler): p.wrap_in_numshots_loop(10) # ZZ experiment - sz = ExperimentSetting( - in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] - ) + sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]]) e = Experiment(settings=[sz], program=p) results = qc.experiment(e) @@ -178,9 +176,7 @@ def test_qc_joint_calibration(client: Client): p.wrap_in_numshots_loop(10000) # ZZ experiment - sz = ExperimentSetting( - in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] - ) + sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]]) e = Experiment(settings=[sz], program=p) results = qc.experiment(e) diff --git a/pyquil/device/_isa.py b/pyquil/device/_isa.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyquil/device/qcs.py b/pyquil/device/qcs.py new file mode 100644 index 000000000..90d9c1225 --- /dev/null +++ b/pyquil/device/qcs.py @@ -0,0 +1,60 @@ +from qcs_api_client.models import InstructionSetArchitecture +from qcs_api_client.operations.sync import get_instruction_set_architecture +from pyquil.external.rpcq import CompilerISA +from pyquil.device.transformers import qcs_isa_to_compiler_isa, qcs_isa_to_graph +from pyquil.device import AbstractDevice +from pyquil.noise import NoiseModel +from pyquil.api import Client +import networkx as nx +from typing import List, Optional + + +class QCSDevice(AbstractDevice): + """ + An AbstractDevice initialized with an ``InstructionSetArchitecture`` returned + from the QCS API. Notably, this class is able to serialize a ``CompilerISA`` based + on the architecture instructions. + """ + + quantum_processor_id: str + _isa: InstructionSetArchitecture + noise_model: Optional[NoiseModel] + + def __init__( + self, + quantum_processor_id: str, + isa: InstructionSetArchitecture, + noise_model: Optional[NoiseModel] = None, + ): + """ + Initialize a new QCSDevice. + + :param quantum_processor_id: The id of the quantum processor. + :param isa: The QCS API ``InstructionSetArchitecture``. + :param noise_model: An optional ``NoiseModel`` for configuring a noisy device on the ``QVM``. + """ + + self.quantum_processor_id = quantum_processor_id + self._isa = isa + self.noise_model = noise_model + + def qubits(self) -> List[int]: + return sorted(node.node_id for node in self._isa.architecture.nodes) + + def qubit_topology(self) -> nx.Graph: + return qcs_isa_to_graph(self._isa) + + def to_compiler_isa(self) -> CompilerISA: + return qcs_isa_to_compiler_isa(self._isa) + + def __str__(self) -> str: + return "".format(self.quantum_processor_id) + + def __repr__(self) -> str: + return str(self) + + +def get_qcs_device(client: Client, quantum_processor_id: str) -> QCSDevice: + isa = client.qcs_request(get_instruction_set_architecture, quantum_processor_id=quantum_processor_id) + + return QCSDevice(quantum_processor_id=quantum_processor_id, isa=isa) diff --git a/pyquil/magic.py b/pyquil/magic.py new file mode 100644 index 000000000..a4dd7282b --- /dev/null +++ b/pyquil/magic.py @@ -0,0 +1,239 @@ +############################################################################## +# Copyright 2018 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. +############################################################################## +import ast +import sys +import functools +import inspect + +from pyquil import gates +from pyquil.quil import Program +from pyquil.quilatom import MemoryReference + +if sys.version_info < (3, 7): + from pyquil.external.contextvars import ContextVar +else: + from contextvars import ContextVar + +_program_context = ContextVar("program") + + +def program_context() -> Program: + """ + Returns the current program in context. Will only work from inside calls to @magicquil. + If called from outside @magicquil will throw a ValueError + + :return: program if one exists + """ + return _program_context.get() + + +def I(qubit) -> None: + program_context().inst(gates.I(qubit)) + + +def X(qubit) -> None: + program_context().inst(gates.X(qubit)) + + +def H(qubit) -> None: + program_context().inst(gates.H(qubit)) + + +def CNOT(qubit1, qubit2) -> None: + program_context().inst(gates.CNOT(qubit1, qubit2)) + + +def MEASURE(qubit) -> MemoryReference: + program_context().inst(gates.MEASURE(qubit, MemoryReference("ro", qubit))) + return MemoryReference("ro", qubit) + + +def _if_statement(test, if_function, else_function) -> None: + """ + Evaluate an if statement within a @magicquil block. + + If the test value is a Quil MemoryReference then unwind it into quil code equivalent to an if + then statement using jumps. Both sides of the if statement need to be evaluated and placed into + separate Programs, which is why we create new program contexts for their evaluation. + + If the test value is not a Quil MemoryReference then fall back to what Python would normally do + with an if statement. + + Params are: + if : + + else: + + + NB: This function must be named exactly _if_statement and be in scope for the ast transformer + """ + if isinstance(test, MemoryReference): + token = _program_context.set(Program()) + if_function() + if_program = _program_context.get() + _program_context.reset(token) + + if else_function: + token = _program_context.set(Program()) + else_function() + else_program = _program_context.get() + _program_context.reset(token) + else: + else_program = None + + program = _program_context.get() + program.if_then(test, if_program, else_program) + else: + if test: + if_function() + elif else_function: + else_function() + + +_EMPTY_ARGUMENTS = ast.arguments( + args=[], posonlyargs=[], vararg=None, kwonlyargs=[], kwarg=None, defaults=[], kw_defaults=[] +) + + +class _IfTransformer(ast.NodeTransformer): + """ + Transformer that unwraps the if and else branches into separate inner functions and then wraps + them in a call to _if_statement. For example: + + .. code-block:: python + + if 1 + 1 == 2: + print('math works') + else: + print('something is broken') + + would be transformed into: + + .. code-block:: python + + def _if_branch(): + print('math works') + def _else_branch(): + print('something is broken') + _if_statement(1 + 1 == 2, _if_branch, _else_branch) + """ + + def visit_If(self, node): + # Must recursively visit the body of both the if and else bodies to handle any nested if + # and else statements. This also conveniently handles elif since those are just treated + # as a nested if/else within the else branch. + # See: https://greentreesnakes.readthedocs.io/en/latest/nodes.html#If + node = self.generic_visit(node) + + if_function = ast.FunctionDef(name="_if_branch", body=node.body, decorator_list=[], args=_EMPTY_ARGUMENTS) + else_function = ast.FunctionDef(name="_else_branch", body=node.orelse, decorator_list=[], args=_EMPTY_ARGUMENTS) + + if_function_name = ast.Name(id="_if_branch", ctx=ast.Load()) + else_function_name = ast.Name(id="_else_branch", ctx=ast.Load()) + + if node.orelse: + return [ + if_function, + else_function, + ast.Expr( + ast.Call( + func=ast.Name(id="_if_statement", ctx=ast.Load()), + args=[node.test, if_function_name, else_function_name], + keywords=[], + ) + ), + ] + else: + return [ + if_function, + ast.Expr( + ast.Call( + func=ast.Name(id="_if_statement", ctx=ast.Load()), + args=[node.test, if_function_name, ast.NameConstant(None)], + keywords=[], + ) + ), + ] + + +def _rewrite_function(f): + """ + Rewrite a function so that any if/else branches are intercepted and their behavior can be + overridden. This is accomplished using 3 steps: + + 1. Get the source of the function and then rewrite the AST using _IfTransformer + 2. Do some small fixups to the tree to make sure + a) the function doesn't have the same name, and + b) the decorator isn't called recursively on the transformed function as well + 3. Bring the variables from the call site back into scope + + :param f: Function to rewrite + :return: Rewritten function + """ + source = inspect.getsource(f) + tree = ast.parse(source) + _IfTransformer().visit(tree) + + ast.fix_missing_locations(tree) + tree.body[0].name = f.__name__ + "_patched" + tree.body[0].decorator_list = [] + + compiled = compile(tree, filename="", mode="exec") + # The first f_back here gets to the body of magicquil() and the second f_back gets to the + # user's call site which is what we want. If we didn't add these manually to the globals it + # wouldn't be possible to call other @magicquil functions from within a @magicquil function. + prev_globals = inspect.currentframe().f_back.f_back.f_globals + # For reasons I don't quite understand it's critical to add locals() here otherwise the + # function will disappear and we won't be able to return it below + exec(compiled, {**prev_globals, **globals()}, locals()) + return locals()[f.__name__ + "_patched"] + + +def magicquil(f): + """ + Decorator to enable a more convenient syntax for writing quil programs. With this decorator + there is no need to keep track of a Program object and regular Python if/else branches can be + used for classical control flow. + + Example usage: + + .. code-block:: python + + @magicquil + def fast_reset(q1): + reg1 = MEASURE(q1, None) + if reg1: + X(q1) + else: + I(q1) + + my_program = fast_reset(0) # this will be a Program object + """ + rewritten_function = _rewrite_function(f) + + @functools.wraps(f) + def wrapper(*args, **kwargs): + if _program_context.get(None) is not None: + rewritten_function(*args, **kwargs) + program = _program_context.get() + else: + token = _program_context.set(Program()) + rewritten_function(*args, **kwargs) + program = _program_context.get() + _program_context.reset(token) + return program + + return wrapper diff --git a/pyquil/numpy_simulator.py b/pyquil/numpy_simulator.py new file mode 100644 index 000000000..00830922a --- /dev/null +++ b/pyquil/numpy_simulator.py @@ -0,0 +1,29 @@ +############################################################################## +# Copyright 2016-2019 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. +############################################################################## + +import warnings + +from pyquil.simulation._numpy import ( # noqa: F401 + NumpyWavefunctionSimulator, + get_measure_probabilities, + targeted_einsum, + targeted_tensordot, +) + +warnings.warn( + "The code in pyquil.numpy_simulator has been moved to pyquil.simulation, " "please update your import statements.", + FutureWarning, +) diff --git a/pyquil/tests/test_api.py b/pyquil/tests/test_api.py index bfa5fdd8d..fdcf8a3f6 100644 --- a/pyquil/tests/test_api.py +++ b/pyquil/tests/test_api.py @@ -71,8 +71,7 @@ def test_sync_run_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): "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, } ).encode(), @@ -136,9 +135,7 @@ def test_sync_run_and_measure(qvm): qvm.run_and_measure(EMPTY_PROGRAM, [0]) -WAVEFUNCTION_PROGRAM = Program( - Declare("ro", "BIT"), H(0), CNOT(0, 1), MEASURE(0, MemoryReference("ro")), H(0) -) +WAVEFUNCTION_PROGRAM = Program(Declare("ro", "BIT"), H(0), CNOT(0, 1), MEASURE(0, MemoryReference("ro")), H(0)) def test_sync_expectation_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): diff --git a/pyquil/tests/test_magic.py b/pyquil/tests/test_magic.py new file mode 100644 index 000000000..f5b24907a --- /dev/null +++ b/pyquil/tests/test_magic.py @@ -0,0 +1,83 @@ +from pyquil.magic import ( + CNOT, + H, + I, + MEASURE, + X, + Program, + magicquil, +) + + +@magicquil +def bell_state(q1, q2): + H(q1) + CNOT(q1, q2) + + +def test_bell_state(): + assert bell_state(0, 1) == Program("H 0\nCNOT 0 1") + + +@magicquil +def fast_reset(q1): + reg1 = MEASURE(q1) + if reg1: + X(q1) + else: + I(q1) + + +def test_fast_reset(): + assert fast_reset(0) == Program("DECLARE ro BIT\nMEASURE 0 ro[0]").if_then( + ("ro", 0), Program("X 0"), Program("I 0") + ) + + +@magicquil +def no_else(q1): + reg1 = MEASURE(q1) + if reg1: + X(q1) + + +def test_no_else(): + assert no_else(0) == Program("DECLARE ro BIT\nMEASURE 0 ro[0]").if_then(("ro", 0), Program("X 0")) + + +@magicquil +def with_elif(q1, q2): + reg1 = MEASURE(q1) + reg2 = MEASURE(q2) + if reg1: + X(q1) + elif reg2: + X(q2) + + +def test_with_elif(): + assert with_elif(0, 1) == Program("DECLARE ro BIT[2]\nMEASURE 0 ro[0]\nMEASURE 1 ro[1]").if_then( + ("ro", 0), Program("X 0"), Program().if_then(("ro", 1), Program("X 1")) + ) + + +@magicquil +def calls_another(q1, q2, q3): + bell_state(q1, q2) + CNOT(q2, q3) + + +def test_calls_another(): + assert calls_another(0, 1, 2) == Program("H 0\nCNOT 0 1\nCNOT 1 2") + + +@magicquil +def still_works_with_bools(): + if 1 + 1 == 2: + H(0) + else: + H(1) + + +def test_stills_works_with_bools(): + assert still_works_with_bools() == Program("H 0") From 52a18074b841a8f2e069030db6b3603687ba5610 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 27 Apr 2021 19:43:33 -0700 Subject: [PATCH 03/61] WIP: Support QVM asynchronous execution with compatibility layer --- pyquil/api/_qam.py | 89 ++++------- pyquil/api/_qvm.py | 76 ++++----- pyquil/compatibility/__init__.py | 0 pyquil/compatibility/v2/__init__.py | 1 + pyquil/compatibility/v2/api/__init__.py | 1 + pyquil/compatibility/v2/api/_qam.py | 48 ++++++ .../compatibility/v2/api/_quantum_computer.py | 9 ++ pyquil/compatibility/v2/api/_qvm.py | 8 + test/unit/test_compatibility_v2_qvm.py | 145 ++++++++++++++++++ 9 files changed, 280 insertions(+), 97 deletions(-) create mode 100644 pyquil/compatibility/__init__.py create mode 100644 pyquil/compatibility/v2/__init__.py create mode 100644 pyquil/compatibility/v2/api/__init__.py create mode 100644 pyquil/compatibility/v2/api/_qam.py create mode 100644 pyquil/compatibility/v2/api/_quantum_computer.py create mode 100644 pyquil/compatibility/v2/api/_qvm.py create mode 100644 test/unit/test_compatibility_v2_qvm.py diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index f13e631a1..391560010 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## +from dataclasses import dataclass, field import warnings from abc import ABC, abstractmethod from collections import defaultdict -from typing import Dict, Sequence, Union, Optional +from typing import Dict, Generic, Sequence, TypeVar, Union, Optional import numpy as np from rpcq.messages import ParameterAref @@ -31,44 +32,25 @@ class QAMError(RuntimeError): pass -class QAM(ABC): - """ - The platonic ideal of this class is as a generic interface describing how a classical computer - interacts with a live quantum computer. Eventually, it will turn into a thin layer over the - QPU and QVM's "QPI" interfaces. +ExecuteResponse = TypeVar("ExecuteResponse") +"""A generic parameter describing the opaque job handle returned from QAM#execute and subclasses.""" - The reality is that neither the QPU nor the QVM currently support a full-on QPI interface, - and so the undignified job of this class is to collect enough state that it can convincingly - pretend to be a QPI-compliant quantum computer. - """ - @_record_call - def __init__(self) -> None: - self._variables_shim: Dict[ParameterAref, Union[int, float]] - self.executable: Optional[QuantumExecutable] - self._memory_results: Dict[str, Optional[np.ndarray]] - self.experiment: Optional[Experiment] - self.reset() +@dataclass +class QAMMemory: + results: Dict[str, Optional[np.ndarray]] = field(default_factory=dict) + variables_shim: Dict[ParameterAref, Union[int, float]] = field(default_factory=dict) - @_record_call - def load(self, executable: QuantumExecutable) -> "QAM": + def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: """ - Initialize a QAM into a fresh state. + Reads from a memory region named ``region_name`` returned from the QAM. - :param executable: Load a compiled executable onto the QAM. + :param region_name: The string naming the declared memory region. + :return: A list of values of the appropriate type. """ - self.status: str - if self.status == "loaded": - warnings.warn("Overwriting previously loaded executable.") - assert self.status in ["connected", "done", "loaded"] - - self._variables_shim = {} - self.executable = executable - self._memory_results = defaultdict(lambda: None) - self.status = "loaded" - return self + assert self.results is not None, "No memory results available" + return self.results.get(region_name) - @_record_call def write_memory( self, *, @@ -77,10 +59,9 @@ def write_memory( offset: Optional[int] = None, ) -> "QAM": """ - Writes a value or unwraps a list of values into a memory region on - the QAM at a specified offset. + Writes a value or unwraps a list of values into a memory region at a specified offset. - :param region_name: Name of the declared memory region on the QAM. + :param region_name: Name of the declared memory region within the target program. :param offset: Integer offset into the memory region to write to. :param value: Value(s) to store at the indicated location. """ @@ -93,44 +74,32 @@ def write_memory( if isinstance(value, (int, float)): aref = ParameterAref(name=region_name, index=offset) - self._variables_shim[aref] = value + self.variables_shim[aref] = value else: for index, v in enumerate(value): aref = ParameterAref(name=region_name, index=offset + index) - self._variables_shim[aref] = v + self.variables_shim[aref] = v return self - @abstractmethod - def run(self) -> "QAM": - """ - Reset the program counter on a QAM and run its loaded Quil program. - """ - assert self.executable is not None - self.status = "running" - return self +@dataclass +class QAMExecutionResult: + executable: QuantumExecutable + memory: QAMMemory - @_record_call - def wait(self) -> "QAM": - """ - Blocks until the QPU enters the halted state. - """ - assert self.status == "running" - self.status = "done" - return self - @_record_call - def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: - """ - Reads from a memory region named region_name on the QAM. +class QAM(ABC, Generic[ExecuteResponse]): + """ + This class acts as a generic interface describing how a classical computer interacts with a + live quantum computer. + """ :param region_name: The string naming the declared memory region. :return: A list of values of the appropriate type. """ - assert self.status == "done" - assert self._memory_results is not None - return self._memory_results[region_name] + Run an executable on a Quantum Abstract Machine, returning a handle to be used to retrieve + results. @_record_call def reset(self) -> None: diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 2618a0c65..afd28d4bd 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -21,7 +21,7 @@ from pyquil._version import pyquil_version from pyquil.api import QuantumExecutable from pyquil.api._error_reporting import _record_call -from pyquil.api._qam import QAM +from pyquil.api._qam import QAM, QAMMemory, QAMExecutionResult from pyquil.api._qvm_client import ( QVMClient, RunProgramRequest, @@ -120,48 +120,27 @@ def connect(self) -> None: raise QVMNotRunning(f"No QVM server running at {self._qvm_client.base_url}") @_record_call - def get_version_info(self) -> str: - """ - Return version information for the QVM. - - :return: String with version information + def execute(self, executable: QuantumExecutable, *, memory: Optional[QAMMemory] = None) -> QVMExecuteResponse: """ - return self._qvm_client.get_version() - - @_record_call - def load(self, executable: QuantumExecutable) -> "QVM": + Synchronously execute the input program to completion. """ - Initialize a QAM and load a program to be executed with a call to :py:func:`run`. - :param executable: A compiled executable. - """ if not isinstance(executable, Program): - raise TypeError("`executable` argument must be a `Program`.") - - super().load(executable) - for region in executable.declarations.keys(): - self._memory_results[region] = np.ndarray((executable.num_shots, 0), dtype=np.int64) - return self + raise TypeError("`QVM#executable` argument must be a `Program`") - @_record_call - def run(self) -> "QVM": - """ - Run a Quil program on the QVM multiple times and return the values stored in the - classical registers designated by the classical_addresses parameter. + if memory is None: + memory = QAMMemory(results={}, variables_shim={}) - :return: An array of bitstrings of shape ``(trials, len(classical_addresses))`` - """ - super().run() - assert isinstance(self.executable, Program) + for region in executable.declarations.keys(): + memory.results[region] = np.ndarray((executable.num_shots, 0), dtype=np.int64) - quil_program = self.executable - trials = quil_program.num_shots - classical_addresses = get_classical_addresses_from_program(quil_program) + trials = executable.num_shots + classical_addresses = get_classical_addresses_from_program(executable) if self.noise_model is not None: - quil_program = apply_noise_model(quil_program, self.noise_model) + quil_program = apply_noise_model(executable, self.noise_model) - quil_program = self.augment_program_with_memory_values(quil_program) + quil_program = self.augment_program_with_memory_values(executable, variables_shim=memory.variables_shim) request = qvm_run_request( quil_program, @@ -173,14 +152,37 @@ def run(self) -> "QVM": ) response = self._qvm_client.run_program(request) ram = {key: np.array(val) for key, val in response.results.items()} - self._memory_results.update(ram) + memory.results.update(ram) - return self + return QAMExecutionResult(executable=executable, memory=memory) - def augment_program_with_memory_values(self, quil_program: Program) -> Program: + @_record_call + def get_results(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult: + """ + Return the results of execution on the QVM. + + Because QVM execution is synchronous, this is a no-op which returns its input. + """ + return cast(QAMExecutionResult, execute_response) + + @_record_call + def get_version_info(self) -> str: + """ + Return version information for the QVM. + + :return: String with version information + """ + return self._qvm_client.get_version() + + @staticmethod + def augment_program_with_memory_values(quil_program: Program, variables_shim: Dict) -> Program: + """ + Store all memory values directly within the Program source for use by the QVM. Returns + a new program and does not mutate the input. + """ p = Program() - for k, v in self._variables_shim.items(): + for k, v in variables_shim.items(): p += MOVE(MemoryReference(name=k.name, offset=k.index), v) p += quil_program diff --git a/pyquil/compatibility/__init__.py b/pyquil/compatibility/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyquil/compatibility/v2/__init__.py b/pyquil/compatibility/v2/__init__.py new file mode 100644 index 000000000..789974ea6 --- /dev/null +++ b/pyquil/compatibility/v2/__init__.py @@ -0,0 +1 @@ +from pyquil.compatibility.v2.api._quantum_computer import QuantumComputer, get_qc diff --git a/pyquil/compatibility/v2/api/__init__.py b/pyquil/compatibility/v2/api/__init__.py new file mode 100644 index 000000000..38c55691b --- /dev/null +++ b/pyquil/compatibility/v2/api/__init__.py @@ -0,0 +1 @@ +from ._qvm import QVM diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py new file mode 100644 index 000000000..15b2f5ef8 --- /dev/null +++ b/pyquil/compatibility/v2/api/_qam.py @@ -0,0 +1,48 @@ +from typing import Optional, Sequence, Union +from pyquil.api._qam import QAM, QAMMemory, QAMExecutionResult, QuantumExecutable + + +class StatefulQAM: + _memory: Optional[QAMMemory] + _loaded_executable: Optional[QuantumExecutable] + _result: Optional[QAMExecutionResult] + + @classmethod + def wrap(cls, qam: QAM) -> "StatefulQAM": + """ + Mutate the provided QAM to add methods and data for backwards compatibility. + """ + qam.__class__ = type("QAM", (qam.__class__, StatefulQAM), {}) + qam.reset() + + def load(self, executable: QuantumExecutable): + self._loaded_executable = executable + return self + + def run(self): + execute_response = self.execute(executable=self._loaded_executable, memory=self._memory) + self._result = self.get_results(execute_response) + self._memory = self._result.memory + return self + + def read_memory(self, region_name: str): + return self._memory.read_memory(region_name=region_name) + + def reset(self): + self._memory = QAMMemory() + self._loaded_executable = None + self._result = None + return self + + def wait(self): + return self + + def write_memory( + self, + *, + region_name: str, + value: Union[int, float, Sequence[int], Sequence[float]], + offset: Optional[int] = None, + ): + self._memory.write_memory(region_name=region_name, value=value, offset=offset) + return self diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py new file mode 100644 index 000000000..abf9af8ad --- /dev/null +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -0,0 +1,9 @@ +from pyquil.api._quantum_computer import QuantumComputer as QuantumComputerV3, get_qc as get_qc_v3 + + +class QuantumComputer(QuantumComputerV3): + pass + + +def get_qc(*args, **kwargs) -> QuantumComputer: + return get_qc_v3(*args, **kwargs) diff --git a/pyquil/compatibility/v2/api/_qvm.py b/pyquil/compatibility/v2/api/_qvm.py new file mode 100644 index 000000000..a473acf0b --- /dev/null +++ b/pyquil/compatibility/v2/api/_qvm.py @@ -0,0 +1,8 @@ +from pyquil.api._qvm import QVM as QVMV3 +from ._qam import StatefulQAM + + +class QVM(QVMV3): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + StatefulQAM.wrap(self) diff --git a/test/unit/test_compatibility_v2_qvm.py b/test/unit/test_compatibility_v2_qvm.py new file mode 100644 index 000000000..8e36f2d84 --- /dev/null +++ b/test/unit/test_compatibility_v2_qvm.py @@ -0,0 +1,145 @@ +import numpy as np +import pytest + +from pyquil import Program +from pyquil.compatibility.v2.api import QVM +from pyquil.api._errors import QVMError +from pyquil.api._qvm import validate_noise_probabilities, validate_qubit_list, prepare_register_list +from pyquil.api import QCSClientConfiguration +from pyquil.gates import MEASURE, X +from pyquil.quilbase import Declare, MemoryReference + + +def test_qvm__default_client(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + + +def test_qvm_run_pqer(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + assert np.mean(bitstrings) > 0.8 + + +def test_qvm_run_just_program(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + assert np.mean(bitstrings) > 0.8 + + +def test_qvm_run_only_pqer(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) + p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) + + qvm.load(p.wrap_in_numshots_loop(1000)) + qvm.run() + qvm.wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (1000, 1) + assert np.mean(bitstrings) > 0.8 + + +def test_qvm_run_region_declared_and_measured(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(Declare("reg", "BIT"), X(0), MEASURE(0, MemoryReference("reg"))) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="reg") + assert bitstrings.shape == (100, 1) + + +def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(Declare("reg", "BIT"), X(0)) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="reg") + assert bitstrings.shape == (100, 0) + + +# For backwards compatibility, we support omitting the declaration for "ro" specifically +def test_qvm_run_region_not_declared_is_measured_ro(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(X(0), MEASURE(0, MemoryReference("ro"))) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (100, 1) + + +def test_qvm_run_region_not_declared_is_measured_non_ro(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(X(0), MEASURE(0, MemoryReference("reg"))) + + with pytest.raises(QVMError, match='Bad memory region name "reg" in MEASURE'): + qvm.load(p).run().wait() + + +def test_qvm_run_region_not_declared_not_measured_ro(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(X(0)) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + bitstrings = qvm.read_memory(region_name="ro") + assert bitstrings.shape == (100, 0) + + +def test_qvm_run_region_not_declared_not_measured_non_ro(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + p = Program(X(0)) + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + assert qvm.read_memory(region_name="reg") is None + + +def test_qvm_version(client_configuration: QCSClientConfiguration): + qvm = QVM(client_configuration=client_configuration) + version = qvm.get_version_info() + + def is_a_version_string(version_string: str): + parts = version_string.split(".") + try: + map(int, parts) + except ValueError: + return False + return True + + assert is_a_version_string(version) + + +def test_validate_noise_probabilities(): + with pytest.raises(TypeError, match="noise_parameter must be a tuple"): + validate_noise_probabilities(1) + with pytest.raises(TypeError, match="noise_parameter values should all be floats"): + validate_noise_probabilities(("a", "b", "c")) + with pytest.raises(ValueError, match="noise_parameter tuple must be of length 3"): + validate_noise_probabilities((0.0, 0.0, 0.0, 0.0)) + with pytest.raises( + ValueError, + match="sum of entries in noise_parameter must be between 0 and 1 \\(inclusive\\)", + ): + validate_noise_probabilities((0.5, 0.5, 0.5)) + with pytest.raises(ValueError, match="noise_parameter values should all be non-negative"): + validate_noise_probabilities((-0.5, -0.5, 1.0)) + + +def test_validate_qubit_list(): + with pytest.raises(TypeError): + validate_qubit_list([-1, 1]) + with pytest.raises(TypeError): + validate_qubit_list(["a", 0], 1) + + +def test_prepare_register_list(): + with pytest.raises(TypeError): + prepare_register_list({"ro": [-1, 1]}) From be0cf498231ae20160a236879135cb537a15cecf Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:02:16 -0700 Subject: [PATCH 04/61] Update: store parameter values on Program, not QAM --- pyquil/api/_qam.py | 61 +++++----------------------- pyquil/api/_qvm.py | 33 ++++----------- pyquil/compatibility/v2/api/_qam.py | 42 ++++++++++--------- pyquil/quil.py | 63 +++++++++++++++++++++++++++-- 4 files changed, 99 insertions(+), 100 deletions(-) diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index 391560010..f67eefc54 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -13,15 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -from dataclasses import dataclass, field -import warnings from abc import ABC, abstractmethod -from collections import defaultdict -from typing import Dict, Generic, Sequence, TypeVar, Union, Optional +from dataclasses import dataclass, field +from typing import Dict, Generic, Optional, TypeVar import numpy as np -from rpcq.messages import ParameterAref - from pyquil.api._abstract_compiler import QuantumExecutable from pyquil.api._error_reporting import _record_call from pyquil.api._abstract_compiler import QuantumExecutable @@ -37,9 +33,9 @@ class QAMError(RuntimeError): @dataclass -class QAMMemory: - results: Dict[str, Optional[np.ndarray]] = field(default_factory=dict) - variables_shim: Dict[ParameterAref, Union[int, float]] = field(default_factory=dict) +class QAMExecutionResult: + executable: QuantumExecutable + memory: Dict[str, Optional[np.ndarray]] = field(default_factory=dict) def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: """ @@ -48,57 +44,20 @@ def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: :param region_name: The string naming the declared memory region. :return: A list of values of the appropriate type. """ - assert self.results is not None, "No memory results available" - return self.results.get(region_name) - - def write_memory( - self, - *, - region_name: str, - value: Union[int, float, Sequence[int], Sequence[float]], - offset: Optional[int] = None, - ) -> "QAM": - """ - Writes a value or unwraps a list of values into a memory region at a specified offset. - - :param region_name: Name of the declared memory region within the target program. - :param offset: Integer offset into the memory region to write to. - :param value: Value(s) to store at the indicated location. - """ - assert self.status in ["loaded", "done"] - - if offset is None: - offset = 0 - elif isinstance(value, Sequence): - warnings.warn("offset should be None when value is a Sequence") - - if isinstance(value, (int, float)): - aref = ParameterAref(name=region_name, index=offset) - self.variables_shim[aref] = value - else: - for index, v in enumerate(value): - aref = ParameterAref(name=region_name, index=offset + index) - self.variables_shim[aref] = v - - return self - - -@dataclass -class QAMExecutionResult: - executable: QuantumExecutable - memory: QAMMemory + assert self.memory is not None, "No memory results available" + return self.memory.get(region_name) class QAM(ABC, Generic[ExecuteResponse]): """ - This class acts as a generic interface describing how a classical computer interacts with a - live quantum computer. + Quantum Abstract Machine: This class acts as a generic interface describing how a classical computer interacts with + a live quantum computer. """ :param region_name: The string naming the declared memory region. :return: A list of values of the appropriate type. """ - Run an executable on a Quantum Abstract Machine, returning a handle to be used to retrieve + Run an executable on a QAM, returning a handle to be used to retrieve results. @_record_call diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index afd28d4bd..0df323e5b 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -21,12 +21,11 @@ from pyquil._version import pyquil_version from pyquil.api import QuantumExecutable from pyquil.api._error_reporting import _record_call -from pyquil.api._qam import QAM, QAMMemory, QAMExecutionResult +from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qvm_client import ( QVMClient, RunProgramRequest, ) -from pyquil.gates import MOVE from pyquil.noise import NoiseModel, apply_noise_model from pyquil.quil import Program, get_classical_addresses_from_program, percolate_declares from pyquil.quilatom import MemoryReference @@ -120,7 +119,7 @@ def connect(self) -> None: raise QVMNotRunning(f"No QVM server running at {self._qvm_client.base_url}") @_record_call - def execute(self, executable: QuantumExecutable, *, memory: Optional[QAMMemory] = None) -> QVMExecuteResponse: + def execute(self, executable: QuantumExecutable) -> QVMExecuteResponse: """ Synchronously execute the input program to completion. """ @@ -128,19 +127,18 @@ def execute(self, executable: QuantumExecutable, *, memory: Optional[QAMMemory] if not isinstance(executable, Program): raise TypeError("`QVM#executable` argument must be a `Program`") - if memory is None: - memory = QAMMemory(results={}, variables_shim={}) + result_memory = {} for region in executable.declarations.keys(): - memory.results[region] = np.ndarray((executable.num_shots, 0), dtype=np.int64) + result_memory[region] = np.ndarray((executable.num_shots, 0), dtype=np.int64) trials = executable.num_shots classical_addresses = get_classical_addresses_from_program(executable) if self.noise_model is not None: - quil_program = apply_noise_model(executable, self.noise_model) + executable = apply_noise_model(executable, self.noise_model) - quil_program = self.augment_program_with_memory_values(executable, variables_shim=memory.variables_shim) + quil_program = executable._set_parameter_values_at_runtime() request = qvm_run_request( quil_program, @@ -152,9 +150,9 @@ def execute(self, executable: QuantumExecutable, *, memory: Optional[QAMMemory] ) response = self._qvm_client.run_program(request) ram = {key: np.array(val) for key, val in response.results.items()} - memory.results.update(ram) + result_memory.update(ram) - return QAMExecutionResult(executable=executable, memory=memory) + return QAMExecutionResult(executable=executable, memory=result_memory) @_record_call def get_results(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult: @@ -174,21 +172,6 @@ def get_version_info(self) -> str: """ return self._qvm_client.get_version() - @staticmethod - def augment_program_with_memory_values(quil_program: Program, variables_shim: Dict) -> Program: - """ - Store all memory values directly within the Program source for use by the QVM. Returns - a new program and does not mutate the input. - """ - p = Program() - - for k, v in variables_shim.items(): - p += MOVE(MemoryReference(name=k.name, offset=k.index), v) - - p += quil_program - - return percolate_declares(p) - def validate_noise_probabilities(noise_parameter: Optional[Tuple[float, float, float]]) -> None: """ diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index 15b2f5ef8..fe2f9c62b 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -1,40 +1,40 @@ from typing import Optional, Sequence, Union -from pyquil.api._qam import QAM, QAMMemory, QAMExecutionResult, QuantumExecutable +from rpcq.messages import ParameterAref +from pyquil.api._qam import QAM, QAMExecutionResult, QuantumExecutable -class StatefulQAM: - _memory: Optional[QAMMemory] + +class StatefulQAM(QAM): _loaded_executable: Optional[QuantumExecutable] _result: Optional[QAMExecutionResult] @classmethod - def wrap(cls, qam: QAM) -> "StatefulQAM": + def wrap(cls, qam: QAM) -> None: """ - Mutate the provided QAM to add methods and data for backwards compatibility. + Mutate the provided QAM to add methods and data for backwards compatibility, + by dynamically mixing in this wrapper class. """ - qam.__class__ = type("QAM", (qam.__class__, StatefulQAM), {}) + qam.__class__ = type(str(qam.__class__.__name__), (StatefulQAM, qam.__class__), {}) qam.reset() - def load(self, executable: QuantumExecutable): + def load(self, executable: QuantumExecutable) -> "QAM": self._loaded_executable = executable return self - def run(self): - execute_response = self.execute(executable=self._loaded_executable, memory=self._memory) - self._result = self.get_results(execute_response) - self._memory = self._result.memory - return self - - def read_memory(self, region_name: str): - return self._memory.read_memory(region_name=region_name) + def read_memory(self, region_name: str) -> "QAM": + assert self._result is not None, "QAM#run must be called before QAM#read_memory" + return self._result.read_memory(region_name=region_name) - def reset(self): - self._memory = QAMMemory() + def reset(self) -> "QAM": self._loaded_executable = None self._result = None return self - def wait(self): + def run(self) -> "QAM": + self._result = super().run(self._loaded_executable) + return self + + def wait(self) -> "QAM": return self def write_memory( @@ -43,6 +43,8 @@ def write_memory( region_name: str, value: Union[int, float, Sequence[int], Sequence[float]], offset: Optional[int] = None, - ): - self._memory.write_memory(region_name=region_name, value=value, offset=offset) + ) -> "QAM": + assert self._loaded_executable is not None, "Executable has not been loaded yet. Call QAM#load first" + parameter_aref = ParameterAref(name=region_name, offset=offset or 0) + self._loaded_executable.set_parameter_value(parameter=parameter_aref, value=value) return self diff --git a/pyquil/quil.py b/pyquil/quil.py index 234a0e287..c1d769e9c 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -37,10 +37,10 @@ ) import numpy as np -from rpcq.messages import NativeQuilMetadata +from rpcq.messages import NativeQuilMetadata, ParameterAref from pyquil._parser.parser import run_parser -from pyquil.gates import MEASURE, RESET +from pyquil.gates import MEASURE, RESET, MOVE from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas, pauli_kraus_map from pyquil.quilatom import ( Label, @@ -110,8 +110,9 @@ ] -class Program(object): - """A list of pyQuil instructions that comprise a quantum program. +class Program: + """ + A list of pyQuil instructions that comprise a quantum program. >>> from pyquil import Program >>> from pyquil.gates import H, CNOT @@ -120,6 +121,8 @@ class Program(object): >>> p += CNOT(0, 1) """ + _variable_values: Dict[ParameterAref, Union[int, float]] + def __init__(self, *instructions: InstructionDesignator): self._defined_gates: List[DefGate] = [] @@ -148,6 +151,8 @@ def __init__(self, *instructions: InstructionDesignator): # default number of shots to loop through self.num_shots = 1 + self._variable_values = {} + # Note to developers: Have you changed this method? Have you changed the fields which # live on `Program`? Please update `Program.copy()`! @@ -186,6 +191,7 @@ def copy_everything_except_instructions(self) -> "Program": # TODO: remove this type: ignore once rpcq._base.Message gets type hints. new_prog.native_quil_metadata = self.native_quil_metadata.copy() # type: ignore new_prog.num_shots = self.num_shots + new_prog._variable_values = {k.replace(): v for k, v in self._variable_values.items()} return new_prog def copy(self) -> "Program": @@ -467,6 +473,55 @@ def measure_all(self, *qubit_reg_pairs: Tuple[QubitDesignator, Optional[MemoryRe self.inst(MEASURE(qubit_index, classical_reg)) return self + def with_parameter_values(self, parameter_values: Dict[Union[str, ParameterAref], Union[int, float]]) -> "Program": + """ + Return a copy of this program with the given parameter values set. + """ + program = self.copy() + for parameter, parameter_value in parameter_values.items(): + program.set_parameter_value(parameter=parameter, value=parameter_value) + return program + + def set_parameter_value( + self, + *, + parameter: Union[ParameterAref, str], + value: Union[int, float, Sequence[int], Sequence[float]], + ) -> "Program": + """ + Mutate the program to set the given parameter value. + + :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. + :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list + is provided, parameter must be a ``str`` or ``parameter.offset == 0``. + """ + if isinstance(parameter, str): + parameter = ParameterAref(name=parameter, index=0) + + if isinstance(value, (int, float)): + self._variable_values[parameter] = value + elif isinstance(value, Sequence): + if parameter.offset != 0: + raise ValueError("Parameter may not have a non-zero offset when its value is a sequence") + + for index, v in enumerate(value): + aref = ParameterAref(name=parameter.name, index=index) + self._variable_values[aref] = v + + return self + + def _set_parameter_values_at_runtime(self) -> "Program": + """ + Store all parameter values directly within the Program using ``MOVE`` instructions. Mutates the receiver. + """ + move_instructions = [ + MOVE(MemoryReference(name=k.name, offset=k.index), v) for k, v in self._variable_values.items() + ] + + self._instructions = [*move_instructions, *self.instructions] + + return percolate_declares(self) + def while_do(self, classical_reg: MemoryReferenceDesignator, q_program: "Program") -> "Program": """ While a classical register at index classical_reg is 1, loop q_program From 94ba3e68e81248289b9feab04b1a3ce9789cd7e5 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:07:46 -0700 Subject: [PATCH 05/61] Tests: Update QVM Tests --- test/unit/test_qvm.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/test/unit/test_qvm.py b/test/unit/test_qvm.py index 7aa290c17..aa3d453a0 100644 --- a/test/unit/test_qvm.py +++ b/test/unit/test_qvm.py @@ -13,20 +13,16 @@ def test_qvm__default_client(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") + result = qvm.run(p.wrap_in_numshots_loop(1000)) + bitstrings = result.read_memory(region_name="ro") assert bitstrings.shape == (1000, 1) def test_qvm_run_pqer(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") + result = qvm.run(p.wrap_in_numshots_loop(1000)) + bitstrings = result.read_memory(region_name="ro") assert bitstrings.shape == (1000, 1) assert np.mean(bitstrings) > 0.8 @@ -34,10 +30,8 @@ def test_qvm_run_pqer(client_configuration: QCSClientConfiguration): def test_qvm_run_just_program(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") + result = qvm.run(p.wrap_in_numshots_loop(1000)) + bitstrings = result.read_memory(region_name="ro") assert bitstrings.shape == (1000, 1) assert np.mean(bitstrings) > 0.8 @@ -46,10 +40,8 @@ def test_qvm_run_only_pqer(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") + result = qvm.run(p.wrap_in_numshots_loop(1000)) + bitstrings = result.read_memory(region_name="ro") assert bitstrings.shape == (1000, 1) assert np.mean(bitstrings) > 0.8 @@ -57,16 +49,16 @@ def test_qvm_run_only_pqer(client_configuration: QCSClientConfiguration): def test_qvm_run_region_declared_and_measured(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(Declare("reg", "BIT"), X(0), MEASURE(0, MemoryReference("reg"))) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="reg") + result = qvm.run(p.wrap_in_numshots_loop(100)) + bitstrings = result.read_memory(region_name="reg") assert bitstrings.shape == (100, 1) def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(Declare("reg", "BIT"), X(0)) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="reg") + result = qvm.run(p.wrap_in_numshots_loop(100)) + bitstrings = result.read_memory(region_name="reg") assert bitstrings.shape == (100, 0) From 0cba8763f484b7ceabae602918907e162f10367b Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 28 Apr 2021 22:02:23 -0700 Subject: [PATCH 06/61] WIP: refactor PyQVM to support new QAM API --- pyquil/pyqvm.py | 80 ++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 1e4b9e71a..4149daf7b 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -20,7 +20,7 @@ import numpy as np from numpy.random.mtrand import RandomState -from pyquil.api import QAM, QuantumExecutable +from pyquil.api import QAM, QuantumExecutable, QAMExecutionResult from pyquil.paulis import PauliTerm, PauliSum from pyquil.quil import Program from pyquil.quilatom import Label, LabelPlaceholder, MemoryReference @@ -211,26 +211,6 @@ def __init__( self.wf_simulator = quantum_simulator_type(n_qubits=n_qubits, rs=self.rs) self._last_measure_program_loc = None - def load(self, executable: QuantumExecutable) -> "PyQVM": - if not isinstance(executable, Program): - raise TypeError("`executable` argument must be a `Program`.") - - # initialize program counter - self.program = executable - self.program_counter = 0 - self._memory_results = {} - - # clear RAM, although it's not strictly clear if this should happen here - self.ram = {} - # if we're clearing RAM, we ought to clear the WF too - self.wf_simulator.reset() - - # grab the gate definitions for future use - self._extract_defined_gates() - - self.status = "loaded" - return self - def _extract_defined_gates(self) -> None: self.defined_gates = dict() assert self.program is not None @@ -247,11 +227,30 @@ def write_memory(self, *, region_name: str, offset: int = 0, value: int = 0) -> self.ram[region_name][offset] = value return self - def run(self) -> "PyQVM": - self.status = "running" + def execute(self, executable: QuantumExecutable) -> "PyQVM": + """ + Execute a program on the PyQVM. + + Note that this subclass QAM is stateful! Subsequent calls to :py:func:`execute` will not + automatically reset the wavefunction or the classical RAM. If this is desired, + consider starting your program with ``RESET``. + + :return: ``self`` to support method chaining. + """ + if not isinstance(executable, Program): + raise TypeError("`executable` argument must be a `Program`") + + self.program = executable + self.program_counter = 0 + self._memory_results = {} + + self.ram = {} + self.wf_simulator.reset() + + # grab the gate definitions for future use + self._extract_defined_gates() self._memory_results = {} - assert self.program is not None for _ in range(self.program.num_shots): self.wf_simulator.reset() self._execute_program() @@ -259,15 +258,14 @@ def run(self) -> "PyQVM": self._memory_results.setdefault(name, list()) self._memory_results[name].append(self.ram[name]) - # TODO: this will need to be removed in merge conflict with #873 - self._bitstrings = self._memory_results["ro"] + self._bitstrings = self._memory_results.get("ro") return self - def wait(self) -> "PyQVM": - assert self.status == "running" - self.status = "done" - return self + def get_results(self, execute_response: "PyQVM") -> QAMExecutionResult: + return QAMExecutionResult( + executable=self.program.copy(), memory={k: v for k, v in self._memory_results.items()} + ) def read_memory(self, *, region_name: str) -> np.ndarray: assert self._memory_results is not None @@ -469,16 +467,16 @@ def _execute_program(self) -> "PyQVM": return self - def execute(self, program: Program) -> "PyQVM": - """ - Execute one outer loop of a program on the QVM. + # def execute(self, program: Program) -> "PyQVM": + # """ + # Execute one outer loop of a program on the QVM. - Note that the QAM is stateful. Subsequent calls to :py:func:`execute` will not - automatically reset the wavefunction or the classical RAM. If this is desired, - consider starting your program with ``RESET``. + # Note that the QAM is stateful. Subsequent calls to :py:func:`execute` will not + # automatically reset the wavefunction or the classical RAM. If this is desired, + # consider starting your program with ``RESET``. - :return: ``self`` to support method chaining. - """ - self.program = program - self._extract_defined_gates() - return self._execute_program() + # :return: ``self`` to support method chaining. + # """ + # self.program = program + # self._extract_defined_gates() + # return self._execute_program() From bda7d678415de461f3a1802252acdb245d8799c7 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 28 Apr 2021 22:03:07 -0700 Subject: [PATCH 07/61] Update: QPU supports new QAM API --- pyquil/api/__init__.py | 3 +- pyquil/api/_qpu.py | 83 +++++++------------ pyquil/api/_quantum_computer.py | 30 +++---- pyquil/compatibility/v2/api/_qpu.py | 8 ++ .../compatibility/v2/api/_quantum_computer.py | 5 +- 5 files changed, 54 insertions(+), 75 deletions(-) create mode 100644 pyquil/compatibility/v2/api/_qpu.py diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py index 9612f3caf..9fb6a9028 100644 --- a/pyquil/api/__init__.py +++ b/pyquil/api/__init__.py @@ -35,6 +35,7 @@ "QVM", "QPU", "BenchmarkConnection", + "QAMExecutionResult", ] from qcs_api_client.client import QCSClientConfiguration @@ -43,7 +44,7 @@ from pyquil.api._compiler import QVMCompiler, QPUCompiler, QuantumExecutable, EncryptedProgram from pyquil.api._engagement_manager import EngagementManager from pyquil.api._error_reporting import pyquil_protect -from pyquil.api._qam import QAM +from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qpu import QPU from pyquil.api._quantum_computer import ( QuantumComputer, diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index a41697150..09dc66fd1 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## +from dataclasses import dataclass import uuid import warnings from collections import defaultdict @@ -24,7 +25,7 @@ from pyquil.api import QuantumExecutable, EncryptedProgram, EngagementManager from pyquil.api._error_reporting import _record_call -from pyquil.api._qam import QAM +from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qpu_client import GetBuffersRequest, QPUClient, BufferResponse, RunProgramRequest from pyquil.quilatom import ( MemoryReference, @@ -99,7 +100,13 @@ def alloc(spec: ParameterSpec) -> np.ndarray: return regions -class QPU(QAM): +@dataclass +class QPUExecuteResponse: + job_id: str + executable: EncryptedProgram + + +class QPU(QAM[QPUExecuteResponse]): @_record_call def __init__( self, @@ -140,70 +147,38 @@ def quantum_processor_id(self) -> str: return self._qpu_client.quantum_processor_id @_record_call - def load(self, executable: QuantumExecutable) -> "QPU": - """ - Initialize a QAM into a fresh state. Load the executable and parse the expressions - in the recalculation table (if any) into pyQuil Expression objects. - - :param executable: Load a compiled executable onto the QAM. - """ - if not isinstance(executable, EncryptedProgram): - raise TypeError( - "`executable` argument must be an `EncryptedProgram`. Make " - "sure you have explicitly compiled your program via `qc.compile` " - "or `qc.compiler.native_quil_to_executable(...)` for more " - "fine-grained control." - ) + def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: + assert isinstance( + executable, EncryptedProgram + ), "QPU#execute requires an rpcq.EncryptedProgram. Create one with QuantumComputer#compile" - super().load(executable) - return self - - @_record_call - def run(self, run_priority: Optional[int] = None) -> "QPU": - """ - Run a pyquil program on the QPU. - - This formats the classified data from the QPU server by stacking measured bits into - an array of shape (trials, classical_addresses). The mapping of qubit to - classical address is backed out from MEASURE instructions in the program, so - only do measurements where there is a 1-to-1 mapping between qubits and classical - addresses. If no MEASURE instructions are present in the program, a 0-by-0 array is - returned. - - :param run_priority: The priority with which to insert jobs into the QPU queue. Lower - integers correspond to higher priority. If not specified, the QPU - object's default priority is used. - :return: The QPU object itself. - """ - super().run() - assert isinstance(self.executable, EncryptedProgram) + assert ( + executable.ro_sources is not None + ), "To run on a QPU, a program must include ``MEASURE``, ``CAPTURE``, and/or ``RAW-CAPTURE`` instructions" request = RunProgramRequest( id=str(uuid.uuid4()), - priority=run_priority if run_priority is not None else self.priority, + priority=self.priority, program=self.executable.program, patch_values=self._build_patch_values(), ) job_id = self._qpu_client.run_program(request).job_id - results = self._get_buffers(job_id) - ro_sources = self.executable.ro_sources + return QPUExecuteResponse(job_id=job_id) - self._memory_results = defaultdict(lambda: None) - if results: - extracted = _extract_memory_regions(self.executable.memory_descriptors, ro_sources, results) + @_record_call + def get_results(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult: + raw_results = self._get_buffers(execute_response.job_id) + ro_sources = execute_response.executable.ro_sources + + result_memory = {} + if raw_results is not None: + extracted = _extract_memory_regions(execute_response.executable.memory_descriptors, ro_sources, raw_results) for name, array in extracted.items(): - self._memory_results[name] = array + result_memory[name] = array elif not ro_sources: - warnings.warn( - "You are running a QPU program with no MEASURE instructions. " - "The result of this program will always be an empty array. Are " - "you sure you didn't mean to measure some of your qubits?" - ) - self._memory_results["ro"] = np.zeros((0, 0), dtype=np.int64) - - self._last_results = results + result_memory["ro"] = np.zeros((0, 0), dtype=np.int64) - return self + QAMExecutionResult(executable=execute_response.executable, results=result_memory) def _get_buffers(self, job_id: str) -> Dict[str, np.ndarray]: """ diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 0b50acb8f..02856f0cf 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -134,22 +134,22 @@ def to_compiler_isa(self) -> CompilerISA: def run( self, executable: QuantumExecutable, - memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None, ) -> np.ndarray: """ - Run a quil executable. If the executable contains declared parameters, then a memory - map must be provided, which defines the runtime values of these parameters. + Run a quil executable. All parameters in the executable must have values applied using + ``Program#with_parameter_values`` or ``Program#set_parameter_value``. - :param executable: The program to run. You are responsible for compiling this first. - :param memory_map: The mapping of declared parameters to their values. The values - are a list of floats or integers. + :param executable: The program to run, compiled as needed for its target QAM. :return: A numpy array of shape (trials, len(ro-register)) that contains 0s and 1s. """ - self.qam.load(executable) - if memory_map: - for region_name, values_list in memory_map.items(): - self.qam.write_memory(region_name=region_name, value=values_list) - return self.qam.run().wait().read_memory(region_name="ro") # type: ignore + result = self.qam.run(executable) + + # QUESTION (@ameyer) - this is consistent with the V2 API, but in that API, + # QuantumComputer#run and QAM#run return different shapes. + # + # Does it make sense to align those now and return a QAMExecutionResult, given that + # it's a breaking change, or is it best to leave this as-is for ease/convenience? + return result.read_memory(region_name="ro") @_record_call def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: @@ -419,14 +419,6 @@ def compile( return self.compiler.native_quil_to_executable(nq_program) - @_record_call - def reset(self) -> None: - """ - Reset the QuantumComputer's QAM and compiler. - """ - self.qam.reset() - self.compiler.reset() - def __str__(self) -> str: return self.name diff --git a/pyquil/compatibility/v2/api/_qpu.py b/pyquil/compatibility/v2/api/_qpu.py new file mode 100644 index 000000000..e89c2af44 --- /dev/null +++ b/pyquil/compatibility/v2/api/_qpu.py @@ -0,0 +1,8 @@ +from pyquil.api._qpu import QPU as QPUV3 +from ._qam import StatefulQAM + + +class QPU(QPUV3): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + StatefulQAM.wrap(self) diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index abf9af8ad..d6dbf10fa 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -1,4 +1,5 @@ from pyquil.api._quantum_computer import QuantumComputer as QuantumComputerV3, get_qc as get_qc_v3 +from ._qam import StatefulQAM class QuantumComputer(QuantumComputerV3): @@ -6,4 +7,6 @@ class QuantumComputer(QuantumComputerV3): def get_qc(*args, **kwargs) -> QuantumComputer: - return get_qc_v3(*args, **kwargs) + qc = get_qc_v3(*args, **kwargs) + StatefulQAM.wrap(qc.qam) + return qc From 32a631564e205ebd72c767e53647a61e87aacaf2 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 18 May 2021 19:07:21 -0700 Subject: [PATCH 08/61] WIP: Improve compatibility interfaces and update tests to match --- pyquil/api/_quantum_computer.py | 2 +- pyquil/compatibility/v2/api/_qam.py | 2 +- .../compatibility/v2/api/_quantum_computer.py | 780 +++++++++++++++++- pyquil/pyqvm.py | 27 +- pyquil/quil.py | 4 +- ...test_compatibility_operator_estimation.py} | 3 +- ...=> test_compatibility_quantum_computer.py} | 38 +- 7 files changed, 796 insertions(+), 60 deletions(-) rename test/unit/{test_operator_estimation.py => test_compatibility_operator_estimation.py} (99%) rename test/unit/{test_quantum_computer.py => test_compatibility_quantum_computer.py} (97%) diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 02856f0cf..e035d6c19 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -144,7 +144,7 @@ def run( """ result = self.qam.run(executable) - # QUESTION (@ameyer) - this is consistent with the V2 API, but in that API, + # QUESTION (for @ameyer) - this is consistent with the V2 API, but in that API, # QuantumComputer#run and QAM#run return different shapes. # # Does it make sense to align those now and return a QAMExecutionResult, given that diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index fe2f9c62b..882d4960c 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -45,6 +45,6 @@ def write_memory( offset: Optional[int] = None, ) -> "QAM": assert self._loaded_executable is not None, "Executable has not been loaded yet. Call QAM#load first" - parameter_aref = ParameterAref(name=region_name, offset=offset or 0) + parameter_aref = ParameterAref(name=region_name, index=offset or 0) self._loaded_executable.set_parameter_value(parameter=parameter_aref, value=value) return self diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index d6dbf10fa..bdb3c56a2 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -1,12 +1,780 @@ -from pyquil.api._quantum_computer import QuantumComputer as QuantumComputerV3, get_qc as get_qc_v3 +############################################################################## +# Copyright 2021 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. +############################################################################## +import itertools +import warnings +from math import log, pi +from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Union, cast + +import numpy as np +from pyquil.api._compiler import AbstractCompiler +from pyquil.api._qam import QAM +from pyquil.api._qpu import QPU +from pyquil.api._quantum_computer import QuantumComputer as QuantumComputerV3 +from pyquil.api._quantum_computer import get_qc as get_qc_v3 +from pyquil.api._qvm import QVM +from pyquil.experiment._main import Experiment +from pyquil.experiment._memory import merge_memory_map_lists +from pyquil.experiment._result import ExperimentResult, bitstrings_to_expectations +from pyquil.experiment._setting import ExperimentSetting +from pyquil.gates import MEASURE, RX +from pyquil.paulis import PauliTerm +from pyquil.quil import Program, validate_supported_quil +from pyquil.quilatom import qubit_index +from qcs_api_client.client import QCSClientConfiguration +from rpcq.messages import PyQuilExecutableResponse, QuiltBinaryExecutableResponse + from ._qam import StatefulQAM +ExecutableDesignator = Union[QuiltBinaryExecutableResponse, PyQuilExecutableResponse] + class QuantumComputer(QuantumComputerV3): - pass + compiler: AbstractCompiler + qam: StatefulQAM + + def __init__( + self, + *, + name: str, + qam: QAM, + device: Any = None, + compiler: AbstractCompiler, + symmetrize_readout: bool = False, + ) -> None: + """ + An interface designed to ease migration from pyQuil v2 to v3, and compatible with most + use cases for the pyQuil v2 QuantumComputer. + + A quantum computer for running quantum programs. + + A quantum computer has various characteristics like supported gates, qubits, qubit + topologies, gate fidelities, and more. A quantum computer also has the ability to + run quantum programs. + + A quantum computer can be a real Rigetti QPU that uses superconducting transmon + qubits to run quantum programs, or it can be an emulator like the Rigetti QVM with + noise models and mimicked topologies. + + :param name: A string identifying this particular quantum computer. + :param qam: A quantum abstract machine which handles executing quantum programs. This + dispatches to a QVM or QPU. + :param device: Ignored and accepted only for backwards compatibility. + :param symmetrize_readout: Whether to apply readout error symmetrization. See + :py:func:`run_symmetrized_readout` for a complete description. + """ + self.name = name + self.qam = qam + StatefulQAM.wrap(self.qam) + self.compiler = compiler + + self.symmetrize_readout = symmetrize_readout + + def run( + self, + executable: ExecutableDesignator, + memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None, + ) -> np.ndarray: + """ + Run a quil executable. If the executable contains declared parameters, then a memory + map must be provided, which defines the runtime values of these parameters. + + :param executable: The program to run. You are responsible for compiling this first. + :param memory_map: The mapping of declared parameters to their values. The values + are a list of floats or integers. + :return: A numpy array of shape (trials, len(ro-register)) that contains 0s and 1s. + """ + self.qam.load(executable) + if memory_map: + for region_name, values_list in memory_map.items(): + self.qam.write_memory(region_name=region_name, value=values_list) + return self.qam.run().read_memory(region_name="ro") + + def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: + """ + Perform readout calibration on the various multi-qubit observables involved in the provided + ``Experiment``. + + :param experiment: The ``Experiment`` to calibrate readout error for. + :return: A list of ``ExperimentResult`` objects that contain the expectation values that + correspond to the scale factors resulting from symmetric readout error. + """ + calibration_experiment = experiment.generate_calibration_experiment() + return cast(List[ExperimentResult], self.experiment(calibration_experiment)) + + def experiment( + self, + experiment: Experiment, + memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None, + ) -> List[ExperimentResult]: + """ + Run an ``Experiment`` on a QVM or QPU backend. An ``Experiment`` is composed of: + + - A main ``Program`` body (or ansatz). + - A collection of ``ExperimentSetting`` objects, each of which encodes a particular + state preparation and measurement. + - A ``SymmetrizationLevel`` for enacting different readout symmetrization strategies. + - A number of shots to collect for each (unsymmetrized) ``ExperimentSetting``. + + Because the main ``Program`` is static from run to run of an ``Experiment``, we can leverage + our platform's Parametric Compilation feature. This means that the ``Program`` can be + compiled only once, and the various alterations due to state preparation, measurement, + and symmetrization can all be realized at runtime by providing a ``memory_map``. Thus, the + steps in the ``experiment`` method are as follows: + + 1. Check to see if this ``Experiment`` has already been loaded into this + ``QuantumComputer`` object. If so, skip to step 2. Otherwise, do the following: + + a. Generate a parameterized program corresponding to the ``Experiment`` + (see the ``Experiment.generate_experiment_program()`` method for more + details on how it changes the main body program to support state preparation, + measurement, and symmetrization). + b. Compile the parameterized program into a parametric (binary) executable, which + contains declared variables that can be assigned at runtime. + + 2. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following: + + a. Build a collection of memory maps that correspond to the various state + preparation, measurement, and symmetrization specifications. + b. Run the parametric executable on the QVM or QPU backend, providing the memory map + to assign variables at runtime. + c. Extract the desired statistics from the classified bitstrings that are produced + by the QVM or QPU backend, and package them in an ``ExperimentResult`` object. + + 3. Return the list of ``ExperimentResult`` objects. + + This method is extremely useful shorthand for running near-term applications and algorithms, + which often have this ansatz + settings structure. + + :param experiment: The ``Experiment`` to run. + :param memory_map: A dictionary mapping declared variables / parameters to their values. + The values are a list of floats or integers. Each float or integer corresponds to + a particular classical memory register. The memory map provided to the ``experiment`` + method corresponds to variables in the main body program that we would like to change + at runtime (e.g. the variational parameters provided to the ansatz of the variational + quantum eigensolver). + :return: A list of ``ExperimentResult`` objects containing the statistics gathered + according to the specifications of the ``Experiment``. + """ + executable = self.qam._loaded_executable + # if this experiment was the last experiment run on this QuantumComputer, + # then use the executable that is already loaded into the object + if executable is None or self.qam._experiment != experiment: + experiment_program = experiment.generate_experiment_program() + executable = self.compile(experiment_program) + self.qam._experiment = experiment + elif isinstance(self.qam, QVM) and isinstance(executable, Program) and self.qam.requires_executable: + executable = self.compiler.native_quil_to_executable(executable) + + if memory_map is None: + memory_map = {} + + results = [] + for settings in experiment: + # TODO: add support for grouped ExperimentSettings + if len(settings) > 1: + raise ValueError("We only support length-1 settings for now.") + setting = settings[0] + + qubits = cast(List[int], setting.out_operator.get_qubits()) + experiment_setting_memory_map = experiment.build_setting_memory_map(setting) + symmetrization_memory_maps = experiment.build_symmetrization_memory_maps(qubits) + merged_memory_maps = merge_memory_map_lists([experiment_setting_memory_map], symmetrization_memory_maps) + + all_bitstrings = [] + # TODO: accomplish symmetrization via batch endpoint + for merged_memory_map in merged_memory_maps: + final_memory_map = {**memory_map, **merged_memory_map} + bitstrings = self.run(executable, memory_map=final_memory_map) + + if "symmetrization" in final_memory_map: + bitmask = np.array(np.array(final_memory_map["symmetrization"]) / np.pi, dtype=int) + bitstrings = np.bitwise_xor(bitstrings, bitmask) + all_bitstrings.append(bitstrings) + symmetrized_bitstrings = np.concatenate(all_bitstrings) + + joint_expectations = [experiment.get_meas_registers(qubits)] + if setting.additional_expectations: + joint_expectations += setting.additional_expectations + expectations = bitstrings_to_expectations(symmetrized_bitstrings, joint_expectations=joint_expectations) + + means = np.mean(expectations, axis=0) + std_errs = np.std(expectations, axis=0, ddof=1) / np.sqrt(len(expectations)) + + joint_results = [] + for qubit_subset, mean, std_err in zip(joint_expectations, means, std_errs): + out_operator = PauliTerm.from_list([(setting.out_operator[i], i) for i in qubit_subset]) + s = ExperimentSetting( + in_state=setting.in_state, + out_operator=out_operator, + additional_expectations=None, + ) + r = ExperimentResult(setting=s, expectation=mean, std_err=std_err, total_counts=len(expectations)) + joint_results.append(r) + + result = ExperimentResult( + setting=setting, + expectation=joint_results[0].expectation, + std_err=joint_results[0].std_err, + total_counts=joint_results[0].total_counts, + additional_results=joint_results[1:], + ) + results.append(result) + return results + + def run_symmetrized_readout( + self, + program: Program, + trials: int, + symm_type: int = 3, + meas_qubits: Optional[List[int]] = None, + ) -> np.ndarray: + r""" + Run a quil program in such a way that the readout error is made symmetric. Enforcing + symmetric readout error is useful in simplifying the assumptions in some near + term error mitigation strategies, see ``measure_observables`` for more information. + + The simplest example is for one qubit. In a noisy device, the probability of accurately + reading the 0 state might be higher than that of the 1 state; due to e.g. amplitude + damping. This makes correcting for readout more difficult. In the simplest case, this + function runs the program normally ``(trials//2)`` times. The other half of the time, + it will insert an ``X`` gate prior to any ``MEASURE`` instruction and then flip the + measured classical bit back. Overall this has the effect of symmetrizing the readout error. + + The details. Consider preparing the input bitstring ``|i>`` (in the computational basis) and + measuring in the Z basis. Then the Confusion matrix for the readout error is specified by + the probabilities + + p(j|i) := Pr(measured = j | prepared = i ). + + In the case of a single qubit i,j \in [0,1] then: + there is no readout error if p(0|0) = p(1|1) = 1. + the readout error is symmetric if p(0|0) = p(1|1) = 1 - epsilon. + the readout error is asymmetric if p(0|0) != p(1|1). + + If your quantum computer has this kind of asymmetric readout error then + ``qc.run_symmetrized_readout`` will symmetrize the readout error. + + The readout error above is only asymmetric on a single bit. In practice the confusion + matrix on n bits need not be symmetric, e.g. for two qubits p(ij|ij) != 1 - epsilon for + all i,j. In these situations a more sophisticated means of symmetrization is needed; and + we use orthogonal arrays (OA) built from Hadamard matrices. + + The symmetrization types are specified by an int; the types available are: + -1 -- exhaustive symmetrization uses every possible combination of flips + 0 -- trivial that is no symmetrization + 1 -- symmetrization using an OA with strength 1 + 2 -- symmetrization using an OA with strength 2 + 3 -- symmetrization using an OA with strength 3 + In the context of readout symmetrization the strength of the orthogonal array enforces + the symmetry of the marginal confusion matrices. + + By default a strength 3 OA is used; this ensures expectations of the form + ```` for bits any bits i,j,k will have symmetric readout errors. Here + expectation of a random variable x as is denote `` = sum_i Pr(i) x_i``. It turns out that + a strength 3 OA is also a strength 2 and strength 1 OA it also ensures ```` and + ```` have symmetric readout errors for any bits b_j and b_i. + + :param program: The program to run symmetrized readout on. + :param trials: The minimum number of times to run the program; it is recommend that this + number should be in the hundreds or thousands. This parameter will be mutated if + necessary. + :param symm_type: the type of symmetrization + :param meas_qubits: An advanced feature. The groups of measurement qubits. Only these + qubits will be symmetrized over, even if the program acts on other qubits. + :return: A numpy array of shape (trials, len(ro-register)) that contains 0s and 1s. + """ + if not isinstance(symm_type, int): + raise ValueError( + "Symmetrization options are indicated by an int. See " "the docstrings for more information." + ) + + if meas_qubits is None: + meas_qubits = list(cast(Set[int], program.get_qubits())) + + # It is desirable to have hundreds or thousands of trials more than the minimum + trials = _check_min_num_trials_for_symmetrized_readout(len(meas_qubits), trials, symm_type) + + sym_programs, flip_arrays = _symmetrization(program, meas_qubits, symm_type) + + # Floor division so e.g. 9 // 8 = 1 and 17 // 8 = 2. + num_shots_per_prog = trials // len(sym_programs) + + if num_shots_per_prog * len(sym_programs) < trials: + warnings.warn( + f"The number of trials was modified from {trials} to " + f"{num_shots_per_prog * len(sym_programs)}. To be consistent with the " + f"number of trials required by the type of readout symmetrization " + f"chosen." + ) + + results = _measure_bitstrings(self, sym_programs, meas_qubits, num_shots_per_prog) + + return _consolidate_symmetrization_outputs(results, flip_arrays) + + def run_and_measure(self, program: Program, trials: int) -> Dict[int, np.ndarray]: + """ + Run the provided state preparation program and measure all qubits. + + The returned data is a dictionary keyed by qubit index because qubits for a given + QuantumComputer may be non-contiguous and non-zero-indexed. To turn this dictionary + into a 2d numpy array of bitstrings, consider:: + + bitstrings = qc.run_and_measure(...) + bitstring_array = np.vstack([bitstrings[q] for q in qc.qubits()]).T + bitstring_array.shape # (trials, len(qc.qubits())) + + .. note:: + + If the target :py:class:`QuantumComputer` is a noiseless :py:class:`QVM` then + only the qubits explicitly used in the program will be measured. Otherwise all + qubits will be measured. In some circumstances this can exhaust the memory + available to the simulator, and this may be manifested by the QVM failing to + respond or timeout. + + .. note:: + + In contrast to :py:class:`QVMConnection.run_and_measure`, this method simulates + noise correctly for noisy QVMs. However, this method is slower for ``trials > 1``. + For faster noise-free simulation, consider + :py:class:`WavefunctionSimulator.run_and_measure`. + + :param program: The state preparation program to run and then measure. + :param trials: The number of times to run the program. + :return: A dictionary keyed by qubit index where the corresponding value is a 1D array of + measured bits. + """ + program = program.copy() + validate_supported_quil(program) + ro = program.declare("ro", "BIT", len(self.qubits())) + measure_used = isinstance(self.qam, QVM) and self.qam.noise_model is None + qubits_to_measure = set(map(qubit_index, program.get_qubits()) if measure_used else self.qubits()) + for i, q in enumerate(qubits_to_measure): + program.inst(MEASURE(q, ro[i])) + program.wrap_in_numshots_loop(trials) + executable = self.compile(program) + bitstring_array = self.run(executable=executable) + bitstring_dict = {} + for i, q in enumerate(qubits_to_measure): + bitstring_dict[q] = bitstring_array[:, i] + for q in set(self.qubits()) - set(qubits_to_measure): + bitstring_dict[q] = np.zeros(trials) + return bitstring_dict + + def compile( + self, + program: Program, + to_native_gates: bool = True, + optimize: bool = True, + *, + protoquil: Optional[bool] = None, + ) -> ExecutableDesignator: + """ + A high-level interface to program compilation. + + Compilation currently consists of two stages. Please see the :py:class:`AbstractCompiler` + docs for more information. This function does all stages of compilation. + + Right now both ``to_native_gates`` and ``optimize`` must be either both set or both + unset. More modular compilation passes may be available in the future. + + Additionally, a call to compile also calls the ``reset`` method if one is running + on the QPU. This is a bit of a sneaky hack to guard against stale compiler connections, + but shouldn't result in any material hit to performance (especially when taking advantage + of parametric compilation for hybrid applications). + + :param program: A Program + :param to_native_gates: Whether to compile non-native gates to native gates. + :param optimize: Whether to optimize the program to reduce the number of operations. + :param protoquil: Whether to restrict the input program to and the compiled program + to protoquil (executable on QPU). A value of ``None`` means defer to server. + :return: An executable binary suitable for passing to :py:func:`QuantumComputer.run`. + """ + + if isinstance(self.qam, QPU): + self.reset() + + flags = [to_native_gates, optimize] + assert all(flags) or all(not f for f in flags), "Must turn quilc all on or all off" + quilc = all(flags) + + if quilc: + nq_program = self.compiler.quil_to_native_quil(program, protoquil=protoquil) + else: + nq_program = program + binary = self.compiler.native_quil_to_executable(nq_program) + return binary + + def reset(self) -> None: + """ + Reset the QuantumComputer's QAM to its initial state. + """ + self.qam.reset() + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return f'QuantumComputer[name="{self.name}"]' + + +def get_qc( + name: str, + *, + as_qvm: Optional[bool] = None, + noisy: Optional[bool] = None, + connection: Any = None, + compiler_timeout: float = 10, + client_configuration: Optional[QCSClientConfiguration] = None, +) -> QuantumComputer: + """ + Compatibility layer to build a QuantumComputer supporting the pyQuil v2 API. + """ + if connection is not None: + raise ValueError( + "`get_qc` no longer supports the `connection` parameter. " + "Please update your code to use the current interface of `pyquil.get_qc`." + ) + + qc = get_qc_v3( + name=name, as_qvm=as_qvm, noisy=noisy, timeout=compiler_timeout, client_configuration=client_configuration + ) + + qc = cast(QuantumComputerV3, qc) + + return QuantumComputer( + name=qc.name, qam=qc.qam, device=None, compiler=qc.compiler, symmetrize_readout=qc.symmetrize_readout + ) + + +def _flip_array_to_prog(flip_array: Tuple[bool], qubits: List[int]) -> Program: + """ + Generate a pre-measurement program that flips the qubit state according to the flip_array of + bools. + + This is used, for example, in symmetrization to produce programs which flip a select subset + of qubits immediately before measurement. + + :param flip_array: tuple of booleans specifying whether the qubit in the corresponding index + should be flipped or not. + :param qubits: list specifying the qubits in order corresponding to the flip_array + :return: Program which flips each qubit (i.e. instructs RX(pi, q)) according to the flip_array. + """ + assert len(flip_array) == len(qubits), "Mismatch of qubits and operations" + prog = Program() + for qubit, flip_output in zip(qubits, flip_array): + if flip_output == 0: + continue + elif flip_output == 1: + prog += Program(RX(pi, qubit)) + else: + raise ValueError("flip_bools should only consist of 0s and/or 1s") + return prog + + +def _symmetrization( + program: Program, meas_qubits: List[int], symm_type: int = 3 +) -> Tuple[List[Program], List[np.ndarray]]: + """ + For the input program generate new programs which flip the measured qubits with an X gate in + certain combinations in order to symmetrize readout. + + An expanded list of programs is returned along with a list of bools which indicates which + qubits are flipped in each program. + + The symmetrization types are specified by an int; the types available are: + + * -1 -- exhaustive symmetrization uses every possible combination of flips + * 0 -- trivial that is no symmetrization + * 1 -- symmetrization using an OA with strength 1 + * 2 -- symmetrization using an OA with strength 2 + * 3 -- symmetrization using an OA with strength 3 + + In the context of readout symmetrization the strength of the orthogonal array enforces the + symmetry of the marginal confusion matrices. + + By default a strength 3 OA is used; this ensures expectations of the form + for bits any bits i,j,k will have symmetric readout errors. Here expectation of a random + variable x as is denote = sum_i Pr(i) x_i. It turns out that a strength 3 OA is also a + strength 2 and strength 1 OA it also ensures and have symmetric readout + errors for any bits b_j and b_i. + + :param programs: a program which will be symmetrized. + :param meas_qubits: the groups of measurement qubits. Only these qubits will be symmetrized + over, even if the program acts on other qubits. + :param sym_type: an int determining the type of symmetrization performed. + :return: a list of symmetrized programs, the corresponding array of bools indicating which + qubits were flipped. + """ + if symm_type < -1 or symm_type > 3: + raise ValueError("symm_type must be one of the following ints [-1, 0, 1, 2, 3].") + elif symm_type == -1: + # exhaustive = all possible binary strings + flip_matrix = np.asarray(list(itertools.product([0, 1], repeat=len(meas_qubits)))) + elif symm_type >= 0: + flip_matrix = _construct_orthogonal_array(len(meas_qubits), symm_type) + + # The next part is not rigorous in the sense that we simply truncate to the desired + # number of qubits. The problem is that orthogonal arrays of a certain strength for an + # arbitrary number of qubits are not known to exist. + flip_matrix = flip_matrix[:, : len(meas_qubits)] + + symm_programs = [] + flip_arrays = [] + for flip_array in flip_matrix: + total_prog_symm = program.copy() + prog_symm = _flip_array_to_prog(flip_array, meas_qubits) + total_prog_symm += prog_symm + symm_programs.append(total_prog_symm) + flip_arrays.append(flip_array) + + return symm_programs, flip_arrays + + +def _consolidate_symmetrization_outputs(outputs: List[np.ndarray], flip_arrays: List[Tuple[bool]]) -> np.ndarray: + """ + Given bitarray results from a series of symmetrization programs, appropriately flip output + bits and consolidate results into new bitarrays. + + :param outputs: a list of the raw bitarrays resulting from running a list of symmetrized + programs; for example, the results returned from _measure_bitstrings + :param flip_arrays: a list of boolean arrays in one-to-one correspondence with the list of + outputs indicating which qubits where flipped before each bitarray was measured. + :return: an np.ndarray consisting of the consolidated bitarray outputs which can be treated as + the symmetrized outputs of the original programs passed into a symmetrization method. See + estimate_observables for example usage. + """ + assert len(outputs) == len(flip_arrays) + + output = [] + for bitarray, flip_array in zip(outputs, flip_arrays): + if len(flip_array) == 0: + output.append(bitarray) + else: + output.append(bitarray ^ flip_array) + + return np.vstack(output) + + +def _measure_bitstrings( + qc: QuantumComputer, programs: List[Program], meas_qubits: List[int], num_shots: int = 600 +) -> List[np.ndarray]: + """ + Wrapper for appending measure instructions onto each program, running the program, + and accumulating the resulting bitarrays. + + :param qc: a quantum computer object on which to run each program + :param programs: a list of programs to run + :param meas_qubits: groups of qubits to measure for each program + :param num_shots: the number of shots to run for each program + :return: a len(programs) long list of num_shots by num_meas_qubits bit arrays of results for + each program. + """ + results = [] + for program in programs: + # copy the program so the original is not mutated + prog = program.copy() + ro = prog.declare("ro", "BIT", len(meas_qubits)) + for idx, q in enumerate(meas_qubits): + prog += MEASURE(q, ro[idx]) + + prog.wrap_in_numshots_loop(num_shots) + prog = qc.compiler.quil_to_native_quil(prog) + exe = qc.compiler.native_quil_to_executable(prog) + shots = qc.run(exe) + results.append(shots) + return results + + +def _construct_orthogonal_array(num_qubits: int, strength: int = 3) -> np.ndarray: + """ + Given a strength and number of qubits this function returns an Orthogonal Array (OA) + on 'n' or more qubits. Sometimes the size of the returned array is larger than num_qubits; + typically the next power of two relative to num_qubits. This is corrected later in the code + flow. + + :param num_qubits: the minimum number of qubits the OA should act on. + :param strength: the statistical "strength" of the OA + :return: a numpy array where the rows represent the different experiments + """ + if strength < 0 or strength > 3: + raise ValueError("'strength' must be one of the following ints [0, 1, 2, 3].") + if strength == 0: + # trivial flip matrix = an array of zeros + flip_matrix = np.zeros((1, num_qubits)).astype(int) + elif strength == 1: + # orthogonal array with strength equal to 1. See Example 1.4 of [OATA], referenced in the + # `construct_strength_two_orthogonal_array` docstrings, for more details. + zero_array = np.zeros((1, num_qubits)) + one_array = np.ones((1, num_qubits)) + flip_matrix = np.concatenate((zero_array, one_array), axis=0).astype(int) + elif strength == 2: + flip_matrix = _construct_strength_two_orthogonal_array(num_qubits) + elif strength == 3: + flip_matrix = _construct_strength_three_orthogonal_array(num_qubits) + + return flip_matrix + + +def _next_power_of_2(x: int) -> int: + return cast(int, 1 if x == 0 else 2 ** (x - 1).bit_length()) + + +# The code below is directly copied from scipy see https://bit.ly/2RjAHJz, the docstrings have +# been modified. +def hadamard(n: int, dtype: np.dtype = int) -> np.ndarray: + """ + Construct a Hadamard matrix. + Constructs an n-by-n Hadamard matrix, using Sylvester's + construction. `n` must be a power of 2. + + Parameters + ---------- + n : int + The order of the matrix. `n` must be a power of 2. + dtype : numpy dtype + The data type of the array to be constructed. + + Returns + ------- + H : (n, n) ndarray + The Hadamard matrix. + + Notes + ----- + .. versionadded:: 0.8.0 + + Examples + -------- + >>> hadamard(2, dtype=complex) + array([[ 1.+0.j, 1.+0.j], + [ 1.+0.j, -1.-0.j]]) + >>> hadamard(4) + array([[ 1, 1, 1, 1], + [ 1, -1, 1, -1], + [ 1, 1, -1, -1], + [ 1, -1, -1, 1]]) + """ + if n < 1: + lg2 = 0 + else: + lg2 = int(log(n, 2)) + if 2 ** lg2 != n: + raise ValueError("n must be an positive integer, and n must be a power of 2") + + H = np.array([[1]], dtype=dtype) + + # Sylvester's construction + for _ in range(0, lg2): + H = np.vstack((np.hstack((H, H)), np.hstack((H, -H)))) + + return H + + +def _construct_strength_three_orthogonal_array(num_qubits: int) -> np.ndarray: + r""" + Given a number of qubits this function returns an Orthogonal Array (OA) + on 'n' qubits where n is the next power of two relative to num_qubits. + + Specifically it returns the OA(2n, n, 2, 3). + + The parameters of the OA(N, k, s, t) are interpreted as + N: Number of rows, level combinations or runs + k: Number of columns, constraints or factors + s: Number of symbols or levels + t: Strength + + See [OATA] for more details. + + [OATA] Orthogonal Arrays: theory and applications + Hedayat, Sloane, Stufken + Springer Science & Business Media, 2012. + https://dx.doi.org/10.1007/978-1-4612-1478-6 + + :param num_qubits: minimum number of qubits the OA should run on. + :return: A numpy array representing the OA with shape N by k + """ + num_qubits_power_of_2 = _next_power_of_2(num_qubits) + H = hadamard(num_qubits_power_of_2) + Hfold = np.concatenate((H, -H), axis=0) + orthogonal_array = ((Hfold + 1) / 2).astype(int) + return orthogonal_array + + +def _construct_strength_two_orthogonal_array(num_qubits: int) -> np.ndarray: + r""" + Given a number of qubits this function returns an Orthogonal Array (OA) on 'n-1' qubits + where n-1 is the next integer lambda so that 4*lambda -1 is larger than num_qubits. + + Specifically it returns the OA(n, n − 1, 2, 2). + + The parameters of the OA(N, k, s, t) are interpreted as + N: Number of rows, level combinations or runs + k: Number of columns, constraints or factors + s: Number of symbols or levels + t: Strength + + See [OATA] for more details. + + [OATA] Orthogonal Arrays: theory and applications + Hedayat, Sloane, Stufken + Springer Science & Business Media, 2012. + https://dx.doi.org/10.1007/978-1-4612-1478-6 + + :param num_qubits: minimum number of qubits the OA should run on. + :return: A numpy array representing the OA with shape N by k + """ + # next line will break post denali at 275 qubits + # valid_num_qubits = 4 * lambda - 1 + valid_numbers = [4 * lam - 1 for lam in range(1, 70)] + # 4 * lambda + four_lam = min(x for x in valid_numbers if x >= num_qubits) + 1 + H = hadamard(_next_power_of_2(four_lam)) + # The minus sign in front of H fixes the 0 <-> 1 inversion relative to the reference [OATA] + orthogonal_array = ((-H[1:, :].T + 1) / 2).astype(int) + return orthogonal_array + + +def _check_min_num_trials_for_symmetrized_readout(num_qubits: int, trials: int, symm_type: int) -> int: + """ + This function sets the minimum number of trials; it is desirable to have hundreds or + thousands of trials more than the minimum. + + :param num_qubits: number of qubits to symmetrize + :param trials: number of trials + :param symm_type: symmetrization type see + :return: possibly modified number of trials + """ + if symm_type < -1 or symm_type > 3: + raise ValueError("symm_type must be one of the following ints [-1, 0, 1, 2, 3].") + + if symm_type == -1: + min_num_trials = 2 ** num_qubits + elif symm_type == 2: + + def _f(x: int) -> int: + return 4 * x - 1 + min_num_trials = min(_f(x) for x in range(1, 1024) if _f(x) >= num_qubits) + 1 + elif symm_type == 3: + min_num_trials = _next_power_of_2(2 * num_qubits) + else: + # symm_type == 0 or symm_type == 1 require one and two trials respectively; ensured by: + min_num_trials = 2 -def get_qc(*args, **kwargs) -> QuantumComputer: - qc = get_qc_v3(*args, **kwargs) - StatefulQAM.wrap(qc.qam) - return qc + if trials < min_num_trials: + trials = min_num_trials + warnings.warn(f"Number of trials was too low, it is now {trials}.") + return trials diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 4149daf7b..02f8f5953 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -229,11 +229,8 @@ def write_memory(self, *, region_name: str, offset: int = 0, value: int = 0) -> def execute(self, executable: QuantumExecutable) -> "PyQVM": """ - Execute a program on the PyQVM. - - Note that this subclass QAM is stateful! Subsequent calls to :py:func:`execute` will not - automatically reset the wavefunction or the classical RAM. If this is desired, - consider starting your program with ``RESET``. + Execute a program on the PyQVM. Note that the state of the instance is reset on each + call to ``execute``. :return: ``self`` to support method chaining. """ @@ -241,7 +238,6 @@ def execute(self, executable: QuantumExecutable) -> "PyQVM": raise TypeError("`executable` argument must be a `Program`") self.program = executable - self.program_counter = 0 self._memory_results = {} self.ram = {} @@ -263,6 +259,11 @@ def execute(self, executable: QuantumExecutable) -> "PyQVM": return self def get_results(self, execute_response: "PyQVM") -> QAMExecutionResult: + """ + Return results from the PyQVM according to the common QAM API. Note that while the + ``execute_response`` is not used, it's accepted in order to conform to that API; it's + unused because the PyQVM, unlike other QAM's, is itself stateful. + """ return QAMExecutionResult( executable=self.program.copy(), memory={k: v for k, v in self._memory_results.items()} ) @@ -466,17 +467,3 @@ def _execute_program(self) -> "PyQVM": halted = self.transition() return self - - # def execute(self, program: Program) -> "PyQVM": - # """ - # Execute one outer loop of a program on the QVM. - - # Note that the QAM is stateful. Subsequent calls to :py:func:`execute` will not - # automatically reset the wavefunction or the classical RAM. If this is desired, - # consider starting your program with ``RESET``. - - # :return: ``self`` to support method chaining. - # """ - # self.program = program - # self._extract_defined_gates() - # return self._execute_program() diff --git a/pyquil/quil.py b/pyquil/quil.py index c1d769e9c..e14634fe5 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -501,8 +501,8 @@ def set_parameter_value( if isinstance(value, (int, float)): self._variable_values[parameter] = value elif isinstance(value, Sequence): - if parameter.offset != 0: - raise ValueError("Parameter may not have a non-zero offset when its value is a sequence") + if parameter.index != 0: + raise ValueError("Parameter may not have a non-zero index when its value is a sequence") for index, v in enumerate(value): aref = ParameterAref(name=parameter.name, index=index) diff --git a/test/unit/test_operator_estimation.py b/test/unit/test_compatibility_operator_estimation.py similarity index 99% rename from test/unit/test_operator_estimation.py rename to test/unit/test_compatibility_operator_estimation.py index 149dbcbc6..dd614ae9e 100644 --- a/test/unit/test_operator_estimation.py +++ b/test/unit/test_compatibility_operator_estimation.py @@ -5,9 +5,10 @@ import numpy as np import pytest -from pyquil import Program, get_qc +from pyquil import Program from pyquil.api import WavefunctionSimulator from pyquil.api import QCSClientConfiguration +from pyquil.compatibility.v2 import get_qc from pyquil.experiment import ( ExperimentSetting, SIC0, diff --git a/test/unit/test_quantum_computer.py b/test/unit/test_compatibility_quantum_computer.py similarity index 97% rename from test/unit/test_quantum_computer.py rename to test/unit/test_compatibility_quantum_computer.py index 8ed99bf29..642f57fd3 100644 --- a/test/unit/test_quantum_computer.py +++ b/test/unit/test_compatibility_quantum_computer.py @@ -1,26 +1,23 @@ import itertools import random +from test.unit.utils import DummyCompiler import networkx as nx import numpy as np import pytest -from rpcq.messages import ParameterAref - -from pyquil import Program -from pyquil import list_quantum_computers +from pyquil import Program, list_quantum_computers from pyquil.api import QCSClientConfiguration -from pyquil.api import QVM, QuantumComputer, get_qc from pyquil.api._quantum_computer import ( - _symmetrization, - _flip_array_to_prog, + _check_min_num_trials_for_symmetrized_readout, + _consolidate_symmetrization_outputs, _construct_orthogonal_array, - _construct_strength_two_orthogonal_array, _construct_strength_three_orthogonal_array, - _parse_name, + _construct_strength_two_orthogonal_array, + _flip_array_to_prog, _get_qvm_with_topology, _measure_bitstrings, - _consolidate_symmetrization_outputs, - _check_min_num_trials_for_symmetrized_readout, + _parse_name, + _symmetrization, ) from pyquil.experiment import ExperimentSetting, Experiment from pyquil.experiment._main import _pauli_to_product_state @@ -32,7 +29,7 @@ from pyquil.pyqvm import PyQVM from pyquil.quantum_processor import NxQuantumProcessor from pyquil.quilbase import Declare, MemoryReference -from test.unit.utils import DummyCompiler +from rpcq.messages import ParameterAref def test_flip_array_to_prog(): @@ -183,23 +180,6 @@ def test_check_min_num_trials_for_symmetrized_readout(): _check_min_num_trials_for_symmetrized_readout(num_qubits=2, trials=-2, symm_type=4) -def test_quantum_processor_stuff(client_configuration: QCSClientConfiguration): - topo = nx.from_edgelist([(0, 4), (0, 99)]) - qc = QuantumComputer( - name="testy!", - qam=None, # not necessary for this test - compiler=DummyCompiler( - quantum_processor=NxQuantumProcessor(topo, gates_2q=["CPHASE"]), client_configuration=client_configuration - ), - ) - assert nx.is_isomorphic(qc.qubit_topology(), topo) - - isa = qc.to_compiler_isa() - - assert isa.edges["0-4"].gates[0].operator == "CPHASE" - assert isa.edges["0-4"].ids == [0, 4] - - # We sometimes narrowly miss the np.mean(parity) < 0.15 assertion, below. Alternatively, that upper # bound could be relaxed. @pytest.mark.flaky(reruns=1) From 23436fa9afd6a2d33996fc94ee33edf4bc2e8bc8 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 16:28:45 -0700 Subject: [PATCH 09/61] Fix: rebasing mistakes --- conftest.py | 203 --------------- pyquil/api/_client.py | 241 ----------------- pyquil/api/_qam.py | 38 +-- pyquil/api/_quantum_computer.py | 6 +- pyquil/api/_quantum_processors.py | 30 --- pyquil/api/_qvm.py | 14 +- pyquil/api/_wavefunction_simulator.py | 13 - pyquil/api/tests/test_compiler.py | 51 ---- pyquil/api/tests/test_quantum_computer.py | 227 ---------------- pyquil/compatibility/v2/api/_qam.py | 5 +- .../compatibility/v2/api/_quantum_computer.py | 8 +- pyquil/magic.py | 239 ----------------- pyquil/numpy_simulator.py | 29 --- pyquil/tests/test_api.py | 242 ------------------ pyquil/tests/test_magic.py | 83 ------ pyquil/tests/test_qvm.py | 145 ----------- pyquil/tests/test_wavefunction_simulator.py | 59 ----- pyquil/tests/utils.py | 32 --- .../test_compatibility_quantum_computer.py | 61 +++-- test/unit/test_qvm.py | 6 +- 20 files changed, 82 insertions(+), 1650 deletions(-) delete mode 100644 conftest.py delete mode 100644 pyquil/api/_client.py delete mode 100644 pyquil/api/_quantum_processors.py delete mode 100644 pyquil/api/tests/test_compiler.py delete mode 100644 pyquil/api/tests/test_quantum_computer.py delete mode 100644 pyquil/magic.py delete mode 100644 pyquil/numpy_simulator.py delete mode 100644 pyquil/tests/test_api.py delete mode 100644 pyquil/tests/test_magic.py delete mode 100644 pyquil/tests/test_qvm.py delete mode 100644 pyquil/tests/test_wavefunction_simulator.py delete mode 100644 pyquil/tests/utils.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 4e4564526..000000000 --- a/conftest.py +++ /dev/null @@ -1,203 +0,0 @@ -import numpy as np -import pytest -from requests import RequestException - -from pyquil.api import ( - QVMConnection, - QVMCompiler, - Client, - BenchmarkConnection, -) -from pyquil.api._errors import UnknownApiError -from pyquil.api._abstract_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 -from pyquil.quil import Program -from pyquil.tests.utils import DummyCompiler - - -@pytest.fixture -def isa_dict(): - return { - "1Q": {"0": {"type": "Xhalves"}, "1": {}, "2": {}, "3": {"dead": True}}, - "2Q": { - "0-1": {}, - "1-2": {"type": "ISWAP"}, - "0-2": {"type": "CPHASE"}, - "0-3": {"dead": True}, - }, - } - - -@pytest.fixture -def specs_dict(): - return { - "1Q": { - "0": { - "f1QRB": 0.99, - "f1QRB_std_err": 0.01, - "f1Q_simultaneous_RB": 0.98, - "f1Q_simultaneous_RB_std_err": 0.02, - "fRO": 0.93, - "T1": 20e-6, - "T2": 15e-6, - }, - "1": { - "f1QRB": 0.989, - "f1QRB_std_err": 0.011, - "f1Q_simultaneous_RB": 0.979, - "f1Q_simultaneous_RB_std_err": 0.021, - "fRO": 0.92, - "T1": 19e-6, - "T2": 12e-6, - }, - "2": { - "f1QRB": 0.983, - "f1QRB_std_err": 0.017, - "f1Q_simultaneous_RB": 0.973, - "f1Q_simultaneous_RB_std_err": 0.027, - "fRO": 0.95, - "T1": 21e-6, - "T2": 16e-6, - }, - "3": { - "f1QRB": 0.988, - "f1QRB_std_err": 0.012, - "f1Q_simultaneous_RB": 0.978, - "f1Q_simultaneous_RB_std_err": 0.022, - "fRO": 0.94, - "T1": 18e-6, - "T2": 11e-6, - }, - }, - "2Q": { - "0-1": {"fBellState": 0.90, "fCZ": 0.89, "fCZ_std_err": 0.01, "fCPHASE": 0.88}, - "1-2": {"fBellState": 0.91, "fCZ": 0.90, "fCZ_std_err": 0.12, "fCPHASE": 0.89}, - "0-2": {"fBellState": 0.92, "fCZ": 0.91, "fCZ_std_err": 0.20, "fCPHASE": 0.90}, - "0-3": {"fBellState": 0.89, "fCZ": 0.88, "fCZ_std_err": 0.03, "fCPHASE": 0.87}, - }, - } - - -@pytest.fixture -def noise_model_dict(): - return { - "gates": [ - { - "gate": "I", - "params": (5.0,), - "targets": (0, 1), - "kraus_ops": [[[[1.0]], [[1.0]]]], - "fidelity": 1.0, - }, - { - "gate": "RX", - "params": (np.pi / 2.0,), - "targets": (0,), - "kraus_ops": [[[[1.0]], [[1.0]]]], - "fidelity": 1.0, - }, - ], - "assignment_probs": {"1": [[1.0, 0.0], [0.0, 1.0]], "0": [[1.0, 0.0], [0.0, 1.0]]}, - } - - -@pytest.fixture -def device_raw(isa_dict, noise_model_dict, specs_dict): - return { - "isa": isa_dict, - "noise_model": noise_model_dict, - "specs": specs_dict, - "is_online": True, - "is_retuning": False, - } - - -@pytest.fixture -def test_device(device_raw): - return Device("test_device", device_raw) - - -@pytest.fixture(scope="session") -def qvm(client: Client): - try: - qvm = QVMConnection(client=client, random_seed=52) - qvm.run(Program(I(0)), []) - return qvm - except QVMVersionMismatch as e: - return pytest.skip("This test requires a different version of the QVM: {}".format(e)) - except Exception as e: - return pytest.skip("This test requires QVM connection: {}".format(e)) - - -@pytest.fixture() -def compiler(test_device, client: Client): - try: - compiler = QVMCompiler(device=test_device, client=client, timeout=1) - compiler.quil_to_native_quil(Program(I(0))) - return compiler - 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() -def dummy_compiler(test_device: Device, client: Client): - return DummyCompiler(test_device, client) - - -@pytest.fixture(scope="session") -def client(): - return Client() - - -@pytest.fixture(scope="session") -def benchmarker(client: Client): - bm = BenchmarkConnection(client=client, timeout=2) - bm.apply_clifford_to_pauli(Program(I(0)), sX(0)) - return bm - - -def _str_to_bool(s): - """Convert either of the strings 'True' or 'False' to their Boolean equivalent""" - if s == "True": - return True - elif s == "False": - return False - else: - raise ValueError("Please specify either True or False") - - -def pytest_addoption(parser): - parser.addoption( - "--use-seed", - action="store", - type=_str_to_bool, - default=True, - help="run operator estimation tests faster by using a fixed random seed", - ) - parser.addoption( - "--runslow", action="store_true", default=False, help="run tests marked as being 'slow'" - ) - - -def pytest_configure(config): - config.addinivalue_line("markers", "slow: mark test as slow to run") - - -def pytest_collection_modifyitems(config, items): - if config.getoption("--runslow"): - # --runslow given in cli: do not skip slow tests - return - skip_slow = pytest.mark.skip(reason="need --runslow option to run") - for item in items: - if "slow" in item.keywords: - item.add_marker(skip_slow) - - -@pytest.fixture() -def use_seed(pytestconfig): - return pytestconfig.getoption("use_seed") diff --git a/pyquil/api/_client.py b/pyquil/api/_client.py deleted file mode 100644 index 4911e08b7..000000000 --- a/pyquil/api/_client.py +++ /dev/null @@ -1,241 +0,0 @@ -import re -from contextlib import contextmanager -from datetime import datetime -from json.decoder import JSONDecodeError -from typing import Optional, Any, Callable, Iterator, cast - -import httpx -import rpcq -from dateutil.parser import parse as parsedate -from dateutil.tz import tzutc -from qcs_api_client.client import QCSClientConfiguration, build_sync_client -from qcs_api_client.models import ( - EngagementWithCredentials, - CreateEngagementRequest, - EngagementCredentials, -) -from qcs_api_client.operations.sync import create_engagement -from qcs_api_client.types import Response -from rpcq import ClientAuthConfig - -from pyquil.api._errors import ApiError, UnknownApiError, TooManyQubitsError, error_mapping -from pyquil.api._logger import logger - - -class Client: - """ - Class for housing application configuration and interacting with network resources. - """ - - _http: Optional[httpx.Client] - _config: QCSClientConfiguration - _has_default_config: bool - _engagement: Optional[EngagementWithCredentials] = None - - def __init__( - self, - *, - http: Optional[httpx.Client] = None, - configuration: Optional[QCSClientConfiguration] = None, - ): - """ - Instantiate a client. - - :param http: optional underlying HTTP client. If none is provided, a default - client will be created using ``configuration``. - :param configuration: QCS configuration. - """ - self._config = configuration or QCSClientConfiguration.load() - self._has_default_config = configuration is None - self._http = http - - def post_json(self, url: str, json: Any, timeout: float = 5) -> httpx.Response: - """ - Post JSON to a URL. Will raise an exception for response statuses >= 400. - - :param url: URL to post to. - :param json: JSON body of request. - :param timeout: Time limit for request, in seconds. - :return: HTTP response corresponding to request. - """ - logger.debug("Sending POST request to %s. Body: %s", url, json) - with self._http_client() as http: # type: httpx.Client - res = http.post(url, json=json, timeout=timeout) - if res.status_code >= 400: - raise _parse_error(res) - return res - - def qcs_request(self, request_fn: Callable[..., Response[Any]], **kwargs: Any) -> Any: - """ - Execute a QCS request. - - :param request_fn: Request function (from ``qcs_api_client.operations.sync``). - :param kwargs: Arguments to pass to request function. - :return: HTTP response corresponding to request. - """ - with self._http_client() as http: # type: httpx.Client - return request_fn(client=http, **kwargs).parsed - - def compiler_rpcq_request( - self, method_name: str, *args: Any, timeout: Optional[float] = None, **kwargs: Any - ) -> Any: - """ - Execute a remote function against the Quil compiler. - - :param method_name: Method name. - :param args: Arguments that will be passed to the remote function. - :param timeout: Optional time limit for request, in seconds. - :param kwargs: Keyword arguments that will be passed to the remote function. - :return: Result from remote function. - """ - return self._rpcq_request(self.quilc_url, method_name, *args, timeout=timeout, **kwargs) - - def processor_rpcq_request( - self, - quantum_processor_id: str, - method_name: str, - *args: Any, - timeout: Optional[float] = None, - **kwargs: Any, - ) -> Any: - """ - Execute a remote function against a processor endpoint. If there is no current engagement, - or if it is invalid, a new engagement will be requested for the given processor. - - :param quantum_processor_id: Processor to engage. - :param method_name: Method name. - :param args: Arguments that will be passed to the remote function. - :param timeout: Optional time limit for request, in seconds. - :param kwargs: Keyword arguments that will be passed to the remote function. - :return: Result from remote function. - """ - # TODO(andrew): handle multiple engagements at once (per processor) - if not _engagement_valid(quantum_processor_id, self._engagement): - self._engagement = self._create_engagement(quantum_processor_id) - - assert self._engagement is not None - - return self._rpcq_request( - self._engagement.address, - method_name, - *args, - timeout=timeout, - auth_config=_to_auth_config(self._engagement.credentials), - **kwargs, - ) - - def _rpcq_request( - self, - endpoint: str, - method_name: str, - *args: Any, - timeout: Optional[float] = None, - auth_config: Optional[ClientAuthConfig] = None, - **kwargs: Any, - ) -> Any: - client = rpcq.Client(endpoint, auth_config=auth_config) - response = client.call(method_name, *args, rpc_timeout=timeout, **kwargs) # type: ignore - client.close() # type: ignore - return response - - def reset(self) -> None: - """ - Clears current engagement and reloads configuration (if not overridden in constructor). - """ - self._engagement = None - if self._has_default_config: - self._config = QCSClientConfiguration.load() - - @property - def qvm_url(self) -> str: - """ - QVM URL from client configuration. - """ - return self._config.profile.applications.pyquil.qvm_url - - @property - def quilc_url(self) -> str: - """ - Quil compiler URL from client configuration. - """ - return self._config.profile.applications.pyquil.quilc_url - - def qvm_version(self) -> str: - """ - Get QVM version string. - """ - response = self.post_json(self.qvm_url, {"type": "version"}) - split_version_string = response.text.split() - try: - qvm_version = split_version_string[0] - except ValueError: - raise TypeError(f"Malformed version string returned by the QVM: {response.text}") - return qvm_version - - @contextmanager - def _http_client(self) -> Iterator[httpx.Client]: - if self._http is None: - with build_sync_client(configuration=self._config) as client: # type: httpx.Client - yield client - else: - yield self._http - - def _create_engagement(self, quantum_processor_id: str) -> EngagementWithCredentials: - return cast( - EngagementWithCredentials, - self.qcs_request( - create_engagement, - json_body=CreateEngagementRequest(quantum_processor_id=quantum_processor_id), - ), - ) - - -def _engagement_valid(quantum_processor_id: str, engagement: Optional[EngagementWithCredentials]) -> bool: - if engagement is None: - return False - - return all( - [ - engagement.credentials.client_public != "", - engagement.credentials.client_secret != "", - engagement.credentials.server_public != "", - parsedate(engagement.expires_at) > datetime.now(tzutc()), - engagement.address != "", - engagement.quantum_processor_id == quantum_processor_id, - ] - ) - - -def _to_auth_config(credentials: EngagementCredentials) -> ClientAuthConfig: - return rpcq.ClientAuthConfig( - client_secret_key=credentials.client_secret.encode(), - client_public_key=credentials.client_public.encode(), - server_public_key=credentials.server_public.encode(), - ) - - -def _parse_error(res: httpx.Response) -> ApiError: - """ - Errors should contain a "status" field with a human readable explanation of - what went wrong as well as a "error_type" field indicating the kind of error that can be mapped - to a Python type. - - There's a fallback error UnknownApiError for other types of exceptions (network issues, api - gateway problems, etc.) - """ - try: - body = res.json() - except JSONDecodeError: - raise UnknownApiError(res.text) - - if "error_type" not in body: - raise UnknownApiError(str(body)) - - error_type = body["error_type"] - status = body["status"] - - if re.search(r"[0-9]+ qubits were requested, but the QVM is limited to [0-9]+ qubits.", status): - return TooManyQubitsError(status) - - error_cls = error_mapping.get(error_type, UnknownApiError) - return error_cls(status) diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index f67eefc54..dae23b447 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -20,7 +20,6 @@ import numpy as np from pyquil.api._abstract_compiler import QuantumExecutable from pyquil.api._error_reporting import _record_call -from pyquil.api._abstract_compiler import QuantumExecutable from pyquil.experiment._main import Experiment @@ -50,26 +49,33 @@ def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: class QAM(ABC, Generic[ExecuteResponse]): """ - Quantum Abstract Machine: This class acts as a generic interface describing how a classical computer interacts with - a live quantum computer. + Quantum Abstract Machine: This class acts as a generic interface describing how a classical + computer interacts with a live quantum computer. """ - :param region_name: The string naming the declared memory region. - :return: A list of values of the appropriate type. + @_record_call + def __init__(self) -> None: + self.experiment: Optional[Experiment] + + @abstractmethod + def execute(self, executable: QuantumExecutable) -> ExecuteResponse: """ Run an executable on a QAM, returning a handle to be used to retrieve results. - @_record_call - def reset(self) -> None: + :param executable: The executable program to be executed by the QAM. + """ + + @abstractmethod + def get_results(self, execute_response: ExecuteResponse) -> QAMExecutionResult: + """ + Retrieve the results associated with a previous call to ``QAM#execute``. + + :param execute_response: The return value from a call to ``execute``. + """ + + def run(self, executable: QuantumExecutable) -> QAMExecutionResult: """ - Reset the Quantum Abstract Machine to its initial state, which is particularly useful - when it has gotten into an unwanted state. This can happen, for example, if the QAM - is interrupted in the middle of a run. + Run an executable to completion on the QAM. """ - self._client.reset() - self._variables_shim = {} - self.executable = None - self._memory_results = defaultdict(lambda: None) - self.experiment = None - self.status = "connected" + return self.get_results(self.execute(executable)) diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index e035d6c19..dbec66a7a 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -46,7 +46,6 @@ from pyquil.api._qam import QAM from pyquil.api._qcs_client import qcs_client from pyquil.api._qpu import QPU -from pyquil.api._quantum_processors import get_device from pyquil.api._qvm import QVM from pyquil.experiment._main import Experiment from pyquil.experiment._memory import merge_memory_map_lists @@ -574,9 +573,10 @@ def _get_qvm_qc( execution_timeout=execution_timeout, ), compiler=QVMCompiler( - quantum_processor=quantum_processor, timeout=compiler_timeout, client_configuration=client_configuration + quantum_processor=quantum_processor, + timeout=compiler_timeout, + client_configuration=client_configuration, ), - compiler=QVMCompiler(device=device, client=client, timeout=compiler_timeout), ) diff --git a/pyquil/api/_quantum_processors.py b/pyquil/api/_quantum_processors.py deleted file mode 100644 index 13b8f574c..000000000 --- a/pyquil/api/_quantum_processors.py +++ /dev/null @@ -1,30 +0,0 @@ -############################################################################## -# Copyright 2018 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. -############################################################################## - -from pyquil.api import Client -from pyquil.api._client import _parse_error -from pyquil.device import Device - - -# TODO(andrew): update to get new ISA and instantiate a device, -# using new endpoints (and client functions) -def get_device(client: Client, quantum_processor_id: str) -> Device: - # STUB FOR NOW ################ - with client._http_client() as client: - res = client.get(f"https://forest-server.qcs.rigetti.com/devices/{quantum_processor_id}") - if res.status_code >= 400: - raise _parse_error(res) - return Device(quantum_processor_id, res.json()["device"]) diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 0df323e5b..d5d6b73f8 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -from typing import Dict, Mapping, Optional, Sequence, Union, Tuple +from typing import Dict, Mapping, Optional, Sequence, Union, Tuple, cast import numpy as np from qcs_api_client.client import QCSClientConfiguration @@ -27,8 +27,7 @@ RunProgramRequest, ) from pyquil.noise import NoiseModel, apply_noise_model -from pyquil.quil import Program, get_classical_addresses_from_program, percolate_declares -from pyquil.quilatom import MemoryReference +from pyquil.quil import Program, get_classical_addresses_from_program class QVMVersionMismatch(Exception): @@ -52,7 +51,12 @@ def check_qvm_version(version: str) -> None: ) -class QVM(QAM): +class QVMExecuteResponse: + executable: Program + memory_results: Dict[str, np.ndarray] + + +class QVM(QAM[QAMExecutionResult]): @_record_call def __init__( self, @@ -79,7 +83,7 @@ def __init__( :param timeout: Time limit for requests, in seconds. :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. """ - super().__init__(client) + super().__init__() if (noise_model is not None) and (gate_noise is not None or measurement_noise is not None): raise ValueError( diff --git a/pyquil/api/_wavefunction_simulator.py b/pyquil/api/_wavefunction_simulator.py index b891cf91a..1de2ecba0 100644 --- a/pyquil/api/_wavefunction_simulator.py +++ b/pyquil/api/_wavefunction_simulator.py @@ -170,19 +170,6 @@ def _expectation(self, prep_prog: Program, operator_programs: Iterable[Program]) response = self._qvm_client.measure_expectation(request) return np.asarray(response.expectations) - def _expectation(self, prep_prog: Program, operator_programs: Iterable[Program]) -> np.ndarray: - if isinstance(operator_programs, Program): - warnings.warn( - "You have provided a Program rather than a list of Programs. The results " - "from expectation will be line-wise expectation values of the " - "operator_programs.", - SyntaxWarning, - ) - - payload = expectation_payload(prep_prog, operator_programs, self.random_seed) - response = self.client.post_json(self.client.qvm_url, payload) - return np.asarray(response.json()) - @_record_call def run_and_measure( self, diff --git a/pyquil/api/tests/test_compiler.py b/pyquil/api/tests/test_compiler.py deleted file mode 100644 index 01ee4c2c0..000000000 --- a/pyquil/api/tests/test_compiler.py +++ /dev/null @@ -1,51 +0,0 @@ -import math - -import pytest -from _pytest.monkeypatch import MonkeyPatch - -from pyquil import Program -from pyquil.api import Client -from pyquil.api._compiler import QPUCompiler -from pyquil.device import Device -from pyquil.gates import RX, MEASURE, RZ -from pyquil.quilatom import FormalArgument -from pyquil.quilbase import DefCalibration - - -# TODO(andrew): tests -# QPUCompiler -# QVMCompiler - - -def simple_program(): - program = Program() - readout = program.declare("ro", "BIT", 3) - program += RX(math.pi / 2, 0) - program += MEASURE(0, readout[0]) - return program - - -def test_invalid_protocol(test_device: Device, monkeypatch: MonkeyPatch): - monkeypatch.setenv( - "QCS_SETTINGS_APPLICATIONS_PYQUIL_QUILC_URL", "not-http-or-tcp://example.com" - ) - client = Client() - - with pytest.raises( - ValueError, - match="Expected compiler URL 'not-http-or-tcp://example.com' to start with 'tcp://'", - ): - QPUCompiler(quantum_processor_id=test_device.name, device=test_device, client=client) - - -def test_compile_with_quilt_calibrations(compiler: QPUCompiler): - program = simple_program() - q = FormalArgument("q") - defn = DefCalibration("H", [], [q], [RZ(math.pi / 2, q), RX(math.pi / 2, q), RZ(math.pi / 2, q)]) - cals = [defn] - program._calibrations = cals - # this should more or less pass through - compilation_result = compiler.quil_to_native_quil(program, protoquil=True) - assert compilation_result.calibrations == cals - assert program.calibrations == cals - assert compilation_result == program diff --git a/pyquil/api/tests/test_quantum_computer.py b/pyquil/api/tests/test_quantum_computer.py deleted file mode 100644 index b1d388d40..000000000 --- a/pyquil/api/tests/test_quantum_computer.py +++ /dev/null @@ -1,227 +0,0 @@ -import numpy as np - -from pyquil import Program -from pyquil.api import QVM, QuantumComputer, get_qc, Client -from pyquil.experiment import ExperimentSetting, Experiment -from pyquil.gates import CNOT, H, RESET, RY, X -from pyquil.noise import NoiseModel -from pyquil.paulis import sX, sY, sZ -from pyquil.tests.utils import DummyCompiler - - -def test_qc_expectation(client: Client, dummy_compiler: DummyCompiler): - qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) - - # bell state program - p = Program() - p += RESET() - p += H(0) - p += CNOT(0, 1) - p.wrap_in_numshots_loop(10) - - # XX, YY, ZZ experiment - sx = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sX(0) * sX(1)) - sy = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sY(0) * sY(1)) - sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1)) - - e = Experiment(settings=[sx, sy, sz], program=p) - - results = qc.experiment(e) - - # XX expectation value for bell state |00> + |11> is 1 - assert np.isclose(results[0].expectation, 1) - assert np.isclose(results[0].std_err, 0) - assert results[0].total_counts == 40 - - # YY expectation value for bell state |00> + |11> is -1 - assert np.isclose(results[1].expectation, -1) - assert np.isclose(results[1].std_err, 0) - assert results[1].total_counts == 40 - - # ZZ expectation value for bell state |00> + |11> is 1 - assert np.isclose(results[2].expectation, 1) - assert np.isclose(results[2].std_err, 0) - assert results[2].total_counts == 40 - - -def test_qc_expectation_larger_lattice(client: Client, dummy_compiler: DummyCompiler): - qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) - - q0 = 2 - q1 = 3 - - # bell state program - p = Program() - p += RESET() - p += H(q0) - p += CNOT(q0, q1) - p.wrap_in_numshots_loop(10) - - # XX, YY, ZZ experiment - sx = ExperimentSetting(in_state=sZ(q0) * sZ(q1), out_operator=sX(q0) * sX(q1)) - sy = ExperimentSetting(in_state=sZ(q0) * sZ(q1), out_operator=sY(q0) * sY(q1)) - sz = ExperimentSetting(in_state=sZ(q0) * sZ(q1), out_operator=sZ(q0) * sZ(q1)) - - e = Experiment(settings=[sx, sy, sz], program=p) - - results = qc.experiment(e) - - # XX expectation value for bell state |00> + |11> is 1 - assert np.isclose(results[0].expectation, 1) - assert np.isclose(results[0].std_err, 0) - assert results[0].total_counts == 40 - - # YY expectation value for bell state |00> + |11> is -1 - assert np.isclose(results[1].expectation, -1) - assert np.isclose(results[1].std_err, 0) - assert results[1].total_counts == 40 - - # ZZ expectation value for bell state |00> + |11> is 1 - assert np.isclose(results[2].expectation, 1) - assert np.isclose(results[2].std_err, 0) - assert results[2].total_counts == 40 - - -def asymmetric_ro_model(qubits: list, p00: float = 0.95, p11: float = 0.90) -> NoiseModel: - aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) - aprobs = {q: aprobs for q in qubits} - return NoiseModel([], aprobs) - - -def test_qc_calibration_1q(client: Client): - # noise model with 95% symmetrized readout fidelity per qubit - noise_model = asymmetric_ro_model([0], 0.945, 0.955) - qc = get_qc("1q-qvm", client=client) - qc.qam.noise_model = noise_model - - # bell state program (doesn't matter) - p = Program() - p += RESET() - p += H(0) - p += CNOT(0, 1) - p.wrap_in_numshots_loop(10000) - - # Z experiment - sz = ExperimentSetting(in_state=sZ(0), out_operator=sZ(0)) - e = Experiment(settings=[sz], program=p) - - results = qc.calibrate(e) - - # Z expectation value should just be 1 - 2 * readout_error - np.isclose(results[0].expectation, 0.9, atol=0.01) - assert results[0].total_counts == 20000 - - -def test_qc_calibration_2q(client: Client): - # noise model with 95% symmetrized readout fidelity per qubit - noise_model = asymmetric_ro_model([0, 1], 0.945, 0.955) - qc = get_qc("2q-qvm", client=client) - qc.qam.noise_model = noise_model - - # bell state program (doesn't matter) - p = Program() - p += RESET() - p += H(0) - p += CNOT(0, 1) - p.wrap_in_numshots_loop(10000) - - # ZZ experiment - sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1)) - e = Experiment(settings=[sz], program=p) - - results = qc.calibrate(e) - - # ZZ expectation should just be (1 - 2 * readout_error_q0) * (1 - 2 * readout_error_q1) - np.isclose(results[0].expectation, 0.81, atol=0.01) - assert results[0].total_counts == 40000 - - -def test_qc_joint_expectation(client: Client, dummy_compiler: DummyCompiler): - qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) - - # |01> state program - p = Program() - p += RESET() - p += X(0) - p.wrap_in_numshots_loop(10) - - # ZZ experiment - sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]]) - e = Experiment(settings=[sz], program=p) - - results = qc.experiment(e) - - # ZZ expectation value for state |01> is -1 - assert np.isclose(results[0].expectation, -1) - assert np.isclose(results[0].std_err, 0) - assert results[0].total_counts == 40 - # Z0 expectation value for state |01> is -1 - assert np.isclose(results[0].additional_results[0].expectation, -1) - assert results[0].additional_results[1].total_counts == 40 - # Z1 expectation value for state |01> is 1 - assert np.isclose(results[0].additional_results[1].expectation, 1) - assert results[0].additional_results[1].total_counts == 40 - - -def test_qc_joint_calibration(client: Client): - # noise model with 95% symmetrized readout fidelity per qubit - noise_model = asymmetric_ro_model([0, 1], 0.945, 0.955) - qc = get_qc("2q-qvm", client=client) - qc.qam.noise_model = noise_model - - # |01> state program - p = Program() - p += RESET() - p += X(0) - p.wrap_in_numshots_loop(10000) - - # ZZ experiment - sz = ExperimentSetting(in_state=sZ(0) * sZ(1), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]]) - e = Experiment(settings=[sz], program=p) - - results = qc.experiment(e) - - # ZZ expectation value for state |01> with 95% RO fid on both qubits is about -0.81 - assert np.isclose(results[0].expectation, -0.81, atol=0.01) - assert results[0].total_counts == 40000 - # Z0 expectation value for state |01> with 95% RO fid on both qubits is about -0.9 - assert np.isclose(results[0].additional_results[0].expectation, -0.9, atol=0.01) - assert results[0].additional_results[1].total_counts == 40000 - # Z1 expectation value for state |01> with 95% RO fid on both qubits is about 0.9 - assert np.isclose(results[0].additional_results[1].expectation, 0.9, atol=0.01) - assert results[0].additional_results[1].total_counts == 40000 - - -def test_qc_expectation_on_qvm(client: Client, dummy_compiler: DummyCompiler): - # regression test for https://github.com/rigetti/forest-tutorials/issues/2 - qc = QuantumComputer(name="testy!", qam=QVM(client=client), compiler=dummy_compiler) - - p = Program() - theta = p.declare("theta", "REAL") - p += RESET() - p += RY(theta, 0) - p.wrap_in_numshots_loop(10000) - - sx = ExperimentSetting(in_state=sZ(0), out_operator=sX(0)) - e = Experiment(settings=[sx], program=p) - - thetas = [-np.pi / 2, 0.0, np.pi / 2] - results = [] - - # Verify that multiple calls to qc.experiment with the same experiment backed by a QVM that - # requires_exectutable does not raise an exception. - for theta in thetas: - results.append(qc.experiment(e, memory_map={"theta": [theta]})) - - assert np.isclose(results[0][0].expectation, -1.0, atol=0.01) - assert np.isclose(results[0][0].std_err, 0) - assert results[0][0].total_counts == 20000 - - # bounds on atol and std_err here are a little loose to try and avoid test flakiness. - assert np.isclose(results[1][0].expectation, 0.0, atol=0.1) - assert results[1][0].std_err < 0.01 - assert results[1][0].total_counts == 20000 - - assert np.isclose(results[2][0].expectation, 1.0, atol=0.01) - assert np.isclose(results[2][0].std_err, 0) - assert results[2][0].total_counts == 20000 diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index 882d4960c..a2d08f6b0 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -14,8 +14,9 @@ def wrap(cls, qam: QAM) -> None: Mutate the provided QAM to add methods and data for backwards compatibility, by dynamically mixing in this wrapper class. """ - qam.__class__ = type(str(qam.__class__.__name__), (StatefulQAM, qam.__class__), {}) - qam.reset() + if not isinstance(qam, StatefulQAM): + qam.__class__ = type(str(qam.__class__.__name__), (StatefulQAM, qam.__class__), {}) + qam.reset() def load(self, executable: QuantumExecutable) -> "QAM": self._loaded_executable = executable diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index bdb3c56a2..f462bdb54 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -175,7 +175,7 @@ def experiment( experiment_program = experiment.generate_experiment_program() executable = self.compile(experiment_program) self.qam._experiment = experiment - elif isinstance(self.qam, QVM) and isinstance(executable, Program) and self.qam.requires_executable: + elif isinstance(self.qam, QVM) and isinstance(executable, Program): executable = self.compiler.native_quil_to_executable(executable) if memory_map is None: @@ -449,7 +449,11 @@ def get_qc( ) qc = get_qc_v3( - name=name, as_qvm=as_qvm, noisy=noisy, timeout=compiler_timeout, client_configuration=client_configuration + name=name, + as_qvm=as_qvm, + noisy=noisy, + compiler_timeout=compiler_timeout, + client_configuration=client_configuration, ) qc = cast(QuantumComputerV3, qc) diff --git a/pyquil/magic.py b/pyquil/magic.py deleted file mode 100644 index a4dd7282b..000000000 --- a/pyquil/magic.py +++ /dev/null @@ -1,239 +0,0 @@ -############################################################################## -# Copyright 2018 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. -############################################################################## -import ast -import sys -import functools -import inspect - -from pyquil import gates -from pyquil.quil import Program -from pyquil.quilatom import MemoryReference - -if sys.version_info < (3, 7): - from pyquil.external.contextvars import ContextVar -else: - from contextvars import ContextVar - -_program_context = ContextVar("program") - - -def program_context() -> Program: - """ - Returns the current program in context. Will only work from inside calls to @magicquil. - If called from outside @magicquil will throw a ValueError - - :return: program if one exists - """ - return _program_context.get() - - -def I(qubit) -> None: - program_context().inst(gates.I(qubit)) - - -def X(qubit) -> None: - program_context().inst(gates.X(qubit)) - - -def H(qubit) -> None: - program_context().inst(gates.H(qubit)) - - -def CNOT(qubit1, qubit2) -> None: - program_context().inst(gates.CNOT(qubit1, qubit2)) - - -def MEASURE(qubit) -> MemoryReference: - program_context().inst(gates.MEASURE(qubit, MemoryReference("ro", qubit))) - return MemoryReference("ro", qubit) - - -def _if_statement(test, if_function, else_function) -> None: - """ - Evaluate an if statement within a @magicquil block. - - If the test value is a Quil MemoryReference then unwind it into quil code equivalent to an if - then statement using jumps. Both sides of the if statement need to be evaluated and placed into - separate Programs, which is why we create new program contexts for their evaluation. - - If the test value is not a Quil MemoryReference then fall back to what Python would normally do - with an if statement. - - Params are: - if : - - else: - - - NB: This function must be named exactly _if_statement and be in scope for the ast transformer - """ - if isinstance(test, MemoryReference): - token = _program_context.set(Program()) - if_function() - if_program = _program_context.get() - _program_context.reset(token) - - if else_function: - token = _program_context.set(Program()) - else_function() - else_program = _program_context.get() - _program_context.reset(token) - else: - else_program = None - - program = _program_context.get() - program.if_then(test, if_program, else_program) - else: - if test: - if_function() - elif else_function: - else_function() - - -_EMPTY_ARGUMENTS = ast.arguments( - args=[], posonlyargs=[], vararg=None, kwonlyargs=[], kwarg=None, defaults=[], kw_defaults=[] -) - - -class _IfTransformer(ast.NodeTransformer): - """ - Transformer that unwraps the if and else branches into separate inner functions and then wraps - them in a call to _if_statement. For example: - - .. code-block:: python - - if 1 + 1 == 2: - print('math works') - else: - print('something is broken') - - would be transformed into: - - .. code-block:: python - - def _if_branch(): - print('math works') - def _else_branch(): - print('something is broken') - _if_statement(1 + 1 == 2, _if_branch, _else_branch) - """ - - def visit_If(self, node): - # Must recursively visit the body of both the if and else bodies to handle any nested if - # and else statements. This also conveniently handles elif since those are just treated - # as a nested if/else within the else branch. - # See: https://greentreesnakes.readthedocs.io/en/latest/nodes.html#If - node = self.generic_visit(node) - - if_function = ast.FunctionDef(name="_if_branch", body=node.body, decorator_list=[], args=_EMPTY_ARGUMENTS) - else_function = ast.FunctionDef(name="_else_branch", body=node.orelse, decorator_list=[], args=_EMPTY_ARGUMENTS) - - if_function_name = ast.Name(id="_if_branch", ctx=ast.Load()) - else_function_name = ast.Name(id="_else_branch", ctx=ast.Load()) - - if node.orelse: - return [ - if_function, - else_function, - ast.Expr( - ast.Call( - func=ast.Name(id="_if_statement", ctx=ast.Load()), - args=[node.test, if_function_name, else_function_name], - keywords=[], - ) - ), - ] - else: - return [ - if_function, - ast.Expr( - ast.Call( - func=ast.Name(id="_if_statement", ctx=ast.Load()), - args=[node.test, if_function_name, ast.NameConstant(None)], - keywords=[], - ) - ), - ] - - -def _rewrite_function(f): - """ - Rewrite a function so that any if/else branches are intercepted and their behavior can be - overridden. This is accomplished using 3 steps: - - 1. Get the source of the function and then rewrite the AST using _IfTransformer - 2. Do some small fixups to the tree to make sure - a) the function doesn't have the same name, and - b) the decorator isn't called recursively on the transformed function as well - 3. Bring the variables from the call site back into scope - - :param f: Function to rewrite - :return: Rewritten function - """ - source = inspect.getsource(f) - tree = ast.parse(source) - _IfTransformer().visit(tree) - - ast.fix_missing_locations(tree) - tree.body[0].name = f.__name__ + "_patched" - tree.body[0].decorator_list = [] - - compiled = compile(tree, filename="", mode="exec") - # The first f_back here gets to the body of magicquil() and the second f_back gets to the - # user's call site which is what we want. If we didn't add these manually to the globals it - # wouldn't be possible to call other @magicquil functions from within a @magicquil function. - prev_globals = inspect.currentframe().f_back.f_back.f_globals - # For reasons I don't quite understand it's critical to add locals() here otherwise the - # function will disappear and we won't be able to return it below - exec(compiled, {**prev_globals, **globals()}, locals()) - return locals()[f.__name__ + "_patched"] - - -def magicquil(f): - """ - Decorator to enable a more convenient syntax for writing quil programs. With this decorator - there is no need to keep track of a Program object and regular Python if/else branches can be - used for classical control flow. - - Example usage: - - .. code-block:: python - - @magicquil - def fast_reset(q1): - reg1 = MEASURE(q1, None) - if reg1: - X(q1) - else: - I(q1) - - my_program = fast_reset(0) # this will be a Program object - """ - rewritten_function = _rewrite_function(f) - - @functools.wraps(f) - def wrapper(*args, **kwargs): - if _program_context.get(None) is not None: - rewritten_function(*args, **kwargs) - program = _program_context.get() - else: - token = _program_context.set(Program()) - rewritten_function(*args, **kwargs) - program = _program_context.get() - _program_context.reset(token) - return program - - return wrapper diff --git a/pyquil/numpy_simulator.py b/pyquil/numpy_simulator.py deleted file mode 100644 index 00830922a..000000000 --- a/pyquil/numpy_simulator.py +++ /dev/null @@ -1,29 +0,0 @@ -############################################################################## -# Copyright 2016-2019 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. -############################################################################## - -import warnings - -from pyquil.simulation._numpy import ( # noqa: F401 - NumpyWavefunctionSimulator, - get_measure_probabilities, - targeted_einsum, - targeted_tensordot, -) - -warnings.warn( - "The code in pyquil.numpy_simulator has been moved to pyquil.simulation, " "please update your import statements.", - FutureWarning, -) diff --git a/pyquil/tests/test_api.py b/pyquil/tests/test_api.py deleted file mode 100644 index fdcf8a3f6..000000000 --- a/pyquil/tests/test_api.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/python -############################################################################## -# 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. -############################################################################## -import json -from math import pi - -import numpy as np -import pytest -from pytest_httpx import HTTPXMock - -from pyquil.api import QVMConnection -from pyquil.device import ISA -from pyquil.gates import CNOT, H, MEASURE, PHASE, Z, RZ, RX, CZ -from pyquil.paulis import PauliTerm -from pyquil.quil import Program -from pyquil.quilatom import MemoryReference -from pyquil.quilbase import Halt, Declare -from pyquil.simulation.tools import program_unitary - -EMPTY_PROGRAM = Program() -BELL_STATE = Program(H(0), CNOT(0, 1)) -BELL_STATE_MEASURE = Program( - Declare("ro", "BIT", 2), - H(0), - CNOT(0, 1), - MEASURE(0, MemoryReference("ro", 0)), - MEASURE(1, MemoryReference("ro", 1)), -) -COMPILED_BELL_STATE = Program( - [ - RZ(pi / 2, 0), - RX(pi / 2, 0), - RZ(-pi / 2, 1), - RX(pi / 2, 1), - CZ(1, 0), - RZ(-pi / 2, 0), - RX(-pi / 2, 1), - RZ(pi / 2, 1), - Halt(), - ] -) -DUMMY_ISA_DICT = {"1Q": {"0": {}, "1": {}}, "2Q": {"0-1": {}}} -DUMMY_ISA = ISA.from_dict(DUMMY_ISA_DICT) - -COMPILED_BYTES_ARRAY = b"SUPER SECRET PACKAGE" -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")] - - -def test_sync_run_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): - mock_qvm = qvm - mock_endpoint = mock_qvm.client.qvm_url - httpx_mock.add_response( - method="POST", - url=mock_endpoint, - match_content=json.dumps( - { - "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", - "rng-seed": 52, - } - ).encode(), - json={"ro": [[0, 0], [1, 1]]}, - ) - - assert mock_qvm.run(BELL_STATE_MEASURE, [0, 1], trials=2) == [[0, 0], [1, 1]] - - # Test no classical addresses - assert mock_qvm.run(BELL_STATE_MEASURE, trials=2) == [[0, 0], [1, 1]] - - with pytest.raises(ValueError): - mock_qvm.run(EMPTY_PROGRAM) - - -def test_sync_run(qvm: QVMConnection): - assert qvm.run(BELL_STATE_MEASURE, [0, 1], trials=2) == [[0, 0], [1, 1]] - - # Test range as well - assert qvm.run(BELL_STATE_MEASURE, range(2), trials=2) == [[0, 0], [1, 1]] - - # Test numpy ints - assert qvm.run(BELL_STATE_MEASURE, np.arange(2), trials=2) == [[0, 0], [1, 1]] - - # Test no classical addresses - assert qvm.run(BELL_STATE_MEASURE, trials=2) == [[0, 0], [1, 1]] - - with pytest.raises(ValueError): - qvm.run(EMPTY_PROGRAM) - - -def test_sync_run_and_measure_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): - mock_qvm = qvm - mock_endpoint = mock_qvm.client.qvm_url - httpx_mock.add_response( - method="POST", - url=mock_endpoint, - match_content=json.dumps( - { - "type": "multishot-measure", - "qubits": [0, 1], - "trials": 2, - "compiled-quil": "H 0\nCNOT 0 1\n", - "rng-seed": 52, - } - ).encode(), - json=[[0, 0], [1, 1]], - ) - - assert mock_qvm.run_and_measure(BELL_STATE, [0, 1], trials=2) == [[0, 0], [1, 1]] - - with pytest.raises(ValueError): - mock_qvm.run_and_measure(EMPTY_PROGRAM, [0]) - - -def test_sync_run_and_measure(qvm): - assert qvm.run_and_measure(BELL_STATE, [0, 1], trials=2) == [[1, 1], [0, 0]] - assert qvm.run_and_measure(BELL_STATE, [0, 1]) == [[1, 1]] - - with pytest.raises(ValueError): - qvm.run_and_measure(EMPTY_PROGRAM, [0]) - - -WAVEFUNCTION_PROGRAM = Program(Declare("ro", "BIT"), H(0), CNOT(0, 1), MEASURE(0, MemoryReference("ro")), H(0)) - - -def test_sync_expectation_mock(qvm: QVMConnection, httpx_mock: HTTPXMock): - mock_qvm = qvm - mock_endpoint = mock_qvm.client.qvm_url - httpx_mock.add_response( - method="POST", - url=mock_endpoint, - match_content=json.dumps( - { - "type": "expectation", - "state-preparation": BELL_STATE.out(), - "operators": ["Z 0\n", "Z 1\n", "Z 0\nZ 1\n"], - "rng-seed": 52, - } - ).encode(), - json=[0.0, 0.0, 1.0], - ) - - 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) - - z0 = PauliTerm("Z", 0) - z1 = PauliTerm("Z", 1) - z01 = z0 * z1 - result = mock_qvm.pauli_expectation(BELL_STATE, [z0, z1, z01]) - exp_expected = [0.0, 0.0, 1.0] - np.testing.assert_allclose(exp_expected, result) - - -def test_sync_expectation(qvm): - result = 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) - - -def test_sync_expectation_2(qvm): - z0 = PauliTerm("Z", 0) - z1 = PauliTerm("Z", 1) - z01 = z0 * z1 - result = qvm.pauli_expectation(BELL_STATE, [z0, z1, z01]) - exp_expected = [0.0, 0.0, 1.0] - np.testing.assert_allclose(exp_expected, result) - - -def test_sync_paulisum_expectation(qvm: QVMConnection, httpx_mock: HTTPXMock): - mock_qvm = qvm - mock_endpoint = mock_qvm.client.qvm_url - httpx_mock.add_response( - method="POST", - url=mock_endpoint, - match_content=json.dumps( - { - "type": "expectation", - "state-preparation": BELL_STATE.out(), - "operators": ["Z 0\nZ 1\n", "Z 0\n", "Z 1\n"], - "rng-seed": 52, - } - ).encode(), - json=[1.0, 0.0, 0.0], - ) - - z0 = PauliTerm("Z", 0) - z1 = PauliTerm("Z", 1) - z01 = z0 * z1 - result = mock_qvm.pauli_expectation(BELL_STATE, 1j * z01 + z0 + z1) - exp_expected = 1j - np.testing.assert_allclose(exp_expected, result) - - -def test_sync_wavefunction(qvm): - qvm.random_seed = 0 # this test uses a stochastic program and assumes we measure 0 - result = qvm.wavefunction(WAVEFUNCTION_PROGRAM) - wf_expected = np.array([0.0 + 0.0j, 0.0 + 0.0j, 0.70710678 + 0.0j, -0.70710678 + 0.0j]) - np.testing.assert_allclose(result.amplitudes, wf_expected) - - -def test_quil_to_native_quil(compiler): - response = compiler.quil_to_native_quil(BELL_STATE) - p_unitary = program_unitary(response, n_qubits=2) - compiled_p_unitary = program_unitary(COMPILED_BELL_STATE, n_qubits=2) - from pyquil.simulation.tools import scale_out_phase - - assert np.allclose(p_unitary, scale_out_phase(compiled_p_unitary, p_unitary)) - - -def test_local_rb_sequence(benchmarker): - response = benchmarker.generate_rb_sequence(2, [PHASE(np.pi / 2, 0), H(0)], seed=52) - assert [prog.out() for prog in response] == [ - "H 0\nPHASE(pi/2) 0\nH 0\nPHASE(pi/2) 0\nPHASE(pi/2) 0\n", - "H 0\nPHASE(pi/2) 0\nH 0\nPHASE(pi/2) 0\nPHASE(pi/2) 0\n", - ] - - -def test_local_conjugate_request(benchmarker): - response = benchmarker.apply_clifford_to_pauli(Program("H 0"), PauliTerm("X", 0, 1.0)) - assert isinstance(response, PauliTerm) - assert str(response) == "(1+0j)*Z0" - - -def test_apply_clifford_to_pauli(benchmarker): - response = benchmarker.apply_clifford_to_pauli(Program("H 0"), PauliTerm("I", 0, 0.34)) - assert response == PauliTerm("I", 0, 0.34) diff --git a/pyquil/tests/test_magic.py b/pyquil/tests/test_magic.py deleted file mode 100644 index f5b24907a..000000000 --- a/pyquil/tests/test_magic.py +++ /dev/null @@ -1,83 +0,0 @@ -from pyquil.magic import ( - CNOT, - H, - I, - MEASURE, - X, - Program, - magicquil, -) - - -@magicquil -def bell_state(q1, q2): - H(q1) - CNOT(q1, q2) - - -def test_bell_state(): - assert bell_state(0, 1) == Program("H 0\nCNOT 0 1") - - -@magicquil -def fast_reset(q1): - reg1 = MEASURE(q1) - if reg1: - X(q1) - else: - I(q1) - - -def test_fast_reset(): - assert fast_reset(0) == Program("DECLARE ro BIT\nMEASURE 0 ro[0]").if_then( - ("ro", 0), Program("X 0"), Program("I 0") - ) - - -@magicquil -def no_else(q1): - reg1 = MEASURE(q1) - if reg1: - X(q1) - - -def test_no_else(): - assert no_else(0) == Program("DECLARE ro BIT\nMEASURE 0 ro[0]").if_then(("ro", 0), Program("X 0")) - - -@magicquil -def with_elif(q1, q2): - reg1 = MEASURE(q1) - reg2 = MEASURE(q2) - if reg1: - X(q1) - elif reg2: - X(q2) - - -def test_with_elif(): - assert with_elif(0, 1) == Program("DECLARE ro BIT[2]\nMEASURE 0 ro[0]\nMEASURE 1 ro[1]").if_then( - ("ro", 0), Program("X 0"), Program().if_then(("ro", 1), Program("X 1")) - ) - - -@magicquil -def calls_another(q1, q2, q3): - bell_state(q1, q2) - CNOT(q2, q3) - - -def test_calls_another(): - assert calls_another(0, 1, 2) == Program("H 0\nCNOT 0 1\nCNOT 1 2") - - -@magicquil -def still_works_with_bools(): - if 1 + 1 == 2: - H(0) - else: - H(1) - - -def test_stills_works_with_bools(): - assert still_works_with_bools() == Program("H 0") diff --git a/pyquil/tests/test_qvm.py b/pyquil/tests/test_qvm.py deleted file mode 100644 index d24bff050..000000000 --- a/pyquil/tests/test_qvm.py +++ /dev/null @@ -1,145 +0,0 @@ -import numpy as np -import pytest - -from pyquil import Program -from pyquil.api import QVM, Client -from pyquil.api._errors import QVMError -from pyquil.api._qvm import validate_noise_probabilities, validate_qubit_list, prepare_register_list -from pyquil.gates import MEASURE, X -from pyquil.quilbase import Declare, MemoryReference - - -def test_qvm__default_client(): - qvm = QVM() - p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (1000, 1) - - -def test_qvm_run_pqer(client: Client): - qvm = QVM(client=client, gate_noise=(0.01, 0.01, 0.01)) - p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (1000, 1) - assert np.mean(bitstrings) > 0.8 - - -def test_qvm_run_just_program(client: Client): - qvm = QVM(client=client, gate_noise=(0.01, 0.01, 0.01)) - p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (1000, 1) - assert np.mean(bitstrings) > 0.8 - - -def test_qvm_run_only_pqer(client: Client): - qvm = QVM(client=client, gate_noise=(0.01, 0.01, 0.01)) - p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) - - qvm.load(p.wrap_in_numshots_loop(1000)) - qvm.run() - qvm.wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (1000, 1) - assert np.mean(bitstrings) > 0.8 - - -def test_qvm_run_region_declared_and_measured(client: Client): - qvm = QVM(client=client) - p = Program(Declare("reg", "BIT"), X(0), MEASURE(0, MemoryReference("reg"))) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="reg") - assert bitstrings.shape == (100, 1) - - -def test_qvm_run_region_declared_not_measured(client: Client): - qvm = QVM(client=client) - p = Program(Declare("reg", "BIT"), X(0)) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="reg") - assert bitstrings.shape == (100, 0) - - -# For backwards compatibility, we support omitting the declaration for "ro" specifically -# TODO(andrew): we should remove this lenient behavior -def test_qvm_run_region_not_declared_is_measured_ro(client: Client): - qvm = QVM(client=client) - p = Program(X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (100, 1) - - -def test_qvm_run_region_not_declared_is_measured_non_ro(client: Client): - qvm = QVM(client=client) - p = Program(X(0), MEASURE(0, MemoryReference("reg"))) - - with pytest.raises(QVMError, match='Bad memory region name "reg" in MEASURE'): - qvm.load(p).run().wait() - - -def test_qvm_run_region_not_declared_not_measured_ro(client: Client): - qvm = QVM(client=client) - p = Program(X(0)) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (100, 0) - - -def test_qvm_run_region_not_declared_not_measured_non_ro(client: Client): - qvm = QVM(client=client) - p = Program(X(0)) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - assert qvm.read_memory(region_name="reg") is None - - -def test_qvm_version(client: Client): - qvm = QVM(client=client) - version = qvm.get_version_info() - - def is_a_version_string(version_string: str): - parts = version_string.split(".") - try: - map(int, parts) - except ValueError: - return False - return True - - assert is_a_version_string(version) - - -def test_validate_noise_probabilities(): - with pytest.raises(TypeError, match="noise_parameter must be a tuple"): - validate_noise_probabilities(1) - with pytest.raises(TypeError, match="noise_parameter values should all be floats"): - validate_noise_probabilities(("a", "b", "c")) - with pytest.raises(ValueError, match="noise_parameter tuple must be of length 3"): - validate_noise_probabilities((0.0, 0.0, 0.0, 0.0)) - with pytest.raises( - ValueError, - match="sum of entries in noise_parameter must be between 0 and 1 \\(inclusive\\)", - ): - validate_noise_probabilities((0.5, 0.5, 0.5)) - with pytest.raises(ValueError, match="noise_parameter values should all be non-negative"): - validate_noise_probabilities((-0.5, -0.5, 1.0)) - - -def test_validate_qubit_list(): - with pytest.raises(TypeError): - validate_qubit_list([-1, 1]) - with pytest.raises(TypeError): - validate_qubit_list(["a", 0], 1) - - -def test_prepare_register_list(): - with pytest.raises(TypeError): - prepare_register_list({"ro": [-1, 1]}) diff --git a/pyquil/tests/test_wavefunction_simulator.py b/pyquil/tests/test_wavefunction_simulator.py deleted file mode 100644 index 5b1d033a8..000000000 --- a/pyquil/tests/test_wavefunction_simulator.py +++ /dev/null @@ -1,59 +0,0 @@ -import numpy as np - -import pytest - -from pyquil import Program -from pyquil.gates import H, CNOT - -from pyquil.api import WavefunctionSimulator, Client -from pyquil.paulis import PauliSum, sZ, sX - - -def test_wavefunction(client: Client): - wfnsim = WavefunctionSimulator(client=client) - bell = Program(H(0), CNOT(0, 1)) - wfn = wfnsim.wavefunction(bell) - np.testing.assert_allclose(wfn.amplitudes, 1 / np.sqrt(2) * np.array([1, 0, 0, 1])) - np.testing.assert_allclose(wfn.probabilities(), [0.5, 0, 0, 0.5]) - assert wfn.pretty_print() == "(0.71+0j)|00> + (0.71+0j)|11>" - - bitstrings = wfn.sample_bitstrings(1000) - parity = np.sum(bitstrings, axis=1) % 2 - assert np.all(parity == 0) - - -def test_random_seed(client: Client): - wfnsim = WavefunctionSimulator(client=client, random_seed=100) - assert wfnsim.random_seed == 100 - - with pytest.raises(TypeError): - WavefunctionSimulator(random_seed="NOT AN INTEGER") - - -def test_expectation(client: Client): - wfnsim = WavefunctionSimulator(client=client) - bell = Program(H(0), CNOT(0, 1)) - expects = wfnsim.expectation(bell, [sZ(0) * sZ(1), sZ(0), sZ(1), sX(0) * sX(1)]) - assert expects.size == 4 - np.testing.assert_allclose(expects, [1, 0, 0, 1]) - - pauli_sum = PauliSum([sZ(0) * sZ(1)]) - expects = wfnsim.expectation(bell, pauli_sum) - assert expects.size == 1 - np.testing.assert_allclose(expects, [1]) - - -def test_run_and_measure(client: Client): - wfnsim = WavefunctionSimulator(client=client) - bell = Program(H(0), CNOT(0, 1)) - bitstrings = wfnsim.run_and_measure(bell, trials=1000) - parity = np.sum(bitstrings, axis=1) % 2 - assert np.all(parity == 0) - - -def test_run_and_measure_qubits(client: Client): - wfnsim = WavefunctionSimulator(client=client) - bell = Program(H(0), CNOT(0, 1)) - bitstrings = wfnsim.run_and_measure(bell, qubits=[0, 100], trials=1000) - assert np.all(bitstrings[:, 1] == 0) - assert 0.4 < np.mean(bitstrings[:, 0]) < 0.6 diff --git a/pyquil/tests/utils.py b/pyquil/tests/utils.py deleted file mode 100644 index 4618bc394..000000000 --- a/pyquil/tests/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -from pyquil.api import Client -from pyquil.device import AbstractDevice -from pyquil.parser import parse -from pyquil.api._abstract_compiler import AbstractCompiler -from pyquil import Program - - -def api_fixture_path(path: str) -> str: - dir_path = os.path.dirname(os.path.realpath(__file__)) - return os.path.join(dir_path, "../api/tests/data", path) - - -def parse_equals(quil_string, *instructions): - expected = list(instructions) - actual = parse(quil_string) - assert expected == actual - - -class DummyCompiler(AbstractCompiler): - def __init__(self, device: AbstractDevice, client: Client): - super().__init__(device=device, client=client, timeout=10) # type: ignore - - def get_version_info(self): - return {} - - def quil_to_native_quil(self, program: Program, *, protoquil=None): - return program - - def native_quil_to_executable(self, nq_program: Program): - return nq_program diff --git a/test/unit/test_compatibility_quantum_computer.py b/test/unit/test_compatibility_quantum_computer.py index 642f57fd3..4cdb9d708 100644 --- a/test/unit/test_compatibility_quantum_computer.py +++ b/test/unit/test_compatibility_quantum_computer.py @@ -7,6 +7,7 @@ import pytest from pyquil import Program, list_quantum_computers from pyquil.api import QCSClientConfiguration + from pyquil.api._quantum_computer import ( _check_min_num_trials_for_symmetrized_readout, _consolidate_symmetrization_outputs, @@ -19,6 +20,8 @@ _parse_name, _symmetrization, ) +from pyquil.compatibility.v2 import QuantumComputer, get_qc +from pyquil.compatibility.v2.api import QVM from pyquil.experiment import ExperimentSetting, Experiment from pyquil.experiment._main import _pauli_to_product_state from pyquil.gates import CNOT, H, RESET, RY, X @@ -411,11 +414,15 @@ def test_qc(client_configuration: QCSClientConfiguration): def test_qc_run(client_configuration: QCSClientConfiguration): qc = get_qc("9q-square-noisy-qvm", client_configuration=client_configuration) - bs = qc.run(qc.compile(Program( - Declare("ro", "BIT", 1), - X(0), - MEASURE(0, ("ro", 0)), - ).wrap_in_numshots_loop(3))) + bs = qc.run( + qc.compile( + Program( + Declare("ro", "BIT", 1), + X(0), + MEASURE(0, ("ro", 0)), + ).wrap_in_numshots_loop(3) + ) + ) assert bs.shape == (3, 1) @@ -486,19 +493,13 @@ def test_reset(client_configuration: QCSClientConfiguration): qc.run(executable=p, memory_map={"theta": [np.pi]}) aref = ParameterAref(name="theta", index=0) - assert qc.qam._variables_shim[aref] == np.pi - assert qc.qam.executable == p - assert qc.qam._memory_results["ro"].shape == (1000, 1) - assert all([bit == 1 for bit in qc.qam._memory_results["ro"]]) - assert qc.qam.status == "done" + assert qc.qam._loaded_executable._variable_values[aref] == np.pi + assert qc.qam._loaded_executable == p + assert qc.qam._result.memory["ro"].shape == (1000, 1) + assert all([bit == 1 for bit in qc.qam._result.memory["ro"]]) qc.reset() - assert qc.qam._variables_shim == {} - assert qc.qam.executable is None - assert qc.qam._memory_results["ro"] is None - assert qc.qam.status == "connected" - def test_get_qvm_with_topology(client_configuration: QCSClientConfiguration): topo = nx.from_edgelist([(5, 6), (6, 7), (10, 11)]) @@ -527,13 +528,17 @@ def test_get_qvm_with_topology_2(client_configuration: QCSClientConfiguration): execution_timeout=5.0, client_configuration=client_configuration, ) - results = qc.run(qc.compile(Program( - Declare("ro", "BIT", 3), - X(5), - MEASURE(5, ("ro", 0)), - MEASURE(6, ("ro", 1)), - MEASURE(7, ("ro", 2)), - ).wrap_in_numshots_loop(5))) + results = qc.run( + qc.compile( + Program( + Declare("ro", "BIT", 3), + X(5), + MEASURE(5, ("ro", 0)), + MEASURE(6, ("ro", 1)), + MEASURE(7, ("ro", 2)), + ).wrap_in_numshots_loop(5) + ) + ) assert results.shape == (5, 3) assert all(r[0] == 1 for r in results) @@ -716,7 +721,9 @@ def test_qc_joint_expectation(client_configuration: QCSClientConfiguration, dumm p.wrap_in_numshots_loop(10) # ZZ experiment - sz = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]]) + sz = ExperimentSetting( + in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] + ) e = Experiment(settings=[sz], program=p) results = qc.experiment(e) @@ -734,8 +741,10 @@ def test_qc_joint_expectation(client_configuration: QCSClientConfiguration, dumm def test_get_qc_noisy_qpu_error(client_configuration: QCSClientConfiguration, dummy_compiler: DummyCompiler): - expected_message = "pyQuil currently does not support initializing a noisy QuantumComputer " \ + expected_message = ( + "pyQuil currently does not support initializing a noisy QuantumComputer " "based on a QCSQuantumProcessor. Change noisy to False or specify the name of a QVM." + ) with pytest.raises(ValueError, match=expected_message): get_qc("Aspen-8", noisy=True) @@ -753,7 +762,9 @@ def test_qc_joint_calibration(client_configuration: QCSClientConfiguration): p.wrap_in_numshots_loop(10000) # ZZ experiment - sz = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]]) + sz = ExperimentSetting( + in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] + ) e = Experiment(settings=[sz], program=p) results = qc.experiment(e) diff --git a/test/unit/test_qvm.py b/test/unit/test_qvm.py index aa3d453a0..ff521f00d 100644 --- a/test/unit/test_qvm.py +++ b/test/unit/test_qvm.py @@ -67,14 +67,14 @@ def test_qvm_run_region_not_declared_is_measured(client_configuration: QCSClient p = Program(X(0), MEASURE(0, MemoryReference("ro"))) with pytest.raises(QVMError, match='Bad memory region name "ro" in MEASURE'): - qvm.load(p).run().wait() + qvm.run(p) def test_qvm_run_region_not_declared_not_measured(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(X(0)) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - assert qvm.read_memory(region_name="ro") is None + result = qvm.run(p.wrap_in_numshots_loop(100)) + assert result.read_memory(region_name="ro") is None def test_qvm_version(client_configuration: QCSClientConfiguration): From b56996faee04d62e3e6c31b404d62038432e60b4 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 17:49:12 -0700 Subject: [PATCH 10/61] Fix: incorrect test assertion --- test/unit/test_compatibility_v2_qvm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/test_compatibility_v2_qvm.py b/test/unit/test_compatibility_v2_qvm.py index 8e36f2d84..7b7b16b0c 100644 --- a/test/unit/test_compatibility_v2_qvm.py +++ b/test/unit/test_compatibility_v2_qvm.py @@ -74,9 +74,9 @@ def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientCon def test_qvm_run_region_not_declared_is_measured_ro(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(X(0), MEASURE(0, MemoryReference("ro"))) - qvm.load(p.wrap_in_numshots_loop(100)).run().wait() - bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (100, 1) + with pytest.raises(QVMError) as excinfo: + qvm.load(p.wrap_in_numshots_loop(100)).run().wait() + assert 'Bad memory region' in str(excinfo) def test_qvm_run_region_not_declared_is_measured_non_ro(client_configuration: QCSClientConfiguration): @@ -92,7 +92,7 @@ def test_qvm_run_region_not_declared_not_measured_ro(client_configuration: QCSCl p = Program(X(0)) qvm.load(p.wrap_in_numshots_loop(100)).run().wait() bitstrings = qvm.read_memory(region_name="ro") - assert bitstrings.shape == (100, 0) + assert bitstrings is None def test_qvm_run_region_not_declared_not_measured_non_ro(client_configuration: QCSClientConfiguration): From b47187a56096f14855fec29af46cee671151133c Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 18:10:31 -0700 Subject: [PATCH 11/61] Fix: rebasing mistake --- pyquil/api/_abstract_compiler.py | 45 ++------------------------------ 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index d7ed2d005..22fac1079 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -import sys from abc import ABC, abstractmethod +from dataclasses import dataclass from typing import Any, Dict, List, Optional, Sequence, Union from qcs_api_client.client import QCSClientConfiguration @@ -34,47 +34,6 @@ from pyquil.quil import Program from pyquil.quilatom import MemoryReference, ExpressionDesignator from pyquil.quilbase import Gate -from pyquil._version import pyquil_version - -if sys.version_info < (3, 7): - from rpcq.external.dataclasses import dataclass -else: - from dataclasses import dataclass - - -class QuilcVersionMismatch(Exception): - pass - - -class QuilcNotRunning(Exception): - pass - - -@dataclass() -class EncryptedProgram: - """ - Encrypted binary, executable on a QPU. - """ - - program: str - """String representation of an encrypted Quil program.""" - - memory_descriptors: Dict[str, ParameterSpec] - """Descriptors for memory executable's regions, mapped by name.""" - - ro_sources: Dict[MemoryReference, str] - """Readout sources, mapped by memory reference.""" - - recalculation_table: Dict[ParameterAref, ExpressionDesignator] - """A mapping from memory references to the original gate arithmetic.""" - - -QuantumExecutable = Union[EncryptedProgram, Program] - -if sys.version_info < (3, 7): - from rpcq.external.dataclasses import dataclass -else: - from dataclasses import dataclass class QuilcVersionMismatch(Exception): @@ -85,7 +44,7 @@ class QuilcNotRunning(Exception): pass -@dataclass() +@dataclass class EncryptedProgram: """ Encrypted binary, executable on a QPU. From 347e0f0d07cb4f83b8f9c301f7f0c3b285af65b7 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 18:10:38 -0700 Subject: [PATCH 12/61] Fix: tests --- test/unit/test_compatibility_quantum_computer.py | 1 - test/unit/test_numpy.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/test_compatibility_quantum_computer.py b/test/unit/test_compatibility_quantum_computer.py index 4cdb9d708..adfbbc46b 100644 --- a/test/unit/test_compatibility_quantum_computer.py +++ b/test/unit/test_compatibility_quantum_computer.py @@ -404,7 +404,6 @@ def test_parse_qc_pyqvm(): def test_qc(client_configuration: QCSClientConfiguration): qc = get_qc("9q-square-noisy-qvm", client_configuration=client_configuration) assert isinstance(qc, QuantumComputer) - assert isinstance(qc.qam, QVM) assert qc.qam.noise_model is not None assert qc.qubit_topology().number_of_nodes() == 9 assert qc.qubit_topology().degree[0] == 2 diff --git a/test/unit/test_numpy.py b/test/unit/test_numpy.py index 0c81838f1..c1044b583 100644 --- a/test/unit/test_numpy.py +++ b/test/unit/test_numpy.py @@ -171,7 +171,7 @@ def test_sample_bitstrings(): def test_expectation_helper(): n_qubits = 3 - wf = np.zeros(shape=((2,) * n_qubits), dtype=np.complex) + wf = np.zeros(shape=((2,) * n_qubits), dtype=np.complex128) wf[0, 0, 0] = 1 z0 = _term_expectation(wf, 0.4 * sZ(0)) assert z0 == 0.4 From 262f0787bea13165fccfe9b1f6406dde27d52117 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 18:53:43 -0700 Subject: [PATCH 13/61] Fix: PyQVM#execute --- pyquil/pyqvm.py | 18 +++++++++++++++++- test/unit/test_reference_density.py | 16 ++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 02f8f5953..cf30e3aa6 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -241,7 +241,7 @@ def execute(self, executable: QuantumExecutable) -> "PyQVM": self._memory_results = {} self.ram = {} - self.wf_simulator.reset() + # self.wf_simulator.reset() # grab the gate definitions for future use self._extract_defined_gates() @@ -254,6 +254,8 @@ def execute(self, executable: QuantumExecutable) -> "PyQVM": self._memory_results.setdefault(name, list()) self._memory_results[name].append(self.ram[name]) + self._memory_results = {k: np.asarray(v) for k, v in self._memory_results.items()} + self._bitstrings = self._memory_results.get("ro") return self @@ -467,3 +469,17 @@ def _execute_program(self) -> "PyQVM": halted = self.transition() return self + + def execute_once(self, program: Program) -> "PyQVM": + """ + Execute one outer loop of a program on the PyQVM without re-initializing its state. + + Note that the PyQVM is stateful. Subsequent calls to :py:func:`execute` will not + automatically reset the wavefunction or the classical RAM. If this is desired, + consider starting your program with ``RESET``. + + :return: ``self`` to support method chaining. + """ + self.program = program + self._extract_defined_gates() + return self._execute_program() diff --git a/test/unit/test_reference_density.py b/test/unit/test_reference_density.py index 1ca22b207..974690b21 100644 --- a/test/unit/test_reference_density.py +++ b/test/unit/test_reference_density.py @@ -145,7 +145,7 @@ def test_kraus_application_bitflip(): ) initial_density = _random_1q_density() qam.wf_simulator.density = initial_density - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = (1 - p) * initial_density + p * qmats.X.dot(initial_density).dot(qmats.X) np.testing.assert_allclose(final_density, qam.wf_simulator.density) @@ -159,7 +159,7 @@ def test_kraus_application_phaseflip(): ) initial_density = _random_1q_density() qam.wf_simulator.density = initial_density - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = (1 - p) * initial_density + p * qmats.Z.dot(initial_density).dot(qmats.Z) np.testing.assert_allclose(final_density, qam.wf_simulator.density) @@ -174,7 +174,7 @@ def test_kraus_application_bitphaseflip(): ) initial_density = _random_1q_density() qam.wf_simulator.density = initial_density - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = (1 - p) * initial_density + p * qmats.Y.dot(initial_density).dot(qmats.Y) np.testing.assert_allclose(final_density, qam.wf_simulator.density) @@ -189,7 +189,7 @@ def test_kraus_application_relaxation(): ) rho = _random_1q_density() qam.wf_simulator.density = rho - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = np.array( [ @@ -209,7 +209,7 @@ def test_kraus_application_dephasing(): ) rho = _random_1q_density() qam.wf_simulator.density = rho - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = np.array([[rho[0, 0], (1 - p) * rho[0, 1]], [(1 - p) * rho[1, 0], rho[1, 1]]]) np.testing.assert_allclose(final_density, qam.wf_simulator.density) @@ -223,7 +223,7 @@ def test_kraus_application_depolarizing(): ) rho = _random_1q_density() qam.wf_simulator.density = rho - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = (1 - p) * rho + (p / 3) * ( qmats.X.dot(rho).dot(qmats.X) + qmats.Y.dot(rho).dot(qmats.Y) + qmats.Z.dot(rho).dot(qmats.Z) @@ -241,7 +241,7 @@ def test_kraus_compound_T1T2_application(): ) rho = _random_1q_density() qam.wf_simulator.density = rho - qam.execute(Program(I(0))) + qam.execute_once(Program(I(0))) final_density = np.array( [ @@ -311,7 +311,7 @@ def test_multiqubit_decay_bellstate(): quantum_simulator_type=ReferenceDensitySimulator, post_gate_noise_probabilities={"relaxation": p1, "dephasing": p2}, ) - qam.execute(program) + qam.execute_once(program) assert np.allclose(qam.wf_simulator.density, state) From 54ffa390a005e18364fb46a91af08d48d1d26871 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 22:53:09 -0700 Subject: [PATCH 14/61] Fix: QVM + experiment compatibility layer --- pyquil/api/_qpu.py | 1 - pyquil/api/_qvm.py | 4 +-- pyquil/compatibility/v2/api/_qam.py | 2 +- .../compatibility/v2/api/_quantum_computer.py | 32 +++++++------------ .../test_compatibility_operator_estimation.py | 2 +- .../test_compatibility_quantum_computer.py | 5 ++- test/unit/test_compatibility_v2_qvm.py | 2 +- 7 files changed, 19 insertions(+), 29 deletions(-) diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 09dc66fd1..5efc2fc07 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -15,7 +15,6 @@ ############################################################################## from dataclasses import dataclass import uuid -import warnings from collections import defaultdict from typing import Dict, List, Optional, Union, cast diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index d5d6b73f8..51a56411a 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -142,10 +142,10 @@ def execute(self, executable: QuantumExecutable) -> QVMExecuteResponse: if self.noise_model is not None: executable = apply_noise_model(executable, self.noise_model) - quil_program = executable._set_parameter_values_at_runtime() + executable._set_parameter_values_at_runtime() request = qvm_run_request( - quil_program, + executable, classical_addresses, trials, self.measurement_noise, diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index a2d08f6b0..8af5ff1ef 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -19,7 +19,7 @@ def wrap(cls, qam: QAM) -> None: qam.reset() def load(self, executable: QuantumExecutable) -> "QAM": - self._loaded_executable = executable + self._loaded_executable = executable.copy() return self def read_memory(self, region_name: str) -> "QAM": diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index f462bdb54..314706be7 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -134,17 +134,14 @@ def experiment( and symmetrization can all be realized at runtime by providing a ``memory_map``. Thus, the steps in the ``experiment`` method are as follows: - 1. Check to see if this ``Experiment`` has already been loaded into this - ``QuantumComputer`` object. If so, skip to step 2. Otherwise, do the following: - - a. Generate a parameterized program corresponding to the ``Experiment`` - (see the ``Experiment.generate_experiment_program()`` method for more - details on how it changes the main body program to support state preparation, - measurement, and symmetrization). - b. Compile the parameterized program into a parametric (binary) executable, which + 1. Generate a parameterized program corresponding to the ``Experiment`` + (see the ``Experiment.generate_experiment_program()`` method for more + details on how it changes the main body program to support state preparation, + measurement, and symmetrization). + 2. Compile the parameterized program into a parametric (binary) executable, which contains declared variables that can be assigned at runtime. - 2. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following: + 3. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following: a. Build a collection of memory maps that correspond to the various state preparation, measurement, and symmetrization specifications. @@ -153,7 +150,7 @@ def experiment( c. Extract the desired statistics from the classified bitstrings that are produced by the QVM or QPU backend, and package them in an ``ExperimentResult`` object. - 3. Return the list of ``ExperimentResult`` objects. + 4. Return the list of ``ExperimentResult`` objects. This method is extremely useful shorthand for running near-term applications and algorithms, which often have this ansatz + settings structure. @@ -168,15 +165,8 @@ def experiment( :return: A list of ``ExperimentResult`` objects containing the statistics gathered according to the specifications of the ``Experiment``. """ - executable = self.qam._loaded_executable - # if this experiment was the last experiment run on this QuantumComputer, - # then use the executable that is already loaded into the object - if executable is None or self.qam._experiment != experiment: - experiment_program = experiment.generate_experiment_program() - executable = self.compile(experiment_program) - self.qam._experiment = experiment - elif isinstance(self.qam, QVM) and isinstance(executable, Program): - executable = self.compiler.native_quil_to_executable(executable) + experiment_program = experiment.generate_experiment_program() + executable = self.compile(experiment_program) if memory_map is None: memory_map = {} @@ -185,7 +175,7 @@ def experiment( for settings in experiment: # TODO: add support for grouped ExperimentSettings if len(settings) > 1: - raise ValueError("We only support length-1 settings for now.") + raise ValueError("settings must be of length 1") setting = settings[0] qubits = cast(List[int], setting.out_operator.get_qubits()) @@ -197,6 +187,7 @@ def experiment( # TODO: accomplish symmetrization via batch endpoint for merged_memory_map in merged_memory_maps: final_memory_map = {**memory_map, **merged_memory_map} + self.qam.reset() bitstrings = self.run(executable, memory_map=final_memory_map) if "symmetrization" in final_memory_map: @@ -232,6 +223,7 @@ def experiment( additional_results=joint_results[1:], ) results.append(result) + return results def run_symmetrized_readout( diff --git a/test/unit/test_compatibility_operator_estimation.py b/test/unit/test_compatibility_operator_estimation.py index dd614ae9e..b32174cc5 100644 --- a/test/unit/test_compatibility_operator_estimation.py +++ b/test/unit/test_compatibility_operator_estimation.py @@ -657,7 +657,7 @@ def test_measure_observables_grouped_expts(client_configuration: QCSClientConfig if use_seed: num_simulations = 1 - qc.qam.random_seed = 4 + qc.qam.random_seed = 2 else: num_simulations = 100 diff --git a/test/unit/test_compatibility_quantum_computer.py b/test/unit/test_compatibility_quantum_computer.py index adfbbc46b..f4887a450 100644 --- a/test/unit/test_compatibility_quantum_computer.py +++ b/test/unit/test_compatibility_quantum_computer.py @@ -488,13 +488,12 @@ def test_reset(client_configuration: QCSClientConfiguration): Declare(name="ro", memory_type="BIT"), RX(MemoryReference("theta"), 0), MEASURE(0, MemoryReference("ro")), - ).wrap_in_numshots_loop(1000) + ).wrap_in_numshots_loop(10) qc.run(executable=p, memory_map={"theta": [np.pi]}) aref = ParameterAref(name="theta", index=0) assert qc.qam._loaded_executable._variable_values[aref] == np.pi - assert qc.qam._loaded_executable == p - assert qc.qam._result.memory["ro"].shape == (1000, 1) + assert qc.qam._result.memory["ro"].shape == (10, 1) assert all([bit == 1 for bit in qc.qam._result.memory["ro"]]) qc.reset() diff --git a/test/unit/test_compatibility_v2_qvm.py b/test/unit/test_compatibility_v2_qvm.py index 7b7b16b0c..0c7259e5c 100644 --- a/test/unit/test_compatibility_v2_qvm.py +++ b/test/unit/test_compatibility_v2_qvm.py @@ -67,7 +67,7 @@ def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientCon p = Program(Declare("reg", "BIT"), X(0)) qvm.load(p.wrap_in_numshots_loop(100)).run().wait() bitstrings = qvm.read_memory(region_name="reg") - assert bitstrings.shape == (100, 0) + assert bitstrings is None # For backwards compatibility, we support omitting the declaration for "ro" specifically From b7aed862d607700c94145db5bedbd231a1fc72bc Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 22:54:18 -0700 Subject: [PATCH 15/61] Fix: Program support for variable values --- pyquil/quil.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/pyquil/quil.py b/pyquil/quil.py index e14634fe5..5919e7c63 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -40,7 +40,7 @@ from rpcq.messages import NativeQuilMetadata, ParameterAref from pyquil._parser.parser import run_parser -from pyquil.gates import MEASURE, RESET, MOVE +from pyquil.gates import DECLARE, MEASURE, RESET, MOVE from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas, pauli_kraus_map from pyquil.quilatom import ( Label, @@ -518,9 +518,18 @@ def _set_parameter_values_at_runtime(self) -> "Program": MOVE(MemoryReference(name=k.name, offset=k.index), v) for k, v in self._variable_values.items() ] - self._instructions = [*move_instructions, *self.instructions] + self.prepend_instructions(move_instructions) + self._sort_declares_to_program_start() - return percolate_declares(self) + return self + + def prepend_instructions(self, instructions: Iterable[AbstractInstruction]) -> "Program": + """ + Prepend instructions to the beginning of the program. + """ + self._instructions = [*instructions, *self._instructions] + self._synthesized_instructions = None + return self def while_do(self, classical_reg: MemoryReferenceDesignator, q_program: "Program") -> "Program": """ @@ -821,6 +830,13 @@ def is_supported_on_qpu(self) -> bool: except ValueError: return False + def _sort_declares_to_program_start(self) -> "Program": + """ + Re-order DECLARE instructions within this program to the beginning, followed by + all other instructions. Reordering is stable among DECLARE and non-DECLARE instructions. + """ + self._instructions = sorted(self._instructions, key=lambda instruction: not isinstance(instruction, Declare)) + def pop(self) -> AbstractInstruction: """ Pops off the last instruction. @@ -885,10 +901,12 @@ def __add__(self, other: InstructionDesignator) -> "Program": p._calibrations = self.calibrations p._waveforms = self.waveforms p._frames = self.frames + p._variable_values = {k.replace(): v for k, v in self._variable_values.items()} if isinstance(other, Program): p.calibrations.extend(other.calibrations) p.waveforms.update(other.waveforms) p.frames.update(other.frames) + p._variable_values.update({k.replace(): v for k, v in other._variable_values.items()}) return p def __iadd__(self, other: InstructionDesignator) -> "Program": @@ -903,6 +921,7 @@ def __iadd__(self, other: InstructionDesignator) -> "Program": self.calibrations.extend(other.calibrations) self.waveforms.update(other.waveforms) self.frames.update(other.frames) + self._variable_values.update({k.replace(): v for k, v in other._variable_values.items()}) return self def __getitem__(self, index: Union[slice, int]) -> Union[AbstractInstruction, "Program"]: From bf828dcd6402981028aabc97548e225e112860f7 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 22:54:52 -0700 Subject: [PATCH 16/61] Style: fix --- pyquil/device/_main.py | 34 ++++++---------------------------- pyquil/noise.py | 2 +- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/pyquil/device/_main.py b/pyquil/device/_main.py index abab1a0f3..94f25191d 100644 --- a/pyquil/device/_main.py +++ b/pyquil/device/_main.py @@ -98,9 +98,7 @@ def __init__(self, name: str, raw: Dict[str, Any]): # TODO: Introduce distinction between supported ISAs and target ISA self._isa = ISA.from_dict(raw["isa"]) if "isa" in raw and raw["isa"] != {} else None self.specs = Specs.from_dict(raw["specs"]) if raw.get("specs") else None - self.noise_model = ( - NoiseModel.from_dict(raw["noise_model"]) if raw.get("noise_model") else None - ) + self.noise_model = NoiseModel.from_dict(raw["noise_model"]) if raw.get("noise_model") else None @property def isa(self) -> Optional[ISA]: @@ -205,13 +203,7 @@ def qubit_type_to_gates(q: Qubit) -> List[Union[GateInfo, MeasureInfo]]: def edge_type_to_gates(e: Edge) -> List[GateInfo]: gates: List[GateInfo] = [] - if ( - e is None - or isinstance(e.type, str) - and "CZ" == e.type - or isinstance(e.type, list) - and "CZ" in e.type - ): + if e is None or isinstance(e.type, str) and "CZ" == e.type or isinstance(e.type, list) and "CZ" in e.type: gates += [ GateInfo( operator="CZ", @@ -253,13 +245,7 @@ def edge_type_to_gates(e: Edge) -> List[GateInfo]: fidelity=safely_get("fCPHASEs", tuple(e.targets), DEFAULT_CPHASE_FIDELITY), ) ] - if ( - e is None - or isinstance(e.type, str) - and "XY" == e.type - or isinstance(e.type, list) - and "XY" in e.type - ): + if e is None or isinstance(e.type, str) and "XY" == e.type or isinstance(e.type, list) and "XY" in e.type: gates += [ GateInfo( operator="XY", @@ -288,14 +274,8 @@ def edge_type_to_gates(e: Edge) -> List[GateInfo]: return gates assert self._isa is not None - qubits = [ - Qubit(id=q.id, type=None, dead=q.dead, gates=qubit_type_to_gates(q)) - for q in self._isa.qubits - ] - edges = [ - Edge(targets=e.targets, type=None, dead=e.dead, gates=edge_type_to_gates(e)) - for e in self._isa.edges - ] + qubits = [Qubit(id=q.id, type=None, dead=q.dead, gates=qubit_type_to_gates(q)) for q in self._isa.qubits] + edges = [Edge(targets=e.targets, type=None, dead=e.dead, gates=edge_type_to_gates(e)) for e in self._isa.edges] return ISA(qubits, edges) def __str__(self) -> str: @@ -321,9 +301,7 @@ def __init__(self, topology: nx.Graph) -> None: def qubit_topology(self) -> nx.Graph: return self.topology - def get_isa( - self, oneq_type: str = "Xhalves", twoq_type: Optional[Union[str, List[str]]] = None - ) -> ISA: + def get_isa(self, oneq_type: str = "Xhalves", twoq_type: Optional[Union[str, List[str]]] = None) -> ISA: return isa_from_graph(self.topology, oneq_type=oneq_type, twoq_type=twoq_type) def get_specs(self) -> Specs: diff --git a/pyquil/noise.py b/pyquil/noise.py index aab1e89e1..a746cfcda 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -577,7 +577,7 @@ def apply_noise_model(prog: "Program", noise_model: NoiseModel) -> "Program": new_prog += i else: new_prog += i - return new_prog + return prog.copy_everything_except_instructions() + new_prog def add_decoherence_noise( From 586155ef92215f0f516514a7e87166d66844f55b Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Sun, 13 Jun 2021 23:19:15 -0700 Subject: [PATCH 17/61] Fix: rebasing mistakes --- pyquil/device/_isa.py | 0 pyquil/device/_main.py | 314 ----------------------------------------- pyquil/device/qcs.py | 60 -------- 3 files changed, 374 deletions(-) delete mode 100644 pyquil/device/_isa.py delete mode 100644 pyquil/device/_main.py delete mode 100644 pyquil/device/qcs.py diff --git a/pyquil/device/_isa.py b/pyquil/device/_isa.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pyquil/device/_main.py b/pyquil/device/_main.py deleted file mode 100644 index 94f25191d..000000000 --- a/pyquil/device/_main.py +++ /dev/null @@ -1,314 +0,0 @@ -############################################################################## -# Copyright 2016-2019 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. -############################################################################## -import warnings -from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Tuple, Union - -import networkx as nx -import numpy as np - -from pyquil.device._isa import Edge, GateInfo, ISA, MeasureInfo, Qubit, isa_from_graph, isa_to_graph -from pyquil.device._specs import Specs, specs_from_graph -from pyquil.noise import NoiseModel - -PERFECT_FIDELITY = 1e0 -PERFECT_DURATION = 1 / 100 -DEFAULT_CZ_DURATION = 200 -DEFAULT_CZ_FIDELITY = 0.89 -DEFAULT_ISWAP_DURATION = 200 -DEFAULT_ISWAP_FIDELITY = 0.90 -DEFAULT_CPHASE_DURATION = 200 -DEFAULT_CPHASE_FIDELITY = 0.85 -DEFAULT_XY_DURATION = 200 -DEFAULT_XY_FIDELITY = 0.86 -DEFAULT_RX_DURATION = 50 -DEFAULT_RX_FIDELITY = 0.95 -DEFAULT_MEASURE_FIDELITY = 0.90 -DEFAULT_MEASURE_DURATION = 2000 - - -class AbstractDevice(ABC): - @abstractmethod - def qubits(self) -> List[int]: - """ - A sorted list of qubits in the device topology. - """ - - @abstractmethod - def qubit_topology(self) -> nx.Graph: - """ - The connectivity of qubits in this device given as a NetworkX graph. - """ - - @abstractmethod - def get_isa(self, oneq_type: str = "Xhalves", twoq_type: str = "CZ") -> ISA: - """ - Construct an ISA suitable for targeting by compilation. - - This will raise an exception if the requested ISA is not supported by the device. - - :param oneq_type: The family of one-qubit gates to target - :param twoq_type: The family of two-qubit gates to target - """ - - @abstractmethod - def get_specs(self) -> Optional[Specs]: - """ - Construct a Specs object required by compilation - """ - - -class Device(AbstractDevice): - """ - A device (quantum chip) that can accept programs. - - Only devices that are online will actively be - accepting new programs. In addition to the ``self._raw`` attribute, two other attributes are - optionally constructed from the entries in ``self._raw`` -- ``isa`` and ``noise_model`` -- which - should conform to the dictionary format required by the ``.from_dict()`` methods for ``ISA`` - and ``NoiseModel``, respectively. - - :ivar dict _raw: Raw JSON response from the server with additional information about the device. - :ivar ISA isa: The instruction set architecture (ISA) for the device. - :ivar NoiseModel noise_model: The noise model for the device. - """ - - # TODO(andrew): update this to take a new ISA object - def __init__(self, name: str, raw: Dict[str, Any]): - """ - :param name: name of the device - :param raw: raw JSON response from the server with additional information about this device. - """ - self.name = name - self._raw = raw - - # TODO: Introduce distinction between supported ISAs and target ISA - self._isa = ISA.from_dict(raw["isa"]) if "isa" in raw and raw["isa"] != {} else None - self.specs = Specs.from_dict(raw["specs"]) if raw.get("specs") else None - self.noise_model = NoiseModel.from_dict(raw["noise_model"]) if raw.get("noise_model") else None - - @property - def isa(self) -> Optional[ISA]: - warnings.warn("Accessing the static ISA is deprecated. Use `get_isa`", DeprecationWarning) - return self._isa - - def qubits(self) -> List[int]: - assert self._isa is not None - return sorted(q.id for q in self._isa.qubits if not q.dead) - - def qubit_topology(self) -> nx.Graph: - """ - The connectivity of qubits in this device given as a NetworkX graph. - """ - assert self._isa is not None - return isa_to_graph(self._isa) - - def get_specs(self) -> Optional[Specs]: - return self.specs - - def get_isa(self, oneq_type: Optional[str] = None, twoq_type: Optional[str] = None) -> ISA: - """ - Construct an ISA suitable for targeting by compilation. - - This will raise an exception if the requested ISA is not supported by the device. - """ - if oneq_type is not None or twoq_type is not None: - raise ValueError( - "oneq_type and twoq_type are both fatally deprecated. If you want to " - "make an ISA with custom gate types, you'll have to do it by hand." - ) - - def safely_get(attr: str, index: Union[int, Tuple[int, ...]], default: Any) -> Any: - if self.specs is None: - return default - - getter = getattr(self.specs, attr, None) - if getter is None: - return default - - array = getter() - if (isinstance(index, int) and index < len(array)) or index in array: - return array[index] - else: - return default - - def qubit_type_to_gates(q: Qubit) -> List[Union[GateInfo, MeasureInfo]]: - gates: List[Union[GateInfo, MeasureInfo]] = [ - MeasureInfo( - operator="MEASURE", - qubit=q.id, - target="_", - fidelity=safely_get("fROs", q.id, DEFAULT_MEASURE_FIDELITY), - duration=DEFAULT_MEASURE_DURATION, - ), - MeasureInfo( - operator="MEASURE", - qubit=q.id, - target=None, - fidelity=safely_get("fROs", q.id, DEFAULT_MEASURE_FIDELITY), - duration=DEFAULT_MEASURE_DURATION, - ), - ] - if q.type is None or "Xhalves" in q.type: - gates += [ - GateInfo( - operator="RZ", - parameters=["_"], - arguments=[q.id], - duration=PERFECT_DURATION, - fidelity=PERFECT_FIDELITY, - ), - GateInfo( - operator="RX", - parameters=[0.0], - arguments=[q.id], - duration=DEFAULT_RX_DURATION, - fidelity=PERFECT_FIDELITY, - ), - ] - gates += [ - GateInfo( - operator="RX", - parameters=[param], - arguments=[q.id], - duration=DEFAULT_RX_DURATION, - fidelity=safely_get("f1QRBs", q.id, DEFAULT_RX_FIDELITY), - ) - for param in [np.pi, -np.pi, np.pi / 2, -np.pi / 2] - ] - if q.type is not None and "WILDCARD" in q.type: - gates += [ - GateInfo( - operator="_", - parameters="_", - arguments=[q.id], - duration=PERFECT_DURATION, - fidelity=PERFECT_FIDELITY, - ) - ] - return gates - - def edge_type_to_gates(e: Edge) -> List[GateInfo]: - gates: List[GateInfo] = [] - if e is None or isinstance(e.type, str) and "CZ" == e.type or isinstance(e.type, list) and "CZ" in e.type: - gates += [ - GateInfo( - operator="CZ", - parameters=[], - arguments=["_", "_"], - duration=DEFAULT_CZ_DURATION, - fidelity=safely_get("fCZs", tuple(e.targets), DEFAULT_CZ_FIDELITY), - ) - ] - if ( - e is None - or isinstance(e.type, str) - and "ISWAP" == e.type - or isinstance(e.type, list) - and "ISWAP" in e.type - ): - gates += [ - GateInfo( - operator="ISWAP", - parameters=[], - arguments=["_", "_"], - duration=DEFAULT_ISWAP_DURATION, - fidelity=safely_get("fISWAPs", tuple(e.targets), DEFAULT_ISWAP_FIDELITY), - ) - ] - if ( - e is None - or isinstance(e.type, str) - and "CPHASE" == e.type - or isinstance(e.type, list) - and "CPHASE" in e.type - ): - gates += [ - GateInfo( - operator="CPHASE", - parameters=["theta"], - arguments=["_", "_"], - duration=DEFAULT_CPHASE_DURATION, - fidelity=safely_get("fCPHASEs", tuple(e.targets), DEFAULT_CPHASE_FIDELITY), - ) - ] - if e is None or isinstance(e.type, str) and "XY" == e.type or isinstance(e.type, list) and "XY" in e.type: - gates += [ - GateInfo( - operator="XY", - parameters=["theta"], - arguments=["_", "_"], - duration=DEFAULT_XY_DURATION, - fidelity=safely_get("fXYs", tuple(e.targets), DEFAULT_XY_FIDELITY), - ) - ] - if ( - e is None - or isinstance(e.type, str) - and "WILDCARD" == e.type - or isinstance(e.type, list) - and "WILDCARD" in e.type - ): - gates += [ - GateInfo( - operator="_", - parameters="_", - arguments=["_", "_"], - duration=PERFECT_DURATION, - fidelity=PERFECT_FIDELITY, - ) - ] - return gates - - assert self._isa is not None - qubits = [Qubit(id=q.id, type=None, dead=q.dead, gates=qubit_type_to_gates(q)) for q in self._isa.qubits] - edges = [Edge(targets=e.targets, type=None, dead=e.dead, gates=edge_type_to_gates(e)) for e in self._isa.edges] - return ISA(qubits, edges) - - def __str__(self) -> str: - return "".format(self.name) - - def __repr__(self) -> str: - return str(self) - - -class NxDevice(AbstractDevice): - """A shim over the AbstractDevice API backed by a NetworkX graph. - - A ``Device`` holds information about the physical device. - Specifically, you might want to know about connectivity, available gates, performance specs, - and more. This class implements the AbstractDevice API for devices not available via - ``get_devices()``. Instead, the user is responsible for constructing a NetworkX - graph which represents a chip topology. - """ - - def __init__(self, topology: nx.Graph) -> None: - self.topology = topology - - def qubit_topology(self) -> nx.Graph: - return self.topology - - def get_isa(self, oneq_type: str = "Xhalves", twoq_type: Optional[Union[str, List[str]]] = None) -> ISA: - return isa_from_graph(self.topology, oneq_type=oneq_type, twoq_type=twoq_type) - - def get_specs(self) -> Specs: - return specs_from_graph(self.topology) - - def qubits(self) -> List[int]: - return sorted(self.topology.nodes) - - def edges(self) -> List[Tuple[Any, ...]]: - return sorted(tuple(sorted(pair)) for pair in self.topology.edges) diff --git a/pyquil/device/qcs.py b/pyquil/device/qcs.py deleted file mode 100644 index 90d9c1225..000000000 --- a/pyquil/device/qcs.py +++ /dev/null @@ -1,60 +0,0 @@ -from qcs_api_client.models import InstructionSetArchitecture -from qcs_api_client.operations.sync import get_instruction_set_architecture -from pyquil.external.rpcq import CompilerISA -from pyquil.device.transformers import qcs_isa_to_compiler_isa, qcs_isa_to_graph -from pyquil.device import AbstractDevice -from pyquil.noise import NoiseModel -from pyquil.api import Client -import networkx as nx -from typing import List, Optional - - -class QCSDevice(AbstractDevice): - """ - An AbstractDevice initialized with an ``InstructionSetArchitecture`` returned - from the QCS API. Notably, this class is able to serialize a ``CompilerISA`` based - on the architecture instructions. - """ - - quantum_processor_id: str - _isa: InstructionSetArchitecture - noise_model: Optional[NoiseModel] - - def __init__( - self, - quantum_processor_id: str, - isa: InstructionSetArchitecture, - noise_model: Optional[NoiseModel] = None, - ): - """ - Initialize a new QCSDevice. - - :param quantum_processor_id: The id of the quantum processor. - :param isa: The QCS API ``InstructionSetArchitecture``. - :param noise_model: An optional ``NoiseModel`` for configuring a noisy device on the ``QVM``. - """ - - self.quantum_processor_id = quantum_processor_id - self._isa = isa - self.noise_model = noise_model - - def qubits(self) -> List[int]: - return sorted(node.node_id for node in self._isa.architecture.nodes) - - def qubit_topology(self) -> nx.Graph: - return qcs_isa_to_graph(self._isa) - - def to_compiler_isa(self) -> CompilerISA: - return qcs_isa_to_compiler_isa(self._isa) - - def __str__(self) -> str: - return "".format(self.quantum_processor_id) - - def __repr__(self) -> str: - return str(self) - - -def get_qcs_device(client: Client, quantum_processor_id: str) -> QCSDevice: - isa = client.qcs_request(get_instruction_set_architecture, quantum_processor_id=quantum_processor_id) - - return QCSDevice(quantum_processor_id=quantum_processor_id, isa=isa) From e0ee0676ee3a5596b5c8e30ae324afa2183ea0a9 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:41:16 -0700 Subject: [PATCH 18/61] Rename compatibility tests --- ...timation.py => test_compatibility_v2_operator_estimation.py} | 0 ...um_computer.py => test_compatibility_v2_quantum_computer.py} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename test/unit/{test_compatibility_operator_estimation.py => test_compatibility_v2_operator_estimation.py} (100%) rename test/unit/{test_compatibility_quantum_computer.py => test_compatibility_v2_quantum_computer.py} (99%) diff --git a/test/unit/test_compatibility_operator_estimation.py b/test/unit/test_compatibility_v2_operator_estimation.py similarity index 100% rename from test/unit/test_compatibility_operator_estimation.py rename to test/unit/test_compatibility_v2_operator_estimation.py diff --git a/test/unit/test_compatibility_quantum_computer.py b/test/unit/test_compatibility_v2_quantum_computer.py similarity index 99% rename from test/unit/test_compatibility_quantum_computer.py rename to test/unit/test_compatibility_v2_quantum_computer.py index f4887a450..0b936391b 100644 --- a/test/unit/test_compatibility_quantum_computer.py +++ b/test/unit/test_compatibility_v2_quantum_computer.py @@ -492,7 +492,7 @@ def test_reset(client_configuration: QCSClientConfiguration): qc.run(executable=p, memory_map={"theta": [np.pi]}) aref = ParameterAref(name="theta", index=0) - assert qc.qam._loaded_executable._variable_values[aref] == np.pi + assert qc.qam._loaded_executable._memory.values[aref] == np.pi assert qc.qam._result.memory["ro"].shape == (10, 1) assert all([bit == 1 for bit in qc.qam._result.memory["ro"]]) From 186ba0c638138f8843fb078d13d7c68b00b84d04 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:42:08 -0700 Subject: [PATCH 19/61] Update: refactor program memory management --- pyquil/_memory.py | 56 ++++++ pyquil/api/__init__.py | 27 +-- pyquil/api/_abstract_compiler.py | 26 ++- pyquil/api/_benchmark.py | 5 +- pyquil/api/_compiler.py | 10 +- pyquil/api/_error_reporting.py | 256 -------------------------- pyquil/api/_memory.py | 54 ++++++ pyquil/api/_qam.py | 2 - pyquil/api/_qpu.py | 80 ++++---- pyquil/api/_quantum_computer.py | 44 ++--- pyquil/api/_qvm.py | 8 +- pyquil/api/_wavefunction_simulator.py | 10 +- pyquil/compatibility/v2/api/_qam.py | 2 +- pyquil/quil.py | 62 ++----- 14 files changed, 239 insertions(+), 403 deletions(-) create mode 100644 pyquil/_memory.py delete mode 100644 pyquil/api/_error_reporting.py create mode 100644 pyquil/api/_memory.py diff --git a/pyquil/_memory.py b/pyquil/_memory.py new file mode 100644 index 000000000..8397eb817 --- /dev/null +++ b/pyquil/_memory.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass, field +from typing import Dict, Sequence, Union + +from rpcq.messages import ParameterAref + + +@dataclass +class Memory: + """ + Memory encapsulates the values to be sent as parameters alongside a program at time of + execution, and read back afterwards. + """ + + values: Dict[ParameterAref, Union[int, float]] = field(default_factory=dict) + + def copy(self) -> "Memory": + """ + Return a deep copy of this Memory object. + """ + return Memory(values={k.replace(): v for k, v in self.values.items()}) + + def write(self, parameter_values: Dict[Union[str, ParameterAref], Union[int, float]]) -> "Memory": + """ + Set the given values for the given parameters. + """ + for parameter, parameter_value in parameter_values.items(): + self._write_value(parameter=parameter, value=parameter_value) + return self + + def _write_value( + self, + *, + parameter: Union[ParameterAref, str], + value: Union[int, float, Sequence[int], Sequence[float]], + ) -> "Memory": + """ + Set the given parameter to the given value. + + :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. + :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list + is provided, parameter must be a ``str`` or ``parameter.offset == 0``. + """ + if isinstance(parameter, str): + parameter = ParameterAref(name=parameter, index=0) + + if isinstance(value, (int, float)): + self.values[parameter] = value + elif isinstance(value, Sequence): + if parameter.index != 0: + raise ValueError("Parameter may not have a non-zero index when its value is a sequence") + + for index, v in enumerate(value): + aref = ParameterAref(name=parameter.name, index=index) + self.values[aref] = v + + return self diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py index 9fb6a9028..1ede3edf8 100644 --- a/pyquil/api/__init__.py +++ b/pyquil/api/__init__.py @@ -18,24 +18,25 @@ """ __all__ = [ - "QCSClientConfiguration", - "QuantumExecutable", + "BenchmarkConnection", "EncryptedProgram", - "QVMCompiler", - "QPUCompiler", "EngagementManager", - "QCSQuantumProcessor", - "pyquil_protect", - "WavefunctionSimulator", - "QuantumComputer", - "list_quantum_computers", "get_qc", + "list_quantum_computers", "local_forest_runtime", + "Memory", + "pyquil_protect", "QAM", - "QVM", - "QPU", - "BenchmarkConnection", "QAMExecutionResult", + "QCSClientConfiguration", + "QCSQuantumProcessor", + "QPU", + "QPUCompiler", + "QuantumComputer", + "QuantumExecutable", + "QVM", + "QVMCompiler", + "WavefunctionSimulator", ] from qcs_api_client.client import QCSClientConfiguration @@ -43,7 +44,7 @@ from pyquil.api._benchmark import BenchmarkConnection from pyquil.api._compiler import QVMCompiler, QPUCompiler, QuantumExecutable, EncryptedProgram from pyquil.api._engagement_manager import EngagementManager -from pyquil.api._error_reporting import pyquil_protect +from pyquil.api._memory import Memory from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qpu import QPU from pyquil.api._quantum_computer import ( diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 22fac1079..3b20a072e 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -26,7 +26,7 @@ from pyquil._version import pyquil_version from pyquil.api._compiler_client import CompilerClient, CompileToNativeQuilRequest -from pyquil.api._error_reporting import _record_call +from pyquil.api._memory import Memory from pyquil.external.rpcq import compiler_isa_to_target_quantum_processor from pyquil.parser import parse_program from pyquil.paulis import PauliTerm @@ -62,6 +62,25 @@ class EncryptedProgram: recalculation_table: Dict[ParameterAref, ExpressionDesignator] """A mapping from memory references to the original gate arithmetic.""" + _memory: Memory + """Memory values (parameters) to be sent with the program.""" + + def copy(self) -> "EncryptedProgram": + """ + Return a deep copy of this EncryptedProgram. + """ + return self.replace(memory=self._memory.copy()) + + def write_memory( + self, + *, + region_name: str, + value: Union[int, float, Sequence[int], Sequence[float]], + offset: Optional[int] = None, + ) -> "Program": + self._memory._write_value(parameter=ParameterAref(name=region_name, index=offset), value=value) + return self + QuantumExecutable = Union[EncryptedProgram, Program] @@ -90,7 +109,6 @@ def get_version_info(self) -> Dict[str, Any]: """ return {"quilc": self._compiler_client.get_version()} - @_record_call def quil_to_native_quil(self, program: Program, *, protoquil: Optional[bool] = None) -> Program: """ Compile an arbitrary quil program according to the ISA of ``self.quantum_processor``. @@ -125,6 +143,7 @@ def quil_to_native_quil(self, program: Program, *, protoquil: Optional[bool] = N ) nq_program.num_shots = program.num_shots nq_program._calibrations = program.calibrations + nq_program._memory = program._memory.copy() return nq_program def _connect(self) -> None: @@ -146,7 +165,6 @@ def native_quil_to_executable(self, nq_program: Program) -> QuantumExecutable: :return: An (opaque) binary executable """ - @_record_call def reset(self) -> None: """ Reset the state of the this compiler. @@ -160,7 +178,7 @@ def _check_quilc_version(version: str) -> None: :param version: quilc version. """ - major, minor, patch = map(int, version.split(".")) + major, minor, _ = map(int, 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 {version} and pyquil {pyquil_version}" diff --git a/pyquil/api/_benchmark.py b/pyquil/api/_benchmark.py index 1b99e14f7..f9a5ad42d 100644 --- a/pyquil/api/_benchmark.py +++ b/pyquil/api/_benchmark.py @@ -23,7 +23,7 @@ ConjugatePauliByCliffordRequest, CompilerClient, ) -from pyquil.api._error_reporting import _record_call + from pyquil.paulis import PauliTerm, is_identity from pyquil.quil import address_qubits, Program from pyquil.quilbase import Gate @@ -34,7 +34,6 @@ class BenchmarkConnection(AbstractBenchmarker): Represents a connection to a server that generates benchmarking data. """ - @_record_call def __init__(self, *, timeout: float = 10.0, client_configuration: Optional[QCSClientConfiguration] = None): """ Client to communicate with the benchmarking data endpoint. @@ -48,7 +47,6 @@ def __init__(self, *, timeout: float = 10.0, client_configuration: Optional[QCSC request_timeout=timeout, ) - @_record_call def apply_clifford_to_pauli(self, clifford: Program, pauli_in: PauliTerm) -> PauliTerm: r""" Given a circuit that consists only of elements of the Clifford group, @@ -86,7 +84,6 @@ def apply_clifford_to_pauli(self, clifford: Program, pauli_in: PauliTerm) -> Pau pauli_out = cast(PauliTerm, pauli_out * PauliTerm(pauli, all_qubits[i])) return cast(PauliTerm, pauli_out * pauli_in.coefficient) - @_record_call def generate_rb_sequence( self, depth: int, diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py index 1c4c13590..cce33ab8c 100644 --- a/pyquil/api/_compiler.py +++ b/pyquil/api/_compiler.py @@ -30,7 +30,7 @@ from rpcq.messages import ParameterSpec from pyquil.api._abstract_compiler import AbstractCompiler, QuantumExecutable, EncryptedProgram -from pyquil.api._error_reporting import _record_call + from pyquil.api._qcs_client import qcs_client from pyquil.api._rewrite_arithmetic import rewrite_arithmetic from pyquil.parser import parse_program, parse @@ -77,7 +77,6 @@ class QPUCompiler(AbstractCompiler): Client to communicate with the compiler and translation service. """ - @_record_call def __init__( self, *, @@ -103,7 +102,6 @@ def __init__( self.quantum_processor_id = quantum_processor_id self._calibration_program: Optional[Program] = None - @_record_call def native_quil_to_executable(self, nq_program: Program) -> QuantumExecutable: arithmetic_response = rewrite_arithmetic(nq_program) @@ -132,6 +130,7 @@ def to_expression(rule: str) -> ExpressionDesignator: recalculation_table={ mref: to_expression(rule) for mref, rule in arithmetic_response.recalculation_table.items() }, + _memory=nq_program._memory.copy(), ) def _get_calibration_program(self) -> Program: @@ -139,13 +138,11 @@ def _get_calibration_program(self) -> Program: response = get_quilt_calibrations(client=qcs_client, quantum_processor_id=self.quantum_processor_id).parsed return parse_program(response.quilt) - @_record_call def refresh_calibration_program(self) -> None: """Refresh the calibration program cache.""" self._calibration_program = self._get_calibration_program() @property # type: ignore - @_record_call def calibration_program(self) -> Program: """ Get the Quil-T calibration program associated with the underlying QPU. @@ -165,7 +162,6 @@ def calibration_program(self) -> Program: assert self._calibration_program is not None return self._calibration_program - @_record_call def reset(self) -> None: """ Reset the state of the QPUCompiler. @@ -186,7 +182,6 @@ class QVMCompiler(AbstractCompiler): Client to communicate with the compiler. """ - @_record_call def __init__( self, *, @@ -207,6 +202,5 @@ def __init__( client_configuration=client_configuration, ) - @_record_call def native_quil_to_executable(self, nq_program: Program) -> QuantumExecutable: return nq_program diff --git a/pyquil/api/_error_reporting.py b/pyquil/api/_error_reporting.py deleted file mode 100644 index a9c3ea783..000000000 --- a/pyquil/api/_error_reporting.py +++ /dev/null @@ -1,256 +0,0 @@ -############################################################################## -# Copyright 2018 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 automatically generating error reports helpful for diagnosing pyQuil errors. - -IMPORTANT NOTE: THIS MODULE USES GLOBAL STATE AND IS NOT ESPECIALLY THREAD-SAFE. - If your threaded code is experiencing pyQuil errors, you'll have to track - your own state and not use this convenient decorator. -""" -import inspect -import json -import logging -import os -import sys -from datetime import datetime, date -from functools import wraps -from typing import List, Dict, Any, Callable, Optional - -from pyquil import Program -from pyquil._version import pyquil_version - -if sys.version_info < (3, 7): - from pyquil.external.dataclasses import dataclass, is_dataclass, asdict -else: - from dataclasses import dataclass, is_dataclass, asdict - -_log = logging.getLogger(__name__) - - -@dataclass -class ErrorReport: - """ - Dump of the current state of a pyQuil program. - """ - - stack_trace: List["StacktraceFrame"] - timestamp: date - call_log: Dict[str, "CallLogValue"] - exception: Exception # noqa: E701 - system_info: Dict[str, str] - - -@dataclass -class StacktraceFrame: - """ - Expanded frame in a stacktrace, suitable for JSON export. - """ - - name: str - filename: str - line_number: int - locals: Dict[str, str] - - -@dataclass(eq=True, frozen=True) -class CallLogKey: - """ - Entry in the call log list, suitable for JSON export. - """ - - name: str - args: List[str] - kwargs: Dict[str, Any] - - def __hash__(self) -> int: - finger_print = (self.name,) + tuple(self.args) + tuple(sorted(self.kwargs.items(), key=lambda i: i[0])) - return hash(finger_print) - - def __repr__(self) -> str: - ret = self.name + "(" - for item in self.args: - ret += repr(item) + ", " - for k, v in self.kwargs.items(): - ret += k + "=" + repr(v) + ", " - ret += ")" - - return ret - - -@dataclass -class CallLogValue: - """ - Entry in the call log list, suitable for JSON export. - """ - - timestamp_in: date - timestamp_out: Optional[date] - return_value: Optional[str] - - -def json_serialization_helper(o: object) -> Any: - if is_dataclass(o): - return asdict(o) - elif isinstance(o, datetime): - return o.isoformat() - elif isinstance(o, Exception): - return repr(o) - else: - raise TypeError("unable to serialize object {}".format(o)) - - -def generate_system_info() -> Dict[str, str]: - system_info = {"python_version": sys.version, "pyquil_version": pyquil_version()} - - return system_info - - -def serialize_object_for_logging(o: object) -> str: - if isinstance(o, Program): - return str(o) - else: - return repr(o) - - -def flatten_log(log: Dict[CallLogKey, CallLogValue]) -> Dict[str, CallLogValue]: - return {repr(k): v for k, v in log.items()} - - -class ErrorContext(object): - """ - Tracks information relevant to error reporting. - """ - - log: Dict[CallLogKey, CallLogValue] = {} - filename = "pyquil_error.log" - - def generate_report(self, exception: Exception, trace: List[inspect.FrameInfo]) -> ErrorReport: - """ - Handle an error generated in a routine decorated with the pyQuil error handler. - - :param exception: Exception object that generated this error. - :param trace: inspect.trace object from the frame that caught the error. - :return: ErrorReport object - """ - stack_trace = [ - StacktraceFrame( - name=item.function, - filename=item.filename, - line_number=item.lineno, - locals={k: serialize_object_for_logging(v) for (k, v) in item.frame.f_locals.items()}, - ) - for item in trace - ] - - system_info = generate_system_info() - - report = ErrorReport( - stack_trace=stack_trace, - timestamp=datetime.utcnow(), - exception=exception, - system_info=system_info, - call_log=flatten_log(self.log), - ) - - return report - - def dump_error(self, exception: Exception, trace: List[inspect.FrameInfo]) -> None: - warn_msg = """ ->>> PYQUIL_PROTECT <<< -An uncaught exception was raised in a function wrapped in pyquil_protect. We are writing out a -log file to "{}". - -Along with a description of what you were doing when the error occurred, send this file to -Rigetti Computing support by email at support@rigetti.com for assistance. ->>> PYQUIL_PROTECT <<< -""".format( - os.path.abspath(self.filename) - ) - - _log.warning(warn_msg) - - report = self.generate_report(exception, trace) - - # overwrite any existing log file - fh = open(self.filename, "w") - fh.write(json.dumps(report, default=json_serialization_helper)) - fh.close() - - -global_error_context: Optional[ErrorContext] = None - - -def pyquil_protect(func: Callable[..., Any], log_filename: str = "pyquil_error.log") -> Callable[..., Any]: - """ - A decorator that sets up an error context, captures errors, and tears down the context. - """ - - def pyquil_protect_wrapper(*args: Any, **kwargs: Any) -> Any: - global global_error_context - - old_error_context = global_error_context - global_error_context = ErrorContext() - global_error_context.filename = log_filename - - try: - val = func(*args, **kwargs) - global_error_context = old_error_context - return val - except Exception as e: - assert global_error_context is not None - global_error_context.dump_error(e, inspect.trace()) - global_error_context = old_error_context - raise - - return pyquil_protect_wrapper - - -def _record_call(func: Callable[..., Any]) -> Callable[..., Any]: - """ - A decorator that logs a call into the global error context. - - This is probably for internal use only. - """ - - @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - global global_error_context - - # log a call as about to take place - if global_error_context is not None: - key = CallLogKey( - name=func.__name__, - args=[serialize_object_for_logging(arg) for arg in args], - kwargs={k: serialize_object_for_logging(v) for k, v in kwargs.items()}, - ) - - pre_entry = CallLogValue(timestamp_in=datetime.utcnow(), timestamp_out=None, return_value=None) - global_error_context.log[key] = pre_entry - - val = func(*args, **kwargs) - - # poke the return value of that call in - if global_error_context is not None: - post_entry = CallLogValue( - timestamp_in=pre_entry.timestamp_in, - timestamp_out=datetime.utcnow(), - return_value=serialize_object_for_logging(val), - ) - global_error_context.log[key] = post_entry - - return val - - return wrapper diff --git a/pyquil/api/_memory.py b/pyquil/api/_memory.py new file mode 100644 index 000000000..6c2da7f80 --- /dev/null +++ b/pyquil/api/_memory.py @@ -0,0 +1,54 @@ +from typing import Dict, Sequence, Union + +from rpcq.messages import ParameterAref + + +class Memory: + """ + Memory encapsulates the values to be sent as parameters alongside a program at time of + execution, and read back afterwards. + """ + + values: Dict[ParameterAref, Union[int, float]] + + def copy(self) -> "Memory": + """ + Return a deep copy of this Memory object. + """ + return Memory(values={k.replace(): v for k, v in self.values.items()}) + + def write(self, parameter_values: Dict[Union[str, ParameterAref], Union[int, float]]) -> "Memory": + """ + Set the given values for the given parameters. + """ + for parameter, parameter_value in parameter_values.items(): + self._write_value(parameter=parameter, value=parameter_value) + return self + + def _write_value( + self, + *, + parameter: Union[ParameterAref, str], + value: Union[int, float, Sequence[int], Sequence[float]], + ) -> "Memory": + """ + Mutate the program to set the given parameter value. + + :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. + :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list + is provided, parameter must be a ``str`` or ``parameter.offset == 0``. + """ + if isinstance(parameter, str): + parameter = ParameterAref(name=parameter, index=0) + + if isinstance(value, (int, float)): + self.values[parameter] = value + elif isinstance(value, Sequence): + if parameter.index != 0: + raise ValueError("Parameter may not have a non-zero index when its value is a sequence") + + for index, v in enumerate(value): + aref = ParameterAref(name=parameter.name, index=index) + self.values[aref] = v + + return self diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index dae23b447..3a5d082df 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -19,7 +19,6 @@ import numpy as np from pyquil.api._abstract_compiler import QuantumExecutable -from pyquil.api._error_reporting import _record_call from pyquil.experiment._main import Experiment @@ -53,7 +52,6 @@ class QAM(ABC, Generic[ExecuteResponse]): computer interacts with a live quantum computer. """ - @_record_call def __init__(self) -> None: self.experiment: Optional[Experiment] diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 5efc2fc07..1c0676ecf 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -23,7 +23,8 @@ from rpcq.messages import ParameterAref, ParameterSpec from pyquil.api import QuantumExecutable, EncryptedProgram, EngagementManager -from pyquil.api._error_reporting import _record_call + +from pyquil._memory import Memory from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qpu_client import GetBuffersRequest, QPUClient, BufferResponse, RunProgramRequest from pyquil.quilatom import ( @@ -102,11 +103,10 @@ def alloc(spec: ParameterSpec) -> np.ndarray: @dataclass class QPUExecuteResponse: job_id: str - executable: EncryptedProgram + _executable: EncryptedProgram class QPU(QAM[QPUExecuteResponse]): - @_record_call def __init__( self, *, @@ -120,11 +120,13 @@ def __init__( A connection to the QPU. :param quantum_processor_id: Processor to run against. - :param priority: The priority with which to insert jobs into the QPU queue. Lower - integers correspond to higher priority. + :param priority: The priority with which to insert jobs into the QPU queue. Lower integers + correspond to higher priority. :param timeout: Time limit for requests, in seconds. - :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. - :param engagement_manager: Optional engagement manager. If none is provided, a default one will be created. + :param client_configuration: Optional client configuration. If none is provided, a default + one will be loaded. + :param engagement_manager: Optional engagement manager. If none is provided, a default one + will be created. """ super().__init__() @@ -145,8 +147,12 @@ def quantum_processor_id(self) -> str: """ID of quantum processor targeted.""" return self._qpu_client.quantum_processor_id - @_record_call def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: + """ + Enqueue a job for execution on the QPU. Returns a ``QPUExecuteResponse``, a + job descriptor which should be passed directly to ``QPU.get_results`` to retrieve + results. + """ assert isinstance( executable, EncryptedProgram ), "QPU#execute requires an rpcq.EncryptedProgram. Create one with QuantumComputer#compile" @@ -158,26 +164,30 @@ def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: request = RunProgramRequest( id=str(uuid.uuid4()), priority=self.priority, - program=self.executable.program, + program=executable.program, patch_values=self._build_patch_values(), ) job_id = self._qpu_client.run_program(request).job_id return QPUExecuteResponse(job_id=job_id) - @_record_call def get_results(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult: + """ + Retrieve results from execution on the QPU. + """ raw_results = self._get_buffers(execute_response.job_id) - ro_sources = execute_response.executable.ro_sources + ro_sources = execute_response._executable.ro_sources result_memory = {} if raw_results is not None: - extracted = _extract_memory_regions(execute_response.executable.memory_descriptors, ro_sources, raw_results) + extracted = _extract_memory_regions( + execute_response._executable.memory_descriptors, ro_sources, raw_results + ) for name, array in extracted.items(): result_memory[name] = array elif not ro_sources: result_memory["ro"] = np.zeros((0, 0), dtype=np.int64) - QAMExecutionResult(executable=execute_response.executable, results=result_memory) + QAMExecutionResult(executable=execute_response._executable, results=result_memory) def _get_buffers(self, job_id: str) -> Dict[str, np.ndarray]: """ @@ -190,16 +200,20 @@ def _get_buffers(self, job_id: str) -> Dict[str, np.ndarray]: buffers = self._qpu_client.get_buffers(request).buffers return {k: decode_buffer(v) for k, v in buffers.items()} - def _build_patch_values(self) -> Dict[str, List[Union[int, float]]]: + @classmethod + def _build_patch_values(cls, program: EncryptedProgram) -> Dict[str, List[Union[int, float]]]: + """ + Construct the patch values from the program to be used in execution. + """ patch_values = {} # Now that we are about to run, we have to resolve any gate parameter arithmetic that was # saved in the executable's recalculation table, and add those values to the variables shim - self._update_variables_shim_with_recalculation_table() + cls._update_memory_with_recalculation_table(program=program) # Initialize our patch table - assert isinstance(self.executable, EncryptedProgram) - recalculation_table = self.executable.recalculation_table + assert isinstance(program, EncryptedProgram) + recalculation_table = program.recalculation_table memory_ref_names = list(set(mr.name for mr in recalculation_table.keys())) if memory_ref_names: assert len(memory_ref_names) == 1, ( @@ -209,23 +223,24 @@ def _build_patch_values(self) -> Dict[str, List[Union[int, float]]]: memory_reference_name = memory_ref_names[0] patch_values[memory_reference_name] = [0.0] * len(recalculation_table) - for name, spec in self.executable.memory_descriptors.items(): + for name, spec in program.memory_descriptors.items(): # NOTE: right now we fake reading out measurement values into classical memory # hence we omit them here from the patch table. - if any(name == mref.name for mref in self.executable.ro_sources): + if any(name == mref.name for mref in program.ro_sources): continue initial_value = 0.0 if spec.type == "REAL" else 0 patch_values[name] = [initial_value] * spec.length # Fill in our patch table - for k, v in self._variables_shim.items(): + for k, v in program._memory.values.items(): patch_values[k.name][k.index] = v return patch_values - def _update_variables_shim_with_recalculation_table(self) -> None: + @classmethod + def _update_memory_with_recalculation_table(cls, program: EncryptedProgram) -> None: """ - Update self._variables_shim with the final values to be patched into the gate parameters, + Update the program's memory with the final values to be patched into the gate parameters, according to the arithmetic expressions in the original program. For example:: @@ -253,8 +268,8 @@ def _update_variables_shim_with_recalculation_table(self) -> None: .. code-block:: python - qpu.write_memory(region_name='theta', value=0.5) - qpu.write_memory(region_name='beta', value=0.1) + compiled_program.memory.write(region_name='theta', value=0.5) + compiled_program.memory.write(region_name='beta', value=0.1) After executing this function, our self.variables_shim in the above example would contain the following: @@ -270,13 +285,14 @@ def _update_variables_shim_with_recalculation_table(self) -> None: Once the _variables_shim is filled, execution continues as with regular binary patching. """ - assert isinstance(self.executable, EncryptedProgram) - for mref, expression in self.executable.recalculation_table.items(): + assert isinstance(program, EncryptedProgram) + for mref, expression in program.recalculation_table.items(): # Replace the user-declared memory references with any values the user has written, # coerced to a float because that is how we declared it. - self._variables_shim[mref] = float(self._resolve_memory_references(expression)) + program._memory.values[mref] = float(cls._resolve_memory_references(expression, memory=program._memory)) - def _resolve_memory_references(self, expression: ExpressionDesignator) -> Union[float, int]: + @classmethod + def _resolve_memory_references(cls, expression: ExpressionDesignator, memory: Memory) -> Union[float, int]: """ Traverse the given Expression, and replace any Memory References with whatever values have been so far provided by the user for those memory spaces. Declared memory defaults @@ -285,19 +301,19 @@ def _resolve_memory_references(self, expression: ExpressionDesignator) -> Union[ :param expression: an Expression """ if isinstance(expression, BinaryExp): - left = self._resolve_memory_references(expression.op1) - right = self._resolve_memory_references(expression.op2) + left = cls._resolve_memory_references(expression.op1, memory=memory) + right = cls._resolve_memory_references(expression.op2, memory=memory) return cast(Union[float, int], expression.fn(left, right)) elif isinstance(expression, Function): return cast( Union[float, int], - expression.fn(self._resolve_memory_references(expression.expression)), + expression.fn(cls._resolve_memory_references(expression.expression)), ) elif isinstance(expression, Parameter): raise ValueError(f"Unexpected Parameter in gate expression: {expression}") elif isinstance(expression, (float, int)): return expression elif isinstance(expression, MemoryReference): - return self._variables_shim.get(ParameterAref(name=expression.name, index=expression.offset), 0) + return memory.values.get(ParameterAref(name=expression.name, index=expression.offset), 0) else: raise ValueError(f"Unexpected expression in gate parameter: {expression}") diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index dbec66a7a..c0873a0e2 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -42,7 +42,7 @@ from pyquil.api import EngagementManager from pyquil.api._abstract_compiler import AbstractCompiler, QuantumExecutable from pyquil.api._compiler import QPUCompiler, QVMCompiler -from pyquil.api._error_reporting import _record_call + from pyquil.api._qam import QAM from pyquil.api._qcs_client import qcs_client from pyquil.api._qpu import QPU @@ -129,7 +129,6 @@ def to_compiler_isa(self) -> CompilerISA: """ return self.compiler.quantum_processor.to_compiler_isa() - @_record_call def run( self, executable: QuantumExecutable, @@ -150,7 +149,6 @@ def run( # it's a breaking change, or is it best to leave this as-is for ease/convenience? return result.read_memory(region_name="ro") - @_record_call def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: """ Perform readout calibration on the various multi-qubit observables involved in the provided @@ -161,10 +159,9 @@ def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: correspond to the scale factors resulting from symmetric readout error. """ calibration_experiment = experiment.generate_calibration_experiment() - return cast(List[ExperimentResult], self.experiment(calibration_experiment)) + return cast(List[ExperimentResult], self.run_experiment(calibration_experiment)) - @_record_call - def experiment( + def run_experiment( self, experiment: Experiment, memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None, @@ -184,17 +181,14 @@ def experiment( and symmetrization can all be realized at runtime by providing a ``memory_map``. Thus, the steps in the ``experiment`` method are as follows: - 1. Check to see if this ``Experiment`` has already been loaded into this - ``QuantumComputer`` object. If so, skip to step 2. Otherwise, do the following: - - a. Generate a parameterized program corresponding to the ``Experiment`` - (see the ``Experiment.generate_experiment_program()`` method for more - details on how it changes the main body program to support state preparation, - measurement, and symmetrization). - b. Compile the parameterized program into a parametric (binary) executable, which - contains declared variables that can be assigned at runtime. + 1. Generate a parameterized program corresponding to the ``Experiment`` + (see the ``Experiment.generate_experiment_program()`` method for more + details on how it changes the main body program to support state preparation, + measurement, and symmetrization). + 2. Compile the parameterized program into a parametric (binary) executable, which + contains declared variables that can be assigned at runtime. - 2. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following: + 3. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following: a. Build a collection of memory maps that correspond to the various state preparation, measurement, and symmetrization specifications. @@ -218,13 +212,9 @@ def experiment( :return: A list of ``ExperimentResult`` objects containing the statistics gathered according to the specifications of the ``Experiment``. """ - executable = self.qam.executable - # if this experiment was the last experiment run on this QuantumComputer, - # then use the executable that is already loaded into the object - if executable is None or self.qam.experiment != experiment: - experiment_program = experiment.generate_experiment_program() - executable = self.compile(experiment_program) - self.qam.experiment = experiment + + experiment_program = experiment.generate_experiment_program() + executable = self.compile(experiment_program) if memory_map is None: memory_map = {} @@ -245,7 +235,9 @@ def experiment( # TODO: accomplish symmetrization via batch endpoint for merged_memory_map in merged_memory_maps: final_memory_map = {**memory_map, **merged_memory_map} - bitstrings = self.run(executable, memory_map=final_memory_map) + executable_copy = executable.copy() + executable_copy._memory.write(final_memory_map) + bitstrings = self.run(executable_copy) if "symmetrization" in final_memory_map: bitmask = np.array(np.array(final_memory_map["symmetrization"]) / np.pi, dtype=int) @@ -282,7 +274,6 @@ def experiment( results.append(result) return results - @_record_call def run_symmetrized_readout( self, program: Program, @@ -373,7 +364,6 @@ def run_symmetrized_readout( return _consolidate_symmetrization_outputs(results, flip_arrays) - @_record_call def compile( self, program: Program, @@ -425,7 +415,6 @@ def __repr__(self) -> str: return f'QuantumComputer[name="{self.name}"]' -@_record_call def list_quantum_computers( qpus: bool = True, qvms: bool = True, @@ -734,7 +723,6 @@ def _get_qvm_based_on_real_quantum_processor( ) -@_record_call def get_qc( name: str, *, diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 51a56411a..6ad61fdc8 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -20,7 +20,7 @@ from pyquil._version import pyquil_version from pyquil.api import QuantumExecutable -from pyquil.api._error_reporting import _record_call + from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qvm_client import ( QVMClient, @@ -57,7 +57,6 @@ class QVMExecuteResponse: class QVM(QAM[QAMExecutionResult]): - @_record_call def __init__( self, noise_model: Optional[NoiseModel] = None, @@ -122,14 +121,13 @@ def connect(self) -> None: except ConnectionError: raise QVMNotRunning(f"No QVM server running at {self._qvm_client.base_url}") - @_record_call def execute(self, executable: QuantumExecutable) -> QVMExecuteResponse: """ Synchronously execute the input program to completion. """ if not isinstance(executable, Program): - raise TypeError("`QVM#executable` argument must be a `Program`") + raise TypeError(f"`QVM#executable` argument must be a `Program`; got {type(executable)}") result_memory = {} @@ -158,7 +156,6 @@ def execute(self, executable: QuantumExecutable) -> QVMExecuteResponse: return QAMExecutionResult(executable=executable, memory=result_memory) - @_record_call def get_results(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult: """ Return the results of execution on the QVM. @@ -167,7 +164,6 @@ def get_results(self, execute_response: QVMExecuteResponse) -> QAMExecutionResul """ return cast(QAMExecutionResult, execute_response) - @_record_call def get_version_info(self) -> str: """ Return version information for the QVM. diff --git a/pyquil/api/_wavefunction_simulator.py b/pyquil/api/_wavefunction_simulator.py index 1de2ecba0..ceff287e4 100644 --- a/pyquil/api/_wavefunction_simulator.py +++ b/pyquil/api/_wavefunction_simulator.py @@ -19,7 +19,7 @@ import numpy as np from qcs_api_client.client import QCSClientConfiguration -from pyquil.api._error_reporting import _record_call + from pyquil.api._qvm import ( validate_qubit_list, validate_noise_probabilities, @@ -38,7 +38,7 @@ class WavefunctionSimulator: - @_record_call + def __init__( self, *, @@ -76,7 +76,7 @@ def __init__( client_configuration = client_configuration or QCSClientConfiguration.load() self._qvm_client = QVMClient(client_configuration=client_configuration, request_timeout=timeout) - @_record_call + def wavefunction( self, quil_program: Program, memory_map: Optional[Dict[str, List[Union[int, float]]]] = None ) -> Wavefunction: @@ -106,7 +106,7 @@ def wavefunction( response = self._qvm_client.get_wavefunction(request) return Wavefunction.from_bit_packed_string(response.wavefunction) - @_record_call + def expectation( self, prep_prog: Program, @@ -170,7 +170,7 @@ def _expectation(self, prep_prog: Program, operator_programs: Iterable[Program]) response = self._qvm_client.measure_expectation(request) return np.asarray(response.expectations) - @_record_call + def run_and_measure( self, quil_program: Program, diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index 8af5ff1ef..c159c9d2f 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -47,5 +47,5 @@ def write_memory( ) -> "QAM": assert self._loaded_executable is not None, "Executable has not been loaded yet. Call QAM#load first" parameter_aref = ParameterAref(name=region_name, index=offset or 0) - self._loaded_executable.set_parameter_value(parameter=parameter_aref, value=value) + self._loaded_executable._memory._write_value(parameter=parameter_aref, value=value) return self diff --git a/pyquil/quil.py b/pyquil/quil.py index 5919e7c63..e2be25e11 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -40,6 +40,7 @@ from rpcq.messages import NativeQuilMetadata, ParameterAref from pyquil._parser.parser import run_parser +from pyquil._memory import Memory from pyquil.gates import DECLARE, MEASURE, RESET, MOVE from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas, pauli_kraus_map from pyquil.quilatom import ( @@ -121,7 +122,7 @@ class Program: >>> p += CNOT(0, 1) """ - _variable_values: Dict[ParameterAref, Union[int, float]] + _memory: Memory def __init__(self, *instructions: InstructionDesignator): self._defined_gates: List[DefGate] = [] @@ -151,7 +152,7 @@ def __init__(self, *instructions: InstructionDesignator): # default number of shots to loop through self.num_shots = 1 - self._variable_values = {} + self._memory = Memory() # Note to developers: Have you changed this method? Have you changed the fields which # live on `Program`? Please update `Program.copy()`! @@ -191,7 +192,7 @@ def copy_everything_except_instructions(self) -> "Program": # TODO: remove this type: ignore once rpcq._base.Message gets type hints. new_prog.native_quil_metadata = self.native_quil_metadata.copy() # type: ignore new_prog.num_shots = self.num_shots - new_prog._variable_values = {k.replace(): v for k, v in self._variable_values.items()} + new_prog._memory = self._memory.copy() return new_prog def copy(self) -> "Program": @@ -473,49 +474,12 @@ def measure_all(self, *qubit_reg_pairs: Tuple[QubitDesignator, Optional[MemoryRe self.inst(MEASURE(qubit_index, classical_reg)) return self - def with_parameter_values(self, parameter_values: Dict[Union[str, ParameterAref], Union[int, float]]) -> "Program": - """ - Return a copy of this program with the given parameter values set. - """ - program = self.copy() - for parameter, parameter_value in parameter_values.items(): - program.set_parameter_value(parameter=parameter, value=parameter_value) - return program - - def set_parameter_value( - self, - *, - parameter: Union[ParameterAref, str], - value: Union[int, float, Sequence[int], Sequence[float]], - ) -> "Program": - """ - Mutate the program to set the given parameter value. - - :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. - :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list - is provided, parameter must be a ``str`` or ``parameter.offset == 0``. - """ - if isinstance(parameter, str): - parameter = ParameterAref(name=parameter, index=0) - - if isinstance(value, (int, float)): - self._variable_values[parameter] = value - elif isinstance(value, Sequence): - if parameter.index != 0: - raise ValueError("Parameter may not have a non-zero index when its value is a sequence") - - for index, v in enumerate(value): - aref = ParameterAref(name=parameter.name, index=index) - self._variable_values[aref] = v - - return self - def _set_parameter_values_at_runtime(self) -> "Program": """ Store all parameter values directly within the Program using ``MOVE`` instructions. Mutates the receiver. """ move_instructions = [ - MOVE(MemoryReference(name=k.name, offset=k.index), v) for k, v in self._variable_values.items() + MOVE(MemoryReference(name=k.name, offset=k.index), v) for k, v in self._memory.values.items() ] self.prepend_instructions(move_instructions) @@ -523,6 +487,16 @@ def _set_parameter_values_at_runtime(self) -> "Program": return self + def write_memory( + self, + *, + region_name: str, + value: Union[int, float, Sequence[int], Sequence[float]], + offset: Optional[int] = None, + ) -> "Program": + self._memory._write_value(parameter=ParameterAref(name=region_name, index=offset or 0), value=value) + return self + def prepend_instructions(self, instructions: Iterable[AbstractInstruction]) -> "Program": """ Prepend instructions to the beginning of the program. @@ -901,12 +875,12 @@ def __add__(self, other: InstructionDesignator) -> "Program": p._calibrations = self.calibrations p._waveforms = self.waveforms p._frames = self.frames - p._variable_values = {k.replace(): v for k, v in self._variable_values.items()} + p._memory = self._memory.copy() if isinstance(other, Program): p.calibrations.extend(other.calibrations) p.waveforms.update(other.waveforms) p.frames.update(other.frames) - p._variable_values.update({k.replace(): v for k, v in other._variable_values.items()}) + p._memory.values.update(other._memory.values) return p def __iadd__(self, other: InstructionDesignator) -> "Program": @@ -921,7 +895,7 @@ def __iadd__(self, other: InstructionDesignator) -> "Program": self.calibrations.extend(other.calibrations) self.waveforms.update(other.waveforms) self.frames.update(other.frames) - self._variable_values.update({k.replace(): v for k, v in other._variable_values.items()}) + self._memory.values.update(other._memory.copy().values) return self def __getitem__(self, index: Union[slice, int]) -> Union[AbstractInstruction, "Program"]: From dded7b71f9f9784686c844e8bf77f6739e15245d Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:42:35 -0700 Subject: [PATCH 20/61] Tests: update to use new API --- test/unit/test_operator_estimation.py | 1900 +++++++++++++++++++++++++ test/unit/test_quantum_computer.py | 810 +++++++++++ 2 files changed, 2710 insertions(+) create mode 100644 test/unit/test_operator_estimation.py create mode 100644 test/unit/test_quantum_computer.py diff --git a/test/unit/test_operator_estimation.py b/test/unit/test_operator_estimation.py new file mode 100644 index 000000000..1edadbc22 --- /dev/null +++ b/test/unit/test_operator_estimation.py @@ -0,0 +1,1900 @@ +import itertools +import random +from math import pi + +import numpy as np +import pytest + +from pyquil import Program +from pyquil.api import WavefunctionSimulator +from pyquil.api import QCSClientConfiguration +from pyquil import get_qc +from pyquil.experiment import ( + ExperimentSetting, + SIC0, + SIC1, + SIC2, + SIC3, + TensorProductState, + Experiment, + minusY, + minusZ, + plusX, + plusY, + plusZ, +) +from pyquil.gates import CNOT, CZ, H, I, RX, RY, RZ, X, Y +from pyquil.operator_estimation import ( + _one_q_sic_prep, + group_experiments, + measure_observables, + _ops_bool_to_prog, + _stats_from_measurements, + _calibration_program, +) +from pyquil.paulis import sI, sX, sY, sZ, PauliSum +from pyquil.quilbase import Pragma + + +def test_measure_observables(client_configuration: QCSClientConfiguration): + expts = [ + ExperimentSetting(TensorProductState(), o1 * o2) + for o1, o2 in itertools.product([sI(0), sX(0), sY(0), sZ(0)], [sI(1), sX(1), sY(1), sZ(1)]) + ] + suite = Experiment(expts, program=Program(X(0), CNOT(0, 1)).wrap_in_numshots_loop(2000)) + assert len(suite) == 4 * 4 + gsuite = group_experiments(suite) + assert len(gsuite) == 3 * 3 # can get all the terms with I for free in this case + + qc = get_qc("2q-qvm", client_configuration=client_configuration) + for res in measure_observables(qc, gsuite): + if res.setting.out_operator in [sI(), sZ(0), sZ(1), sZ(0) * sZ(1)]: + assert np.abs(res.expectation) > 0.9 + else: + assert np.abs(res.expectation) < 0.1 + + +def _random_2q_programs(n_progs=3): + """Generate random programs that consist of single qubit rotations, a CZ, and single + qubit rotations. + """ + r = random.Random(52) + + def RI(qubit, angle): + # throw away angle so we can randomly choose the identity + return I(qubit) + + def _random_1q_gate(qubit): + return r.choice([RI, RX, RY, RZ])(qubit=qubit, angle=r.uniform(0, 2 * pi)) + + for _ in range(n_progs): + prog = Program() + prog += _random_1q_gate(0) + prog += _random_1q_gate(1) + prog += CZ(0, 1) + prog += _random_1q_gate(0) + prog += _random_1q_gate(1) + yield prog.wrap_in_numshots_loop(10000) + + +@pytest.mark.slow +def test_measure_observables_many_progs(client_configuration: QCSClientConfiguration): + expts = [ + ExperimentSetting(TensorProductState(), o1 * o2) + for o1, o2 in itertools.product([sI(0), sX(0), sY(0), sZ(0)], [sI(1), sX(1), sY(1), sZ(1)]) + ] + + qc = get_qc("2q-qvm", client_configuration=client_configuration) + qc.qam.random_seed = 0 + for prog in _random_2q_programs(): + suite = Experiment(expts, program=prog) + assert len(suite) == 4 * 4 + gsuite = group_experiments(suite) + assert len(gsuite) == 3 * 3 # can get all the terms with I for free in this case + + wfn = WavefunctionSimulator(client_configuration=client_configuration) + wfn_exps = {} + for expt in expts: + wfn_exps[expt] = wfn.expectation(gsuite.program, PauliSum([expt.out_operator])) + + for res in measure_observables(qc, gsuite): + np.testing.assert_allclose(wfn_exps[res.setting], res.expectation, atol=2e-2) + + +def test_append(): + expts = [ + [ + ExperimentSetting(TensorProductState(), sX(0) * sI(1)), + ExperimentSetting(TensorProductState(), sI(0) * sX(1)), + ], + [ + ExperimentSetting(TensorProductState(), sZ(0) * sI(1)), + ExperimentSetting(TensorProductState(), sI(0) * sZ(1)), + ], + ] + suite = Experiment(settings=expts, program=Program(X(0), Y(1))) + suite.append(ExperimentSetting(TensorProductState(), sY(0) * sX(1))) + assert (len(str(suite))) > 0 + + +def test_no_complex_coeffs(client_configuration: QCSClientConfiguration): + qc = get_qc("2q-qvm", client_configuration=client_configuration) + suite = Experiment( + [ExperimentSetting(TensorProductState(), 1.0j * sY(0))], + program=Program(X(0)).wrap_in_numshots_loop(2000), + ) + with pytest.raises(ValueError): + list(measure_observables(qc, suite)) + + +def test_identity(client_configuration: QCSClientConfiguration): + qc = get_qc("2q-qvm", client_configuration=client_configuration) + suite = Experiment([ExperimentSetting(plusZ(0), 0.123 * sI(0))], program=Program(X(0))) + result = list(measure_observables(qc, suite))[0] + assert result.expectation == 0.123 + + +def test_sic_process_tomo(client_configuration: QCSClientConfiguration): + qc = get_qc("2q-qvm", client_configuration=client_configuration) + process = Program(X(0)) + settings = [] + for in_state in [SIC0, SIC1, SIC2, SIC3]: + for out_op in [sI, sX, sY, sZ]: + settings += [ExperimentSetting(in_state=in_state(q=0), out_operator=out_op(q=0))] + + experiment = Experiment(settings=settings, program=process) + results = list(measure_observables(qc, experiment)) + assert len(results) == 4 * 4 + + +def test_measure_observables_symmetrize(client_configuration: QCSClientConfiguration): + """ + Symmetrization alone should not change the outcome on the QVM + """ + expts = [ + ExperimentSetting(TensorProductState(), o1 * o2) + for o1, o2 in itertools.product([sI(0), sX(0), sY(0), sZ(0)], [sI(1), sX(1), sY(1), sZ(1)]) + ] + suite = Experiment(expts, program=Program(X(0), CNOT(0, 1)).wrap_in_numshots_loop(10000)) + assert len(suite) == 4 * 4 + gsuite = group_experiments(suite) + assert len(gsuite) == 3 * 3 # can get all the terms with I for free in this case + + qc = get_qc("2q-qvm", client_configuration=client_configuration) + for res in measure_observables(qc, gsuite, calibrate_readout=None): + if res.setting.out_operator in [sI(), sZ(0), sZ(1), sZ(0) * sZ(1)]: + assert np.abs(res.expectation) > 0.9 + else: + assert np.abs(res.expectation) < 0.1 + + +def test_measure_observables_symmetrize_calibrate(client_configuration: QCSClientConfiguration): + """ + Symmetrization + calibration should not change the outcome on the QVM + """ + expts = [ + ExperimentSetting(TensorProductState(), o1 * o2) + for o1, o2 in itertools.product([sI(0), sX(0), sY(0), sZ(0)], [sI(1), sX(1), sY(1), sZ(1)]) + ] + suite = Experiment(expts, program=Program(X(0), CNOT(0, 1)).wrap_in_numshots_loop(10000)) + assert len(suite) == 4 * 4 + gsuite = group_experiments(suite) + assert len(gsuite) == 3 * 3 # can get all the terms with I for free in this case + + qc = get_qc("2q-qvm", client_configuration=client_configuration) + for res in measure_observables(qc, gsuite): + if res.setting.out_operator in [sI(), sZ(0), sZ(1), sZ(0) * sZ(1)]: + assert np.abs(res.expectation) > 0.9 + else: + assert np.abs(res.expectation) < 0.1 + + +def test_measure_observables_zero_expectation(client_configuration: QCSClientConfiguration): + """ + Testing case when expectation value of observable should be close to zero + """ + qc = get_qc("2q-qvm", client_configuration=client_configuration) + exptsetting = ExperimentSetting(plusZ(0), sX(0)) + suite = Experiment([exptsetting], program=Program(I(0)).wrap_in_numshots_loop(10000)) + result = list(measure_observables(qc, suite))[0] + np.testing.assert_almost_equal(result.expectation, 0.0, decimal=1) + + +def test_measure_observables_no_symm_calibr_raises_error(client_configuration: QCSClientConfiguration): + qc = get_qc("2q-qvm", client_configuration=client_configuration) + exptsetting = ExperimentSetting(plusZ(0), sX(0)) + suite = Experiment([exptsetting], program=Program(I(0)), symmetrization=0) + with pytest.raises(ValueError): + list(measure_observables(qc, suite, calibrate_readout="plus-eig")) + + +def test_ops_bool_to_prog(): + qubits = [0, 2, 3] + ops_strings = list(itertools.product([0, 1], repeat=len(qubits))) + d_expected = { + (0, 0, 0): "", + (0, 0, 1): "X 3\n", + (0, 1, 0): "X 2\n", + (0, 1, 1): "X 2\nX 3\n", + (1, 0, 0): "X 0\n", + (1, 0, 1): "X 0\nX 3\n", + (1, 1, 0): "X 0\nX 2\n", + (1, 1, 1): "X 0\nX 2\nX 3\n", + } + for op_str in ops_strings: + p = _ops_bool_to_prog(op_str, qubits) + assert str(p) == d_expected[op_str] + + +def test_stats_from_measurements(): + bs_results = np.array([[0, 1] * 10]) + d_qub_idx = {0: 0, 1: 1} + setting = ExperimentSetting(TensorProductState(), sZ(0) * sX(1)) + n_shots = 2000 + + obs_mean, obs_var = _stats_from_measurements(bs_results, d_qub_idx, setting, n_shots) + assert obs_mean == -1.0 + assert obs_var == 0.0 + + +def test_measure_observables_uncalibrated_asymmetric_readout( + client_configuration: QCSClientConfiguration, use_seed: bool +): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + runs = 1 + else: + runs = 100 + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program() + p00, p11 = 0.90, 0.80 + p.define_noisy_readout(0, p00=p00, p11=p11) + p.wrap_in_numshots_loop(2000) + expt_list = [expt1, expt2, expt3] + tomo_expt = Experiment(settings=expt_list * runs, program=p, symmetrization=0) + expected_expectation_z_basis = 2 * p00 - 1 + + expect_arr = np.zeros(runs * len(expt_list)) + + for idx, res in enumerate(measure_observables(qc, tomo_expt, calibrate_readout=None)): + expect_arr[idx] = res.expectation + + assert np.isclose(np.mean(expect_arr[::3]), expected_expectation_z_basis, atol=2e-2) + assert np.isclose(np.mean(expect_arr[1::3]), expected_expectation_z_basis, atol=2e-2) + assert np.isclose(np.mean(expect_arr[2::3]), expected_expectation_z_basis, atol=2e-2) + + +def test_measure_observables_uncalibrated_symmetric_readout( + client_configuration: QCSClientConfiguration, use_seed: bool +): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + runs = 1 + else: + runs = 100 + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program().wrap_in_numshots_loop(2000) + p00, p11 = 0.90, 0.80 + p.define_noisy_readout(0, p00=p00, p11=p11) + expt_list = [expt1, expt2, expt3] + tomo_expt = Experiment(settings=expt_list * runs, program=p) + expected_symm_error = (p00 + p11) / 2 + expected_expectation_z_basis = expected_symm_error * (1) + (1 - expected_symm_error) * (-1) + + uncalibr_e = np.zeros(runs * len(expt_list)) + + for idx, res in enumerate(measure_observables(qc, tomo_expt, calibrate_readout=None)): + uncalibr_e[idx] = res.expectation + + assert np.isclose(np.mean(uncalibr_e[::3]), expected_expectation_z_basis, atol=2e-2) + assert np.isclose(np.mean(uncalibr_e[1::3]), expected_expectation_z_basis, atol=2e-2) + assert np.isclose(np.mean(uncalibr_e[2::3]), expected_expectation_z_basis, atol=2e-2) + + +def test_measure_observables_calibrated_symmetric_readout(client_configuration: QCSClientConfiguration, use_seed: bool): + # expecting the result +1 for calibrated readout + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program() + p.wrap_in_numshots_loop(2000) + p.define_noisy_readout(0, p00=0.99, p11=0.80) + tomo_expt = Experiment(settings=[expt1, expt2, expt3], program=p) + + expectations = [] + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt)) + expectations.append([res.expectation for res in expt_results]) + expectations = np.array(expectations) + results = np.mean(expectations, axis=0) + np.testing.assert_allclose(results, 1.0, atol=2e-2) + + +def test_measure_observables_result_zero_symmetrization_calibration( + client_configuration: QCSClientConfiguration, use_seed: bool +): + # expecting expectation value to be 0 with symmetrization/calibration + qc = get_qc("9q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sZ(0)) + expt2 = ExperimentSetting(TensorProductState(minusZ(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(minusY(0)), sX(0)) + expt_settings = [expt1, expt2, expt3] + p = Program() + p00, p11 = 0.99, 0.80 + p.define_noisy_readout(0, p00=p00, p11=p11) + p.wrap_in_numshots_loop(2000) + tomo_expt = Experiment(settings=expt_settings, program=p) + + expectations = [] + raw_expectations = [] + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt)) + expectations.append([res.expectation for res in expt_results]) + raw_expectations.append([res.raw_expectation for res in expt_results]) + expectations = np.array(expectations) + raw_expectations = np.array(raw_expectations) + results = np.mean(expectations, axis=0) + raw_results = np.mean(raw_expectations) + np.testing.assert_allclose(results, 0.0, atol=2e-2) + np.testing.assert_allclose(raw_results, 0.0, atol=2e-2) + + +def test_measure_observables_result_zero_no_noisy_readout(client_configuration: QCSClientConfiguration, use_seed: bool): + # expecting expectation value to be 0 with no symmetrization/calibration + # and no noisy readout + qc = get_qc("9q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sZ(0)) + expt2 = ExperimentSetting(TensorProductState(minusZ(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusY(0)), sX(0)) + expt_settings = [expt1, expt2, expt3] + p = Program().wrap_in_numshots_loop(2000) + tomo_expt = Experiment(settings=expt_settings, program=p, symmetrization=0) + + expectations = [] + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt, calibrate_readout=None)) + expectations.append([res.expectation for res in expt_results]) + expectations = np.array(expectations) + results = np.mean(expectations, axis=0) + np.testing.assert_allclose(results, 0.0, atol=2e-2) + + +def test_measure_observables_result_zero_no_symm_calibr(client_configuration: QCSClientConfiguration, use_seed: bool): + # expecting expectation value to be nonzero with symmetrization/calibration + qc = get_qc("9q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 3 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sZ(0)) + expt2 = ExperimentSetting(TensorProductState(minusZ(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(minusY(0)), sX(0)) + expt_settings = [expt1, expt2, expt3] + p = Program().wrap_in_numshots_loop(2000) + p00, p11 = 0.99, 0.80 + p.define_noisy_readout(0, p00=p00, p11=p11) + tomo_expt = Experiment(settings=expt_settings, program=p, symmetrization=0) + + expectations = [] + expected_result = (p00 * 0.5 + (1 - p11) * 0.5) - ((1 - p00) * 0.5 + p11 * 0.5) + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt, calibrate_readout=None)) + expectations.append([res.expectation for res in expt_results]) + expectations = np.array(expectations) + results = np.mean(expectations, axis=0) + np.testing.assert_allclose(results, expected_result, atol=2e-2) + + +def test_measure_observables_2q_readout_error_one_measured( + client_configuration: QCSClientConfiguration, use_seed: bool +): + # 2q readout errors, but only 1 qubit measured + qc = get_qc("9q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 3 + np.random.seed(0) + runs = 1 + else: + runs = 100 + qubs = [0, 1] + expt = ExperimentSetting(TensorProductState(plusZ(qubs[0]) * plusZ(qubs[1])), sZ(qubs[0])) + p = Program().wrap_in_numshots_loop(5000) + p.define_noisy_readout(0, 0.999, 0.85) + p.define_noisy_readout(1, 0.999, 0.75) + tomo_experiment = Experiment(settings=[expt] * runs, program=p) + + raw_e = np.zeros(runs) + obs_e = np.zeros(runs) + cal_e = np.zeros(runs) + + for idx, res in enumerate(measure_observables(qc, tomo_experiment)): + raw_e[idx] = res.raw_expectation + obs_e[idx] = res.expectation + cal_e[idx] = res.calibration_expectation + + assert np.isclose(np.mean(raw_e), 0.849, atol=2e-2) + assert np.isclose(np.mean(obs_e), 1.0, atol=2e-2) + assert np.isclose(np.mean(cal_e), 0.849, atol=2e-2) + + +def test_measure_observables_inherit_noise_errors(client_configuration: QCSClientConfiguration): + qc = get_qc("3q-qvm", client_configuration=client_configuration) + # specify simplest experiments + expt1 = ExperimentSetting(TensorProductState(), sZ(0)) + expt2 = ExperimentSetting(TensorProductState(), sZ(1)) + expt3 = ExperimentSetting(TensorProductState(), sZ(2)) + # specify a Program with multiple sources of noise + p = Program(X(0), Y(1), H(2)) + # defining several bit-flip channels + kraus_ops_X = [ + np.sqrt(1 - 0.3) * np.array([[1, 0], [0, 1]]), + np.sqrt(0.3) * np.array([[0, 1], [1, 0]]), + ] + kraus_ops_Y = [ + np.sqrt(1 - 0.2) * np.array([[1, 0], [0, 1]]), + np.sqrt(0.2) * np.array([[0, 1], [1, 0]]), + ] + kraus_ops_H = [ + np.sqrt(1 - 0.1) * np.array([[1, 0], [0, 1]]), + np.sqrt(0.1) * np.array([[0, 1], [1, 0]]), + ] + # replacing all the gates with bit-flip channels + p.define_noisy_gate("X", [0], kraus_ops_X) + p.define_noisy_gate("Y", [1], kraus_ops_Y) + p.define_noisy_gate("H", [2], kraus_ops_H) + # defining readout errors + p.define_noisy_readout(0, 0.99, 0.80) + p.define_noisy_readout(1, 0.95, 0.85) + p.define_noisy_readout(2, 0.97, 0.78) + + tomo_expt = Experiment(settings=[expt1, expt2, expt3], program=p) + + calibr_prog1 = _calibration_program(qc, tomo_expt, expt1) + calibr_prog2 = _calibration_program(qc, tomo_expt, expt2) + calibr_prog3 = _calibration_program(qc, tomo_expt, expt3) + expected_prog = """PRAGMA READOUT-POVM 0 "(0.99 0.19999999999999996 0.010000000000000009 0.8)" +PRAGMA READOUT-POVM 1 "(0.95 0.15000000000000002 0.050000000000000044 0.85)" +PRAGMA READOUT-POVM 2 "(0.97 0.21999999999999997 0.030000000000000027 0.78)" +PRAGMA ADD-KRAUS X 0 "(0.8366600265340756 0.0 0.0 0.8366600265340756)" +PRAGMA ADD-KRAUS X 0 "(0.0 0.5477225575051661 0.5477225575051661 0.0)" +PRAGMA ADD-KRAUS Y 1 "(0.8944271909999159 0.0 0.0 0.8944271909999159)" +PRAGMA ADD-KRAUS Y 1 "(0.0 0.4472135954999579 0.4472135954999579 0.0)" +PRAGMA ADD-KRAUS H 2 "(0.9486832980505138 0.0 0.0 0.9486832980505138)" +PRAGMA ADD-KRAUS H 2 "(0.0 0.31622776601683794 0.31622776601683794 0.0)" +""" + assert calibr_prog1.out() == Program(expected_prog).out() + assert calibr_prog2.out() == Program(expected_prog).out() + assert calibr_prog3.out() == Program(expected_prog).out() + + +def test_expectations_sic0(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(SIC0(0), sX(0)) + expt2 = ExperimentSetting(SIC0(0), sY(0)) + expt3 = ExperimentSetting(SIC0(0), sZ(0)) + tomo_expt = Experiment(settings=[expt1, expt2, expt3], program=Program().wrap_in_numshots_loop(2000)) + + results_unavged = [] + for _ in range(num_simulations): + measured_results = [] + for res in measure_observables(qc, tomo_expt): + measured_results.append(res.expectation) + results_unavged.append(measured_results) + + results_unavged = np.array(results_unavged) + results = np.mean(results_unavged, axis=0) + expected_results = np.array([0, 0, 1]) + np.testing.assert_allclose(results, expected_results, atol=2e-2) + + +def test_expectations_sic1(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(SIC1(0), sX(0)) + expt2 = ExperimentSetting(SIC1(0), sY(0)) + expt3 = ExperimentSetting(SIC1(0), sZ(0)) + tomo_expt = Experiment(settings=[expt1, expt2, expt3], program=Program().wrap_in_numshots_loop(2000)) + + results_unavged = [] + for _ in range(num_simulations): + measured_results = [] + for res in measure_observables(qc, tomo_expt): + measured_results.append(res.expectation) + results_unavged.append(measured_results) + + results_unavged = np.array(results_unavged) + results = np.mean(results_unavged, axis=0) + expected_results = np.array([2 * np.sqrt(2) / 3, 0, -1 / 3]) + np.testing.assert_allclose(results, expected_results, atol=2e-2) + + +def test_expectations_sic2(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(SIC2(0), sX(0)) + expt2 = ExperimentSetting(SIC2(0), sY(0)) + expt3 = ExperimentSetting(SIC2(0), sZ(0)) + tomo_expt = Experiment(settings=[expt1, expt2, expt3], program=Program().wrap_in_numshots_loop(2000)) + + results_unavged = [] + for _ in range(num_simulations): + measured_results = [] + for res in measure_observables(qc, tomo_expt): + measured_results.append(res.expectation) + results_unavged.append(measured_results) + + results_unavged = np.array(results_unavged) + results = np.mean(results_unavged, axis=0) + expected_results = np.array( + [ + (2 * np.sqrt(2) / 3) * np.cos(2 * np.pi / 3), + -(2 * np.sqrt(2) / 3) * np.sin(2 * np.pi / 3), + -1 / 3, + ] + ) + np.testing.assert_allclose(results, expected_results, atol=2e-2) + + +def test_expectations_sic3(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt1 = ExperimentSetting(SIC3(0), sX(0)) + expt2 = ExperimentSetting(SIC3(0), sY(0)) + expt3 = ExperimentSetting(SIC3(0), sZ(0)) + tomo_expt = Experiment(settings=[expt1, expt2, expt3], program=Program().wrap_in_numshots_loop(2000)) + + results_unavged = [] + for _ in range(num_simulations): + measured_results = [] + for res in measure_observables(qc, tomo_expt): + measured_results.append(res.expectation) + results_unavged.append(measured_results) + + results_unavged = np.array(results_unavged) + results = np.mean(results_unavged, axis=0) + expected_results = np.array( + [ + (2 * np.sqrt(2) / 3) * np.cos(2 * np.pi / 3), + (2 * np.sqrt(2) / 3) * np.sin(2 * np.pi / 3), + -1 / 3, + ] + ) + np.testing.assert_allclose(results, expected_results, atol=2e-2) + + +def test_sic_conditions(client_configuration: QCSClientConfiguration): + """ + Test that the SIC states indeed yield SIC-POVMs + """ + wfn_sim = WavefunctionSimulator(client_configuration=client_configuration) + + # condition (i) -- sum of all projectors equal identity times dimensionality + result = np.zeros((2, 2)) + + for i in range(4): + if i == 0: + amps = np.array([1, 0]) + else: + sic = _one_q_sic_prep(i, 0) + wfn = wfn_sim.wavefunction(sic) + amps = wfn.amplitudes + proj = np.outer(amps, amps.conj()) + result = np.add(result, proj) + np.testing.assert_allclose(result / 2, np.eye(2), atol=2e-2) + + # condition (ii) -- tr(proj_a . proj_b) = 1 / 3, for a != b + for comb in itertools.combinations([0, 1, 2, 3], 2): + if comb[0] == 0: + sic_a = Program(I(0)) + else: + sic_a = _one_q_sic_prep(comb[0], 0) + sic_b = _one_q_sic_prep(comb[1], 0) + + wfn_a = wfn_sim.wavefunction(sic_a) + wfn_b = wfn_sim.wavefunction(sic_b) + + amps_a = wfn_a.amplitudes + amps_b = wfn_b.amplitudes + + proj_a = np.outer(amps_a, amps_a.conj()) + proj_b = np.outer(amps_b, amps_b.conj()) + + assert np.isclose(np.trace(proj_a.dot(proj_b)), 1 / 3) + + +def test_measure_observables_grouped_expts(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("3q-qvm", client_configuration=client_configuration) + + if use_seed: + num_simulations = 1 + qc.qam.random_seed = 2 + else: + num_simulations = 100 + + # this more explicitly uses the list-of-lists-of-ExperimentSettings in TomographyExperiment + # create experiments in different groups + expt1_group1 = ExperimentSetting(SIC1(0) * plusX(1), sZ(0) * sX(1)) + expt2_group1 = ExperimentSetting(plusX(1) * minusY(2), sX(1) * sY(2)) + expts_group1 = [expt1_group1, expt2_group1] + + expt1_group2 = ExperimentSetting(plusX(0) * SIC0(1), sX(0) * sZ(1)) + expt2_group2 = ExperimentSetting(SIC0(1) * minusY(2), sZ(1) * sY(2)) + expt3_group2 = ExperimentSetting(plusX(0) * minusY(2), sX(0) * sY(2)) + expts_group2 = [expt1_group2, expt2_group2, expt3_group2] + # create a list-of-lists-of-ExperimentSettings + expt_settings = [expts_group1, expts_group2] + # and use this to create a TomographyExperiment suite + tomo_expt = Experiment(settings=expt_settings, program=Program().wrap_in_numshots_loop(2000)) + + results_unavged = [] + for _ in range(num_simulations): + measured_results = [] + for res in measure_observables(qc, tomo_expt): + measured_results.append(res.expectation) + results_unavged.append(measured_results) + + results_unavged = np.array(results_unavged) + results = np.mean(results_unavged, axis=0) + expected_results = np.array([-1 / 3, -1, 1, -1, -1]) + np.testing.assert_allclose(results, expected_results, atol=2e-2) + + +def _point_channel_fidelity_estimate(v, dim=2): + """:param v: array of expectation values + :param dim: dimensionality of the Hilbert space""" + return (1.0 + np.sum(v) + dim) / (dim * (dim + 1)) + + +def test_bit_flip_channel_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + np.random.seed(0) + qc.qam.random_seed = 0 + num_expts = 1 + else: + num_expts = 100 + + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare noisy bit-flip channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # the bit flip channel is composed of two Kraus operations -- + # applying the X gate with probability `prob`, and applying the identity gate + # with probability `1 - prob` + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[0, 1], [1, 0]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + + # prepare Experiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = 1 - (2 / 3) * prob + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_dephasing_channel_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare noisy dephasing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for the dephasing channel + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = 1 - (2 / 3) * prob + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_depolarizing_channel_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare noisy depolarizing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for the depolarizing channel + kraus_ops = [ + np.sqrt(3 * prob + 1) / 2 * np.array([[1, 0], [0, 1]]), + np.sqrt(1 - prob) / 2 * np.array([[0, 1], [1, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[0, -1j], [1j, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = (1 + prob) / 2 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_unitary_channel_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare unitary channel as an RY rotation program for some random angle + theta = np.random.uniform(0.0, 2 * np.pi) + # unitary (RY) channel + p = Program(RY(theta, 0)).wrap_in_numshots_loop(2000) + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = (1 / 6) * ((2 * np.cos(theta / 2)) ** 2 + 2) + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_bit_flip_channel_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare noisy bit-flip channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # the bit flip channel is composed of two Kraus operations -- + # applying the X gate with probability `prob`, and applying the identity gate + # with probability `1 - prob` + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[0, 1], [1, 0]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + # add some readout error + p.define_noisy_readout(0, 0.95, 0.82) + + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = 1 - (2 / 3) * prob + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_dephasing_channel_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare noisy dephasing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for the dephasing channel + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + # add some readout error + p.define_noisy_readout(0, 0.95, 0.82) + + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = 1 - (2 / 3) * prob + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_depolarizing_channel_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare noisy depolarizing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for the depolarizing channel + kraus_ops = [ + np.sqrt(3 * prob + 1) / 2 * np.array([[1, 0], [0, 1]]), + np.sqrt(1 - prob) / 2 * np.array([[0, 1], [1, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[0, -1j], [1j, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + # add some readout error + p.define_noisy_readout(0, 0.95, 0.82) + + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = (1 + prob) / 2 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_unitary_channel_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + """ + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + expt_list = [expt1, expt2, expt3] + + # prepare unitary channel as an RY rotation program for some random angle + theta = np.random.uniform(0.0, 2 * np.pi) + # unitary (RY) channel + p = Program(RY(theta, 0)).wrap_in_numshots_loop(2000) + # add some readout error + p.define_noisy_readout(0, 0.95, 0.82) + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results) + # how close is this channel to the identity operator + expected_fidelity = (1 / 6) * ((2 * np.cos(theta / 2)) ** 2 + 2) + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_2q_unitary_channel_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + """ + We use Eqn (5) of https://arxiv.org/abs/quant-ph/0701138 to compare the fidelity + This tests if our dimensionality factors are correct, even in the presence + of readout errors + """ + qc = get_qc("2q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment settings + + expt1 = ExperimentSetting(TensorProductState(plusX(0)), sX(0)) + expt2 = ExperimentSetting(TensorProductState(plusY(0)), sY(0)) + expt3 = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + + expt4 = ExperimentSetting(TensorProductState(plusX(1)), sX(1)) + expt5 = ExperimentSetting(TensorProductState(plusX(0) * plusX(1)), sX(0) * sX(1)) + expt6 = ExperimentSetting(TensorProductState(plusY(0) * plusX(1)), sY(0) * sX(1)) + expt7 = ExperimentSetting(TensorProductState(plusZ(0) * plusX(1)), sZ(0) * sX(1)) + + expt8 = ExperimentSetting(TensorProductState(plusY(1)), sY(1)) + expt9 = ExperimentSetting(TensorProductState(plusX(0) * plusY(1)), sX(0) * sY(1)) + expt10 = ExperimentSetting(TensorProductState(plusY(0) * plusY(1)), sY(0) * sY(1)) + expt11 = ExperimentSetting(TensorProductState(plusZ(0) * plusY(1)), sZ(0) * sY(1)) + + expt12 = ExperimentSetting(TensorProductState(plusZ(1)), sZ(1)) + expt13 = ExperimentSetting(TensorProductState(plusX(0) * plusZ(1)), sX(0) * sZ(1)) + expt14 = ExperimentSetting(TensorProductState(plusY(0) * plusZ(1)), sY(0) * sZ(1)) + expt15 = ExperimentSetting(TensorProductState(plusZ(0) * plusZ(1)), sZ(0) * sZ(1)) + + expt_list = [ + expt1, + expt2, + expt3, + expt4, + expt5, + expt6, + expt7, + expt8, + expt9, + expt10, + expt11, + expt12, + expt13, + expt14, + expt15, + ] + + # prepare unitary channel as an RY rotation program for some random angle + theta1, theta2 = np.random.uniform(0.0, 2 * np.pi, size=2) + # unitary (RY) channel + p = Program(RY(theta1, 0), RY(theta2, 1)).wrap_in_numshots_loop(5000) + # add some readout error + p.define_noisy_readout(0, 0.95, 0.82) + p.define_noisy_readout(1, 0.99, 0.73) + # prepare TomographyExperiment + process_exp = Experiment(settings=expt_list, program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_channel_fidelity_estimate(results, dim=4) + # how close is this channel to the identity operator + expected_fidelity = (1 / 5) * ((2 * np.cos(theta1 / 2) * np.cos(theta2 / 2)) ** 2 + 1) + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_measure_1q_observable_raw_expectation(client_configuration: QCSClientConfiguration, use_seed: bool): + # testing that we get correct raw expectation in terms of readout errors + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + expt = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program().wrap_in_numshots_loop(2000) + p00, p11 = 0.99, 0.80 + p.define_noisy_readout(0, p00=p00, p11=p11) + tomo_expt = Experiment(settings=[expt], program=p) + + raw_expectations = [] + for _ in range(num_expts): + expt_results = list(measure_observables(qc, tomo_expt)) + raw_expectations.append([res.raw_expectation for res in expt_results]) + raw_expectations = np.array(raw_expectations) + result = np.mean(raw_expectations, axis=0) + + # calculate expected raw_expectation + eps_not = (p00 + p11) / 2 + eps = 1 - eps_not + expected_result = 1 - 2 * eps + np.testing.assert_allclose(result, expected_result, atol=2e-2) + + +def test_measure_1q_observable_raw_variance(client_configuration: QCSClientConfiguration, use_seed: bool): + # testing that we get correct raw std_err in terms of readout errors + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + expt = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program().wrap_in_numshots_loop(2000) + p00, p11 = 0.99, 0.80 + p.define_noisy_readout(0, p00=p00, p11=p11) + tomo_expt = Experiment(settings=[expt], program=p) + + raw_std_errs = [] + for _ in range(num_expts): + expt_results = list(measure_observables(qc, tomo_expt)) + raw_std_errs.append([res.raw_std_err for res in expt_results]) + raw_std_errs = np.array(raw_std_errs) + result = np.mean(raw_std_errs, axis=0) + + # calculate expected raw_expectation + eps_not = (p00 + p11) / 2 + eps = 1 - eps_not + expected_result = np.sqrt((1 - (1 - 2 * eps) ** 2) / p.num_shots) + np.testing.assert_allclose(result, expected_result, atol=2e-2) + + +def test_measure_1q_observable_calibration_expectation(client_configuration: QCSClientConfiguration, use_seed: bool): + # testing that we get correct calibration expectation in terms of readout errors + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + expt = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program().wrap_in_numshots_loop(2000) + p00, p11 = 0.93, 0.77 + p.define_noisy_readout(0, p00=p00, p11=p11) + tomo_expt = Experiment(settings=[expt], program=p) + + calibration_expectations = [] + for _ in range(num_expts): + expt_results = list(measure_observables(qc, tomo_expt)) + calibration_expectations.append([res.calibration_expectation for res in expt_results]) + calibration_expectations = np.array(calibration_expectations) + result = np.mean(calibration_expectations, axis=0) + + # calculate expected raw_expectation + eps_not = (p00 + p11) / 2 + eps = 1 - eps_not + expected_result = 1 - 2 * eps + np.testing.assert_allclose(result, expected_result, atol=2e-2) + + +def test_measure_1q_observable_calibration_variance(client_configuration: QCSClientConfiguration, use_seed: bool): + # testing that we get correct calibration std_err in terms of readout errors + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + expt = ExperimentSetting(TensorProductState(plusZ(0)), sZ(0)) + p = Program().wrap_in_numshots_loop(2000) + p00, p11 = 0.93, 0.77 + p.define_noisy_readout(0, p00=p00, p11=p11) + tomo_expt = Experiment(settings=[expt], program=p) + + raw_std_errs = [] + for _ in range(num_expts): + expt_results = list(measure_observables(qc, tomo_expt)) + raw_std_errs.append([res.raw_std_err for res in expt_results]) + raw_std_errs = np.array(raw_std_errs) + result = np.mean(raw_std_errs, axis=0) + + # calculate expected raw_expectation + eps_not = (p00 + p11) / 2 + eps = 1 - eps_not + expected_result = np.sqrt((1 - (1 - 2 * eps) ** 2) / p.num_shots) + np.testing.assert_allclose(result, expected_result, atol=2e-2) + + +def test_uncalibrated_asymmetric_readout_nontrivial_1q_state( + client_configuration: QCSClientConfiguration, use_seed: bool +): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + runs = 1 + else: + runs = 100 + expt = ExperimentSetting(TensorProductState(), sZ(0)) + # pick some random value for RX rotation + theta = np.random.uniform(0.0, 2 * np.pi) + p = Program(RX(theta, 0)).wrap_in_numshots_loop(2000) + # pick some random (but sufficiently large) asymmetric readout errors + p00, p11 = np.random.uniform(0.7, 0.99, size=2) + p.define_noisy_readout(0, p00=p00, p11=p11) + expt_list = [expt] + tomo_expt = Experiment(settings=expt_list * runs, program=p, symmetrization=0) + # calculate expected expectation value + amp_sqr0 = (np.cos(theta / 2)) ** 2 + amp_sqr1 = (np.sin(theta / 2)) ** 2 + expected_expectation = (p00 * amp_sqr0 + (1 - p11) * amp_sqr1) - ((1 - p00) * amp_sqr0 + p11 * amp_sqr1) + + expect_arr = np.zeros(runs * len(expt_list)) + + for idx, res in enumerate(measure_observables(qc, tomo_expt, calibrate_readout=None)): + expect_arr[idx] = res.expectation + + assert np.isclose(np.mean(expect_arr), expected_expectation, atol=2e-2) + + +def test_uncalibrated_symmetric_readout_nontrivial_1q_state( + client_configuration: QCSClientConfiguration, use_seed: bool +): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + runs = 1 + else: + runs = 100 + expt = ExperimentSetting(TensorProductState(), sZ(0)) + # pick some random value for RX rotation + theta = np.random.uniform(0.0, 2 * np.pi) + p = Program(RX(theta, 0)).wrap_in_numshots_loop(2000) + # pick some random (but sufficiently large) asymmetric readout errors + p00, p11 = np.random.uniform(0.7, 0.99, size=2) + p.define_noisy_readout(0, p00=p00, p11=p11) + expt_list = [expt] + tomo_expt = Experiment(settings=expt_list * runs, program=p, symmetrization=-1) + # calculate expected expectation value + amp_sqr0 = (np.cos(theta / 2)) ** 2 + amp_sqr1 = (np.sin(theta / 2)) ** 2 + symm_prob = (p00 + p11) / 2 + expected_expectation = (symm_prob * amp_sqr0 + (1 - symm_prob) * amp_sqr1) - ( + (1 - symm_prob) * amp_sqr0 + symm_prob * amp_sqr1 + ) + + expect_arr = np.zeros(runs * len(expt_list)) + + for idx, res in enumerate(measure_observables(qc, tomo_expt, calibrate_readout=None)): + expect_arr[idx] = res.expectation + + assert np.isclose(np.mean(expect_arr), expected_expectation, atol=2e-2) + + +def test_calibrated_symmetric_readout_nontrivial_1q_state(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + runs = 1 + else: + runs = 100 + expt = ExperimentSetting(TensorProductState(), sZ(0)) + # pick some random value for RX rotation + theta = np.random.uniform(0.0, 2 * np.pi) + p = Program(RX(theta, 0)).wrap_in_numshots_loop(2000) + # pick some random (but sufficiently large) asymmetric readout errors + p00, p11 = np.random.uniform(0.7, 0.99, size=2) + p.define_noisy_readout(0, p00=p00, p11=p11) + expt_list = [expt] + tomo_expt = Experiment(settings=expt_list * runs, program=p, symmetrization=-1) + # calculate expected expectation value + amp_sqr0 = (np.cos(theta / 2)) ** 2 + amp_sqr1 = (np.sin(theta / 2)) ** 2 + expected_expectation = amp_sqr0 - amp_sqr1 + + expect_arr = np.zeros(runs * len(expt_list)) + + for idx, res in enumerate(measure_observables(qc, tomo_expt, calibrate_readout="plus-eig")): + expect_arr[idx] = res.expectation + + assert np.isclose(np.mean(expect_arr), expected_expectation, atol=2e-2) + + +def test_measure_2q_observable_raw_statistics(client_configuration: QCSClientConfiguration, use_seed: bool): + """Testing that we get correct exhaustively symmetrized statistics + in terms of readout errors. + Note: this only tests for exhaustive symmetrization in the presence + of uncorrelated errors + """ + qc = get_qc("2q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt = ExperimentSetting(TensorProductState(), sZ(0) * sZ(1)) + p = Program().wrap_in_numshots_loop(5000) + p00, p11 = 0.99, 0.80 + q00, q11 = 0.93, 0.76 + p.define_noisy_readout(0, p00=p00, p11=p11) + p.define_noisy_readout(1, p00=q00, p11=q11) + tomo_expt = Experiment(settings=[expt], program=p) + + raw_expectations = [] + raw_std_errs = [] + + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt)) + raw_expectations.append([res.raw_expectation for res in expt_results]) + raw_std_errs.append([res.raw_std_err for res in expt_results]) + + raw_expectations = np.array(raw_expectations) + raw_std_errs = np.array(raw_std_errs) + result_expectation = np.mean(raw_expectations, axis=0) + result_std_err = np.mean(raw_std_errs, axis=0) + + # calculate relevant conditional probabilities, given |00> state + # notation used: pijmn means p(ij|mn) + p0000 = (p00 + p11) * (q00 + q11) / 4 + p0100 = (p00 + p11) * (2 - q00 - q11) / 4 + p1000 = (q00 + q11) * (2 - p00 - p11) / 4 + p1100 = (2 - p00 - p11) * (2 - q00 - q11) / 4 + # calculate expectation value of Z^{\otimes 2} + z_expectation = (p0000 + p1100) - (p0100 + p1000) + # calculate standard deviation of the mean + simulated_std_err = np.sqrt((1 - z_expectation ** 2) / p.num_shots) + # compare against simulated results + np.testing.assert_allclose(result_expectation, z_expectation, atol=2e-2) + np.testing.assert_allclose(result_std_err, simulated_std_err, atol=2e-2) + + +def test_raw_statistics_2q_nontrivial_nonentangled_state(client_configuration: QCSClientConfiguration, use_seed: bool): + """Testing that we get correct exhaustively symmetrized statistics + in terms of readout errors, even for non-trivial 2q nonentangled states + Note: this only tests for exhaustive symmetrization in the presence + of uncorrelated errors + """ + qc = get_qc("2q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt = ExperimentSetting(TensorProductState(), sZ(0) * sZ(1)) + theta1, theta2 = np.random.uniform(0.0, 2 * np.pi, size=2) + p = Program(RX(theta1, 0), RX(theta2, 1)).wrap_in_numshots_loop(5000) + p00, p11, q00, q11 = np.random.uniform(0.70, 0.99, size=4) + p.define_noisy_readout(0, p00=p00, p11=p11) + p.define_noisy_readout(1, p00=q00, p11=q11) + tomo_expt = Experiment(settings=[expt], program=p) + + raw_expectations = [] + raw_std_errs = [] + + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt)) + raw_expectations.append([res.raw_expectation for res in expt_results]) + raw_std_errs.append([res.raw_std_err for res in expt_results]) + raw_expectations = np.array(raw_expectations) + raw_std_errs = np.array(raw_std_errs) + result_expectation = np.mean(raw_expectations, axis=0) + result_std_err = np.mean(raw_std_errs, axis=0) + + # calculate relevant conditional probabilities, given |00> state + # notation used: pijmn means p(ij|mn) + p0000 = (p00 + p11) * (q00 + q11) / 4 + p0100 = (p00 + p11) * (2 - q00 - q11) / 4 + p1000 = (q00 + q11) * (2 - p00 - p11) / 4 + p1100 = (2 - p00 - p11) * (2 - q00 - q11) / 4 + # calculate relevant conditional probabilities, given |01> state + p0001 = p0100 + p0101 = p0000 + p1001 = (2 - p00 - p11) * (2 - q00 - q11) / 4 + p1101 = (2 - p00 - p11) * (q00 + q11) / 4 + # calculate relevant conditional probabilities, given |10> state + p0010 = p1000 + p0110 = p1001 + p1010 = p0000 + p1110 = (p00 + p11) * (2 - q00 - q11) / 4 + # calculate relevant conditional probabilities, given |11> state + p0011 = p1100 + p0111 = p1101 + p1011 = p1110 + p1111 = p0000 + # calculate amplitudes squared of pure state + alph00 = (np.cos(theta1 / 2) * np.cos(theta2 / 2)) ** 2 + alph01 = (np.cos(theta1 / 2) * np.sin(theta2 / 2)) ** 2 + alph10 = (np.sin(theta1 / 2) * np.cos(theta2 / 2)) ** 2 + alph11 = (np.sin(theta1 / 2) * np.sin(theta2 / 2)) ** 2 + # calculate probabilities of various bitstrings + pr00 = p0000 * alph00 + p0001 * alph01 + p0010 * alph10 + p0011 * alph11 + pr01 = p0100 * alph00 + p0101 * alph01 + p0110 * alph10 + p0111 * alph11 + pr10 = p1000 * alph00 + p1001 * alph01 + p1010 * alph10 + p1011 * alph11 + pr11 = p1100 * alph00 + p1101 * alph01 + p1110 * alph10 + p1111 * alph11 + # calculate Z^{\otimes 2} expectation, and error of the mean + z_expectation = (pr00 + pr11) - (pr01 + pr10) + simulated_std_err = np.sqrt((1 - z_expectation ** 2) / p.num_shots) + # compare against simulated results + np.testing.assert_allclose(result_expectation, z_expectation, atol=2e-2) + np.testing.assert_allclose(result_std_err, simulated_std_err, atol=2e-2) + + +def test_raw_statistics_2q_nontrivial_entangled_state(client_configuration: QCSClientConfiguration, use_seed: bool): + """Testing that we get correct exhaustively symmetrized statistics + in terms of readout errors, even for non-trivial 2q entangled states. + Note: this only tests for exhaustive symmetrization in the presence + of uncorrelated errors + """ + qc = get_qc("2q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_simulations = 1 + else: + num_simulations = 100 + expt = ExperimentSetting(TensorProductState(), sZ(0) * sZ(1)) + theta = np.random.uniform(0.0, 2 * np.pi) + p = Program(RX(theta, 0), CNOT(0, 1)).wrap_in_numshots_loop(5000) + p00, p11, q00, q11 = np.random.uniform(0.70, 0.99, size=4) + p.define_noisy_readout(0, p00=p00, p11=p11) + p.define_noisy_readout(1, p00=q00, p11=q11) + tomo_expt = Experiment(settings=[expt], program=p) + + raw_expectations = [] + raw_std_errs = [] + + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt)) + raw_expectations.append([res.raw_expectation for res in expt_results]) + raw_std_errs.append([res.raw_std_err for res in expt_results]) + raw_expectations = np.array(raw_expectations) + raw_std_errs = np.array(raw_std_errs) + result_expectation = np.mean(raw_expectations, axis=0) + result_std_err = np.mean(raw_std_errs, axis=0) + + # calculate relevant conditional probabilities, given |00> state + # notation used: pijmn means p(ij|mn) + p0000 = (p00 + p11) * (q00 + q11) / 4 + p0100 = (p00 + p11) * (2 - q00 - q11) / 4 + p1000 = (q00 + q11) * (2 - p00 - p11) / 4 + p1100 = (2 - p00 - p11) * (2 - q00 - q11) / 4 + # calculate relevant conditional probabilities, given |11> state + p0011 = p1100 + p0111 = (2 - p00 - p11) * (q00 + q11) / 4 + p1011 = (p00 + p11) * (2 - q00 - q11) / 4 + p1111 = p0000 + # calculate amplitudes squared of pure state + alph00 = (np.cos(theta / 2)) ** 2 + alph11 = (np.sin(theta / 2)) ** 2 + # calculate probabilities of various bitstrings + pr00 = p0000 * alph00 + p0011 * alph11 + pr01 = p0100 * alph00 + p0111 * alph11 + pr10 = p1000 * alph00 + p1011 * alph11 + pr11 = p1100 * alph00 + p1111 * alph11 + # calculate Z^{\otimes 2} expectation, and error of the mean + z_expectation = (pr00 + pr11) - (pr01 + pr10) + simulated_std_err = np.sqrt((1 - z_expectation ** 2) / p.num_shots) + # compare against simulated results + np.testing.assert_allclose(result_expectation, z_expectation, atol=2e-2) + np.testing.assert_allclose(result_std_err, simulated_std_err, atol=2e-2) + + +@pytest.mark.flaky(reruns=1) +def test_corrected_statistics_2q_nontrivial_nonentangled_state( + client_configuration: QCSClientConfiguration, use_seed: bool +): + """Testing that we can successfully correct for observed statistics + in the presence of readout errors, even for 2q nontrivial but + nonentangled states. + Note: this only tests for exhaustive symmetrization in the presence + of uncorrelated errors + """ + qc = get_qc("2q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(13) + num_simulations = 1 + else: + num_simulations = 100 + + expt = ExperimentSetting(TensorProductState(), sZ(0) * sZ(1)) + theta1, theta2 = np.random.uniform(0.0, 2 * np.pi, size=2) + p = Program(RX(theta1, 0), RX(theta2, 1)).wrap_in_numshots_loop(5000) + p00, p11, q00, q11 = np.random.uniform(0.70, 0.99, size=4) + p.define_noisy_readout(0, p00=p00, p11=p11) + p.define_noisy_readout(1, p00=q00, p11=q11) + tomo_expt = Experiment(settings=[expt], program=p) + + expectations = [] + std_errs = [] + + for _ in range(num_simulations): + expt_results = list(measure_observables(qc, tomo_expt)) + expectations.append([res.expectation for res in expt_results]) + std_errs.append([res.std_err for res in expt_results]) + expectations = np.array(expectations) + std_errs = np.array(std_errs) + result_expectation = np.mean(expectations, axis=0) + result_std_err = np.mean(std_errs, axis=0) + + # calculate amplitudes squared of pure state + alph00 = (np.cos(theta1 / 2) * np.cos(theta2 / 2)) ** 2 + alph01 = (np.cos(theta1 / 2) * np.sin(theta2 / 2)) ** 2 + alph10 = (np.sin(theta1 / 2) * np.cos(theta2 / 2)) ** 2 + alph11 = (np.sin(theta1 / 2) * np.sin(theta2 / 2)) ** 2 + # calculate Z^{\otimes 2} expectation, and error of the mean + expected_expectation = (alph00 + alph11) - (alph01 + alph10) + expected_std_err = np.sqrt(np.var(expectations)) + # compare against simulated results + np.testing.assert_allclose(result_expectation, expected_expectation, atol=2e-2) + np.testing.assert_allclose(result_std_err, expected_std_err, atol=2e-2) + + +def _point_state_fidelity_estimate(v, dim=2): + """:param v: array of expectation values + :param dim: dimensionality of the Hilbert space""" + return (1.0 + np.sum(v)) / dim + + +def test_bit_flip_state_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # prepare noisy bit-flip channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # the bit flip channel is composed of two Kraus operations -- + # applying the X gate with probability `prob`, and applying the identity gate + # with probability `1 - prob` + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[0, 1], [1, 0]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is the mixed state to |0> + expected_fidelity = 1 - prob + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_dephasing_state_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # prepare noisy dephasing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for dephasing channel + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is the mixed state to |0> + expected_fidelity = 1 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_depolarizing_state_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # prepare noisy depolarizing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for depolarizing channel + kraus_ops = [ + np.sqrt(3 * prob + 1) / 2 * np.array([[1, 0], [0, 1]]), + np.sqrt(1 - prob) / 2 * np.array([[0, 1], [1, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[0, -1j], [1j, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is the mixed state to |0> + expected_fidelity = (1 + prob) / 2 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_unitary_state_fidelity(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # rotate |0> state by some random angle about X axis + theta = np.random.uniform(0.0, 2 * np.pi) + p = Program(RX(theta, 0)).wrap_in_numshots_loop(2000) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is this state to |0> + expected_fidelity = (np.cos(theta / 2)) ** 2 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_bit_flip_state_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # prepare noisy bit-flip channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # the bit flip channel is composed of two Kraus operations -- + # applying the X gate with probability `prob`, and applying the identity gate + # with probability `1 - prob` + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[0, 1], [1, 0]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + p.define_noisy_readout(0, 0.95, 0.76) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is the mixed state to |0> + expected_fidelity = 1 - prob + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_dephasing_state_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # prepare noisy dephasing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for dephasing channel + kraus_ops = [ + np.sqrt(1 - prob) * np.array([[1, 0], [0, 1]]), + np.sqrt(prob) * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + p.define_noisy_readout(0, 0.95, 0.76) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is the mixed state to |0> + expected_fidelity = 1 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_depolarizing_state_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # prepare noisy depolarizing channel as program for some random value of probability + prob = np.random.uniform(0.1, 0.5) + # Kraus operators for depolarizing channel + kraus_ops = [ + np.sqrt(3 * prob + 1) / 2 * np.array([[1, 0], [0, 1]]), + np.sqrt(1 - prob) / 2 * np.array([[0, 1], [1, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[0, -1j], [1j, 0]]), + np.sqrt(1 - prob) / 2 * np.array([[1, 0], [0, -1]]), + ] + p = Program(Pragma("PRESERVE_BLOCK"), I(0), Pragma("END_PRESERVE_BLOCK")).wrap_in_numshots_loop(2000) + p.define_noisy_gate("I", [0], kraus_ops) + p.define_noisy_readout(0, 0.95, 0.76) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is the mixed state to |0> + expected_fidelity = (1 + prob) / 2 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) + + +def test_unitary_state_fidelity_readout_error(client_configuration: QCSClientConfiguration, use_seed: bool): + qc = get_qc("1q-qvm", client_configuration=client_configuration) + if use_seed: + qc.qam.random_seed = 0 + np.random.seed(0) + num_expts = 1 + else: + num_expts = 100 + # prepare experiment setting + expt = ExperimentSetting(TensorProductState(), sZ(0)) + + # rotate |0> state by some random angle about X axis + theta = np.random.uniform(0.0, 2 * np.pi) + p = Program(RX(theta, 0)).wrap_in_numshots_loop(2000) + p.define_noisy_readout(0, 0.95, 0.76) + + # prepare TomographyExperiment + process_exp = Experiment(settings=[expt], program=p) + # list to store experiment results + expts = [] + for _ in range(num_expts): + expt_results = [] + for res in measure_observables(qc, process_exp): + expt_results.append(res.expectation) + expts.append(expt_results) + + expts = np.array(expts) + results = np.mean(expts, axis=0) + estimated_fidelity = _point_state_fidelity_estimate(results) + # how close is this state to |0> + expected_fidelity = (np.cos(theta / 2)) ** 2 + np.testing.assert_allclose(expected_fidelity, estimated_fidelity, atol=2e-2) diff --git a/test/unit/test_quantum_computer.py b/test/unit/test_quantum_computer.py new file mode 100644 index 000000000..9b5c2ae08 --- /dev/null +++ b/test/unit/test_quantum_computer.py @@ -0,0 +1,810 @@ +import itertools +import random +from test.unit.utils import DummyCompiler + +import networkx as nx +import numpy as np +import pytest +from pyquil import Program, list_quantum_computers +from pyquil.api import QCSClientConfiguration +from pyquil.api._quantum_computer import ( + QuantumComputer, + _check_min_num_trials_for_symmetrized_readout, + _consolidate_symmetrization_outputs, + _construct_orthogonal_array, + _construct_strength_three_orthogonal_array, + _construct_strength_two_orthogonal_array, + _flip_array_to_prog, + _get_qvm_with_topology, + _measure_bitstrings, + _parse_name, + _symmetrization, + get_qc, +) +from pyquil.api._qvm import QVM +from pyquil.experiment import Experiment, ExperimentSetting +from pyquil.experiment._main import _pauli_to_product_state +from pyquil.gates import CNOT, MEASURE, RESET, RX, RY, H, I, X +from pyquil.noise import NoiseModel, decoherence_noise_with_asymmetric_ro +from pyquil.paulis import sX, sY, sZ +from pyquil.pyqvm import PyQVM +from pyquil.quantum_processor import NxQuantumProcessor +from pyquil.quilbase import Declare, MemoryReference +from rpcq.messages import ParameterAref + + +def test_flip_array_to_prog(): + # no flips + flip_prog = _flip_array_to_prog((0, 0, 0, 0, 0, 0), [0, 1, 2, 3, 4, 5]) + assert flip_prog.out().splitlines() == [] + # mixed flips + flip_prog = _flip_array_to_prog((1, 0, 1, 0, 1, 1), [0, 1, 2, 3, 4, 5]) + assert flip_prog.out().splitlines() == ["RX(pi) 0", "RX(pi) 2", "RX(pi) 4", "RX(pi) 5"] + # flip all + flip_prog = _flip_array_to_prog((1, 1, 1, 1, 1, 1), [0, 1, 2, 3, 4, 5]) + assert flip_prog.out().splitlines() == [ + "RX(pi) 0", + "RX(pi) 1", + "RX(pi) 2", + "RX(pi) 3", + "RX(pi) 4", + "RX(pi) 5", + ] + + +def test_symmetrization(): + prog = Program(I(0), I(1)) + meas_qubits = [0, 1] + # invalid input if symm_type < -1 or > 3 + with pytest.raises(ValueError): + _, _ = _symmetrization(prog, meas_qubits, symm_type=-2) + with pytest.raises(ValueError): + _, _ = _symmetrization(prog, meas_qubits, symm_type=4) + # exhaustive symm + sym_progs, flip_array = _symmetrization(prog, meas_qubits, symm_type=-1) + assert sym_progs[0].out().splitlines() == ["I 0", "I 1"] + assert sym_progs[1].out().splitlines() == ["I 0", "I 1", "RX(pi) 1"] + assert sym_progs[2].out().splitlines() == ["I 0", "I 1", "RX(pi) 0"] + assert sym_progs[3].out().splitlines() == ["I 0", "I 1", "RX(pi) 0", "RX(pi) 1"] + right = [np.array([0, 0]), np.array([0, 1]), np.array([1, 0]), np.array([1, 1])] + assert all([np.allclose(x, y) for x, y in zip(flip_array, right)]) + # strength 0 i.e. no symm + sym_progs, flip_array = _symmetrization(prog, meas_qubits, symm_type=-1) + assert sym_progs[0].out().splitlines() == ["I 0", "I 1"] + right = [np.array([0, 0])] + assert all([np.allclose(x, y) for x, y in zip(flip_array, right)]) + # strength 1 + sym_progs, flip_array = _symmetrization(prog, meas_qubits, symm_type=1) + assert sym_progs[0].out().splitlines() == ["I 0", "I 1"] + assert sym_progs[1].out().splitlines() == ["I 0", "I 1", "RX(pi) 0", "RX(pi) 1"] + right = [np.array([0, 0]), np.array([1, 1])] + assert all([np.allclose(x, y) for x, y in zip(flip_array, right)]) + # strength 2 + sym_progs, flip_array = _symmetrization(prog, meas_qubits, symm_type=2) + assert sym_progs[0].out().splitlines() == ["I 0", "I 1"] + assert sym_progs[1].out().splitlines() == ["I 0", "I 1", "RX(pi) 0"] + assert sym_progs[2].out().splitlines() == ["I 0", "I 1", "RX(pi) 1"] + assert sym_progs[3].out().splitlines() == ["I 0", "I 1", "RX(pi) 0", "RX(pi) 1"] + right = [np.array([0, 0]), np.array([1, 0]), np.array([0, 1]), np.array([1, 1])] + assert all([np.allclose(x, y) for x, y in zip(flip_array, right)]) + # strength 3 + sym_progs, flip_array = _symmetrization(prog, meas_qubits, symm_type=3) + assert sym_progs[0].out().splitlines() == ["I 0", "I 1", "RX(pi) 0", "RX(pi) 1"] + assert sym_progs[1].out().splitlines() == ["I 0", "I 1", "RX(pi) 0"] + assert sym_progs[2].out().splitlines() == ["I 0", "I 1"] + assert sym_progs[3].out().splitlines() == ["I 0", "I 1", "RX(pi) 1"] + right = [np.array([1, 1]), np.array([1, 0]), np.array([0, 0]), np.array([0, 1])] + assert all([np.allclose(x, y) for x, y in zip(flip_array, right)]) + + +def test_construct_orthogonal_array(): + # check for valid inputs + with pytest.raises(ValueError): + _construct_orthogonal_array(3, strength=-1) + with pytest.raises(ValueError): + _construct_orthogonal_array(3, strength=4) + with pytest.raises(ValueError): + _construct_orthogonal_array(3, strength=100) + + +def test_construct_strength_three_orthogonal_array(): + # This is test is table 1.3 in [OATA]. Next to the np.array below the "line" number refers to + # the row in table 1.3. It is not important that the rows are switched! Specifically + # "A permutation of the runs or factors in an orthogonal array results in an orthogonal + # array with the same parameters." page 27 of [OATA]. + # + # [OATA] Orthogonal Arrays Theory and Applications + # Hedayat, Sloane, Stufken + # Springer, 1999 + answer = np.array( + [ + [1, 1, 1, 1], # line 8 + [1, 0, 1, 0], # line 6 + [1, 1, 0, 0], # line 7 + [1, 0, 0, 1], # line 5 + [0, 0, 0, 0], # line 1 + [0, 1, 0, 1], # line 3 + [0, 0, 1, 1], # line 2 + [0, 1, 1, 0], + ] + ) # line 4 + assert np.allclose(_construct_strength_three_orthogonal_array(4), answer) + + +def test_construct_strength_two_orthogonal_array(): + # This is example 1.5 in [OATA]. Next to the np.array below the "line" number refers to + # the row in example 1.5. + answer = np.array([[0, 0, 0], [1, 0, 1], [0, 1, 1], [1, 1, 0]]) # line 1 # line 3 # line 2 # line 4 + assert np.allclose(_construct_strength_two_orthogonal_array(3), answer) + + +def test_measure_bitstrings(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(2)) + dummy_compiler = DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration) + qc_pyqvm = QuantumComputer(name="testy!", qam=PyQVM(n_qubits=2), compiler=dummy_compiler) + qc_forest = QuantumComputer( + name="testy!", + qam=QVM(client_configuration=client_configuration, gate_noise=(0.00, 0.00, 0.00)), + compiler=dummy_compiler, + ) + prog = Program(I(0), I(1)) + meas_qubits = [0, 1] + sym_progs, flip_array = _symmetrization(prog, meas_qubits, symm_type=-1) + results = _measure_bitstrings(qc_pyqvm, sym_progs, meas_qubits, num_shots=1) + # test with pyQVM + answer = [np.array([[0, 0]]), np.array([[0, 1]]), np.array([[1, 0]]), np.array([[1, 1]])] + assert all([np.allclose(x, y) for x, y in zip(results, answer)]) + # test with regular QVM + results = _measure_bitstrings(qc_forest, sym_progs, meas_qubits, num_shots=1) + assert all([np.allclose(x, y) for x, y in zip(results, answer)]) + + +def test_consolidate_symmetrization_outputs(): + flip_arrays = [np.array([[0, 0]]), np.array([[0, 1]]), np.array([[1, 0]]), np.array([[1, 1]])] + # if results = flip_arrays should be a matrix of zeros + ans1 = np.array([[0, 0], [0, 0], [0, 0], [0, 0]]) + assert np.allclose(_consolidate_symmetrization_outputs(flip_arrays, flip_arrays), ans1) + results = [np.array([[0, 0]]), np.array([[0, 0]]), np.array([[0, 0]]), np.array([[0, 0]])] + # results are all zero then output should be + ans2 = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) + assert np.allclose(_consolidate_symmetrization_outputs(results, flip_arrays), ans2) + + +def test_check_min_num_trials_for_symmetrized_readout(): + # trials = -2 should get bumped up to 4 trials + with pytest.warns(Warning): + assert _check_min_num_trials_for_symmetrized_readout(num_qubits=2, trials=-2, symm_type=-1) == 4 + # can't have symm_type < -2 or > 3 + with pytest.raises(ValueError): + _check_min_num_trials_for_symmetrized_readout(num_qubits=2, trials=-2, symm_type=-2) + with pytest.raises(ValueError): + _check_min_num_trials_for_symmetrized_readout(num_qubits=2, trials=-2, symm_type=4) + + +# We sometimes narrowly miss the np.mean(parity) < 0.15 assertion, below. Alternatively, that upper +# bound could be relaxed. +@pytest.mark.flaky(reruns=1) +def test_run(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(3)) + qc = QuantumComputer( + name="testy!", + qam=QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)), + compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), + ) + bitstrings = qc.run( + Program( + Declare("ro", "BIT", 3), + H(0), + CNOT(0, 1), + CNOT(1, 2), + MEASURE(0, MemoryReference("ro", 0)), + MEASURE(1, MemoryReference("ro", 1)), + MEASURE(2, MemoryReference("ro", 2)), + ).wrap_in_numshots_loop(1000) + ) + + assert bitstrings.shape == (1000, 3) + parity = np.sum(bitstrings, axis=1) % 3 + assert 0 < np.mean(parity) < 0.15 + + +def test_run_pyqvm_noiseless(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(3)) + qc = QuantumComputer( + name="testy!", + qam=PyQVM(n_qubits=3), + compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), + ) + prog = Program(H(0), CNOT(0, 1), CNOT(1, 2)) + ro = prog.declare("ro", "BIT", 3) + for q in range(3): + prog += MEASURE(q, ro[q]) + bitstrings = qc.run(prog.wrap_in_numshots_loop(1000)) + + assert bitstrings.shape == (1000, 3) + parity = np.sum(bitstrings, axis=1) % 3 + assert np.mean(parity) == 0 + + +def test_run_pyqvm_noisy(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(3)) + qc = QuantumComputer( + name="testy!", + qam=PyQVM(n_qubits=3, post_gate_noise_probabilities={"relaxation": 0.01}), + compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), + ) + prog = Program(H(0), CNOT(0, 1), CNOT(1, 2)) + ro = prog.declare("ro", "BIT", 3) + for q in range(3): + prog += MEASURE(q, ro[q]) + bitstrings = qc.run(prog.wrap_in_numshots_loop(1000)) + + assert bitstrings.shape == (1000, 3) + parity = np.sum(bitstrings, axis=1) % 3 + assert 0 < np.mean(parity) < 0.15 + + +def test_readout_symmetrization(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(3)) + noise_model = decoherence_noise_with_asymmetric_ro(quantum_processor.to_compiler_isa()) + qc = QuantumComputer( + name="testy!", + qam=QVM(client_configuration=client_configuration, noise_model=noise_model), + compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), + ) + + prog = Program( + Declare("ro", "BIT", 2), + I(0), + X(1), + MEASURE(0, MemoryReference("ro", 0)), + MEASURE(1, MemoryReference("ro", 1)), + ) + prog.wrap_in_numshots_loop(1000) + + bs1 = qc.run(prog) + avg0_us = np.mean(bs1[:, 0]) + avg1_us = 1 - np.mean(bs1[:, 1]) + diff_us = avg1_us - avg0_us + assert diff_us > 0.03 + + prog = Program( + I(0), + X(1), + ) + bs2 = qc.run_symmetrized_readout(prog, 1000) + avg0_s = np.mean(bs2[:, 0]) + avg1_s = 1 - np.mean(bs2[:, 1]) + diff_s = avg1_s - avg0_s + assert diff_s < 0.05 + + +@pytest.mark.slow +def test_run_symmetrized_readout_error(client_configuration: QCSClientConfiguration): + # This test checks if the function runs for any possible input on a small number of qubits. + # Locally this test was run on all 8 qubits, but it was slow. + qc = get_qc("8q-qvm", client_configuration=client_configuration) + sym_type_vec = [-1, 0, 1, 2, 3] + prog_vec = [Program(I(x) for x in range(0, 3))[0:n] for n in range(0, 4)] + trials_vec = list(range(0, 5)) + for prog, trials, sym_type in itertools.product(prog_vec, trials_vec, sym_type_vec): + print(qc.run_symmetrized_readout(prog, trials, sym_type)) + + +def test_list_qc(): + qc_names = list_quantum_computers(qpus=False) + # TODO: update with deployed qpus + assert qc_names == ["9q-square-qvm", "9q-square-noisy-qvm"] + + +def test_parse_qc_name(): + name, qvm_type, noisy = _parse_name("9q-generic", None, None) + assert name == "9q-generic" + assert qvm_type is None + assert not noisy + + name, qvm_type, noisy = _parse_name("9q-generic-qvm", None, None) + assert name == "9q-generic" + assert qvm_type == "qvm" + assert not noisy + + name, qvm_type, noisy = _parse_name("9q-generic-noisy-qvm", None, None) + assert name == "9q-generic" + assert qvm_type == "qvm" + assert noisy + + +def test_parse_qc_flags(): + name, qvm_type, noisy = _parse_name("9q-generic", False, False) + assert name == "9q-generic" + assert qvm_type is None + assert not noisy + + name, qvm_type, noisy = _parse_name("9q-generic", True, None) + assert name == "9q-generic" + assert qvm_type == "qvm" + assert not noisy + + name, qvm_type, noisy = _parse_name("9q-generic", True, True) + assert name == "9q-generic" + assert qvm_type == "qvm" + assert noisy + + +def test_parse_qc_redundant(): + name, qvm_type, noisy = _parse_name("9q-generic", False, False) + assert name == "9q-generic" + assert qvm_type is None + assert not noisy + + name, qvm_type, noisy = _parse_name("9q-generic-qvm", True, False) + assert name == "9q-generic" + assert qvm_type == "qvm" + assert not noisy + + name, qvm_type, noisy = _parse_name("9q-generic-noisy-qvm", True, True) + assert name == "9q-generic" + assert qvm_type == "qvm" + assert noisy + + +def test_parse_qc_conflicting(): + with pytest.raises(ValueError) as e: + name, qvm_type, noisy = _parse_name("9q-generic-qvm", False, False) + + assert e.match(r".*but you have specified `as_qvm=False`") + + with pytest.raises(ValueError) as e: + name, qvm_type, noisy = _parse_name("9q-generic-noisy-qvm", True, False) + assert e.match(r".*but you have specified `noisy=False`") + + +def test_parse_qc_strip(): + # Originally used `str.strip` to remove the suffixes. This is not correct! + name, _, _ = _parse_name("mvq-qvm", None, None) + assert name == "mvq" + + name, _, _ = _parse_name("mvq-noisy-qvm", None, None) + assert name == "mvq" + + +def test_parse_qc_no_prefix(): + prefix, qvm_type, noisy = _parse_name("qvm", None, None) + assert qvm_type == "qvm" + assert not noisy + assert prefix == "" + + prefix, qvm_type, noisy = _parse_name("", True, None) + assert qvm_type == "qvm" + assert not noisy + assert prefix == "" + + +def test_parse_qc_no_prefix_2(): + prefix, qvm_type, noisy = _parse_name("noisy-qvm", None, None) + assert qvm_type == "qvm" + assert noisy + assert prefix == "" + + prefix, qvm_type, noisy = _parse_name("", True, True) + assert qvm_type == "qvm" + assert noisy + assert prefix == "" + + +def test_parse_qc_pyqvm(): + prefix, qvm_type, noisy = _parse_name("9q-generic-pyqvm", None, None) + assert prefix == "9q-generic" + assert qvm_type == "pyqvm" + assert not noisy + + +def test_qc(client_configuration: QCSClientConfiguration): + qc = get_qc("9q-square-noisy-qvm", client_configuration=client_configuration) + assert isinstance(qc, QuantumComputer) + assert qc.qam.noise_model is not None + assert qc.qubit_topology().number_of_nodes() == 9 + assert qc.qubit_topology().degree[0] == 2 + assert qc.qubit_topology().degree[4] == 4 + assert str(qc) == "9q-square-noisy-qvm" + + +def test_qc_run(client_configuration: QCSClientConfiguration): + qc = get_qc("9q-square-noisy-qvm", client_configuration=client_configuration) + bs = qc.run( + qc.compile( + Program( + Declare("ro", "BIT", 1), + X(0), + MEASURE(0, ("ro", 0)), + ).wrap_in_numshots_loop(3) + ) + ) + assert bs.shape == (3, 1) + + +def test_nq_qvm_qc(client_configuration: QCSClientConfiguration): + for n_qubits in [2, 4, 7, 19]: + qc = get_qc(f"{n_qubits}q-qvm", client_configuration=client_configuration) + for q1, q2 in itertools.permutations(range(n_qubits), r=2): + assert (q1, q2) in qc.qubit_topology().edges + assert qc.name == f"{n_qubits}q-qvm" + + +def test_qc_noisy(client_configuration: QCSClientConfiguration): + qc = get_qc("5q", as_qvm=True, noisy=True, client_configuration=client_configuration) + assert isinstance(qc, QuantumComputer) + + +def test_qc_compile(dummy_compiler: DummyCompiler, client_configuration: QCSClientConfiguration): + qc = get_qc("5q", as_qvm=True, noisy=True, client_configuration=client_configuration) + qc.compiler = dummy_compiler + prog = Program() + prog += H(0) + assert qc.compile(prog) == prog + + +def test_qc_error(client_configuration: QCSClientConfiguration): + # QVM is not a QPU + with pytest.raises(ValueError): + get_qc("9q-square-noisy-qvm", as_qvm=False, client_configuration=client_configuration) + + with pytest.raises(ValueError): + get_qc("5q", as_qvm=False, client_configuration=client_configuration) + + +def test_run_with_parameters(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(3)) + qc = QuantumComputer( + name="testy!", + qam=QVM(client_configuration=client_configuration), + compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), + ) + executable = Program( + Declare(name="theta", memory_type="REAL"), + Declare(name="ro", memory_type="BIT"), + RX(MemoryReference("theta"), 0), + MEASURE(0, MemoryReference("ro")), + ).wrap_in_numshots_loop(1000) + + executable.write_memory(region_name="theta", value=np.pi) + bitstrings = qc.run(executable) + + assert bitstrings.shape == (1000, 1) + assert all([bit == 1 for bit in bitstrings]) + + +def test_reset(client_configuration: QCSClientConfiguration): + quantum_processor = NxQuantumProcessor(nx.complete_graph(3)) + qc = QuantumComputer( + name="testy!", + qam=QVM(client_configuration=client_configuration), + compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), + ) + p = Program( + Declare(name="theta", memory_type="REAL"), + Declare(name="ro", memory_type="BIT"), + RX(MemoryReference("theta"), 0), + MEASURE(0, MemoryReference("ro")), + ).wrap_in_numshots_loop(10) + p.write_memory(region_name="theta", value=np.pi) + result = qc.qam.run(p) + + aref = ParameterAref(name="theta", index=0) + assert p._memory.values[aref] == np.pi + assert result.memory["ro"].shape == (10, 1) + assert all([bit == 1 for bit in result.memory["ro"]]) + + +def test_get_qvm_with_topology(client_configuration: QCSClientConfiguration): + topo = nx.from_edgelist([(5, 6), (6, 7), (10, 11)]) + # Note to developers: perhaps make `get_qvm_with_topology` public in the future + qc = _get_qvm_with_topology( + name="test-qvm", + topology=topo, + noisy=False, + qvm_type="qvm", + compiler_timeout=5.0, + execution_timeout=5.0, + client_configuration=client_configuration, + ) + assert len(qc.qubits()) == 5 + assert min(qc.qubits()) == 5 + + +def test_get_qvm_with_topology_2(client_configuration: QCSClientConfiguration): + topo = nx.from_edgelist([(5, 6), (6, 7)]) + qc = _get_qvm_with_topology( + name="test-qvm", + topology=topo, + noisy=False, + qvm_type="qvm", + compiler_timeout=5.0, + execution_timeout=5.0, + client_configuration=client_configuration, + ) + results = qc.run( + qc.compile( + Program( + Declare("ro", "BIT", 3), + X(5), + MEASURE(5, ("ro", 0)), + MEASURE(6, ("ro", 1)), + MEASURE(7, ("ro", 2)), + ).wrap_in_numshots_loop(5) + ) + ) + assert results.shape == (5, 3) + assert all(r[0] == 1 for r in results) + + +def test_parse_mix_qvm_and_noisy_flag(): + # https://github.com/rigetti/pyquil/issues/764 + name, qvm_type, noisy = _parse_name("1q-qvm", as_qvm=None, noisy=True) + assert noisy + + +def test_noisy(client_configuration: QCSClientConfiguration): + # https://github.com/rigetti/pyquil/issues/764 + p = Program( + Declare("ro", "BIT", 1), + X(0), + MEASURE(0, ("ro", 0)), + ).wrap_in_numshots_loop(10000) + qc = get_qc("1q-qvm", noisy=True, client_configuration=client_configuration) + result = qc.run(qc.compile(p)) + assert result.mean() < 1.0 + + +def test_orthogonal_array(): + def bit_array_to_int(bit_array): + output = 0 + for bit in bit_array: + output = (output << 1) | bit + return output + + def check_random_columns(oa, strength): + num_q = oa.shape[1] + num_cols = min(num_q, strength) + column_idxs = random.sample(range(num_q), num_cols) + occurences = {entry: 0 for entry in range(2 ** num_cols)} + for row in oa[:, column_idxs]: + occurences[bit_array_to_int(row)] += 1 + assert all([count == occurences[0] for count in occurences.values()]) + + for strength in [0, 1, 2, 3]: + for num_q in range(1, 64): + oa = _construct_orthogonal_array(num_q, strength=strength) + for _ in range(10): + check_random_columns(oa, strength) + + +def test_qc_expectation(client_configuration: QCSClientConfiguration, dummy_compiler: DummyCompiler): + qc = QuantumComputer(name="testy!", qam=QVM(client_configuration=client_configuration), compiler=dummy_compiler) + + # bell state program + p = Program() + p += RESET() + p += H(0) + p += CNOT(0, 1) + p.wrap_in_numshots_loop(10) + + # XX, YY, ZZ experiment + sx = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sX(0) * sX(1)) + sy = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sY(0) * sY(1)) + sz = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1)) + + e = Experiment(settings=[sx, sy, sz], program=p) + + results = qc.run_experiment(e) + + # XX expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[0].expectation, 1) + assert np.isclose(results[0].std_err, 0) + assert results[0].total_counts == 40 + + # YY expectation value for bell state |00> + |11> is -1 + assert np.isclose(results[1].expectation, -1) + assert np.isclose(results[1].std_err, 0) + assert results[1].total_counts == 40 + + # ZZ expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[2].expectation, 1) + assert np.isclose(results[2].std_err, 0) + assert results[2].total_counts == 40 + + +def test_qc_expectation_larger_lattice(client_configuration: QCSClientConfiguration, dummy_compiler: DummyCompiler): + qc = QuantumComputer(name="testy!", qam=QVM(client_configuration=client_configuration), compiler=dummy_compiler) + + q0 = 2 + q1 = 3 + + # bell state program + p = Program() + p += RESET() + p += H(q0) + p += CNOT(q0, q1) + p.wrap_in_numshots_loop(10) + + # XX, YY, ZZ experiment + sx = ExperimentSetting(in_state=_pauli_to_product_state(sZ(q0) * sZ(q1)), out_operator=sX(q0) * sX(q1)) + sy = ExperimentSetting(in_state=_pauli_to_product_state(sZ(q0) * sZ(q1)), out_operator=sY(q0) * sY(q1)) + sz = ExperimentSetting(in_state=_pauli_to_product_state(sZ(q0) * sZ(q1)), out_operator=sZ(q0) * sZ(q1)) + + e = Experiment(settings=[sx, sy, sz], program=p) + + results = qc.run_experiment(e) + + # XX expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[0].expectation, 1) + assert np.isclose(results[0].std_err, 0) + assert results[0].total_counts == 40 + + # YY expectation value for bell state |00> + |11> is -1 + assert np.isclose(results[1].expectation, -1) + assert np.isclose(results[1].std_err, 0) + assert results[1].total_counts == 40 + + # ZZ expectation value for bell state |00> + |11> is 1 + assert np.isclose(results[2].expectation, 1) + assert np.isclose(results[2].std_err, 0) + assert results[2].total_counts == 40 + + +def asymmetric_ro_model(qubits: list, p00: float = 0.95, p11: float = 0.90) -> NoiseModel: + aprobs = np.array([[p00, 1 - p00], [1 - p11, p11]]) + aprobs = {q: aprobs for q in qubits} + return NoiseModel([], aprobs) + + +def test_qc_calibration_1q(client_configuration: QCSClientConfiguration): + # noise model with 95% symmetrized readout fidelity per qubit + noise_model = asymmetric_ro_model([0], 0.945, 0.955) + qc = get_qc("1q-qvm", client_configuration=client_configuration) + qc.qam.noise_model = noise_model + + # bell state program (doesn't matter) + p = Program() + p += RESET() + p += H(0) + p += CNOT(0, 1) + p.wrap_in_numshots_loop(10000) + + # Z experiment + sz = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0)), out_operator=sZ(0)) + e = Experiment(settings=[sz], program=p) + + results = qc.calibrate(e) + + # Z expectation value should just be 1 - 2 * readout_error + np.isclose(results[0].expectation, 0.9, atol=0.01) + assert results[0].total_counts == 20000 + + +def test_qc_calibration_2q(client_configuration: QCSClientConfiguration): + # noise model with 95% symmetrized readout fidelity per qubit + noise_model = asymmetric_ro_model([0, 1], 0.945, 0.955) + qc = get_qc("2q-qvm", client_configuration=client_configuration) + qc.qam.noise_model = noise_model + + # bell state program (doesn't matter) + p = Program() + p += RESET() + p += H(0) + p += CNOT(0, 1) + p.wrap_in_numshots_loop(10000) + + # ZZ experiment + sz = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1)) + e = Experiment(settings=[sz], program=p) + + results = qc.calibrate(e) + + # ZZ expectation should just be (1 - 2 * readout_error_q0) * (1 - 2 * readout_error_q1) + np.isclose(results[0].expectation, 0.81, atol=0.01) + assert results[0].total_counts == 40000 + + +def test_qc_joint_expectation(client_configuration: QCSClientConfiguration, dummy_compiler: DummyCompiler): + qc = QuantumComputer(name="testy!", qam=QVM(client_configuration=client_configuration), compiler=dummy_compiler) + + # |01> state program + p = Program() + p += RESET() + p += X(0) + p.wrap_in_numshots_loop(10) + + # ZZ experiment + sz = ExperimentSetting( + in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] + ) + e = Experiment(settings=[sz], program=p) + + results = qc.run_experiment(e) + + # ZZ expectation value for state |01> is -1 + assert np.isclose(results[0].expectation, -1) + assert np.isclose(results[0].std_err, 0) + assert results[0].total_counts == 40 + # Z0 expectation value for state |01> is -1 + assert np.isclose(results[0].additional_results[0].expectation, -1) + assert results[0].additional_results[1].total_counts == 40 + # Z1 expectation value for state |01> is 1 + assert np.isclose(results[0].additional_results[1].expectation, 1) + assert results[0].additional_results[1].total_counts == 40 + + +def test_get_qc_noisy_qpu_error(client_configuration: QCSClientConfiguration, dummy_compiler: DummyCompiler): + expected_message = ( + "pyQuil currently does not support initializing a noisy QuantumComputer " + "based on a QCSQuantumProcessor. Change noisy to False or specify the name of a QVM." + ) + with pytest.raises(ValueError, match=expected_message): + get_qc("Aspen-8", noisy=True) + + +def test_qc_joint_calibration(client_configuration: QCSClientConfiguration): + # noise model with 95% symmetrized readout fidelity per qubit + noise_model = asymmetric_ro_model([0, 1], 0.945, 0.955) + qc = get_qc("2q-qvm", client_configuration=client_configuration) + qc.qam.noise_model = noise_model + + # |01> state program + p = Program() + p += RESET() + p += X(0) + p.wrap_in_numshots_loop(10000) + + # ZZ experiment + sz = ExperimentSetting( + in_state=_pauli_to_product_state(sZ(0) * sZ(1)), out_operator=sZ(0) * sZ(1), additional_expectations=[[0], [1]] + ) + e = Experiment(settings=[sz], program=p) + + results = qc.run_experiment(e) + + # ZZ expectation value for state |01> with 95% RO fid on both qubits is about -0.81 + assert np.isclose(results[0].expectation, -0.81, atol=0.01) + assert results[0].total_counts == 40000 + # Z0 expectation value for state |01> with 95% RO fid on both qubits is about -0.9 + assert np.isclose(results[0].additional_results[0].expectation, -0.9, atol=0.01) + assert results[0].additional_results[1].total_counts == 40000 + # Z1 expectation value for state |01> with 95% RO fid on both qubits is about 0.9 + assert np.isclose(results[0].additional_results[1].expectation, 0.9, atol=0.01) + assert results[0].additional_results[1].total_counts == 40000 + + +def test_qc_expectation_on_qvm(client_configuration: QCSClientConfiguration, dummy_compiler: DummyCompiler): + # regression test for https://github.com/rigetti/forest-tutorials/issues/2 + qc = QuantumComputer(name="testy!", qam=QVM(client_configuration=client_configuration), compiler=dummy_compiler) + + p = Program() + theta = p.declare("theta", "REAL") + p += RESET() + p += RY(theta, 0) + p.wrap_in_numshots_loop(10000) + + sx = ExperimentSetting(in_state=_pauli_to_product_state(sZ(0)), out_operator=sX(0)) + e = Experiment(settings=[sx], program=p) + + thetas = [-np.pi / 2, 0.0, np.pi / 2] + results = [] + + # Verify that multiple calls to qc.experiment with the same experiment backed by a QVM that + # requires_exectutable does not raise an exception. + for theta in thetas: + results.append(qc.run_experiment(e, memory_map={"theta": [theta]})) + + assert np.isclose(results[0][0].expectation, -1.0, atol=0.01) + assert np.isclose(results[0][0].std_err, 0) + assert results[0][0].total_counts == 20000 + + # bounds on atol and std_err here are a little loose to try and avoid test flakiness. + assert np.isclose(results[1][0].expectation, 0.0, atol=0.1) + assert results[1][0].std_err < 0.01 + assert results[1][0].total_counts == 20000 + + assert np.isclose(results[2][0].expectation, 1.0, atol=0.01) + assert np.isclose(results[2][0].std_err, 0) + assert results[2][0].total_counts == 20000 From 011d3771538c71d4092908e55085011e2576af47 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:43:11 -0700 Subject: [PATCH 21/61] Docs: update --- docs/source/advanced_usage.rst | 21 +++----- docs/source/basics.rst | 6 ++- docs/source/migration.rst | 79 ++++++++++++++++++++++++++++-- docs/source/noise.rst | 2 +- docs/source/quilt_parametric.ipynb | 8 +-- 5 files changed, 95 insertions(+), 21 deletions(-) diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index 4f148b85c..f4d70d736 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -35,14 +35,8 @@ locally): Concurrency ~~~~~~~~~~~ -Using pyQuil for concurrent programming is as simple as calling :py:func:`~pyquil.get_qc()` from within a given thread -or process, then using the returned :py:class:`~pyquil.api.QuantumComputer` as usual. While -:py:class:`~pyquil.api.QuantumComputer` objects as a whole are not safe to share between threads or processes (due to -state related to currently-running compilation or execution requests), some information they use is. Information related -to client configuration (:py:class:`~pyquil.api.QCSClientConfiguration`) and QPU auth -(:py:class:`~pyquil.api.EngagementManager`) can be safely extracted and shared among -:py:class:`~pyquil.api.QuantumComputer` instances, as shown below, to save your code from redundant disk reads and -auth-related HTTP requests. +:py:class:`~pyquil.api.QuantumComputer` objects are safe to share between threads or processes, +enabling you to execute and retrieve results for multiple programs or parameter values at once. .. note:: The QVM processes incoming requests in parallel, while a QPU may process them sequentially or in parallel @@ -50,6 +44,10 @@ auth-related HTTP requests. QPU, try increasing the ``execution_timeout`` parameter on calls to :py:func:`~pyquil.get_qc()` (specified in seconds). +.. note:: + We suggest running jobs with a minimum of 2x parallelism, so that the QVM or QPU + is fully occupied while your program runs and no time is wasted in between jobs. + Using Multithreading -------------------- @@ -60,13 +58,11 @@ Using Multithreading from pyquil import get_qc, Program from pyquil.api import EngagementManager, QCSClientConfiguration - configuration = QCSClientConfiguration.load() - engagement_manager = EngagementManager(client_configuration=configuration) + qc = get_qc("Aspen-8", client_configuration=configuration) def run(program: Program): - qc = get_qc("Aspen-8", client_configuration=configuration, engagement_manager=engagement_manager) return qc.run(qc.compile(program)) @@ -90,11 +86,10 @@ Using Multiprocessing configuration = QCSClientConfiguration.load() - engagement_manager = EngagementManager(client_configuration=configuration) + qc = get_qc("Aspen-8", client_configuration=configuration) def run(program: Program): - qc = get_qc("Aspen-8", client_configuration=configuration, engagement_manager=engagement_manager) return qc.run(qc.compile(program)) diff --git a/docs/source/basics.rst b/docs/source/basics.rst index 0238eb7dd..cee798380 100644 --- a/docs/source/basics.rst +++ b/docs/source/basics.rst @@ -299,8 +299,12 @@ filled in for, say, 200 values between :math:`0` and :math:`2\pi`. We demonstrat parametric_measurements = [] for theta in np.linspace(0, 2 * np.pi, 200): + # Set the desired parameter value in executable memory + executable.write_memory(region_name='theta', value=theta) + # Get the results of the run with the value we want to execute with - bitstrings = qc.run(executable, {'theta': [theta]}) + bitstrings = qc.run(executable) + # Store our results parametric_measurements.append(bitstrings) diff --git a/docs/source/migration.rst b/docs/source/migration.rst index 81ecab8e4..cfaaf9755 100644 --- a/docs/source/migration.rst +++ b/docs/source/migration.rst @@ -9,6 +9,79 @@ to the code affected. Most users should only need to make minimal changes. If you've supplied ``PyquilConfig`` objects to functions (or used the ``QVM_URL`` and ``QUILC_URL`` environment variables) to override configuration, see :ref:`pyquil_configuration`. -Lastly, pyQuil v3 relies on an updated authentication model. To get going smoothly, you should install the new `QCS CLI -`_ and log in with it before using pyQuil v3 -against real QPUs. +Authentication +-------------- + +pyQuil v3 relies on an updated authentication model. To get started, install the new `QCS CLI +`_ and +log in with it before using pyQuil v3 with QCS and live QPUs. If the following CLI command succeeds and +returns results, then we expect pyQuil authentication to work as well: + +.. code:: + + qcs api list-quantum-processors + + +Parameters & Memory +------------------- + +In order to give the user more control over and visibility into program execution, especially in +parallel, objects such as ``QuantumComputer``, ``QAM``, ``QPU``, and ``QVM`` are no longer stateful +with respect to individual programs. These objects (such as ``QuantumComputer``) are now safe to +share among different threads, so you can execute and retrieve results in parallel for even better +performance. (See :doc:`advanced_usage` for more information). + +However, this required three small but important changes: + +1. ``write_memory`` is no longer a method on ``QAM`` but rather on ``Program`` and ``EncryptedProgram``. +2. ``qc.run()`` no longer accepts a ``memory_map`` argument. All memory values must be set directly + on the ``Program`` or ``EncryptedProgram`` using ``write_memory``. +3. ``QAM.load()``, ``QAM.wait()``, and ``QAM.reset()`` no longer exist, because the + ``QAM`` no longer "stores" program state. + +This means that you should now execute your programs using one of these options: + +.. code:: python + + qc = get_qc("Aspen-X") + program = Program() + theta = program.declare('theta', 'REAL') + program += RZ(theta, 0) + exe = qc.compile(program) + + # Previously, we would have called ``qc.qam.write_memory`` instead + exe.write_memory(region_name='theta', value=np.pi) + + # Option 1 + qc.run(exe) + + # Option 2 + qc.qam.run(exe) + + # Option 3 + job = qc.qam.execute(exe) + result = qc.qam.get_results(job) + + # Run our program 10 times, enqueuing all the programs before retrieving results for any of them + jobs = [qc.qam.execute(exe) for _ in range(10)] + results = [qc.qam.get_results(job) for job in jobs] + + +Compatibility Utilities +----------------------- + +We understand that the changes above regarding parameters might cause difficulty in migration, +especially for large projects and lengthy scripts. So, to ease the migration path, we've added +the following utility classes which allow you to upgrade your pyQuil projects without having to +change any code. + +.. code:: python + + from pyquil.compatibility.v2 import get_qc, QuantumComputer + from pyquil.compatibility.v2.api import QAM, QVM, QPU + +You can use these imported objects exactly how you already use their counterparts in pyQuil v2. +Once you've verified that your scripts still work with v3, we recommend that you gradually convert +them to use the new versions of each object. This compatibility layer won't see any new +development, and without fully upgrading you'd miss out on all the new features to come in the +future. diff --git a/docs/source/noise.rst b/docs/source/noise.rst index 176c498e6..611d84d0a 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -710,7 +710,7 @@ gate noise, respectively. MEASURE(0, ("ro", 0)), MEASURE(1, ("ro", 1)), ]) - bitstrings = np.array(qc.run(noisy, [0,1], 1000)) + bitstrings = qc.run(noisy) # Expectation of Z0 and Z1 z0, z1 = 1 - 2*np.mean(bitstrings, axis=0) diff --git a/docs/source/quilt_parametric.ipynb b/docs/source/quilt_parametric.ipynb index b8e9f2547..8b13ab7c8 100644 --- a/docs/source/quilt_parametric.ipynb +++ b/docs/source/quilt_parametric.ipynb @@ -158,7 +158,8 @@ "detunings = np.linspace(-3e6, 3e6, 100)\n", "dprobs = []\n", "for detuning in detunings:\n", - " results = qc.run(exe, {'detuning': [detuning]})\n", + " exe.write_memory(region_name='detuning', value=detuning)\n", + " results = qc.run(exe)\n", " p1 = np.sum(results)/len(results)\n", " dprobs.append(p1)" ] @@ -279,7 +280,8 @@ "scales = np.linspace(1e-4, 1.0, 20)\n", "sprobs = []\n", "for scale in scales:\n", - " results = qc.run(exe, {'scale': [scale]})\n", + " exe.write_memory(region_name='scale', value=scale)\n", + " results = qc.run(exe)\n", " p1 = np.sum(results)/len(results)\n", " sprobs.append(p1)" ] @@ -475,4 +477,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file From f13e6844326d2683827d9b307725377feb80c041 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:43:20 -0700 Subject: [PATCH 22/61] Docs: update changelog --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e30c02e44..d4c360dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,15 @@ Changelog ### Announcements +- pyQuil now directly supports the QCS API v1.0, offering you better performance and more + granular data about QCS quantum processors. + - Python 3.6 is no longer supported. Python 3.7, 3.8, and 3.9 are supported. + +- `pyquil.compatibility.v2` provides a number of classes which support the pyQuil v2 API, such as + `QuantumComputer` and `get_qc`; `pyquil.compatibility.v2.api` offers `QPU` and `QVM`. These may be + used to incrementally migrate from v2 to v3, but should not be relied on indefinitely, as the + underlying mechanics of these two versions continue to diverge in the future. ### Improvements and Changes @@ -119,6 +127,20 @@ Changelog are passed to the QVM by `WavefunctionSimulator.run_and_measure()` and `WavefunctionSimulator.wavefunction()`. - `noise.estimate_assignment_probs()` now accepts a `QuantumComputer` instead of `QVMConnection`. + +- `QAM` and its subclasses (such as `QPU` and `QVM`) do not store any information specific to the state + of execution requests, and thus are safe to be used concurrently by different requests. `QAM.run` + is now composed of two intermediate calls: + - `QAM.execute` starts execution of the provided executable, returning an opaque handle. + - `QAM.get_results` uses the opaque handle returned by `execute` to retrieve the result values. + + Note that this means that `QAM.load`, `QAM.reset`, and `QAM.wait` no longer exist. + +- `PyQVM.execute` has been renamed to `PyQVM.execute_once` to execute a single program from start + to finish within the context of the existing `PyQVM` state. `PyQVM` is the only stateful `QAM`. + `PyQVM.execute` now implements `QAM.execute` and resets the `PyQVM` state prior to program execution. + +- `QuantumComputer.experiment` has been renamed to `QuantumComputer.run_experiment`. ### Bugfixes From f7fc1110ff25498ada06e99f2f9c71a2733447a1 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:51:35 -0700 Subject: [PATCH 23/61] Docs: update changelog --- CHANGELOG.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c360dc5..53d6dde22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ Changelog `QuantumComputer` and `get_qc`; `pyquil.compatibility.v2.api` offers `QPU` and `QVM`. These may be used to incrementally migrate from v2 to v3, but should not be relied on indefinitely, as the underlying mechanics of these two versions continue to diverge in the future. - ### Improvements and Changes - Added support and documentation for concurrent compilation and execution (see "Advanced Usage" in docs) @@ -32,8 +31,7 @@ Changelog - `ForestConnection` and `ForestSession` have been removed. Connection information is now managed via `api.QCSClientConfiguration` and `api.EngagementManager`. -- `QVMCompiler` now produces a `Program` instead of a `PyQuilExecutableResponse`. As a result, `QVM.load()` only - accepts a `Program`, and `QVM.requires_executable` has been removed. +- `QVMCompiler` now produces a `Program` instead of a `PyQuilExecutableResponse`. - `QPU.get_version_info()` has been removed. @@ -92,7 +90,7 @@ Changelog - `get_qc()` no longer accepts `"9q-generic"` (deprecated). Use `"9q-square"` instead. -- Removed `QAM.read_from_memory_region()` (deprecated). Use `QAM.read_memory()` instead. +- Removed `QAM.read_from_memory_region()` (deprecated). Use `QAMExecutionResult.read_memory()` instead. - Removed `local_qvm()` (deprecated). Use `local_forest_runtime()` instead. @@ -134,7 +132,15 @@ Changelog - `QAM.execute` starts execution of the provided executable, returning an opaque handle. - `QAM.get_results` uses the opaque handle returned by `execute` to retrieve the result values. - Note that this means that `QAM.load`, `QAM.reset`, and `QAM.wait` no longer exist. + These new calls can be used to enqueue multiple programs for execution prior to retrieving + results for any of them. Note that this new pattern means that `QAM.load`, `QAM.reset`, and + `QAM.wait` no longer exist. + +- `QAM.run` no longer accepts a `memory_map` argument. Memory values must be written onto + executable directly with `Program.write_memory()` and `EncryptedProgram.write_memory()` instead. + +- `QuantumComputer`, `QAM`, `QPU`, and `QVM` are now safe to share across threads and processes, + as they no longer store request-related state. - `PyQVM.execute` has been renamed to `PyQVM.execute_once` to execute a single program from start to finish within the context of the existing `PyQVM` state. `PyQVM` is the only stateful `QAM`. From 7f6d46a4894aa44750e780e9cd2fa2abf7a94fbf Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 19:59:20 -0700 Subject: [PATCH 24/61] Fix: remove duplicate Memory definition --- pyquil/_memory.py | 2 +- pyquil/api/__init__.py | 2 -- pyquil/api/_abstract_compiler.py | 13 +++----- pyquil/api/_memory.py | 54 -------------------------------- 4 files changed, 5 insertions(+), 66 deletions(-) delete mode 100644 pyquil/api/_memory.py diff --git a/pyquil/_memory.py b/pyquil/_memory.py index 8397eb817..5d11b25b7 100644 --- a/pyquil/_memory.py +++ b/pyquil/_memory.py @@ -34,7 +34,7 @@ def _write_value( value: Union[int, float, Sequence[int], Sequence[float]], ) -> "Memory": """ - Set the given parameter to the given value. + Mutate the program to set the given parameter value. :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py index 1ede3edf8..db7aa4938 100644 --- a/pyquil/api/__init__.py +++ b/pyquil/api/__init__.py @@ -24,7 +24,6 @@ "get_qc", "list_quantum_computers", "local_forest_runtime", - "Memory", "pyquil_protect", "QAM", "QAMExecutionResult", @@ -44,7 +43,6 @@ from pyquil.api._benchmark import BenchmarkConnection from pyquil.api._compiler import QVMCompiler, QPUCompiler, QuantumExecutable, EncryptedProgram from pyquil.api._engagement_manager import EngagementManager -from pyquil.api._memory import Memory from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qpu import QPU from pyquil.api._quantum_computer import ( diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 3b20a072e..5e63016f3 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -17,23 +17,18 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Sequence, Union -from qcs_api_client.client import QCSClientConfiguration -from rpcq.messages import ( - ParameterSpec, - ParameterAref, - NativeQuilMetadata, -) - +from pyquil._memory import Memory from pyquil._version import pyquil_version from pyquil.api._compiler_client import CompilerClient, CompileToNativeQuilRequest -from pyquil.api._memory import Memory from pyquil.external.rpcq import compiler_isa_to_target_quantum_processor from pyquil.parser import parse_program from pyquil.paulis import PauliTerm from pyquil.quantum_processor import AbstractQuantumProcessor from pyquil.quil import Program -from pyquil.quilatom import MemoryReference, ExpressionDesignator +from pyquil.quilatom import ExpressionDesignator, MemoryReference from pyquil.quilbase import Gate +from qcs_api_client.client import QCSClientConfiguration +from rpcq.messages import NativeQuilMetadata, ParameterAref, ParameterSpec class QuilcVersionMismatch(Exception): diff --git a/pyquil/api/_memory.py b/pyquil/api/_memory.py deleted file mode 100644 index 6c2da7f80..000000000 --- a/pyquil/api/_memory.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Dict, Sequence, Union - -from rpcq.messages import ParameterAref - - -class Memory: - """ - Memory encapsulates the values to be sent as parameters alongside a program at time of - execution, and read back afterwards. - """ - - values: Dict[ParameterAref, Union[int, float]] - - def copy(self) -> "Memory": - """ - Return a deep copy of this Memory object. - """ - return Memory(values={k.replace(): v for k, v in self.values.items()}) - - def write(self, parameter_values: Dict[Union[str, ParameterAref], Union[int, float]]) -> "Memory": - """ - Set the given values for the given parameters. - """ - for parameter, parameter_value in parameter_values.items(): - self._write_value(parameter=parameter, value=parameter_value) - return self - - def _write_value( - self, - *, - parameter: Union[ParameterAref, str], - value: Union[int, float, Sequence[int], Sequence[float]], - ) -> "Memory": - """ - Mutate the program to set the given parameter value. - - :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. - :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list - is provided, parameter must be a ``str`` or ``parameter.offset == 0``. - """ - if isinstance(parameter, str): - parameter = ParameterAref(name=parameter, index=0) - - if isinstance(value, (int, float)): - self.values[parameter] = value - elif isinstance(value, Sequence): - if parameter.index != 0: - raise ValueError("Parameter may not have a non-zero index when its value is a sequence") - - for index, v in enumerate(value): - aref = ParameterAref(name=parameter.name, index=index) - self.values[aref] = v - - return self From eb4f91a7bf3f79d53f7bbc1ae87c73ac54135abf Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 20:02:00 -0700 Subject: [PATCH 25/61] Remove requirements.txt --- requirements.txt | 44 -------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3d65e1389..000000000 --- a/requirements.txt +++ /dev/null @@ -1,44 +0,0 @@ -# runtime deps -numpy==1.19.* # TODO: update to ^1.20 before v3 -scipy -lark -qcs-api-client == 0.6.2.dev1088410470 # TODO: update before v3 -requests -networkx >= 2.0.0 - - -# rigetti packages -rpcq >= 3.6.0 - -# optional latex deps -ipython - -# test deps -black==19.10b0 -coveralls -flake8 -flake8-bugbear==20.1.4 -mypy==0.740 -pytest -pytest-xdist # parallel execution -pytest-cov -pytest-timeout -pytest-rerunfailures -pytest-httpx - -# docs -sphinx>=3.0.0 -sphinx_rtd_theme -sphinx_autodoc_typehints>=1.11.0 -ipykernel -nbsphinx -recommonmark - -# pypi -twine - -# depdendency of contextvars, which we vendor -immutables==0.6 - -# pinning for mypy incompatibility -ruamel.yaml <= 0.16.5 From fd2f3f5d97be226c879c63009a8983e4694d40bd Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 20:02:22 -0700 Subject: [PATCH 26/61] Docs: fix inline docs errors --- pyquil/api/_qpu.py | 4 ++-- pyquil/pyqvm.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 1c0676ecf..ef03359fb 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -268,8 +268,8 @@ def _update_memory_with_recalculation_table(cls, program: EncryptedProgram) -> N .. code-block:: python - compiled_program.memory.write(region_name='theta', value=0.5) - compiled_program.memory.write(region_name='beta', value=0.1) + compiled_program.write_memory(region_name='theta', value=0.5) + compiled_program.write_memory(region_name='beta', value=0.1) After executing this function, our self.variables_shim in the above example would contain the following: diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index cf30e3aa6..53937ba7a 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -474,7 +474,7 @@ def execute_once(self, program: Program) -> "PyQVM": """ Execute one outer loop of a program on the PyQVM without re-initializing its state. - Note that the PyQVM is stateful. Subsequent calls to :py:func:`execute` will not + Note that the PyQVM is stateful. Subsequent calls to :py:func:`execute_once` will not automatically reset the wavefunction or the classical RAM. If this is desired, consider starting your program with ``RESET``. From 60bd2d841528eac71d54fb05deb753fa5d20f1a5 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 20:15:03 -0700 Subject: [PATCH 27/61] Fix: stateful QAM leftovers --- pyquil/api/_qpu.py | 2 +- pyquil/api/_quantum_computer.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index ef03359fb..642da11be 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -165,7 +165,7 @@ def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: id=str(uuid.uuid4()), priority=self.priority, program=executable.program, - patch_values=self._build_patch_values(), + patch_values=self._build_patch_values(executable), ) job_id = self._qpu_client.run_program(request).job_id return QPUExecuteResponse(job_id=job_id) diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index c0873a0e2..543d7e219 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -394,9 +394,6 @@ def compile( :return: An executable binary suitable for passing to :py:func:`QuantumComputer.run`. """ - if isinstance(self.qam, QPU): - self.reset() - flags = [to_native_gates, optimize] assert all(flags) or all(not f for f in flags), "Must turn quilc all on or all off" quilc = all(flags) From 95bf9b413735bbe87e3474d62afc5d187035af52 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:32:28 -0700 Subject: [PATCH 28/61] Fix: address PR feedback --- docs/source/advanced_usage.rst | 4 ++-- docs/source/migration.rst | 19 +++++++------------ pyquil/api/_abstract_compiler.py | 2 +- pyquil/api/_quantum_computer.py | 2 +- pyquil/compatibility/v2/api/__init__.py | 2 ++ .../compatibility/v2/api/_quantum_computer.py | 2 +- pyquil/pyqvm.py | 2 +- pyquil/quil.py | 1 + test/unit/test_compatibility_v2_qvm.py | 1 - 9 files changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index f4d70d736..7d13d66f6 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -56,7 +56,7 @@ Using Multithreading from multiprocessing.pool import ThreadPool from pyquil import get_qc, Program - from pyquil.api import EngagementManager, QCSClientConfiguration + from pyquil.api import QCSClientConfiguration configuration = QCSClientConfiguration.load() qc = get_qc("Aspen-8", client_configuration=configuration) @@ -82,7 +82,7 @@ Using Multiprocessing from multiprocessing.pool import Pool from pyquil import get_qc, Program - from pyquil.api import EngagementManager, QCSClientConfiguration + from pyquil.api import QCSClientConfiguration configuration = QCSClientConfiguration.load() diff --git a/docs/source/migration.rst b/docs/source/migration.rst index cfaaf9755..baf2a3ca6 100644 --- a/docs/source/migration.rst +++ b/docs/source/migration.rst @@ -13,13 +13,8 @@ Authentication -------------- pyQuil v3 relies on an updated authentication model. To get started, install the new `QCS CLI -`_ and -log in with it before using pyQuil v3 with QCS and live QPUs. If the following CLI command succeeds and -returns results, then we expect pyQuil authentication to work as well: - -.. code:: - - qcs api list-quantum-processors +`_ and +log in with it before using pyQuil v3 with QCS and live QPUs. Parameters & Memory @@ -27,7 +22,7 @@ Parameters & Memory In order to give the user more control over and visibility into program execution, especially in parallel, objects such as ``QuantumComputer``, ``QAM``, ``QPU``, and ``QVM`` are no longer stateful -with respect to individual programs. These objects (such as ``QuantumComputer``) are now safe to +with respect to individual programs. These objects are now safe to share among different threads, so you can execute and retrieve results in parallel for even better performance. (See :doc:`advanced_usage` for more information). @@ -53,18 +48,18 @@ This means that you should now execute your programs using one of these options: exe.write_memory(region_name='theta', value=np.pi) # Option 1 - qc.run(exe) + ro_bitstring = qc.run(exe) # Option 2 - qc.qam.run(exe) + result = qc.qam.run(exe) # Option 3 job = qc.qam.execute(exe) - result = qc.qam.get_results(job) + result = qc.qam.get_result(job) # Run our program 10 times, enqueuing all the programs before retrieving results for any of them jobs = [qc.qam.execute(exe) for _ in range(10)] - results = [qc.qam.get_results(job) for job in jobs] + results = [qc.qam.get_result(job) for job in jobs] Compatibility Utilities diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 5e63016f3..682e121e4 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -72,7 +72,7 @@ def write_memory( region_name: str, value: Union[int, float, Sequence[int], Sequence[float]], offset: Optional[int] = None, - ) -> "Program": + ) -> "EncryptedProgram": self._memory._write_value(parameter=ParameterAref(name=region_name, index=offset), value=value) return self diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 543d7e219..82f73983e 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -82,7 +82,7 @@ def __init__( run quantum programs. A quantum computer can be a real Rigetti QPU that uses superconducting transmon - qubits to run quantum programs, or it can be an emulator like the Rigetti QVM with + qubits to run quantum programs, or it can be an emulator like the QVM with noise models and mimicked topologies. :param name: A string identifying this particular quantum computer. diff --git a/pyquil/compatibility/v2/api/__init__.py b/pyquil/compatibility/v2/api/__init__.py index 38c55691b..92f899fd9 100644 --- a/pyquil/compatibility/v2/api/__init__.py +++ b/pyquil/compatibility/v2/api/__init__.py @@ -1 +1,3 @@ +from ._qam import QAM +from ._qpu import QPU from ._qvm import QVM diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index 314706be7..c5b2b4c5e 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -65,7 +65,7 @@ def __init__( run quantum programs. A quantum computer can be a real Rigetti QPU that uses superconducting transmon - qubits to run quantum programs, or it can be an emulator like the Rigetti QVM with + qubits to run quantum programs, or it can be an emulator like the QVM with noise models and mimicked topologies. :param name: A string identifying this particular quantum computer. diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 53937ba7a..9a28c281c 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -241,7 +241,7 @@ def execute(self, executable: QuantumExecutable) -> "PyQVM": self._memory_results = {} self.ram = {} - # self.wf_simulator.reset() + self.wf_simulator.reset() # grab the gate definitions for future use self._extract_defined_gates() diff --git a/pyquil/quil.py b/pyquil/quil.py index e2be25e11..546138c13 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -123,6 +123,7 @@ class Program: """ _memory: Memory + """Contents of memory to be used as program parameters during execution""" def __init__(self, *instructions: InstructionDesignator): self._defined_gates: List[DefGate] = [] diff --git a/test/unit/test_compatibility_v2_qvm.py b/test/unit/test_compatibility_v2_qvm.py index 0c7259e5c..79c756b5e 100644 --- a/test/unit/test_compatibility_v2_qvm.py +++ b/test/unit/test_compatibility_v2_qvm.py @@ -70,7 +70,6 @@ def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientCon assert bitstrings is None -# For backwards compatibility, we support omitting the declaration for "ro" specifically def test_qvm_run_region_not_declared_is_measured_ro(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(X(0), MEASURE(0, MemoryReference("ro"))) From 0b071d73778be44b5e6c07bb65de6b6c5536caf3 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:34:46 -0700 Subject: [PATCH 29/61] No code: address Program thread safety in docs --- docs/source/advanced_usage.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index 7d13d66f6..205ea9aac 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -37,6 +37,8 @@ Concurrency :py:class:`~pyquil.api.QuantumComputer` objects are safe to share between threads or processes, enabling you to execute and retrieve results for multiple programs or parameter values at once. +Note that :py:class`~pyquil.Program` and :py:class`~pyquil.api.EncryptedProgram` are **not** +thread-safe, and should be copied (with ``copy()``) before use in a concurrent context. .. note:: The QVM processes incoming requests in parallel, while a QPU may process them sequentially or in parallel From b9f3d190fcb9838c17bc4f8358284bf67c3102c9 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 15:36:52 -0700 Subject: [PATCH 30/61] No code: fix docs --- docs/source/migration.rst | 2 +- pyquil/_memory.py | 4 ++-- pyquil/compatibility/v2/api/_quantum_computer.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/migration.rst b/docs/source/migration.rst index baf2a3ca6..0d05ef9e4 100644 --- a/docs/source/migration.rst +++ b/docs/source/migration.rst @@ -75,7 +75,7 @@ change any code. from pyquil.compatibility.v2 import get_qc, QuantumComputer from pyquil.compatibility.v2.api import QAM, QVM, QPU -You can use these imported objects exactly how you already use their counterparts in pyQuil v2. +You can use these imported objects similarly how you use their counterparts in pyQuil v2. Once you've verified that your scripts still work with v3, we recommend that you gradually convert them to use the new versions of each object. This compatibility layer won't see any new development, and without fully upgrading you'd miss out on all the new features to come in the diff --git a/pyquil/_memory.py b/pyquil/_memory.py index 5d11b25b7..7231b6832 100644 --- a/pyquil/_memory.py +++ b/pyquil/_memory.py @@ -36,8 +36,8 @@ def _write_value( """ Mutate the program to set the given parameter value. - :param ParameterAref|str parameter: Name of the memory region, or parameter reference with offset. - :param int|float|Sequence[int]|Sequence[float] value: the value or values to set for this parameter. If a list + :param parameter: Name of the memory region, or parameter reference with offset. + :param value: the value or values to set for this parameter. If a list is provided, parameter must be a ``str`` or ``parameter.offset == 0``. """ if isinstance(parameter, str): diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index c5b2b4c5e..a58ee2cd0 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -432,7 +432,8 @@ def get_qc( client_configuration: Optional[QCSClientConfiguration] = None, ) -> QuantumComputer: """ - Compatibility layer to build a QuantumComputer supporting the pyQuil v2 API. + Compatibility layer to build a QuantumComputer supporting an API closely + similar to that in pyQuil v2. """ if connection is not None: raise ValueError( From 17f21a7f767c49787f24eebf6e39605b05409ffb Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 15:45:53 -0700 Subject: [PATCH 31/61] Fix: QVM#get_results return shape --- pyquil/api/_qvm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 6ad61fdc8..641ab455a 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -162,7 +162,10 @@ def get_results(self, execute_response: QVMExecuteResponse) -> QAMExecutionResul Because QVM execution is synchronous, this is a no-op which returns its input. """ - return cast(QAMExecutionResult, execute_response) + return QAMExecutionResult( + executable=execute_response.executable, + memory=execute_response.memory_results + ) def get_version_info(self) -> str: """ From 6aaa270ae3c5f1cce579f4e17cc3ad2b3dbee4bb Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 15:48:25 -0700 Subject: [PATCH 32/61] Update: rename QAM.get_results() to QAM.get_result() --- CHANGELOG.md | 2 +- pyquil/api/_qam.py | 4 ++-- pyquil/api/_qpu.py | 4 ++-- pyquil/api/_qvm.py | 2 +- pyquil/pyqvm.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d6dde22..679bc0214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,7 +130,7 @@ Changelog of execution requests, and thus are safe to be used concurrently by different requests. `QAM.run` is now composed of two intermediate calls: - `QAM.execute` starts execution of the provided executable, returning an opaque handle. - - `QAM.get_results` uses the opaque handle returned by `execute` to retrieve the result values. + - `QAM.get_result` uses the opaque handle returned by `execute` to retrieve the result values. These new calls can be used to enqueue multiple programs for execution prior to retrieving results for any of them. Note that this new pattern means that `QAM.load`, `QAM.reset`, and diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index 3a5d082df..e29b648fc 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -65,7 +65,7 @@ def execute(self, executable: QuantumExecutable) -> ExecuteResponse: """ @abstractmethod - def get_results(self, execute_response: ExecuteResponse) -> QAMExecutionResult: + def get_result(self, execute_response: ExecuteResponse) -> QAMExecutionResult: """ Retrieve the results associated with a previous call to ``QAM#execute``. @@ -76,4 +76,4 @@ def run(self, executable: QuantumExecutable) -> QAMExecutionResult: """ Run an executable to completion on the QAM. """ - return self.get_results(self.execute(executable)) + return self.get_result(self.execute(executable)) diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 642da11be..e3f56da42 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -150,7 +150,7 @@ def quantum_processor_id(self) -> str: def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: """ Enqueue a job for execution on the QPU. Returns a ``QPUExecuteResponse``, a - job descriptor which should be passed directly to ``QPU.get_results`` to retrieve + job descriptor which should be passed directly to ``QPU.get_result`` to retrieve results. """ assert isinstance( @@ -170,7 +170,7 @@ def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: job_id = self._qpu_client.run_program(request).job_id return QPUExecuteResponse(job_id=job_id) - def get_results(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult: + def get_result(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult: """ Retrieve results from execution on the QPU. """ diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 641ab455a..0a8ac425d 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -156,7 +156,7 @@ def execute(self, executable: QuantumExecutable) -> QVMExecuteResponse: return QAMExecutionResult(executable=executable, memory=result_memory) - def get_results(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult: + def get_result(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult: """ Return the results of execution on the QVM. diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 9a28c281c..924e5704a 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -260,7 +260,7 @@ def execute(self, executable: QuantumExecutable) -> "PyQVM": return self - def get_results(self, execute_response: "PyQVM") -> QAMExecutionResult: + def get_result(self, execute_response: "PyQVM") -> QAMExecutionResult: """ Return results from the PyQVM according to the common QAM API. Note that while the ``execute_response`` is not used, it's accepted in order to conform to that API; it's From 8124327a4aedafaa100fc51f4676a6a64d850871 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 16:04:16 -0700 Subject: [PATCH 33/61] Fix: QVMExecuteResponse --- pyquil/api/_qvm.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 0a8ac425d..4944015c7 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -13,7 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -from typing import Dict, Mapping, Optional, Sequence, Union, Tuple, cast +from dataclasses import dataclass +from typing import Dict, Mapping, Optional, Sequence, Union, Tuple import numpy as np from qcs_api_client.client import QCSClientConfiguration @@ -50,13 +51,13 @@ def check_qvm_version(version: str) -> None: "Must use QVM >= 1.8.0 with pyquil >= 2.8.0, but you " f"have QVM {version} and pyquil {pyquil_version}" ) - +@dataclass class QVMExecuteResponse: executable: Program - memory_results: Dict[str, np.ndarray] + memory: Dict[str, np.ndarray] -class QVM(QAM[QAMExecutionResult]): +class QVM(QAM[QVMExecuteResponse]): def __init__( self, noise_model: Optional[NoiseModel] = None, @@ -154,7 +155,7 @@ def execute(self, executable: QuantumExecutable) -> QVMExecuteResponse: ram = {key: np.array(val) for key, val in response.results.items()} result_memory.update(ram) - return QAMExecutionResult(executable=executable, memory=result_memory) + return QVMExecuteResponse(executable=executable, memory=result_memory) def get_result(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult: """ @@ -164,7 +165,7 @@ def get_result(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult """ return QAMExecutionResult( executable=execute_response.executable, - memory=execute_response.memory_results + memory=execute_response.memory ) def get_version_info(self) -> str: From 1c82890c654821dcf8af0f95502dd1f28885f135 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 16:04:58 -0700 Subject: [PATCH 34/61] Fix: rename generic ExecuteResponse --- pyquil/api/_qam.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index e29b648fc..b0f3bb186 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -26,7 +26,7 @@ class QAMError(RuntimeError): pass -ExecuteResponse = TypeVar("ExecuteResponse") +T = TypeVar("T") """A generic parameter describing the opaque job handle returned from QAM#execute and subclasses.""" @@ -46,7 +46,7 @@ def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: return self.memory.get(region_name) -class QAM(ABC, Generic[ExecuteResponse]): +class QAM(ABC, Generic[T]): """ Quantum Abstract Machine: This class acts as a generic interface describing how a classical computer interacts with a live quantum computer. @@ -56,7 +56,7 @@ def __init__(self) -> None: self.experiment: Optional[Experiment] @abstractmethod - def execute(self, executable: QuantumExecutable) -> ExecuteResponse: + def execute(self, executable: QuantumExecutable) -> T: """ Run an executable on a QAM, returning a handle to be used to retrieve results. @@ -65,7 +65,7 @@ def execute(self, executable: QuantumExecutable) -> ExecuteResponse: """ @abstractmethod - def get_result(self, execute_response: ExecuteResponse) -> QAMExecutionResult: + def get_result(self, execute_response: T) -> QAMExecutionResult: """ Retrieve the results associated with a previous call to ``QAM#execute``. From 85886a388edb0b026fab92555aff12e5918ebdfe Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 16:48:34 -0700 Subject: [PATCH 35/61] Breaking: return execution results data as readout_data, not memory --- pyquil/api/_qam.py | 13 +- pyquil/api/_quantum_computer.py | 29 ++-- pyquil/api/_qvm.py | 6 +- pyquil/api/_wavefunction_simulator.py | 4 - pyquil/compatibility/v2/api/_qam.py | 5 +- .../compatibility/v2/api/_quantum_computer.py | 145 +++++++++++++++++- pyquil/pyqvm.py | 2 +- .../test_compatibility_v2_quantum_computer.py | 7 +- test/unit/test_quantum_computer.py | 35 +++-- test/unit/test_qvm.py | 14 +- test/unit/test_reference_density.py | 4 +- 11 files changed, 191 insertions(+), 73 deletions(-) diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index b0f3bb186..19a6b44ae 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -33,17 +33,10 @@ class QAMError(RuntimeError): @dataclass class QAMExecutionResult: executable: QuantumExecutable - memory: Dict[str, Optional[np.ndarray]] = field(default_factory=dict) + """The executable corresponding to this result.""" - def read_memory(self, *, region_name: str) -> Optional[np.ndarray]: - """ - Reads from a memory region named ``region_name`` returned from the QAM. - - :param region_name: The string naming the declared memory region. - :return: A list of values of the appropriate type. - """ - assert self.memory is not None, "No memory results available" - return self.memory.get(region_name) + readout_data: Dict[str, Optional[np.ndarray]] = field(default_factory=dict) + """Readout data returned from the QAM, keyed on the name of the readout register or post-processing node.""" class QAM(ABC, Generic[T]): diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 82f73983e..6a71a4a01 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -43,7 +43,7 @@ from pyquil.api._abstract_compiler import AbstractCompiler, QuantumExecutable from pyquil.api._compiler import QPUCompiler, QVMCompiler -from pyquil.api._qam import QAM +from pyquil.api._qam import QAM, QAMExecutionResult from pyquil.api._qcs_client import qcs_client from pyquil.api._qpu import QPU from pyquil.api._qvm import QVM @@ -132,22 +132,15 @@ def to_compiler_isa(self) -> CompilerISA: def run( self, executable: QuantumExecutable, - ) -> np.ndarray: + ) -> QAMExecutionResult: """ Run a quil executable. All parameters in the executable must have values applied using - ``Program#with_parameter_values`` or ``Program#set_parameter_value``. + ``Program#write_memory``. - :param executable: The program to run, compiled as needed for its target QAM. - :return: A numpy array of shape (trials, len(ro-register)) that contains 0s and 1s. + :param executable: The program to run, previously compiled as needed for its target QAM. + :return: execution result including readout data. """ - result = self.qam.run(executable) - - # QUESTION (for @ameyer) - this is consistent with the V2 API, but in that API, - # QuantumComputer#run and QAM#run return different shapes. - # - # Does it make sense to align those now and return a QAMExecutionResult, given that - # it's a breaking change, or is it best to leave this as-is for ease/convenience? - return result.read_memory(region_name="ro") + return self.qam.run(executable) def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: """ @@ -232,12 +225,11 @@ def run_experiment( merged_memory_maps = merge_memory_map_lists([experiment_setting_memory_map], symmetrization_memory_maps) all_bitstrings = [] - # TODO: accomplish symmetrization via batch endpoint for merged_memory_map in merged_memory_maps: final_memory_map = {**memory_map, **merged_memory_map} executable_copy = executable.copy() executable_copy._memory.write(final_memory_map) - bitstrings = self.run(executable_copy) + bitstrings = self.run(executable_copy).readout_data.get("ro") if "symmetrization" in final_memory_map: bitmask = np.array(np.array(final_memory_map["symmetrization"]) / np.pi, dtype=int) @@ -1113,9 +1105,10 @@ def _measure_bitstrings( prog.wrap_in_numshots_loop(num_shots) prog = qc.compiler.quil_to_native_quil(prog) - exe = qc.compiler.native_quil_to_executable(prog) - shots = qc.run(exe) - results.append(shots) + executable = qc.compiler.native_quil_to_executable(prog) + result = qc.run(executable) + shot_values = result.readout_data.get("ro") + results.append(shot_values) return results diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 4944015c7..217f79044 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -51,6 +51,7 @@ def check_qvm_version(version: str) -> None: "Must use QVM >= 1.8.0 with pyquil >= 2.8.0, but you " f"have QVM {version} and pyquil {pyquil_version}" ) + @dataclass class QVMExecuteResponse: executable: Program @@ -163,10 +164,7 @@ def get_result(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult Because QVM execution is synchronous, this is a no-op which returns its input. """ - return QAMExecutionResult( - executable=execute_response.executable, - memory=execute_response.memory - ) + return QAMExecutionResult(executable=execute_response.executable, readout_data=execute_response.memory) def get_version_info(self) -> str: """ diff --git a/pyquil/api/_wavefunction_simulator.py b/pyquil/api/_wavefunction_simulator.py index ceff287e4..cd6190fca 100644 --- a/pyquil/api/_wavefunction_simulator.py +++ b/pyquil/api/_wavefunction_simulator.py @@ -38,7 +38,6 @@ class WavefunctionSimulator: - def __init__( self, *, @@ -76,7 +75,6 @@ def __init__( client_configuration = client_configuration or QCSClientConfiguration.load() self._qvm_client = QVMClient(client_configuration=client_configuration, request_timeout=timeout) - def wavefunction( self, quil_program: Program, memory_map: Optional[Dict[str, List[Union[int, float]]]] = None ) -> Wavefunction: @@ -106,7 +104,6 @@ def wavefunction( response = self._qvm_client.get_wavefunction(request) return Wavefunction.from_bit_packed_string(response.wavefunction) - def expectation( self, prep_prog: Program, @@ -170,7 +167,6 @@ def _expectation(self, prep_prog: Program, operator_programs: Iterable[Program]) response = self._qvm_client.measure_expectation(request) return np.asarray(response.expectations) - def run_and_measure( self, quil_program: Program, diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index c159c9d2f..74fb4975f 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -1,4 +1,5 @@ from typing import Optional, Sequence, Union +import numpy as np from rpcq.messages import ParameterAref from pyquil.api._qam import QAM, QAMExecutionResult, QuantumExecutable @@ -22,9 +23,9 @@ def load(self, executable: QuantumExecutable) -> "QAM": self._loaded_executable = executable.copy() return self - def read_memory(self, region_name: str) -> "QAM": + def read_memory(self, region_name: str) -> np.ndarray: assert self._result is not None, "QAM#run must be called before QAM#read_memory" - return self._result.read_memory(region_name=region_name) + return self._result.readout_data.get(region_name) def reset(self) -> "QAM": self._loaded_executable = None diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index a58ee2cd0..597f07161 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -14,12 +14,17 @@ # limitations under the License. ############################################################################## import itertools +from pyquil.pyqvm import PyQVM import warnings from math import log, pi from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Union, cast +import networkx as nx import numpy as np -from pyquil.api._compiler import AbstractCompiler +from qcs_api_client.client import QCSClientConfiguration +from rpcq.messages import PyQuilExecutableResponse, QuiltBinaryExecutableResponse + +from pyquil.api._compiler import AbstractCompiler, QVMCompiler from pyquil.api._qam import QAM from pyquil.api._qpu import QPU from pyquil.api._quantum_computer import QuantumComputer as QuantumComputerV3 @@ -30,11 +35,12 @@ from pyquil.experiment._result import ExperimentResult, bitstrings_to_expectations from pyquil.experiment._setting import ExperimentSetting from pyquil.gates import MEASURE, RX +from pyquil.noise import NoiseModel, decoherence_noise_with_asymmetric_ro from pyquil.paulis import PauliTerm +from pyquil.quantum_processor import AbstractQuantumProcessor, NxQuantumProcessor +from pyquil.quantum_processor.graph import NxQuantumProcessor from pyquil.quil import Program, validate_supported_quil from pyquil.quilatom import qubit_index -from qcs_api_client.client import QCSClientConfiguration -from rpcq.messages import PyQuilExecutableResponse, QuiltBinaryExecutableResponse from ._qam import StatefulQAM @@ -173,7 +179,6 @@ def experiment( results = [] for settings in experiment: - # TODO: add support for grouped ExperimentSettings if len(settings) > 1: raise ValueError("settings must be of length 1") setting = settings[0] @@ -184,7 +189,6 @@ def experiment( merged_memory_maps = merge_memory_map_lists([experiment_setting_memory_map], symmetrization_memory_maps) all_bitstrings = [] - # TODO: accomplish symmetrization via batch endpoint for merged_memory_map in merged_memory_maps: final_memory_map = {**memory_map, **merged_memory_map} self.qam.reset() @@ -775,3 +779,134 @@ def _f(x: int) -> int: trials = min_num_trials warnings.warn(f"Number of trials was too low, it is now {trials}.") return trials + + +def _get_qvm_or_pyqvm( + *, + client_configuration: QCSClientConfiguration, + qvm_type: str, + noise_model: Optional[NoiseModel], + quantum_processor: Optional[AbstractQuantumProcessor], + execution_timeout: float, +) -> Union[QVM, PyQVM]: + if qvm_type == "qvm": + return QVM(noise_model=noise_model, timeout=execution_timeout, client_configuration=client_configuration) + elif qvm_type == "pyqvm": + assert quantum_processor is not None + return PyQVM(n_qubits=quantum_processor.qubit_topology().number_of_nodes()) + + raise ValueError("Unknown qvm type {}".format(qvm_type)) + + +def _get_qvm_qc( + *, + client_configuration: QCSClientConfiguration, + name: str, + qvm_type: str, + quantum_processor: AbstractQuantumProcessor, + compiler_timeout: float, + execution_timeout: float, + noise_model: Optional[NoiseModel], +) -> QuantumComputer: + """Construct a QuantumComputer backed by a QVM. + + This is a minimal wrapper over the QuantumComputer, QVM, and QVMCompiler constructors. + + :param client_configuration: Client configuration. + :param name: A string identifying this particular quantum computer. + :param qvm_type: The type of QVM. Either qvm or pyqvm. + :param quantum_processor: A quantum_processor following the AbstractQuantumProcessor interface. + :param noise_model: An optional noise model + :param compiler_timeout: Time limit for compilation requests, in seconds. + :param execution_timeout: Time limit for execution requests, in seconds. + :return: A QuantumComputer backed by a QVM with the above options. + """ + + return QuantumComputer( + name=name, + qam=_get_qvm_or_pyqvm( + client_configuration=client_configuration, + qvm_type=qvm_type, + noise_model=noise_model, + quantum_processor=quantum_processor, + execution_timeout=execution_timeout, + ), + compiler=QVMCompiler( + quantum_processor=quantum_processor, + timeout=compiler_timeout, + client_configuration=client_configuration, + ), + ) + + +def _get_qvm_with_topology( + *, + client_configuration: QCSClientConfiguration, + name: str, + topology: nx.Graph, + noisy: bool, + qvm_type: str, + compiler_timeout: float, + execution_timeout: float, +) -> QuantumComputer: + """Construct a QVM with the provided topology. + + :param client_configuration: Client configuration. + :param name: A name for your quantum computer. This field does not affect behavior of the + constructed QuantumComputer. + :param topology: A graph representing the desired qubit connectivity. + :param noisy: Whether to include a generic noise model. If you want more control over + the noise model, please construct your own :py:class:`NoiseModel` and use + :py:func:`_get_qvm_qc` instead of this function. + :param qvm_type: The type of QVM. Either 'qvm' or 'pyqvm'. + :param compiler_timeout: Time limit for compilation requests, in seconds. + :param execution_timeout: Time limit for execution requests, in seconds. + :return: A pre-configured QuantumComputer + """ + # Note to developers: consider making this function public and advertising it. + quantum_processor = NxQuantumProcessor(topology=topology) + if noisy: + noise_model: Optional[NoiseModel] = decoherence_noise_with_asymmetric_ro( + isa=quantum_processor.to_compiler_isa() + ) + else: + noise_model = None + return _get_qvm_qc( + client_configuration=client_configuration, + name=name, + qvm_type=qvm_type, + quantum_processor=quantum_processor, + noise_model=noise_model, + compiler_timeout=compiler_timeout, + execution_timeout=execution_timeout, + ) + + +def _measure_bitstrings( + qc: QuantumComputer, programs: List[Program], meas_qubits: List[int], num_shots: int = 600 +) -> List[np.ndarray]: + """ + Wrapper for appending measure instructions onto each program, running the program, + and accumulating the resulting bitarrays. + + :param qc: a quantum computer object on which to run each program + :param programs: a list of programs to run + :param meas_qubits: groups of qubits to measure for each program + :param num_shots: the number of shots to run for each program + :return: a len(programs) long list of num_shots by num_meas_qubits bit arrays of results for + each program. + """ + results = [] + for program in programs: + # copy the program so the original is not mutated + prog = program.copy() + ro = prog.declare("ro", "BIT", len(meas_qubits)) + for idx, q in enumerate(meas_qubits): + prog += MEASURE(q, ro[idx]) + + prog.wrap_in_numshots_loop(num_shots) + prog = qc.compiler.quil_to_native_quil(prog) + executable = qc.compiler.native_quil_to_executable(prog) + shots = qc.run(executable) + results.append(shots) + return results diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 924e5704a..70f23e1e2 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -267,7 +267,7 @@ def get_result(self, execute_response: "PyQVM") -> QAMExecutionResult: unused because the PyQVM, unlike other QAM's, is itself stateful. """ return QAMExecutionResult( - executable=self.program.copy(), memory={k: v for k, v in self._memory_results.items()} + executable=self.program.copy(), readout_data={k: v for k, v in self._memory_results.items()} ) def read_memory(self, *, region_name: str) -> np.ndarray: diff --git a/test/unit/test_compatibility_v2_quantum_computer.py b/test/unit/test_compatibility_v2_quantum_computer.py index 0b936391b..caea70dfa 100644 --- a/test/unit/test_compatibility_v2_quantum_computer.py +++ b/test/unit/test_compatibility_v2_quantum_computer.py @@ -15,12 +15,11 @@ _construct_strength_three_orthogonal_array, _construct_strength_two_orthogonal_array, _flip_array_to_prog, - _get_qvm_with_topology, - _measure_bitstrings, _parse_name, _symmetrization, ) from pyquil.compatibility.v2 import QuantumComputer, get_qc +from pyquil.compatibility.v2.api._quantum_computer import _get_qvm_with_topology, _measure_bitstrings from pyquil.compatibility.v2.api import QVM from pyquil.experiment import ExperimentSetting, Experiment from pyquil.experiment._main import _pauli_to_product_state @@ -493,8 +492,8 @@ def test_reset(client_configuration: QCSClientConfiguration): aref = ParameterAref(name="theta", index=0) assert qc.qam._loaded_executable._memory.values[aref] == np.pi - assert qc.qam._result.memory["ro"].shape == (10, 1) - assert all([bit == 1 for bit in qc.qam._result.memory["ro"]]) + assert qc.qam._result.readout_data["ro"].shape == (10, 1) + assert all([bit == 1 for bit in qc.qam._result.readout_data["ro"]]) qc.reset() diff --git a/test/unit/test_quantum_computer.py b/test/unit/test_quantum_computer.py index 9b5c2ae08..0f778a641 100644 --- a/test/unit/test_quantum_computer.py +++ b/test/unit/test_quantum_computer.py @@ -191,7 +191,7 @@ def test_run(client_configuration: QCSClientConfiguration): qam=QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)), compiler=DummyCompiler(quantum_processor=quantum_processor, client_configuration=client_configuration), ) - bitstrings = qc.run( + result = qc.run( Program( Declare("ro", "BIT", 3), H(0), @@ -202,6 +202,7 @@ def test_run(client_configuration: QCSClientConfiguration): MEASURE(2, MemoryReference("ro", 2)), ).wrap_in_numshots_loop(1000) ) + bitstrings = result.readout_data.get('ro') assert bitstrings.shape == (1000, 3) parity = np.sum(bitstrings, axis=1) % 3 @@ -219,7 +220,8 @@ def test_run_pyqvm_noiseless(client_configuration: QCSClientConfiguration): ro = prog.declare("ro", "BIT", 3) for q in range(3): prog += MEASURE(q, ro[q]) - bitstrings = qc.run(prog.wrap_in_numshots_loop(1000)) + result = qc.run(prog.wrap_in_numshots_loop(1000)) + bitstrings = result.readout_data.get('ro') assert bitstrings.shape == (1000, 3) parity = np.sum(bitstrings, axis=1) % 3 @@ -237,7 +239,8 @@ def test_run_pyqvm_noisy(client_configuration: QCSClientConfiguration): ro = prog.declare("ro", "BIT", 3) for q in range(3): prog += MEASURE(q, ro[q]) - bitstrings = qc.run(prog.wrap_in_numshots_loop(1000)) + result = qc.run(prog.wrap_in_numshots_loop(1000)) + bitstrings = result.readout_data.get('ro') assert bitstrings.shape == (1000, 3) parity = np.sum(bitstrings, axis=1) % 3 @@ -262,9 +265,10 @@ def test_readout_symmetrization(client_configuration: QCSClientConfiguration): ) prog.wrap_in_numshots_loop(1000) - bs1 = qc.run(prog) - avg0_us = np.mean(bs1[:, 0]) - avg1_us = 1 - np.mean(bs1[:, 1]) + result_1 = qc.run(prog) + bitstrings_1 = result_1.readout_data.get('ro') + avg0_us = np.mean(bitstrings_1[:, 0]) + avg1_us = 1 - np.mean(bitstrings_1[:, 1]) diff_us = avg1_us - avg0_us assert diff_us > 0.03 @@ -272,9 +276,9 @@ def test_readout_symmetrization(client_configuration: QCSClientConfiguration): I(0), X(1), ) - bs2 = qc.run_symmetrized_readout(prog, 1000) - avg0_s = np.mean(bs2[:, 0]) - avg1_s = 1 - np.mean(bs2[:, 1]) + bitstrings_2 = qc.run_symmetrized_readout(prog, 1000) + avg0_s = np.mean(bitstrings_2[:, 0]) + avg1_s = 1 - np.mean(bitstrings_2[:, 1]) diff_s = avg1_s - avg0_s assert diff_s < 0.05 @@ -293,7 +297,6 @@ def test_run_symmetrized_readout_error(client_configuration: QCSClientConfigurat def test_list_qc(): qc_names = list_quantum_computers(qpus=False) - # TODO: update with deployed qpus assert qc_names == ["9q-square-qvm", "9q-square-noisy-qvm"] @@ -419,7 +422,7 @@ def test_qc_run(client_configuration: QCSClientConfiguration): MEASURE(0, ("ro", 0)), ).wrap_in_numshots_loop(3) ) - ) + ).readout_data.get('ro') assert bs.shape == (3, 1) @@ -468,7 +471,7 @@ def test_run_with_parameters(client_configuration: QCSClientConfiguration): ).wrap_in_numshots_loop(1000) executable.write_memory(region_name="theta", value=np.pi) - bitstrings = qc.run(executable) + bitstrings = qc.run(executable).readout_data.get('ro') assert bitstrings.shape == (1000, 1) assert all([bit == 1 for bit in bitstrings]) @@ -492,8 +495,8 @@ def test_reset(client_configuration: QCSClientConfiguration): aref = ParameterAref(name="theta", index=0) assert p._memory.values[aref] == np.pi - assert result.memory["ro"].shape == (10, 1) - assert all([bit == 1 for bit in result.memory["ro"]]) + assert result.readout_data["ro"].shape == (10, 1) + assert all([bit == 1 for bit in result.readout_data["ro"]]) def test_get_qvm_with_topology(client_configuration: QCSClientConfiguration): @@ -533,7 +536,7 @@ def test_get_qvm_with_topology_2(client_configuration: QCSClientConfiguration): MEASURE(7, ("ro", 2)), ).wrap_in_numshots_loop(5) ) - ) + ).readout_data.get('ro') assert results.shape == (5, 3) assert all(r[0] == 1 for r in results) @@ -552,7 +555,7 @@ def test_noisy(client_configuration: QCSClientConfiguration): MEASURE(0, ("ro", 0)), ).wrap_in_numshots_loop(10000) qc = get_qc("1q-qvm", noisy=True, client_configuration=client_configuration) - result = qc.run(qc.compile(p)) + result = qc.run(qc.compile(p)).readout_data.get('ro') assert result.mean() < 1.0 diff --git a/test/unit/test_qvm.py b/test/unit/test_qvm.py index ff521f00d..49f3b8170 100644 --- a/test/unit/test_qvm.py +++ b/test/unit/test_qvm.py @@ -14,7 +14,7 @@ def test_qvm__default_client(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) result = qvm.run(p.wrap_in_numshots_loop(1000)) - bitstrings = result.read_memory(region_name="ro") + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 1) @@ -22,7 +22,7 @@ def test_qvm_run_pqer(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) result = qvm.run(p.wrap_in_numshots_loop(1000)) - bitstrings = result.read_memory(region_name="ro") + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 1) assert np.mean(bitstrings) > 0.8 @@ -31,7 +31,7 @@ def test_qvm_run_just_program(client_configuration: QCSClientConfiguration): qvm = QVM(client_configuration=client_configuration, gate_noise=(0.01, 0.01, 0.01)) p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) result = qvm.run(p.wrap_in_numshots_loop(1000)) - bitstrings = result.read_memory(region_name="ro") + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 1) assert np.mean(bitstrings) > 0.8 @@ -41,7 +41,7 @@ def test_qvm_run_only_pqer(client_configuration: QCSClientConfiguration): p = Program(Declare("ro", "BIT"), X(0), MEASURE(0, MemoryReference("ro"))) result = qvm.run(p.wrap_in_numshots_loop(1000)) - bitstrings = result.read_memory(region_name="ro") + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 1) assert np.mean(bitstrings) > 0.8 @@ -50,7 +50,7 @@ def test_qvm_run_region_declared_and_measured(client_configuration: QCSClientCon qvm = QVM(client_configuration=client_configuration) p = Program(Declare("reg", "BIT"), X(0), MEASURE(0, MemoryReference("reg"))) result = qvm.run(p.wrap_in_numshots_loop(100)) - bitstrings = result.read_memory(region_name="reg") + bitstrings = result.readout_data.get("reg") assert bitstrings.shape == (100, 1) @@ -58,7 +58,7 @@ def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientCon qvm = QVM(client_configuration=client_configuration) p = Program(Declare("reg", "BIT"), X(0)) result = qvm.run(p.wrap_in_numshots_loop(100)) - bitstrings = result.read_memory(region_name="reg") + bitstrings = result.readout_data.get("reg") assert bitstrings.shape == (100, 0) @@ -74,7 +74,7 @@ def test_qvm_run_region_not_declared_not_measured(client_configuration: QCSClien qvm = QVM(client_configuration=client_configuration) p = Program(X(0)) result = qvm.run(p.wrap_in_numshots_loop(100)) - assert result.read_memory(region_name="ro") is None + assert result.readout_data.get("ro") is None def test_qvm_version(client_configuration: QCSClientConfiguration): diff --git a/test/unit/test_reference_density.py b/test/unit/test_reference_density.py index 974690b21..61a8f45bd 100644 --- a/test/unit/test_reference_density.py +++ b/test/unit/test_reference_density.py @@ -373,7 +373,7 @@ def test_set_initial_state(client_configuration: QCSClientConfiguration): qc_density.qam.wf_simulator.set_initial_state(rho1).reset() - out = [qc_density.run(prog) for _ in range(0, 4)] + out = [qc_density.run(prog).readout_data['ro'] for _ in range(0, 4)] ans = [np.array([[1]]), np.array([[1]]), np.array([[1]]), np.array([[1]])] assert all([np.allclose(x, y) for x, y in zip(out, ans)]) @@ -382,7 +382,7 @@ def test_set_initial_state(client_configuration: QCSClientConfiguration): results = qc_density.run(qc_density.compile(progRAM)) ans = {0: np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])} - assert np.allclose(results[0], ans[0]) + assert np.allclose(results.readout_data['ro'][0], ans[0]) # test reverting ReferenceDensitySimulator to the default state rho0 = np.array([[1.0, 0.0], [0.0, 0.0]]) From 78220296672b556fa0ef83085bc10a4af0852f0a Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 17:22:22 -0700 Subject: [PATCH 36/61] No code: update docs to reflect readout data --- CHANGELOG.md | 10 +++++++++- docs/source/advanced_usage.rst | 4 ++-- docs/source/basics.rst | 3 ++- docs/source/intro.rst | 12 ++++++------ docs/source/migration.rst | 19 ++++++++++++++++++- docs/source/noise.rst | 16 ++++++++-------- docs/source/qvm.rst | 3 ++- docs/source/start.rst | 4 ++-- 8 files changed, 49 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679bc0214..5a88ce3fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,7 +147,15 @@ Changelog `PyQVM.execute` now implements `QAM.execute` and resets the `PyQVM` state prior to program execution. - `QuantumComputer.experiment` has been renamed to `QuantumComputer.run_experiment`. - + +- Results returned from execution are now referred to as `readout_data` rather than `memory`, reflecting the reality + that the memory of the QAM is not currently exposed to the user. The exception to this rule is the stateful `PyQVM`, + whose state is maintained within the pyQuil process and whose memory _may truly be inspected._ For that, + `PyQVM.read_memory` remains available. + +- `QuantumComputer.run` now returns a `QAMExecutionResult` rather than the readout data from the `ro` readout + source. To access those same readout results, use `qc.run().readout_data.get('ro')`. This allows access to other + execution-related information and other readout sources. ### Bugfixes [v2.28.1](https://github.com/rigetti/pyquil/compare/v2.28.0..v2.28.1) (May 5, 2021) diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index 205ea9aac..7b84d5de6 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -65,7 +65,7 @@ Using Multithreading def run(program: Program): - return qc.run(qc.compile(program)) + return qc.run(qc.compile(program)).readout_data.get("ro") programs = [Program("DECLARE ro BIT", "RX(pi) 0", "MEASURE 0 ro").wrap_in_numshots_loop(10)] * 20 @@ -92,7 +92,7 @@ Using Multiprocessing def run(program: Program): - return qc.run(qc.compile(program)) + return qc.run(qc.compile(program)).readout_data.get("ro") programs = [Program("DECLARE ro BIT", "RX(pi) 0", "MEASURE 0 ro").wrap_in_numshots_loop(10)] * 20 diff --git a/docs/source/basics.rst b/docs/source/basics.rst index cee798380..921ce7311 100644 --- a/docs/source/basics.rst +++ b/docs/source/basics.rst @@ -80,7 +80,8 @@ program on the Quantum Virtual Machine (QVM). We just have to add a few lines to qc = get_qc('1q-qvm') # You can make any 'nq-qvm' this way for any reasonable 'n' executable = qc.compile(p) result = qc.run(executable) - print(result) + bitstrings = result.readout_data.get('ro') + print(bitstrings) Congratulations! You just ran your program on the QVM. The returned value should be: diff --git a/docs/source/intro.rst b/docs/source/intro.rst index e4cca04e7..9709224bc 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -597,7 +597,7 @@ measurements. This functionality is emulated by :py:func:`QuantumComputer.run`. qc = get_qc('9q-square-qvm') - print (qc.run(qc.compile(p))) + print (qc.run(qc.compile(p)).readout_data.get("ro")) .. parsed-literal:: @@ -615,7 +615,7 @@ qubit before measurement then we obtain: p += Program(X(0)) # Flip the qubit p.measure(0, classical_register[0]) # Measure the qubit - print (qc.run(qc.compile(p))) + print (qc.run(qc.compile(p)).readout_data.get("ro")) .. parsed-literal:: @@ -636,7 +636,7 @@ times then we always get the same outcome: trials = 10 p.wrap_in_numshots_loop(shots=trials) - print (qc.run(qc.compile(p))) + print (qc.run(qc.compile(p)).readout_data.get("ro")) .. parsed-literal:: @@ -705,7 +705,7 @@ extra power over regular bits. p.wrap_in_numshots_loop(shots=10) # We see probabilistic results of about half 1's and half 0's - print (qc.run(qc.compile(p))) + print (qc.run(qc.compile(p)).readout_data.get("ro")) .. parsed-literal:: @@ -799,7 +799,7 @@ quantum operations to run. p.measure(7, ro[7]) # Run and check register [7] - print (qc.run(qc.compile(p))) + print (qc.run(qc.compile(p)).readout_data.get("ro")) .. parsed-literal:: @@ -824,7 +824,7 @@ increasing chance of halting, but that may run forever! p.inst(X(0)).while_do(ro[0], inside_loop) qc = get_qc('9q-square-qvm') - print (qc.run(qc.compile(p))) + print (qc.run(qc.compile(p)).readout_data.get("ro")) .. parsed-literal:: diff --git a/docs/source/migration.rst b/docs/source/migration.rst index 0d05ef9e4..4b93bde71 100644 --- a/docs/source/migration.rst +++ b/docs/source/migration.rst @@ -48,7 +48,7 @@ This means that you should now execute your programs using one of these options: exe.write_memory(region_name='theta', value=np.pi) # Option 1 - ro_bitstring = qc.run(exe) + result = qc.run(exe) # Option 2 result = qc.qam.run(exe) @@ -61,6 +61,23 @@ This means that you should now execute your programs using one of these options: jobs = [qc.qam.execute(exe) for _ in range(10)] results = [qc.qam.get_result(job) for job in jobs] +Additionally, ``QuantumComputer.run()`` and ``QAM.run()`` now both return ``QAMExecutionResult``, rather +than the bitstrings which were read out following execution. So, to retrieve the bitstrings from region ``ro`` +like you would have in pyQuil v2, query the readout data: + +.. code:: python + + result = qc.run(exe) + bitstrings = result.readout_data.get('ro') + +Note that there is no "memory" in the result shape -- yet. Readout data is inherently collected and returned +differently than memory values are, and that nuance is no longer masked in pyQuil v3. All of the results +that, in pyQuil v2, you would have expected to retrieve using ``QAM.read_memory()`` are now present in +``QAMExecutionResult.readout_data``. + +The notable exception to this is ``PyQVM``, which remains stateful, and whose memory you can still inspect using +``PyQVM.read_memory()``. + Compatibility Utilities ----------------------- diff --git a/docs/source/noise.rst b/docs/source/noise.rst index 611d84d0a..03c0c440c 100644 --- a/docs/source/noise.rst +++ b/docs/source/noise.rst @@ -381,7 +381,7 @@ state decays to the :math:`\ket{0}` state. p.define_noisy_gate("I", [0], append_damping_to_gate(np.eye(2), damping_per_I)) p.wrap_in_numshots_loop(trials) qc.qam.random_seed = int(num_I) - res = qc.run(p) + res = qc.run(p).readout_data.get("ro") results_damping.append([np.mean(res), np.std(res) / np.sqrt(trials)]) results_damping = np.array(results_damping) @@ -541,7 +541,7 @@ good starting point.** p.define_noisy_gate("CZ", [0, 1], corrupted_CZ) p.wrap_in_numshots_loop(trials) qc.qam.random_seed = jj - res = qc.run(p) + res = qc.run(p).readout_data.get("ro") results.append(res) results = np.array(results) @@ -710,7 +710,7 @@ gate noise, respectively. MEASURE(0, ("ro", 0)), MEASURE(1, ("ro", 1)), ]) - bitstrings = qc.run(noisy) + bitstrings = qc.run(noisy).readout_data.get("ro") # Expectation of Z0 and Z1 z0, z1 = 1 - 2*np.mean(bitstrings, axis=0) @@ -1008,7 +1008,7 @@ Example 1: Rabi Sequence with Noisy Readout p.define_noisy_readout(0, p00=p00, p11=p00) ro = p.declare("ro", "BIT", 1) p.measure(0, ro[0]) - res = qc.run(p) + res = qc.run(p).readout_data.get("ro") results_rabi[jj, kk] = np.sum(res) .. parsed-literal:: @@ -1134,7 +1134,7 @@ Pauli-Z moments that indicate the qubit correlations are corrupted (and correcte ) ghz_prog.wrap_in_numshots_loop(10000) print(ghz_prog) - results = qc.run(ghz_prog) + results = qc.run(ghz_prog).readout_data.get("ro") .. parsed-literal:: @@ -1152,7 +1152,7 @@ Pauli-Z moments that indicate the qubit correlations are corrupted (and correcte noisy_ghz = header + ghz_prog noisy_ghz.wrap_in_numshots_loop(10000) print(noisy_ghz) - noisy_results = qc.run(noisy_ghz) + noisy_results = qc.run(noisy_ghz).readout_data.get("ro") .. parsed-literal:: @@ -1307,9 +1307,9 @@ we should always measure ``1``. ).wrap_in_numshots_loop(10) print("Without Noise:") - print(qc.run(p)) + print(qc.run(p).readout_data.get("ro")) print("With Noise:") - print(noisy_qc.run(p)) + print(noisy_qc.run(p).readout_data.get("ro")) .. parsed-literal:: diff --git a/docs/source/qvm.rst b/docs/source/qvm.rst index b8c2950dc..4ed125e30 100644 --- a/docs/source/qvm.rst +++ b/docs/source/qvm.rst @@ -314,7 +314,8 @@ For example: p.wrap_in_numshots_loop(5) executable = qc.compile(p) - bitstrings = qc.run(executable) # .run takes in a compiled program + result = qc.run(executable) # .run takes in a compiled program + bitstrings = result.readout_data.get("ro") print(bitstrings) The results returned is a *list of lists of integers*. In the above case, that's diff --git a/docs/source/start.rst b/docs/source/start.rst index 38b92890b..b50aa03a4 100644 --- a/docs/source/start.rst +++ b/docs/source/start.rst @@ -297,7 +297,7 @@ allows us to declare classical memory regions so that we can receive data from t with local_forest_runtime(): qvm = get_qc('9q-square-qvm') - results = qvm.run(qvm.compile(prog))) + bitstrings = qvm.run(qvm.compile(prog))).readout_data.get("ro") Next, let's construct our Bell State. @@ -319,7 +319,7 @@ an entangled state between qubits 0 and 1 (that's what the "CNOT" gate does). Fi # run the program on a QVM qc = get_qc('9q-square-qvm') - result = qc.run(qc.compile(p)) + result = qc.run(qc.compile(p)).readout_data.get("ro") print(result[0]) print(result[1]) From cbcb387afb0a2778151b96da5490d0a8593d07d1 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Thu, 17 Jun 2021 10:49:57 -0700 Subject: [PATCH 37/61] Fix: remove QAM.experiment attribute --- pyquil/api/_qam.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index 19a6b44ae..33f86d05e 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -45,9 +45,6 @@ class QAM(ABC, Generic[T]): computer interacts with a live quantum computer. """ - def __init__(self) -> None: - self.experiment: Optional[Experiment] - @abstractmethod def execute(self, executable: QuantumExecutable) -> T: """ From f68cae9801035de72d12729c8e50517970dfb0a0 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Thu, 17 Jun 2021 10:50:58 -0700 Subject: [PATCH 38/61] Fix: make QPUCompiler.calibration_program thread-safe --- pyquil/api/_compiler.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py index cce33ab8c..8349b76ac 100644 --- a/pyquil/api/_compiler.py +++ b/pyquil/api/_compiler.py @@ -14,6 +14,7 @@ # limitations under the License. ############################################################################## import logging +import threading from contextlib import contextmanager from typing import Dict, Optional, cast, List, Iterator @@ -77,6 +78,8 @@ class QPUCompiler(AbstractCompiler): Client to communicate with the compiler and translation service. """ + _calibration_program_lock: threading.Lock + def __init__( self, *, @@ -101,6 +104,7 @@ def __init__( self.quantum_processor_id = quantum_processor_id self._calibration_program: Optional[Program] = None + self._calibration_program_lock = threading.Lock() def native_quil_to_executable(self, nq_program: Program) -> QuantumExecutable: arithmetic_response = rewrite_arithmetic(nq_program) @@ -153,11 +157,12 @@ def calibration_program(self) -> Program: hardware. :returns: A Program object containing the calibration definitions.""" - if self._calibration_program is None: - try: - self.refresh_calibration_program() - except Exception as ex: - raise RuntimeError("Could not refresh calibrations") from ex + with self._calibration_program_lock: + if self._calibration_program is None: + try: + self.refresh_calibration_program() + except Exception as ex: + raise RuntimeError("Could not refresh calibrations") from ex assert self._calibration_program is not None return self._calibration_program From 92875d36f3cd7212878ff22d3ffb611307747131 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Thu, 17 Jun 2021 11:55:36 -0700 Subject: [PATCH 39/61] CI: remove entry from mypy config --- mypy.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index 39ff7bebf..35112aed5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -36,10 +36,6 @@ ignore_errors = True [mypy-conftest] ignore_errors = True -# Ignore errors in the pyquil/magic.py file -[mypy-pyquil.magic] -ignore_errors = True - # Ignore errors in the pyquil/parser.py file # TODO(notmgsk): Add types [mypy-pyquil.parser] From 4f210e8462063f957e2843b76ee73b6846854dc4 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Thu, 17 Jun 2021 11:55:59 -0700 Subject: [PATCH 40/61] Fix: type check failures --- pyquil/_memory.py | 11 ++-- pyquil/api/_abstract_compiler.py | 5 +- pyquil/api/_compiler.py | 2 +- pyquil/api/_qam.py | 4 +- pyquil/api/_qpu.py | 6 +- pyquil/api/_quantum_computer.py | 10 ++- pyquil/api/_qvm.py | 2 +- pyquil/compatibility/v2/api/_qam.py | 24 ++++--- pyquil/compatibility/v2/api/_qpu.py | 3 +- .../compatibility/v2/api/_quantum_computer.py | 64 +++++-------------- pyquil/compatibility/v2/api/_qvm.py | 3 +- pyquil/noise.py | 5 +- pyquil/pyqvm.py | 4 +- pyquil/quil.py | 2 +- 14 files changed, 66 insertions(+), 79 deletions(-) diff --git a/pyquil/_memory.py b/pyquil/_memory.py index 7231b6832..41364dce5 100644 --- a/pyquil/_memory.py +++ b/pyquil/_memory.py @@ -1,8 +1,11 @@ from dataclasses import dataclass, field -from typing import Dict, Sequence, Union +import dataclasses +from typing import Dict, Mapping, Sequence, Union from rpcq.messages import ParameterAref +ParameterValue = Union[int, float, Sequence[int], Sequence[float]] + @dataclass class Memory: @@ -17,9 +20,9 @@ def copy(self) -> "Memory": """ Return a deep copy of this Memory object. """ - return Memory(values={k.replace(): v for k, v in self.values.items()}) + return Memory(values={dataclasses.replace(k): v for k, v in self.values.items()}) - def write(self, parameter_values: Dict[Union[str, ParameterAref], Union[int, float]]) -> "Memory": + def write(self, parameter_values: Mapping[Union[str, ParameterAref], ParameterValue]) -> "Memory": """ Set the given values for the given parameters. """ @@ -31,7 +34,7 @@ def _write_value( self, *, parameter: Union[ParameterAref, str], - value: Union[int, float, Sequence[int], Sequence[float]], + value: ParameterValue, ) -> "Memory": """ Mutate the program to set the given parameter value. diff --git a/pyquil/api/_abstract_compiler.py b/pyquil/api/_abstract_compiler.py index 682e121e4..5b88c964e 100644 --- a/pyquil/api/_abstract_compiler.py +++ b/pyquil/api/_abstract_compiler.py @@ -15,6 +15,7 @@ ############################################################################## from abc import ABC, abstractmethod from dataclasses import dataclass +import dataclasses from typing import Any, Dict, List, Optional, Sequence, Union from pyquil._memory import Memory @@ -64,7 +65,7 @@ def copy(self) -> "EncryptedProgram": """ Return a deep copy of this EncryptedProgram. """ - return self.replace(memory=self._memory.copy()) + return dataclasses.replace(self, memory=self._memory.copy()) def write_memory( self, @@ -73,7 +74,7 @@ def write_memory( value: Union[int, float, Sequence[int], Sequence[float]], offset: Optional[int] = None, ) -> "EncryptedProgram": - self._memory._write_value(parameter=ParameterAref(name=region_name, index=offset), value=value) + self._memory._write_value(parameter=ParameterAref(name=region_name, index=(offset or 0)), value=value) return self diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py index 8349b76ac..a162979b7 100644 --- a/pyquil/api/_compiler.py +++ b/pyquil/api/_compiler.py @@ -146,7 +146,7 @@ def refresh_calibration_program(self) -> None: """Refresh the calibration program cache.""" self._calibration_program = self._get_calibration_program() - @property # type: ignore + @property def calibration_program(self) -> Program: """ Get the Quil-T calibration program associated with the underlying QPU. diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index 33f86d05e..e7cffde20 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -15,7 +15,7 @@ ############################################################################## from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Dict, Generic, Optional, TypeVar +from typing import Dict, Generic, Mapping, Optional, TypeVar import numpy as np from pyquil.api._abstract_compiler import QuantumExecutable @@ -35,7 +35,7 @@ class QAMExecutionResult: executable: QuantumExecutable """The executable corresponding to this result.""" - readout_data: Dict[str, Optional[np.ndarray]] = field(default_factory=dict) + readout_data: Mapping[str, Optional[np.ndarray]] = field(default_factory=dict) """Readout data returned from the QAM, keyed on the name of the readout register or post-processing node.""" diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index e3f56da42..95e177336 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -168,7 +168,7 @@ def execute(self, executable: QuantumExecutable) -> QPUExecuteResponse: patch_values=self._build_patch_values(executable), ) job_id = self._qpu_client.run_program(request).job_id - return QPUExecuteResponse(job_id=job_id) + return QPUExecuteResponse(_executable=executable, job_id=job_id) def get_result(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult: """ @@ -187,7 +187,7 @@ def get_result(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult elif not ro_sources: result_memory["ro"] = np.zeros((0, 0), dtype=np.int64) - QAMExecutionResult(executable=execute_response._executable, results=result_memory) + return QAMExecutionResult(executable=execute_response._executable, readout_data=result_memory) def _get_buffers(self, job_id: str) -> Dict[str, np.ndarray]: """ @@ -307,7 +307,7 @@ def _resolve_memory_references(cls, expression: ExpressionDesignator, memory: Me elif isinstance(expression, Function): return cast( Union[float, int], - expression.fn(cls._resolve_memory_references(expression.expression)), + expression.fn(cls._resolve_memory_references(expression.expression, memory=memory)), ) elif isinstance(expression, Parameter): raise ValueError(f"Unexpected Parameter in gate expression: {expression}") diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 6a71a4a01..74443dede 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -21,6 +21,8 @@ from contextlib import contextmanager from math import pi, log from typing import ( + Any, + Dict, Tuple, Iterator, Mapping, @@ -38,6 +40,7 @@ from qcs_api_client.client import QCSClientConfiguration from qcs_api_client.models import ListQuantumProcessorsResponse from qcs_api_client.operations.sync import list_quantum_processors +from rpcq.messages import ParameterAref from pyquil.api import EngagementManager from pyquil.api._abstract_compiler import AbstractCompiler, QuantumExecutable @@ -70,7 +73,7 @@ def __init__( self, *, name: str, - qam: QAM, + qam: QAM[Any], compiler: AbstractCompiler, symmetrize_readout: bool = False, ) -> None: @@ -152,7 +155,7 @@ def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: correspond to the scale factors resulting from symmetric readout error. """ calibration_experiment = experiment.generate_calibration_experiment() - return cast(List[ExperimentResult], self.run_experiment(calibration_experiment)) + return self.run_experiment(calibration_experiment) def run_experiment( self, @@ -228,8 +231,10 @@ def run_experiment( for merged_memory_map in merged_memory_maps: final_memory_map = {**memory_map, **merged_memory_map} executable_copy = executable.copy() + final_memory_map = cast(Mapping[Union[str, ParameterAref], Union[int, float]], final_memory_map) executable_copy._memory.write(final_memory_map) bitstrings = self.run(executable_copy).readout_data.get("ro") + assert bitstrings is not None if "symmetrization" in final_memory_map: bitmask = np.array(np.array(final_memory_map["symmetrization"]) / np.pi, dtype=int) @@ -1108,6 +1113,7 @@ def _measure_bitstrings( executable = qc.compiler.native_quil_to_executable(prog) result = qc.run(executable) shot_values = result.readout_data.get("ro") + assert shot_values is not None results.append(shot_values) return results diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 217f79044..985a80106 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -55,7 +55,7 @@ def check_qvm_version(version: str) -> None: @dataclass class QVMExecuteResponse: executable: Program - memory: Dict[str, np.ndarray] + memory: Mapping[str, np.ndarray] class QVM(QAM[QVMExecuteResponse]): diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index 74fb4975f..230a508e8 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -1,42 +1,48 @@ -from typing import Optional, Sequence, Union +from typing import Any, Optional, Sequence, TypeVar, Union, cast import numpy as np from rpcq.messages import ParameterAref from pyquil.api._qam import QAM, QAMExecutionResult, QuantumExecutable +T = TypeVar("T") -class StatefulQAM(QAM): + +class StatefulQAM(QAM[T]): _loaded_executable: Optional[QuantumExecutable] _result: Optional[QAMExecutionResult] @classmethod - def wrap(cls, qam: QAM) -> None: + def wrap(cls, qam: QAM[T]) -> None: """ Mutate the provided QAM to add methods and data for backwards compatibility, by dynamically mixing in this wrapper class. """ if not isinstance(qam, StatefulQAM): qam.__class__ = type(str(qam.__class__.__name__), (StatefulQAM, qam.__class__), {}) + qam = cast(StatefulQAM[T], qam) qam.reset() - def load(self, executable: QuantumExecutable) -> "QAM": + def load(self, executable: QuantumExecutable) -> "StatefulQAM[T]": self._loaded_executable = executable.copy() return self def read_memory(self, region_name: str) -> np.ndarray: assert self._result is not None, "QAM#run must be called before QAM#read_memory" - return self._result.readout_data.get(region_name) + data = self._result.readout_data.get(region_name) + assert data is not None + return data - def reset(self) -> "QAM": + def reset(self) -> "StatefulQAM[T]": self._loaded_executable = None self._result = None return self - def run(self) -> "QAM": + def run(self) -> "StatefulQAM[T]": # type: ignore + assert self._loaded_executable is not None self._result = super().run(self._loaded_executable) return self - def wait(self) -> "QAM": + def wait(self) -> "StatefulQAM[T]": return self def write_memory( @@ -45,7 +51,7 @@ def write_memory( region_name: str, value: Union[int, float, Sequence[int], Sequence[float]], offset: Optional[int] = None, - ) -> "QAM": + ) -> "StatefulQAM[T]": assert self._loaded_executable is not None, "Executable has not been loaded yet. Call QAM#load first" parameter_aref = ParameterAref(name=region_name, index=offset or 0) self._loaded_executable._memory._write_value(parameter=parameter_aref, value=value) diff --git a/pyquil/compatibility/v2/api/_qpu.py b/pyquil/compatibility/v2/api/_qpu.py index e89c2af44..1407b8866 100644 --- a/pyquil/compatibility/v2/api/_qpu.py +++ b/pyquil/compatibility/v2/api/_qpu.py @@ -1,8 +1,9 @@ +from typing import Any, Iterable, Mapping from pyquil.api._qpu import QPU as QPUV3 from ._qam import StatefulQAM class QPU(QPUV3): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) StatefulQAM.wrap(self) diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index 597f07161..1b3ad3de9 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -28,7 +28,7 @@ from pyquil.api._qam import QAM from pyquil.api._qpu import QPU from pyquil.api._quantum_computer import QuantumComputer as QuantumComputerV3 -from pyquil.api._quantum_computer import get_qc as get_qc_v3 +from pyquil.api._quantum_computer import get_qc as get_qc_v3, QuantumExecutable from pyquil.api._qvm import QVM from pyquil.experiment._main import Experiment from pyquil.experiment._memory import merge_memory_map_lists @@ -44,18 +44,16 @@ from ._qam import StatefulQAM -ExecutableDesignator = Union[QuiltBinaryExecutableResponse, PyQuilExecutableResponse] - class QuantumComputer(QuantumComputerV3): compiler: AbstractCompiler - qam: StatefulQAM + qam: StatefulQAM[Any] def __init__( self, *, name: str, - qam: QAM, + qam: QAM[Any], device: Any = None, compiler: AbstractCompiler, symmetrize_readout: bool = False, @@ -82,15 +80,15 @@ def __init__( :py:func:`run_symmetrized_readout` for a complete description. """ self.name = name - self.qam = qam StatefulQAM.wrap(self.qam) + self.qam = cast(StatefulQAM[Any], qam) self.compiler = compiler self.symmetrize_readout = symmetrize_readout - def run( + def run( # type: ignore self, - executable: ExecutableDesignator, + executable: QuantumExecutable, memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None, ) -> np.ndarray: """ @@ -118,7 +116,7 @@ def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: correspond to the scale factors resulting from symmetric readout error. """ calibration_experiment = experiment.generate_calibration_experiment() - return cast(List[ExperimentResult], self.experiment(calibration_experiment)) + return self.experiment(calibration_experiment) def experiment( self, @@ -205,7 +203,7 @@ def experiment( joint_expectations += setting.additional_expectations expectations = bitstrings_to_expectations(symmetrized_bitstrings, joint_expectations=joint_expectations) - means = np.mean(expectations, axis=0) + means = cast(np.ndarray, np.mean(expectations, axis=0)) std_errs = np.std(expectations, axis=0, ddof=1) / np.sqrt(len(expectations)) joint_results = [] @@ -376,7 +374,7 @@ def compile( optimize: bool = True, *, protoquil: Optional[bool] = None, - ) -> ExecutableDesignator: + ) -> QuantumExecutable: """ A high-level interface to program compilation. @@ -453,8 +451,6 @@ def get_qc( client_configuration=client_configuration, ) - qc = cast(QuantumComputerV3, qc) - return QuantumComputer( name=qc.name, qam=qc.qam, device=None, compiler=qc.compiler, symmetrize_readout=qc.symmetrize_readout ) @@ -487,7 +483,7 @@ def _flip_array_to_prog(flip_array: Tuple[bool], qubits: List[int]) -> Program: def _symmetrization( program: Program, meas_qubits: List[int], symm_type: int = 3 -) -> Tuple[List[Program], List[np.ndarray]]: +) -> Tuple[List[Program], List[Tuple[bool]]]: """ For the input program generate new programs which flip the measured qubits with an X gate in certain combinations in order to symmetrize readout. @@ -593,8 +589,8 @@ def _measure_bitstrings( prog.wrap_in_numshots_loop(num_shots) prog = qc.compiler.quil_to_native_quil(prog) - exe = qc.compiler.native_quil_to_executable(prog) - shots = qc.run(exe) + executable = qc.compiler.native_quil_to_executable(prog) + shots = qc.run(executable) results.append(shots) return results @@ -635,7 +631,7 @@ def _next_power_of_2(x: int) -> int: # The code below is directly copied from scipy see https://bit.ly/2RjAHJz, the docstrings have # been modified. -def hadamard(n: int, dtype: np.dtype = int) -> np.ndarray: +def hadamard(n: int, dtype: np.dtype[Any] = np.dtype("int")) -> np.ndarray: """ Construct a Hadamard matrix. Constructs an n-by-n Hadamard matrix, using Sylvester's @@ -710,7 +706,7 @@ def _construct_strength_three_orthogonal_array(num_qubits: int) -> np.ndarray: num_qubits_power_of_2 = _next_power_of_2(num_qubits) H = hadamard(num_qubits_power_of_2) Hfold = np.concatenate((H, -H), axis=0) - orthogonal_array = ((Hfold + 1) / 2).astype(int) + orthogonal_array = cast(np.ndarray, ((Hfold + 1) / 2).astype(int)) return orthogonal_array @@ -744,7 +740,7 @@ def _construct_strength_two_orthogonal_array(num_qubits: int) -> np.ndarray: four_lam = min(x for x in valid_numbers if x >= num_qubits) + 1 H = hadamard(_next_power_of_2(four_lam)) # The minus sign in front of H fixes the 0 <-> 1 inversion relative to the reference [OATA] - orthogonal_array = ((-H[1:, :].T + 1) / 2).astype(int) + orthogonal_array = cast(np.ndarray, ((-H[1:, :].T + 1) / 2).astype(int)) return orthogonal_array @@ -880,33 +876,3 @@ def _get_qvm_with_topology( compiler_timeout=compiler_timeout, execution_timeout=execution_timeout, ) - - -def _measure_bitstrings( - qc: QuantumComputer, programs: List[Program], meas_qubits: List[int], num_shots: int = 600 -) -> List[np.ndarray]: - """ - Wrapper for appending measure instructions onto each program, running the program, - and accumulating the resulting bitarrays. - - :param qc: a quantum computer object on which to run each program - :param programs: a list of programs to run - :param meas_qubits: groups of qubits to measure for each program - :param num_shots: the number of shots to run for each program - :return: a len(programs) long list of num_shots by num_meas_qubits bit arrays of results for - each program. - """ - results = [] - for program in programs: - # copy the program so the original is not mutated - prog = program.copy() - ro = prog.declare("ro", "BIT", len(meas_qubits)) - for idx, q in enumerate(meas_qubits): - prog += MEASURE(q, ro[idx]) - - prog.wrap_in_numshots_loop(num_shots) - prog = qc.compiler.quil_to_native_quil(prog) - executable = qc.compiler.native_quil_to_executable(prog) - shots = qc.run(executable) - results.append(shots) - return results diff --git a/pyquil/compatibility/v2/api/_qvm.py b/pyquil/compatibility/v2/api/_qvm.py index a473acf0b..75fda26c2 100644 --- a/pyquil/compatibility/v2/api/_qvm.py +++ b/pyquil/compatibility/v2/api/_qvm.py @@ -1,8 +1,9 @@ +from typing import Any from pyquil.api._qvm import QVM as QVMV3 from ._qam import StatefulQAM class QVM(QVMV3): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) StatefulQAM.wrap(self) diff --git a/pyquil/noise.py b/pyquil/noise.py index a746cfcda..15b33ea58 100644 --- a/pyquil/noise.py +++ b/pyquil/noise.py @@ -818,4 +818,7 @@ def estimate_assignment_probs( def _run(qc: "QuantumComputer", program: "Program") -> List[List[int]]: - return cast(List[List[int]], qc.run(qc.compiler.native_quil_to_executable(program)).tolist()) + result = qc.run(qc.compiler.native_quil_to_executable(program)) + bitstrings = result.readout_data.get("ro") + assert bitstrings is not None + return cast(List[List[int]], bitstrings.tolist()) diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 70f23e1e2..bb5f95e25 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -154,7 +154,7 @@ def do_post_gate_noise(self, noise_type: str, noise_prob: float, qubits: List[in """ -class PyQVM(QAM): +class PyQVM(QAM["PyQVM"]): def __init__( self, n_qubits: int, @@ -222,7 +222,6 @@ def _extract_defined_gates(self) -> None: self.defined_gates[dg.name] = dg.matrix def write_memory(self, *, region_name: str, offset: int = 0, value: int = 0) -> "PyQVM": - assert self.status in ["loaded", "done"] assert region_name != "ro" self.ram[region_name][offset] = value return self @@ -266,6 +265,7 @@ def get_result(self, execute_response: "PyQVM") -> QAMExecutionResult: ``execute_response`` is not used, it's accepted in order to conform to that API; it's unused because the PyQVM, unlike other QAM's, is itself stateful. """ + assert self.program is not None return QAMExecutionResult( executable=self.program.copy(), readout_data={k: v for k, v in self._memory_results.items()} ) diff --git a/pyquil/quil.py b/pyquil/quil.py index 546138c13..93006be5a 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -805,7 +805,7 @@ def is_supported_on_qpu(self) -> bool: except ValueError: return False - def _sort_declares_to_program_start(self) -> "Program": + def _sort_declares_to_program_start(self) -> None: """ Re-order DECLARE instructions within this program to the beginning, followed by all other instructions. Reordering is stable among DECLARE and non-DECLARE instructions. From 54c47a7ccbe54e90f668c33276ab3cd0d6519b53 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:14:20 -0700 Subject: [PATCH 41/61] Fix: move EngagementManager._lock to the instance --- pyquil/api/_engagement_manager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 1d8032230..61b147beb 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -32,7 +32,8 @@ class EngagementManager: Fetches (and caches) engagements for use when accessing a QPU. """ - _lock = threading.Lock() + _lock: threading.Lock + """Lock used to ensure that only one engagement request is in flight at once.""" def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: """ @@ -42,6 +43,7 @@ def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: """ self._client_configuration = client_configuration self._cached_engagements: Dict[str, EngagementWithCredentials] = {} + self._lock = threading.Lock() def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = 10.0) -> EngagementWithCredentials: """ @@ -52,8 +54,7 @@ def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = :param request_timeout: Timeout for request, in seconds. :return: Fetched or cached engagement. """ - EngagementManager._lock.acquire() - try: + with self._lock: if not self._engagement_valid(self._cached_engagements.get(quantum_processor_id)): with qcs_client( client_configuration=self._client_configuration, request_timeout=request_timeout @@ -63,8 +64,6 @@ def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = client=client, json_body=request ).parsed return self._cached_engagements[quantum_processor_id] - finally: - EngagementManager._lock.release() @staticmethod def _engagement_valid(engagement: Optional[EngagementWithCredentials]) -> bool: From 271de7f81e3ea968c90292a9b6f2a3f4ea3b5c9b Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:19:37 -0700 Subject: [PATCH 42/61] Style: fix --- pyquil/api/_engagement_manager.py | 6 ++++-- pyquil/api/_qam.py | 3 +-- pyquil/api/_quantum_computer.py | 1 - pyquil/compatibility/v2/api/_qam.py | 2 +- pyquil/compatibility/v2/api/_qpu.py | 2 +- pyquil/compatibility/v2/api/_quantum_computer.py | 4 +--- .../transformers/qcs_isa_to_compiler_isa.py | 4 ++-- pyquil/quil.py | 2 +- 8 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 61b147beb..9e6b2ea8a 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -15,9 +15,8 @@ ############################################################################## import threading from datetime import datetime -from typing import Dict, Optional +from typing import Dict, Optional, TYPE_CHECKING -import httpx from dateutil.parser import parse as parsedate from dateutil.tz import tzutc from qcs_api_client.client import QCSClientConfiguration @@ -26,6 +25,9 @@ from pyquil.api._qcs_client import qcs_client +if TYPE_CHECKING: + import httpx + class EngagementManager: """ diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index e7cffde20..5312cc548 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -15,11 +15,10 @@ ############################################################################## from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Dict, Generic, Mapping, Optional, TypeVar +from typing import Generic, Mapping, Optional, TypeVar import numpy as np from pyquil.api._abstract_compiler import QuantumExecutable -from pyquil.experiment._main import Experiment class QAMError(RuntimeError): diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 74443dede..87942bb50 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -22,7 +22,6 @@ from math import pi, log from typing import ( Any, - Dict, Tuple, Iterator, Mapping, diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index 230a508e8..b4b452914 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Sequence, TypeVar, Union, cast +from typing import Optional, Sequence, TypeVar, Union, cast import numpy as np from rpcq.messages import ParameterAref diff --git a/pyquil/compatibility/v2/api/_qpu.py b/pyquil/compatibility/v2/api/_qpu.py index 1407b8866..f2bf8fc88 100644 --- a/pyquil/compatibility/v2/api/_qpu.py +++ b/pyquil/compatibility/v2/api/_qpu.py @@ -1,4 +1,4 @@ -from typing import Any, Iterable, Mapping +from typing import Any from pyquil.api._qpu import QPU as QPUV3 from ._qam import StatefulQAM diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index 1b3ad3de9..c3b444170 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -22,7 +22,6 @@ import networkx as nx import numpy as np from qcs_api_client.client import QCSClientConfiguration -from rpcq.messages import PyQuilExecutableResponse, QuiltBinaryExecutableResponse from pyquil.api._compiler import AbstractCompiler, QVMCompiler from pyquil.api._qam import QAM @@ -38,7 +37,6 @@ from pyquil.noise import NoiseModel, decoherence_noise_with_asymmetric_ro from pyquil.paulis import PauliTerm from pyquil.quantum_processor import AbstractQuantumProcessor, NxQuantumProcessor -from pyquil.quantum_processor.graph import NxQuantumProcessor from pyquil.quil import Program, validate_supported_quil from pyquil.quilatom import qubit_index @@ -631,7 +629,7 @@ def _next_power_of_2(x: int) -> int: # The code below is directly copied from scipy see https://bit.ly/2RjAHJz, the docstrings have # been modified. -def hadamard(n: int, dtype: np.dtype[Any] = np.dtype("int")) -> np.ndarray: +def hadamard(n: int, dtype: np.dtype = int) -> np.ndarray: # type: ignore """ Construct a Hadamard matrix. Constructs an n-by-n Hadamard matrix, using Sylvester's diff --git a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py index 2dc13bd2c..9c5774620 100644 --- a/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py +++ b/pyquil/quantum_processor/transformers/qcs_isa_to_compiler_isa.py @@ -76,11 +76,11 @@ def qcs_isa_to_compiler_isa(isa: InstructionSetArchitecture) -> CompilerISA: else: raise QCSISAParseError("unexpected operation node count: {}".format(operation.node_count)) - for qubit_id, qubit in device.qubits.items(): + for qubit in device.qubits.values(): if len(qubit.gates) == 0: qubit.dead = True - for edge_id, edge in device.edges.items(): + for edge in device.edges.values(): if len(edge.gates) == 0: edge.dead = True diff --git a/pyquil/quil.py b/pyquil/quil.py index 93006be5a..b8c9fa9bd 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -41,7 +41,7 @@ from pyquil._parser.parser import run_parser from pyquil._memory import Memory -from pyquil.gates import DECLARE, MEASURE, RESET, MOVE +from pyquil.gates import MEASURE, RESET, MOVE from pyquil.noise import _check_kraus_ops, _create_kraus_pragmas, pauli_kraus_map from pyquil.quilatom import ( Label, From 9de7c88910429787123021f5279ec97a3e592e3c Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Mon, 21 Jun 2021 12:40:57 -0700 Subject: [PATCH 43/61] No code: fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a88ce3fd..42f4d5295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,7 +90,7 @@ Changelog - `get_qc()` no longer accepts `"9q-generic"` (deprecated). Use `"9q-square"` instead. -- Removed `QAM.read_from_memory_region()` (deprecated). Use `QAMExecutionResult.read_memory()` instead. +- Removed `QAM.read_from_memory_region()` (deprecated). Use `QAMExecutionResult.readout_data.get(region_name)` instead. - Removed `local_qvm()` (deprecated). Use `local_forest_runtime()` instead. From 330f49d671428edec52b61b4f17c0943c825296c Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Mon, 21 Jun 2021 12:41:06 -0700 Subject: [PATCH 44/61] No code: fix e2e test --- test/e2e/test_e2e.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/test_e2e.py b/test/e2e/test_e2e.py index bdfa36cae..5eb90f8c1 100644 --- a/test/e2e/test_e2e.py +++ b/test/e2e/test_e2e.py @@ -14,6 +14,7 @@ # limitations under the License. ############################################################################## from multiprocessing.pool import Pool, ThreadPool +import numpy as np from qcs_api_client.client import QCSClientConfiguration @@ -69,10 +70,10 @@ def run_program( quantum_processor_id: str, client_configuration: QCSClientConfiguration, engagement_manager: EngagementManager, -): +)-> np.ndarray: qc = get_qc( quantum_processor_id, client_configuration=client_configuration, engagement_manager=engagement_manager, ) - return qc.run(qc.compile(program)) + return qc.run(qc.compile(program)).readout_data['ro'] From 38ffbbde3a924521bb802514ec315d5a2ad4601c Mon Sep 17 00:00:00 2001 From: Andrew Meyer Date: Tue, 22 Jun 2021 15:26:19 -0700 Subject: [PATCH 45/61] Fix: fix e2e tests --- test/e2e/conftest.py | 11 ++++++++--- test/e2e/test_e2e.py | 38 ++++++++++++-------------------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/test/e2e/conftest.py b/test/e2e/conftest.py index 7bb6532bd..df383bba1 100644 --- a/test/e2e/conftest.py +++ b/test/e2e/conftest.py @@ -3,17 +3,22 @@ import pytest from qcs_api_client.client import QCSClientConfiguration -from pyquil.api import EngagementManager +from pyquil import get_qc +from pyquil.api import EngagementManager, QuantumComputer @pytest.fixture() -def quantum_processor_id() -> str: +def qc(client_configuration: QCSClientConfiguration, engagement_manager: EngagementManager) -> QuantumComputer: quantum_processor_id = os.environ.get("TEST_QUANTUM_PROCESSOR") if quantum_processor_id is None: raise Exception("'TEST_QUANTUM_PROCESSOR' env var required for e2e tests.") - return quantum_processor_id + return get_qc( + quantum_processor_id, + client_configuration=client_configuration, + engagement_manager=engagement_manager, + ) @pytest.fixture() diff --git a/test/e2e/test_e2e.py b/test/e2e/test_e2e.py index 5eb90f8c1..d6b2582a4 100644 --- a/test/e2e/test_e2e.py +++ b/test/e2e/test_e2e.py @@ -14,12 +14,11 @@ # limitations under the License. ############################################################################## from multiprocessing.pool import Pool, ThreadPool -import numpy as np -from qcs_api_client.client import QCSClientConfiguration +import numpy as np -from pyquil import get_qc, Program -from pyquil.api import EngagementManager +from pyquil import Program +from pyquil.api import QuantumComputer from pyquil.gates import H, CNOT, MEASURE from pyquil.quilbase import Declare @@ -32,18 +31,14 @@ ).wrap_in_numshots_loop(1000) -def test_basic_program(quantum_processor_id: str, client_configuration: QCSClientConfiguration): - qc = get_qc(quantum_processor_id, client_configuration=client_configuration) - - results = qc.run(qc.compile(TEST_PROGRAM)) +def test_basic_program(qc: QuantumComputer): + results = qc.run(qc.compile(TEST_PROGRAM)).readout_data["ro"] assert results.shape == (1000, 2) -def test_multithreading( - quantum_processor_id: str, client_configuration: QCSClientConfiguration, engagement_manager: EngagementManager -): - args = [(TEST_PROGRAM, quantum_processor_id, client_configuration, engagement_manager) for _ in range(20)] +def test_multithreading(qc: QuantumComputer): + args = [(TEST_PROGRAM, qc) for _ in range(20)] with ThreadPool(10) as pool: results = pool.starmap(run_program, args) @@ -52,10 +47,8 @@ def test_multithreading( assert result.shape == (1000, 2) -def test_multiprocessing( - quantum_processor_id: str, client_configuration: QCSClientConfiguration, engagement_manager: EngagementManager -): - args = [(TEST_PROGRAM, quantum_processor_id, client_configuration, engagement_manager) for _ in range(20)] +def test_multiprocessing(qc: QuantumComputer): + args = [(TEST_PROGRAM, qc) for _ in range(20)] with Pool(10) as pool: results = pool.starmap(run_program, args) @@ -66,14 +59,7 @@ def test_multiprocessing( # NOTE: This must be outside of the test function, or multiprocessing complains that it can't be pickled def run_program( - program: Program, - quantum_processor_id: str, - client_configuration: QCSClientConfiguration, - engagement_manager: EngagementManager, -)-> np.ndarray: - qc = get_qc( - quantum_processor_id, - client_configuration=client_configuration, - engagement_manager=engagement_manager, - ) + program: Program, + qc: QuantumComputer, +) -> np.ndarray: return qc.run(qc.compile(program)).readout_data['ro'] From d2daf19c0ecef7fda025652475e809429f0b977a Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:34:59 -0700 Subject: [PATCH 46/61] No code: fix test mock --- test/unit/test_noise.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/test_noise.py b/test/unit/test_noise.py index d2ebf0766..69e0b3c8e 100644 --- a/test/unit/test_noise.py +++ b/test/unit/test_noise.py @@ -4,6 +4,7 @@ import numpy as np import pytest +from pyquil.api._qam import QAMExecutionResult from pyquil.gates import RZ, RX, I, CZ from pyquil.noise import ( pauli_kraus_map, @@ -290,9 +291,10 @@ def test_estimate_assignment_probs(mock_qc_class: mock.MagicMock, mock_compiler_ mock_compiler.native_quil_to_executable.return_value = Program() mock_qc.compiler = mock_compiler + mock_qc mock_qc.run.side_effect = [ - np.array([[0]]) * int(round(p00 * trials)) + np.array([[1]]) * int(round((1 - p00) * trials)), # I gate results - np.array([[1]]) * int(round(p11 * trials)) + np.array([[0]]) * int(round((1 - p11) * trials)), # X gate results + QAMExecutionResult(executable=None, readout_data={'ro': np.array([[0]]) * int(round(p00 * trials)) + np.array([[1]]) * int(round((1 - p00) * trials))}), # I gate results + QAMExecutionResult(executable=None, readout_data={'ro': np.array([[1]]) * int(round(p11 * trials)) + np.array([[0]]) * int(round((1 - p11) * trials))}), # X gate results ] ap_target = np.array([[p00, 1 - p11], [1 - p00, p11]]) From 969d96d53da41180bfc69c80d1d49b76fd62c84f Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:35:20 -0700 Subject: [PATCH 47/61] No code: update Readme example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea7528640..f96b84756 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,8 @@ ro = p.declare('ro', 'BIT', 2) p += MEASURE(0, ro[0]) p += MEASURE(1, ro[1]) p.wrap_in_numshots_loop(10) - -qvm.run(p).tolist() + +qvm.run(p).readout_data['ro'].tolist() ``` The output of the above program should look something like the following, From 35208120d23a67303a7c49e4ec8a0bfb64e89106 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:36:13 -0700 Subject: [PATCH 48/61] Fix: compatibility QuantumComputer --- pyquil/compatibility/v2/api/_qam.py | 3 +-- pyquil/compatibility/v2/api/_quantum_computer.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyquil/compatibility/v2/api/_qam.py b/pyquil/compatibility/v2/api/_qam.py index b4b452914..3f958a17f 100644 --- a/pyquil/compatibility/v2/api/_qam.py +++ b/pyquil/compatibility/v2/api/_qam.py @@ -26,10 +26,9 @@ def load(self, executable: QuantumExecutable) -> "StatefulQAM[T]": self._loaded_executable = executable.copy() return self - def read_memory(self, region_name: str) -> np.ndarray: + def read_memory(self, region_name: str) -> Optional[np.ndarray]: assert self._result is not None, "QAM#run must be called before QAM#read_memory" data = self._result.readout_data.get(region_name) - assert data is not None return data def reset(self) -> "StatefulQAM[T]": diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index c3b444170..46ff18cd8 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -78,7 +78,7 @@ def __init__( :py:func:`run_symmetrized_readout` for a complete description. """ self.name = name - StatefulQAM.wrap(self.qam) + StatefulQAM.wrap(qam) self.qam = cast(StatefulQAM[Any], qam) self.compiler = compiler From f93c60606f7631ace19a7377585ac34cb8146318 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:37:04 -0700 Subject: [PATCH 49/61] No code: fix example --- docs/source/basics.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/basics.rst b/docs/source/basics.rst index 921ce7311..eca6475b4 100644 --- a/docs/source/basics.rst +++ b/docs/source/basics.rst @@ -304,7 +304,7 @@ filled in for, say, 200 values between :math:`0` and :math:`2\pi`. We demonstrat executable.write_memory(region_name='theta', value=theta) # Get the results of the run with the value we want to execute with - bitstrings = qc.run(executable) + bitstrings = qc.run(executable).readout_data.get("ro") # Store our results parametric_measurements.append(bitstrings) From db1c94a029ff850e4c93a7ec66c0ed12e24f2602 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:43:30 -0700 Subject: [PATCH 50/61] No code: add documentation on QAM.execute --- docs/source/qvm.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/qvm.rst b/docs/source/qvm.rst index 4ed125e30..c392e5322 100644 --- a/docs/source/qvm.rst +++ b/docs/source/qvm.rst @@ -336,6 +336,16 @@ for more details about declaring and accessing classical memory regions. .. _new_topology: +``.execute`` and ``.get_result`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``.run(...)`` method is itself a convenience wrapper around two other methods: ``.execute(...)`` and +``.get_result(...)``. ``run`` makes your program appear synchronous (request and then wait for the response), +when in reality on some backends (such as a live QPU), execution is in fact asynchronous (request execution, +then request results at a later time). For finer-grained control over your program execution process, +you can use these two methods in place of ``.run``. This is most useful when you want to execute work +concurrently - for that, please see "Advanced Usage." + Providing Your Own Quantum Processor Topology --------------------------------------------- From df6d35528102c2c351006e2d89df8801b79d80b4 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:52:56 -0700 Subject: [PATCH 51/61] Fix: replace threading.Lock with multiprocessing.Lock --- pyquil/api/_compiler.py | 6 +++--- pyquil/api/_engagement_manager.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py index a162979b7..02f2b8c4a 100644 --- a/pyquil/api/_compiler.py +++ b/pyquil/api/_compiler.py @@ -14,7 +14,7 @@ # limitations under the License. ############################################################################## import logging -import threading +import multiprocessing from contextlib import contextmanager from typing import Dict, Optional, cast, List, Iterator @@ -78,7 +78,7 @@ class QPUCompiler(AbstractCompiler): Client to communicate with the compiler and translation service. """ - _calibration_program_lock: threading.Lock + _calibration_program_lock: multiprocessing.Lock def __init__( self, @@ -104,7 +104,7 @@ def __init__( self.quantum_processor_id = quantum_processor_id self._calibration_program: Optional[Program] = None - self._calibration_program_lock = threading.Lock() + self._calibration_program_lock = multiprocessing.Lock() def native_quil_to_executable(self, nq_program: Program) -> QuantumExecutable: arithmetic_response = rewrite_arithmetic(nq_program) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 9e6b2ea8a..5c557f10a 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## -import threading +import multiprocessing from datetime import datetime from typing import Dict, Optional, TYPE_CHECKING @@ -34,7 +34,7 @@ class EngagementManager: Fetches (and caches) engagements for use when accessing a QPU. """ - _lock: threading.Lock + _lock: multiprocessing.Lock """Lock used to ensure that only one engagement request is in flight at once.""" def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: @@ -45,7 +45,7 @@ def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: """ self._client_configuration = client_configuration self._cached_engagements: Dict[str, EngagementWithCredentials] = {} - self._lock = threading.Lock() + self._lock = multiprocessing.Lock() def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = 10.0) -> EngagementWithCredentials: """ From f19354bc1e71e91f297a5457612942a25371d7fa Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 21:31:54 -0700 Subject: [PATCH 52/61] Fix: Program#copy --- pyquil/quil.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyquil/quil.py b/pyquil/quil.py index b8c9fa9bd..2fabf8642 100644 --- a/pyquil/quil.py +++ b/pyquil/quil.py @@ -186,6 +186,7 @@ def copy_everything_except_instructions(self) -> "Program": """ new_prog = Program() new_prog._calibrations = self.calibrations.copy() + new_prog._declarations = self._declarations.copy() new_prog._waveforms = self.waveforms.copy() new_prog._defined_gates = self._defined_gates.copy() new_prog._frames = self.frames.copy() From d3ae981e7d6c5bf440e51fc1a4bcd23022533215 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 21:32:28 -0700 Subject: [PATCH 53/61] No code: fix multiprocessing.Lock type checks --- pyquil/api/_compiler.py | 4 ++-- pyquil/api/_engagement_manager.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyquil/api/_compiler.py b/pyquil/api/_compiler.py index 02f2b8c4a..fd23394b1 100644 --- a/pyquil/api/_compiler.py +++ b/pyquil/api/_compiler.py @@ -78,7 +78,7 @@ class QPUCompiler(AbstractCompiler): Client to communicate with the compiler and translation service. """ - _calibration_program_lock: multiprocessing.Lock + _calibration_program_lock: multiprocessing.Lock # type: ignore def __init__( self, @@ -157,7 +157,7 @@ def calibration_program(self) -> Program: hardware. :returns: A Program object containing the calibration definitions.""" - with self._calibration_program_lock: + with self._calibration_program_lock: # type: ignore if self._calibration_program is None: try: self.refresh_calibration_program() diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 5c557f10a..bf6c95b4a 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -34,7 +34,7 @@ class EngagementManager: Fetches (and caches) engagements for use when accessing a QPU. """ - _lock: multiprocessing.Lock + _lock: multiprocessing.Lock # type: ignore """Lock used to ensure that only one engagement request is in flight at once.""" def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: @@ -56,7 +56,7 @@ def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = :param request_timeout: Timeout for request, in seconds. :return: Fetched or cached engagement. """ - with self._lock: + with self._lock: # type: ignore if not self._engagement_valid(self._cached_engagements.get(quantum_processor_id)): with qcs_client( client_configuration=self._client_configuration, request_timeout=request_timeout From 87ec0064eaba977fd5b4545894b5e23919abb94b Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 21:32:51 -0700 Subject: [PATCH 54/61] Fix: type check --- pyquil/compatibility/v2/api/_quantum_computer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyquil/compatibility/v2/api/_quantum_computer.py b/pyquil/compatibility/v2/api/_quantum_computer.py index 46ff18cd8..ab0821f58 100644 --- a/pyquil/compatibility/v2/api/_quantum_computer.py +++ b/pyquil/compatibility/v2/api/_quantum_computer.py @@ -102,7 +102,9 @@ def run( # type: ignore if memory_map: for region_name, values_list in memory_map.items(): self.qam.write_memory(region_name=region_name, value=values_list) - return self.qam.run().read_memory(region_name="ro") + result = self.qam.run().read_memory(region_name="ro") + assert result is not None + return result def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: """ From f81617154e91f58a1dd236a170751df12373b38d Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 21:33:09 -0700 Subject: [PATCH 55/61] No code: fix autodoc toctree --- docs/source/apidocs/quantum_computer.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/apidocs/quantum_computer.rst b/docs/source/apidocs/quantum_computer.rst index 645e2f09e..ea4085842 100644 --- a/docs/source/apidocs/quantum_computer.rst +++ b/docs/source/apidocs/quantum_computer.rst @@ -17,7 +17,6 @@ Quantum Computer ~QuantumComputer.run ~QuantumComputer.calibrate - ~QuantumComputer.experiment ~QuantumComputer.run_symmetrized_readout ~QuantumComputer.qubits ~QuantumComputer.qubit_topology From 50260b81a3052fa905acd1736c67d7552cfacdf7 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 21:48:14 -0700 Subject: [PATCH 56/61] Fix: style --- pyquil/api/_engagement_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index bf6c95b4a..c3598d2df 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -34,7 +34,7 @@ class EngagementManager: Fetches (and caches) engagements for use when accessing a QPU. """ - _lock: multiprocessing.Lock # type: ignore + _lock: multiprocessing.Lock # type: ignore """Lock used to ensure that only one engagement request is in flight at once.""" def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: From 82fef98e29a4de350cde80710a4a582fcef3a5d0 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 22:39:13 -0700 Subject: [PATCH 57/61] No code: fix docs --- docs/source/migration.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/migration.rst b/docs/source/migration.rst index 4b93bde71..a60ff62c4 100644 --- a/docs/source/migration.rst +++ b/docs/source/migration.rst @@ -29,10 +29,8 @@ performance. (See :doc:`advanced_usage` for more information). However, this required three small but important changes: 1. ``write_memory`` is no longer a method on ``QAM`` but rather on ``Program`` and ``EncryptedProgram``. -2. ``qc.run()`` no longer accepts a ``memory_map`` argument. All memory values must be set directly - on the ``Program`` or ``EncryptedProgram`` using ``write_memory``. -3. ``QAM.load()``, ``QAM.wait()``, and ``QAM.reset()`` no longer exist, because the - ``QAM`` no longer "stores" program state. +2. ``qc.run()`` no longer accepts a ``memory_map`` argument. All memory values must be set directly on the ``Program`` or ``EncryptedProgram`` using ``write_memory``. +3. ``QAM.load()``, ``QAM.wait()``, and ``QAM.reset()`` no longer exist, because the ``QAM`` no longer "stores" program state. This means that you should now execute your programs using one of these options: From 091cef7e0ed263dfb2ce81db99a4c13044a2e26b Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 22:42:11 -0700 Subject: [PATCH 58/61] No code: fix test assertion --- test/unit/test_compatibility_v2_qvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_compatibility_v2_qvm.py b/test/unit/test_compatibility_v2_qvm.py index 79c756b5e..a2a118215 100644 --- a/test/unit/test_compatibility_v2_qvm.py +++ b/test/unit/test_compatibility_v2_qvm.py @@ -67,7 +67,7 @@ def test_qvm_run_region_declared_not_measured(client_configuration: QCSClientCon p = Program(Declare("reg", "BIT"), X(0)) qvm.load(p.wrap_in_numshots_loop(100)).run().wait() bitstrings = qvm.read_memory(region_name="reg") - assert bitstrings is None + assert bitstrings.shape == (100, 0) def test_qvm_run_region_not_declared_is_measured_ro(client_configuration: QCSClientConfiguration): From 621c5506b2091ecab7a4527b36cac571b26519d5 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 22 Jun 2021 22:43:56 -0700 Subject: [PATCH 59/61] No code: restore original test random_seed --- test/unit/test_compatibility_v2_operator_estimation.py | 2 +- test/unit/test_operator_estimation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/test_compatibility_v2_operator_estimation.py b/test/unit/test_compatibility_v2_operator_estimation.py index b32174cc5..dd614ae9e 100644 --- a/test/unit/test_compatibility_v2_operator_estimation.py +++ b/test/unit/test_compatibility_v2_operator_estimation.py @@ -657,7 +657,7 @@ def test_measure_observables_grouped_expts(client_configuration: QCSClientConfig if use_seed: num_simulations = 1 - qc.qam.random_seed = 2 + qc.qam.random_seed = 4 else: num_simulations = 100 diff --git a/test/unit/test_operator_estimation.py b/test/unit/test_operator_estimation.py index 1edadbc22..57d971ffb 100644 --- a/test/unit/test_operator_estimation.py +++ b/test/unit/test_operator_estimation.py @@ -657,7 +657,7 @@ def test_measure_observables_grouped_expts(client_configuration: QCSClientConfig if use_seed: num_simulations = 1 - qc.qam.random_seed = 2 + qc.qam.random_seed = 4 else: num_simulations = 100 From 46d442d68df7f6cb90cf99c8071dbeef24641a12 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:49:56 -0700 Subject: [PATCH 60/61] No code: allow QPU e2e test failure to allow for manual runs within reservations --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 80ac3c596..130baee5f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -75,6 +75,7 @@ Test Unit (3.7): Test e2e QPU (3.7): stage: test + allow_failure: true image: python:3.7 coverage: '/TOTAL.*?(\d+)\%/' script: @@ -101,6 +102,7 @@ Test Unit (3.8): Test e2e QPU (3.8): stage: test + allow_failure: true image: python:3.8 coverage: '/TOTAL.*?(\d+)\%/' script: @@ -127,6 +129,7 @@ Test Unit (3.9): Test e2e QPU (3.9): stage: test + allow_failure: true image: python:3.9 coverage: '/TOTAL.*?(\d+)\%/' script: From 24552ffdc0a63def5c5032552152b00e22b2c707 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 23 Jun 2021 11:15:32 -0700 Subject: [PATCH 61/61] No code: use Rigetti fork of dephell --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 130baee5f..8a2c73b21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ cache: - .venv .install-dephell: &install-dephell # Install dephell to auto-generate setup.py - - curl -L https://raw.githubusercontent.com/dephell/dephell/v.0.8.3/install.py | python + - curl -L https://raw.githubusercontent.com/rigetti/dephell/master/install.py | python .install-npm: &install-npm - curl -sL https://deb.nodesource.com/setup_12.x | bash -