Source code for qiskit_ionq.ionq_backend

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2018.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# Copyright 2020 IonQ, Inc. (www.ionq.com)
#
# 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.

"""IonQ provider backends."""

from __future__ import annotations

import abc
from datetime import datetime
from typing import Literal, TYPE_CHECKING
import warnings

from qiskit.circuit import QuantumCircuit
from qiskit.providers import BackendV1 as Backend
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, get_n_qubits

if TYPE_CHECKING:
    from .ionq_provider import IonQProvider


class Calibration:
    """
    IonQ backend calibration data.

    This class is a simple wrapper for IonQ hardware calibration data.
    """

    def __init__(self, data):
        self._data = data

    @property
    def uuid(self) -> str:
        """The ID of the calibration.

        Returns:
            str: The ID.
        """
        return self._data["id"]

    @property
    def num_qubits(self) -> int:
        """The number of qubits available.

        Returns:
            int: A number of qubits.
        """
        return int(self._data["qubits"])

    @property
    def target(self) -> str:
        """The target calibrated hardware.

        Returns:
            str: The name of the target hardware backend.
        """
        return self._data["backend"]

    @property
    def calibration_time(self) -> datetime:
        """Time of the measurement, in UTC.

        Returns:
            datetime.datetime: A datetime object with the time.
        """
        return datetime.fromtimestamp(self._data["date"])

    @property
    def fidelities(self) -> dict:
        """Fidelity for single-qubit (1q) and two-qubit (2q) gates, and State
        Preparation and Measurement (spam) operations.

        Currently provides only mean fidelity; additional statistical data will
        be added in the future.

        Returns:
            dict: A dict containing fidelity data for 1a, 2q, and spam.
        """
        return self._data["fidelity"]

    @property
    def timings(self) -> dict:
        """Various system property timings. All times expressed as seconds.

        Timings currently include::

            * ``t1``
            * ``t2``
            * ``1q``
            * ``2q``
            * ``readout``
            * ``reset``

        Returns:
            dict: A dictionary of timings.
        """
        return self._data["timing"]

    @property
    def connectivity(self) -> list[tuple[int, int]]:
        """Returns connectivity data.

        Returns:
            list[tuple[int, int]]: An array of valid, unordered tuples of
                possible qubits for executing two-qubit gates
        """
        return self._data["connectivity"]


