diff --git a/build_tools/conda_py3_qt5_env.yml b/build_tools/conda_py3_qt5_env.yml index 490e6bce84..50bc93fbd4 100644 --- a/build_tools/conda_py3_qt5_env.yml +++ b/build_tools/conda_py3_qt5_env.yml @@ -105,3 +105,4 @@ dependencies: - tinycc==1.1 - webencodings==0.5.1 - xhtml2pdf==0.2.1 + - uncertainties==3.1.6 diff --git a/build_tools/conda_py3_qt5_osx.yml b/build_tools/conda_py3_qt5_osx.yml index eed0cb7644..213bfb85f3 100644 --- a/build_tools/conda_py3_qt5_osx.yml +++ b/build_tools/conda_py3_qt5_osx.yml @@ -114,3 +114,4 @@ dependencies: - reportlab==3.4.0 - webencodings==0.5.1 - xhtml2pdf==0.2.2 + - uncertainties==3.1.6 diff --git a/build_tools/conda_qt5_min_ubuntu.yml b/build_tools/conda_qt5_min_ubuntu.yml index a7a392fc26..57d4464dc6 100644 --- a/build_tools/conda_qt5_min_ubuntu.yml +++ b/build_tools/conda_qt5_min_ubuntu.yml @@ -29,5 +29,6 @@ dependencies: - bumps - xhtml2pdf==0.2.2 - qt5reactor==0.5 + - uncertainties==3.1.6 - pillow==8.1.2 - h5py==3.1 diff --git a/build_tools/conda_qt5_osx.yml b/build_tools/conda_qt5_osx.yml index 6c9942247f..1f08b2979a 100644 --- a/build_tools/conda_qt5_osx.yml +++ b/build_tools/conda_qt5_osx.yml @@ -148,3 +148,4 @@ dependencies: - qt5reactor==0.5 - unittest-xml-reporting - lxml==4.6.3 + - uncertainties==3.1.6 diff --git a/build_tools/conda_qt5_ubuntu.yml b/build_tools/conda_qt5_ubuntu.yml index 867b4e51fa..263067954c 100644 --- a/build_tools/conda_qt5_ubuntu.yml +++ b/build_tools/conda_qt5_ubuntu.yml @@ -126,4 +126,5 @@ dependencies: - werkzeug==0.14.1 - xhtml2pdf==0.2.2 - zope.interface==4.5.0 + - uncertainties==3.1.6 diff --git a/build_tools/conda_qt5_win.yml b/build_tools/conda_qt5_win.yml index 463788f052..0b5fba9d49 100644 --- a/build_tools/conda_qt5_win.yml +++ b/build_tools/conda_qt5_win.yml @@ -35,3 +35,4 @@ dependencies: - qt5reactor==0.5 - tinycc==1.1 - pillow==8.1.2 + - uncertainties==3.1.6 diff --git a/build_tools/conda_qt5_win_commercial.yml b/build_tools/conda_qt5_win_commercial.yml index 3c341581a1..5d97ef014d 100644 --- a/build_tools/conda_qt5_win_commercial.yml +++ b/build_tools/conda_qt5_win_commercial.yml @@ -33,5 +33,7 @@ dependencies: - ../../../PyQt5_commercial-5.12.2-5.12.3-cp35.cp36.cp37.cp38-none-win_amd64.whl - qt5reactor==0.5 - tinycc==1.1 + - uncertainties==3.1.6 - pillow==8.1.2 - - h5py==3.1 + - h5py==3.1 + diff --git a/src/sas/sascalc/fit/BumpsFitting.py b/src/sas/sascalc/fit/BumpsFitting.py index e61734089e..c0bdd6eff9 100644 --- a/src/sas/sascalc/fit/BumpsFitting.py +++ b/src/sas/sascalc/fit/BumpsFitting.py @@ -6,8 +6,11 @@ import traceback import numpy as np +from uncertainties import ufloat, correlated_values from bumps import fitters +from bumps.parameter import unique as unique_parameters + try: from bumps.options import FIT_CONFIG # Default bumps to use the Levenberg-Marquardt optimizer @@ -279,35 +282,73 @@ def fit(self, msg_q=None, result = run_bumps(problem, handler, curr_thread) if handler is not None: handler.update_fit(last=True) + propagate_errors = all(model.constraints for model in models) + uncertainty_error = None + if propagate_errors: + # TODO: shouldn't reference internal parameters of fit problem + varying = problem._parameters + # Propagate uncertainty through the parameter expressions + # We are going to abuse bumps a little here and stuff uncertainty + # objects into the parameter values, then update all the + # derived parameters with uncertainty propagation. We need to + # avoid triggering a model recalc since the uncertainty objects + # will not be working with sasmodels. + # TODO: if dream then use forward MC to evaluate uncertainty + # TODO: move uncertainty propagation into bumps + # TODO: should scale stderr by sqrt(chisq/DOF) if dy is unknown + values, errs, cov = result['value'], result["stderr"], result[ + 'covariance'] + assert values is not None + # Turn all parameters (fixed and varying) into uncertainty objects with + # zero uncertainty. + for p in unique_parameters(problem.model_parameters()): + p.value = ufloat(p.value, 0) + # then update the computed standard deviation of fitted parameters + if len(varying) < 2: + varying[0].value = ufloat(values[0], errs[0]) + else: + fitted = (correlated_values(values, cov) if not errs else + [ufloat(value, np.nan) for value in values]) + + for p, v in zip(varying, fitted): + p.value = v + try: + problem.setp_hook() + except np.linalg.LinAlgError: + fitted = [ufloat(value, err) for value, err in zip( + values, errs)] + uncertainty_error = True + for p, v in zip(varying, fitted): + p.value = v + problem.setp_hook() + except TypeError: + propagate_errors = False + uncertainty_error = True - # TODO: shouldn't reference internal parameters of fit problem - varying = problem._parameters # collect the results all_results = [] for M in problem.models: fitness = M.fitness - fitted_index = [varying.index(p) for p in fitness.fitted_pars] - param_list = fitness.fitted_par_names + fitness.computed_par_names - R = FResult(model=fitness.model, data=fitness.data, - param_list=param_list) + par_names = fitness.fitted_par_names + fitness.computed_par_names + pars = fitness.fitted_pars + fitness.computed_pars + R = FResult(model=fitness.model, + data=fitness.data, + param_list=par_names) R.theory = fitness.theory() R.residuals = fitness.residuals() R.index = fitness.data.idx R.fitter_id = self.fitter_id - # TODO: should scale stderr by sqrt(chisq/DOF) if dy is unknown R.success = result['success'] if R.success: - if result['stderr'] is None: - R.stderr = np.NaN*np.ones(len(param_list)) - else: - R.stderr = np.hstack((result['stderr'][fitted_index], - np.NaN*np.ones(len(fitness.computed_pars)))) - R.pvec = np.hstack((result['value'][fitted_index], - [p.value for p in fitness.computed_pars])) - R.fitness = np.sum(R.residuals**2)/(fitness.numpoints() - len(fitted_index)) + R.pvec = (np.array([p.value.n for p in pars]) if + propagate_errors else result['value']) + R.stderr = (np.array([p.value.s for p in pars]) if + propagate_errors else result["stderr"]) + DOF = max(1, fitness.numpoints() - len(fitness.fitted_pars)) + R.fitness = np.sum(R.residuals ** 2) / DOF else: - R.stderr = np.NaN*np.ones(len(param_list)) - R.pvec = np.asarray([p.value for p in fitness.fitted_pars+fitness.computed_pars]) + R.pvec = np.asarray([p.value for p in pars]) + R.stderr = np.NaN * np.ones(len(pars)) R.fitness = np.NaN R.convergence = result['convergence'] if result['uncertainty'] is not None: @@ -367,10 +408,13 @@ def abort_test(): success = best is not None try: stderr = fitdriver.stderr() if success else None + cov = (fitdriver.cov() if not hasattr(fitdriver.fitter, 'state') else + np.cov(fitdriver.fitter.state.draw().points.T)) except Exception as exc: errors.append(str(exc)) errors.append(traceback.format_exc()) stderr = None + cov = None return { 'value': best if success else None, 'stderr': stderr, @@ -378,4 +422,5 @@ def abort_test(): 'convergence': convergence, 'uncertainty': getattr(fitdriver.fitter, 'state', None), 'errors': '\n'.join(errors), + 'covariance': cov, }