forked from IPKM/nmreval
		
	# Conflicts: # src/gui_qt/_py/basewindow.py # src/gui_qt/_py/tnmh_dialog.py # src/gui_qt/dsc/glass_dialog.py # src/gui_qt/main/mainwindow.py # src/gui_qt/main/management.py # src/gui_qt/math/binning.py # src/nmreval/data/dsc.py # src/nmreval/dsc/hodge.py # src/resources/_ui/basewindow.ui # src/resources/_ui/tnmh_dialog.ui
		
			
				
	
	
		
			417 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			417 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from __future__ import annotations
 | 
						|
 | 
						|
import re
 | 
						|
from collections import OrderedDict
 | 
						|
from pathlib import Path
 | 
						|
from typing import Any
 | 
						|
 | 
						|
import numpy as np
 | 
						|
from scipy.stats import f as fdist
 | 
						|
from scipy.interpolate import interp1d
 | 
						|
 | 
						|
from ._meta import MultiModel
 | 
						|
from .parameter import Parameter
 | 
						|
from ..data.points import Points
 | 
						|
from ..data.signals import Signal
 | 
						|
from ..utils.text import convert
 | 
						|
 | 
						|
 | 
						|
class FitResultCreator:
 | 
						|
    @staticmethod
 | 
						|
    def make_from_session(x_orig: np.ndarray, y_orig: np.ndarray, idx: int, kwargs: dict[Any]) -> FitResult:
 | 
						|
        params = OrderedDict()
 | 
						|
 | 
						|
        for key, pbest, err in zip(kwargs['pnames'], kwargs['parameter'], kwargs['error']):
 | 
						|
            params[key] = Parameter(pbest)
 | 
						|
            params[key].error = err
 | 
						|
 | 
						|
        _x = kwargs['x']
 | 
						|
        _y = kwargs['y']
 | 
						|
 | 
						|
        if len(_x) != len(x_orig):
 | 
						|
            f = interp1d(_x, _y)
 | 
						|
            rng = (x_orig >= np.min(_x)) * (x_orig <= np.max(_x))
 | 
						|
            resid = f(x_orig[rng]) - y_orig[rng]
 | 
						|
        else:
 | 
						|
            resid = kwargs['y'] - y_orig
 | 
						|
 | 
						|
        stats = FitResultCreator.calc_statistics(resid, _y)
 | 
						|
 | 
						|
        return FitResult(kwargs['x'], kwargs['y'], x_orig, y_orig, params, dict(kwargs['choice']), resid, 0, 0,
 | 
						|
                         kwargs['name'], stats, idx)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def make_with_model(model, x_orig, y_orig, p, fun_kwargs, idx, nobs, nvar, corr, pcorr) -> FitResult:
 | 
						|
        if np.all(x_orig > 0) and (np.max(x_orig) > 100 * np.min(x_orig)):
 | 
						|
            islog = True
 | 
						|
        else:
 | 
						|
            islog = False
 | 
						|
 | 
						|
        if len(x_orig) < 51:
 | 
						|
            if islog:
 | 
						|
                _x = np.geomspace(np.min(x_orig), np.max(x_orig), num=10*x_orig.size-9)
 | 
						|
            else:
 | 
						|
                _x = np.linspace(np.min(x_orig), np.max(x_orig), num=10*x_orig.size-9)
 | 
						|
        else:
 | 
						|
            _x = x_orig
 | 
						|
 | 
						|
        try:
 | 
						|
            pnames = model.pnames
 | 
						|
        except AttributeError:
 | 
						|
            pnames = model.params
 | 
						|
 | 
						|
        parameters = OrderedDict([(k, v) for k, v in zip(pnames, p)])
 | 
						|
        p_final = [p.value for p in parameters.values()]
 | 
						|
 | 
						|
        resid = model.func(p_final, x_orig, **fun_kwargs) - y_orig
 | 
						|
 | 
						|
        actual_mode = -1
 | 
						|
        if 'complex_mode' in fun_kwargs:
 | 
						|
            actual_mode = fun_kwargs['complex_mode']
 | 
						|
            fun_kwargs['complex_mode'] = 0
 | 
						|
 | 
						|
        _y = model.func(p_final, _x, **fun_kwargs)
 | 
						|
 | 
						|
        if not actual_mode < 0:
 | 
						|
            if actual_mode == 1:
 | 
						|
                _y.imag = 0
 | 
						|
            elif actual_mode == 2:
 | 
						|
                _y.real = 0
 | 
						|
 | 
						|
            fun_kwargs['complex_mode'] = actual_mode
 | 
						|
 | 
						|
        stats = FitResultCreator.calc_statistics(_y, resid, nobs, nvar)
 | 
						|
        varied = [p.var for p in parameters.values()]
 | 
						|
 | 
						|
        if corr is None:
 | 
						|
            correlation = np.eye(len(p_final), len(p_final))
 | 
						|
            partial_correlation = np.eye(len(p_final), len(p_final))
 | 
						|
 | 
						|
        else:
 | 
						|
            if len(corr) < len(p_final):
 | 
						|
                correlation = np.eye(len(p_final), len(p_final))
 | 
						|
                partial_correlation = np.eye(len(p_final), len(p_final))
 | 
						|
                # probably some fixed variables
 | 
						|
                j = 0
 | 
						|
                for i in range(len(corr)):
 | 
						|
                    while not varied[j]:
 | 
						|
                        j += 1
 | 
						|
                    correlation[j, varied] = corr[i]
 | 
						|
                    partial_correlation[j, varied] = pcorr[i]
 | 
						|
                    j += 1
 | 
						|
            else:
 | 
						|
                correlation = corr
 | 
						|
                partial_correlation = pcorr
 | 
						|
 | 
						|
        return FitResult(_x, _y, x_orig, y_orig, parameters, fun_kwargs, resid,
 | 
						|
                         nobs, nvar, model.name, stats,
 | 
						|
                         idx=idx, corr=correlation, pcorr=partial_correlation,
 | 
						|
                         islog=islog, func=model)
 | 
						|
 | 
						|
    @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
 | 
						|
        }
 | 
						|
 | 
						|
        if dof != 0:
 | 
						|
            stats['adj. R^2'] = 1 - (nobs-1)/dof * (1-r)
 | 
						|
            stats['red. chi^2'] = chi / dof if dof != 0 else 0
 | 
						|
 | 
						|
        if dof != 1:
 | 
						|
            stats['AICc'] = stats['AIC'] + 2*(nvar+1)*nvar / (dof-1)
 | 
						|
 | 
						|
        return stats
 | 
						|
 | 
						|
 | 
						|
class FitResult(Points):
 | 
						|
 | 
						|
    def __init__(self, x: np.ndarray, y: np.ndarray,
 | 
						|
                 x_data: np.ndarray, y_data: np.ndarray,
 | 
						|
                 params: dict, fun_kwargs: dict,
 | 
						|
                 resid: np.ndarray, nobs: int, nvar: int, name: str, stats: dict,
 | 
						|
                 idx=None, corr=None, pcorr=None, islog=False, func=None,
 | 
						|
                 **kwargs):
 | 
						|
 | 
						|
        self.parameter, name = self._prepare_names(params, name)
 | 
						|
        label = kwargs.get('label', name)
 | 
						|
        super().__init__(x=x, y=y, name=label, **kwargs)
 | 
						|
 | 
						|
        self.residual = resid
 | 
						|
        self.idx = idx
 | 
						|
        self.statistics = stats
 | 
						|
        self.nobs = nobs
 | 
						|
        self.nvar = nvar
 | 
						|
        self.fun_kwargs = fun_kwargs
 | 
						|
        self.correlation = corr
 | 
						|
        self.partial_correlation = pcorr
 | 
						|
        self.islog = islog
 | 
						|
        self.iscomplex = np.iscomplexobj(self.y)
 | 
						|
        self.x_data = x_data
 | 
						|
        self.y_data = y_data
 | 
						|
        self._model_name = name
 | 
						|
        self._func = func
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _prepare_names(parameter: dict, modelname: str):
 | 
						|
        pattern = re.compile(r'(\\?\w[\\\w .-]*(?:_{[\w\\ .-]})?)(\(\d+\))')
 | 
						|
 | 
						|
        split_funcs = {g2: g1 for (g1, g2) in pattern.findall(modelname)}
 | 
						|
 | 
						|
        parameter_dic = {}
 | 
						|
        for pname, pvalue in parameter.items():
 | 
						|
            nice_name = pname
 | 
						|
            nice_func = ''
 | 
						|
            m = pattern.match(pname)
 | 
						|
            if m:
 | 
						|
                func_number = m.group(2)
 | 
						|
                nice_name = m.group(1)
 | 
						|
                if func_number in split_funcs:
 | 
						|
                    nice_func = split_funcs[func_number]
 | 
						|
 | 
						|
            pvalue.name = nice_name
 | 
						|
            pvalue.function = nice_func
 | 
						|
            parameter_dic[pname] = pvalue
 | 
						|
 | 
						|
        modelname = re.sub(r'\(\d+\)', '', modelname)
 | 
						|
        if modelname[0] == '(' and modelname[-1] == ')':
 | 
						|
            modelname = modelname[1:-1]
 | 
						|
 | 
						|
        return parameter_dic, modelname
 | 
						|
 | 
						|
    @property
 | 
						|
    def model_name(self):
 | 
						|
        return self._model_name
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        try:
 | 
						|
            return 'Fit: ' + self.name
 | 
						|
        except AttributeError:
 | 
						|
            return 'FitObject'
 | 
						|
 | 
						|
    @property
 | 
						|
    def func(self):
 | 
						|
        if isinstance(self._func, MultiModel):
 | 
						|
            return self._func.func
 | 
						|
        else:
 | 
						|
            return self._func
 | 
						|
 | 
						|
    @property
 | 
						|
    def p_final(self):
 | 
						|
        return [pp.value for pp in self.parameter.values()]
 | 
						|
 | 
						|
    @property
 | 
						|
    def dof(self):
 | 
						|
        return self.nobs-self.nvar
 | 
						|
 | 
						|
    def pprint(self, statistics=True, correlations=True):
 | 
						|
        print('Fit result:')
 | 
						|
        print('  model :', self.name)
 | 
						|
        print('  #data :', self.nobs)
 | 
						|
        print('  #var  :', self.nvar)
 | 
						|
        print('\nParameter')
 | 
						|
        print(self.parameter_string())
 | 
						|
 | 
						|
        if statistics:
 | 
						|
            print('Statistics')
 | 
						|
            for k, v in self.statistics.items():
 | 
						|
                print(f'  {k} : {v:.4f}')
 | 
						|
 | 
						|
        if correlations and self.correlation is not None:
 | 
						|
            print('\nCorrelation (partial corr.)')
 | 
						|
            print(self._correlation_string())
 | 
						|
            print()
 | 
						|
 | 
						|
    def parameter_string(self):
 | 
						|
        ret_val = ''
 | 
						|
 | 
						|
        for pval in self.parameter.values():
 | 
						|
            print(pval)
 | 
						|
            ret_val += convert(str(pval), old='tex', new='str') + '\n'
 | 
						|
 | 
						|
        if self.fun_kwargs:
 | 
						|
            for k, v in self.fun_kwargs.items():
 | 
						|
                ret_val += f'  {k}: {v}\n'
 | 
						|
 | 
						|
        return ret_val
 | 
						|
 | 
						|
    def _correlation_string(self):
 | 
						|
        ret_val = ''
 | 
						|
        for p_i, p_j, corr_ij, pcorr_ij in self.correlation_list():
 | 
						|
            ret_val += '  {} / {} : {:.4f} ({:.4f})\n'.format(convert(p_i, old='tex', new='str'),
 | 
						|
                                                              convert(p_j, old='tex', new='str'),
 | 
						|
                                                              corr_ij, pcorr_ij)
 | 
						|
        return ret_val
 | 
						|
 | 
						|
    def correlation_list(self, limit=0.1):
 | 
						|
        correlations = []
 | 
						|
 | 
						|
        if self.correlation is not None:
 | 
						|
            pnames = list(self.parameter.keys())
 | 
						|
 | 
						|
            corr = np.triu(self.correlation, k=1)
 | 
						|
 | 
						|
            for i, j in zip(*np.unravel_index(np.argsort(np.abs(corr), axis=None)[::-1],
 | 
						|
                                              self.correlation.shape)):
 | 
						|
                if i == j:
 | 
						|
                    continue
 | 
						|
 | 
						|
                if abs(corr[i, j]) < limit:
 | 
						|
                    break
 | 
						|
 | 
						|
                correlations.append((pnames[i], pnames[j], self.correlation[i, j], self.partial_correlation[i, j]))
 | 
						|
 | 
						|
        return correlations
 | 
						|
 | 
						|
    def savetxt(self, fname: str | Path, err: bool = True):
 | 
						|
        header = self.name + '\n'
 | 
						|
        for pval in self.parameter.values():
 | 
						|
            header += '{}\t{}\t{}\n'.format(convert(pval.name, old='tex', new='str'), pval.value, pval.error)
 | 
						|
        if self.fun_kwargs:
 | 
						|
            for k, v in self.fun_kwargs.items():
 | 
						|
                header += '{}\t{}\n'.format(convert(k, old='tex', new='str'),
 | 
						|
                                            convert(str(v), old='tex', new='str'))
 | 
						|
 | 
						|
        if self.iscomplex == 'complex':
 | 
						|
            np.savetxt(fname, np.c_[self.x, self.y.real, self.y.imag], header=header)
 | 
						|
        else:
 | 
						|
            np.savetxt(fname, np.c_[self.x, self.y], header=header)
 | 
						|
 | 
						|
    def save_parameter(self, fname: str | Path, label: str = None, overwrite: bool = False):
 | 
						|
        path = Path(fname)
 | 
						|
        if not path.is_file():
 | 
						|
            overwrite = True
 | 
						|
 | 
						|
        writemode = 'w' if overwrite else 'a'
 | 
						|
 | 
						|
        if label is None:
 | 
						|
            label = self.value
 | 
						|
 | 
						|
        with path.open(writemode) as f:
 | 
						|
            if overwrite or not path.exists():
 | 
						|
                f.write('# xvalue(1)\t')
 | 
						|
                for i, pname in enumerate(self.parameter.keys()):
 | 
						|
                    raw_name = convert(pname, old='tex', new='str')
 | 
						|
                    f.write(f'{raw_name}({2*i+2})\t{raw_name}_err({2*i+3})\t')
 | 
						|
                f.write('\n')
 | 
						|
 | 
						|
            f.write(str(label).replace(' ', '') + '\t')
 | 
						|
            for p in self.parameter.values():
 | 
						|
                if p.error is not None:
 | 
						|
                    err = p.error
 | 
						|
                else:
 | 
						|
                    err = 0.
 | 
						|
                f.write(f'{p.value:.8e}\t{err:.8e}\t')
 | 
						|
 | 
						|
            if self.fun_kwargs:
 | 
						|
                f.write('# ')
 | 
						|
                for k, v in self.fun_kwargs.items():
 | 
						|
                    f.write(f"{convert(k, old='tex', new='str')}: {convert(str(v), old='tex', new='str')}\t")
 | 
						|
            f.write('\n')
 | 
						|
            f.write(f'# line above from: {self.name} (model: {self.model_name})')
 | 
						|
            f.write('\n')
 | 
						|
 | 
						|
    def f_test(self, chi2: float, dof: float):
 | 
						|
        if 'red. chi^2' not in self.statistics or dof == self.dof:
 | 
						|
            f_value = 1e318
 | 
						|
        else:
 | 
						|
            f_value = (chi2-self.statistics['chi^2']) / (dof-self.dof) / self.statistics['red. chi^2']
 | 
						|
 | 
						|
        try:
 | 
						|
            prob_f = 1-fdist.cdf(f_value, dof-self.dof, self.dof)
 | 
						|
        except:
 | 
						|
            prob_f = 0
 | 
						|
 | 
						|
        return f_value, prob_f
 | 
						|
 | 
						|
    def get_state(self):
 | 
						|
        state = super().get_state()
 | 
						|
 | 
						|
        for attr in ['idx', 'fun_kwargs', 'nobs', 'nvar',
 | 
						|
                     'islog', 'iscomplex', 'x_data', 'y_data']:
 | 
						|
            state[attr] = getattr(self, attr)
 | 
						|
 | 
						|
        state['name'] = self._model_name
 | 
						|
        state['label'] = self.name
 | 
						|
        state['corr'] = self.correlation
 | 
						|
        state['pcorr'] = self.partial_correlation
 | 
						|
        state['stats'] = self.statistics
 | 
						|
        state['resid'] = self.residual
 | 
						|
        state['params'] = {k: v.get_state() for k, v in self.parameter.items()}
 | 
						|
 | 
						|
        state['mode'] = 'fit'
 | 
						|
 | 
						|
        return state
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def set_state(state, **kwargs):
 | 
						|
        state['params'] = {k: Parameter.set_state(v) for k, v in state.pop('params').items()}
 | 
						|
        data = FitResult(**state)
 | 
						|
 | 
						|
        return data
 | 
						|
 | 
						|
    def with_new_x(self, x_values):
 | 
						|
        if self.func is None:
 | 
						|
            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()
 | 
						|
        y_values = self.func.func(self.p_final, x_values, **self.fun_kwargs)
 | 
						|
        if not actual_mode < 0:
 | 
						|
            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)
 | 
						|
 | 
						|
        return new_fit
 | 
						|
 | 
						|
    def sub(self, x_values):
 | 
						|
        part_functions = []
 | 
						|
        actual_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)):
 | 
						|
            if not actual_mode < 0:
 | 
						|
                if actual_mode == 1:
 | 
						|
                    sub_y.imag = 0
 | 
						|
                elif actual_mode == 2:
 | 
						|
                    sub_y.real = 0
 | 
						|
 | 
						|
                part_functions.append(Signal(x_values, sub_y, name=sub_name))
 | 
						|
 | 
						|
            else:
 | 
						|
                part_functions.append(Points(x_values, sub_y, name=sub_name))
 | 
						|
 | 
						|
        if actual_mode > 0:
 | 
						|
            self.fun_kwargs['complex_mode'] = actual_mode
 | 
						|
 | 
						|
        return part_functions
 |