Source code for qbraid.runtime.qudora.device

# Copyright 2025 qBraid
#
# 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.

"""
Module defining QUDORA device class

"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from qbraid.programs.typer import get_qasm_type_alias
from qbraid.runtime.device import QuantumDevice
from qbraid.runtime.enums import DeviceStatus
from qbraid.runtime.exceptions import QbraidRuntimeError

from .job import QudoraJob

if TYPE_CHECKING:
    import qbraid.runtime
    import qbraid.runtime.qudora.provider


[docs] class QudoraDeviceError(QbraidRuntimeError): """Class for errors raised while processing a QUDORA device."""
# Maps QUDORA ``BackendStatusName`` values to qBraid ``DeviceStatus``. _DEVICE_STATUS_MAP = { "Idle": DeviceStatus.ONLINE, "Executing": DeviceStatus.ONLINE, "Calibrating": DeviceStatus.UNAVAILABLE, "Unresponsive": DeviceStatus.OFFLINE, } # Maps qBraid QASM type aliases to the QUDORA ``language`` values. _QASM_LANGUAGE_MAP = {"qasm2": "OpenQASM2", "qasm3": "OpenQASM3"}
[docs] class QudoraDevice(QuantumDevice): """QUDORA quantum device interface."""
[docs] def __init__( self, profile: qbraid.runtime.TargetProfile, session: qbraid.runtime.qudora.provider.QudoraSession, ): super().__init__(profile=profile) self._session = session
@property def session(self) -> qbraid.runtime.qudora.provider.QudoraSession: """Return the QUDORA session.""" return self._session def __str__(self): """String representation of the QudoraDevice object.""" return f"{self.__class__.__name__}('{self.id}')" def status(self) -> DeviceStatus: """Return the current status of the QUDORA device. Raises: QudoraDeviceError: If QUDORA reports a ``BackendStatusName`` that is not mapped. Failing here is deliberate: defaulting an unrecognized value would let the device advertise a status it never reported. """ status_name = self.session.get_backend_status(self.profile["qudora_backend_id"]) try: return _DEVICE_STATUS_MAP[status_name] except KeyError as err: raise QudoraDeviceError( f"Unrecognized QUDORA backend status '{status_name}'. " f"Known values: {sorted(_DEVICE_STATUS_MAP)}." ) from err def available_settings(self) -> dict[str, Any]: """Return the configurable QUDORA backend settings and their schema. These are the keys accepted in ``backend_settings`` at submit time — for QUDORA's simulators, the noise parameters ``measurement_error_probability``, ``two_qubit_gate_noise_strength``, ``single_qubit_gate_noise_strength``, and ``dephasing_T2_time`` — each entry carrying its ``default`` (and any bounds). Derived from the backend's ``user_settings_schema``, which every backend publishes; empty only when that schema declares no properties. """ schema = self.profile["user_settings_schema"] return schema.get("properties", {}) @staticmethod def _detect_language(program: str) -> str: """Return the QUDORA ``language`` (``OpenQASM2``/``OpenQASM3``) for a QASM string. Version detection is delegated to qBraid's shared QASM typing rather than scanning for a line starting with ``OPENQASM``: that scan reads a version named inside a block comment as the program's own, and accepts a header missing its semicolon. Raises: QasmError: If the OpenQASM version cannot be determined. ValueError: If the program is a QASM dialect QUDORA does not accept. """ alias = get_qasm_type_alias(program) try: return _QASM_LANGUAGE_MAP[alias] except KeyError as err: raise ValueError( f"QUDORA accepts {sorted(_QASM_LANGUAGE_MAP)} programs, got '{alias}'." ) from err # The base signature is deliberately narrowed: QUDORA accepts only OpenQASM strings, and # the extra arguments are the vendor's documented submit options. # pylint:disable-next=arguments-differ def submit( # type: ignore[override] self, run_input: str | list[str], shots: int = 100, name: str | None = None, backend_settings: dict[str, Any] | None = None, ) -> QudoraJob: """Submit one or more OpenQASM programs to the QUDORA device. Args: run_input: An OpenQASM 2/3 string, or a list of them for a batch job. shots: Number of repetitions per program. name: Optional job name. Defaults to ``"qbraid"`` rather than being omitted: QUDORA's submit schema requires ``name`` to be a string, and rejects both a ``null`` value and a missing key with a 422. backend_settings: Optional QUDORA backend settings (e.g. noise parameters). Returns: The submitted :class:`~qbraid.runtime.qudora.QudoraJob`. """ programs = run_input if isinstance(run_input, list) else [run_input] max_programs = self.profile["max_programs_per_job"] if len(programs) > max_programs: raise ValueError( f"Number of programs ({len(programs)}) exceeds the device's maximum of " f"{max_programs} programs per job." ) languages = {self._detect_language(program) for program in programs} if len(languages) != 1: raise ValueError( "All programs in a single QUDORA job must use the same OpenQASM version." ) body = { "name": name or "qbraid", "target": self.id, "language": languages.pop(), "shots": [shots] * len(programs), "input_data": programs, "backend_settings": backend_settings or {}, } job_id = self.session.create_job(body) if job_id is None: raise ValueError("QUDORA job submission did not return a job id.") return QudoraJob(job_id=str(job_id), session=self.session, device=self, shots=shots)