From 229b98bd36c493834ebbe2b8fcbdd53ce535a7ea Mon Sep 17 00:00:00 2001 From: x10sa Date: Tue, 25 Aug 2026 09:52:33 +0200 Subject: [PATCH] updated plotting of scans and added the option to fit features --- pxii_bec/macros/mx_basics.py | 142 ++++++++++++++++++-------------- pxii_bec/macros/mx_methods.py | 149 ++++++++++++++++++++++++++-------- 2 files changed, 195 insertions(+), 96 deletions(-) diff --git a/pxii_bec/macros/mx_basics.py b/pxii_bec/macros/mx_basics.py index 045462e..22158ef 100755 --- a/pxii_bec/macros/mx_basics.py +++ b/pxii_bec/macros/mx_basics.py @@ -376,13 +376,28 @@ def plot_live_data_bec( wf.y_label = signal_name wf.plot(device_x=motor_name, device_y=signal_name) +def plot_live_data( + wf, + motor_name, + signal_name, + title="Live scan", +): + wf.clear_all() + wf.title = title + wf.x_label = motor_name + wf.y_label = signal_name + wf.plot( + device_x=motor_name, + device_y=signal_name, + ) def plot_fitted_data_bec( data, fit_result, window_name="Fitting", + plot_widget=None ): """ Plot fitted data and display fitting parameters in the specified window. @@ -402,7 +417,10 @@ def plot_fitted_data_bec( as 'centre', 'fwhm', 'height', and the fitted model stored under 'lmfit_result', with its 'best_fit' attribute representing the fitted data. """ - wf, text_box = select_bec_window(window_name) + if plot_widget is not None: + wf = plot_widget + else: + wf, text_box = select_bec_window(window_name) fit_text = ( f"Fit parameters: Centre = {fit_result['centre']:.5g}, " @@ -412,7 +430,8 @@ def plot_fitted_data_bec( f"Chi sq = {fit_result['chi_sq']:.3g}\n" f"SNR = {fit_result['snr']:.3g}" ) - text_box.set_plain_text(fit_text) + if plot_widget is None: + text_box.set_plain_text(fit_text) wf.clear_all() wf.title = f"Scan: {data['scan_number']}" @@ -423,74 +442,75 @@ def plot_fitted_data_bec( # wf.Fit.set(symbol_size = 0) wf.get_curve('Fit').set(symbol_size=0) -def plot_feature_bec( - data, - result, - window_name="Feature", -): - """ - Plot feature data and indicate the detected edges and centre. - """ +# def plot_feature_bec( +# data, +# result, +# window_name="Feature", +# ): +# """ +# Plot feature data and indicate the detected edges and centre. +# """ - wf, text_box = select_bec_window(window_name) +# wf, text_box = select_bec_window(window_name) - feature_text = ( - f"Feature centre = {result['centre']:.5g}\n" - f"Left edge = {result['left_edge']:.5g}\n" - f"Right edge = {result['right_edge']:.5g}\n" - f"Width = {result['width']:.5g}\n" - f"Threshold = {result['threshold']:.4g}" - ) +# feature_text = ( +# f"Feature centre = {result['centre']:.5g}\n" +# f"Left edge = {result['left_edge']:.5g}\n" +# f"Right edge = {result['right_edge']:.5g}\n" +# f"Width = {result['width']:.5g}\n" +# f"Threshold = {result['threshold']:.4g}" +# ) - text_box.set_plain_text(feature_text) +# text_box.set_plain_text(feature_text) - wf.clear_all() - wf.title = f"Scan: {data['scan_number']}" - wf.x_label = data["motor_name"] - wf.y_label = data["signal_name"] +# wf.clear_all() +# # wf.title = f"Scan: {data['scan_number']}" +# wf.title = "What now??" +# wf.x_label = data["motor_name"] +# wf.y_label = data["signal_name"] - x = data["x_data"] - y = data["y_data"] +# x = data["x_data"] +# y = data["y_data"] - wf.plot( - x=x, - y=y, - label="Data", - ) +# wf.plot( +# x=x, +# y=y, +# label="Data", +# ) - ymin = np.min(y) - ymax = np.max(y) +# ymin = np.min(y) +# ymax = np.max(y) - # Horizontal threshold - wf.plot( - x=[np.min(x), np.max(x)], - y=[result["threshold"], result["threshold"]], - label="Threshold", - ) +# # Horizontal threshold +# wf.plot( +# x=[np.min(x), np.max(x)], +# y=[result["threshold"], result["threshold"]], +# label="Threshold", +# ) - # Left edge - wf.plot( - x=[result["left_edge"], result["left_edge"]], - y=[ymin, ymax], - label="Left edge", - ) +# # Left edge +# wf.plot( +# x=[result["left_edge"], result["left_edge"]], +# y=[ymin, ymax], +# label="Left edge", +# ) - # Right edge - wf.plot( - x=[result["right_edge"], result["right_edge"]], - y=[ymin, ymax], - label="Right edge", - ) +# # Right edge +# wf.plot( +# x=[result["right_edge"], result["right_edge"]], +# y=[ymin, ymax], +# label="Right edge", +# ) - # Centre - wf.plot( - x=[result["centre"], result["centre"]], - y=[ymin, ymax], - label="Centre", - ) +# # Centre +# wf.plot( +# x=[result["centre"], result["centre"]], +# y=[ymin, ymax], +# label="Centre", +# ) - # Remove symbols from guide lines - wf.get_curve("Threshold").set(symbol_size=0) - wf.get_curve("Left edge").set(symbol_size=0) - wf.get_curve("Right edge").set(symbol_size=0) - wf.get_curve("Centre").set(symbol_size=0) \ No newline at end of file +# # Remove symbols from guide lines +# wf.get_curve("Threshold").set(symbol_size=0) +# wf.get_curve("Left edge").set(symbol_size=0) +# wf.get_curve("Right edge").set(symbol_size=0) +# wf.get_curve("Centre").set(symbol_size=0) \ No newline at end of file diff --git a/pxii_bec/macros/mx_methods.py b/pxii_bec/macros/mx_methods.py index 6e4024e..0775450 100755 --- a/pxii_bec/macros/mx_methods.py +++ b/pxii_bec/macros/mx_methods.py @@ -108,6 +108,7 @@ def go_to_peak( negative: bool = False, gap: bool = False, window_name: str = "Fitting", + plot_widget = None, ): """ Go to the peak of a signal by scanning a motor within a specified range and @@ -143,7 +144,15 @@ def go_to_peak( print(f"Signal name is {signal_name}") # wf.plot(x_name=motor_name, y_name=signal_name) if plot: - plot_live_data_bec(motor_name, signal_name, window_name=window_name) + if plot_widget is not None: + plot_live_data( + plot_widget, + motor_name, + signal_name, + title=f"{motor_name} scan" + ) + else: + plot_live_data_bec(motor_name, signal_name, window_name=window_name) # Validate and calculate step size step_size = calculate_step_size(start, stop, steps) @@ -204,7 +213,7 @@ def go_to_peak( # Plot the fitted data if plot = True if plot: - plot_fitted_data_bec(data, fit_result, window_name=window_name) + plot_fitted_data_bec(data, fit_result, window_name=window_name, plot_widget = plot_widget) # If gomax is set then move to the maximum value, rather than the fit centre if gomax: @@ -368,22 +377,18 @@ def find_feature( stop: float, steps: int, feature: str = "peak", + offset=None, fraction: float = 0.5, relative: bool = FitDefaults.RELATIVE_MODE, plot: bool = True, settle: float = FitDefaults.SETTLE_TIME, confirm: bool = True, window_name: str = "Fitting", + plot_widget=None, ): """ Scan a motor, determine the centre of a high- or low-signal region, and move the motor to that position. - - feature="peak": - Centre of high-signal region, e.g. collimator. - - feature="dip": - Centre of low-signal region, e.g. beamstop. """ # Get motor and signal names @@ -395,14 +400,6 @@ def find_feature( print(f"Signal name is {signal_name}") - # Start live BEC plot - if plot: - plot_live_data_bec( - motor_name, - signal_name, - window_name=window_name, - ) - # Calculate step size step_size = calculate_step_size(start, stop, steps) @@ -427,6 +424,24 @@ def find_feature( input("Press Enter to continue...") + # ------------------------------------------------- + # Set up live plot BEFORE starting the scan + # ------------------------------------------------- + if plot: + if plot_widget is not None: + plot_live_data( + plot_widget, + motor_name, + signal_name, + title=f"{motor_name} scan", + ) + else: + plot_live_data_bec( + motor_name, + signal_name, + window_name=window_name, + ) + # Perform scan scan_result = scans.line_scan( motor_device, @@ -456,25 +471,30 @@ def find_feature( } # Find feature centre - result = find_feature_centre( - data["x_data"], - data["y_data"], - feature=feature, - fraction=fraction, - ) - - print(f"\nLeft edge = {result['left_edge']:.4f}") - print(f"Right edge = {result['right_edge']:.4f}") - print(f"Width = {result['width']:.4f}") - print(f"Centre = {result['centre']:.4f}") - - # Add feature information to BEC plot - if plot: - plot_feature_bec( - data, - result, - window_name=window_name, + if feature in ("peak", "dip"): + result = find_feature_centre( + data["x_data"], + data["y_data"], + feature=feature, + fraction=fraction, ) + elif feature == "edge_offset": + result = find_edge_offset( + data["x_data"], + data["y_data"], + feature=feature, + offset=offset, + ) + else: + raise ValueError(f"Unknown feature type {feature}") + if feature in ("peak", "dip"): + print(f"\nLeft edge = {result['left_edge']:.4f}") + print(f"Right edge = {result['right_edge']:.4f}") + print(f"Width = {result['width']:.4f}") + print(f"Centre = {result['centre']:.4f}") + elif feature == "edge_offset": + print(f"Edge = {result['edge']:.4f}") + print(f"Centre = {result['centre']:.4f}") # Move safely to centre move_to_position( @@ -484,10 +504,69 @@ def find_feature( data, ) + if plot and plot_widget is not None: + # Replace the live subscription with static completed scan data + plot_widget.clear_all() + + plot_widget.plot( + x=data["x_data"], + y=data["y_data"], + label="Scan data", + ) + # Mark centre + + plot_widget.plot( + x=[result["centre"], result["centre"]], + y=[ + np.min(data["y_data"]), + np.max(data["y_data"]), + ], + label="Centre", + ) + + + plot_widget.title = f"{motor_name} scan" + plot_widget.x_label = motor_name + plot_widget.y_label = signal_name + return result -import numpy as np +def find_edge_offset(x, y, fraction=0.5, feature="edge_offset", offset=0.0): + """ + Find a single signal edge and calculate a target position + at a known offset from that edge. + """ + x = np.asarray(x) + y = np.asarray(y) + + y_min = np.min(y) + y_max = np.max(y) + + threshold = y_min + fraction * (y_max - y_min) + + # Find threshold crossings + crossings = np.where(np.diff(np.sign(y - threshold)) != 0)[0] + + if len(crossings) == 0: + raise RuntimeError("No edge found in scan") + + # If we expect only one useful edge, use the strongest/first one + i = crossings[0] + + # Linear interpolation for more accurate edge position + x1, x2 = x[i], x[i + 1] + y1, y2 = y[i], y[i + 1] + + edge = x1 + (threshold - y1) * (x2 - x1) / (y2 - y1) + + centre = edge + offset + + return { + "edge": edge, + "centre": centre, + "threshold": threshold, + } def find_feature_centre(x, y, feature="peak", fraction=0.5): """