# 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"]