diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index e26280479e..8d7c82e691 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -41,6 +41,9 @@ ARTIQ-9 (Unreleased) - State files are now automatically backed up upon successful loading. - Experiments can now use ``restart_applet`` CCB command to restart applets. - Interactive argument widgets now remember their layout between runs. + - Waveform Viewer now has a ``uniform_interval`` checkbox, equivalent to the ``-u`` option + in ``artiq_coreanalyzer``. In this mode, each event advances the RTIO waveform trace by one step, + with additional ``interval`` and ``timestamp`` traces indicating the actual time. * ``artiq_session`` has been revamped. Output now indicates the originating process. Dashboard is started after the controller manager. All processes are cleanly terminated after dashboard is closed. diff --git a/artiq/coredevice/comm_analyzer.py b/artiq/coredevice/comm_analyzer.py index c49fed5e77..98566fbd21 100644 --- a/artiq/coredevice/comm_analyzer.py +++ b/artiq/coredevice/comm_analyzer.py @@ -697,6 +697,8 @@ def get_channel_list(devices): ref_period = DEFAULT_REF_PERIOD precision = max(0, math.ceil(math.log10(1 / ref_period) - 6)) manager.get_channel("rtio_slack", 64, ty=WaveformType.ANALOG, precision=precision, unit="us") + manager.get_channel("timestamp", 64, ty=WaveformType.VECTOR) + manager.get_channel("interval", 64, ty=WaveformType.ANALOG, precision=precision, unit="us") return manager.channels @@ -721,7 +723,9 @@ def decoded_dump_to_target(manager, devices, dump, uniform_interval): if ref_period is None: logger.warning("unable to determine core device ref_period") ref_period = DEFAULT_REF_PERIOD - if not uniform_interval: + if uniform_interval: + manager.set_timescale_ps(1e12) + else: manager.set_timescale_ps(ref_period*1e12) dds_sysclk = get_dds_sysclk(devices) if dds_sysclk is None: diff --git a/artiq/dashboard/waveform.py b/artiq/dashboard/waveform.py index bfa13509c3..3bc350c79b 100644 --- a/artiq/dashboard/waveform.py +++ b/artiq/dashboard/waveform.py @@ -26,6 +26,7 @@ WAVEFORM_MIN_HEIGHT = 50 WAVEFORM_MAX_HEIGHT = 200 +PREFIXES_STR = "pnumkMG" class ProxyClient(): @@ -426,8 +427,7 @@ def __init__(self, parent): self._ref_axis.setFixedHeight(45) self._ref_axis.setMenuEnabled(False) self._top = pg.AxisItem("top") - self._top.setScale(1e-12) - self._top.setLabel(units="s") + self.updateTopAxis(False) self._ref_axis.setAxisItems({"top": self._top}) layout.addWidget(self._ref_axis) @@ -539,6 +539,14 @@ def _create_waveform(self, row): action.triggered.connect(self.confirm_delete_dialog.open) w.addAction(action) return w + + def updateTopAxis(self, uniform_interval): + if uniform_interval: + self._top.setScale(1e0) + self._top.setLabel(text= "event index", units="events") + else: + self._top.setScale(1e-12) + self._top.setLabel(text= "time", units="s") def _delete_waveform(self, waveform): row = self._splitter.indexOf(waveform) @@ -621,6 +629,8 @@ def __init__(self, parent): self._text = "" self._value = 0 self._timescale = 1 + self._suffix = "s" + self._uniform_interval = False self.setDisplayValue(0) self.textChanged.connect(self._onTextChange) self.returnPressed.connect(self._onReturnPress) @@ -628,19 +638,26 @@ def __init__(self, parent): def setTimescale(self, timescale): self._timescale = timescale + def updateUniformInterval(self, uniform_interval): + self._uniform_interval = uniform_interval + self._suffix = "event" if uniform_interval else "s" + self.setDisplayValue(0) + def _onTextChange(self, text): self._text = text def setDisplayValue(self, value): self._value = value self._text = pg.siFormat(value * 1e-12 * self._timescale, - suffix="s", + suffix=self._suffix, allowUnicode=False, precision=15) self.setText(self._text) def _setValueFromText(self, text): try: + if text.strip().endswith(tuple(PREFIXES_STR)): + text += self._suffix self._value = pg.siEval(text) * (1e12 / self._timescale) except: logger.error("Error when parsing cursor time input", exc_info=True) @@ -716,13 +733,11 @@ def __init__(self, timeout, timer, timer_backoff): self._ddb = None self._dump = None + self._uniform_interval = False - self._waveform_data = { - "timescale": 1, - "stopped_x": None, - "logs": dict(), - "data": dict(), - } + self._waveform_data = self._new_waveform_data() + self._waveform_data_non_uniform_interval = self._new_waveform_data() + self._waveform_data_uniform_interval = self._new_waveform_data() self._current_dir = os.getcwd() @@ -749,6 +764,11 @@ def __init__(self, timeout, timer, timer_backoff): lambda: asyncio.ensure_future(exc_to_warning(self.proxy_client.trigger_proxy_task()))) grid.addWidget(self._request_dump_btn, 0, 1) + self._uniform_interval_checkbox = QtWidgets.QCheckBox("Uniform Interval") + self._uniform_interval_checkbox.setToolTip("Display waveforms with uniform time intervals") + self._uniform_interval_checkbox.checkStateChanged.connect(self._update_uniform_interval) + grid.addWidget(self._uniform_interval_checkbox, 0, 2) + self._add_channel_dialog = _AddChannelDialog(self, self._channel_model) self._add_channel_dialog.accepted.connect(self._add_channels) @@ -758,7 +778,7 @@ def __init__(self, timeout, timer, timer_backoff): QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_FileDialogListView)) self._add_btn.clicked.connect(self._add_channel_dialog.open) - grid.addWidget(self._add_btn, 0, 2) + grid.addWidget(self._add_btn, 0, 3) self._file_menu = QtWidgets.QMenu() self._add_async_action("Open trace...", self.load_trace) @@ -769,6 +789,7 @@ def __init__(self, timeout, timer, timer_backoff): self._menu_btn.setMenu(self._file_menu) self._waveform_view = _WaveformView(self) + self._waveform_view.updateTopAxis(self._uniform_interval) self._waveform_view.setModel(self._waveform_model) grid.addWidget(self._waveform_view, 1, 0, colspan=12) @@ -778,12 +799,21 @@ def __init__(self, timeout, timer, timer_backoff): QtWidgets.QApplication.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_TitleBarMaxButton)) self._reset_zoom_btn.clicked.connect(self._waveform_view.resetZoom) - grid.addWidget(self._reset_zoom_btn, 0, 3) + grid.addWidget(self._reset_zoom_btn, 0, 4) self._cursor_control = _CursorTimeControl(self) + self._cursor_control.updateUniformInterval(self._uniform_interval) self._waveform_view.cursorMove.connect(self._cursor_control.setDisplayValue) self._cursor_control.submit.connect(self._waveform_view.onCursorMove) - grid.addWidget(self._cursor_control, 0, 4, colspan=6) + grid.addWidget(self._cursor_control, 0, 5, colspan=6) + + def _new_waveform_data(self): + return { + "timescale": 1, + "stopped_x": None, + "logs": dict(), + "data": dict(), + } def _add_async_action(self, label, coro): action = QtGui.QAction(label, self) @@ -802,8 +832,17 @@ def _add_channels(self): def on_dump_receive(self, dump): self._dump = dump decoded_dump = comm_analyzer.decode_dump(dump) - waveform_data = comm_analyzer.decoded_dump_to_waveform_data(self._ddb, decoded_dump) - self._waveform_data.update(waveform_data) + waveform_data_non_uniform = comm_analyzer.decoded_dump_to_waveform_data(self._ddb, decoded_dump, False) + self._waveform_data_non_uniform_interval.update(waveform_data_non_uniform) + waveform_data_uniform = comm_analyzer.decoded_dump_to_waveform_data(self._ddb, decoded_dump, True) + self._waveform_data_uniform_interval.update(waveform_data_uniform) + self.reload_waveform() + + def reload_waveform(self): + if self._uniform_interval: + self._waveform_data = self._waveform_data_uniform_interval + else: + self._waveform_data = self._waveform_data_non_uniform_interval self._channel_model.update(self._waveform_data['logs']) self._waveform_model.update_all(self._waveform_data['data']) self._waveform_view.setStoppedX(self._waveform_data['stopped_x']) @@ -902,7 +941,7 @@ async def save_channels(self): logger.error("Failed to save channel list", exc_info=True) def _process_ddb(self): - channel_list = comm_analyzer.get_channel_list(self._ddb) + channel_list = self._get_channel_list() self._channel_model.clear() self._channel_model.update(channel_list) desc = self._ddb.get("core_analyzer") @@ -925,3 +964,19 @@ def notify_ddb(self, mod): async def stop(self): if self.proxy_client is not None: await self.proxy_client.close() + + def _get_channel_list(self): + channel_list = comm_analyzer.get_channel_list(self._ddb) + if not self._uniform_interval: + channel_list.pop("interval", None) + channel_list.pop("timestamp", None) + return channel_list + + def _update_uniform_interval(self): + self._uniform_interval = self._uniform_interval_checkbox.isChecked() + channel_list = self._get_channel_list() + self._channel_model.clear() + self._channel_model.update(channel_list) + self._waveform_view.updateTopAxis(self._uniform_interval) + self._cursor_control.updateUniformInterval(self._uniform_interval) + self.reload_waveform()