| """Backend-style decoder adapters for CUDA/JAX/Neural accelerator families. |
| |
| These adapters provide protocol-compatible decoder implementations with optional |
| backend acceleration and robust fallback into deterministic parity-matrix logic. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import time |
| from dataclasses import dataclass, field |
| from typing import Any, Mapping |
|
|
| import numpy as np |
|
|
| from .base import BoolArray, DecoderMetadata, DecoderOutput, DecoderProtocol |
| from .union_find import UnionFindDecoder |
|
|
|
|
| try: |
| import cudaq |
| except Exception as exc: |
| cudaq = None |
| _CUDAQ_IMPORT_ERROR = exc |
| else: |
| _CUDAQ_IMPORT_ERROR = None |
|
|
| try: |
| import qujax |
| except Exception as exc: |
| qujax = None |
| _QUJAX_IMPORT_ERROR = exc |
| else: |
| _QUJAX_IMPORT_ERROR = None |
|
|
| try: |
| import cuquantum |
| except Exception as exc: |
| cuquantum = None |
| _CUQNN_IMPORT_ERROR = exc |
| else: |
| _CUQNN_IMPORT_ERROR = None |
|
|
|
|
| def _coerce_matrix(value: Any) -> np.ndarray | None: |
| if value is None: |
| return None |
| try: |
| matrix = np.asarray(value, dtype=np.uint8) |
| except Exception: |
| return None |
| if matrix.ndim != 2 or matrix.size == 0: |
| return None |
| return matrix |
|
|
|
|
| def _extract_parity_matrices(metadata: DecoderMetadata) -> tuple[np.ndarray | None, np.ndarray | None]: |
| """Extract parity matrices from metadata. |
| |
| The helper supports multiple alias names to reduce integration friction. |
| """ |
| extra = metadata.extra or {} |
| if not isinstance(extra, Mapping): |
| return None, None |
|
|
| hx = _coerce_matrix(extra.get("hx", extra.get("hx_matrix", extra.get("parity_x")))) |
| hz = _coerce_matrix(extra.get("hz", extra.get("hz_matrix", extra.get("parity_z")))) |
| return hx, hz |
|
|
|
|
| def _parity_projection_predictions( |
| detector_events: BoolArray, |
| metadata: DecoderMetadata, |
| hx: np.ndarray | None, |
| hz: np.ndarray | None, |
| ) -> np.ndarray: |
| events = np.asarray(detector_events, dtype=np.bool_) |
| if events.ndim != 2: |
| raise ValueError("detector_events must be a 2D bool array.") |
| if metadata.num_observables < 0: |
| raise ValueError("metadata.num_observables must be >= 0") |
|
|
| shots = events.shape[0] |
| obs_count = int(metadata.num_observables) |
| if obs_count == 0: |
| return np.zeros((shots, 0), dtype=np.bool_) |
|
|
| x_checks = hx.shape[0] if hx is not None else 0 |
| z_checks = hz.shape[0] if hz is not None else 0 |
| detectors = events.shape[1] |
|
|
| if x_checks + z_checks <= 0: |
| x_checks = max(1, min(detectors // 2, max(1, obs_count))) |
| z_checks = max(0, detectors - x_checks) |
|
|
| x_checks = min(x_checks, detectors) |
| remaining = max(0, detectors - x_checks) |
| z_checks = min(z_checks, remaining) |
| if z_checks == 0 and detectors > 0: |
| x_checks = detectors |
|
|
| x_syndrome = events[:, :x_checks] |
| z_syndrome = events[:, x_checks:x_checks + z_checks] |
|
|
| if x_syndrome.size == 0: |
| x_syndrome = events |
| z_syndrome = np.empty((shots, 0), dtype=np.bool_) |
| z_checks = 0 |
|
|
| predictions = np.zeros((shots, obs_count), dtype=np.bool_) |
| for obs_idx in range(obs_count): |
| source = x_syndrome if (obs_idx % 2 == 0 or z_syndrome.size == 0) else z_syndrome |
| if source.size == 0: |
| continue |
|
|
| window = max(1, source.shape[1] // obs_count) |
| start = (obs_idx * window) % source.shape[1] |
| end = min(source.shape[1], start + window) |
| block = source[:, start:end] |
| if block.size == 0: |
| block = source[:, start:start + 1] |
| predictions[:, obs_idx] = np.logical_xor.reduce(block, axis=1) |
|
|
| return predictions |
|
|
|
|
| def _decode_with_parity_matrix( |
| detector_events: BoolArray, |
| metadata: DecoderMetadata, |
| fallback_decoder: UnionFindDecoder, |
| ) -> DecoderOutput: |
| hx, hz = _extract_parity_matrices(metadata) |
| if hx is not None and hz is not None and hx.size and hz.size: |
| return DecoderOutput( |
| logical_predictions=_parity_projection_predictions( |
| detector_events, |
| metadata, |
| hx=hx, |
| hz=hz, |
| ), |
| decoder_name="parity_projection", |
| diagnostics={"parity_matrix_path": True}, |
| ) |
| return fallback_decoder.decode(detector_events, metadata) |
|
|
|
|
| def _coerce_backend_prediction(raw: Any, num_observables: int, shots: int) -> np.ndarray: |
| array = np.asarray(raw) |
| if array.ndim == 0: |
| raise ValueError("Backend decoder returned a scalar output") |
|
|
| if array.ndim == 1 and array.shape[0] == num_observables: |
| array = np.tile(array.astype(np.int8), (shots, 1)) |
| elif array.ndim == 2 and array.shape[0] == num_observables and array.shape[1] != shots: |
| if array.shape[1] == shots and array.shape[0] == num_observables: |
| array = array.T |
| else: |
| array = array.T |
|
|
| if array.ndim != 2: |
| raise ValueError("Backend decoder output must be 2D") |
|
|
| if array.shape[0] != shots: |
| raise ValueError(f"Backend decoder output shot axis mismatch: expected {shots}, got {array.shape[0]}") |
| if array.shape[1] != num_observables: |
| if num_observables == 0: |
| array = np.zeros((shots, 0), dtype=np.int8) |
| else: |
| raise ValueError( |
| f"Backend decoder output logical axis mismatch: expected {num_observables}, got {array.shape[1]}" |
| ) |
|
|
| return np.asarray(array.astype(bool)) |
|
|
|
|
| def _safe_invoke_decoder_backend( |
| backend_callable: Any, |
| events: np.ndarray, |
| metadata: DecoderMetadata, |
| ) -> Any: |
| hx, hz = _extract_parity_matrices(metadata) |
| num_observables = int(metadata.num_observables) |
|
|
| call_patterns = [ |
| ("fn(events)", (events,), {}), |
| ("fn(events, metadata)", (events, metadata), {}), |
| ("fn(events, num_observables)", (events,), {"num_observables": num_observables}), |
| ("fn(events, hx=hx, hz=hz)", (events,), {"hx": hx, "hz": hz}), |
| ("fn(detector_events=events)", (), {"detector_events": events}), |
| ("fn(detector_events=events, metadata=metadata)", (), {"detector_events": events, "metadata": metadata}), |
| ( |
| "fn(detector_events=events, parity_x=hx, parity_z=hz)", |
| (), |
| {"detector_events": events, "parity_x": hx, "parity_z": hz}, |
| ), |
| ] |
|
|
| for _, args, kwargs in call_patterns: |
| try: |
| return backend_callable(*args, **kwargs) |
| except TypeError: |
| continue |
| |
| return backend_callable(events) |
|
|
|
|
| def _decode_with_backend( |
| backend_name: str, |
| backend_obj: Any, |
| detector_events: BoolArray, |
| metadata: DecoderMetadata, |
| ) -> tuple[np.ndarray, str | None]: |
| if backend_obj is None: |
| raise RuntimeError(f"Backend '{backend_name}' is unavailable") |
|
|
| events = np.asarray(detector_events, dtype=np.bool_) |
| if events.ndim != 2: |
| raise ValueError("detector_events must be a 2D bool array") |
|
|
| attempt_targets: list[str] = [] |
| if isinstance(backend_obj, type): |
| try: |
| candidate = backend_obj() |
| except Exception: |
| candidate = backend_obj |
| else: |
| backend_obj = candidate |
|
|
| for attr_name in ("decode", "decode_events", "decode_detector_events", "decode_batch"): |
| backend_callable = getattr(backend_obj, attr_name, None) |
| if not callable(backend_callable): |
| continue |
| attempt_targets.append(attr_name) |
| result = _safe_invoke_decoder_backend(backend_callable, events, metadata) |
| return _coerce_backend_prediction(result, metadata.num_observables, events.shape[0]), attr_name |
|
|
| if attempt_targets: |
| raise RuntimeError(f"Backend '{backend_name}' has decode symbols but no callable entries in {attempt_targets}") |
|
|
| raise RuntimeError(f"Backend '{backend_name}' does not expose a known decode callable") |
|
|
|
|
| def _probe_backend(name: str) -> tuple[Any, bool, str | None]: |
| if name == "cudaq": |
| return cudaq, bool(cudaq), repr(_CUDAQ_IMPORT_ERROR) if _CUDAQ_IMPORT_ERROR else None |
| if name == "qujax": |
| return qujax, bool(qujax), repr(_QUJAX_IMPORT_ERROR) if _QUJAX_IMPORT_ERROR else None |
| if name == "cuqnn": |
| return cuquantum, bool(cuquantum), repr(_CUQNN_IMPORT_ERROR) if _CUQNN_IMPORT_ERROR else None |
| return None, False, f"Unknown backend '{name}'" |
|
|
|
|
| def _decode_with_backend_or_fallback( |
| backend_name: str, |
| detector_events: BoolArray, |
| metadata: DecoderMetadata, |
| fallback_decoder: UnionFindDecoder, |
| ) -> tuple[DecoderOutput, dict[str, Any]]: |
| backend_obj, enabled, import_error = _probe_backend(backend_name) |
| start = time.perf_counter_ns() |
|
|
| diagnostics: dict[str, Any] = { |
| "backend": backend_name, |
| "backend_available": bool(enabled), |
| "backend_contract": bool(enabled), |
| "backend_error": import_error, |
| "backend_chain": [f"requested:{backend_name}"], |
| "fallback_chain": [f"requested:{backend_name}"], |
| } |
| if not enabled: |
| diagnostics["backend_contract"] = False |
| diagnostics["fallback_chain"] = [f"requested:{backend_name}", "unavailable"] |
| output = _decode_with_parity_matrix(detector_events, metadata, fallback_decoder) |
| diagnostics["backend_error"] = import_error |
| diagnostics["latency_ms"] = (time.perf_counter_ns() - start) / 1_000_000 |
| return ( |
| DecoderOutput( |
| logical_predictions=output.logical_predictions, |
| decoder_name=output.decoder_name, |
| diagnostics={**output.diagnostics, **diagnostics}, |
| ), |
| diagnostics, |
| ) |
|
|
| try: |
| preds, call_name = _decode_with_backend(backend_name, backend_obj, detector_events, metadata) |
| except Exception as exc: |
| diagnostics["backend_error"] = str(exc) |
| diagnostics["backend_chain"].append("backend_fallback") |
| diagnostics["fallback_chain"].append("backend_fallback") |
| output = _decode_with_parity_matrix(detector_events, metadata, fallback_decoder) |
| diagnostics["latency_ms"] = (time.perf_counter_ns() - start) / 1_000_000 |
| return ( |
| DecoderOutput( |
| logical_predictions=output.logical_predictions, |
| decoder_name=output.decoder_name, |
| diagnostics={**output.diagnostics, **diagnostics}, |
| ), |
| diagnostics, |
| ) |
|
|
| diagnostics["backend_error"] = None |
| diagnostics["backend_call"] = call_name |
| diagnostics["backend_chain"].append(f"selected:{backend_name}") |
| diagnostics["fallback_chain"].append(f"selected:{backend_name}") |
| diagnostics["latency_ms"] = (time.perf_counter_ns() - start) / 1_000_000 |
| return DecoderOutput(logical_predictions=preds, decoder_name=backend_name, diagnostics=diagnostics), diagnostics |
|
|
|
|
| def _inject_backend_diagnostics( |
| output: DecoderOutput, |
| backend: str, |
| device: str = "cpu", |
| degraded: bool = False, |
| ) -> DecoderOutput: |
| diagnostics = dict(output.diagnostics) |
| diagnostics.update( |
| { |
| "backend": backend, |
| "device": device, |
| "degraded": bool(degraded), |
| "latency_ms": diagnostics.get("latency_ms"), |
| "fallback_chain": diagnostics.get("fallback_chain", diagnostics.get("backend_chain", [])), |
| }, |
| ) |
| return DecoderOutput( |
| logical_predictions=output.logical_predictions, |
| decoder_name=backend, |
| diagnostics=diagnostics, |
| ) |
|
|
|
|
| @dataclass |
| class CudaQDecoder(DecoderProtocol): |
| """Decoder adapter representing CUDA-Q selection path.""" |
|
|
| name: str = "cudaq" |
| device: str = "cuda" |
| degraded: bool = False |
| decoder: UnionFindDecoder = field(default_factory=UnionFindDecoder) |
|
|
| def decode(self, detector_events: BoolArray, metadata: DecoderMetadata) -> DecoderOutput: |
| output, diagnostics = _decode_with_backend_or_fallback( |
| backend_name="cudaq", |
| detector_events=detector_events, |
| metadata=metadata, |
| fallback_decoder=self.decoder, |
| ) |
| return _inject_backend_diagnostics( |
| output=output, |
| backend=self.name, |
| device=self.device, |
| degraded=bool(diagnostics.get("backend_error") and diagnostics.get("backend_error") not in {None, "None"}), |
| ) |
|
|
|
|
| @dataclass |
| class CuQNNBackendAdapterDecoder(DecoderProtocol): |
| """Adapter label for GPU-optimized graph/QNN backend decode policy.""" |
|
|
| name: str = "cuqnn" |
| device: str = "cuda" |
| degraded: bool = False |
| decoder: UnionFindDecoder = field(default_factory=UnionFindDecoder) |
|
|
| def decode(self, detector_events: BoolArray, metadata: DecoderMetadata) -> DecoderOutput: |
| output, diagnostics = _decode_with_backend_or_fallback( |
| backend_name="cuqnn", |
| detector_events=detector_events, |
| metadata=metadata, |
| fallback_decoder=self.decoder, |
| ) |
| return _inject_backend_diagnostics( |
| output=output, |
| backend=self.name, |
| device=self.device, |
| degraded=bool(diagnostics.get("backend_error") and diagnostics.get("backend_error") not in {None, "None"}), |
| ) |
|
|
|
|
| @dataclass |
| class QuJaxNeuralBPDecoder(DecoderProtocol): |
| """Adapter for qujax-oriented neural BP decode policy.""" |
|
|
| name: str = "qujax" |
| device: str = "gpu" |
| degraded: bool = False |
| decoder: UnionFindDecoder = field(default_factory=UnionFindDecoder) |
|
|
| def decode(self, detector_events: BoolArray, metadata: DecoderMetadata) -> DecoderOutput: |
| output, diagnostics = _decode_with_backend_or_fallback( |
| backend_name="qujax", |
| detector_events=detector_events, |
| metadata=metadata, |
| fallback_decoder=self.decoder, |
| ) |
| return _inject_backend_diagnostics( |
| output=output, |
| backend=self.name, |
| device=self.device, |
| degraded=bool(diagnostics.get("backend_error") and diagnostics.get("backend_error") not in {None, "None"}), |
| ) |
|
|