# Multistate discrimination

 This tutorial is applicable to all SHFQA Instruments.

## Goals and Requirements

The goal of this tutorial is to demonstrate how to use SHFQA to perform multistate discrimination using Zurich Instruments Toolkit API.

 Users can download all zhinst-toolkit example files introduced in this tutorial from GitHub, https://github.com/zhinst/zhinst-toolkit/tree/main/examples.

## Preparation

The tutorial starts with the instrument in the default configuration (e.g., after a power cycle). For an optimal tutorial experience, please follow these preparation steps:

• ensure that the version of the LabOne Python API, LabOne and the Firmware of the SHFQA device, zhinst-toolkit (pip install zhinst) and Python (3.7 or newer) are updated and compatible,

• make sure that the Instrument is powered on and connected by Ethernet to your local area network (LAN) where the host computer resides or by USB (Maintenance port) to the host computer,

• start LabOne and open the LabOne Graphical User Interface using the default web browser,

• connect the SHFQA channel 1 output (input) to the readout input (output) line, see SHFQA connection.

Figure 1. SHFQA connection.

## Tutorial

The tutorial uses simulated qudit data as readout signal to explain how to measure integration weights and how to discriminate the qudits.

1. Connect the Instrument

Create a toolkit session to the data server and connect the Instrument with the device ID, e.g. 'DEV12001', see Connecting to the Instrument.

# load the LabOne API and other necessary packages
from zhinst.toolkit import Session, SHFQAChannelMode, Waveforms
from zhinst.utils.shfqa.multistate import QuditSettings
import numpy as np
import matplotlib.pyplot as plt
import textwrap

DEVICE_ID = 'DEV12036'
SERVER_HOST = '10.42.11.5'

session = Session(SERVER_HOST)              # connect to data server
device = session.connect_device(DEVICE_ID)  # connect to device
SHFQA_SAMPLING_FREQUENCY = 2.0e9            # in units of Hz
2. Configure the Channel

Configure the Channel such that the center frequency is 5 GHz, the output range is -5 dBm, the input range is 0 dBm, and the channel mode is the Readout mode. Both input and output of the Channel are turned on.

CHANNEL_INDEX = 0 # physical Channel 1

device.qachannels[CHANNEL_INDEX].configure_channel(
center_frequency=5e9, # in units of Hz
input_range=0,        # in units of dBm
output_range=-5,      # in units of dBm
)
device.qachannels[CHANNEL_INDEX].input.on(1)  # turn on Channel Input
device.qachannels[CHANNEL_INDEX].output.on(1) # turn on Channel Output

To simulate different qudit states, readout waveforms are generated from simulated readout envelopes, see R. Blanchetti, PRL 105, 223601 (2010). The way to generate and upload readout waveforms is the same no matter whether using multistate discrimination mode or not.

For each qudit state, a separate simulated waveform needs to be uploaded. Thus, the number of qudits that can be measured in a single channel is restricted by the maximum number of waveform memory slots, 8 or 16 per channel. 4 qudits (0: qutrit, 1: ququad, 2: qutrit, 3: qubit) with in total 12 states will be measured if the Instrument is SHFQA2 or SHFQC with 16W option or SHFQA4, 2 qudits (0: qutrit, 1: ququad) with in total 7 states will be measured if the instrument is SHFQA2 or SHFQC without 16W option. The simulated readout envelopes are loaded and used according to this qudits setting, the .csv file can be found in GitHub. With the number of qudits, qudit type and offset frequency setting, the readout waveforms are generated and uploaded to the waveform memory (12 or 7 waveform memory slots are used), shown in Figure 2. The plotting function can be found in the GitHub.

In real measurement, number of qudit readout waveforms required to be generated and uploaded is same as the number of qudits.

# dictionary mapping the qudit index to the number of states
if device.max_qubits_per_channel >= 16:
QUDITS_NUM_STATES = {0: 3, 1: 4, 2: 3, 3: 2}
else:
QUDITS_NUM_STATES = {0: 3, 1: 4}

# Note: The total number of states is restricted limited by the total number of waveform generator units.
total_num_states = sum(QUDITS_NUM_STATES.values())
assert total_num_states <= device.max_qubits_per_channel, (
"Cannot upload all simulated waveforms as the total number of states, "
f"summed over all qudits, amounts to {total_num_states}, "
f"which is more than the number of {device.max_qubits_per_channel} "
"generator waveforms on the device."
)

# load simulated reference traces (envelope only)

# check that enough simulated traces are available to cover all states
assert len(signals_simulated) >= max(QUDITS_NUM_STATES.values())

# Note: The number of samples will also be used for the scope measurement
num_samples = signals_simulated.shape[1]

# generate readout signal of all qudits
signals_time = np.linspace(0, num_samples / SHFQA_SAMPLING_FREQUENCY, num_samples) # time axis
QUDITS_FREQUENCIES = {0: -10e6, 1: -5e6, 2: 0e6, 3: 5e6, 4: 10e6} # readout offset frequency
qudits_signals = {}

for qudit_idx, num_states in QUDITS_NUM_STATES.items():
states_signals = []
for signal_idx, signal in enumerate(signals_simulated[:num_states]):
states_signals.append(
signal
* np.exp(2j * np.pi * QUDITS_FREQUENCIES[qudit_idx] * signals_time)
/ len(QUDITS_NUM_STATES) # this has to be scaled down by dividing the number of qudits
)
qudits_signals[qudit_idx] = states_signals

# convert the qudit signal to the waveform which can be uploaded to the memory
WAVEFORM_IDX_MAPPING = {}
simulated_waveforms = Waveforms()

waveform_idx = 0
for qudit_idx, states_signals in qudits_signals.items():
for state_idx, signal in enumerate(states_signals):
simulated_waveforms.assign_waveform(slot=waveform_idx, wave1=signal)
WAVEFORM_IDX_MAPPING[(qudit_idx, state_idx)] = waveform_idx
waveform_idx += 1

# upload the waveforms to the device
device.qachannels[CHANNEL_INDEX].generator.clearwave()
device.qachannels[CHANNEL_INDEX].generator.write_to_waveform_memory(simulated_waveforms)
Figure 2. Simulated readout waveforms of 4 qudits. Blue line: state |0>. Orange line: state |1>. Green line: state |2>. Red line: state |3>.
4. Measure integration weight

Integration weights of each qudit can be calculated by taking the difference of any 2 reference traces of each qudit, i.e. $$n$$ reference traces and $$n(n-1)/2$$ integration weights for a qudit with $$n$$ states. The reference trace means the readout signal acquired by the Instrument when a qudit prepared in one of the states to be discriminated, for example state |1>, and the rest qudits remain in ground states.

To measure reference traces, all simulated readout pulses will be sent out sequentially with the Generator and recorded by the Scope. The SeqC code in the Generator is written such that each state of a qudit is sequentially measured in a for loop, and 4 qudits are therefore measured with 4 subsequent for loops, then the measurement is repeated 2000 times. The Scope records each state with 1 segment and averages it by 2000 times. The data downloaded after the averaging is then reshaped to build qudit reference traces, see Figure 3.

In real measurement, only one generator mask (the first argument of startQA) is needed for one qudit therefore using gen_mask = (1 << {qudit_idx}) in the most inner for loop instead. The setting of integration mask does not matter in this step because the interested signals are the frequency down-converted signal before integration.

def simulated_qudit_seqc_program(
qudits_num_states,
wvfm_idx_mapping,
num_repetitions,
cycle_time=4e-6,
):

seqc_program = textwrap.dedent(
f"""
const PLAY_ZERO_CYCLES = {cycle_time} * DEVICE_SAMPLE_RATE;
info("PLAY_ZERO_CYCLES: %d", PLAY_ZERO_CYCLES);
// repeate the measurement
repeat({num_repetitions}) {{
"""
)

# generate maks to enable the integration of all qudits in the dictionary
for qudit_idx in qudits_num_states.keys():

