Education / Quantum Programming

Quantum Programming · Chapter 06

Sampling and Estimation

How does a circuit produce the numerical result a program needs?

In this chapter · 3 sections
  1. 6.1 Execution Primitives
  2. 6.2 Statistical Readout
  3. 6.3 Repeated Workloads
Reading guide · preparation and approach

Starting point: Earlier programming chapters; relevant constructions from Quantum Computing and Quantum Algorithms.

Anchor example: Compare sampling and observable estimation for a parameterized qubit.

This lesson includes explanations, worked examples, and problems with solutions. Advanced results whose proofs are outside the chapter are identified explicitly.

We can now build components, bind their angles and check their matrices. But a program rarely wants a matrix or a raw table of counts. It wants one number: the probability of an outcome, say, or the average of an observable, and often that number at many settings of a parameter.

How does a circuit produce the numerical result a program needs? Two kinds of request turn out to need two interfaces, one returning samples and one returning expectation values. Their answers come from finite numbers of shots, so we must derive how uncertain they are, and how to compute an average ourselves from bit strings. Finally, asking for the same number at many angles leads to parameter arrays, whose shapes decide which experiments are actually run.

A single one-qubit circuit, \(R_y(\theta)\ket0\), carries us through all of it.

6.1 Execution Primitives

The rotation prepares \(R_y(\theta)\ket0=\cos(\theta/2)\ket0+\sin(\theta/2)\ket1\). Two natural questions about it have different answers. The probability of measuring one is \(p(1)=\sin^2(\theta/2)\). The average of \(Z\) is \(\cos^2(\theta/2)-\sin^2(\theta/2)=\cos\theta\). Which interface should answer each question, and where in its result do we find the number? We take the interfaces first and their results second.

6.1.1 V2 Sampler and Estimator

We already have a sampler, and from its counts we could compute \(\langle Z\rangle\) ourselves. Why would we want a separate interface for averages? For one qubit and one observable, we would not. But an observable such as \(-ZZ-XX\) needs measurements in more than one basis, and larger observables need many. Choosing those bases and combining their results is work that the software, or a remote service, can do for us, and can do with techniques of its own.

So Qiskit offers two primitives, the standard interfaces we met as reference primitives. A Sampler takes measured circuits and returns their outcomes. An Estimator takes circuits without measurements, together with observables, and returns expectation values. Their current versions are called V2.

Both accept many requests in one call. Each request is written as a tuple, a fixed-order group of values in parentheses, such as (qc, SparsePauliOp("Z"), values). For an Estimator the order is circuit, observable, parameter values. Qiskit calls one such request a primitive unified bloc, or PUB. A PUB is a grouping of inputs, nothing more: it is not a gate, and it does not promise that its parts run together on a device. The companion uses the local StatevectorSampler and StatevectorEstimator, so no account is involved.

6.1.2 Local implementations and result structures

Once a run finishes, where is our number? The result holds one entry per PUB, in the order we sent them, so result[0] belongs to the first PUB. For an Estimator, result[0].data.evs is an array of expectation values, one for each set of parameter values in that PUB.

For a Sampler, result[0].data is organized by classical register name. We have always used measure_all(), which creates a register named meas, so we read data.meas. A circuit with its own register named c would be read as data.c. So an access expression copied from another program can fail, or read the wrong register, unless we check the register names first.

Problem 6.1. At θ=π/3, predict p(1) and the Z mean.

Worked solution

They are 1/4 and 1/2. A result near zero would suggest a wrong angle, observable, or parameter mapping.

6.2 Statistical Readout

The local Estimator computes \(\cos\theta\) exactly. A device cannot: it repeats the circuit a finite number of times and averages. How far from the true value should we expect such an estimate to land? And when we only have bit strings, how do we turn them into an average ourselves? We derive the uncertainty first, then the decoding.

6.2.1 Shots, precision, and uncertainty

Suppose we run the circuit \(R\) times at \(\theta=\pi/2\), where \(\langle Z\rangle=0\). How close to 0 will our estimate be?

