Skip to content

Commit

Permalink
update: pyquil dependency to v4 (#13)
Browse files Browse the repository at this point in the history
* update: pyquil dependency to v4

* chore: fix lint maybe

* chore: poetry lock

* chore: style fix

* chore: wip, couple questions

* chore: all result shapes are qvm now

* chore: fix readme local tests are passing

* chore: compatibile substitution

* chore: remove unused lint skips

* chore: lint mint

* chore: upgrade mypy

* chore: update to stable v4 and remove unused imports

* chore: update lockfile

* chore: lock poetry ci version

* chore: lock poetry ci version

* Update pyquil_for_azure_quantum/__init__.py

Co-authored-by: Marquess Valdez <marquessavaldez@gmail.com>

* chore: remove kwards

* chore: add kwargs with note

* chore: I was not stylish enough

* chore: update python supported bounds

---------

Co-authored-by: Marquess Valdez <marquessavaldez@gmail.com>
  • Loading branch information
jselig-rigetti and MarquessV authored Sep 29, 2023
1 parent 8e22555 commit f289cac
Show file tree
Hide file tree
Showing 8 changed files with 1,278 additions and 1,495 deletions.
21 changes: 13 additions & 8 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
on:
pull_request
name: Checks

env:
# Issues with `poetry install` on windows with python 3.9 otherwise.
POETRY_VERSION: "1.3.2"

jobs:
style:
name: Style Checks
Expand All @@ -15,7 +20,7 @@ jobs:
${{ runner.os }}-poetry-
- name: Install Dependencies
run: |
pip install poetry
pip install poetry==${{ env.POETRY_VERSION }}
poetry config virtualenvs.in-project true
poetry install
- name: Style Checks
Expand All @@ -33,7 +38,7 @@ jobs:
${{ runner.os }}-poetry-
- name: Install Dependencies
run: |
pip install poetry
pip install poetry==${{ env.POETRY_VERSION }}
poetry config virtualenvs.in-project true
poetry install
- name: Lint Checks
Expand All @@ -47,10 +52,10 @@ jobs:
- windows-latest
- macos-latest
python:
- 3.7
- 3.8
- 3.9
- "3.10"
- "3.11"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand All @@ -65,13 +70,13 @@ jobs:
${{ runner.os }}-python-${{ matrix.python }}-poetry-
- name: Install Dependencies
run: |
pip install poetry
pip install poetry==${{ env.POETRY_VERSION }}
poetry config virtualenvs.in-project true
poetry install
- name: Test
run: poetry run make test-no-qcs
env:
TEST_QUANTUM_PROCESSOR: Aspen-M-2
TEST_QUANTUM_PROCESSOR: Aspen-M-3
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
Expand All @@ -84,10 +89,10 @@ jobs:
strategy:
matrix:
python:
- 3.7
- 3.8
- 3.9
- "3.10"
- "3.11"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -102,7 +107,7 @@ jobs:
${{ runner.os }}-python-${{ matrix.python }}-poetry-
- name: Install Dependencies
run: |
pip install poetry
pip install poetry==${{ env.POETRY_VERSION }}
poetry config virtualenvs.in-project true
poetry install
- name: Setup QCS Settings
Expand All @@ -115,7 +120,7 @@ jobs:
- name: Test
run: poetry run make test-requires-qcs
env:
TEST_QUANTUM_PROCESSOR: Aspen-M-2
TEST_QUANTUM_PROCESSOR: Aspen-M-3
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea/
.venv/
.env
__pycache__/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ compiled = qpu.compile(program)
memory_map = {"theta": [[0.0], [np.pi], [2 * np.pi]]}
results = qpu.run_batch(compiled, memory_map) # This is a list of results, one for each parameter set.

results_0 = results[0].readout_data["ro"]
results_pi = results[1].readout_data["ro"]
results_2pi = results[2].readout_data["ro"]
results_0 = results[0].get_register_map().get("ro")
results_pi = results[1].get_register_map().get("ro")
results_2pi = results[2].get_register_map().get("ro")
```

> Microsoft, Microsoft Azure, and Azure Quantum are trademarks of the Microsoft group of companies.
Expand Down
2,619 changes: 1,203 additions & 1,416 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ classifiers = [
]

[tool.poetry.dependencies]
python = ">=3.7,<3.11"
pyquil = { version = "^3.1.0", allow-prereleases = true }
python = ">=3.8,<3.12"
pyquil = "^4.0.0"
azure-quantum = ">=0.27,<1.0"
lazy-object-proxy = "^1.7.1"
wrapt = "^1.14.0"
Expand All @@ -25,7 +25,7 @@ scipy = "^1.6.1"

[tool.poetry.dev-dependencies]
pytest = "^7.1.1"
mypy = "^0.950"
mypy = "^0.991"
isort = "^5.10.1"
black = "^22.3.0"
pylint = "^2.13.7"
Expand Down
104 changes: 47 additions & 57 deletions pyquil_for_azure_quantum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@

from dataclasses import dataclass
from os import environ
from typing import Dict, List, Optional, Tuple, Union, cast
from typing import Any, Dict, List, Optional, Union, cast

from azure.quantum import Job, Workspace
from azure.quantum.target.rigetti import InputParams, Result, Rigetti
from azure.quantum.target.rigetti import InputParams, Result, Rigetti, RigettiTarget
from lazy_object_proxy import Proxy
from numpy import array, split
from pyquil.api import QAM, QAMExecutionResult, QuantumComputer, get_qc
from numpy import split
from pyquil.api import QAM, MemoryMap, QAMExecutionResult, QuantumComputer, get_qc
from pyquil.quil import Program
from qcs_sdk import ExecutionData, RegisterData, ResultData # pylint: disable=no-name-in-module
from qcs_sdk.qvm import QVMResultData # pylint: disable=no-name-in-module
from wrapt import ObjectProxy

ParamValue = Union[int, float]
Expand All @@ -46,11 +48,9 @@ def __init__(
self,
program: Program,
skip_quilc: bool,
memory_map: Optional[Dict[str, List[List[ParamValue]]]] = None,
) -> None:
super().__init__(program)
self.skip_quilc = skip_quilc
self.memory_map = memory_map

def copy(self) -> "AzureProgram":
"""Perform a shallow copy of this program.
Expand All @@ -60,35 +60,6 @@ def copy(self) -> "AzureProgram":
"""
return AzureProgram(self.__wrapped__.copy(), self.skip_quilc)

def get_memory(self) -> Optional[Dict[str, List[List[ParamValue]]]]:
"""Retrieve the memory map for this program formatted in the way that Azure expects"""
if self.memory_map is not None:
return self.memory_map
memory = self._memory.values
if len(memory) == 0:
return None
memory_indexes_and_values_per_name: Dict[str, List[Tuple[int, ParamValue]]] = {}
for ref, value in memory.items():
if ref.name not in memory_indexes_and_values_per_name:
memory_indexes_and_values_per_name[ref.name] = []
memory_indexes_and_values_per_name[ref.name].append((ref.index, value))
memory_indexes_and_values_per_name = {
k: sorted(v, key=lambda x: x[0]) for k, v in memory_indexes_and_values_per_name.items()
}
substitutions: Dict[str, List[List[float]]] = {}
for name, indexes_and_values in memory_indexes_and_values_per_name.items():
values = []
expected_index = 0
for index, value in indexes_and_values:
while index != expected_index:
# Pad missing values with zeros, just like pyquil does when patching
values.append(0.0)
expected_index += 1
values.append(value)
expected_index += 1
substitutions[name] = [values]
return substitutions


# pylint: disable-next=too-few-public-methods
class AzureQuantumComputer(QuantumComputer):
Expand All @@ -101,7 +72,7 @@ def __init__(self, *, target: str, qpu_name: str):
compiler = Proxy(lambda: get_qc(qpu_name).compiler)
super().__init__(name=qpu_name, qam=qam, compiler=compiler)

# pylint: disable-next=no-self-use,unused-argument
# pylint: disable-next=unused-argument
def compile(
self,
program: Program,
Expand Down Expand Up @@ -182,7 +153,7 @@ def get_qvm() -> AzureQuantumComputer:
Raises:
KeyError: If required environment variables are not set.
"""
return AzureQuantumComputer(target="rigetti.sim.qvm", qpu_name="qvm")
return AzureQuantumComputer(target=RigettiTarget.QVM.value, qpu_name="qvm")


@dataclass
Expand Down Expand Up @@ -225,12 +196,13 @@ def __init__(self, *, target: str) -> None:
name=target,
)

# pylint: disable-next=useless-super-delegation
def run(self, executable: AzureProgram) -> QAMExecutionResult: # type: ignore[override]
"""Run the executable and wait for its results"""
return super().run(executable)

def execute(self, executable: AzureProgram, name: str = "pyquil-azure-job") -> AzureJob: # type: ignore[override]
def execute( # type: ignore[override]
self,
executable: AzureProgram,
memory_map: Optional[MemoryMap] = None,
name: str = "pyquil-azure-job",
**_kwargs: Any, # unused, but defined here to match QAM superclass.
) -> AzureJob:
"""Run an AzureProgram on Azure Quantum. Unlike normal QAM this does not accept a ``QuantumExecutable``.
You should build the ``AzureProgram`` via ``AzureQuantumComputer.compile``.
Expand All @@ -247,19 +219,15 @@ def execute(self, executable: AzureProgram, name: str = "pyquil-azure-job") -> A
input_params = InputParams(
count=executable.num_shots,
skip_quilc=executable.skip_quilc,
substitutions={k: [v] for k, v in memory_map.items()} if memory_map is not None else None,
)
memory = executable.get_memory()
if memory is not None:
input_params.substitutions = memory

job = self._target.submit(
str(executable),
name=name,
input_params=input_params,
)
return AzureJob(job=job, executable=executable)

# pylint: disable-next=no-self-use
def get_result(self, execute_response: AzureJob) -> QAMExecutionResult:
"""Wait for a ``Job`` to complete, then return the results
Expand All @@ -269,10 +237,18 @@ def get_result(self, execute_response: AzureJob) -> QAMExecutionResult:
job = execute_response.job
job.wait_until_completed()
result = Result(job)
numpified = {k: array(v) for k, v in result.data_per_register.items()}

# pylint: disable-next=fixme
# TODO: as of https://github.com/microsoft/qdk-python/blob/4d6f7f75c8c7d8467f87936b1aaef449de1e0bf6/azure-quantum/azure/quantum/target/rigetti/result.py#L47
# both QVM and QC result shapes take the memory-map form as in the QVMResultData.
# When the Rigetti target returns results with mappings, the QPUResultData can be constructed.
memory = {k: RegisterData(v) for k, v in result.data_per_register.items()}
result_data = ResultData.from_qvm(QVMResultData.from_memory_map(memory=memory))

data = ExecutionData(result_data=result_data)
return QAMExecutionResult(
executable=execute_response.executable,
readout_data=numpified,
data=data,
)

def run_batch(
Expand Down Expand Up @@ -329,20 +305,34 @@ def run_batch(
f"{param_name} has length {len(param_values)} but {num_params} were expected."
)

executable = executable.copy()
input_params = InputParams(
count=executable.num_shots, skip_quilc=executable.skip_quilc, substitutions=memory_map
count=executable.num_shots,
skip_quilc=executable.skip_quilc,
substitutions=memory_map,
)

job = self._target.submit(
str(executable),
name=name,
input_params=input_params,
)
combined_result = self.get_result(AzureJob(job, executable))
azure_job = AzureJob(job=job, executable=executable)
combined_result = self.get_result(azure_job)
if num_params is None or num_params == 1:
return [combined_result]
split_results = split(combined_result.readout_data["ro"], num_params) # type: ignore
return [QAMExecutionResult(executable, {"ro": result}) for result in split_results]


_RawData = Union[int, float, List[float]]
ro_matrix = combined_result.data.result_data.to_register_map().get_register_matrix("ro")
if ro_matrix is None:
return []

split_results = split(ro_matrix.to_ndarray(), num_params)
output = [
QAMExecutionResult(
executable,
ExecutionData(
ResultData.from_qvm(QVMResultData.from_memory_map(memory={"ro": RegisterData(result.tolist())}))
),
)
for result in split_results
]
return output
2 changes: 1 addition & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def qpu() -> AzureQuantumComputer:
quantum_processor_id = environ.get("TEST_QUANTUM_PROCESSOR")

if quantum_processor_id is None:
raise Exception("'TEST_QUANTUM_PROCESSOR' env var required for e2e tests.")
raise ValueError("'TEST_QUANTUM_PROCESSOR' env var required for e2e tests.")

return get_qpu(
quantum_processor_id,
Expand Down
14 changes: 7 additions & 7 deletions test/test_e2e_no_qcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
# pylint: disable-next=invalid-name
def test_basic_program(qc: AzureQuantumComputer, basic_program: Program) -> None:
"""A smoke test of a very basic program running through Azure Quantum with minimal assertions"""
results = qc.run(qc.compile(basic_program)).readout_data["ro"]
results = qc.run(qc.compile(basic_program)).get_register_map().get("ro")
assert results is not None
assert results.shape == (1000, 2)

Expand All @@ -43,8 +43,8 @@ def test_parametric_program(qc: AzureQuantumComputer) -> None:

all_results = []
for theta in [0, np.pi, 2 * np.pi]:
compiled.write_memory(region_name="theta", value=theta)
results = qc.run(compiled).readout_data["ro"]
memory_map: Dict[str, List[float]] = {"theta": [theta]}
results = qc.run(executable=compiled, memory_map=memory_map).get_register_map().get("ro")
assert results is not None
all_results.append(np.mean(results))

Expand All @@ -60,7 +60,7 @@ def test_parametric_program(qc: AzureQuantumComputer) -> None:

def test_quil_t(qpu: AzureQuantumComputer) -> None:
"""Test skipping ``quilc`` on the backend by passing a program which will not compile (Quil-T)"""
results = qpu.run(qpu.compile(QUIL_T_PROGRAM, to_native_gates=False)).readout_data["ro"]
results = qpu.run(qpu.compile(QUIL_T_PROGRAM, to_native_gates=False)).get_register_map().get("ro")
assert results is not None

assert np.mean(results) > 0.5
Expand All @@ -74,13 +74,13 @@ def test_run_batch(qc: AzureQuantumComputer) -> None:
memory_map: Dict[str, List[List[float]]] = {"theta": [[0], [np.pi], [2 * np.pi]]}
results = qc.run_batch(compiled, memory_map)

results_0 = results[0].readout_data["ro"]
results_0 = results[0].get_register_map().get("ro")
assert results_0 is not None
results_0_mean = np.mean(results_0)
results_pi = results[1].readout_data["ro"]
results_pi = results[1].get_register_map().get("ro")
assert results_pi is not None
results_pi_mean = np.mean(results_pi)
results_2pi = results[2].readout_data["ro"]
results_2pi = results[2].get_register_map().get("ro")
assert results_2pi is not None
results_2pi_mean = np.mean(results_2pi)
if qc.name == "qvm":
Expand Down

0 comments on commit f289cac

Please sign in to comment.