Results and post-processing#

Raw results#

class QCut.RawResult(results: list[list[dict[int, CircuitResult]]], shots: int, experiment=None)[source]#

Bases: object

Simple wrapper class for raw experiment results. Stores the raw results, the shot count they were taken at, and the experiment they came from, so that QCut.estimate_expectation_values() can be called on the result alone rather than having the caller carry a second object around.

__init__(results: list[list[dict[int, CircuitResult]]], shots: int, experiment=None)[source]#
property experiment#

The experiment these results came from, if it was recorded.

property shots: int#

The shot count the experiment was run at.

result() → list[list[dict[int, CircuitResult]]][source]#

Get raw results for all experiments, as [group][observable][subcircuit].

Each subcircuit holds a CircuitResult, which is what the backend or sampler returned rather than counts taken out of it. Call CircuitResult.counts() on one for its counts.

class QCut.CircuitResult(result: object, index: int = 0, scale: float = 1.0, label_filter: tuple[tuple, ...]=<factory>)[source]#

Bases: object

One subcircuit’s result within one group, before it is turned into counts.

Holds what the backend or the sampler handed back rather than counts taken out of it, so running an experiment does no arithmetic of its own and the estimate can be recomputed from the same results.

scale brings the circuit back to the nominal shot count, since a batch runs at whatever its own circuits asked for. label_filter is the communicating wire cuts’ selection: several groups share one measuring circuit and each keeps only the shots carrying the label it answers, so the selection belongs to the group’s own result rather than to the circuit they all read.

result: object#
index: int = 0#
scale: float = 1.0#
label_filter: tuple[tuple, ...]#
raw_counts() → dict[str, float][source]#

Counts as measured, brought to the nominal shot count.

counts() → dict[str, float][source]#

What this group takes from the circuit: rescaled, then label-selected.

__init__(result: object, index: int = 0, scale: float = 1.0, label_filter: tuple[tuple, ...]=<factory>) → None#

Expectation values#

QCut.estimate_expectation_values(results: RawResult) → ndarray[source]#

Calculate the estimated expectation values.

The estimate is the quasiprobability sum itself,

\[\langle O \rangle = (-1)^{w+1} \sum_g c_g E_g\]

over the subcircuit groups, where \(c_g\) is the group’s coefficient, \(w\) counts the wire cuts and \(E_g\) is the group’s own estimate: over the outcomes of its subcircuits, the product of their probabilities times the observable’s eigenvalue times the sign the mid-circuit measurements carry. The parity is the qpd register’s sign convention: every one of its bits maps 0 to -1, so an unwritten bit contributes -1 and only the number allocated per subcircuit survives.

Multi-qubit observables pick up a further \((-1)^{m+1}\) for their \(m\) qubits.

\(E_g\) factorises over the subcircuits, so each is read once per group rather than walking the product of their outcomes, and the observables sharing a measurement setting are read together rather than one pass each. See _outcome_spectrum().

Parameters:

results (RawResult) – raw results from experiment circuits, carrying the experiment they came from.

Returns:

one expectation value per observable, shaped like the observables the experiment was built with, so a single observable comes back as a zero-dimensional array

Return type:

np.ndarray

QCut.estimate_standard_errors(results: RawResult) → ndarray[source]#

An upper bound on the shot noise in estimate_expectation_values().

Each Pauli term is the sum \(\sum_g c_g E_g\) over the groups, each group estimated from \(n\) shots of outcomes in \([-1, 1]\), so its standard error is at most

\[\sqrt{\sum_g c_g^2 / n \; + \; \gamma^2 / D}\]

with \(\gamma = \sum_g |c_g|\). The second term is there only when the decomposition was sampled, \(D\) being the number of draws. This is the stratified-sampling variance with each group’s variance taken at its largest, see arXiv:2602.11245. An observable \(\sum_j a_j P_j\) gets \(\sum_j |a_j|\) times that.

It covers sampling error only, not device noise or bias.

Parameters:

results (RawResult) – raw results from experiment circuits, carrying the experiment they came from.

Returns:

one standard error per observable, shaped like the expectation values.

Return type:

np.ndarray

Probability distributions#

QCut.estimate_probabilities(result: RawResult) → QuasiProbabilities[source]#

Reconstruct the distribution over the qubits the experiment measured.

The circuits do not grow with the number of qubits asked for. Z observables all commute, so they share one measurement setting and the experiment is the size it would have been for a single observable. Neither does the reading. What comes back is held as a product over the subcircuits rather than as a table, so building it costs nothing in k – see _separable_form(). Only asking for every bitstring at once does, and QuasiProbabilities.top(), QuasiProbabilities.probability_of() and QuasiProbabilities.marginal() answer without doing that.

The bitstrings are written with the first of the chosen qubits last, so a subset given as [2, 0] reads qubit 2 as the rightmost character.

Parameters:

result (RawResult) – results of an experiment built with qubits.

Returns:

the distribution, which also offers the projected distribution and a scaling to counts.

Return type:

QuasiProbabilities

Raises:

ValueError – the experiment was built with observables rather than qubits, so it does not carry the full set this needs.

class QCut.QuasiProbabilities(data, shots: int | None = None)[source]#

Bases: Mapping

A reconstructed distribution over bitstrings.

A mapping from bitstring to quasi-probability, so it indexes, iterates and plots like a dict. The values are held as a product over the subcircuits, which is what lets top(), probability_of() and marginal() answer without ever building the 2**k entries the mapping would have. Iterating, values() and items() do build them, once, and keep them.

