# 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)