diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py index 28820542..738469e7 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/aerotech.py @@ -40,8 +40,7 @@ class cSAXSAerotech: Without scan_number the plot shows the last scan and then follows the running and the next aerotech_cont_grid scans, line by line; with scan_number it shows that scan. Calling it again reuses the window. The points are sent as plain - data, so the plot shows every exposure (no downsampling); the thin lines - connect the points of each cell column. + data, so the plot shows every exposure (no downsampling), as markers. Examples -------- @@ -70,7 +69,10 @@ class cSAXSAerotech: window = self.gui.windows[self.WINDOW] else: window = self.gui.new(self.WINDOW, timeout=self.GUI_RPC_TIMEOUT) - waveform = window.new("Waveform", object_name=self.PLOT, timeout=self.GUI_RPC_TIMEOUT) + # reuse the plot of an earlier call (new() would add a second one) + waveform = window.widget_map().get(self.PLOT) + if waveform is None: + waveform = window.new("Waveform", object_name=self.PLOT, timeout=self.GUI_RPC_TIMEOUT) waveform.clear_all() waveform.title = "aerotech_cont_grid: exposure centres" waveform.x_label = "X centre (aerox)" @@ -82,7 +84,6 @@ class cSAXSAerotech: curve.set_color(color) curve.set_symbol("o") curve.set_symbol_size(4) - curve.set_pen_width(1) curves[direction] = curve return waveform, curves @@ -108,11 +109,10 @@ def _lines_from_file(file_path: str) -> list[tuple[np.ndarray, np.ndarray]]: def _column_order(lines: list[tuple[np.ndarray, np.ndarray]], backward: bool): """ - Points of lines of one direction, ordered by cell column, the columns alternately - up and down. X then increases along the curve (up to the jitter within a column), - which the curve's clip-to-view (bisection on X) needs, and the connecting line runs - along the columns instead of zigzagging. Lines are in measurement order, so a - backward line is reversed to cell order. + Points of lines of one direction, ordered by cell column. X then increases along + the curve (up to the jitter within a column), which the curve's clip-to-view + (bisection on X) needs. Lines are in measurement order, so a backward line is + reversed to cell order. """ cells_x = np.array([line[0][::-1] if backward else line[0] for line in lines]) cells_y = np.array([line[1][::-1] if backward else line[1] for line in lines]) @@ -133,7 +133,18 @@ def _draw(curves: dict, lines: list[tuple[np.ndarray, np.ndarray]]) -> None: curves[direction].set_data([np.nan], [np.nan]) continue x, y = _column_order(selected, backward=first == 1) - curves[direction].set_data(x, y) + curves[direction].set_data(*_markers_only(x, y)) + + +def _markers_only(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """ + A NaN after every point: the curve draws no connecting lines (a non-finite value is a + gap), only the markers. X stays finite and sorted for the clip-to-view. + """ + x2 = np.repeat(x, 2) + y2 = np.full(2 * len(y), np.nan) + y2[0::2] = y + return x2, y2 class _LivePositions: diff --git a/docs/user/saxs/aerotech_stages/aerotech_stages.md b/docs/user/saxs/aerotech_stages/aerotech_stages.md index 6fe14aa3..6cc71d5b 100644 --- a/docs/user/saxs/aerotech_stages/aerotech_stages.md +++ b/docs/user/saxs/aerotech_stages/aerotech_stages.md @@ -91,8 +91,8 @@ csaxs.aerotech.plot(scan_number=8494) # a finished scan csaxs.aerotech.stop_plot() # stop following (the plot stays) ``` -Every exposure is shown. The thin lines connect the points of each cell column; they -only guide the eye. +Every exposure is shown as a marker. Forward and backward lines alternate, so the two +colours alternate in Y by one line spacing. ### Examples diff --git a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py index 2970bcfb..b53ed302 100644 --- a/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py +++ b/tests/tests_bec_ipython_client/test_cSAXS_aerotech.py @@ -28,6 +28,7 @@ def _snake(n_lines=4, n_points=5, step=0.01): def _client(windows=None): gui = mock.MagicMock() gui.windows = windows if windows is not None else {} + gui.new.return_value.widget_map.return_value = {} client = mock.MagicMock(gui=gui) client.connector.get.return_value = None return client @@ -77,18 +78,23 @@ def test_plot_history_draws_forward_and_backward_curves(): assert waveform.lock_aspect_ratio is True for curve in (forward, backward): x, y = curve.set_data.call_args.args - assert len(x) == 10 and np.all(np.diff(x) >= 0) + # markers only: a NaN after every point, X finite and increasing + assert len(x) == 20 and np.all(np.diff(x) >= 0) and np.isfinite(x).all() + assert np.isfinite(y[0::2]).all() and np.isnan(y[1::2]).all() client.connector.register.assert_not_called() # history: no live subscription -def test_plot_reuses_open_window(): +def test_plot_reuses_open_window_and_plot(): window = mock.MagicMock() + existing = mock.MagicMock() + window.widget_map.return_value = {"aerotech_positions": existing} client = _client(windows={"aerotech": window}) tool = cSAXSAerotech(client) with mock.patch.object(tool, "_history_lines", return_value=_snake()): - tool.plot(scan_number=1) + assert tool.plot(scan_number=1) is existing client.gui.new.assert_not_called() - window.new.assert_called_once() + window.new.assert_not_called() + existing.clear_all.assert_called_once_with() def test_plot_without_gui_raises(): @@ -125,7 +131,7 @@ def test_live_follows_an_aerotech_scan_line_by_line(): tool.stop_plot() x, _ = backward.set_data.call_args.args - assert len(x) == 5 + assert len(x) == 2 * 5 client.connector.unregister.assert_called()