Metadata-Version: 2.4
Name: shotwise
Version: 0.1.1
Summary: Defensible error bars for quantum measurement data. Counts in, publishable numbers out.
Project-URL: Homepage, https://github.com/Ali-X2/shotwise
Project-URL: Issues, https://github.com/Ali-X2/shotwise/issues
Author: Wahaj Ali
License: Apache-2.0
License-File: LICENSE
Keywords: confidence-intervals,error-bars,expectation-values,quantum-computing,shot-noise,statistics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: numpy>=1.22
Requires-Dist: scipy>=1.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# shotwise 📊

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

[![CI](https://github.com/Ali-X2/shotwise/actions/workflows/ci.yml/badge.svg)](https://github.com/Ali-X2/shotwise/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/shotwise.svg)](https://pypi.org/project/shotwise/)
[![Python](https://img.shields.io/pypi/pyversions/shotwise.svg)](https://pypi.org/project/shotwise/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](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](#-the-problem)
- [Installation](#-installation)
- [Quick start](#-quick-start)
- [API reference](#-api-reference)
- [Choosing a method](#-choosing-a-method)
- [Limitations](#-what-this-library-does-not-do)
- [Roadmap](#-roadmap)
- [Contributing](#-contributing)
- [License](#-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.

```python
>>> 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`](docs/coverage.md), regenerated from source rather than typed by hand.

---

## 📦 Installation

```bash
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.

<details>
<summary>Install from source / for development</summary>

```bash
git clone https://github.com/Ali-X2/shotwise
cd shotwise
pip install -e ".[dev]"
pytest
```
</details>

---

## 🚀 Quick start

```python
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

```python
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](#-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 parity
- `X`, `Y`, `Z` → **include** this position

```python
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 🔢

```python
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 🎚️

```python
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:

```python
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 ⏱️

```python
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.

```python
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 📏

```python
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 🔧

```python
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 |

<details>
<summary>Why multi-term observables aren't in 0.1</summary>

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.
</details>

---

## 🤝 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.

```bash
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](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.
