207-noncomplex-fits (#244)
All checks were successful
Build AppImage / Explore-Gitea-Actions (push) Successful in 1m51s

Co-authored-by: Dominik Demuth <dominik.demuth@physik.tu-darmstadt.de>
Reviewed-on: #244

closes #207
This commit is contained in:
Dominik Demuth 2024-02-11 17:40:50 +00:00
parent 8d3ab75c97
commit 80d9c7098c
5 changed files with 152 additions and 117 deletions

View File

@ -503,11 +503,25 @@ class UpperManagement(QtCore.QObject):
we = we_option we = we_option
if m_complex is None or m_complex == 1: if m_complex is None or m_complex == 1:
# model is not complex: m_complex = None
# model is complex, fit real part: m_complex = 1
_y = data_i.y.real _y = data_i.y.real
elif m_complex == 2 and np.iscomplexobj(data_i.y): data_complex = 1
_y = data_i.y.imag elif m_complex == 2:
# model is complex, fit imag part: m_complex = 2
if np.iscomplexobj(data_i.y):
# data is complex, use imag part
_y = data_i.y.imag
data_complex = 2
else:
# data is real
_y = data_i.y
data_complex = 1
else: else:
# model is complex, fit complex: m_complex = 0
# use data as given (complex or not)
_y = data_i.y _y = data_i.y
data_complex = 0
_x = data_i.x _x = data_i.x
@ -524,9 +538,9 @@ class UpperManagement(QtCore.QObject):
try: try:
if isinstance(we, str): if isinstance(we, str):
d = fit_d.Data(_x[inside], _y[inside], we=we, idx=set_id) d = fit_d.Data(_x[inside], _y[inside], we=we, idx=set_id, complex_type=data_complex)
else: else:
d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id) d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id, complex_type=data_complex)
except Exception as e: except Exception as e:
raise Exception(f'Setting data failed for {set_id}') raise Exception(f'Setting data failed for {set_id}')

View File

@ -6,8 +6,8 @@ from .model import Model
from .parameter import Parameters, Parameter from .parameter import Parameters, Parameter
class Data(object): class Data:
def __init__(self, x, y, we=None, idx=None): def __init__(self, x, y, we=None, idx=None, complex_type: int = 0):
self.x = np.asarray(x) self.x = np.asarray(x)
self.y = np.asarray(y) self.y = np.asarray(y)
if self.y.shape[0] != self.x.shape[0]: if self.y.shape[0] != self.x.shape[0]:
@ -20,6 +20,7 @@ class Data(object):
self.parameter = Parameters() self.parameter = Parameters()
self.para_keys: list = [] self.para_keys: list = []
self.fun_kwargs = {} self.fun_kwargs = {}
self.complex_type = complex_type
def __len__(self): def __len__(self):
return self.y.shape[0] return self.y.shape[0]

View File

