diff --git a/qiskit_ionq/helpers.py b/qiskit_ionq/helpers.py index 1b7e89b..726f826 100644 --- a/qiskit_ionq/helpers.py +++ b/qiskit_ionq/helpers.py @@ -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 @@ -423,7 +426,9 @@ 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.get( + "error_mitigation" + ) if error_mitigation and isinstance(error_mitigation, ErrorMitigation): ionq_json["error_mitigation"] = error_mitigation.value return json.dumps(ionq_json, cls=SafeEncoder) @@ -474,10 +479,74 @@ 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, _fallback=100) -> 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() + # could use provider.get_calibration_data().get("qubits", 36) + try: + return requests.get( + url=f"{url}/characterizations/backends/{backend}/current", + headers={"Authorization": f"apiKey {token}"}, + timeout=5, + ).json()["qubits"] + except Exception as exception: # pylint: disable=broad-except + warnings.warn( + f"Unable to get qubit count for {backend}: {exception}. Defaulting to {_fallback}." + ) + return _fallback + + __all__ = [ "qiskit_to_ionq", "qiskit_circ_to_ionq_circ", "compress_to_metadata_string", "decompress_metadata_string", "get_user_agent", + "resolve_credentials", + "get_n_qubits", ] diff --git a/qiskit_ionq/ionq_backend.py b/qiskit_ionq/ionq_backend.py index df5be38..35fc7d0 100644 --- a/qiskit_ionq/ionq_backend.py +++ b/qiskit_ionq/ionq_backend.py @@ -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: @@ -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, @@ -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, diff --git a/qiskit_ionq/ionq_provider.py b/qiskit_ionq/ionq_provider.py index faf900f..24fd8ee 100644 --- a/qiskit_ionq/ionq_provider.py +++ b/qiskit_ionq/ionq_provider.py @@ -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 diff --git a/qiskit_ionq/version.py b/qiskit_ionq/version.py index 7dc5cf2..2148a24 100644 --- a/qiskit_ionq/version.py +++ b/qiskit_ionq/version.py @@ -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: diff --git a/test/conftest.py b/test/conftest.py index 4d88fd1..2f0012a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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 @@ -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", diff --git a/test/ionq_backend/test_base_backend.py b/test/ionq_backend/test_base_backend.py index 589924d..46da08c 100644 --- a/test/ionq_backend/test_base_backend.py +++ b/test/ionq_backend/test_base_backend.py @@ -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):