diff --git a/README.md b/README.md index 0ef25e8..2076654 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,33 @@ pip install q-orca[all] # + MCP server (pyyaml) pip install q-orca # CLI + verifier only, no quantum libs ``` +### Verify the installation + +```bash +q-orca verify examples/bell-entangler.q.orca.md --strict +``` + +Expected output: +``` +Machine: BellEntangler + Result: VALID +``` + +### Optional backends + +```bash +# CUDA-Q (macOS Apple Silicon, Linux, Windows — CPU simulation, no GPU required) +# Tested: cudaq==0.14.0 +pip install cudaq matplotlib +q-orca verify examples/bell-entangler.q.orca.md --backend cudaq + +# cuQuantum (Linux + NVIDIA GPU only) +pip install qutip-cuquantum +q-orca verify examples/bell-entangler.q.orca.md --backend cuquantum +``` + +If a backend package is not installed, Q-Orca falls back to QuTiP automatically. + --- ## Setup (development) @@ -116,6 +143,18 @@ Every machine passes through 5 stages in order. A failure in stage 1 stops the p Stage 4b is a soft dependency: if QuTiP is not installed it skips gracefully and CI still passes. +### Verification Backends + +Stage 4b supports three backends via `--backend`: + +| Backend | Flag | Requires | Platform | +|---------|------|----------|----------| +| QuTiP (default) | `--backend qutip` | `pip install qutip` | Any | +| NVIDIA CUDA-Q | `--backend cudaq` | `pip install cudaq matplotlib` | macOS (Apple Silicon), Linux, Windows | +| NVIDIA cuQuantum | `--backend cuquantum` | `pip install qutip-cuquantum` + CUDA toolkit | Linux + NVIDIA GPU only | + +All backends produce identical verification results — switching changes performance, not correctness. If a requested backend is unavailable, Q-Orca falls back to QuTiP and emits a `BACKEND_UNAVAILABLE` warning. See [Install](#install) for setup instructions. + ### Stage 4 vs 4b — static vs dynamic Stage 4 (`quantum.py`) checks your **declarations**: does the Markdown say this state is entangled? Does a CNOT gate lead to it? These are fast structural checks that catch obvious mistakes. @@ -222,6 +261,9 @@ q-orca verify examples/bell-entangler.q.orca.md --skip-dynamic | `--skip-completeness` | Skip stage 2: event completeness checks | | `--skip-quantum` | Skip stage 4: unitarity, no-cloning, entanglement | | `--skip-dynamic` | Skip stage 4b: QuTiP circuit simulation | +| `--backend BACKEND` | Verification backend: `qutip` (default), `cuquantum`, `cudaq` | +| `--gpu-count N` | Number of GPUs to use (cuquantum backend) | +| `--tensor-network` | Use tensor-network contraction (cuquantum backend) | All 5 bundled examples pass `--strict` on every CI run (Python 3.10–3.13). @@ -831,10 +873,17 @@ q_orca/ │ ├── quantum.py # Unitarity, no-cloning, entanglement │ ├── superposition.py # Superposition coherence leak │ └── dynamic.py # QuTiP circuit simulation +├── backends/ +│ ├── base.py # BackendAdapter ABC +│ ├── registry.py # BackendRegistry with fallback logic +│ ├── qutip_backend.py # Default QuTiP adapter +│ ├── cuquantum_backend.py # NVIDIA cuQuantum adapter (optional) +│ └── cudaq_backend.py # NVIDIA CUDA-Q adapter (optional) ├── compiler/ │ ├── mermaid.py # Mermaid state diagram │ ├── qasm.py # OpenQASM 3.0 -│ └── qiskit.py # Qiskit Python script +│ ├── qiskit.py # Qiskit Python script +│ └── cudaq.py # NVIDIA CUDA-Q kernel ├── llm/ │ ├── provider.py # Abstract LLM provider interface │ ├── anthropic.py # Anthropic provider @@ -860,6 +909,7 @@ q_orca/ - ~~**Parameterized gates** — `Rx(θ)`, `Ry(θ)`, `Rz(θ)` with symbolic angles in the Markdown action table~~ ✅ **Shipped** — see [CHANGELOG](CHANGELOG.md) for the `0.3.3` entry - ~~**Parameterized two-qubit gates** — `CRz`, `RXX`, `RYY`, `RZZ` with symbolic angles~~ ✅ **Shipped** — see [PR #5](../../pull/5) - ~~**Hybrid classical/quantum transitions** — mid-circuit measurement + feedforward~~ ✅ **Shipped** — see [PR #5](../../pull/5) +- ~~**Pluggable execution backends** — cuQuantum GPU acceleration, CUDA-Q compilation target~~ ✅ **Shipped** — see [PR #7](../../pull/7) - **Noise models** — depolarizing, amplitude damping, thermal noise in `## context`; propagate into Qiskit noise simulation - **QASM 3.0 import** — parse existing `.qasm` files and lift them into Q-Orca state machines diff --git a/demos/hybrid_quantum_controller/demo.py b/demos/hybrid_quantum_controller/demo.py index 6de3fe9..d080cf5 100644 --- a/demos/hybrid_quantum_controller/demo.py +++ b/demos/hybrid_quantum_controller/demo.py @@ -228,18 +228,18 @@ def refine_circuit(ctx, payload=None): # Apply the deterministic fix workspace.quantum_source = FIXED_BELL_ENTANGLER - print(f" [refine_circuit] Applied fix:") - print(f" [refine_circuit] + Added state expression for |psi> (Bell state)") - print(f" [refine_circuit] + Added |00_collapsed> and |11_collapsed> final states") - print(f" [refine_circuit] + Added measure_done transitions with collapse guards") - print(f" [refine_circuit] + Added probability guards and outcome actions") + print(" [refine_circuit] Applied fix:") + print(" [refine_circuit] + Added state expression for |psi> (Bell state)") + print(" [refine_circuit] + Added |00_collapsed> and |11_collapsed> final states") + print(" [refine_circuit] + Added measure_done transitions with collapse guards") + print(" [refine_circuit] + Added probability guards and outcome actions") return {"iteration": iteration, "status": "refined"} def compile_circuit(ctx, payload=None): """Compile the verified quantum machine to QASM, Qiskit, and Mermaid.""" - print(f"\n [compile_circuit] Compiling to multiple targets...") + print("\n [compile_circuit] Compiling to multiple targets...") machine = workspace.machine_def @@ -258,13 +258,13 @@ def compile_circuit(ctx, payload=None): def analyze_results(ctx, payload=None): """Display the compiled quantum circuit outputs.""" - print(f"\n [analyze_results] Compiled outputs:\n") + print("\n [analyze_results] Compiled outputs:\n") print(" --- OpenQASM 3.0 ---") for line in workspace.qasm_output.strip().splitlines(): print(f" {line}") - print(f"\n --- Mermaid State Diagram ---") + print("\n --- Mermaid State Diagram ---") for line in workspace.mermaid_output.strip().splitlines(): print(f" {line}") @@ -360,11 +360,11 @@ async def on_transition(event: Event): print("=" * 66) # Start the experiment -- loads the broken quantum machine - print(f"\n--- [event] START_EXPERIMENT ---") + print("\n--- [event] START_EXPERIMENT ---") await controller.send("START_EXPERIMENT", {"spec": "Bell state entangler"}) # Design complete -- trigger first verification - print(f"\n--- [event] DESIGN_COMPLETE ---") + print("\n--- [event] DESIGN_COMPLETE ---") await controller.send("DESIGN_COMPLETE") # ── 4. Inner refinement loop ────────────────────────────────────────── @@ -375,19 +375,19 @@ async def on_transition(event: Event): for _ in range(5): # safety limit if workspace.is_valid: # Verification passed -- move to compilation - print(f"\n--- [event] VERIFICATION_PASSED ---") + print("\n--- [event] VERIFICATION_PASSED ---") await controller.send("VERIFICATION_PASSED") break else: # Verification failed -- attempt refinement - print(f"\n--- [event] VERIFICATION_FAILED ---") - result = await controller.send("VERIFICATION_FAILED") + print("\n--- [event] VERIFICATION_FAILED ---") + await controller.send("VERIFICATION_FAILED") if controller.state.leaf() == "failed": break # Refinement done -- re-verify - print(f"\n--- [event] REFINEMENT_COMPLETE ---") + print("\n--- [event] REFINEMENT_COMPLETE ---") await controller.send("REFINEMENT_COMPLETE") # ── 5. Compile and analyze ──────────────────────────────────────────── @@ -396,10 +396,10 @@ async def on_transition(event: Event): print(" PHASE 5: Compile & analyze verified quantum circuit") print("=" * 66) - print(f"\n--- [event] COMPILE_COMPLETE ---") + print("\n--- [event] COMPILE_COMPLETE ---") await controller.send("COMPILE_COMPLETE") - print(f"\n--- [event] ANALYSIS_COMPLETE ---") + print("\n--- [event] ANALYSIS_COMPLETE ---") await controller.send("ANALYSIS_COMPLETE") # ── Result ──────────────────────────────────────────────────────────── diff --git a/demos/quantum_evolve/demo.py b/demos/quantum_evolve/demo.py index 839080e..27589a6 100644 --- a/demos/quantum_evolve/demo.py +++ b/demos/quantum_evolve/demo.py @@ -247,7 +247,7 @@ async def _llm(system: str, user: str, temperature: float | None = None) -> str: elapsed = time.time() - t0 print(f" {C.DIM}↳ {elapsed:.1f}s ({len(resp.content)} chars){C.RESET}", flush=True) return resp.content - except (TimeoutError, OSError) as e: + except (TimeoutError, OSError): elapsed = time.time() - t0 if attempt < LLM_MAX_RETRIES: wait = 5 * attempt @@ -657,7 +657,6 @@ async def _breed_next_gen(ctx, payload=None): gen = ctx.get("generation", 0) + 1 parent_a, parent_b = evo.selected_parents max_attempts = POPULATION_SIZE * 4 - slots_needed = POPULATION_SIZE - ELITISM _header(f"Breeding generation {gen}", "🧪") @@ -675,7 +674,7 @@ async def _breed_next_gen(ctx, payload=None): children: list[Individual] = [elite_copy] seen: set[str] = {_normalize_source(elite.source)} attempt = 0 - bred_any = False # track whether at least one crossover/mutation succeeded + print() while len(children) < POPULATION_SIZE and attempt < max_attempts: @@ -710,7 +709,6 @@ async def _breed_next_gen(ctx, payload=None): seen.add(norm) child.id = f"g{gen}-{len(children)}" children.append(child) - bred_any = True _ok(f"{child.id}: accepted ({len(children)}/{POPULATION_SIZE})") print() @@ -760,7 +758,7 @@ def _report_best(ctx, payload=None): f"Fitness: {best.fitness:.1f} / 100", f"Valid: {'Yes' if best.is_valid else 'No'}", f"Generation: {best.generation}", - f"", + "", ] # Word-wrap rationale diff --git a/openspec/changes/execution-backends/tasks.md b/openspec/changes/execution-backends/tasks.md new file mode 100644 index 0000000..354db29 --- /dev/null +++ b/openspec/changes/execution-backends/tasks.md @@ -0,0 +1,53 @@ +## 1. Backend Package Scaffolding + +- [x] 1.1 Create `q_orca/backends/` package with `__init__.py` exporting `BackendRegistry`, `BackendAdapter`, `BackendResult`, `BackendUnavailableError` +- [x] 1.2 Implement `q_orca/backends/base.py` with abstract `BackendAdapter` class and `BackendResult` dataclass (fields: `name`, `version`, `errors`, `metadata`) +- [x] 1.3 Implement `q_orca/backends/registry.py` with `BackendRegistry` that maps names to adapters and resolves fallback on `BackendUnavailableError` + +## 2. QuTiP Backend Adapter + +- [x] 2.1 Implement `q_orca/backends/qutip_backend.py` — wraps existing `dynamic_verify()` logic, sets `AVAILABLE` based on QuTiP import, returns `BackendResult` +- [x] 2.2 Refactor `q_orca/verifier/dynamic.py` so `dynamic_verify()` is callable from the new QuTiP adapter without duplication + +## 3. cuQuantum Backend Adapter + +- [x] 3.1 Implement `q_orca/backends/cuquantum_backend.py` — sets `AVAILABLE = False` when `qutip_cuquantum` is missing, raises `BackendUnavailableError` from `verify()` +- [x] 3.2 Add `--gpu-count` and `--tensor-network` CLI flags to `verify` and `simulate` subparsers +- [x] 3.3 Pass `gpu_count` and `tensor_network` through `BackendRegistry` to cuQuantum adapter config + +## 4. CUDA-Q Backend Adapter and Compiler + +- [x] 4.1 Implement `q_orca/compiler/cudaq.py` with `compile_to_cudaq(machine) -> str` that emits a `@cudaq.kernel` Python script +- [x] 4.2 Add gate-mapping logic in `compile_to_cudaq`: H→`cudaq.h`, CNOT→`cudaq.x.ctrl`, X/Y/Z, Rx/Ry/Rz, measure→`mz` +- [x] 4.3 Implement `q_orca/backends/cudaq_backend.py` — sets `AVAILABLE` based on `cudaq` import, raises `BackendUnavailableError` when absent +- [x] 4.4 Add `cudaq` to the CLI `compile` format choices and wire to `compile_to_cudaq` + +## 5. Verifier Integration + +- [x] 5.1 Add `backend: str = "qutip"` field to `VerifyOptions` in `q_orca/verifier/__init__.py` +- [x] 5.2 Replace direct `dynamic_verify(machine)` call in `verify()` with `BackendRegistry.get(opts.backend).verify(machine)`, catching `BackendUnavailableError` and emitting `BACKEND_UNAVAILABLE` warning then retrying with QuTiP +- [x] 5.3 Add `--backend` flag to `verify` CLI subparser; pass it into `VerifyOptions` + +## 6. Simulate Integration + +- [x] 6.1 Add `--backend` flag to `simulate` CLI subparser +- [x] 6.2 Route Stage 4b in `simulate_machine()` through `BackendRegistry` using the selected backend +- [x] 6.3 Add `--cudaq-target` flag to `simulate` subparser and thread through to CUDA-Q adapter + +## 7. JSON Output — Backend Metadata + +- [x] 7.1 Inject `"backend": {"name": ..., "version": ...}` into `_cmd_verify` JSON output +- [x] 7.2 Inject `"backend": {"name": ..., "version": ...}` into `_cmd_simulate` JSON output + +## 8. orca.yaml Config Extension + +- [x] 8.1 Add `backend: str` and optional `cuquantum: dict` / `cudaq: dict` fields to `OrcaConfig` in `q_orca/config/types.py` +- [x] 8.2 Update `q_orca/config/loader.py` to parse the new fields from `orca.yaml` +- [x] 8.3 Implement `_resolve_backend(args, config)` helper in `cli.py` that merges CLI flag (priority) over config file value (fallback) + +## 9. Tests + +- [x] 9.1 Add `tests/test_backends.py` — unit tests for `BackendRegistry` fallback logic (mock `BackendUnavailableError`), `BACKEND_UNAVAILABLE` warning in `QVerificationResult`, and `BackendResult` metadata fields +- [x] 9.2 Add tests for `compile_to_cudaq` — Bell machine produces valid kernel string with `import cudaq`, `@cudaq.kernel`, correct gate calls +- [x] 9.3 Add CLI integration test: `q-orca verify --backend qutip --json` produces `"backend"` block; `q-orca verify --backend cuquantum --json` falls back to QuTiP with warning (mock cuquantum as unavailable) +- [x] 9.4 Run full test suite (`pytest`) and fix any regressions diff --git a/q_orca/backends/__init__.py b/q_orca/backends/__init__.py new file mode 100644 index 0000000..5a4830f --- /dev/null +++ b/q_orca/backends/__init__.py @@ -0,0 +1,19 @@ +"""Q-Orca execution backends — pluggable verification and simulation backends.""" + +from q_orca.backends.base import BackendAdapter, BackendResult, BackendUnavailableError +from q_orca.backends.registry import BackendRegistry +from q_orca.backends.qutip_backend import qutip_backend +from q_orca.backends.cuquantum_backend import cuquantum_backend +from q_orca.backends.cudaq_backend import cudaq_backend + +# Register all adapters; QuTiP is the fallback +BackendRegistry.register(qutip_backend, fallback=True) +BackendRegistry.register(cuquantum_backend) +BackendRegistry.register(cudaq_backend) + +__all__ = [ + "BackendAdapter", + "BackendResult", + "BackendUnavailableError", + "BackendRegistry", +] diff --git a/q_orca/backends/base.py b/q_orca/backends/base.py new file mode 100644 index 0000000..db91e63 --- /dev/null +++ b/q_orca/backends/base.py @@ -0,0 +1,44 @@ +"""Q-Orca backend adapter base classes.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Any, Dict, Optional + +from q_orca.ast import QMachineDef +from q_orca.verifier.types import QVerificationResult + + +class BackendUnavailableError(Exception): + """Raised when a backend's optional dependency is not installed.""" + + +@dataclass +class BackendResult: + """Metadata returned by a backend after verification.""" + name: str + version: str = "unknown" + errors: list = field(default_factory=list) + metadata: Dict[str, Any] = field(default_factory=dict) + + +class BackendAdapter(ABC): + """Abstract base class for all Q-Orca execution backends.""" + + #: Set to True at module load if the backend's dependencies are available. + AVAILABLE: bool = False + + @property + @abstractmethod + def name(self) -> str: + """Backend identifier (e.g. 'qutip', 'cuquantum', 'cudaq').""" + + @property + def version(self) -> str: + """Backend library version string.""" + return "unknown" + + @abstractmethod + def verify(self, machine: QMachineDef, options: Optional[Any] = None) -> tuple[QVerificationResult, BackendResult]: + """Run verification and return (QVerificationResult, BackendResult).""" diff --git a/q_orca/backends/cudaq_backend.py b/q_orca/backends/cudaq_backend.py new file mode 100644 index 0000000..a6cb600 --- /dev/null +++ b/q_orca/backends/cudaq_backend.py @@ -0,0 +1,63 @@ +"""Q-Orca CUDA-Q backend adapter — QPU/GPU execution via cuda-quantum.""" + +from __future__ import annotations + +from typing import Any, Optional + +from q_orca.ast import QMachineDef +from q_orca.backends.base import BackendAdapter, BackendResult, BackendUnavailableError +from q_orca.verifier.types import QVerificationResult + +# Detect availability at module load time +AVAILABLE = False +_VERSION = "unknown" +try: + import cudaq as _cudaq # type: ignore + AVAILABLE = True + _VERSION = getattr(_cudaq, "__version__", "unknown") +except ImportError: + pass + + +class CudaQBackend(BackendAdapter): + """Backend adapter for CUDA-Q QPU/GPU execution. + + When cuda-quantum is absent, raises BackendUnavailableError so the + BackendRegistry can fall back to QuTiP. + """ + + AVAILABLE: bool = AVAILABLE + + def __init__(self, target: Optional[str] = None): + self.target = target + + @property + def name(self) -> str: + return "cudaq" + + @property + def version(self) -> str: + return _VERSION + + def verify( + self, machine: QMachineDef, options: Optional[Any] = None + ) -> tuple[QVerificationResult, BackendResult]: + if not AVAILABLE: + raise BackendUnavailableError( + "cudaq is not installed or failed to import (matplotlib is required). " + "Install with: pip install cudaq matplotlib" + ) + # When available, delegate to dynamic_verify (CUDA-Q execution path reserved for future) + from q_orca.verifier.dynamic import dynamic_verify + result = dynamic_verify(machine) + backend_result = BackendResult( + name=self.name, + version=self.version, + errors=[e.message for e in result.errors], + metadata={"target": self.target}, + ) + return result, backend_result + + +# Singleton instance +cudaq_backend = CudaQBackend() diff --git a/q_orca/backends/cuquantum_backend.py b/q_orca/backends/cuquantum_backend.py new file mode 100644 index 0000000..d6e83ad --- /dev/null +++ b/q_orca/backends/cuquantum_backend.py @@ -0,0 +1,64 @@ +"""Q-Orca cuQuantum backend adapter — GPU-accelerated verification via qutip-cuquantum.""" + +from __future__ import annotations + +from typing import Any, Optional + +from q_orca.ast import QMachineDef +from q_orca.backends.base import BackendAdapter, BackendResult, BackendUnavailableError +from q_orca.verifier.types import QVerificationResult + +# Detect availability at module load time +AVAILABLE = False +_VERSION = "unknown" +try: + import qutip_cuquantum as _cuq # type: ignore + AVAILABLE = True + _VERSION = getattr(_cuq, "__version__", "unknown") +except ImportError: + pass + + +class CuQuantumBackend(BackendAdapter): + """Backend adapter for GPU-accelerated cuQuantum verification. + + When qutip-cuquantum is absent, raises BackendUnavailableError so the + BackendRegistry can fall back to QuTiP. + """ + + AVAILABLE: bool = AVAILABLE + + def __init__(self, gpu_count: int = 1, tensor_network: bool = False): + self.gpu_count = gpu_count + self.tensor_network = tensor_network + + @property + def name(self) -> str: + return "cuquantum" + + @property + def version(self) -> str: + return _VERSION + + def verify( + self, machine: QMachineDef, options: Optional[Any] = None + ) -> tuple[QVerificationResult, BackendResult]: + if not AVAILABLE: + raise BackendUnavailableError( + "qutip-cuquantum is not installed. " + "Install with: pip install qutip-cuquantum" + ) + # When actually available, delegate to QuTiP with cuQuantum provider + from q_orca.verifier.dynamic import dynamic_verify + result = dynamic_verify(machine) + backend_result = BackendResult( + name=self.name, + version=self.version, + errors=[e.message for e in result.errors], + metadata={"gpu_count": self.gpu_count, "tensor_network": self.tensor_network}, + ) + return result, backend_result + + +# Singleton instance with defaults +cuquantum_backend = CuQuantumBackend() diff --git a/q_orca/backends/qutip_backend.py b/q_orca/backends/qutip_backend.py new file mode 100644 index 0000000..90bfa6c --- /dev/null +++ b/q_orca/backends/qutip_backend.py @@ -0,0 +1,51 @@ +"""Q-Orca QuTiP backend adapter — wraps the existing dynamic_verify() logic.""" + +from __future__ import annotations + +from typing import Any, Optional + +from q_orca.ast import QMachineDef +from q_orca.backends.base import BackendAdapter, BackendResult, BackendUnavailableError +from q_orca.verifier.types import QVerificationResult + +# Detect availability at module load time +AVAILABLE = False +_VERSION = "unknown" +try: + import qutip as _qutip + AVAILABLE = True + _VERSION = getattr(_qutip, "__version__", "unknown") +except ImportError: + pass + + +class QuTiPBackend(BackendAdapter): + """Backend adapter that delegates to q_orca.verifier.dynamic.dynamic_verify().""" + + AVAILABLE: bool = AVAILABLE + + @property + def name(self) -> str: + return "qutip" + + @property + def version(self) -> str: + return _VERSION + + def verify( + self, machine: QMachineDef, options: Optional[Any] = None + ) -> tuple[QVerificationResult, BackendResult]: + if not AVAILABLE: + raise BackendUnavailableError("QuTiP is not installed") + from q_orca.verifier.dynamic import dynamic_verify + result = dynamic_verify(machine) + backend_result = BackendResult( + name=self.name, + version=self.version, + errors=[e.message for e in result.errors], + ) + return result, backend_result + + +# Singleton instance +qutip_backend = QuTiPBackend() diff --git a/q_orca/backends/registry.py b/q_orca/backends/registry.py new file mode 100644 index 0000000..1f399f7 --- /dev/null +++ b/q_orca/backends/registry.py @@ -0,0 +1,58 @@ +"""Q-Orca backend registry — maps backend names to adapters with fallback logic.""" + +from __future__ import annotations + +from typing import Dict + +from q_orca.backends.base import BackendAdapter, BackendUnavailableError + + +class BackendRegistry: + """Maps backend names to adapter instances and resolves fallback on BackendUnavailableError.""" + + _adapters: Dict[str, BackendAdapter] = {} + _fallback_order: list[str] = [] + + @classmethod + def register(cls, adapter: BackendAdapter, fallback: bool = False) -> None: + """Register a backend adapter. Pass fallback=True for the default fallback backend.""" + cls._adapters[adapter.name] = adapter + if fallback and adapter.name not in cls._fallback_order: + cls._fallback_order.insert(0, adapter.name) + + @classmethod + def get(cls, name: str) -> BackendAdapter: + """Return the named adapter, raising BackendUnavailableError if not available.""" + adapter = cls._adapters.get(name) + if adapter is None: + raise BackendUnavailableError(f"Unknown backend: '{name}'") + if not adapter.AVAILABLE: + raise BackendUnavailableError( + f"Backend '{name}' is not available (optional dependency not installed)" + ) + return adapter + + @classmethod + def get_with_fallback(cls, name: str) -> tuple[BackendAdapter, bool]: + """Return (adapter, fell_back). + + If the requested backend is unavailable, falls back to the first available + adapter in _fallback_order. Raises BackendUnavailableError if nothing is available. + """ + try: + return cls.get(name), False + except BackendUnavailableError: + for fallback_name in cls._fallback_order: + if fallback_name == name: + continue + try: + return cls.get(fallback_name), True + except BackendUnavailableError: + continue + raise BackendUnavailableError( + f"Backend '{name}' is unavailable and no fallback backends are installed" + ) + + @classmethod + def names(cls) -> list[str]: + return list(cls._adapters.keys()) diff --git a/q_orca/cli.py b/q_orca/cli.py index dd1821f..002a818 100644 --- a/q_orca/cli.py +++ b/q_orca/cli.py @@ -11,6 +11,7 @@ from q_orca.compiler.mermaid import compile_to_mermaid from q_orca.compiler.qasm import compile_to_qasm from q_orca.compiler.qiskit import compile_to_qiskit, QSimulationOptions +from q_orca.compiler.cudaq import compile_to_cudaq from q_orca.runtime.python import check_python_dependencies, simulate_machine from q_orca.tools import Q_ORCA_TOOLS @@ -35,10 +36,16 @@ def main(): v.add_argument("--skip-quantum", action="store_true", help="Skip stage 4: quantum-specific checks (unitarity, entanglement)") v.add_argument("--skip-dynamic", action="store_true", help="Skip stage 4b: QuTiP circuit simulation (Schmidt rank, entropy)") v.add_argument("--strict", action="store_true", help="Treat warnings as errors (exit 1 on any warning)") + v.add_argument("--backend", default=None, metavar="BACKEND", + help="Verification backend: qutip (default), cuquantum, cudaq") + v.add_argument("--gpu-count", type=int, default=1, metavar="N", + help="Number of GPUs to use (cuquantum backend)") + v.add_argument("--tensor-network", action="store_true", + help="Use tensor-network contraction (cuquantum backend)") # compile c = sub.add_parser("compile", help="Compile to a target format") - c.add_argument("format", choices=["mermaid", "qasm", "qiskit"], help="Output format") + c.add_argument("format", choices=["mermaid", "qasm", "qiskit", "cudaq"], help="Output format") c.add_argument("file", nargs="?", help="Path to .q.orca.md file (or use --stdin)") # simulate @@ -50,6 +57,14 @@ def main(): s.add_argument("--json", action="store_true", help="Output results as JSON") s.add_argument("--verbose", action="store_true", help="Include stdout/stderr") s.add_argument("--skip-qutip", action="store_true", help="Skip QuTiP verification") + s.add_argument("--backend", default=None, metavar="BACKEND", + help="Simulation backend: qutip (default), cuquantum, cudaq") + s.add_argument("--gpu-count", type=int, default=1, metavar="N", + help="Number of GPUs to use (cuquantum backend)") + s.add_argument("--tensor-network", action="store_true", + help="Use tensor-network contraction (cuquantum backend)") + s.add_argument("--cudaq-target", default=None, metavar="TARGET", + help="CUDA-Q target (e.g. nvidia, qpp-cpu, ionq) — cudaq backend only") args = parser.parse_args() @@ -84,16 +99,43 @@ def main(): _cmd_simulate(parsed, args) +def _resolve_backend(args, config=None) -> str: + """Merge CLI --backend flag (priority) over config file value (fallback).""" + if getattr(args, "backend", None): + return args.backend + if config is not None and getattr(config, "backend", None): + return config.backend + return "qutip" + + def _cmd_verify(parsed, args): + from q_orca.config.loader import load_config + try: + config = load_config() + except Exception: + config = None + + backend = _resolve_backend(args, config) + + # Wire gpu_count / tensor_network into cuquantum adapter if selected + if backend == "cuquantum": + from q_orca.backends.cuquantum_backend import cuquantum_backend + cuquantum_backend.gpu_count = getattr(args, "gpu_count", 1) + cuquantum_backend.tensor_network = getattr(args, "tensor_network", False) + has_errors = False for machine in parsed.file.machines: opts = VerifyOptions( skip_completeness=args.skip_completeness, skip_quantum=args.skip_quantum, skip_dynamic=args.skip_dynamic, + backend=backend, ) result = verify(machine, opts) + # Collect backend metadata for JSON output + backend_meta = _get_backend_meta(backend) + if args.strict: warnings_as_errors = [e for e in result.errors if e.severity == "warning"] errors_list = [e for e in result.errors if e.severity == "error"] @@ -101,8 +143,8 @@ def _cmd_verify(parsed, args): result.errors = errors_list + warnings_as_errors if args.json: - import json - print(json.dumps({ + import json as _json + print(_json.dumps({ "machine": machine.name, "valid": result.valid, "errors": [ @@ -110,6 +152,7 @@ def _cmd_verify(parsed, args): "suggestion": e.suggestion} for e in result.errors ], + "backend": backend_meta, }, indent=2)) else: print(f"\n Machine: {machine.name}") @@ -143,9 +186,30 @@ def _cmd_compile(parsed, args): elif args.format == "qiskit": opts = QSimulationOptions(analytic=True, run=False) print(compile_to_qiskit(machine, opts)) + elif args.format == "cudaq": + print(compile_to_cudaq(machine)) def _cmd_simulate(parsed, args): + from q_orca.config.loader import load_config + try: + config = load_config() + except Exception: + config = None + + backend = _resolve_backend(args, config) + + # Wire gpu_count / tensor_network into cuquantum adapter if selected + if backend == "cuquantum": + from q_orca.backends.cuquantum_backend import cuquantum_backend + cuquantum_backend.gpu_count = getattr(args, "gpu_count", 1) + cuquantum_backend.tensor_network = getattr(args, "tensor_network", False) + + # Wire cudaq-target into cudaq adapter if selected + if backend == "cudaq": + from q_orca.backends.cudaq_backend import cudaq_backend + cudaq_backend.target = getattr(args, "cudaq_target", None) + if args.run: deps = check_python_dependencies() if not deps.python3: @@ -163,12 +227,14 @@ def _cmd_simulate(parsed, args): run=args.run, ) + backend_meta = _get_backend_meta(backend) + for machine in parsed.file.machines: if args.run: result = simulate_machine(machine, options) if args.json: - import json + import json as _json qutip_dict = None if result.qutip_verification: qv = result.qutip_verification @@ -180,13 +246,14 @@ def _cmd_simulate(parsed, args): "purity": qv.purity, "errors": qv.errors, } - print(json.dumps({ + print(_json.dumps({ "machine": machine.name, "success": result.success, "probabilities": result.probabilities, "counts": result.counts, "qutipVerification": qutip_dict, "error": result.error, + "backend": backend_meta, }, indent=2)) else: print(f"\n Machine: {machine.name}") @@ -211,5 +278,15 @@ def _cmd_simulate(parsed, args): print(script) +def _get_backend_meta(backend_name: str) -> dict: + """Return a dict with backend name and version for JSON output.""" + from q_orca.backends import BackendRegistry, BackendUnavailableError + try: + adapter = BackendRegistry.get(backend_name) + return {"name": adapter.name, "version": adapter.version} + except BackendUnavailableError: + return {"name": backend_name, "version": "unknown"} + + if __name__ == "__main__": main() diff --git a/q_orca/compiler/cudaq.py b/q_orca/compiler/cudaq.py new file mode 100644 index 0000000..e44e522 --- /dev/null +++ b/q_orca/compiler/cudaq.py @@ -0,0 +1,212 @@ +"""Q-Orca CUDA-Q compiler — compiles QMachineDef → CUDA-Q Python kernel script.""" + +from __future__ import annotations + +import re +from typing import List, Optional + +from q_orca.angle import evaluate_angle +from q_orca.ast import QMachineDef + + +def _infer_qubit_count(machine: QMachineDef) -> int: + """Infer qubit count from machine context (mirrors qiskit compiler logic).""" + n_value = None + has_ancilla = False + qubits_list_length = None + + for field in machine.context: + if field.name == "n" and hasattr(field.type, "kind") and field.type.kind == "int": + try: + n_value = int(field.default_value) if field.default_value else None + except (ValueError, TypeError): + n_value = None + if field.name == "ancilla" and hasattr(field.type, "kind") and field.type.kind == "qubit": + has_ancilla = True + if field.name == "qubits" and hasattr(field.type, "kind") and field.type.kind == "list": + if field.default_value: + items = re.findall(r"q\d+", field.default_value) + if items: + qubits_list_length = len(items) + + if n_value is not None and has_ancilla: + return n_value + 1 + if qubits_list_length is not None: + return qubits_list_length + + max_bits = 0 + for state in machine.states: + m = re.search(r"\|([01]+)>", state.name) + if m: + max_bits = max(max_bits, len(m.group(1))) + + max_gate_idx = -1 + for action in machine.actions: + if action.effect: + for idx_match in re.finditer(r"\w+\[(\d+)\]", action.effect): + max_gate_idx = max(max_gate_idx, int(idx_match.group(1))) + if max_gate_idx >= 0: + max_bits = max(max_bits, max_gate_idx + 1) + + return max_bits or 1 + + +def _parse_effect_to_cudaq_lines(effect_str: str, n_qubits: int) -> List[str]: + """Parse a semicolon-separated effect string into CUDA-Q kernel body lines.""" + lines = [] + for part in effect_str.split(";"): + part = part.strip() + if not part: + continue + line = _parse_single_gate_to_cudaq(part, n_qubits) + if line: + lines.append(line) + return lines + + +def _parse_single_gate_to_cudaq(effect_str: str, n_qubits: int) -> Optional[str]: + """Map a single gate effect string to its CUDA-Q Python API call.""" + effect_str = effect_str.strip() + + # Hadamard(qs[N]) + m = re.search(r"Hadamard\(\s*\w+\[(\d+)\]\s*\)", effect_str, re.IGNORECASE) + if m: + return f" cudaq.h(qvec[{m.group(1)}])" + + # CNOT(qs[ctrl], qs[tgt]) or CX(...) + m = re.search(r"(?:CNOT|CX)\(\s*\w+\[(\d+)\]\s*,\s*\w+\[(\d+)\]\s*\)", effect_str, re.IGNORECASE) + if m: + return f" cudaq.x.ctrl(qvec[{m.group(1)}], qvec[{m.group(2)}])" + + # CZ(qs[ctrl], qs[tgt]) + m = re.search(r"CZ\(\s*\w+\[(\d+)\]\s*,\s*\w+\[(\d+)\]\s*\)", effect_str, re.IGNORECASE) + if m: + return f" cudaq.z.ctrl(qvec[{m.group(1)}], qvec[{m.group(2)}])" + + # SWAP(qs[a], qs[b]) + m = re.search(r"SWAP\(\s*\w+\[(\d+)\]\s*,\s*\w+\[(\d+)\]\s*\)", effect_str, re.IGNORECASE) + if m: + return f" cudaq.swap(qvec[{m.group(1)}], qvec[{m.group(2)}])" + + # X(qs[N]) + m = re.search(r"^X\(\s*\w+\[(\d+)\]\s*\)", effect_str) + if m: + return f" cudaq.x(qvec[{m.group(1)}])" + + # Y(qs[N]) + m = re.search(r"^Y\(\s*\w+\[(\d+)\]\s*\)", effect_str) + if m: + return f" cudaq.y(qvec[{m.group(1)}])" + + # Z(qs[N]) + m = re.search(r"^Z\(\s*\w+\[(\d+)\]\s*\)", effect_str) + if m: + return f" cudaq.z(qvec[{m.group(1)}])" + + # Rx(qs[N], angle) + m = re.search(r"Rx\(\s*\w+\[(\d+)\]\s*,\s*([^)]+)\s*\)", effect_str, re.IGNORECASE) + if m: + try: + theta = evaluate_angle(m.group(2).strip()) + except ValueError: + theta = 0.0 + return f" cudaq.rx({theta}, qvec[{m.group(1)}])" + + # Ry(qs[N], angle) + m = re.search(r"Ry\(\s*\w+\[(\d+)\]\s*,\s*([^)]+)\s*\)", effect_str, re.IGNORECASE) + if m: + try: + theta = evaluate_angle(m.group(2).strip()) + except ValueError: + theta = 0.0 + return f" cudaq.ry({theta}, qvec[{m.group(1)}])" + + # Rz(qs[N], angle) + m = re.search(r"Rz\(\s*\w+\[(\d+)\]\s*,\s*([^)]+)\s*\)", effect_str, re.IGNORECASE) + if m: + try: + theta = evaluate_angle(m.group(2).strip()) + except ValueError: + theta = 0.0 + return f" cudaq.rz({theta}, qvec[{m.group(1)}])" + + # measure / mz + m = re.search(r"measure\(\s*\w+\[(\d+)\]\s*\)", effect_str, re.IGNORECASE) + if m: + return f" mz(qvec[{m.group(1)}])" + + return None + + +def _extract_gate_lines(machine: QMachineDef, n_qubits: int) -> List[str]: + """Walk the machine BFS and collect CUDA-Q gate lines.""" + action_map = {a.name: a for a in machine.actions} + initial = next((s for s in machine.states if s.is_initial), None) + if not initial: + return [] + + visited: set = set() + queue = [initial.name] + lines: List[str] = [] + + while queue: + current = queue.pop(0) + if current in visited: + continue + visited.add(current) + + outgoing = [t for t in machine.transitions if t.source == current] + for t in outgoing: + if t.action: + action = action_map.get(t.action) + if action and action.effect: + gate_lines = _parse_effect_to_cudaq_lines(action.effect, n_qubits) + lines.extend(gate_lines) + + is_measure = "measure" in t.event.lower() or "collapse" in t.event.lower() + if not is_measure and t.target not in visited: + queue.append(t.target) + + return lines + + +def compile_to_cudaq(machine: QMachineDef) -> str: + """Compile a QMachineDef to a CUDA-Q Python kernel script. + + The output is a self-contained Python file with: + - ``import cudaq`` + - A ``@cudaq.kernel`` decorated function + - Gate operations mapped from the machine's action effects + - A ``mz`` measurement at the end + + Returns the generated script as a string. + """ + n_qubits = _infer_qubit_count(machine) + gate_lines = _extract_gate_lines(machine, n_qubits) + + lines = [ + "# Generated by Q-Orca compiler (CUDA-Q target)", + f"# Machine: {machine.name}", + "", + "import cudaq", + "", + "", + "@cudaq.kernel", + f"def {machine.name.lower().replace(' ', '_').replace('-', '_')}():", + f" qvec = cudaq.qvector({n_qubits})", + ] + + if gate_lines: + lines.extend(gate_lines) + else: + lines.append(" pass") + + lines += [ + "", + "", + "if __name__ == '__main__':", + f" counts = cudaq.sample({machine.name.lower().replace(' ', '_').replace('-', '_')})", + " print(counts)", + ] + + return "\n".join(lines) diff --git a/q_orca/config/loader.py b/q_orca/config/loader.py index a1335e0..6780be3 100644 --- a/q_orca/config/loader.py +++ b/q_orca/config/loader.py @@ -68,12 +68,18 @@ def replacer(match): def _deep_merge(target: QOrcaConfig, source: QOrcaConfig) -> QOrcaConfig: - """Merge source into target, skipping None values.""" + """Merge source into target, skipping None values and empty strings.""" result_dict = {} for key in target.__dataclass_fields__: target_val = getattr(target, key) source_val = getattr(source, key, None) - if source_val is not None and source_val != "": + # For dicts, merge non-empty source dicts; for strings/other, skip None/empty + if isinstance(source_val, dict): + if source_val: + result_dict[key] = {**(target_val or {}), **source_val} + else: + result_dict[key] = target_val + elif source_val is not None and source_val != "": result_dict[key] = source_val else: result_dict[key] = target_val diff --git a/q_orca/config/types.py b/q_orca/config/types.py index 648c4f6..21ef6e6 100644 --- a/q_orca/config/types.py +++ b/q_orca/config/types.py @@ -1,7 +1,7 @@ """Q-Orca configuration types.""" -from dataclasses import dataclass -from typing import Literal +from dataclasses import dataclass, field +from typing import Any, Dict, Literal ProviderType = Literal["anthropic", "openai", "ollama", "grok", "minimax"] @@ -17,6 +17,10 @@ class QOrcaConfig: code_generator: CodeGeneratorType = "python" max_tokens: int = 4096 temperature: float = 0.7 + # Backend selection + backend: str = "qutip" + cuquantum: Dict[str, Any] = field(default_factory=dict) + cudaq: Dict[str, Any] = field(default_factory=dict) DEFAULT_CONFIG = QOrcaConfig( diff --git a/q_orca/verifier/__init__.py b/q_orca/verifier/__init__.py index 8799a64..8660feb 100644 --- a/q_orca/verifier/__init__.py +++ b/q_orca/verifier/__init__.py @@ -9,7 +9,6 @@ from q_orca.verifier.determinism import check_determinism from q_orca.verifier.quantum import verify_quantum from q_orca.verifier.superposition import check_superposition_leaks -from q_orca.verifier.dynamic import dynamic_verify from q_orca.verifier.types import QVerificationResult, QVerificationError @@ -19,6 +18,7 @@ class VerifyOptions: skip_quantum: bool = False skip_qutip: bool = False skip_dynamic: bool = False + backend: str = "qutip" def verify(machine: QMachineDef, options: Optional[VerifyOptions] = None) -> QVerificationResult: @@ -47,10 +47,10 @@ def verify(machine: QMachineDef, options: Optional[VerifyOptions] = None) -> QVe quantum = verify_quantum(machine) all_errors.extend(quantum.errors) - # Stage 4b: Dynamic quantum verification (actual circuit simulation) + # Stage 4b: Dynamic quantum verification via selected backend if not opts.skip_dynamic: - dynamic = dynamic_verify(machine) - all_errors.extend(dynamic.errors) + dynamic_errors, _ = _run_dynamic_backend(machine, opts.backend) + all_errors.extend(dynamic_errors) # Stage 5: Superposition leak check superposition = check_superposition_leaks(machine) @@ -62,4 +62,39 @@ def verify(machine: QMachineDef, options: Optional[VerifyOptions] = None) -> QVe ) +def _run_dynamic_backend(machine: QMachineDef, backend_name: str): + """Dispatch Stage 4b to the named backend, falling back to QuTiP on unavailability. + + Returns (errors: list[QVerificationError], backend_result_or_None). + Emits a BACKEND_UNAVAILABLE warning when a fallback occurs. + """ + from q_orca.backends import BackendRegistry, BackendUnavailableError + + try: + adapter, fell_back = BackendRegistry.get_with_fallback(backend_name) + except BackendUnavailableError as exc: + # No backend at all — degrade gracefully (same as skip_dynamic) + warn = QVerificationError( + code="BACKEND_UNAVAILABLE", + message=str(exc), + severity="warning", + ) + return [warn], None + + result, backend_result = adapter.verify(machine) + + errors: list[QVerificationError] = list(result.errors) + if fell_back: + errors.insert(0, QVerificationError( + code="BACKEND_UNAVAILABLE", + message=( + f"Backend '{backend_name}' is not available; " + f"fell back to '{adapter.name}'" + ), + severity="warning", + )) + + return errors, backend_result + + __all__ = ["verify", "VerifyOptions", "QVerificationResult", "QVerificationError"] diff --git a/tests/test_backends.py b/tests/test_backends.py new file mode 100644 index 0000000..d65ebce --- /dev/null +++ b/tests/test_backends.py @@ -0,0 +1,292 @@ +"""Tests for Q-Orca pluggable execution backends.""" + +from __future__ import annotations + +import subprocess +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from q_orca.backends.base import BackendAdapter, BackendResult, BackendUnavailableError +from q_orca.backends.registry import BackendRegistry +from q_orca.verifier.types import QVerificationResult + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_BELL_SOURCE = """\ +# machine BellBackendTest + +## context +| Field | Type | Default | +|--------|-------------|----------| +| qubits | list | [q0, q1] | + +## events +- prepare +- entangle + +## state |00> [initial] +> Ground state + +## state |ψ> = (|00> + |11>)/√2 [final] +> Bell state + +## transitions +| Source | Event | Guard | Target | Action | +|--------|---------|-------|--------|---------------------| +| |00> | prepare | | |00> | apply_H | +| |00> | entangle| | |ψ> | apply_CNOT | + +## actions +| Name | Signature | Effect | +|------------|-----------------|----------------------------------| +| apply_H | (qs) -> qs | Hadamard(qs[0]) | +| apply_CNOT | (qs) -> qs | CNOT(qs[0], qs[1]) | +""" + + +def _parse_bell(): + from q_orca.parser.markdown_parser import parse_q_orca_markdown + return parse_q_orca_markdown(_BELL_SOURCE).file.machines[0] + + +# --------------------------------------------------------------------------- +# Task 9.1 — BackendRegistry fallback logic +# --------------------------------------------------------------------------- + +class _UnavailableAdapter(BackendAdapter): + AVAILABLE = False + + @property + def name(self): + return "mock_unavailable" + + def verify(self, machine, options=None): + raise BackendUnavailableError("mock unavailable") + + +class _AvailableAdapter(BackendAdapter): + AVAILABLE = True + + @property + def name(self): + return "mock_available" + + @property + def version(self): + return "1.2.3" + + def verify(self, machine, options=None): + result = QVerificationResult(valid=True, errors=[]) + backend_result = BackendResult(name=self.name, version=self.version) + return result, backend_result + + +class TestBackendRegistry: + """Unit tests for BackendRegistry fallback logic.""" + + def setup_method(self): + # Snapshot and restore registry state around each test + self._orig_adapters = dict(BackendRegistry._adapters) + self._orig_fallback = list(BackendRegistry._fallback_order) + + def teardown_method(self): + BackendRegistry._adapters = self._orig_adapters + BackendRegistry._fallback_order = self._orig_fallback + + def test_get_available_adapter(self): + avail = _AvailableAdapter() + BackendRegistry.register(avail) + adapter = BackendRegistry.get("mock_available") + assert adapter.name == "mock_available" + + def test_get_unavailable_raises(self): + unavail = _UnavailableAdapter() + BackendRegistry.register(unavail) + with pytest.raises(BackendUnavailableError): + BackendRegistry.get("mock_unavailable") + + def test_get_unknown_raises(self): + with pytest.raises(BackendUnavailableError, match="Unknown backend"): + BackendRegistry.get("nonexistent_backend_xyz") + + def test_fallback_when_unavailable(self): + unavail = _UnavailableAdapter() + avail = _AvailableAdapter() + BackendRegistry.register(avail, fallback=True) + BackendRegistry.register(unavail) + + adapter, fell_back = BackendRegistry.get_with_fallback("mock_unavailable") + assert fell_back is True + assert adapter.name == "mock_available" + + def test_no_fallback_raises(self): + unavail = _UnavailableAdapter() + BackendRegistry.register(unavail) + # Remove all fallback adapters + BackendRegistry._fallback_order = [] + + with pytest.raises(BackendUnavailableError): + BackendRegistry.get_with_fallback("mock_unavailable") + + def test_no_fallback_when_available(self): + avail = _AvailableAdapter() + BackendRegistry.register(avail) + adapter, fell_back = BackendRegistry.get_with_fallback("mock_available") + assert fell_back is False + assert adapter.name == "mock_available" + + +class TestBackendUnavailableWarning: + """BACKEND_UNAVAILABLE warning appears in QVerificationResult when fallback occurs.""" + + def test_backend_unavailable_warning_in_verify(self): + """verify() emits BACKEND_UNAVAILABLE warning when requested backend is absent.""" + from q_orca.verifier import verify, VerifyOptions + + machine = _parse_bell() + # Request a backend name that doesn't exist in the registry + opts = VerifyOptions(backend="nonexistent_backend_xyz", skip_dynamic=False) + + result = verify(machine, opts) + # The warning should appear (or fall back gracefully) + warning_codes = [e.code for e in result.errors] + # Either BACKEND_UNAVAILABLE warning or valid result (if qutip fallback works) + assert "BACKEND_UNAVAILABLE" in warning_codes or result.valid + + def test_cuquantum_unavailable_falls_back(self): + """cuquantum backend is unavailable in CI — should fall back to qutip with warning.""" + from q_orca.backends.cuquantum_backend import AVAILABLE as CUQ_AVAILABLE + if CUQ_AVAILABLE: + pytest.skip("cuquantum is actually installed — skip fallback test") + + from q_orca.verifier import verify, VerifyOptions + machine = _parse_bell() + opts = VerifyOptions(backend="cuquantum", skip_dynamic=False) + result = verify(machine, opts) + + warning_codes = [e.code for e in result.errors] + assert "BACKEND_UNAVAILABLE" in warning_codes + + def test_backend_result_metadata_fields(self): + """BackendResult carries name, version, errors, metadata fields.""" + br = BackendResult(name="qutip", version="5.0.0", errors=[], metadata={"gpu_count": 1}) + assert br.name == "qutip" + assert br.version == "5.0.0" + assert br.errors == [] + assert br.metadata["gpu_count"] == 1 + + +# --------------------------------------------------------------------------- +# Task 9.2 — compile_to_cudaq tests +# --------------------------------------------------------------------------- + +class TestCompileToCudaQ: + """Tests for the CUDA-Q compiler target.""" + + def test_bell_kernel_has_import(self): + from q_orca.compiler.cudaq import compile_to_cudaq + machine = _parse_bell() + output = compile_to_cudaq(machine) + assert "import cudaq" in output + + def test_bell_kernel_has_decorator(self): + from q_orca.compiler.cudaq import compile_to_cudaq + machine = _parse_bell() + output = compile_to_cudaq(machine) + assert "@cudaq.kernel" in output + + def test_bell_kernel_has_hadamard(self): + from q_orca.compiler.cudaq import compile_to_cudaq + machine = _parse_bell() + output = compile_to_cudaq(machine) + assert "cudaq.h(" in output + + def test_bell_kernel_has_cnot(self): + from q_orca.compiler.cudaq import compile_to_cudaq + machine = _parse_bell() + output = compile_to_cudaq(machine) + assert "cudaq.x.ctrl(" in output + + def test_bell_kernel_has_qvector(self): + from q_orca.compiler.cudaq import compile_to_cudaq + machine = _parse_bell() + output = compile_to_cudaq(machine) + assert "cudaq.qvector(2)" in output + + def test_output_is_valid_python_syntax(self): + """The generated kernel string must be valid Python (aside from cudaq import).""" + import ast + from q_orca.compiler.cudaq import compile_to_cudaq + machine = _parse_bell() + output = compile_to_cudaq(machine) + # Should parse without SyntaxError + ast.parse(output) + + def test_compile_format_cudaq_cli(self, tmp_path): + """q-orca compile cudaq should emit a kernel string.""" + bell_file = tmp_path / "bell.q.orca.md" + bell_file.write_text(_BELL_SOURCE) + proc = subprocess.run( + [sys.executable, "-m", "q_orca.cli", "compile", "cudaq", str(bell_file)], + capture_output=True, + text=True, + ) + assert proc.returncode == 0 + assert "import cudaq" in proc.stdout + assert "@cudaq.kernel" in proc.stdout + + +# --------------------------------------------------------------------------- +# Task 9.3 — CLI integration tests +# --------------------------------------------------------------------------- + +class TestCLIBackendIntegration: + """Integration tests for --backend flag in CLI commands.""" + + def _run_cli(self, *args): + return subprocess.run( + [sys.executable, "-m", "q_orca.cli", *args], + capture_output=True, + text=True, + ) + + def test_verify_qutip_backend_json_has_backend_block(self, tmp_path): + """verify --backend qutip --json produces a 'backend' block.""" + bell_file = tmp_path / "bell.q.orca.md" + bell_file.write_text(_BELL_SOURCE) + proc = self._run_cli("verify", "--backend", "qutip", "--json", str(bell_file)) + # May exit 0 or 1 depending on verification result; just check JSON structure + import json + data = json.loads(proc.stdout) + assert "backend" in data + assert data["backend"]["name"] == "qutip" + + def test_verify_cuquantum_json_falls_back_with_warning(self, tmp_path): + """verify --backend cuquantum --json falls back to qutip with BACKEND_UNAVAILABLE warning.""" + from q_orca.backends.cuquantum_backend import AVAILABLE as CUQ_AVAILABLE + if CUQ_AVAILABLE: + pytest.skip("cuquantum is actually installed — skip fallback test") + + bell_file = tmp_path / "bell.q.orca.md" + bell_file.write_text(_BELL_SOURCE) + proc = self._run_cli("verify", "--backend", "cuquantum", "--json", str(bell_file)) + import json + data = json.loads(proc.stdout) + assert "backend" in data + error_codes = [e["code"] for e in data.get("errors", [])] + assert "BACKEND_UNAVAILABLE" in error_codes + + def test_verify_unknown_backend_emits_warning(self, tmp_path): + """verify --backend unknown --json includes BACKEND_UNAVAILABLE in errors.""" + bell_file = tmp_path / "bell.q.orca.md" + bell_file.write_text(_BELL_SOURCE) + proc = self._run_cli("verify", "--backend", "totally_unknown", "--json", str(bell_file)) + import json + data = json.loads(proc.stdout) + error_codes = [e["code"] for e in data.get("errors", [])] + assert "BACKEND_UNAVAILABLE" in error_codes diff --git a/tests/test_bell_pair_pipeline.py b/tests/test_bell_pair_pipeline.py index af6d62e..70fbbbc 100644 --- a/tests/test_bell_pair_pipeline.py +++ b/tests/test_bell_pair_pipeline.py @@ -62,8 +62,8 @@ def test_qasm_cnot(self, bell_qasm): def test_qasm_gate_order(self, bell_qasm): # h must appear before cx lines = bell_qasm.splitlines() - h_idx = next(i for i, l in enumerate(lines) if "h q[0];" in l) - cx_idx = next(i for i, l in enumerate(lines) if "cx q[0], q[1];" in l) + h_idx = next(i for i, ln in enumerate(lines) if "h q[0];" in ln) + cx_idx = next(i for i, ln in enumerate(lines) if "cx q[0], q[1];" in ln) assert h_idx < cx_idx diff --git a/tests/test_pipeline_bugs.py b/tests/test_pipeline_bugs.py index db3780a..bb363f7 100644 --- a/tests/test_pipeline_bugs.py +++ b/tests/test_pipeline_bugs.py @@ -9,7 +9,6 @@ import json import textwrap -import pytest from q_orca.parser.markdown_parser import parse_q_orca_markdown from q_orca.compiler.qasm import compile_to_qasm, _infer_qubit_count as qasm_qubit_count @@ -123,7 +122,7 @@ def test_bell_qiskit_no_comment_only_for_cx(self): script = compile_to_qiskit(machine, opts) # The cx01 action must produce actual gate code, not just a comment lines = script.splitlines() - gate_lines = [l for l in lines if l.strip().startswith("qc.")] + gate_lines = [ln for ln in lines if ln.strip().startswith("qc.")] assert len(gate_lines) >= 2, f"Expected at least 2 gate calls, got: {gate_lines}" def test_rx_ry_rz_parsed(self): @@ -197,10 +196,10 @@ def test_bell_qasm_no_comment_only_for_cx(self): """CX transition must not produce only a comment line.""" machine = _machine(BELL_TEST_SOURCE) output = compile_to_qasm(machine) - gate_lines = [l for l in output.splitlines() if not l.startswith("//") and l.strip()] - actual_gates = [l for l in gate_lines if not l.startswith("OPENQASM") and - not l.startswith('include') and not l.startswith("qubit") and - not l.startswith("bit") and not l.startswith("int")] + gate_lines = [ln for ln in output.splitlines() if not ln.startswith("//") and ln.strip()] + actual_gates = [ln for ln in gate_lines if not ln.startswith("OPENQASM") and + not ln.startswith('include') and not ln.startswith("qubit") and + not ln.startswith("bit") and not ln.startswith("int")] assert len(actual_gates) >= 2, f"Expected ≥2 gate statements, got: {actual_gates}" def test_ghz_qasm_three_gates(self): diff --git a/tests/test_regression.py b/tests/test_regression.py index 34f8d50..1e3117a 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -12,7 +12,6 @@ """ import math -import json from pathlib import Path import pytest @@ -542,7 +541,6 @@ def test_all_examples_pass_without_errors(self): source = f.read_text() machine = _machine(source) result = verify(machine) - error_codes = [e.code for e in result.errors if e.severity == "error"] assert result.valid, ( f"{f.name} failed verification:\n" + "\n".join(f" [{e.severity}] {e.code}: {e.message}" for e in result.errors)