ChessAnalysisPipeline 0.0.11__py3-none-any.whl → 0.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ChessAnalysisPipeline might be problematic. Click here for more details.

CHAP/utils/fit.py CHANGED
@@ -118,7 +118,9 @@ class Fit:
118
118
  self._result = None
119
119
  self._try_linear_fit = True
120
120
  self._param_constraint = None
121
+ self._fwhm_min = None
121
122
  self._fwhm_max = None
123
+ self._sigma_min = None
122
124
  self._sigma_max = None
123
125
  self._y = None
124
126
  self._y_norm = None
@@ -851,12 +853,20 @@ class Fit:
851
853
 
852
854
  def create_multipeak_model(
853
855
  self, centers=None, fit_type=None, peak_models=None,
854
- center_exprs=None, background=None, param_constraint=False,
855
- fwhm_max=None):
856
+ center_exprs=None, background=None, param_constraint=True,
857
+ fwhm_min=None, fwhm_max=None, centers_range=None):
856
858
  """Create a multipeak model."""
857
859
  # System modules
858
860
  from re import search as re_search
859
861
 
862
+ # Third party modules
863
+ from asteval import Interpreter
864
+
865
+ if centers_range is None:
866
+ centers_range = (self._x[0], self._x[-1])
867
+ elif not is_index_range(centers_range, ge=self._x[0], le=self._x[-1]):
868
+ raise ValueError(
869
+ f'Invalid parameter centers_range ({centers_range})')
860
870
  if self._model is not None:
861
871
  if self._fit_type == 'uniform' and fit_type != 'uniform':
862
872
  logger.info('Use the existing multipeak model to refit a '
@@ -872,10 +882,12 @@ class Fit:
872
882
  self._best_errors, scale_factor_index, 0)
873
883
  for name, par in self._parameters.items():
874
884
  if re_search('peak\d+_center', name) is not None:
875
- par.set(min=min_value, vary=True, expr=None)
885
+ par.set(
886
+ min=centers_range[0], max=centers_range[1],
887
+ vary=True, expr=None)
876
888
  self._parameter_bounds[name] = {
877
- 'min': min_value,
878
- 'max': np.inf,
889
+ 'min': centers_range[0],
890
+ 'max': centers_range[1],
879
891
  }
880
892
  else:
881
893
  for name, par in self._parameters.items():
@@ -942,11 +954,15 @@ class Fit:
942
954
  raise ValueError(
943
955
  f'Invalid parameter fit_type ({fit_type})')
944
956
  self._fit_type = fit_type
957
+ self._fwhm_min = fwhm_min
945
958
  self._fwhm_max = fwhm_max
959
+ self._sigma_min = None
946
960
  self._sigma_max = None
947
961
  if param_constraint:
948
962
  self._param_constraint = True
949
963
  min_value = FLOAT_MIN
964
+ if self._fwhm_min is not None:
965
+ self._sigma_min = np.zeros(num_peaks)
950
966
  if self._fwhm_max is not None:
951
967
  self._sigma_max = np.zeros(num_peaks)
952
968
  else:
@@ -1045,7 +1061,13 @@ class Fit:
1045
1061
  f'Invalid parameter background ({background})')
1046
1062
 
1047
1063
  # Add peaks and set initial fit parameters
1064
+ ast = Interpreter()
1048
1065
  if num_peaks == 1:
1066
+ sig_min = None
1067
+ if self._sigma_min is not None:
1068
+ ast(f'fwhm = {self._fwhm_min}')
1069
+ sig_min = ast(fwhm_factor[peak_models[0]])
1070
+ self._sigma_min[0] = sig_min
1049
1071
  sig_max = None
1050
1072
  if self._sigma_max is not None:
1051
1073
  ast(f'fwhm = {self._fwhm_max}')
@@ -1055,14 +1077,20 @@ class Fit:
1055
1077
  peak_models[0],
1056
1078
  parameters=(
1057
1079
  {'name': 'amplitude', 'min': min_value},
1058
- {'name': 'center', 'value': centers[0], 'min': min_value},
1059
- {'name': 'sigma', 'min': min_value, 'max': sig_max},
1080
+ {'name': 'center', 'value': centers[0],
1081
+ 'min': centers_range[0], 'max': centers_range[1]},
1082
+ {'name': 'sigma', 'min': sig_min, 'max': sig_max},
1060
1083
  ))
