Statevector testing utilities¶
Download Notebook - statevector_testing.ipynb
Simulation lets you check quantum amplitudes, complete operations, and individual measurement branches. These examples introduce the helpers for each task.
Inspect a state: compare its amplitudes and relative phases.
Extract an operation: check its action on every computational-basis input.
Post-select a branch: inspect the state or operation conditioned on chosen outcomes.
Replay measurements: exercise success, failure, and correction paths predictably.
Run the cells in order; later examples reuse earlier imports and definitions.
from guppylang.std.quantum import (
discard_array,
discard,
x,
ry,
cx,
qubit,
h
)
from guppylang.std.angles import angle
from guppylang import guppy
from guppylang.std.debug import state_output
from guppylang.std.builtins import comptime, array, nat
from guppyalgos.utils import qarray
import numpy as np
Inspect a statevector¶
Use get_statevector to inspect the complex amplitudes of a prepared state:
Record the state with
state_output("result_state", state)before discarding the qubits.The tag must be
"result_state"; this is a simulation snapshot, not a measurement.The returned NumPy array contains the amplitudes \(a_x\), including their relative phases.
The example below applies the same Y rotation to each of four qubits.
n_qubits = 4
@guppy
def main_sv() -> None:
state = qarray(n_qubits)
for i in range(n_qubits):
ry(state[i], angle(3.14/2))
state_output("result_state", state)
discard_array(state)
from guppyalgos.tests.helpers import get_statevector, get_unitary
get_statevector(main_sv, n_qubits)
array([ 0.37096824-0.00000000e+00j, -0.2972016 +1.81983495e-17j,
-0.2972016 +1.81983495e-17j, 0.23810338-2.91592544e-17j,
-0.2972016 +1.81983495e-17j, 0.23810338-2.91592544e-17j,
0.23810338-2.91592544e-17j, -0.19075678+3.50414518e-17j,
-0.2972016 +1.81983495e-17j, 0.23810338-2.91592544e-17j,
0.23810338-2.91592544e-17j, -0.19075678+3.50414518e-17j,
0.23810338-2.91592544e-17j, -0.19075678+3.50414518e-17j,
-0.19075678+3.50414518e-17j, 0.152825 -3.74313289e-17j])
Extract a unitary matrix¶
Use get_unitary to check a whole operation. Each column is the output for one computational-basis input:
Supply a circuit that accepts one qubit array and leaves it available to its caller.
The helper prepares the basis inputs and records the outputs for you; no
state_outputcall is needed.Compare complex matrix entries when relative phases matter. Measurement probabilities alone cannot detect every phase error.
@guppy
def main_unitary[n_s: nat](state: array[qubit, n_s]) -> None:
for i in range(n_qubits):
ry(state[i], angle(3.14/2))
get_unitary(main_unitary, n_qubits)
array([[ 0.37096824+0.00000000e+00j, 0.2972016 +2.27152533e-17j,
0.2972016 +2.27152533e-17j, 0.23810338+3.63966991e-17j,
0.2972016 +2.27152533e-17j, 0.23810338+3.63966991e-17j,
0.23810338+3.63966991e-17j, 0.19075678+4.37388816e-17j,
0.2972016 +2.27152533e-17j, 0.23810338+3.63966991e-17j,
0.23810338+3.63966991e-17j, 0.19075678+4.37388816e-17j,
0.23810338+3.63966991e-17j, 0.19075678+4.37388816e-17j,
0.19075678+4.37388816e-17j, 0.152825 +4.67219358e-17j],
[-0.2972016 +1.81983495e-17j, 0.37096824-4.09136029e-17j,
-0.23810338-3.61872232e-18j, 0.2972016 -1.00627234e-17j,
-0.23810338-3.61872232e-18j, 0.2972016 -1.00627234e-17j,
-0.19075678-1.74787705e-17j, 0.23810338+1.01365879e-17j,
-0.23810338-3.61872232e-18j, 0.2972016 -1.00627234e-17j,
-0.19075678-1.74787705e-17j, 0.23810338+1.01365879e-17j,
-0.19075678-1.74787705e-17j, 0.23810338+1.01365879e-17j,
-0.152825 -2.56836196e-17j, 0.19075678+2.27005655e-17j],
[-0.2972016 +1.81983495e-17j, -0.23810338-3.61872232e-18j,
0.37096824-4.09136029e-17j, 0.2972016 -1.00627234e-17j,
-0.23810338-3.61872232e-18j, -0.19075678-1.74787705e-17j,
0.2972016 -1.00627234e-17j, 0.23810338+1.01365879e-17j,
-0.23810338-3.61872232e-18j, -0.19075678-1.74787705e-17j,
0.2972016 -1.00627234e-17j, 0.23810338+1.01365879e-17j,
-0.19075678-1.74787705e-17j, -0.152825 -2.56836196e-17j,
0.23810338+1.01365879e-17j, 0.19075678+2.27005655e-17j],
[ 0.23810338-2.91592544e-17j, -0.2972016 +5.09763263e-17j,
-0.2972016 +5.09763263e-17j, 0.37096824-8.18272057e-17j,
0.19075678-8.78134068e-18j, -0.23810338+2.26413888e-17j,
-0.23810338+2.26413888e-17j, 0.2972016 -4.28407001e-17j,
0.19075678-8.78134068e-18j, -0.23810338+2.26413888e-17j,
-0.23810338+2.26413888e-17j, 0.2972016 -4.28407001e-17j,
0.152825 +4.64530345e-18j, -0.19075678+3.55954569e-18j,
-0.19075678+3.55954569e-18j, 0.23810338-1.61235232e-17j],
[-0.2972016 +1.81983495e-17j, -0.23810338-3.61872232e-18j,
-0.23810338-3.61872232e-18j, -0.19075678-1.74787705e-17j,
0.37096824-4.09136029e-17j, 0.2972016 -1.00627234e-17j,
0.2972016 -1.00627234e-17j, 0.23810338+1.01365879e-17j,
-0.23810338-3.61872232e-18j, -0.19075678-1.74787705e-17j,
-0.19075678-1.74787705e-17j, -0.152825 -2.56836196e-17j,
0.2972016 -1.00627234e-17j, 0.23810338+1.01365879e-17j,
0.23810338+1.01365879e-17j, 0.19075678+2.27005655e-17j],
[ 0.23810338-2.91592544e-17j, -0.2972016 +5.09763263e-17j,
0.19075678-8.78134068e-18j, -0.23810338+2.26413888e-17j,
-0.2972016 +5.09763263e-17j, 0.37096824-8.18272057e-17j,
-0.23810338+2.26413888e-17j, 0.2972016 -4.28407001e-17j,
0.19075678-8.78134068e-18j, -0.23810338+2.26413888e-17j,
0.152825 +4.64530345e-18j, -0.19075678+3.55954569e-18j,
-0.23810338+2.26413888e-17j, 0.2972016 -4.28407001e-17j,
-0.19075678+3.55954569e-18j, 0.23810338-1.61235232e-17j],
[ 0.23810338-2.91592544e-17j, 0.19075678-8.78134068e-18j,
-0.2972016 +5.09763263e-17j, -0.23810338+2.26413888e-17j,
-0.2972016 +5.09763263e-17j, -0.23810338+2.26413888e-17j,
0.37096824-8.18272057e-17j, 0.2972016 -4.28407001e-17j,
0.19075678-8.78134068e-18j, 0.152825 +4.64530345e-18j,
-0.23810338+2.26413888e-17j, -0.19075678+3.55954569e-18j,
-0.23810338+2.26413888e-17j, -0.19075678+3.55954569e-18j,
0.2972016 -4.28407001e-17j, 0.23810338-1.61235232e-17j],
[-0.19075678+3.50414518e-17j, 0.23810338-5.54193656e-17j,
0.23810338-5.54193656e-17j, -0.2972016 +8.37543030e-17j,
0.23810338-5.54193656e-17j, -0.2972016 +8.37543030e-17j,
-0.2972016 +8.37543030e-17j, 0.37096824-1.22740809e-16j,
-0.152825 +1.63930127e-17j, 0.19075678-2.98196568e-17j,
0.19075678-2.98196568e-17j, -0.23810338+4.89015000e-17j,
0.19075678-2.98196568e-17j, -0.23810338+4.89015000e-17j,
-0.23810338+4.89015000e-17j, 0.2972016 -7.56186769e-17j],
[-0.2972016 +1.81983495e-17j, -0.23810338-3.61872232e-18j,
-0.23810338-3.61872232e-18j, -0.19075678-1.74787705e-17j,
-0.23810338-3.61872232e-18j, -0.19075678-1.74787705e-17j,
-0.19075678-1.74787705e-17j, -0.152825 -2.56836196e-17j,
0.37096824-4.09136029e-17j, 0.2972016 -1.00627234e-17j,
0.2972016 -1.00627234e-17j, 0.23810338+1.01365879e-17j,
0.2972016 -1.00627234e-17j, 0.23810338+1.01365879e-17j,
0.23810338+1.01365879e-17j, 0.19075678+2.27005655e-17j],
[ 0.23810338-2.91592544e-17j, -0.2972016 +5.09763263e-17j,
0.19075678-8.78134068e-18j, -0.23810338+2.26413888e-17j,
0.19075678-8.78134068e-18j, -0.23810338+2.26413888e-17j,
0.152825 +4.64530345e-18j, -0.19075678+3.55954569e-18j,
-0.2972016 +5.09763263e-17j, 0.37096824-8.18272057e-17j,
-0.23810338+2.26413888e-17j, 0.2972016 -4.28407001e-17j,
-0.23810338+2.26413888e-17j, 0.2972016 -4.28407001e-17j,
-0.19075678+3.55954569e-18j, 0.23810338-1.61235232e-17j],
[ 0.23810338-2.91592544e-17j, 0.19075678-8.78134068e-18j,
-0.2972016 +5.09763263e-17j, -0.23810338+2.26413888e-17j,
0.19075678-8.78134068e-18j, 0.152825 +4.64530345e-18j,
-0.23810338+2.26413888e-17j, -0.19075678+3.55954569e-18j,
-0.2972016 +5.09763263e-17j, -0.23810338+2.26413888e-17j,
0.37096824-8.18272057e-17j, 0.2972016 -4.28407001e-17j,
-0.23810338+2.26413888e-17j, -0.19075678+3.55954569e-18j,
0.2972016 -4.28407001e-17j, 0.23810338-1.61235232e-17j],
[-0.19075678+3.50414518e-17j, 0.23810338-5.54193656e-17j,
0.23810338-5.54193656e-17j, -0.2972016 +8.37543030e-17j,
-0.152825 +1.63930127e-17j, 0.19075678-2.98196568e-17j,
0.19075678-2.98196568e-17j, -0.23810338+4.89015000e-17j,
0.23810338-5.54193656e-17j, -0.2972016 +8.37543030e-17j,
-0.2972016 +8.37543030e-17j, 0.37096824-1.22740809e-16j,
0.19075678-2.98196568e-17j, -0.23810338+4.89015000e-17j,
-0.23810338+4.89015000e-17j, 0.2972016 -7.56186769e-17j],
[ 0.23810338-2.91592544e-17j, 0.19075678-8.78134068e-18j,
0.19075678-8.78134068e-18j, 0.152825 +4.64530345e-18j,
-0.2972016 +5.09763263e-17j, -0.23810338+2.26413888e-17j,
-0.23810338+2.26413888e-17j, -0.19075678+3.55954569e-18j,
-0.2972016 +5.09763263e-17j, -0.23810338+2.26413888e-17j,
-0.23810338+2.26413888e-17j, -0.19075678+3.55954569e-18j,
0.37096824-8.18272057e-17j, 0.2972016 -4.28407001e-17j,
0.2972016 -4.28407001e-17j, 0.23810338-1.61235232e-17j],
[-0.19075678+3.50414518e-17j, 0.23810338-5.54193656e-17j,
-0.152825 +1.63930127e-17j, 0.19075678-2.98196568e-17j,
0.23810338-5.54193656e-17j, -0.2972016 +8.37543030e-17j,
0.19075678-2.98196568e-17j, -0.23810338+4.89015000e-17j,
0.23810338-5.54193656e-17j, -0.2972016 +8.37543030e-17j,
0.19075678-2.98196568e-17j, -0.23810338+4.89015000e-17j,
-0.2972016 +8.37543030e-17j, 0.37096824-1.22740809e-16j,
-0.23810338+4.89015000e-17j, 0.2972016 -7.56186769e-17j],
[-0.19075678+3.50414518e-17j, -0.152825 +1.63930127e-17j,
0.23810338-5.54193656e-17j, 0.19075678-2.98196568e-17j,
0.23810338-5.54193656e-17j, 0.19075678-2.98196568e-17j,
-0.2972016 +8.37543030e-17j, -0.23810338+4.89015000e-17j,
0.23810338-5.54193656e-17j, 0.19075678-2.98196568e-17j,
-0.2972016 +8.37543030e-17j, -0.23810338+4.89015000e-17j,
-0.2972016 +8.37543030e-17j, -0.23810338+4.89015000e-17j,
0.37096824-1.22740809e-16j, 0.2972016 -7.56186769e-17j],
[ 0.152825 -3.74313289e-17j, -0.19075678+5.60797680e-17j,
-0.19075678+5.60797680e-17j, 0.23810338-8.16794767e-17j,
-0.19075678+5.60797680e-17j, 0.23810338-8.16794767e-17j,
0.23810338-8.16794767e-17j, -0.2972016 +1.16532280e-16j,
-0.19075678+5.60797680e-17j, 0.23810338-8.16794767e-17j,
0.23810338-8.16794767e-17j, -0.2972016 +1.16532280e-16j,
0.23810338-8.16794767e-17j, -0.2972016 +1.16532280e-16j,
-0.2972016 +1.16532280e-16j, 0.37096824-1.63654411e-16j]])
Allow for internal work qubits¶
Pass
n_extra_qubitswhen the circuit allocates work qubits internally.Return those work qubits to \(|0\rangle\) before discarding them, so they are disentangled from the logical register.
The two CX gates below uncompute the work qubit. The remaining logical operation is X:
from typing import no_type_check
@guppy
@no_type_check
def unitary_with_work_qubit(state: array[qubit, 1]) -> None:
work = qubit()
cx(state[0], work)
cx(state[0], work)
x(state[0])
discard(work)
unitary = get_unitary(
unitary_with_work_qubit,
n_qubits=1,
n_extra_qubits=1,
)
expected = np.array([[0, 1], [1, 0]])
assert np.allclose(unitary / unitary[0, 1], expected)
unitary
Post-select a state¶
Post-selection keeps the branch associated with chosen measurement outcomes. For an ancilla outcome \(b\), the remaining system state is
Its squared norm gives the outcome probability. When that probability is nonzero, normalization gives the conditional state:
Tag the registers with
state_outputand use those names in the selection dictionary.Falseselects \(0\);Trueselects \(1\).get_statevector_projectedreturns the state of the unprojected qubits.Use
renormalize=Truefor the conditional state, orFalseto preserve the branch weight.
This example selects \(|000\rangle\) on the ancilla register tagged "projection".
n_state_qubits = 2
n_ancilla_qubits = 3
n_total_qubits = n_state_qubits + n_ancilla_qubits
@guppy
def main_sv_project() -> None:
state = qarray(n_state_qubits)
ancilla = qarray(n_ancilla_qubits)
for i in range(n_state_qubits):
ry(state[i], angle(3.14/2))
for i in range(n_ancilla_qubits):
ry(ancilla[i], angle(3.14/2))
for i in range(n_ancilla_qubits):
for j in range(n_state_qubits):
cx(state[j], ancilla[i])
# projection qubits to be measured
state_output("projection", ancilla)
state_output("system", state)
discard_array(state)
discard_array(ancilla)
from guppyalgos.tests.helpers import get_statevector_projected
post_selection = {"projection": [False for _ in range(n_ancilla_qubits)]} # postselect on all ancillas being 0
get_statevector_projected(main_sv_project, n_total_qubits, post_selection, renormalize=True)
array([0.7556292 -5.07091231e-16j, 0.31129088+1.81104011e-17j,
0.31129088+5.43312033e-17j, 0.48499534+2.17324813e-16j])
Select outcomes on several registers¶
A selection dictionary can include several named registers. The helper applies their projections in dictionary order, allowing you to test a joint success condition, such as the ancilla and phase registers both being zero in a QSVT routine.
Record disjoint registers at the same point in the circuit, with no computation between their snapshots.
n_state_qubits = 2
n_phase_qubits = 3
n_total_qubits = n_state_qubits + n_phase_qubits + 1
state_preparation_angle = np.pi / 4
@guppy
def main() -> None:
phase_qreg = qarray(n_phase_qubits)
state_qreg = qarray(n_state_qubits)
ancilla_qreg = qarray(1)
# Non-eigenstate input in computational basis.
ry(state_qreg[0], angle(state_preparation_angle))
x(state_qreg[1])
for i in range(n_phase_qubits):
h(phase_qreg[i])
h(ancilla_qreg[0])
for i in range(n_phase_qubits):
cx(phase_qreg[i], ancilla_qreg[0])
for i in range(n_state_qubits):
cx(state_qreg[i], ancilla_qreg[0])
h(ancilla_qreg[0])
state_output("phase", phase_qreg)
state_output("ancilla", ancilla_qreg)
state_output("system", state_qreg)
discard_array(phase_qreg)
discard_array(state_qreg)
discard_array(ancilla_qreg)
Select \(|0\rangle\) on "ancilla" and \(|000\rangle\) on "phase". The returned statevector describes the remaining system register.
from guppyalgos.tests.helpers import get_statevector_projected, statevector_projected_selene
import numpy as np
postselect_dict = {"ancilla": [False], "phase": [False, False, False]}
get_statevector_projected(main, n_total_qubits, postselect_dict)
array([ 2.02524568e-17-2.17207347e-32j, 5.77861354e-17-7.47527359e-33j,
-8.70182863e-17-3.30747719e-01j, -3.04173840e-16-9.43719209e-01j])
Keep a state object for further inspection¶
Use statevector_projected_selene when you need a SeleneQuestState rather than a NumPy array. The returned_specified_qubits argument marks the qubit IDs of interest for subsequent projections or inspection.
statevector_projected_selene(main, n_total_qubits, postselect_dict, returned_specified_qubits=[3,4])
SubQuestState(state=array([ 2.02524568e-17-2.17207347e-32j, 5.77861354e-17-7.47527359e-33j,
-8.70182863e-17-3.30747719e-01j, -3.04173840e-16-9.43719209e-01j]), total_qubits=2, specified_qubits=[3, 4])
Extract a post-selected operation¶
Use get_unitary_projected to extract the operation associated with chosen initial and final ancilla states:
This block need not be unitary. Its scale matters: for a normalized input \(|\psi\rangle\), the probability of outcome \(b\) is \(\lVert A_{b,a}|\psi\rangle\rVert^2\).
Put projected registers first in the circuit signature, followed by the system register.
The selection dictionary’s key order determines the projected-register argument order.
The helper records those registers for post-selection.
Use matching keys in the pre-selection and post-selection dictionaries; omit pre-selection to initialize all projected registers to zero.
Below, the ancillas start in \(|111\rangle\) and are projected onto \(|000\rangle\).
@guppy
def main_unitary_projected[n_a: nat, n_s: nat](ancilla: array[qubit, n_a], state: array[qubit, n_s]) -> None:
for i in range(len(state)):
ry(state[i], angle(3.14/2))
for i in range(len(ancilla)):
ry(ancilla[i], angle(3.14/2))
for i in range(len(state)):
for j in range(len(ancilla)):
cx(state[i], ancilla[j])
from guppyalgos.tests.helpers import get_unitary_projected
post_selection = {"ancilla": [False for _ in range(n_ancilla_qubits)]}
pre_selection = {"ancilla": [True for _ in range(n_ancilla_qubits)]}
get_unitary_projected(main_unitary_projected, n_state_qubits, post_selection, pre_selection)
array([[ 6.93889390e-17+0.14887239j, 1.42420307e-17+0.11926928j,
1.42420307e-17+0.11926928j, 3.94574074e-17+0.09555271j],
[-1.80411242e-16-0.23194517j, 1.28749977e-16+0.28951489j,
-6.68895356e-17-0.18582312j, 1.39427393e-16+0.23194517j],
[-1.66533454e-16-0.23194517j, -6.68895356e-17-0.18582312j,
2.67527855e-16+0.28951489j, 1.53305181e-16+0.23194517j],
[-6.93889390e-18+0.09555271j, -7.30313681e-18-0.11926928j,
-7.30313681e-18-0.11926928j, -3.72795415e-17+0.14887239j]])
Allow for internal work qubits¶
Pass n_extra_qubits for work qubits allocated inside the circuit, beyond its explicit register arguments. The helper reserves that additional emulator capacity while extracting the logical operation.
As with get_unitary, internal work qubits must return to \(|0\rangle\) before being discarded. The example below leaves the projected register unchanged and applies X to the system.
from typing import no_type_check
from guppyalgos.tests.helpers import get_unitary_projected
@guppy
@no_type_check
def projected_unitary_with_work_qubit(
projection: array[qubit, 1],
state: array[qubit, 1],
) -> None:
work = qubit()
cx(state[0], work)
cx(state[0], work)
x(state[0])
discard(work)
projected = get_unitary_projected(
projected_unitary_with_work_qubit,
n_state_qubits=1,
post_select_dict={"projection": [False]},
n_extra_qubits=1,
)
expected = np.array([[0, 1], [1, 0]])
assert np.allclose(projected / projected[0, 1], expected)
projected
Inspect only selected registers¶
Use get_total_state_on_only_specified_registers to inspect a collection of tagged registers while excluding unused ancillas.
Identify the registers by their
state_outputtags.Record the snapshots together, with no computation between them.
The selected registers must be disentangled from everything excluded. They may be entangled with each other.
The helper returns a state object and a mapping from register tags to qubit IDs.
Here, the control and target registers contain six qubits altogether, so their state has \(2^6=64\) amplitudes. The separate ancilla is excluded.
from guppyalgos.utils import transversal
from selene_sim import Quest
from guppyalgos.tests.helpers import get_total_state_on_only_specified_registers
@guppy
def main_get_total_state_on_only_specified_registers() -> None:
control_reg = qarray(3)
target_reg = qarray(3)
ancilla = qubit()
x(control_reg[1])
transversal(cx, control_reg, target_reg)
# state results for desired registers
state_output("control", control_reg)
state_output("target", target_reg)
discard_array(control_reg)
discard_array(target_reg)
discard(ancilla)
res = main_get_total_state_on_only_specified_registers.emulator(7).run()
states = Quest.extract_states_dict(res.results[0].entries)
# returns the state and a dictionary mapping from the tags to the qubit ids for each register
non_ancilla_state, spec_qubit_dict = get_total_state_on_only_specified_registers(
states, ["target", "control"]
)
control_qubits = spec_qubit_dict["control"]
target_qubits = spec_qubit_dict["target"]
# length of non_ancilla_state will exclude the ancilla, so be 2**6=64,
print(len(non_ancilla_state.state))
64
Replay measurement outcomes¶
Selene’s QuantumReplay lets you specify measurement outcomes in advance. This makes tests of mid-circuit measurements, feedforward, and retries reproducible.
The example below uses rotation resource states until a success flag is measured. It checks two paths:
[True]: success on the first attempt.[False, False, True]: two failures followed by success.
After each failure, the correction angle doubles; attempt \(k\) uses \(2^{k-1}\theta\). Comparing the final rotation across paths checks that the corrections produce the intended result.
Requested outcomes must have non-negligible probability in the simulated state. Replay selects a possible branch rather than assigning an arbitrary quantum state.
from selene_sim import QuantumReplay
from guppylang.std.quantum import measure, h, rz, s, sdg
import numpy as np
@guppy
def repeat_until_success_ry_example() -> None:
"""Perform repeat-until-success Ry rotation."""
q = qubit()
sdg(q)
h(q)
theta = angle(0.1)
attempts = 0
while True:
attempts += 1
a = qubit()
h(a)
rz(a, theta * (2 ** (attempts - 1)))
cx(q, a)
if not measure(a).read():
continue
break
h(q)
s(q)
state_output("result_state",q)
discard(q)
# set up the replay plugin with desired measurements and Quest as a backend
n_shots = 2
rus_replay_sim = QuantumReplay(simulator=Quest(), measurements=[[True], [False, False, True]])
em_result = (
repeat_until_success_ry_example.emulator(2).with_simulator(rus_replay_sim).with_shots(n_shots).run()
)
# print the rotation angle applied in each case
for shot_result in em_result.results:
states = Quest.extract_states_dict(shot_result)
sv = states["result_state"].state
res = abs(sv[3]) ** 2
theta = 2 * np.arcsin(np.sqrt(res)) / np.pi
print(theta)
0.10000000000000005
0.10000000000000002