State Preparation and Pauli Readout
By default, the gate-level compiler reproduces a circuit’s whole unitary: its action on every input state. An experiment rarely needs that. It starts from one known state, and it ends by reading one quantity. The compiler can use both facts to return a shorter circuit.
A quantum computer usually has a single-shot readout. Each shot measures every qubit in the \(Z\) basis and gives one bit per qubit. The expectation value of any Pauli string follows from these bits. A star-topology device such as an NV center usually reads only its central qubit, and only as \(Z\) averaged over many shots. Any other Pauli string, such as \(XXX\) on three qubits, has to be turned into that one readout, which takes extra gates, entanglers among them. The Pauli readout does this: you append the string you want to the circuit, and the compiler compiles the circuit and the change of readout together, as one unitary.
You will see:
how to compile a circuit for one start state, and a state preparation from the state alone;
how to compile a Pauli readout, and what the device reads in the end;
how a reset compiles into the reset the device performs;
FixedCROTGateSetwithspam_qubits=(0, 1), which resets and reads two qubits at once.
The Gate Compilers tutorial introduces the configurations used here.
Setup
This notebook needs quasar and the plotting extra:
pip install --find-links=wheels/ ".[plot]"
%matplotlib inline
import quasar
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, random_statevector
State preparation
A circuit compiled as a whole unitary is right on every input state. An experiment usually starts from one known state, here \(\lvert 000\rangle\). A circuit that has to be right on that one state only is usually shorter. Two tasks use this:
You have only the state to prepare, and no circuit for it.
compile_state(target)compiles a circuit that prepares it.You have a circuit, and it always starts from the same state.
compile(circuit, initial_state=...)compiles what the circuit does to that state.
Both run the same compile. The second one computes the state the circuit produces from its start state, and prepares that state.
Preparing a given state
compile_state takes the state to prepare, here a random 3-qubit state, and returns a circuit
that prepares it from \(\lvert 000\rangle\):
gateset = quasar.CROTGateSet(n_qubits=3)
config = quasar.AdaptiveConfig(tolerance_error=1e-8, seed=1)
compiler = quasar.GateCompiler(gateset, config=config)
target = random_statevector(8, seed=5)
qc_prep = compiler.compile_state(target)
qc_prep.draw(output="mpl");
Its entangler count, and its error. state_preparation_error is one minus the fidelity of the
prepared state with the target, so the preparation is exact:
{"crot": qc_prep.count_ops()["crot"], "state preparation error": compiler.state_preparation_error(target, qc_prep)}
{'crot': 3, 'state preparation error': 1.0893308477477603e-10}
A circuit run from a known state
The GHZ circuit below starts from \(\lvert 000\rangle\), the basis state 0. We compile it once as a whole
unitary, and once with initial_state=0:
qc_ghz = QuantumCircuit(3)
qc_ghz.h(0)
qc_ghz.cx(0, 1)
qc_ghz.cx(1, 2)
qc_ghz_unitary = compiler.compile(qc_ghz)
qc_ghz_from_zero = compiler.compile(qc_ghz, initial_state=0)
{"whole unitary": qc_ghz_unitary.count_ops()["crot"], "from |000>": qc_ghz_from_zero.count_ops()["crot"]}
{'whole unitary': 4, 'from |000>': 2}
From \(\lvert 000\rangle\) the circuit needs half the entanglers. It is right on \(\lvert 000\rangle\) only, so
state_preparation_error compares the state it produces with the state the GHZ circuit produces.
The compiled circuit is exact:
compiler.state_preparation_error(Statevector(qc_ghz), qc_ghz_from_zero)
4.075517701096487e-11
Pauli readout
To measure the expectation value of a Pauli string, append it to the circuit with
append_pauli_readout.
The string is in Qiskit’s order: its last letter is qubit 0, so in "IXZ" \(Z\) acts on
qubit 0 (see the Conventions section of the Concepts page).
qc_readout = quasar.append_pauli_readout(qc_ghz, "XXX")
qc_readout.draw(output="mpl", cregbundle=False);
The device reads one fixed thing: \(Z\) on its readable qubit. The compiler turns the requested string into that readout and folds the change into the native gates. The compiled circuit ends with the readout the device performs:
qc_readout_compiled = compiler.compile(qc_readout)
qc_readout_compiled.draw(output="mpl", cregbundle=False);
The PPM box at the end is the readout. remove_pauli_readouts strips it off and returns it:
qc_native, readouts = quasar.remove_pauli_readouts(qc_readout_compiled)
{
"requested": qc_readout_compiled.metadata["pauli_readout"],
"the device reads": readouts,
"crot": qc_native.count_ops().get("crot", 0),
}
{'requested': ['XXX'], 'the device reads': ['IIZ'], 'crot': 4}
The requested \(XXX\) became \(Z\) on qubit 0, the one readout this device has. The entanglers
the compiled circuit holds implement the circuit and that change of readout together. A readout
is not unitary, so compile does not take initial_state for a circuit with a readout. It
raises ValidationError.
Reset
A circuit usually assumes that every qubit starts in \(\lvert 0\rangle\). On a star-topology device this is not
always true. Its reset may reach only some of its qubits, and set them together. The
other qubits then start in an unknown state. A gate set names the qubits the device resets
in spam_qubits. SPAM stands for state preparation and measurement: the device
resets and reads out the same qubits, and only those. The default, spam_qubits=(0,), is the
central qubit alone. With spam_qubits=(0, 1) the device resets qubits 0 and 1 together,
as one pair, and reads them out together (see Two-qubit readout below).
A circuit that needs a known start asks for it with reset. A qubit outside the pair gets
\(\lvert 0\rangle\) by a swap with a reset qubit, and the pair is reset again. Here the
whole register is reset, and the GHZ circuit from above follows:
qc_reset = QuantumCircuit(3)
qc_reset.reset([0, 1, 2])
qc_reset.compose(qc_ghz, inplace=True)
qc_reset.draw(output="mpl");
lower_resets shows the steps the device performs for it:
gateset_spam = quasar.FixedCROTGateSet(n_qubits=3, spam_qubits=(0, 1))
gateset_spam.lower_resets(qc_reset).draw(output="mpl");
The pair is reset, the three cx gates swap qubits 0 and 2, and the pair is reset again.
Barriers keep every gate on its side of a reset. These steps overwrite qubits 0 and 1, so a
reset of qubit 2 alone raises an error. compile performs this step itself, so a circuit with resets compiles directly:
compiler_spam = quasar.GateCompiler(gateset_spam, config=config)
qc_reset_compiled = compiler_spam.compile(qc_reset)
qc_reset_compiled.draw(output="mpl");
Each reset of the pair is followed by the native gates of its segment: five crotxpi2 swap
qubits 0 and 2, and four build the GHZ state.
Two-qubit readout
With spam_qubits=(0, 1), the device performs a population readout of qubits 0 and 1
together. Averaged over many runs, it gives the population of each of the pair’s four basis
states. These give \(Z\) on either qubit, and their product \(ZZ\):
gateset_spam.pauli_readouts_native()
('IIZ', 'IZI', 'IZZ')
A Bell pair on qubits 0 and 1, with \(ZZ\) read on the pair:
qc_bell = QuantumCircuit(3)
qc_bell.h(0)
qc_bell.cx(0, 1)
qc_zz = quasar.append_pauli_readout(qc_bell, "IZZ")
qc_zz.draw(output="mpl", cregbundle=False);
The circuit is compiled for two groups.
First the default spam_qubits=(0,), which reads qubit 0 alone:
gateset_fixed = quasar.FixedCROTGateSet(n_qubits=3)
compiler_fixed = quasar.GateCompiler(gateset_fixed, config=config)
qc_zz_fixed = compiler_fixed.compile(qc_zz)
qc_zz_fixed.draw(output="mpl", cregbundle=False);
Then spam_qubits=(0, 1):
With the default spam_qubits=(0,), the device reads only qubit 0. The compiler moves the
product \(ZZ\) onto that qubit, so the circuit ends with PPM(Z) and takes four crotxpi2. With
spam_qubits=(0, 1) the device reads the requested \(ZZ\) as it is, so the circuit ends with
PPM(ZZ) and takes three.
Two Pauli strings from one readout
One readout of the pair also answers two Pauli strings at once, when they commute.
append_pauli_readout takes both. Reading \(XX\) and \(ZZ\) of the Bell pair is a Bell measurement:
qc_bell_measurement = quasar.append_pauli_readout(qc_bell, ["IXX", "IZZ"])
qc_bell_measurement_compiled = compiler_spam.compile(qc_bell_measurement)
qc_bell_measurement_compiled.draw(output="mpl", cregbundle=False);
To read \(XX\) and \(ZZ\), the compiler turns them into \(Z\) on qubit 0 and \(Z\) on qubit 1. The
change of readout that does this is the Bell circuit run backwards, so the two cancel up to
single-qubit gates, and the compiled circuit holds no entangler. This is a special case: the
circuit here is exactly the inverse of the change of readout, and after another circuit the change
costs entanglers. The two PPM(Z) boxes deliver \(XX\) on qubit 0 and \(ZZ\) on qubit 1. The same
readout also reads the product of the two. metadata names what each string the readout
delivers equals: the two native strings, and their product with its sign:
qc_bell_measurement_compiled.metadata["pauli_readout_products"]
{'IIZ': 'IXX', 'IZI': 'IZZ', 'IZZ': '-IYY'}
The product of \(XX\) and \(ZZ\) is \(-YY\), so the pair’s own \(ZZ\) value is \(-\langle YY\rangle\). Any two commuting, independent strings can be read together in this way, and their product comes along.
Where to go next
Pulse Compilers tutorial — lowering a compiled circuit to hardware pulses.
Qubit Layout tutorial — letting the compiler choose which qubit sits where.
The API reference for the full surface.