[docs] class IonQBackend(Backend): """IonQ Backend base class.""" _client = None def __init__(self, *args, **kwargs) -> None: # Add IonQ equivalences ionq_equivalence_library.add_equivalences() super().__init__(*args, **kwargs) @classmethod def _default_options(cls) -> Options: return Options( shots=1024, job_settings=None, error_mitigation=None, extra_query_params={}, extra_metadata={}, ) @property def client(self) -> ionq_client.IonQClient: """A lazily populated IonQ API Client. Returns: IonQClient: An instance of a REST API client """ if self._client is None: self._client = self.create_client() return self._client
[docs] def create_client(self) -> ionq_client.IonQClient: """Create an IonQ REST API Client using provider credentials. Raises: IonQCredentialsError: If the provider's :attr:`credentials <IonQProvider.credentials>` does not have a ``"token"`` or ``"url"`` key, or if their values are ``None``. Returns: IonQClient: An instance of a REST API client. """ credentials = self._provider.credentials try: token = credentials["token"] except KeyError as ex: raise exceptions.IonQCredentialsError( "Credentials `token` not present in provider." ) from ex if token is None: raise exceptions.IonQCredentialsError( "Credentials `token` may not be None!" ) try: url = credentials["url"] except KeyError as ex: raise exceptions.IonQCredentialsError( "Credentials `url` not present in provider." ) from ex if url is None: raise exceptions.IonQCredentialsError("Credentials `url` may not be None!") return ionq_client.IonQClient(token, url, self._provider.custom_headers)
# pylint: disable=missing-type-doc,missing-param-doc,arguments-differ,arguments-renamed
[docs] def run(self, circuit: QuantumCircuit, **kwargs) -> ionq_job.IonQJob: """Create and run a job on an IonQ Backend. Args: circuit (:class:`QuantumCircuit <qiskit.circuit.QuantumCircuit>`): A Qiskit QuantumCircuit object. Returns: IonQJob: A reference to the job that was submitted. """ if not all( ( self.has_valid_mapping(circ) for circ in (circuit if isinstance(circuit, list) else [circuit]) ) ): warnings.warn( "Circuit is not measuring any qubits", UserWarning, stacklevel=2, ) for kwarg in kwargs: if not hasattr(self.options, kwarg): warnings.warn( f"Option {kwarg} is not used by this backend", UserWarning, stacklevel=2, ) if "shots" not in kwargs: kwargs["shots"] = self.options.shots # TODO: Should we merge the two maps, or warn if both are set? if "job_settings" not in kwargs: kwargs["job_settings"] = self.options.job_settings elif self.options.job_settings is not None: warnings.warn( ( "Option job_settings is set on the backend, and on the request. " "Ignoring the backend specified option." ), UserWarning, stacklevel=2, ) passed_args = kwargs job = ionq_job.IonQJob( self, None, self.client, circuit=circuit, passed_args=passed_args, ) job.submit() return job
[docs] def retrieve_job(self, job_id: str) -> ionq_job.IonQJob: """get a job from a specific backend, by job id.""" return ionq_job.IonQJob(self, job_id, self.client)
[docs] def retrieve_jobs(self, job_ids: list[str]) -> list[ionq_job.IonQJob]: """get a list of jobs from a specific backend, job id""" return [ionq_job.IonQJob(self, job_id, self.client) for job_id in job_ids]
[docs] def cancel_job(self, job_id: str) -> dict: """cancels a job from a specific backend, by job id.""" return self.client.cancel_job(job_id)
[docs] def cancel_jobs(self, job_ids: list[str]) -> list[dict]: """cancels a list of jobs from a specific backend, job id""" return [self.client.cancel_job(job_id) for job_id in job_ids]
[docs] def has_valid_mapping(self, circuit: QuantumCircuit) -> bool: """checks if the circuit has at least one valid qubit -> bit measurement. Args: circuit (:class:`QuantumCircuit <qiskit.circuit.QuantumCircuit>`): A Qiskit QuantumCircuit object. Returns: boolean: if the circuit has valid mappings """ # Check if a qubit is measured for instruction, _, cargs in circuit.data: if instruction.name == "measure" and len(cargs): return True # If no mappings are found, return False return False
# TODO: Implement backend status checks.
[docs] def status(self) -> BackendStatus: """Return a backend status object to the caller. Returns: BackendStatus: the status of the backend. """ return BackendStatus( backend_name=self.name(), backend_version="1", operational=True, pending_jobs=0, status_msg="", )
[docs] def calibration(self) -> Calibration | None: """Fetch the most recent calibration data for this backend. Returns: Calibration: A calibration data wrapper. """ backend_name = self.name().replace("_", ".") calibration_data = self.client.get_calibration_data(backend_name) if calibration_data is None: return None return Calibration(calibration_data)
[docs] @abc.abstractmethod def with_name(self, name, **kwargs) -> IonQBackend: """Helper method that returns this backend with a more specific target system.""" pass
[docs] @abc.abstractmethod def gateset(self) -> Literal["qis", "native"]: """Helper method returning the gateset this backend is targeting."""
def __eq__(self, other) -> bool: if isinstance(other, self.__class__): return self.name() == other.name() and self.gateset() == other.gateset() else: return False def __ne__(self, other) -> bool: return not self.__eq__(other)
[docs] class IonQSimulatorBackend(IonQBackend): """ IonQ Backend for running simulated jobs. .. ATTENTION:: When noise_model ideal is specified, the maximum shot-count for a state vector sim is always ``1``. .. ATTENTION:: When noise_model ideal is specified, calling :meth:`get_counts <qiskit_ionq.ionq_job.IonQJob.get_counts>` on a job processed by this backend will return counts expressed as probabilites, rather than a multiple of shots. """ @classmethod def _default_options(cls) -> Options: return Options( shots=1024, job_settings=None, sampler_seed=None, noise_model="ideal", extra_query_params={}, extra_metadata={}, ) # pylint: disable=missing-type-doc,missing-param-doc,arguments-differ,useless-super-delegation
[docs] def run(self, circuit: QuantumCircuit, **kwargs) -> ionq_job.IonQJob: """Create and run a job on IonQ's Simulator Backend. .. WARNING: The maximum shot-count for a state vector sim is always ``1``. As a result, the ``shots`` keyword argument in this method is ignored. Args: circuit (:class:`QuantumCircuit <qiskit.circuit.QuantumCircuit>`): A Qiskit QuantumCircuit object. Returns: IonQJob: A reference to the job that was submitted. """ return super().run(circuit, **kwargs)
[docs] def calibration(self) -> None: """Simulators have no calibration data. Returns: NoneType: None """ return None
[docs] def gateset(self) -> Literal["qis", "native"]: return self._gateset
def __init__( self, provider, name: str = "simulator", gateset: Literal["qis", "native"] = "qis", ): """Base class for interfacing with an IonQ backend""" self._gateset = gateset config = BackendConfiguration.from_dict( { "backend_name": ( "ionq_" + name if not name.startswith("ionq_") else name ), "backend_version": "0.0.1", "simulator": True, "local": False, "coupling_map": None, "description": "IonQ simulator", "basis_gates": GATESET_MAP[gateset], "memory": False, # Varied based on noise model, but enforced server-side. "n_qubits": get_n_qubits(name), "conditional": False, "max_shots": 1, "max_experiments": 1, "open_pulse": False, "gates": [ { "name": "gpi", "parameters": ["phi"], "qasm_def": "gate gpi(phi) q { U(pi, 0, pi) q; U(0, 0, 4 * phi * pi) q;}", }, { "name": "gpi2", "parameters": ["phi"], "qasm_def": "gate gpi2(phi) q \ { \ U(0, 0, -2 * phi * pi) q; \ U(pi/2, -pi/2, pi/2) q; \ U(0, 0, 2 * phi * pi) q; \ }", }, { "name": "ms", "parameters": ["phi0", "phi1", "theta"], "qasm_def": "gate ms(phi0, phi1, theta) q0, q1 \ { \ ctrl @ U(pi , 0, pi) q1, q0; \ U(pi, 0, pi) q0; \ ctrl @ U(2 * theta * pi, 2 * (phi0 + phi1) * pi - pi / 2, pi / 2 - 2 * (phi0 + phi1) * pi) q0, q1; \ U(pi, 0, pi) q0; \ ctrl @ U(2 * theta * pi, -2 * (phi0 - phi1) * pi - pi / 2, pi / 2 + 2 * (phi0 - phi1) * pi) q0, q1; \ ctrl @ U(pi , 0, pi) q1, q0; \ }", }, { "name": "zz", "parameters": ["theta"], "qasm_def": "gate zz(theta) q0, q1 \ { \ ctrl @ U(pi , 0, pi) q0, q1; \ U(0, 0, 2 * theta * pi) q1; \ ctrl @ U(pi , 0, pi) q0, q1; \ }", }, ], } ) super().__init__(configuration=config, provider=provider)
[docs] def with_name(self, name, **kwargs) -> IonQSimulatorBackend: """Helper method that returns this backend with a more specific target system.""" return IonQSimulatorBackend(self._provider, name, **kwargs)
[docs] class IonQQPUBackend(IonQBackend): """IonQ Backend for running qpu-based jobs."""
[docs] def gateset(self) -> Literal["qis", "native"]: return self._gateset
def __init__( self, provider: IonQProvider, name: str = "ionq_qpu", gateset: Literal["qis", "native"] = "qis", ): self._gateset = gateset config = BackendConfiguration.from_dict( { "backend_name": name, "backend_version": "0.0.1", "simulator": False, "local": False, "coupling_map": None, "description": "IonQ QPU", "basis_gates": GATESET_MAP[gateset], "memory": False, # 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": get_n_qubits(name), "conditional": False, "max_shots": 10000, "max_experiments": 1, "open_pulse": False, "gates": [ { "name": "gpi", "parameters": ["phi"], "qasm_def": "gate gpi(phi) q { U(pi, 0, pi) q; U(0, 0, 4 * phi * pi) q;}", }, { "name": "gpi2", "parameters": ["phi"], "qasm_def": "gate gpi2(phi) q \ { \ U(0, 0, -2 * phi * pi) q; \ U(pi/2, -pi/2, pi/2) q; \ U(0, 0, 2 * phi * pi) q; \ }", }, { "name": "ms", "parameters": ["phi0", "phi1", "theta"], "qasm_def": "gate ms(phi0, phi1, theta) q0, q1 \ { \ ctrl @ U(pi , 0, pi) q1, q0; \ U(pi, 0, pi) q0; \ ctrl @ U(2 * theta * pi, 2 * (phi0 + phi1) * pi - pi / 2, pi / 2 - 2 * (phi0 + phi1) * pi) q0, q1; \ U(pi, 0, pi) q0; \ ctrl @ U(2 * theta * pi, -2 * (phi0 - phi1) * pi - pi / 2, pi / 2 + 2 * (phi0 - phi1) * pi) q0, q1; \ ctrl @ U(pi , 0, pi) q1, q0; \ }", }, { "name": "zz", "parameters": ["theta"], "qasm_def": "gate zz(theta) q0, q1 \ { \ ctrl @ U(pi , 0, pi) q0, q1; \ U(0, 0, 2 * theta * pi) q1; \ ctrl @ U(pi , 0, pi) q0, q1; \ }", }, ], } ) super().__init__(configuration=config, provider=provider)
[docs] def with_name(self, name: str, **kwargs) -> IonQQPUBackend: """Helper method that returns this backend with a more specific target system.""" return IonQQPUBackend(self._provider, name, **kwargs)
__all__ = ["IonQBackend", "IonQQPUBackend", "IonQSimulatorBackend"]