# generate n (n is the number of qudits) for loops sequentially
# in each for loop, specific simulated qudit readout signal is used for each state of a qudit
for qudit_idx, num_states in qudits_num_states.items():
first_wave_idx = wvfm_idx_mapping[(qudit_idx, 0)] # index of the first state of the qudit

seqc_program += textwrap.indent(
textwrap.dedent(
f"""
// generate and measure refrence traces for qudit {qudit_idx}
for(cvar i = 0; i < {num_states}; i++) {{
// mask to enable the playback of the simulated trace
// for a specific qudit state
cvar gen_mask = (1 << ({first_wave_idx} + i));
// cvar gen_mask = (1 << {qudit_idx}); // for real measurement

// wait for the next repetition period
playZero(PLAY_ZERO_CYCLES);

// play back different waveforms based on the bit mask
// and measure the qudit
}}
"""
),
"    ",
)

seqc_program += textwrap.dedent(
"""
} // end of repeat({num_repetitions})
"""
)
return seqc_program

# generate and upload the sequence
NUM_REPETITONS = 2000
seqc_program = simulated_qudit_seqc_program(
QUDITS_NUM_STATES, WAVEFORM_IDX_MAPPING, num_repetitions=NUM_REPETITONS
)

# configure the scope
SCOPE_IDX = 0  # only one scope on the device
SCOPE_CHANNEL = 0 # from 0 to 3, 4 in total
SCOPE_TRIGGER_CHANNEL = f"chan{CHANNEL_INDEX}seqmon0" # the scope will be triggered by the sequence monitor trigger
SCOPE_TRIGGER_DELAY = 200e-9 # start recording 200 ns later after receiving a trigger

device.scopes[SCOPE_IDX].configure(
input_select={SCOPE_CHANNEL: f"channel{CHANNEL_INDEX}_signal_input"},
num_samples=num_samples,
trigger_input=SCOPE_TRIGGER_CHANNEL,
num_segments=total_num_states,
num_averages=NUM_REPETITONS,
trigger_delay=SCOPE_TRIGGER_DELAY,
)

# arm the scope
device.scopes[SCOPE_CHANNEL].run(single=True)

# set the integration delay equals to the scope trigger delay,
# so the recorded data can be used for state discrimination directly.

# run the sequencer
device.qachannels[CHANNEL_INDEX].generator.enable_sequencer(single=True)

# get the scope results and reshape it
scope_data_segments = np.reshape(
scope_data[SCOPE_CHANNEL], [total_num_states, num_samples]
)

# build list of reference traces for each qudit
qudits_ref_traces = {}
for qudit_idx, num_states in QUDITS_NUM_STATES.items():
ref_traces = []
for state_idx in range(num_states):
ref_traces.append(
scope_data_segments[WAVEFORM_IDX_MAPPING[(qudit_idx, state_idx)]]
)

qudits_ref_traces[qudit_idx] = ref_traces
Figure 3. Reference traces of 4 qudits. Blue line: qudit in state |0> and the rest qudits are in state |0>. Orange line: qudit in state |1> and the rest qudits are in state |0>. Green line: qudit in state |2> and the rest qudits are in state |0>. Red line: qudit in state |3> and the rest qudits are state in |0>.

All integration weights calculated by the utility function QuditSettings with the reference traces are shown in Figure 4.

all_qudit_settings = { }
for qudit_idx, ref_traces in qudits_ref_traces.items():
all_qudit_settings[qudit_idx] = QuditSettings(ref_traces)
Figure 4. Integration weights of 4 qudits. For a qudit with $$n$$ states, the first $$n-1$$ integration weights are calculated from reference trace difference of state |0> and |$$i$$>, $$i$$ is an integer from 1 to $$n-1$$). For $$n > 2$$, the additional integration weights are calculated from reference trace difference of state |$$j$$> and |$$k$$>, $$j$$ and $$k$$ are integers, $$j$$ is from 1 to $$n - 1$$, $$k$$ is from 2 to $$n-1$$, and $$j < k$$.
5. Discriminate qudit state

To discriminate qudit state, thresholds and assignment matrix are required. Thresholds are used to discriminate the results after integration and get 0 or 1, and assignment matrix are used to convert the result after thresholding to the correct digital representation of qudit state are required. These are calculated by the utility function QuditSettings too, therefore no additional measurement and manual calculation are needed. Figure 5 shows the histogram of qudits at each state and the thresholds returned from QuditSettings. The script to measure and plot the histogram can be found on GitHub and Online Documentation.

Figure 5. Histogram of qudits at different states and thresholds. Blue: qudit in state |0>. Orange: qudit in state |1>. Green: qudit in state |2>. Red: qudit in state |3>. For qudit with $$n$$ states, Threshold $$i$$ is the threshold between state |0> and |$$i+1$$> if $$i < n-1$$ or between state |$$j$$> and |$$k$$> if $$i > = n - 1$$, where $$i,\ j$$ and $$k$$ are integers. $$i$$ is from 0 to $$n(n-1)/2-1$$, $$j$$ is from 1 to $$n - 2$$, $$k$$ is from 2 to $$n-1$$, and $$j < k$$.

The default state discrimination mode is 2-state discrimination, so the multistate readout mode has to be enabled for this measurement. Before upload new qudit settings, all qudits should be disabled to avoid mixing up of the new and old settings. The readout result source has to be result_of_discrimination to get digital results representing qudit states. To show how measurement result looks like after state discrimination, the same sequence program used for integration weight measurement, and the simulated qudit readout waveforms are reused. The Instrument sequentially sends out simulated readout signal according to each state of all qudits, and integrates the down-converted signal with $$n-1$$ weights and gets $$n(n-1)/2$$ integrated results, where $$n$$ is the number of qudit states. In the sequence, 4 integrator masks are used for all qudits for simplicity, and only first $$n-1$$ integrators are used directly for integration. The $$n(n-1)/2$$ results are from the direct integration with $$n-1$$ integrators and $$(n-1)(n-2)/2$$ differences of the integration results. They are discriminated by $$n(n-1)/2$$ thresholds, and then converted by the assignment matrix to 2-bit data representing the qudit state. The measurement is repeated 2000 times and the first 24 results of all qudits are shown in Figure 6.

Based on the measured results and expected readout states, the fidelity matrix can be calculated. All calculation and plotting functions are detailed on the Online Documentation page and the GitHub page.

# enable the multistate discrimination

# disable all qudits before configure them

# configure the new qudit settings on the device
for qudit_idx, qudit_settings in all_qudit_settings.items():
qudit_settings
)

result_length = NUM_REPETITONS * total_num_states

# configure the result logger
result_length=result_length, result_source="result_of_discrimination"
)

# arm the result logger

# run the sequencer
device.qachannels[CHANNEL_INDEX].generator.enable_sequencer(single=True)

qudits_results = device.qachannels[
CHANNEL_INDEX
].readout.multistate.get_qudits_results()
Figure 6. Multistate discrimination of qudits. Only 24 out of 2000 data are shown in the plot.

In real measurement, multistate discrimination of multiple qudits can be done in parallel by running a new SeqC program shown below before the above script. In the sequence, output readout waveform is generated by adding up 4 readout waveforms according to 4 qudits, and 12 integrators according to 12 states in total are used. The result after discrimination can be sent to a control instrument for feedback experiment via DIO or via ZSync through a PQSC, e.g. active reset.

seqc_program = textwrap.dedent(
f"""
const PLAY_ZERO_CYCLES = {cycle_time} * DEVICE_SAMPLE_RATE;
info("PLAY_ZERO_CYCLES: %d", PLAY_ZERO_CYCLES);
// repeate the measurement
repeat({num_repetitions}) {{
// wait for the next repetition period
playZero(PLAY_ZERO_CYCLES);

// play back readout waveform of 4 qudits
// and measure the qudits
startQA(QA_GEN_0 | QA_GEN_1 | QA_GEN_2 | QA_GEN_3, QA_INT_ALL, true,  0, 0x0);
}}
"""
)
device.qachannels[CHANNEL_INDEX].generator.load_sequencer_program(seqc_program)