Source code for uniqc.backend_adapter.dummy_backend

"""Canonical dummy backend identifiers and local backend fixtures."""

from __future__ import annotations

import dataclasses
import re
from typing import Any

from uniqc.backend_adapter.backend_info import BackendInfo, Platform, QubitTopology

_VIRTUAL_LINE_RE = re.compile(r"^virtual-line-(\d+)$")
_VIRTUAL_GRID_RE = re.compile(r"^virtual-grid-(\d+)x(\d+)$")
_MPS_LINEAR_RE = re.compile(r"^mps-linear-(\d+)((?::[a-zA-Z_][a-zA-Z0-9_]*=[^:]+)*)$")


[docs] @dataclasses.dataclass(frozen=True, slots=True) class DummyBackendSpec: """Resolved configuration for one dummy backend identifier.""" identifier: str name: str description: str available_qubits: list[int] | None = None available_topology: list[list[int]] | None = None chip_characterization: Any | None = None source_platform: Platform | None = None source_name: str | None = None noise_source: str = "none" simulator_kind: str = "default" simulator_kwargs: dict[str, Any] | None = None
def _normalise_available_qubits(value: Any) -> list[int] | None: if value is None: return None if isinstance(value, int): return list(range(value)) return [int(q) for q in value] def _normalise_topology(value: Any) -> list[list[int]] | None: if value is None: return None return [[int(edge[0]), int(edge[1])] for edge in value]
[docs] def virtual_line_topology(num_qubits: int) -> list[list[int]]: if num_qubits < 1: raise ValueError("virtual-line-N requires N >= 1") return [[i, i + 1] for i in range(num_qubits - 1)]
def _parse_mps_kwargs(suffix: str) -> dict[str, Any]: """Parse the optional ``:key=value:...`` suffix of a ``dummy:local:mps-linear-N`` id. Recognised keys: ``chi`` (alias ``chi_max``, int), ``cutoff`` (alias ``svd_cutoff``, float), ``seed`` (int). """ parsed: dict[str, Any] = {} if not suffix: return parsed for chunk in suffix.lstrip(":").split(":"): if not chunk: continue if "=" not in chunk: raise ValueError(f"MPS dummy backend kwargs must be 'key=value', got '{chunk}'") key, raw = chunk.split("=", 1) key = key.strip().lower() raw = raw.strip() if key in ("chi", "chi_max"): parsed["chi_max"] = int(raw) elif key in ("cutoff", "svd_cutoff"): parsed["svd_cutoff"] = float(raw) elif key == "seed": parsed["seed"] = int(raw) else: raise ValueError( f"Unknown MPS dummy backend kwarg '{key}'. Supported: chi (chi_max), cutoff (svd_cutoff), seed." ) return parsed
[docs] def virtual_grid_topology(rows: int, cols: int) -> list[list[int]]: if rows < 1 or cols < 1: raise ValueError("virtual-grid-RxC requires R >= 1 and C >= 1") edges: list[list[int]] = [] for r in range(rows): for c in range(cols): q = r * cols + c if c + 1 < cols: edges.append([q, q + 1]) if r + 1 < rows: edges.append([q, q + cols]) return edges
def _originq_alias(name: str) -> str: aliases = { "wk180": "WK_C180", "wk-c180": "WK_C180", "wk_c180": "WK_C180", "wuyuan:wk180": "WK_C180", "pqpumesh8": "PQPUMESH8", } return aliases.get(name.lower(), name) def _normalise_source_name(platform: Platform, name: str) -> str: cleaned = name.strip() if platform == Platform.ORIGINQ: cleaned = _originq_alias(cleaned) return cleaned def _find_cached_chip(platform: Platform, name: str) -> Any | None: from uniqc.cli.chip_cache import get_chip, list_cached_chips candidates = [name] if platform == Platform.ORIGINQ: candidates.append(_originq_alias(name)) for candidate in candidates: chip = get_chip(platform, candidate) if chip is not None: return chip wanted = {candidate.lower() for candidate in candidates} wanted.update(candidate.replace("-", "_").lower() for candidate in candidates) for chip in list_cached_chips(platform): names = { chip.chip_name.lower(), chip.full_id.lower(), chip.chip_name.replace("-", "_").lower(), } if names & wanted: return chip return None def _fetch_chip_characterization(platform: Platform, name: str) -> Any | None: if platform == Platform.ORIGINQ: from uniqc.backend_adapter.task.adapters.originq_adapter import OriginQAdapter from uniqc.cli.chip_cache import save_chip chip = OriginQAdapter().get_chip_characterization(name) if chip is not None: save_chip(chip) return chip return None
[docs] def resolve_dummy_backend( identifier: str = "dummy:local:simulator", *, allow_fetch: bool = True, **overrides: Any, ) -> DummyBackendSpec: """Resolve a canonical dummy backend identifier. Supported identifiers: - ``dummy:local:simulator``: unconstrained noiseless local simulator. - ``dummy:local:virtual-line-N``: noiseless N-qubit line topology. - ``dummy:local:virtual-grid-RxC``: noiseless R by C grid topology. - ``dummy:local:mps-linear-N``: MPS simulator on N-qubit linear chain. - ``dummy:<platform>:<backend>``: noisy simulator using cached/fetched chip characterization for a real backend. Bare ``"dummy"`` and ``"dummy:local"`` are no longer accepted; use ``"dummy:local:simulator"`` instead. """ identifier = (identifier or "dummy:local:simulator").strip() if identifier in ("dummy", "dummy:local"): raise ValueError( f"Backend identifier {identifier!r} is not allowed. Use the canonical 'dummy:local:simulator' form instead." ) if identifier.startswith("dummy:"): suffix = identifier.split(":", 1)[1].strip() # Canonical local form: ``dummy:local:simulator`` / # ``dummy:local:virtual-line-N`` / ``dummy:local:virtual-grid-RxC`` / # ``dummy:local:mps-linear-N``. Strip the leading ``local:`` and # fall through to the existing topology / chip dispatch. if suffix == "local:simulator": spec = DummyBackendSpec( identifier="dummy:local:simulator", name="dummy:local:simulator", description="Unconstrained noiseless local simulator", ) # Apply override block at the bottom of the function. return _apply_dummy_overrides(spec, **overrides) if suffix.startswith("local:"): suffix = suffix[len("local:") :].strip() line_match = _VIRTUAL_LINE_RE.match(suffix) grid_match = _VIRTUAL_GRID_RE.match(suffix) mps_match = _MPS_LINEAR_RE.match(suffix) if mps_match: n = int(mps_match.group(1)) mps_kwargs = _parse_mps_kwargs(mps_match.group(2) or "") chi_str = f", chi={mps_kwargs['chi_max']}" if "chi_max" in mps_kwargs else "" canonical = f"dummy:local:{suffix}" spec = DummyBackendSpec( identifier=canonical, name=canonical, description=(f"Noiseless MPS simulator on a {n}-qubit linear chain{chi_str}"), available_qubits=list(range(n)), available_topology=virtual_line_topology(n), simulator_kind="mps", simulator_kwargs=mps_kwargs, ) elif line_match: n = int(line_match.group(1)) canonical = f"dummy:local:{suffix}" spec = DummyBackendSpec( identifier=canonical, name=canonical, description=f"Noiseless virtual {n}-qubit line topology", available_qubits=list(range(n)), available_topology=virtual_line_topology(n), ) elif grid_match: rows = int(grid_match.group(1)) cols = int(grid_match.group(2)) n = rows * cols canonical = f"dummy:local:{suffix}" spec = DummyBackendSpec( identifier=canonical, name=canonical, description=f"Noiseless virtual {rows}x{cols} grid topology", available_qubits=list(range(n)), available_topology=virtual_grid_topology(rows, cols), ) else: parts = suffix.split(":", 1) if len(parts) != 2: raise ValueError( "Dummy backend must be 'dummy:local:simulator', " "'dummy:local:virtual-line-N', 'dummy:local:virtual-grid-RxC', " "'dummy:local:mps-linear-N', or 'dummy:<platform>:<backend>'." ) try: source_platform = Platform(parts[0]) except ValueError: raise ValueError(f"Unknown dummy source platform: {parts[0]}") from None if source_platform == Platform.DUMMY: raise ValueError("Nested dummy backend identifiers are not supported") source_name = _normalise_source_name(source_platform, parts[1]) chip = overrides.get("chip_characterization") or _find_cached_chip(source_platform, source_name) if chip is None and allow_fetch: chip = _fetch_chip_characterization(source_platform, source_name) if chip is None: raise ValueError( f"No chip characterization available for {source_platform.value}:{source_name}. " "Run 'uniqc backend chip-display <platform>/<backend> --update' first, " "or pass chip_characterization explicitly." ) available_qubits = list(int(q) for q in getattr(chip, "available_qubits", ())) available_topology = [[int(e.u), int(e.v)] for e in getattr(chip, "connectivity", ())] spec = DummyBackendSpec( identifier=f"dummy:{source_platform.value}:{source_name}", name=f"{source_platform.value}:{source_name}", description=f"Noisy local simulator calibrated from {source_platform.value}:{source_name}", available_qubits=available_qubits, available_topology=available_topology, chip_characterization=chip, source_platform=source_platform, source_name=source_name, noise_source="chip_characterization", ) else: raise ValueError(f"Not a dummy backend identifier: {identifier}") return _apply_dummy_overrides(spec, **overrides)
def _apply_dummy_overrides(spec: DummyBackendSpec, **overrides: Any) -> DummyBackendSpec: """Apply user-supplied overrides (chip / qubits / topology / noise) onto a spec.""" available_qubits_override = overrides.get("available_qubits") available_topology_override = overrides.get("available_topology") chip_override = overrides.get("chip_characterization") available_qubits = _normalise_available_qubits( spec.available_qubits if available_qubits_override is None else available_qubits_override ) available_topology = _normalise_topology( spec.available_topology if available_topology_override is None else available_topology_override ) chip_characterization = spec.chip_characterization if chip_override is None else chip_override noise_source = spec.noise_source if chip_characterization is not None: noise_source = "chip_characterization" elif overrides.get("noise_model") is not None: noise_source = "noise_model" return dataclasses.replace( spec, available_qubits=available_qubits, available_topology=available_topology, chip_characterization=chip_characterization, noise_source=noise_source, )
[docs] def dummy_adapter_kwargs(identifier: str, **overrides: Any) -> dict[str, Any]: spec = resolve_dummy_backend(identifier, **overrides) return { "backend_id": spec.identifier, "chip_characterization": spec.chip_characterization, "noise_model": overrides.get("noise_model"), "available_qubits": spec.available_qubits, "available_topology": spec.available_topology, "simulator_kind": spec.simulator_kind, "simulator_kwargs": spec.simulator_kwargs, }
def _chip_averages(chip: Any) -> tuple[float | None, float | None, float | None, float | None, float | None]: def avg(values: list[float]) -> float | None: return sum(values) / len(values) if values else None oneq = [float(q.single_gate_fidelity) for q in chip.single_qubit_data if q.single_gate_fidelity is not None] readout = [float(q.avg_readout_fidelity) for q in chip.single_qubit_data if q.avg_readout_fidelity is not None] t1 = [float(q.t1) for q in chip.single_qubit_data if q.t1 is not None] t2 = [float(q.t2) for q in chip.single_qubit_data if q.t2 is not None] twoq = [float(g.fidelity) for pair in chip.two_qubit_data for g in pair.gates if g.fidelity is not None] return avg(oneq), avg(twoq), avg(readout), avg(t1), avg(t2) def _info_from_spec(spec: DummyBackendSpec) -> BackendInfo: topology = tuple(QubitTopology(u=edge[0], v=edge[1]) for edge in (spec.available_topology or [])) num_qubits = len(spec.available_qubits or []) avg_1q = avg_2q = avg_readout = t1 = t2 = None if spec.chip_characterization is not None: avg_1q, avg_2q, avg_readout, t1, t2 = _chip_averages(spec.chip_characterization) if spec.simulator_kind == "mps": kind_label = "mps-line" elif spec.source_platform: kind_label = "hardware-noisy" elif topology: kind_label = "virtual" else: kind_label = "ideal" return BackendInfo( platform=Platform.DUMMY, name=spec.name, description=spec.description, num_qubits=num_qubits, topology=topology, status="available", is_simulator=True, is_hardware=False, extra={ "available": True, "dummy_backend_id": spec.identifier, "dummy_kind": kind_label, "noise_source": spec.noise_source, "source_platform": spec.source_platform.value if spec.source_platform else None, "source_name": spec.source_name, "available_qubits": spec.available_qubits or [], "simulator_kind": spec.simulator_kind, "simulator_kwargs": dict(spec.simulator_kwargs or {}), }, avg_1q_fidelity=avg_1q, avg_2q_fidelity=avg_2q, avg_readout_fidelity=avg_readout, coherence_t1=t1, coherence_t2=t2, )
[docs] def list_dummy_backend_infos() -> list[BackendInfo]: """Return dummy backends that should be shown in backend lists. Chip-backed identifiers such as ``dummy:originq:WK_C180`` are intentionally not listed here. They are rule-based submission targets documented for users, not standalone backend cards in the management UI. """ specs = [ resolve_dummy_backend("dummy:local:simulator", allow_fetch=False), resolve_dummy_backend("dummy:local:virtual-line-3", allow_fetch=False), resolve_dummy_backend("dummy:local:virtual-grid-2x2", allow_fetch=False), resolve_dummy_backend("dummy:local:mps-linear-3", allow_fetch=False), ] seen: set[str] = set() infos: list[BackendInfo] = [] for spec in specs: if spec.identifier in seen: continue seen.add(spec.identifier) infos.append(_info_from_spec(spec)) return infos