Skip to content
This repository was archived by the owner on Jan 18, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion artiq/coredevice/comm_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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:
Expand Down
85 changes: 70 additions & 15 deletions artiq/dashboard/waveform.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

WAVEFORM_MIN_HEIGHT = 50
WAVEFORM_MAX_HEIGHT = 200
PREFIXES_STR = "pnumkMG"


class ProxyClient():
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -621,26 +629,35 @@ 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)

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,
Comment thread
hubuke marked this conversation as resolved.
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)
Expand Down Expand Up @@ -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()

Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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'])
Expand Down Expand Up @@ -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")
Expand All @@ -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()