@ -361,7 +361,7 @@ class FitRoutine(object):
with np.errstate(all='ignore'): with np.errstate(all='ignore'):
res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0)) res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0))
err, corr, partial_corr = self._calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape) err, corr, partial_corr = _calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape)
self.make_results(data, res.x, var, data.para_keys, res.jac.shape, self.make_results(data, res.x, var, data.para_keys, res.jac.shape,
err=err, corr=corr, partial_corr=partial_corr) err=err, corr=corr, partial_corr=partial_corr)
@ -375,7 +375,7 @@ class FitRoutine(object):
with np.errstate(all='ignore'): with np.errstate(all='ignore'):
res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0)) res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0))
err, corr, partial_corr = self._calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape) err, corr, partial_corr = _calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape)
for v, var_pars_k in zip(data, data_pars): for v, var_pars_k in zip(data, data_pars):
self.make_results(v, res.x, var, var_pars_k, res.jac.shape, self.make_results(v, res.x, var, var_pars_k, res.jac.shape,
err=err, corr=corr, partial_corr=partial_corr) err=err, corr=corr, partial_corr=partial_corr)
@ -458,9 +458,17 @@ class FitRoutine(object):
self.make_results(v, res.beta, var, var_pars_k, (sum(len(d) for d in data), len(p0)), self.make_results(v, res.beta, var, var_pars_k, (sum(len(d) for d in data), len(p0)),
err=res.sd_beta, corr=corr, partial_corr=partial_corr) err=res.sd_beta, corr=corr, partial_corr=partial_corr)
def make_results(self, data, p, var_pars, used_pars, shape, def make_results(
err=None, corr=None, partial_corr=None): self,
data: Data,
p: list[float],
var_pars: list[str],
used_pars: list[str],
shape: tuple[int, int],
err: list[float] = None,
corr: np.ndarray = None,
partial_corr: np.ndarray = None,
):
if err is None: if err is None:
err = [0] * len(p) err = [0] * len(p)
@ -498,52 +506,54 @@ class FitRoutine(object):
model = data.get_model() model = data.get_model()
self.result[idx] = FitResultCreator.make_with_model( self.result[idx] = FitResultCreator.make_with_model(
model, model=model,
data.x, x_orig=data.x,
data.y, y_orig=data.y,
actual_parameters, p=actual_parameters,
data.fun_kwargs, fun_kwargs=data.fun_kwargs,
data.we_string, we=data.we_string,
data.idx, idx=data.idx,
*shape, nobs=shape[0],
nvar=shape[1],
corr=actual_corr, corr=actual_corr,
pcorr=actual_pcorr, pcorr=actual_pcorr,
data_mode=data.complex_type,
) )
return self.result return self.result
@staticmethod
def _calc_error(jac, chi, nobs, nvars):
# copy of scipy.curve_fit to calculate covariance
# noinspection PyTupleAssignmentBalance
try:
_, s, vt = la.svd(jac, full_matrices=False)
except ValueError as e:
# this may be issue #39: On entry to DGESSD parameter had an illegal value
# catch this exception and ignore error calculation
logger.error(f'Error calculation failed with {e.args}')
pcov = None
else:
threshold = EPS * max(jac.shape) * s[0]
s = s[s > threshold]
vt = vt[:s.size]
pcov = np.dot(vt.T / s**2, vt) * chi / (nobs - nvars)
if pcov is None: def _calc_error(jac, chi, nobs, nvars):
_err = np.zeros(nvars) # copy of scipy.curve_fit to calculate covariance
corr = np.zeros((nvars, nvars)) # noinspection PyTupleAssignmentBalance
else: try:
_err = np.sqrt(np.diag(pcov)) _, s, vt = la.svd(jac, full_matrices=False)
corr = pcov / (_err[:, None] * _err[None, :]) except ValueError as e:
# this may be issue #39: On entry to DGESSD parameter had an illegal value
# catch this exception and ignore error calculation
logger.error(f'Error calculation failed with {e.args}')
pcov = None
else:
threshold = EPS * max(jac.shape) * s[0]
s = s[s > threshold]
vt = vt[:s.size]
pcov = np.dot(vt.T / s**2, vt) * chi / (nobs - nvars)
corr = corr.astype(np.float64) if pcov is None:
try: _err = np.zeros(nvars)
corr_inv = np.linalg.inv(corr) corr = np.zeros((nvars, nvars))
corr_inv_diag = np.diag(np.sqrt(1 / np.diag(corr_inv))) else:
partial_corr = -1. * np.dot(np.dot(corr_inv_diag, corr_inv), corr_inv_diag) # Partial correlation matrix _err = np.sqrt(np.diag(pcov))
partial_corr[np.diag_indices_from(partial_corr)] = 1. corr = pcov / (_err[:, None] * _err[None, :])
except np.linalg.LinAlgError:
partial_corr = corr
return _err, corr, partial_corr corr = corr.astype(np.float64)
try:
corr_inv = np.linalg.inv(corr)
corr_inv_diag = np.diag(np.sqrt(1 / np.diag(corr_inv)))
partial_corr = -1. * np.dot(np.dot(corr_inv_diag, corr_inv), corr_inv_diag) # Partial correlation matrix
partial_corr[np.diag_indices_from(partial_corr)] = 1.
except np.linalg.LinAlgError:
partial_corr = corr
return _err, corr, partial_corr

View File

@ -9,7 +9,7 @@ from ._meta import MultiModel
from .parameter import Parameters, Parameter from .parameter import Parameters, Parameter
class Model(object): class Model:
def __init__(self, model, *args, **kwargs): def __init__(self, model, *args, **kwargs):
self.idx = kwargs.pop('idx', None) self.idx = kwargs.pop('idx', None)

View File

@ -11,6 +11,7 @@ from scipy.stats import f as fdist
from scipy.interpolate import interp1d from scipy.interpolate import interp1d
from ._meta import MultiModel from ._meta import MultiModel
from .model import Model
from .parameter import Parameter from .parameter import Parameter
from ..data.points import Points from ..data.points import Points
from ..data.signals import Signal from ..data.signals import Signal
@ -36,17 +37,30 @@ class FitResultCreator:
else: else:
resid = kwargs['y'] - y_orig resid = kwargs['y'] - y_orig
stats = FitResultCreator.calc_statistics(resid, _y) stats = calc_statistics(resid, _y)
return FitResult(kwargs['x'], kwargs['y'], x_orig, y_orig, params, dict(kwargs['choice']), resid, 0, 0, return FitResult(
kwargs['name'], stats, idx) x=kwargs['x'],
y=kwargs['y'],
x_data=x_orig,
y_data=y_orig,
params=params,
fun_kwargs=dict(kwargs['choice']),
resid=resid,
nobs=0,
nvar=0,
we='',
name=kwargs['name'],
stats=stats,
idx=idx,
)
@staticmethod @staticmethod
def make_with_model( def make_with_model(
model: 'Model', model: 'Model',
x_orig: np.ndarray, x_orig: np.ndarray,
y_orig: np.ndarray, y_orig: np.ndarray,
p: 'Parameters', p: list,
fun_kwargs: dict, fun_kwargs: dict,
we: str, we: str,
idx: str | None, idx: str | None,
@ -54,6 +68,7 @@ class FitResultCreator:
nvar: int, nvar: int,
corr: np.ndarray, corr: np.ndarray,
pcorr: np.ndarray, pcorr: np.ndarray,
data_mode: int,
) -> FitResult: ) -> FitResult:
if np.all(x_orig > 0) and (np.max(x_orig) > 100 * np.min(x_orig)): if np.all(x_orig > 0) and (np.max(x_orig) > 100 * np.min(x_orig)):
islog = True islog = True
@ -83,17 +98,11 @@ class FitResultCreator:
actual_mode = fun_kwargs['complex_mode'] actual_mode = fun_kwargs['complex_mode']
fun_kwargs['complex_mode'] = 0 fun_kwargs['complex_mode'] = 0
_y = model.func(p_final, _x, **fun_kwargs) _y = check_complex(model.func(p_final, _x, **fun_kwargs), actual_mode, data_mode)
if not actual_mode < 0: fun_kwargs['complex_mode'] = actual_mode
if actual_mode == 1:
_y.imag = 0
elif actual_mode == 2:
_y.real = 0
fun_kwargs['complex_mode'] = actual_mode stats = calc_statistics(_y, resid, nobs, nvar)
stats = FitResultCreator.calc_statistics(_y, resid, nobs, nvar)
varied = [p.var for p in parameters.values()] varied = [p.var for p in parameters.values()]
if corr is None: if corr is None:
@ -134,38 +143,9 @@ class FitResultCreator:
pcorr=partial_correlation, pcorr=partial_correlation,
islog=islog, islog=islog,
func=model, func=model,
data_complex=data_mode,
) )
@staticmethod
def calc_statistics(y, residual, nobs=None, nvar=None):
chi = (residual**2).sum()
try:
r = 1 - chi/((y-np.mean(y))**2).sum()
except RuntimeWarning:
r = -9999
if nobs is None:
nobs = 1
if nvar is None:
nvar = 0
dof = nobs - nvar
loglikehood = nobs * np.log(chi / nobs)
stats = {
'chi^2': chi,
'R^2': r,
'AIC': loglikehood + 2 * nvar,
'BIC': loglikehood + np.log(nobs) * nvar,
'adj. R^2': 1 - (nobs-1) / (dof+1e-13) * (1-r),
'red. chi^2': chi / (dof + 1e-13),
}
stats['AICc'] = stats['AIC'] + 2*(nvar+1)*nvar / (dof - 1 + 1e-13)
return stats
class FitResult(Points): class FitResult(Points):
@ -188,7 +168,8 @@ class FitResult(Points):
pcorr: np.ndarray = None, pcorr: np.ndarray = None,
islog: bool = False, islog: bool = False,
func=None, func=None,
**kwargs data_complex: int = 1,
**kwargs,
): ):
self.parameter, name = self._prepare_names(params, name) self.parameter, name = self._prepare_names(params, name)
@ -210,6 +191,7 @@ class FitResult(Points):
self.y_data = y_data self.y_data = y_data
self._model_name = name self._model_name = name
self._func = func self._func = func
self._data_complex = data_complex
@staticmethod @staticmethod
def _prepare_names(parameter: dict, modelname: str): def _prepare_names(parameter: dict, modelname: str):
@ -418,20 +400,9 @@ class FitResult(Points):
if self.func is None: if self.func is None:
raise ValueError('no fit function available to calculate new y values') raise ValueError('no fit function available to calculate new y values')
actual_mode = -1
if 'complex_mode' in self.fun_kwargs:
actual_mode = self.fun_kwargs['complex_mode']
self.fun_kwargs['complex_mode'] = 0
new_fit = self.copy() new_fit = self.copy()
y_values = self.func.func(self.p_final, x_values, **self.fun_kwargs) y_values = self.func.func(self.p_final, x_values, **self.fun_kwargs)
if not actual_mode < 0: y_values = check_complex(y_values, self.fun_kwargs.get('complex_mode', -1), self._data_complex)
if actual_mode == 1:
y_values.imag = 0
elif actual_mode == 2:
y_values.real = 0
self.fun_kwargs['complex_mode'] = actual_mode
new_fit.set_data(x_values, y_values, y_err=0.0) new_fit.set_data(x_values, y_values, y_err=0.0)
@ -442,20 +413,13 @@ class FitResult(Points):
raise ValueError('no fit function available to calculate new y values') raise ValueError('no fit function available to calculate new y values')
part_functions = [] part_functions = []
actual_mode = -1 actual_mode = self.fun_kwargs.get('complex_mode', -1)
if 'complex_mode' in self.fun_kwargs:
actual_mode = self.fun_kwargs['complex_mode']
self.fun_kwargs['complex_mode'] = 0
for sub_name, sub_y in zip(self.func.sub_name(), self.func.sub(self.p_final, x_values, **self.fun_kwargs)): for sub_name, sub_y in zip(self.func.sub_name(), self.func.sub(self.p_final, x_values, **self.fun_kwargs)):
if not actual_mode < 0: sub_y = check_complex(sub_y, actual_mode, self._data_complex)
if actual_mode == 1:
sub_y.imag = 0
elif actual_mode == 2:
sub_y.real = 0
if np.iscomplexobj(sub_y):
part_functions.append(Signal(x_values, sub_y, name=sub_name)) part_functions.append(Signal(x_values, sub_y, name=sub_name))
else: else:
part_functions.append(Points(x_values, sub_y, name=sub_name)) part_functions.append(Points(x_values, sub_y, name=sub_name))
@ -463,3 +427,49 @@ class FitResult(Points):
self.fun_kwargs['complex_mode'] = actual_mode self.fun_kwargs['complex_mode'] = actual_mode
return part_functions return part_functions
def check_complex(y, model_complex, data_complex):
if not np.iscomplexobj(y):
return y
if model_complex == 1:
y.imag = 0
if data_complex == 1:
y = y.real
elif model_complex == 2:
y.real = 0
if data_complex == 1:
y = y.imag
return y
def calc_statistics(y, residual, nobs=None, nvar=None):
chi = (residual**2).sum()
try:
r = 1 - chi/((y-np.mean(y))**2).sum()
except RuntimeWarning:
r = -9999
if nobs is None:
nobs = 1
if nvar is None:
nvar = 0
dof = nobs - nvar
loglikehood = nobs * np.log(chi / nobs)
stats = {
'chi^2': chi,
'R^2': r,
'AIC': loglikehood + 2 * nvar,
'BIC': loglikehood + np.log(nobs) * nvar,
'adj. R^2': 1 - (nobs-1) / (dof+1e-13) * (1-r),
'red. chi^2': chi / (dof + 1e-13),
}
stats['AICc'] = stats['AIC'] + 2*(nvar+1)*nvar / (dof - 1 + 1e-13)
return stats