diff --git a/bec_widgets/widgets/plots/waveform/waveform.py b/bec_widgets/widgets/plots/waveform/waveform.py index d1994758..77ac64c8 100644 --- a/bec_widgets/widgets/plots/waveform/waveform.py +++ b/bec_widgets/widgets/plots/waveform/waveform.py @@ -1144,13 +1144,13 @@ class Waveform(PlotBase): to reduce the number of copies in between cycles. Be careful when refactoring this part as it will affect the performance of the async readback. + Async curves support plotting against 'index' or other 'device_signal'. No 'auto' or 'timestamp'. + The fallback mechanism for 'auto' and 'timestamp' is to use the 'index'. + Note: We create data_plot_x and data_plot_y and modify them within this function to avoid creating new arrays. This is important for performance. - We adjust the variables based on instruction type. - - add: Add the new data to the existing data. - - add_slice: Add the new data to the existing data and set the slice index. - - replace: Replace the existing data with the new data. + Support update instructions are 'add', 'add_slice', and 'replace'. Args: msg(dict): Message with the async data. @@ -1196,14 +1196,31 @@ class Waveform(PlotBase): # Replace is trivial, no need to modify data_plot_y - # Get the x data if 'timestamp' is selected, otherwise compute it - data_plot_x = async_data["timestamp"] - if plot_mode == "timestamp" and instruction != "add_slice": - if data_plot_x is not None and x_data is not None: - data_plot_x = np.hstack((x_data, data_plot_x)) - else: + # Get x data for plotting + if plot_mode in ["index", "auto", "timestamp"]: data_plot_x = np.linspace(0, len(data_plot_y) - 1, len(data_plot_y)) - # Set the x data + self._auto_adjust_async_curve_settings(curve, len(data_plot_y)) + curve.setData(data_plot_x, data_plot_y) + # Move on in the loop + continue + # Only consider device signals that are async for now, fallback is index + x_device_entry = self.x_axis_mode["entry"] + async_data = msg["signals"].get(x_device_entry, None) + # Make sure the signal exists, otherwise fall back to index + if async_data is None: + # Try to grab the data from device signals + data_plot_x = self._get_x_data(plot_mode, x_device_entry) + if len(data_plot_x) != len(data_plot_y): + # If the data is not the same length, fall back to index + logger.warning( + f"Async data for curve {curve.name()} is None. Falling back to index." + ) + data_plot_x = np.linspace(0, len(data_plot_y) - 1, len(data_plot_y)) + elif x_data is not None: + data_plot_x = np.hstack((x_data, async_data["value"])) + else: + data_plot_x = async_data["value"] + # Plot the data self._auto_adjust_async_curve_settings(curve, len(data_plot_y)) curve.setData(data_plot_x, data_plot_y) diff --git a/tests/end-2-end/test_plotting_framework_e2e.py b/tests/end-2-end/test_plotting_framework_e2e.py index 0ffedf1f..2e647ed8 100644 --- a/tests/end-2-end/test_plotting_framework_e2e.py +++ b/tests/end-2-end/test_plotting_framework_e2e.py @@ -135,13 +135,13 @@ def test_async_plotting(qtbot, bec_client_lib, connected_client_gui_obj): # Wait for the scan to finish and the data to be available in history # Wait until scan_id is in history - def _wait_for_scan_in_hisotry(): + def _wait_for_scan_in_history(): if len(client.history) == 0: return False # Once items appear in storage, the last one hast to be the one we just scanned return client.history[-1].metadata.bec["scan_id"] == status.scan.scan_id - qtbot.waitUntil(_wait_for_scan_in_hisotry, timeout=10000) + qtbot.waitUntil(_wait_for_scan_in_history, timeout=10000) last_scan_data = client.history[-1] # check plotted data x_data, y_data = curve.get_data() diff --git a/tests/unit_tests/test_waveform_next_gen.py b/tests/unit_tests/test_waveform_next_gen.py index f47a5dee..903f4b2b 100644 --- a/tests/unit_tests/test_waveform_next_gen.py +++ b/tests/unit_tests/test_waveform_next_gen.py @@ -353,7 +353,7 @@ def test_update_async_curves(monkeypatch, qtbot, mocked_client): wf = create_widget(qtbot, Waveform, client=mocked_client) c = wf.plot(arg1="async_device", label="async_device-async_device") wf._async_curves = [c] - wf.x_mode = "timestamp" + wf.x_mode = "timestamp" # Timestamp is not supported, fallback to index. dummy_scan = create_dummy_scan_item() wf.scan_item = dummy_scan @@ -366,7 +366,7 @@ def test_update_async_curves(monkeypatch, qtbot, mocked_client): monkeypatch.setattr(c, "setData", fake_setData) wf.update_async_curves() - np.testing.assert_array_equal(recorded.get("x"), [11, 21, 31]) + np.testing.assert_array_equal(recorded.get("x"), [0, 1, 2]) np.testing.assert_array_equal(recorded.get("y"), [1, 2, 3]) @@ -528,12 +528,10 @@ def test_setup_async_curve(qtbot, mocked_client, monkeypatch): assert "async_device" in endpoint_called -@pytest.mark.parametrize("x_mode", ("timestamp", "index")) -def test_on_async_readback_add_update(qtbot, mocked_client, x_mode): +def test_on_async_readback_add_update(qtbot, mocked_client): """ Test that on_async_readback extends or replaces async data depending on metadata instruction. - For 'timestamp' mode, new timestamps are appended to x_data. - For 'index' mode, x_data simply increases by integer index. + 'Index' mode """ wf = create_widget(qtbot, Waveform, client=mocked_client) wf.scan_item = create_dummy_scan_item() @@ -543,7 +541,7 @@ def test_on_async_readback_add_update(qtbot, mocked_client, x_mode): c.setData([0, 1, 2], [10, 11, 12]) # Set the x_axis_mode - wf.x_axis_mode["name"] = x_mode + wf.x_axis_mode["name"] = "index" ############# Test add ################ @@ -554,10 +552,7 @@ def test_on_async_readback_add_update(qtbot, mocked_client, x_mode): x_data, y_data = c.get_data() assert len(x_data) == 5 # Check x_data based on x_mode - if x_mode == "timestamp": - np.testing.assert_array_equal(x_data, [0, 1, 2, 1001, 1002]) - else: # x_mode == "index" - np.testing.assert_array_equal(x_data, [0, 1, 2, 3, 4]) + np.testing.assert_array_equal(x_data, [0, 1, 2, 3, 4]) np.testing.assert_array_equal(y_data, [10, 11, 12, 100, 200]) @@ -566,11 +561,7 @@ def test_on_async_readback_add_update(qtbot, mocked_client, x_mode): metadata2 = {"async_update": {"max_shape": [None], "type": "replace"}} wf.on_async_readback(msg2, metadata2) x_data2, y_data2 = c.get_data() - if x_mode == "timestamp": - np.testing.assert_array_equal(x_data2, [555]) - else: - - np.testing.assert_array_equal(x_data2, [0]) + np.testing.assert_array_equal(x_data2, [0]) np.testing.assert_array_equal(y_data2, [999])