Skip to main content

Defensible error bars for quantum measurement data. Counts in, publishable numbers out.

Project description

shotwise 📊

Defensible error bars for quantum measurement data. Counts in, numbers you can publish out.

CI PyPI Python License

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

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.

  • Iexclude this position from the parity
  • X, Y, Zinclude 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. shotwise sees classical bits. It cannot tell whether you rotated into X before measuring, so X, Y and Z are 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shotwise-0.1.1.tar.gz (21.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shotwise-0.1.1-py3-none-any.whl (18.0 kB view details)

Uploaded Python 3

File details

Details for the file shotwise-0.1.1.tar.gz.

File metadata

  • Download URL: shotwise-0.1.1.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

Hashes for shotwise-0.1.1.tar.gz
Algorithm Hash digest
SHA256 1bf0642571fa445f0a49248f3fbe3181e1c9923e2e48261e3245fe1a46c0374b
MD5 0ecfc7f8c1b0e0d7e39b1595cd0dc99f
BLAKE2b-256 c3d112a948e0785896314c8922d377c3b1518b7e342d41f4d0cd63a1b7418b69

See more details on using hashes here.

Provenance

The following attestation bundles were made for shotwise-0.1.1.tar.gz:

Publisher: release.yml on Ali-X2/shotwise

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shotwise-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: shotwise-0.1.1-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

Hashes for shotwise-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 754ca08590c25621d326764bd84f49d4d61ccaac8fa991bbcbd210059915c604
MD5 d01830362df8c1dbbd366feab530072d
BLAKE2b-256 9144afeec57510d11e45286c1f188b84be778a7c8e7c821d152332572322f2a9

See more details on using hashes here.

Provenance

The following attestation bundles were made for shotwise-0.1.1-py3-none-any.whl:

Publisher: release.yml on Ali-X2/shotwise

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page