1061
1084
  else:
1062
1085
  if fit_type == 'uniform':
1063
1086
  self.add_parameter(
1064
1087
  name='scale_factor', value=1.0, min=min_value)
1065
1088
  for i in range(num_peaks):
1089
+ sig_min = None
1090
+ if self._sigma_min is not None:
1091
+ ast(f'fwhm = {self._fwhm_min}')
1092
+ sig_min = ast(fwhm_factor[peak_models[i]])
1093
+ self._sigma_min[i] = sig_min
1066
1094
  sig_max = None
1067
1095
  if self._sigma_max is not None:
1068
1096
  ast(f'fwhm = {self._fwhm_max}')
@@ -1074,8 +1102,7 @@ class Fit:
1074
1102
  parameters=(
1075
1103
  {'name': 'amplitude', 'min': min_value},
1076
1104
  {'name': 'center', 'expr': center_exprs[i]},
1077
- {'name': 'sigma', 'min': min_value,
1078
- 'max': sig_max},
1105
+ {'name': 'sigma', 'min': sig_min, 'max': sig_max},
1079
1106
  ))
1080
1107
  else:
1081
1108
  self.add_model(
@@ -1084,7 +1111,7 @@ class Fit:
1084
1111
  parameters=(
1085
1112
  {'name': 'amplitude', 'min': min_value},
1086
1113
  {'name': 'center', 'value': centers[i],
1087
- 'min': min_value},
1114
+ 'min': centers_range[0], 'max': centers_range[1]},
1088
1115
  {'name': 'sigma', 'min': min_value,
1089
1116
  'max': sig_max},
1090
1117
  ))
@@ -1102,7 +1129,7 @@ class Fit:
1102
1129
  # Third party modules
1103
1130
  from asteval import Interpreter
1104
1131
 
1105
- # Check inputs
1132
+ # Check input parameters
1106
1133
  if self._model is None:
1107
1134
  logger.error('Undefined fit model')
1108
1135
  return None
@@ -1121,6 +1148,11 @@ class Fit:
1121
1148
  f'Invalid value of keyword argument guess ({guess})')
1122
1149
  else:
1123
1150
  guess = False
1151
+ if self._result is not None:
1152
+ if guess:
1153
+ logger.warning(
1154
+ 'Ignoring input parameter guess during refitting')
1155
+ guess = False
1124
1156
  if 'try_linear_fit' in kwargs:
1125
1157
  try_linear_fit = kwargs.pop('try_linear_fit')
1126
1158
  if not isinstance(try_linear_fit, bool):
@@ -1133,16 +1165,6 @@ class Fit:
1133
1165
  '(not yet supported for callable models)')
1134
1166
  else:
1135
1167
  self._try_linear_fit = try_linear_fit
1136
- if self._result is not None:
1137
- if guess:
1138
- logger.warning(
1139
- 'Ignoring input parameter guess during refitting')
1140
- guess = False
1141
-
1142
- # Check for circular expressions
1143
- # RV
1144
- # for name1, par1 in self._parameters.items():
1145
- # if par1.expr is not None:
1146
1168
 
1147
1169
  # Apply mask if supplied:
1148
1170
  if 'mask' in kwargs:
@@ -1171,14 +1193,17 @@ class Fit:
1171
1193
  # Should work for other peak-like models,
1172
1194
  # but will need tests first
1173
1195
  for component in self._model.components:
1174
- if component._name == 'gaussian':
1196
+ if isinstance(component, GaussianModel):
1175
1197
  center = self._parameters[
1176
1198
  f"{component.prefix}center"].value
1177
1199
  height_init, cen_init, fwhm_init = \
1178
1200
  self.guess_init_peak(
1179
1201
  xx, yy, center_guess=center,
1180
1202
  use_max_for_center=False)
