Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using the uncertainties module to propagate errors #1916

Merged
merged 4 commits into from
Oct 13, 2021
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 98 additions & 51 deletions src/sas/sascalc/fit/BumpsFitting.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""
BumpsFitting module runs the bumps optimizer.
"""
import logging
import os
from datetime import timedelta, datetime
import traceback
import uncertainties

import numpy as np
from uncertainties import ufloat, correlated_values

import bumps
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
Expand Down Expand Up @@ -282,77 +282,124 @@ 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
# TODO: shouldn't reference internal parameters of fit problem
varying = problem._parameters

values, errs, cov = result['value'], result['stderr'], result[
'covariance']
assert values is not None and errs is not None

# 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

if len(varying) < 2:
# Use the standard error as the error in the parameter
for param, val, err in zip(varying, values, errs):
# Convert all varying parameters to uncertainties objects
param.value = uncertainties.ufloat(val, err)
else:
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
uncertainties.correlated_values(values, cov)
except:
# No convergance
for param, val, err in zip(varying, values, errs):
# Convert all varying parameters to uncertainties objects
param.value = uncertainties.ufloat(val, err)
else:
# Use the covariance matrix to calculate error in the parameter
fitted = uncertainties.correlated_values(values, cov)
for param, val in zip(varying, fitted):
param.value = val

# Propagate correlated uncertainty through constraints.
problem.setp_hook()

# collect the results
all_results = []

for M in problem.models:
fitness = M.fitness
par_names = fitness.fitted_par_names + fitness.computed_par_names
pars = fitness.fitted_pars + fitness.computed_pars
par_names = fitness.fitted_par_names + fitness.computed_par_names

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']
R.convergence = result['convergence']
if result['uncertainty'] is not None:
R.uncertainty_state = result['uncertainty']

if R.success:
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"])
pvec = list()
stderr = list()
for p in pars:
# If p is already defined as an uncertainties object it is not constrained based on another
# parameter
if isinstance(p.value, uncertainties.core.Variable) or \
isinstance(p.value, uncertainties.core.AffineScalarFunc):
# value.n returns value p
pvec.append(p.value.n)
# value.n returns error in p
stderr.append(p.value.s)
# p constrained based on another parameter
else:
# Details of p
param_model, param_name = p.name.split(".")[0], p.name.split(".")[1]
# Constraints applied on p, list comprehension most efficient method, will always return a
# list with 1 entry
constraints = [model.constraints for model in models if model.name == param_model][0]
# Parameters p is constrained on.
reference_params = [v for v in varying if str(v.name) in str(constraints[param_name])]
err_exp = str(constraints[param_name])
# Convert string entries into variable names within the code.
for i, index in enumerate(reference_params):
err_exp = err_exp.replace(reference_params[index].name, f"reference_params[{index}].value")
try:
# Evaluate a string containing constraints as if it where a line of code
pvec.append(eval(err_exp).n)
stderr.append(eval(err_exp).s)
except NameError as e:
pvec.append(p.value)
stderr.append(0)
# Get model causing error
name_error = e.args[0].split()[1].strip("'")
# Safety net if following code does not work
error_param = name_error
# Get parameter causing error
constraints_sections = constraints[param_name].split(".")
for i in range(len(constraints_sections)):
if name_error in constraints_sections[i]:
error_param = f"{name_error}.{constraints_sections[i+1]}"
logging.error(f"Constraints ordered incorrectly. Attempting to constrain {p}, based on "
f"{error_param}, however {error_param} is not defined itself. This is "
f"because {error_param} is also constrained.\n"
f"The fitting will continue, but {name_error} will be incorrect.")
logging.error(e)
except Exception as e:
logging.error(e)
pvec.append(p.value)
stderr.append(0)

R.pvec = (np.array(pvec))
R.stderr = (np.array(stderr))
DOF = max(1, fitness.numpoints() - len(fitness.fitted_pars))
R.fitness = np.sum(R.residuals ** 2) / DOF
else:
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:
R.uncertainty_state = result['uncertainty']

all_results.append(R)
all_results[0].mesg = result['errors']

Expand Down