Add pure-Python results post-processing helpers#68
Conversation
Add ionq_core.results with NumPy-free helpers over the probability mappings returned by the results endpoints: probabilities_to_counts, relabel_to_bitstrings, marginal, and expectation_z. Follows IonQ's big-endian state-key convention (qubit 0 is the most significant bit) and re-exports the helpers from the package root. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
One note to aid review of the bit-ordering choice: this PR decodes register-keyed state integers big-endian, so qubit 0 is the most-significant bit (e.g. the three-qubit integer |
|
Thanks. v0.4 keys are little-endian (qubit 0 = least significant bit), verified against the live API, so |
Summary
Closes #57.
Adds
ionq_core/results.py, a hand-written, NumPy-free module of post-processing helpers over the register-keyed probability mapping returned by the results endpoints (get_job_probabilities,get_variant_probabilities,get_variant_histogram) viaGetResultsResponse.additional_properties. The helpers operate on a plainMapping[str, float], so they cover both the job and variant endpoints and are testable without HTTP, and they give the downstream wrappers (qiskit-ionq,cirq-ionq,pennylane-ionq) a common base instead of re-deriving these conversions.Public API (declared in
__all__, re-exported from the package root):probabilities_to_counts(probabilities, shots)— integer counts summing exactly toshots, via largest-remainder rounding with deterministic tie-breaking (ascending state value).relabel_to_bitstrings(probabilities, num_qubits)— integer state keys to zero-padded bitstrings.marginal(probabilities, qubits, num_qubits)— marginal over a subset of qubits, preserving the requested qubit order in the reduced key.expectation_z(probabilities, num_qubits)— ⟨Z⊗…⊗Z⟩ parity,Σ p(x)·(−1)^popcount(x).Bit-ordering convention. State keys are decoded big-endian, matching the IonQ Cloud API: qubit 0 is the most significant bit of the integer key (the leftmost bit of the bitstring), so the three-qubit integer
2is"010". This is documented once in the module docstring and applied consistently across all four helpers;marginalextracts qubitqwith(state >> (num_qubits - 1 - q)) & 1, consistent with the referencecirq-ionqdecoding.Like
ionq_core/gates.py, the module is pure-Python (math+ stdlib only) — no new runtime dependencies and no NumPy. Inputs are validated (finite, non-negative probabilities; in-range integer state keys; distinct in-range qubit indices; normalization forprobabilities_to_counts) with clearValueErrormessages.ionq_core/__init__.pyand its templatecustom-templates/package_init.py.jinjaboth gainresultsin the module list (kept aligned so thegeneratedworkflow stays green), andCHANGELOG.mdhas a matching### Addedentry under[Unreleased].Test plan
tests/test_results.pyadds a captured two-qubit Bell-state simulator response ({"0": 0.5, "3": 0.5}) and a three-qubit GHZ response as fixtures, plus largest-remainder rounding, big-endian relabeling, marginal ordering/collapse, parity, and input-validation edge cases.ionq_core/results.pyreaches 100% branch coverage on its own; the full suite keeps the repo's--cov-fail-under=100gate satisfied.Ran locally (
uv, Python 3.11):uv run pytest→ 271 passed, 28 deselected, total coverage 100% (branch).uv run ruff check→ All checks passed.uv run ruff format --check→ 33 files already formatted.uv run ty check ionq_core/→ All checks passed.uvx pre-commit run --files …→ trailing-whitespace, end-of-file, gitleaks, ruff check, ruff format all pass.Integration tests (
tests/integration/, live API) were not run; this change is pure-Python and exercised entirely by the offline unit tests above.