Skip to content

Commit

Permalink
Trim ionq_ prefix from backends lookup (#196)
Browse files Browse the repository at this point in the history
* Trim ionq_ prefix from backends lookup

@splch do you think we could target having tests added for this before releasing 0.5.6? I'm a little uncomfortable with it given the hiccups re: launch

* Update helpers.py

* Update helpers.py

* add test for get_n_qubits

* fix ionq_qpu. 8 length

* add url and headers to tests

* fix linting

* fix tests (add pytest to tox)

* fix 3.9 typing

* fix precommit typing error

* ignore the 6/5 pos arg errors

* fix pre-commit length error

---------

Co-authored-by: Spencer Churchill <[email protected]>
  • Loading branch information
Cynocracy and splch authored Sep 20, 2024
1 parent 1661652 commit 97c7ae4
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 20 deletions.
2 changes: 1 addition & 1 deletion qiskit_ionq/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def from_response(cls, response: requests.Response) -> IonQAPIError:
error_type = error_data.get("type") or error_type
return cls(message, status_code, headers, body, error_type)

def __init__(self, message, status_code, headers, body, error_type):
def __init__(self, message, status_code, headers, body, error_type): # pylint: disable=too-many-positional-arguments
super().__init__(message)
self.status_code = status_code
self.headers = headers
Expand Down
15 changes: 12 additions & 3 deletions qiskit_ionq/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,9 @@ def get_register_sizes_and_labels(
return sizes, labels


def compress_to_metadata_string(metadata: dict | list) -> str: # pylint: disable=invalid-name
def compress_to_metadata_string(
metadata: dict | list,
) -> str: # pylint: disable=invalid-name
"""
Convert a metadata object to a compact string format (dumped, gzipped, base64 encoded)
for storing in IonQ API metadata
Expand All @@ -330,7 +332,9 @@ def compress_to_metadata_string(metadata: dict | list) -> str: # pylint: disabl
return encoded.decode()


def decompress_metadata_string(input_string: str) -> dict | list: # pylint: disable=invalid-name
def decompress_metadata_string(
input_string: str,
) -> dict | list: # pylint: disable=invalid-name
"""
Convert compact string format (dumped, gzipped, base64 encoded) from
IonQ API metadata back into a dict or list of dicts relevant to building
Expand Down Expand Up @@ -541,8 +545,13 @@ def get_n_qubits(backend: str, _fallback=100) -> int:
token = creds.get("token")
# could use provider.get_calibration_data().get("qubits", 36)
try:
target = (
backend.split("ionq_qpu.")[1]
if backend.startswith("ionq_qpu.")
else backend
)
return requests.get(
url=f"{url}/characterizations/backends/{backend}/current",
url=f"{url}/characterizations/backends/{target}/current",
headers={"Authorization": f"apiKey {token}"},
timeout=5,
).json()["qubits"]
Expand Down
6 changes: 3 additions & 3 deletions qiskit_ionq/ionq_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class IonQClient:

def __init__(
self,
token: str | None = None,
url: str | None = None,
custom_headers: dict | None = None,
token: Optional[str] = None,
url: Optional[str] = None,
custom_headers: Optional[dict] = None,
):
self._token = token
self._custom_headers = custom_headers or {}
Expand Down
19 changes: 11 additions & 8 deletions qiskit_ionq/ionq_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from __future__ import annotations

import warnings
from typing import Any, Union, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Union, Optional
import numpy as np

from qiskit import QuantumCircuit
Expand Down Expand Up @@ -81,7 +81,7 @@ def get_bitvalue(bitstring, bit):

def _build_counts(
data, num_qubits, clbits, shots, use_sampler=False, sampler_seed=None
):
): # pylint: disable=too-many-positional-arguments
"""Map IonQ's ``counts`` onto qiskit's ``counts`` model.
.. NOTE:: For simulator jobs, this method builds counts using a randomly
Expand Down Expand Up @@ -163,11 +163,14 @@ class IonQJob(JobV1):
def __init__(
self,
backend: ionq_backend.IonQBackend,
job_id: str | None,
client: ionq_client.IonQClient | None = None,
circuit: QuantumCircuit | None = None,
passed_args: dict | None = None,
):
job_id: Optional[str] = None,
client: Optional[ionq_client.IonQClient] = None,
circuit: Optional[QuantumCircuit] = None,
passed_args: Optional[dict] = None,
): # pylint: disable=too-many-positional-arguments
assert (
job_id is not None or circuit is not None
), "Job must have a job_id or circuit"
super().__init__(backend, job_id)
self._client = client or backend.client
self._result = None
Expand Down Expand Up @@ -214,7 +217,7 @@ def submit(self) -> None:
response = self._client.submit_job(job=self)
self._job_id = response["id"]

def get_counts(self, circuit: QuantumCircuit | None = None) -> dict:
def get_counts(self, circuit: Optional[QuantumCircuit] = None) -> dict:
"""Return the counts for the job.
.. ATTENTION::
Expand Down
10 changes: 5 additions & 5 deletions qiskit_ionq/ionq_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

import logging

from typing import Callable, Literal
from typing import Callable, Literal, Optional

from qiskit.providers.exceptions import QiskitBackendNotFoundError
from qiskit.providers.providerutils import filter_backends
Expand All @@ -55,9 +55,9 @@ class IonQProvider:

def __init__(
self,
token: str | None = None,
url: str | None = None,
custom_headers: dict | None = None,
token: Optional[str] = None,
url: Optional[str] = None,
custom_headers: Optional[dict] = None,
):
super().__init__()
self.custom_headers = custom_headers
Expand Down Expand Up @@ -112,7 +112,7 @@ def __init__(self, backends: list[ionq_backend.Backend]):
setattr(self, backend.name(), backend)

def __call__(
self, name: str | None = None, filters: Callable | None = None, **kwargs
self, name: Optional[str] = None, filters: Optional[Callable] = None, **kwargs
) -> list[ionq_backend.Backend]:
"""A listing of all backends from this provider.
Expand Down
68 changes: 68 additions & 0 deletions test/helpers/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"""Test the helper functions."""

import re
from unittest.mock import patch, MagicMock
from qiskit_ionq.ionq_client import IonQClient
from qiskit_ionq.helpers import get_n_qubits


def test_user_agent_header():
Expand All @@ -47,3 +49,69 @@ def test_user_agent_header():
# Checks whether there is at-least 3 version strings from qiskit-ionq, qiskit-terra, python.
has_all_version_strings = len(re.findall(r"\s*([\d.]+)", generated_user_agent)) >= 3
assert all_user_agent_keywords_avail and has_all_version_strings


def test_get_n_qubits_success():
"""Test get_n_qubits returns correct number of qubits and checks correct URL."""
with patch("requests.get") as mock_get:
mock_response = MagicMock()
mock_response.json.return_value = {"qubits": 11}
mock_get.return_value = mock_response

backend = "ionq_qpu.aria-1"
result = get_n_qubits(backend)

expected_url = (
"https://api.ionq.co/v0.3/characterizations/backends/aria-1/current"
)

# Create a regular expression to match the Authorization header with an apiKey
expected_headers = {"Authorization": re.compile(r"apiKey\s+\S+")}

# Check the arguments of the last call to `requests.get`
mock_get.assert_called()
_, kwargs = mock_get.call_args
assert (
kwargs["url"] == expected_url
), f"Expected URL {expected_url}, but got {kwargs['url']}"

# Assert that the headers contain the apiKey in the expected format
assert re.match(
expected_headers["Authorization"], kwargs["headers"]["Authorization"]
), (
f"Expected headers to match {expected_headers['Authorization'].pattern}, "
f"but got {kwargs['headers']['Authorization']}"
)

assert result == 11, f"Expected 11 qubits, but got {result}"


def test_get_n_qubits_fallback():
"""Test get_n_qubits returns fallback number of qubits and checks correct URL on failure."""
with patch("requests.get", side_effect=Exception("Network error")) as mock_get:
backend = "aria-1"
result = get_n_qubits(backend)

expected_url = (
"https://api.ionq.co/v0.3/characterizations/backends/aria-1/current"
)

# Create a regular expression to match the Authorization header with an apiKey
expected_headers = {"Authorization": re.compile(r"apiKey\s+\S+")}

# Check the arguments of the last call to `requests.get`
mock_get.assert_called()
_, kwargs = mock_get.call_args
assert (
kwargs["url"] == expected_url
), f"Expected URL {expected_url}, but got {kwargs['url']}"

# Assert that the headers contain the apiKey in the expected format
assert re.match(
expected_headers["Authorization"], kwargs["headers"]["Authorization"]
), (
f"Expected headers to match {expected_headers['Authorization'].pattern}, "
f"but got {kwargs['headers']['Authorization']}"
)

assert result == 100, f"Expected fallback of 100 qubits, but got {result}"
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ setenv =
VIRTUAL_ENV={envdir}
LANGUAGE=en_US
LC_ALL=en_US.utf-8
commands = pytest

[testenv:lint]
deps =
Expand Down

0 comments on commit 97c7ae4

Please sign in to comment.