"""IBM Quantum backend adapter using QiskitRuntimeService.
Uses ``QiskitRuntimeService`` from ``qiskit-ibm-runtime`` to list backends
and submit/query tasks. This is the recommended IBM approach as of 2024+,
superseding the raw REST API which is blocked by Cloudflare on quantum.ibm.com.
QiskitRuntimeService reference:
https://docs.quantum.ibm.com/qiskit-ibm-runtime
"""
from __future__ import annotations
import warnings
from typing import Any
from uniqc.backend_adapter.task.adapters.base import (
QuantumAdapter,
)
_SINGLE_QUBIT_GATE_PRIORITY = ("sx", "x", "id")
_TWO_QUBIT_GATE_PRIORITY = ("cz", "ecr", "cx")
def _avg(values: list[float]) -> float | None:
"""Return the arithmetic mean of a list, or None if the list is empty."""
return sum(values) / len(values) if values else None
def _call_or_value(value: Any) -> Any:
return value() if callable(value) else value
def _backend_configuration(backend: Any) -> Any | None:
try:
return _call_or_value(getattr(backend, "configuration", None))
except Exception:
return None
def _backend_name(backend: Any) -> str:
return str(_call_or_value(getattr(backend, "name", "")))
def _backend_is_simulator(backend: Any) -> bool:
try:
return bool(_call_or_value(getattr(backend, "simulator", False)))
except Exception:
return False
def _as_qubit_tuple(qargs: Any) -> tuple[int, ...]:
if isinstance(qargs, int):
return (int(qargs),)
try:
return tuple(int(q) for q in qargs)
except TypeError:
return ()
def _iter_target_instruction_props(target: Any, gate_names: tuple[str, ...], arity: int):
"""Yield ``(gate_name, qubits, props)`` from a Qiskit Target."""
if target is None:
return
for gate_name in gate_names:
try:
ops = target[gate_name]
except Exception:
continue
if not hasattr(ops, "items"):
continue
for qargs, props in ops.items():
qubits = _as_qubit_tuple(qargs)
if len(qubits) == arity and props is not None:
yield gate_name, qubits, props
def _target_error(target: Any, gate_names: tuple[str, ...], qubits: tuple[int, ...]) -> tuple[str, float | None]:
for gate_name, qargs, props in _iter_target_instruction_props(target, gate_names, len(qubits)):
if qargs == qubits and getattr(props, "error", None) is not None:
return gate_name, float(props.error)
return "", None
def _properties_gate_error(
properties: Any, gate_names: tuple[str, ...], qubits: tuple[int, ...]
) -> tuple[str, float | None]:
if properties is None:
return "", None
for gate_name in gate_names:
try:
error = properties.gate_error(gate_name, qubits)
except Exception:
continue
if error is not None:
return gate_name, float(error)
return "", None
def _normalize_coupling_map(raw: Any) -> list[tuple[int, int]]:
if raw is None:
return []
try:
if hasattr(raw, "get_edges"):
raw = raw.get_edges()
except Exception:
pass
edges: list[tuple[int, int]] = []
try:
iterator = iter(raw)
except TypeError:
return []
for edge in iterator:
try:
u, v = edge
except (TypeError, ValueError):
continue
edges.append((int(u), int(v)))
return edges
def _backend_coupling_map(backend: Any, cfg: Any | None = None) -> list[tuple[int, int]]:
for raw in (getattr(backend, "coupling_map", None), getattr(cfg, "coupling_map", None)):
edges = _normalize_coupling_map(raw)
if edges:
return edges
try:
target = backend.target
coupling_map = target.build_coupling_map() if target is not None else None
edges = _normalize_coupling_map(coupling_map)
if edges:
return edges
except Exception:
pass
return []
def _get_backend_properties(backend: Any, *, refresh: bool = False) -> Any | None:
try:
properties = getattr(backend, "properties", None)
if callable(properties):
try:
return properties(refresh=refresh)
except TypeError:
return properties()
return properties
except Exception:
return None
def _compute_ibm_fidelity(b: Any) -> dict[str, Any]:
"""Compute average fidelity and coherence metrics for an IBM backend.
Uses ``backend.target`` for gate errors and ``backend.qubit_properties``
for T1/T2 and readout error. Returns None for all fields on simulators
or when the data is unavailable.
Returns:
dict with keys: avg_1q_fidelity, avg_2q_fidelity, avg_readout_fidelity,
coherence_t1 (microseconds), coherence_t2 (microseconds).
"""
try:
target = b.target
except Exception:
return {
"avg_1q_fidelity": None,
"avg_2q_fidelity": None,
"avg_readout_fidelity": None,
"coherence_t1": None,
"coherence_t2": None,
}
if target is None:
return {
"avg_1q_fidelity": None,
"avg_2q_fidelity": None,
"avg_readout_fidelity": None,
"coherence_t1": None,
"coherence_t2": None,
}
properties = _get_backend_properties(b)
# Single-qubit gate fidelity: prefer SX, then X/ID if SX is absent.
sq_errors: list[float] = []
seen_qubits: set[int] = set()
for _gate_name, qpair, props in _iter_target_instruction_props(target, _SINGLE_QUBIT_GATE_PRIORITY, 1):
if qpair[0] in seen_qubits or getattr(props, "error", None) is None:
continue
sq_errors.append(float(props.error))
seen_qubits.add(qpair[0])
if not sq_errors:
for q in range(getattr(b, "num_qubits", 0) or 0):
_gate, error = _properties_gate_error(properties, _SINGLE_QUBIT_GATE_PRIORITY, (q,))
if error is not None:
sq_errors.append(error)
# Two-qubit gate fidelity: prefer CZ, then ECR/CX depending on backend generation.
tq_errors: list[float] = []
seen_edges: set[tuple[int, int]] = set()
for _gate_name, qpair, props in _iter_target_instruction_props(target, _TWO_QUBIT_GATE_PRIORITY, 2):
key = tuple(sorted(qpair))
if key in seen_edges or getattr(props, "error", None) is None:
continue
tq_errors.append(float(props.error))
seen_edges.add(key)
if not tq_errors:
for edge in _backend_coupling_map(b):
_gate, error = _properties_gate_error(properties, _TWO_QUBIT_GATE_PRIORITY, edge)
if error is not None:
tq_errors.append(error)
# Coherence and readout: qubit_properties gives T1/T2 (seconds)
t1s, t2s, ro_errors = [], [], []
num_qubits = b.num_qubits
try:
for q in range(num_qubits):
try:
qp = b.qubit_properties(q)
if qp.t1 is not None:
t1s.append(qp.t1 * 1e6) # seconds → μs
if qp.t2 is not None:
t2s.append(qp.t2 * 1e6)
except Exception:
pass
except Exception:
pass
# Readout error from properties if qubit_properties didn't have it
try:
props = properties
if props:
for q in range(num_qubits):
try:
re = props.readout_error(q)
if re is not None:
ro_errors.append(re)
except Exception:
pass
except Exception:
pass
return {
"avg_1q_fidelity": _avg([1 - e for e in sq_errors]) if sq_errors else None,
"avg_2q_fidelity": _avg([1 - e for e in tq_errors]) if tq_errors else None,
"avg_readout_fidelity": _avg([1 - e for e in ro_errors]) if ro_errors else None,
"coherence_t1": _avg(t1s),
"coherence_t2": _avg(t2s),
}
[docs]
class IBMAdapter(QuantumAdapter):
"""Deprecated: delegates to QiskitAdapter.
.. deprecated::
IBMAdapter is deprecated. Use :class:`QiskitAdapter` instead,
which uses ``qiskit-ibm-runtime`` for task submission.
This class is kept for backwards compatibility and delegates all
operations to an internal QiskitAdapter instance.
"""
name = "ibm"
def __init__(self, proxy: dict[str, str] | str | None = None) -> None:
warnings.warn(
"IBMAdapter is deprecated. Use QiskitAdapter instead. "
"It provides the same functionality via qiskit-ibm-runtime.",
DeprecationWarning,
stacklevel=2,
)
from uniqc.backend_adapter.task.adapters.qiskit_adapter import QiskitAdapter
self._delegate = QiskitAdapter(proxy=proxy)
# -------------------------------------------------------------------------
# Forward all methods to the delegate QiskitAdapter
# -------------------------------------------------------------------------
[docs]
def is_available(self) -> bool:
return self._delegate.is_available()
def _get_service(self):
return self._delegate._service
[docs]
def list_backends(self) -> list[dict[str, Any]]:
"""List IBM backends — delegates to :class:`QiskitAdapter`."""
return self._delegate.list_backends()
[docs]
def translate_circuit(self, originir: str) -> Any:
return self._delegate.translate_circuit(originir)
[docs]
def submit(self, circuit: Any, *, shots: int = 1000, **kwargs: Any) -> str:
return self._delegate.submit(circuit, shots=shots, **kwargs)
[docs]
def submit_batch(self, circuits: list[Any], *, shots: int = 1000, **kwargs: Any) -> list[str]:
return self._delegate.submit_batch(circuits, shots=shots, **kwargs)
[docs]
def query(self, taskid: str) -> dict[str, Any]:
return self._delegate.query(taskid)
[docs]
def query_batch(self, taskids: list[str]) -> dict[str, Any]:
return self._delegate.query_batch(taskids)
[docs]
def query_sync(
self,
taskid: str | list[str],
interval: float = 2.0,
timeout: float = 60.0,
retry: int = 5,
) -> list[dict[str, Any]]:
return self._delegate.query_sync(taskid, interval=interval, timeout=timeout, retry=retry)
# -------------------------------------------------------------------------
# Chip characterization
# -------------------------------------------------------------------------
[docs]
def get_chip_characterization(self, backend_name: str):
"""Return per-qubit and per-pair calibration data for an IBM backend.
Delegates to :class:`QiskitAdapter`.
Parameters
----------
backend_name:
IBM backend name, e.g. ``"ibm_brisbane"``.
Returns
-------
ChipCharacterization or None
"""
return self._delegate.get_chip_characterization(backend_name)
def _chip_characterization_from_backend(backend: Any, *, backend_name: str | None = None):
"""Build ``ChipCharacterization`` from an IBM backend object."""
from datetime import datetime, timezone
from uniqc.backend_adapter.backend_info import Platform, QubitTopology
from uniqc.cli.chip_info import (
ChipCharacterization,
ChipGlobalInfo,
SingleQubitData,
TwoQubitData,
TwoQubitGateData,
)
name = backend_name or _backend_name(backend)
try:
target = backend.target
except Exception:
target = None
properties = _get_backend_properties(backend, refresh=True)
cfg = _backend_configuration(backend)
num_qubits = int(getattr(backend, "num_qubits", 0) or 0)
# Per-qubit data
single_qubit_data: list[SingleQubitData] = []
for q in range(num_qubits):
t1: float | None = None
t2: float | None = None
sx_fidelity: float | None = None
ro_fid_0: float | None = None
ro_fid_1: float | None = None
avg_ro: float | None = None
# Gate errors from target, with BackendProperties fallback.
_gate, error = _target_error(target, _SINGLE_QUBIT_GATE_PRIORITY, (q,))
if error is None:
_gate, error = _properties_gate_error(properties, _SINGLE_QUBIT_GATE_PRIORITY, (q,))
if error is not None:
sx_fidelity = 1.0 - error
# T1/T2 from qubit_properties
try:
qp = backend.qubit_properties(q)
if qp.t1 is not None:
t1 = qp.t1 * 1e6 # seconds → μs
if qp.t2 is not None:
t2 = qp.t2 * 1e6
except Exception:
pass
# Readout error: prefer Target measure instruction, then BackendProperties.
_gate, readout_error = _target_error(target, ("measure",), (q,))
if readout_error is not None:
avg_ro = 1.0 - readout_error
try:
if properties:
if avg_ro is None:
re = properties.readout_error(q)
if re is not None:
avg_ro = 1.0 - float(re)
qubit_props = properties.qubit_property(q)
p10 = qubit_props.get("prob_meas1_prep0", (None,))[0]
p01 = qubit_props.get("prob_meas0_prep1", (None,))[0]
if p10 is not None:
ro_fid_0 = 1.0 - float(p10)
if p01 is not None:
ro_fid_1 = 1.0 - float(p01)
if avg_ro is None and ro_fid_0 is not None and ro_fid_1 is not None:
avg_ro = (ro_fid_0 + ro_fid_1) / 2.0
except Exception:
pass
single_qubit_data.append(
SingleQubitData(
qubit_id=q,
t1=t1,
t2=t2,
single_gate_fidelity=sx_fidelity,
readout_fidelity_0=ro_fid_0,
readout_fidelity_1=ro_fid_1,
avg_readout_fidelity=avg_ro,
)
)
# Per-pair data from target CZ/ECR/CX, with BackendProperties fallback.
two_qubit_data: dict[tuple[int, int], TwoQubitData] = {}
for gname, qpair, props in _iter_target_instruction_props(target, _TWO_QUBIT_GATE_PRIORITY, 2):
if getattr(props, "error", None) is None:
continue
u, v = qpair
key = tuple(sorted((u, v)))
existing = two_qubit_data.get(key)
gate_data = TwoQubitGateData(gate=gname, fidelity=1.0 - float(props.error))
if existing is None:
two_qubit_data[key] = TwoQubitData(qubit_u=u, qubit_v=v, gates=(gate_data,))
else:
two_qubit_data[key] = TwoQubitData(
qubit_u=existing.qubit_u,
qubit_v=existing.qubit_v,
gates=existing.gates + (gate_data,),
)
if not two_qubit_data:
for edge in _backend_coupling_map(backend, cfg):
gname, error = _properties_gate_error(properties, _TWO_QUBIT_GATE_PRIORITY, edge)
if error is None:
continue
u, v = edge
two_qubit_data[tuple(sorted(edge))] = TwoQubitData(
qubit_u=u,
qubit_v=v,
gates=(TwoQubitGateData(gate=gname, fidelity=1.0 - error),),
)
# Global info from configuration / target.
try:
basis_gates: list[str] = list(getattr(backend, "basis_gates", []) or getattr(cfg, "basis_gates", []) or [])
if not basis_gates and target is not None and hasattr(target, "operation_names"):
basis_gates = list(target.operation_names)
dt_s: float | None = getattr(cfg, "dt", None)
sq_gate_time: float | None = float(dt_s) * 1e9 if dt_s is not None else None
except Exception:
basis_gates = []
sq_gate_time = None
# Classify basis gates into 1Q / 2Q
sq_gates, tq_gates = [], []
for g in basis_gates:
g_lower = str(g).lower()
if (
g_lower
in {
"h",
"x",
"y",
"z",
"s",
"sx",
"sdg",
"sxdg",
"t",
"tdg",
"i",
"id",
"rx",
"ry",
"rz",
"u1",
"u2",
"u3",
"r",
"rphi",
"rphi90",
"rphi180",
}
and g_lower not in sq_gates
):
sq_gates.append(g_lower)
elif g_lower in {"cx", "cz", "ecr", "swap", "iswap", "xx", "yy", "zz", "xy"} and g_lower not in tq_gates:
tq_gates.append(g_lower)
# 2Q gate time: use the average target duration where available.
tq_durations = [
float(props.duration) * 1e9
for _gname, _qpair, props in _iter_target_instruction_props(target, _TWO_QUBIT_GATE_PRIORITY, 2)
if getattr(props, "duration", None) is not None
]
tq_gate_time: float | None = _avg(tq_durations)
# Calibration timestamp
calibrated_at: str | None = None
try:
if properties is not None and getattr(properties, "last_update_date", None) is not None:
calibrated_at = str(properties.last_update_date)
except Exception:
pass
if calibrated_at is None:
calibrated_at = datetime.now(timezone.utc).isoformat()
return ChipCharacterization(
platform=Platform.IBM,
chip_name=name,
full_id=f"ibm:{name}",
available_qubits=tuple(range(num_qubits)),
connectivity=tuple(QubitTopology(u=u, v=v) for u, v in _backend_coupling_map(backend, cfg)),
single_qubit_data=tuple(single_qubit_data),
two_qubit_data=tuple(two_qubit_data.values()),
global_info=ChipGlobalInfo(
single_qubit_gates=tuple(sq_gates),
two_qubit_gates=tuple(tq_gates),
single_qubit_gate_time=sq_gate_time,
two_qubit_gate_time=tq_gate_time,
),
calibrated_at=calibrated_at,
)