Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get num of qubits from api or use 36 and fix EM sampler #188

Merged
merged 11 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion qiskit_ionq/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
import base64
import platform
import warnings
import os
import requests
from dotenv import dotenv_values

from qiskit import __version__ as qiskit_terra_version
from qiskit.circuit import controlledgate as q_cgates
Expand Down Expand Up @@ -423,7 +426,7 @@ def qiskit_to_ionq(
settings = passed_args.get("job_settings") or None
if settings is not None:
ionq_json["settings"] = settings
error_mitigation = passed_args.get("error_mitigation")
error_mitigation = passed_args.get("error_mitigation") or backend.options.error_mitigation
if error_mitigation and isinstance(error_mitigation, ErrorMitigation):
ionq_json["error_mitigation"] = error_mitigation.value
return json.dumps(ionq_json, cls=SafeEncoder)
Expand Down Expand Up @@ -474,6 +477,73 @@ def default(self, o):
return "unknown"



def resolve_credentials(token: str = None, url: str = None):
"""Resolve credentials for use in IonQ API calls.

If the provided ``token`` and ``url`` are both ``None``, then these values
are loaded from the ``IONQ_API_TOKEN`` and ``IONQ_API_URL``
environment variables, respectively.

If no url is discovered, then ``https://api.ionq.co/v0.3`` is used.

Args:
token (str): IonQ API access token.
url (str, optional): IonQ API url. Defaults to ``None``.

Returns:
dict[str]: A dict with "token" and "url" keys, for use by a client.
"""
env_token = (
dotenv_values().get("QISKIT_IONQ_API_TOKEN") # first check for dotenv values
or dotenv_values().get("IONQ_API_KEY")
or dotenv_values().get("IONQ_API_TOKEN")
or os.getenv("QISKIT_IONQ_API_TOKEN") # then check for global env values
or os.getenv("IONQ_API_KEY")
or os.getenv("IONQ_API_TOKEN")
)
env_url = (
dotenv_values().get("QISKIT_IONQ_API_URL")
or dotenv_values().get("IONQ_API_URL")
or os.getenv("QISKIT_IONQ_API_URL")
or os.getenv("IONQ_API_URL")
)
return {
"token": token or env_token,
"url": url or env_url or "https://api.ionq.co/v0.3",
}



def get_n_qubits(backend: str) -> int:
"""Get the number of qubits for a given backend.

Args:
backend (str): The name of the backend.

Returns:
int: The number of qubits for the backend.
"""
url, token = resolve_credentials().values()
try:
return requests.get(
url=f"{url}/characterizations/backends/{backend}/current",
headers={"Authorization": f"apiKey {token}"},
timeout=5,
).json()["qubits"]
except Exception: # pylint: disable=broad-except
if backend == "ionq_qpu.harmony":
return 11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this? falling back to a hard-coded value when the API is down feels a little weird

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the user might not have an api_key set, and we require that to get characterization data (including the number of qubits)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I meant more, the part where we code in the current system sizes, would worry it could quickly become stale, like the new method with warning and a fixed cap, thanks for changing!

elif backend == "ionq_qpu.aria-1":
return 25
elif backend == "ionq_qpu.aria-2":
return 25
elif backend == "ionq_qpu.forte-1":
return 36
else:
return 36
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have a hard time understanding what this code was doing, since it falls back during an exception but doesn't' log it or what value it's deciding. Could you add logs here? (or alternatively, fall back to Infinity or something >> 36?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't do inifinity because the transpiler freezes (i guess it tries to create a graph of length #qubits lol



__all__ = [
"qiskit_to_ionq",
"qiskit_circ_to_ionq_circ",
Expand Down
8 changes: 4 additions & 4 deletions qiskit_ionq/ionq_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
import warnings

from qiskit.providers import BackendV1 as Backend
from qiskit.providers.models import BackendConfiguration
from qiskit.providers.models.backendconfiguration import BackendConfiguration
from qiskit.providers.models.backendstatus import BackendStatus
from qiskit.providers import Options

from . import exceptions, ionq_client, ionq_job, ionq_equivalence_library
from .helpers import GATESET_MAP
from .helpers import GATESET_MAP, get_n_qubits


class Calibration:
Expand Down Expand Up @@ -407,7 +407,7 @@ def __init__(self, provider, name="simulator", gateset="qis"):
"basis_gates": GATESET_MAP[gateset],
"memory": False,
# Varied based on noise model, but enforced server-side.
"n_qubits": 35,
"n_qubits": get_n_qubits(name),
"conditional": False,
"max_shots": 1,
"max_experiments": 1,
Expand Down Expand Up @@ -482,7 +482,7 @@ def __init__(self, provider, name="ionq_qpu", gateset="qis"):
# This is a generic backend for all IonQ hardware, the server will do more specific
# qubit count checks. In the future, dynamic backend configuration from the server
# will be used in place of these hard-coded caps.
"n_qubits": 35,
"n_qubits": get_n_qubits(name),
"conditional": False,
"max_shots": 10000,
"max_experiments": 1,
Expand Down
38 changes: 1 addition & 37 deletions qiskit_ionq/ionq_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,52 +27,16 @@
"""Provider for interacting with IonQ backends"""

import logging
import os
from dotenv import dotenv_values

from qiskit.providers.exceptions import QiskitBackendNotFoundError
from qiskit.providers.providerutils import filter_backends
from .helpers import resolve_credentials

from . import ionq_backend

logger = logging.getLogger(__name__)


def resolve_credentials(token: str = None, url: str = None):
"""Resolve credentials for use in IonQ Client API calls.

If the provided ``token`` and ``url`` are both ``None``, then these values
are loaded from the ``IONQ_API_TOKEN`` and ``IONQ_API_URL``
environment variables, respectively.

If no url is discovered, then ``https://api.ionq.co/v0.3`` is used.

Args:
token (str): IonQ API access token.
url (str, optional): IonQ API url. Defaults to ``None``.

Returns:
dict[str]: A dict with "token" and "url" keys, for use by a client.
"""
env_token = (
dotenv_values().get("QISKIT_IONQ_API_TOKEN") # first check for dotenv values
or dotenv_values().get("IONQ_API_KEY")
or dotenv_values().get("IONQ_API_TOKEN")
or os.getenv("QISKIT_IONQ_API_TOKEN") # then check for global env values
or os.getenv("IONQ_API_KEY")
or os.getenv("IONQ_API_TOKEN")
)
env_url = (
dotenv_values().get("QISKIT_IONQ_API_URL")
or dotenv_values().get("IONQ_API_URL")
or os.getenv("QISKIT_IONQ_API_URL")
or os.getenv("IONQ_API_URL")
)
return {
"token": token or env_token,
"url": url or env_url or "https://api.ionq.co/v0.3",
}


class IonQProvider:
"""Provider for interacting with IonQ backends
Expand Down
2 changes: 1 addition & 1 deletion qiskit_ionq/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
pkg_parent = pathlib.Path(__file__).parent.parent.absolute()

# major, minor, patch
VERSION_INFO = ".".join(map(str, (0, 5, 4)))
VERSION_INFO = ".".join(map(str, (0, 5, 5)))


def _minimal_ext_cmd(cmd: List[str]) -> bytes:
Expand Down
4 changes: 2 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"""global pytest fixtures"""
import pytest
import requests_mock as _requests_mock
from qiskit.providers import models as q_models
from qiskit.providers.models.backendconfiguration import BackendConfiguration
from requests_mock import adapter as rm_adapter

from qiskit_ionq import ionq_backend, ionq_job, ionq_provider
Expand All @@ -43,7 +43,7 @@ def gateset(self):
def __init__(
self, provider, name="ionq_mock_backend"
): # pylint: disable=redefined-outer-name
config = q_models.BackendConfiguration.from_dict(
config = BackendConfiguration.from_dict(
{
"backend_name": name,
"backend_version": "0.0.1",
Expand Down
3 changes: 1 addition & 2 deletions test/ionq_backend/test_base_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,7 @@ def test_warn_null_mappings(mock_backend, requests_mock):

with pytest.warns(UserWarning) as warninfo:
mock_backend.run(qc)
assert len(warninfo) == 1
assert str(warninfo[-1].message) == "Circuit is not measuring any qubits"
assert "Circuit is not measuring any qubits" in {str(w.message) for w in warninfo}


def test_multiexp_job(mock_backend, requests_mock):
Expand Down
Loading