Defensible error bars for quantum measurement data. Counts in, publishable numbers out.
Reason this release was yanked:
Incorrect author metadata; superseded by 0.1.1
Project description
shotwise 📊
Defensible error bars for quantum measurement data. Counts in, numbers you can publish out.
No quantum SDK required. shotwise takes the plain {bitstring: count} dictionaries that Qiskit, Cirq, Braket and PennyLane already produce, and hands back an expectation value with an interval that is actually correct.
📑 Contents
- The problem
- Installation
- Quick start
- API reference
- Choosing a method
- Limitations
- Roadmap
- Contributing
- License
🔴 The problem
Run a clean circuit. Get 8192 out of 8192 shots in even parity. Report it the way essentially every quantum paper does:
<ZZ> = 1.0000 ± 0.0000
A zero-width error bar on a physical quantity is nonsense. You did not measure <ZZ> to infinite precision. You measured it 8192 times and it never disagreed with you. Those are different claims.
>>> import shotwise as sw
>>> sw.pauli({"00": 8192}, "ZZ")
1 [0.999063, 1] (95% wilson, n=8192)
This is not a rounding quibble. For a Pauli operator, <P> = 2p − 1 where p is an ordinary Bernoulli parameter — so the right tool is a binomial interval, and the familiar mean ± std/√n convention is the Wald interval, the one binomial interval known to fail exactly where quantum data lives.
How often does a nominal 95% Wald interval actually contain the truth? Computed exactly, not simulated:
true <P> |
shots | Wald | Wilson |
|---|---|---|---|
| 0.998 | 20 | 🔴 0.020 | 🟢 0.980 |
| 0.998 | 50 | 🔴 0.049 | 🟢 0.951 |
| 0.998 | 200 | 🔴 0.181 | 🟢 0.983 |
| 0.998 | 1000 | 🔴 0.632 | 🟢 0.920 |
A thousand shots on a circuit sitting at 0.998 — an unremarkable result — and the reported 95% interval is right 63% of the time. Full grid in docs/coverage.md, regenerated from source rather than typed by hand.
📦 Installation
pip install shotwise
Python 3.10, 3.11, 3.12 and 3.13 are supported and tested in CI on every push.
Dependencies are deliberately minimal — numpy >= 1.22 and scipy >= 1.9, nothing else. No Qiskit, no PennyLane, not even as an optional extra. If your SDK can produce a dictionary, shotwise can read it.
Install from source / for development
git clone https://github.com/Ali-X2/shotwise
cd shotwise
pip install -e ".[dev]"
pytest
🚀 Quick start
import shotwise as sw
counts = {"00": 4021, "11": 3987, "01": 96, "10": 88}
sw.pauli(counts, "ZZ")
# 0.955078 [0.948199, 0.961062] (95% wilson, n=8192)
That's the whole library in one line. Three public functions do the rest.
📖 API reference
sw.pauli(...) — estimate one Pauli expectation value
sw.pauli(
counts, # {bitstring: shots}
pauli_string=None, # e.g. "ZZI"
*,
qubits=None, # alternative to pauli_string
bit_order="little", # "little" (Qiskit) or "big"
confidence=0.95, # nominal coverage
method="wilson", # interval method
) # -> Estimate
| parameter | type | default | meaning |
|---|---|---|---|
counts |
dict[str, int] |
required | Measurement outcomes. Whitespace in keys is ignored. |
pauli_string |
str |
None |
Aligned position by position with your bitstring keys. |
qubits |
list[int] |
None |
Qubit indices instead of a Pauli string. Mutually exclusive with pauli_string. |
bit_order |
str |
"little" |
How qubit indices map to string positions. Ignored when pauli_string is used. |
confidence |
float |
0.95 |
Any value in (0, 1). |
method |
str |
"wilson" |
See Choosing a method. |
Writing the Pauli string ✍️
The string lines up character by character with your bitstring keys, so you write it the way you read your counts and endianness never bites you.
I→ exclude this position from the parityX,Y,Z→ include this position
counts = {"00": 4021, "11": 3987, "01": 96, "10": 88}
sw.pauli(counts, "ZZ") # both qubits -> correlation
# 0.955078 [0.948199, 0.961062] (95% wilson, n=8192)
sw.pauli(counts, "ZI") # left position only
# 0.00512695 [-0.0165248, 0.0267739] (95% wilson, n=8192)
sw.pauli(counts, "IZ") # right position only
# 0.00317383 [-0.0184772, 0.0248219] (95% wilson, n=8192)
☝️ Note the GHZ signature: each qubit alone sits at ~0, but their correlation is ~0.96. That's the entanglement, and it falls straight out of the parity.
Using qubit indices instead 🔢
sw.pauli({"01": 1000}, qubits=[0], bit_order="little") # Qiskit convention
# -1 [-1, -0.992346] (95% wilson, n=1000)
sw.pauli({"01": 1000}, qubits=[0], bit_order="big")
# 1 [0.992346, 1] (95% wilson, n=1000)
Tuning confidence and method 🎚️
sw.pauli(counts, "ZZ", confidence=0.99)
# 0.955078 [0.945839, 0.962771] (99% wilson, n=8192)
sw.pauli(counts, "ZZ", method="clopper-pearson")
# 0.955078 [0.948188, 0.961276] (95% clopper-pearson, n=8192)
The Estimate object 📋
Every field it used to reach the answer travels with the answer:
e = sw.pauli(counts, "ZZ")
e.value # 0.955078125
e.lo # 0.9481989043532899
e.hi # 0.9610620395805378
e.shots # 8192
e.halfwidth # 0.006431567613623956
e.confidence # 0.95
e.method # 'wilson'
It's a frozen dataclass, so it's hashable, safe to stash in a results table, and impossible to mutate after the fact.
sw.shots_needed(...) — plan before you book device time ⏱️
sw.shots_needed(
halfwidth, # target precision on <P>
expectation=0.0, # your prior guess at <P>
*,
confidence=0.95,
method="wilson",
) # -> int
This inverts the interval you actually intend to report, rather than the z²/ε² rule of thumb — which is the Wald formula and therefore inherits Wald's pessimism near the boundary.
sw.shots_needed(0.01) # 38411 (no prior, worst case)
sw.shots_needed(0.01, expectation=0.9) # 7316
sw.shots_needed(0.01, expectation=0.99) # 868
sw.shots_needed(0.005, expectation=0.99) # 3218
sw.shots_needed(0.01, confidence=0.99) # 66343
| target | assumed <P> |
z²/ε² rule |
shotwise |
saving |
|---|---|---|---|---|
| 0.01 | 0.0 | 38,415 | 38,411 | 1.0× |
| 0.01 | 0.9 | 38,415 | 7,316 | 5.3× |
| 0.01 | 0.99 | 38,415 | 868 | 🎯 44× |
| 0.005 | 0.99 | 153,659 | 3,218 | 🎯 48× |
With no prior it agrees with the rule of thumb, as it must. Tell it you expect a clean circuit and it stops charging you for a coin flip.
sw.precision_at(...) — the forward question 📏
sw.precision_at(4096, expectation=0.95)
# 0.009581...
"I have 4096 shots and expect roughly 0.95 — what precision will I get?" Useful when the shot budget is fixed and you need to know whether the experiment is worth running at all.
Lower-level helpers 🔧
sw.parity_counts(counts, "ZZ")
# (8008, 8192) -> (even-parity shots, total shots)
sw.parity_counts({"0 1": 512, "1 1": 512}, "ZZ")
# (512, 1024) -> Qiskit multi-register keys just work
sw.interval(8192, 8192, 0.95, "wilson")
# (0.9995312917117791, 1.0) -> raw Bernoulli interval on p
🎯 Choosing a method
| method | use when |
|---|---|
wilson (default) |
almost always |
clopper-pearson |
a referee wants a guarantee; never drops below nominal anywhere tested |
jeffreys |
you want a Bayesian reading of the same number |
agresti-coull |
you need something you can derive on a whiteboard |
wald |
❌ never — provided only so you can reproduce what you're replacing |
⚠️ Coverage oscillates rather than sitting flat at 0.95, because k is an integer. This is intrinsic to binomial intervals and no method escapes it — see Brown, Cai & DasGupta, Interval Estimation for a Binomial Proportion, Statistical Science 16(2), 2001. What separates the methods is trough depth, which is measured and pinned by the test suite.
🚧 What this library does not do
Being clear about this matters more than the feature list.
- ❌ It does not know your measurement basis.
shotwisesees classical bits. It cannot tell whether you rotated into X before measuring, soX,YandZare treated identically — they mark a qubit as participating in the parity. Getting the rotation right on the device is your job, and this library will not catch that mistake. - ❌ It does not correct for noise. These intervals quantify sampling uncertainty only. A biased device gives you a tight interval around the wrong answer, and no number of shots fixes that.
- ❌ It does not do multi-term observables yet. See the roadmap — this is deliberate, not an oversight.
🗺️ Roadmap
| version | scope |
|---|---|
| 0.1 ✅ | single Pauli terms, five interval methods, shot planning |
| 0.2 | multi-term observables with per-measurement-group empirical Bernstein bounds |
| 0.3 | bias and variance propagation through readout mitigation and ZNE |
| 0.4 | variance-aware shot allocation across Pauli groups |
Why multi-term observables aren't in 0.1
A working implementation existed and was cut before release. It combined per-term intervals with a Bonferroni correction, which is rigorous but scales badly: because the bound assumes every term errs in the same direction at once, its width grows like m·σ while the truth concentrates like √m·σ. Measured at 300 terms, the interval came out 33× wider than necessary — rigorous and unusable, which is the worse failure for quantum chemistry, where Hamiltonians run to hundreds or millions of terms.
The right fix is a different decomposition. Commuting Paulis measured in a shared basis are all computable from the same shot, so each shot yields one realization of X = Σⱼ wⱼsⱼ, and an empirical Bernstein bound applies directly to that. That needs an API taking counts per measurement group rather than per term, so it ships in 0.2 rather than being bolted on now and broken later.
🤝 Contributing
Issues and pull requests are welcome. The test suite is the argument, so changes to interval behaviour need a coverage justification, not just green CI.
pip install -e ".[dev]"
ruff check .
pytest
python docs/generate_coverage.py # regenerate the coverage tables
tests/test_coverage.py computes coverage exactly — a finite sum over all binomial outcomes, no RNG and no seed — so results are deterministic and CI never flakes. If a change ever makes Wald look acceptable, that test fails, and the failure is the correct outcome.
📄 License
Apache License 2.0 — see LICENSE.
Free for commercial and academic use, with an explicit patent grant and no copyleft obligation on your own code.
📚 Citing
If shotwise contributed to published results, please cite the repository along with the underlying statistics:
Brown, L.D., Cai, T.T. & DasGupta, A. (2001). Interval Estimation for a Binomial Proportion. Statistical Science, 16(2), 101–133.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shotwise-0.1.0.tar.gz.
File metadata
- Download URL: shotwise-0.1.0.tar.gz
- Upload date:
- Size: 21.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2cc2042ef9d6f114172a7ca16ec23df36d009b1ca19f621148750307fe30239a
|
|
| MD5 |
1d5612e01dc7de5ee7d729680341c222
|
|
| BLAKE2b-256 |
9c3f5688ca2f70ecd34b6aa44c42e0adb40123f8950e9bd6744c4404dcc894e7
|
Provenance
The following attestation bundles were made for shotwise-0.1.0.tar.gz:
Publisher:
release.yml on Ali-X2/shotwise
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shotwise-0.1.0.tar.gz -
Subject digest:
2cc2042ef9d6f114172a7ca16ec23df36d009b1ca19f621148750307fe30239a - Sigstore transparency entry: 2314305671
- Sigstore integration time:
-
Permalink:
Ali-X2/shotwise@0e9998c53b4d1f3e546e31647b672b01b03d591f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Ali-X2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0e9998c53b4d1f3e546e31647b672b01b03d591f -
Trigger Event:
release
-
Statement type:
File details
Details for the file shotwise-0.1.0-py3-none-any.whl.
File metadata
- Download URL: shotwise-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97165467f139964a7eb785c1f7319c63ac56f46296c234537ef9b38e596c6ae0
|
|
| MD5 |
0d407ff64d9be9ca544f7ca036ad8c93
|
|
| BLAKE2b-256 |
ee7af4facaeca38e34f7ff6b16313685f41ffcdbb3b3798a38e0a7b2eb00ae72
|
Provenance
The following attestation bundles were made for shotwise-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Ali-X2/shotwise
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shotwise-0.1.0-py3-none-any.whl -
Subject digest:
97165467f139964a7eb785c1f7319c63ac56f46296c234537ef9b38e596c6ae0 - Sigstore transparency entry: 2314305675
- Sigstore integration time:
-
Permalink:
Ali-X2/shotwise@0e9998c53b4d1f3e546e31647b672b01b03d591f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Ali-X2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0e9998c53b4d1f3e546e31647b672b01b03d591f -
Trigger Event:
release
-
Statement type: