diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..2a5b54bc Binary files /dev/null and b/.DS_Store differ diff --git a/active_reset_full/IQ_blobs.py b/active_reset_full/IQ_blobs.py new file mode 100644 index 00000000..8d97749f --- /dev/null +++ b/active_reset_full/IQ_blobs.py @@ -0,0 +1,152 @@ +""" +IQ_blobs.py: template for performing a single shot discrimination and active reset +""" +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager +from qm import SimulationConfig, LoopbackInterface +from configuration import * +from macros import single_measurement, reset_qubit +from qualang_tools.analysis import two_state_discriminator, DiscriminatorDataclass + +from joe_testing.example_reset_comparison import generate_discrimination_data + +############################## +# Program-specific variables # +############################## +n_shot = 100 # Number of acquired shots +cooldown_time = 16 #0.0005 * qubit_T1 // 4 # Cooldown time in clock cycles (4ns) + +################### +# The QUA program # +################### + +qubits = [1, 5, 34] + +def generate_reset_program(reset_function_name, reset_function_settings): + + with program() as measure_iq_blobs: + n = declare(int) # Averaging index + Ig_st = [declare_stream() for _ in range(len(qubits))] + Qg_st = [declare_stream() for _ in range(len(qubits))] + Ie_st = [declare_stream() for _ in range(len(qubits))] + Qe_st = [declare_stream() for _ in range(len(qubits))] + + for i, qubit in enumerate(qubits): + + with for_(n, 0, n < n_shot, n + 1): + + # Reset qubit state + + reset_qubit("cooldown_time", cooldown_time=cooldown_time) + # Measure the ground state + align("qubit", "resonator") + I_g, Q_g = single_measurement() + # Reset qubit state + reset_qubit(reset_function_settings.get('macro'), **reset_function_settings.get('settings')) + # Excited state measurement + align("qubit", "resonator") + play("pi", "qubit") + # Measure the excited state + I_e, Q_e = single_measurement() + # Save data to the stream processing + save(I_g, Ig_st[i]) + save(Q_g, Qg_st[i]) + save(I_e, Ie_st[i]) + save(Q_e, Qe_st[i]) + + with stream_processing(): + Ig_st[i].with_timestamps().save_all(f"Ig_{i}") + Qg_st[i].with_timestamps().save_all(f"Qg_{i}") + Ie_st[i].with_timestamps().save_all(f"Ie_{i}") + Qe_st[i].with_timestamps().save_all(f"Qe_{i}") + + return measure_iq_blobs + + +# this works for one qubit only +def run_and_format(reset_program, qmm, simulation=True): + if simulation: + return generate_discrimination_data() + + simulation_config = SimulationConfig( + duration=400000, simulation_interface=LoopbackInterface([("con1", 3, "con1", 1)]) + ) + job = qmm.simulate(config, reset_program, simulation_config) + + results_dataclass_list = [] + + + for i, qubit in enumerate(qubits): + results = fetching_tool(job, data_list=[f"Ie_{i}", f"Qe_{i}", f"Ig_{i}", f"Qg_{i}"], mode="wait_for_all") + # Fetch results + + all_data = results.fetch_all() + + + + I_g, Q_g, I_e, Q_e = [data['value'] for data in all_data] + timestamps = [data['timestamp'] for data in all_data][0] + + runtime = (timestamps[-1] - timestamps[0]) / n_shot + + # Plot data + angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(I_g, Q_g, I_e, Q_e, b_print=False, + b_plot=False) + + results_dataclass = DiscriminatorDataclass(f'Qubit {qubit}', angle, threshold, fidelity, gg, ge, eg, ee, I_g, Q_g, I_e, Q_e) + results_dataclass._add_attribute('runtime', runtime) + results_dataclass_list.append(results_dataclass) + + return results_dataclass_list + + # return generate_discrimination_data() + + else: + qm = qmm.open_qm(config) + job = qm.execute(reset_program) + # Get results from QUA program + results_dataclass_list = [] + for i, qubit in enumerate(qubits): + + results = fetching_tool(job, data_list=[f"Ie_{i}", f"Qe_{i}", f"Ig_{i}", f"Qg_{i}"], mode="wait_for_all") + # Fetch results + I_e, Q_e, I_g, Q_g = results.fetch_all() + # Plot data + angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(I_g, Q_g, I_e, Q_e, b_print=True, b_plot=True) + + results_dataclass = DiscriminatorDataclass(angle, threshold, fidelity, gg, ge, eg, ee, I_g, Q_g, I_e, Q_e) + results_dataclass_list.append(results_dataclass) + + return results_dataclass_list + + + + # If the readout fidelity is satisfactory enough, then the angle and threshold can be updated in the config file. + + + + +# if __name__ == '__main__': +# ##################################### +# # Open Communication with the QOP # +# ##################################### +# qmm = QuantumMachinesManager(qop_ip) +# measure_iq_blobs = generate_reset_program() +# +# simulation = False +# if simulation: +# simulation_config = SimulationConfig( +# duration=28000, simulation_interface=LoopbackInterface([("con1", 3, "con1", 1)]) +# ) +# job = qmm.simulate(config, measure_iq_blobs, simulation_config) +# job.get_simulated_samples().con1.plot() +# else: +# qm = qmm.open_qm(config) +# job = qm.execute(measure_iq_blobs) +# # Get results from QUA program +# results = fetching_tool(job, data_list=["Ie", "Qe", "Ig", "Qg"], mode="wait_for_all") +# # Fetch results +# I_e, Q_e, I_g, Q_g = results.fetch_all() +# # Plot data +# angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(I_g, Q_g, I_e, Q_e, b_print=True, b_plot=True) +# # If the readout fidelity is satisfactory enough, then the angle and threshold can be updated in the config file. diff --git a/active_reset_full/README.md b/active_reset_full/README.md new file mode 100644 index 00000000..2b08d874 --- /dev/null +++ b/active_reset_full/README.md @@ -0,0 +1,3 @@ +# Compare reset + +A page on how to use the reset GUI. \ No newline at end of file diff --git a/active_reset_full/compare_reset.py b/active_reset_full/compare_reset.py new file mode 100644 index 00000000..118a091b --- /dev/null +++ b/active_reset_full/compare_reset.py @@ -0,0 +1,84 @@ +""" +Jobs for this file + +1. Connect to a QM +2. run a reset program +3. format the data using the data handler +4. put the data into the gui +5. run the gui + +""" + +from qm.QuantumMachinesManager import QuantumMachinesManager +from configuration import * + +from IQ_blobs import generate_reset_program, run_and_format +from qualang_tools.plot import launch_reset_gui +from qm.simulate.credentials import create_credentials + + +def compare_reset(reset_dictionary): + """ + Compares the results of multiple reset paradigms with a GUI to visualise the differences. + + @param reset_dictionary: A dictionary with format: + + { + [name of cooldown method (str)]: {'macro': [name of macro function from macros (str)] , + 'settings': [dictionary of keyword arguments for the macro function (dict)]}, + } + + eg: + { + 'Cooldown': {'macro': 'cooldown', 'settings': {'cooldown_time': 8}}, + 'Active threshold 1': {'macro': 'active', 'settings': {'threshold': -0.003, 'max_tries': 3}}, + 'Active threshold 2': {'macro': 'active', 'settings': {'threshold': -0.005, 'max_tries': 5}} + } + + + @return: results dictionary of format {name of cooldown method: list of result_dataclass objects with the results + of the two-state discrimination output for each qubit} + """ + + ##################################### + # Open Communication with the QOP # + ##################################### + # qmm = QuantumMachinesManager(qop_ip) + + # connecting with Theo's credentials + qmm = QuantumMachinesManager( + host="theo-4c195fa0.dev.quantum-machines.co", + port=443, + credentials=create_credentials()) + + results_dict = {} + + for reset_function_name, reset_function_settings in reset_dictionary.items(): + + reset_program = generate_reset_program(reset_function_name, reset_function_settings) + results_dataclass = run_and_format(reset_program, qmm, simulation=True) + + results_dict[reset_function_name] = results_dataclass + + launch_reset_gui(results_dict) + return results_dict + + +if __name__ == '__main__': + reset_dictionary = { + 'Cooldown': {'macro': 'cooldown', 'settings': {'cooldown_time': 8}}, + 'Active threshold 1': {'macro': 'active', 'settings': {'threshold': -0.003, 'max_tries': 3}}, + 'Active threshold 2': {'macro': 'active', 'settings': {'threshold': -0.005, 'max_tries': 5}} + } + + results_dictionary = compare_reset(reset_dictionary) + + + + + + + + + + diff --git a/active_reset_full/configuration.py b/active_reset_full/configuration.py new file mode 100644 index 00000000..139960d9 --- /dev/null +++ b/active_reset_full/configuration.py @@ -0,0 +1,410 @@ +import numpy as np +from scipy.signal.windows import gaussian +from qualang_tools.config.waveform_tools import drag_gaussian_pulse_waveforms +from qualang_tools.units import unit +from qualang_tools.plot import interrupt_on_close +from qualang_tools.results import progress_counter, fetching_tool + + +####################### +# AUXILIARY FUNCTIONS # +####################### + +# IQ imbalance matrix +def IQ_imbalance(g, phi): + """ + Creates the correction matrix for the mixer imbalance caused by the gain and phase imbalances, more information can + be seen here: + https://docs.qualang.io/libs/examples/mixer-calibration/#non-ideal-mixer + :param g: relative gain imbalance between the I & Q ports. (unit-less), set to 0 for no gain imbalance. + :param phi: relative phase imbalance between the I & Q ports (radians), set to 0 for no phase imbalance. + """ + c = np.cos(phi) + s = np.sin(phi) + N = 1 / ((1 - g**2) * (2 * c**2 - 1)) + return [float(N * x) for x in [(1 - g) * c, (1 + g) * s, (1 - g) * s, (1 + g) * c]] + + +############# +# VARIABLES # +############# +u = unit() + +qop_ip = "127.0.0.1" + +# Qubits +qubit_LO = 7.4 * u.GHz # Used only for mixer correction and frequency rescaling for plots or computation +qubit_IF = 110 * u.MHz +mixer_qubit_g = 0.0 +mixer_qubit_phi = 0.0 + +qubit_T1 = int(10 * u.us) + +const_len = 100 +const_amp = 50 * u.mV + +pi_len = 100 +pi_amp = 0.05 + +drag_coef = 0 +anharmonicity = -200 * u.MHz +AC_stark_detuning = 0 * u.MHz + +gauss_len = 200 +gauss_sigma = gauss_len / 5 +gauss_amp = 0.25 +gauss_wf = gauss_amp * gaussian(gauss_len, gauss_sigma) + +x180_len = 40 +x180_sigma = x180_len / 5 +x180_amp = 0.35 +x180_wf, x180_der_wf = np.array( + drag_gaussian_pulse_waveforms(x180_amp, x180_len, x180_sigma, drag_coef, anharmonicity, AC_stark_detuning) +) +x180_I_wf = x180_wf +x180_Q_wf = x180_der_wf +# No DRAG when alpha=0, it's just a gaussian. + +x90_len = x180_len +x90_sigma = x90_len / 5 +x90_amp = x180_amp / 2 +x90_wf, x90_der_wf = np.array( + drag_gaussian_pulse_waveforms(x90_amp, x90_len, x90_sigma, drag_coef, anharmonicity, AC_stark_detuning) +) +x90_I_wf = x90_wf +x90_Q_wf = x90_der_wf +# No DRAG when alpha=0, it's just a gaussian. + +minus_x90_len = x180_len +minus_x90_sigma = minus_x90_len / 5 +minus_x90_amp = -x90_amp +minus_x90_wf, minus_x90_der_wf = np.array( + drag_gaussian_pulse_waveforms( + minus_x90_amp, + minus_x90_len, + minus_x90_sigma, + drag_coef, + anharmonicity, + AC_stark_detuning, + ) +) +minus_x90_I_wf = minus_x90_wf +minus_x90_Q_wf = minus_x90_der_wf +# No DRAG when alpha=0, it's just a gaussian. + +y180_len = x180_len +y180_sigma = y180_len / 5 +y180_amp = x180_amp +y180_wf, y180_der_wf = np.array( + drag_gaussian_pulse_waveforms(y180_amp, y180_len, y180_sigma, drag_coef, anharmonicity, AC_stark_detuning) +) +y180_I_wf = (-1) * y180_der_wf +y180_Q_wf = y180_wf +# No DRAG when alpha=0, it's just a gaussian. + +y90_len = x180_len +y90_sigma = y90_len / 5 +y90_amp = y180_amp / 2 +y90_wf, y90_der_wf = np.array( + drag_gaussian_pulse_waveforms(y90_amp, y90_len, y90_sigma, drag_coef, anharmonicity, AC_stark_detuning) +) +y90_I_wf = (-1) * y90_der_wf +y90_Q_wf = y90_wf +# No DRAG when alpha=0, it's just a gaussian. + +minus_y90_len = y180_len +minus_y90_sigma = minus_y90_len / 5 +minus_y90_amp = -y90_amp +minus_y90_wf, minus_y90_der_wf = np.array( + drag_gaussian_pulse_waveforms( + minus_y90_amp, + minus_y90_len, + minus_y90_sigma, + drag_coef, + anharmonicity, + AC_stark_detuning, + ) +) +minus_y90_I_wf = (-1) * minus_y90_der_wf +minus_y90_Q_wf = minus_y90_wf +# No DRAG when alpha=0, it's just a gaussian. + +# Resonator +resonator_LO = 4.8 * u.GHz # Used only for mixer correction and frequency rescaling for plots or computation +resonator_IF = 60 * u.MHz +mixer_resonator_g = 0.0 +mixer_resonator_phi = 0.0 + +readout_len = 200 +readout_amp = 0.25 + +time_of_flight = 24 + +# Flux line +const_flux_len = 200 +const_flux_amp = 0.45 + +# IQ Plane Angle +rotation_angle = (0 / 180) * np.pi +# Threshold for single shot g-e discrimination +ge_threshold = 0.0 + +config = { + "version": 1, + "controllers": { + "con1": { + "analog_outputs": { + 1: {"offset": 0.0}, # I qubit + 2: {"offset": 0.0}, # Q qubit + 3: {"offset": 0.0}, # I resonator + 4: {"offset": 0.0}, # Q resonator + 5: {"offset": 0.0}, # flux line + }, + "digital_outputs": { + 1: {}, + }, + "analog_inputs": { + 1: {"offset": 0.0, "gain_db": 0}, # I from down-conversion + 2: {"offset": 0.0, "gain_db": 0}, # Q from down-conversion + }, + }, + }, + "elements": { + "qubit": { + "mixInputs": { + "I": ("con1", 1), + "Q": ("con1", 2), + "lo_frequency": qubit_LO, + "mixer": "mixer_qubit", + }, + "intermediate_frequency": qubit_IF, + "operations": { + "cw": "const_pulse", + "gauss": "gaussian_pulse", + "pi": "pi_pulse", + "pi_half": "pi_half_pulse", + "x180": "x180_pulse", + "x90": "x90_pulse", + "-x90": "-x90_pulse", + "y90": "y90_pulse", + "y180": "y180_pulse", + "-y90": "-y90_pulse", + }, + }, + "resonator": { + "mixInputs": { + "I": ("con1", 3), + "Q": ("con1", 4), + "lo_frequency": resonator_LO, + "mixer": "mixer_resonator", + }, + "intermediate_frequency": resonator_IF, + "operations": { + "cw": "const_pulse", + "readout": "readout_pulse", + }, + "outputs": { + "out1": ("con1", 1), + "out2": ("con1", 2), + }, + "time_of_flight": time_of_flight, + "smearing": 0, + }, + "flux_line": { + "singleInput": { + "port": ("con1", 5), + }, + "operations": { + "const": "const_flux_pulse", + }, + }, + "flux_line_sticky": { + "singleInput": { + "port": ("con1", 5), + }, + "hold_offset": {"duration": 1}, # in clock cycles (4ns) + "operations": { + "const": "const_flux_pulse", + }, + }, + }, + "pulses": { + "const_single_pulse": { + "operation": "control", + "length": const_len, + "waveforms": { + "single": "const_wf", + }, + }, + "const_flux_pulse": { + "operation": "control", + "length": const_flux_len, + "waveforms": { + "single": "const_flux_wf", + }, + }, + "const_pulse": { + "operation": "control", + "length": const_len, + "waveforms": { + "I": "const_wf", + "Q": "zero_wf", + }, + }, + "pi_pulse": { + "operation": "control", + "length": pi_len, + "waveforms": { + "I": "pi_wf", + "Q": "zero_wf", + }, + }, + "pi_half_pulse": { + "operation": "control", + "length": pi_len, + "waveforms": { + "I": "pi_half_wf", + "Q": "zero_wf", + }, + }, + "x90_pulse": { + "operation": "control", + "length": x90_len, + "waveforms": { + "I": "x90_wf", + "Q": "x90_der_wf", + }, + }, + "x180_pulse": { + "operation": "control", + "length": x180_len, + "waveforms": { + "I": "x180_wf", + "Q": "x180_der_wf", + }, + }, + "-x90_pulse": { + "operation": "control", + "length": minus_x90_len, + "waveforms": { + "I": "minus_x90_wf", + "Q": "minus_x90_der_wf", + }, + }, + "y90_pulse": { + "operation": "control", + "length": y90_len, + "waveforms": { + "I": "y90_der_wf", + "Q": "y90_wf", + }, + }, + "y180_pulse": { + "operation": "control", + "length": y180_len, + "waveforms": { + "I": "y180_der_wf", + "Q": "y180_wf", + }, + }, + "-y90_pulse": { + "operation": "control", + "length": minus_y90_len, + "waveforms": { + "I": "minus_y90_der_wf", + "Q": "minus_y90_wf", + }, + }, + "gaussian_pulse": { + "operation": "control", + "length": gauss_len, + "waveforms": { + "I": "gauss_wf", + "Q": "zero_wf", + }, + }, + "readout_pulse": { + "operation": "measurement", + "length": readout_len, + "waveforms": { + "I": "readout_wf", + "Q": "zero_wf", + }, + "integration_weights": { + "cos": "cosine_weights", + "sin": "sine_weights", + "minus_sin": "minus_sine_weights", + "rotated_cos": "rotated_cosine_weights", + "rotated_sin": "rotated_sine_weights", + "rotated_minus_sin": "rotated_minus_sine_weights", + }, + "digital_marker": "ON", + }, + }, + "waveforms": { + "const_wf": {"type": "constant", "sample": const_amp}, + "pi_wf": {"type": "constant", "sample": pi_amp}, + "pi_half_wf": {"type": "constant", "sample": pi_amp / 2}, + "const_flux_wf": {"type": "constant", "sample": const_flux_amp}, + "zero_wf": {"type": "constant", "sample": 0.0}, + "gauss_wf": {"type": "arbitrary", "samples": gauss_wf.tolist()}, + "x90_wf": {"type": "arbitrary", "samples": x90_wf.tolist()}, + "x90_der_wf": {"type": "arbitrary", "samples": x90_der_wf.tolist()}, + "x180_wf": {"type": "arbitrary", "samples": x180_wf.tolist()}, + "x180_der_wf": {"type": "arbitrary", "samples": x180_der_wf.tolist()}, + "minus_x90_wf": {"type": "arbitrary", "samples": minus_x90_wf.tolist()}, + "minus_x90_der_wf": {"type": "arbitrary", "samples": minus_x90_der_wf.tolist()}, + "y90_wf": {"type": "arbitrary", "samples": y90_wf.tolist()}, + "y90_der_wf": {"type": "arbitrary", "samples": y90_der_wf.tolist()}, + "y180_wf": {"type": "arbitrary", "samples": y180_wf.tolist()}, + "y180_der_wf": {"type": "arbitrary", "samples": y180_der_wf.tolist()}, + "minus_y90_wf": {"type": "arbitrary", "samples": minus_y90_wf.tolist()}, + "minus_y90_der_wf": {"type": "arbitrary", "samples": minus_y90_der_wf.tolist()}, + "readout_wf": {"type": "constant", "sample": readout_amp}, + }, + "digital_waveforms": { + "ON": {"samples": [(1, 0)]}, + }, + "integration_weights": { + "cosine_weights": { + "cosine": [(1.0, readout_len)], + "sine": [(0.0, readout_len)], + }, + "sine_weights": { + "cosine": [(0.0, readout_len)], + "sine": [(1.0, readout_len)], + }, + "minus_sine_weights": { + "cosine": [(0.0, readout_len)], + "sine": [(-1.0, readout_len)], + }, + "rotated_cosine_weights": { + "cosine": [(np.cos(rotation_angle), readout_len)], + "sine": [(-np.sin(rotation_angle), readout_len)], + }, + "rotated_sine_weights": { + "cosine": [(np.sin(rotation_angle), readout_len)], + "sine": [(np.cos(rotation_angle), readout_len)], + }, + "rotated_minus_sine_weights": { + "cosine": [(-np.sin(rotation_angle), readout_len)], + "sine": [(-np.cos(rotation_angle), readout_len)], + }, + }, + "mixers": { + "mixer_qubit": [ + { + "intermediate_frequency": qubit_IF, + "lo_frequency": qubit_LO, + "correction": IQ_imbalance(mixer_qubit_g, mixer_qubit_phi), + } + ], + "mixer_resonator": [ + { + "intermediate_frequency": resonator_IF, + "lo_frequency": resonator_LO, + "correction": IQ_imbalance(mixer_resonator_g, mixer_resonator_phi), + } + ], + }, +} \ No newline at end of file diff --git a/active_reset_full/macros.py b/active_reset_full/macros.py new file mode 100644 index 00000000..75e1c7cc --- /dev/null +++ b/active_reset_full/macros.py @@ -0,0 +1,179 @@ +""" +This file contains useful QUA macros meant to simplify and ease QUA programs. +All the macros below have been written and tested with the basic configuration. If you modify this configuration +(elements, operations, integration weights...) these macros will need to be modified accordingly. +""" + +from qm.qua import * + +############## +# QUA macros # +############## + + +def reset_qubit(method, **kwargs): + """ + Macro to reset the qubit state. + + If method is 'cooldown', then the variable cooldown_time (in clock cycles) must be provided as a python integer > 4. + + **Example**: reset_qubit('cooldown', cooldown_times=500) + + If method is 'active', then 3 parameters are available as listed below. + + **Example**: reset_qubit('active', threshold=-0.003, max_tries=3) + + :param method: Method the reset the qubit state. Can be either 'cooldown' or 'active'. + :type method: str + :key cooldown_time: qubit relaxation time in clock cycle, needed if method is 'cooldown'. Must be an integer > 4. + :key threshold: threshold to discriminate between the ground and excited state, needed if method is 'active'. + :key max_tries: python integer for the maximum number of tries used to perform active reset, + needed if method is 'active'. Must be an integer > 0 and default value is 1. + :key Ig: A QUA variable for the information in the `I` quadrature used for active reset. If not given, a new + variable will be created. Must be of type `Fixed`. + :return: + """ + if method == "cooldown": + # Check cooldown_time + cooldown_time = kwargs.get("cooldown_time", None) + if (cooldown_time is None) or (cooldown_time < 4): + raise Exception("'cooldown_time' must be an integer > 4 clock cycles") + # Reset qubit state + wait(cooldown_time, "qubit") + elif method == "active": + # Check threshold + threshold = kwargs.get("threshold", None) + if threshold is None: + raise Exception("'threshold' must be specified for active reset.") + # Check max_tries + max_tries = kwargs.get("max_tries", 1) + if (max_tries is None) or (not float(max_tries).is_integer()) or (max_tries < 1): + raise Exception("'max_tries' must be an integer > 0.") + # Check Ig + Ig = kwargs.get("Ig", None) + # Reset qubit state + return active_reset(threshold, max_tries=max_tries, Ig=Ig) + + +# Macro for performing active reset until successful for a given number of tries. +def active_reset(threshold, max_tries=1, Ig=None): + """Macro for performing active reset until successful for a given number of tries. + + :param threshold: threshold for the 'I' quadrature discriminating between ground and excited state. + :param max_tries: python integer for the maximum number of tries used to perform active reset. Must >= 1. + :param Ig: A QUA variable for the information in the `I` quadrature. Should be of type `Fixed`. If not given, a new + variable will be created + :return: A QUA variable for the information in the `I` quadrature and the number of tries after success. + """ + if Ig is None: + Ig = declare(fixed) + if (max_tries < 1) or (not float(max_tries).is_integer()): + raise Exception("max_count must be an integer >= 1.") + # Initialize Ig to be > threshold + assign(Ig, threshold + 2**-28) + # Number of tries for active reset + counter = declare(int) + # Reset the number of tries + assign(counter, 0) + + # Perform active feedback + align("qubit", "resonator") + # Use a while loop and counter for other protocols and tests + with while_((Ig > threshold) & (counter < max_tries)): + # Measure the resonator + measure( + "readout", + "resonator", + None, + dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", Ig), + ) + # Play a pi pulse to get back to the ground state + play("pi", "qubit", condition=(Ig > threshold)) + # Increment the number of tries + assign(counter, counter + 1) + return Ig, counter + + +# Macro for measuring the qubit state with single shot +def single_measurement(threshold=None, state=None, I=None, Q=None): + """ + A macro for performing the single-shot readout, with the ability to perform state discrimination. + If `threshold` is given, the information in the `I` quadrature will be compared against the threshold and `state` + would be `True` if `I > threshold`. + Note that it is assumed that the results are rotated such that all the information is in the `I` quadrature. + + :param threshold: Optional. The threshold to compare `I` against. + :param state: A QUA variable for the state information, only used when a threshold is given. + Should be of type `bool`. If not given, a new variable will be created + :param I: A QUA variable for the information in the `I` quadrature. Should be of type `Fixed`. If not given, a new + variable will be created + :param Q: A QUA variable for the information in the `Q` quadrature. Should be of type `Fixed`. If not given, a new + variable will be created + :return: Three QUA variables populated with the results of the readout: (`state` (only if threshold is not None), `I`, `Q`) + """ + if I is None: + I = declare(fixed) + if Q is None: + Q = declare(fixed) + if (threshold is not None) and (state is None): + state = declare(bool) + measure( + "readout", + "resonator", + None, + dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", I), + dual_demod.full("rotated_minus_sin", "out1", "rotated_cos", "out2", Q), + ) + if threshold is not None: + assign(state, I > threshold) + return state, I, Q + else: + return I, Q + + +# Macro for measuring the averaged ground and excited states for calibration +def ge_averaged_measurement(cooldown_time, n_avg): + """Macro measuring the qubit's ground and excited states n_avg times. The averaged values for the corresponding I + and Q quadratures can be retrieved using the stream processing context manager `Ig_st.average().save("Ig")` for instance. + + :param cooldown_time: cooldown time between two successive qubit state measurements in clock cycle unit (4ns). + :param n_avg: number of averaging iterations. Must be a python integer. + :return: streams for the 'I' and 'Q' data for the ground and excited states respectively: [Ig_st, Qg_st, Ie_st, Qe_st]. + """ + n = declare(int) + I = declare(fixed) + Q = declare(fixed) + Ig_st = declare_stream() + Qg_st = declare_stream() + Ie_st = declare_stream() + Qe_st = declare_stream() + with for_(n, 0, n < n_avg, n + 1): + # Ground state calibration + align("qubit", "resonator") + measure( + "readout", + "resonator", + None, + dual_demod.full("cos", "out1", "sin", "out2", I), + dual_demod.full("minus_sin", "out1", "cos", "out2", Q), + ) + wait(cooldown_time, "resonator", "qubit") + save(I, Ig_st) + save(Q, Qg_st) + + # Excited state calibration + align("qubit", "resonator") + play("pi", "qubit") + align("qubit", "resonator") + measure( + "readout", + "resonator", + None, + dual_demod.full("cos", "out1", "sin", "out2", I), + dual_demod.full("minus_sin", "out1", "cos", "out2", Q), + ) + wait(cooldown_time, "resonator", "qubit") + save(I, Ie_st) + save(Q, Qe_st) + + return Ig_st, Qg_st, Ie_st, Qe_st \ No newline at end of file diff --git a/joe_testing/active_reset.py b/joe_testing/active_reset.py new file mode 100644 index 00000000..be8e8f36 --- /dev/null +++ b/joe_testing/active_reset.py @@ -0,0 +1,78 @@ +""" +IQ_blobs.py: template for performing a single shot discrimination and active reset +""" +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager +from qm import SimulationConfig, LoopbackInterface +from configuration import * +from macros import single_measurement, active_reset, reset_qubit +import matplotlib.pyplot as plt + +############################## +# Program-specific variables # +############################## +threshold = ge_threshold # Threshold used for ge state discrimination +n_shot = 10000 # Number of acquired shots +max_tries = 2 # Maximum number of tries for active reset (no feedback if set to 0) +cooldown_time = 5 * qubit_T1 // 4 # Cooldown time in clock cycles (4ns) + +################### +# The QUA program # +################### + +def create_reset_program(reset_type: str): + + with program() as active_reset_prog: + n = declare(int) # Averaging index + Ig_st = declare_stream() + Qg_st = declare_stream() + tries_st = declare_stream() + state_st = declare_stream() + + with for_(n, 0, n < n_shot, n + 1): + # Measure and perform active reset + I_g, total_tries = active_reset(threshold=threshold, max_tries=max_tries, Ig=None) + # I_g, total_tries = reset_qubit(method='active', threshold=threshold, max_tries=max_tries, Ig=I_g) + # Check active feedback by measuring again + ground, I_g, Q_g = single_measurement(threshold=threshold, state=None, I=I_g, Q=None) + # Save data to the stream processing + save(I_g, Ig_st) + save(Q_g, Qg_st) + save(ground, state_st) + save(total_tries, tries_st) + + with stream_processing(): + Ig_st.save_all("Ig") + Qg_st.save_all("Qg") + state_st.save_all("ground") + tries_st.average().save("average_tries") + +##################################### +# Open Communication with the QOP # +##################################### +qmm = QuantumMachinesManager(qop_ip) + +simulation = False +if simulation: + simulation_config = SimulationConfig( + duration=28000, simulation_interface=LoopbackInterface([("con1", 3, "con1", 1)]) + ) + job = qmm.simulate(config, active_reset_prog, simulation_config) + job.get_simulated_samples().con1.plot() +else: + qm = qmm.open_qm(config) + job = qm.execute(active_reset_prog) + # Get results from QUA program + results = fetching_tool(job, data_list=["Ig", "Qg", "ground", "average_tries"], mode="wait_for_all") + # Fetch results + Ig, Qg, ground, average_tries = results.fetch_all() + # Plot data + fig = plt.figure(figsize=(7, 5)) + plt.cla() + plt.scatter(Ig, Qg, color="b", alpha=0.1, label=f"ground ({np.average(ground)*100:.1f}%)") + plt.axvline(threshold, color="k", label="ge threshold") + plt.axis("equal") + plt.xlabel("I") + plt.ylabel("Q") + plt.legend() + plt.title(f"Active reset after {average_tries:.0f}/{max_tries} tries in average.") diff --git a/joe_testing/configuration.py b/joe_testing/configuration.py new file mode 100644 index 00000000..2fc4a2b3 --- /dev/null +++ b/joe_testing/configuration.py @@ -0,0 +1,79 @@ +# copied from somewhere on github + +import numpy as np + +readout_len = 1000 + +config = { + "version": 1, + "controllers": { + "con1": { + "analog_outputs": { + 1: {"offset": +0.0}, + }, + "analog_inputs": { + 1: {"offset": +0.0}, + }, + } + }, + "elements": { + "qe1": { + "singleInput": {"port": ("con1", 1)}, + "outputs": {"output1": ("con1", 1)}, + "intermediate_frequency": 100e6, + "operations": { + "readout": "readout_pulse", + "long_readout": "long_readout_pulse", + }, + "time_of_flight": 24, + "smearing": 0, + }, + }, + "pulses": { + "readout_pulse": { + "operation": "measure", + "length": readout_len, + "waveforms": {"single": "ramp_wf"}, + "digital_marker": "ON", + "integration_weights": {"cos": "cosine_weights", "sin": "sine_weights"}, + }, + "long_readout_pulse": { + "operation": "measure", + "length": 2 * readout_len, + "waveforms": {"single": "ramp_wf2"}, + "digital_marker": "ON", + "integration_weights": { + "cos": "long_cosine_weights", + "sin": "sine_weights", + }, + }, + }, + "waveforms": { + "const_wf": {"type": "constant", "sample": 0.2}, + "ramp_wf": { + "type": "arbitrary", + "samples": np.linspace(0, -0.5, readout_len).tolist(), + }, + "ramp_wf2": { + "type": "arbitrary", + "samples": np.linspace(0, -0.5, readout_len).tolist() + np.linspace(0, -0.5, readout_len).tolist(), + }, + }, + "digital_waveforms": { + "ON": {"samples": [(1, 0)]}, + }, + "integration_weights": { + "cosine_weights": { + "cosine": [(1.0, readout_len)], + "sine": [(0.0, readout_len)], + }, + "long_cosine_weights": { + "cosine": [(1.0, 2 * readout_len)], + "sine": [(0.0, 2 * readout_len)], + }, + "sine_weights": { + "cosine": [(0.0, readout_len)], + "sine": [(1.0, readout_len)], + }, + }, +} \ No newline at end of file diff --git a/joe_testing/example_reset_comparison.py b/joe_testing/example_reset_comparison.py new file mode 100644 index 00000000..5e8bfee8 --- /dev/null +++ b/joe_testing/example_reset_comparison.py @@ -0,0 +1,42 @@ +# File to show for example how the reset comparison gui could work + +import numpy as np +from qualang_tools.analysis.multi_qubit_discriminator import ActiveResetGUI +from PyQt5.QtWidgets import QApplication +import sys + + + +def generate_discrimination_data(): + from qualang_tools.analysis.multi_qubit_discriminator.independent_multi_qubit_discriminator import independent_multi_qubit_discriminator + + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 10)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 10)).T + + igs, qgs = iq_state_g + ies, qes = iq_state_e + + results_list = np.stack([igs, qgs, ies, qes], axis=1) + + + results = independent_multi_qubit_discriminator(results_list, b_plot=False, b_print=False) + [result._add_attribute('runtime', 100 * np.random.rand()) for result in results] + return results + + +# output would be a dictionary like this: +reset_dict = { + 'Passive reset': generate_discrimination_data(), + 'Active reset': generate_discrimination_data(), + 'New method': generate_discrimination_data(), + 'Other method': generate_discrimination_data() +} + +def main(): + app = QApplication(sys.argv) + ex = ActiveResetGUI(reset_dict) + # sys.exit(app.exec_()) + app.exec_() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/joe_testing/fake_iq_data.py b/joe_testing/fake_iq_data.py new file mode 100644 index 00000000..e695e32b --- /dev/null +++ b/joe_testing/fake_iq_data.py @@ -0,0 +1,30 @@ +import numpy as np +import matplotlib.pyplot as plt + +from qualang_tools.analysis.discriminator import two_state_discriminator + +# generate some fake readout data in i and q planes +# I is x, Q is y + +iq_state_g = np.random.multivariate_normal((0, -0.2), ((0.5, 0.), (0., 0.5)), 5000).T +iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((0.5, 0), (0, 0.5)), 5000).T + +# plot to check + +plt.scatter(*iq_state_g, label='ground') +plt.scatter(*iq_state_e, label='excited') +plt.legend() +plt.show() + + + +Ig, Qg = iq_state_g +Ie, Qe = iq_state_e + + +angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(Ig, Qg, Ie, Qe, b_print=True, b_plot=True) + + + + + diff --git a/joe_testing/joe_testing_discriminator.py b/joe_testing/joe_testing_discriminator.py new file mode 100644 index 00000000..35a426a7 --- /dev/null +++ b/joe_testing/joe_testing_discriminator.py @@ -0,0 +1,135 @@ +""" +IQ_blobs.py: Measure the qubit in the ground and excited state to create the IQ blobs. +If the separation and the fidelity are good enough, gives the parameters needed for active reset +""" +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager +from configuration import * +from qualang_tools.analysis.discriminator import two_state_discriminator + +import logging + +logging.basicConfig(level='INFO') + +logger = logging.getLogger(__name__) + + +qubit_T1 = 100e-6 +qop_ip = '172.16.2.103' +port = 85 + + +################### +# The QUA program # +################### + +n_runs = 10000 + +cooldown_time = 5 * qubit_T1 // 4 + +with program() as IQ_blobs: + n = declare(int) + I_g = declare(fixed) + Q_g = declare(fixed) + I_g_st = declare_stream() + Q_g_st = declare_stream() + I_e = declare(fixed) + Q_e = declare(fixed) + I_e_st = declare_stream() + Q_e_st = declare_stream() + + with for_(n, 0, n < n_runs, n + 1): + measure( + "readout", + "resonator", + None, + dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", I_g), + dual_demod.full("rotated_minus_sin", "out1", "rotated_cos", "out2", Q_g), + ) + save(I_g, I_g_st) + save(Q_g, Q_g_st) + wait(cooldown_time, "resonator") + + align() # global align + + play("pi", "qubit") + align("qubit", "resonator") + measure( + "readout", + "resonator", + None, + dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", I_e), + dual_demod.full("rotated_minus_sin", "out1", "rotated_cos", "out2", Q_e), + ) + save(I_e, I_e_st) + save(Q_e, Q_e_st) + wait(cooldown_time, "resonator") + + with stream_processing(): + I_g_st.save_all("I_g") + Q_g_st.save_all("Q_g") + I_e_st.save_all("I_e") + Q_e_st.save_all("Q_e") + +##################################### +# Open Communication with the QOP # +##################################### + +logger.info("Trying to connect to qop ip...") +qmm = QuantumMachinesManager(qop_ip, port) +logger.info("connected.") + +qm = qmm.open_qm(config) +logger.info("set config") + +job = qm.execute(IQ_blobs) +res_handles = job.result_handles +res_handles.wait_for_all_values() +Ig = res_handles.get("I_g").fetch_all()["value"] +Qg = res_handles.get("Q_g").fetch_all()["value"] +Ie = res_handles.get("I_e").fetch_all()["value"] +Qe = res_handles.get("Q_e").fetch_all()["value"] + +angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(Ig, Qg, Ie, Qe, b_print=True, b_plot=True) + +######################################### +# The two_state_discriminator gives us the rotation angle which makes it such that all of the information will be in +# the I axis. This is being done by setting the `rotation_angle` parameter in the configuration. +# See this for more information: https://qm-docs.qualang.io/guides/demod#rotating-the-iq-plane +# Once we do this, we can perform active reset using: +######################################### +# +# # Active reset: +# with if_(I > threshold): +# play("pi", "qubit") +# +######################################### +# +# # Active reset (faster): +# play("pi", "qubit", condition=I > threshold) +# +######################################### +# +# # Repeat until success active reset +# with while_(I > threshold): +# play("pi", "qubit") +# align("qubit", "resonator") +# measure("readout", "resonator", None, +# dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", I)) +# +######################################### +# +# # Repeat until success active reset, up to 3 iterations +# count = declare(int) +# assign(count, 0) +# cont_condition = declare(bool) +# assign(cont_condition, ((I > threshold) & (count < 3))) +# with while_(cont_condition): +# play("pi", "qubit") +# align("qubit", "resonator") +# measure("readout", "resonator", None, +# dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", I)) +# assign(count, count + 1) +# assign(cont_condition, ((I > threshold) & (count < 3))) +# +######################################### \ No newline at end of file diff --git a/joe_testing/macros.py b/joe_testing/macros.py new file mode 100644 index 00000000..eb05c6cc --- /dev/null +++ b/joe_testing/macros.py @@ -0,0 +1,171 @@ +""" +This file contains useful QUA macros meant to simplify and ease QUA programs. +All the macros below have been written and tested with the basic configuration. If you modify this configuration +(elements, operations, integration weights...) these macros will need to be modified accordingly. +""" + +from qm.qua import * + +############## +# QUA macros # +############## + + +def reset_qubit(method, **kwargs): + """ + Macro to reset the qubit state. + If method is 'cooldown', then the variable cooldown_time (in clock cycles) must be provided as a python integer > 4. + **Example**: reset_qubit('cooldown', cooldown_times=500) + If method is 'active', then 3 parameters are available as listed below. + **Example**: reset_qubit('active', threshold=-0.003, max_tries=3) + :param method: Method the reset the qubit state. Can be either 'cooldown' or 'active'. + :type method: str + :key cooldown_time: qubit relaxation time in clock cycle, needed if method is 'cooldown'. Must be an integer > 4. + :key threshold: threshold to discriminate between the ground and excited state, needed if method is 'active'. + :key max_tries: python integer for the maximum number of tries used to perform active reset, + needed if method is 'active'. Must be an integer > 0 and default value is 1. + :key Ig: A QUA variable for the information in the `I` quadrature used for active reset. If not given, a new + variable will be created. Must be of type `Fixed`. + :return: + """ + if method == "cooldown": + # Check cooldown_time + cooldown_time = kwargs.get("cooldown_time", None) + if (cooldown_time is None) or (cooldown_time < 4): + raise Exception("'cooldown_time' must be an integer > 4 clock cycles") + # Reset qubit state + wait(cooldown_time, "qubit") + elif method == "active": + # Check threshold + threshold = kwargs.get("threshold", None) + if threshold is None: + raise Exception("'threshold' must be specified for active reset.") + # Check max_tries + max_tries = kwargs.get("max_tries", 1) + if (max_tries is None) or (not float(max_tries).is_integer()) or (max_tries < 1): + raise Exception("'max_tries' must be an integer > 0.") + # Check Ig + Ig = kwargs.get("Ig", None) + # Reset qubit state + return active_reset(threshold, max_tries=max_tries, Ig=Ig) + + +# Macro for performing active reset until successful for a given number of tries. +def active_reset(threshold, max_tries=1, Ig=None): + """Macro for performing active reset until successful for a given number of tries. + :param threshold: threshold for the 'I' quadrature discriminating between ground and excited state. + :param max_tries: python integer for the maximum number of tries used to perform active reset. Must >= 1. + :param Ig: A QUA variable for the information in the `I` quadrature. Should be of type `Fixed`. If not given, a new + variable will be created + :return: A QUA variable for the information in the `I` quadrature and the number of tries after success. + """ + if Ig is None: + Ig = declare(fixed) + if (max_tries < 1) or (not float(max_tries).is_integer()): + raise Exception("max_count must be an integer >= 1.") + # Initialize Ig to be > threshold + assign(Ig, threshold + 2**-28) + # Number of tries for active reset + counter = declare(int) + # Reset the number of tries + assign(counter, 0) + + # Perform active feedback + align("qubit", "resonator") + # Use a while loop and counter for other protocols and tests + with while_((Ig > threshold) & (counter < max_tries)): + # Measure the resonator + measure( + "readout", + "resonator", + None, + dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", Ig), + ) + # Play a pi pulse to get back to the ground state + play("pi", "qubit", condition=(Ig > threshold)) + # Increment the number of tries + assign(counter, counter + 1) + return Ig, counter + + +# Macro for measuring the qubit state with single shot +def single_measurement(threshold=None, state=None, I=None, Q=None): + """ + A macro for performing the single-shot readout, with the ability to perform state discrimination. + If `threshold` is given, the information in the `I` quadrature will be compared against the threshold and `state` + would be `True` if `I > threshold`. + Note that it is assumed that the results are rotated such that all the information is in the `I` quadrature. + :param threshold: Optional. The threshold to compare `I` against. + :param state: A QUA variable for the state information, only used when a threshold is given. + Should be of type `bool`. If not given, a new variable will be created + :param I: A QUA variable for the information in the `I` quadrature. Should be of type `Fixed`. If not given, a new + variable will be created + :param Q: A QUA variable for the information in the `Q` quadrature. Should be of type `Fixed`. If not given, a new + variable will be created + :return: Three QUA variables populated with the results of the readout: (`state` (only if threshold is not None), `I`, `Q`) + """ + if I is None: + I = declare(fixed) + if Q is None: + Q = declare(fixed) + if (threshold is not None) and (state is None): + state = declare(bool) + measure( + "readout", + "resonator", + None, + dual_demod.full("rotated_cos", "out1", "rotated_sin", "out2", I), + dual_demod.full("rotated_minus_sin", "out1", "rotated_cos", "out2", Q), + ) + if threshold is not None: + assign(state, I > threshold) + return state, I, Q + else: + return I, Q + + +# Macro for measuring the averaged ground and excited states for calibration +def ge_averaged_measurement(cooldown_time, n_avg): + """Macro measuring the qubit's ground and excited states n_avg times. The averaged values for the corresponding I + and Q quadratures can be retrieved using the stream processing context manager `Ig_st.average().save("Ig")` for instance. + :param cooldown_time: cooldown time between two successive qubit state measurements in clock cycle unit (4ns). + :param n_avg: number of averaging iterations. Must be a python integer. + :return: streams for the 'I' and 'Q' data for the ground and excited states respectively: [Ig_st, Qg_st, Ie_st, Qe_st]. + """ + n = declare(int) + I = declare(fixed) + Q = declare(fixed) + Ig_st = declare_stream() + Qg_st = declare_stream() + Ie_st = declare_stream() + Qe_st = declare_stream() + with for_(n, 0, n < n_avg, n + 1): + # Ground state calibration + align("qubit", "resonator") + measure( + "readout", + "resonator", + None, + dual_demod.full("cos", "out1", "sin", "out2", I), + dual_demod.full("minus_sin", "out1", "cos", "out2", Q), + ) + wait(cooldown_time, "resonator", "qubit") + save(I, Ig_st) + save(Q, Qg_st) + + # Excited state calibration + align("qubit", "resonator") + play("pi", "qubit") + align("qubit", "resonator") + measure( + "readout", + "resonator", + None, + dual_demod.full("cos", "out1", "sin", "out2", I), + dual_demod.full("minus_sin", "out1", "cos", "out2", Q), + ) + wait(cooldown_time, "resonator", "qubit") + save(I, Ie_st) + save(Q, Qe_st) + + return Ig_st, Qg_st, Ie_st, Qe_st \ No newline at end of file diff --git a/joe_testing/reset_data_collection.py b/joe_testing/reset_data_collection.py new file mode 100644 index 00000000..4a1c8805 --- /dev/null +++ b/joe_testing/reset_data_collection.py @@ -0,0 +1,15 @@ +# example of how the reset data collection could go + +from qualang_tools.analysis import DiscriminatorDataclass +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager + + +def run_reset_program(qua_reset_program, host, port): + # connect and open QM + qmm = QuantumMachinesManager(host=host, port=port) + qm = qmm.open_qm() + + + + diff --git a/qualang_tools/addons/InteractivePlotLib.py b/qualang_tools/addons/InteractivePlotLib.py index cdc45ca0..6d477b00 100644 --- a/qualang_tools/addons/InteractivePlotLib.py +++ b/qualang_tools/addons/InteractivePlotLib.py @@ -252,7 +252,7 @@ def user_interaction(self, type, event): ) elif type == "keyboard_click": - if event.key == "ctrl+v": + if event.key_layout == "ctrl+v": clip.OpenClipboard() data = clip.GetClipboardData() clip.CloseClipboard() @@ -286,95 +286,95 @@ def user_interaction(self, type, event): print(out_2d) self.ax.pcolormesh(x_values, y_values, out_2d, shading="auto") - if event.key == "m": + if event.key_layout == "m": self.marker_mode = not self.marker_mode - if event.key == "shift+m" or event.key == "M": + if event.key_layout == "shift+m" or event.key_layout == "M": self.marker_list.append([]) - if event.key == "alt+m": + if event.key_layout == "alt+m": self.marker_list = [[]] self.marker_mode = False - if event.key == "a": + if event.key_layout == "a": self.ax.axis("equal") - if event.key == "v": + if event.key_layout == "v": self.state = InteractivePlotLibFigure.StateMachineVoronoi(self) - if event.key == "alt+v": + if event.key_layout == "alt+v": self.voronoi_obj.remove() - if event.key == "f": + if event.key_layout == "f": self.state = InteractivePlotLibFigure.StateMachineFit(self) - if event.key == "g": + if event.key_layout == "g": self.grid_state = not self.grid_state self.ax.grid(self.grid_state) - if event.key == "alt+f": + if event.key_layout == "alt+f": self.remove_fit() - if event.key == "shift+s" or event.key == "S": + if event.key_layout == "shift+s" or event.key_layout == "S": if self.doc.doc: self.doc.doc(list(self.master_obj.figs.keys())) - if event.key == "s": + if event.key_layout == "s": if self.doc.doc: self.doc.doc([plt.gcf().number]) - if event.key == "r": + if event.key_layout == "r": self.state = InteractivePlotLibFigure.StateMachineRect(self) - if event.key == "alt+r": + if event.key_layout == "alt+r": self.rectangle = None - if event.key == "n": + if event.key_layout == "n": self.master_obj.figure() - if event.key == "alt+l": + if event.key_layout == "alt+l": self.ax.get_legend().set_visible( not self.ax.get_legend().get_visible() ) - if event.key == "p" and self.plot_type == "plot": + if event.key_layout == "p" and self.plot_type == "plot": self.convert_to_pcolormesh() self.plot_type = "mesh" if self.line_selected: - if event.key == "t": + if event.key_layout == "t": self.line_selected.transpose() - if event.key == "l" and ( + if event.key_layout == "l" and ( self.plot_type == "pcolor" or self.plot_type == "mesh" ): self.line_selected.convert_to_lines() self.line_selected = None self.plot_type = "plot" - if event.key == "c": + if event.key_layout == "c": self.state = InteractivePlotLibFigure.StateMachineColor(self) - if event.key == ":": + if event.key_layout == ":": self.state = InteractivePlotLibFigure.StateMachineCommand(self) - if event.key == "ctrl+c": + if event.key_layout == "ctrl+c": self.line_selected.copy_to_clipboard() - if event.key == "delete": + if event.key_layout == "delete": self.line_selected.delete() - if event.key == "alt+delete": + if event.key_layout == "alt+delete": self.line_selected.delete_neg() - if event.key == "up": + if event.key_layout == "up": self.line_selected.correct_order(1) - if event.key == "down": + if event.key_layout == "down": self.line_selected.correct_order(-1) - if event.key.isnumeric(): - self.line_selected.line_width = int(event.key) + if event.key_layout.isnumeric(): + self.line_selected.line_width = int(event.key_layout) self.line_selected.emphasize_line_width() elif type == "keyboard_release": @@ -1050,7 +1050,7 @@ def __init__(self_state, sup_self): def event(self_state, type, event): if type == "keyboard_click": - if event.key == "down": + if event.key_layout == "down": if self_state.index < len(self_state.lines) - 1: self_state.text_obj.done() self_state.index += 1 @@ -1061,7 +1061,7 @@ def event(self_state, type, event): self_state.update_legend, ) - elif event.key == "up": + elif event.key_layout == "up": if self_state.index > 0: self_state.text_obj.done() self_state.index -= 1 @@ -1071,7 +1071,7 @@ def event(self_state, type, event): ) else: - self_state.done = self_state.text_obj.react_to_key_press(event.key) + self_state.done = self_state.text_obj.react_to_key_press(event.key_layout) elif type == "mouse_click": self_state.done = self_state.text_obj.done() @@ -1125,7 +1125,7 @@ def run_command(self_state, command, type): def event(self_state, type, event): if type == "keyboard_click": - self_state.done = self_state.text_obj.react_to_key_press(event.key) + self_state.done = self_state.text_obj.react_to_key_press(event.key_layout) elif type == "mouse_click": self_state.done = self_state.text_obj.done() @@ -1279,7 +1279,7 @@ def convert_function(x): def event(self_state, type, event): if type == "keyboard_click": - self_state.done = self_state.text_obj.react_to_key_press(event.key) + self_state.done = self_state.text_obj.react_to_key_press(event.key_layout) elif type == "mouse_click": self_state.done = self_state.text_obj.done() @@ -1451,7 +1451,7 @@ def __init__(self_state, sup_self): def event(self_state, type, event): if type == "keyboard_click": - self_state.done = self_state.text_obj.react_to_key_press(event.key) + self_state.done = self_state.text_obj.react_to_key_press(event.key_layout) if type == "mouse_click": self_state.done = self_state.text_obj.done() if self_state.done: @@ -1497,7 +1497,7 @@ def event(self_state, type, event): if type == "keyboard_click" or type == "mouse_click": self_state.done = self_state.text_obj.done() if type == "keyboard_click": - self_state.sup_self.line_selected.color(event.key) + self_state.sup_self.line_selected.color(event.key_layout) else: self_state.sup_self.user_interaction("mouse_click", event) @@ -1646,7 +1646,7 @@ def __init__(self_state, sup_self): def event(self_state, type, event): if type == "keyboard_click": - self_state.done = self_state.text_obj.react_to_key_press(event.key) + self_state.done = self_state.text_obj.react_to_key_press(event.key_layout) elif type == "mouse_click": self_state.done = self_state.text_obj.done() diff --git a/qualang_tools/analysis/__init__.py b/qualang_tools/analysis/__init__.py index 40ec314e..4f79c208 100644 --- a/qualang_tools/analysis/__init__.py +++ b/qualang_tools/analysis/__init__.py @@ -1,5 +1,5 @@ from qualang_tools.analysis.discriminator import two_state_discriminator __all__ = [ - "two_state_discriminator", + "two_state_discriminator" ] diff --git a/qualang_tools/analysis/multi_qubit_discriminator/README.md b/qualang_tools/analysis/multi_qubit_discriminator/README.md new file mode 100644 index 00000000..8d272f8e --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator/README.md @@ -0,0 +1,79 @@ +# Multi-qubit discriminator package + +This package contains tools designed to help with user analysis of multi-qubit systems. +There are two graphical user interfaces (GUIs), a multi-qubit discriminator method to run readout +discrimination on existing readout data, and a dataclass to store the results of each discriminator run. +These tools are explained in detail below. + + + +## Independent multi-qubit discriminator + +IQ blob data from a single qubit can be run through the `discriminator` method in the `analysis` package of `qualang_tools`. +For a multi-qubit system, it may be simpler to use the `independent_multi_qubit_discriminator` method from this +package. + +### Example usage + +```python + +IQ_results: array-like object # e.g. [[ig0, qg0, ie0, qe0], [ig1, qg1, ie1, qe1]] + +results_dataclasses = independent_multi_qubit_discriminator( + IQ_results, b_print=False, b_plot=False, text=False +) +``` + +Running this returns a list of dataclasses (see below) which box together the inputs and outputs of the discriminator for each qubit. +This allows for easy use of the data, especially with the GUIs. + + +## Results dataclass + +The results dataclass (`_DiscriminatorDataclass`) holds the inputs (i.e. the IQ blob data) and outputs (i.e. discriminator results such as +fidelities, rotation angle, and threshold) of the discrimination run, as well as some helper methods for ease of use. This class can be used directly, but its +main use is to wrap the data together so the GUIs are more stable. The `independent_multi_qubit_discriminator` method returns one dataclass per qubit. These lists are then +used by both GUIs in the package. + +## Active reset GUI + +NOTE: we could rename this as it is not just for active reset. + +The active reset GUI is a tool used for comparing the results of different qubit reset approaches. The rotated IQ blobs are displayed, along with other +important information about the outcome of the experiments such as the fidelities and rotation angles. The time taken for each reset approach is also displayed, but this information must be added +to each of the dataclasses. + +The GUI takes a dictionary of the form `{name: results_list, name2: results_list2}` where `name` is the name of the type of reset used to generate the +`results_list` via the `independent_multi_qubit_discriminator` function. The `launch_reset_gui` function is included to take care of starting the Qt application through which the GUI runs. + +### Example usage + +```python +reset_dict = { + 'Passive reset': results_0, # list of results_dataclasses generated by independent_multi_qubit_discriminator + 'Active reset': results_1, + 'Two-threshold active reset': results_2, +} + +program = launch_reset_gui(reset_dict) +``` + +## Discriminator GUI + +The discriminator GUI can be used to display and visualise information about each qubit's readout as well as statistics about the array of qubits. +The first tab of the GUI displays a list of the qubits, ordered by their readout fidelity (mean of GG and EE fidelities). It also displays statistics about the array of quits. Information and plots of individual +qubits are on the second tab, and can be accessed by double-clicking on the relevant qubit in the list of the dashboard tab. Alternatively, you can switch to the qubits tab and select the relevant qubit from the +dropdown list. A helper function, `launch_discriminator_gui`, is included. This takes care of launching the Qt application. + + +### Example usage + +```python +results_list = independent_multi_qubit_discriminator(data) # data is in the format described above in the independent multi-qubit discriminator section + +program = launch_discriminator_gui(results_list) +``` + + + + diff --git a/qualang_tools/analysis/multi_qubit_discriminator/__init__.py b/qualang_tools/analysis/multi_qubit_discriminator/__init__.py new file mode 100644 index 00000000..31af8484 --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator/__init__.py @@ -0,0 +1,12 @@ +from .active_reset_gui import ActiveResetGUI, launch_reset_gui +from .discriminator_gui import DiscriminatorGui, launch_discriminator_gui +from .independent_multi_qubit_discriminator import independent_multi_qubit_discriminator +from .independent_multi_qubit_discriminator import _DiscriminatorDataclass + +__all__ = [ + ActiveResetGUI, + DiscriminatorGui, + independent_multi_qubit_discriminator, + launch_reset_gui, + launch_discriminator_gui +] \ No newline at end of file diff --git a/qualang_tools/analysis/multi_qubit_discriminator/active_reset_gui.py b/qualang_tools/analysis/multi_qubit_discriminator/active_reset_gui.py new file mode 100644 index 00000000..9f5284f9 --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator/active_reset_gui.py @@ -0,0 +1,312 @@ +import sys +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import pyqtgraph as pg +import time +import numpy as np + + +class ActiveResetGUI(QWidget): + def __init__(self, reset_results_dictionary): + super(ActiveResetGUI, self).__init__() + + self.reset_results_dictionary = reset_results_dictionary + self.num_qubits = len(list(self.reset_results_dictionary.values())[0]) + + self._initialise_ui() + + self._populate_list() + + self.hbox.addWidget(self.information) + self.hbox.addWidget(self.plot_area) + + self.setLayout(self.hbox) + self.show() + + def _initialise_ui(self): + """ + Initialise the user interface of the GUI. Sets the colour scheme, qapplication style, and the geometry + of the window. The two main parts of the GUI are the info_area (on the left) and the plot_area (on the right). + + The info area contains the dropdown list so we can select a specific qubit, a table with information about the + qubit being looked at, a legend for the colours on the plots, and check boxes to select which plots are being + shown. The plot area contains a column for each readout type, with each of these boxes containing rotated + IQ blobs and a histogram of the I-projected data. + + @return: + """ + + self.hbox = QHBoxLayout(self) + + self.ground_state_colour = (100, 149, 237) + self.excited_state_colour = (255, 185, 15) + + QApplication.setStyle(QStyleFactory.create("Cleanlooks")) + + self.setGeometry(100, 100, 1200, 500) + self.setWindowTitle("Qubit reset comparison") + + self._initialise_info_area() + self._initialise_plot_area() + + # connect buttons + for check_box in self.check_boxes: + check_box.toggled.connect(self.toggle_views) + check_box.setChecked(True) + + self.qubit_list.currentIndexChanged.connect(self.update_plots) + + self.show() + + def _initialise_plot_area(self): + """ + Sets up the plot area. It's a horizontal box layout, meaning widgets are added in a row until a new row is called. + For each type of reset (name in self.reset_results_dictionary.keys()), we add a column containing two plots: + the IQ blob and the 1d histogram. + """ + + self.plot_area = QWidget() + self.plot_layout = QHBoxLayout() + self.plot_area.setLayout(self.plot_layout) + + # plot_regions are each two-plot window for a specific reset type + self.plot_regions = [] + + for name in self.reset_results_dictionary.keys(): + plot_region = pg.GraphicsLayoutWidget() + self.plot_layout.addWidget(plot_region, stretch=1) + self.plot_regions.append(plot_region) + plot_region.addItem( + pg.PlotItem( + title=f'{name}'.replace("_", " ") + ), + 0, + 0, + ) + plot_region.addItem(pg.PlotItem(), 1, 0) + + def _initialise_info_area(self): + """ + Sets up the info area with the required widgets. It's a vbox layout which means widgets are added row-by-row in + a single column until a new column is called. + """ + + # create some widgets + self.information = QWidget() + self.information_layout = QVBoxLayout() + self.information.setLayout(self.information_layout) + + self.qubit_list = QComboBox() + + self.check_boxes = [] + for key in self.reset_results_dictionary.keys(): + self.check_boxes.append(QCheckBox(str(key).replace("_", " "))) + + self.info_box = pg.TableWidget(3, len(self.reset_results_dictionary)) + self.set_table() + + self.information_layout.addWidget(self.qubit_list) + self.information_layout.addWidget(self.info_box) + + self.info_box.horizontalHeader().setVisible(False) + self.info_box.verticalHeader().setVisible(False) + + # width of table + 3 pixels means we do not get a horizontal scroll bar. +3 to prevent + # wider characters bringing a scroll bar in + table_size = self.info_box.sizeHint() + self.info_box.setMaximumHeight(int(table_size.height())) + self.info_box.setMinimumWidth(int(table_size.width())) + self.info_box.setMaximumWidth(int(table_size.width())) + self.qubit_list.setMaximumWidth(int(table_size.width())) + self.info_box.setShowGrid(False) + + self.ground_state_label = QLabel("Ground state") + self.excited_state_label = QLabel("Excited state") + + self.ground_state_label.setAlignment(Qt.AlignCenter) + self.excited_state_label.setAlignment(Qt.AlignCenter) + + self.ground_state_label.setStyleSheet( + f"background-color:rgb{self.ground_state_colour}; border-radius:5px" + ) + self.excited_state_label.setStyleSheet( + f"background-color:rgb{self.excited_state_colour}; border-radius:5px" + ) + + self.information_layout.addWidget(self.ground_state_label) + self.information_layout.addWidget(self.excited_state_label) + + self.excited_state_label.setMaximumWidth(int(table_size.width())) + self.ground_state_label.setMaximumWidth(int(table_size.width())) + + self.excited_state_label.setMaximumHeight(80) + self.ground_state_label.setMaximumHeight(80) + + for check_box in self.check_boxes: + self.information_layout.addWidget(check_box) + + self.information_layout.addWidget(QFrame()) + + def toggle_views(self): + """ + All check boxes are connected to this function so whenever any of them are changed, it is called. This saves + having multiple functions at very low time overhead. If a checkbox isChecked()==True, this shows the respective + plots in the plot area. If it is not checked, the plots it corresponds to are hidden. + """ + + for check_box, plot_region in zip(self.check_boxes, self.plot_regions): + if check_box.isChecked(): + plot_region.show() + else: + plot_region.hide() + + def generate_table(self): + """ + Data in the QTableWidget is added in rows as a nested list. To keep the headers large I have added them as + the first row of the table and turned off the actual headers. + + @return: the list of lists representing rows of the table. + """ + overall_table = [["", "Fidelity (%)", "Time (ns)"]] + + for reset_type, results_datasets in self.reset_results_dictionary.items(): + qubit_data = results_datasets[self.qubit_list.currentIndex()] + row = [ + reset_type, + f"{qubit_data.fidelity:.2f}", + f"{qubit_data.runtime:.2f}", + ] + overall_table.append(row) + + return overall_table + + def set_table(self): + """ + Sets the values for the table in the info area. + """ + data = self.generate_table() + self.info_box.setData(data) + + def plot_to_scatter(self, scatter_plot, result_dataclass): + """ + Adds the required data (IQ blobs) from the result_dataclass to the scatter_plot object + @param scatter_plot: the plot onto which the data will be added + @param result_dataclass: the dataclass containing the IQ DATA + """ + + rotated_data_g = pg.ScatterPlotItem( + result_dataclass.ig_rotated, + result_dataclass.qg_rotated, + brush=(*self.ground_state_colour, 100), + symbol="s", + size="2", + pen=pg.mkPen(None), + ) + + rotated_data_e = pg.ScatterPlotItem( + result_dataclass.ie_rotated, + result_dataclass.qe_rotated, + brush=(*self.excited_state_colour, 100), + symbol="s", + size="2", + pen=pg.mkPen(None), + ) + + scatter_plot.addItem(rotated_data_g) + scatter_plot.addItem(rotated_data_e) + + scatter_plot.addLine( + x=result_dataclass.threshold, + label=f"{result_dataclass.threshold:.2f}, θ={(np.random.rand() * 1000) % 90:.2f}°", + labelOpts={"position": 0.9}, + pen={"color": "white", "dash": [20, 20]}, + ) + + scatter_plot.setAspectLocked() + return scatter_plot + + def plot_to_histogram(self, histogram_plot, result_dataclass): + """ + Processes and adds the 1d histogram data stored in result_dataclass to the histogram_plot object + @param histogram_plot: histogram plot onto which the data will be added + @param result_dataclass: dataclass containing the IQ blob data that will then be processed into histogram data + @return: + """ + + y, x = np.histogram( + result_dataclass.ig_rotated, bins=80 + ) # , bins=np.linspace(-3, 8, 80)) + y2, x2 = np.histogram( + result_dataclass.ie_rotated, bins=80 + ) # , bins=np.linspace(-3, 8, 80)) + + histogram_plot.plot( + x, + y, + stepMode="center", + fillLevel=0, + fillOutline=True, + brush=(*self.ground_state_colour, 200), + pen=pg.mkPen(None), + ) + histogram_plot.plot( + x2, + y2, + stepMode="center", + fillLevel=0, + fillOutline=True, + brush=(*self.excited_state_colour, 200), + pen=pg.mkPen(None), + ) + + histogram_plot.addLine( + x=result_dataclass.threshold, + label=f"{result_dataclass.threshold:.2f}", + labelOpts={"position": 0.95}, + pen={"color": "white", "dash": [20, 20]}, + ) + + return histogram_plot + + def update_plots(self): + """ + Updater function that selects the correct data values to present on the screen when a different qubit is + selected from the qubit list + """ + + qubit_id = self.qubit_list.currentIndex() + + for plot_region, (data_key, data_list) in zip( + self.plot_regions, self.reset_results_dictionary.items() + ): + data = data_list[qubit_id] + scatter = plot_region.getItem(0, 0) + histogram = plot_region.getItem(1, 0) + + scatter.clear() + histogram.clear() + + self.plot_to_scatter(scatter, data) + self.plot_to_histogram(histogram, data) + + self.set_table() + + def _populate_list(self): + """ + Populates the dropdown list with the qubit names given as the name attributes of the result dataclasses. + """ + + key = list(self.reset_results_dictionary.keys())[0] + + dataclasses = self.reset_results_dictionary[key] + + for result in dataclasses: + self.qubit_list.addItem(result.name) + + +def launch_reset_gui(data_dictionary): + + app = QApplication(sys.argv) + program = ActiveResetGUI(data_dictionary) + app.exec_() diff --git a/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py new file mode 100644 index 00000000..174f642b --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py @@ -0,0 +1,451 @@ +""" +A GUI for presenting state discrimination data for a multi-qubit setup. +""" + +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +import pyqtgraph as pg +import numpy as np + + +# TODO: sort out the axes so plot 4 has the axes around the image rather than the plot area + + +class DiscriminatorGui(QWidget): + def __init__(self, results_dataclasses): + """ + GUI for presenting per-qubit readout information as well as a general overview dashboard which + contains some information. More to be added later. + + @param results_dataclasses: results dataclass + """ + + self.num_qubits = len(results_dataclasses) + self.results_dataclasses = results_dataclasses + super(DiscriminatorGui, self).__init__() + self.initialise_ui() + self.setup_dashboard_tab() + self._populate_list() + self._list_by_fidelity() + self.show() + + def setup_dashboard_tab(self): + """ + Sets up the dashboard tab with overview information about the qubit register. + @return: + """ + + # set the widget colour here - maybe can be set in a json somewhere + self.dashboard_widget_colour = (244, 244, 244) + self.dashboard_tab_layout = QGridLayout() + + self.dashboard_tab.setLayout(self.dashboard_tab_layout) + + # widets + + # make read only + self.dashboard_list = QTableWidget() + self.dashboard_list.setRowCount(self.num_qubits) + self.dashboard_list.setColumnCount(2) + + # make table read-only + self.dashboard_list.setEditTriggers(QTableWidget.NoEditTriggers) + self.dashboard_list.setMinimumWidth(self.dashboard_list.sizeHint().width()) + self.dashboard_list.setShowGrid(False) + + self.dashboard_list.setHorizontalHeaderItem( + 0, QTableWidgetItem("Qubits by fidelity") + ) + self.dashboard_list.setHorizontalHeaderItem(1, QTableWidgetItem("Fidelity")) + + # self.dashboard_list.setGeometry() + self.average_fidelity = np.mean( + [result.fidelity for result in self.results_dataclasses] + ) + + fidelity_average = QLabel(f"Average fidelity is {self.average_fidelity:.2f}%") + average_overlap = QLabel(f"Average overlap is {0.1}") + + fidelity_average.setStyleSheet( + f"background-color:rgb{self.dashboard_widget_colour}" + ) + average_overlap.setStyleSheet( + f"background-color:rgb{self.dashboard_widget_colour}" + ) + + fidelity_average.setAlignment(Qt.AlignCenter) + average_overlap.setAlignment(Qt.AlignCenter) + + metadata = QLabel(f"Some other statistics") + + error_correlations = QLabel("Error correlations") + + metadata.setStyleSheet(f"background-color:rgb{self.dashboard_widget_colour}") + error_correlations.setStyleSheet( + f"background-color:rgb{self.dashboard_widget_colour}" + ) + + metadata.setAlignment(Qt.AlignCenter) + error_correlations.setAlignment(Qt.AlignCenter) + + self.dashboard_tab_layout.addWidget(self.dashboard_list, 0, 0, 5, 1) + self.dashboard_tab_layout.addWidget(fidelity_average, 0, 1, 1, 2) + self.dashboard_tab_layout.addWidget(average_overlap, 0, 3, 1, 2) + self.dashboard_tab_layout.addWidget(metadata, 1, 1, 1, 2) + self.dashboard_tab_layout.addWidget(error_correlations, 1, 3, 1, 2) + + self.dashboard_list.itemDoubleClicked.connect(self.switch_to_qubit_tab) + self.dashboard_list.setMaximumWidth(200) + + def initialise_ui(self): + """ + Initialise the main UI for the per-qubit tab. + @return: + """ + + main_layout = QGridLayout() + + self.readout_tab = QWidget() + self.dashboard_tab = QWidget() + + self.tabs = QTabWidget() + + self.tabs.addTab(self.dashboard_tab, "Dashboard") + self.tabs.addTab(self.readout_tab, "Qubits") + + box_layout = QHBoxLayout(self) + + self.readout_tab.setLayout(box_layout) + + self.ground_state_colour = (100, 149, 237) + self.excited_state_colour = (255, 185, 15) + + # create some widgets + + self.left = pg.LayoutWidget() + self.right = pg.LayoutWidget() + + self.qubit_list = QComboBox() + + self.key_layout = QVBoxLayout() + self.key = QWidget() + self.key.setLayout(self.key_layout) + + self.ground_state_label = QLabel("Ground state") + self.excited_state_label = QLabel("Excited state") + + self.ground_state_label.setAlignment(Qt.AlignCenter) + self.excited_state_label.setAlignment(Qt.AlignCenter) + + self.ground_state_label.setStyleSheet( + f"background-color:rgb{self.ground_state_colour}; border-radius:5px" + ) + self.excited_state_label.setStyleSheet( + f"background-color:rgb{self.excited_state_colour}; border-radius:5px" + ) + + self.key_layout.addWidget(self.ground_state_label) + self.key_layout.addWidget(self.excited_state_label) + + self.graphics_window = pg.GraphicsLayoutWidget() + self.plt1 = self.graphics_window.addPlot( + title='Original data' + ) + self.plt2 = self.graphics_window.addPlot( + title='Rotated data' + ) + self.graphics_window.nextRow() + self.plt3 = self.graphics_window.addPlot( + title='1D Histogram' + ) + self.plt4 = self.graphics_window.addPlot( + title='Fidelities' + ) + + self.left.addWidget(self.qubit_list, 0, 0) + self.left.addWidget(self.key, 1, 0) + # add a blank frame to take up some space so the state key labels aren't massive + self.left.addWidget(QFrame(), 2, 0, 3, 1) + + self.right.addWidget(self.graphics_window) + + splitter = QSplitter(Qt.Horizontal) + splitter.addWidget(self.left) + splitter.addWidget(self.right) + + box_layout.addWidget(splitter) + main_layout.addWidget(self.tabs, 0, 0) + + self.setLayout(main_layout) + + # self.layout().addWidget(self.tabs) + + QApplication.setStyle(QStyleFactory.create("Cleanlooks")) + + self.setGeometry(100, 100, 1100, 700) + self.setWindowTitle("Readout viewer") + + self.qubit_list.currentIndexChanged.connect(self.update_plots) + + def switch_to_qubit_tab(self): + """ + function that switches to a specific qubit's tab when it is selected from the dashboard list + """ + + unsorted_qubit_id = self.dashboard_list.currentRow() + # sorted_qubit_ids is a list of tuples (qubit_id, fidelity). Get id by taking 0 index + qubit_id = self.sorted_qubit_ids[unsorted_qubit_id][0] + + self.qubit_list.setCurrentIndex(qubit_id) + self.update_plots() + self.tabs.setCurrentIndex(1) + + def clear_plots(self): + """ + Clears all plots on the per-qubit view so they can be updated and we don't end up with the plots + all sitting on top of each other + """ + self.plt1.clear() + self.plt2.clear() + self.plt3.clear() + self.plt4.clear() + + def _generate_unrotated_scatter_plot(self, result): + """ + Function to generate the first plot (unrotated scatter plot) + @param result: the result dataclass corresponding to the qubit which we are plotting information about + """ + + ig, qg, ie, qe = result.get_data() + + original_data_g = pg.ScatterPlotItem( + ig, + qg, + brush=(*self.ground_state_colour, 100), + symbol="s", + size="2", + pen=pg.mkPen(None), + ) + + original_data_e = pg.ScatterPlotItem( + ie, + qe, + brush=(*self.excited_state_colour, 100), + symbol="s", + size="2", + pen=pg.mkPen(None), + ) + + self.plt1.addItem(original_data_g) + self.plt1.addItem(original_data_e) + self.plt1.setAspectLocked() + + def _generate_rotated_data_plot(self, result): + """ + Generates the second plot (rotated data). + @param result: the result dataclass corresponding to the qubit which we are plotting information about + @return: + """ + + ig_rotated, qg_rotated, ie_rotated, qe_rotated = result.get_rotated_data() + + rotated_data_g = pg.ScatterPlotItem( + ig_rotated, + qg_rotated, + brush=(*self.ground_state_colour, 100), + symbol="s", + size="2", + pen=pg.mkPen(None), + ) + + rotated_data_e = pg.ScatterPlotItem( + ie_rotated, + qe_rotated, + brush=(*self.excited_state_colour, 100), + symbol="s", + size="2", + pen=pg.mkPen(None), + ) + + self.plt2.addItem(rotated_data_g) + self.plt2.addItem(rotated_data_e) + self.plt2.setAspectLocked() + + def _generate_1d_histogram(self, result): + """ + Generates the third plot (the 1d histogram corresponding to the rotated data) + @param result: the result dataclass corresponding to the qubit which we are plotting information about + """ + + ig_hist_y, ig_hist_x = np.histogram(result.ig_rotated, bins=80) + ie_hist_y, ie_hist_x = np.histogram(result.ie_rotated, bins=80) + + self.plt3.plot( + ig_hist_x, + ig_hist_y, + stepMode="center", + fillLevel=0, + fillOutline=False, + brush=(*self.ground_state_colour, 200), + pen=pg.mkPen(None), + ) + + self.plt3.plot( + ie_hist_x, + ie_hist_y, + stepMode="center", + fillLevel=0, + fillOutline=False, + brush=(*self.excited_state_colour, 200), + pen=pg.mkPen(None), + ) + + self.threshold_line = self.plt3.addLine( + x=result.threshold, + label=f"{result.threshold:.2f}", + labelOpts={"position": 0.95}, + pen={"color": "white", "dash": [20, 20]}, + ) + + def _generate_confusion_matrix_plot(self, result): + """ + Generates the confusion matrix plot showing the state preparation vs measurement probabilities. + @param result: the result dataclass corresponding to the qubit which we are plotting information about + """ + + img = pg.ImageItem(image=result.confusion_matrix(), rect=[1, 1, 1, 1]) + img.setColorMap("viridis") + self.plt4.addItem(img) + self.plt4.invertY(True) + self.plt4.setAspectLocked() + self.plt4.showAxes(True) + + # all of this needs relabelling to prep_g, meas_g ... etc + + gg_label = pg.TextItem("|g>", anchor=(1, 0.5)) + ge_label = pg.TextItem("|g>", anchor=(0.5, 0)) + eg_label = pg.TextItem("|e>", anchor=(1, 0.5)) + ee_label = pg.TextItem("|e>", anchor=(0.5, 0)) + + # anchor so we set the centre position of the text rather than the top left + gg_fid_label = pg.TextItem( + f"{100 * result.gg:.2f}%", color=(0, 0, 0), anchor=(0.5, 0.5) + ) + ge_fid_label = pg.TextItem( + f"{100 * result.ge:.2f}%", color=(255, 255, 255), anchor=(0.5, 0.5) + ) + eg_fid_label = pg.TextItem( + f"{100 * result.eg:.2f}%", color=(255, 255, 255), anchor=(0.5, 0.5) + ) + ee_fid_label = pg.TextItem( + f"{100 * result.ee:.2f}%", color=(0, 0, 0), anchor=(0.5, 0.5) + ) + + gg_label.setPos(1, 1.25) + ge_label.setPos(1.25, 2) + eg_label.setPos(1, 1.75) + ee_label.setPos(1.75, 2) + + gg_fid_label.setPos(1.25, 1.25) + ge_fid_label.setPos(1.75, 1.25) + eg_fid_label.setPos(1.25, 1.75) + ee_fid_label.setPos(1.75, 1.75) + + x_axis = self.plt4.getAxis("bottom") + y_axis = self.plt4.getAxis("left") + + x_axis.setRange(1, 2) + y_axis.setRange(1, 2) + + self.plt4.setXRange(1, 2) + self.plt4.setYRange(1, 2) + + x_axis.setLabel("Measured") + y_axis.setLabel("Prepared") + + x_axis.setTicks([[(1.25, "|g>"), (1.75, "|e>")]]) + y_axis.setTicks([[(1.25, "|g>"), (1.75, "|e>")]]) + + self.plt4.addItem(gg_fid_label) + self.plt4.addItem(ge_fid_label) + self.plt4.addItem(eg_fid_label) + self.plt4.addItem(ee_fid_label) + + def update_plots(self): + """ + Clears and updates all the plots on the qubit-specific tab so they show data from the correct qubit + @return: + """ + + self.clear_plots() + + index = self.qubit_list.currentIndex() + result = self.results_dataclasses[index] + + self._generate_unrotated_scatter_plot(result) + self._generate_rotated_data_plot(result) + self._generate_1d_histogram(result) + self._generate_confusion_matrix_plot(result) + + def _populate_list(self): + """ + Helper function to generate the list of qubits on the per-qubit tab so they can be cycled through + """ + + for i in range(self.num_qubits): + self.qubit_list.addItem(f"Qubit {i + 1}") + + def _list_by_fidelity(self): + + unsorted_qubit_fidelities = [ + result.fidelity for result in self.results_dataclasses + ] + qubit_ids = range(0, self.num_qubits) + + self.sorted_qubit_ids = [ + (qubit_id, fidelity) + for qubit_id, fidelity in sorted( + zip(qubit_ids, unsorted_qubit_fidelities), key=lambda pair: pair[1] + ) + ][::-1] + + for i, (qubit_id, fidelity) in enumerate(self.sorted_qubit_ids): + qubit_name = f"Qubit {qubit_id + 1}" + + self.dashboard_list.setItem(i, 0, QTableWidgetItem(qubit_name)) + self.dashboard_list.setItem(i, 1, QTableWidgetItem(f"{fidelity:.2f}%")) + + +def launch_discriminator_gui(results): + app = pg.mkQApp() + loader = DiscriminatorGui(results) + pg.exec() + + +# example with fake data +if __name__ == "__main__": + + num_qubits = 10 + + from qualang_tools.analysis.multi_qubit_discriminator.independent_multi_qubit_discriminator import ( + independent_multi_qubit_discriminator, + ) + + iq_state_g = np.random.multivariate_normal( + (0, -0.2), ((1.5, 0.0), (0.0, 1.5)), (5000, num_qubits) + ).T + iq_state_e = np.random.multivariate_normal( + (-1.8, -3.0), ((1.5, 0), (0, 1.5)), (5000, num_qubits) + ).T + + igs, qgs = iq_state_g + ies, qes = iq_state_e + + results_list = np.stack([igs, qgs, ies, qes], axis=1) + + results = independent_multi_qubit_discriminator( + results_list, b_plot=False, b_print=False + ) + + launch_discriminator_gui(results) + diff --git a/qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py new file mode 100644 index 00000000..0abe35fc --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py @@ -0,0 +1,200 @@ +import numpy as np +import matplotlib.pyplot as plt +import itertools +from tqdm import tqdm +from dataclasses import dataclass + +from qualang_tools.analysis.discriminator import two_state_discriminator + + +def _list_is_rectangular(list): + for item in list: + if len(item) != len(list[0]): + return False + + return True + + +# switch from igs, iqs, ies, qes to list of [ig, iq, ie, qe], [ig2, iq2, ie2, qe2] +def independent_multi_qubit_discriminator( + results_list, b_print=True, b_plot=False, text=False +): + assert _list_is_rectangular( + results_list + ), "there is missing data in the results list." + + result_dataclasses = [] + + for i, result in enumerate(results_list): + result_dataclass = _DiscriminatorDataclass( + f"Qubit_{i}", + *two_state_discriminator(*result, b_print=b_print, b_plot=b_plot), + *result, + ) + + result_dataclasses.append(result_dataclass) + + # recursively calculate the overall independent confusion matrix + A = result_dataclasses[0].confusion_matrix() + for i in tqdm(range(0, len(result_dataclasses) - 1)): + B = result_dataclasses[i + 1].confusion_matrix() + A = np.kron(A, B) + + # rename the variable to make things a little clearer + outcome = A + fig, ax = plt.subplots() + ax.imshow(outcome) + + num_qubits = result_dataclasses.__len__() + + if text: + state_strings = _generate_labels(num_qubits) + ticks = np.arange(0, 2**num_qubits) + ax.set_xticks(ticks) + ax.set_yticks(ticks) + + ax.set_xticklabels(labels=state_strings) + ax.set_yticklabels(labels=state_strings) + + ax.set_ylabel("Prepared") + ax.set_xlabel("Measured") + + ids = list(itertools.product(np.arange(0, outcome.__len__()), repeat=2)) + + for id in ids: + # if on the diagonal id[0] == id[1] and the imshow pixel will be light so make text dark. + # otherwise pixel will be dark so make text light + color = "k" if np.all(np.diff(id) == 0) else "w" + ax.text( + *id, f"{100 * outcome[id]:.1f}%", ha="center", va="center", color=color + ) + + ax.set_title("Fidelities") + plt.show() + + return result_dataclasses + + +def _generate_labels(length): + out = "{0:b}".format(length) + + strings = list(itertools.product([0, 1], repeat=length)) + out = [] + + # if we want to use g/e instead of 0/1 + for string in strings: + edit_string = "".join(str(x) for x in string) + + edit_string = edit_string.replace("0", "g") + edit_string = edit_string.replace("1", "e") + + state_string = "|" + edit_string + ">" + out.append(state_string) + + return out + + +@dataclass +class _DiscriminatorDataclass: + """ + Dataclass for holding the results from a two state discriminator run. + Helper method self.confusion_matrix() generates the confusion matrix from this data. + """ + + name: str + + # parameters + angle: float + threshold: float + fidelity: float + gg: np.ndarray + ge: np.ndarray + eg: np.ndarray + ee: np.ndarray + + # data + ig: np.ndarray + qg: np.ndarray + ie: np.ndarray + qe: np.ndarray + + def __post_init__(self): + """ + adds rotated data to the dataclass + @return: None + """ + self.generate_rotation_data() + + def _add_attribute(self, attribute_name, value): + self.__setattr__(attribute_name, value) + + def confusion_matrix(self): + """ + Generates and returns the 2x2 state confusion matrix + @return: 2x2 confusion matrix of state fidelity + """ + return np.array([[self.gg, self.ge], [self.eg, self.ee]]) + + def get_params(self): + """ + Helper method to quickly obtain useful parameters held in the dataclass + @return: parameters obtained from the discrimination + """ + return ( + self.angle, + self.threshold, + self.fidelity, + self.gg, + self.ge, + self.eg, + self.ee, + ) + + def get_data(self): + """ + Helper method to obtain the data stored in the dataclass + @return: ground and excited state I/Q data. + """ + return self.ig, self.qg, self.ie, self.qe + + def get_rotated_data(self): + """ + Helper method to return the rotated (PCA) data from the measurement. + @return: ground and excited state I/Q data that has been rotated so maximum information is in I plane. + """ + return self.ig_rotated, self.qg_rotated, self.ie_rotated, self.qe_rotated + + def generate_rotation_data(self): + """ + Generates the rotated (PCA) data from the measurement. + @return: None + """ + C = np.cos(self.angle) + S = np.sin(self.angle) + + # Condition for having e > Ig + if np.mean((self.ig - self.ie) * C - (self.qg - self.qe) * S) > 0: + self.angle += np.pi + C = np.cos(self.angle) + S = np.sin(self.angle) + + self.ig_rotated = self.ig * C - self.qg * S + self.qg_rotated = self.ig * S + self.qg * C + self.ie_rotated = self.ie * C - self.qe * S + self.qe_rotated = self.ie * S + self.qe * C + + +if __name__ == "__main__": + iq_state_g = np.random.multivariate_normal( + (0, -0.2), ((1.5, 0.0), (0.0, 1.5)), (5000, 15) + ).T + iq_state_e = np.random.multivariate_normal( + (-1.8, -3.0), ((1.5, 0), (0, 1.5)), (5000, 15) + ).T + + Igs, Qgs = iq_state_g + Ies, Qes = iq_state_e + + results_list = np.stack([Igs, Qgs, Ies, Qes], axis=1) + + results_dataclasses = independent_multi_qubit_discriminator(results_list) diff --git a/qualang_tools/config/gui.py b/qualang_tools/config/gui.py index d6ca1d0e..73e3b54a 100644 --- a/qualang_tools/config/gui.py +++ b/qualang_tools/config/gui.py @@ -79,7 +79,7 @@ def render_page_content(pathname): content = html.Div(id="page-content", style=CONTENT_STYLE) -app.layout = html.Div([dcc.Location(id="url"), sidebar, content]) +app.main_layout = html.Div([dcc.Location(id="url"), sidebar, content]) guiserver = app.server diff --git a/qualang_tools/control_panel/gui.py b/qualang_tools/control_panel/gui.py new file mode 100644 index 00000000..1860dd15 --- /dev/null +++ b/qualang_tools/control_panel/gui.py @@ -0,0 +1,444 @@ +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +import pyqtgraph as pg +import numpy as np +import datetime +from qualang_tools.control_panel import ManualOutputControl + + +class ControlPanelGui(QWidget): + + def __init__(self, config, ports=None): + + + super(ControlPanelGui, self).__init__() + + self.test = True + + self.config = config + + self.analogue_outputs = {} + self.digital_outputs = {} + + self.ports = ports + + if not self.test: + + if self.ports: + self.manual_output_control = ManualOutputControl.ports(**ports, host="172.16.2.103", port=85) + + else: + self.manual_output_control = ManualOutputControl(self.config, host="172.16.2.103", port=85) + + self.initialise_ui() + self._perform_health_check() + + self.show() + + + def initialise_ui(self): + + self.main_layout = QVBoxLayout() + + self.layout = QHBoxLayout() + + + + self.text_layout_widget = pg.LayoutWidget() + self.general_buttons = QWidget() + self.general_buttons_layout = QVBoxLayout() + self.general_buttons.setLayout(self.general_buttons_layout) + self.elements_layout = pg.LayoutWidget() + + self.setLayout(self.main_layout) + + self.info_box = QTextEdit() + self.info_box.setReadOnly(True) + + self.main_layout.addLayout(self.layout, stretch=3) + self.main_layout.addWidget(self.text_layout_widget, stretch=1) + + # self.layout.addWidget(self.left, stretch=2) + self.layout.addWidget(self.general_buttons, stretch=1) + self.layout.addWidget(self.elements_layout, stretch=5) + + self.middle_buttons_setup() + self.text_layout_widget.addWidget(self.info_box) + + # self.set_up_tabs() + self.set_up_main_page() + + self.setGeometry(50, 50, 1300, 800) + self.setWindowTitle('Control panel') + + self.add_info_to_box('Control panel set up') + + + def _turn_off_all_digital(self): + for _, widget in self.digital_outputs.items(): + widget.togglebutton.setChecked(False) + + def _turn_off_all_analogue(self): + for _, widget in self.analogue_outputs.items(): + widget.togglebutton.setChecked(False) + + def middle_buttons_setup(self): + self.general_buttons_layout.addWidget(QFrame()) + + self.turn_off_analogue_button = QPushButton('Turn off analogue outputs') + self.turn_off_analogue_button.clicked.connect(self._turn_off_all_analogue) + self.general_buttons_layout.addWidget(self.turn_off_analogue_button) + + self.turn_off_digital_button = QPushButton('Turn off digital outputs') + self.turn_off_digital_button.clicked.connect(self._turn_off_all_digital) + self.general_buttons_layout.addWidget(self.turn_off_digital_button) + + self.turn_off_all_button = QPushButton('Turn off all outputs') + self.turn_off_all_button.clicked.connect(self._turn_off_all) + self.general_buttons_layout.addWidget(self.turn_off_all_button) + + + self.perform_health_check = QPushButton('Perform health check') + self.perform_health_check.clicked.connect(self._perform_health_check) + self.general_buttons_layout.addWidget(self.perform_health_check) + + self.close_all = QPushButton('Close QMs') + self.close_all.clicked.connect(self._close_all) + self.general_buttons_layout.addWidget(self.close_all) + + self.general_buttons_layout.addWidget(QFrame()) + + def _close_all(self): + self.manual_output_control.close() + self.add_info_to_box('Closed all Quantum Machines') + + def add_info_to_box(self, text_to_add): + string = f'{datetime.datetime.now().strftime("%H:%M:%S")}'.ljust(15, ' ') + f'{text_to_add}' + + self.info_box.append(string) + + def _perform_health_check(self): + + if not self.test: + health_check_result = self.manual_output_control.qmm.perform_healthcheck() + health_check_string = 'passed' if health_check_result else 'failed' + self.add_info_to_box(f'Health check result: {health_check_string}') + else: + self.add_info_to_box('in test mode - cannot perform health check') + + def _turn_off_all(self): + self._turn_off_all_digital() + self._turn_off_all_analogue() + + def set_up_main_page(self): + + title = 'Ports' if self.ports is not None else 'Elements' + + self.elements_widget = pg.LayoutWidget() + + self.make_elements_page(title) + self.elements_layout.addWidget(self.elements_widget) + + def make_elements_page(self, title): + + elements_per_row = 4 + + analogue_elements, digital_elements = self.get_elements() + title_widget = QLabel(f'{title}') + title_widget.setMaximumHeight(30) + self.elements_widget.addWidget(title_widget) + self.elements_widget.nextRow() + + for i, analogue_element in enumerate(analogue_elements, 1): + + self.elements_widget.addWidget(self.make_analogue_element_widget(analogue_element)) + + if i % elements_per_row == 0: + self.elements_widget.nextRow() + + for j, digital_element in enumerate(digital_elements, 1): + + self.elements_widget.addWidget(self.make_digital_element_widget(digital_element)) + + if (i + j) % elements_per_row == 0: + self.elements_widget.nextRow() + + + def make_analogue_element_widget(self, name): + + widget = AnalogueElementWidget(name, self) + self.analogue_outputs[name] = widget + return widget + + def make_digital_element_widget(self, name): + + widget = DigitalElementWidget(name, self) + self.digital_outputs[name] = widget + return widget + + def get_elements(self): + + if self.test: + elements = self.config.get('elements') + analogue_elements = [] + digital_elements = [] + + for key, dict in elements.items(): + if 'digitalInputs' in dict.keys(): + digital_elements.append(key) + else: + analogue_elements.append(key) + + return analogue_elements, digital_elements + + else: + return self.manual_output_control.analog_elements, self.manual_output_control.digital_elements + + +class AnalogueElementWidget(QGroupBox): + + def __init__(self, name, app_window): + super(AnalogueElementWidget, self).__init__(name) + self.name = name + self.app_window = app_window + self.setAlignment(Qt.AlignCenter) + self.vbox = QVBoxLayout() + self.vbox.setAlignment(Qt.AlignCenter) + self.setLayout(self.vbox) + + self.amplitude_label = QLabel('Amplitude (mV)') + + self.amplitude = QDoubleSpinBox() + self.amplitude.valueChanged.connect(self.set_amplitude) + self.amplitude.setRange(-500, 500) + self.amplitude.setSingleStep(1) + + self.frequency_label = QLabel('Frequency (MHz)') + + self.frequency = QDoubleSpinBox() + self.frequency.valueChanged.connect(self.set_frequency) + self.frequency.setRange(-300, 300) + self.frequency.setSingleStep(int(1)) + + self.togglebutton = QPushButton('Off') + self.togglebutton.setCheckable(True) + self.togglebutton.setChecked(False) + + self.vbox.addWidget(self.amplitude_label) + self.vbox.addWidget(self.amplitude) + self.vbox.addWidget(self.frequency_label) + self.vbox.addWidget(self.frequency) + self.vbox.addWidget(self.togglebutton) + + self.togglebutton.toggled.connect(self.toggle_element_on_off) + + def set_amplitude(self): + self.app_window.manual_output_control.set_amplitude(self.name, self.amplitude.value() * 1e-3) # given in mv + # self.app_window.add_info_to_box(f'{self.name} set to {self.amplitude.value()} mV') + + def set_frequency(self): + self.app_window.manual_output_control.set_frequency(self.name, self.frequency.value() * 1e6) + # self.app_window.add_info_to_box(f'{self.name} set to {self.frequency.value()} MHz') + + def toggle_element_on_off(self): + if self.togglebutton.isChecked(): + self.togglebutton.setText('On') + + if not self.app_window.test: + print(self.app_window.manual_output_control.analog_status()) + self.set_amplitude() + self.set_frequency() + self.app_window.manual_output_control.turn_on_element(self.name) + + self.app_window.add_info_to_box(f'{self.name} turned on with amplitude {self.amplitude.value()}' + f' mV and frequency {self.frequency.value()} MHz') + + + else: + self.togglebutton.setText('Off') + + if not self.app_window.test: + self.app_window.manual_output_control.turn_off_elements(self.name) + + self.app_window.add_info_to_box(f'{self.name} turned off') + + +class DigitalElementWidget(QGroupBox): + + def __init__(self, name, app_window): + super(DigitalElementWidget, self).__init__(name) + + + self.name = name + self.app_window = app_window + self.setAlignment(Qt.AlignCenter) + self.vbox = QVBoxLayout() + self.vbox.setAlignment(Qt.AlignCenter) + self.setLayout(self.vbox) + + self.togglebutton = QPushButton('Off') + self.togglebutton.setCheckable(True) + self.togglebutton.setChecked(False) + + self.vbox.addWidget(self.togglebutton) + + self.togglebutton.toggled.connect(self.toggle_element_on_off) + + def toggle_element_on_off(self): + + # currently on + if self.togglebutton.isChecked(): + self.togglebutton.setText('On') + + if not self.app_window.test: + self.app_window.manual_output_control.digital_on(self.name) + + self.app_window.add_info_to_box(f'{self.name} turned on') + + + else: + self.togglebutton.setText('Off') + + if not self.app_window.test: + self.app_window.manual_output_control.digital_off(self.name) + + self.app_window.add_info_to_box(f'{self.name} turned off') + + + +if __name__ == '__main__': + qop_ip = "172.16.2.103" + readout_time = 256 # ns + + config = { + "version": 1, + "controllers": { + "con1": { + "analog_outputs": {1: {"offset": 0.0}, # G1 + 2: {"offset": 0.0}, # G2 + 3: {"offset": 0.0}, # I qubit + 4: {"offset": 0.0}, # Q qubit + 5: {"offset": 0.0}, # I resonator + 6: {"offset": 0.0}, # Q resonator + }, + "digital_outputs": + {i: {} for i in range(1, 11)}, + + "analog_inputs": { + 1: {"offset": 0.0}, + 2: {"offset": 0.0}, + }, + }, + + }, + "elements": { + "G1_sticky": { + "singleInput": {"port": ("con1", 1)}, + "hold_offset": {"duration": 12}, + "operations": { + "sweep": "sweep", + }, + }, + "G2_sticky": { + "singleInput": {"port": ("con1", 2)}, + "hold_offset": {"duration": 12}, + "operations": { + "sweep": "sweep", + }, + }, + "G1": { + "singleInput": {"port": ("con1", 1)}, + "operations": { + "sweep": "sweep", + }, + }, + "G2": { + "singleInput": {"port": ("con1", 2)}, + "operations": { + "sweep": "sweep", + }, + }, + "RF": { + "singleInput": {"port": ("con1", 3)}, + "time_of_flight": 200, + "smearing": 0, + "intermediate_frequency": 100e6, + "outputs": {"out1": ("con1", 1)}, + "operations": {"measure": "measure"}, + }, + 'trigger_x': { + "digitalInputs": { + "trigger_qdac": { + 'port': ('con1', 1), + 'delay': 0, + 'buffer': 0 + } + }, + 'operations': { + 'trig': 'trigger' + } + }, + 'trigger_y': { + "digitalInputs": { + "trigger_qdac": { + 'port': ('con1', 2), + 'delay': 0, + 'buffer': 0 + } + }, + 'operations': { + 'trig': 'trigger' + } + }, + + }, + "pulses": { + "sweep": { + "operation": "control", + "length": 100, + "waveforms": { + "single": "sweep", + }, + }, + "trigger": { + "operation": "control", + "length": 100, + "digital_marker": "ON", + }, + "measure": { + "operation": "measurement", + "length": readout_time, + "waveforms": {"single": "measure"}, + "digital_marker": "ON", + "integration_weights": { + "cos": "cos", + "sin": "sin", + }, + }, + } + , + "waveforms": { + "sweep": {"type": "constant", "sample": 0.5}, + "measure": {"type": "constant", "sample": 0.001}, + "zero": {"type": "constant", "sample": 0.00}, + }, + "digital_waveforms": {"ON": {"samples": [(1, 0)]}}, + "integration_weights": { + "cos": { + "cosine": [(1.0, readout_time)], + "sine": [(0.0, readout_time)], + }, + "sin": { + "cosine": [(0.0, readout_time)], + "sine": [(1.0, readout_time)], + }, + }, + } + + def main(): + app = pg.mkQApp() + # loader = multiQubitReadoutPresenter(results) + loader = ControlPanelGui(config)#, ports={'analog_ports': [1, (4, 5), 6]}) + pg.exec() + + main() \ No newline at end of file diff --git a/qualang_tools/plot/__init__.py b/qualang_tools/plot/__init__.py index 1b507128..101bb43e 100644 --- a/qualang_tools/plot/__init__.py +++ b/qualang_tools/plot/__init__.py @@ -1,3 +1,9 @@ from qualang_tools.plot.plot import interrupt_on_close +from qualang_tools.analysis.multi_qubit_discriminator.discriminator_gui import DiscriminatorGui -__all__ = ["interrupt_on_close"] +__all__ = [ + "interrupt_on_close", + "ActiveResetGUI", + "DiscriminatorGui", + "launch_reset_gui" +] diff --git a/qualang_tools/plot/example.py b/qualang_tools/plot/example.py new file mode 100644 index 00000000..4d44802f --- /dev/null +++ b/qualang_tools/plot/example.py @@ -0,0 +1,48 @@ +import numpy as np +from gui import GUI + + +def fake_function(x): + x0 = (np.random.rand()) * 0.8 + return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) + + +def fake_data(): + x = np.linspace(0, 1, 100) + y = fake_function(x) + y += np.random.rand(y.size) * 0.2 + return x * 1e6, y + 3 * np.random.rand() + + +# set up qubit grid shape +i = 2 +j = 2 +grid_size = (i, j) + +# launch the gui +gui = GUI(grid_size, ipython=True) + +# the address for each gui element is a tuple (i, j) +qubits = ((0, 0), (0, 1), (1, 0), (1, 1)) +x = np.linspace(-1, 2, 100) +y = np.linspace(-1, 3, 100) + +random_data = lambda: np.fromfunction( + lambda i, j: np.sin(i / 8 * np.random.rand()) * j / 128, (100, 100), dtype=float +) + np.random.rand(100, 100) + + +# you have three options: 0d (text), 1d, and 2d. +# for 0d, just have the data item (text or digit) +# for 1d, you need to provide x and y data, both 1d array-like objects +# for 2d, you need to provide x and y data (1d array-like), and your 2d array-like data array + +# the way to do it is to call gui.plot_xd(qubit_address (e.g. (0, 0), layer name, x, y....) + +for qubit in qubits: + gui.plot_0d(qubit, "Fidelity", f"{np.random.rand() * 100:.2f}%") + gui.plot_1d(qubit, "spect", *fake_data()) + gui.plot_2d(qubit, "osc", x, y, random_data()) + + +# if you like, you can update the data in a layer by gui.update_layer(qubit_address (i, j), layer_name, data) diff --git a/qualang_tools/plot/gui/README.md b/qualang_tools/plot/gui/README.md new file mode 100644 index 00000000..474617dd --- /dev/null +++ b/qualang_tools/plot/gui/README.md @@ -0,0 +1,70 @@ +# GUI + +This package is for an interactive plotter for use with +multiple qubit systems. The GUI is initialised with a tuple that +sets up the size of the grid - typically you would use one cell per pixel, +so the tuple represents the shape of the qubit grid. + +Using ```ipython=True``` as a flag means if the script in which you initialise the +GUI is run through an ipython console (such as in pycharm), the plotter remains interactive +and does not block the python thread. + +The GUI is a grid with each cell representing data from a given qubit. Plots pertaining to each +qubit can be stacked on top of each other, with 0d (text), 1d, and 2d plots available. +You can then select which plot to display by selecting it from the list within the +cell. + +The following code demonstrates a simple example of how to use the GUI. The included example.py file +may also be a helpful start. + +```python +from grid_gui import GUI +import numpy as np +# set up qubit grid shape +qubit_grid_shape = (4, 4) + +# launch the gui +gui = GUI(qubit_grid_shape, ipython=True) + +# the address for each gui element is a tuple (i, j), representing its position in the grid. +# not every cell in the GUI needs data added to it, which may be the case if only some qubits +# are being used for a given experiment. +qubit_addresses = ((0, 0), (0, 1), (1, 0), (1, 1)) + +x_len = 100 +y_len = 100 +x = np.linspace(-1, 2, x_len) +y = np.linspace(-1, 3, y_len) + +random_data = lambda: np.fromfunction( + lambda i, j: np.sin(i / 8 * np.random.rand()) * j / 128, (x_len, y_len), dtype=float +) + np.random.rand(x_len, y_len) + + +# you have three options: 0d (text), 1d, and 2d. +# for 0d, just have the data item (text or digit) +# for 1d, you need to provide x and y data, both 1d array-like objects +# for 2d, you need to provide x and y data (1d array-like), and your 2d array-like data array + +# the way to do it is to call gui.plot_xd(qubit_address (e.g. (0, 0), layer name, x, y....) + +for qubit in qubit_addresses: + gui.plot_0d(qubit, "Fidelity", f"{np.random.rand() * 100:.2f}%") + gui.plot_1d(qubit, "spect", *fake_data()) + gui.plot_2d(qubit, "osc", x, y, random_data()) + + +# if you like, you can update the data in a layer by gui.update_layer(qubit_address (i, j), layer_name, data) +``` + +To add a layer to cell (i, j), you can call one of three methods: +```gui.plot_0d(address, name, string)```, ```gui.plot_1d(address, name, x, y)```, +or ```gui.plot_2d(address, name, x, y, z)```. This adds a layer called name to the plot widget in cell +address (address should be a tuple (i, j)), with the relevant data. For a 0d plot, you are plotting a value +that is converted to a string and displayed in the middle of the plot window. + +![](image.png) + +1D and 2D plots, when hovered over by the mouse, present crosshairs and display the x, y values +under the mouse position. Double clicking the mouse puts the x, y coordinates in the console +that the GUI is being run from. \ No newline at end of file diff --git a/qualang_tools/plot/gui/__init__.py b/qualang_tools/plot/gui/__init__.py new file mode 100644 index 00000000..de74afec --- /dev/null +++ b/qualang_tools/plot/gui/__init__.py @@ -0,0 +1 @@ +from .grid_gui import GUI diff --git a/qualang_tools/plot/gui/grid_gui.py b/qualang_tools/plot/gui/grid_gui.py new file mode 100644 index 00000000..523dfb15 --- /dev/null +++ b/qualang_tools/plot/gui/grid_gui.py @@ -0,0 +1,117 @@ +""" +A GUI for presenting state discrimination data for a multi-qubit setup. +""" + + +from PyQt5.QtWidgets import * +import pyqtgraph as pg +import numpy as np +import time + +from .widget import StackedWidget + + +class GUI(QWidget): + def __init__(self, grid_size, ipython=True): + self.app = pg.mkQApp() + + super(GUI, self).__init__() + + self.grid_size = grid_size + + self.initialise_ui() + # self.setup_controls() + self.setup_qubit_plot_grid() + + if ipython: + from IPython import get_ipython + + ipy = get_ipython() + ipy.run_line_magic("gui", "qt5") + + else: + pg.exec() + + def initialise_ui(self): + self.main_window = QMainWindow() + self.main_window.resize(1000, 800) + self.setAutoFillBackground(True) + + self.main_window.setCentralWidget(self) + self.main_layout = QVBoxLayout() + self.setLayout(self.main_layout) + + self.main_window.setWindowTitle("Quantum Machines qubit viewer") + + self.main_window.show() + + def setup_controls(self): + + self.control_layout = QHBoxLayout() + self.main_layout.addLayout(self.control_layout) + + self.reset_views_button = QPushButton("Reset views") + self.control_layout.addWidget(self.reset_views_button) + self.reset_views_button.clicked.connect(self._reset_views) + + self.set_to_fidelity_button = QPushButton("Show fidelities") + self.set_to_data_button = QPushButton("Show data") + self.control_layout.addWidget(self.set_to_fidelity_button) + self.control_layout.addWidget(self.set_to_data_button) + + self.set_to_data_button.clicked.connect(self._set_all_to_data) + self.set_to_fidelity_button.clicked.connect(self._set_all_to_fidelity) + + def _set_all_to_data(self): + for row in self.widgets: + for widget in row: + # if widget.view_widget.hidden and hasattr(widget.view_widget, 'plot_item'): + # widget.view_widget.toggle_hide() + widget.Stack.setCurrentIndex(0) + + def _set_all_to_fidelity(self): + for row in self.widgets: + for widget in row: + # if not widget.view_widget.hidden and hasattr(widget.view_widget, 'plot_item'): + # widget.view_widget.toggle_hide() + widget.Stack.setCurrentIndex(1) + + def _reset_views(self): + for row in self.widgets: + for widget in row: + widget.Stack.currentWidget().autoRange() + + def setup_qubit_plot_grid(self): + self.plot_grid_layout = QGridLayout() + self.main_layout.addLayout(self.plot_grid_layout) + + self.widgets = [] + + for i in range(self.grid_size[0]): + row = [] + self.plot_grid_layout.setRowStretch(i, 1) + for j in range(self.grid_size[1]): + self.plot_grid_layout.setColumnStretch(j, 1) + widget = StackedWidget(f"qubit [{i}{j}]") + self.plot_grid_layout.addWidget(widget, i, j) + row.append(widget) + self.widgets.append(row) + + def get_widget(self, index): + return self.widgets[index[0]][index[1]] + + def update_layer(self, index, layer_name, x, y=None, z=None): + widget = self.get_widget(index) + widget.update_layer_data(layer_name, x, y, z) + + def plot_1d(self, index, name, x, y): + widget = self.get_widget(index) + widget.add_1d_plot(name, x, y) + + def plot_2d(self, index, name, x, y, z): + widget = self.get_widget(index) + widget.add_2d_plot(name, x, y, z) + + def plot_0d(self, index, name, value): + widget = self.get_widget(index) + widget.add_0d_plot(name, value) diff --git a/qualang_tools/plot/gui/image.png b/qualang_tools/plot/gui/image.png new file mode 100644 index 00000000..b8b01fdf Binary files /dev/null and b/qualang_tools/plot/gui/image.png differ diff --git a/qualang_tools/plot/gui/widget.py b/qualang_tools/plot/gui/widget.py new file mode 100644 index 00000000..855f3ace --- /dev/null +++ b/qualang_tools/plot/gui/widget.py @@ -0,0 +1,234 @@ +import pyqtgraph as pg +import numpy as np + +from PyQt5.QtWidgets import * + + +def generate_color(value): + """ + Generates RBG color on a smooth scale from 0 = red to 1 = green + @param value: the value of the parameter (0 <= value <= 1) + @return: the RGB tuple (0<->255, 0<->255, 0) + """ + return (255 * (1 - value), 255 * value, 0.0) + + +class TextWidget(pg.PlotWidget): + def __init__( + self, name, value, parent=None, background="default", plotItem=None, **kargs + ): + """ + A widget that displays the 'value' parameter as a string in the centre of the viewing window. + @param name: the widget's name + @param value: the string/number that will be plotted + @param parent: the parent widget + @param background: + @param plotItem: + @param kargs: + """ + super().__init__( + name=name, parent=parent, background=background, plotItem=plotItem, **kargs + ) + + self.setTitle(name) + self.hideAxis("bottom") + self.hideAxis("left") + self.setMouseEnabled(x=False, y=False) + + # color = generate_color(value) + self.text = pg.TextItem(f"{value}", anchor=(0.5, 0.5)) # , color=color) + + self.addItem(self.text) + + def set_data(self, value, *args): + """ + Set the plotted string's value + @param value: + @param args: + @return: + """ + self.text.setText(value) + + +class ViewerWidget(pg.PlotWidget): + def __init__( + self, + name, + dimensionality, + parent=None, + background="default", + plotItem=None, + **kargs, + ): + """ + A widget to display either 1d or 2d data, with methods to update the data as required. + Includes crosshairs when the mouse is hovered over it, and also shows the x/y coordinates on + the title when the mouse is hovering. Double clicking will print the x/y value to the ipython console + in which the widget is being run. + + @param name: the widget's name + @param dimensionality: the dimensionality of the data so we can use the correct method for updating data if required + @param parent: this widget's parent widget + @param background: + @param plotItem: + @param kargs: + """ + super().__init__( + name=name, parent=parent, background=background, plotItem=plotItem, **kargs + ) + + self.name = name + self.dimensionality = dimensionality + self.vLine = pg.InfiniteLine(angle=90, movable=False) + self.hLine = pg.InfiniteLine(angle=0, movable=False) + + self.addItem(self.vLine, ignoreBounds=True) + self.addItem(self.hLine, ignoreBounds=True) + + self.lines = [self.hLine, self.vLine] + [line.hide() for line in self.lines] + + self.setTitle(name) + self.hideAxis("bottom") + self.hideAxis("left") + + def set_data(self, x, y, z=None): + """ + Set the data in the widget. Automatically updates 1 or 2d data (line plot vs image display) based on parameters + @param x: x data + @param y: y data + @param z: z data, optional - will display 2d plot if provided. + @return: + """ + + self.showAxis("bottom") + self.showAxis("left") + + if z is None: + if hasattr(self, "plot_item"): + self.plot_item.setData(x, y) + else: + self.plot_item = self.plot(x, y) + + else: + pos = (np.min(x), np.min(y)) + + # scale from pixel index values to x/y values + scale = ( + np.max(x) - np.min(x), + np.max(y) - np.min(y), + ) / np.array(z.shape) + + if hasattr(self, "plot_item"): + self.plot_item.setImage(z) + self.plot_item.resetTransform() + self.plot_item.translate(*pos) + self.plot_item.scale(*scale) + + else: + self.plot_item = pg.ImageItem(image=z) + self.addItem(self.plot_item) + self.plot_item.translate(*pos) + self.plot_item.scale(*scale) + + + def leaveEvent(self, ev): + """Mouse left PlotWidget""" + self.hLine.hide() + self.vLine.hide() + + self.setTitle(self.name) + + def enterEvent(self, ev): + """Mouse enter PlotWidget""" + self.hLine.show() + self.vLine.show() + + def mouseMoveEvent(self, ev): + """Mouse moved in PlotWidget""" + + if self.sceneBoundingRect().contains(ev.pos()): + mousePoint = self.plotItem.vb.mapSceneToView(ev.pos()) + self.vLine.setPos(mousePoint.x()) + self.hLine.setPos(mousePoint.y()) + + # if not self.hidden: + self.setTitle(f"{mousePoint.x():.2f}, {mousePoint.y():.2f}") + + def mouseDoubleClickEvent(self, ev): + """On double click, the x/y value of the data will be printed to the console""" + mouse_point = self.plotItem.vb.mapSceneToView(ev.pos()) + print(f"x: {mouse_point.x()}, y: {mouse_point.y()}") + + +class StackedWidget(QFrame): + def __init__(self, qubit_name): + """ + A widget container that has multiple plots, but only displays one at a time. A list is generated + which contains the names of the widget layers. When the layer is clicked in the list, that layer + is displayed. + @param qubit_name: a name for the widget which is displayed as a title on all the plots. + """ + super(StackedWidget, self).__init__() + + self.name = qubit_name + self.layer_list = QListWidget() + self.layer_list.setMaximumWidth(int(self.layer_list.sizeHintForColumn(0) * 1.2)) + + self.Stack = QStackedWidget(self) + hbox = QHBoxLayout(self) + hbox.addWidget(self.layer_list, stretch=1) + hbox.addWidget(self.Stack) + + self.setLayout(hbox) + self.layer_list.currentRowChanged.connect(self.display) + + self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + self.setLineWidth(1) + + self.layer_list.model().rowsInserted.connect(self.display_latest) + + def display_latest(self): + self.display(len(self.layer_list) - 1) + + def add_0d_plot(self, name, text): + self._add_layer(name, text) + + def add_1d_plot(self, name, x, y): + self._add_layer(name, x, y) + + def add_2d_plot(self, name, x, y, z): + self._add_layer(name, x, y, z) + + def _add_layer(self, name, x, y=None, z=None): + + assert ( + name not in self._get_layer_names() + ), "Cannot use duplicate names for layers" + + if y is None and z is None: + widget = TextWidget(self.name, x) + else: + widget = ViewerWidget(name=self.name, dimensionality=1) + widget.set_data(x, y, z) + + self.Stack.addWidget(widget) + self.layer_list.insertItem(len(self.layer_list), name) + + # resize each time we add a new member to the list + self.layer_list.setMaximumWidth(int(self.layer_list.sizeHintForColumn(0) * 1.2)) + + def display(self, i): + self.Stack.setCurrentIndex(i) + + def update_layer_data(self, layer_name, x=None, y=None, z=None): + + widget = self.get_layer(layer_name) + widget.set_data(x, y, z) + + def get_layer(self, name): + index = self._get_layer_names().index(name) + return self.Stack.widget(index) + + def _get_layer_names(self): + return [self.layer_list.item(x).text() for x in range(self.layer_list.count())]