Each shot gives a bit, and we record \(s=+1\) for outcome 0 and \(s=-1\) for outcome 1, the two eigenvalues of \(Z\). The mean of \(s\) is \(p(0)-p(1)\), and since \(p(0)=1-p(1)\), \[\langle Z\rangle=1-2p(1).\] Because \(s^2=1\), the variance of one shot is \(\mathbb E[s^2]-\langle Z\rangle^2=1-\langle Z\rangle^2\). As with the coin, averaging \(R\) independent shots divides the variance by \(R\): \[\operatorname{Var}\bar s=\frac{1-\langle Z\rangle^2}{R},\qquad \operatorname{Var}\hat p(1)=\frac{p(1)\bigl(1-p(1)\bigr)}{R}.\] The two agree: \(\bar s=1-2\hat p(1)\), so its variance is four times that of \(\hat p(1)\), and \(4p(1-p)=1-(1-2p)^2\).

At \(\theta=\pi/2\) with \(R=4096\), the standard deviation of \(\bar s\) is \(1/64\approx0.016\). This standard deviation of an estimate is called its standard error. Doubling the shots divides it by \(\sqrt2\), so each extra digit of accuracy costs a hundred times more shots.

An Estimator lets us ask for a target standard error, its precision. StatevectorEstimator uses precision 0 by default and then returns the exact value. With a positive precision it adds random noise of about that size, to imitate a finite-shot estimate. Either way it is not a model of a noisy device.

6.2.2 Bit strings and classical postprocessing

Now suppose we have two-qubit counts and want \(\langle ZZ\rangle\). How do we compute it? The Z outcome of qubit \(j\) is \(+1\) when its bit is 0 and \(-1\) when it is 1. \(ZZ\) is the product of the two, so it is \(+1\) when the bits are equal and \(-1\) when they differ. Hence \[\widehat{\langle ZZ\rangle}=\frac{N_{\text{equal}}-N_{\text{unequal}}}{R}.\]

For \(ZI\), by contrast, only the left character, qubit 1, matters. So we must decode each string with the measurement mapping before computing. A mapping that swaps the bits leaves \(ZZ\) unchanged but exchanges \(ZI\) and \(IZ\).

What about small entries of a count table? Dividing counts by \(R\) gives an empirical distribution. An outcome with probability \(p\) has standard error \(\sqrt{p(1-p)/R}\), which relative to \(p\) is about \(1/\sqrt{pR}\). At \(p=0.001\) and \(R=4096\) we expect about 4 counts, and the relative uncertainty is about 50%. Small entries are the least reliable.

Problem 6.2. How does doubling shots change a standard error?

Worked solution

It reduces the standard error by √2, assuming independent identically distributed shots. It does not remove systematic bias.

6.3 Repeated Workloads

We usually want \(\langle Z\rangle\) not at one angle but at many. Must we build and send one circuit per angle? A symbolic parameter lets one circuit serve every angle. What remains is to describe the list of angles so that the primitive runs the experiments we mean, and to keep their results in order.

6.3.1 Parameter sweeps

How do we ask for \(\theta=0,\pi/2,\pi\) in one PUB? The companion writes values = [[0], [np.pi / 2], [np.pi]], a list of lists. Each inner list is one row, holding one value for each parameter of the circuit. Our circuit has one parameter, so each row has one entry, and three rows ask for three experiments. The array has shape \(3\times1\), and the predicted results are \(\cos\theta=1,0,-1\).

Shapes decide which experiments run, so a mistake can change the workload without any warning. Suppose we also want \(\langle X\rangle\) at the same three angles. Passing the two observables as a flat list, of shape 2, beside three rows, of shape 3, raises an error, because the two shapes cannot be matched. Passing them as [[Z], [X]], of shape \(2\times1\), gives a \(2\times3\) result: every observable at every angle. The Estimator combines the shapes by NumPy’s broadcasting rule, which stretches a dimension of length 1 to match the other array. So we check the shape of evs before trusting the order of its entries.

6.3.2 Batching inputs and interpreting results

When we have several PUBs, why send them in one call rather than one at a time? Each call has overhead, and on a remote service each job may wait in a queue. Sending several PUBs in one call, called batching, saves that overhead. It does not reduce the work: every circuit, parameter row, observable and shot still costs what it costs. Since results come back in PUB order, we keep the parameter order and the result order together in the saved record.

The companion keeps its two sources of output apart. First the Estimator computes the exact three-point sweep and checks it with np.allclose. The middle value prints as about \(2\times10^{-16}\) rather than 0, a rounding error of the kind tolerances exist for. Then the program binds \(\theta=\pi/2\), adds measurements and samples 4096 shots, so the counts should be close to an even split.

"""Runnable companion for Quantum Programming, Chapter 6."""
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

theta = Parameter("theta")
qc = QuantumCircuit(1)
qc.ry(theta, 0)
values = [[0], [np.pi / 2], [np.pi]]
result = StatevectorEstimator().run([(qc, SparsePauliOp("Z"), values)]).result()
assert np.allclose(result[0].data.evs, [1, 0, -1])
measured = qc.assign_parameters({theta: np.pi / 2})
measured.measure_all()
counts = StatevectorSampler(seed=9).run([measured], shots=4096).result()[0].data.meas.get_counts()
print("Exact means:", result[0].data.evs, "sampled counts:", counts)

Companion script: chapter_06.py.

With seed 9 the sampled counts were 2051 zeros and 2045 ones, which gives \(\bar s=6/4096\approx0.0015\), well within one standard error of the exact value 0.

Problem 6.3. Why keep a circuit without measurements for the Estimator?

Worked solution

It represents the prepared state whose observable is requested. The estimation implementation chooses its measurement procedure; terminal measurements in the input can conflict with that contract.

Every circuit so far acted on inputs we wrote in by hand. Many algorithms instead assume an oracle, a circuit that evaluates a function on whatever input it receives. How do we build one, rather than assume it exists?

Sources and further study: IBM, StatevectorEstimator; IBM, StatevectorSampler.

Summary

Questions and worked problems

These exercises draw on the whole chapter. Attempt each question before opening a hint, explain your reasoning, then compare it with the worked solution.

1. Quick check §

For ry(\(2\pi/3\))\(\ket0\), predict \(p(1)\) and \(\langle Z\rangle\), and check that they satisfy \(\langle Z\rangle=1-2p(1)\).

Hint 1

\(p(1)=\sin^2(\theta/2)\).

Hint 2

\(\langle Z\rangle=\cos\theta\).

Worked solution

\(p(1)=\sin^2(\pi/3)=0.75\) and \(\langle Z\rangle=\cos(2\pi/3)=-0.5=1-2(0.75)\). The Sampler would report counts near \(1024/3072\) for \(4096\) shots; the Estimator reports the mean \(-0.5\). They are two views of the same distribution.

Revisit the relevant explanation

2. Quick check §

A sampled estimate gives \(\langle Z\rangle\approx0.2\) from \(1000\) shots. Find its standard error, and the shots needed to reach a standard error of \(0.01\).

Hint 1

Standard error \(\sqrt{(1-\langle Z\rangle^2)/R}\).

Hint 2

Solve \(0.96/R=10^{-4}\).

Worked solution

Standard error \(\sqrt{0.96/1000}\approx0.031\). Reaching \(0.01\) needs \(R=0.96/10^{-4}=9600\) shots: about ten times more for three times the precision, the familiar \(1/\sqrt R\) scaling.

Revisit the relevant explanation

3. Quick check §

A user passes a circuit that ends in measure_all to StatevectorEstimator together with the observable Z. What happens, and what should they pass instead?

from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

circuit = QuantumCircuit(1)
circuit.ry(1.0, 0)
circuit.measure_all()
StatevectorEstimator().run([(circuit, SparsePauliOp("Z"))]).result()
Hint 1

The Estimator evaluates an expectation on the circuit’s state.

Hint 2

Remove the measurements.

Worked solution

The Estimator raises a QiskitError (“Cannot apply instruction with classical bits: measure”). Expectation values are defined for the state before measurement; the Estimator handles any basis changes and sampling internally. Pass the circuit without measurements, and keep a separate measured copy for the Sampler.

Revisit the relevant explanation

4. Worked problem §

One Estimator PUB combines the parameterized ry circuit with observables arranged as [[Z], [X]] and parameter values [[0], [\(\pi/2\)], [\(\pi\)]]. (a) Predict the shape of data.evs. (b) Predict its entries. (c) Why is checking the shape part of a correct program?

Hint 1

Shapes broadcast: \((2,1)\) observables with \((3,)\) parameter sets give \((2,3)\).

Hint 2

\(\langle Z\rangle=\cos\theta\), \(\langle X\rangle=\sin\theta\).

Worked solution

(a) Broadcasting the observables array of shape \((2,1)\) against three parameter sets gives shape \((2,3)\). (b) The Z row is \(\cos\theta=(1,0,-1)\) and the X row is \(\sin\theta=(0,1,0)\). (c) A different arrangement, say observables of shape \((2,)\), would fail to broadcast against three parameter sets, or would pair them differently. Asserting the expected shape catches workload mistakes that numerical checks on one entry would miss.

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

theta = Parameter("theta")
circuit = QuantumCircuit(1)
circuit.ry(theta, 0)
observables = [[SparsePauliOp("Z")], [SparsePauliOp("X")]]
values = [[0], [np.pi / 2], [np.pi]]
evs = StatevectorEstimator().run([(circuit, observables, values)]).result()[0].data.evs
assert evs.shape == (2, 3)
assert np.allclose(evs, [[1, 0, -1], [0, 1, 0]])

Revisit the relevant explanation

5. Worked problem §

A two-qubit state has qubit 0 in ry(\(\pi/3\))\(\ket0\) and qubit 1 in \(\ket0\). The measurement maps qubit 0 to classical bit 1 and qubit 1 to classical bit 0. (a) Estimate \(\langle IZ\rangle\) (Z on qubit 0) from the counts, decoding the map correctly. (b) What would a student get by treating the rightmost count bit as qubit 0? (c) Does the mapping matter for \(\langle ZZ\rangle\)?

Hint 1

With the swapped map, qubit 0 appears as the left bit of each count string.

Hint 2

\(\langle Z\rangle=\cos(\pi/3)=0.5\) for qubit 0.

Worked solution

(a) Qubit 0’s value is in classical bit 1, the left character. Averaging \(+1\) for 0 and \(-1\) for 1 over that character gives about \(0.5=\cos(\pi/3)\). (b) The rightmost character holds qubit 1, which is always \(0\), so the student would report \(1\). (c) ZZ depends only on whether the two bits are equal, which is unchanged by swapping them, so it is immune to this mistake; single-qubit observables are not.

import numpy as np
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

circuit = QuantumCircuit(2, 2)
circuit.ry(np.pi / 3, 0)
circuit.measure(0, 1)
circuit.measure(1, 0)
shots = 8192
counts = StatevectorSampler(seed=4).run([circuit], shots=shots).result()[0].data.c.get_counts()
qubit0 = sum((1 if bits[0] == "0" else -1) * n for bits, n in counts.items()) / shots
wrong = sum((1 if bits[1] == "0" else -1) * n for bits, n in counts.items()) / shots
assert abs(qubit0 - 0.5) < 0.05 and wrong == 1.0

Revisit the relevant explanation

6. Worked problem §

An experiment must estimate \(\langle Z\rangle+\langle X\rangle\) for ry(\(\pi/3\))\(\ket0\) with \(10\,000\) total shots, split between a Z-basis and an X-basis circuit. (a) Find the single-shot variances. (b) Compare the variance of the estimate for an equal split and for the allocation \(R_j\propto\sigma_j\). (c) Is the improvement worth the bookkeeping here?

Hint 1

\(\langle Z\rangle=0.5\), \(\langle X\rangle=\sin(\pi/3)\approx0.866\).

Hint 2

Optimal variance \((\sigma_Z+\sigma_X)^2/R\).

Worked solution

(a) \(\sigma_Z^2=1-0.25=0.75\) and \(\sigma_X^2=1-0.75=0.25\). (b) Equal split: \(0.75/5000+0.25/5000=2.0\times10^{-4}\). Optimal: \(R_Z\approx6340\), \(R_X\approx3660\), giving \((0.866+0.5)^2/10^4\approx1.87\times10^{-4}\). (c) Only a \(7\%\) reduction here. The gain becomes large when coefficients or variances differ strongly, as for Hamiltonians with many terms; the variances must themselves be estimated first.

Revisit the relevant explanation

7. Worked problem §

Estimate a small probability: a circuit outputs 1 with probability \(p=0.002\). (a) With \(1000\) shots, what is the expected count and the relative standard error of \(\hat p\)? (b) How many shots give a \(10\%\) relative error? (c) If zero ones are observed in \(1000\) shots, what upper bound on \(p\) holds at about \(95\%\) confidence?

Hint 1

Relative error \(\sqrt{(1-p)/(pR)}\).

Hint 2

Zero counts: upper bound about \(3/R\).

Worked solution

(a) Expected count \(2\), relative error \(\sqrt{0.998/(0.002\times1000)}\approx0.71\): the estimate is barely meaningful. (b) \(0.1^2=0.998/(0.002R)\) gives \(R\approx5\times10^4\). (c) About \(3/1000=0.003\). Small entries of a normalized count table carry large relative uncertainty, so rare outcomes need either many more shots or an estimation method designed for them.

Revisit the relevant explanation

8. Misconception §

A student argues: “Doubling the shots halves the uncertainty, so going from \(1000\) to \(2000\) shots takes my standard error from \(0.03\) to \(0.015\).” Correct the claim and give the shots that actually halve it.

Hint 1

Standard error scales as \(1/\sqrt R\).

Hint 2

\(\sqrt2\approx1.41\).

Worked solution

Standard error scales as \(1/\sqrt R\), so doubling the shots reduces it by \(\sqrt2\), from \(0.03\) to about \(0.021\). Halving it requires four times the shots, \(4000\). Every extra digit of precision costs a factor of one hundred in shots, which is why measurement budgets dominate many estimation workloads.

Revisit the relevant explanation

9. Extension §

Compare two ways of running a \(100\)-point parameter sweep of \(\langle Z\rangle\) for ry(theta): one PUB with a \((100,1)\) array of values, and \(100\) separate PUBs with one value each. Show that the expectation values agree, and explain why the single-PUB form is preferable.

Hint 1

Both describe the same mathematical workload.

Hint 2

Consider overhead, ordering, and result bookkeeping.

Worked solution

The two runs return the same \(100\) values of \(\cos\theta\). A single PUB states the experiment’s structure explicitly (one circuit, one observable, a value array), lets the primitive reuse the circuit and compilation, and returns one array whose order matches the input array, reducing bookkeeping errors. Separate PUBs repeat overhead and scatter results across many objects; on remote services they may also be scheduled and billed separately.

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp

theta = Parameter("theta")
circuit = QuantumCircuit(1)
circuit.ry(theta, 0)
angles = np.linspace(0, np.pi, 100)
z = SparsePauliOp("Z")
together = StatevectorEstimator().run([(circuit, z, angles.reshape(-1, 1))]).result()[0].data.evs
separate = [r.data.evs for r in StatevectorEstimator().run([(circuit, z, [[a]]) for a in angles]).result()]
assert np.allclose(together, np.cos(angles)) and np.allclose(np.ravel(separate), together)

Revisit the relevant explanation

10. Extension §

Write a function that converts Sampler counts for two measured qubits into an estimate of any two-qubit diagonal Pauli observable (II, IZ, ZI, or ZZ), given the label and assuming the default measure_all mapping. Test it on the Bell state, where \(\langle ZZ\rangle=1\) and \(\langle ZI\rangle=\langle IZ\rangle=0\).

Hint 1

For a label character Z on qubit \(k\), multiply by \((-1)^{\text{bit}_k}\).

Hint 2

Character \(j\) of the label acts on qubit \(n-1-j\), and so does character \(j\) of the count string.

Worked solution

Each shot contributes the product of \(\pm1\) factors from the qubits where the label has Z. Since both labels and count strings put qubit 0 on the right, character \(j\) of the label pairs with character \(j\) of the bit string. On the Bell state the estimates are \(1\) for ZZ and close to \(0\) for ZI and IZ. Encoding the ordering rule once in a tested helper is safer than reimplementing it in every analysis.

from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler

def diagonal_expectation(counts, label):
    total = sum(counts.values())
    value = 0
    for bits, n in counts.items():
        sign = 1
        for pauli, bit in zip(label, bits):
            if pauli == "Z" and bit == "1":
                sign = -sign
        value += sign * n
    return value / total

bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
bell.measure_all()
counts = StatevectorSampler(seed=2).run([bell], shots=4096).result()[0].data.meas.get_counts()
assert diagonal_expectation(counts, "ZZ") == 1
assert abs(diagonal_expectation(counts, "ZI")) < 0.1 and abs(diagonal_expectation(counts, "IZ")) < 0.1

Revisit the relevant explanation

Search this book

Search the course.