Analysing Results with DataArrays¶
Note
DataArray conversion requires BinaryCount format results — the SDK
default. If you pass a custom
CompilerConfig, ensure it includes
results_format=QuantumResultsFormat().binary_count(). Passing results
in any other format (e.g. raw()) raises ValueError.
In BinaryCount format the server returns shot counts keyed by observed
bitstring, e.g. {"c": {"00": 479, "11": 521}}. This page shows how to
convert those counts into a labelled 2-D
scipp DataArray, which makes it straightforward
to:
Slice by bit value — inspect a specific bitstring or a range of values
Aggregate — sum counts across all runs, or across all bitstrings
Filter — keep only bitstrings matching a wildcard pattern (e.g.
"1*"for all states where the most-significant bit is1)Change endianness — translate between OQC’s big-endian notation and Qiskit’s little-endian notation
Understanding the DataArray structure¶
results_to_counts_dataarray() converts
a list of JobResult objects into a 2-D
array:
Row axis (
dim="result") — one row per execution run.Column axis (
dim="bit") — one column per unique bitstring observed across all supplied results. Bitstrings absent from a particular run are filled with0.
Two coordinates label the column axis:
"bit"— the physical integer value of each bitstring (always big-endian, regardless of the endianness option)."011"→3."bitstring"— the human-readable string label in whichever endianness convention was requested.
Measurement counts for a 2-qubit Bell state (|Φ+⟩), 2 runs:
bit=0 bit=3
("00") ("11")
result=0: 479 521 ← run 1
result=1: 463 537 ← run 2
This structure lets you use scipp’s slicing and reduction operations directly without writing any custom loops.
Quick reference¶
import scipp as sc
from oqc_qcaas_sdk import Endianness
from oqc_qcaas_sdk.dataarray_utils import bits
# --- convert ---
da = outputs.to_counts_dataarray("c") # from JobOutputProxy
da = result.to_counts_dataarray("c") # from a single JobResult
# --- slice a single bitstring ---
da["bit", bits("11")] # select column where bit value = 3
# --- slice a range (half-open interval on the bit coordinate) ---
da["bit", bits("00"):bits("10")] # columns 0 and 1 i.e. "00" and "01"
# --- sum across runs ---
sc.sum(da, dim="result") # total counts per bitstring
# --- sum across bitstrings ---
sc.sum(da, dim="bit") # total shots per run
# --- wildcard filter via proxy ---
high = outputs.like("1*", creg="c") # MSB = 1 (big-endian / OQC)
high = outputs.like("*1", creg="c",
endianness=Endianness.LITTLE) # same states, Qiskit notation
The bits() helper¶
bits() converts a bitstring to a
unit-less scipp scalar so it can be used as an index or slice bound.
>>> from oqc_qcaas_sdk.dataarray_utils import bits, Endianness
>>> import scipp as sc
>>> bits("00") # "00" big-endian → 0
<scipp.Variable> () int64 <no unit> 0
>>> bits("11") # "11" big-endian → 3
<scipp.Variable> () int64 <no unit> 3
>>> bits("01") # "01" big-endian → 1
<scipp.Variable> () int64 <no unit> 1
OQC uses big-endian notation: the leftmost character is the
most-significant bit. Qiskit uses little-endian (leftmost = least
significant). The same physical quantum state |01⟩ (qubit 1 measured as
1, qubit 0 as 0) is written "01" in OQC notation and "10" in Qiskit.
bits() accepts an endianness argument so both notations reach the same
column:
>>> bool(bits("01", Endianness.BIG) == bits("10", Endianness.LITTLE))
True
The "bit" coordinate in every DataArray always stores the physical
(big-endian) integer value, so bits()-based slicing is consistent
regardless of which endianness you chose when building the DataArray.
Wildcard filtering with like()¶
like() filters a proxy
to the bitstring columns matching a pattern. Use '*' to match either bit,
'0' or '1' to require an exact value. The pattern length must equal
the register width.
# Keep only bitstrings where both qubits are the same (correlated states)
correlated = outputs.like("0*0", creg="c") # first and last bit both '0'... for 3-qubit
# Big-endian: MSB = '1'
excited = outputs.like("1*", creg="c")
# Equivalent in Qiskit little-endian convention
excited_le = outputs.like("*1", creg="c", endianness=Endianness.LITTLE)
filter_counts_dataarray() is the lower-
level equivalent that operates directly on an existing DataArray.
Using DataArrays with CompositeJob¶
When execute() completes it returns a
JobOutputProxy containing one
JobResult per child job. Because
to_counts_dataarray and like both operate on a proxy, building a
combined DataArray from all children is a single call — each child job
becomes one row:
outputs = await cjob.execute()
da = outputs.to_counts_dataarray("c")
The resulting DataArray has shape (n_children, n_bitstrings). You can
then index or aggregate it using standard scipp operations:
da["result", 0]— counts from child job 0da["result", 1]— counts from child job 1sc.sum(da, dim="result")— totals across all children
The same is true via outputs after
execution:
da = cjob.outputs.to_counts_dataarray("c")
Partial success and errors¶
Not every child job is guaranteed to produce a
JobResult. If some children fail, their
outputs are JobError objects. Both
to_counts_dataarray and like skip errors and only include successful
results — but they emit a UserWarning to make the omission explicit,
so the DataArray will have fewer rows than children in that case.
If you want strict all-or-nothing behaviour — raise immediately rather than
silently drop errors — call raise_for_error()
first:
# Raises on the first child error; otherwise returns self for chaining
da = outputs.raise_for_error().to_counts_dataarray("c")
If all children failed and there are no results at all, to_counts_dataarray
raises ValueError with a message indicating that the proxy may be
all-errors or empty.
Working with Qiskit output¶
Qiskit returns bitstrings in little-endian order — the leftmost character represents qubit 0 (the least-significant bit). OQC uses big-endian — the leftmost character is the most-significant bit.
The same entangled state that OQC labels "01" (qubit 1 = 0, qubit 0 = 1)
is labelled "10" by Qiskit. Pass endianness=Endianness.LITTLE to have
the DataArray’s "bitstring" coordinate and all wildcard patterns work in
Qiskit’s convention, while the underlying "bit" integer coordinate remains
the same physical value:
>>> from oqc_qcaas_sdk.dataarray_utils import (
... bits, Endianness, results_to_counts_dataarray
... )
>>> from oqc_qcaas_sdk.job_output import JobResult
>>> import uuid, scipp as sc
>>> # Simulated OQC result for a 2-qubit register
>>> result = JobResult(data={"c": {"00": 479, "01": 10, "10": 12, "11": 499}})
>>> da_be = results_to_counts_dataarray([result], creg="c")
>>> da_le = results_to_counts_dataarray([result], creg="c",
... endianness=Endianness.LITTLE)
>>> # OQC big-endian labels
>>> list(da_be.coords["bitstring"].values)
['00', '01', '10', '11']
>>> # Qiskit little-endian labels (each bitstring reversed)
>>> list(da_le.coords["bitstring"].values)
['00', '10', '01', '11']
>>> # The physical "bit" coordinate is identical in both cases
>>> list(da_be.coords["bit"].values) == list(da_le.coords["bit"].values)
True
>>> # bits() accepts the same endianness argument — the same physical column
>>> # is selected regardless of which convention you work in:
>>> int(da_be["bit", bits("01", Endianness.BIG)].values[0])
10
>>> int(da_le["bit", bits("10", Endianness.LITTLE)].values[0])
10