Analyzing results

Every circuit executed on MIMIQ produces a QCSResults object. You get one from getresult, a vector of them from getresults (see Cloud Execution), or by loading a results file with loadproto. This page explains what it contains and how to turn it into counts, probabilities and plots.

The examples build a QCSResults by hand, so that they run without a connection to MIMIQ. A result returned by the service works in exactly the same way.

A results object

QCSResults can be constructed from its fields, in this order: simulator name, simulator version, fidelities, average gate errors, classical register samples, Z-register contents, amplitudes and timings.

using MimiqCircuits
using Random

# Fake the output of a 3-qubit GHZ circuit measured 1000 times: the two
# outcomes 000 and 111 appear with equal probability, and a few samples have
# one bit flipped, as a noisy device would produce.
rng = MersenneTwister(42)
cstates = map(1:1000) do _
    bits = fill(rand(rng, Bool), 3)
    if rand(rng) < 0.05
        k = rand(rng, 1:3)
        bits[k] = !bits[k]
    end
    BitString(bits)
end

res = QCSResults(
    "MIMIQ-StateVector",                       # simulator
    "x.y.z",                                   # simulator version
    [1.0],                                     # one fidelity per execution
    [0.0],                                     # one gate error per execution
    cstates,                                   # one BitString per sample
    [ComplexF64[0.9]],                         # Z-register, one vector per execution
    Dict(bs"000" => 1 / sqrt(2) + 0im),        # requested amplitudes
    Dict("total" => 0.012, "apply" => 0.004, "sample" => 0.006),
)
QCSResults
Simulator
MIMIQ-StateVector x.y.z
Timings
apply time4.0ms
sample time6.0ms
total time12.0ms
Fidelity estimate
Single run value1.0
Average multiqubit error estimate
Single run value0.0
Statistics
Number of executions1
Number of samples1000
Number of amplitudes1
creg (most sampled)
bs"111"484
bs"000"472
bs"101"10
bs"110"9
bs"011"9
bs"001"6
bs"010"6
bs"100"4
zreg (most sampled)
0.91
Amplitudes
bs"000"0.7071067811865475 + 0.0im

Printing a result shows a summary: simulator, timings, fidelity, the five most frequent samples and the number of executions, amplitudes and samples.

Fields

FieldTypeContent
simulatorStringname of the simulator that ran the circuit
versionStringversion of that simulator
fidelitiesVector{Float64}fidelity estimate of each execution, see Fidelity and Error estimates
avggateerrorsVector{Float64}average multi-qubit gate error of each execution
cstatesVector{BitString}the classical register of each sample
zstatesVector{Vector{ComplexF64}}the Z-register of each execution
amplitudesDict{BitString,ComplexF64}amplitudes requested with the bitstrings keyword of submit
timingsDict{String,Float64}time in seconds spent in each stage, under "total" the whole run

How many entries fidelities, avggateerrors and zstates hold depends on how the circuit was sampled (see Understanding sampling). A circuit without mid-circuit measurements or other non-unitary operations is executed once and sampled many times, so these vectors have one entry. A circuit that needs one execution per sample has one entry per sample.

# Number of samples, and the first few of them.
length(res.cstates), res.cstates[1:3]
(1000, BitString[bs"000", bs"000", bs"111"])
# Lowest fidelity over all executions.
minimum(res.fidelities)
1.0
# Total run time in seconds.
res.timings["total"]
0.012

Counts and probabilities

histsamples counts how many times each classical-register value was sampled and returns a Dict{BitString,Int}:

counts = histsamples(res)
Dict{BitString, Int64} with 8 entries:
  bs"110" => 9
  bs"100" => 4
  bs"000" => 472
  bs"011" => 9
  bs"111" => 484
  bs"001" => 6
  bs"010" => 6
  bs"101" => 10

Sort it to find the most frequent outcomes, and divide by the number of samples to estimate probabilities:

nsamples = length(res.cstates)
probs = Dict(b => n / nsamples for (b, n) in counts)

# Outcomes from most to least likely.
sort(collect(probs); by=last, rev=true)
8-element Vector{Pair{BitString, Float64}}:
 bs"111" => 0.484
 bs"000" => 0.472
 bs"101" => 0.01
 bs"011" => 0.009
 bs"110" => 0.009
 bs"001" => 0.006
 bs"010" => 0.006
 bs"100" => 0.004

The outcomes are BitStrings, so the bitstring helpers apply to them. Bit i of the BitString is the classical bit i of the circuit:

b = bs"011"
b[1], nonzeros(b), to01(b), bitstring_to_integer(b)
(false, [2, 3], "011", 6)

bitstring_to_integer reads bit 1 as the least significant bit.

Estimates of observables follow directly from the samples. For example, the expectation value of $Z_1 Z_2$ is the average of $(-1)^{b_1 + b_2}$:

zz = sum(s -> (-1)^(s[1] + s[2]), res.cstates) / nsamples
0.942

Z-register and amplitudes

Operations such as ExpectationValue, Amplitude or VonNeumannEntropy write their output into the Z-register (see Statistical Operations). Each element of zstates is the content of the Z-register at the end of one execution, indexed like the zvars of the circuit:

# Value written to Z-register 1 in the first (here the only) execution.
res.zstates[1][1]
0.9 + 0.0im

When there is one execution per sample, average over them to get the estimate:

sum(z -> z[1], res.zstates) / length(res.zstates)
0.9 + 0.0im

Amplitudes requested through the bitstrings keyword of submit are stored in amplitudes, keyed by bitstring:

abs2(res.amplitudes[bs"000"])
0.4999999999999999

Plotting

A QCSResults can be plotted directly with Plots. The plot is a bar chart of the most frequent outcomes.

using Plots
plot(res)
GKS: cannot open display - headless operation mode active

Bar chart of the sampled outcomes

The keyword max_outcomes (default 15) sets how many outcomes are shown, and endianess (:big by default, or :little) sets the order in which the bits of each outcome are written on the axis.

plot(res; max_outcomes=4, endianess=:little)

Bar chart of the four most frequent outcomes

Saving and loading

Results are saved in the protobuf format with saveproto and read back with loadproto. This is also the format of the result files you can download from the MIMIQ web interface.

fname = joinpath(mktempdir(), "results.pb")
saveproto(fname, res)

loaded = loadproto(fname, QCSResults)
histsamples(loaded) == histsamples(res)
true

saveresults and loadresults do the same with shorter calls: saveresults(fname, res) and loadresults(fname).

saveproto writes one result per file, so save each element of the vector returned by getresults separately.