q1simulator 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ __version__ = "1.0.1"
2
+
3
+ from .q1simulator import Q1Simulator
4
+ from .cluster import Cluster, ClusterModule
5
+ from .q1plotter import Q1Plotter
6
+ from .q1viewer import plot_q1asm_file, plot_q1asm_files, PlotDef
7
+
8
+ from .gui.program_browser import Q1ProgramBrowser
@@ -0,0 +1,56 @@
1
+ import os
2
+
3
+ import numpy as np
4
+ import xarray as xr
5
+
6
+
7
+ class AnalogueFilter:
8
+ warned = False
9
+
10
+ def __init__(self, model_name: str, output_frequency: float = 4e9):
11
+ self.output_frequency = output_frequency
12
+
13
+ if not AnalogueFilter.warned:
14
+ print(f"WARNING: Analogue output of simulated {model_name} can differ "
15
+ "significantly from real hardware!")
16
+ AnalogueFilter.warned = True
17
+
18
+ pulse_response_dir = os.path.dirname(__file__)
19
+ if model_name == "QCM":
20
+ fname = pulse_response_dir + "/Qblox_QCM_pulse_response.hdf5"
21
+ amplitude_correction = 1.003
22
+ self.min_vstep = 5.0/2**16 # 0.076 mV
23
+ else:
24
+ raise Exception(f"Uknown model_name {model_name}")
25
+
26
+ pulse_response = xr.open_dataset(fname, engine='h5netcdf')
27
+ t_response = pulse_response.coords['t'].data
28
+ self.pulse_response = pulse_response['y'].data * amplitude_correction
29
+ sr = round(1/(t_response[1]-t_response[0]))
30
+ sub_sample = int(round(sr / (output_frequency*1e-9)))
31
+ self.sr = sr // sub_sample
32
+ self.pulse_response = self.pulse_response[::sub_sample]
33
+
34
+ self.n_before = round(-t_response[0] * self.sr)
35
+ self.n_after = len(self.pulse_response) - self.n_before - 1
36
+
37
+ def quantize_amplitude(self, wave):
38
+ return np.round(wave/self.min_vstep) * self.min_vstep
39
+
40
+ def get_awg_output(self, t, samples, analogue_shift: float | None = 0.0):
41
+ samples = self.quantize_amplitude(samples)
42
+ if t[1] - t[0] != 1:
43
+ raise Exception(f"Expecting input with 1 ns sample period. Got {t[1]-t[0]}")
44
+ t = np.linspace(t[0], t[-1]+1, len(t)*self.sr, endpoint=False)
45
+ d = np.zeros(len(samples)*self.sr)
46
+ d[::self.sr] = samples
47
+
48
+ d = np.convolve(d, self.pulse_response)
49
+ return t+analogue_shift, d[self.n_before: -self.n_after]
50
+
51
+ def apply_filter(self, samples):
52
+ """Apply filter to samples without quantizing or shifting time."""
53
+ d = np.zeros(len(samples)*self.sr)
54
+ d[::self.sr] = samples
55
+ d = np.convolve(d, self.pulse_response)
56
+ return d[self.n_before: -self.n_after]
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+
8
+ @dataclass
9
+ class SampledOutput:
10
+ t_min: int
11
+ t_max: int
12
+ sample_rate: int # GSa/s
13
+ data: NDArray
14
+
15
+ def get_time_data(self):
16
+ """Time is [ns]"""
17
+ n = (self.t_max-self.t_min)*self.sample_rate
18
+ return np.linspace(self.t_min, self.t_max, n, endpoint=False)
19
+
20
+ def __add__(self, rhs: SampledOutput):
21
+ if self.t_min != rhs.t_min:
22
+ raise Exception(f"t_min not equal for sample output: {self.t_min} <> {rhs.t_min}")
23
+ if self.sample_rate != rhs.sample_rate:
24
+ raise Exception(f"sample rates not equal for sample output: {self.sample_rate} <> {rhs.sample_rate}")
25
+
26
+ t_max = max(self.t_max, rhs.t_max)
27
+ if self.t_max < t_max:
28
+ lhs_data = np.zeros(t_max * self.sample_rate)
29
+ lhs_data[:self.t_max * self.sample_rate] = self.data
30
+ else:
31
+ lhs_data = self.data
32
+ if rhs.t_max < t_max:
33
+ rhs_data = np.zeros(t_max * self.sample_rate)
34
+ rhs_data[:rhs.t_max * self.sample_rate] = rhs.data
35
+ else:
36
+ rhs_data = rhs.data
37
+
38
+ return SampledOutput(self.t_min, t_max, self.sample_rate, lhs_data+rhs_data)
39
+
40
+
41
+ @dataclass
42
+ class MarkerOutput:
43
+ t_min: int
44
+ t_max: int
45
+ points: list[tuple[int, int]]
46
+
47
+ def get_xy_lines(self):
48
+ line = np.array(self.points).T
49
+ return (line[0], line[1])
50
+
51
+ def to_samples(self):
52
+ n = self.t_max - self.t_min
53
+ data = np.zeros(n)
54
+ ip = 0
55
+ points = self.points
56
+ while points[ip][0] < self.t_min:
57
+ ip += 1
58
+ if int(points[ip][1]) == int(points[ip+1][1]):
59
+ # Skip point if level doesn't change.
60
+ # This can occur at start: [[0, 0], [0, 0], [0, 1], ...]
61
+ ip += 1
62
+ it = points[ip][0]
63
+ data[:it] = points[ip][1]
64
+ ip += 1
65
+ while ip+1 < len(points) and points[ip+1][0] <= self.t_max:
66
+ it1 = points[ip][0]
67
+ it2 = points[ip+1][0]
68
+ data[it1:it2] = points[ip][1]
69
+ ip += 2
70
+ return SampledOutput(self.t_min, self.t_max, 1, data)
71
+
72
+ def __add__(self, rhs: MarkerOutput | SampledOutput):
73
+ if isinstance(rhs, MarkerOutput):
74
+ rhs = rhs.to_samples()
75
+ return self.to_samples() + rhs
76
+
77
+ def __radd__(self, lhs: MarkerOutput | SampledOutput):
78
+ return self + lhs
q1simulator/cluster.py ADDED
@@ -0,0 +1,216 @@
1
+ import logging
2
+ from functools import partial
3
+
4
+ import matplotlib.pyplot as pt
5
+ import qcodes as qc
6
+
7
+ from qblox_instruments import (
8
+ InstrumentClass, InstrumentType,
9
+ SystemStatuses, SystemStatus, SystemStatusSlotFlags,
10
+ )
11
+
12
+ from .qblox_version import check_qblox_instrument_version
13
+ from .q1simulator import Q1Module, run_sequencers
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ClusterModule(qc.InstrumentChannel, Q1Module):
20
+ def __init__(self, root_instrument, name, slot, n_sequencers=6, sim_type=None):
21
+ super().__init__(root_instrument, name)
22
+ self._slot = slot
23
+ super().init_module(n_sequencers, sim_type)
24
+
25
+ def present(self):
26
+ return True
27
+
28
+ @property
29
+ def slot_idx(self):
30
+ return self._slot
31
+
32
+
33
+ class EmptySlot(qc.InstrumentChannel):
34
+ def __init__(self, root_instrument, name):
35
+ super().__init__(root_instrument, name)
36
+
37
+ def present(self):
38
+ return False
39
+
40
+
41
+ class Cluster(qc.Instrument):
42
+ _cluster_parameters = [
43
+ 'trigger_monitor_latest',
44
+ ]
45
+ _log_only_params = [
46
+ 'led_brightness',
47
+ ]
48
+
49
+ def __init__(self, name, modules={}):
50
+ check_qblox_instrument_version()
51
+ super().__init__(name)
52
+
53
+ # TODO return trigger count
54
+ for par_name in self._cluster_parameters:
55
+ self.add_parameter(par_name, set_cmd=partial(self._set, par_name))
56
+ for i in range(1, 16):
57
+ par_name = f'trigger{i}_monitor_count'
58
+ self.add_parameter(par_name, set_cmd=partial(self._set, par_name))
59
+
60
+ for par_name in self._log_only_params:
61
+ self.add_parameter(par_name,
62
+ set_cmd=partial(self._log_set, par_name))
63
+
64
+ self._modules = {}
65
+ for slot in range(1, 21):
66
+ name = f'module{slot}'
67
+ if slot in modules:
68
+ module = ClusterModule(self, name, slot, sim_type=modules[slot])
69
+ else:
70
+ module = EmptySlot(self, name)
71
+ self.add_submodule(name, module)
72
+ self._modules[slot] = module
73
+
74
+ def get_idn(self):
75
+ return dict(vendor='Q1Simulator', model='Cluster', serial='', firmware='')
76
+
77
+ @property
78
+ def instrument_class(self):
79
+ return InstrumentClass.CLUSTER
80
+
81
+ @property
82
+ def instrument_type(self):
83
+ return InstrumentType.MM
84
+
85
+ def get_num_system_error(self):
86
+ return 0
87
+
88
+ def get_system_error(self):
89
+ return '0,"No error"'
90
+
91
+ def get_system_status(self):
92
+ return SystemStatus(
93
+ SystemStatuses.OKAY,
94
+ [],
95
+ SystemStatusSlotFlags({}))
96
+
97
+ def get_connected_modules(self, filter_fn=None):
98
+ result = {}
99
+ for slot, module in self._modules.items():
100
+ if module.present():
101
+ result[slot] = module
102
+ return result
103
+
104
+ def _check_module_present(self, slot):
105
+ if not self._modules[slot].present():
106
+ raise Exception(f'No module in slot {slot}')
107
+
108
+ def _set(self, name, value):
109
+ logger.info(f'{self.name}:{name}={value}')
110
+
111
+ def get_sequencer_status(self, slot, seq_nr, timeout=0):
112
+ return self._modules[slot].get_sequencer_status(seq_nr, timeout)
113
+
114
+ def get_acquisition_status(self, slot, seq_nr, timeout=0):
115
+ return self.self._modules[slot].get_acquisition_status(seq_nr, timeout)
116
+
117
+ def arm_sequencer(self, slot: int | None = None, sequencer: int | None = None) -> None:
118
+ if slot is not None:
119
+ self._check_module_present(slot)
120
+ self._modules[slot].arm_sequencer(sequencer)
121
+ else:
122
+ for module in self.get_connected_modules().values():
123
+ module.arm_sequencer(sequencer)
124
+
125
+ def start_sequencer(self, slot: int | None = None, sequencer: int | None = None) -> None:
126
+ if slot is not None:
127
+ self._check_module_present(slot)
128
+ modules = [self._modules[slot]]
129
+ else:
130
+ modules = self.get_connected_modules().values()
131
+
132
+ # Get list of armed sequencers
133
+ # pass to sequence executor
134
+ sequencers = []
135
+ for module in modules:
136
+ seq_numbers = [sequencer] if sequencer is not None else module.armed_seq
137
+ for seq_number in seq_numbers:
138
+ sequencers.append(module.sequencers[seq_number])
139
+
140
+ run_sequencers(sequencers)
141
+
142
+ def stop_sequencer(self, slot: int | None = None, sequencer: int | None = None) -> None:
143
+ if slot is not None:
144
+ self._check_module_present(slot)
145
+ self._modules[slot].stop_sequencer(sequencer)
146
+ else:
147
+ for module in self.get_connected_modules().values():
148
+ module.stop_sequencer(sequencer)
149
+
150
+ @property
151
+ def modules(self):
152
+ return list(self.submodules.values())
153
+
154
+ def reset(self):
155
+ self.invalidate_cache()
156
+ for module in self.get_connected_modules().values():
157
+ module.reset()
158
+
159
+ def _log_set(self, name, value):
160
+ logger.info(f'{self.name}: {name}={value}')
161
+
162
+ def config(self, name, value):
163
+ for module in self.get_connected_modules().values():
164
+ module.config(name, value)
165
+
166
+ def get_simulation_end_time(self):
167
+ return max(module.get_simulation_end_time() for module in self.get_connected_modules().values())
168
+
169
+ def plot(self,
170
+ t_min: float | None = None,
171
+ t_max: float | None = None,
172
+ channels: list[str] | list[int] | None = None,
173
+ modules: list[int] | None = None,
174
+ create_figure: bool | str = True,
175
+ analogue_filter: bool = False,
176
+ analogue_output_frequency: float = 4e9,
177
+ output_per_sequencer: bool = True,
178
+ **kwargs):
179
+ """Plots the simulated output of the cluster.
180
+
181
+ Args:
182
+ t_min: minimum time in the plot.
183
+ t_max: maximum time in the plot.
184
+ channels: If not None specifies the channels to plot by name or sequencer number.
185
+ modules: If not None specifies the modules to plot by slot number.
186
+ create_figure:
187
+ If True create a new figure.
188
+ If False only pyplot.plot() is called without creating figure or setting axis labels.
189
+ If "modules" creates a new figure per module.
190
+ analogue_filter: plot result after applying (estimated) analog filter.
191
+ analogue_output_frequency: sample rate of analogue output
192
+ output_per_sequencer:
193
+ if True: plot data for individual sequencers.
194
+ if False: plot data for physical front panel output.
195
+ """
196
+ if create_figure is True:
197
+ pt.figure()
198
+ pt.title('Cluster')
199
+ pt.grid(True)
200
+ pt.xlabel('[ns]')
201
+ pt.ylabel('[V]')
202
+ for slot, module in self.get_connected_modules().items():
203
+ if modules is not None and slot not in modules:
204
+ # skip module
205
+ continue
206
+ if create_figure == "module":
207
+ pt.figure()
208
+ pt.title(module.label)
209
+ pt.grid(True)
210
+ pt.xlabel('[ns]')
211
+ pt.ylabel('[V]')
212
+ module.plot(t_min=t_min, t_max=t_max, channels=channels, analogue_filter=analogue_filter,
213
+ analogue_output_frequency=analogue_output_frequency,
214
+ output_per_sequencer=output_per_sequencer)
215
+ pt.legend()
216
+ pt.show()
File without changes
@@ -0,0 +1,114 @@
1
+ import logging
2
+ import os
3
+
4
+ from qtpy import QtCore, QtWidgets
5
+
6
+ from q1simulator.gui.program_details import ProgramDetailsWidget
7
+ from q1simulator.gui.qt_utils import (
8
+ # qt_log_exception,
9
+ qt_init,
10
+ qt_create_app,
11
+ qt_run_app,
12
+ qt_set_darkstyle,
13
+ )
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ _app = None
20
+
21
+
22
+ class Q1ProgramBrowser(QtWidgets.QMainWindow):
23
+ """
24
+ Q1Program browser.
25
+ """
26
+
27
+ _WINDOW_TITLE: str = "Q1Program Browser"
28
+
29
+ def __init__(
30
+ self,
31
+ path: str | None = None,
32
+ gui_style: str | None = None
33
+ ):
34
+ """Creates program browser
35
+
36
+ Args:
37
+ path: base directory. If None uses current working directory.
38
+ gui_style: if "dark" uses dark style, otherwise normal style.
39
+ """
40
+ global _app
41
+ logger.debug("Init program browser")
42
+
43
+ qt_app_runing = qt_init()
44
+ if not qt_app_runing:
45
+ # note: store reference to avoid garbage collection.
46
+ # reference is also used to restart browser 2nd time in Python console.
47
+ _app = qt_create_app()
48
+
49
+ if gui_style == "dark":
50
+ qt_set_darkstyle()
51
+
52
+ super().__init__()
53
+
54
+ if path is None:
55
+ path = os.getcwd()
56
+
57
+ self._path = path
58
+
59
+ self.setWindowTitle(self._WINDOW_TITLE)
60
+ self.resize(860, 600)
61
+ self.create_ui()
62
+ self.fill_program_list()
63
+ self.show()
64
+
65
+ if _app is not None:
66
+ qt_run_app(_app)
67
+
68
+ def create_ui(self):
69
+ content = QtWidgets.QWidget()
70
+ self._list_widget = QtWidgets.QListWidget()
71
+ self._list_widget.setMinimumWidth(300)
72
+ self._list_widget.itemSelectionChanged.connect(self._show_program_info)
73
+
74
+ self._program_details = ProgramDetailsWidget(self)
75
+
76
+ layout = QtWidgets.QHBoxLayout()
77
+ layout.addWidget(self._list_widget)
78
+ layout.addWidget(self._program_details, 1)
79
+
80
+ content.setLayout(layout)
81
+ self.setCentralWidget(content)
82
+
83
+ @QtCore.Slot()
84
+ def _show_program_info(self):
85
+ selected_items = self._list_widget.selectedItems()
86
+ if not selected_items:
87
+ return
88
+ item = selected_items[0]
89
+ path = item.data(QtCore.Qt.UserRole)
90
+ self._program_details.show_details(path)
91
+
92
+ def fill_program_list(self):
93
+ w = self._list_widget
94
+ w.clear()
95
+ for entry in os.scandir(self._path):
96
+ if not entry.is_dir():
97
+ continue
98
+ i = w.count()
99
+ w.addItem(entry.name)
100
+ item = w.item(i)
101
+ item.setData(QtCore.Qt.UserRole, entry.path)
102
+
103
+ def _show_error_message(self, title, message):
104
+ msg = QtWidgets.QMessageBox()
105
+ msg.setIcon(QtWidgets.QMessageBox.Critical)
106
+ msg.setText(message)
107
+ msg.setWindowTitle("Q1ProgramBrowser: " + title)
108
+ msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
109
+ msg.exec_()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ path = r"C:\measurements\qblox_programs"
114
+ ui = Q1ProgramBrowser(path, gui_style="dark")
@@ -0,0 +1,138 @@
1
+ import json
2
+ from qtpy import QtWidgets, QtCore
3
+
4
+ from .program_plotter import plot_simulation
5
+
6
+
7
+ class ProgramDetailsWidget(QtWidgets.QWidget):
8
+ def __init__(self,
9
+ parent: QtWidgets.QWidget,
10
+ ):
11
+ super().__init__(parent)
12
+ self._prog_config = None
13
+ self._path = None
14
+
15
+ layout = QtWidgets.QVBoxLayout()
16
+ layout.setContentsMargins(0, 0, 0, 0)
17
+ self.setLayout(layout)
18
+
19
+ self._table = self.create_table()
20
+ layout.addWidget(self._table, 1)
21
+ layout.addLayout(self.create_controls())
22
+
23
+ self._plot_btn.clicked.connect(self.plot)
24
+
25
+ def create_table(self):
26
+ table = QtWidgets.QTreeWidget()
27
+ table.setMinimumHeight(400)
28
+ table.setMinimumWidth(540)
29
+ table.setColumnCount(7)
30
+ table.setColumnWidth(0, 100)
31
+ table.setColumnWidth(1, 100)
32
+ table.setColumnWidth(2, 50)
33
+ table.setColumnWidth(3, 50)
34
+ table.setColumnWidth(4, 50)
35
+ table.setColumnWidth(5, 90)
36
+ table.setColumnWidth(6, 80)
37
+ table.setHeaderLabels([
38
+ "Channel",
39
+ "Module",
40
+ "Paths",
41
+ "Out",
42
+ "In",
43
+ "NCO",
44
+ "Duration",
45
+ ])
46
+ table.setRootIsDecorated(False)
47
+ return table
48
+
49
+ def create_controls(self):
50
+ min_time = QtWidgets.QSpinBox()
51
+ min_time.setRange(0, 100_000_000)
52
+ min_time.setSingleStep(1000)
53
+ self._min_time_spin = min_time
54
+ max_time = QtWidgets.QSpinBox()
55
+ max_time.setSpecialValueText("Duration")
56
+ max_time.setRange(0, 100_000_000)
57
+ max_time.setSingleStep(1000)
58
+ self._max_time_spin = max_time
59
+
60
+ self._analogue_cb = QtWidgets.QCheckBox("Analogue filter")
61
+ self._plot_btn = QtWidgets.QPushButton("Plot")
62
+
63
+ layout = QtWidgets.QHBoxLayout()
64
+ layout.addWidget(QtWidgets.QLabel("t min."))
65
+ layout.addWidget(min_time)
66
+ layout.addWidget(QtWidgets.QLabel("t max."))
67
+ layout.addWidget(max_time)
68
+ layout.addWidget(self._analogue_cb)
69
+ layout.addWidget(self._plot_btn)
70
+
71
+ return layout
72
+
73
+ def show_details(self, path):
74
+ table = self._table
75
+ table.clear()
76
+ self._prog_config = None
77
+ try:
78
+ with open(path + "/q1program.json", "r") as fp:
79
+ data = json.load(fp)
80
+ self._path = path
81
+ self._prog_config = data
82
+ duration = -1
83
+ rows = []
84
+ for ch_name, settings in data.items():
85
+ f_nco = settings["nco"]
86
+ seq_duration = settings.get("duration")
87
+ if seq_duration is not None:
88
+ duration = max(seq_duration, duration)
89
+ duration_text = f"{seq_duration} ns"
90
+ else:
91
+ duration_text = "--"
92
+ q1asm = settings.get("sequence")
93
+ if not q1asm:
94
+ duration_text = "no sequence"
95
+
96
+ item = QtWidgets.QTreeWidgetItem(table)
97
+ item.setText(0, ch_name)
98
+ item.setData(0, QtCore.Qt.CheckStateRole, QtCore.Qt.Checked if q1asm else QtCore.Qt.Unchecked)
99
+
100
+ item.setText(1, settings["module"])
101
+ item.setText(2, str(settings["paths"]))
102
+ item.setText(3, str(settings["out_channels"]))
103
+ item.setText(4, str(settings.get("in_channels", "")))
104
+ item.setText(5, f"{f_nco/1e6} MHz" if f_nco is not None else "")
105
+ item.setText(6, duration_text)
106
+ rows.append(item)
107
+ table.insertTopLevelItems(0, rows)
108
+
109
+ # Show duration
110
+
111
+ except FileNotFoundError:
112
+ print("'program_config.json' not found")
113
+
114
+ def plot(self):
115
+ if not self._prog_config:
116
+ print("no program")
117
+ return
118
+ table = self._table
119
+ channels = []
120
+ for i in range(table.topLevelItemCount()):
121
+ item = table.topLevelItem(i)
122
+ if item.checkState(0) == QtCore.Qt.Checked:
123
+ channels.append(item.text(0))
124
+
125
+ min_time = self._min_time_spin.value()
126
+ max_time = self._max_time_spin.value()
127
+ if max_time == 0:
128
+ max_time = None
129
+
130
+ plot_simulation(
131
+ self._path,
132
+ self._prog_config,
133
+ channels,
134
+ min_time=min_time,
135
+ max_time=max_time,
136
+ analogue_filter=self._analogue_cb.isChecked(),
137
+ analogue_output_frequency=4e9,
138
+ )