1181
- if (self._fwhm_max is not None
1203
+ if (self._fwhm_min is not None
1204
+ and fwhm_init < self._fwhm_min):
1205
+ fwhm_init = self._fwhm_min
1206
+ elif (self._fwhm_max is not None
1182
1207
  and fwhm_init > self._fwhm_max):
1183
1208
  fwhm_init = self._fwhm_max
1184
1209
  ast(f'fwhm = {fwhm_init}')
@@ -1292,9 +1317,7 @@ class Fit:
1292
1317
  self._parameter_bounds = {
1293
1318
  name:{'min': par.min, 'max': par.max}
1294
1319
  for name, par in self._parameters.items() if par.vary}
1295
- for par in self._parameters.values():
1296
- if par.vary:
1297
- par.set(value=self._reset_par_at_boundary(par, par.value))
1320
+ self._reset_par_at_boundary()
1298
1321
 
1299
1322
  # Perform the fit
1300
1323
  fit_kws = None
@@ -1842,39 +1865,39 @@ class Fit:
1842
1865
  if self._result.residual is not None:
1843
1866
  self._result.residual *= self._norm[1]
1844
1867
 
1845
- def _reset_par_at_boundary(self, par, value):
1846
- assert par.vary
1847
- name = par.name
1848
- _min = self._parameter_bounds[name]['min']
1849
- _max = self._parameter_bounds[name]['max']
1850
- if np.isinf(_min):
1851
- if not np.isinf(_max):
1852
- if self._parameter_norms.get(name, False):
1853
- upp = _max-0.1*self._y_range
1854
- elif _max == 0.0:
1855
- upp = _max-0.1
1856
- else:
1857
- upp = _max-0.1*abs(_max)
1858
- if value >= upp:
1859
- return upp
1860
- else:
1861
- if np.isinf(_max):
1862
- if self._parameter_norms.get(name, False):
1863
- low = _min + 0.1*self._y_range
1864
- elif _min == 0.0:
1865
- low = _min+0.1
1868
+ def _reset_par_at_boundary(self):
1869
+ for name, par in self._parameters.items():
1870
+ if par.vary:
1871
+ value = par.value
1872
+ _min = self._parameter_bounds[name]['min']
1873
+ _max = self._parameter_bounds[name]['max']
1874
+ if np.isinf(_min):
1875
+ if not np.isinf(_max):
1876
+ if self._parameter_norms.get(name, False):
1877
+ upp = _max-0.1*self._y_range
1878
+ elif _max == 0.0:
1879
+ upp = _max-0.1
1880
+ else:
1881
+ upp = _max-0.1*abs(_max)
1882
+ if value >= upp:
1883
+ par.set(value=upp)
1866
1884
  else:
1867
- low = _min + 0.1*abs(_min)
1868
- if value <= low:
1869
- return low
1870
- else:
1871
- low = 0.9*_min + 0.1*_max
1872
- upp = 0.1*_min + 0.9*_max
1873
- if value <= low:
1874
- return low
1875
- if value >= upp:
1876
- return upp
1877
- return value
1885
+ if np.isinf(_max):
1886
+ if self._parameter_norms.get(name, False):
1887
+ low = _min + 0.1*self._y_range
1888
+ elif _min == 0.0:
1889
+ low = _min+0.1
1890
+ else:
1891
+ low = _min + 0.1*abs(_min)
1892
+ if value <= low:
1893
+ par.set(value=low)
1894
+ else:
1895
+ low = 0.9*_min + 0.1*_max
1896
+ upp = 0.1*_min + 0.9*_max
1897
+ if value <= low:
1898
+ par.set(value=low)
1899
+ if value >= upp:
1900
+ par.set(value=upp)
1878
1901
 
1879
1902
 
1880
1903
  class FitMap(Fit):
@@ -1917,7 +1940,7 @@ class FitMap(Fit):
1917
1940
  raise ValueError('Invalid parameter ymap ({ymap})')
1918
1941
  self._ymap = ymap
1919
1942
 
1920
- # Verify the input parameters
1943
+ # Check input parameters
1921
1944
  if self._x.ndim != 1:
1922
1945
  raise ValueError(f'Invalid dimension for input x {self._x.ndim}')
1923
1946
  if self._ymap.ndim < 2:
@@ -2314,7 +2337,7 @@ class FitMap(Fit):
2314
2337
  logger.warning(
2315
2338
  f'The requested number of processors ({num_proc}) exceeds the '
2316
2339
  'maximum number of processors, num_proc reduced to '
2317
- f'({cpu_count()})')
2340
+ f'{cpu_count()}')
2318
2341
  num_proc = cpu_count()
