""" lowering.py — spec → OpenQASM 3. The "lowering pass" from the architecture diagram. For the four circuit-builder actions (RANDOM, BELL, GROVER, QAOA) we build the circuit via `circuits.py` and emit OpenQASM 3. VQE is a variational procedure (many circuits), so it lowers to a description of its ansatz circuit rather than a single shot program. OpenQASM 3 emission uses the `openqasm3` package if available; otherwise we fall back to Qiskit's built-in QASM2 exporter so the pipeline never hard-fails on a missing optional dependency (useful in minimal local test envs). """ from __future__ import annotations from qiskit import QuantumCircuit from .spec import QuantumSpec from . import circuits def _emit(qc: QuantumCircuit) -> str: """ Emit a QuantumCircuit as OpenQASM 3, falling back to QASM 2. Exporter preference, most-expressive first: 1. `openqasm3.dumps` — true OpenQASM 3 (the architecture's target). 2. `qiskit.qasm2.dumps` — Qiskit 2.x's stable QASM2 exporter. 3. legacy `QuantumCircuit.qasm()` — older Qiskit versions only. The pipeline must never hard-fail on a missing exporter. """ try: import openqasm3 # noqa: WPS433 return openqasm3.dumps(qc) except ImportError: pass try: from qiskit import qasm2 # Qiskit 2.x return qasm2.dumps(qc) except Exception: pass fn = getattr(qc, "qasm", None) # legacy, pre-2.0 if callable(fn): return fn() raise RuntimeError("No QASM exporter available on this Qiskit version.") def lower(spec: QuantumSpec) -> tuple[str, QuantumCircuit]: """ Lower a validated spec to (qasm_source, circuit). Returns a 2-tuple so the caller gets the OpenQASM text for the receipt / UI without re-exporting, and the live circuit object for execution. """ if spec.action == "VQE": # VQE is variational; emit the ansatz template at θ=0 as the canonical # representative. The engine re-parameterises it during optimisation. qc = circuits.vqe_ansatz(0.0) qasm = _emit(qc) return qasm, qc builder = circuits.BUILDERS[spec.action] qc = builder(spec) qasm = _emit(qc) return qasm, qc