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
from typing import Literal, Sequence, TYPE_CHECKING
import warnings

from qiskit.circuit import QuantumCircuit, Parameter
from qiskit.circuit.library import (
    Measure,
    Reset,
    CHGate,
    XGate,
    CPhaseGate,
    CRXGate,
    CRYGate,
    CRZGate,
    CSXGate,
    CXGate,
    CYGate,
    CZGate,
    HGate,
    IGate,
    MCPhaseGate,
    MCXGate,
    PhaseGate,
    RXGate,
    RXXGate,
    RYGate,
    RYYGate,
    RZGate,
    RZZGate,
    SGate,
    SdgGate,
    SwapGate,
    SXGate,
    SXdgGate,
    TGate,
    TdgGate,
    YGate,
    ZGate,
    PauliEvolutionGate,
)
from qiskit.providers import BackendV2 as Backend, Options
from qiskit.transpiler import Target, CouplingMap

from qiskit_ionq.ionq_gates import GPIGate, GPI2Gate, MSGate, ZZGate
from . import ionq_equivalence_library, ionq_job, ionq_client, exceptions
from .helpers import GATESET_MAP, get_n_qubits, warn_bad_transpile_level
from .ionq_client import Characterization

if TYPE_CHECKING:  # pragma: no cover
    from .ionq_provider import IonQProvider


