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.
- The problem
- Installation
- Quick start
- API reference
- Choosing a method
- Limitations
- Roadmap
- Contributing
- License
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.
pip install shotwisePython 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]"
pytestimport 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.
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. |
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.
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)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)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(
halfwidth, # target precision on <P>
expectation=0.0, # your prior guess at <P>
*,
confidence=0.95,
method="wilson",
) # -> intThis 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(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.
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| 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 |
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.
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.
| 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.
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 tablestests/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.
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.
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.