2319
2342
  if 'try_no_bounds' in kwargs:
2320
2343
  self._try_no_bounds = kwargs.pop('try_no_bounds')
@@ -2469,9 +2492,7 @@ class FitMap(Fit):
2469
2492
  self._parameter_bounds = {
2470
2493
  name:{'min': par.min, 'max': par.max}
2471
2494
  for name, par in self._parameters.items() if par.vary}
2472
- for name, par in self._parameters.items():
2473
- if par.vary:
2474
- par.set(value=self._reset_par_at_boundary(par, par.value))
2495
+ self._reset_par_at_boundary()
2475
2496
 
2476
2497
  # Set parameter bounds to unbound
2477
2498
  # (only use bounds when fit fails)
@@ -2587,7 +2608,7 @@ class FitMap(Fit):
2587
2608
  if num_proc > num_fit:
2588
2609
  logger.warning(
2589
2610
  f'The requested number of processors ({num_proc}) exceeds '
2590
- f'the number of fits, num_proc reduced to ({num_fit})')
2611
+ f'the number of fits, num_proc reduced to {num_fit}')
2591
2612
  num_proc = num_fit
2592
2613
  num_fit_per_proc = 1
2593
2614
  else:
@@ -2684,23 +2705,112 @@ class FitMap(Fit):
2684
2705
  self._fit(n_start+n, current_best_values, **kwargs)
2685
2706
 
2686
2707
  def _fit(self, n, current_best_values, return_result=False, **kwargs):
2708
+ # Check input parameters
2709
+ if 'rel_amplitude_cutoff' in kwargs:
2710
+ rel_amplitude_cutoff = kwargs.pop('rel_amplitude_cutoff')
2711
+ if (rel_amplitude_cutoff is not None
2712
+ and not is_num(rel_amplitude_cutoff, gt=0.0, lt=1.0)):
2713
+ logger.warning(
2714
+ 'Ignoring invalid parameter rel_amplitude_cutoff '
2715
+ f'in FitMap._fit() ({rel_amplitude_cutoff})')
2716
+ rel_amplitude_cutoff = None
2717
+ else:
2718
+ rel_amplitude_cutoff = None
2719
+
2720
+ # Regular full fit
2721
+ result = self._fit_with_bounds_check(n, current_best_values, **kwargs)
2722
+
2723
+ if rel_amplitude_cutoff is not None:
2724
+ # Third party modules
2725
+ from lmfit.models import (
2726
+ GaussianModel,
2727
+ LorentzianModel,
2728
+ )
2729
+
2730
+ # Check for low amplitude peaks and refit without them
2731
+ amplitudes = []
2732
+ names = []
2733
+ for component in result.components:
2734
+ if isinstance(component, (GaussianModel, LorentzianModel)):
2735
+ for name in component.param_names:
2736
+ if 'amplitude' in name:
2737
+ amplitudes.append(result.params[name].value)
2738
+ names.append(name)
2739
+ if amplitudes:
2740
+ refit = False
2741
+ amplitudes = np.asarray(amplitudes)/sum(amplitudes)
2742
+ parameters_save = deepcopy(self._parameters)
2743
+ for i, (name, amp) in enumerate(zip(names, amplitudes)):
2744
+ if abs(amp) < rel_amplitude_cutoff:
2745
+ self._parameters[name].set(
2746
+ value=0.0, min=0.0, vary=False)
2747
+ self._parameters[
2748
+ name.replace('amplitude', 'center')].set(
2749
+ vary=False)
2750
+ self._parameters[
2751
+ name.replace('amplitude', 'sigma')].set(
2752
+ value=0.0, min=0.0, vary=False)
2753
+ refit = True
2754
+ if refit:
2755
+ result = self._fit_with_bounds_check(
2756
+ n, current_best_values, **kwargs)
2757
+ # for name in names:
2758
+ # result.params[name].error = 0.0
2759
+ # Reset fixed amplitudes back to default
2760
+ self._parameters = deepcopy(parameters_save)
2761
+
2762
+ if result.redchi >= self._redchi_cutoff:
2763
+ result.success = False
2764
+ if result.nfev == result.max_nfev:
2765
+ if result.redchi < self._redchi_cutoff:
2766
+ result.success = True
2767
+ self._max_nfev_flat[n] = True
2768
+ if result.success:
2769
+ assert all(
2770
+ True for par in current_best_values
2771
+ if par in result.params.values())
2772
+ for par in result.params.values():
2773
+ if par.vary:
2774
+ current_best_values[par.name] = par.value
2775
+ else:
2776
+ logger.warning(f'Fit for n = {n} failed: {result.lmdif_message}')
2777
+ # Renormalize the data and results
2778
+ self._renormalize(n, result)
2779
+ if self._print_report:
2780
+ print(result.fit_report(show_correl=False))
2781
+ if self._plot:
2782
+ dims = np.unravel_index(n, self._map_shape)
2783
+ if self._inv_transpose is not None:
2784
+ dims = tuple(
2785
+ dims[self._inv_transpose[i]] for i in range(len(dims)))
2786
+ super().plot(
2787
+ result=result, y=np.asarray(self._ymap[dims]),
2788
+ plot_comp_legends=True, skip_init=self._skip_init,
2789
+ title=str(dims))
2790
+ if return_result:
2791
+ return result
2792
+ return None
2793
+
2794
+ def _fit_with_bounds_check(self, n, current_best_values, **kwargs):
2687
2795
  # Set parameters to current best values, but prevent them from
