From bd85d5e673509ac076f56141aa0273890302147f Mon Sep 17 00:00:00 2001 From: x12sa Date: Sun, 20 Sep 2026 11:03:06 +0200 Subject: [PATCH] fix(xrayeye): make DAP sine fit converge by fixing start values and signed amplitude The xray eye alignment fit (A*sin(f*x+shift)+intercept, 5 points) relied on lmfit's SineModel.guess() for the start values. Its FFT-based shift guess is arbitrary for 5 points and often left the fit stuck at the shift bound (2*pi) far from the data, so the plotted fit missed the submitted points and wrong parameters were loaded into flomni. Override all five parameters so the DAP server skips guess(): signed amplitude (start 30), shift start 0 within +-pi, intercept start 0, frequency and slope still fixed. Shared by the x and y waveforms. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj --- .../bec_widgets/widgets/xray_eye/x_ray_eye.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index f11a4a54..1263764e 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -34,6 +34,26 @@ from qtpy.QtWidgets import ( logger = bec_logger.logger CAMERA = ("cam_xeye", "image") +# Start values / bounds for the DAP fit A*sin(f*x + shift) + intercept of the +# alignment offsets vs. angle (5 points, 0..180 deg, f fixed to 1 deg in rad). +# All five parameters are given so the DAP server does not use lmfit's +# SineModel.guess(): its FFT-based shift guess is arbitrary for 5 points and +# regularly left the fit stuck at the shift bound (2*pi) far from the data. +# A signed amplitude (SineModel defaults to A >= 0) plus a fixed start +# (A=30, shift=0, intercept=0) converged to the global least-squares optimum +# in 1500/1500 random test datasets; A >= 0 failed in a few percent. +SINE_FIT_DAP_PARAMETERS = [ + { + "amplitude": {"value": 30, "vary": True, "min": -1e6, "max": 1e6}, + "frequency": {"value": 0.0174533, "vary": False, "min": 0.01, "max": 0.02}, + "shift": {"value": 0, "vary": True, "min": -math.pi, "max": math.pi}, + }, + { + "slope": {"value": 0, "vary": False, "min": 0.0, "max": 0.02}, + "intercept": {"value": 0, "vary": True, "min": -1e6, "max": 1e6}, + }, +] + class TargetCrosshair: """ @@ -615,10 +635,7 @@ class OMNY_XRayEye(BECWidget, QWidget): y=[1], label="fit-x", dap=["SineModel", "LinearModel"], - dap_parameters=[ - {"frequency": {"value": 0.0174533, "vary": False, "min": 0.01, "max": 0.02}}, - {"slope": {"value": 0, "vary": False, "min": 0.0, "max": 0.02}}, - ], + dap_parameters=SINE_FIT_DAP_PARAMETERS, dap_oversample=5, ) self.waveform_y.plot( @@ -626,10 +643,7 @@ class OMNY_XRayEye(BECWidget, QWidget): y=[2], label="fit-y", dap=["SineModel", "LinearModel"], - dap_parameters=[ - {"frequency": {"value": 0.0174533, "vary": False, "min": 0.01, "max": 0.02}}, - {"slope": {"value": 0, "vary": False, "min": 0.0, "max": 0.02}}, - ], + dap_parameters=SINE_FIT_DAP_PARAMETERS, dap_oversample=5, ) self.fit_x = self.waveform_x.curves[0]