@@ -125,6 +125,7 @@ class FitParamDataSet(DataSet):
125125 format = StringItem (_ ("Format" ), default = "%.3f" ).set_pos (col = 1 )
126126 logscale = BoolItem (_ ("Logarithmic" ), _ ("Scale" ))
127127 unit = StringItem (_ ("Unit" ), default = "" ).set_pos (col = 1 )
128+ locked = BoolItem (_ ("Fixed value during optimization" ), _ ("Lock" ))
128129
129130
130131class FitParam :
@@ -140,6 +141,8 @@ class FitParam:
140141 format: format of the parameter. Default is "%.3f".
141142 size_offset: size offset of the parameter. Default is 0.
142143 unit: unit of the parameter. Default is "".
144+ locked: if True, the parameter value is locked and will not be modified
145+ by the automatic fit. Default is False.
143146 """
144147
145148 def __init__ (
@@ -153,6 +156,7 @@ def __init__(
153156 format : str = "%.3f" ,
154157 size_offset : int = 0 ,
155158 unit : str = "" ,
159+ locked : bool = False ,
156160 ):
157161 self .name = name
158162 self .value = value
@@ -162,6 +166,7 @@ def __init__(
162166 self .steps = steps
163167 self .format = format
164168 self .unit = unit
169+ self .locked = locked
165170 self .prefix_label = None
166171 self .lineedit = None
167172 self .unit_label = None
@@ -188,6 +193,7 @@ def copy(self) -> FitParam:
188193 self .format ,
189194 self ._size_offset ,
190195 self .unit ,
196+ self .locked ,
191197 )
192198
193199 def create_widgets (self , parent : QWidget , refresh_callback : Callable ) -> None :
@@ -258,6 +264,8 @@ def set_text(self, fmt: str = None) -> None:
258264 fmt: format (default: None)
259265 """
260266 style = "<span style='color: #444444'><b>{}</b></span>"
267+ if self .locked :
268+ style = "<span style='color: #888888'><b>{} 🔒</b></span>"
261269 self .prefix_label .setText (style .format (self .name ))
262270 if self .value is None :
263271 value_str = ""
@@ -266,7 +274,10 @@ def set_text(self, fmt: str = None) -> None:
266274 fmt = self .format
267275 value_str = fmt % self .value
268276 self .lineedit .setText (value_str )
269- self .lineedit .setDisabled (bool (self .value == self .min and self .max == self .min ))
277+ is_disabled = bool (
278+ self .locked or (self .value == self .min and self .max == self .min )
279+ )
280+ self .lineedit .setDisabled (is_disabled )
270281
271282 def line_editing_finished (self ):
272283 """Line editing finished"""
@@ -303,7 +314,7 @@ def update_slider_value(self):
303314 elif self .value == self .min and self .max == self .min :
304315 self .slider .hide ()
305316 else :
306- self .slider .setEnabled (True )
317+ self .slider .setEnabled (not self . locked )
307318 if self .slider .parentWidget () and self .slider .parentWidget ().isVisible ():
308319 self .slider .show ()
309320 if self .logscale :
@@ -666,40 +677,73 @@ def compute_imin_imax(self) -> None:
666677 self .i_min = self .x .searchsorted (self .autofit_prm .xmin )
667678 self .i_max = self .x .searchsorted (self .autofit_prm .xmax , side = "right" )
668679
669- def errorfunc (self , params : list [float ]) -> np .ndarray :
680+ def get_full_params (self , free_params : np .ndarray ) -> list [float ]:
681+ """Build full parameter list from free parameters
682+
683+ Args:
684+ free_params: values of unlocked parameters only
685+
686+ Returns:
687+ Full parameter list with locked parameters at their fixed values
688+ """
689+ full_params = []
690+ free_idx = 0
691+ for p in self .fitparams :
692+ if p .locked :
693+ full_params .append (p .value )
694+ else :
695+ full_params .append (free_params [free_idx ])
696+ free_idx += 1
697+ return full_params
698+
699+ def errorfunc (self , free_params : np .ndarray ) -> np .ndarray :
670700 """Get error function
671701
672702 Args:
673- params: fit parameter values
703+ free_params: values of unlocked fit parameters
674704
675705 Returns:
676706 Error function
677707 """
678708 x = self .x [self .i_min : self .i_max ]
679709 y = self .y [self .i_min : self .i_max ]
680710 fitargs , fitkwargs = self .get_fitfunc_arguments ()
711+ params = self .get_full_params (free_params )
681712 return y - self .fitfunc (x , params , * fitargs , ** fitkwargs )
682713
683714 def autofit (self ) -> None :
684715 """Autofit"""
685716 meth = self .autofit_prm .method
686- x0 = np .array ([p .value for p in self .fitparams ])
717+
718+ # Extract only unlocked parameters for optimization
719+ free_params = np .array ([p .value for p in self .fitparams if not p .locked ])
720+
721+ # If all parameters are locked, nothing to optimize
722+ if len (free_params ) == 0 :
723+ return
724+
687725 if meth == "lq" :
688- x = self .autofit_lq (x0 )
726+ x = self .autofit_lq (free_params )
689727 elif meth == "simplex" :
690- x = self .autofit_simplex (x0 )
728+ x = self .autofit_simplex (free_params )
691729 elif meth == "powel" :
692- x = self .autofit_powel (x0 )
730+ x = self .autofit_powel (free_params )
693731 elif meth == "bfgs" :
694- x = self .autofit_bfgs (x0 )
732+ x = self .autofit_bfgs (free_params )
695733 elif meth == "l_bfgs_b" :
696- x = self .autofit_l_bfgs (x0 )
734+ x = self .autofit_l_bfgs (free_params )
697735 elif meth == "cg" :
698- x = self .autofit_cg (x0 )
736+ x = self .autofit_cg (free_params )
699737 else :
700738 return
701- for v , p in zip (x , self .fitparams ):
702- p .value = v
739+
740+ # Restore optimized values only to unlocked parameters
741+ free_idx = 0
742+ for p in self .fitparams :
743+ if not p .locked :
744+ p .value = x [free_idx ]
745+ free_idx += 1
746+
703747 self .refresh ()
704748 for prm in self .fitparams :
705749 prm .update ()
@@ -776,7 +820,8 @@ def autofit_l_bfgs(self, x0: np.ndarray) -> np.ndarray:
776820 Fitted values
777821 """
778822 prm = self .autofit_prm
779- bounds = [(p .min , p .max ) for p in self .fitparams ]
823+ # Build bounds only for unlocked parameters
824+ bounds = [(p .min , p .max ) for p in self .fitparams if not p .locked ]
780825
781826 x , _f , _d = fmin_l_bfgs_b (
782827 self .get_norm_func (), x0 , pgtol = prm .gtol , approx_grad = 1 , bounds = bounds
0 commit comments