2688
2796
  # sitting at boundaries
2689
2797
  if self._new_parameters is None:
2690
2798
  # Initial fit
2691
2799
  for name, value in current_best_values.items():
2692
2800
  par = self._parameters[name]
2693
- par.set(value=self._reset_par_at_boundary(par, value))
2801
+ if par.vary:
2802
+ par.set(value=value)
2694
2803
  else:
2695
2804
  # Refit
2696
2805
  for i, name in enumerate(self._best_parameters):
2697
2806
  par = self._parameters[name]
2698
- if name in self._new_parameters:
2699
- if name in current_best_values:
2700
- par.set(value=self._reset_par_at_boundary(
2701
- par, current_best_values[name]))
2702
- elif par.expr is None:
2703
- par.set(value=self._best_values[i][n])
2807
+ if par.vary:
2808
+ if name in self._new_parameters:
2809
+ if name in current_best_values:
2810
+ par.set(value=current_best_values[name])
2811
+ elif par.expr is None:
2812
+ par.set(value=self._best_values[i][n])
2813
+ self._reset_par_at_boundary()
2704
2814
  if self._mask is None:
2705
2815
  result = self._model.fit(
2706
2816
  self._ymap_norm[n], self._parameters, x=self._x, **kwargs)
@@ -2710,35 +2820,39 @@ class FitMap(Fit):
2710
2820
  x=self._x[~self._mask], **kwargs)
2711
2821
  out_of_bounds = False
2712
2822
  for name, par in self._parameter_bounds.items():
2713
- value = result.params[name].value
2714
- if not np.isinf(par['min']) and value < par['min']:
2715
- out_of_bounds = True
2716
- break
2717
- if not np.isinf(par['max']) and value > par['max']:
2718
- out_of_bounds = True
2719
- break
2823
+ if self._parameters[name].vary:
2824
+ value = result.params[name].value
2825
+ if not np.isinf(par['min']) and value < par['min']:
2826
+ out_of_bounds = True
2827
+ break
2828
+ if not np.isinf(par['max']) and value > par['max']:
2829
+ out_of_bounds = True
2830
+ break
2720
2831
  self._out_of_bounds_flat[n] = out_of_bounds
2721
2832
  if self._try_no_bounds and out_of_bounds:
2722
2833
  # Rerun fit with parameter bounds in place
2723
2834
  for name, par in self._parameter_bounds.items():
