diff --git a/pxii_bec/macros/mxcomm_analysis.py b/pxii_bec/macros/mxcomm_analysis.py index 7662d71..5bd2805 100644 --- a/pxii_bec/macros/mxcomm_analysis.py +++ b/pxii_bec/macros/mxcomm_analysis.py @@ -151,7 +151,7 @@ def fit(data, fit_params): } -def check_peak_fit(x_data, result): +def check_peak_fit(x_data, result, min_fwhm_steps=3): """Check whether a fitted peak is reliable Parameters: X data, results from the fit step Returns: success or problems""" @@ -166,13 +166,19 @@ def check_peak_fit(x_data, result): centre = result["centre"] fwhm = result["fwhm"] + step_size = np.mean(np.diff(x_data)) + fwhm_steps = result["fwhm"] / abs(step_size) + problems = [] if not x_min + 0.1 * scan_range < centre < x_max - 0.1 * scan_range: problems.append("peak centre is too close to scan edge") - if fwhm < 3 * step: - problems.append("peak is narrower than 3 scan steps") + if fwhm < min_fwhm_steps * step: + problems.append( + f"Peak FWHM spans only {fwhm_steps:.2f} scan steps " + f"(minimum {min_fwhm_steps})" + ) if fwhm > 0.7 * scan_range: problems.append("peak is too broad for scan range") diff --git a/pxii_bec/macros/mxcomm_plot.py b/pxii_bec/macros/mxcomm_plot.py index ab9b7b5..7431ff3 100644 --- a/pxii_bec/macros/mxcomm_plot.py +++ b/pxii_bec/macros/mxcomm_plot.py @@ -76,7 +76,7 @@ def plot_fitted_data(data, fit_result, window_name="Fitting", plot_widget=None, f"FWHM = {fit_result['fwhm']:.5f}\n" f"Height = {fit_result['height']:.4g}\n" f"Model = {fit_result['model']}\n" - f"Chi sq = {fit_result['chi_sq']:.3g}\n" + f"Centre_StdErr = {fit_result['centre_stderr']:.3g}\n" f"SNR = {fit_result['snr']:.3g}" ) diff --git a/pxii_bec/macros/mxcomm_scan.py b/pxii_bec/macros/mxcomm_scan.py index ff38f04..c007e2f 100755 --- a/pxii_bec/macros/mxcomm_scan.py +++ b/pxii_bec/macros/mxcomm_scan.py @@ -117,87 +117,161 @@ def go_to_peak( gomax: bool = False, negative: bool = False, gap: bool = False, + min_fwhm_steps: float = 3, window_name: str = "Fitting", plot_widget=None, ): """ - Go to the peak of a signal by scanning a motor within a specified range and - identifying the optimal position based on signal peak data. + Scan a motor and move either to the fitted peak centre or the + highest measured data point. - Parameters: - motor_device: The motor device to be scanned. - signal_device: The signal device to monitor during the scan. - start (float): The starting position of the scan. Ignored if `relative` is True. - stop (float): The ending position of the scan. Ignored if `relative` is True. - steps (int): The number of steps to divide the scan range into. - relative (bool, optional): If True, interpret `start` and `stop` as relative to - the current motor position. Defaults to RELATIVE_MODE constant. - plot (bool, optional): If True, plot the scan data and the fitted results. - Defaults to True. - settle (float, optional): The time in seconds to wait after each step for the - signal to stabilize. Defaults to DEFAULT_SETTLE_TIME constant. - confirm (bool, optional): If True, ask for user confirmation before starting - the scan. Defaults to True. + If gomax=False: + Fit the scan, validate the fit using check_peak_fit(), + and move to the fitted centre. - Raises: - Exception: Raises exceptions potentially raised by dependent functions or - operations such as plotting, fitting, or motor movement. + If gomax=True: + Skip peak-fit validation and move directly to the highest + measured signal value. Information about whether the maximum + occurred at a scan boundary is returned. - Returns: - fit parameters + Parameters + ---------- + motor_device + Motor device to scan. + signal_device + Signal device to monitor. + start : float + Scan start position. + stop : float + Scan end position. + steps : int + Number of scan steps. + relative : bool + If True, start and stop are relative to current motor position. + plot : bool + Plot scan data/results. + settle : float + Settling time at each scan point. + confirm : bool + Ask for confirmation before starting scan. + gomax : bool + If True, move to highest measured point rather than fitted centre. + negative : bool + Fit a negative-going peak. + gap : bool + Apply minimum-gap protection when using fitted peak position. + min_fwhm_steps : float + Minimum allowed fitted FWHM in scan steps. Only used when + gomax=False. + window_name : str + BEC GUI window name. + plot_widget + Optional explicit plot widget. + + Returns + ------- + dict + Result information. + + For a successful fitted peak: + success + problems + centre + fwhm + height + chi_sq + centre_stderr + snr + + For gomax=True: + success + problems + x_max + max_index + at_lower_boundary + at_upper_boundary + + For a rejected fit: + success=False + problems=list of fit rejection reasons """ + + # ------------------------------------------------------------ # Get motor and signal names + # ------------------------------------------------------------ + mres = get_value(motor_device, True) motor_name = mres["root"] + sres = get_value(signal_device, True) signal_name = sres["root"] + print(f"Signal name is {signal_name}") - # 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) + # ------------------------------------------------------------ + # Set up live plotting + # ------------------------------------------------------------ if plot: plot_live_data( - motor_name, + motor_name, signal_name, window_name=window_name, - plot_widget=plot_widget, - title=f"{motor_name} scan") + plot_widget=plot_widget, + title=f"{motor_name} scan", + ) + + # ------------------------------------------------------------ + # Validate scan range and get starting position + # ------------------------------------------------------------ - # Validate and calculate step size step_size = calculate_step_size(start, stop, steps) - # Confirm the scan range starting_position = mres["value"] + print(f"Starting motor position = {starting_position}") if confirm: if relative: scan_start = starting_position + start scan_end = starting_position + stop + print( - f"\nScanning from {scan_start: .6g} to {scan_end: .6g} in " - f"{steps} steps of size {step_size}" + f"\nScanning from {scan_start:.6g} to {scan_end:.6g} " + f"in {steps} steps of size {step_size}" ) print(f"Relative mode = {relative}") + else: print( - f"\nScanning from {start: .5g} to {stop: .5g} in {steps} steps of size {step_size}" + f"\nScanning from {start:.5g} to {stop:.5g} " + f"in {steps} steps of size {step_size}" ) print(f"Relative mode = {relative}") + input("Press Enter to continue...") - # Perform the scan + # ------------------------------------------------------------ + # Perform scan + # ------------------------------------------------------------ + scan_result = scans.line_scan( - motor_device, start, stop, steps=steps, relative=relative, settling_time=settle + motor_device, + start, + stop, + steps=steps, + relative=relative, + settling_time=settle, ) - motor_data = scan_result.scan.live_data[mres["live_root"]][mres["name"]].val - # # motor_data = scan_result.scan.live_data[motor_name][motor_name].val - # sres = get_value(signal_name, True) - signal_data = scan_result.scan.live_data[sres["root"]][sres["name"]].val + + motor_data = ( + scan_result.scan.live_data[mres["live_root"]][mres["name"]].val + ) + + signal_data = ( + scan_result.scan.live_data[sres["root"]][sres["name"]].val + ) + scan_number = "Current" data = { @@ -209,16 +283,87 @@ def go_to_peak( "scan_number": scan_number, } - # Define and fit model to scan data + # ============================================================ + # GOMAX MODE + # ============================================================ + # + # Do not fit or apply peak-shape validation. + # Simply use the highest measured point. + # + + if gomax: + + if negative: + max_index = int(np.argmin(data["y_data"])) + else: + max_index = int(np.argmax(data["y_data"])) + + x_max = float(data["x_data"][max_index]) + + at_lower_boundary = max_index == 0 + at_upper_boundary = max_index == len(data["x_data"]) - 1 + + print(f"Maximum measured signal at {x_max:.6g}") + + if at_lower_boundary: + print("Maximum is at the lower scan boundary") + + if at_upper_boundary: + print("Maximum is at the upper scan boundary") + + # Plot raw scan rather than trying to assess a fitted peak + if plot: + plot_scan_data( + data, + window_name=window_name, + plot_widget=plot_widget, + title=f"{motor_name} scan - maximum at {x_max:.6g}", + ) + + # Move to measured maximum + move_to_scan_position( + data["motor_device"], + data["motor_name"], + x_max, + data, + ) + + return { + "success": True, + "problems": [], + "x_max": x_max, + "max_index": max_index, + "at_lower_boundary": at_lower_boundary, + "at_upper_boundary": at_upper_boundary, + } + + # ============================================================ + # FITTED PEAK MODE + # ============================================================ + fit_params = create_fit_parameters( - deriv=False, negative=False, model=FitDefaults.MODEL, baseline=FitDefaults.BASELINE + deriv=False, + negative=negative, + model=FitDefaults.MODEL, + baseline=FitDefaults.BASELINE, ) + fit_result = fit(data, fit_params) - check = check_peak_fit(data["x_data"], fit_result) + # ------------------------------------------------------------ + # Validate fitted peak + # ------------------------------------------------------------ + + check = check_peak_fit( + data["x_data"], + fit_result, + min_fwhm_steps=min_fwhm_steps, + ) if not check["success"]: + print("Peak fit rejected:") + for problem in check["problems"]: print(f" - {problem}") @@ -229,42 +374,67 @@ def go_to_peak( plot_widget=plot_widget, title=f"{motor_name} scan - FIT REJECTED", ) - print(f"Returning motor to start position, {starting_position:.5g}") + + print( + f"Returning motor to start position, " + f"{starting_position:.5g}" + ) + restore_position( motor_device, starting_position, ) - return None + return { + "success": False, + "problems": check["problems"], + } + # ------------------------------------------------------------ + # Plot accepted fit + # ------------------------------------------------------------ - - # Plot the fitted data if plot = True if plot: - plot_fitted_data(data, fit_result, window_name=window_name, plot_widget=plot_widget) + plot_fitted_data( + data, + fit_result, + window_name=window_name, + plot_widget=plot_widget, + ) + + # ------------------------------------------------------------ + # Move to fitted peak centre + # ------------------------------------------------------------ + + centre = fit_result["centre"] + + if gap and centre < 4.5: + print( + f"Fitted gap position {centre:.5g} mm is below " + "minimum gap of 4.5 mm" + ) + umv(dev.id_gap, 4.5) - # If gomax is set then move to the maximum value, rather than the fit centre - if gomax: - value = fit_result["x_max"] - print(f"Max position is at {value}") - move_to_scan_position(data["motor_device"], data["motor_name"], fit_result["x_max"], data) else: - if gap: - if fit_result["centre"] < 4.5: - umv(dev.id_gap, 4.5) - else: - move_to_scan_position( - data["motor_device"], data["motor_name"], fit_result["centre"], data - ) - else: - # Safely move the motor to the peak position - move_to_scan_position(data["motor_device"], data["motor_name"], fit_result["centre"], data) + move_to_scan_position( + data["motor_device"], + data["motor_name"], + centre, + data, + ) + + # ------------------------------------------------------------ + # Return successful fit information + # ------------------------------------------------------------ return { - "centre": fit_result["centre"], + "success": True, + "problems": [], + "centre": centre, "fwhm": float(fit_result["fwhm"]), "height": float(fit_result["height"]), "chi_sq": float(fit_result["chi_sq"]), + "centre_stderr": float(fit_result["centre_stderr"]), "snr": float(fit_result["snr"]), }