From d9972d5c02b339cca546f5409b7fa812a8109946 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Mon, 3 Oct 2022 13:20:01 +0200 Subject: [PATCH 01/25] test joe --- qualang_tools/analysis/discriminator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qualang_tools/analysis/discriminator.py b/qualang_tools/analysis/discriminator.py index a40fa3f0..68f1edfb 100644 --- a/qualang_tools/analysis/discriminator.py +++ b/qualang_tools/analysis/discriminator.py @@ -10,6 +10,9 @@ def _false_detections(threshold, Ig, Ie): false_detections_var = np.sum(Ig < threshold) + np.sum(Ie > threshold) return false_detections_var +def print_hello(): + print("hello") + def two_state_discriminator(Ig, Qg, Ie, Qe, b_print=True, b_plot=True): """ From ed2f5f54f3f8635cba19bf34a1a901c0a74353cd Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Mon, 3 Oct 2022 16:22:41 +0200 Subject: [PATCH 02/25] 2 qubit fidelity working --- joe_testing_discriminator/configuration.py | 79 ++++++++++ joe_testing_discriminator/fake_iq_data.py | 30 ++++ .../joe_testing_discriminator.py | 135 ++++++++++++++++++ qualang_tools/analysis/discriminator.py | 3 - .../analysis/multi_qubit_discriminator.py | 103 +++++++++++++ 5 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 joe_testing_discriminator/configuration.py create mode 100644 joe_testing_discriminator/fake_iq_data.py create mode 100644 joe_testing_discriminator/joe_testing_discriminator.py create mode 100644 qualang_tools/analysis/multi_qubit_discriminator.py diff --git a/joe_testing_discriminator/configuration.py b/joe_testing_discriminator/configuration.py new file mode 100644 index 00000000..2fc4a2b3 --- /dev/null +++ b/joe_testing_discriminator/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_discriminator/fake_iq_data.py b/joe_testing_discriminator/fake_iq_data.py new file mode 100644 index 00000000..e695e32b --- /dev/null +++ b/joe_testing_discriminator/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_discriminator/joe_testing_discriminator.py b/joe_testing_discriminator/joe_testing_discriminator.py new file mode 100644 index 00000000..35a426a7 --- /dev/null +++ b/joe_testing_discriminator/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/qualang_tools/analysis/discriminator.py b/qualang_tools/analysis/discriminator.py index 68f1edfb..a40fa3f0 100644 --- a/qualang_tools/analysis/discriminator.py +++ b/qualang_tools/analysis/discriminator.py @@ -10,9 +10,6 @@ def _false_detections(threshold, Ig, Ie): false_detections_var = np.sum(Ig < threshold) + np.sum(Ie > threshold) return false_detections_var -def print_hello(): - print("hello") - def two_state_discriminator(Ig, Qg, Ie, Qe, b_print=True, b_plot=True): """ diff --git a/qualang_tools/analysis/multi_qubit_discriminator.py b/qualang_tools/analysis/multi_qubit_discriminator.py new file mode 100644 index 00000000..6d7cd9e8 --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator.py @@ -0,0 +1,103 @@ +import numpy as np +import matplotlib.pyplot as plt +import itertools + +from discriminator import two_state_discriminator + +def multi_qubit_discriminator(Igs, Qgs, Ies, Qes): + + + assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout for all qubits" + + + ggs, ges, egs, ees = [], [], [], [] + + confusion_matrices = [] + + + for Ig, Qg, Ie, Qe in zip(Igs, Qgs, Ies, Qes): + + angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(Ig, Qg, Ie, Qe, b_print=True) + ggs.append(gg) + ges.append(ge) + egs.append(eg) + ees.append(ee) + + confusion_matrices.append(np.array([ + [gg, ge], + [eg, ee] + ])) + + + + return ggs, ges, egs, ees, confusion_matrices + +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 + + + + +if __name__ == '__main__': + + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 2)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 2)).T + + igs, qgs = iq_state_g + ies, qes = iq_state_e + + ggs, ges, egs, ees, confusion_matrices = multi_qubit_discriminator(igs, qgs, ies, qes) + + # need to fix this so it's [ab], c etc [[ab] c][d] + outcome = np.kron(*confusion_matrices) + + fig, ax = plt.subplots() + ax.imshow(outcome) + + num_qubits = igs.__len__() + + state_strings = generate_labels(num_qubits) + + ticks = np.arange(0, outcome.__len__()) + 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=num_qubits)) + + 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.diff(id) == 0 else 'w' + ax.text(*id, f"{100 * outcome[id]:.1f}%", ha="center", va="center", color=color) + + # + # ax.text(0, 0, f"{100 * gg:.1f}%", ha="center", va="center", color="k") + # ax.text(1, 0, f"{100 * ge:.1f}%", ha="center", va="center", color="w") + # ax.text(0, 1, f"{100 * eg:.1f}%", ha="center", va="center", color="w") + # ax.text(1, 1, f"{100 * ee:.1f}%", ha="center", va="center", color="k") + ax.set_title("Fidelities") + + plt.show() From 83d010df75d382848be42338a372f8cf751ca5ae Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Mon, 3 Oct 2022 16:31:30 +0200 Subject: [PATCH 03/25] n qubit discriminator working - processing only --- .../analysis/multi_qubit_discriminator.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/qualang_tools/analysis/multi_qubit_discriminator.py b/qualang_tools/analysis/multi_qubit_discriminator.py index 6d7cd9e8..474424b4 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator.py +++ b/qualang_tools/analysis/multi_qubit_discriminator.py @@ -56,8 +56,8 @@ def generate_labels(length): if __name__ == '__main__': - iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 2)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 2)).T + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 4)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 4)).T igs, qgs = iq_state_g ies, qes = iq_state_e @@ -65,8 +65,16 @@ def generate_labels(length): ggs, ges, egs, ees, confusion_matrices = multi_qubit_discriminator(igs, qgs, ies, qes) # need to fix this so it's [ab], c etc [[ab] c][d] - outcome = np.kron(*confusion_matrices) + A = confusion_matrices[0] + + for i in range(0, len(confusion_matrices) - 1): + + B = confusion_matrices[i + 1] + + A = np.kron(A, B) + + outcome = A fig, ax = plt.subplots() ax.imshow(outcome) @@ -84,13 +92,16 @@ def generate_labels(length): ax.set_ylabel("Prepared") ax.set_xlabel("Measured") - ids = list(itertools.product(np.arange(0, outcome.__len__()), repeat=num_qubits)) - + ids = list(itertools.product(np.arange(0, outcome.__len__()), repeat=2)) + print(ids) 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.diff(id) == 0 else 'w' + color = 'k' if np.all(np.diff(id) == 0) else 'w' + + print(id) + ax.text(*id, f"{100 * outcome[id]:.1f}%", ha="center", va="center", color=color) # From 873a1191f6741371845aadcbd0c8a277c853d643 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 5 Oct 2022 10:55:54 +0200 Subject: [PATCH 04/25] gui --- qualang_tools/analysis/a.py | 0 qualang_tools/analysis/dataPresenter.py | 172 +++++++++ .../analysis/exampleLoaderTemplate_generic.py | 63 ++++ .../independent_multi_qubit_discriminator.py | 168 +++++++++ qualang_tools/analysis/mpltest.py | 35 ++ .../analysis/multi_qubit_discriminator.py | 114 ------ qualang_tools/analysis/tabbed_widget.py | 339 ++++++++++++++++++ qualang_tools/analysis/viewer.py | 45 +++ 8 files changed, 822 insertions(+), 114 deletions(-) create mode 100644 qualang_tools/analysis/a.py create mode 100644 qualang_tools/analysis/dataPresenter.py create mode 100644 qualang_tools/analysis/exampleLoaderTemplate_generic.py create mode 100644 qualang_tools/analysis/independent_multi_qubit_discriminator.py create mode 100644 qualang_tools/analysis/mpltest.py delete mode 100644 qualang_tools/analysis/multi_qubit_discriminator.py create mode 100644 qualang_tools/analysis/tabbed_widget.py create mode 100644 qualang_tools/analysis/viewer.py diff --git a/qualang_tools/analysis/a.py b/qualang_tools/analysis/a.py new file mode 100644 index 00000000..e69de29b diff --git a/qualang_tools/analysis/dataPresenter.py b/qualang_tools/analysis/dataPresenter.py new file mode 100644 index 00000000..24164242 --- /dev/null +++ b/qualang_tools/analysis/dataPresenter.py @@ -0,0 +1,172 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets +import exampleLoaderTemplate_generic as ui_template +from argparse import Namespace +from collections import OrderedDict +import numpy as np +from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget + +class multiQubitReadoutPresenter(QtWidgets.QMainWindow): + + def __init__(self, results_dataclasses): + QtWidgets.QMainWindow.__init__(self) + self.ui = ui_template.Ui_Form() + self.cw = QtWidgets.QWidget() + self.setCentralWidget(self.cw) + self.ui.setupUi(self.cw) + self.setWindowTitle("Readout viewer") + + app = QtWidgets.QApplication.instance() + policy = QtWidgets.QSizePolicy.Policy.Expanding + + self.results_dataclasses = results_dataclasses + + self.curListener = None + self.itemCache = [] + # self.populateTree(self.ui.exampleTree.invisibleRootItem(), utils.examples_) + # self.ui.exampleTree.expandAll() + + self.resize(1200, 800) + self.show() + self.ui.splitter.setSizes([250, 950]) + + for i in range(1, len(results_dataclasses) + 1): + self.ui.qubitsList.addItem( + 'Qubit {}'.format(i) + ) + + # self.oldText = self.ui.codeView.toPlainText() + self.ui.loadBtn.clicked.connect(self.printer) + # self.ui.exampleTree.currentItemChanged.connect(self.showFile) + self.ui.qubitsList.itemDoubleClicked.connect(self.printer) + # self.ui.codeView.textChanged.connect(self.onTextChange) + # self.codeBtn.clicked.connect(self.runEditedCode) + # self.updateCodeViewTabWidth(self.ui.codeView.font()) + + def printer(self): + qubit_idx = self.ui.qubitsList.currentRow() + + results = self.results_dataclasses[qubit_idx] + + angle, threshold, fidelity, gg, ge, eg, ee = results.get_params() + ig, qg, ie, qe = results.get_data() + + # this should happen in the results dataclass not here + C = np.cos(angle) + S = np.sin(angle) + # Condition for having e > Ig + if np.mean((ig - ie) * C - (qg - qe) * S) > 0: + angle += np.pi + C = np.cos(angle) + S = np.sin(angle) + + ig_rotated = ig * C - qg * S + qg_rotated = ig * S + qg * C + + ie_rotated = ie * C - qe * S + qe_rotated = ie * S + qe * C + + + mw = MatplotlibWidget(parent=self.ui.readoutViewer) + + scatter = mw.getFigure().add_subplot(221) + rotated_scatter = mw.getFigure().add_subplot(222) + hist = mw.getFigure().add_subplot(223) + matrix = mw.getFigure().add_subplot(224) + + scatter.plot(ig, qg, '.', alpha=0.1, markersize=2, label='Ground') + scatter.plot(ie, qe, '.', alpha=0.1, markersize=2, label='Excited') + scatter.axis("equal") + scatter.legend(["Ground", "Excited"], loc='lower right') + scatter.set_xlabel("I") + scatter.set_ylabel("Q") + scatter.set_title("Original Data") + + rotated_scatter.plot(ig_rotated, qg_rotated, ".", alpha=0.1, label="Ground", markersize=2) + rotated_scatter.plot(ie_rotated, qe_rotated, ".", alpha=0.1, label="Excited", markersize=2) + rotated_scatter.axis("equal") + rotated_scatter.set_xlabel("I") + rotated_scatter.set_ylabel("Q") + rotated_scatter.set_title("Rotated Data") + + hist.hist(ig_rotated, bins=50, alpha=0.75, label="Ground") + hist.hist(ie_rotated, bins=50, alpha=0.75, label="Excited") + hist.axvline(x=threshold, color="k", ls="--", alpha=0.5) + text_props = dict( + horizontalalignment="center", + verticalalignment="center", + transform=hist.transAxes + ) + hist.text(0.7, 0.9, f"{threshold:.3e}", text_props) + hist.set_xlabel("I") + hist.set_title("1D Histogram") + + + matrix.imshow(results.confusion_matrix()) + matrix.set_xticks([0, 1]) + matrix.set_yticks([0, 1]) + matrix.set_xticklabels(labels=["|g>", "|e>"]) + matrix.set_yticklabels(labels=["|g>", "|e>"]) + matrix.set_ylabel("Prepared") + matrix.set_xlabel("Measured") + matrix.text(0, 0, f"{100 * gg:.1f}%", ha="center", va="center", color="k") + matrix.text(1, 0, f"{100 * ge:.1f}%", ha="center", va="center", color="w") + matrix.text(0, 1, f"{100 * eg:.1f}%", ha="center", va="center", color="w") + matrix.text(1, 1, f"{100 * ee:.1f}%", ha="center", va="center", color="k") + matrix.set_title("Fidelities") + + mw.getFigure().tight_layout() + mw.draw() + + self.ui.readoutViewer.addWidget( + mw, 0, 0 + ) + + # + # plot = self.ui.readoutViewer.addPlot(0, 0) + # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) + # plot = self.ui.readoutViewer.addPlot(0, 1) + # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) + # plot = self.ui.readoutViewer.addPlot(1, 0) + # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) + # plot = self.ui.readoutViewer.addPlot(1, 1) + # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) + + + + + # def showFile(self): + # fn = self.currentFile() + # text = self.getExampleContent(fn) + # self.ui.codeView.setPlainText(text) + # self.ui.loadedFileLabel.setText(fn) + # self.codeBtn.hide() + + def populateTree(self, root, examples): + bold_font = None + for key, val in examples.items(): + item = QtWidgets.QTreeWidgetItem([key]) + self.itemCache.append(item) # PyQt 4.9.6 no longer keeps references to these wrappers, + # so we need to make an explicit reference or else the .file + # attribute will disappear. + if isinstance(val, OrderedDict): + self.populateTree(item, val) + elif isinstance(val, Namespace): + item.file = val.filename + if 'recommended' in val: + if bold_font is None: + bold_font = item.font(0) + bold_font.setBold(True) + item.setFont(0, bold_font) + else: + item.file = val + root.addChild(item) + + + +def main(): + app = pg.mkQApp() + loader = multiQubitReadoutPresenter() + pg.exec() + + diff --git a/qualang_tools/analysis/exampleLoaderTemplate_generic.py b/qualang_tools/analysis/exampleLoaderTemplate_generic.py new file mode 100644 index 00000000..7e910c08 --- /dev/null +++ b/qualang_tools/analysis/exampleLoaderTemplate_generic.py @@ -0,0 +1,63 @@ +# Form implementation generated from reading ui file '../pyqtgraph/examples/exampleLoaderTemplate.ui' +# +# Created by: PyQt6 UI code generator 6.2.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from pyqtgraph.Qt import QtCore, QtGui, QtWidgets +import pyqtgraph as pg + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(846, 552) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setObjectName("gridLayout_2") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.loadBtn = QtWidgets.QPushButton(self.layoutWidget) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 6, 0, 2, 2) + # self.exampleTree = QtWidgets.QTreeWidget(self.layoutWidget) + # self.exampleTree.setObjectName("exampleTree") + # self.exampleTree.headerItem().setText(0, "1") + # self.exampleTree.header().setVisible(False) + + self.qubitsList = QtWidgets.QListWidget() + + self.gridLayout.addWidget(self.qubitsList, 3, 0, 1, 2) + self.layoutWidget1 = QtWidgets.QWidget(self.splitter) + self.layoutWidget1.setObjectName("layoutWidget1") + self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + font = QtGui.QFont() + font.setBold(True) + + # self.readoutViewer = pg.PlotWidget() + # self.readoutViewer = pg.GraphicsLayoutWidget() + # self.readoutViewer = QtWidgets.QWidget() + self.readoutViewer = pg.LayoutWidget() + + font = QtGui.QFont() + font.setFamily("Courier New") + self.readoutViewer.setFont(font) + self.readoutViewer.setObjectName("readoutViewer") + self.verticalLayout.addWidget(self.readoutViewer) + self.gridLayout_2.addWidget(self.splitter, 1, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "PyQtGraph")) + self.loadBtn.setText(_translate("Form", "Show data")) diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py new file mode 100644 index 00000000..ef111ddb --- /dev/null +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -0,0 +1,168 @@ +import numpy as np +import matplotlib.pyplot as plt +import itertools +from tqdm import tqdm +from dataclasses import dataclass + +from tkinter import * +from tkinter import ttk + +from discriminator import two_state_discriminator + + +@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. + """ + + # 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 confusion_matrix(self): + return np.array([ + [self.gg, self.ge], + [self.eg, self.ee] + ]) + + def get_params(self): + return self.angle, self.threshold, self.fidelity, self.gg, self.ge, self.eg, self.ee + + def get_data(self): + return self.ig, self.qg, self.ie, self.qe + + +def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): + assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" + + result_dataclasses = [] + + for Ig, Qg, Ie, Qe in zip(Igs, Qgs, Ies, Qes): + result_dataclass = DiscriminatorDataclass( + *two_state_discriminator(Ig, Qg, Ie, Qe, b_print=b_print, b_plot=b_plot), + Ig, Qg, Ie, Qe + ) + + 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 + +def build_widget(num): + + root = Tk() + root.title("Readout statistics") + + # Add a grid + mainframe = Frame(root, height=500, width=500) + mainframe.grid(column=0, row=0, sticky=(W)) + mainframe.columnconfigure(0, weight=1) + mainframe.rowconfigure(0, weight=5) + mainframe.pack(pady=10, padx=10) + + # Create a Tkinter variable + tkvar = StringVar(root) + + # Dictionary with options + choices = {'Qubit {}'.format(i) for i in range(num)} + tkvar.set('Qubit 0') # set the default option + + popupMenu = OptionMenu(mainframe, tkvar, *choices) + Label(mainframe, text="Select qubit").grid(row=0, column=0) + popupMenu.grid(row=1, column=0) + + # on change dropdown value + def change_dropdown(*args): + print(tkvar.get()) + + # link function to change dropdown + tkvar.trace('w', change_dropdown) + root.geometry("600x500") + root.mainloop() + +from dataPresenter import multiQubitReadoutPresenter +import pyqtgraph as pg + +if __name__ == '__main__': + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 32)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T + + igs, qgs = iq_state_g + ies, qes = iq_state_e + + results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) + + + def main(): + app = pg.mkQApp() + loader = multiQubitReadoutPresenter(results) + pg.exec() + + main() diff --git a/qualang_tools/analysis/mpltest.py b/qualang_tools/analysis/mpltest.py new file mode 100644 index 00000000..d1ec27d4 --- /dev/null +++ b/qualang_tools/analysis/mpltest.py @@ -0,0 +1,35 @@ +import sys +import matplotlib +matplotlib.use('Qt5Agg') + +from PyQt5 import QtCore, QtWidgets + +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +from matplotlib.figure import Figure + + +class MplCanvas(FigureCanvasQTAgg): + + def __init__(self, parent=None, width=5, height=4, dpi=100): + fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = fig.add_subplot(111) + super(MplCanvas, self).__init__(fig) + + +class MainWindow(QtWidgets.QMainWindow): + + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + + # Create the maptlotlib FigureCanvas object, + # which defines a single set of axes as self.axes. + sc = MplCanvas(self, width=5, height=4, dpi=100) + sc.axes.plot([0,1,2,3,4], [10,1,20,3,40]) + self.setCentralWidget(sc) + + self.show() + + +app = QtWidgets.QApplication(sys.argv) +w = MainWindow() +app.exec_() \ No newline at end of file diff --git a/qualang_tools/analysis/multi_qubit_discriminator.py b/qualang_tools/analysis/multi_qubit_discriminator.py deleted file mode 100644 index 474424b4..00000000 --- a/qualang_tools/analysis/multi_qubit_discriminator.py +++ /dev/null @@ -1,114 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import itertools - -from discriminator import two_state_discriminator - -def multi_qubit_discriminator(Igs, Qgs, Ies, Qes): - - - assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout for all qubits" - - - ggs, ges, egs, ees = [], [], [], [] - - confusion_matrices = [] - - - for Ig, Qg, Ie, Qe in zip(Igs, Qgs, Ies, Qes): - - angle, threshold, fidelity, gg, ge, eg, ee = two_state_discriminator(Ig, Qg, Ie, Qe, b_print=True) - ggs.append(gg) - ges.append(ge) - egs.append(eg) - ees.append(ee) - - confusion_matrices.append(np.array([ - [gg, ge], - [eg, ee] - ])) - - - - return ggs, ges, egs, ees, confusion_matrices - -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 - - - - -if __name__ == '__main__': - - iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 4)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 4)).T - - igs, qgs = iq_state_g - ies, qes = iq_state_e - - ggs, ges, egs, ees, confusion_matrices = multi_qubit_discriminator(igs, qgs, ies, qes) - - # need to fix this so it's [ab], c etc [[ab] c][d] - - A = confusion_matrices[0] - - for i in range(0, len(confusion_matrices) - 1): - - B = confusion_matrices[i + 1] - - A = np.kron(A, B) - - outcome = A - fig, ax = plt.subplots() - ax.imshow(outcome) - - num_qubits = igs.__len__() - - state_strings = generate_labels(num_qubits) - - ticks = np.arange(0, outcome.__len__()) - 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)) - print(ids) - 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' - - print(id) - - ax.text(*id, f"{100 * outcome[id]:.1f}%", ha="center", va="center", color=color) - - # - # ax.text(0, 0, f"{100 * gg:.1f}%", ha="center", va="center", color="k") - # ax.text(1, 0, f"{100 * ge:.1f}%", ha="center", va="center", color="w") - # ax.text(0, 1, f"{100 * eg:.1f}%", ha="center", va="center", color="w") - # ax.text(1, 1, f"{100 * ee:.1f}%", ha="center", va="center", color="k") - ax.set_title("Fidelities") - - plt.show() diff --git a/qualang_tools/analysis/tabbed_widget.py b/qualang_tools/analysis/tabbed_widget.py new file mode 100644 index 00000000..accac6a6 --- /dev/null +++ b/qualang_tools/analysis/tabbed_widget.py @@ -0,0 +1,339 @@ +import keyword +import os +import pkgutil +import re +import subprocess +import sys +from argparse import Namespace +from collections import OrderedDict +from functools import lru_cache + +import pyqtgraph as pg +from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets + +app = pg.mkQApp() + + +path = os.path.abspath(os.path.dirname(__file__)) +sys.path.insert(0, path) + +import exampleLoaderTemplate_generic as ui_template +import utils + +# based on https://github.com/art1415926535/PyQt5-syntax-highlighting + +QRegularExpression = QtCore.QRegularExpression + +QFont = QtGui.QFont +QColor = QtGui.QColor +QTextCharFormat = QtGui.QTextCharFormat +QSyntaxHighlighter = QtGui.QSyntaxHighlighter + + +def charFormat(color, style='', background=None): + """ + Return a QTextCharFormat with the given attributes. + """ + _color = QColor() + if type(color) is not str: + _color.setRgb(color[0], color[1], color[2]) + else: + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Weight.Bold) + if 'italic' in style: + _format.setFontItalic(True) + if background is not None: + _format.setBackground(pg.mkColor(background)) + + return _format + + +def unnestedDict(exDict): + """Converts a dict-of-dicts to a singly nested dict for non-recursive parsing""" + out = {} + for kk, vv in exDict.items(): + if isinstance(vv, dict): + out.update(unnestedDict(vv)) + else: + out[kk] = vv + return out + + + +class ExampleLoader(QtWidgets.QMainWindow): + # update qtLibCombo item order to match bindings in the UI file and recreate + # the templates files if you change bindings. + bindings = {'PyQt6': 0, 'PySide6': 1, 'PyQt5': 2, 'PySide2': 3} + modules = tuple(m.name for m in pkgutil.iter_modules()) + def __init__(self): + QtWidgets.QMainWindow.__init__(self) + self.ui = ui_template.Ui_Form() + self.cw = QtWidgets.QWidget() + self.setCentralWidget(self.cw) + self.ui.setupUi(self.cw) + self.setWindowTitle("PyQtGraph Examples") + self.codeBtn = QtWidgets.QPushButton('Run Edited Code') + self.codeLayout = QtWidgets.QGridLayout() + self.ui.readoutViewer.setLayout(self.codeLayout) + app = QtWidgets.QApplication.instance() + app.paletteChanged.connect(self.updateTheme) + policy = QtWidgets.QSizePolicy.Policy.Expanding + self.codeLayout.addItem(QtWidgets.QSpacerItem(100,100, policy, policy), 0, 0) + self.codeLayout.addWidget(self.codeBtn, 1, 1) + self.codeBtn.hide() + + self.curListener = None + self.ui.qtLibCombo.addItems(self.bindings.keys()) + self.ui.qtLibCombo.setCurrentIndex(self.bindings[QT_LIB]) + + + + self.itemCache = [] + self.populateTree(self.ui.qubitsList.invisibleRootItem(), utils.examples_) + self.ui.qubitsList.expandAll() + + self.resize(1000,500) + self.show() + self.ui.splitter.setSizes([250,750]) + + self.oldText = self.ui.readoutViewer.toPlainText() + self.ui.loadBtn.clicked.connect(self.loadFile) + self.ui.qubitsList.currentItemChanged.connect(self.showFile) + self.ui.qubitsList.itemDoubleClicked.connect(self.loadFile) + self.ui.readoutViewer.textChanged.connect(self.onTextChange) + self.codeBtn.clicked.connect(self.runEditedCode) + self.updateCodeViewTabWidth(self.ui.readoutViewer.font()) + + def updateCodeViewTabWidth(self,font): + """ + Change the codeView tabStopDistance to 4 spaces based on the size of the current font + """ + fm = QtGui.QFontMetrics(font) + tabWidth = fm.horizontalAdvance(' ' * 4) + # the default value is 80 pixels! that's more than 2x what we want. + self.ui.readoutViewer.setTabStopDistance(tabWidth) + + def showEvent(self, event) -> None: + super(ExampleLoader, self).showEvent(event) + disabledColor = QColor(QtCore.Qt.GlobalColor.red) + for name, idx in self.bindings.items(): + disableBinding = name not in self.modules + if disableBinding: + item = self.ui.qtLibCombo.model().item(idx) + item.setData(disabledColor, QtCore.Qt.ItemDataRole.ForegroundRole) + item.setEnabled(False) + item.setToolTip(f'{item.text()} is not installed') + + def onTextChange(self): + """ + textChanged fires when the highlighter is reassigned the same document. + Prevent this from showing "run edited code" by checking for actual + content change + """ + newText = self.ui.readoutViewer.toPlainText() + if newText != self.oldText: + self.oldText = newText + self.codeEdited() + + def filterByTitle(self, text): + self.showExamplesByTitle(self.getMatchingTitles(text)) + self.hl.setDocument(self.ui.readoutViewer.document()) + + def filterByContent(self, text=None): + # If the new text isn't valid regex, fail early and highlight the search filter red to indicate a problem + # to the user + validRegex = True + try: + re.compile(text) + self.ui.exampleFilter.setStyleSheet('') + except re.error: + colors = DarkThemeColors if app.property('darkMode') else LightThemeColors + errorColor = pg.mkColor(colors.Red) + validRegex = False + errorColor.setAlpha(100) + # Tuple prints nicely :) + self.ui.exampleFilter.setStyleSheet(f'background: rgba{errorColor.getRgb()}') + if not validRegex: + return + checkDict = unnestedDict(utils.examples_) + self.hl.searchText = text + # Need to reapply to current document + self.hl.setDocument(self.ui.readoutViewer.document()) + titles = [] + text = text.lower() + for kk, vv in checkDict.items(): + if isinstance(vv, Namespace): + vv = vv.filename + filename = os.path.join(path, vv) + contents = self.getExampleContent(filename).lower() + if text in contents: + titles.append(kk) + self.showExamplesByTitle(titles) + + def getMatchingTitles(self, text, exDict=None, acceptAll=False): + if exDict is None: + exDict = utils.examples_ + text = text.lower() + titles = [] + for kk, vv in exDict.items(): + matched = acceptAll or text in kk.lower() + if isinstance(vv, dict): + titles.extend(self.getMatchingTitles(text, vv, acceptAll=matched)) + elif matched: + titles.append(kk) + return titles + + def showExamplesByTitle(self, titles): + QTWI = QtWidgets.QTreeWidgetItemIterator + flag = QTWI.IteratorFlag.NoChildren + treeIter = QTWI(self.ui.qubitsList, flag) + item = treeIter.value() + while item is not None: + parent = item.parent() + show = (item.childCount() or item.text(0) in titles) + item.setHidden(not show) + + # If all children of a parent are gone, hide it + if parent: + hideParent = True + for ii in range(parent.childCount()): + if not parent.child(ii).isHidden(): + hideParent = False + break + parent.setHidden(hideParent) + + treeIter += 1 + item = treeIter.value() + + def simulate_black_mode(self): + """ + used to simulate MacOS "black mode" on other platforms + intended for debug only, as it manage only the QPlainTextEdit + """ + # first, a dark background + c = QtGui.QColor('#171717') + p = self.ui.readoutViewer.palette() + p.setColor(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Base, c) + p.setColor(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Base, c) + self.ui.readoutViewer.setPalette(p) + # then, a light font + f = QtGui.QTextCharFormat() + f.setForeground(QtGui.QColor('white')) + self.ui.readoutViewer.setCurrentCharFormat(f) + # finally, override application automatic detection + app = QtWidgets.QApplication.instance() + app.setProperty('darkMode', True) + + def updateTheme(self): + self.hl = PythonHighlighter(self.ui.readoutViewer.document()) + + def populateTree(self, root, examples): + bold_font = None + for key, val in examples.items(): + item = QtWidgets.QTreeWidgetItem([key]) + self.itemCache.append(item) # PyQt 4.9.6 no longer keeps references to these wrappers, + # so we need to make an explicit reference or else the .file + # attribute will disappear. + if isinstance(val, OrderedDict): + self.populateTree(item, val) + elif isinstance(val, Namespace): + item.file = val.filename + if 'recommended' in val: + if bold_font is None: + bold_font = item.font(0) + bold_font.setBold(True) + item.setFont(0, bold_font) + else: + item.file = val + root.addChild(item) + + def currentFile(self): + item = self.ui.qubitsList.currentItem() + if hasattr(item, 'file'): + return os.path.join(path, item.file) + return None + + def loadFile(self, edited=False): + qtLib = self.ui.qtLibCombo.currentText() + env = dict(os.environ, PYQTGRAPH_QT_LIB=qtLib) + example_path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.dirname(os.path.dirname(example_path)) + env['PYTHONPATH'] = f'{path}' + if edited: + proc = subprocess.Popen([sys.executable, '-'], stdin=subprocess.PIPE, cwd=example_path, env=env) + code = self.ui.readoutViewer.toPlainText().encode('UTF-8') + proc.stdin.write(code) + proc.stdin.close() + else: + fn = self.currentFile() + if fn is None: + return + subprocess.Popen([sys.executable, fn], cwd=path, env=env) + + def showFile(self): + fn = self.currentFile() + text = self.getExampleContent(fn) + self.ui.readoutViewer.setPlainText(text) + self.ui.loadedFileLabel.setText(fn) + self.codeBtn.hide() + + @lru_cache(100) + def getExampleContent(self, filename): + if filename is None: + self.ui.readoutViewer.clear() + return + if os.path.isdir(filename): + filename = os.path.join(filename, '__main__.py') + with open(filename, "r") as currentFile: + text = currentFile.read() + return text + + def codeEdited(self): + self.codeBtn.show() + + def runEditedCode(self): + self.loadFile(edited=True) + + def keyPressEvent(self, event): + super().keyPressEvent(event) + if not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier): + return + key = event.key() + Key = QtCore.Qt.Key + + # Allow quick navigate to search + if key == Key.Key_F: + self.ui.exampleFilter.setFocus() + event.accept() + return + + if key not in [Key.Key_Plus, Key.Key_Minus, Key.Key_Underscore, Key.Key_Equal, Key.Key_0]: + return + font = self.ui.readoutViewer.font() + oldSize = font.pointSize() + if key == Key.Key_Plus or key == Key.Key_Equal: + font.setPointSize(oldSize + max(oldSize*.15, 1)) + elif key == Key.Key_Minus or key == Key.Key_Underscore: + newSize = oldSize - max(oldSize*.15, 1) + font.setPointSize(max(newSize, 1)) + elif key == Key.Key_0: + # Reset to original size + font.setPointSize(10) + self.ui.readoutViewer.setFont(font) + self.updateCodeViewTabWidth(font) + event.accept() + +def main(): + app = pg.mkQApp() + loader = ExampleLoader() + loader.ui.qubitsList.setCurrentIndex( + loader.ui.qubitsList.model().index(0, 0) + ) + pg.exec() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/qualang_tools/analysis/viewer.py b/qualang_tools/analysis/viewer.py new file mode 100644 index 00000000..5a6bfc47 --- /dev/null +++ b/qualang_tools/analysis/viewer.py @@ -0,0 +1,45 @@ +import sys +from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTabWidget, QGridLayout, QVBoxLayout +import pyqtgraph as pg + +class App(QWidget): + + def __init__(self): + + super().__init__() + + self.resize(1000, 800) + + mainLayout = QGridLayout() + + vLayout1 = QVBoxLayout() + + # first tab + self.tab1 = QWidget() + self.tab1.layout = QGridLayout() + self.tab1.layout.addWidget(QLabel('test')) + + self.qb_list = QListWidget() + self.readoutViewer = pg.LayoutWidget() + + self.tab1.layout.addWidget(self.qb_list, 3, 0, 1,2 ) + + + + self.tab1.setLayout(self.tab1.layout) + # second tab + self.tab2 = QWidget() + + self.tabs = QTabWidget() + + self.tabs.addTab(self.tab1, 'Readout viewer') + self.tabs.addTab(self.tab2, 'all viewer') + + mainLayout.addWidget(self.tabs, 0, 0) + self.setLayout(mainLayout) + +app = QApplication(sys.argv) +demo = App() +demo.show() + +sys.exit(app.exec()) From 5fe3bba7ca32d2f4c14c65c3b483ab044908f5b1 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 5 Oct 2022 11:27:57 +0200 Subject: [PATCH 05/25] gui --- qualang_tools/analysis/dataPresenter.py | 44 ++----- .../analysis/exampleLoaderTemplate_generic.py | 9 +- .../independent_multi_qubit_discriminator.py | 27 ++++- qualang_tools/analysis/viewer.py | 107 +++++++++++++++--- 4 files changed, 130 insertions(+), 57 deletions(-) diff --git a/qualang_tools/analysis/dataPresenter.py b/qualang_tools/analysis/dataPresenter.py index 24164242..065632ec 100644 --- a/qualang_tools/analysis/dataPresenter.py +++ b/qualang_tools/analysis/dataPresenter.py @@ -10,6 +10,12 @@ class multiQubitReadoutPresenter(QtWidgets.QMainWindow): def __init__(self, results_dataclasses): QtWidgets.QMainWindow.__init__(self) + + + self.tabs = QtWidgets.QTabWidget() + self.tab1 = QtWidgets.QWidget() + self.tab2 = QtWidgets.QWidget() + self.ui = ui_template.Ui_Form() self.cw = QtWidgets.QWidget() self.setCentralWidget(self.cw) @@ -43,6 +49,10 @@ def __init__(self, results_dataclasses): # self.codeBtn.clicked.connect(self.runEditedCode) # self.updateCodeViewTabWidth(self.ui.codeView.font()) + self.tabs.addTab(self.tab1, 'Readout viewer') + self.tabs.addTab(self.tab2, 'All viewer') + self.ui.gridLayout.addWidget(self.tabs, 0, 0) + def printer(self): qubit_idx = self.ui.qubitsList.currentRow() @@ -51,20 +61,7 @@ def printer(self): angle, threshold, fidelity, gg, ge, eg, ee = results.get_params() ig, qg, ie, qe = results.get_data() - # this should happen in the results dataclass not here - C = np.cos(angle) - S = np.sin(angle) - # Condition for having e > Ig - if np.mean((ig - ie) * C - (qg - qe) * S) > 0: - angle += np.pi - C = np.cos(angle) - S = np.sin(angle) - - ig_rotated = ig * C - qg * S - qg_rotated = ig * S + qg * C - - ie_rotated = ie * C - qe * S - qe_rotated = ie * S + qe * C + ig_rotated, qg_rotated, ie_rotated, qe_rotated = results.get_rotated_data() mw = MatplotlibWidget(parent=self.ui.readoutViewer) @@ -122,25 +119,6 @@ def printer(self): mw, 0, 0 ) - # - # plot = self.ui.readoutViewer.addPlot(0, 0) - # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) - # plot = self.ui.readoutViewer.addPlot(0, 1) - # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) - # plot = self.ui.readoutViewer.addPlot(1, 0) - # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) - # plot = self.ui.readoutViewer.addPlot(1, 1) - # plot.plot(np.random.normal(size=100), np.random.normal(size=100)) - - - - - # def showFile(self): - # fn = self.currentFile() - # text = self.getExampleContent(fn) - # self.ui.codeView.setPlainText(text) - # self.ui.loadedFileLabel.setText(fn) - # self.codeBtn.hide() def populateTree(self, root, examples): bold_font = None diff --git a/qualang_tools/analysis/exampleLoaderTemplate_generic.py b/qualang_tools/analysis/exampleLoaderTemplate_generic.py index 7e910c08..f884c32f 100644 --- a/qualang_tools/analysis/exampleLoaderTemplate_generic.py +++ b/qualang_tools/analysis/exampleLoaderTemplate_generic.py @@ -23,13 +23,7 @@ def setupUi(self, Form): self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setObjectName("gridLayout") - self.loadBtn = QtWidgets.QPushButton(self.layoutWidget) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 6, 0, 2, 2) - # self.exampleTree = QtWidgets.QTreeWidget(self.layoutWidget) - # self.exampleTree.setObjectName("exampleTree") - # self.exampleTree.headerItem().setText(0, "1") - # self.exampleTree.header().setVisible(False) + self.qubitsList = QtWidgets.QListWidget() @@ -60,4 +54,3 @@ def setupUi(self, Form): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "PyQtGraph")) - self.loadBtn.setText(_translate("Form", "Show data")) diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py index ef111ddb..4da40475 100644 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -8,7 +8,7 @@ from tkinter import ttk from discriminator import two_state_discriminator - +from viewer import App @dataclass class DiscriminatorDataclass: @@ -32,6 +32,9 @@ class DiscriminatorDataclass: ie: np.ndarray qe: np.ndarray + def __post_init__(self): + self.generate_rotation_data() + def confusion_matrix(self): return np.array([ [self.gg, self.ge], @@ -44,6 +47,24 @@ def get_params(self): def get_data(self): return self.ig, self.qg, self.ie, self.qe + def get_rotated_data(self): + return self.ig_rotated, self.qg_rotated, self.ie_rotated, self.qe_rotated + + def generate_rotation_data(self): + # this should happen in the results dataclass not here + 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 + def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" @@ -159,10 +180,10 @@ def change_dropdown(*args): results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) - def main(): app = pg.mkQApp() - loader = multiQubitReadoutPresenter(results) + # loader = multiQubitReadoutPresenter(results) + loader = App(results) pg.exec() main() diff --git a/qualang_tools/analysis/viewer.py b/qualang_tools/analysis/viewer.py index 5a6bfc47..3ce3673b 100644 --- a/qualang_tools/analysis/viewer.py +++ b/qualang_tools/analysis/viewer.py @@ -1,10 +1,14 @@ -import sys from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTabWidget, QGridLayout, QVBoxLayout + import pyqtgraph as pg +from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets +import exampleLoaderTemplate_generic as ui_template + +from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget class App(QWidget): - def __init__(self): + def __init__(self, results_dataclasses): super().__init__() @@ -16,17 +20,15 @@ def __init__(self): # first tab self.tab1 = QWidget() - self.tab1.layout = QGridLayout() - self.tab1.layout.addWidget(QLabel('test')) - - self.qb_list = QListWidget() - self.readoutViewer = pg.LayoutWidget() - self.tab1.layout.addWidget(self.qb_list, 3, 0, 1,2 ) + # self.tab1.layout.addWidget(self.qb_list, 3, 0, 1,2 ) + self.tab1.ui = ui_template.Ui_Form() + # self.tab1.setCentralWidget(self.tab1) + self.tab1.ui.setupUi(self.tab1) - self.tab1.setLayout(self.tab1.layout) + # self.tab1.setLayout(self.tab1.layout) # second tab self.tab2 = QWidget() @@ -38,8 +40,87 @@ def __init__(self): mainLayout.addWidget(self.tabs, 0, 0) self.setLayout(mainLayout) -app = QApplication(sys.argv) -demo = App() -demo.show() -sys.exit(app.exec()) + self.setWindowTitle("Readout viewer") + + app = QtWidgets.QApplication.instance() + policy = QtWidgets.QSizePolicy.Policy.Expanding + + self.results_dataclasses = results_dataclasses + + self.curListener = None + self.itemCache = [] + + self.resize(1200, 800) + self.show() + self.tab1.ui.splitter.setSizes([250, 950]) + + for i in range(1, len(results_dataclasses) + 1): + self.tab1.ui.qubitsList.addItem( + 'Qubit {}'.format(i) + ) + + self.tab1.ui.qubitsList.itemDoubleClicked.connect(self.printer) + + def printer(self): + qubit_idx = self.tab1.ui.qubitsList.currentRow() + + results = self.results_dataclasses[qubit_idx] + + angle, threshold, fidelity, gg, ge, eg, ee = results.get_params() + ig, qg, ie, qe = results.get_data() + ig_rotated, qg_rotated, ie_rotated, qe_rotated = results.get_rotated_data() + + mw = MatplotlibWidget(parent=self.tab1.ui.readoutViewer) + + scatter = mw.getFigure().add_subplot(221) + rotated_scatter = mw.getFigure().add_subplot(222) + hist = mw.getFigure().add_subplot(223) + matrix = mw.getFigure().add_subplot(224) + + scatter.plot(ig, qg, '.', alpha=0.1, markersize=2, label='Ground') + scatter.plot(ie, qe, '.', alpha=0.1, markersize=2, label='Excited') + scatter.axis("equal") + scatter.legend(["Ground", "Excited"], loc='lower right') + scatter.set_xlabel("I") + scatter.set_ylabel("Q") + scatter.set_title("Original Data") + + rotated_scatter.plot(ig_rotated, qg_rotated, ".", alpha=0.1, label="Ground", markersize=2) + rotated_scatter.plot(ie_rotated, qe_rotated, ".", alpha=0.1, label="Excited", markersize=2) + rotated_scatter.axis("equal") + rotated_scatter.set_xlabel("I") + rotated_scatter.set_ylabel("Q") + rotated_scatter.set_title("Rotated Data") + + hist.hist(ig_rotated, bins=50, alpha=0.75, label="Ground") + hist.hist(ie_rotated, bins=50, alpha=0.75, label="Excited") + hist.axvline(x=threshold, color="k", ls="--", alpha=0.5) + text_props = dict( + horizontalalignment="center", + verticalalignment="center", + transform=hist.transAxes + ) + hist.text(0.7, 0.9, f"{threshold:.3e}", text_props) + hist.set_xlabel("I") + hist.set_title("1D Histogram") + + matrix.imshow(results.confusion_matrix()) + matrix.set_xticks([0, 1]) + matrix.set_yticks([0, 1]) + matrix.set_xticklabels(labels=["|g>", "|e>"]) + matrix.set_yticklabels(labels=["|g>", "|e>"]) + matrix.set_ylabel("Prepared") + matrix.set_xlabel("Measured") + matrix.text(0, 0, f"{100 * gg:.1f}%", ha="center", va="center", color="k") + matrix.text(1, 0, f"{100 * ge:.1f}%", ha="center", va="center", color="w") + matrix.text(0, 1, f"{100 * eg:.1f}%", ha="center", va="center", color="w") + matrix.text(1, 1, f"{100 * ee:.1f}%", ha="center", va="center", color="k") + matrix.set_title("Fidelities") + + mw.getFigure().tight_layout() + mw.draw() + + self.tab1.ui.readoutViewer.addWidget( + mw, 0, 0 + ) From 288beafd065500725af31dca72ba178a7e53c667 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Tue, 11 Oct 2022 11:14:29 +0200 Subject: [PATCH 06/25] discriminator gui updated to pyqt --- discriminator_gui/discriminator_gui.py | 143 ++++++ .../active_reset_data_generator.py | 86 ++++ joe_testing_active_reset/active_reset_gui.py | 142 ++++++ .../active_reset_gui_old.py | 218 ++++++++ joe_testing_active_reset/basic_program.py | 176 +++++++ .../build_active_reset_program.py | 153 ++++++ .../build_passive_reset_program.py | 0 joe_testing_active_reset/configuration.py | 475 ++++++++++++++++++ joe_testing_active_reset/fake_data.py | 35 ++ joe_testing_active_reset/sample_generator.py | 64 +++ joe_testing_active_reset/t1_basic.py | 110 ++++ .../t1_cycle_simulator.py | 27 + qualang_tools/analysis/active_reset.py | 0 .../independent_multi_qubit_discriminator.py | 9 +- qualang_tools/analysis/mpltest.py | 35 -- qualang_tools/analysis/tabbed_widget.py | 339 ------------- qualang_tools/config/gui.py | 2 +- 17 files changed, 1635 insertions(+), 379 deletions(-) create mode 100644 discriminator_gui/discriminator_gui.py create mode 100644 joe_testing_active_reset/active_reset_data_generator.py create mode 100644 joe_testing_active_reset/active_reset_gui.py create mode 100644 joe_testing_active_reset/active_reset_gui_old.py create mode 100644 joe_testing_active_reset/basic_program.py create mode 100644 joe_testing_active_reset/build_active_reset_program.py rename qualang_tools/analysis/a.py => joe_testing_active_reset/build_passive_reset_program.py (100%) create mode 100644 joe_testing_active_reset/configuration.py create mode 100644 joe_testing_active_reset/fake_data.py create mode 100644 joe_testing_active_reset/sample_generator.py create mode 100644 joe_testing_active_reset/t1_basic.py create mode 100644 joe_testing_active_reset/t1_cycle_simulator.py create mode 100644 qualang_tools/analysis/active_reset.py delete mode 100644 qualang_tools/analysis/mpltest.py delete mode 100644 qualang_tools/analysis/tabbed_widget.py diff --git a/discriminator_gui/discriminator_gui.py b/discriminator_gui/discriminator_gui.py new file mode 100644 index 00000000..92f3dde0 --- /dev/null +++ b/discriminator_gui/discriminator_gui.py @@ -0,0 +1,143 @@ +import sys +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import pyqtgraph as pg +import numpy as np + +class DiscriminatorGui(QWidget): + def __init__(self, results_dataclasses): + + self.num_qubits = len(results_dataclasses) + self.results_dataclasses = results_dataclasses + super(DiscriminatorGui, self).__init__() + self.initUI() + self._populate_list() + + + def initUI(self): + + hbox = QHBoxLayout(self) + + # create some widgets + + self.left = pg.LayoutWidget() + self.right = pg.LayoutWidget() + + self.qubit_list = QComboBox() + + + 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(QFrame(), 2, 0) + + self.right.addWidget(self.graphics_window) + + splitter = QSplitter(Qt.Horizontal) + splitter.addWidget(self.left) + splitter.addWidget(self.right) + + + hbox.addWidget(splitter) + + self.setLayout(hbox) + + QApplication.setStyle(QStyleFactory.create('Cleanlooks')) + + self.setGeometry(100, 100, 1200, 500) + self.setWindowTitle('Qubit reset comparison') + + # self.qubit_list.itemDoubleClicked.connect(self.func) + self.qubit_list.currentIndexChanged.connect(self.func) + + + self.show() + + def generate_fake_histograms(self, a, b): + ## make interesting distribution of values + vals1 = np.random.normal(size=500) + vals2 = np.random.normal(size=260, loc=4) + ## compute standard histogram + y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) + y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) + return a * y, x, b * y2, x2 + + def clear_plots(self): + self.plt1.clear() + self.plt2.clear() + self.plt3.clear() + self.plt4.clear() + + def func(self): + + self.clear_plots() + + index = self.qubit_list.currentIndex() + result = self.results_dataclasses[index] + + angle, threshold, fidelity, gg, ge, eg, ee = result.get_params() + ig, qg, ie, qe = result.get_data() + ig_rotated, qg_rotated, ie_rotated, qe_rotated = result.get_rotated_data() + + ## Using stepMode="center" causes the plot to draw two lines for each sample. + ## notice that len(x) == len(y)+1 + + original_data_g = pg.ScatterPlotItem(ig, qg, brush=(50, 50, 50, 199)) + original_data_e = pg.ScatterPlotItem(ie, qe, brush=(0, 0, 100, 100)) + self.plt1.addItem(original_data_g) + self.plt1.addItem(original_data_e) + + + rotated_data_g = pg.ScatterPlotItem(ig_rotated, qg_rotated, brush=(50, 50, 50, 199)) + rotated_data_e = pg.ScatterPlotItem(ie_rotated, qe_rotated, brush=(0, 0, 100, 100)) + self.plt2.addItem(rotated_data_g) + self.plt2.addItem(rotated_data_e) + + ig_hist_y, ig_hist_x = np.histogram(ig_rotated, bins=50) + ie_hist_y, ie_hist_x = np.histogram(ie_rotated, bins=50) + + self.plt3.plot(ig_hist_x, ig_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(0, 0, 0, 255)) + self.plt3.plot(ie_hist_x, ie_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(255, 255, 255, 150)) + + img = pg.ImageItem(image=result.confusion_matrix()) + self.plt4.addItem(img) + self.plt4.showAxes(False) + + + + def _populate_list(self): + + for i in range(self.num_qubits): + self.qubit_list.addItem( + f'Qubit {i + 1}' + ) + + + + + +if __name__ == '__main__': + + from qualang_tools.analysis.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, 32)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T + + igs, qgs = iq_state_g + ies, qes = iq_state_e + + results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) + + def main(): + app = pg.mkQApp() + # loader = multiQubitReadoutPresenter(results) + loader = DiscriminatorGui(results) + pg.exec() + + main() diff --git a/joe_testing_active_reset/active_reset_data_generator.py b/joe_testing_active_reset/active_reset_data_generator.py new file mode 100644 index 00000000..4ea1840a --- /dev/null +++ b/joe_testing_active_reset/active_reset_data_generator.py @@ -0,0 +1,86 @@ +#%% +from platform import mac_ver +from quam import QuAM +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager +from qm.simulate import SimulationConfig +from qualang_tools.loops import from_array +from qualang_tools.results import fetching_tool, progress_counter +from qualang_tools.analysis.discriminator import two_state_discriminator + +import matplotlib.pyplot as plt + +import numpy as np +# from IPython.display import clear_output + + +# %% +n_shots = int(2000) +num_qb = 6 + +n_total = num_qb * n_shots +cooldown_time = int(0.6e-3 / 4e-9) + +def construct_active_reset_program(): + + with program() as active_reset_program: + raw_st = declare_stream(adc_trace=True) + n = declare(int) + n_cnt = declare(int, value=0) + # threshold = declare(fixed) + + I = [declare(fixed) for idx in range(num_qb)] + I_herald = [declare(fixed) for idx in range(num_qb)] + Q = [declare(fixed) for idx in range(num_qb)] + I_st = [declare_stream() for idx in range(num_qb)] + Q_st = [declare_stream() for idx in range(num_qb)] + N_st = declare_stream() + + threshold = [] + for idx in range(num_qb): + threshold.append(machine.readout_resonators[idx].threshold) + + with for_(n, 0, n < n_shots, n + 1): + for idx in range(num_qb): + # update_frequency(f"qb{idx}", 0) + # reset_phase(f"qb{idx}") + + play("x90", f"qb{idx}") + # play("x180", f"qb{idx}") + + wait(int(200e-9 // 4e-9), f"qb{idx}") + + align(f"qb{idx}", f"rr{idx}") + + measure("readout", f"rr{idx}", None, + dual_demod.full("rotated_cos", "out1", "rotated_minus_sin", "out2", I_herald[idx])) + align(f"qb{idx}", f"rr{idx}") + + wait(int(4e-6 // 4e-9), f"qb{idx}") + + with if_(I_herald[idx] < threshold[idx]): + # pi + play("x180", f"qb{idx}") + + align(f"qb{idx}", f"rr{idx}") + + measure("readout", f"rr{idx}", None, + dual_demod.full("rotated_cos", "out1", "rotated_minus_sin", "out2", I[idx]), + dual_demod.full("rotated_sin", "out1", "rotated_cos", "out2", Q[idx])) + + wait(cooldown_time, f"rr{idx}") + # wait(int(1e-6//4e-9), f"rr{idx}") + + save(I[idx], I_st[idx]) + save(Q[idx], Q_st[idx]) + assign(n_cnt, n_cnt + 1) + save(n_cnt, N_st) + + with stream_processing(): + N_st.save("N") + + for idx in range(num_qb): + I_st[idx].save_all(f'I{idx}') + Q_st[idx].save_all(f'Q{idx}') + + return active_reset_program \ No newline at end of file diff --git a/joe_testing_active_reset/active_reset_gui.py b/joe_testing_active_reset/active_reset_gui.py new file mode 100644 index 00000000..bb3bb11a --- /dev/null +++ b/joe_testing_active_reset/active_reset_gui.py @@ -0,0 +1,142 @@ +import sys +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import pyqtgraph as pg +import numpy as np + +class ActiveResetGUI(QWidget): + def __init__(self, num_qubits): + + self.num_qubits = num_qubits + super(ActiveResetGUI, self).__init__() + self.initUI() + self._populate_list() + + + def initUI(self): + + hbox = QHBoxLayout(self) + + # create some widgets + + self.left = pg.LayoutWidget() + self.right = pg.LayoutWidget() + + self.qubit_list = QComboBox() + + self.info_box = pg.TableWidget(3, 3) + + self.info_box.setData([ + ['', 'Passive', 'Active'], + ['Fidelity', '98%', '85%'], + ['Time', '100 ns', '90 ns'] + ]) + + + self.graphics_window = pg.GraphicsLayoutWidget() + self.plt1 = self.graphics_window.addPlot(title='Passive reset') + self.plt2 = self.graphics_window.addPlot(title='Active reset') + + self.left.addWidget(self.qubit_list, 0, 0) + self.left.addWidget(self.info_box, 1, 0) + self.left.addWidget(QFrame(), 2, 0) + + self.info_box.horizontalHeader().setVisible(False) + self.info_box.verticalHeader().setVisible(False) + + self.right.addWidget(self.graphics_window) + + splitter = QSplitter(Qt.Horizontal) + splitter.addWidget(self.left) + splitter.addWidget(self.right) + + # 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() + first_col_width = table_size.width() + 3 + splitter.setSizes([first_col_width + 3, 950]) + self.info_box.setMaximumHeight(int(table_size.height() * (3/4))) + self.info_box.setShowGrid(False) + + + hbox.addWidget(splitter) + + self.setLayout(hbox) + + QApplication.setStyle(QStyleFactory.create('Cleanlooks')) + + self.setGeometry(100, 100, 1200, 500) + self.setWindowTitle('Qubit reset comparison') + + # self.qubit_list.itemDoubleClicked.connect(self.func) + self.qubit_list.currentIndexChanged.connect(self.func) + + + self.show() + + def generate_fake_histograms(self, a, b): + ## make interesting distribution of values + vals1 = np.random.normal(size=500) + vals2 = np.random.normal(size=260, loc=4) + ## compute standard histogram + y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) + y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) + return a * y, x, b * y2, x2 + + def generate_random_table_data(self): + + pf, af = np.random.rand(2) * 100 + pt, at = np.random.rand(2) / 5e8 + + return pf, pt, af, at + + def generate_table(self, pf, pt, af, at): + table_data = [ + ['', 'Passive', 'Active'], + ['Fidelity', f'{pf:.2f}%', f'{af:.2f}%'], + ['Time', f'{(pt * 1e9):.2f} ns', f'{(at * 1e9):.2f} ns'] + ] + + return table_data + + def set_table(self, pf, pt, af, at): + data = self.generate_table(pf, pt, af, at) + self.info_box.setData(data) + + def func(self): + + self.plt1.clear() + self.plt2.clear() + + y, x, y2, x2 = self.generate_fake_histograms(1, 1) + + ## Using stepMode="center" causes the plot to draw two lines for each sample. + ## notice that len(x) == len(y)+1 + self.plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) + self.plt1.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + + y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) + self.plt2.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) + self.plt2.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + + self.set_table(*self.generate_random_table_data()) + + + def _populate_list(self): + + for i in range(self.num_qubits): + self.qubit_list.addItem( + f'Qubit {i + 1}' + ) + + + +def main(): + app = QApplication(sys.argv) + ex = ActiveResetGUI(32) + # sys.exit(app.exec_()) + app.exec_() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/joe_testing_active_reset/active_reset_gui_old.py b/joe_testing_active_reset/active_reset_gui_old.py new file mode 100644 index 00000000..98a71551 --- /dev/null +++ b/joe_testing_active_reset/active_reset_gui_old.py @@ -0,0 +1,218 @@ +# from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTabWidget, QGridLayout, \ +# QVBoxLayout + +from PyQt5.QtWidgets import * + +import numpy as np +import pyqtgraph as pg +from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets + +from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget + + +# just use widget doesn't need an app window +class ActiveReset(QApplication): + + # def __init__(self, num_qubits): + # super().__init__([]) + # self.num_qubits = num_qubits + # + # self.main_window = QWidget() + # self.main_window.setWindowTitle('Active reset viewer') + # + # self.splitter = QSplitter() + # self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) + # self.layout_widget = QtWidgets.QWidget(self.splitter) + # + # + # # create some widgets + # self.qubit_list = QListWidget() + # + # self.graphics_window = pg.GraphicsLayoutWidget() + # self.plt1 = self.graphics_window.addPlot() + # self.plt2 = self.graphics_window.addPlot() + # + # + # # self.graphics_window.ci.setBorder((50, 50, 100)) + # + # # self.passive_reset_layout = self.graphics_window.addLayout() + # # self.passive_reset_layout.addLabel("Passive reset") + # # self.passive_reset_layout.nextRow() + # # self.passive_view_box = self.passive_reset_layout.addViewBox() + # # + # # self.active_reset_layout = self.graphics_window.addLayout() + # # self.active_reset_layout.addLabel("Active reset") + # # self.active_reset_layout.nextRow() + # + # # self.active_view_box = self.active_reset_layout.addViewBox() + # self.text = QWidget() + # self.text_layout = QGridLayout() + # self.text.setLayout(self.text_layout) + # self.set_up_text_area() + # + # self.main_layout = QGridLayout() + # + # self.main_window.setLayout(self.main_layout) + # + # + # self.main_layout.addWidget(self.qubit_list, 0, 0, 2, 1) + # self.main_layout.addWidget(self.text, 2, 0, 2, 1) + # # self.main_layout.addWidget(self.splitter, 0, 2, 1, 1) + # self.main_layout.addWidget(self.graphics_window, 0, 3, 4, 6) + # + # self.populate_list() + # self.qubit_list.itemDoubleClicked.connect(self.func) + # + # + # self.main_window.show() + + def __init__(self, num_qubits): + super().__init__([]) + self.num_qubits = num_qubits + + self.main_window = QWidget() + self.main_window.setWindowTitle('Active reset viewer') + + + self.splitter = QSplitter() + + + + # create some widgets + self.qubit_list = QListWidget() + + self.graphics_window = pg.GraphicsLayoutWidget() + self.plt1 = self.graphics_window.addPlot() + self.plt2 = self.graphics_window.addPlot() + + self.text = QWidget() + self.text_layout = QGridLayout() + self.text.setLayout(self.text_layout) + self.set_up_text_area() + + + self.graphics_window.ci.setBorder((50, 50, 100)) + + self.passive_reset_layout = self.graphics_window.addLayout() + self.passive_reset_layout.addLabel("Passive reset") + self.passive_reset_layout.nextRow() + self.passive_view_box = self.passive_reset_layout.addViewBox() + + self.active_reset_layout = self.graphics_window.addLayout() + self.active_reset_layout.addLabel("Active reset") + self.active_reset_layout.nextRow() + + self.active_view_box = self.active_reset_layout.addViewBox() + + + self.main_layout = QGridLayout() + + self.main_window.setLayout(self.main_layout) + + + self.main_layout.addWidget(self.qubit_list, 0, 0, 2, 1) + self.main_layout.addWidget(self.text, 2, 0, 2, 1) + # self.main_layout.addWidget(self.splitter, 0, 2, 1, 1) + self.main_layout.addWidget(self.graphics_window, 0, 3, 4, 6) + + self.populate_list() + self.qubit_list.itemDoubleClicked.connect(self.func) + + + self.main_window.show() + + def set_up_text_area(self): + + self.info_title = QLabel('Fidelity information for qubit ') + self.info_passive_title = QLabel('Passive') + self.info_active_title = QLabel('Active') + + self.info_passive_fid = QLabel('Fidelity: ') + self.info_passive_time = QLabel('Time: ') + + self.info_active_fid = QLabel('Fidelity: ') + self.info_active_time = QLabel('Time: ') + + self.text_layout.addWidget(self.info_title, 0, 0, 1, 1) + self.text_layout.addWidget(self.info_passive_title, 1, 0) + self.text_layout.addWidget(self.info_active_title, 1, 1) + + self.text_layout.addWidget(self.info_passive_fid, 2, 0) + self.text_layout.addWidget(self.info_passive_time, 3, 0) + self.text_layout.addWidget(self.info_active_fid, 2, 1) + self.text_layout.addWidget(self.info_active_time, 3, 1) + + + def update_text_area(self, qubit_id): + + self.info_title.setText(f'Fidelity information for qubit {qubit_id}') + + self.info_passive_fid.setText(f'Fidelity: {100 * (1 - (0.2 * np.random.rand())):.1f}%') + self.info_passive_time.setText(f'Time: {98.5} ns') + self.info_active_fid.setText(f'Fidelity: {100 * (1 - (0.2 * np.random.rand())):.1f}%') + self.info_active_time.setText(f'Time: {100} ns') + + def func(self): + + self.plt1.clear() + self.plt2.clear() + + y, x, y2, x2 = self.generate_fake_histograms(1, 1) + + ## Using stepMode="center" causes the plot to draw two lines for each sample. + ## notice that len(x) == len(y)+1 + self.plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) + self.plt1.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + + y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) + self.plt2.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) + self.plt2.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + + """ + def func(self): + + qubit = self.qubit_list.currentRow() + + # clear the view boxes so we don't plot histograms on top of each other + self.passive_view_box.clear() + self.active_view_box.clear() + y, x, y2, x2 = self.generate_fake_histograms(1, 1) + figure1 = pg.PlotDataItem(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) + figure2 = pg.PlotDataItem(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + self.passive_view_box.addItem(figure1) + self.passive_view_box.addItem(figure2) + + y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) + figure3 = pg.PlotDataItem(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) + figure4 = pg.PlotDataItem(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + + self.active_view_box.addItem(figure3) + self.active_view_box.addItem(figure4) + + self.update_text_area(qubit + 1) + + """ + + + def generate_fake_histograms(self, a, b): + ## make interesting distribution of values + vals1 = np.random.normal(size=500) + vals2 = np.random.normal(size=260, loc=4) + ## compute standard histogram + y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) + y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) + return a * y, x, b * y2, x2 + + def populate_list(self): + + for i in range(self.num_qubits): + self.qubit_list.addItem( + f'Qubit {i + 1}' + ) + + + + +if __name__ == '__main__': + app = ActiveReset(32) + app.exec_() diff --git a/joe_testing_active_reset/basic_program.py b/joe_testing_active_reset/basic_program.py new file mode 100644 index 00000000..9bcccded --- /dev/null +++ b/joe_testing_active_reset/basic_program.py @@ -0,0 +1,176 @@ +import numpy as np +Amp = 0.5 + +config = { + 'version': 1, + + 'controllers': { + 'con1': { + 'type': 'opx1', + 'analog_outputs': { + 1: {'offset': +0.0}, + 2: {'offset': +0.0}, + 3: {'offset': +0.0}, + 4: {'offset': +0.0}, + }, + 'digital_outputs': { + 1: {}, + }, + 'analog_inputs': { + 1: {'offset': +0.0}, + 2: {'offset': +0.0}, + } + } + }, + + 'elements': { + 'qubit': { + 'mixInputs': { + 'I': ('con1', 1), + 'Q': ('con1', 2), + 'lo_frequency': 5.10e9, + 'mixer': 'mixer_qubit' + }, + 'intermediate_frequency': 5.15e6, + 'operations': { + 'gauss_pulse': 'gauss_pulse_in' + }, + }, + 'resonator': { + 'mixInputs': { + 'I': ('con1', 3), + 'Q': ('con1', 4), + 'lo_frequency': 6.00e9, + 'mixer': 'mixer_res' + }, + 'intermediate_frequency': 6.12e6, + 'operations': { + 'readout': 'meas_pulse_in', + }, + 'time_of_flight': 180, + 'smearing': 0, + 'outputs': { + 'out1': ('con1', 1), + 'out2': ('con1', 2) + } + + }, + }, + + 'pulses': { + 'meas_pulse_in': { + 'operation': 'measurement', + 'length': 20, + 'waveforms': { + 'I': 'exc_wf', + 'Q': 'zero_wf' + }, + 'integration_weights': { + 'cos': 'cos', + 'sin': 'sin', + 'minus_sin': 'minus_sin', + }, + 'digital_marker': 'marker1' + }, + 'gauss_pulse_in': { + 'operation': 'control', + 'length': 20, + 'waveforms': { + 'I': 'gauss_wf', + 'Q': 'zero_wf' + }, + } + }, + + 'waveforms': { + 'exc_wf': { + 'type': 'constant', + 'sample': 0.479 + }, + 'zero_wf': { + 'type': 'constant', + 'sample': 0.0 + }, + 'gauss_wf': { + 'type': 'arbitrary', + 'samples': [0.005, 0.013, + 0.02935, 0.05899883936462147, + 0.10732436763802927, 0.1767030571463228, + 0.2633180579359862, 0.35514694106994277, + 0.43353720001453067, 0.479, 0.479, + 0.4335372000145308, 0.3551469410699429, + 0.26331805793598645, 0.17670305714632292, + 0.10732436763802936, 0.05899883936462152, + 0.029354822126316085, 0.01321923408389493, + 0.005387955348880817] + } + }, + + 'digital_waveforms': { + 'marker1': { + 'samples': [(1, 4), (0, 2), (1, 1), (1, 0)] + } + }, + + 'integration_weights': { + 'cos': { + 'cosine': [(4.0,20)], + 'sine': [(0.0,20)] + }, + 'sin': { + 'cosine': [(0.0,20)], + 'sine': [(4.0,20)] + }, + 'minus_sin': { + 'cosine': [(0.0,20)], + 'sine': [(-4.0,20)] + }, + }, + + 'mixers': { + 'mixer_res': [ + {'intermediate_frequency': 6.12e6, 'lo_freq': 6.00e9, 'correction': [1.0, 0.0, 0.0, 1.0]} + ], + 'mixer_qubit': [ + {'intermediate_frequency': 5.15e6, 'lo_freq': 5.10e9, 'correction': [1.0, 0.0, 0.0, 1.0]} + ], + } +} + +gaus_pulse_len = 20 # nsec +gaus_arg = np.linspace(-3, 3, gaus_pulse_len) +gaus_wf = np.exp(-gaus_arg**2/2) +gaus_wf = Amp * gaus_wf / np.max(gaus_wf) + +HOST = '172.16.2.103' +PORT = 80 + +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager + +qmm = QuantumMachinesManager(host=HOST, port=PORT) + +qm = qmm.open_qm(config) + +with program() as program: + + I = declare(fixed) + Q = declare(fixed) + a = declare(fixed) + n_rep = declare(int) + + # this is the averaging loop - the number of repeititions. + with for_(n_rep, 0, n_rep < 100, n_rep + 1): + + with for_(a, 0., a < 1, a + 0.01): + + play('gauss_pulse' * amp(a), '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)) + + + + + diff --git a/joe_testing_active_reset/build_active_reset_program.py b/joe_testing_active_reset/build_active_reset_program.py new file mode 100644 index 00000000..ed728fd2 --- /dev/null +++ b/joe_testing_active_reset/build_active_reset_program.py @@ -0,0 +1,153 @@ +import numpy as np +Amp = 0.5 + +config = { + 'version': 1, + + 'controllers': { + 'con1': { + 'type': 'opx1', + 'analog_outputs': { + 1: {'offset': +0.0}, + 2: {'offset': +0.0}, + 3: {'offset': +0.0}, + 4: {'offset': +0.0}, + }, + 'digital_outputs': { + 1: {}, + }, + 'analog_inputs': { + 1: {'offset': +0.0}, + 2: {'offset': +0.0}, + } + } + }, + + 'elements': { + 'qubit': { + 'mixInputs': { + 'I': ('con1', 1), + 'Q': ('con1', 2), + 'lo_frequency': 5.10e9, + 'mixer': 'mixer_qubit' + }, + 'intermediate_frequency': 5.15e6, + 'operations': { + 'gauss_pulse': 'gauss_pulse_in' + }, + }, + 'resonator': { + 'mixInputs': { + 'I': ('con1', 3), + 'Q': ('con1', 4), + 'lo_frequency': 6.00e9, + 'mixer': 'mixer_res' + }, + 'intermediate_frequency': 6.12e6, + 'operations': { + 'readout': 'meas_pulse_in', + }, + 'time_of_flight': 180, + 'smearing': 0, + 'outputs': { + 'out1': ('con1', 1), + 'out2': ('con1', 2) + } + + }, + }, + + 'pulses': { + 'meas_pulse_in': { + 'operation': 'measurement', + 'length': 20, + 'waveforms': { + 'I': 'exc_wf', + 'Q': 'zero_wf' + }, + 'integration_weights': { + 'cos': 'cos', + 'sin': 'sin', + 'minus_sin': 'minus_sin', + }, + 'digital_marker': 'marker1' + }, + 'gauss_pulse_in': { + 'operation': 'control', + 'length': 20, + 'waveforms': { + 'I': 'gauss_wf', + 'Q': 'zero_wf' + }, + } + }, + + 'waveforms': { + 'exc_wf': { + 'type': 'constant', + 'sample': 0.479 + }, + 'zero_wf': { + 'type': 'constant', + 'sample': 0.0 + }, + 'gauss_wf': { + 'type': 'arbitrary', + 'samples': [0.005, 0.013, + 0.02935, 0.05899883936462147, + 0.10732436763802927, 0.1767030571463228, + 0.2633180579359862, 0.35514694106994277, + 0.43353720001453067, 0.479, 0.479, + 0.4335372000145308, 0.3551469410699429, + 0.26331805793598645, 0.17670305714632292, + 0.10732436763802936, 0.05899883936462152, + 0.029354822126316085, 0.01321923408389493, + 0.005387955348880817] + } + }, + + 'digital_waveforms': { + 'marker1': { + 'samples': [(1, 4), (0, 2), (1, 1), (1, 0)] + } + }, + + 'integration_weights': { + 'cos': { + 'cosine': [(4.0,20)], + 'sine': [(0.0,20)] + }, + 'sin': { + 'cosine': [(0.0,20)], + 'sine': [(4.0,20)] + }, + 'minus_sin': { + 'cosine': [(0.0,20)], + 'sine': [(-4.0,20)] + }, + }, + + 'mixers': { + 'mixer_res': [ + {'intermediate_frequency': 6.12e6, 'lo_freq': 6.00e9, 'correction': [1.0, 0.0, 0.0, 1.0]} + ], + 'mixer_qubit': [ + {'intermediate_frequency': 5.15e6, 'lo_freq': 5.10e9, 'correction': [1.0, 0.0, 0.0, 1.0]} + ], + } +} + +gaus_pulse_len = 20 # nsec +gaus_arg = np.linspace(-3, 3, gaus_pulse_len) +gaus_wf = np.exp(-gaus_arg**2/2) +gaus_wf = Amp * gaus_wf / np.max(gaus_wf) + + + + +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager + +qmm = QuantumMachinesManager() + +qm = qmm.open_qm(config) diff --git a/qualang_tools/analysis/a.py b/joe_testing_active_reset/build_passive_reset_program.py similarity index 100% rename from qualang_tools/analysis/a.py rename to joe_testing_active_reset/build_passive_reset_program.py diff --git a/joe_testing_active_reset/configuration.py b/joe_testing_active_reset/configuration.py new file mode 100644 index 00000000..dd7d984d --- /dev/null +++ b/joe_testing_active_reset/configuration.py @@ -0,0 +1,475 @@ +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" +qop_ip = '172.16.2.103' + +# Qubits +qubit_IF = 50 * u.MHz +qubit_LO = 7 * u.GHz +mixer_qubit_g = 0.0 +mixer_qubit_phi = 0.0 + +qubit_T1 = int(10 * u.us) + +saturation_len = 1000 +saturation_amp = 0.1 +const_len = 100 +const_amp = 0.1 +square_pi_len = 100 +square_pi_amp = 0.1 + +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) + +displace_len = 40 +displace_sigma = displace_len / 5 +displace_amp = 0.35 +displace_wf = displace_amp * gaussian(displace_len, displace_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_IF = 60 * u.MHz +resonator_LO = 5.5 * u.GHz +mixer_resonator_g = 0.0 +mixer_resonator_phi = 0.0 + +time_of_flight = 180 + +short_readout_len = 500 +short_readout_amp = 0.4 +readout_len = 5000 +readout_amp = 0.2 +long_readout_len = 50000 +long_readout_amp = 0.1 + +# IQ Plane +rotation_angle = (0.0 / 180) * np.pi +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 + }, + "digital_outputs": {}, + "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", + "saturation": "saturation_pulse", + "gauss": "gaussian_pulse", + "pi": "x180_pulse", + "pi_half": "x90_pulse", + "x90": "x90_pulse", + "x180": "x180_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", + "displace": "displace_pulse", + "short_readout": "short_readout_pulse", + "readout": "readout_pulse", + "long_readout": "long_readout_pulse", + }, + "outputs": { + "out1": ("con1", 1), + "out2": ("con1", 2), + }, + "time_of_flight": time_of_flight, + "smearing": 0, + }, + }, + "pulses": { + "const_pulse": { + "operation": "control", + "length": const_len, + "waveforms": { + "I": "const_wf", + "Q": "zero_wf", + }, + }, + "square_pi_pulse": { + "operation": "control", + "length": square_pi_len, + "waveforms": { + "I": "square_pi_wf", + "Q": "zero_wf", + }, + }, + "saturation_pulse": { + "operation": "control", + "length": saturation_len, + "waveforms": {"I": "saturation_drive_wf", "Q": "zero_wf"}, + }, + "gaussian_pulse": { + "operation": "control", + "length": gauss_len, + "waveforms": { + "I": "gauss_wf", + "Q": "zero_wf", + }, + }, + "displace_pulse": { + "operation": "control", + "length": displace_len, + "waveforms": { + "I": "displace_wf", + "Q": "displace_wf", + }, + }, + "x90_pulse": { + "operation": "control", + "length": x90_len, + "waveforms": { + "I": "x90_I_wf", + "Q": "x90_Q_wf", + }, + }, + "x180_pulse": { + "operation": "control", + "length": x180_len, + "waveforms": { + "I": "x180_I_wf", + "Q": "x180_Q_wf", + }, + }, + "-x90_pulse": { + "operation": "control", + "length": minus_x90_len, + "waveforms": { + "I": "minus_x90_I_wf", + "Q": "minus_x90_Q_wf", + }, + }, + "y90_pulse": { + "operation": "control", + "length": y90_len, + "waveforms": { + "I": "y90_I_wf", + "Q": "y90_Q_wf", + }, + }, + "y180_pulse": { + "operation": "control", + "length": y180_len, + "waveforms": { + "I": "y180_I_wf", + "Q": "y180_Q_wf", + }, + }, + "-y90_pulse": { + "operation": "control", + "length": minus_y90_len, + "waveforms": { + "I": "minus_y90_I_wf", + "Q": "minus_y90_Q_wf", + }, + }, + "short_readout_pulse": { + "operation": "measurement", + "length": short_readout_len, + "waveforms": { + "I": "short_readout_wf", + "Q": "zero_wf", + }, + "integration_weights": { + "cos": "short_cosine_weights", + "sin": "short_sine_weights", + "minus_sin": "short_minus_sine_weights", + "rotated_cos": "short_rotated_cosine_weights", + "rotated_sin": "short_rotated_sine_weights", + "rotated_minus_sin": "short_rotated_minus_sine_weights", + }, + "digital_marker": "ON", + }, + "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", + }, + "long_readout_pulse": { + "operation": "measurement", + "length": long_readout_len, + "waveforms": { + "I": "long_readout_wf", + "Q": "zero_wf", + }, + "integration_weights": { + "cos": "long_cosine_weights", + "sin": "long_sine_weights", + "minus_sin": "long_minus_sine_weights", + "rotated_cos": "long_rotated_cosine_weights", + "rotated_sin": "long_rotated_sine_weights", + "rotated_minus_sin": "long_rotated_minus_sine_weights", + }, + "digital_marker": "ON", + }, + }, + "waveforms": { + "const_wf": {"type": "constant", "sample": const_amp}, + "saturation_drive_wf": {"type": "constant", "sample": saturation_amp}, + "square_pi_wf": {"type": "constant", "sample": square_pi_amp}, + "displace_wf": {"type": "arbitrary", "samples": displace_wf.tolist()}, + "zero_wf": {"type": "constant", "sample": 0.0}, + "gauss_wf": {"type": "arbitrary", "samples": gauss_wf.tolist()}, + "x90_I_wf": {"type": "arbitrary", "samples": x90_I_wf.tolist()}, + "x90_Q_wf": {"type": "arbitrary", "samples": x90_Q_wf.tolist()}, + "x180_I_wf": {"type": "arbitrary", "samples": x180_I_wf.tolist()}, + "x180_Q_wf": {"type": "arbitrary", "samples": x180_Q_wf.tolist()}, + "minus_x90_I_wf": {"type": "arbitrary", "samples": minus_x90_I_wf.tolist()}, + "minus_x90_Q_wf": {"type": "arbitrary", "samples": minus_x90_Q_wf.tolist()}, + "y90_Q_wf": {"type": "arbitrary", "samples": y90_Q_wf.tolist()}, + "y90_I_wf": {"type": "arbitrary", "samples": y90_I_wf.tolist()}, + "y180_Q_wf": {"type": "arbitrary", "samples": y180_Q_wf.tolist()}, + "y180_I_wf": {"type": "arbitrary", "samples": y180_I_wf.tolist()}, + "minus_y90_Q_wf": {"type": "arbitrary", "samples": minus_y90_Q_wf.tolist()}, + "minus_y90_I_wf": {"type": "arbitrary", "samples": minus_y90_I_wf.tolist()}, + "short_readout_wf": {"type": "constant", "sample": short_readout_amp}, + "readout_wf": {"type": "constant", "sample": readout_amp}, + "long_readout_wf": {"type": "constant", "sample": long_readout_amp}, + }, + "digital_waveforms": { + "ON": {"samples": [(1, 0)]}, + }, + "integration_weights": { + "short_cosine_weights": { + "cosine": [(1.0, short_readout_len)], + "sine": [(0.0, short_readout_len)], + }, + "short_sine_weights": { + "cosine": [(0.0, short_readout_len)], + "sine": [(1.0, short_readout_len)], + }, + "short_minus_sine_weights": { + "cosine": [(0.0, short_readout_len)], + "sine": [(-1.0, short_readout_len)], + }, + "short_rotated_cosine_weights": { + "cosine": [(np.cos(rotation_angle), short_readout_len)], + "sine": [(-np.sin(rotation_angle), short_readout_len)], + }, + "short_rotated_sine_weights": { + "cosine": [(np.sin(rotation_angle), short_readout_len)], + "sine": [(np.cos(rotation_angle), short_readout_len)], + }, + "short_rotated_minus_sine_weights": { + "cosine": [(-np.sin(rotation_angle), short_readout_len)], + "sine": [(-np.cos(rotation_angle), short_readout_len)], + }, + "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)], + }, + "long_cosine_weights": { + "cosine": [(1.0, long_readout_len)], + "sine": [(0.0, long_readout_len)], + }, + "long_sine_weights": { + "cosine": [(0.0, long_readout_len)], + "sine": [(1.0, long_readout_len)], + }, + "long_minus_sine_weights": { + "cosine": [(0.0, long_readout_len)], + "sine": [(-1.0, long_readout_len)], + }, + "long_rotated_cosine_weights": { + "cosine": [(np.cos(rotation_angle), long_readout_len)], + "sine": [(-np.sin(rotation_angle), long_readout_len)], + }, + "long_rotated_sine_weights": { + "cosine": [(np.sin(rotation_angle), long_readout_len)], + "sine": [(np.cos(rotation_angle), long_readout_len)], + }, + "long_rotated_minus_sine_weights": { + "cosine": [(-np.sin(rotation_angle), long_readout_len)], + "sine": [(-np.cos(rotation_angle), long_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/joe_testing_active_reset/fake_data.py b/joe_testing_active_reset/fake_data.py new file mode 100644 index 00000000..c655af89 --- /dev/null +++ b/joe_testing_active_reset/fake_data.py @@ -0,0 +1,35 @@ +import numpy as np +import matplotlib.pyplot as plt + +from t1_cycle_simulator import t1_cycle + + + + + +def active_reset_experiment_simulator(v_0, v_1, sigma, num_samples, threshold): + + # generate random sample + + + # based on that sample, if high state reset to low state + + # measure + pass + +v_0 = 0.001 +v_1 = 0.01 +sigma = 0.04 ** 2 +T1 = 350e-6 +integration_time = 15e-6 +num_samples = 4000 +prob_state_1 = 0.5 + +data = t1_cycle(v_0, v_1, sigma,T1, num_samples, prob_state_1, integration_time) + +plt.figure() +plt.hist(data, bins=30) +plt.xlim([-0.04, 0.04]) +plt.show() + + diff --git a/joe_testing_active_reset/sample_generator.py b/joe_testing_active_reset/sample_generator.py new file mode 100644 index 00000000..bbce34b3 --- /dev/null +++ b/joe_testing_active_reset/sample_generator.py @@ -0,0 +1,64 @@ +""" +Created on 04/08/2022 +@author jdh +""" + +import numpy as np + + +# runs at around 0.1 ns per sample (2000 samples in ~200 us) +# obviously big overhead if doing small number of samples. +def _generate_spin_samples(v_rf_s, v_rf_t, sigma, T1, number_of_samples, integration_time, triplets): + """ + triplets tells you which samples are triplets (to start with). This way this method can be used to simulate + rabi or t1 cycle + Triplets have a probability of decaying set by the decay rate. + :param number_of_samples: + :param triplet_probability: + :return: + """ + + + + # quicker to generate all the samples first and then pick which ones we want + # based on the variable above (triplets) + + singlet_samples = np.random.normal( + loc=v_rf_s, + scale=sigma, + size=number_of_samples + ) + + singlet_samples_for_triplet_decay = np.random.normal( + loc=v_rf_s, + scale=sigma, + size=number_of_samples + ) + + triplet_samples = np.random.normal( + loc=v_rf_t, + scale=sigma, + size=number_of_samples + ) + + # add decay to the tT1,riplet samples + # ratio of decay time to integration time gives how far along + # v axis that samples moves towards singlet (representing it decaying + # at some point through the measurement window) + + decay_times = np.random.exponential( + scale=T1, + size=number_of_samples + ) + + # how much of the measurement was a triplet is given by this ratio. + # clipped to be max 1 + triplet_contribution = np.clip( + decay_times / integration_time, 0, 1 + ) + + decayed_triplet_samples = (triplet_contribution * triplet_samples) + ( + (1 - triplet_contribution) * singlet_samples_for_triplet_decay + ) + + return np.where(triplets, decayed_triplet_samples, singlet_samples) \ No newline at end of file diff --git a/joe_testing_active_reset/t1_basic.py b/joe_testing_active_reset/t1_basic.py new file mode 100644 index 00000000..dd0ae0c8 --- /dev/null +++ b/joe_testing_active_reset/t1_basic.py @@ -0,0 +1,110 @@ +""" +T1.py: Measures T1 +""" +from qm.qua import * +from qm.QuantumMachinesManager import QuantumMachinesManager +from configuration import * +import matplotlib.pyplot as plt +import numpy as np +from qm import SimulationConfig +from qualang_tools.loops import from_array + +from qm.simulate.credentials import create_credentials + +import logging +logging.basicConfig(level='INFO') +logger = logging.getLogger(__name__) + +################### +# The QUA program # +################### + +tau_min = 4 # in clock cycles +tau_max = 100 # in clock cycles +d_tau = 2 # in clock cycles +taus = np.arange(tau_min, tau_max + 0.1, d_tau) # + 0.1 to add t_max to taus + +n_avg = 1e4 +cooldown_time = 5 * qubit_T1 // 4 + +with program() as T1: + n = declare(int) + n_st = declare_stream() + I = declare(fixed) + I_st = declare_stream() + Q = declare(fixed) + Q_st = declare_stream() + tau = declare(int) + + with for_(n, 0, n < n_avg, n + 1): + with for_(*from_array(tau, taus)): + play("pi", "qubit") + wait(tau, "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), + ) + save(I, I_st) + save(Q, Q_st) + wait(cooldown_time, "resonator") + save(n, n_st) + + with stream_processing(): + I_st.buffer(len(taus)).average().save("I") + Q_st.buffer(len(taus)).average().save("Q") + n_st.save("iteration") + +##################################### +# Open Communication with the QOP # +##################################### +logger.info('connecting to qmm...') +qmm = QuantumMachinesManager( + host="theo-4c195fa0.dev.quantum-machines.co", + port=443, + credentials=create_credentials()) +logger.info('connected.') + +####################### +# Simulate or execute # +####################### + +simulate = True + +if simulate: + logger.info("simulating") + simulation_config = SimulationConfig(duration=1000) # in clock cycles + + logger.info('simulation config loaded') + job = qmm.simulate(config, T1, simulation_config) + + logger.info('job executed') + + job.get_simulated_samples().con1.plot() + +else: + qm = qmm.open_qm(config) + + job = qm.execute(T1) + # Get results from QUA program + results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live") + # Live plotting + fig = plt.figure() + interrupt_on_close(fig, job) # Interrupts the job when closing the figure + while results.is_processing(): + # Fetch results + I, Q, iteration = results.fetch_all() + # Progress bar + progress_counter(iteration, n_avg, start_time=results.get_start_time()) + # Plot results + plt.cla() + plt.plot(4 * taus, I, ".", label="I") + plt.plot(4 * taus, Q, ".", label="Q") + plt.xlabel("Decay time [ns]") + plt.ylabel("I & Q amplitude [a.u.]") + plt.title("T1 measurement") + plt.legend() + plt.pause(0.1) \ No newline at end of file diff --git a/joe_testing_active_reset/t1_cycle_simulator.py b/joe_testing_active_reset/t1_cycle_simulator.py new file mode 100644 index 00000000..4c5e5fbf --- /dev/null +++ b/joe_testing_active_reset/t1_cycle_simulator.py @@ -0,0 +1,27 @@ +""" +Created on 04/08/2022 +@author jdh +""" + +from sample_generator import _generate_spin_samples + +import numpy as np + + +def t1_cycle(v_rf_s, v_rf_t, sigma, T1, number_of_samples, triplet_probability, integration_time): + """ + :param v_rf_s: mean singlet measurement value + :param v_rf_t: mean triplet measurement value + :param sigma: gaussian measurement noise + :param T1: rate parameter for average spin lifetime + :param number_of_samples: self + :param triplet_probability: probability of loading a triplet for each measurement (stick to 0.5) + :param integration_time: integration time for each measurement + :return: simulated data for a rabi experiment + """ + + # draw samples. 0 means singlet, 1 means triplet + triplets = np.random.binomial(1, triplet_probability, number_of_samples) + + return _generate_spin_samples(v_rf_s, v_rf_t, sigma, T1, number_of_samples, integration_time, triplets) + diff --git a/qualang_tools/analysis/active_reset.py b/qualang_tools/analysis/active_reset.py new file mode 100644 index 00000000..e69de29b diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py index 4da40475..7b170a83 100644 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -7,8 +7,8 @@ from tkinter import * from tkinter import ttk -from discriminator import two_state_discriminator -from viewer import App +from .discriminator import two_state_discriminator +# from .viewer import App @dataclass class DiscriminatorDataclass: @@ -168,10 +168,11 @@ def change_dropdown(*args): root.geometry("600x500") root.mainloop() -from dataPresenter import multiQubitReadoutPresenter -import pyqtgraph as pg if __name__ == '__main__': + from dataPresenter import multiQubitReadoutPresenter + import pyqtgraph as pg + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 32)).T iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T diff --git a/qualang_tools/analysis/mpltest.py b/qualang_tools/analysis/mpltest.py deleted file mode 100644 index d1ec27d4..00000000 --- a/qualang_tools/analysis/mpltest.py +++ /dev/null @@ -1,35 +0,0 @@ -import sys -import matplotlib -matplotlib.use('Qt5Agg') - -from PyQt5 import QtCore, QtWidgets - -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg -from matplotlib.figure import Figure - - -class MplCanvas(FigureCanvasQTAgg): - - def __init__(self, parent=None, width=5, height=4, dpi=100): - fig = Figure(figsize=(width, height), dpi=dpi) - self.axes = fig.add_subplot(111) - super(MplCanvas, self).__init__(fig) - - -class MainWindow(QtWidgets.QMainWindow): - - def __init__(self, *args, **kwargs): - super(MainWindow, self).__init__(*args, **kwargs) - - # Create the maptlotlib FigureCanvas object, - # which defines a single set of axes as self.axes. - sc = MplCanvas(self, width=5, height=4, dpi=100) - sc.axes.plot([0,1,2,3,4], [10,1,20,3,40]) - self.setCentralWidget(sc) - - self.show() - - -app = QtWidgets.QApplication(sys.argv) -w = MainWindow() -app.exec_() \ No newline at end of file diff --git a/qualang_tools/analysis/tabbed_widget.py b/qualang_tools/analysis/tabbed_widget.py deleted file mode 100644 index accac6a6..00000000 --- a/qualang_tools/analysis/tabbed_widget.py +++ /dev/null @@ -1,339 +0,0 @@ -import keyword -import os -import pkgutil -import re -import subprocess -import sys -from argparse import Namespace -from collections import OrderedDict -from functools import lru_cache - -import pyqtgraph as pg -from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets - -app = pg.mkQApp() - - -path = os.path.abspath(os.path.dirname(__file__)) -sys.path.insert(0, path) - -import exampleLoaderTemplate_generic as ui_template -import utils - -# based on https://github.com/art1415926535/PyQt5-syntax-highlighting - -QRegularExpression = QtCore.QRegularExpression - -QFont = QtGui.QFont -QColor = QtGui.QColor -QTextCharFormat = QtGui.QTextCharFormat -QSyntaxHighlighter = QtGui.QSyntaxHighlighter - - -def charFormat(color, style='', background=None): - """ - Return a QTextCharFormat with the given attributes. - """ - _color = QColor() - if type(color) is not str: - _color.setRgb(color[0], color[1], color[2]) - else: - _color.setNamedColor(color) - - _format = QTextCharFormat() - _format.setForeground(_color) - if 'bold' in style: - _format.setFontWeight(QFont.Weight.Bold) - if 'italic' in style: - _format.setFontItalic(True) - if background is not None: - _format.setBackground(pg.mkColor(background)) - - return _format - - -def unnestedDict(exDict): - """Converts a dict-of-dicts to a singly nested dict for non-recursive parsing""" - out = {} - for kk, vv in exDict.items(): - if isinstance(vv, dict): - out.update(unnestedDict(vv)) - else: - out[kk] = vv - return out - - - -class ExampleLoader(QtWidgets.QMainWindow): - # update qtLibCombo item order to match bindings in the UI file and recreate - # the templates files if you change bindings. - bindings = {'PyQt6': 0, 'PySide6': 1, 'PyQt5': 2, 'PySide2': 3} - modules = tuple(m.name for m in pkgutil.iter_modules()) - def __init__(self): - QtWidgets.QMainWindow.__init__(self) - self.ui = ui_template.Ui_Form() - self.cw = QtWidgets.QWidget() - self.setCentralWidget(self.cw) - self.ui.setupUi(self.cw) - self.setWindowTitle("PyQtGraph Examples") - self.codeBtn = QtWidgets.QPushButton('Run Edited Code') - self.codeLayout = QtWidgets.QGridLayout() - self.ui.readoutViewer.setLayout(self.codeLayout) - app = QtWidgets.QApplication.instance() - app.paletteChanged.connect(self.updateTheme) - policy = QtWidgets.QSizePolicy.Policy.Expanding - self.codeLayout.addItem(QtWidgets.QSpacerItem(100,100, policy, policy), 0, 0) - self.codeLayout.addWidget(self.codeBtn, 1, 1) - self.codeBtn.hide() - - self.curListener = None - self.ui.qtLibCombo.addItems(self.bindings.keys()) - self.ui.qtLibCombo.setCurrentIndex(self.bindings[QT_LIB]) - - - - self.itemCache = [] - self.populateTree(self.ui.qubitsList.invisibleRootItem(), utils.examples_) - self.ui.qubitsList.expandAll() - - self.resize(1000,500) - self.show() - self.ui.splitter.setSizes([250,750]) - - self.oldText = self.ui.readoutViewer.toPlainText() - self.ui.loadBtn.clicked.connect(self.loadFile) - self.ui.qubitsList.currentItemChanged.connect(self.showFile) - self.ui.qubitsList.itemDoubleClicked.connect(self.loadFile) - self.ui.readoutViewer.textChanged.connect(self.onTextChange) - self.codeBtn.clicked.connect(self.runEditedCode) - self.updateCodeViewTabWidth(self.ui.readoutViewer.font()) - - def updateCodeViewTabWidth(self,font): - """ - Change the codeView tabStopDistance to 4 spaces based on the size of the current font - """ - fm = QtGui.QFontMetrics(font) - tabWidth = fm.horizontalAdvance(' ' * 4) - # the default value is 80 pixels! that's more than 2x what we want. - self.ui.readoutViewer.setTabStopDistance(tabWidth) - - def showEvent(self, event) -> None: - super(ExampleLoader, self).showEvent(event) - disabledColor = QColor(QtCore.Qt.GlobalColor.red) - for name, idx in self.bindings.items(): - disableBinding = name not in self.modules - if disableBinding: - item = self.ui.qtLibCombo.model().item(idx) - item.setData(disabledColor, QtCore.Qt.ItemDataRole.ForegroundRole) - item.setEnabled(False) - item.setToolTip(f'{item.text()} is not installed') - - def onTextChange(self): - """ - textChanged fires when the highlighter is reassigned the same document. - Prevent this from showing "run edited code" by checking for actual - content change - """ - newText = self.ui.readoutViewer.toPlainText() - if newText != self.oldText: - self.oldText = newText - self.codeEdited() - - def filterByTitle(self, text): - self.showExamplesByTitle(self.getMatchingTitles(text)) - self.hl.setDocument(self.ui.readoutViewer.document()) - - def filterByContent(self, text=None): - # If the new text isn't valid regex, fail early and highlight the search filter red to indicate a problem - # to the user - validRegex = True - try: - re.compile(text) - self.ui.exampleFilter.setStyleSheet('') - except re.error: - colors = DarkThemeColors if app.property('darkMode') else LightThemeColors - errorColor = pg.mkColor(colors.Red) - validRegex = False - errorColor.setAlpha(100) - # Tuple prints nicely :) - self.ui.exampleFilter.setStyleSheet(f'background: rgba{errorColor.getRgb()}') - if not validRegex: - return - checkDict = unnestedDict(utils.examples_) - self.hl.searchText = text - # Need to reapply to current document - self.hl.setDocument(self.ui.readoutViewer.document()) - titles = [] - text = text.lower() - for kk, vv in checkDict.items(): - if isinstance(vv, Namespace): - vv = vv.filename - filename = os.path.join(path, vv) - contents = self.getExampleContent(filename).lower() - if text in contents: - titles.append(kk) - self.showExamplesByTitle(titles) - - def getMatchingTitles(self, text, exDict=None, acceptAll=False): - if exDict is None: - exDict = utils.examples_ - text = text.lower() - titles = [] - for kk, vv in exDict.items(): - matched = acceptAll or text in kk.lower() - if isinstance(vv, dict): - titles.extend(self.getMatchingTitles(text, vv, acceptAll=matched)) - elif matched: - titles.append(kk) - return titles - - def showExamplesByTitle(self, titles): - QTWI = QtWidgets.QTreeWidgetItemIterator - flag = QTWI.IteratorFlag.NoChildren - treeIter = QTWI(self.ui.qubitsList, flag) - item = treeIter.value() - while item is not None: - parent = item.parent() - show = (item.childCount() or item.text(0) in titles) - item.setHidden(not show) - - # If all children of a parent are gone, hide it - if parent: - hideParent = True - for ii in range(parent.childCount()): - if not parent.child(ii).isHidden(): - hideParent = False - break - parent.setHidden(hideParent) - - treeIter += 1 - item = treeIter.value() - - def simulate_black_mode(self): - """ - used to simulate MacOS "black mode" on other platforms - intended for debug only, as it manage only the QPlainTextEdit - """ - # first, a dark background - c = QtGui.QColor('#171717') - p = self.ui.readoutViewer.palette() - p.setColor(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Base, c) - p.setColor(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Base, c) - self.ui.readoutViewer.setPalette(p) - # then, a light font - f = QtGui.QTextCharFormat() - f.setForeground(QtGui.QColor('white')) - self.ui.readoutViewer.setCurrentCharFormat(f) - # finally, override application automatic detection - app = QtWidgets.QApplication.instance() - app.setProperty('darkMode', True) - - def updateTheme(self): - self.hl = PythonHighlighter(self.ui.readoutViewer.document()) - - def populateTree(self, root, examples): - bold_font = None - for key, val in examples.items(): - item = QtWidgets.QTreeWidgetItem([key]) - self.itemCache.append(item) # PyQt 4.9.6 no longer keeps references to these wrappers, - # so we need to make an explicit reference or else the .file - # attribute will disappear. - if isinstance(val, OrderedDict): - self.populateTree(item, val) - elif isinstance(val, Namespace): - item.file = val.filename - if 'recommended' in val: - if bold_font is None: - bold_font = item.font(0) - bold_font.setBold(True) - item.setFont(0, bold_font) - else: - item.file = val - root.addChild(item) - - def currentFile(self): - item = self.ui.qubitsList.currentItem() - if hasattr(item, 'file'): - return os.path.join(path, item.file) - return None - - def loadFile(self, edited=False): - qtLib = self.ui.qtLibCombo.currentText() - env = dict(os.environ, PYQTGRAPH_QT_LIB=qtLib) - example_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.dirname(os.path.dirname(example_path)) - env['PYTHONPATH'] = f'{path}' - if edited: - proc = subprocess.Popen([sys.executable, '-'], stdin=subprocess.PIPE, cwd=example_path, env=env) - code = self.ui.readoutViewer.toPlainText().encode('UTF-8') - proc.stdin.write(code) - proc.stdin.close() - else: - fn = self.currentFile() - if fn is None: - return - subprocess.Popen([sys.executable, fn], cwd=path, env=env) - - def showFile(self): - fn = self.currentFile() - text = self.getExampleContent(fn) - self.ui.readoutViewer.setPlainText(text) - self.ui.loadedFileLabel.setText(fn) - self.codeBtn.hide() - - @lru_cache(100) - def getExampleContent(self, filename): - if filename is None: - self.ui.readoutViewer.clear() - return - if os.path.isdir(filename): - filename = os.path.join(filename, '__main__.py') - with open(filename, "r") as currentFile: - text = currentFile.read() - return text - - def codeEdited(self): - self.codeBtn.show() - - def runEditedCode(self): - self.loadFile(edited=True) - - def keyPressEvent(self, event): - super().keyPressEvent(event) - if not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier): - return - key = event.key() - Key = QtCore.Qt.Key - - # Allow quick navigate to search - if key == Key.Key_F: - self.ui.exampleFilter.setFocus() - event.accept() - return - - if key not in [Key.Key_Plus, Key.Key_Minus, Key.Key_Underscore, Key.Key_Equal, Key.Key_0]: - return - font = self.ui.readoutViewer.font() - oldSize = font.pointSize() - if key == Key.Key_Plus or key == Key.Key_Equal: - font.setPointSize(oldSize + max(oldSize*.15, 1)) - elif key == Key.Key_Minus or key == Key.Key_Underscore: - newSize = oldSize - max(oldSize*.15, 1) - font.setPointSize(max(newSize, 1)) - elif key == Key.Key_0: - # Reset to original size - font.setPointSize(10) - self.ui.readoutViewer.setFont(font) - self.updateCodeViewTabWidth(font) - event.accept() - -def main(): - app = pg.mkQApp() - loader = ExampleLoader() - loader.ui.qubitsList.setCurrentIndex( - loader.ui.qubitsList.model().index(0, 0) - ) - pg.exec() - -if __name__ == '__main__': - main() \ No newline at end of file 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 From 6d1c168de835d950a9cb3b9473abec04a4513d3c Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Tue, 11 Oct 2022 16:40:32 +0200 Subject: [PATCH 07/25] discriminator gui upgrades --- discriminator_gui/discriminator_gui.py | 128 ++++++++++++++++++------- 1 file changed, 94 insertions(+), 34 deletions(-) diff --git a/discriminator_gui/discriminator_gui.py b/discriminator_gui/discriminator_gui.py index 92f3dde0..3416905b 100644 --- a/discriminator_gui/discriminator_gui.py +++ b/discriminator_gui/discriminator_gui.py @@ -1,23 +1,28 @@ import sys from PyQt5.QtCore import * +from PyQt5 import QtCore from PyQt5.QtGui import * from PyQt5.QtWidgets import * import pyqtgraph as pg import numpy as np +# TODO: create brushes to use throughout for ground/excited states +# TODO: sort out the axes so plot 4 has the axes around the image rather than the plot area +# TODO: add tab with additional info from Niv + class DiscriminatorGui(QWidget): def __init__(self, results_dataclasses): self.num_qubits = len(results_dataclasses) self.results_dataclasses = results_dataclasses super(DiscriminatorGui, self).__init__() - self.initUI() + self.initialise_ui() self._populate_list() - def initUI(self): + def initialise_ui(self): - hbox = QHBoxLayout(self) + box_layout = QHBoxLayout(self) # create some widgets @@ -44,14 +49,14 @@ def initUI(self): splitter.addWidget(self.right) - hbox.addWidget(splitter) + box_layout.addWidget(splitter) - self.setLayout(hbox) + self.setLayout(box_layout) QApplication.setStyle(QStyleFactory.create('Cleanlooks')) - self.setGeometry(100, 100, 1200, 500) - self.setWindowTitle('Qubit reset comparison') + self.setGeometry(100, 100, 1100, 700) + self.setWindowTitle('Readout viewer') # self.qubit_list.itemDoubleClicked.connect(self.func) self.qubit_list.currentIndexChanged.connect(self.func) @@ -59,14 +64,6 @@ def initUI(self): self.show() - def generate_fake_histograms(self, a, b): - ## make interesting distribution of values - vals1 = np.random.normal(size=500) - vals2 = np.random.normal(size=260, loc=4) - ## compute standard histogram - y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) - y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) - return a * y, x, b * y2, x2 def clear_plots(self): self.plt1.clear() @@ -74,42 +71,105 @@ def clear_plots(self): self.plt3.clear() self.plt4.clear() - def func(self): + def _generate_plot_1(self, result): - self.clear_plots() - - index = self.qubit_list.currentIndex() - result = self.results_dataclasses[index] - - angle, threshold, fidelity, gg, ge, eg, ee = result.get_params() ig, qg, ie, qe = result.get_data() - ig_rotated, qg_rotated, ie_rotated, qe_rotated = result.get_rotated_data() - - ## Using stepMode="center" causes the plot to draw two lines for each sample. - ## notice that len(x) == len(y)+1 - original_data_g = pg.ScatterPlotItem(ig, qg, brush=(50, 50, 50, 199)) - original_data_e = pg.ScatterPlotItem(ie, qe, brush=(0, 0, 100, 100)) + original_data_g = pg.ScatterPlotItem(ig, qg, brush=(100, 149, 237, 100), fillOutline=False) + original_data_e = pg.ScatterPlotItem(ie, qe, brush=(255, 185, 15, 100)) self.plt1.addItem(original_data_g) self.plt1.addItem(original_data_e) + self.plt1.setAspectLocked() + def _generate_plot_2(self, result): - rotated_data_g = pg.ScatterPlotItem(ig_rotated, qg_rotated, brush=(50, 50, 50, 199)) - rotated_data_e = pg.ScatterPlotItem(ie_rotated, qe_rotated, brush=(0, 0, 100, 100)) + ig_rotated, qg_rotated, ie_rotated, qe_rotated = result.get_rotated_data() + rotated_data_g = pg.ScatterPlotItem(ig_rotated, qg_rotated, brush=(100, 149, 237, 100)) + rotated_data_e = pg.ScatterPlotItem(ie_rotated, qe_rotated, brush=(255, 185, 15, 100)) self.plt2.addItem(rotated_data_g) self.plt2.addItem(rotated_data_e) + self.plt2.setAspectLocked() + + def _generate_plot_3(self, result): + + 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) + - ig_hist_y, ig_hist_x = np.histogram(ig_rotated, bins=50) - ie_hist_y, ie_hist_x = np.histogram(ie_rotated, bins=50) self.plt3.plot(ig_hist_x, ig_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(0, 0, 0, 255)) self.plt3.plot(ie_hist_x, ie_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(255, 255, 255, 150)) + self.threshold_line = self.plt3.addLine(x=result.threshold, + label=f'{result.threshold:.2f}', + labelOpts={'position': 0.95}, + pen={'color': 'white', 'dash': [20, 20]}) - img = pg.ImageItem(image=result.confusion_matrix()) + + def _generate_plot_4(self, result): + + img = pg.ImageItem(image=result.confusion_matrix(), rect=[1, 1, 1, 1]) + img.setColorMap('viridis') self.plt4.addItem(img) - self.plt4.showAxes(False) + 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 func(self): + + self.clear_plots() + + index = self.qubit_list.currentIndex() + result = self.results_dataclasses[index] + self._generate_plot_1(result) + self._generate_plot_2(result) + self._generate_plot_3(result) + self._generate_plot_4(result) def _populate_list(self): From a64a9a0e418c7c2aa51fcc68f77208d71b7ee605 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 12 Oct 2022 13:09:22 +0200 Subject: [PATCH 08/25] dashboard started --- discriminator_gui/discriminator_gui.py | 156 +++++++++++++++++++-- qualang_tools/addons/InteractivePlotLib.py | 72 +++++----- 2 files changed, 180 insertions(+), 48 deletions(-) diff --git a/discriminator_gui/discriminator_gui.py b/discriminator_gui/discriminator_gui.py index 3416905b..35db73dd 100644 --- a/discriminator_gui/discriminator_gui.py +++ b/discriminator_gui/discriminator_gui.py @@ -17,13 +17,51 @@ def __init__(self, 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): + + self.dashboard_tab_layout = QGridLayout() + + self.dashboard_tab.setLayout(self.dashboard_tab_layout) + + # widets + + self.dashboard_list = QListWidget() + fidelity_average = QLabel(f'Average fidelity is {98}%') + average_overlap = QLabel(f'Average overlap is {0.1}') + + self.dashboard_tab_layout.addWidget(self.dashboard_list, 0, 0, 2, 1) + self.dashboard_tab_layout.addWidget(fidelity_average, 0, 1, 1, 5) + self.dashboard_tab_layout.addWidget(average_overlap, 0, 6, 1, 5) + self.dashboard_list.itemDoubleClicked.connect(self.switch_to_qubit_tab) + + self.dashboard_list.setMaximumWidth(200) def initialise_ui(self): + 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() @@ -31,6 +69,25 @@ def initialise_ui(self): 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') @@ -40,7 +97,9 @@ def initialise_ui(self): self.plt4 = self.graphics_window.addPlot(title='Fidelities') self.left.addWidget(self.qubit_list, 0, 0) - self.left.addWidget(QFrame(), 2, 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) @@ -50,19 +109,28 @@ def initialise_ui(self): box_layout.addWidget(splitter) + main_layout.addWidget(self.tabs, 0, 0) + + self.setLayout(main_layout) - self.setLayout(box_layout) + # self.layout().addWidget(self.tabs) QApplication.setStyle(QStyleFactory.create('Cleanlooks')) self.setGeometry(100, 100, 1100, 700) self.setWindowTitle('Readout viewer') - # self.qubit_list.itemDoubleClicked.connect(self.func) - self.qubit_list.currentIndexChanged.connect(self.func) + self.qubit_list.currentIndexChanged.connect(self.update_plots) - self.show() + def switch_to_qubit_tab(self): + + unsorted_qubit_id = self.dashboard_list.currentRow() + qubit_id = self.sorted_qubit_ids[unsorted_qubit_id] + + self.qubit_list.setCurrentIndex(qubit_id) + self.update_plots() + self.tabs.setCurrentIndex(1) def clear_plots(self): @@ -75,8 +143,25 @@ def _generate_plot_1(self, result): ig, qg, ie, qe = result.get_data() - original_data_g = pg.ScatterPlotItem(ig, qg, brush=(100, 149, 237, 100), fillOutline=False) - original_data_e = pg.ScatterPlotItem(ie, qe, brush=(255, 185, 15, 100)) + 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() @@ -84,8 +169,25 @@ def _generate_plot_1(self, result): def _generate_plot_2(self, result): ig_rotated, qg_rotated, ie_rotated, qe_rotated = result.get_rotated_data() - rotated_data_g = pg.ScatterPlotItem(ig_rotated, qg_rotated, brush=(100, 149, 237, 100)) - rotated_data_e = pg.ScatterPlotItem(ie_rotated, qe_rotated, brush=(255, 185, 15, 100)) + + 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() @@ -97,8 +199,24 @@ def _generate_plot_3(self, result): - self.plt3.plot(ig_hist_x, ig_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(0, 0, 0, 255)) - self.plt3.plot(ie_hist_x, ie_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(255, 255, 255, 150)) + self.plt3.plot( + ig_hist_x, ig_hist_y, + stepMode="center", + fillLevel=0, + fillOutline=False, + brush=(*self.ground_state_colour, 255), + pen=pg.mkPen(None) + ) + + self.plt3.plot( + ie_hist_x, ie_hist_y, + stepMode="center", + fillLevel=0, + fillOutline=False, + brush=(*self.excited_state_colour, 150), + pen=pg.mkPen(None) + ) + self.threshold_line = self.plt3.addLine(x=result.threshold, label=f'{result.threshold:.2f}', labelOpts={'position': 0.95}, @@ -159,7 +277,7 @@ def _generate_plot_4(self, result): self.plt4.addItem(ee_fid_label) - def func(self): + def update_plots(self): self.clear_plots() @@ -178,6 +296,20 @@ def _populate_list(self): f'Qubit {i + 1}' ) + def _list_by_fidelity(self): + + unsorted_qubit_fidelities = [result.fidelity for result in self.results_dataclasses] + qubit_names = [f'Qubit {i}' for i in range(1, self.num_qubits + 1)] + qubit_ids = range(0, self.num_qubits) + # out = [(fid, x) for fid, x in sorted(zip(unsorted_qubit_list, qubit_names), key=lambda pair: pair[0])] + # print(out) + + self.sorted_qubit_ids = [id for fid, id in sorted(zip(unsorted_qubit_fidelities, qubit_ids), key=lambda pair: pair[0])][::-1] + + for fidelity, qubit_name in sorted(zip(unsorted_qubit_fidelities, qubit_names), key=lambda pair: pair[0])[::-1]: + self.dashboard_list.addItem(f"{qubit_name:<9} ({fidelity:.2f}%)") + + 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() From c72c1883e6d31a3b611ae767c11a707e0d4b472f Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 12 Oct 2022 16:00:06 +0200 Subject: [PATCH 09/25] dashboard --- discriminator_gui/discriminator_gui.py | 61 +++++++++++++++++++++----- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/discriminator_gui/discriminator_gui.py b/discriminator_gui/discriminator_gui.py index 35db73dd..5c6633e2 100644 --- a/discriminator_gui/discriminator_gui.py +++ b/discriminator_gui/discriminator_gui.py @@ -6,6 +6,7 @@ import pyqtgraph as pg import numpy as np + # TODO: create brushes to use throughout for ground/excited states # TODO: sort out the axes so plot 4 has the axes around the image rather than the plot area # TODO: add tab with additional info from Niv @@ -23,21 +24,60 @@ def __init__(self, results_dataclasses): self.show() + def setup_dashboard_tab(self): + + self.dashboard_widget_colour = (130, 170, 170) self.dashboard_tab_layout = QGridLayout() self.dashboard_tab.setLayout(self.dashboard_tab_layout) # widets - self.dashboard_list = QListWidget() - fidelity_average = QLabel(f'Average fidelity is {98}%') + # 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}') - self.dashboard_tab_layout.addWidget(self.dashboard_list, 0, 0, 2, 1) - self.dashboard_tab_layout.addWidget(fidelity_average, 0, 1, 1, 5) - self.dashboard_tab_layout.addWidget(average_overlap, 0, 6, 1, 5) + fidelity_average.setStyleSheet(f"background-color:rgb{self.dashboard_widget_colour}; border-radius:5px") + average_overlap.setStyleSheet(f"background-color:rgb{self.dashboard_widget_colour}; border-radius:5px") + + 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}; border-radius:5px") + error_correlations.setStyleSheet(f"background-color:rgb{self.dashboard_widget_colour}; border-radius:5px") + + 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) @@ -204,7 +244,7 @@ def _generate_plot_3(self, result): stepMode="center", fillLevel=0, fillOutline=False, - brush=(*self.ground_state_colour, 255), + brush=(*self.ground_state_colour, 200), pen=pg.mkPen(None) ) @@ -213,7 +253,7 @@ def _generate_plot_3(self, result): stepMode="center", fillLevel=0, fillOutline=False, - brush=(*self.excited_state_colour, 150), + brush=(*self.excited_state_colour, 200), pen=pg.mkPen(None) ) @@ -306,10 +346,11 @@ def _list_by_fidelity(self): self.sorted_qubit_ids = [id for fid, id in sorted(zip(unsorted_qubit_fidelities, qubit_ids), key=lambda pair: pair[0])][::-1] - for fidelity, qubit_name in sorted(zip(unsorted_qubit_fidelities, qubit_names), key=lambda pair: pair[0])[::-1]: - self.dashboard_list.addItem(f"{qubit_name:<9} ({fidelity:.2f}%)") - + for i, (fidelity, qubit_name) in enumerate(sorted(zip(unsorted_qubit_fidelities, qubit_names), key=lambda pair: pair[0])[::-1]): + # self.dashboard_list.addItem(f"{qubit_name:<9} ({fidelity:.2f}%)") + self.dashboard_list.setItem(i, 0, QTableWidgetItem(qubit_name)) + self.dashboard_list.setItem(i, 1, QTableWidgetItem(f'{fidelity:.2f}%')) From aaa12cf1a1db2db339e7f3e6b4c86aa61e158cb2 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 12 Oct 2022 16:52:56 +0200 Subject: [PATCH 10/25] active reset --- joe_testing_active_reset/active_reset_gui.py | 149 ++++++++++++++++--- 1 file changed, 125 insertions(+), 24 deletions(-) diff --git a/joe_testing_active_reset/active_reset_gui.py b/joe_testing_active_reset/active_reset_gui.py index bb3bb11a..b8a6054f 100644 --- a/joe_testing_active_reset/active_reset_gui.py +++ b/joe_testing_active_reset/active_reset_gui.py @@ -10,11 +10,11 @@ def __init__(self, num_qubits): self.num_qubits = num_qubits super(ActiveResetGUI, self).__init__() - self.initUI() + self.initialise_ui() self._populate_list() - def initUI(self): + def initialise_ui(self): hbox = QHBoxLayout(self) @@ -27,16 +27,18 @@ def initUI(self): self.info_box = pg.TableWidget(3, 3) - self.info_box.setData([ - ['', 'Passive', 'Active'], - ['Fidelity', '98%', '85%'], - ['Time', '100 ns', '90 ns'] - ]) + + self.info_box.setData(self.generate_table(98, 86, 100e-9, 90e-9)) self.graphics_window = pg.GraphicsLayoutWidget() - self.plt1 = self.graphics_window.addPlot(title='Passive reset') - self.plt2 = self.graphics_window.addPlot(title='Active reset') + self.plt1 = self.graphics_window.addPlot(title='Passive reset IQ') + self.plt2 = self.graphics_window.addPlot(title='Active reset IQ') + + self.graphics_window.nextRow() + self.plt3 = self.graphics_window.addPlot(title='1D histogram') + self.plt4 = self.graphics_window.addPlot(title='1D histogram') + self.left.addWidget(self.qubit_list, 0, 0) self.left.addWidget(self.info_box, 1, 0) @@ -45,22 +47,24 @@ def initUI(self): self.info_box.horizontalHeader().setVisible(False) self.info_box.verticalHeader().setVisible(False) + self.right.addWidget(self.graphics_window) - splitter = QSplitter(Qt.Horizontal) - splitter.addWidget(self.left) - splitter.addWidget(self.right) # 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() - first_col_width = table_size.width() + 3 - splitter.setSizes([first_col_width + 3, 950]) self.info_box.setMaximumHeight(int(table_size.height() * (3/4))) + self.info_box.setMaximumWidth(int(table_size.width() * (3/4))) self.info_box.setShowGrid(False) + self.ground_state_colour = (100, 149, 237) + self.excited_state_colour = (255, 185, 15) - hbox.addWidget(splitter) + + + hbox.addWidget(self.left) + hbox.addWidget(self.right) self.setLayout(hbox) @@ -70,7 +74,7 @@ def initUI(self): self.setWindowTitle('Qubit reset comparison') # self.qubit_list.itemDoubleClicked.connect(self.func) - self.qubit_list.currentIndexChanged.connect(self.func) + self.qubit_list.currentIndexChanged.connect(self.update_plots) self.show() @@ -104,21 +108,118 @@ def set_table(self, pf, pt, af, at): data = self.generate_table(pf, pt, af, at) self.info_box.setData(data) - def func(self): - - self.plt1.clear() - self.plt2.clear() + def _generate_plot_1(self): + rotated_data_g = pg.ScatterPlotItem( + np.random.normal(1, 0.2, 5000), + np.random.normal(1, 0.2, 5000), + brush=(*self.ground_state_colour, 100), + symbol='s', + size='2', + pen=pg.mkPen(None) + ) + + rotated_data_e = pg.ScatterPlotItem( + np.random.normal(1.5, 0.2, 5000), + np.random.normal(1, 0.2, 5000), + brush=(*self.excited_state_colour, 100), + symbol='s', + size='2', + pen=pg.mkPen(None) + ) + + self.plt1.addItem(rotated_data_g) + self.plt1.addItem(rotated_data_e) + + plt1_threshold = 1.25 + + self.plt1.addLine( + x=plt1_threshold, + label=f'{plt1_threshold:.2f}, θ={(np.random.rand() * 1000) % 90:.2f}°', + labelOpts={'position': 0.9}, + pen={'color': 'white', 'dash': [20, 20]} + ) + + self.plt1.setAspectLocked() + + def _generate_plot_2(self): + + + rotated_data_g = pg.ScatterPlotItem( + np.random.normal(1, 0.2, 5000), + np.random.normal(1, 0.2, 5000), + brush=(*self.ground_state_colour, 100), + symbol='s', + size='2', + pen=pg.mkPen(None) + ) + + rotated_data_e = pg.ScatterPlotItem( + np.random.normal(1.5, 0.2, 5000), + np.random.normal(1, 0.2, 5000), + 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) + + plt2_threshold = 1.25 + + self.plt2.addLine( + x=plt2_threshold, + label=f'{plt2_threshold:.2f}, θ={(np.random.rand() * 1000) % 90:.2f}°', + labelOpts={'position': 0.9}, + pen={'color': 'white', 'dash': [20, 20]} + ) + + self.plt2.setAspectLocked() + + def _generate_plot_3(self): y, x, y2, x2 = self.generate_fake_histograms(1, 1) ## Using stepMode="center" causes the plot to draw two lines for each sample. ## notice that len(x) == len(y)+1 - self.plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) - self.plt1.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + self.plt3.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.ground_state_colour, 200), pen=pg.mkPen(None)) + self.plt3.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.excited_state_colour, 200), pen=pg.mkPen(None)) + + plt3_threshold = np.mean([np.median(x), np.median(x2)]) + (0.2 * np.random.rand()) + + self.plt3.addLine( + x=plt3_threshold, + label=f'{plt3_threshold:.2f}', + labelOpts={'position': 0.95}, + pen={'color': 'white', 'dash': [20, 20]} + ) + + def _generate_plot_4(self): y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) - self.plt2.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) - self.plt2.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) + self.plt4.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.ground_state_colour, 200), pen=pg.mkPen(None)) + self.plt4.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.excited_state_colour, 200), pen=pg.mkPen(None)) + + plt4_threshold = np.mean([np.median(x), np.median(x2)]) + (0.2 * np.random.rand()) + + self.plt4.addLine( + x=plt4_threshold, + label=f'{plt4_threshold:.2f}', + labelOpts={'position': 0.95}, + pen={'color': 'white', 'dash': [20, 20]} + ) + + def update_plots(self): + + self.plt1.clear() + self.plt2.clear() + self.plt3.clear() + self.plt4.clear() + + self._generate_plot_1() + self._generate_plot_2() + self._generate_plot_3() + self._generate_plot_4() self.set_table(*self.generate_random_table_data()) From 891574ad22e3dd8f6158456efc79c3a8d64b775c Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Thu, 13 Oct 2022 11:58:58 +0200 Subject: [PATCH 11/25] cleaned up, comments, removed dead code --- discriminator_gui/discriminator_gui.py | 97 +++++--- .../active_reset_data_generator.py | 86 ------- .../active_reset_gui_old.py | 218 ------------------ joe_testing_active_reset/sample_generator.py | 64 ----- joe_testing_active_reset/t1_basic.py | 110 --------- .../t1_cycle_simulator.py | 27 --- qualang_tools/analysis/__init__.py | 1 + qualang_tools/analysis/active_reset.py | 0 qualang_tools/analysis/dataPresenter.py | 150 ------------ .../analysis/exampleLoaderTemplate_generic.py | 56 ----- .../independent_multi_qubit_discriminator.py | 114 +-------- qualang_tools/analysis/results_dataclass.py | 85 +++++++ qualang_tools/analysis/viewer.py | 126 ---------- 13 files changed, 157 insertions(+), 977 deletions(-) delete mode 100644 joe_testing_active_reset/active_reset_data_generator.py delete mode 100644 joe_testing_active_reset/active_reset_gui_old.py delete mode 100644 joe_testing_active_reset/sample_generator.py delete mode 100644 joe_testing_active_reset/t1_basic.py delete mode 100644 joe_testing_active_reset/t1_cycle_simulator.py delete mode 100644 qualang_tools/analysis/active_reset.py delete mode 100644 qualang_tools/analysis/dataPresenter.py delete mode 100644 qualang_tools/analysis/exampleLoaderTemplate_generic.py create mode 100644 qualang_tools/analysis/results_dataclass.py delete mode 100644 qualang_tools/analysis/viewer.py diff --git a/discriminator_gui/discriminator_gui.py b/discriminator_gui/discriminator_gui.py index 5c6633e2..3f53eefc 100644 --- a/discriminator_gui/discriminator_gui.py +++ b/discriminator_gui/discriminator_gui.py @@ -1,18 +1,24 @@ -import sys +""" +A GUI for presenting state discrimination data for a multi-qubit setup. +""" + from PyQt5.QtCore import * -from PyQt5 import QtCore -from PyQt5.QtGui import * from PyQt5.QtWidgets import * import pyqtgraph as pg import numpy as np -# TODO: create brushes to use throughout for ground/excited states # TODO: sort out the axes so plot 4 has the axes around the image rather than the plot area -# TODO: add tab with additional info from Niv 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 @@ -26,9 +32,13 @@ def __init__(self, results_dataclasses): def setup_dashboard_tab(self): + """ + Sets up the dashboard tab with overview information about the qubit register. + @return: + """ - - self.dashboard_widget_colour = (130, 170, 170) + # 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) @@ -54,17 +64,18 @@ def setup_dashboard_tab(self): 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}; border-radius:5px") - average_overlap.setStyleSheet(f"background-color:rgb{self.dashboard_widget_colour}; border-radius:5px") + 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}; border-radius:5px") - error_correlations.setStyleSheet(f"background-color:rgb{self.dashboard_widget_colour}; border-radius:5px") + 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) @@ -79,11 +90,14 @@ def setup_dashboard_tab(self): 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() @@ -164,9 +178,13 @@ def initialise_ui(self): 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() - qubit_id = self.sorted_qubit_ids[unsorted_qubit_id] + # 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() @@ -174,12 +192,20 @@ def switch_to_qubit_tab(self): 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_plot_1(self, result): + 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() @@ -206,7 +232,12 @@ def _generate_plot_1(self, result): self.plt1.addItem(original_data_e) self.plt1.setAspectLocked() - def _generate_plot_2(self, result): + 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() @@ -232,7 +263,11 @@ def _generate_plot_2(self, result): self.plt2.addItem(rotated_data_e) self.plt2.setAspectLocked() - def _generate_plot_3(self, result): + 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) @@ -263,7 +298,11 @@ def _generate_plot_3(self, result): pen={'color': 'white', 'dash': [20, 20]}) - def _generate_plot_4(self, result): + 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') @@ -318,18 +357,25 @@ def _generate_plot_4(self, result): 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_plot_1(result) - self._generate_plot_2(result) - self._generate_plot_3(result) - self._generate_plot_4(result) + 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( @@ -339,15 +385,12 @@ def _populate_list(self): def _list_by_fidelity(self): unsorted_qubit_fidelities = [result.fidelity for result in self.results_dataclasses] - qubit_names = [f'Qubit {i}' for i in range(1, self.num_qubits + 1)] qubit_ids = range(0, self.num_qubits) - # out = [(fid, x) for fid, x in sorted(zip(unsorted_qubit_list, qubit_names), key=lambda pair: pair[0])] - # print(out) - self.sorted_qubit_ids = [id for fid, id in sorted(zip(unsorted_qubit_fidelities, qubit_ids), key=lambda pair: pair[0])][::-1] + 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, (fidelity, qubit_name) in enumerate(sorted(zip(unsorted_qubit_fidelities, qubit_names), key=lambda pair: pair[0])[::-1]): - # self.dashboard_list.addItem(f"{qubit_name:<9} ({fidelity:.2f}%)") + 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}%')) diff --git a/joe_testing_active_reset/active_reset_data_generator.py b/joe_testing_active_reset/active_reset_data_generator.py deleted file mode 100644 index 4ea1840a..00000000 --- a/joe_testing_active_reset/active_reset_data_generator.py +++ /dev/null @@ -1,86 +0,0 @@ -#%% -from platform import mac_ver -from quam import QuAM -from qm.qua import * -from qm.QuantumMachinesManager import QuantumMachinesManager -from qm.simulate import SimulationConfig -from qualang_tools.loops import from_array -from qualang_tools.results import fetching_tool, progress_counter -from qualang_tools.analysis.discriminator import two_state_discriminator - -import matplotlib.pyplot as plt - -import numpy as np -# from IPython.display import clear_output - - -# %% -n_shots = int(2000) -num_qb = 6 - -n_total = num_qb * n_shots -cooldown_time = int(0.6e-3 / 4e-9) - -def construct_active_reset_program(): - - with program() as active_reset_program: - raw_st = declare_stream(adc_trace=True) - n = declare(int) - n_cnt = declare(int, value=0) - # threshold = declare(fixed) - - I = [declare(fixed) for idx in range(num_qb)] - I_herald = [declare(fixed) for idx in range(num_qb)] - Q = [declare(fixed) for idx in range(num_qb)] - I_st = [declare_stream() for idx in range(num_qb)] - Q_st = [declare_stream() for idx in range(num_qb)] - N_st = declare_stream() - - threshold = [] - for idx in range(num_qb): - threshold.append(machine.readout_resonators[idx].threshold) - - with for_(n, 0, n < n_shots, n + 1): - for idx in range(num_qb): - # update_frequency(f"qb{idx}", 0) - # reset_phase(f"qb{idx}") - - play("x90", f"qb{idx}") - # play("x180", f"qb{idx}") - - wait(int(200e-9 // 4e-9), f"qb{idx}") - - align(f"qb{idx}", f"rr{idx}") - - measure("readout", f"rr{idx}", None, - dual_demod.full("rotated_cos", "out1", "rotated_minus_sin", "out2", I_herald[idx])) - align(f"qb{idx}", f"rr{idx}") - - wait(int(4e-6 // 4e-9), f"qb{idx}") - - with if_(I_herald[idx] < threshold[idx]): - # pi - play("x180", f"qb{idx}") - - align(f"qb{idx}", f"rr{idx}") - - measure("readout", f"rr{idx}", None, - dual_demod.full("rotated_cos", "out1", "rotated_minus_sin", "out2", I[idx]), - dual_demod.full("rotated_sin", "out1", "rotated_cos", "out2", Q[idx])) - - wait(cooldown_time, f"rr{idx}") - # wait(int(1e-6//4e-9), f"rr{idx}") - - save(I[idx], I_st[idx]) - save(Q[idx], Q_st[idx]) - assign(n_cnt, n_cnt + 1) - save(n_cnt, N_st) - - with stream_processing(): - N_st.save("N") - - for idx in range(num_qb): - I_st[idx].save_all(f'I{idx}') - Q_st[idx].save_all(f'Q{idx}') - - return active_reset_program \ No newline at end of file diff --git a/joe_testing_active_reset/active_reset_gui_old.py b/joe_testing_active_reset/active_reset_gui_old.py deleted file mode 100644 index 98a71551..00000000 --- a/joe_testing_active_reset/active_reset_gui_old.py +++ /dev/null @@ -1,218 +0,0 @@ -# from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTabWidget, QGridLayout, \ -# QVBoxLayout - -from PyQt5.QtWidgets import * - -import numpy as np -import pyqtgraph as pg -from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets - -from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget - - -# just use widget doesn't need an app window -class ActiveReset(QApplication): - - # def __init__(self, num_qubits): - # super().__init__([]) - # self.num_qubits = num_qubits - # - # self.main_window = QWidget() - # self.main_window.setWindowTitle('Active reset viewer') - # - # self.splitter = QSplitter() - # self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) - # self.layout_widget = QtWidgets.QWidget(self.splitter) - # - # - # # create some widgets - # self.qubit_list = QListWidget() - # - # self.graphics_window = pg.GraphicsLayoutWidget() - # self.plt1 = self.graphics_window.addPlot() - # self.plt2 = self.graphics_window.addPlot() - # - # - # # self.graphics_window.ci.setBorder((50, 50, 100)) - # - # # self.passive_reset_layout = self.graphics_window.addLayout() - # # self.passive_reset_layout.addLabel("Passive reset") - # # self.passive_reset_layout.nextRow() - # # self.passive_view_box = self.passive_reset_layout.addViewBox() - # # - # # self.active_reset_layout = self.graphics_window.addLayout() - # # self.active_reset_layout.addLabel("Active reset") - # # self.active_reset_layout.nextRow() - # - # # self.active_view_box = self.active_reset_layout.addViewBox() - # self.text = QWidget() - # self.text_layout = QGridLayout() - # self.text.setLayout(self.text_layout) - # self.set_up_text_area() - # - # self.main_layout = QGridLayout() - # - # self.main_window.setLayout(self.main_layout) - # - # - # self.main_layout.addWidget(self.qubit_list, 0, 0, 2, 1) - # self.main_layout.addWidget(self.text, 2, 0, 2, 1) - # # self.main_layout.addWidget(self.splitter, 0, 2, 1, 1) - # self.main_layout.addWidget(self.graphics_window, 0, 3, 4, 6) - # - # self.populate_list() - # self.qubit_list.itemDoubleClicked.connect(self.func) - # - # - # self.main_window.show() - - def __init__(self, num_qubits): - super().__init__([]) - self.num_qubits = num_qubits - - self.main_window = QWidget() - self.main_window.setWindowTitle('Active reset viewer') - - - self.splitter = QSplitter() - - - - # create some widgets - self.qubit_list = QListWidget() - - self.graphics_window = pg.GraphicsLayoutWidget() - self.plt1 = self.graphics_window.addPlot() - self.plt2 = self.graphics_window.addPlot() - - self.text = QWidget() - self.text_layout = QGridLayout() - self.text.setLayout(self.text_layout) - self.set_up_text_area() - - - self.graphics_window.ci.setBorder((50, 50, 100)) - - self.passive_reset_layout = self.graphics_window.addLayout() - self.passive_reset_layout.addLabel("Passive reset") - self.passive_reset_layout.nextRow() - self.passive_view_box = self.passive_reset_layout.addViewBox() - - self.active_reset_layout = self.graphics_window.addLayout() - self.active_reset_layout.addLabel("Active reset") - self.active_reset_layout.nextRow() - - self.active_view_box = self.active_reset_layout.addViewBox() - - - self.main_layout = QGridLayout() - - self.main_window.setLayout(self.main_layout) - - - self.main_layout.addWidget(self.qubit_list, 0, 0, 2, 1) - self.main_layout.addWidget(self.text, 2, 0, 2, 1) - # self.main_layout.addWidget(self.splitter, 0, 2, 1, 1) - self.main_layout.addWidget(self.graphics_window, 0, 3, 4, 6) - - self.populate_list() - self.qubit_list.itemDoubleClicked.connect(self.func) - - - self.main_window.show() - - def set_up_text_area(self): - - self.info_title = QLabel('Fidelity information for qubit ') - self.info_passive_title = QLabel('Passive') - self.info_active_title = QLabel('Active') - - self.info_passive_fid = QLabel('Fidelity: ') - self.info_passive_time = QLabel('Time: ') - - self.info_active_fid = QLabel('Fidelity: ') - self.info_active_time = QLabel('Time: ') - - self.text_layout.addWidget(self.info_title, 0, 0, 1, 1) - self.text_layout.addWidget(self.info_passive_title, 1, 0) - self.text_layout.addWidget(self.info_active_title, 1, 1) - - self.text_layout.addWidget(self.info_passive_fid, 2, 0) - self.text_layout.addWidget(self.info_passive_time, 3, 0) - self.text_layout.addWidget(self.info_active_fid, 2, 1) - self.text_layout.addWidget(self.info_active_time, 3, 1) - - - def update_text_area(self, qubit_id): - - self.info_title.setText(f'Fidelity information for qubit {qubit_id}') - - self.info_passive_fid.setText(f'Fidelity: {100 * (1 - (0.2 * np.random.rand())):.1f}%') - self.info_passive_time.setText(f'Time: {98.5} ns') - self.info_active_fid.setText(f'Fidelity: {100 * (1 - (0.2 * np.random.rand())):.1f}%') - self.info_active_time.setText(f'Time: {100} ns') - - def func(self): - - self.plt1.clear() - self.plt2.clear() - - y, x, y2, x2 = self.generate_fake_histograms(1, 1) - - ## Using stepMode="center" causes the plot to draw two lines for each sample. - ## notice that len(x) == len(y)+1 - self.plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) - self.plt1.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) - - y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) - self.plt2.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) - self.plt2.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) - - """ - def func(self): - - qubit = self.qubit_list.currentRow() - - # clear the view boxes so we don't plot histograms on top of each other - self.passive_view_box.clear() - self.active_view_box.clear() - y, x, y2, x2 = self.generate_fake_histograms(1, 1) - figure1 = pg.PlotDataItem(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) - figure2 = pg.PlotDataItem(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) - self.passive_view_box.addItem(figure1) - self.passive_view_box.addItem(figure2) - - y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) - figure3 = pg.PlotDataItem(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0, 0, 255, 150)) - figure4 = pg.PlotDataItem(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(255, 69, 0, 150)) - - self.active_view_box.addItem(figure3) - self.active_view_box.addItem(figure4) - - self.update_text_area(qubit + 1) - - """ - - - def generate_fake_histograms(self, a, b): - ## make interesting distribution of values - vals1 = np.random.normal(size=500) - vals2 = np.random.normal(size=260, loc=4) - ## compute standard histogram - y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) - y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) - return a * y, x, b * y2, x2 - - def populate_list(self): - - for i in range(self.num_qubits): - self.qubit_list.addItem( - f'Qubit {i + 1}' - ) - - - - -if __name__ == '__main__': - app = ActiveReset(32) - app.exec_() diff --git a/joe_testing_active_reset/sample_generator.py b/joe_testing_active_reset/sample_generator.py deleted file mode 100644 index bbce34b3..00000000 --- a/joe_testing_active_reset/sample_generator.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Created on 04/08/2022 -@author jdh -""" - -import numpy as np - - -# runs at around 0.1 ns per sample (2000 samples in ~200 us) -# obviously big overhead if doing small number of samples. -def _generate_spin_samples(v_rf_s, v_rf_t, sigma, T1, number_of_samples, integration_time, triplets): - """ - triplets tells you which samples are triplets (to start with). This way this method can be used to simulate - rabi or t1 cycle - Triplets have a probability of decaying set by the decay rate. - :param number_of_samples: - :param triplet_probability: - :return: - """ - - - - # quicker to generate all the samples first and then pick which ones we want - # based on the variable above (triplets) - - singlet_samples = np.random.normal( - loc=v_rf_s, - scale=sigma, - size=number_of_samples - ) - - singlet_samples_for_triplet_decay = np.random.normal( - loc=v_rf_s, - scale=sigma, - size=number_of_samples - ) - - triplet_samples = np.random.normal( - loc=v_rf_t, - scale=sigma, - size=number_of_samples - ) - - # add decay to the tT1,riplet samples - # ratio of decay time to integration time gives how far along - # v axis that samples moves towards singlet (representing it decaying - # at some point through the measurement window) - - decay_times = np.random.exponential( - scale=T1, - size=number_of_samples - ) - - # how much of the measurement was a triplet is given by this ratio. - # clipped to be max 1 - triplet_contribution = np.clip( - decay_times / integration_time, 0, 1 - ) - - decayed_triplet_samples = (triplet_contribution * triplet_samples) + ( - (1 - triplet_contribution) * singlet_samples_for_triplet_decay - ) - - return np.where(triplets, decayed_triplet_samples, singlet_samples) \ No newline at end of file diff --git a/joe_testing_active_reset/t1_basic.py b/joe_testing_active_reset/t1_basic.py deleted file mode 100644 index dd0ae0c8..00000000 --- a/joe_testing_active_reset/t1_basic.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -T1.py: Measures T1 -""" -from qm.qua import * -from qm.QuantumMachinesManager import QuantumMachinesManager -from configuration import * -import matplotlib.pyplot as plt -import numpy as np -from qm import SimulationConfig -from qualang_tools.loops import from_array - -from qm.simulate.credentials import create_credentials - -import logging -logging.basicConfig(level='INFO') -logger = logging.getLogger(__name__) - -################### -# The QUA program # -################### - -tau_min = 4 # in clock cycles -tau_max = 100 # in clock cycles -d_tau = 2 # in clock cycles -taus = np.arange(tau_min, tau_max + 0.1, d_tau) # + 0.1 to add t_max to taus - -n_avg = 1e4 -cooldown_time = 5 * qubit_T1 // 4 - -with program() as T1: - n = declare(int) - n_st = declare_stream() - I = declare(fixed) - I_st = declare_stream() - Q = declare(fixed) - Q_st = declare_stream() - tau = declare(int) - - with for_(n, 0, n < n_avg, n + 1): - with for_(*from_array(tau, taus)): - play("pi", "qubit") - wait(tau, "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), - ) - save(I, I_st) - save(Q, Q_st) - wait(cooldown_time, "resonator") - save(n, n_st) - - with stream_processing(): - I_st.buffer(len(taus)).average().save("I") - Q_st.buffer(len(taus)).average().save("Q") - n_st.save("iteration") - -##################################### -# Open Communication with the QOP # -##################################### -logger.info('connecting to qmm...') -qmm = QuantumMachinesManager( - host="theo-4c195fa0.dev.quantum-machines.co", - port=443, - credentials=create_credentials()) -logger.info('connected.') - -####################### -# Simulate or execute # -####################### - -simulate = True - -if simulate: - logger.info("simulating") - simulation_config = SimulationConfig(duration=1000) # in clock cycles - - logger.info('simulation config loaded') - job = qmm.simulate(config, T1, simulation_config) - - logger.info('job executed') - - job.get_simulated_samples().con1.plot() - -else: - qm = qmm.open_qm(config) - - job = qm.execute(T1) - # Get results from QUA program - results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live") - # Live plotting - fig = plt.figure() - interrupt_on_close(fig, job) # Interrupts the job when closing the figure - while results.is_processing(): - # Fetch results - I, Q, iteration = results.fetch_all() - # Progress bar - progress_counter(iteration, n_avg, start_time=results.get_start_time()) - # Plot results - plt.cla() - plt.plot(4 * taus, I, ".", label="I") - plt.plot(4 * taus, Q, ".", label="Q") - plt.xlabel("Decay time [ns]") - plt.ylabel("I & Q amplitude [a.u.]") - plt.title("T1 measurement") - plt.legend() - plt.pause(0.1) \ No newline at end of file diff --git a/joe_testing_active_reset/t1_cycle_simulator.py b/joe_testing_active_reset/t1_cycle_simulator.py deleted file mode 100644 index 4c5e5fbf..00000000 --- a/joe_testing_active_reset/t1_cycle_simulator.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Created on 04/08/2022 -@author jdh -""" - -from sample_generator import _generate_spin_samples - -import numpy as np - - -def t1_cycle(v_rf_s, v_rf_t, sigma, T1, number_of_samples, triplet_probability, integration_time): - """ - :param v_rf_s: mean singlet measurement value - :param v_rf_t: mean triplet measurement value - :param sigma: gaussian measurement noise - :param T1: rate parameter for average spin lifetime - :param number_of_samples: self - :param triplet_probability: probability of loading a triplet for each measurement (stick to 0.5) - :param integration_time: integration time for each measurement - :return: simulated data for a rabi experiment - """ - - # draw samples. 0 means singlet, 1 means triplet - triplets = np.random.binomial(1, triplet_probability, number_of_samples) - - return _generate_spin_samples(v_rf_s, v_rf_t, sigma, T1, number_of_samples, integration_time, triplets) - diff --git a/qualang_tools/analysis/__init__.py b/qualang_tools/analysis/__init__.py index 40ec314e..b8068d23 100644 --- a/qualang_tools/analysis/__init__.py +++ b/qualang_tools/analysis/__init__.py @@ -1,5 +1,6 @@ from qualang_tools.analysis.discriminator import two_state_discriminator + __all__ = [ "two_state_discriminator", ] diff --git a/qualang_tools/analysis/active_reset.py b/qualang_tools/analysis/active_reset.py deleted file mode 100644 index e69de29b..00000000 diff --git a/qualang_tools/analysis/dataPresenter.py b/qualang_tools/analysis/dataPresenter.py deleted file mode 100644 index 065632ec..00000000 --- a/qualang_tools/analysis/dataPresenter.py +++ /dev/null @@ -1,150 +0,0 @@ -import pyqtgraph as pg -from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets -import exampleLoaderTemplate_generic as ui_template -from argparse import Namespace -from collections import OrderedDict -import numpy as np -from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget - -class multiQubitReadoutPresenter(QtWidgets.QMainWindow): - - def __init__(self, results_dataclasses): - QtWidgets.QMainWindow.__init__(self) - - - self.tabs = QtWidgets.QTabWidget() - self.tab1 = QtWidgets.QWidget() - self.tab2 = QtWidgets.QWidget() - - self.ui = ui_template.Ui_Form() - self.cw = QtWidgets.QWidget() - self.setCentralWidget(self.cw) - self.ui.setupUi(self.cw) - self.setWindowTitle("Readout viewer") - - app = QtWidgets.QApplication.instance() - policy = QtWidgets.QSizePolicy.Policy.Expanding - - self.results_dataclasses = results_dataclasses - - self.curListener = None - self.itemCache = [] - # self.populateTree(self.ui.exampleTree.invisibleRootItem(), utils.examples_) - # self.ui.exampleTree.expandAll() - - self.resize(1200, 800) - self.show() - self.ui.splitter.setSizes([250, 950]) - - for i in range(1, len(results_dataclasses) + 1): - self.ui.qubitsList.addItem( - 'Qubit {}'.format(i) - ) - - # self.oldText = self.ui.codeView.toPlainText() - self.ui.loadBtn.clicked.connect(self.printer) - # self.ui.exampleTree.currentItemChanged.connect(self.showFile) - self.ui.qubitsList.itemDoubleClicked.connect(self.printer) - # self.ui.codeView.textChanged.connect(self.onTextChange) - # self.codeBtn.clicked.connect(self.runEditedCode) - # self.updateCodeViewTabWidth(self.ui.codeView.font()) - - self.tabs.addTab(self.tab1, 'Readout viewer') - self.tabs.addTab(self.tab2, 'All viewer') - self.ui.gridLayout.addWidget(self.tabs, 0, 0) - - def printer(self): - qubit_idx = self.ui.qubitsList.currentRow() - - results = self.results_dataclasses[qubit_idx] - - angle, threshold, fidelity, gg, ge, eg, ee = results.get_params() - ig, qg, ie, qe = results.get_data() - - ig_rotated, qg_rotated, ie_rotated, qe_rotated = results.get_rotated_data() - - - mw = MatplotlibWidget(parent=self.ui.readoutViewer) - - scatter = mw.getFigure().add_subplot(221) - rotated_scatter = mw.getFigure().add_subplot(222) - hist = mw.getFigure().add_subplot(223) - matrix = mw.getFigure().add_subplot(224) - - scatter.plot(ig, qg, '.', alpha=0.1, markersize=2, label='Ground') - scatter.plot(ie, qe, '.', alpha=0.1, markersize=2, label='Excited') - scatter.axis("equal") - scatter.legend(["Ground", "Excited"], loc='lower right') - scatter.set_xlabel("I") - scatter.set_ylabel("Q") - scatter.set_title("Original Data") - - rotated_scatter.plot(ig_rotated, qg_rotated, ".", alpha=0.1, label="Ground", markersize=2) - rotated_scatter.plot(ie_rotated, qe_rotated, ".", alpha=0.1, label="Excited", markersize=2) - rotated_scatter.axis("equal") - rotated_scatter.set_xlabel("I") - rotated_scatter.set_ylabel("Q") - rotated_scatter.set_title("Rotated Data") - - hist.hist(ig_rotated, bins=50, alpha=0.75, label="Ground") - hist.hist(ie_rotated, bins=50, alpha=0.75, label="Excited") - hist.axvline(x=threshold, color="k", ls="--", alpha=0.5) - text_props = dict( - horizontalalignment="center", - verticalalignment="center", - transform=hist.transAxes - ) - hist.text(0.7, 0.9, f"{threshold:.3e}", text_props) - hist.set_xlabel("I") - hist.set_title("1D Histogram") - - - matrix.imshow(results.confusion_matrix()) - matrix.set_xticks([0, 1]) - matrix.set_yticks([0, 1]) - matrix.set_xticklabels(labels=["|g>", "|e>"]) - matrix.set_yticklabels(labels=["|g>", "|e>"]) - matrix.set_ylabel("Prepared") - matrix.set_xlabel("Measured") - matrix.text(0, 0, f"{100 * gg:.1f}%", ha="center", va="center", color="k") - matrix.text(1, 0, f"{100 * ge:.1f}%", ha="center", va="center", color="w") - matrix.text(0, 1, f"{100 * eg:.1f}%", ha="center", va="center", color="w") - matrix.text(1, 1, f"{100 * ee:.1f}%", ha="center", va="center", color="k") - matrix.set_title("Fidelities") - - mw.getFigure().tight_layout() - mw.draw() - - self.ui.readoutViewer.addWidget( - mw, 0, 0 - ) - - - def populateTree(self, root, examples): - bold_font = None - for key, val in examples.items(): - item = QtWidgets.QTreeWidgetItem([key]) - self.itemCache.append(item) # PyQt 4.9.6 no longer keeps references to these wrappers, - # so we need to make an explicit reference or else the .file - # attribute will disappear. - if isinstance(val, OrderedDict): - self.populateTree(item, val) - elif isinstance(val, Namespace): - item.file = val.filename - if 'recommended' in val: - if bold_font is None: - bold_font = item.font(0) - bold_font.setBold(True) - item.setFont(0, bold_font) - else: - item.file = val - root.addChild(item) - - - -def main(): - app = pg.mkQApp() - loader = multiQubitReadoutPresenter() - pg.exec() - - diff --git a/qualang_tools/analysis/exampleLoaderTemplate_generic.py b/qualang_tools/analysis/exampleLoaderTemplate_generic.py deleted file mode 100644 index f884c32f..00000000 --- a/qualang_tools/analysis/exampleLoaderTemplate_generic.py +++ /dev/null @@ -1,56 +0,0 @@ -# Form implementation generated from reading ui file '../pyqtgraph/examples/exampleLoaderTemplate.ui' -# -# Created by: PyQt6 UI code generator 6.2.2 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from pyqtgraph.Qt import QtCore, QtGui, QtWidgets -import pyqtgraph as pg - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(846, 552) - self.gridLayout_2 = QtWidgets.QGridLayout(Form) - self.gridLayout_2.setObjectName("gridLayout_2") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - - - self.qubitsList = QtWidgets.QListWidget() - - self.gridLayout.addWidget(self.qubitsList, 3, 0, 1, 2) - self.layoutWidget1 = QtWidgets.QWidget(self.splitter) - self.layoutWidget1.setObjectName("layoutWidget1") - self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - font = QtGui.QFont() - font.setBold(True) - - # self.readoutViewer = pg.PlotWidget() - # self.readoutViewer = pg.GraphicsLayoutWidget() - # self.readoutViewer = QtWidgets.QWidget() - self.readoutViewer = pg.LayoutWidget() - - font = QtGui.QFont() - font.setFamily("Courier New") - self.readoutViewer.setFont(font) - self.readoutViewer.setObjectName("readoutViewer") - self.verticalLayout.addWidget(self.readoutViewer) - self.gridLayout_2.addWidget(self.splitter, 1, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "PyQtGraph")) diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py index 7b170a83..27426ca8 100644 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -2,70 +2,9 @@ import matplotlib.pyplot as plt import itertools from tqdm import tqdm -from dataclasses import dataclass - -from tkinter import * -from tkinter import ttk from .discriminator import two_state_discriminator -# from .viewer import App - -@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. - """ - - # 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): - self.generate_rotation_data() - - def confusion_matrix(self): - return np.array([ - [self.gg, self.ge], - [self.eg, self.ee] - ]) - - def get_params(self): - return self.angle, self.threshold, self.fidelity, self.gg, self.ge, self.eg, self.ee - - def get_data(self): - return self.ig, self.qg, self.ie, self.qe - - def get_rotated_data(self): - return self.ig_rotated, self.qg_rotated, self.ie_rotated, self.qe_rotated - - def generate_rotation_data(self): - # this should happen in the results dataclass not here - 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 - - +from .results_dataclass import DiscriminatorDataclass def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" @@ -136,55 +75,4 @@ def generate_labels(length): return out -def build_widget(num): - - root = Tk() - root.title("Readout statistics") - - # Add a grid - mainframe = Frame(root, height=500, width=500) - mainframe.grid(column=0, row=0, sticky=(W)) - mainframe.columnconfigure(0, weight=1) - mainframe.rowconfigure(0, weight=5) - mainframe.pack(pady=10, padx=10) - - # Create a Tkinter variable - tkvar = StringVar(root) - - # Dictionary with options - choices = {'Qubit {}'.format(i) for i in range(num)} - tkvar.set('Qubit 0') # set the default option - - popupMenu = OptionMenu(mainframe, tkvar, *choices) - Label(mainframe, text="Select qubit").grid(row=0, column=0) - popupMenu.grid(row=1, column=0) - - # on change dropdown value - def change_dropdown(*args): - print(tkvar.get()) - - # link function to change dropdown - tkvar.trace('w', change_dropdown) - root.geometry("600x500") - root.mainloop() - - -if __name__ == '__main__': - from dataPresenter import multiQubitReadoutPresenter - import pyqtgraph as pg - - iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 32)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T - - igs, qgs = iq_state_g - ies, qes = iq_state_e - - results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) - - def main(): - app = pg.mkQApp() - # loader = multiQubitReadoutPresenter(results) - loader = App(results) - pg.exec() - main() diff --git a/qualang_tools/analysis/results_dataclass.py b/qualang_tools/analysis/results_dataclass.py new file mode 100644 index 00000000..d0a13ef6 --- /dev/null +++ b/qualang_tools/analysis/results_dataclass.py @@ -0,0 +1,85 @@ +""" +Dataclass for holding the results after data has been run through the +two-state discriminator. +""" + +import numpy as np +from dataclasses import dataclass + +@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. + """ + + # 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 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 diff --git a/qualang_tools/analysis/viewer.py b/qualang_tools/analysis/viewer.py deleted file mode 100644 index 3ce3673b..00000000 --- a/qualang_tools/analysis/viewer.py +++ /dev/null @@ -1,126 +0,0 @@ -from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTabWidget, QGridLayout, QVBoxLayout - -import pyqtgraph as pg -from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets -import exampleLoaderTemplate_generic as ui_template - -from pyqtgraph.widgets.MatplotlibWidget import MatplotlibWidget - -class App(QWidget): - - def __init__(self, results_dataclasses): - - super().__init__() - - self.resize(1000, 800) - - mainLayout = QGridLayout() - - vLayout1 = QVBoxLayout() - - # first tab - self.tab1 = QWidget() - - - # self.tab1.layout.addWidget(self.qb_list, 3, 0, 1,2 ) - - self.tab1.ui = ui_template.Ui_Form() - # self.tab1.setCentralWidget(self.tab1) - self.tab1.ui.setupUi(self.tab1) - - # self.tab1.setLayout(self.tab1.layout) - # second tab - self.tab2 = QWidget() - - self.tabs = QTabWidget() - - self.tabs.addTab(self.tab1, 'Readout viewer') - self.tabs.addTab(self.tab2, 'all viewer') - - mainLayout.addWidget(self.tabs, 0, 0) - self.setLayout(mainLayout) - - - self.setWindowTitle("Readout viewer") - - app = QtWidgets.QApplication.instance() - policy = QtWidgets.QSizePolicy.Policy.Expanding - - self.results_dataclasses = results_dataclasses - - self.curListener = None - self.itemCache = [] - - self.resize(1200, 800) - self.show() - self.tab1.ui.splitter.setSizes([250, 950]) - - for i in range(1, len(results_dataclasses) + 1): - self.tab1.ui.qubitsList.addItem( - 'Qubit {}'.format(i) - ) - - self.tab1.ui.qubitsList.itemDoubleClicked.connect(self.printer) - - def printer(self): - qubit_idx = self.tab1.ui.qubitsList.currentRow() - - results = self.results_dataclasses[qubit_idx] - - angle, threshold, fidelity, gg, ge, eg, ee = results.get_params() - ig, qg, ie, qe = results.get_data() - ig_rotated, qg_rotated, ie_rotated, qe_rotated = results.get_rotated_data() - - mw = MatplotlibWidget(parent=self.tab1.ui.readoutViewer) - - scatter = mw.getFigure().add_subplot(221) - rotated_scatter = mw.getFigure().add_subplot(222) - hist = mw.getFigure().add_subplot(223) - matrix = mw.getFigure().add_subplot(224) - - scatter.plot(ig, qg, '.', alpha=0.1, markersize=2, label='Ground') - scatter.plot(ie, qe, '.', alpha=0.1, markersize=2, label='Excited') - scatter.axis("equal") - scatter.legend(["Ground", "Excited"], loc='lower right') - scatter.set_xlabel("I") - scatter.set_ylabel("Q") - scatter.set_title("Original Data") - - rotated_scatter.plot(ig_rotated, qg_rotated, ".", alpha=0.1, label="Ground", markersize=2) - rotated_scatter.plot(ie_rotated, qe_rotated, ".", alpha=0.1, label="Excited", markersize=2) - rotated_scatter.axis("equal") - rotated_scatter.set_xlabel("I") - rotated_scatter.set_ylabel("Q") - rotated_scatter.set_title("Rotated Data") - - hist.hist(ig_rotated, bins=50, alpha=0.75, label="Ground") - hist.hist(ie_rotated, bins=50, alpha=0.75, label="Excited") - hist.axvline(x=threshold, color="k", ls="--", alpha=0.5) - text_props = dict( - horizontalalignment="center", - verticalalignment="center", - transform=hist.transAxes - ) - hist.text(0.7, 0.9, f"{threshold:.3e}", text_props) - hist.set_xlabel("I") - hist.set_title("1D Histogram") - - matrix.imshow(results.confusion_matrix()) - matrix.set_xticks([0, 1]) - matrix.set_yticks([0, 1]) - matrix.set_xticklabels(labels=["|g>", "|e>"]) - matrix.set_yticklabels(labels=["|g>", "|e>"]) - matrix.set_ylabel("Prepared") - matrix.set_xlabel("Measured") - matrix.text(0, 0, f"{100 * gg:.1f}%", ha="center", va="center", color="k") - matrix.text(1, 0, f"{100 * ge:.1f}%", ha="center", va="center", color="w") - matrix.text(0, 1, f"{100 * eg:.1f}%", ha="center", va="center", color="w") - matrix.text(1, 1, f"{100 * ee:.1f}%", ha="center", va="center", color="k") - matrix.set_title("Fidelities") - - mw.getFigure().tight_layout() - mw.draw() - - self.tab1.ui.readoutViewer.addWidget( - mw, 0, 0 - ) From c7733103a9a386af0da4c46924a59cbc2bd7ebb8 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Fri, 14 Oct 2022 16:28:18 +0200 Subject: [PATCH 12/25] reset gui allows for multiple types of reset --- .../configuration.py | 0 joe_testing/example_reset_comparison.py | 39 ++ .../fake_iq_data.py | 0 .../joe_testing_discriminator.py | 0 joe_testing_active_reset/active_reset_gui.py | 243 --------- joe_testing_active_reset/basic_program.py | 176 ------- .../build_active_reset_program.py | 153 ------ .../build_passive_reset_program.py | 0 joe_testing_active_reset/configuration.py | 475 ------------------ joe_testing_active_reset/fake_data.py | 35 -- .../independent_multi_qubit_discriminator.py | 13 + qualang_tools/plot/__init__.py | 8 +- qualang_tools/plot/active_reset_gui.py | 245 +++++++++ .../plot}/discriminator_gui.py | 0 14 files changed, 304 insertions(+), 1083 deletions(-) rename {joe_testing_discriminator => joe_testing}/configuration.py (100%) create mode 100644 joe_testing/example_reset_comparison.py rename {joe_testing_discriminator => joe_testing}/fake_iq_data.py (100%) rename {joe_testing_discriminator => joe_testing}/joe_testing_discriminator.py (100%) delete mode 100644 joe_testing_active_reset/active_reset_gui.py delete mode 100644 joe_testing_active_reset/basic_program.py delete mode 100644 joe_testing_active_reset/build_active_reset_program.py delete mode 100644 joe_testing_active_reset/build_passive_reset_program.py delete mode 100644 joe_testing_active_reset/configuration.py delete mode 100644 joe_testing_active_reset/fake_data.py create mode 100644 qualang_tools/plot/active_reset_gui.py rename {discriminator_gui => qualang_tools/plot}/discriminator_gui.py (100%) diff --git a/joe_testing_discriminator/configuration.py b/joe_testing/configuration.py similarity index 100% rename from joe_testing_discriminator/configuration.py rename to joe_testing/configuration.py diff --git a/joe_testing/example_reset_comparison.py b/joe_testing/example_reset_comparison.py new file mode 100644 index 00000000..10b701a8 --- /dev/null +++ b/joe_testing/example_reset_comparison.py @@ -0,0 +1,39 @@ +# File to show for example how the reset comparison gui could work + +import numpy as np +from qualang_tools.plot import ActiveResetGUI +from PyQt5.QtWidgets import QApplication +import sys + + + +def generate_discrimination_data(): + from qualang_tools.analysis.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, 32)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T + + igs, qgs = iq_state_g + ies, qes = iq_state_e + + results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) + + 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_discriminator/fake_iq_data.py b/joe_testing/fake_iq_data.py similarity index 100% rename from joe_testing_discriminator/fake_iq_data.py rename to joe_testing/fake_iq_data.py diff --git a/joe_testing_discriminator/joe_testing_discriminator.py b/joe_testing/joe_testing_discriminator.py similarity index 100% rename from joe_testing_discriminator/joe_testing_discriminator.py rename to joe_testing/joe_testing_discriminator.py diff --git a/joe_testing_active_reset/active_reset_gui.py b/joe_testing_active_reset/active_reset_gui.py deleted file mode 100644 index b8a6054f..00000000 --- a/joe_testing_active_reset/active_reset_gui.py +++ /dev/null @@ -1,243 +0,0 @@ -import sys -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import pyqtgraph as pg -import numpy as np - -class ActiveResetGUI(QWidget): - def __init__(self, num_qubits): - - self.num_qubits = num_qubits - super(ActiveResetGUI, self).__init__() - self.initialise_ui() - self._populate_list() - - - def initialise_ui(self): - - hbox = QHBoxLayout(self) - - # create some widgets - - self.left = pg.LayoutWidget() - self.right = pg.LayoutWidget() - - self.qubit_list = QComboBox() - - self.info_box = pg.TableWidget(3, 3) - - - self.info_box.setData(self.generate_table(98, 86, 100e-9, 90e-9)) - - - self.graphics_window = pg.GraphicsLayoutWidget() - self.plt1 = self.graphics_window.addPlot(title='Passive reset IQ') - self.plt2 = self.graphics_window.addPlot(title='Active reset IQ') - - self.graphics_window.nextRow() - self.plt3 = self.graphics_window.addPlot(title='1D histogram') - self.plt4 = self.graphics_window.addPlot(title='1D histogram') - - - self.left.addWidget(self.qubit_list, 0, 0) - self.left.addWidget(self.info_box, 1, 0) - self.left.addWidget(QFrame(), 2, 0) - - self.info_box.horizontalHeader().setVisible(False) - self.info_box.verticalHeader().setVisible(False) - - - self.right.addWidget(self.graphics_window) - - - # 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() * (3/4))) - self.info_box.setMaximumWidth(int(table_size.width() * (3/4))) - self.info_box.setShowGrid(False) - - self.ground_state_colour = (100, 149, 237) - self.excited_state_colour = (255, 185, 15) - - - - hbox.addWidget(self.left) - hbox.addWidget(self.right) - - self.setLayout(hbox) - - QApplication.setStyle(QStyleFactory.create('Cleanlooks')) - - self.setGeometry(100, 100, 1200, 500) - self.setWindowTitle('Qubit reset comparison') - - # self.qubit_list.itemDoubleClicked.connect(self.func) - self.qubit_list.currentIndexChanged.connect(self.update_plots) - - - self.show() - - def generate_fake_histograms(self, a, b): - ## make interesting distribution of values - vals1 = np.random.normal(size=500) - vals2 = np.random.normal(size=260, loc=4) - ## compute standard histogram - y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) - y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) - return a * y, x, b * y2, x2 - - def generate_random_table_data(self): - - pf, af = np.random.rand(2) * 100 - pt, at = np.random.rand(2) / 5e8 - - return pf, pt, af, at - - def generate_table(self, pf, pt, af, at): - table_data = [ - ['', 'Passive', 'Active'], - ['Fidelity', f'{pf:.2f}%', f'{af:.2f}%'], - ['Time', f'{(pt * 1e9):.2f} ns', f'{(at * 1e9):.2f} ns'] - ] - - return table_data - - def set_table(self, pf, pt, af, at): - data = self.generate_table(pf, pt, af, at) - self.info_box.setData(data) - - def _generate_plot_1(self): - rotated_data_g = pg.ScatterPlotItem( - np.random.normal(1, 0.2, 5000), - np.random.normal(1, 0.2, 5000), - brush=(*self.ground_state_colour, 100), - symbol='s', - size='2', - pen=pg.mkPen(None) - ) - - rotated_data_e = pg.ScatterPlotItem( - np.random.normal(1.5, 0.2, 5000), - np.random.normal(1, 0.2, 5000), - brush=(*self.excited_state_colour, 100), - symbol='s', - size='2', - pen=pg.mkPen(None) - ) - - self.plt1.addItem(rotated_data_g) - self.plt1.addItem(rotated_data_e) - - plt1_threshold = 1.25 - - self.plt1.addLine( - x=plt1_threshold, - label=f'{plt1_threshold:.2f}, θ={(np.random.rand() * 1000) % 90:.2f}°', - labelOpts={'position': 0.9}, - pen={'color': 'white', 'dash': [20, 20]} - ) - - self.plt1.setAspectLocked() - - def _generate_plot_2(self): - - - rotated_data_g = pg.ScatterPlotItem( - np.random.normal(1, 0.2, 5000), - np.random.normal(1, 0.2, 5000), - brush=(*self.ground_state_colour, 100), - symbol='s', - size='2', - pen=pg.mkPen(None) - ) - - rotated_data_e = pg.ScatterPlotItem( - np.random.normal(1.5, 0.2, 5000), - np.random.normal(1, 0.2, 5000), - 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) - - plt2_threshold = 1.25 - - self.plt2.addLine( - x=plt2_threshold, - label=f'{plt2_threshold:.2f}, θ={(np.random.rand() * 1000) % 90:.2f}°', - labelOpts={'position': 0.9}, - pen={'color': 'white', 'dash': [20, 20]} - ) - - self.plt2.setAspectLocked() - - def _generate_plot_3(self): - - y, x, y2, x2 = self.generate_fake_histograms(1, 1) - - ## Using stepMode="center" causes the plot to draw two lines for each sample. - ## notice that len(x) == len(y)+1 - self.plt3.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.ground_state_colour, 200), pen=pg.mkPen(None)) - self.plt3.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.excited_state_colour, 200), pen=pg.mkPen(None)) - - plt3_threshold = np.mean([np.median(x), np.median(x2)]) + (0.2 * np.random.rand()) - - self.plt3.addLine( - x=plt3_threshold, - label=f'{plt3_threshold:.2f}', - labelOpts={'position': 0.95}, - pen={'color': 'white', 'dash': [20, 20]} - ) - - def _generate_plot_4(self): - - y, x, y2, x2 = self.generate_fake_histograms(0.2, 1.8) - self.plt4.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.ground_state_colour, 200), pen=pg.mkPen(None)) - self.plt4.plot(x2, y2, stepMode="center", fillLevel=0, fillOutline=True, brush=(*self.excited_state_colour, 200), pen=pg.mkPen(None)) - - plt4_threshold = np.mean([np.median(x), np.median(x2)]) + (0.2 * np.random.rand()) - - self.plt4.addLine( - x=plt4_threshold, - label=f'{plt4_threshold:.2f}', - labelOpts={'position': 0.95}, - pen={'color': 'white', 'dash': [20, 20]} - ) - - def update_plots(self): - - self.plt1.clear() - self.plt2.clear() - self.plt3.clear() - self.plt4.clear() - - self._generate_plot_1() - self._generate_plot_2() - self._generate_plot_3() - self._generate_plot_4() - - self.set_table(*self.generate_random_table_data()) - - - def _populate_list(self): - - for i in range(self.num_qubits): - self.qubit_list.addItem( - f'Qubit {i + 1}' - ) - - - -def main(): - app = QApplication(sys.argv) - ex = ActiveResetGUI(32) - # sys.exit(app.exec_()) - app.exec_() - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/joe_testing_active_reset/basic_program.py b/joe_testing_active_reset/basic_program.py deleted file mode 100644 index 9bcccded..00000000 --- a/joe_testing_active_reset/basic_program.py +++ /dev/null @@ -1,176 +0,0 @@ -import numpy as np -Amp = 0.5 - -config = { - 'version': 1, - - 'controllers': { - 'con1': { - 'type': 'opx1', - 'analog_outputs': { - 1: {'offset': +0.0}, - 2: {'offset': +0.0}, - 3: {'offset': +0.0}, - 4: {'offset': +0.0}, - }, - 'digital_outputs': { - 1: {}, - }, - 'analog_inputs': { - 1: {'offset': +0.0}, - 2: {'offset': +0.0}, - } - } - }, - - 'elements': { - 'qubit': { - 'mixInputs': { - 'I': ('con1', 1), - 'Q': ('con1', 2), - 'lo_frequency': 5.10e9, - 'mixer': 'mixer_qubit' - }, - 'intermediate_frequency': 5.15e6, - 'operations': { - 'gauss_pulse': 'gauss_pulse_in' - }, - }, - 'resonator': { - 'mixInputs': { - 'I': ('con1', 3), - 'Q': ('con1', 4), - 'lo_frequency': 6.00e9, - 'mixer': 'mixer_res' - }, - 'intermediate_frequency': 6.12e6, - 'operations': { - 'readout': 'meas_pulse_in', - }, - 'time_of_flight': 180, - 'smearing': 0, - 'outputs': { - 'out1': ('con1', 1), - 'out2': ('con1', 2) - } - - }, - }, - - 'pulses': { - 'meas_pulse_in': { - 'operation': 'measurement', - 'length': 20, - 'waveforms': { - 'I': 'exc_wf', - 'Q': 'zero_wf' - }, - 'integration_weights': { - 'cos': 'cos', - 'sin': 'sin', - 'minus_sin': 'minus_sin', - }, - 'digital_marker': 'marker1' - }, - 'gauss_pulse_in': { - 'operation': 'control', - 'length': 20, - 'waveforms': { - 'I': 'gauss_wf', - 'Q': 'zero_wf' - }, - } - }, - - 'waveforms': { - 'exc_wf': { - 'type': 'constant', - 'sample': 0.479 - }, - 'zero_wf': { - 'type': 'constant', - 'sample': 0.0 - }, - 'gauss_wf': { - 'type': 'arbitrary', - 'samples': [0.005, 0.013, - 0.02935, 0.05899883936462147, - 0.10732436763802927, 0.1767030571463228, - 0.2633180579359862, 0.35514694106994277, - 0.43353720001453067, 0.479, 0.479, - 0.4335372000145308, 0.3551469410699429, - 0.26331805793598645, 0.17670305714632292, - 0.10732436763802936, 0.05899883936462152, - 0.029354822126316085, 0.01321923408389493, - 0.005387955348880817] - } - }, - - 'digital_waveforms': { - 'marker1': { - 'samples': [(1, 4), (0, 2), (1, 1), (1, 0)] - } - }, - - 'integration_weights': { - 'cos': { - 'cosine': [(4.0,20)], - 'sine': [(0.0,20)] - }, - 'sin': { - 'cosine': [(0.0,20)], - 'sine': [(4.0,20)] - }, - 'minus_sin': { - 'cosine': [(0.0,20)], - 'sine': [(-4.0,20)] - }, - }, - - 'mixers': { - 'mixer_res': [ - {'intermediate_frequency': 6.12e6, 'lo_freq': 6.00e9, 'correction': [1.0, 0.0, 0.0, 1.0]} - ], - 'mixer_qubit': [ - {'intermediate_frequency': 5.15e6, 'lo_freq': 5.10e9, 'correction': [1.0, 0.0, 0.0, 1.0]} - ], - } -} - -gaus_pulse_len = 20 # nsec -gaus_arg = np.linspace(-3, 3, gaus_pulse_len) -gaus_wf = np.exp(-gaus_arg**2/2) -gaus_wf = Amp * gaus_wf / np.max(gaus_wf) - -HOST = '172.16.2.103' -PORT = 80 - -from qm.qua import * -from qm.QuantumMachinesManager import QuantumMachinesManager - -qmm = QuantumMachinesManager(host=HOST, port=PORT) - -qm = qmm.open_qm(config) - -with program() as program: - - I = declare(fixed) - Q = declare(fixed) - a = declare(fixed) - n_rep = declare(int) - - # this is the averaging loop - the number of repeititions. - with for_(n_rep, 0, n_rep < 100, n_rep + 1): - - with for_(a, 0., a < 1, a + 0.01): - - play('gauss_pulse' * amp(a), '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)) - - - - - diff --git a/joe_testing_active_reset/build_active_reset_program.py b/joe_testing_active_reset/build_active_reset_program.py deleted file mode 100644 index ed728fd2..00000000 --- a/joe_testing_active_reset/build_active_reset_program.py +++ /dev/null @@ -1,153 +0,0 @@ -import numpy as np -Amp = 0.5 - -config = { - 'version': 1, - - 'controllers': { - 'con1': { - 'type': 'opx1', - 'analog_outputs': { - 1: {'offset': +0.0}, - 2: {'offset': +0.0}, - 3: {'offset': +0.0}, - 4: {'offset': +0.0}, - }, - 'digital_outputs': { - 1: {}, - }, - 'analog_inputs': { - 1: {'offset': +0.0}, - 2: {'offset': +0.0}, - } - } - }, - - 'elements': { - 'qubit': { - 'mixInputs': { - 'I': ('con1', 1), - 'Q': ('con1', 2), - 'lo_frequency': 5.10e9, - 'mixer': 'mixer_qubit' - }, - 'intermediate_frequency': 5.15e6, - 'operations': { - 'gauss_pulse': 'gauss_pulse_in' - }, - }, - 'resonator': { - 'mixInputs': { - 'I': ('con1', 3), - 'Q': ('con1', 4), - 'lo_frequency': 6.00e9, - 'mixer': 'mixer_res' - }, - 'intermediate_frequency': 6.12e6, - 'operations': { - 'readout': 'meas_pulse_in', - }, - 'time_of_flight': 180, - 'smearing': 0, - 'outputs': { - 'out1': ('con1', 1), - 'out2': ('con1', 2) - } - - }, - }, - - 'pulses': { - 'meas_pulse_in': { - 'operation': 'measurement', - 'length': 20, - 'waveforms': { - 'I': 'exc_wf', - 'Q': 'zero_wf' - }, - 'integration_weights': { - 'cos': 'cos', - 'sin': 'sin', - 'minus_sin': 'minus_sin', - }, - 'digital_marker': 'marker1' - }, - 'gauss_pulse_in': { - 'operation': 'control', - 'length': 20, - 'waveforms': { - 'I': 'gauss_wf', - 'Q': 'zero_wf' - }, - } - }, - - 'waveforms': { - 'exc_wf': { - 'type': 'constant', - 'sample': 0.479 - }, - 'zero_wf': { - 'type': 'constant', - 'sample': 0.0 - }, - 'gauss_wf': { - 'type': 'arbitrary', - 'samples': [0.005, 0.013, - 0.02935, 0.05899883936462147, - 0.10732436763802927, 0.1767030571463228, - 0.2633180579359862, 0.35514694106994277, - 0.43353720001453067, 0.479, 0.479, - 0.4335372000145308, 0.3551469410699429, - 0.26331805793598645, 0.17670305714632292, - 0.10732436763802936, 0.05899883936462152, - 0.029354822126316085, 0.01321923408389493, - 0.005387955348880817] - } - }, - - 'digital_waveforms': { - 'marker1': { - 'samples': [(1, 4), (0, 2), (1, 1), (1, 0)] - } - }, - - 'integration_weights': { - 'cos': { - 'cosine': [(4.0,20)], - 'sine': [(0.0,20)] - }, - 'sin': { - 'cosine': [(0.0,20)], - 'sine': [(4.0,20)] - }, - 'minus_sin': { - 'cosine': [(0.0,20)], - 'sine': [(-4.0,20)] - }, - }, - - 'mixers': { - 'mixer_res': [ - {'intermediate_frequency': 6.12e6, 'lo_freq': 6.00e9, 'correction': [1.0, 0.0, 0.0, 1.0]} - ], - 'mixer_qubit': [ - {'intermediate_frequency': 5.15e6, 'lo_freq': 5.10e9, 'correction': [1.0, 0.0, 0.0, 1.0]} - ], - } -} - -gaus_pulse_len = 20 # nsec -gaus_arg = np.linspace(-3, 3, gaus_pulse_len) -gaus_wf = np.exp(-gaus_arg**2/2) -gaus_wf = Amp * gaus_wf / np.max(gaus_wf) - - - - -from qm.qua import * -from qm.QuantumMachinesManager import QuantumMachinesManager - -qmm = QuantumMachinesManager() - -qm = qmm.open_qm(config) diff --git a/joe_testing_active_reset/build_passive_reset_program.py b/joe_testing_active_reset/build_passive_reset_program.py deleted file mode 100644 index e69de29b..00000000 diff --git a/joe_testing_active_reset/configuration.py b/joe_testing_active_reset/configuration.py deleted file mode 100644 index dd7d984d..00000000 --- a/joe_testing_active_reset/configuration.py +++ /dev/null @@ -1,475 +0,0 @@ -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" -qop_ip = '172.16.2.103' - -# Qubits -qubit_IF = 50 * u.MHz -qubit_LO = 7 * u.GHz -mixer_qubit_g = 0.0 -mixer_qubit_phi = 0.0 - -qubit_T1 = int(10 * u.us) - -saturation_len = 1000 -saturation_amp = 0.1 -const_len = 100 -const_amp = 0.1 -square_pi_len = 100 -square_pi_amp = 0.1 - -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) - -displace_len = 40 -displace_sigma = displace_len / 5 -displace_amp = 0.35 -displace_wf = displace_amp * gaussian(displace_len, displace_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_IF = 60 * u.MHz -resonator_LO = 5.5 * u.GHz -mixer_resonator_g = 0.0 -mixer_resonator_phi = 0.0 - -time_of_flight = 180 - -short_readout_len = 500 -short_readout_amp = 0.4 -readout_len = 5000 -readout_amp = 0.2 -long_readout_len = 50000 -long_readout_amp = 0.1 - -# IQ Plane -rotation_angle = (0.0 / 180) * np.pi -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 - }, - "digital_outputs": {}, - "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", - "saturation": "saturation_pulse", - "gauss": "gaussian_pulse", - "pi": "x180_pulse", - "pi_half": "x90_pulse", - "x90": "x90_pulse", - "x180": "x180_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", - "displace": "displace_pulse", - "short_readout": "short_readout_pulse", - "readout": "readout_pulse", - "long_readout": "long_readout_pulse", - }, - "outputs": { - "out1": ("con1", 1), - "out2": ("con1", 2), - }, - "time_of_flight": time_of_flight, - "smearing": 0, - }, - }, - "pulses": { - "const_pulse": { - "operation": "control", - "length": const_len, - "waveforms": { - "I": "const_wf", - "Q": "zero_wf", - }, - }, - "square_pi_pulse": { - "operation": "control", - "length": square_pi_len, - "waveforms": { - "I": "square_pi_wf", - "Q": "zero_wf", - }, - }, - "saturation_pulse": { - "operation": "control", - "length": saturation_len, - "waveforms": {"I": "saturation_drive_wf", "Q": "zero_wf"}, - }, - "gaussian_pulse": { - "operation": "control", - "length": gauss_len, - "waveforms": { - "I": "gauss_wf", - "Q": "zero_wf", - }, - }, - "displace_pulse": { - "operation": "control", - "length": displace_len, - "waveforms": { - "I": "displace_wf", - "Q": "displace_wf", - }, - }, - "x90_pulse": { - "operation": "control", - "length": x90_len, - "waveforms": { - "I": "x90_I_wf", - "Q": "x90_Q_wf", - }, - }, - "x180_pulse": { - "operation": "control", - "length": x180_len, - "waveforms": { - "I": "x180_I_wf", - "Q": "x180_Q_wf", - }, - }, - "-x90_pulse": { - "operation": "control", - "length": minus_x90_len, - "waveforms": { - "I": "minus_x90_I_wf", - "Q": "minus_x90_Q_wf", - }, - }, - "y90_pulse": { - "operation": "control", - "length": y90_len, - "waveforms": { - "I": "y90_I_wf", - "Q": "y90_Q_wf", - }, - }, - "y180_pulse": { - "operation": "control", - "length": y180_len, - "waveforms": { - "I": "y180_I_wf", - "Q": "y180_Q_wf", - }, - }, - "-y90_pulse": { - "operation": "control", - "length": minus_y90_len, - "waveforms": { - "I": "minus_y90_I_wf", - "Q": "minus_y90_Q_wf", - }, - }, - "short_readout_pulse": { - "operation": "measurement", - "length": short_readout_len, - "waveforms": { - "I": "short_readout_wf", - "Q": "zero_wf", - }, - "integration_weights": { - "cos": "short_cosine_weights", - "sin": "short_sine_weights", - "minus_sin": "short_minus_sine_weights", - "rotated_cos": "short_rotated_cosine_weights", - "rotated_sin": "short_rotated_sine_weights", - "rotated_minus_sin": "short_rotated_minus_sine_weights", - }, - "digital_marker": "ON", - }, - "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", - }, - "long_readout_pulse": { - "operation": "measurement", - "length": long_readout_len, - "waveforms": { - "I": "long_readout_wf", - "Q": "zero_wf", - }, - "integration_weights": { - "cos": "long_cosine_weights", - "sin": "long_sine_weights", - "minus_sin": "long_minus_sine_weights", - "rotated_cos": "long_rotated_cosine_weights", - "rotated_sin": "long_rotated_sine_weights", - "rotated_minus_sin": "long_rotated_minus_sine_weights", - }, - "digital_marker": "ON", - }, - }, - "waveforms": { - "const_wf": {"type": "constant", "sample": const_amp}, - "saturation_drive_wf": {"type": "constant", "sample": saturation_amp}, - "square_pi_wf": {"type": "constant", "sample": square_pi_amp}, - "displace_wf": {"type": "arbitrary", "samples": displace_wf.tolist()}, - "zero_wf": {"type": "constant", "sample": 0.0}, - "gauss_wf": {"type": "arbitrary", "samples": gauss_wf.tolist()}, - "x90_I_wf": {"type": "arbitrary", "samples": x90_I_wf.tolist()}, - "x90_Q_wf": {"type": "arbitrary", "samples": x90_Q_wf.tolist()}, - "x180_I_wf": {"type": "arbitrary", "samples": x180_I_wf.tolist()}, - "x180_Q_wf": {"type": "arbitrary", "samples": x180_Q_wf.tolist()}, - "minus_x90_I_wf": {"type": "arbitrary", "samples": minus_x90_I_wf.tolist()}, - "minus_x90_Q_wf": {"type": "arbitrary", "samples": minus_x90_Q_wf.tolist()}, - "y90_Q_wf": {"type": "arbitrary", "samples": y90_Q_wf.tolist()}, - "y90_I_wf": {"type": "arbitrary", "samples": y90_I_wf.tolist()}, - "y180_Q_wf": {"type": "arbitrary", "samples": y180_Q_wf.tolist()}, - "y180_I_wf": {"type": "arbitrary", "samples": y180_I_wf.tolist()}, - "minus_y90_Q_wf": {"type": "arbitrary", "samples": minus_y90_Q_wf.tolist()}, - "minus_y90_I_wf": {"type": "arbitrary", "samples": minus_y90_I_wf.tolist()}, - "short_readout_wf": {"type": "constant", "sample": short_readout_amp}, - "readout_wf": {"type": "constant", "sample": readout_amp}, - "long_readout_wf": {"type": "constant", "sample": long_readout_amp}, - }, - "digital_waveforms": { - "ON": {"samples": [(1, 0)]}, - }, - "integration_weights": { - "short_cosine_weights": { - "cosine": [(1.0, short_readout_len)], - "sine": [(0.0, short_readout_len)], - }, - "short_sine_weights": { - "cosine": [(0.0, short_readout_len)], - "sine": [(1.0, short_readout_len)], - }, - "short_minus_sine_weights": { - "cosine": [(0.0, short_readout_len)], - "sine": [(-1.0, short_readout_len)], - }, - "short_rotated_cosine_weights": { - "cosine": [(np.cos(rotation_angle), short_readout_len)], - "sine": [(-np.sin(rotation_angle), short_readout_len)], - }, - "short_rotated_sine_weights": { - "cosine": [(np.sin(rotation_angle), short_readout_len)], - "sine": [(np.cos(rotation_angle), short_readout_len)], - }, - "short_rotated_minus_sine_weights": { - "cosine": [(-np.sin(rotation_angle), short_readout_len)], - "sine": [(-np.cos(rotation_angle), short_readout_len)], - }, - "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)], - }, - "long_cosine_weights": { - "cosine": [(1.0, long_readout_len)], - "sine": [(0.0, long_readout_len)], - }, - "long_sine_weights": { - "cosine": [(0.0, long_readout_len)], - "sine": [(1.0, long_readout_len)], - }, - "long_minus_sine_weights": { - "cosine": [(0.0, long_readout_len)], - "sine": [(-1.0, long_readout_len)], - }, - "long_rotated_cosine_weights": { - "cosine": [(np.cos(rotation_angle), long_readout_len)], - "sine": [(-np.sin(rotation_angle), long_readout_len)], - }, - "long_rotated_sine_weights": { - "cosine": [(np.sin(rotation_angle), long_readout_len)], - "sine": [(np.cos(rotation_angle), long_readout_len)], - }, - "long_rotated_minus_sine_weights": { - "cosine": [(-np.sin(rotation_angle), long_readout_len)], - "sine": [(-np.cos(rotation_angle), long_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/joe_testing_active_reset/fake_data.py b/joe_testing_active_reset/fake_data.py deleted file mode 100644 index c655af89..00000000 --- a/joe_testing_active_reset/fake_data.py +++ /dev/null @@ -1,35 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt - -from t1_cycle_simulator import t1_cycle - - - - - -def active_reset_experiment_simulator(v_0, v_1, sigma, num_samples, threshold): - - # generate random sample - - - # based on that sample, if high state reset to low state - - # measure - pass - -v_0 = 0.001 -v_1 = 0.01 -sigma = 0.04 ** 2 -T1 = 350e-6 -integration_time = 15e-6 -num_samples = 4000 -prob_state_1 = 0.5 - -data = t1_cycle(v_0, v_1, sigma,T1, num_samples, prob_state_1, integration_time) - -plt.figure() -plt.hist(data, bins=30) -plt.xlim([-0.04, 0.04]) -plt.show() - - diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py index 27426ca8..abb7a133 100644 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -5,6 +5,7 @@ from .discriminator import two_state_discriminator from .results_dataclass import DiscriminatorDataclass + def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" @@ -76,3 +77,15 @@ def generate_labels(length): return out +if __name__ == '__main__': + + + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 15)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 15)).T + + Igs, Qgs = iq_state_g + Ies, Qes = iq_state_e + + independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes) + + diff --git a/qualang_tools/plot/__init__.py b/qualang_tools/plot/__init__.py index 1b507128..f238888a 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.plot.active_reset_gui import ActiveResetGUI +from qualang_tools.plot.discriminator_gui import DiscriminatorGui -__all__ = ["interrupt_on_close"] +__all__ = [ + "interrupt_on_close", + "ActiveResetGUI", + "DiscriminatorGui" +] diff --git a/qualang_tools/plot/active_reset_gui.py b/qualang_tools/plot/active_reset_gui.py new file mode 100644 index 00000000..d8c74946 --- /dev/null +++ b/qualang_tools/plot/active_reset_gui.py @@ -0,0 +1,245 @@ +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): + + 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): + + self.plot_area = QWidget() + # self.plot_area = pg.LayoutWidget() + 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): + + # 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, 3) + + self.info_box.setData(self.generate_table(98, 86, 100e-9, 90e-9)) + + 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 generate_fake_histograms(self, a, b): + ## make interesting distribution of values + vals1 = np.random.normal(size=500) + vals2 = np.random.normal(size=260, loc=4) + ## compute standard histogram + y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) + y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) + return a * y, x, b * y2, x2 + + def toggle_views(self): + + 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_random_table_data(self): + + pf, af = np.random.rand(2) * 100 + pt, at = np.random.rand(2) / 5e8 + print(pt, at) + return pf, pt, af, at + + def generate_table(self, pf, pt, af, at): + table_data = [ + ['', 'Passive', 'Active'], + ['Fidelity', f'{pf:.2f}%', f'{af:.2f}%'], + ['Time', f'{(pt):.2f} ns', f'{(at * 1e9):.2f} ns'] + ] + + return table_data + + def set_table(self, pf, pt, af, at): + data = self.generate_table(pf, pt, af, at) + self.info_box.setData(data) + + def plot_to_scatter(self, scatter_plot, result_dataclass): + + 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): + + y, x = np.histogram(result_dataclass.ig_rotated, bins=np.linspace(-3, 8, 80)) + y2, x2 = np.histogram(result_dataclass.ie_rotated, 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): + + 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) + + def _populate_list(self): + + for i in range(self.num_qubits): + self.qubit_list.addItem( + f'Qubit {i + 1}' + ) + + +def main(): + app = QApplication(sys.argv) + ex = ActiveResetGUI({'passive_reset': [], 'active_reset': [], 'different_reset': []}) + # sys.exit(app.exec_()) + app.exec_() + + +if __name__ == '__main__': + main() diff --git a/discriminator_gui/discriminator_gui.py b/qualang_tools/plot/discriminator_gui.py similarity index 100% rename from discriminator_gui/discriminator_gui.py rename to qualang_tools/plot/discriminator_gui.py From 6498211d216b6abea6443219aaf1a977159071b1 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Fri, 14 Oct 2022 16:59:04 +0200 Subject: [PATCH 13/25] started program for data collection --- joe_testing/reset_data_collection.py | 15 +++++++++++++++ qualang_tools/analysis/__init__.py | 3 ++- qualang_tools/plot/active_reset_gui.py | 10 +--------- 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 joe_testing/reset_data_collection.py 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/analysis/__init__.py b/qualang_tools/analysis/__init__.py index b8068d23..1d5a078a 100644 --- a/qualang_tools/analysis/__init__.py +++ b/qualang_tools/analysis/__init__.py @@ -1,6 +1,7 @@ from qualang_tools.analysis.discriminator import two_state_discriminator - +from qualang_tools.analysis.results_dataclass import DiscriminatorDataclass __all__ = [ "two_state_discriminator", + "DiscriminatorDataclass" ] diff --git a/qualang_tools/plot/active_reset_gui.py b/qualang_tools/plot/active_reset_gui.py index d8c74946..ee1828d4 100644 --- a/qualang_tools/plot/active_reset_gui.py +++ b/qualang_tools/plot/active_reset_gui.py @@ -6,6 +6,7 @@ import time import numpy as np +# TODO: fix the table information so it updates and is taken from somewhere class ActiveResetGUI(QWidget): @@ -122,15 +123,6 @@ def _initialise_info_area(self): self.information_layout.addWidget(QFrame()) - def generate_fake_histograms(self, a, b): - ## make interesting distribution of values - vals1 = np.random.normal(size=500) - vals2 = np.random.normal(size=260, loc=4) - ## compute standard histogram - y, x = np.histogram(vals1, bins=np.linspace(-3, 8, 80)) - y2, x2 = np.histogram(vals2, bins=np.linspace(-3, 8, 80)) - return a * y, x, b * y2, x2 - def toggle_views(self): for check_box, plot_region in zip(self.check_boxes, self.plot_regions): From 54f000ac5f5c036892318b64b4e49a7b719628f8 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Mon, 17 Oct 2022 17:56:35 +0200 Subject: [PATCH 14/25] started program for data collection --- active_reset_full/IQ_blobs.py | 152 +++++++ active_reset_full/README.md | 3 + active_reset_full/compare_reset.py | 84 ++++ active_reset_full/configuration.py | 410 ++++++++++++++++++ active_reset_full/macros.py | 179 ++++++++ joe_testing/active_reset.py | 78 ++++ joe_testing/example_reset_comparison.py | 6 +- joe_testing/macros.py | 171 ++++++++ .../independent_multi_qubit_discriminator.py | 11 +- qualang_tools/analysis/results_dataclass.py | 4 + qualang_tools/plot/__init__.py | 4 +- qualang_tools/plot/active_reset_gui.py | 105 +++-- 12 files changed, 1168 insertions(+), 39 deletions(-) create mode 100644 active_reset_full/IQ_blobs.py create mode 100644 active_reset_full/README.md create mode 100644 active_reset_full/compare_reset.py create mode 100644 active_reset_full/configuration.py create mode 100644 active_reset_full/macros.py create mode 100644 joe_testing/active_reset.py create mode 100644 joe_testing/macros.py diff --git a/active_reset_full/IQ_blobs.py b/active_reset_full/IQ_blobs.py new file mode 100644 index 00000000..c6ed4769 --- /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/example_reset_comparison.py b/joe_testing/example_reset_comparison.py index 10b701a8..fca07ed4 100644 --- a/joe_testing/example_reset_comparison.py +++ b/joe_testing/example_reset_comparison.py @@ -10,14 +10,14 @@ def generate_discrimination_data(): from qualang_tools.analysis.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, 32)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T + 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 = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) - + [result.add_attribute('runtime', 100 * np.random.rand()) for result in results] return results 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/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py index abb7a133..5730a20c 100644 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -6,14 +6,17 @@ from .discriminator import two_state_discriminator from .results_dataclass import DiscriminatorDataclass + def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" result_dataclasses = [] - for Ig, Qg, Ie, Qe in zip(Igs, Qgs, Ies, Qes): + for i, (Ig, Qg, Ie, Qe) in enumerate(zip(Igs, Qgs, Ies, Qes)): result_dataclass = DiscriminatorDataclass( - *two_state_discriminator(Ig, Qg, Ie, Qe, b_print=b_print, b_plot=b_plot), + f'Qubit_{i}', + *two_state_discriminator( + Ig, Qg, Ie, Qe, b_print=b_print, b_plot=b_plot), Ig, Qg, Ie, Qe ) @@ -78,8 +81,6 @@ def generate_labels(length): if __name__ == '__main__': - - iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 15)).T iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 15)).T @@ -87,5 +88,3 @@ def generate_labels(length): Ies, Qes = iq_state_e independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes) - - diff --git a/qualang_tools/analysis/results_dataclass.py b/qualang_tools/analysis/results_dataclass.py index d0a13ef6..cedd0c83 100644 --- a/qualang_tools/analysis/results_dataclass.py +++ b/qualang_tools/analysis/results_dataclass.py @@ -12,6 +12,7 @@ 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 @@ -35,6 +36,9 @@ def __post_init__(self): """ 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 diff --git a/qualang_tools/plot/__init__.py b/qualang_tools/plot/__init__.py index f238888a..48d0f47f 100644 --- a/qualang_tools/plot/__init__.py +++ b/qualang_tools/plot/__init__.py @@ -1,9 +1,11 @@ from qualang_tools.plot.plot import interrupt_on_close from qualang_tools.plot.active_reset_gui import ActiveResetGUI from qualang_tools.plot.discriminator_gui import DiscriminatorGui +from qualang_tools.plot.active_reset_gui import launch_reset_gui __all__ = [ "interrupt_on_close", "ActiveResetGUI", - "DiscriminatorGui" + "DiscriminatorGui", + "launch_reset_gui" ] diff --git a/qualang_tools/plot/active_reset_gui.py b/qualang_tools/plot/active_reset_gui.py index ee1828d4..0b7ad4c8 100644 --- a/qualang_tools/plot/active_reset_gui.py +++ b/qualang_tools/plot/active_reset_gui.py @@ -6,8 +6,6 @@ import time import numpy as np -# TODO: fix the table information so it updates and is taken from somewhere - class ActiveResetGUI(QWidget): def __init__(self, reset_results_dictionary): @@ -27,6 +25,17 @@ def __init__(self, reset_results_dictionary): 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) @@ -51,6 +60,11 @@ def _initialise_ui(self): 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_area = pg.LayoutWidget() @@ -69,6 +83,10 @@ def _initialise_plot_area(self): 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() @@ -81,9 +99,8 @@ def _initialise_info_area(self): for key in self.reset_results_dictionary.keys(): self.check_boxes.append(QCheckBox(str(key).replace('_', ' '))) - self.info_box = pg.TableWidget(3, 3) - - self.info_box.setData(self.generate_table(98, 86, 100e-9, 90e-9)) + 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) @@ -124,6 +141,11 @@ def _initialise_info_area(self): 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(): @@ -131,27 +153,36 @@ def toggle_views(self): else: plot_region.hide() - def generate_random_table_data(self): + 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. - pf, af = np.random.rand(2) * 100 - pt, at = np.random.rand(2) / 5e8 - print(pt, at) - return pf, pt, af, at + @return: the list of lists representing rows of the table. + """ + overall_table = [['', 'Fidelity (%)', 'Time (ns)']] - def generate_table(self, pf, pt, af, at): - table_data = [ - ['', 'Passive', 'Active'], - ['Fidelity', f'{pf:.2f}%', f'{af:.2f}%'], - ['Time', f'{(pt):.2f} ns', f'{(at * 1e9):.2f} 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 table_data + return overall_table - def set_table(self, pf, pt, af, at): - data = self.generate_table(pf, pt, af, at) + 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, @@ -185,9 +216,15 @@ def plot_to_scatter(self, scatter_plot, result_dataclass): 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=np.linspace(-3, 8, 80)) - y2, x2 = np.histogram(result_dataclass.ie_rotated, bins=np.linspace(-3, 8, 80)) + 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)) @@ -204,6 +241,10 @@ def plot_to_histogram(self, histogram_plot, result_dataclass): 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() @@ -218,20 +259,26 @@ def update_plots(self): 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 i in range(self.num_qubits): + for result in dataclasses: self.qubit_list.addItem( - f'Qubit {i + 1}' + result.name ) -def main(): +def launch_reset_gui(data_dictionary): + app = QApplication(sys.argv) - ex = ActiveResetGUI({'passive_reset': [], 'active_reset': [], 'different_reset': []}) - # sys.exit(app.exec_()) + program = ActiveResetGUI(data_dictionary) app.exec_() - -if __name__ == '__main__': - main() From 68e27b8dc6b22c47f980b81d9cf03ad3e545c472 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 16 Nov 2022 14:22:32 +0000 Subject: [PATCH 15/25] guis and discriminator --- joe_testing/example_reset_comparison.py | 5 +- .../independent_multi_qubit_discriminator.py | 91 +++- qualang_tools/control_panel/gui.py | 444 ++++++++++++++++++ qualang_tools/plot/discriminator_gui.py | 10 +- 4 files changed, 536 insertions(+), 14 deletions(-) create mode 100644 qualang_tools/control_panel/gui.py diff --git a/joe_testing/example_reset_comparison.py b/joe_testing/example_reset_comparison.py index fca07ed4..93f30153 100644 --- a/joe_testing/example_reset_comparison.py +++ b/joe_testing/example_reset_comparison.py @@ -16,7 +16,10 @@ def generate_discrimination_data(): igs, qgs = iq_state_g ies, qes = iq_state_e - results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) + 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 diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py index 5730a20c..65dd1bc5 100644 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/independent_multi_qubit_discriminator.py @@ -3,30 +3,39 @@ import itertools from tqdm import tqdm -from .discriminator import two_state_discriminator +from qualang_tools.analysis.discriminator import two_state_discriminator from .results_dataclass import DiscriminatorDataclass +def list_is_rectangular(list): -def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): - assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" + 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, (Ig, Qg, Ie, Qe) in enumerate(zip(Igs, Qgs, Ies, Qes)): + for i, result in enumerate(results_list): result_dataclass = DiscriminatorDataclass( f'Qubit_{i}', *two_state_discriminator( - Ig, Qg, Ie, Qe, b_print=b_print, b_plot=b_plot), - Ig, Qg, Ie, Qe + *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) + 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 @@ -60,6 +69,62 @@ def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_pl return result_dataclasses +# this method is identical but it takes four arguments, each a list with the ith element belonging to the ith qubit. +# instead the method above takes a list where the first argument is a four-list of [ig, qg, ie, qe]. + +# def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): +# assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" +# +# result_dataclasses = [] +# +# for i, (Ig, Qg, Ie, Qe) in enumerate(zip(Igs, Qgs, Ies, Qes)): +# result_dataclass = DiscriminatorDataclass( +# f'Qubit_{i}', +# *two_state_discriminator( +# Ig, Qg, Ie, Qe, b_print=b_print, b_plot=b_plot), +# Ig, Qg, Ie, Qe +# ) +# +# 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) @@ -87,4 +152,10 @@ def generate_labels(length): Igs, Qgs = iq_state_g Ies, Qes = iq_state_e - independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes) + results_list = np.stack([Igs, Qgs, Ies, Qes], axis=1) + + # old method + # independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes) + + # new method + independent_multi_qubit_discriminator(results_list) \ No newline at end of file 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/discriminator_gui.py b/qualang_tools/plot/discriminator_gui.py index 3f53eefc..293e1590 100644 --- a/qualang_tools/plot/discriminator_gui.py +++ b/qualang_tools/plot/discriminator_gui.py @@ -400,15 +400,19 @@ def _list_by_fidelity(self): if __name__ == '__main__': + num_qubits = 10 + from qualang_tools.analysis.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, 32)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, 32)).T + iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, num_qubits)).T + iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, num_qubits)).T igs, qgs = iq_state_g ies, qes = iq_state_e - results = independent_multi_qubit_discriminator(igs, qgs, ies, qes, b_plot=False, b_print=False) + results_list = np.stack([igs, qgs, ies, qes], axis=1) + + results = independent_multi_qubit_discriminator(results_list, b_plot=False, b_print=False) def main(): app = pg.mkQApp() From 12b8468208d85c39067222f3ba79b47bef956af1 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Thu, 17 Nov 2022 10:52:08 +0000 Subject: [PATCH 16/25] ran black --- active_reset_full/IQ_blobs.py | 2 +- joe_testing/example_reset_comparison.py | 4 +- qualang_tools/analysis/__init__.py | 2 +- .../independent_multi_qubit_discriminator.py | 161 -------------- .../multi_qubit_discriminator/README.md | 0 .../multi_qubit_discriminator/__init__.py | 0 .../active_reset_gui.py | 104 +++++---- .../discriminator_gui.py | 208 ++++++++++-------- .../independent_multi_qubit_discriminator.py | 201 +++++++++++++++++ qualang_tools/analysis/results_dataclass.py | 89 -------- qualang_tools/plot/__init__.py | 4 +- 11 files changed, 389 insertions(+), 386 deletions(-) delete mode 100644 qualang_tools/analysis/independent_multi_qubit_discriminator.py create mode 100644 qualang_tools/analysis/multi_qubit_discriminator/README.md create mode 100644 qualang_tools/analysis/multi_qubit_discriminator/__init__.py rename qualang_tools/{plot => analysis/multi_qubit_discriminator}/active_reset_gui.py (77%) rename qualang_tools/{plot => analysis/multi_qubit_discriminator}/discriminator_gui.py (68%) create mode 100644 qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py delete mode 100644 qualang_tools/analysis/results_dataclass.py diff --git a/active_reset_full/IQ_blobs.py b/active_reset_full/IQ_blobs.py index c6ed4769..8d97749f 100644 --- a/active_reset_full/IQ_blobs.py +++ b/active_reset_full/IQ_blobs.py @@ -94,7 +94,7 @@ def run_and_format(reset_program, qmm, simulation=True): 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._add_attribute('runtime', runtime) results_dataclass_list.append(results_dataclass) return results_dataclass_list diff --git a/joe_testing/example_reset_comparison.py b/joe_testing/example_reset_comparison.py index 93f30153..eb1ad2ff 100644 --- a/joe_testing/example_reset_comparison.py +++ b/joe_testing/example_reset_comparison.py @@ -8,7 +8,7 @@ def generate_discrimination_data(): - from qualang_tools.analysis.independent_multi_qubit_discriminator import independent_multi_qubit_discriminator + 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 @@ -20,7 +20,7 @@ def generate_discrimination_data(): 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] + [result._add_attribute('runtime', 100 * np.random.rand()) for result in results] return results diff --git a/qualang_tools/analysis/__init__.py b/qualang_tools/analysis/__init__.py index 1d5a078a..e0bec355 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 -from qualang_tools.analysis.results_dataclass import DiscriminatorDataclass +from qualang_tools.analysis.multi_qubit_discriminator.results_dataclass import DiscriminatorDataclass __all__ = [ "two_state_discriminator", diff --git a/qualang_tools/analysis/independent_multi_qubit_discriminator.py b/qualang_tools/analysis/independent_multi_qubit_discriminator.py deleted file mode 100644 index 65dd1bc5..00000000 --- a/qualang_tools/analysis/independent_multi_qubit_discriminator.py +++ /dev/null @@ -1,161 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import itertools -from tqdm import tqdm - -from qualang_tools.analysis.discriminator import two_state_discriminator -from .results_dataclass import DiscriminatorDataclass - -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 - -# this method is identical but it takes four arguments, each a list with the ith element belonging to the ith qubit. -# instead the method above takes a list where the first argument is a four-list of [ig, qg, ie, qe]. - -# def independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes, b_print=True, b_plot=True, text=False): -# assert len(Igs) == len(Qgs) == len(Ies) == len(Qes), "we don't have full readout information for all qubits" -# -# result_dataclasses = [] -# -# for i, (Ig, Qg, Ie, Qe) in enumerate(zip(Igs, Qgs, Ies, Qes)): -# result_dataclass = DiscriminatorDataclass( -# f'Qubit_{i}', -# *two_state_discriminator( -# Ig, Qg, Ie, Qe, b_print=b_print, b_plot=b_plot), -# Ig, Qg, Ie, Qe -# ) -# -# 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 - - -if __name__ == '__main__': - iq_state_g = np.random.multivariate_normal((0, -0.2), ((1.5, 0.), (0., 1.5)), (5000, 15)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((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) - - # old method - # independent_multi_qubit_discriminator(Igs, Qgs, Ies, Qes) - - # new method - independent_multi_qubit_discriminator(results_list) \ No newline at end of file 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..e69de29b 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..e69de29b diff --git a/qualang_tools/plot/active_reset_gui.py b/qualang_tools/analysis/multi_qubit_discriminator/active_reset_gui.py similarity index 77% rename from qualang_tools/plot/active_reset_gui.py rename to qualang_tools/analysis/multi_qubit_discriminator/active_reset_gui.py index 0b7ad4c8..9f5284f9 100644 --- a/qualang_tools/plot/active_reset_gui.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/active_reset_gui.py @@ -42,10 +42,10 @@ def _initialise_ui(self): self.ground_state_colour = (100, 149, 237) self.excited_state_colour = (255, 185, 15) - QApplication.setStyle(QStyleFactory.create('Cleanlooks')) + QApplication.setStyle(QStyleFactory.create("Cleanlooks")) self.setGeometry(100, 100, 1200, 500) - self.setWindowTitle('Qubit reset comparison') + self.setWindowTitle("Qubit reset comparison") self._initialise_info_area() self._initialise_plot_area() @@ -67,7 +67,6 @@ def _initialise_plot_area(self): """ self.plot_area = QWidget() - # self.plot_area = pg.LayoutWidget() self.plot_layout = QHBoxLayout() self.plot_area.setLayout(self.plot_layout) @@ -78,10 +77,15 @@ def _initialise_plot_area(self): 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( + 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 @@ -97,7 +101,7 @@ def _initialise_info_area(self): self.check_boxes = [] for key in self.reset_results_dictionary.keys(): - self.check_boxes.append(QCheckBox(str(key).replace('_', ' '))) + self.check_boxes.append(QCheckBox(str(key).replace("_", " "))) self.info_box = pg.TableWidget(3, len(self.reset_results_dictionary)) self.set_table() @@ -117,14 +121,18 @@ def _initialise_info_area(self): 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 = 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.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) @@ -160,23 +168,26 @@ def generate_table(self): @return: the list of lists representing rows of the table. """ - overall_table = [['', 'Fidelity (%)', 'Time (ns)']] + 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}'] + 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 @@ -188,18 +199,18 @@ def plot_to_scatter(self, scatter_plot, result_dataclass): result_dataclass.ig_rotated, result_dataclass.qg_rotated, brush=(*self.ground_state_colour, 100), - symbol='s', - size='2', - pen=pg.mkPen(None) + 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) + symbol="s", + size="2", + pen=pg.mkPen(None), ) scatter_plot.addItem(rotated_data_g) @@ -207,9 +218,9 @@ def plot_to_scatter(self, scatter_plot, result_dataclass): 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]} + 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() @@ -223,19 +234,37 @@ def plot_to_histogram(self, histogram_plot, result_dataclass): @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)) + 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]} + label=f"{result_dataclass.threshold:.2f}", + labelOpts={"position": 0.95}, + pen={"color": "white", "dash": [20, 20]}, ) return histogram_plot @@ -248,7 +277,9 @@ def update_plots(self): qubit_id = self.qubit_list.currentIndex() - for plot_region, (data_key, data_list) in zip(self.plot_regions, self.reset_results_dictionary.items()): + 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) @@ -271,9 +302,7 @@ def _populate_list(self): dataclasses = self.reset_results_dictionary[key] for result in dataclasses: - self.qubit_list.addItem( - result.name - ) + self.qubit_list.addItem(result.name) def launch_reset_gui(data_dictionary): @@ -281,4 +310,3 @@ def launch_reset_gui(data_dictionary): app = QApplication(sys.argv) program = ActiveResetGUI(data_dictionary) app.exec_() - diff --git a/qualang_tools/plot/discriminator_gui.py b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py similarity index 68% rename from qualang_tools/plot/discriminator_gui.py rename to qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py index 293e1590..f413171e 100644 --- a/qualang_tools/plot/discriminator_gui.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py @@ -10,8 +10,8 @@ # TODO: sort out the axes so plot 4 has the axes around the image rather than the plot area -class DiscriminatorGui(QWidget): +class DiscriminatorGui(QWidget): def __init__(self, results_dataclasses): """ GUI for presenting per-qubit readout information as well as a general overview dashboard which @@ -29,8 +29,6 @@ def __init__(self, results_dataclasses): self._list_by_fidelity() self.show() - - def setup_dashboard_tab(self): """ Sets up the dashboard tab with overview information about the qubit register. @@ -55,44 +53,50 @@ def setup_dashboard_tab(self): 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.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]) + 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 = 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.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') + metadata = QLabel(f"Some other statistics") - error_correlations = QLabel('Error correlations') + 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}") + 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. @@ -106,8 +110,8 @@ def initialise_ui(self): self.tabs = QTabWidget() - self.tabs.addTab(self.dashboard_tab, 'Dashboard') - self.tabs.addTab(self.readout_tab, 'Qubits') + self.tabs.addTab(self.dashboard_tab, "Dashboard") + self.tabs.addTab(self.readout_tab, "Qubits") box_layout = QHBoxLayout(self) @@ -127,28 +131,36 @@ def initialise_ui(self): 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 = 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.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.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.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) @@ -161,7 +173,6 @@ def initialise_ui(self): splitter.addWidget(self.left) splitter.addWidget(self.right) - box_layout.addWidget(splitter) main_layout.addWidget(self.tabs, 0, 0) @@ -169,14 +180,13 @@ def initialise_ui(self): # self.layout().addWidget(self.tabs) - QApplication.setStyle(QStyleFactory.create('Cleanlooks')) + QApplication.setStyle(QStyleFactory.create("Cleanlooks")) self.setGeometry(100, 100, 1100, 700) - self.setWindowTitle('Readout viewer') + 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 @@ -190,7 +200,6 @@ def switch_to_qubit_tab(self): 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 @@ -213,21 +222,20 @@ def _generate_unrotated_scatter_plot(self, result): ig, qg, brush=(*self.ground_state_colour, 100), - symbol='s', - size='2', - pen=pg.mkPen(None) + 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) + symbol="s", + size="2", + pen=pg.mkPen(None), ) - self.plt1.addItem(original_data_g) self.plt1.addItem(original_data_e) self.plt1.setAspectLocked() @@ -245,18 +253,18 @@ def _generate_rotated_data_plot(self, result): ig_rotated, qg_rotated, brush=(*self.ground_state_colour, 100), - symbol='s', - size='2', - pen=pg.mkPen(None) + 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) + symbol="s", + size="2", + pen=pg.mkPen(None), ) self.plt2.addItem(rotated_data_g) @@ -272,31 +280,32 @@ def _generate_1d_histogram(self, result): 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, + ig_hist_x, + ig_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(*self.ground_state_colour, 200), - pen=pg.mkPen(None) + pen=pg.mkPen(None), ) self.plt3.plot( - ie_hist_x, ie_hist_y, + ie_hist_x, + ie_hist_y, stepMode="center", fillLevel=0, fillOutline=False, brush=(*self.excited_state_colour, 200), - pen=pg.mkPen(None) + 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]}) - + 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): """ @@ -305,25 +314,32 @@ def _generate_confusion_matrix_plot(self, result): """ img = pg.ImageItem(image=result.confusion_matrix(), rect=[1, 1, 1, 1]) - img.setColorMap('viridis') + 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)) + 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_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) @@ -335,8 +351,8 @@ def _generate_confusion_matrix_plot(self, result): 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 = self.plt4.getAxis("bottom") + y_axis = self.plt4.getAxis("left") x_axis.setRange(1, 2) y_axis.setRange(1, 2) @@ -344,18 +360,17 @@ def _generate_confusion_matrix_plot(self, result): self.plt4.setXRange(1, 2) self.plt4.setYRange(1, 2) - x_axis.setLabel('Measured') - y_axis.setLabel('Prepared') + 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>')]]) + 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 @@ -378,41 +393,52 @@ def _populate_list(self): """ for i in range(self.num_qubits): - self.qubit_list.addItem( - f'Qubit {i + 1}' - ) + self.qubit_list.addItem(f"Qubit {i + 1}") def _list_by_fidelity(self): - unsorted_qubit_fidelities = [result.fidelity for result in self.results_dataclasses] + 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] + 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}' + 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}%')) - - + self.dashboard_list.setItem(i, 1, QTableWidgetItem(f"{fidelity:.2f}%")) -if __name__ == '__main__': +if __name__ == "__main__": num_qubits = 10 - from qualang_tools.analysis.independent_multi_qubit_discriminator import independent_multi_qubit_discriminator + 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, num_qubits)).T - iq_state_e = np.random.multivariate_normal((-1.8, -3.), ((1.5, 0), (0, 1.5)), (5000, num_qubits)).T + 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) + results = independent_multi_qubit_discriminator( + results_list, b_plot=False, b_print=False + ) def main(): app = pg.mkQApp() 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..ad288279 --- /dev/null +++ b/qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py @@ -0,0 +1,201 @@ +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) + + # new method + independent_multi_qubit_discriminator(results_list) diff --git a/qualang_tools/analysis/results_dataclass.py b/qualang_tools/analysis/results_dataclass.py deleted file mode 100644 index cedd0c83..00000000 --- a/qualang_tools/analysis/results_dataclass.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Dataclass for holding the results after data has been run through the -two-state discriminator. -""" - -import numpy as np -from dataclasses import dataclass - -@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 diff --git a/qualang_tools/plot/__init__.py b/qualang_tools/plot/__init__.py index 48d0f47f..101bb43e 100644 --- a/qualang_tools/plot/__init__.py +++ b/qualang_tools/plot/__init__.py @@ -1,7 +1,5 @@ from qualang_tools.plot.plot import interrupt_on_close -from qualang_tools.plot.active_reset_gui import ActiveResetGUI -from qualang_tools.plot.discriminator_gui import DiscriminatorGui -from qualang_tools.plot.active_reset_gui import launch_reset_gui +from qualang_tools.analysis.multi_qubit_discriminator.discriminator_gui import DiscriminatorGui __all__ = [ "interrupt_on_close", From c33070f985705c94001e6f3b78b52026e697c7c9 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Thu, 17 Nov 2022 13:30:33 +0000 Subject: [PATCH 17/25] added readme --- joe_testing/example_reset_comparison.py | 2 +- qualang_tools/analysis/__init__.py | 4 +- .../multi_qubit_discriminator/README.md | 79 +++++++++++++++++++ .../multi_qubit_discriminator/__init__.py | 11 +++ .../discriminator_gui.py | 14 ++-- .../independent_multi_qubit_discriminator.py | 3 +- 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/joe_testing/example_reset_comparison.py b/joe_testing/example_reset_comparison.py index eb1ad2ff..5e8bfee8 100644 --- a/joe_testing/example_reset_comparison.py +++ b/joe_testing/example_reset_comparison.py @@ -1,7 +1,7 @@ # File to show for example how the reset comparison gui could work import numpy as np -from qualang_tools.plot import ActiveResetGUI +from qualang_tools.analysis.multi_qubit_discriminator import ActiveResetGUI from PyQt5.QtWidgets import QApplication import sys diff --git a/qualang_tools/analysis/__init__.py b/qualang_tools/analysis/__init__.py index e0bec355..4f79c208 100644 --- a/qualang_tools/analysis/__init__.py +++ b/qualang_tools/analysis/__init__.py @@ -1,7 +1,5 @@ from qualang_tools.analysis.discriminator import two_state_discriminator -from qualang_tools.analysis.multi_qubit_discriminator.results_dataclass import DiscriminatorDataclass __all__ = [ - "two_state_discriminator", - "DiscriminatorDataclass" + "two_state_discriminator" ] diff --git a/qualang_tools/analysis/multi_qubit_discriminator/README.md b/qualang_tools/analysis/multi_qubit_discriminator/README.md index e69de29b..8d272f8e 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator/README.md +++ 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 index e69de29b..7ebeb0ea 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator/__init__.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/__init__.py @@ -0,0 +1,11 @@ +from .active_reset_gui import ActiveResetGUI, launch_reset_gui +from .discriminator_gui import DiscriminatorGui +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 +] \ No newline at end of file diff --git a/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py index f413171e..6ebe3fac 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py @@ -416,6 +416,13 @@ def _list_by_fidelity(self): self.dashboard_list.setItem(i, 1, QTableWidgetItem(f"{fidelity:.2f}%")) +def lauch_discriminator_gui(results): + app = pg.mkQApp() + loader = DiscriminatorGui(results) + pg.exec() + + +# example with fake data if __name__ == "__main__": num_qubits = 10 @@ -440,10 +447,5 @@ def _list_by_fidelity(self): results_list, b_plot=False, b_print=False ) - def main(): - app = pg.mkQApp() - # loader = multiQubitReadoutPresenter(results) - loader = DiscriminatorGui(results) - pg.exec() + lauch_discriminator_gui(results) - main() 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 index ad288279..0abe35fc 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/independent_multi_qubit_discriminator.py @@ -197,5 +197,4 @@ def generate_rotation_data(self): results_list = np.stack([Igs, Qgs, Ies, Qes], axis=1) - # new method - independent_multi_qubit_discriminator(results_list) + results_dataclasses = independent_multi_qubit_discriminator(results_list) From 1f934fc880b447fd31a5a2a6dbe164072195d5e4 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Thu, 17 Nov 2022 13:31:13 +0000 Subject: [PATCH 18/25] typo --- qualang_tools/analysis/multi_qubit_discriminator/__init__.py | 5 +++-- .../analysis/multi_qubit_discriminator/discriminator_gui.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/qualang_tools/analysis/multi_qubit_discriminator/__init__.py b/qualang_tools/analysis/multi_qubit_discriminator/__init__.py index 7ebeb0ea..31af8484 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator/__init__.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/__init__.py @@ -1,5 +1,5 @@ from .active_reset_gui import ActiveResetGUI, launch_reset_gui -from .discriminator_gui import DiscriminatorGui +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 @@ -7,5 +7,6 @@ ActiveResetGUI, DiscriminatorGui, independent_multi_qubit_discriminator, - launch_reset_gui + launch_reset_gui, + launch_discriminator_gui ] \ No newline at end of file diff --git a/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py index 6ebe3fac..174f642b 100644 --- a/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py +++ b/qualang_tools/analysis/multi_qubit_discriminator/discriminator_gui.py @@ -416,7 +416,7 @@ def _list_by_fidelity(self): self.dashboard_list.setItem(i, 1, QTableWidgetItem(f"{fidelity:.2f}%")) -def lauch_discriminator_gui(results): +def launch_discriminator_gui(results): app = pg.mkQApp() loader = DiscriminatorGui(results) pg.exec() @@ -447,5 +447,5 @@ def lauch_discriminator_gui(results): results_list, b_plot=False, b_print=False ) - lauch_discriminator_gui(results) + launch_discriminator_gui(results) From c9a91bfebd6c89ee1084f730a9e3de4ffc16cb85 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Mon, 27 Mar 2023 10:37:25 +0300 Subject: [PATCH 19/25] working on plotter for niv --- niv_plotter/__init__.py | 0 niv_plotter/grid_gui.py | 73 +++++ niv_plotter/grid_gui_class.py | 115 ++++++++ niv_plotter/gui.py | 460 ++++++++++++++++++++++++++++++++ niv_plotter/main.py | 487 ++++++++++++++++++++++++++++++++++ niv_plotter/mp.py | 18 ++ niv_plotter/mp_gui.py | 51 ++++ niv_plotter/test.py | 8 + niv_plotter/testing.py | 75 ++++++ niv_plotter/testing_mp.py | 288 ++++++++++++++++++++ niv_plotter/widget.py | 83 ++++++ 11 files changed, 1658 insertions(+) create mode 100644 niv_plotter/__init__.py create mode 100644 niv_plotter/grid_gui.py create mode 100644 niv_plotter/grid_gui_class.py create mode 100644 niv_plotter/gui.py create mode 100644 niv_plotter/main.py create mode 100644 niv_plotter/mp.py create mode 100644 niv_plotter/mp_gui.py create mode 100644 niv_plotter/test.py create mode 100644 niv_plotter/testing.py create mode 100644 niv_plotter/testing_mp.py create mode 100644 niv_plotter/widget.py diff --git a/niv_plotter/__init__.py b/niv_plotter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/niv_plotter/grid_gui.py b/niv_plotter/grid_gui.py new file mode 100644 index 00000000..a37b86a4 --- /dev/null +++ b/niv_plotter/grid_gui.py @@ -0,0 +1,73 @@ +""" +Demonstrate the use of layouts to control placement of multiple plots / views / +labels +""" + +import numpy as np +import pyqtgraph as pg +import threading +from multiprocessing import Process + + +def fake_function(x): + x0 = (0.5 - np.random.rand()) * 0.8 + return (1 / (1 + ((x - x0) / 0.25)**2)) + +def fake_data(): + x = np.linspace(-1, 1, 100) + y = fake_function(x) + y += np.random.rand(y.size) * 0.2 + return x, y + +def click_function(): + print('something has been clicked') + + +qubit_array_shape = (5, 5) + +qubits = ((0, 0), (0, 1), (1, 1), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)) + +x, y = qubit_array_shape + +app = pg.mkQApp("Gradiant Layout Example") +view = pg.GraphicsView() +l = pg.GraphicsLayout(border=(100,100,100)) +view.setCentralItem(l) +view.show() +view.setWindowTitle("Niv's special viewer") +view.resize(800,600) + + + +plots = [] +for i in range(x): + row = [] + for j in range(y): + plot = l.addPlot(title=f'Qubit[{i}{j}]') + row.append(plot) + plots.append(row) + l.nextRow() + +for i, row in enumerate(plots): + for j, plot in enumerate(row): + if (i, j) in qubits: + plot.plot(*fake_data()) + else: + plot.hideAxis('bottom') + plot.hideAxis('left') + plot.hideButtons() + + + + +def launch_program(): + app = pg.mkQApp() + pg.exec() + +if __name__ == '__main__': + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * + import sys + x = Process(target=launch_program(), daemon=True) + x.start() + x.join() diff --git a/niv_plotter/grid_gui_class.py b/niv_plotter/grid_gui_class.py new file mode 100644 index 00000000..728af359 --- /dev/null +++ b/niv_plotter/grid_gui_class.py @@ -0,0 +1,115 @@ +""" +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 +from multiprocessing import Process + + +class MyPlotWidget(pg.PlotWidget): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # self.scene() is a pyqtgraph.GraphicsScene.GraphicsScene.GraphicsScene + self.scene().sigMouseClicked.connect(self.mouse_clicked) + + + def mouse_clicked(self, mouseClickEvent): + # mouseClickEvent is a pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent + print('clicked plot 0x{:x}, event: {}'.format(id(self), mouseClickEvent)) + + +class GUI(QWidget): + def __init__(self): + """ + 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.app = pg.mkQApp() + + self.qubit_array_shape = (3,3) + self.x, self.y = self.qubit_array_shape + self.qubits = ((0, 0), (0, 1), (1, 1), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)) + + super(GUI, self).__init__() + self.initialise_ui() + self.setup_plots() + + # pg.exec() + + + def initialise_ui(self): + self.view = pg.GraphicsView() + self.l = pg.GraphicsLayout(border=(100, 100, 100)) + self.view.setCentralItem(self.l) + self.view.show() + self.view.setWindowTitle("Niv's special viewer") + self.view.resize(800, 600) + + def setup_plots(self): + self.plots = [] + for i in range(self.x): + row = [] + for j in range(self.y): + plot = self.l.addPlot(title=f'Qubit[{i}{j}]') + row.append(plot) + + + if (i, j) in self.qubits: + plot.plot(*self.fake_data()) + else: + plot.hideAxis('bottom') + plot.hideAxis('left') + plot.hideButtons() + + plot.scene().sigMouseClicked.connect(self.mouse_clicked) + + + + self.plots.append(row) + self.l.nextRow() + print(self.l.childItems()) + + def mouse_clicked(self, event): + # print(f'{event}') + + pass + + def fake_function(self, x): + x0 = (0.5 - np.random.rand()) * 0.8 + return 1-(1 / (1 + ((x - x0) / 0.25)**2)) + + def fake_data(self): + x = np.linspace(-1, 1, 100) + y = self.fake_function(x) + y += np.random.rand(y.size) * 0.2 + return x, y + + def get_plots(self): + return self.plots + + + + + + + +def launch_discriminator_gui(): + # app = pg.mkQApp() + loader = GUI() + pg.exec() + return loader + + +# example with fake data +if __name__ == "__main__": + # proc = Process(target=launch_discriminator_gui, daemon=True) + x = launch_discriminator_gui() + # proc.start()/ diff --git a/niv_plotter/gui.py b/niv_plotter/gui.py new file mode 100644 index 00000000..65050983 --- /dev/null +++ b/niv_plotter/gui.py @@ -0,0 +1,460 @@ +""" +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.qubit_array_shape = (5, 5) + + 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.setup_plots() + 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.plot_grid = pg.GraphicsLayoutWidget() + + + self.dashboard_tab_layout.addWidget(self.dashboard_list, 0, 0, 5, 1) + self.dashboard_tab_layout.addWidget(self.plot_grid, 0, 1) + + self.dashboard_list.itemDoubleClicked.connect(self.switch_to_qubit_tab) + self.dashboard_list.setMaximumWidth(200) + + def setup_plots(self): + + x, y = self.qubit_array_shape + self.plots = [] + + for i in range(x): + row = [] + for j in range(y): + plot = self.plot_grid.addPlot() + row.append(plot) + self.plots.append(row) + self.plot_grid.nextRow() + + self.add_fake_data_to_plots() + + def add_fake_data_to_plots(self): + + for row in self.plots: + for plot in row: + plot_item = pg.PlotDataItem( + *self._fake_data() + ) + plot.clear() + plot.addItem(plot_item) + + def _fake_data(self): + x = np.linspace(-1, 1, 100) + y = self._function(x) + y += np.random.rand(y.size) * 0.2 + + return x, y + + def _function(self, x): + return (1 / (1 + x**2)) + + 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("Niv's special GUI") + + 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/niv_plotter/main.py b/niv_plotter/main.py new file mode 100644 index 00000000..b4156d8a --- /dev/null +++ b/niv_plotter/main.py @@ -0,0 +1,487 @@ +""" +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 GUI(QWidget): + def __init__(self): + """ + 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.qubit_array_shape = (5, 5) + + super(GUI, self).__init__() + self.initialise_ui() + self.setup_plots() + self.setup_list() + # self.setup_dashboard_tab() + # self._populate_list() + # self._list_by_fidelity() + self.show() + + def initialise_ui(self): + + self.main_layout = QGridLayout() + + self.plot_grid = pg.GraphicsLayoutWidget() + self.list_widget = pg.LayoutWidget() + + self.main_layout.addWidget(self.list_widget, 0, 0) + self.main_layout.addWidget(self.plot_grid, 0, 1) + + self.setLayout(self.main_layout) + + + self.setWindowTitle("Niv's special GUI") + + def setup_list(self): + + self.list = QTableWidget() + self.list.setRowCount(10) + self.list.setColumnCount(2) + + # make table read-only + self.list.setEditTriggers(QTableWidget.NoEditTriggers) + self.list.setMinimumWidth(self.list.sizeHint().width()) + self.list.setShowGrid(False) + self.list.setMaximumWidth(200) + + self.list_widget.addWidget(self.list) + + + + def setup_plots(self): + + x, y = self.qubit_array_shape + self.plots = [] + + for i in range(x): + row = [] + for j in range(y): + plot = self.plot_grid.addPlot() + row.append(plot) + self.plots.append(row) + self.plot_grid.nextRow() + + self.add_fake_data_to_plots() + + def add_fake_data_to_plots(self): + + for row in self.plots: + for plot in row: + plot_item = pg.PlotDataItem( + *self._fake_data() + ) + plot.clear() + plot.addItem(plot_item) + + def _fake_data(self): + x = np.linspace(-1, 1, 100) + y = self._function(x) + y += np.random.rand(y.size) * 0.2 + + return x, y + + def _function(self, x): + return (1 / (1 + x**2)) + + 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") + + # 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.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(): + app = pg.mkQApp() + loader = GUI() + pg.exec() + + +# example with fake data +if __name__ == "__main__": + + launch_discriminator_gui() + diff --git a/niv_plotter/mp.py b/niv_plotter/mp.py new file mode 100644 index 00000000..76e546ec --- /dev/null +++ b/niv_plotter/mp.py @@ -0,0 +1,18 @@ +import pyqtgraph as pg +pg.mkQApp() + +# Create remote process with a plot window +import pyqtgraph.multiprocess as mp +proc = mp.QtProcess() +rpg = proc._import('pyqtgraph') +plotwin = rpg.plot() +curve = plotwin.plot(pen='y') + +# create an empty list in the remote process +data = proc.transfer([]) + +# Send new data to the remote process and plot it +# We use the special argument _callSync='off' because we do +# not want to wait for a return value. +data.extend([1,5,2,4,3], _callSync='off') +curve.setData(y=data, _callSync='off') diff --git a/niv_plotter/mp_gui.py b/niv_plotter/mp_gui.py new file mode 100644 index 00000000..338fa670 --- /dev/null +++ b/niv_plotter/mp_gui.py @@ -0,0 +1,51 @@ +import pyqtgraph as pg +import numpy as np + + +pg.mkQApp() + + +def fake_function(x): + x0 = (0.5 - np.random.rand()) * 0.8 + return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) + + +def fake_data(): + x = np.linspace(-1, 1, 100) + y = fake_function(x) + y += np.random.rand(y.size) * 0.2 + return x, y + +x = 5 +y = 5 +qubits = ((1, 1), (2, 2)) + + +# Create remote process with a plot window +import pyqtgraph.multiprocess as mp +proc = mp.QtProcess() +rpg = proc._import('pyqtgraph') + +view = rpg.GraphicsView() +l = rpg.GraphicsLayout(border=(100, 100, 100)) +view.setCentralItem(l) +view.show() + +plots = [] + +for i in range(x): + row = [] + for j in range(y): + plot = l.addPlot(title=f'Qubit[{i}{j}]') + row.append(plot) + + if (i, j) in qubits: + plot.plot(*fake_data()) + else: + plot.hideAxis('bottom') + plot.hideAxis('left') + plot.hideButtons() + + plots.append(row) + l.nextRow() + diff --git a/niv_plotter/test.py b/niv_plotter/test.py new file mode 100644 index 00000000..775441cc --- /dev/null +++ b/niv_plotter/test.py @@ -0,0 +1,8 @@ +from grid_gui_class import GUI +import numpy as np + +x = np.random.rand(400) + +gui = GUI() + +y = np.random.rand(300) \ No newline at end of file diff --git a/niv_plotter/testing.py b/niv_plotter/testing.py new file mode 100644 index 00000000..11b3dbfe --- /dev/null +++ b/niv_plotter/testing.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Exmaple pattern of setting up PyQt to run +under iPython and not block, by default. +Code modified from: +ZetCode PyQt4 tutorial +In this example, we receive data from +a QtGui.QInputDialog dialog. +author: Jan Bodnar +website: zetcode.com +last edited: October 2011 +""" + +import sys +from PyQt5 import QtGui, QtCore + +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * + +class Example(QWidget): + + def __init__(self): + super(Example, self).__init__() + + self.initUI() + + def initUI(self): + + self.btn = QPushButton('Dialog', self) + self.btn.move(20, 20) + self.btn.clicked.connect(self.showDialog) + + self.le = QLineEdit(self) + self.le.move(130, 22) + + self.setGeometry(300, 300, 290, 150) + self.setWindowTitle('Input dialog') + self.show() + + def showDialog(self): + + text, ok = QInputDialog.getText(self, 'Input Dialog', + 'Enter your name:') + + if ok: + self.le.setText(str(text)) + +def start_gui(block=False): + try: + if not block & __IPYTHON__: + from IPython.lib.inputhook import enable_gui + app = enable_gui('qt4') + else: + raise ImportError + except (ImportError, NameError): + app = QtCore.QCoreApplication.instance() + if app is None: + app = QApplication(sys.argv) + + ex = Example() + + try: + from IPython.lib.guisupport import start_event_loop_qt4 + start_event_loop_qt4(app) + return ex + except ImportError: + app.exec_() + +def _main(): + start_gui(block=True) + +if __name__ == '__main__': + _main() \ No newline at end of file diff --git a/niv_plotter/testing_mp.py b/niv_plotter/testing_mp.py new file mode 100644 index 00000000..d9b47ab3 --- /dev/null +++ b/niv_plotter/testing_mp.py @@ -0,0 +1,288 @@ +""" +Created on 16/09/2021 +@author jdh +@author bvs +""" + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.dockarea import * +import sys +from pathlib import Path, PosixPath +from multiprocessing import Process +import pyqtgraph.exporters +from logging import getLogger +import numpy as np +# from qgor.plotters.Plot_Widgets import Dock as CustomDock +from datetime import date +from time import gmtime, strftime + +from .widget import Plot_1D_Widget +from PyQt5.QtGui import QIcon + +import json + + +def load_json(path): + """ + a function to load a json + @param path: the path of the json to load + @return: the json as a dict + """ + with open(path) as f: + return json.load(f) + +def save_json(path, dict): + """ + a function to load a json + @param path: the path of the json to load + @return: the json as a dict + """ + # if the file already exists, load it and add to the dict to be saved + if path.is_file(): + dict = {**load_json(path=path), **dict} + + with open(path, "w") as f: + json.dump(dict, f, indent=4, sort_keys=False) + +logger = getLogger(__name__) + +def get_widget_axis_centres(widget): + centres = np.array( + [widget.x_mm[-1] + widget.x_mm[0], + widget.y_mm[-1] + widget.y_mm[0]] + ) / 2 + + return centres + + +class Plotter_Base: + def __init__( + self, + folder, + axis, + mode="read", + options_path=Path(__file__).parent / "plotter.json", + additional_metadata_dict={} + ): + """ + Initialise the plotter window and generate the first plots + @param folder: The data folder that contains the measurement data + @param axis: The combinations of variables to plot. A list of tuples. Each tuple contains + either two or three elements for either a 1D or 2D plot. + """ + + self.folder = folder + self.axis = axis + self.additional_metadata_dict = additional_metadata_dict + + # duplicate parameter names cause a bug in the plotter. This removes that possibility. + for i, ax in enumerate(axis): + assert_message = ( + "{} contains a duplicate parameter name. Consider renaming it!".format( + ax + ) + ) + # if length of set == length of list then there are no duplicates. + if not set(ax).__len__() == ax.__len__(): + logger.warning(assert_message) + + # loading the json options + self.options_path = options_path + + # loading the plot_options + self.options = load_json(self.options_path) + + self.screenshot_saved = False + + def save(self): + + type_action_mapping = { + PosixPath: lambda x: str(x.resolve()), + } + + do_not_save = ["mm_w", "mm_r", "folder", "options_path", "options", "process", 'processors', 'variables'] + + data = {} + for key, value in self.__dict__.items(): + if key not in do_not_save: + class_hash = type(value) + action = type_action_mapping.get(class_hash, lambda x: x) + data[key] = action.__call__(value) + + # add some other things to the meta data + data['time_of_measurement'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + + save_json(self.folder / "meta_data.json", data) + + def get_data(self): + return { + variable: self.mm_r.__getattribute__(variable) + for variable in self.mm_r.units.keys() + } + + def get_plots_data(self): + return [ + tuple(self.mm_r.__getattribute__(name) for name in ax) for ax in self.axis + ] + + def plot(self): + self.process = Process(target=self._plot, args=(), daemon=True) + logger.debug("plotting process initialised") + self.process.start() + logger.debug("plotter process started") + + def close_plot(self): + self.process.kill() + + def create_buttons_widget(self): + + # create dock for the buttons widget to live in + self.button_dock = Dock("Lab notebook", size=(10, 10), closable=True) + self.area.addDock(self.button_dock, 'right') + + # start with crosshairs off + self.crosshairs_on = 0 + + # widget for buttons to live in + self.buttons_widget = pg.LayoutWidget() + + # create the buttons/gui elements + # self.save_layout = QtGui.QPushButton('save dock state') + # self.restore_layout = QtGui.QPushButton('restore dock state') + + self.crosshairs_button = QtGui.QPushButton('turn on crosshairs') + self.corner_button = QtGui.QPushButton('corner detection') + + self.text_for_saving = QtGui.QTextEdit("id: {}".format(self.folder.stem)) + self.save_to_notebook_button = QtGui.QPushButton('save to lab notebook') + self.saved_indicator = QtGui.QLabel('not saved') + self.file_title = QtGui.QLineEdit('saved file title') + + # grey out restore (will be enabled once the save button has been clicked) + # self.restore_layout.setEnabled(False) + + # position the gui elements + self.buttons_widget.addWidget(self.file_title, row=0, col=0) + self.buttons_widget.addWidget(self.saved_indicator, row=1, col=0) + self.buttons_widget.addWidget(self.save_to_notebook_button, row=2, col=0) + self.buttons_widget.addWidget(self.text_for_saving, row=3, col=0) + self.buttons_widget.addWidget(self.crosshairs_button, row=4, col=0) + + self.button_dock.addWidget(self.buttons_widget) + + # link the buttons to the functions + self.save_to_notebook_button.clicked.connect(self.save_interesting_file) + self.crosshairs_button.clicked.connect(self.toggle_crosshairs) + + def toggle_crosshairs(self): + # toggler (crosshairs on is either 0 or 1) + self.crosshairs_on += 1 + self.crosshairs_on %= 2 + + if self.crosshairs_on: + self.crosshairs() + self.crosshairs_button.setText('turn off crosshairs') + else: + self.remove_crosshairs() + self.crosshairs_button.setText('turn on crosshairs') + + def _plot(self): + # plotting function dictionary to select right function for plotting + # images or line plots + length_plot_mapping = {2: self._create_1d_plot, 3: self._create_2d_plot} + + # open the qtgraph window + self.app = QtGui.QApplication(["qgor {}".format(self.folder.stem)]) + self.win = QtGui.QMainWindow() + + icon_path = str(Path(__file__).parent / self.options.get("icon")) + icon = QIcon(icon_path) + self.win.setWindowIcon(icon) + + self.area = DockArea() + self.win.setCentralWidget(self.area) + + # for saving a png of the image. Needs to be above the creation of the buttons + directory = self.folder.parent / "pngs" + self.image_filename = "{}.png".format(str(directory / self.folder.stem)) + + pg.setConfigOptions( + background=self.options.get("background", "k"), + foreground=self.options.get("foreground", "d"), + ) + + # setting the window size + window_size = self.options.get("window_size") + self.win.resize(*window_size) + self.win.setWindowTitle("{}".format(self.folder.stem)) + + # list for keeping track of widgets so they can be updated + self.widgets = [] + + for plot_axis in self.axis: + # duplicates in plotter cause bugs so make sure this isn't the case. Sometimes it is needed if you want to + # monitor the value of a set parameter, in which case you must wrap the measured parameter with a class that has + # a .get() method that calls the other parameter's .get() + duplicate_assertion_string = ( + "duplicate name in plotted/measured parameters. If you need to monitor a set " + "parameter please wrap one of the duplicate parameters." + ) + + assert ( + set(plot_axis).__len__() == plot_axis.__len__() + ), duplicate_assertion_string + + # select the plot function based on the length of the data + plot_function = length_plot_mapping.get(plot_axis.__len__()) + plot_function.__call__(*plot_axis) + + self.create_buttons_widget() + self.win.show() + + self._register_update() + if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"): + QtGui.QApplication.instance().exec_() + + # self.save_png() + + + + + def _create_1d_plot(self, x, y): + + # get data + x_mm = self.mm_r.__getattribute__(x) + y_mm = self.mm_r.__getattribute__(y) + + + + assert x_mm.size == y_mm.size, "x_mm.size != y_mm.size -- {} != {}".format( + x_mm.size, y_mm.size + ) + + axis = { + "x": {"name": x, "unit": self.mm_r.units.get(x)}, + "y": {"name": y, "unit": self.mm_r.units.get(y)}, + } + + # create the plot and dock it lives in + plot_1d_options = self.options.get("plot_1d_options") + widget = Plot_1D_Widget(x_mm=x_mm, y_mm=y_mm, axis=axis, **plot_1d_options) + self.widgets.append(widget) + + # add the dock and plot to the window area + self.area.addDock(widget.dock) + + + def _update(self): + for widget in self.widgets: + widget.update() + + def _register_update(self): + self.timer = pg.QtCore.QTimer() + self.timer.timeout.connect(self._update) + + update_time = self.options.get("update_time") + self.timer.start(update_time) # how often the self.update is called [ms] + diff --git a/niv_plotter/widget.py b/niv_plotter/widget.py new file mode 100644 index 00000000..019ad395 --- /dev/null +++ b/niv_plotter/widget.py @@ -0,0 +1,83 @@ +""" +Created on 17/09/2021 +@author barnaby +""" +import numpy as np +import pyqtgraph as pg +from .Custom_Dock import Dock + + +class Plot_1D_Widget: + def __init__( + self, + x_mm, + y_mm, + axis: dict = {"x": {"name": "x", "unit": ""}, "y": {"name": "y", "unit": ""}}, + line_colour: tuple = (0, 0, 0), + downsampling_mode: str = "peak", + clip_to_view: bool = True, + anti_aliasing: bool = True, + closeable: bool = False, + background_colour: str = "black", + size: tuple = (500, 500), + ): + self.x_mm, self.y_mm = x_mm, y_mm + + x_axis = axis.get("x") + y_axis = axis.get("y") + + # create a dock for the plot to live in + self.dock = Dock(y_axis.get("name"), size=tuple(size), closable=closeable) + # the dock can carry the variable name :) + plot = pg.PlotWidget(title="") + + # setting the axis + plot.setLabel("bottom", x_axis.get("name", "x"), x_axis.get("unit", "")) + plot.setLabel("left", y_axis.get("name", "y"), y_axis.get("unit", "")) + + # setting the performance options + plot.setDownsampling(mode=downsampling_mode) + plot.setClipToView(clip_to_view) + plot.setAntialiasing(anti_aliasing) + + # finding the args where nan values don't exist + args = np.logical_and( + np.logical_not(np.isnan(x_mm)), np.logical_not(np.isnan(y_mm)) + ) + + # drawing the line + self.line_plot = plot.plot(x_mm[args], y_mm[args], pen=tuple(line_colour)) + self.dock.addWidget(plot) + + self.init_copies() + + self.finished = False + + def init_copies(self): + self.x = np.full_like(self.x_mm, fill_value=np.nan) + self.y = np.full_like(self.y_mm, fill_value=np.nan) + + def create_copies(self): + # creating copies of the memory maps + self.x = self.x_mm.__array__().copy() + self.y = self.y_mm.__array__().copy() + + def update(self): + + data_changed = False + for mm, copy in zip([self.x_mm, self.y_mm], [self.x, self.y]): + if not np.array_equal(mm, copy): + data_changed = True + + if data_changed: + # finding the args where nan values don't exist + args = np.logical_and( + np.logical_not(np.isnan(self.x_mm)), np.logical_not(np.isnan(self.y_mm)) + ) + + self.line_plot.setData(self.x_mm[args], self.y_mm[args]) + self.create_copies() + + if not self.finished: + if not np.isnan(np.sum(self.y_mm)): + self.finished = True \ No newline at end of file From b1187210efbf5dd005c6b046ab6d7bea3d5e36fb Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Mon, 27 Mar 2023 14:47:08 +0300 Subject: [PATCH 20/25] plotter --- niv_plotter/example.py | 23 +++++++++ niv_plotter/grid_gui_class.py | 94 +++++++++++++++++++++++++++-------- niv_plotter/interacive.py | 40 +++++++++++++++ niv_plotter/test.py | 12 ++--- 4 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 niv_plotter/example.py create mode 100644 niv_plotter/interacive.py diff --git a/niv_plotter/example.py b/niv_plotter/example.py new file mode 100644 index 00000000..f2b386ae --- /dev/null +++ b/niv_plotter/example.py @@ -0,0 +1,23 @@ +from grid_gui_class import GUI +import numpy as np + +def fake_function(x): + x0 = (0.5 - np.random.rand()) * 0.8 + return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) + + +def fake_data(): + x = np.linspace(-1, 1, 100) + y = fake_function(x) + y += np.random.rand(y.size) * 0.2 + return x, y + + +# run something +qubit_arrangement = (4, 5) +qubits = ((0, 0), (1, 1), (2, 2), (2, 3), (3, 2), (3, 3), (3, 4)) +data = {qubit: fake_data() for qubit in qubits} + + + +gui = GUI(qubit_arrangement, data, ipython=True) diff --git a/niv_plotter/grid_gui_class.py b/niv_plotter/grid_gui_class.py index 728af359..2f3cd6df 100644 --- a/niv_plotter/grid_gui_class.py +++ b/niv_plotter/grid_gui_class.py @@ -17,14 +17,13 @@ def __init__(self, **kwargs): # self.scene() is a pyqtgraph.GraphicsScene.GraphicsScene.GraphicsScene self.scene().sigMouseClicked.connect(self.mouse_clicked) - def mouse_clicked(self, mouseClickEvent): # mouseClickEvent is a pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent print('clicked plot 0x{:x}, event: {}'.format(id(self), mouseClickEvent)) class GUI(QWidget): - def __init__(self): + def __init__(self, array_shape, data_dict, ipython=False): """ GUI for presenting per-qubit readout information as well as a general overview dashboard which contains some information. More to be added later. @@ -34,36 +33,95 @@ def __init__(self): self.app = pg.mkQApp() - self.qubit_array_shape = (3,3) + # self.qubit_array_shape = (8, 8) + self.qubit_array_shape = array_shape self.x, self.y = self.qubit_array_shape - self.qubits = ((0, 0), (0, 1), (1, 1), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)) + # self.qubits = ((0, 0), (0, 1), (1, 1), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)) + self.data = data_dict + self.qubits = tuple(data_dict.keys()) super(GUI, self).__init__() + self.initialise_ui() - self.setup_plots() + self.setup_plot_items() + self.set_plot_data() - # pg.exec() + # self.setup_plots() + if not ipython: + pg.exec() + + if ipython: + from IPython import get_ipython + ipy = get_ipython() + ipy.run_line_magic('gui', 'qt5') def initialise_ui(self): self.view = pg.GraphicsView() - self.l = pg.GraphicsLayout(border=(100, 100, 100)) - self.view.setCentralItem(self.l) + self.plot_layout = pg.GraphicsLayout(border=(100, 100, 100)) + self.view.setCentralItem(self.plot_layout) self.view.show() - self.view.setWindowTitle("Niv's special viewer") + self.view.setWindowTitle("Quantum Machines qubit viewer") self.view.resize(800, 600) - def setup_plots(self): + def update_plot(self, index, data): + + plot = self.get_plot(index) + + plot.clear() + plot.plot(*data) + + plot.showAxis('bottom') + plot.showAxis('left') + plot.showButtons() + + def set_plot_to_text(self, index, text): + plot = self.get_plot(index) + plot.clear() + + + def update_plot_title(self, index, new_title): + + plot = self.get_plot(index) + plot.setTitle(new_title) + + def set_text(self, index, text): + self.plot_layout.removeItem(self.plot_layout.getItem(*index)) + self.plot_layout.addLabel(text, *index) + + def get_plot(self, index): + i, j = index + return self.plots[i][j] + + def setup_plot_items(self): + self.plots = [] + for i in range(self.x): row = [] for j in range(self.y): - plot = self.l.addPlot(title=f'Qubit[{i}{j}]') + plot = self.plot_layout.addPlot(title=f'qubit [{i}{j}]') row.append(plot) + plot.hideAxis('bottom') + plot.hideAxis('left') + plot.hideButtons() + self.plots.append(row) + self.plot_layout.nextRow() + + def set_plot_data(self): + for index, data in self.data.items(): + self.update_plot(index, data) + def setup_plots(self): + self.plots = [] + for i in range(self.x): + row = [] + for j in range(self.y): + plot = self.plot_layout.addPlot(title=f'Qubit[{i}{j}]') + row.append(plot) if (i, j) in self.qubits: - plot.plot(*self.fake_data()) + plot.plot(*self.data[(i, j)]) else: plot.hideAxis('bottom') plot.hideAxis('left') @@ -71,11 +129,8 @@ def setup_plots(self): plot.scene().sigMouseClicked.connect(self.mouse_clicked) - - self.plots.append(row) - self.l.nextRow() - print(self.l.childItems()) + self.plot_layout.nextRow() def mouse_clicked(self, event): # print(f'{event}') @@ -84,7 +139,7 @@ def mouse_clicked(self, event): def fake_function(self, x): x0 = (0.5 - np.random.rand()) * 0.8 - return 1-(1 / (1 + ((x - x0) / 0.25)**2)) + return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) def fake_data(self): x = np.linspace(-1, 1, 100) @@ -96,11 +151,6 @@ def get_plots(self): return self.plots - - - - - def launch_discriminator_gui(): # app = pg.mkQApp() loader = GUI() diff --git a/niv_plotter/interacive.py b/niv_plotter/interacive.py new file mode 100644 index 00000000..0a7dd378 --- /dev/null +++ b/niv_plotter/interacive.py @@ -0,0 +1,40 @@ +import sys +from PyQt5 import QtWidgets, QtGui, QtCore + +class MainWindow(QtWidgets.QWidget): + def __init__(self): + # call super class constructor + super(MainWindow, self).__init__() + # build the objects one by one + layout = QtWidgets.QVBoxLayout(self) + self.pb_load = QtWidgets.QPushButton('Load') + self.pb_clear= QtWidgets.QPushButton('Clear') + self.edit = QtWidgets.QTextEdit() + layout.addWidget(self.edit) + layout.addWidget(self.pb_load) + layout.addWidget(self.pb_clear) + # connect the callbacks to the push-buttons + self.pb_load.clicked.connect(self.callback_pb_load) + self.pb_clear.clicked.connect(self.callback_pb_clear) + + def callback_pb_load(self): + self.edit.append('hello world') + def callback_pb_clear(self): + self.edit.clear() + +def create_window(window_class): + """Create a Qt window in Python, or interactively in IPython with Qt GUI + event loop integration. + """ + app_created = False + app = QtCore.QCoreApplication.instance() + if app is None: + app = QtWidgets.QApplication(sys.argv) + app_created = True + app.references = set() + window = window_class() + app.references.add(window) + window.show() + if app_created: + app.exec_() + return window diff --git a/niv_plotter/test.py b/niv_plotter/test.py index 775441cc..1ad55c41 100644 --- a/niv_plotter/test.py +++ b/niv_plotter/test.py @@ -1,8 +1,4 @@ -from grid_gui_class import GUI -import numpy as np - -x = np.random.rand(400) - -gui = GUI() - -y = np.random.rand(300) \ No newline at end of file +# from grid_gui_class import GUI +# from interacive import MainWindow +# +# from PyQt5 import QtWidgets \ No newline at end of file From 31945c2614655994fc34c6c31c6fa44551f37ec6 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Tue, 28 Mar 2023 16:33:34 +0300 Subject: [PATCH 21/25] interactive plotter working --- niv_plotter/gui/__init__.py | 0 niv_plotter/gui/grid_gui.py | 0 niv_plotter/gui/widget.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 niv_plotter/gui/__init__.py create mode 100644 niv_plotter/gui/grid_gui.py create mode 100644 niv_plotter/gui/widget.py diff --git a/niv_plotter/gui/__init__.py b/niv_plotter/gui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/niv_plotter/gui/grid_gui.py b/niv_plotter/gui/grid_gui.py new file mode 100644 index 00000000..e69de29b diff --git a/niv_plotter/gui/widget.py b/niv_plotter/gui/widget.py new file mode 100644 index 00000000..e69de29b From 95ea792dcc85c0e00509d3bd8b6b72e35b57f106 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Tue, 28 Mar 2023 19:02:11 +0300 Subject: [PATCH 22/25] --- niv_plotter/example.py | 3 +- niv_plotter/grid_gui_class.py | 91 ++++++------------ niv_plotter/gui/grid_gui.py | 117 +++++++++++++++++++++++ niv_plotter/gui/widget.py | 172 ++++++++++++++++++++++++++++++++++ niv_plotter/testing.py | 97 +++++++------------ 5 files changed, 353 insertions(+), 127 deletions(-) diff --git a/niv_plotter/example.py b/niv_plotter/example.py index f2b386ae..625f0fcb 100644 --- a/niv_plotter/example.py +++ b/niv_plotter/example.py @@ -10,7 +10,7 @@ def fake_data(): x = np.linspace(-1, 1, 100) y = fake_function(x) y += np.random.rand(y.size) * 0.2 - return x, y + return x * 1e6, y # run something @@ -19,5 +19,4 @@ def fake_data(): data = {qubit: fake_data() for qubit in qubits} - gui = GUI(qubit_arrangement, data, ipython=True) diff --git a/niv_plotter/grid_gui_class.py b/niv_plotter/grid_gui_class.py index 2f3cd6df..416b921e 100644 --- a/niv_plotter/grid_gui_class.py +++ b/niv_plotter/grid_gui_class.py @@ -9,13 +9,13 @@ from multiprocessing import Process -class MyPlotWidget(pg.PlotWidget): +class PlotWidget(pg.GraphicsWidget): def __init__(self, **kwargs): super().__init__(**kwargs) - # self.scene() is a pyqtgraph.GraphicsScene.GraphicsScene.GraphicsScene - self.scene().sigMouseClicked.connect(self.mouse_clicked) + def mouseDoubleClickEvent(self, QGraphicsSceneMouseEvent): + print('test') def mouse_clicked(self, mouseClickEvent): # mouseClickEvent is a pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent @@ -43,8 +43,8 @@ def __init__(self, array_shape, data_dict, ipython=False): super(GUI, self).__init__() self.initialise_ui() - self.setup_plot_items() - self.set_plot_data() + self.setup_plot_layouts() + # self.set_plot_data() # self.setup_plots() @@ -66,7 +66,7 @@ def initialise_ui(self): def update_plot(self, index, data): - plot = self.get_plot(index) + plot = self.get_layout(index) plot.clear() plot.plot(*data) @@ -76,90 +76,57 @@ def update_plot(self, index, data): plot.showButtons() def set_plot_to_text(self, index, text): - plot = self.get_plot(index) - plot.clear() + plot = self.get_layout(index) + + plot.addItem(pg.TextItem('99.9')) + # + # plot.clear() + # + # plot.addItem(PlotWidget()) + + def set_plot_custom(self, index): + box = self.plot_layout.getViewBox(index) + return box def update_plot_title(self, index, new_title): - plot = self.get_plot(index) + plot = self.get_layout(index) plot.setTitle(new_title) def set_text(self, index, text): self.plot_layout.removeItem(self.plot_layout.getItem(*index)) self.plot_layout.addLabel(text, *index) - def get_plot(self, index): + def get_layout(self, index): i, j = index - return self.plots[i][j] + return self.plot_layouts[i][j] - def setup_plot_items(self): + def setup_plot_layouts(self): - self.plots = [] + self.plot_layouts = [] for i in range(self.x): row = [] for j in range(self.y): - plot = self.plot_layout.addPlot(title=f'qubit [{i}{j}]') + # plot = self.plot_layout.addPlot(title=f'qubit [{i}{j}]') + plot = self.plot_layout.addLayout(rowspan=1, colspan=1) row.append(plot) - plot.hideAxis('bottom') - plot.hideAxis('left') - plot.hideButtons() - self.plots.append(row) + # plot.hideAxis('bottom') + # plot.hideAxis('left') + # plot.hideButtons() + self.plot_layouts.append(row) self.plot_layout.nextRow() def set_plot_data(self): for index, data in self.data.items(): self.update_plot(index, data) - def setup_plots(self): - self.plots = [] - for i in range(self.x): - row = [] - for j in range(self.y): - plot = self.plot_layout.addPlot(title=f'Qubit[{i}{j}]') - row.append(plot) - - if (i, j) in self.qubits: - plot.plot(*self.data[(i, j)]) - else: - plot.hideAxis('bottom') - plot.hideAxis('left') - plot.hideButtons() - - plot.scene().sigMouseClicked.connect(self.mouse_clicked) - - self.plots.append(row) - self.plot_layout.nextRow() - def mouse_clicked(self, event): # print(f'{event}') pass - def fake_function(self, x): - x0 = (0.5 - np.random.rand()) * 0.8 - return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) - - def fake_data(self): - x = np.linspace(-1, 1, 100) - y = self.fake_function(x) - y += np.random.rand(y.size) * 0.2 - return x, y - def get_plots(self): - return self.plots - - -def launch_discriminator_gui(): - # app = pg.mkQApp() - loader = GUI() - pg.exec() - return loader - + return self.plot_layouts -# example with fake data -if __name__ == "__main__": - # proc = Process(target=launch_discriminator_gui, daemon=True) - x = launch_discriminator_gui() - # proc.start()/ diff --git a/niv_plotter/gui/grid_gui.py b/niv_plotter/gui/grid_gui.py index e69de29b..2d308a75 100644 --- a/niv_plotter/gui/grid_gui.py +++ b/niv_plotter/gui/grid_gui.py @@ -0,0 +1,117 @@ +""" +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 + +from widget import ViewerWidget + + +class MainWidget(QWidget): + + def __init__(self, grid_size, ipython=True): + self.app = pg.mkQApp() + + super(MainWidget, 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.setWindowTitle( + 'Quantum Machines' + ) + + 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.hidden and hasattr(widget, 'plot_item'): + widget.toggle_hide() + + + def _set_all_to_fidelity(self): + for row in self.widgets: + for widget in row: + if not widget.hidden and hasattr(widget, 'plot_item'): + widget.toggle_hide() + def _reset_views(self): + for row in self.widgets: + for widget in row: + widget.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 = [] + for j in range(self.grid_size[1]): + widget = ViewerWidget(name=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 set_widget_data(self, index, data, dimensionality): + widget = self.get_widget(index) + widget.set_data(data, dimensionality=dimensionality) + + +if __name__ == '__main__': + grid_size = (3, 5) + + gui = MainWidget(grid_size, ipython=True) + + qubits = ((0, 0), (0, 2), (1, 2), (2, 2)) + qubit_data = {qubit: np.random.rand(100, 2) for qubit in qubits} + + for qubit, data in qubit_data.items(): + gui.set_widget_data(qubit, data, dimensionality=1) diff --git a/niv_plotter/gui/widget.py b/niv_plotter/gui/widget.py index e69de29b..96da6c1f 100644 --- a/niv_plotter/gui/widget.py +++ b/niv_plotter/gui/widget.py @@ -0,0 +1,172 @@ +import pyqtgraph as pg +import numpy as np +import time + + +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 + + +def find_nearest(array, value): + array = np.asarray(array) + idx = (np.abs(array - value)).argmin() + return idx + + +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.) + + +# if data.shape[1] == 2: +# # self.plot_item = self.plot(data) +# self.dimensionality = 1 +# +# else: +# # self.plot_item = pg.ImageItem(image=data) +# # self.addItem(self.plot_item) +# self.dimensionality = 2 + + +class ViewerWidget(pg.PlotWidget): + + + + def __init__(self, name, parent=None, background='default', plotItem=None, **kargs): + super().__init__(name=name, parent=parent, background=background, plotItem=plotItem, **kargs) + + self.name = name + + 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') + + self.hidden = True + + value = np.random.rand() + color = generate_color(value) + self.text = pg.TextItem(f'{value * 100:.2f}%', anchor=(0.5, 0.5), color=color) + + self.addItem(self.text) + self.text.hide() + + + def set_data(self, data, dimensionality): + + self.showAxis('bottom') + self.showAxis('left') + + self.hidden = False + + if hasattr(self, 'plot_item'): + + if dimensionality == 1: + self.plot_item.setData(data) + + else: + self.plot_item.setImage(data) + + else: + if dimensionality == 1: + self.plot_item = self.plot(data) + else: + self.plot_item = pg.ImageItem(image=data) + self.addItem(self.plot_item) + + def leaveEvent(self, ev): + """Mouse left PlotWidget""" + self.hLine.hide() + self.vLine.hide() + + self.setTitle(self.name) + + def enterEvent(self, ev): + """Mouse enter PlotWidget""" + if not self.hidden: + 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): + + if not self.hidden: + mousePoint = self.plotItem.vb.mapSceneToView(ev.pos()) + print(mousePoint.x()) + print(mousePoint.y()) + + self.toggle_hide() + + def hide_plot(self): + [line.hide() for line in self.lines] + self.hideAxis('bottom') + self.hideAxis('left') + self.hideButtons() + self.setMouseEnabled(False) + + if hasattr(self, 'plot_item'): + self.plot_item.hide() + + + def show_plot(self): + + [line.show() for line in self.lines] + self.showAxis('bottom') + self.showAxis('left') + self.showButtons() + self.setMouseEnabled(True) + + if hasattr(self, 'plot_item'): + self.plot_item.show() + + def show_text(self): + self.text.show() + + def hide_text(self): + self.text.hide() + + def toggle_hide(self): + if self.hidden: + self.show_plot() + self.hide_text() + self.autoRange() + self.setMouseEnabled(x=True, y=True) + + else: + self.hide_plot() + self.show_text() + self.setTitle(self.name) + self.autoRange() + self.setMouseEnabled(x=False, y=False) + + self.hidden = not self.hidden diff --git a/niv_plotter/testing.py b/niv_plotter/testing.py index 11b3dbfe..5e24e6de 100644 --- a/niv_plotter/testing.py +++ b/niv_plotter/testing.py @@ -1,75 +1,46 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Exmaple pattern of setting up PyQt to run -under iPython and not block, by default. -Code modified from: -ZetCode PyQt4 tutorial -In this example, we receive data from -a QtGui.QInputDialog dialog. -author: Jan Bodnar -website: zetcode.com -last edited: October 2011 -""" - import sys -from PyQt5 import QtGui, QtCore - -from PyQt5.QtCore import * -from PyQt5.QtWidgets import * - -class Example(QWidget): - - def __init__(self): - super(Example, self).__init__() - - self.initUI() - - def initUI(self): - self.btn = QPushButton('Dialog', self) - self.btn.move(20, 20) - self.btn.clicked.connect(self.showDialog) +import numpy as np +import pyqtgraph as pg +from PyQt5.QtWidgets import QApplication - self.le = QLineEdit(self) - self.le.move(130, 22) - self.setGeometry(300, 300, 290, 150) - self.setWindowTitle('Input dialog') - self.show() +class CrosshairPlotWidget(pg.PlotWidget): - def showDialog(self): + def __init__(self, parent=None, background='default', plotItem=None, **kargs): + super().__init__(parent=parent, background=background, plotItem=plotItem, **kargs) + 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.hLine.hide() + self.vLine.hide() - text, ok = QInputDialog.getText(self, 'Input Dialog', - 'Enter your name:') + def leaveEvent(self, ev): + """Mouse left PlotWidget""" + self.hLine.hide() + self.vLine.hide() - if ok: - self.le.setText(str(text)) + def enterEvent(self, ev): + """Mouse enter PlotWidget""" + self.hLine.show() + self.vLine.show() -def start_gui(block=False): - try: - if not block & __IPYTHON__: - from IPython.lib.inputhook import enable_gui - app = enable_gui('qt4') - else: - raise ImportError - except (ImportError, NameError): - app = QtCore.QCoreApplication.instance() - if app is None: - app = QApplication(sys.argv) + 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()) - ex = Example() - try: - from IPython.lib.guisupport import start_event_loop_qt4 - start_event_loop_qt4(app) - return ex - except ImportError: - app.exec_() +if __name__ == "__main__": + app = QApplication(sys.argv) -def _main(): - start_gui(block=True) + x = [0, 1, 2, 3, 4, 5] + y = np.random.normal(size=6) + plot = CrosshairPlotWidget() + plot.plot(x, y) + plot.show() -if __name__ == '__main__': - _main() \ No newline at end of file + app.exec() \ No newline at end of file From 23685e776b84b18c66f6bea11456caf7eaf1da56 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 29 Mar 2023 15:17:26 +0300 Subject: [PATCH 23/25] stacked plotter --- niv_plotter/gui/grid_gui.py | 41 ++++--- niv_plotter/gui/widget.py | 113 ++++++++++++++++--- niv_plotter/testing.py | 213 ++++++++++++++++++++++++++++++------ 3 files changed, 298 insertions(+), 69 deletions(-) diff --git a/niv_plotter/gui/grid_gui.py b/niv_plotter/gui/grid_gui.py index 2d308a75..c2eecc10 100644 --- a/niv_plotter/gui/grid_gui.py +++ b/niv_plotter/gui/grid_gui.py @@ -7,8 +7,8 @@ import pyqtgraph as pg import numpy as np -from widget import ViewerWidget - +from widget import ViewerWidgetOld +from widget import StackedWidget class MainWidget(QWidget): @@ -69,19 +69,20 @@ def setup_controls(self): def _set_all_to_data(self): for row in self.widgets: for widget in row: - if widget.hidden and hasattr(widget, 'plot_item'): - widget.toggle_hide() - + # 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.hidden and hasattr(widget, 'plot_item'): - widget.toggle_hide() + # 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.autoRange() + widget.view_widget.autoRange() def setup_qubit_plot_grid(self): self.plot_grid_layout = QGridLayout() @@ -91,9 +92,15 @@ def setup_qubit_plot_grid(self): for i in range(self.grid_size[0]): row = [] + self.plot_grid_layout.setRowStretch(i, 1) for j in range(self.grid_size[1]): - widget = ViewerWidget(name=f'qubit [{i}{j}]') + self.plot_grid_layout.setColumnStretch(j, 1) + # widget = ViewerWidget(name=f'qubit [{i}{j}]') + widget = StackedWidget() self.plot_grid_layout.addWidget(widget, i, j) + # self.plot_grid_layout.addWidget(widget, i, j) + # widget = ViewerWidget(name=f'qubit [{i}{j}]') + # self.plot_grid_layout.addLayout(widget, i, j) row.append(widget) self.widgets.append(row) @@ -102,16 +109,18 @@ def get_widget(self, index): def set_widget_data(self, index, data, dimensionality): widget = self.get_widget(index) - widget.set_data(data, dimensionality=dimensionality) + widget.view_widget.set_data(data, dimensionality=dimensionality) if __name__ == '__main__': - grid_size = (3, 5) + i = 4 + j = 4 + grid_size = (i, j) gui = MainWidget(grid_size, ipython=True) - qubits = ((0, 0), (0, 2), (1, 2), (2, 2)) - qubit_data = {qubit: np.random.rand(100, 2) for qubit in qubits} - - for qubit, data in qubit_data.items(): - gui.set_widget_data(qubit, data, dimensionality=1) + qubits = ((0, 0), (1, 1), (1, 2),(2, 2), (2, 3), (3, 2), (3, 3)) + qubit_data = {qubit: np.random.rand(100, 100) for qubit in qubits} + # + # for qubit, data in qubit_data.items(): + # gui.set_widget_data(qubit, data, dimensionality=2) diff --git a/niv_plotter/gui/widget.py b/niv_plotter/gui/widget.py index 96da6c1f..8fe41286 100644 --- a/niv_plotter/gui/widget.py +++ b/niv_plotter/gui/widget.py @@ -1,8 +1,8 @@ import pyqtgraph as pg import numpy as np import time - - +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * def fake_function(x): x0 = (np.random.rand()) * 0.8 return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) @@ -30,19 +30,7 @@ def generate_color(value): return (255 * (1 - value), 255 * value, 0.) -# if data.shape[1] == 2: -# # self.plot_item = self.plot(data) -# self.dimensionality = 1 -# -# else: -# # self.plot_item = pg.ImageItem(image=data) -# # self.addItem(self.plot_item) -# self.dimensionality = 2 - - -class ViewerWidget(pg.PlotWidget): - - +class ViewerWidgetOld(pg.PlotWidget): def __init__(self, name, parent=None, background='default', plotItem=None, **kargs): super().__init__(name=name, parent=parent, background=background, plotItem=plotItem, **kargs) @@ -71,7 +59,6 @@ def __init__(self, name, parent=None, background='default', plotItem=None, **kar self.addItem(self.text) self.text.hide() - def set_data(self, data, dimensionality): self.showAxis('bottom') @@ -137,7 +124,6 @@ def hide_plot(self): if hasattr(self, 'plot_item'): self.plot_item.hide() - def show_plot(self): [line.show() for line in self.lines] @@ -155,6 +141,15 @@ def show_text(self): def hide_text(self): self.text.hide() + def keyPressEvent(self, ev): + """ + Remove the widget if the delete key is pressed while the widget is in mouse focus + @param ev: key press event, containing the information about the press ∂ + @return: + """ + if ev.key() == 16777223: + self.hide() + def toggle_hide(self): if self.hidden: self.show_plot() @@ -170,3 +165,87 @@ def toggle_hide(self): self.setMouseEnabled(x=False, y=False) self.hidden = not self.hidden + + + +class RadioButtons(QWidget): + def __init__(self): + QWidget.__init__(self) + layout = QGridLayout() + self.setLayout(layout) + + radiobutton = QRadioButton("Australia") + radiobutton.setChecked(True) + radiobutton.country = "Australia" + radiobutton.toggled.connect(self.onClicked) + layout.addWidget(radiobutton, 0, 0) + + radiobutton = QRadioButton("China") + radiobutton.country = "China" + radiobutton.toggled.connect(self.onClicked) + layout.addWidget(radiobutton, 0, 1) + + radiobutton = QRadioButton("Japan") + radiobutton.country = "Japan" + radiobutton.toggled.connect(self.onClicked) + layout.addWidget(radiobutton, 0, 2) + + + + def onClicked(self): + radioButton = self.sender() + if radioButton.isChecked(): + print("Country is %s" % (radioButton.country)) + + + def leaveEvent(self, ev): + """Mouse left PlotWidget""" + self.hide() + def enterEvent(self, ev): + """Mouse enter PlotWidget""" + self.show() + + + + +class StackedWidget(QFrame): + + def __init__(self): + super(StackedWidget, self).__init__() + self.leftlist = QListWidget() + self.leftlist.insertItem(0, 'one') + self.leftlist.insertItem(1, 'two') + self.leftlist.insertItem(2, 'three') + self.leftlist.setMaximumWidth(self.leftlist.minimumSizeHint().width()) + + self.stack1 = ViewerWidgetOld(name='2d measure') + self.stack2 = ViewerWidgetOld(name='1d trace') + self.stack3 = ViewerWidgetOld(name='2d also ') + + self.stack1.set_data(np.random.rand(100, 100), dimensionality=2) + self.stack2.set_data(np.stack([np.linspace(0, 1, 100), np.random.rand(100)]).T, dimensionality=1) + self.stack3.set_data(np.random.rand(100, 100), dimensionality=2) + + # self.stack1UI() + # self.stack2UI() + # self.stack3UI() + + self.Stack = QStackedWidget(self) + self.Stack.addWidget(self.stack1) + self.Stack.addWidget(self.stack2) + self.Stack.addWidget(self.stack3) + + hbox = QHBoxLayout(self) + hbox.addWidget(self.leftlist, stretch=1) + hbox.addWidget(self.Stack) + + self.setLayout(hbox) + self.leftlist.currentRowChanged.connect(self.display) + + self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + self.setLineWidth(1) + self.add_layer() + def add_layer(self):#, name, data): + print(len(self.leftlist)) + def display(self, i): + self.Stack.setCurrentIndex(i) diff --git a/niv_plotter/testing.py b/niv_plotter/testing.py index 5e24e6de..fd1261c5 100644 --- a/niv_plotter/testing.py +++ b/niv_plotter/testing.py @@ -5,42 +5,183 @@ from PyQt5.QtWidgets import QApplication -class CrosshairPlotWidget(pg.PlotWidget): - - def __init__(self, parent=None, background='default', plotItem=None, **kargs): - super().__init__(parent=parent, background=background, plotItem=plotItem, **kargs) - 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.hLine.hide() - self.vLine.hide() - - def leaveEvent(self, ev): - """Mouse left PlotWidget""" - self.hLine.hide() - self.vLine.hide() - - 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 __name__ == "__main__": +# class CrosshairPlotWidget(pg.PlotWidget): +# +# def __init__(self, parent=None, background='default', plotItem=None, **kargs): +# super().__init__(parent=parent, background=background, plotItem=plotItem, **kargs) +# 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.hLine.hide() +# self.vLine.hide() +# +# def leaveEvent(self, ev): +# """Mouse left PlotWidget""" +# self.hLine.hide() +# self.vLine.hide() +# +# 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 __name__ == "__main__": +# app = QApplication(sys.argv) +# +# x = [0, 1, 2, 3, 4, 5] +# y = np.random.normal(size=6) +# plot = CrosshairPlotWidget() +# plot.plot(x, y) +# plot.show() +# +# app.exec() + +import sys +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from gui.widget import ViewerWidgetOld + + +# class stackedExample(QWidget): +# +# def __init__(self): +# super(stackedExample, self).__init__() +# self.leftlist = QListWidget() +# +# self.leftlist.insertItem(0, 'Contact') +# self.leftlist.insertItem(1, 'Personal') +# self.leftlist.insertItem(2, 'Educational') +# +# self.stack1 = QWidget() +# self.stack2 = QWidget() +# self.stack3 = QWidget() +# +# self.stack1UI() +# self.stack2UI() +# self.stack3UI() +# +# self.Stack = QStackedWidget(self) +# self.Stack.addWidget(self.stack1) +# self.Stack.addWidget(self.stack2) +# self.Stack.addWidget(self.stack3) +# +# hbox = QHBoxLayout(self) +# hbox.addWidget(self.leftlist) +# hbox.addWidget(self.Stack) +# +# self.setLayout(hbox) +# self.leftlist.currentRowChanged.connect(self.display) +# self.setGeometry(300, 50, 10, 10) +# self.setWindowTitle('StackedWidget demo') +# self.show() +# +# +# def stack1UI(self): +# layout = QFormLayout() +# layout.addRow("Name", QLineEdit()) +# layout.addRow("Address", QLineEdit()) +# # self.setTabText(0,"Contact Details") +# self.stack1.setLayout(layout) +# +# +# def stack2UI(self): +# layout = QFormLayout() +# sex = QHBoxLayout() +# sex.addWidget(QRadioButton("Male")) +# sex.addWidget(QRadioButton("Female")) +# layout.addRow(QLabel("Sex"), sex) +# layout.addRow("Date of Birth", QLineEdit()) +# +# self.stack2.setLayout(layout) +# +# +# def stack3UI(self): +# layout = QHBoxLayout() +# layout.addWidget(QLabel("subjects")) +# layout.addWidget(QCheckBox("Physics")) +# layout.addWidget(QCheckBox("Maths")) +# self.stack3.setLayout(layout) +# +# +# def display(self, i): +# self.Stack.setCurrentIndex(i) + + +class StackedWidget(QWidget): + + def __init__(self): + super(StackedWidget, self).__init__() + self.leftlist = QListWidget() + self.leftlist.insertItem(0, 'one') + self.leftlist.insertItem(1, 'two') + self.leftlist.insertItem(2, 'three') + self.leftlist.setMaximumWidth(self.leftlist.minimumSizeHint().width()) + + self.stack1 = ViewerWidgetOld(name='2d measure') + self.stack2 = ViewerWidgetOld(name='1d trace') + self.stack3 = ViewerWidgetOld(name='2d also ') + + self.stack1.set_data(np.random.rand(100, 100), dimensionality=2) + self.stack2.set_data(np.stack([np.linspace(0, 1, 100), np.random.rand(100)]).T, dimensionality=1) + self.stack3.set_data(np.random.rand(100, 100), dimensionality=2) + + # self.stack1UI() + # self.stack2UI() + # self.stack3UI() + + self.Stack = QStackedWidget(self) + self.Stack.addWidget(self.stack1) + self.Stack.addWidget(self.stack2) + self.Stack.addWidget(self.stack3) + + hbox = QHBoxLayout(self) + hbox.addWidget(self.leftlist, stretch=1) + hbox.addWidget(self.Stack) + + self.setLayout(hbox) + self.leftlist.currentRowChanged.connect(self.display) + self.setGeometry(40, 30, 400, 300) + self.setWindowTitle('StackedWidget demo') + self.show() + + + def stack1UI(self): + layout = QFormLayout() + self.stack1.setLayout(layout) + + + def stack2UI(self): + pass + + + def stack3UI(self): + layout = QHBoxLayout() + layout.addWidget(QLabel("subjects")) + layout.addWidget(QCheckBox("Physics")) + layout.addWidget(QCheckBox("Maths")) + self.stack3.setLayout(layout) + + + def display(self, i): + self.Stack.setCurrentIndex(i) + + + +def main(): app = QApplication(sys.argv) + ex = StackedWidget() + sys.exit(app.exec_()) - x = [0, 1, 2, 3, 4, 5] - y = np.random.normal(size=6) - plot = CrosshairPlotWidget() - plot.plot(x, y) - plot.show() - app.exec() \ No newline at end of file +if __name__ == '__main__': + main() \ No newline at end of file From 3a999d70cc284395eca4b7b04779cb9a9c09183e Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Wed, 29 Mar 2023 19:00:37 +0300 Subject: [PATCH 24/25] without 2d axes working --- niv_plotter/gui/grid_gui.py | 64 ++++++++-- niv_plotter/gui/widget.py | 241 ++++++++++++++---------------------- niv_plotter/testing.py | 8 +- 3 files changed, 151 insertions(+), 162 deletions(-) diff --git a/niv_plotter/gui/grid_gui.py b/niv_plotter/gui/grid_gui.py index c2eecc10..898b51a8 100644 --- a/niv_plotter/gui/grid_gui.py +++ b/niv_plotter/gui/grid_gui.py @@ -2,12 +2,13 @@ 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 +import time +from threading import Thread -from widget import ViewerWidgetOld from widget import StackedWidget class MainWidget(QWidget): @@ -20,7 +21,7 @@ def __init__(self, grid_size, ipython=True): self.grid_size = grid_size self.initialise_ui() - self.setup_controls() + # self.setup_controls() self.setup_qubit_plot_grid() if ipython: @@ -82,7 +83,7 @@ def _set_all_to_fidelity(self): def _reset_views(self): for row in self.widgets: for widget in row: - widget.view_widget.autoRange() + widget.Stack.currentWidget().autoRange() def setup_qubit_plot_grid(self): self.plot_grid_layout = QGridLayout() @@ -96,7 +97,7 @@ def setup_qubit_plot_grid(self): for j in range(self.grid_size[1]): self.plot_grid_layout.setColumnStretch(j, 1) # widget = ViewerWidget(name=f'qubit [{i}{j}]') - widget = StackedWidget() + widget = StackedWidget(f'qubit [{i}{j}]') self.plot_grid_layout.addWidget(widget, i, j) # self.plot_grid_layout.addWidget(widget, i, j) # widget = ViewerWidget(name=f'qubit [{i}{j}]') @@ -111,16 +112,55 @@ def set_widget_data(self, index, data, dimensionality): widget = self.get_widget(index) widget.view_widget.set_data(data, dimensionality=dimensionality) + def add_layer_to_widget(self, index, name, data, dimensionality): + widget = self.get_widget(index) + widget._add_layer(name, data, dimensionality) + + def update_layer(self, index, layer_name, data): + widget = self.get_widget(index) + widget.update_layer_data(layer_name, data) + + def plot_1d(self, index, name, data): + widget = self.get_widget(index) + widget.add_1d_plot(name, data) + + def plot_2d(self, index, name, x, y, data): + widget = self.get_widget(index) + widget.add_2d_plot(name, x, y, data) + + def plot_0d(self, index, name, value): + widget = self.get_widget(index) + widget.add_0d_plot(name, value) + + def update_data(self): + while True: + self.update_layer((0, 1), 'osc', np.fromfunction(lambda i, j: np.sin(i/8 * np.random.rand())*j/128, (100, 100), dtype=float) + np.random.rand(100, 100)) + time.sleep(0.24) if __name__ == '__main__': - i = 4 - j = 4 + + 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 + + + i = 5 + j = 5 grid_size = (i, j) gui = MainWidget(grid_size, ipython=True) - qubits = ((0, 0), (1, 1), (1, 2),(2, 2), (2, 3), (3, 2), (3, 3)) - qubit_data = {qubit: np.random.rand(100, 100) for qubit in qubits} - # - # for qubit, data in qubit_data.items(): - # gui.set_widget_data(qubit, data, dimensionality=2) + qubits = ((0, 0), (0, 1), (1, 0), (1, 1)) + qubit_data = {qubit: np.fromfunction(lambda i, j: np.sin(i/8 * np.random.rand())*j/128, (100, 100), dtype=float) + np.random.rand(100, 100) for qubit in qubits} + + for qubit, data in qubit_data.items(): + gui.plot_0d(qubit, 'Fidelity', f'{np.random.rand() * 100:.2f}%') + gui.plot_2d(qubit, 'osc', data) + gui.plot_1d(qubit, 'spect', np.stack(fake_data()).T) diff --git a/niv_plotter/gui/widget.py b/niv_plotter/gui/widget.py index 8fe41286..848d3c08 100644 --- a/niv_plotter/gui/widget.py +++ b/niv_plotter/gui/widget.py @@ -3,6 +3,9 @@ import time from PyQt5.QtCore import * from PyQt5.QtWidgets import * +from PyQt5 import QtGui + + def fake_function(x): x0 = (np.random.rand()) * 0.8 return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) @@ -30,13 +33,29 @@ def generate_color(value): return (255 * (1 - value), 255 * value, 0.) -class ViewerWidgetOld(pg.PlotWidget): +class TextWidget(pg.PlotWidget): - def __init__(self, name, parent=None, background='default', plotItem=None, **kargs): + def __init__(self, name, value, parent=None, background='default', plotItem=None, **kargs): super().__init__(name=name, parent=parent, background=background, plotItem=plotItem, **kargs) - self.name = name + 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) + +class ViewerWidget(pg.PlotWidget): + + def __init__(self, name, dimensionality, parent=None, background='default', plotItem=None, **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) @@ -50,35 +69,41 @@ def __init__(self, name, parent=None, background='default', plotItem=None, **kar self.hideAxis('bottom') self.hideAxis('left') - self.hidden = True - - value = np.random.rand() - color = generate_color(value) - self.text = pg.TextItem(f'{value * 100:.2f}%', anchor=(0.5, 0.5), color=color) - - self.addItem(self.text) - self.text.hide() - - def set_data(self, data, dimensionality): + def set_data(self, data, x=None, y=None): self.showAxis('bottom') self.showAxis('left') - self.hidden = False - if hasattr(self, 'plot_item'): - if dimensionality == 1: + if self.dimensionality == 1: self.plot_item.setData(data) - else: - self.plot_item.setImage(data) + elif self.dimensionality == 2: + 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(data.shape) + + self.plot_item.setImage(data, pos=pos, scale=scale) else: - if dimensionality == 1: + if self.dimensionality == 1: self.plot_item = self.plot(data) - else: - self.plot_item = pg.ImageItem(image=data) + elif self.dimensionality == 2: + + 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(data.shape) + + self.plot_item = pg.ImageItem(image=data, pos=pos, scale=scale) self.addItem(self.plot_item) def leaveEvent(self, ev): @@ -90,9 +115,8 @@ def leaveEvent(self, ev): def enterEvent(self, ev): """Mouse enter PlotWidget""" - if not self.hidden: - self.hLine.show() - self.vLine.show() + self.hLine.show() + self.vLine.show() def mouseMoveEvent(self, ev): """Mouse moved in PlotWidget""" @@ -102,150 +126,75 @@ def mouseMoveEvent(self, ev): self.vLine.setPos(mousePoint.x()) self.hLine.setPos(mousePoint.y()) - if not self.hidden: - self.setTitle(f'{mousePoint.x():.2f}, {mousePoint.y():.2f}') + # if not self.hidden: + self.setTitle(f'{mousePoint.x():.2f}, {mousePoint.y():.2f}') def mouseDoubleClickEvent(self, ev): + mouse_point = self.plotItem.vb.mapSceneToView(ev.pos()) + print(f'x: {mouse_point.x()}, y: {mouse_point.y()}') - if not self.hidden: - mousePoint = self.plotItem.vb.mapSceneToView(ev.pos()) - print(mousePoint.x()) - print(mousePoint.y()) - - self.toggle_hide() - - def hide_plot(self): - [line.hide() for line in self.lines] - self.hideAxis('bottom') - self.hideAxis('left') - self.hideButtons() - self.setMouseEnabled(False) - - if hasattr(self, 'plot_item'): - self.plot_item.hide() - - def show_plot(self): - - [line.show() for line in self.lines] - self.showAxis('bottom') - self.showAxis('left') - self.showButtons() - self.setMouseEnabled(True) - if hasattr(self, 'plot_item'): - self.plot_item.show() - - def show_text(self): - self.text.show() - - def hide_text(self): - self.text.hide() - - def keyPressEvent(self, ev): - """ - Remove the widget if the delete key is pressed while the widget is in mouse focus - @param ev: key press event, containing the information about the press ∂ - @return: - """ - if ev.key() == 16777223: - self.hide() - - def toggle_hide(self): - if self.hidden: - self.show_plot() - self.hide_text() - self.autoRange() - self.setMouseEnabled(x=True, y=True) - - else: - self.hide_plot() - self.show_text() - self.setTitle(self.name) - self.autoRange() - self.setMouseEnabled(x=False, y=False) +class StackedWidget(QFrame): - self.hidden = not self.hidden + def __init__(self, qubit_name): + super(StackedWidget, self).__init__() + self.name = qubit_name + self.layer_list = QListWidget() + self.layer_list.setMaximumWidth(int(self.layer_list.sizeHintForColumn(0) * 1.2)) -class RadioButtons(QWidget): - def __init__(self): - QWidget.__init__(self) - layout = QGridLayout() - self.setLayout(layout) + self.Stack = QStackedWidget(self) + hbox = QHBoxLayout(self) + hbox.addWidget(self.layer_list, stretch=1) + hbox.addWidget(self.Stack) - radiobutton = QRadioButton("Australia") - radiobutton.setChecked(True) - radiobutton.country = "Australia" - radiobutton.toggled.connect(self.onClicked) - layout.addWidget(radiobutton, 0, 0) + self.setLayout(hbox) + self.layer_list.currentRowChanged.connect(self.display) - radiobutton = QRadioButton("China") - radiobutton.country = "China" - radiobutton.toggled.connect(self.onClicked) - layout.addWidget(radiobutton, 0, 1) + self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + self.setLineWidth(1) + # + # self.layer_list.model().rowsInserted.connect(self.display_latest) - radiobutton = QRadioButton("Japan") - radiobutton.country = "Japan" - radiobutton.toggled.connect(self.onClicked) - layout.addWidget(radiobutton, 0, 2) + def add_0d_plot(self, name, text): + self._add_layer(name, text, 0) + def add_1d_plot(self, name, data): + self._add_layer(name, data, 1) - def onClicked(self): - radioButton = self.sender() - if radioButton.isChecked(): - print("Country is %s" % (radioButton.country)) + def add_2d_plot(self, name, data): + self._add_layer(name, data, 2) + def _add_layer(self, name, data, dimensionality): - def leaveEvent(self, ev): - """Mouse left PlotWidget""" - self.hide() - def enterEvent(self, ev): - """Mouse enter PlotWidget""" - self.show() + assert name not in self._get_layer_names(), 'Cannot use duplicate names for layers' + self.layer_list.insertItem(len(self.layer_list), name) + if dimensionality == 0: + widget = TextWidget(self.name, data) + else: + widget = ViewerWidget(name=self.name, dimensionality=dimensionality) + widget.set_data(data) + self.Stack.addWidget(widget) + # resize each time we add a new member to the list + self.layer_list.setMaximumWidth(int(self.layer_list.sizeHintForColumn(0) * 1.2)) -class StackedWidget(QFrame): - - def __init__(self): - super(StackedWidget, self).__init__() - self.leftlist = QListWidget() - self.leftlist.insertItem(0, 'one') - self.leftlist.insertItem(1, 'two') - self.leftlist.insertItem(2, 'three') - self.leftlist.setMaximumWidth(self.leftlist.minimumSizeHint().width()) - - self.stack1 = ViewerWidgetOld(name='2d measure') - self.stack2 = ViewerWidgetOld(name='1d trace') - self.stack3 = ViewerWidgetOld(name='2d also ') - - self.stack1.set_data(np.random.rand(100, 100), dimensionality=2) - self.stack2.set_data(np.stack([np.linspace(0, 1, 100), np.random.rand(100)]).T, dimensionality=1) - self.stack3.set_data(np.random.rand(100, 100), dimensionality=2) + def display(self, i): + self.Stack.setCurrentIndex(i) + def update_layer_data(self, layer_name, data): - # self.stack1UI() - # self.stack2UI() - # self.stack3UI() + widget = self.get_layer(layer_name) + widget.set_data(data) - self.Stack = QStackedWidget(self) - self.Stack.addWidget(self.stack1) - self.Stack.addWidget(self.stack2) - self.Stack.addWidget(self.stack3) + def get_layer(self, name): + index = self._get_layer_names().index(name) + return self.Stack.widget(index) - hbox = QHBoxLayout(self) - hbox.addWidget(self.leftlist, stretch=1) - hbox.addWidget(self.Stack) + def _get_layer_names(self): + return [self.layer_list.item(x).text() for x in range(self.layer_list.count())] - self.setLayout(hbox) - self.leftlist.currentRowChanged.connect(self.display) - self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) - self.setLineWidth(1) - self.add_layer() - def add_layer(self):#, name, data): - print(len(self.leftlist)) - def display(self, i): - self.Stack.setCurrentIndex(i) diff --git a/niv_plotter/testing.py b/niv_plotter/testing.py index fd1261c5..2de194d0 100644 --- a/niv_plotter/testing.py +++ b/niv_plotter/testing.py @@ -49,7 +49,7 @@ from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * -from gui.widget import ViewerWidgetOld +from gui.widget import ViewerWidget # class stackedExample(QWidget): @@ -127,9 +127,9 @@ def __init__(self): self.leftlist.insertItem(2, 'three') self.leftlist.setMaximumWidth(self.leftlist.minimumSizeHint().width()) - self.stack1 = ViewerWidgetOld(name='2d measure') - self.stack2 = ViewerWidgetOld(name='1d trace') - self.stack3 = ViewerWidgetOld(name='2d also ') + self.stack1 = ViewerWidget(name='2d measure') + self.stack2 = ViewerWidget(name='1d trace') + self.stack3 = ViewerWidget(name='2d also ') self.stack1.set_data(np.random.rand(100, 100), dimensionality=2) self.stack2.set_data(np.stack([np.linspace(0, 1, 100), np.random.rand(100)]).T, dimensionality=1) From 8bff4153681cb95605d33ecc62e45106c54a08d3 Mon Sep 17 00:00:00 2001 From: joseph hickie Date: Thu, 30 Mar 2023 18:47:06 +0300 Subject: [PATCH 25/25] added readme to plotter, cleaned up i have not added a colormap to the plots whcih may be the next thing to do --- .DS_Store | Bin 0 -> 6148 bytes niv_plotter/__init__.py | 0 niv_plotter/example.py | 22 - niv_plotter/grid_gui.py | 73 --- niv_plotter/grid_gui_class.py | 132 ----- niv_plotter/gui.py | 460 ----------------- niv_plotter/gui/__init__.py | 0 niv_plotter/gui/widget.py | 200 ------- niv_plotter/interacive.py | 40 -- niv_plotter/main.py | 487 ------------------ niv_plotter/mp.py | 18 - niv_plotter/mp_gui.py | 51 -- niv_plotter/test.py | 4 - niv_plotter/testing.py | 187 ------- niv_plotter/testing_mp.py | 288 ----------- niv_plotter/widget.py | 83 --- qualang_tools/plot/example.py | 48 ++ qualang_tools/plot/gui/README.md | 70 +++ qualang_tools/plot/gui/__init__.py | 1 + .../plot}/gui/grid_gui.py | 83 +-- qualang_tools/plot/gui/image.png | Bin 0 -> 527100 bytes qualang_tools/plot/gui/widget.py | 234 +++++++++ 22 files changed, 370 insertions(+), 2111 deletions(-) create mode 100644 .DS_Store delete mode 100644 niv_plotter/__init__.py delete mode 100644 niv_plotter/example.py delete mode 100644 niv_plotter/grid_gui.py delete mode 100644 niv_plotter/grid_gui_class.py delete mode 100644 niv_plotter/gui.py delete mode 100644 niv_plotter/gui/__init__.py delete mode 100644 niv_plotter/gui/widget.py delete mode 100644 niv_plotter/interacive.py delete mode 100644 niv_plotter/main.py delete mode 100644 niv_plotter/mp.py delete mode 100644 niv_plotter/mp_gui.py delete mode 100644 niv_plotter/test.py delete mode 100644 niv_plotter/testing.py delete mode 100644 niv_plotter/testing_mp.py delete mode 100644 niv_plotter/widget.py create mode 100644 qualang_tools/plot/example.py create mode 100644 qualang_tools/plot/gui/README.md create mode 100644 qualang_tools/plot/gui/__init__.py rename {niv_plotter => qualang_tools/plot}/gui/grid_gui.py (54%) create mode 100644 qualang_tools/plot/gui/image.png create mode 100644 qualang_tools/plot/gui/widget.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2a5b54bc0f084f6b6f16fcee86467ce2c53d09a8 GIT binary patch literal 6148 zcmeHK!AiqG5S?wSO({YT3VK`cTCmkB7B3;zA26Z^mDtdt!I&*=+8jzDcl{xM#P4xt zcPp0KgBOu9Q)b`n>`azo?{64pqd^#}5F_5hK^%?JM&m;iilvRsvQu`d&b|MnGC%F6 zPa(5FYJ(YQCob*OI*Eb7J#i<0f zz3H^sZlBiW@kwh|m(%uPqb^%VC$m|_+1Wohy9}P=mqfi9P6ht`v}{?N!y6ha%A5GZ zBv#2IM$EJ4S&Ym8Gr$b2IRp0CbE<1zCzr", 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/niv_plotter/gui/__init__.py b/niv_plotter/gui/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/niv_plotter/gui/widget.py b/niv_plotter/gui/widget.py deleted file mode 100644 index 848d3c08..00000000 --- a/niv_plotter/gui/widget.py +++ /dev/null @@ -1,200 +0,0 @@ -import pyqtgraph as pg -import numpy as np -import time -from PyQt5.QtCore import * -from PyQt5.QtWidgets import * -from PyQt5 import QtGui - - -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 - - -def find_nearest(array, value): - array = np.asarray(array) - idx = (np.abs(array - value)).argmin() - return idx - - -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.) - - -class TextWidget(pg.PlotWidget): - - def __init__(self, name, value, parent=None, background='default', plotItem=None, **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) - - -class ViewerWidget(pg.PlotWidget): - - def __init__(self, name, dimensionality, parent=None, background='default', plotItem=None, **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, data, x=None, y=None): - - self.showAxis('bottom') - self.showAxis('left') - - if hasattr(self, 'plot_item'): - - if self.dimensionality == 1: - self.plot_item.setData(data) - - elif self.dimensionality == 2: - 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(data.shape) - - self.plot_item.setImage(data, pos=pos, scale=scale) - - else: - if self.dimensionality == 1: - self.plot_item = self.plot(data) - elif self.dimensionality == 2: - - 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(data.shape) - - self.plot_item = pg.ImageItem(image=data, pos=pos, scale=scale) - self.addItem(self.plot_item) - - 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): - 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): - 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 add_0d_plot(self, name, text): - self._add_layer(name, text, 0) - - def add_1d_plot(self, name, data): - self._add_layer(name, data, 1) - - def add_2d_plot(self, name, data): - self._add_layer(name, data, 2) - - def _add_layer(self, name, data, dimensionality): - - assert name not in self._get_layer_names(), 'Cannot use duplicate names for layers' - - self.layer_list.insertItem(len(self.layer_list), name) - - if dimensionality == 0: - widget = TextWidget(self.name, data) - else: - widget = ViewerWidget(name=self.name, dimensionality=dimensionality) - widget.set_data(data) - self.Stack.addWidget(widget) - - # 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, data): - - widget = self.get_layer(layer_name) - widget.set_data(data) - - 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())] - - diff --git a/niv_plotter/interacive.py b/niv_plotter/interacive.py deleted file mode 100644 index 0a7dd378..00000000 --- a/niv_plotter/interacive.py +++ /dev/null @@ -1,40 +0,0 @@ -import sys -from PyQt5 import QtWidgets, QtGui, QtCore - -class MainWindow(QtWidgets.QWidget): - def __init__(self): - # call super class constructor - super(MainWindow, self).__init__() - # build the objects one by one - layout = QtWidgets.QVBoxLayout(self) - self.pb_load = QtWidgets.QPushButton('Load') - self.pb_clear= QtWidgets.QPushButton('Clear') - self.edit = QtWidgets.QTextEdit() - layout.addWidget(self.edit) - layout.addWidget(self.pb_load) - layout.addWidget(self.pb_clear) - # connect the callbacks to the push-buttons - self.pb_load.clicked.connect(self.callback_pb_load) - self.pb_clear.clicked.connect(self.callback_pb_clear) - - def callback_pb_load(self): - self.edit.append('hello world') - def callback_pb_clear(self): - self.edit.clear() - -def create_window(window_class): - """Create a Qt window in Python, or interactively in IPython with Qt GUI - event loop integration. - """ - app_created = False - app = QtCore.QCoreApplication.instance() - if app is None: - app = QtWidgets.QApplication(sys.argv) - app_created = True - app.references = set() - window = window_class() - app.references.add(window) - window.show() - if app_created: - app.exec_() - return window diff --git a/niv_plotter/main.py b/niv_plotter/main.py deleted file mode 100644 index b4156d8a..00000000 --- a/niv_plotter/main.py +++ /dev/null @@ -1,487 +0,0 @@ -""" -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 GUI(QWidget): - def __init__(self): - """ - 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.qubit_array_shape = (5, 5) - - super(GUI, self).__init__() - self.initialise_ui() - self.setup_plots() - self.setup_list() - # self.setup_dashboard_tab() - # self._populate_list() - # self._list_by_fidelity() - self.show() - - def initialise_ui(self): - - self.main_layout = QGridLayout() - - self.plot_grid = pg.GraphicsLayoutWidget() - self.list_widget = pg.LayoutWidget() - - self.main_layout.addWidget(self.list_widget, 0, 0) - self.main_layout.addWidget(self.plot_grid, 0, 1) - - self.setLayout(self.main_layout) - - - self.setWindowTitle("Niv's special GUI") - - def setup_list(self): - - self.list = QTableWidget() - self.list.setRowCount(10) - self.list.setColumnCount(2) - - # make table read-only - self.list.setEditTriggers(QTableWidget.NoEditTriggers) - self.list.setMinimumWidth(self.list.sizeHint().width()) - self.list.setShowGrid(False) - self.list.setMaximumWidth(200) - - self.list_widget.addWidget(self.list) - - - - def setup_plots(self): - - x, y = self.qubit_array_shape - self.plots = [] - - for i in range(x): - row = [] - for j in range(y): - plot = self.plot_grid.addPlot() - row.append(plot) - self.plots.append(row) - self.plot_grid.nextRow() - - self.add_fake_data_to_plots() - - def add_fake_data_to_plots(self): - - for row in self.plots: - for plot in row: - plot_item = pg.PlotDataItem( - *self._fake_data() - ) - plot.clear() - plot.addItem(plot_item) - - def _fake_data(self): - x = np.linspace(-1, 1, 100) - y = self._function(x) - y += np.random.rand(y.size) * 0.2 - - return x, y - - def _function(self, x): - return (1 / (1 + x**2)) - - 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") - - # 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.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(): - app = pg.mkQApp() - loader = GUI() - pg.exec() - - -# example with fake data -if __name__ == "__main__": - - launch_discriminator_gui() - diff --git a/niv_plotter/mp.py b/niv_plotter/mp.py deleted file mode 100644 index 76e546ec..00000000 --- a/niv_plotter/mp.py +++ /dev/null @@ -1,18 +0,0 @@ -import pyqtgraph as pg -pg.mkQApp() - -# Create remote process with a plot window -import pyqtgraph.multiprocess as mp -proc = mp.QtProcess() -rpg = proc._import('pyqtgraph') -plotwin = rpg.plot() -curve = plotwin.plot(pen='y') - -# create an empty list in the remote process -data = proc.transfer([]) - -# Send new data to the remote process and plot it -# We use the special argument _callSync='off' because we do -# not want to wait for a return value. -data.extend([1,5,2,4,3], _callSync='off') -curve.setData(y=data, _callSync='off') diff --git a/niv_plotter/mp_gui.py b/niv_plotter/mp_gui.py deleted file mode 100644 index 338fa670..00000000 --- a/niv_plotter/mp_gui.py +++ /dev/null @@ -1,51 +0,0 @@ -import pyqtgraph as pg -import numpy as np - - -pg.mkQApp() - - -def fake_function(x): - x0 = (0.5 - np.random.rand()) * 0.8 - return 1 - (1 / (1 + ((x - x0) / 0.25) ** 2)) - - -def fake_data(): - x = np.linspace(-1, 1, 100) - y = fake_function(x) - y += np.random.rand(y.size) * 0.2 - return x, y - -x = 5 -y = 5 -qubits = ((1, 1), (2, 2)) - - -# Create remote process with a plot window -import pyqtgraph.multiprocess as mp -proc = mp.QtProcess() -rpg = proc._import('pyqtgraph') - -view = rpg.GraphicsView() -l = rpg.GraphicsLayout(border=(100, 100, 100)) -view.setCentralItem(l) -view.show() - -plots = [] - -for i in range(x): - row = [] - for j in range(y): - plot = l.addPlot(title=f'Qubit[{i}{j}]') - row.append(plot) - - if (i, j) in qubits: - plot.plot(*fake_data()) - else: - plot.hideAxis('bottom') - plot.hideAxis('left') - plot.hideButtons() - - plots.append(row) - l.nextRow() - diff --git a/niv_plotter/test.py b/niv_plotter/test.py deleted file mode 100644 index 1ad55c41..00000000 --- a/niv_plotter/test.py +++ /dev/null @@ -1,4 +0,0 @@ -# from grid_gui_class import GUI -# from interacive import MainWindow -# -# from PyQt5 import QtWidgets \ No newline at end of file diff --git a/niv_plotter/testing.py b/niv_plotter/testing.py deleted file mode 100644 index 2de194d0..00000000 --- a/niv_plotter/testing.py +++ /dev/null @@ -1,187 +0,0 @@ -import sys - -import numpy as np -import pyqtgraph as pg -from PyQt5.QtWidgets import QApplication - - -# class CrosshairPlotWidget(pg.PlotWidget): -# -# def __init__(self, parent=None, background='default', plotItem=None, **kargs): -# super().__init__(parent=parent, background=background, plotItem=plotItem, **kargs) -# 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.hLine.hide() -# self.vLine.hide() -# -# def leaveEvent(self, ev): -# """Mouse left PlotWidget""" -# self.hLine.hide() -# self.vLine.hide() -# -# 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 __name__ == "__main__": -# app = QApplication(sys.argv) -# -# x = [0, 1, 2, 3, 4, 5] -# y = np.random.normal(size=6) -# plot = CrosshairPlotWidget() -# plot.plot(x, y) -# plot.show() -# -# app.exec() - -import sys -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from gui.widget import ViewerWidget - - -# class stackedExample(QWidget): -# -# def __init__(self): -# super(stackedExample, self).__init__() -# self.leftlist = QListWidget() -# -# self.leftlist.insertItem(0, 'Contact') -# self.leftlist.insertItem(1, 'Personal') -# self.leftlist.insertItem(2, 'Educational') -# -# self.stack1 = QWidget() -# self.stack2 = QWidget() -# self.stack3 = QWidget() -# -# self.stack1UI() -# self.stack2UI() -# self.stack3UI() -# -# self.Stack = QStackedWidget(self) -# self.Stack.addWidget(self.stack1) -# self.Stack.addWidget(self.stack2) -# self.Stack.addWidget(self.stack3) -# -# hbox = QHBoxLayout(self) -# hbox.addWidget(self.leftlist) -# hbox.addWidget(self.Stack) -# -# self.setLayout(hbox) -# self.leftlist.currentRowChanged.connect(self.display) -# self.setGeometry(300, 50, 10, 10) -# self.setWindowTitle('StackedWidget demo') -# self.show() -# -# -# def stack1UI(self): -# layout = QFormLayout() -# layout.addRow("Name", QLineEdit()) -# layout.addRow("Address", QLineEdit()) -# # self.setTabText(0,"Contact Details") -# self.stack1.setLayout(layout) -# -# -# def stack2UI(self): -# layout = QFormLayout() -# sex = QHBoxLayout() -# sex.addWidget(QRadioButton("Male")) -# sex.addWidget(QRadioButton("Female")) -# layout.addRow(QLabel("Sex"), sex) -# layout.addRow("Date of Birth", QLineEdit()) -# -# self.stack2.setLayout(layout) -# -# -# def stack3UI(self): -# layout = QHBoxLayout() -# layout.addWidget(QLabel("subjects")) -# layout.addWidget(QCheckBox("Physics")) -# layout.addWidget(QCheckBox("Maths")) -# self.stack3.setLayout(layout) -# -# -# def display(self, i): -# self.Stack.setCurrentIndex(i) - - -class StackedWidget(QWidget): - - def __init__(self): - super(StackedWidget, self).__init__() - self.leftlist = QListWidget() - self.leftlist.insertItem(0, 'one') - self.leftlist.insertItem(1, 'two') - self.leftlist.insertItem(2, 'three') - self.leftlist.setMaximumWidth(self.leftlist.minimumSizeHint().width()) - - self.stack1 = ViewerWidget(name='2d measure') - self.stack2 = ViewerWidget(name='1d trace') - self.stack3 = ViewerWidget(name='2d also ') - - self.stack1.set_data(np.random.rand(100, 100), dimensionality=2) - self.stack2.set_data(np.stack([np.linspace(0, 1, 100), np.random.rand(100)]).T, dimensionality=1) - self.stack3.set_data(np.random.rand(100, 100), dimensionality=2) - - # self.stack1UI() - # self.stack2UI() - # self.stack3UI() - - self.Stack = QStackedWidget(self) - self.Stack.addWidget(self.stack1) - self.Stack.addWidget(self.stack2) - self.Stack.addWidget(self.stack3) - - hbox = QHBoxLayout(self) - hbox.addWidget(self.leftlist, stretch=1) - hbox.addWidget(self.Stack) - - self.setLayout(hbox) - self.leftlist.currentRowChanged.connect(self.display) - self.setGeometry(40, 30, 400, 300) - self.setWindowTitle('StackedWidget demo') - self.show() - - - def stack1UI(self): - layout = QFormLayout() - self.stack1.setLayout(layout) - - - def stack2UI(self): - pass - - - def stack3UI(self): - layout = QHBoxLayout() - layout.addWidget(QLabel("subjects")) - layout.addWidget(QCheckBox("Physics")) - layout.addWidget(QCheckBox("Maths")) - self.stack3.setLayout(layout) - - - def display(self, i): - self.Stack.setCurrentIndex(i) - - - -def main(): - app = QApplication(sys.argv) - ex = StackedWidget() - sys.exit(app.exec_()) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/niv_plotter/testing_mp.py b/niv_plotter/testing_mp.py deleted file mode 100644 index d9b47ab3..00000000 --- a/niv_plotter/testing_mp.py +++ /dev/null @@ -1,288 +0,0 @@ -""" -Created on 16/09/2021 -@author jdh -@author bvs -""" - -import pyqtgraph as pg -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.dockarea import * -import sys -from pathlib import Path, PosixPath -from multiprocessing import Process -import pyqtgraph.exporters -from logging import getLogger -import numpy as np -# from qgor.plotters.Plot_Widgets import Dock as CustomDock -from datetime import date -from time import gmtime, strftime - -from .widget import Plot_1D_Widget -from PyQt5.QtGui import QIcon - -import json - - -def load_json(path): - """ - a function to load a json - @param path: the path of the json to load - @return: the json as a dict - """ - with open(path) as f: - return json.load(f) - -def save_json(path, dict): - """ - a function to load a json - @param path: the path of the json to load - @return: the json as a dict - """ - # if the file already exists, load it and add to the dict to be saved - if path.is_file(): - dict = {**load_json(path=path), **dict} - - with open(path, "w") as f: - json.dump(dict, f, indent=4, sort_keys=False) - -logger = getLogger(__name__) - -def get_widget_axis_centres(widget): - centres = np.array( - [widget.x_mm[-1] + widget.x_mm[0], - widget.y_mm[-1] + widget.y_mm[0]] - ) / 2 - - return centres - - -class Plotter_Base: - def __init__( - self, - folder, - axis, - mode="read", - options_path=Path(__file__).parent / "plotter.json", - additional_metadata_dict={} - ): - """ - Initialise the plotter window and generate the first plots - @param folder: The data folder that contains the measurement data - @param axis: The combinations of variables to plot. A list of tuples. Each tuple contains - either two or three elements for either a 1D or 2D plot. - """ - - self.folder = folder - self.axis = axis - self.additional_metadata_dict = additional_metadata_dict - - # duplicate parameter names cause a bug in the plotter. This removes that possibility. - for i, ax in enumerate(axis): - assert_message = ( - "{} contains a duplicate parameter name. Consider renaming it!".format( - ax - ) - ) - # if length of set == length of list then there are no duplicates. - if not set(ax).__len__() == ax.__len__(): - logger.warning(assert_message) - - # loading the json options - self.options_path = options_path - - # loading the plot_options - self.options = load_json(self.options_path) - - self.screenshot_saved = False - - def save(self): - - type_action_mapping = { - PosixPath: lambda x: str(x.resolve()), - } - - do_not_save = ["mm_w", "mm_r", "folder", "options_path", "options", "process", 'processors', 'variables'] - - data = {} - for key, value in self.__dict__.items(): - if key not in do_not_save: - class_hash = type(value) - action = type_action_mapping.get(class_hash, lambda x: x) - data[key] = action.__call__(value) - - # add some other things to the meta data - data['time_of_measurement'] = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) - - save_json(self.folder / "meta_data.json", data) - - def get_data(self): - return { - variable: self.mm_r.__getattribute__(variable) - for variable in self.mm_r.units.keys() - } - - def get_plots_data(self): - return [ - tuple(self.mm_r.__getattribute__(name) for name in ax) for ax in self.axis - ] - - def plot(self): - self.process = Process(target=self._plot, args=(), daemon=True) - logger.debug("plotting process initialised") - self.process.start() - logger.debug("plotter process started") - - def close_plot(self): - self.process.kill() - - def create_buttons_widget(self): - - # create dock for the buttons widget to live in - self.button_dock = Dock("Lab notebook", size=(10, 10), closable=True) - self.area.addDock(self.button_dock, 'right') - - # start with crosshairs off - self.crosshairs_on = 0 - - # widget for buttons to live in - self.buttons_widget = pg.LayoutWidget() - - # create the buttons/gui elements - # self.save_layout = QtGui.QPushButton('save dock state') - # self.restore_layout = QtGui.QPushButton('restore dock state') - - self.crosshairs_button = QtGui.QPushButton('turn on crosshairs') - self.corner_button = QtGui.QPushButton('corner detection') - - self.text_for_saving = QtGui.QTextEdit("id: {}".format(self.folder.stem)) - self.save_to_notebook_button = QtGui.QPushButton('save to lab notebook') - self.saved_indicator = QtGui.QLabel('not saved') - self.file_title = QtGui.QLineEdit('saved file title') - - # grey out restore (will be enabled once the save button has been clicked) - # self.restore_layout.setEnabled(False) - - # position the gui elements - self.buttons_widget.addWidget(self.file_title, row=0, col=0) - self.buttons_widget.addWidget(self.saved_indicator, row=1, col=0) - self.buttons_widget.addWidget(self.save_to_notebook_button, row=2, col=0) - self.buttons_widget.addWidget(self.text_for_saving, row=3, col=0) - self.buttons_widget.addWidget(self.crosshairs_button, row=4, col=0) - - self.button_dock.addWidget(self.buttons_widget) - - # link the buttons to the functions - self.save_to_notebook_button.clicked.connect(self.save_interesting_file) - self.crosshairs_button.clicked.connect(self.toggle_crosshairs) - - def toggle_crosshairs(self): - # toggler (crosshairs on is either 0 or 1) - self.crosshairs_on += 1 - self.crosshairs_on %= 2 - - if self.crosshairs_on: - self.crosshairs() - self.crosshairs_button.setText('turn off crosshairs') - else: - self.remove_crosshairs() - self.crosshairs_button.setText('turn on crosshairs') - - def _plot(self): - # plotting function dictionary to select right function for plotting - # images or line plots - length_plot_mapping = {2: self._create_1d_plot, 3: self._create_2d_plot} - - # open the qtgraph window - self.app = QtGui.QApplication(["qgor {}".format(self.folder.stem)]) - self.win = QtGui.QMainWindow() - - icon_path = str(Path(__file__).parent / self.options.get("icon")) - icon = QIcon(icon_path) - self.win.setWindowIcon(icon) - - self.area = DockArea() - self.win.setCentralWidget(self.area) - - # for saving a png of the image. Needs to be above the creation of the buttons - directory = self.folder.parent / "pngs" - self.image_filename = "{}.png".format(str(directory / self.folder.stem)) - - pg.setConfigOptions( - background=self.options.get("background", "k"), - foreground=self.options.get("foreground", "d"), - ) - - # setting the window size - window_size = self.options.get("window_size") - self.win.resize(*window_size) - self.win.setWindowTitle("{}".format(self.folder.stem)) - - # list for keeping track of widgets so they can be updated - self.widgets = [] - - for plot_axis in self.axis: - # duplicates in plotter cause bugs so make sure this isn't the case. Sometimes it is needed if you want to - # monitor the value of a set parameter, in which case you must wrap the measured parameter with a class that has - # a .get() method that calls the other parameter's .get() - duplicate_assertion_string = ( - "duplicate name in plotted/measured parameters. If you need to monitor a set " - "parameter please wrap one of the duplicate parameters." - ) - - assert ( - set(plot_axis).__len__() == plot_axis.__len__() - ), duplicate_assertion_string - - # select the plot function based on the length of the data - plot_function = length_plot_mapping.get(plot_axis.__len__()) - plot_function.__call__(*plot_axis) - - self.create_buttons_widget() - self.win.show() - - self._register_update() - if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"): - QtGui.QApplication.instance().exec_() - - # self.save_png() - - - - - def _create_1d_plot(self, x, y): - - # get data - x_mm = self.mm_r.__getattribute__(x) - y_mm = self.mm_r.__getattribute__(y) - - - - assert x_mm.size == y_mm.size, "x_mm.size != y_mm.size -- {} != {}".format( - x_mm.size, y_mm.size - ) - - axis = { - "x": {"name": x, "unit": self.mm_r.units.get(x)}, - "y": {"name": y, "unit": self.mm_r.units.get(y)}, - } - - # create the plot and dock it lives in - plot_1d_options = self.options.get("plot_1d_options") - widget = Plot_1D_Widget(x_mm=x_mm, y_mm=y_mm, axis=axis, **plot_1d_options) - self.widgets.append(widget) - - # add the dock and plot to the window area - self.area.addDock(widget.dock) - - - def _update(self): - for widget in self.widgets: - widget.update() - - def _register_update(self): - self.timer = pg.QtCore.QTimer() - self.timer.timeout.connect(self._update) - - update_time = self.options.get("update_time") - self.timer.start(update_time) # how often the self.update is called [ms] - diff --git a/niv_plotter/widget.py b/niv_plotter/widget.py deleted file mode 100644 index 019ad395..00000000 --- a/niv_plotter/widget.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Created on 17/09/2021 -@author barnaby -""" -import numpy as np -import pyqtgraph as pg -from .Custom_Dock import Dock - - -class Plot_1D_Widget: - def __init__( - self, - x_mm, - y_mm, - axis: dict = {"x": {"name": "x", "unit": ""}, "y": {"name": "y", "unit": ""}}, - line_colour: tuple = (0, 0, 0), - downsampling_mode: str = "peak", - clip_to_view: bool = True, - anti_aliasing: bool = True, - closeable: bool = False, - background_colour: str = "black", - size: tuple = (500, 500), - ): - self.x_mm, self.y_mm = x_mm, y_mm - - x_axis = axis.get("x") - y_axis = axis.get("y") - - # create a dock for the plot to live in - self.dock = Dock(y_axis.get("name"), size=tuple(size), closable=closeable) - # the dock can carry the variable name :) - plot = pg.PlotWidget(title="") - - # setting the axis - plot.setLabel("bottom", x_axis.get("name", "x"), x_axis.get("unit", "")) - plot.setLabel("left", y_axis.get("name", "y"), y_axis.get("unit", "")) - - # setting the performance options - plot.setDownsampling(mode=downsampling_mode) - plot.setClipToView(clip_to_view) - plot.setAntialiasing(anti_aliasing) - - # finding the args where nan values don't exist - args = np.logical_and( - np.logical_not(np.isnan(x_mm)), np.logical_not(np.isnan(y_mm)) - ) - - # drawing the line - self.line_plot = plot.plot(x_mm[args], y_mm[args], pen=tuple(line_colour)) - self.dock.addWidget(plot) - - self.init_copies() - - self.finished = False - - def init_copies(self): - self.x = np.full_like(self.x_mm, fill_value=np.nan) - self.y = np.full_like(self.y_mm, fill_value=np.nan) - - def create_copies(self): - # creating copies of the memory maps - self.x = self.x_mm.__array__().copy() - self.y = self.y_mm.__array__().copy() - - def update(self): - - data_changed = False - for mm, copy in zip([self.x_mm, self.y_mm], [self.x, self.y]): - if not np.array_equal(mm, copy): - data_changed = True - - if data_changed: - # finding the args where nan values don't exist - args = np.logical_and( - np.logical_not(np.isnan(self.x_mm)), np.logical_not(np.isnan(self.y_mm)) - ) - - self.line_plot.setData(self.x_mm[args], self.y_mm[args]) - self.create_copies() - - if not self.finished: - if not np.isnan(np.sum(self.y_mm)): - self.finished = True \ No newline at end of file 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/niv_plotter/gui/grid_gui.py b/qualang_tools/plot/gui/grid_gui.py similarity index 54% rename from niv_plotter/gui/grid_gui.py rename to qualang_tools/plot/gui/grid_gui.py index 898b51a8..523dfb15 100644 --- a/niv_plotter/gui/grid_gui.py +++ b/qualang_tools/plot/gui/grid_gui.py @@ -7,16 +7,15 @@ import pyqtgraph as pg import numpy as np import time -from threading import Thread -from widget import StackedWidget +from .widget import StackedWidget -class MainWidget(QWidget): +class GUI(QWidget): def __init__(self, grid_size, ipython=True): self.app = pg.mkQApp() - super(MainWidget, self).__init__() + super(GUI, self).__init__() self.grid_size = grid_size @@ -26,18 +25,14 @@ def __init__(self, grid_size, ipython=True): if ipython: from IPython import get_ipython + ipy = get_ipython() - ipy.run_line_magic('gui', 'qt5') + ipy.run_line_magic("gui", "qt5") else: pg.exec() def initialise_ui(self): - - self.setWindowTitle( - 'Quantum Machines' - ) - self.main_window = QMainWindow() self.main_window.resize(1000, 800) self.setAutoFillBackground(True) @@ -46,7 +41,7 @@ def initialise_ui(self): self.main_layout = QVBoxLayout() self.setLayout(self.main_layout) - self.main_window.setWindowTitle('Quantum Machines qubit viewer') + self.main_window.setWindowTitle("Quantum Machines qubit viewer") self.main_window.show() @@ -55,12 +50,12 @@ def setup_controls(self): self.control_layout = QHBoxLayout() self.main_layout.addLayout(self.control_layout) - self.reset_views_button = QPushButton('Reset views') + 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.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) @@ -80,6 +75,7 @@ def _set_all_to_fidelity(self): # 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: @@ -96,71 +92,26 @@ def setup_qubit_plot_grid(self): self.plot_grid_layout.setRowStretch(i, 1) for j in range(self.grid_size[1]): self.plot_grid_layout.setColumnStretch(j, 1) - # widget = ViewerWidget(name=f'qubit [{i}{j}]') - widget = StackedWidget(f'qubit [{i}{j}]') + widget = StackedWidget(f"qubit [{i}{j}]") self.plot_grid_layout.addWidget(widget, i, j) - # self.plot_grid_layout.addWidget(widget, i, j) - # widget = ViewerWidget(name=f'qubit [{i}{j}]') - # self.plot_grid_layout.addLayout(widget, i, j) row.append(widget) self.widgets.append(row) def get_widget(self, index): return self.widgets[index[0]][index[1]] - def set_widget_data(self, index, data, dimensionality): + def update_layer(self, index, layer_name, x, y=None, z=None): widget = self.get_widget(index) - widget.view_widget.set_data(data, dimensionality=dimensionality) + widget.update_layer_data(layer_name, x, y, z) - def add_layer_to_widget(self, index, name, data, dimensionality): + def plot_1d(self, index, name, x, y): widget = self.get_widget(index) - widget._add_layer(name, data, dimensionality) + widget.add_1d_plot(name, x, y) - def update_layer(self, index, layer_name, data): + def plot_2d(self, index, name, x, y, z): widget = self.get_widget(index) - widget.update_layer_data(layer_name, data) - - def plot_1d(self, index, name, data): - widget = self.get_widget(index) - widget.add_1d_plot(name, data) - - def plot_2d(self, index, name, x, y, data): - widget = self.get_widget(index) - widget.add_2d_plot(name, x, y, data) + 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) - - def update_data(self): - while True: - self.update_layer((0, 1), 'osc', np.fromfunction(lambda i, j: np.sin(i/8 * np.random.rand())*j/128, (100, 100), dtype=float) + np.random.rand(100, 100)) - time.sleep(0.24) - -if __name__ == '__main__': - - 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 - - - i = 5 - j = 5 - grid_size = (i, j) - - gui = MainWidget(grid_size, ipython=True) - - qubits = ((0, 0), (0, 1), (1, 0), (1, 1)) - qubit_data = {qubit: np.fromfunction(lambda i, j: np.sin(i/8 * np.random.rand())*j/128, (100, 100), dtype=float) + np.random.rand(100, 100) for qubit in qubits} - - for qubit, data in qubit_data.items(): - gui.plot_0d(qubit, 'Fidelity', f'{np.random.rand() * 100:.2f}%') - gui.plot_2d(qubit, 'osc', data) - gui.plot_1d(qubit, 'spect', np.stack(fake_data()).T) diff --git a/qualang_tools/plot/gui/image.png b/qualang_tools/plot/gui/image.png new file mode 100644 index 0000000000000000000000000000000000000000..b8b01fdfca0931ed47d981e2b3295e292d548298 GIT binary patch literal 527100 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

7J)^f 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

sfmAMN2j# zxF``>w8j83Nlpz^MTQiA8=MGKt7b4U`gKwJHnEIh3G;ls@QKXFz_R#C7ij>z}QKj5b!$9y_-Yi@lKa1^5HKSD5fvKo6 znu^}#w*2N2@$?oQq#{K&L<)F)tHBp7-_pYWZ{tIrvGQOv5=E8iF85QTw@2VB?#*O! zg5NxtId+XSC%y0p${(yuH|gI-4~+|IIIT(QUL#ZQ5dYn+!Ri->cQG%Qs+)XnsY^VO zEBIuvSXt4%A-0UpyFf4<7(jF8d8gxDf@yY8rFK5*0MK3?0_~9oiqOm58-$;hiZ~OZ#(k91^DR6m?y_|j&}pWVnCa()jJ1o6#1j!$E27MOyU2i zWl+8%Da#s|&LyV+?%~#0&K?9v<Y*ek1;m25MyY%nCuX`z{~U>LD5NUT*)13IFO2nD7*^ zI4LNcJQS4`@5UGp6p9V5{LiM$t!c~YlBwPvVepQfcd_-UW3PLi9;$bB<^#NC>sxF@ z^g&mHf%o9W8!#>vGJCO)yIF0Q`dc2=JwJF#m=@YVzkliM?~UGSn%e?TCgg9>70??R zM$`D;h^D!u%wV)%ci;UNynJHo`jTyG`jYR|?h7}Gim}|^)p4P4%;2%0AYI>12FDv@ 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

bRc_N@Tz9hy1>p9VTT_GuT-aj1X040!nr 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%0A@9T3Stig@9E_I?7jYYF|U#-*E{mqN5M52tRh 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

XkFLI(uVd`xhv{RvqxKgDJDTF0iwHje5+ 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 literal 0 HcmV?d00001 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())]