Results and post-processing#
Raw results#
- class QCut.RawResult(results: list[list[dict[int, CircuitResult]]], shots: int, experiment=None)[source]#
Bases:
objectSimple 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. CallCircuitResult.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:
objectOne 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.
scalebrings the circuit back to the nominal shot count, since a batch runs at whatever its own circuits asked for.label_filteris 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, ...]#
- 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, andQuasiProbabilities.top(),QuasiProbabilities.probability_of()andQuasiProbabilities.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:
- 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:
MappingA 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()andmarginal()answer without ever building the2**kentries the mapping would have. Iterating,values()anditems()do build them, once, and keep them.The three dict views,
quasi_probabilities(),nearest_probabilities()andcounts(), report theDEFAULT_TOPmost likely bitstrings unless given atopof their own, since a distribution over more than a few qubits has more entries than is usually needed.top=Noneasks for all of them. Onlyquasi_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_erroris worked out from.
- Returns:
the distribution, nothing materialised.
- Return type:
- 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**kquasi-probabilities, entryxbeing the bitstringformat(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:
- 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()orcounts(). 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**widthvalues. It is built on the first call and kept, so asking twice costs once.- Returns:
2**widthquasi-probabilities, entryxbeing the bitstringformat(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 –
countis 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:
- 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.
Nonefor the whole distribution, in numerical order. Capped at2**width. Defaults toDEFAULT_TOP.- Returns:
the quasi-probabilities, keyed by bitstring.
- Return type:
dict[str, float]
- Raises:
ValueError –
topis neitherNonenor 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
topsets how much is reported, not how much is computed.With
top=Nonethe 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.
Nonefor all of them, in numerical order. Defaults toDEFAULT_TOP.- Returns:
the projected distribution, keyed by bitstring.
- Return type:
dict[str, float]
- Raises:
ValueError –
topis neitherNonenor 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.
Nonefor all of them. Defaults toDEFAULT_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
topis neitherNonenor positive.