[docs] class IonQBackend(Backend): """Common functionality for all IonQ backends (simulator and QPU).""" _client: ionq_client.IonQClient | None = None def __init__( self, *, provider: IonQProvider, name: str, description: str, gateset: Literal["qis", "native"], num_qubits: int, simulator: bool, backend_version: str = "0.0.1", max_shots: int | None = None, max_experiments: int | None = None, **initial_options, ): """Build a new IonQ backend instance.""" # Register IonQ-specific gate equivalences once per process. ionq_equivalence_library.add_equivalences() super().__init__( provider=provider, name=name, description=description, backend_version=backend_version, ) # Immutable facts self._gateset: Literal["qis", "native"] = gateset self._basis_gates: Sequence[str] = tuple(GATESET_MAP[gateset]) self._num_qubits: int = num_qubits self._simulator: bool = simulator self._max_experiments: int | None = max_experiments self._max_shots: int | None = max_shots # Target (basis & connectivity) self._target = self._make_target() # Apply initial options if any if initial_options: self.options.update_options(**initial_options) # Warn if optimization_level is set to a bad value (for IonQ) warn_bad_transpile_level() @classmethod def _default_options(cls) -> Options: """Dynamic (user-tuneable) backend options.""" return Options( shots=1024, job_settings=None, error_mitigation=None, extra_query_params={}, extra_metadata={}, sampler_seed=None, # simulator-only (harmless on QPU) noise_model="ideal", # simulator-only ) @property def target(self) -> Target | None: return self._target @property def num_qubits(self) -> int: return self._num_qubits @property def basis_gates(self) -> Sequence[str]: """Return the basis gates for this backend.""" return self._basis_gates @property def coupling_map(self) -> CouplingMap: """IonQ hardware is fully connected.""" return CouplingMap.from_full(self._num_qubits) @property def max_circuits(self) -> int | None: return self._max_experiments
[docs] def gateset(self) -> Literal["qis", "native"]: """Active gateset (``"qis"`` or ``"native"``).""" return self._gateset
@property def client(self) -> ionq_client.IonQClient: """Return the IonQ client for this backend.""" if self._client is None: self._client = self._create_client() return self._client def _create_client(self) -> ionq_client.IonQClient: creds = self._provider.credentials if "token" not in creds: raise exceptions.IonQCredentialsError( "Credentials `token` not present in provider." ) token = creds["token"] if token is None: raise exceptions.IonQCredentialsError( "Credentials `token` may not be None!" ) if "url" not in creds: raise exceptions.IonQCredentialsError( "Credentials `url` not present in provider." ) url = creds["url"] if url is None: raise exceptions.IonQCredentialsError("Credentials `url` may not be None!") return ionq_client.IonQClient(token, url, self._provider.custom_headers) @property def _api_backend_name(self) -> str: """Backend name used by the IonQ API (e.g., `qpu.aria-1`).""" # QPU names are `ionq_qpu.*` locally; API expects `qpu.*` return self.name.replace("ionq_qpu", "qpu")
[docs] def run( self, run_input: QuantumCircuit | Sequence[QuantumCircuit], **options ) -> ionq_job.IonQJob: """Create and run a job on an IonQ Backend. Args: run_input: A single or list of Qiskit QuantumCircuit object(s). **options: Additional options for the job. Returns: IonQJob: A reference to the job that was submitted. """ circuits = run_input if isinstance(run_input, (list, tuple)) else [run_input] if not all(self._has_measurements(c) for c in circuits): warnings.warn( "Circuit is not measuring any qubits", UserWarning, stacklevel=2 ) # Merge default options with user overrides run_opts = {**self.options.__dict__, **options} job = ionq_job.IonQJob( backend=self, job_id=None, client=self.client, circuit=run_input, passed_args=run_opts, ) job.submit() return job
[docs] def retrieve_job(self, job_id: str) -> ionq_job.IonQJob: """Retrieve a job by its ID.""" return ionq_job.IonQJob(self, job_id, self.client)
[docs] def retrieve_jobs(self, job_ids: Sequence[str]) -> Sequence[ionq_job.IonQJob]: """Retrieve multiple jobs by their IDs.""" return [ionq_job.IonQJob(self, jid, self.client) for jid in job_ids]
[docs] def cancel_job(self, job_id: str) -> dict: """Cancel a job by its ID.""" return self.client.cancel_job(job_id)
[docs] def cancel_jobs(self, job_ids: Sequence[str]) -> Sequence[dict]: """Cancel a list of jobs by their IDs.""" return [self.client.cancel_job(job_id) for job_id in job_ids]
[docs] def calibration(self) -> Characterization | None: """Return the latest characterization data (None for simulator).""" if self._simulator: return None return self.client.get_calibration_data(self._api_backend_name, limit=1)
[docs] def status(self) -> bool: """True if the backend is currently available.""" cal = self.calibration() return bool(cal and getattr(cal, "status", "available") == "available")
def __eq__(self, other): if not isinstance(other, IonQBackend): return NotImplemented return (self.name, self._gateset) == (other.name, other._gateset) def __hash__(self): return hash((self.name, self._gateset)) def _make_target(self) -> Target: """Build a Target exposing either QIS or IonQ-native gates.""" tgt = Target(num_qubits=self._num_qubits) if self._gateset == "qis": theta = Parameter("θ") for gate in ( # 1-qubit (fixed) IGate(), XGate(), YGate(), ZGate(), HGate(), SGate(), SdgGate(), SXGate(), SXdgGate(), TGate(), TdgGate(), # 1-qubit (parameterized) RXGate(theta), RYGate(theta), RZGate(theta), PhaseGate(theta), # 2-qubit (fixed) CXGate(), CYGate(), CZGate(), CHGate(), CSXGate(), SwapGate(), # 2-qubit (parameterized) CRXGate(theta), CRYGate(theta), CRZGate(theta), CPhaseGate(theta), RXXGate(theta), RYYGate(theta), RZZGate(theta), ): tgt.add_instruction(gate) tgt.add_instruction(MCXGate, name="mcx") tgt.add_instruction(MCPhaseGate, name="mcphase") tgt.add_instruction(PauliEvolutionGate, name="PauliEvolution") else: # 1q native phi = Parameter("φ") for gate in (GPIGate(phi), GPI2Gate(phi)): tgt.add_instruction(gate) # 2q native if "forte" in self.name.lower(): theta = Parameter("θ") tgt.add_instruction(ZZGate(theta)) else: phi0, phi1, theta = Parameter("φ0"), Parameter("φ1"), Parameter("θ") tgt.add_instruction(MSGate(phi0, phi1, theta)) # Always allow measure/reset for cls in (Measure, Reset): tgt.add_instruction(cls()) return tgt @staticmethod def _has_measurements(circ: QuantumCircuit) -> bool: return any(inst.operation.name == "measure" for inst in circ.data)
[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 probabilities, rather than a multiple of shots. """ def __init__( self, provider: IonQProvider, name: str = "simulator", gateset: Literal["qis", "native"] = "qis", **initial_options, ): backend_name = name if name.startswith("ionq_") else f"ionq_{name}" super().__init__( provider=provider, name=backend_name, description="IonQ cloud simulator", gateset=gateset, num_qubits=get_n_qubits(name), simulator=True, max_shots=1, max_experiments=None, **initial_options, )
[docs] def with_name(self, name: str, **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 trapped-ion hardware back-ends (Aria/Alpine: MS; Forte: ZZ).""" def __init__( self, provider: IonQProvider, name: str = "ionq_qpu", gateset: Literal["qis", "native"] = "qis", **initial_options, ): super().__init__( provider=provider, name=name, description="IonQ trapped-ion QPU", gateset=gateset, num_qubits=get_n_qubits(name), simulator=False, max_shots=10_000, max_experiments=None, **initial_options, )
[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", "IonQSimulatorBackend", "IonQQPUBackend"]