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.
Metadata
Release files for shotwise 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| shotwise-0.1.1.tar.gz | 21.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shotwise-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.8 kB
Release files / shotwise-0.1.1.tar.gz
| Download URL | shotwise-0.1.1.tar.gz |
|---|---|
| Size | 21.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1bf0642571fa445f0a49248f3fbe3181e1c9923e2e48261e3245fe1a46c0374b
|
|
BLAKE2b-256 checksum How to use checksums |
c3d112a948e0785896314c8922d377c3b1518b7e342d41f4d0cd63a1b7418b69
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency logRelease files / shotwise-0.1.1-py3-none-any.whl
| Download URL | shotwise-0.1.1-py3-none-any.whl |
|---|---|
| Size | 18.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
754ca08590c25621d326764bd84f49d4d61ccaac8fa991bbcbd210059915c604
|
|
BLAKE2b-256 checksum How to use checksums |
9144afeec57510d11e45286c1f188b84be778a7c8e7c821d152332572322f2a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency log