Skip to content

Measurement (shot sampling & pure-state fidelity)

A real quantum device never hands back a statevector -- it hands back a stream of individual measurement outcomes, one per shot, that you have to run many times and count up to approximate the underlying distribution. sample_counts does that simulated-device step (a DenseSVSimulator equivalent of Qiskit's get_counts()). statevector_fidelity answers a different question: how close are two known pure states to each other -- useful for grading a result against an ideal target, something no real device measurement alone can tell you.

Step 1. Finite-shot counts from a real statevector

import numpy as np
import dense_evolution as de

qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; cx q[0],q[1];'
circuit = de.QASMParser().parse(qasm)
sim = de.DenseSVSimulator(2)
sim.run_circuit_jit(circuit.to_tuples())
sv = sim.get_statevector()

de.sample_counts(sv, n_shots=1000, rng=np.random.default_rng(0))
{'00': 473, '11': 527}

sv is the same Bell state built on the Simulator page -- sample_counts(statevector, n_shots, rng) draws n_shots independent outcomes from its exact Born-rule probabilities and returns a dict of bitstring counts, the same shape get_probabilities() would predict exactly (50/50 here) but with the real sampling noise a finite number of shots actually has -- 473/527, not 500/500. Passing rng makes the draw reproducible; omit it for a fresh random draw each call.

Step 2. How close are two pure states?

de.statevector_fidelity(sv, sv)
0.9999999999999996
qasm2 = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; h q[0]; h q[1];'
sim2 = de.DenseSVSimulator(2)
sim2.run_circuit_jit(de.QASMParser().parse(qasm2).to_tuples())
sv2 = sim2.get_statevector()

de.statevector_fidelity(sv, sv2)
0.4999999999999998

statevector_fidelity(a, b) is |<a|b>|^2, computed directly on two statevectors -- no density matrix built first, unlike uhlmann_fidelity (this function's mixed-state counterpart). A state compared to itself gives 1 (up to floating-point rounding); the Bell state against |++> (independent H on each qubit, no entanglement) gives exactly 0.5 -- the two states share half their overlap, not zero and not identical.


Details

When to use which fidelity: statevector_fidelity needs both states to be pure (exact statevectors, as any noiseless simulation produces) -- for a genuinely mixed state (a real device's noisy output, or anything already reduced via partial_trace), use uhlmann_fidelity instead, which takes density matrices.

measurement

Measurement-related conveniences: turning a statevector into finite-shot counts the way real hardware and every other SDK report results (Qiskit's get_counts(), PennyLane's qml.counts()), and comparing two pure states directly without building a density matrix first.

Sampling a statevector into shot counts is another pattern found hand-duplicated across this package's own experiment scripts -- always the same rng.choice(dim, p=probs) + np.bincount (or np.unique) couple of lines, rewritten fresh each time.

sample_counts

sample_counts(statevector, n_shots, rng=None)

Simulate n_shots projective measurements of every qubit in the computational basis, returning a Qiskit-style counts dict.

Parameters:

Name Type Description Default
statevector (array - like, shape(2 ** n_qubits))
required
n_shots int

Number of measurement shots to sample, must be >= 1.

required
rng Generator

A fresh numpy.random.default_rng() is used if not given -- pass one explicitly for reproducible sampling.

None

Returns:

Type Description
dict[str, int]

Bitstring keys read left-to-right as qubit 0 upward (matching DenseSVSimulator's own qubit-0-is-MSB layout and pauli_expectation's string convention), each mapped to how many of the n_shots samples landed on it. Only bitstrings with at least one hit appear (unobserved outcomes are simply absent, as in Qiskit's get_counts()).

Examples:

>>> import dense_evolution as de
>>> sim = de.DenseSVSimulator(2)
>>> sim.run_circuit([('h', 0), ('cx', 0, 1)])
>>> de.sample_counts(sim.get_statevector(), 1000)
{'00': 494, '11': 506}
Source code in dense_evolution/utils/measurement.py
def sample_counts(statevector, n_shots, rng=None):
    """
    Simulate n_shots projective measurements of every qubit in the
    computational basis, returning a Qiskit-style counts dict.

    Parameters
    ----------
    statevector : array-like, shape (2**n_qubits,)
    n_shots : int
        Number of measurement shots to sample, must be >= 1.
    rng : numpy.random.Generator, optional
        A fresh `numpy.random.default_rng()` is used if not given --
        pass one explicitly for reproducible sampling.

    Returns
    -------
    dict[str, int]
        Bitstring keys read left-to-right as qubit 0 upward (matching
        DenseSVSimulator's own qubit-0-is-MSB layout and
        pauli_expectation's string convention), each mapped to how many
        of the n_shots samples landed on it. Only bitstrings with at
        least one hit appear (unobserved outcomes are simply absent, as
        in Qiskit's get_counts()).

    Examples
    --------
    >>> import dense_evolution as de
    >>> sim = de.DenseSVSimulator(2)
    >>> sim.run_circuit([('h', 0), ('cx', 0, 1)])
    >>> de.sample_counts(sim.get_statevector(), 1000)
    {'00': 494, '11': 506}
    """
    statevector = np.asarray(statevector)
    dim = statevector.shape[0]
    n_qubits = dim.bit_length() - 1
    if 1 << n_qubits != dim:
        raise ValueError(f"statevector length {dim} is not a power of 2")
    if n_shots < 1:
        raise ValueError(f"n_shots must be >= 1, got {n_shots}")

    probs = np.abs(statevector) ** 2
    total = probs.sum()
    if not np.isfinite(total) or total <= 0:
        raise ValueError("statevector has zero or non-finite total probability")
    probs = probs / total

    if rng is None:
        rng = np.random.default_rng()
    outcomes = rng.choice(dim, size=n_shots, p=probs)
    values, freqs = np.unique(outcomes, return_counts=True)

    return {format(int(v), f'0{n_qubits}b'): int(f) for v, f in zip(values, freqs)}

statevector_fidelity

statevector_fidelity(statevector_a, statevector_b)

Fidelity ||^2 between two pure statevectors -- the cheap, direct pure-state counterpart to uhlmann_fidelity (which compares two full density matrices, needed for the mixed/noisy states that zne_density_matrix and friends work with). Use this one whenever both states are pure; it never builds an O(dim^2) density matrix.

Parameters:

Name Type Description Default
statevector_a (array - like, shape(2 ** n_qubits))

Must have the same shape.

required
statevector_b (array - like, shape(2 ** n_qubits))

Must have the same shape.

required

Returns:

Type Description
float

In [0, 1] up to floating-point error. 1.0 for identical states (up to global phase), 0.0 for orthogonal states.

Source code in dense_evolution/utils/measurement.py
def statevector_fidelity(statevector_a, statevector_b):
    """
    Fidelity |<a|b>|^2 between two pure statevectors -- the cheap, direct
    pure-state counterpart to `uhlmann_fidelity` (which compares two full
    density matrices, needed for the mixed/noisy states that
    `zne_density_matrix` and friends work with). Use this one whenever
    both states are pure; it never builds an O(dim^2) density matrix.

    Parameters
    ----------
    statevector_a, statevector_b : array-like, shape (2**n_qubits,)
        Must have the same shape.

    Returns
    -------
    float
        In [0, 1] up to floating-point error. 1.0 for identical states
        (up to global phase), 0.0 for orthogonal states.
    """
    a = np.asarray(statevector_a)
    b = np.asarray(statevector_b)
    if a.shape != b.shape:
        raise ValueError(f"statevector shapes differ: {a.shape} vs {b.shape}")
    return float(np.abs(np.vdot(a, b)) ** 2)