updated plotting of scans and added the option to fit features
CI for pxii_bec / test (push) Successful in 37s

This commit is contained in:
x10sa
2026-08-25 09:52:33 +02:00
parent 06bcf6df5e
commit 229b98bd36
2 changed files with 195 additions and 96 deletions
+81 -61
View File
@@ -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)
# # 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)
+114 -35
View File
@@ -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):
"""