The three dict views, quasi_probabilities(), nearest_probabilities() and counts(), report the DEFAULT_TOP most likely bitstrings unless given a top of their own, since a distribution over more than a few qubits has more entries than is usually needed. top=None asks for all of them. Only quasi_probabilities() gets out of building them all, since the other two project onto the nearest physical distribution first, and what that projection is depends on every value.

shots#

what the experiment ran at, carried so counts() has a scale to work with by default.

Type:

int | None

qubits#

the qubits the distribution spans, in the order their bits are read back. The first is the last character of a key.

Type:

list[int]

__init__(data, shots: int | None = None)[source]#

Init from a mapping of bitstring to value, which must cover every bitstring.

Parameters:
  • data (Mapping[str, float]) – the quasi-probabilities, keyed by bitstring.

  • shots (int) – what the experiment ran at, if it is known.

classmethod from_distribution(distribution: SeparableDistribution, shots: int | None = None, qubits: list[int] | None = None, term_error: float | None = None) → QuasiProbabilities[source]#

Wrap a separable distribution without expanding it.

Parameters:
  • distribution (SeparableDistribution) – the reconstruction, as _separable_form() builds it.

  • shots (int) – what the experiment ran at, if it is known.

  • qubits (list[int]) – the qubits it spans, in the order their bits are read back. Defaults to naming the bits themselves.

  • term_error (float) – the standard-error bound of one Z string’s estimate, which standard_error is worked out from.

Returns:

the distribution, nothing materialised.

Return type:

QuasiProbabilities

classmethod from_array(values: ndarray, shots: int | None = None, qubits: list[int] | None = None, term_error: float | None = None) → QuasiProbabilities[source]#

Wrap values already worked out, indexed by outcome.

Parameters:
  • values (np.ndarray) – 2**k quasi-probabilities, entry x being the bitstring format(x, f"0{k}b").

  • shots (int) – what the experiment ran at, if it is known.

  • qubits (list[int]) – the qubits it spans, in the order their bits are read back. Defaults to naming the bits themselves.

  • term_error (float) – as in from_distribution().

Returns:

the distribution.

Return type:

QuasiProbabilities

property width: int#

How many qubits the distribution spans.

property standard_error: float | None#

An upper bound on the shot noise of each quasi-probability.

One bitstring’s probability is the sum of the \(2^k - 1\) Z strings over the \(k\) qubits, each weighted \(2^{-k}\), plus a constant. So it gets \(1 - 2^{-k}\) times the bound QCut.estimate_standard_errors() gives one Z string, the same for every bitstring. It is loose for unlikely bitstrings.

It does not apply to nearest_probabilities() or counts(). None if the distribution was not reconstructed from an experiment.

probabilities() → ndarray[source]#

Every value at once.

This is the one accessor that holds 2**width values. It is built on the first call and kept, so asking twice costs once.

Returns:

2**width quasi-probabilities, entry x being the bitstring format(x, f"0{width}b").

Return type:

np.ndarray

probability_of(bitstring: str) → float[source]#

One bitstring’s quasi-probability, without building the rest.

Costs one pass over the experiment’s groups per subcircuit, so it does not grow with how many qubits the distribution spans.

Parameters:

bitstring (str) – the bitstring, first chosen qubit last.

Returns:

its quasi-probability, which may be negative.

Return type:

float

Raises:

KeyError – the bitstring is not one of this distribution’s.

top(count: int) → dict[str, float][source]#

The most likely bitstrings, without building the rest.

Parameters:

count (int) – how many bitstrings to return. Capped at 2**width.

Returns:

the bitstrings and their quasi-probabilities, most likely first. Values may be negative and are not projected – projection needs the whole distribution, so it is only offered by nearest_probabilities().

Return type:

dict[str, float]

Raises:

ValueError – count is not positive.

marginal(qubits: list[int]) → QuasiProbabilities[source]#

The distribution over a subset of the qubits, summed over the rest.

Parameters:

qubits (list[int]) – the qubits to keep, from qubits, in the order their bits should be read back.

Returns:

the marginal distribution.

Return type:

QuasiProbabilities

Raises:

ValueError – a qubit is not one this distribution spans.

quasi_probabilities(top: int | None = 10) → dict[str, float][source]#

The values as reconstructed, negative ones included.

Parameters:

top (int) – how many of the most likely bitstrings to return, most likely first. None for the whole distribution, in numerical order. Capped at 2**width. Defaults to DEFAULT_TOP.

Returns:

the quasi-probabilities, keyed by bitstring.

Return type:

dict[str, float]

Raises:

ValueError – top is neither None nor positive.

nearest_probabilities(top: int | None = 10) → dict[str, float][source]#

The closest true distribution.

The projection is over the whole distribution however few bitstrings are asked for, since the mass clipped off the negative entries has to go somewhere. So top sets how much is reported, not how much is computed.

With top=None the bitstrings clipped to zero are kept, so the result covers the same keys as the quasi-probabilities it came from.

Parameters:

top (int) – how many of the most likely bitstrings to return, most likely first. None for all of them, in numerical order. Defaults to DEFAULT_TOP.

Returns:

the projected distribution, keyed by bitstring.

Return type:

dict[str, float]

Raises:

ValueError – top is neither None nor positive.

counts(shots: int | None = None, top: int | None = 10) → dict[str, float][source]#

The distribution scaled to a shot count.

Parameters:
  • shots (int) – what to scale by. Defaults to the shots the experiment ran at.

  • top (int) – how many of the most likely bitstrings to return, most likely first. None for all of them. Defaults to DEFAULT_TOP.

Returns:

the projected distribution scaled by shots.

Return type:

dict[str, float]

Raises:

ValueError – no shot count was carried and none was given, or top is neither None nor positive.