2724
- self._parameters[name].set(min=par['min'], max=par['max'])
2835
+ if self._parameters[name].vary:
2836
+ self._parameters[name].set(min=par['min'], max=par['max'])
2725
2837
  # Set parameters to current best values, but prevent them
2726
2838
  # from sitting at boundaries
2727
2839
  if self._new_parameters is None:
2728
2840
  # Initial fit
2729
2841
  for name, value in current_best_values.items():
2730
2842
  par = self._parameters[name]
2731
- par.set(value=self._reset_par_at_boundary(par, value))
2843
+ if par.vary:
2844
+ par.set(value=value)
2732
2845
  else:
2733
2846
  # Refit
2734
2847
  for i, name in enumerate(self._best_parameters):
2735
2848
  par = self._parameters[name]
2736
- if name in self._new_parameters:
2737
- if name in current_best_values:
2738
- par.set(value=self._reset_par_at_boundary(par,
2739
- current_best_values[name]))
2740
- elif par.expr is None:
2741
- par.set(value=self._best_values[i][n])
2849
+ if par.vary:
2850
+ if name in self._new_parameters:
2851
+ if name in current_best_values:
2852
+ par.set(value=current_best_values[name])
2853
+ elif par.expr is None:
2854
+ par.set(value=self._best_values[i][n])
2855
+ self._reset_par_at_boundary()
2742
2856
  if self._mask is None:
2743
2857
  result = self._model.fit(
2744
2858
  self._ymap_norm[n], self._parameters, x=self._x, **kwargs)
@@ -2748,48 +2862,18 @@ class FitMap(Fit):
2748
2862
  x=self._x[~self._mask], **kwargs)
2749
2863
  out_of_bounds = False
2750
2864
  for name, par in self._parameter_bounds.items():
2751
- value = result.params[name].value
2752
- if not np.isinf(par['min']) and value < par['min']:
2753
- out_of_bounds = True
2754
- break
2755
- if not np.isinf(par['max']) and value > par['max']:
2756
- out_of_bounds = True
2757
- break
2758
- # Reset parameters back to unbound
2759
- for name in self._parameter_bounds.keys():
2760
- self._parameters[name].set(min=-np.inf, max=np.inf)
2865
+ if self._parameters[name].vary:
2866
+ value = result.params[name].value
2867
+ if not np.isinf(par['min']) and value < par['min']:
2868
+ out_of_bounds = True
2869
+ break
2870
+ if not np.isinf(par['max']) and value > par['max']:
2871
+ out_of_bounds = True
2872
+ break
2873
+ # Reset parameters back to unbound
2874
+ self._parameters[name].set(min=-np.inf, max=np.inf)
2761
2875
  assert not out_of_bounds
2762
- if result.redchi >= self._redchi_cutoff:
2763
- result.success = False
2764
- if result.nfev == result.max_nfev:
2765
- if result.redchi < self._redchi_cutoff:
2766
- result.success = True
2767
- self._max_nfev_flat[n] = True
2768
- if result.success:
2769
- assert all(
2770
- True for par in current_best_values
2771
- if par in result.params.values())
2772
- for par in result.params.values():
2773
- if par.vary:
2774
- current_best_values[par.name] = par.value
2775
- else:
2776
- logger.warning(f'Fit for n = {n} failed: {result.lmdif_message}')
2777
- # Renormalize the data and results
2778
- self._renormalize(n, result)
2779
- if self._print_report:
2780
- print(result.fit_report(show_correl=False))
2781
- if self._plot:
2782
- dims = np.unravel_index(n, self._map_shape)
2783
- if self._inv_transpose is not None:
2784
- dims = tuple(
2785
- dims[self._inv_transpose[i]] for i in range(len(dims)))
2786
- super().plot(
2787
- result=result, y=np.asarray(self._ymap[dims]),
2788
- plot_comp_legends=True, skip_init=self._skip_init,
2789
- title=str(dims))
2790
- if return_result:
2791
- return result
2792
- return None
2876
+ return result
2793
2877
 
2794
2878
  def _renormalize(self, n, result):
2795
2879
  self._redchi_flat[n] = np.float64(result.redchi)