From 7a500282021bafd257967c9568c57ea2a0d33356 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 24 Jul 2023 20:12:55 +0200 Subject: [PATCH 01/23] add namespace to parameter --- src/nmreval/fit/data.py | 2 +- src/nmreval/fit/minimizer.py | 4 + src/nmreval/fit/parameter.py | 137 ++++++++++++++++++++++++----------- 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/src/nmreval/fit/data.py b/src/nmreval/fit/data.py index 42b95bb..5192832 100644 --- a/src/nmreval/fit/data.py +++ b/src/nmreval/fit/data.py @@ -102,7 +102,7 @@ class Data(object): if ub is None: ub = model.ub - self.para_keys = self.parameter.add_parameter(parameter, var=var, lb=lb, ub=ub) + self.para_keys = self.parameter.add_parameter(parameter, var=var, lb=lb, ub=ub, names=model.params) if fun_kwargs is not None: self.fun_kwargs.update(fun_kwargs) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 57e1b56..5305a0a 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -273,8 +273,12 @@ class FitRoutine(object): def __cost_scipy(self, p, data, varpars, used_pars): for keys, values in zip(varpars, p): self.parameter[keys].scaled_value = values + self.parameter._namespace[keys] = self.parameter[keys].scaled_value + + print(id(self.parameter._namespace), id(self.parameter[keys].namespace)) actual_parameters = [self.parameter[keys].value for keys in used_pars] + print(actual_parameters) return data.cost(actual_parameters) def __cost_odr(self, p, data, varpars, used_pars): diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index fcaa284..58dfe63 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -9,6 +9,10 @@ import numpy as np class Parameters(dict): count = count() + def __init__(self): + super().__init__() + self._namespace = {} + def __str__(self): return 'Parameters:\n' + '\n'.join([str(k)+': '+str(v) for k, v in self.items()]) @@ -22,7 +26,7 @@ class Parameters(dict): return super().__getitem__(item) @staticmethod - def _prep_bounds(val, p_len: int) -> list: + def _prep_bounds(val: list, p_len: int) -> list: # helper function to ensure that bounds and variable are of parameter shape if isinstance(val, (Number, bool)) or val is None: return [val] * p_len @@ -34,10 +38,10 @@ class Parameters(dict): return [val[0]] * p_len else: - raise ValueError('Input {} has wrong dimensions'.format(val)) + raise ValueError(f'Input {val} has wrong dimensions') - def add_parameter(self, param, var=None, lb=None, ub=None): - if isinstance(param, Number): + def add_parameter(self, param: float | list[float], var=None, lb=None, ub=None, names=None) -> list: + if isinstance(param, (float, int)): param = [param] p_len = len(param) @@ -50,26 +54,42 @@ class Parameters(dict): new_keys = [] for i in range(p_len): new_idx = next(self.count) - new_keys.append(new_idx) - - self[new_idx] = Parameter(param[i], var=var[i], lb=lb[i], ub=ub[i]) + if names is not None: + key = names[i] + else: + key = f'_p{new_idx}' + new_keys.append(key) + self[key] = Parameter(param[i], var=var[i], lb=lb[i], ub=ub[i]) return new_keys - def copy(self): - p = Parameters() + def add(self, name: str | Parameter, value: str | float | int = None, *, var=True, lb=-np.inf, ub=np.inf): + if isinstance(name, Parameter): + par = name + else: + par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) + self[name] = par + par.eval_allowed = False + self._namespace[name] = par.value + par.namespace = self._namespace + par.eval_allowed = True + + def copy(self) -> Parameters: + print('huhuuhuh') + new_para_dict = Parameters() for k, v in self.items(): - p[k] = Parameter(v.value, var=v.var, lb=v.lb, ub=v.ub) + new_para = v.copy() + new_para_dict.add(new_para) - if len(p) == 0: - return p + # if len(p) == 0: + # return p - max_k = max(p.keys()) - c = next(p.count) - while c < max_k: - c = next(p.count) + # max_k = int(max(p.keys(), key=lambda x: int(k[2:]))[2:]) + # c = next(p.count) + # while c < max_k: + # c = next(p.count) - return p + return new_para_dict def get_state(self): return {k: v.get_state() for k, v in self.items()} @@ -79,33 +99,40 @@ class Parameter: """ Container for one parameter """ - __slots__ = ['name', 'value', 'error', 'init_val', 'var', 'lb', 'ub', 'scale', 'function'] - - def __init__(self, value: float, var: bool = True, lb: float = -np.inf, ub: float = np.inf): - self.lb = lb if lb is not None else -np.inf - self.ub = ub if ub is not None else np.inf - - if self.lb <= value <= self.ub: - self.value = value - else: - print(value, self.lb, self.ub) - raise ValueError('Value of parameter is outside bounds') - - self.init_val = value - - with np.errstate(divide='ignore'): - # throws RuntimeWarning for zeros - self.scale = 10**(np.floor(np.log10(np.abs(self.value)))) - - if self.scale == 0: - self.scale = 1. + def __init__(self, name: str, value: float | str, var: bool = True, lb: float = -np.inf, ub: float = np.inf): + self.scale = 1 self.var = bool(var) if var is not None else True self.error = None if self.var is False else 0.0 - self.name = '' + self.name = name self.function = '' + self.lb = lb if lb is not None else -np.inf + self.ub = ub if ub is not None else np.inf + self._value = None + self.namespace = None + self.eval_allowed = True + self._expr = None - def __str__(self): + if isinstance(value, str): + self._expr = value + self.var = False + + else: + if self.lb <= value <= self.ub: + self._value = value + else: + raise ValueError('Value of parameter is outside bounds') + + self.init_val = value + + with np.errstate(divide='ignore'): + # throws RuntimeWarning for zeros + self.scale = 10**(np.floor(np.log10(np.abs(self.value)))) + + if self.scale == 0: + self.scale = 1. + + def __str__(self) -> str: start = '' if self.name: if self.function: @@ -116,9 +143,15 @@ class Parameter: if self.var: return start + f'{self.value:.4g} +/- {self.error:.4g}, init={self.init_val}' else: - return start + f'{self.value:} (fixed)' + ret_string = start + f'{self.value:}' + if self._expr is None: + ret_string += ' (fixed)' + else: + ret_string += f' (calc: {self._expr})' - def __add__(self, other: Parameter | float) -> float: + return ret_string + + def __add__(self, other: Parameter | float | int) -> float: if isinstance(other, (float, int)): return self.value + other elif isinstance(other, Parameter): @@ -132,8 +165,15 @@ class Parameter: return self.value / self.scale @scaled_value.setter - def scaled_value(self, value): - self.value = value * self.scale + def scaled_value(self, value: float): + self._value = value * self.scale + + @property + def value(self) -> float: + if self._expr is not None and self.eval_allowed: + return eval(self._expr, self.namespace) + elif self._value is not None: + return self._value @property def scaled_error(self): @@ -148,7 +188,7 @@ class Parameter: def get_state(self): - return {slot: getattr(self, slot) for slot in self.__slots__} + return {slot: getattr(self, slot) for slot in self.__slots__} @staticmethod def set_state(state: dict): @@ -165,3 +205,12 @@ class Parameter: name += ' (' + self.function + ')' return name + + def copy(self) -> Parameter: + if self._expr: + val = self._expr + else: + val = self._value + para_copy = Parameter(name=self.name, value=val, var=self.var, lb=self.lb, ub=self.ub) + + return para_copy From ca130eaa147513f12ed7b80bec55574744bef144 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 27 Jul 2023 18:58:22 +0200 Subject: [PATCH 02/23] eval parameter --- src/nmreval/fit/minimizer.py | 14 ++++++-------- src/nmreval/fit/parameter.py | 4 +++- src/nmreval/models/spectrum.py | 11 ++++++----- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 5305a0a..9a5ede5 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -103,7 +103,7 @@ class FitRoutine(object): linked_sender[v] = set() self.parameter.update(v.parameter.copy()) - # set temporaray model + # set temporary model if v.model is None: v.model = self.fit_model self._no_own_model.append(v) @@ -111,9 +111,9 @@ class FitRoutine(object): # register model if v.model not in _found_models: _found_models[v.model] = [] - m_param = v.model.parameter.copy() - self.parameter.update(m_param) - + # m_param = v.model.parameter.copy() + # self.parameter.update(m_param) + # _found_models[v.model].append(v) if v.model not in linked_sender: @@ -208,6 +208,7 @@ class FitRoutine(object): return self.result def _prep_data(self, data): + if data.get_model() is None: data._model = self.fit_model self._no_own_model.append(data) @@ -273,12 +274,9 @@ class FitRoutine(object): def __cost_scipy(self, p, data, varpars, used_pars): for keys, values in zip(varpars, p): self.parameter[keys].scaled_value = values - self.parameter._namespace[keys] = self.parameter[keys].scaled_value - - print(id(self.parameter._namespace), id(self.parameter[keys].namespace)) + self.parameter[keys].namespace[keys] = self.parameter[keys].value actual_parameters = [self.parameter[keys].value for keys in used_pars] - print(actual_parameters) return data.cost(actual_parameters) def __cost_odr(self, p, data, varpars, used_pars): diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 58dfe63..a7800a3 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -66,6 +66,7 @@ class Parameters(dict): def add(self, name: str | Parameter, value: str | float | int = None, *, var=True, lb=-np.inf, ub=np.inf): if isinstance(name, Parameter): par = name + name = par.name else: par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) self[name] = par @@ -75,7 +76,6 @@ class Parameters(dict): par.eval_allowed = True def copy(self) -> Parameters: - print('huhuuhuh') new_para_dict = Parameters() for k, v in self.items(): new_para = v.copy() @@ -171,6 +171,8 @@ class Parameter: @property def value(self) -> float: if self._expr is not None and self.eval_allowed: + print(self._expr, self.namespace) + print(eval(self._expr, self.namespace)) return eval(self._expr, self.namespace) elif self._value is not None: return self._value diff --git a/src/nmreval/models/spectrum.py b/src/nmreval/models/spectrum.py index 229cf66..2a59aee 100644 --- a/src/nmreval/models/spectrum.py +++ b/src/nmreval/models/spectrum.py @@ -35,8 +35,8 @@ class Gaussian: class Lorentzian: type = 'Spectrum' name = 'Lorentzian' - equation = 'A (2/\pi)w/[4*(x-\mu)^{2} + w^{2}] + A_{0}' - params = ['A', '\mu', 'w', 'A_{0}'] + equation = r'A (2/\pi)w/[4*(x-\mu)^{2} + w^{2}] + A_{0}' + params = ['A', r'\mu', 'w', 'A_{0}'] ext_params = None bounds = [(0, None), (None, None), (0, None), (None, None)] @@ -56,15 +56,16 @@ class Lorentzian: off (float): baseline """ + print('Lorentzian', a, mu, sigma, off) return (a/np.pi) * 2*sigma / (4*(x-mu)**2 + sigma**2) + off class PseudoVoigt: type = 'Spectrum' name = 'Pseudo Voigt' - equation = 'A [R*2/\pi*w/[4*(x-\mu)^{2} + w^{2}] + ' \ - '(1-R)*sqrt(4*ln(2)/pi)/w*exp(-4*ln(2)[(x-\mu)/w]^{2})] + A_{0}' - params = ['A', 'R', '\mu', 'w', 'A_{0}'] + equation = r'A [R*2/\pi*w/[4*(x-\mu)^{2} + w^{2}] + ' \ + r'(1-R)*sqrt(4*ln(2)/pi)/w*exp(-4*ln(2)[(x-\mu)/w]^{2})] + A_{0}' + params = ['A', 'R', r'\mu', 'w', 'A_{0}'] ext_params = None bounds = [(0, None), (0, 1), (None, None), (0, None)] From c601e77cec18d230be4769bc8901493de7a2396b Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 30 Jul 2023 19:34:59 +0200 Subject: [PATCH 03/23] fit with expression works with single fit --- src/nmreval/fit/parameter.py | 13 ++++++++++--- src/nmreval/models/spectrum.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index a7800a3..4090574 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -2,6 +2,7 @@ from __future__ import annotations from numbers import Number from itertools import count +from re import sub import numpy as np @@ -69,6 +70,9 @@ class Parameters(dict): name = par.name else: par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) + + name = _prepare_namespace_string(name) + self[name] = par par.eval_allowed = False self._namespace[name] = par.value @@ -114,6 +118,7 @@ class Parameter: self._expr = None if isinstance(value, str): + value = _prepare_namespace_string(value) self._expr = value self.var = False @@ -171,9 +176,7 @@ class Parameter: @property def value(self) -> float: if self._expr is not None and self.eval_allowed: - print(self._expr, self.namespace) - print(eval(self._expr, self.namespace)) - return eval(self._expr, self.namespace) + return eval(self._expr, {}, self.namespace) elif self._value is not None: return self._value @@ -216,3 +219,7 @@ class Parameter: para_copy = Parameter(name=self.name, value=val, var=self.var, lb=self.lb, ub=self.ub) return para_copy + + +def _prepare_namespace_string(expr: str) -> str: + return sub('[\(\)\{.\}\\\]', '_', expr) diff --git a/src/nmreval/models/spectrum.py b/src/nmreval/models/spectrum.py index 2a59aee..1c78de2 100644 --- a/src/nmreval/models/spectrum.py +++ b/src/nmreval/models/spectrum.py @@ -56,7 +56,7 @@ class Lorentzian: off (float): baseline """ - print('Lorentzian', a, mu, sigma, off) + # print('Lorentzian', a, mu, sigma, off) return (a/np.pi) * 2*sigma / (4*(x-mu)**2 + sigma**2) + off From 7762758958b0209c85c32aebf15be0143b42efb9 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Fri, 4 Aug 2023 17:58:48 +0200 Subject: [PATCH 04/23] save original expression --- src/nmreval/fit/parameter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 4090574..879b22e 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -116,8 +116,10 @@ class Parameter: self.namespace = None self.eval_allowed = True self._expr = None + self._expr_disp = None if isinstance(value, str): + self._expr_disp = value value = _prepare_namespace_string(value) self._expr = value self.var = False @@ -152,7 +154,7 @@ class Parameter: if self._expr is None: ret_string += ' (fixed)' else: - ret_string += f' (calc: {self._expr})' + ret_string += f' (calc: {self._expr_disp})' return ret_string @@ -213,7 +215,7 @@ class Parameter: def copy(self) -> Parameter: if self._expr: - val = self._expr + val = self._expr_disp else: val = self._value para_copy = Parameter(name=self.name, value=val, var=self.var, lb=self.lb, ub=self.ub) From 88a32ea7fd3a5130f55af0d04d6f12f9b02c953e Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 15 Aug 2023 18:44:08 +0200 Subject: [PATCH 05/23] multiple single fits working --- src/nmreval/fit/minimizer.py | 18 +++++++++++------- src/nmreval/fit/model.py | 14 +++----------- src/nmreval/fit/parameter.py | 2 +- src/nmreval/models/spectrum.py | 1 - 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 9a5ede5..26f45c5 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -136,6 +136,8 @@ class FitRoutine(object): linked_sender[repl].add(par) linked_sender[par].add(repl) + print(_found_models) + for mm, m_data in _found_models.items(): if mm.global_parameter: for dd in m_data: @@ -235,7 +237,7 @@ class FitRoutine(object): var = [] data_pars = [] - # loopyloop over data that belong to one fit (linked or global) + # loopy-loop over data that belong to one fit (linked or global) for data in data_group: actual_pars = [] for i, (p_k, v_k) in enumerate(data.parameter.items()): @@ -273,10 +275,10 @@ class FitRoutine(object): # COST FUNCTIONS: f(x) - y (least_square, minimize), and f(x) (ODR) def __cost_scipy(self, p, data, varpars, used_pars): for keys, values in zip(varpars, p): - self.parameter[keys].scaled_value = values - self.parameter[keys].namespace[keys] = self.parameter[keys].value + data.parameter[keys].scaled_value = values + data.parameter[keys].namespace[keys] = data.parameter[keys].value - actual_parameters = [self.parameter[keys].value for keys in used_pars] + actual_parameters = [data.parameter[keys].value for keys in used_pars] return data.cost(actual_parameters) def __cost_odr(self, p, data, varpars, used_pars): @@ -438,15 +440,17 @@ class FitRoutine(object): # update parameter values for keys, p_value, err_value in zip(var_pars, p, err): - self.parameter[keys].scaled_value = p_value - self.parameter[keys].scaled_error = err_value + data.parameter[keys].scaled_value = p_value + data.parameter[keys].scaled_error = err_value + data.parameter[keys].namespace[keys] = data.parameter[keys].value + combinations = list(product(var_pars, var_pars)) actual_parameters = [] corr_idx = [] for i, p_i in enumerate(used_pars): - actual_parameters.append(self.parameter[p_i]) + actual_parameters.append(data.parameter[p_i]) for j, p_j in enumerate(used_pars): try: # find the position of the parameter combinations diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index c0121da..bfca558 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -80,23 +80,15 @@ class Model(object): self.fun_kwargs = {k: v.default for k, v in inspect.signature(model.func).parameters.items() if v.default is not inspect.Parameter.empty} - def set_global_parameter(self, idx, p, var=None, lb=None, ub=None, default_bounds=False): - if idx is None: - self.parameter = Parameters() - self.global_parameter = {} - return - + def set_global_parameter(self, key, value, var=None, lb=None, ub=None, default_bounds=False): + idx = [self.params.index(key)] if default_bounds: if lb is None: lb = [self.lb[i] for i in idx] if ub is None: ub = [self.lb[i] for i in idx] - gp = self.parameter.add_parameter(p, var=var, lb=lb, ub=ub) - for k, v in zip(idx, gp): - self.global_parameter[k] = v - - return gp + self.parameter.add(key, value, var=var, lb=lb, ub=ub) @staticmethod def _prep(param_len, val): diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 879b22e..4b5b821 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -60,7 +60,7 @@ class Parameters(dict): else: key = f'_p{new_idx}' new_keys.append(key) - self[key] = Parameter(param[i], var=var[i], lb=lb[i], ub=ub[i]) + self[key] = Parameter(key, param[i], var=var[i], lb=lb[i], ub=ub[i]) return new_keys diff --git a/src/nmreval/models/spectrum.py b/src/nmreval/models/spectrum.py index 1c78de2..30b6917 100644 --- a/src/nmreval/models/spectrum.py +++ b/src/nmreval/models/spectrum.py @@ -56,7 +56,6 @@ class Lorentzian: off (float): baseline """ - # print('Lorentzian', a, mu, sigma, off) return (a/np.pi) * 2*sigma / (4*(x-mu)**2 + sigma**2) + off From 5d43ccb05d52f632a5c2299978a43450895a4af4 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 24 Aug 2023 16:19:09 +0200 Subject: [PATCH 06/23] fit with global parameter --- src/nmreval/fit/data.py | 16 ++- src/nmreval/fit/minimizer.py | 143 +++++++++++++++------------ src/nmreval/fit/model.py | 1 - src/nmreval/fit/parameter.py | 186 ++++++++++++++++------------------- 4 files changed, 180 insertions(+), 166 deletions(-) diff --git a/src/nmreval/fit/data.py b/src/nmreval/fit/data.py index 5192832..e33bfcf 100644 --- a/src/nmreval/fit/data.py +++ b/src/nmreval/fit/data.py @@ -1,7 +1,7 @@ import numpy as np from .model import Model -from .parameter import Parameters +from .parameter import Parameters, Parameter class Data(object): @@ -16,7 +16,7 @@ class Data(object): self.model = None self.minimizer = None self.parameter = Parameters() - self.para_keys = None + self.para_keys: list = [] self.fun_kwargs = {} def __len__(self): @@ -123,6 +123,18 @@ class Data(object): else: return [p.value for p in self.minimizer.parameters[self.parameter]] + def replace_parameter(self, key: str, parameter: Parameter) -> None: + tobereplaced = None + for k, v in self.parameter.items(): + if v.name == parameter.name: + tobereplaced = k + break + + if tobereplaced is None: + raise KeyError(f'Global parameter {key} not found in list of parameters') + self.para_keys[self.para_keys.index(tobereplaced)] = key + self.parameter.replace_parameter(tobereplaced, key, parameter) + def cost(self, p): """ Cost function :math:`y-f(p, x)` diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 26f45c5..71ab2c6 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -21,6 +21,32 @@ class FitAbortException(Exception): pass +# COST FUNCTIONS: f(x) - y (least_square, minimize), and f(x) (ODR) +def _cost_scipy_glob(p: list[float], data: list[Data], varpars: list[str], used_pars: list[list[str]]): + # replace values + for keys, values in zip(varpars, p): + for data_i in data: + if keys in data_i.parameter: + data_i.parameter[keys].scaled_value = values + data_i.parameter[keys].namespace[keys] = data_i.parameter[keys].value + r = [] + # unpack parameter and calculate y values and concatenate all + for values, p_idx in zip(data, used_pars): + actual_parameters = [values.parameter[keys].value for keys in p_idx] + r = np.r_[r, values.cost(actual_parameters)] + + return r + + +def _cost_scipy(p, data, varpars, used_pars): + for keys, values in zip(varpars, p): + data.parameter[keys].scaled_value = values + data.parameter[keys].namespace[keys] = data.parameter[keys].value + + actual_parameters = [data.parameter[keys].value for keys in used_pars] + return data.cost(actual_parameters) + + class FitRoutine(object): def __init__(self, mode='lsq'): self._fitmethod = mode @@ -101,7 +127,8 @@ class FitRoutine(object): for v in self.data: linked_sender[v] = set() - self.parameter.update(v.parameter.copy()) + for k, p_i in v.parameter.items(): + self.parameter.add_parameter(k, p_i.copy()) # set temporary model if v.model is None: @@ -111,6 +138,8 @@ class FitRoutine(object): # register model if v.model not in _found_models: _found_models[v.model] = [] + for k, p in v.model.parameter.items(): + self.parameter.add_parameter(k, p) # m_param = v.model.parameter.copy() # self.parameter.update(m_param) # @@ -120,38 +149,39 @@ class FitRoutine(object): linked_sender[v.model] = set() linked_parameter = {} - for par, par_parm, repl, repl_par in self.linked: - if isinstance(par, Data): - if isinstance(repl, Data): - linked_parameter[par.para_keys[par_parm]] = repl.para_keys[repl_par] - else: - linked_parameter[par.para_keys[par_parm]] = repl.global_parameter[repl_par] - - else: - if isinstance(repl, Data): - par.global_parameter[par_parm] = repl.para_keys[repl_par] - else: - par.global_parameter[par_parm] = repl.global_parameter[repl_par] - - linked_sender[repl].add(par) - linked_sender[par].add(repl) - - print(_found_models) + # for par, par_parm, repl, repl_par in self.linked: + # if isinstance(par, Data): + # if isinstance(repl, Data): + # linked_parameter[par.para_keys[par_parm]] = repl.para_keys[repl_par] + # else: + # linked_parameter[par.para_keys[par_parm]] = repl.parameter[repl_par] + # + # else: + # if isinstance(repl, Data): + # par.global_parameter[par_parm] = repl.para_keys[repl_par] + # else: + # par.global_parameter[par_parm] = repl.global_parameter[repl_par] + # + # linked_sender[repl].add(par) + # linked_sender[par].add(repl) for mm, m_data in _found_models.items(): - if mm.global_parameter: + # print('has global', mm.parameter) + if mm.parameter: for dd in m_data: linked_sender[mm].add(dd) linked_sender[dd].add(mm) coupled_data = [] visited_data = [] + # print('linked', linked_sender) for s in linked_sender.keys(): if s in visited_data: continue sub_graph = [] self.find_paths(s, linked_sender, sub_graph, visited_data) if sub_graph: + # print('sub', sub_graph) coupled_data.append(sub_graph) return coupled_data, linked_parameter @@ -177,6 +207,8 @@ class FitRoutine(object): fit_groups, linked_parameter = self.prepare_links() + # print(fit_groups, self.linked) + for data_groups in fit_groups: if len(data_groups) == 1 and not self.linked: data = data_groups[0] @@ -210,7 +242,6 @@ class FitRoutine(object): return self.result def _prep_data(self, data): - if data.get_model() is None: data._model = self.fit_model self._no_own_model.append(data) @@ -237,22 +268,29 @@ class FitRoutine(object): var = [] data_pars = [] + # print(data_group) + # loopy-loop over data that belong to one fit (linked or global) for data in data_group: - actual_pars = [] - for i, (p_k, v_k) in enumerate(data.parameter.items()): - p_k_used = p_k - v_k_used = v_k + # is parameter replaced by global parameter? + for k, v in data.model.parameter.items(): + data.replace_parameter(k, v) - # is parameter replaced by global parameter? - if i in data.model.global_parameter: - p_k_used = data.model.global_parameter[i] - v_k_used = self.parameter[p_k_used] + actual_pars = [] + for i, p_k in enumerate(data.para_keys): + # print(i, p_k) + p_k_used = p_k + v_k_used = data.parameter[p_k] + + # if i in data.model.parameter: + # p_k_used = data.model.parameter[i] + # v_k_used = self.parameter[p_k_used] + # data.parameter.add_parameter(i, data.model.parameter[i]) # links trump global parameter - if p_k_used in linked: - p_k_used = linked[p_k_used] - v_k_used = self.parameter[p_k_used] + # if p_k_used in linked: + # p_k_used = linked[p_k_used] + # v_k_used = self.parameter[p_k_used] actual_pars.append(p_k_used) # parameter is variable and was not found before as shared parameter @@ -262,6 +300,8 @@ class FitRoutine(object): ub.append(v_k_used.ub / v_k_used.scale) var.append(p_k_used) + # print('aloha, ', actual_pars) + data_pars.append(actual_pars) return data_pars, p0, lb, ub, var @@ -272,16 +312,8 @@ class FitRoutine(object): self._no_own_model = [] - # COST FUNCTIONS: f(x) - y (least_square, minimize), and f(x) (ODR) - def __cost_scipy(self, p, data, varpars, used_pars): - for keys, values in zip(varpars, p): - data.parameter[keys].scaled_value = values - data.parameter[keys].namespace[keys] = data.parameter[keys].value - actual_parameters = [data.parameter[keys].value for keys in used_pars] - return data.cost(actual_parameters) - - def __cost_odr(self, p, data, varpars, used_pars): + def __cost_odr(self, p: list[float], data: Data, varpars: list[str], used_pars: list[str]): for keys, values in zip(varpars, p): self.parameter[keys].scaled_value = values @@ -289,19 +321,6 @@ class FitRoutine(object): return data.func(actual_parameters, data.x) - def __cost_scipy_glob(self, p, data, varpars, used_pars): - # replace values - for keys, values in zip(varpars, p): - self.parameter[keys].scaled_value = values - - r = [] - # unpack parameter and calculate y values and concatenate all - for values, p_idx in zip(data, used_pars): - actual_parameters = [self.parameter[keys].value for keys in p_idx] - r = np.r_[r, values.cost(actual_parameters)] - - return r - def __cost_odr_glob(self, p, data, varpars, used_pars): # replace values for keys, values in zip(varpars, p): @@ -323,7 +342,7 @@ class FitRoutine(object): if self._abort: raise FitAbortException(f'Fit aborted by user') - return self.__cost_scipy(p, data, var, data.para_keys) + return _cost_scipy(p, data, var, data.para_keys) with np.errstate(all='ignore'): res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0)) @@ -337,7 +356,7 @@ class FitRoutine(object): self.step += 1 if self._abort: raise FitAbortException(f'Fit aborted by user') - return self.__cost_scipy_glob(p, data, var, data_pars) + return _cost_scipy_glob(p, data, var, data_pars) with np.errstate(all='ignore'): res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0)) @@ -352,7 +371,7 @@ class FitRoutine(object): self.step += 1 if self._abort: raise FitAbortException(f'Fit aborted by user') - return (self.__cost_scipy(p, data, var, data.para_keys)**2).sum() + return (_cost_scipy(p, data, var, data.para_keys) ** 2).sum() with np.errstate(all='ignore'): res = optimize.minimize(cost, p0, bounds=[(b1, b2) for (b1, b2) in zip(lb, ub)], @@ -365,7 +384,7 @@ class FitRoutine(object): self.step += 1 if self._abort: raise FitAbortException(f'Fit aborted by user') - return (self.__cost_scipy_glob(p, data, var, data_pars)**2).sum() + return (_cost_scipy_glob(p, data, var, data_pars) ** 2).sum() with np.errstate(all='ignore'): res = optimize.minimize(cost, p0, bounds=[(b1, b2) for (b1, b2) in zip(lb, ub)], @@ -438,12 +457,13 @@ class FitRoutine(object): if err is None: err = [0] * len(p) + print(p, var_pars, used_pars) # update parameter values for keys, p_value, err_value in zip(var_pars, p, err): - data.parameter[keys].scaled_value = p_value - data.parameter[keys].scaled_error = err_value - data.parameter[keys].namespace[keys] = data.parameter[keys].value - + if keys in data.parameter: + data.parameter[keys].scaled_value = p_value + data.parameter[keys].scaled_error = err_value + data.parameter[keys].namespace[keys] = data.parameter[keys].value combinations = list(product(var_pars, var_pars)) actual_parameters = [] @@ -511,3 +531,4 @@ class FitRoutine(object): partial_corr = corr return _err, corr, partial_corr + diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index bfca558..76a9da2 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -25,7 +25,6 @@ class Model(object): self.ub = [i if i is not None else inf for i in self.ub] self.parameter = Parameters() - self.global_parameter = {} self.is_complex = None self._complex_part = False diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 4b5b821..0ccb3c6 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -1,99 +1,84 @@ from __future__ import annotations -from numbers import Number +import re from itertools import count -from re import sub - +from io import StringIO import numpy as np class Parameters(dict): - count = count() + parameter_counter = count() + namespace: dict = {} def __init__(self): super().__init__() - self._namespace = {} + self._mapping: dict = {} - def __str__(self): - return 'Parameters:\n' + '\n'.join([str(k)+': '+str(v) for k, v in self.items()]) + def __str__(self) -> str: + return 'Parameters:\n' + '\n'.join([f'{k}: {v}' for k, v in self.items()]) - def __getitem__(self, item): - if isinstance(item, (list, tuple, np.ndarray)): - values = [] - for item_i in item: - values.append(super().__getitem__(item_i)) - return values + def __getitem__(self, item) -> Parameter: + if item in self._mapping: + return super().__getitem__(self._mapping[item]) else: return super().__getitem__(item) - @staticmethod - def _prep_bounds(val: list, p_len: int) -> list: - # helper function to ensure that bounds and variable are of parameter shape - if isinstance(val, (Number, bool)) or val is None: - return [val] * p_len + def __setitem__(self, key, value): + super().__setitem__(key, value) - elif len(val) == p_len: - return val + def add(self, + name: str, + value: str | float | int = None, + *, + var: bool = True, + lb: float = -np.inf, ub: float = np.inf) -> Parameter: - elif len(val) == 1: - return [val[0]] * p_len + par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) - else: - raise ValueError(f'Input {val} has wrong dimensions') + key = f'p{next(Parameters.parameter_counter)}' - def add_parameter(self, param: float | list[float], var=None, lb=None, ub=None, names=None) -> list: - if isinstance(param, (float, int)): - param = [param] + self.add_parameter(key, par) - p_len = len(param) + return par - # make list if only single value is given - var = self._prep_bounds(var, p_len) - lb = self._prep_bounds(lb, p_len) - ub = self._prep_bounds(ub, p_len) + def add_parameter(self, key: str, parameter: Parameter): + self._mapping[parameter.name] = key + self[key] = parameter - new_keys = [] - for i in range(p_len): - new_idx = next(self.count) - if names is not None: - key = names[i] - else: - key = f'_p{new_idx}' - new_keys.append(key) - self[key] = Parameter(key, param[i], var=var[i], lb=lb[i], ub=ub[i]) + self._mapping[parameter.name] = key - return new_keys + self[key] = parameter + parameter.eval_allowed = False + self.namespace[key] = parameter.value + parameter.namespace = self.namespace + parameter.eval_allowed = True - def add(self, name: str | Parameter, value: str | float | int = None, *, var=True, lb=-np.inf, ub=np.inf): - if isinstance(name, Parameter): - par = name - name = par.name - else: - par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) + # look for variables in expression and replace with valid names + for p in self.values(): + if p._expr is not None: + expression = p._expr + for n, k in self._mapping.items(): + expression = re.sub(re.escape(n), k, expression) - name = _prepare_namespace_string(name) + p._expr = expression - self[name] = par - par.eval_allowed = False - self._namespace[name] = par.value - par.namespace = self._namespace - par.eval_allowed = True + def replace_parameter(self, key_out: str, key_in: str, parameter: Parameter): + for k, v in self._mapping.items(): + if v == key_out: + self._mapping[k] = key_in + break - def copy(self) -> Parameters: - new_para_dict = Parameters() - for k, v in self.items(): - new_para = v.copy() - new_para_dict.add(new_para) + self.add_parameter(key_in, parameter) + del self.namespace[key_out] - # if len(p) == 0: - # return p - - # max_k = int(max(p.keys(), key=lambda x: int(k[2:]))[2:]) - # c = next(p.count) - # while c < max_k: - # c = next(p.count) - - return new_para_dict + for p in self.values(): + try: + p.value + except NameError: + expression = p._expr_disp + for n, k in self._mapping.items(): + expression = re.sub(re.escape(n), k, expression) + p._expr = expression def get_state(self): return {k: v.get_state() for k, v in self.items()} @@ -105,25 +90,23 @@ class Parameter: """ def __init__(self, name: str, value: float | str, var: bool = True, lb: float = -np.inf, ub: float = np.inf): - self.scale = 1 - self.var = bool(var) if var is not None else True - self.error = None if self.var is False else 0.0 - self.name = name - self.function = '' - self.lb = lb if lb is not None else -np.inf - self.ub = ub if ub is not None else np.inf - self._value = None - self.namespace = None - self.eval_allowed = True - self._expr = None - self._expr_disp = None + self._value: float | None = None + self.var: bool = bool(var) if var is not None else True + self.error: None | float = None if self.var is False else 0.0 + self.name: str = name + self.function: str = "" + + self.lb: None | float = lb if lb is not None else -np.inf + self.ub: float | None = ub if ub is not None else np.inf + self.namespace: dict = {} + self.eval_allowed: bool = True + self._expr: None | str = None + self._expr_disp: None | str = None if isinstance(value, str): self._expr_disp = value - value = _prepare_namespace_string(value) self._expr = value self.var = False - else: if self.lb <= value <= self.ub: self._value = value @@ -140,23 +123,24 @@ class Parameter: self.scale = 1. def __str__(self) -> str: - start = '' + start = StringIO() if self.name: if self.function: - start = f'{self.name} ({self.function}): ' + start.write(f"{self.name} ({self.function}): ") else: - start = self.name + ': ' + start.write(self.name) + start.write(": ") if self.var: - return start + f'{self.value:.4g} +/- {self.error:.4g}, init={self.init_val}' + start.write(f"{self.value:.4g} +/- {self.error:.4g}, init={self.init_val}") else: - ret_string = start + f'{self.value:}' + start.write(f"{self.value:}") if self._expr is None: - ret_string += ' (fixed)' + start.write(" (fixed)") else: - ret_string += f' (calc: {self._expr_disp})' + start.write(f" (calc: {self._expr_disp})") - return ret_string + return start.getvalue() def __add__(self, other: Parameter | float | int) -> float: if isinstance(other, (float, int)): @@ -168,11 +152,11 @@ class Parameter: return self.__add__(other) @property - def scaled_value(self): + def scaled_value(self) -> float: return self.value / self.scale @scaled_value.setter - def scaled_value(self, value: float): + def scaled_value(self, value: float) -> None: self._value = value * self.scale @property @@ -183,22 +167,21 @@ class Parameter: return self._value @property - def scaled_error(self): + def scaled_error(self) -> None | float: if self.error is None: return self.error else: return self.error / self.scale @scaled_error.setter - def scaled_error(self, value): + def scaled_error(self, value) -> None: self.error = value * self.scale - def get_state(self): - + def get_state(self) -> dict: return {slot: getattr(self, slot) for slot in self.__slots__} @staticmethod - def set_state(state: dict): + def set_state(state: dict) -> Parameter: par = Parameter(state.pop('value')) for k, v in state.items(): setattr(par, k, v) @@ -206,10 +189,10 @@ class Parameter: return par @property - def full_name(self): + def full_name(self) -> str: name = self.name if self.function: - name += ' (' + self.function + ')' + name += f" ({self.function})" return name @@ -219,9 +202,8 @@ class Parameter: else: val = self._value para_copy = Parameter(name=self.name, value=val, var=self.var, lb=self.lb, ub=self.ub) + para_copy._expr = self._expr + para_copy.namespace = self.namespace return para_copy - -def _prepare_namespace_string(expr: str) -> str: - return sub('[\(\)\{.\}\\\]', '_', expr) From 0b8f4932b25f4ad16b7309575044ee568a6f50ac Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Fri, 25 Aug 2023 18:46:36 +0200 Subject: [PATCH 07/23] seems mostly to be working --- src/gui_qt/fit/fit_parameter.py | 4 +- src/gui_qt/fit/fitwindow.py | 11 ++++- src/gui_qt/main/management.py | 4 +- src/nmreval/fit/data.py | 43 ++++++++++++++----- src/nmreval/fit/minimizer.py | 75 ++++++++++++++------------------- src/nmreval/fit/model.py | 12 +++++- src/nmreval/fit/parameter.py | 45 +++++++++++++++----- src/nmreval/models/diffusion.py | 2 +- 8 files changed, 123 insertions(+), 73 deletions(-) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 682893e..292c220 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -227,7 +227,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): kw_p = {} p = [] if global_p is None: - global_p = {'p': [], 'idx': [], 'var': [], 'ub': [], 'lb': []} + global_p = {'value': [], 'idx': [], 'var': [], 'ub': [], 'lb': []} for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)): if isinstance(g, FitModelWidget): @@ -235,7 +235,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): p.append(globs[i]) if is_global[i]: if i not in global_p['idx']: - global_p['p'].append(globs[i]) + global_p['value'].append(globs[i]) global_p['idx'].append(i) global_p['var'].append(is_fixed[i]) global_p['ub'].append(ub[i]) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index 519216e..ea66eef 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -221,7 +221,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): parameter: dict = None, add_idx: bool = False, cnt: int = 0) -> tuple[dict, int]: if parameter is None: parameter = {'parameter': {}, 'lb': (), 'ub': (), 'var': [], - 'glob': {'idx': [], 'p': [], 'var': [], 'lb': [], 'ub': []}, + 'glob': {'idx': [], 'value': [], 'var': [], 'lb': [], 'ub': []}, 'links': [], 'color': []} for i, f in enumerate(model): @@ -269,7 +269,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if f['children']: # recurse for children - child_parameter, cnt = self._prepare(f['children'], parameter=parameter, add_idx=add_idx, cnt=cnt) + _, cnt = self._prepare(f['children'], parameter=parameter, add_idx=add_idx, cnt=cnt) return parameter, cnt @@ -288,6 +288,13 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if k in data: parameter, _ = self._prepare(mod, function_use=data[k], add_idx=isinstance(func, MultiModel)) + + # convert positions of global parameter to corresponding names + global_parameter: dict = parameter['glob'] + positions = global_parameter.pop('idx') + global_parameter['key'] = [pname for i, pname in enumerate(func.params) if i in positions] + # print(global_parameter) + if parameter is None: return diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 191fe7c..dfc98d7 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -467,7 +467,9 @@ class UpperManagement(QtCore.QObject): model_globs = model_p['glob'] if model_globs: - m.set_global_parameter(**model_p['glob']) + for parameter_args in zip(*model_globs.values()): + m.set_global_parameter(**{k: v for k, v in zip(model_globs.keys(), parameter_args)}) + # m.set_global_parameter(**model_p['glob']) for links_i in links: self.fitter.set_link_parameter((models[links_i[0]], links_i[1]), diff --git a/src/nmreval/fit/data.py b/src/nmreval/fit/data.py index e33bfcf..0d7ed86 100644 --- a/src/nmreval/fit/data.py +++ b/src/nmreval/fit/data.py @@ -68,12 +68,19 @@ class Data(object): def get_model(self): return self.model - def set_parameter(self, parameter, var=None, ub=None, lb=None, - default_bounds=False, fun_kwargs=None): + def set_parameter(self, + values: list[float], + *, + var: list[bool] = None, + ub: list[float] = None, + lb: list[float] = None, + default_bounds: bool = False, + fun_kwargs: dict = None + ): """ Creates parameter for this data. If no Model is available, it falls back to the model - :param parameter: list of parameters + :param values: list of parameters :param var: list of boolean or boolean; False fixes parameter at given list index. Single value is broadcast to all parameter :param ub: list of upper boundaries or float; Single value is broadcast to all parameter. @@ -87,23 +94,37 @@ class Data(object): model = self.model if model is None: # Data has no unique - if self.minimizer is None: - model = None - else: + if self.minimizer is not None: model = self.minimizer.fit_model - self.fun_kwargs.update(model.fun_kwargs) if model is None: raise ValueError('No model found, please set model before parameters') - if default_bounds: - if lb is None: + if len(values) != len(model.params): + raise ValueError('Number of given parameter does not match number of model parameters') + + if var is None: + var = [True] * len(values) + + if lb is None: + if default_bounds: lb = model.lb - if ub is None: + else: + lb = [None] * len(values) + + if ub is None: + if default_bounds: ub = model.ub + else: + ub = [None] * len(values) - self.para_keys = self.parameter.add_parameter(parameter, var=var, lb=lb, ub=ub, names=model.params) + arg_names = ['name', 'value', 'var', 'lb', 'ub'] + for parameter_arg in zip(model.params, values, var, lb, ub): + self.parameter.add(**{arg_name: arg_value for arg_name, arg_value in zip(arg_names, parameter_arg)}) + self.para_keys = list(self.parameter.keys()) + + self.fun_kwargs.update(model.fun_kwargs) if fun_kwargs is not None: self.fun_kwargs.update(fun_kwargs) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 71ab2c6..2dbca81 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings from itertools import product @@ -26,7 +28,7 @@ def _cost_scipy_glob(p: list[float], data: list[Data], varpars: list[str], used_ # replace values for keys, values in zip(varpars, p): for data_i in data: - if keys in data_i.parameter: + if keys in data_i.parameter.keys(): data_i.parameter[keys].scaled_value = values data_i.parameter[keys].namespace[keys] = data_i.parameter[keys].value r = [] @@ -53,7 +55,6 @@ class FitRoutine(object): self.data = [] self.fit_model = None self._no_own_model = [] - self.parameter = Parameters() self.result = [] self.linked = [] self._abort = False @@ -107,28 +108,25 @@ class FitRoutine(object): return self.fit_model - def set_link_parameter(self, parameter: tuple, replacement: tuple): + def set_link_parameter(self, dismissed_param: tuple[Model | Data, str], replacement: tuple[Model, str]): if isinstance(replacement[0], Model): - if replacement[1] not in replacement[0].global_parameter: - raise KeyError(f'Parameter at pos {replacement[1]} of ' - f'model {str(replacement[0])} is not global') + if replacement[1] not in replacement[0].parameter: + raise KeyError(f'Parameter {replacement[1]} of ' + f'model {replacement[0]} is not global') - if isinstance(parameter[0], Model): - warnings.warn(f'Replaced parameter at pos {parameter[1]} in {str(parameter[0])} ' + if isinstance(dismissed_param[0], Model): + warnings.warn(f'Replaced parameter {dismissed_param[1]} in {dismissed_param[0]} ' f'becomes global with linkage.') - self.linked.append((*parameter, *replacement)) + self.linked.append((*dismissed_param, *replacement)) def prepare_links(self): self._no_own_model = [] - self.parameter = Parameters() _found_models = {} linked_sender = {} for v in self.data: linked_sender[v] = set() - for k, p_i in v.parameter.items(): - self.parameter.add_parameter(k, p_i.copy()) # set temporary model if v.model is None: @@ -138,35 +136,29 @@ class FitRoutine(object): # register model if v.model not in _found_models: _found_models[v.model] = [] - for k, p in v.model.parameter.items(): - self.parameter.add_parameter(k, p) - # m_param = v.model.parameter.copy() - # self.parameter.update(m_param) - # + _found_models[v.model].append(v) if v.model not in linked_sender: linked_sender[v.model] = set() linked_parameter = {} - # for par, par_parm, repl, repl_par in self.linked: - # if isinstance(par, Data): - # if isinstance(repl, Data): - # linked_parameter[par.para_keys[par_parm]] = repl.para_keys[repl_par] - # else: - # linked_parameter[par.para_keys[par_parm]] = repl.parameter[repl_par] - # - # else: - # if isinstance(repl, Data): - # par.global_parameter[par_parm] = repl.para_keys[repl_par] - # else: - # par.global_parameter[par_parm] = repl.global_parameter[repl_par] - # - # linked_sender[repl].add(par) - # linked_sender[par].add(repl) + for dismiss_model, dismiss_param, replace_model, replace_param in self.linked: + linked_sender[replace_model].add(dismiss_model) + linked_sender[replace_model].add(replace_model) + + replace_key = replace_model.parameter.get_key(replace_param) + dismiss_key = dismiss_model.parameter.get_key(dismiss_param) + + if isinstance(replace_model, Data): + linked_parameter[dismiss_key] = replace_key + else: + # print('dismiss model', dismiss_model.parameter) + # print('replace model', replace_model.parameter) + dismiss_model.parameter.replace_parameter(dismiss_key, replace_key, replace_model.parameter[replace_key]) + # print('after replacement', dismiss_model.parameter) for mm, m_data in _found_models.items(): - # print('has global', mm.parameter) if mm.parameter: for dd in m_data: linked_sender[mm].add(dd) @@ -174,14 +166,12 @@ class FitRoutine(object): coupled_data = [] visited_data = [] - # print('linked', linked_sender) for s in linked_sender.keys(): if s in visited_data: continue sub_graph = [] self.find_paths(s, linked_sender, sub_graph, visited_data) if sub_graph: - # print('sub', sub_graph) coupled_data.append(sub_graph) return coupled_data, linked_parameter @@ -203,12 +193,8 @@ class FitRoutine(object): def run(self, mode='lsq'): self._abort = False - self.parameter = Parameters() fit_groups, linked_parameter = self.prepare_links() - - # print(fit_groups, self.linked) - for data_groups in fit_groups: if len(data_groups) == 1 and not self.linked: data = data_groups[0] @@ -226,6 +212,7 @@ class FitRoutine(object): self._odr_single(data, p0_k, var_pars_k) else: + # print('INSIDE RUN') data_pars, p0, lb, ub, var_pars = self._prep_global(data_groups, linked_parameter) if mode == 'lsq': @@ -262,18 +249,21 @@ class FitRoutine(object): return pp, lb, ub, var_pars def _prep_global(self, data_group, linked): + # print('PREP GLOBAL') + # print(data_group, linked) + p0 = [] lb = [] ub = [] var = [] data_pars = [] - # print(data_group) - # loopy-loop over data that belong to one fit (linked or global) for data in data_group: # is parameter replaced by global parameter? + # print('SET GLOBAL') for k, v in data.model.parameter.items(): + # print(k, v) data.replace_parameter(k, v) actual_pars = [] @@ -312,7 +302,6 @@ class FitRoutine(object): self._no_own_model = [] - def __cost_odr(self, p: list[float], data: Data, varpars: list[str], used_pars: list[str]): for keys, values in zip(varpars, p): self.parameter[keys].scaled_value = values @@ -361,6 +350,7 @@ class FitRoutine(object): with np.errstate(all='ignore'): 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) for v, var_pars_k in zip(data, data_pars): self.make_results(v, res.x, var, var_pars_k, res.jac.shape, @@ -457,7 +447,6 @@ class FitRoutine(object): if err is None: err = [0] * len(p) - print(p, var_pars, used_pars) # update parameter values for keys, p_value, err_value in zip(var_pars, p, err): if keys in data.parameter: diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index 76a9da2..a4db952 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -79,7 +79,14 @@ class Model(object): self.fun_kwargs = {k: v.default for k, v in inspect.signature(model.func).parameters.items() if v.default is not inspect.Parameter.empty} - def set_global_parameter(self, key, value, var=None, lb=None, ub=None, default_bounds=False): + def set_global_parameter(self, + key: str, + value: float | str, + var: bool = None, + lb: float = None, + ub: float = None, + default_bounds: bool = False + ): idx = [self.params.index(key)] if default_bounds: if lb is None: @@ -87,7 +94,8 @@ class Model(object): if ub is None: ub = [self.lb[i] for i in idx] - self.parameter.add(key, value, var=var, lb=lb, ub=ub) + p = self.parameter.add(key, value, var=var, lb=lb, ub=ub) + p.is_global = True @staticmethod def _prep(param_len, val): diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 0ccb3c6..e9bf2c1 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -8,6 +8,7 @@ import numpy as np class Parameters(dict): parameter_counter = count() + # is one global namespace a good idea? namespace: dict = {} def __init__(self): @@ -24,7 +25,14 @@ class Parameters(dict): return super().__getitem__(item) def __setitem__(self, key, value): - super().__setitem__(key, value) + self.add_parameter(key, value) + + def __contains__(self, item): + for v in self.values(): + if item == v.name: + return True + + return False def add(self, name: str, @@ -34,7 +42,6 @@ class Parameters(dict): lb: float = -np.inf, ub: float = np.inf) -> Parameter: par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) - key = f'p{next(Parameters.parameter_counter)}' self.add_parameter(key, par) @@ -43,11 +50,8 @@ class Parameters(dict): def add_parameter(self, key: str, parameter: Parameter): self._mapping[parameter.name] = key - self[key] = parameter + super().__setitem__(key, parameter) - self._mapping[parameter.name] = key - - self[key] = parameter parameter.eval_allowed = False self.namespace[key] = parameter.value parameter.namespace = self.namespace @@ -63,13 +67,17 @@ class Parameters(dict): p._expr = expression def replace_parameter(self, key_out: str, key_in: str, parameter: Parameter): + # print('replace par', key_out, key_in, parameter) + # print('name', parameter.name) + + self.add_parameter(key_in, parameter) for k, v in self._mapping.items(): if v == key_out: self._mapping[k] = key_in break - self.add_parameter(key_in, parameter) - del self.namespace[key_out] + if key_out in self.namespace: + del self.namespace[key_out] for p in self.values(): try: @@ -80,6 +88,13 @@ class Parameters(dict): expression = re.sub(re.escape(n), k, expression) p._expr = expression + def get_key(self, name: str) -> str | None: + for k, v in self.items(): + if name == v.name: + return k + + return + def get_state(self): return {k: v.get_state() for k, v in self.items()} @@ -102,6 +117,7 @@ class Parameter: self.eval_allowed: bool = True self._expr: None | str = None self._expr_disp: None | str = None + self.is_global = False if isinstance(value, str): self._expr_disp = value @@ -126,15 +142,19 @@ class Parameter: start = StringIO() if self.name: if self.function: - start.write(f"{self.name} ({self.function}): ") + start.write(f"{self.name} ({self.function})") else: start.write(self.name) - start.write(": ") + + if self.is_global: + start.write("*") + + start.write(": ") if self.var: start.write(f"{self.value:.4g} +/- {self.error:.4g}, init={self.init_val}") else: - start.write(f"{self.value:}") + start.write(f"{self.value:.4g}") if self._expr is None: start.write(" (fixed)") else: @@ -204,6 +224,9 @@ class Parameter: para_copy = Parameter(name=self.name, value=val, var=self.var, lb=self.lb, ub=self.ub) para_copy._expr = self._expr para_copy.namespace = self.namespace + para_copy.is_global = self.is_global + para_copy.error = self.error + para_copy.function = self.function return para_copy diff --git a/src/nmreval/models/diffusion.py b/src/nmreval/models/diffusion.py index 4e03f92..32dba92 100644 --- a/src/nmreval/models/diffusion.py +++ b/src/nmreval/models/diffusion.py @@ -125,7 +125,7 @@ class Peschier: q = nucleus*g*tp r1s, r1f = 1 / t1s, 1 / t1f - kf, ks = pf*k, (1-pf)*k + kf, ks = (1-pf)*k, pf*k a_plus = 0.5 * (d*q*q + kf + ks + r1f + r1s + np.sqrt((d*q*q + kf + r1f - ks - r1s)**2 + 4*kf*ks)) a_minu = 0.5 * (d*q*q + kf + ks + r1f + r1s - np.sqrt((d*q*q + kf + r1f - ks - r1s)**2 + 4*kf*ks)) From d17d0f251ece24d712de8425e00fe2e78940b9b3 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 26 Aug 2023 20:08:13 +0200 Subject: [PATCH 08/23] work on linked models --- src/nmreval/fit/minimizer.py | 14 ++------------ src/nmreval/fit/model.py | 6 ++++-- src/nmreval/fit/result.py | 7 +++---- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 2dbca81..c4b8fae 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -153,10 +153,8 @@ class FitRoutine(object): if isinstance(replace_model, Data): linked_parameter[dismiss_key] = replace_key else: - # print('dismiss model', dismiss_model.parameter) - # print('replace model', replace_model.parameter) - dismiss_model.parameter.replace_parameter(dismiss_key, replace_key, replace_model.parameter[replace_key]) - # print('after replacement', dismiss_model.parameter) + p = dismiss_model.set_global_parameter(dismiss_param, replace_key) + p._expr_disp = replace_param for mm, m_data in _found_models.items(): if mm.parameter: @@ -212,7 +210,6 @@ class FitRoutine(object): self._odr_single(data, p0_k, var_pars_k) else: - # print('INSIDE RUN') data_pars, p0, lb, ub, var_pars = self._prep_global(data_groups, linked_parameter) if mode == 'lsq': @@ -249,8 +246,6 @@ class FitRoutine(object): return pp, lb, ub, var_pars def _prep_global(self, data_group, linked): - # print('PREP GLOBAL') - # print(data_group, linked) p0 = [] lb = [] @@ -261,14 +256,11 @@ class FitRoutine(object): # loopy-loop over data that belong to one fit (linked or global) for data in data_group: # is parameter replaced by global parameter? - # print('SET GLOBAL') for k, v in data.model.parameter.items(): - # print(k, v) data.replace_parameter(k, v) actual_pars = [] for i, p_k in enumerate(data.para_keys): - # print(i, p_k) p_k_used = p_k v_k_used = data.parameter[p_k] @@ -290,8 +282,6 @@ class FitRoutine(object): ub.append(v_k_used.ub / v_k_used.scale) var.append(p_k_used) - # print('aloha, ', actual_pars) - data_pars.append(actual_pars) return data_pars, p0, lb, ub, var diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index a4db952..faca53d 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -6,7 +6,7 @@ from typing import Sized from numpy import inf from ._meta import MultiModel -from .parameter import Parameters +from .parameter import Parameters, Parameter class Model(object): @@ -86,7 +86,7 @@ class Model(object): lb: float = None, ub: float = None, default_bounds: bool = False - ): + ) -> Parameter: idx = [self.params.index(key)] if default_bounds: if lb is None: @@ -97,6 +97,8 @@ class Model(object): p = self.parameter.add(key, value, var=var, lb=lb, ub=ub) p.is_global = True + return p + @staticmethod def _prep(param_len, val): if isinstance(val, Sized): diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index a2a83d1..09c550d 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -223,6 +223,7 @@ class FitResult(Points): return self.nobs-self.nvar def pprint(self, statistics=True, correlations=True): + sstream = StringIO() print('Fit result:') print(' model :', self.name) print(' #data :', self.nobs) @@ -243,7 +244,7 @@ class FitResult(Points): def parameter_string(self): ret_val = '' - for pval in self.parameter.values(): + for pkey, pval in self.parameter.items(): ret_val += convert(str(pval), old='tex', new='str') + '\n' if self.fun_kwargs: @@ -255,9 +256,7 @@ class FitResult(Points): 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) + ret_val += f" {convert(p_i, old='tex', new='str')} / {convert(p_j, old='tex', new='str')} : {corr_ij:.4f} ({pcorr_ij:.4f})\n" return ret_val def correlation_list(self, limit=0.1): From 5a153585ee45119f5118b079800aaa9511df20a9 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 29 Aug 2023 19:11:56 +0200 Subject: [PATCH 09/23] write error for global fits --- src/nmreval/fit/minimizer.py | 2 +- src/nmreval/fit/result.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index c4b8fae..9e43046 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -439,7 +439,7 @@ class FitRoutine(object): # update parameter values for keys, p_value, err_value in zip(var_pars, p, err): - if keys in data.parameter: + if keys in data.parameter.keys(): data.parameter[keys].scaled_value = p_value data.parameter[keys].scaled_error = err_value data.parameter[keys].namespace[keys] = data.parameter[keys].value diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index 09c550d..9cf6d5e 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -223,7 +223,6 @@ class FitResult(Points): return self.nobs-self.nvar def pprint(self, statistics=True, correlations=True): - sstream = StringIO() print('Fit result:') print(' model :', self.name) print(' #data :', self.nobs) From d2e63a5ee307dd07abe029ed23278f17ca949b3f Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 29 Aug 2023 19:44:09 +0200 Subject: [PATCH 10/23] refactor odr --- src/nmreval/fit/minimizer.py | 92 ++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 9e43046..b9dec07 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -49,6 +49,37 @@ def _cost_scipy(p, data, varpars, used_pars): return data.cost(actual_parameters) +def _cost_odr(p: list[float], data: Data, varpars: list[str], used_pars: list[str], fitmode: int=0): + for keys, values in zip(varpars, p): + data.parameter[keys].scaled_value = values + data.parameter[keys].namespace[keys] = data.parameter[keys].value + + actual_parameters = [data.parameter[keys].value for keys in used_pars] + + return data.func(actual_parameters, data.x) + + +def _cost_odr_glob(p: list[float], data: list[Data], var_pars: list[str], used_pars: list[str]): + # replace values + for data_i in data: + _update_parameter(data_i, var_pars, p) + + r = [] + # unpack parameter and calculate y values and concatenate all + for values, p_idx in zip(data, used_pars): + actual_parameters = [values.parameter[keys].value for keys in p_idx] + r = np.r_[r, values.func(actual_parameters, values.x)] + + return r + + +def _update_parameter(data: Data, varied_keys: list[str], parameter: list[float]): + for keys, values in zip(varied_keys, parameter): + if keys in data.parameter.keys(): + data.parameter[keys].scaled_value = values + data.parameter[keys].namespace[keys] = data.parameter[keys].value + + class FitRoutine(object): def __init__(self, mode='lsq'): self._fitmethod = mode @@ -189,7 +220,7 @@ class FitRoutine(object): logger.info('Fit aborted by user') self._abort = True - def run(self, mode='lsq'): + def run(self, mode: str = 'lsq'): self._abort = False fit_groups, linked_parameter = self.prepare_links() @@ -246,7 +277,6 @@ class FitRoutine(object): return pp, lb, ub, var_pars def _prep_global(self, data_group, linked): - p0 = [] lb = [] ub = [] @@ -264,16 +294,6 @@ class FitRoutine(object): p_k_used = p_k v_k_used = data.parameter[p_k] - # if i in data.model.parameter: - # p_k_used = data.model.parameter[i] - # v_k_used = self.parameter[p_k_used] - # data.parameter.add_parameter(i, data.model.parameter[i]) - - # links trump global parameter - # if p_k_used in linked: - # p_k_used = linked[p_k_used] - # v_k_used = self.parameter[p_k_used] - actual_pars.append(p_k_used) # parameter is variable and was not found before as shared parameter if v_k_used.var and p_k_used not in var: @@ -292,27 +312,6 @@ class FitRoutine(object): self._no_own_model = [] - def __cost_odr(self, p: list[float], data: Data, varpars: list[str], used_pars: list[str]): - for keys, values in zip(varpars, p): - self.parameter[keys].scaled_value = values - - actual_parameters = [self.parameter[keys].value for keys in used_pars] - - return data.func(actual_parameters, data.x) - - def __cost_odr_glob(self, p, data, varpars, used_pars): - # replace values - for keys, values in zip(varpars, p): - self.parameter[keys].scaled_value = values - - r = [] - # unpack parameter and calculate y values and concatenate all - for values, p_idx in zip(data, used_pars): - actual_parameters = [self.parameter[keys].value for keys in p_idx] - r = np.r_[r, values.func(actual_parameters, values.x)] - - return r - def _least_squares_single(self, data, p0, lb, ub, var): self.step = 0 @@ -380,13 +379,18 @@ class FitRoutine(object): self.step += 1 if self._abort: raise FitAbortException(f'Fit aborted by user') - return self.__cost_odr(p, data, var_pars, data.para_keys) + return _cost_odr(p, data, var_pars, data.para_keys) odr_model = odr.Model(func) + corr, partial_corr, res = self._odr_fit(odr_data, odr_model, p0) + + self.make_results(data, res.beta, var_pars, data.para_keys, (len(data), len(p0)), + err=res.sd_beta, corr=corr, partial_corr=partial_corr) + + def _odr_fit(self, odr_data, odr_model, p0): o = odr.ODR(odr_data, odr_model, beta0=p0) res = o.run() - corr = res.cov_beta / (res.sd_beta[:, None] * res.sd_beta[None, :]) * res.res_var try: corr_inv = np.linalg.inv(corr) @@ -395,16 +399,14 @@ class FitRoutine(object): partial_corr[np.diag_indices_from(partial_corr)] = 1. except np.linalg.LinAlgError: partial_corr = corr - - self.make_results(data, res.beta, var_pars, data.para_keys, (len(data), len(p0)), - err=res.sd_beta, corr=corr, partial_corr=partial_corr) + return corr, partial_corr, res def _odr_global(self, data, p0, var, data_pars): def func(p, _): self.step += 1 if self._abort: raise FitAbortException(f'Fit aborted by user') - return self.__cost_odr_glob(p, data, var, data_pars) + return _cost_odr_glob(p, data, var, data_pars) x = [] y = [] @@ -415,17 +417,7 @@ class FitRoutine(object): odr_data = odr.Data(x, y) odr_model = odr.Model(func) - o = odr.ODR(odr_data, odr_model, beta0=p0, ifixb=var) - res = o.run() - - corr = res.cov_beta / (res.sd_beta[:, None] * res.sd_beta[None, :]) * res.res_var - 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 + corr, partial_corr, res = self._odr_fit(odr_data, odr_model, p0) for v, var_pars_k in zip(data, data_pars): self.make_results(v, res.beta, var, var_pars_k, (sum(len(d) for d in data), len(p0)), From 311157a01a742121fce2a3aee5e76cd44f5e11a9 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 3 Sep 2023 20:17:07 +0200 Subject: [PATCH 11/23] fix parameter setting after fitting multiple models --- src/gui_qt/fit/fitwindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index ea66eef..55116b7 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -465,9 +465,9 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): # which data uses which model data = self.data_table.collect_data(default=self.default_combobox.currentData()) - glob_fit_parameter = [] - for fitted_model, fitted_data in data.items(): + glob_fit_parameter = [] + for fit_id, fit_curve in parameter.items(): if fit_id in fitted_data: fit_parameter = list(fit_curve.parameter.values()) From 53c58b2bbb7cb3db58e877fdab3083daa855c72d Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 6 Sep 2023 17:45:57 +0200 Subject: [PATCH 12/23] disable validators --- src/gui_qt/fit/fit_forms.py | 15 ++++++++------- src/gui_qt/fit/fit_parameter.py | 11 +++++++---- src/nmreval/fit/minimizer.py | 2 ++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index f0f63f7..983a0db 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -19,9 +19,9 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): self.parametername.setText(label + ' ') - validator = QtGui.QDoubleValidator() - validator.setDecimals(9) - self.parameter_line.setValidator(validator) + # validator = QtGui.QDoubleValidator() + # validator.setDecimals(9) + # self.parameter_line.setValidator(validator) self.parameter_line.setText('1') self.parameter_line.setMaximumWidth(60) self.lineEdit.setMaximumWidth(60) @@ -97,10 +97,11 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): try: p = float(self.parameter_line.text().replace(',', '.')) except ValueError: - _ = QtWidgets.QMessageBox().warning(self, 'Invalid value', - f'{self.parametername.text()} contains invalid values', - QtWidgets.QMessageBox.Cancel) - return None + p = self.parameter_line.text().replace(',', '.') + # _ = QtWidgets.QMessageBox().warning(self, 'Invalid value', + # f'{self.parametername.text()} contains invalid values', + # QtWidgets.QMessageBox.Cancel) + # return None if self.checkBox.isChecked(): try: diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 292c220..b461d23 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -124,10 +124,12 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): if idx is None: idx = self.global_parameter.index(self.sender()) - self.glob_values[idx] = float(value) + # self.glob_values[idx] = float(value) + self.glob_values[idx] = value if self.data_values[self.comboBox.currentData()][idx] is None: self.data_parameter[idx].blockSignals(True) - self.data_parameter[idx].value = float(value) + # self.data_parameter[idx].value = float(value) + self.data_parameter[idx].value = value self.data_parameter[idx].blockSignals(False) @QtCore.pyqtSlot(str, object) @@ -300,7 +302,7 @@ class ParameterSingleWidget(QtWidgets.QWidget): self.label.setText(convert(name)) self.label.setToolTip('IIf this is bold then this parameter is only for this data. otherwise the general parameter is used and displayed') - self.value_line.setValidator(QtGui.QDoubleValidator()) + # self.value_line.setValidator(QtGui.QDoubleValidator()) self.value_line.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0) self.reset_button.clicked.connect(lambda x: self.removeSingleValue.emit()) @@ -334,7 +336,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): @value.setter def value(self, val): - self.value_line.setText(f'{float(val):.5g}') + # self.value_line.setText(f'{float(val):.5g}') + self.value_line.setText(f'{val}') def show_as_local_parameter(self, is_local): if is_local: diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index b9dec07..355769d 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -223,6 +223,8 @@ class FitRoutine(object): def run(self, mode: str = 'lsq'): self._abort = False + print('run') + fit_groups, linked_parameter = self.prepare_links() for data_groups in fit_groups: if len(data_groups) == 1 and not self.linked: From 5e55f067237aae1d30f4c7f91df6e81160e31b11 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 7 Sep 2023 19:30:10 +0200 Subject: [PATCH 13/23] add completer to general fit linedit --- src/gui_qt/fit/fit_forms.py | 23 ++++++++++++++++++----- src/gui_qt/fit/fit_parameter.py | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 983a0db..d49fac9 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -19,8 +19,6 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): self.parametername.setText(label + ' ') - # validator = QtGui.QDoubleValidator() - # validator.setDecimals(9) # self.parameter_line.setValidator(validator) self.parameter_line.setText('1') self.parameter_line.setMaximumWidth(60) @@ -38,15 +36,21 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): if fixed: self.fixed_check.hide() - self.menu = QtWidgets.QMenu(self) - self.add_links() - self.is_linked = None self.parameter_pos = None self.func_idx = None self._linetext = '1' + self.completer = QtWidgets.QCompleter() + self.completer.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion) + self.parameter_hint_model = QtGui.QStandardItemModel() + self.completer.setModel(self.parameter_hint_model) + self.parameter_line.setCompleter(self.completer) + + self.menu = QtWidgets.QMenu(self) + self.add_links() + @property def name(self): return convert(self.parametername.text().strip(), old='html', new='str') @@ -130,16 +134,25 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): parameter = {} self.menu.clear() + self.parameter_hint_model.clear() + ac = QtWidgets.QAction('Link to...', self) ac.triggered.connect(self.link_parameter) self.menu.addAction(ac) for model_key, model_funcs in parameter.items(): m = QtWidgets.QMenu('Model ' + model_key, self) + hint_key = model_key + for func_name, func_params in model_funcs.items(): m2 = QtWidgets.QMenu(func_name, m) + hint_key += f'.{func_name}' for p_name, idx in func_params: ac = QtWidgets.QAction(p_name, m2) + hint_key += f'.{p_name}' + item = QtGui.QStandardItem(hint_key) + item.setData((model_key, *idx)) + self.parameter_hint_model.appendRow(item) ac.setData((model_key, *idx)) ac.triggered.connect(self.link_parameter) m2.addAction(ac) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index b461d23..7c75f73 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -339,7 +339,7 @@ class ParameterSingleWidget(QtWidgets.QWidget): # self.value_line.setText(f'{float(val):.5g}') self.value_line.setText(f'{val}') - def show_as_local_parameter(self, is_local): + def show_as_local_parameter(self, is_local: bool): if is_local: self.label.setStyleSheet('font-weight: bold;') else: From e4dbaf2b91db88e9c053f86fccea901f730fdcb4 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 11 Sep 2023 18:09:08 +0200 Subject: [PATCH 14/23] work on ui --- src/gui_qt/_py/fitmodelwidget.py | 19 ++-- src/gui_qt/fit/fit_forms.py | 150 ++++++++-------------------- src/gui_qt/fit/fit_parameter.py | 14 ++- src/gui_qt/fit/fitfunction.py | 3 +- src/gui_qt/fit/fitwindow.py | 5 +- src/nmreval/fit/minimizer.py | 5 +- src/resources/_ui/fitmodelwidget.ui | 30 +----- 7 files changed, 62 insertions(+), 164 deletions(-) diff --git a/src/gui_qt/_py/fitmodelwidget.py b/src/gui_qt/_py/fitmodelwidget.py index a41f14b..f183f36 100644 --- a/src/gui_qt/_py/fitmodelwidget.py +++ b/src/gui_qt/_py/fitmodelwidget.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/fitmodelwidget.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/fitmodelwidget.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # -# WARNING! All changes made in this file will be lost! +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets @@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_FitParameter(object): def setupUi(self, FitParameter): FitParameter.setObjectName("FitParameter") - FitParameter.resize(365, 78) + FitParameter.resize(365, 66) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -36,7 +37,7 @@ class Ui_FitParameter(object): self.parametername.setObjectName("parametername") self.horizontalLayout_2.addWidget(self.parametername) self.parameter_line = LineEdit(FitParameter) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.parameter_line.sizePolicy().hasHeightForWidth()) @@ -44,20 +45,12 @@ class Ui_FitParameter(object): self.parameter_line.setText("") self.parameter_line.setObjectName("parameter_line") self.horizontalLayout_2.addWidget(self.parameter_line) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem) self.fixed_check = QtWidgets.QCheckBox(FitParameter) self.fixed_check.setObjectName("fixed_check") self.horizontalLayout_2.addWidget(self.fixed_check) self.global_checkbox = QtWidgets.QCheckBox(FitParameter) self.global_checkbox.setObjectName("global_checkbox") self.horizontalLayout_2.addWidget(self.global_checkbox) - self.toolButton = QtWidgets.QToolButton(FitParameter) - self.toolButton.setText("") - self.toolButton.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.toolButton.setArrowType(QtCore.Qt.RightArrow) - self.toolButton.setObjectName("toolButton") - self.horizontalLayout_2.addWidget(self.toolButton) self.verticalLayout.addLayout(self.horizontalLayout_2) self.frame = QtWidgets.QFrame(FitParameter) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index d49fac9..0a4179f 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -19,16 +19,16 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): self.parametername.setText(label + ' ') - # self.parameter_line.setValidator(validator) self.parameter_line.setText('1') - self.parameter_line.setMaximumWidth(60) - self.lineEdit.setMaximumWidth(60) - self.lineEdit_2.setMaximumWidth(60) + self.parameter_line.setMaximumWidth(160) + self.lineEdit.setMaximumWidth(100) + self.lineEdit_2.setMaximumWidth(100) self.label_3.setText(f'< {label} <') self.checkBox.stateChanged.connect(self.enableBounds) self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) + self.parameter_line.editingFinished.connect(self.update_parameter) self.parameter_line.values_requested.connect(lambda: self.value_requested.emit(self)) self.parameter_line.editingFinished.connect(lambda: self.value_changed.emit(self.parameter_line.text())) self.fixed_check.toggled.connect(self.set_fixed) @@ -36,20 +36,12 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): if fixed: self.fixed_check.hide() - self.is_linked = None self.parameter_pos = None self.func_idx = None self._linetext = '1' - self.completer = QtWidgets.QCompleter() - self.completer.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion) - self.parameter_hint_model = QtGui.QStandardItemModel() - self.completer.setModel(self.parameter_hint_model) - self.parameter_line.setCompleter(self.completer) - self.menu = QtWidgets.QMenu(self) - self.add_links() @property def name(self): @@ -73,39 +65,24 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None, fixed: bool = None, glob: bool = None): - if p is None: - # bad hack: linked parameter return (None, linked parameter) - # if p is None -> parameter is linked to argument given by bds - self.link_parameter(linkto=bds) - else: - ptext = f'{p:.4g}' + ptext = f'{p:.4g}' - self.set_parameter_string(ptext) + self.set_parameter_string(ptext) - if bds is not None: - self.set_bounds(*bds) + if bds is not None: + self.set_bounds(*bds) - if fixed is not None: - self.fixed_check.setCheckState(QtCore.Qt.Unchecked if fixed else QtCore.Qt.Checked) + if fixed is not None: + self.fixed_check.setCheckState(QtCore.Qt.Unchecked if fixed else QtCore.Qt.Checked) - if glob is not None: - self.global_checkbox.setCheckState(QtCore.Qt.Checked if glob else QtCore.Qt.Unchecked) + if glob is not None: + self.global_checkbox.setCheckState(QtCore.Qt.Checked if glob else QtCore.Qt.Unchecked) def get_parameter(self): - if self.is_linked: - try: - p = float(self._linetext) - except ValueError: - p = 1.0 - else: - try: - p = float(self.parameter_line.text().replace(',', '.')) - except ValueError: - p = self.parameter_line.text().replace(',', '.') - # _ = QtWidgets.QMessageBox().warning(self, 'Invalid value', - # f'{self.parametername.text()} contains invalid values', - # QtWidgets.QMessageBox.Cancel) - # return None + try: + p = float(self.parameter_line.text().replace(',', '.')) + except ValueError: + p = self.parameter_line.text().replace(',', '.') if self.checkBox.isChecked(): try: @@ -122,84 +99,26 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): bounds = (lb, rb) - return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked(), self.is_linked + return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked() @QtCore.pyqtSlot(bool) def set_fixed(self, state: bool): # self.global_checkbox.setVisible(not state) self.frame.setVisible(not state) - def add_links(self, parameter: dict = None): - if parameter is None: - parameter = {} - self.menu.clear() - - self.parameter_hint_model.clear() - - ac = QtWidgets.QAction('Link to...', self) - ac.triggered.connect(self.link_parameter) - self.menu.addAction(ac) - - for model_key, model_funcs in parameter.items(): - m = QtWidgets.QMenu('Model ' + model_key, self) - hint_key = model_key - - for func_name, func_params in model_funcs.items(): - m2 = QtWidgets.QMenu(func_name, m) - hint_key += f'.{func_name}' - for p_name, idx in func_params: - ac = QtWidgets.QAction(p_name, m2) - hint_key += f'.{p_name}' - item = QtGui.QStandardItem(hint_key) - item.setData((model_key, *idx)) - self.parameter_hint_model.appendRow(item) - ac.setData((model_key, *idx)) - ac.triggered.connect(self.link_parameter) - m2.addAction(ac) - m.addMenu(m2) - self.menu.addMenu(m) - - self.toolButton.setMenu(self.menu) - @QtCore.pyqtSlot() - def link_parameter(self, linkto=None): - if linkto is None: - action = self.sender() - else: - action = False - for m in self.menu.actions(): - if m.menu(): - for a in m.menu().actions(): - if a.data() == linkto: - action = a - break - if action: - break - - if (self.func_idx, self.parameter_pos) == action.data(): - return + def update_parameter(self): + new_value = self.parameter_line.text() + if not new_value: + self.parameter_line.setText('1') try: - new_text = f'Linked to {action.parentWidget().title()}.{action.text()}' - self._linetext = self.parameter_line.text() - self.parameter_line.setText(new_text) - self.parameter_line.setEnabled(False) - self.global_checkbox.hide() - self.global_checkbox.blockSignals(True) - self.global_checkbox.setCheckState(QtCore.Qt.Checked) - self.global_checkbox.blockSignals(False) - self.frame.hide() - self.is_linked = action.data() + float(new_value) + is_text = False + except ValueError: + is_text = True - except AttributeError: - self.parameter_line.setText(self._linetext) - self.parameter_line.setEnabled(True) - if self.fixed_check.isEnabled(): - self.global_checkbox.show() - self.frame.show() - self.is_linked = None - - self.state_changed.emit() + self.set_fixed(is_text) class QSaveModelDialog(QtWidgets.QDialog, Ui_SaveDialog): @@ -294,8 +213,17 @@ class FitModelTree(QtWidgets.QTreeWidget): idx = item.data(0, self.counterRole) self.itemRemoved.emit(idx) - def add_function(self, idx: int, cnt: int, op: int, name: str, color: QtGui.QColor | str | tuple, - parent: QtWidgets.QTreeWidgetItem = None, children: list = None, active: bool = True, **kwargs): + def add_function(self, + idx: int, + cnt: int, + op: int, + name: str, + color: QtGui.QColor | str | tuple, + parent: QtWidgets.QTreeWidgetItem = None, + children: list = None, + active: bool = True, + param_names: list[str] = None, + **kwargs): """ Add function to tree and dictionary of functions. """ @@ -310,6 +238,10 @@ class FitModelTree(QtWidgets.QTreeWidget): it.setData(0, self.counterRole, cnt) it.setData(0, self.operatorRole, op) it.setText(0, name) + if param_names is not None: + it.setToolTip(0, + 'Parameter names:\n' + + '\n'.join(f'{pn}({cnt})' for pn in param_names)) it.setForeground(0, QtGui.QBrush(color)) it.setIcon(0, get_icon(self.icons[op])) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 7c75f73..ae41510 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -114,10 +114,10 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.scrollwidget.layout().addStretch(1) self.scrollwidget2.layout().addStretch(1) - def set_links(self, parameter): - for w in self.global_parameter: - if isinstance(w, FitModelWidget): - w.add_links(parameter) + # def set_links(self, parameter): + # for w in self.global_parameter: + # if isinstance(w, FitModelWidget): + # w.add_links(parameter) @QtCore.pyqtSlot(str) def change_global_parameter(self, value: str, idx: int = None): @@ -203,17 +203,15 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): is_global = [] is_fixed = [] globs = [] - is_linked = [] for g in self.global_parameter: if isinstance(g, FitModelWidget): - p_i, bds_i, fixed_i, global_i, link_i = g.get_parameter() + p_i, bds_i, fixed_i, global_i = g.get_parameter() globs.append(p_i) bds.append(bds_i) is_fixed.append(fixed_i) is_global.append(global_i) - is_linked.append(link_i) lb, ub = list(zip(*bds)) @@ -267,7 +265,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): data_parameter[sid] = (p, kw_p) - return data_parameter, lb, ub, is_fixed, global_p, is_linked + return data_parameter, lb, ub, is_fixed, global_p def set_parameter(self, set_id: str | None, parameter: list[float]) -> int: num_parameter = list(filter(lambda g: not isinstance(g, SelectionWidget), self.global_parameter)) diff --git a/src/gui_qt/fit/fitfunction.py b/src/gui_qt/fit/fitfunction.py index 0d1c412..2b74641 100644 --- a/src/gui_qt/fit/fitfunction.py +++ b/src/gui_qt/fit/fitfunction.py @@ -128,7 +128,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): self.newFunction.emit(idx, cnt) - self.add_function(idx, cnt, op, name, col) + self.add_function(idx, cnt, op, name, col, param_names=self.functions[idx].params) def add_function(self, idx: int, cnt: int, op: int, name: str, color: str | tuple[float, float, float] | BaseColor, **kwargs): @@ -141,6 +141,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): qcolor = QtGui.QColor.fromRgbF(*color) else: qcolor = QtGui.QColor(color) + self.functree.add_function(idx, cnt, op, name, qcolor, **kwargs) f = self.functions[idx] diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index 55116b7..d0b88be 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -116,7 +116,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): # collect parameter names etc. to allow linkage self._func_list[self._current_model] = self.functionwidget.get_parameter_list() - dialog.set_links(self._func_list) + # dialog.set_links(self._func_list) # show same tab (general parameter/Data parameter) tab_idx = 0 @@ -229,7 +229,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): continue try: - p, lb, ub, var, glob, links = self.param_widgets[f['cnt']].get_parameter(function_use) + p, lb, ub, var, glob = self.param_widgets[f['cnt']].get_parameter(function_use) except ValueError as e: _ = QtWidgets.QMessageBox().warning(self, 'Invalid value', str(e), QtWidgets.QMessageBox.Ok) @@ -240,7 +240,6 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): parameter['lb'] += lb parameter['ub'] += ub parameter['var'] += var - parameter['links'] += links parameter['color'] += [f['color']] cnt = f['cnt'] diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 355769d..bc07df9 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -223,8 +223,6 @@ class FitRoutine(object): def run(self, mode: str = 'lsq'): self._abort = False - print('run') - fit_groups, linked_parameter = self.prepare_links() for data_groups in fit_groups: if len(data_groups) == 1 and not self.linked: @@ -256,6 +254,9 @@ class FitRoutine(object): self.unprep_run() + for r in self.result: + r.pprint() + return self.result def _prep_data(self, data): diff --git a/src/resources/_ui/fitmodelwidget.ui b/src/resources/_ui/fitmodelwidget.ui index 02069fb..ffc0b93 100755 --- a/src/resources/_ui/fitmodelwidget.ui +++ b/src/resources/_ui/fitmodelwidget.ui @@ -7,7 +7,7 @@ 0 0 365 - 78 + 66 @@ -62,7 +62,7 @@ - + 0 0 @@ -78,19 +78,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - @@ -105,19 +92,6 @@ - - - - - - - QToolButton::InstantPopup - - - Qt::RightArrow - - - From 3af5cb0301277da95bb0fe3cc4a5d4806e61b552 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 16 Sep 2023 14:16:45 +0200 Subject: [PATCH 15/23] add todos --- src/nmreval/fit/minimizer.py | 15 +++++++++++++-- src/nmreval/fit/parameter.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 50fa4e4..5594fd1 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -29,6 +29,7 @@ def _cost_scipy_glob(p: list[float], data: list[Data], varpars: list[str], used_ for keys, values in zip(varpars, p): for data_i in data: if keys in data_i.parameter.keys(): + # TODO move this to scaled_value setter data_i.parameter[keys].scaled_value = values data_i.parameter[keys].namespace[keys] = data_i.parameter[keys].value r = [] @@ -220,7 +221,7 @@ class FitRoutine(object): logger.info('Fit aborted by user') self._abort = True - def run(self, mode: str=None): + def run(self, mode: str = None): self._abort = False if mode is None: @@ -262,6 +263,16 @@ class FitRoutine(object): return self.result + def make_preview(self, x: np.ndarray) -> list[np.ndarray]: + y_pred = [] + fit_groups, linked_parameter = self.prepare_links() + for data_groups in fit_groups: + data = data_groups[0] + actual_parameters = [p.value for p in data.parameter.values()] + y_pred.append(data.func(actual_parameters, x)) + + return y_pred + def _prep_data(self, data): if data.get_model() is None: data._model = self.fit_model @@ -317,6 +328,7 @@ class FitRoutine(object): d._model = None self._no_own_model = [] + Parameters.reset() def _least_squares_single(self, data, p0, lb, ub, var): self.step = 0 @@ -345,7 +357,6 @@ class FitRoutine(object): with np.errstate(all='ignore'): 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) for v, var_pars_k in zip(data, data_pars): self.make_results(v, res.x, var, var_pars_k, res.jac.shape, diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index e9bf2c1..6d5457e 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -88,6 +88,15 @@ class Parameters(dict): expression = re.sub(re.escape(n), k, expression) p._expr = expression + def fix(self): + for v in self.keys(): + v._value = v.value + v.namespace = {} + + @staticmethod + def reset(): + Parameters.namespace = {} + def get_key(self, name: str) -> str | None: for k, v in self.items(): if name == v.name: @@ -104,6 +113,7 @@ class Parameter: Container for one parameter """ + # TODO Parameter should know its own key def __init__(self, name: str, value: float | str, var: bool = True, lb: float = -np.inf, ub: float = np.inf): self._value: float | None = None self.var: bool = bool(var) if var is not None else True @@ -181,6 +191,7 @@ class Parameter: @property def value(self) -> float: + # TODO first _value, then _expr if self._expr is not None and self.eval_allowed: return eval(self._expr, {}, self.namespace) elif self._value is not None: From 03d172bade234d2d28d1017bd62ba93dc84ae7f6 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 18 Sep 2023 11:43:28 +0200 Subject: [PATCH 16/23] use Parameter when collecting fit values --- src/gui_qt/fit/fit_forms.py | 10 ++--- src/gui_qt/fit/fit_parameter.py | 67 ++++++++++++++++----------------- src/gui_qt/fit/fitwindow.py | 22 ++++++----- src/nmreval/fit/parameter.py | 15 +++++--- 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 2d5f85c..13e225b 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -19,14 +19,16 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): super().__init__(parent) self.setupUi(self) - self.parametername.setText(label + ' ') + self.name = label + + self.parametername.setText(convert(label) + ' ') self.parameter_line.setText('1') self.parameter_line.setMaximumWidth(160) self.lineEdit.setMaximumWidth(100) self.lineEdit_2.setMaximumWidth(100) - self.label_3.setText(f'< {label} <') + self.label_3.setText(f'< {convert(label)} <') self.checkBox.stateChanged.connect(self.enableBounds) self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) @@ -46,10 +48,6 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): self.menu = QtWidgets.QMenu(self) - @property - def name(self): - return convert(self.parametername.text().strip(), old='html', new='str') - def set_parameter_string(self, p: str): self.parameter_line.setText(p) self.parameter_line.setToolTip(p) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index f4894f0..47b6ae0 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -1,5 +1,8 @@ from __future__ import annotations +from typing import Optional + +from nmreval.fit.parameter import Parameter from nmreval.utils.text import convert from ..Qt import QtWidgets, QtCore, QtGui @@ -62,8 +65,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.glob_values = [1] * len(func.params) for k, v in enumerate(func.params): - name = convert(v) - widgt = FitModelWidget(label=name, parent=self.scrollwidget) + widgt = FitModelWidget(label=v, parent=self.scrollwidget) widgt.parameter_pos = k widgt.func_idx = idx try: @@ -83,7 +85,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.global_parameter.append(widgt) self.scrollwidget.layout().addWidget(widgt) - widgt2 = ParameterSingleWidget(name=name, parent=self.scrollwidget2) + widgt2 = ParameterSingleWidget(name=v, parent=self.scrollwidget2) widgt2.valueChanged.connect(self.change_single_parameter) widgt2.removeSingleValue.connect(self.change_single_parameter) widgt2.installEventFilter(self) @@ -206,62 +208,51 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): if sid not in self.data_values: self.data_values[sid] = [None] * len(self.data_parameter) - def get_parameter(self, use_func=None): + def get_parameter(self, use_func=None) -> tuple[dict[str, list[Parameter]], list[Optional[Parameter]]]: bds = [] is_global = [] is_fixed = [] - globs = [] + + param_general = [] for g in self.global_parameter: if isinstance(g, FitModelWidget): p_i, bds_i, fixed_i, global_i = g.get_parameter() + parameter_i = Parameter(name=g.name, value=p_i, lb=bds_i[0], ub=bds_i[1], var=fixed_i) + param_general.append(parameter_i) - globs.append(p_i) bds.append(bds_i) is_fixed.append(fixed_i) is_global.append(global_i) - lb, ub = list(zip(*bds)) - data_parameter = {} if use_func is None: use_func = list(self.data_values.keys()) - global_p = None for sid, parameter in self.data_values.items(): if sid not in use_func: continue kw_p = {} p = [] - if global_p is None: - global_p = {'value': [], 'idx': [], 'var': [], 'ub': [], 'lb': []} for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)): if isinstance(g, FitModelWidget): - if (p_i is None) or is_global[i]: - p.append(globs[i]) - if is_global[i]: - if i not in global_p['idx']: - global_p['value'].append(globs[i]) - global_p['idx'].append(i) - global_p['var'].append(is_fixed[i]) - global_p['ub'].append(ub[i]) - global_p['lb'].append(lb[i]) + if p_i is None: + # set has no oen value + p.append(param_general[i].copy()) else: - p.append(p_i) + lb, ub = bds[i] + try: + if not (lb < p[i] < ub): + raise ValueError(f'Parameter {g.name} is outside bounds ({lb}, {ub})') + except TypeError: + pass - try: - if p[i] > ub[i]: - raise ValueError(f'Parameter {g.name} is outside bounds ({lb[i]}, {ub[i]})') - except TypeError: - pass - - try: - if p[i] < lb[i]: - raise ValueError(f'Parameter {g.name} is outside bounds ({lb[i]}, {ub[i]})') - except TypeError: - pass + # create Parameter + p.append( + Parameter(name=g.name, value=p_i, lb=lb, ub=ub, var=is_fixed[i]) + ) else: if p_i is None: @@ -271,9 +262,17 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): else: kw_p[g.argname] = p_i + global_parameter = [] + for param, global_flag in zip(param_general, is_global): + if global_flag: + global_parameter.append(param) + else: + global_parameter.append(None) + + data_parameter[sid] = (p, kw_p) - return data_parameter, lb, ub, is_fixed, global_p + return data_parameter, global_parameter def set_parameter(self, set_id: str | None, parameter: list[float]) -> int: num_parameter = list(filter(lambda g: not isinstance(g, SelectionWidget), self.global_parameter)) @@ -304,7 +303,7 @@ class ParameterSingleWidget(QtWidgets.QWidget): self._init_ui() - self._name = name + self.name = name self.label.setText(convert(name)) self.label.setToolTip('If this is bold then this parameter is only for this data. ' 'Otherwise, the general parameter is used and displayed') diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index d0b88be..e3b708f 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -220,27 +220,31 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): def _prepare(self, model: list, function_use: list = None, parameter: dict = None, add_idx: bool = False, cnt: int = 0) -> tuple[dict, int]: if parameter is None: - parameter = {'parameter': {}, 'lb': (), 'ub': (), 'var': [], - 'glob': {'idx': [], 'value': [], 'var': [], 'lb': [], 'ub': []}, - 'links': [], 'color': []} + parameter = { + 'parameter': {}, + 'glob': [], + 'links': [], + 'color': [], + } for i, f in enumerate(model): + print(i, f) if not f['active']: continue try: - p, lb, ub, var, glob = self.param_widgets[f['cnt']].get_parameter(function_use) + p, glob = self.param_widgets[f['cnt']].get_parameter(function_use) except ValueError as e: _ = QtWidgets.QMessageBox().warning(self, 'Invalid value', str(e), QtWidgets.QMessageBox.Ok) return {}, -1 - p_len = len(parameter['lb']) + print(p) + print(glob) + p_len = len(p) + parameter['color'].append(f['color']) - parameter['lb'] += lb - parameter['ub'] += ub - parameter['var'] += var - parameter['color'] += [f['color']] + print(parameter) cnt = f['cnt'] diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 6d5457e..bc79daa 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -190,20 +190,23 @@ class Parameter: self._value = value * self.scale @property - def value(self) -> float: + def value(self) -> float | None: # TODO first _value, then _expr + if self._value is not None: + return self._value + if self._expr is not None and self.eval_allowed: return eval(self._expr, {}, self.namespace) - elif self._value is not None: - return self._value + + return @property def scaled_error(self) -> None | float: - if self.error is None: - return self.error - else: + if self.error is not None: return self.error / self.scale + return + @scaled_error.setter def scaled_error(self, value) -> None: self.error = value * self.scale From bd1a227e4c517f053ece775939c6c4f159788918 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 18 Sep 2023 13:52:10 +0200 Subject: [PATCH 17/23] use Parameter when collecting fit values --- src/gui_qt/fit/fit_parameter.py | 19 +++++----- src/gui_qt/fit/fitwindow.py | 66 ++++++++++++++++----------------- src/gui_qt/main/management.py | 20 ++++------ src/nmreval/fit/data.py | 43 +++++++++++++-------- src/nmreval/fit/model.py | 30 +++++++++------ 5 files changed, 94 insertions(+), 84 deletions(-) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 47b6ae0..7426c1b 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -208,11 +208,10 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): if sid not in self.data_values: self.data_values[sid] = [None] * len(self.data_parameter) - def get_parameter(self, use_func=None) -> tuple[dict[str, list[Parameter]], list[Optional[Parameter]]]: + def get_parameter(self, use_func=None) -> tuple[dict, list]: bds = [] is_global = [] is_fixed = [] - param_general = [] for g in self.global_parameter: @@ -262,16 +261,16 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): else: kw_p[g.argname] = p_i - global_parameter = [] - for param, global_flag in zip(param_general, is_global): - if global_flag: - global_parameter.append(param) - else: - global_parameter.append(None) - - data_parameter[sid] = (p, kw_p) + global_parameter = [] + for param, global_flag in zip(param_general, is_global): + if global_flag: + param.is_global = True + global_parameter.append(param) + else: + global_parameter.append(None) + return data_parameter, global_parameter def set_parameter(self, set_id: str | None, parameter: list[float]) -> int: diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index e3b708f..3f9ce48 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -9,6 +9,7 @@ import numpy as np from pyqtgraph import mkPen from nmreval.fit._meta import MultiModel, ModelFactory +from nmreval.fit.model import Model from nmreval.fit.result import FitResult from .fit_forms import FitTableWidget @@ -219,16 +220,16 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): def _prepare(self, model: list, function_use: list = None, parameter: dict = None, add_idx: bool = False, cnt: int = 0) -> tuple[dict, int]: + if parameter is None: parameter = { - 'parameter': {}, - 'glob': [], + 'data_parameter': {}, + 'global_parameter': [], 'links': [], 'color': [], } for i, f in enumerate(model): - print(i, f) if not f['active']: continue @@ -239,33 +240,22 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): QtWidgets.QMessageBox.Ok) return {}, -1 - print(p) - print(glob) - p_len = len(p) parameter['color'].append(f['color']) - - print(parameter) + parameter['global_parameter'].extend(glob) cnt = f['cnt'] - for p_k, v_k in p.items(): if add_idx: kw_k = {f'{k}_{cnt}': v for k, v in v_k[1].items()} else: kw_k = v_k[1] - if p_k in parameter['parameter']: - params, kw = parameter['parameter'][p_k] + if p_k in parameter['data_parameter']: + params, kw = parameter['data_parameter'][p_k] params += v_k[0] kw.update(kw_k) else: - parameter['parameter'][p_k] = (v_k[0], kw_k) - - for g_k, g_v in glob.items(): - if g_k != 'idx': - parameter['glob'][g_k] += g_v - else: - parameter['glob']['idx'] += [idx_i + p_len for idx_i in g_v] + parameter['data_parameter'][p_k] = (v_k[0], kw_k) if add_idx: cnt += 1 @@ -283,37 +273,43 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): data = self.data_table.collect_data(default=self.default_combobox.currentData()) func_dict = {} - for k, mod in self.models.items(): - func, order, param_len = ModelFactory.create_from_list(mod) + for model_name, model_parameter in self.models.items(): + func, order, param_len = ModelFactory.create_from_list(model_parameter) if func is None: continue - if k in data: - parameter, _ = self._prepare(mod, function_use=data[k], add_idx=isinstance(func, MultiModel)) + func = Model(func) - # convert positions of global parameter to corresponding names - global_parameter: dict = parameter['glob'] - positions = global_parameter.pop('idx') - global_parameter['key'] = [pname for i, pname in enumerate(func.params) if i in positions] - # print(global_parameter) + if model_name in data: + parameter, _ = self._prepare(model_parameter, function_use=data[model_name], add_idx=isinstance(func, MultiModel)) if parameter is None: return + for (data_parameter, _) in parameter['data_parameter'].values(): + for pname, param in zip(func.params, data_parameter): + param.name = pname + + if self._complex[model_name] is not None: + for p_k, p_v in parameter['data_parameter'].items(): + p_v[1].update({'complex_mode': self._complex[model_name]}) + parameter['data_parameter'][p_k] = p_v[0], p_v[1] + + for pname, param_value in zip(func.params, parameter['global_parameter']): + if param_value is not None: + param_value.name = pname + func.set_global_parameter(param_value) + parameter['func'] = func parameter['order'] = order parameter['len'] = param_len - parameter['complex'] = self._complex[k] - if self._complex[k] is not None: - for p_k, p_v in parameter['parameter'].items(): - p_v[1].update({'complex_mode': self._complex[k]}) - parameter['parameter'][p_k] = p_v[0], p_v[1] + parameter['complex'] = self._complex[model_name] - func_dict[k] = parameter + func_dict[model_name] = parameter replaceable = [] - for k, v in func_dict.items(): + for model_name, v in func_dict.items(): for i, link_i in enumerate(v['links']): if link_i is None: continue @@ -344,7 +340,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): QtWidgets.QMessageBox.Ok) return - replaceable.append((k, i, rep_model, repl_idx)) + replaceable.append((model_name, i, rep_model, repl_idx)) replace_value = None for p_k in f['parameter'].values(): diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index e4a0dc4..8d4c4ee 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -441,21 +441,22 @@ class UpperManagement(QtCore.QObject): # all-encompassing error catch try: for model_id, model_p in parameter.items(): - m = Model(model_p['func']) + m = model_p['func'] models[model_id] = m m_complex = model_p['complex'] + print(model_p) # sets are not in active order but in order they first appeared in fit dialog # iterate over order of set id in active order and access parameter inside loop # instead of directly looping - list_ids = list(model_p['parameter'].keys()) + list_ids = list(model_p['data_parameter'].keys()) set_order = [self.active_id.index(i) for i in list_ids] for pos in set_order: set_id = list_ids[pos] data_i = self.data[set_id] - set_params = model_p['parameter'][set_id] + set_params = model_p['data_parameter'][set_id] if we_option.lower() == 'deltay': we = data_i.y_err**2 @@ -485,18 +486,13 @@ class UpperManagement(QtCore.QObject): d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id) d.set_model(m) - d.set_parameter(set_params[0], var=model_p['var'], - lb=model_p['lb'], ub=model_p['ub'], - fun_kwargs=set_params[1]) + d.set_parameter(set_params[0], fun_kwargs=set_params[1]) + # d.set_parameter(set_params[0], var=model_p['var'], + # lb=model_p['lb'], ub=model_p['ub'], + # fun_kwargs=set_params[1]) self.fitter.add_data(d) - model_globs = model_p['glob'] - if model_globs: - for parameter_args in zip(*model_globs.values()): - m.set_global_parameter(**{k: v for k, v in zip(model_globs.keys(), parameter_args)}) - # m.set_global_parameter(**model_p['glob']) - for links_i in links: self.fitter.set_link_parameter((models[links_i[0]], links_i[1]), (models[links_i[2]], links_i[3])) diff --git a/src/nmreval/fit/data.py b/src/nmreval/fit/data.py index 0d7ed86..4a34409 100644 --- a/src/nmreval/fit/data.py +++ b/src/nmreval/fit/data.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import numpy as np from .model import Model @@ -69,7 +71,7 @@ class Data(object): return self.model def set_parameter(self, - values: list[float], + values: list[float | Parameter], *, var: list[bool] = None, ub: list[float] = None, @@ -103,24 +105,33 @@ class Data(object): if len(values) != len(model.params): raise ValueError('Number of given parameter does not match number of model parameters') - if var is None: - var = [True] * len(values) + is_parameter = [isinstance(v, Parameter) for v in values] + if all(is_parameter): + for p_i in values: + key = f"p{next(Parameters.parameter_counter)}" + self.parameter.add_parameter(key, p_i) + elif any(is_parameter): + raise ValueError('list of parameter are not all float of Parameter') - if lb is None: - if default_bounds: - lb = model.lb - else: - lb = [None] * len(values) + else: + if var is None: + var = [True] * len(values) - if ub is None: - if default_bounds: - ub = model.ub - else: - ub = [None] * len(values) + if lb is None: + if default_bounds: + lb = model.lb + else: + lb = [None] * len(values) - arg_names = ['name', 'value', 'var', 'lb', 'ub'] - for parameter_arg in zip(model.params, values, var, lb, ub): - self.parameter.add(**{arg_name: arg_value for arg_name, arg_value in zip(arg_names, parameter_arg)}) + if ub is None: + if default_bounds: + ub = model.ub + else: + ub = [None] * len(values) + + arg_names = ['name', 'value', 'var', 'lb', 'ub'] + for parameter_arg in zip(model.params, values, var, lb, ub): + self.parameter.add(**{arg_name: arg_value for arg_name, arg_value in zip(arg_names, parameter_arg)}) self.para_keys = list(self.parameter.keys()) diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index faca53d..c80a2a3 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -80,22 +80,30 @@ class Model(object): if v.default is not inspect.Parameter.empty} def set_global_parameter(self, - key: str, - value: float | str, + key: str | Parameter, + value: float | str = None, + *, var: bool = None, lb: float = None, ub: float = None, - default_bounds: bool = False + default_bounds: bool = False, ) -> Parameter: - idx = [self.params.index(key)] - if default_bounds: - if lb is None: - lb = [self.lb[i] for i in idx] - if ub is None: - ub = [self.lb[i] for i in idx] - p = self.parameter.add(key, value, var=var, lb=lb, ub=ub) - p.is_global = True + if isinstance(key, Parameter): + p = key + key = f'p{next(Parameters.parameter_counter)}' + self.parameter.add_parameter(key, p) + + else: + idx = [self.params.index(key)] + if default_bounds: + if lb is None: + lb = [self.lb[i] for i in idx] + if ub is None: + ub = [self.lb[i] for i in idx] + + p = self.parameter.add(key, value, var=var, lb=lb, ub=ub) + p.is_global = True return p From 1d22f22901396851266594e27570a73b38dcdfc1 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 18 Sep 2023 15:30:06 +0200 Subject: [PATCH 18/23] Parameter in preview --- src/gui_qt/fit/fit_parameter.py | 2 +- src/gui_qt/fit/fitwindow.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 7426c1b..49568cf 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -237,7 +237,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)): if isinstance(g, FitModelWidget): - if p_i is None: + if (p_i is None) and (not is_global[i]): # set has no oen value p.append(param_general[i].copy()) else: diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index 3f9ce48..b93ebec 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -9,6 +9,7 @@ import numpy as np from pyqtgraph import mkPen from nmreval.fit._meta import MultiModel, ModelFactory +from nmreval.fit.data import Data from nmreval.fit.model import Model from nmreval.fit.result import FitResult @@ -419,20 +420,14 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self.preview_lines = [] for k, model in models_parameters.items(): - f = model['func'] + f = Model(model['func']) is_complex = self._complex[k] - parameters = model['parameter'] + parameters = model['data_parameter'] color = model['color'] - seen_parameter = [] - for p, kwargs in parameters.values(): - if (p, kwargs) in seen_parameter: - # plot only previews with different parameter - continue - - seen_parameter.append((p, kwargs)) + print(pp.value for pp in p) if is_complex is not None: y = f.func(x, *p, complex_mode=is_complex, **kwargs) From 8d994bb9b478a606c70d71fb4ecdd10e4bad5d91 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 18 Sep 2023 17:39:31 +0200 Subject: [PATCH 19/23] fixed preview --- src/gui_qt/fit/fitwindow.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index b93ebec..9888685 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -11,6 +11,7 @@ from pyqtgraph import mkPen from nmreval.fit._meta import MultiModel, ModelFactory from nmreval.fit.data import Data from nmreval.fit.model import Model +from nmreval.fit.parameter import Parameters from nmreval.fit.result import FitResult from .fit_forms import FitTableWidget @@ -419,25 +420,37 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): def make_previews(self, x, models_parameters: dict): self.preview_lines = [] + # needed to create namespace + param_dict = Parameters() + + cnt = 0 + for model in models_parameters.values(): + f = model['func'] + for parameter_list in model['data_parameter'].values(): + for i, p_value in enumerate(parameter_list[0]): + p_value.name = f.params[i] + param_dict.add_parameter(f'a{cnt}', p_value) + cnt += 1 + for k, model in models_parameters.items(): - f = Model(model['func']) + f = model['func'] is_complex = self._complex[k] parameters = model['data_parameter'] color = model['color'] for p, kwargs in parameters.values(): - print(pp.value for pp in p) + p_value = [pp.value for pp in p] if is_complex is not None: - y = f.func(x, *p, complex_mode=is_complex, **kwargs) + y = f.func(x, *p_value, complex_mode=is_complex, **kwargs) if np.iscomplexobj(y): self.preview_lines.append(PlotItem(x=x, y=y.real, pen=mkPen(width=3))) self.preview_lines.append(PlotItem(x=x, y=y.imag, pen=mkPen(width=3))) else: self.preview_lines.append(PlotItem(x=x, y=y, pen=mkPen(width=3))) else: - y = f.func(x, *p, **kwargs) + y = f.func(x, *p_value, **kwargs) self.preview_lines.append(PlotItem(x=x, y=y, pen=mkPen(width=3))) if isinstance(f, MultiModel): @@ -445,7 +458,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if is_complex is not None: sub_kwargs.update({'complex_mode': is_complex}) - for i, s in enumerate(f.subs(x, *p, **sub_kwargs)): + for i, s in enumerate(f.subs(x, *p_value, **sub_kwargs)): pen_i = mkPen(QtGui.QColor.fromRgbF(*color[i])) if np.iscomplexobj(s): self.preview_lines.append(PlotItem(x=x, y=s.real, pen=pen_i)) @@ -453,6 +466,8 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): else: self.preview_lines.append(PlotItem(x=x, y=s, pen=pen_i)) + param_dict.clear() + return self.preview_lines def set_parameter(self, parameter: dict[str, FitResult]): From 41d90bb15f3c20415d70e522a6dc580af8b366cb Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 19 Sep 2023 11:33:52 +0200 Subject: [PATCH 20/23] minor fixes --- src/gui_qt/fit/fit_forms.py | 1 + src/gui_qt/fit/fit_parameter.py | 4 ++-- src/nmreval/fit/parameter.py | 29 ++++++++++++----------------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 13e225b..67f27d2 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -118,6 +118,7 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): is_text = False except ValueError: is_text = True + self.global_checkbox.setCheckState(False) self.set_fixed(is_text) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 49568cf..e6b5a11 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -237,13 +237,13 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)): if isinstance(g, FitModelWidget): - if (p_i is None) and (not is_global[i]): + if (p_i is None) or is_global[i]: # set has no oen value p.append(param_general[i].copy()) else: lb, ub = bds[i] try: - if not (lb < p[i] < ub): + if not (lb < p_i < ub): raise ValueError(f'Parameter {g.name} is outside bounds ({lb}, {ub})') except TypeError: pass diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index bc79daa..502f35f 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -57,14 +57,7 @@ class Parameters(dict): parameter.namespace = self.namespace parameter.eval_allowed = True - # look for variables in expression and replace with valid names - for p in self.values(): - if p._expr is not None: - expression = p._expr - for n, k in self._mapping.items(): - expression = re.sub(re.escape(n), k, expression) - - p._expr = expression + self.update_namespace() def replace_parameter(self, key_out: str, key_in: str, parameter: Parameter): # print('replace par', key_out, key_in, parameter) @@ -79,15 +72,6 @@ class Parameters(dict): if key_out in self.namespace: del self.namespace[key_out] - for p in self.values(): - try: - p.value - except NameError: - expression = p._expr_disp - for n, k in self._mapping.items(): - expression = re.sub(re.escape(n), k, expression) - p._expr = expression - def fix(self): for v in self.keys(): v._value = v.value @@ -107,6 +91,17 @@ class Parameters(dict): def get_state(self): return {k: v.get_state() for k, v in self.items()} + def update_namespace(self): + for p in self.values(): + try: + p.value + except NameError: + expression = p._expr_disp + for n, k in self._mapping.items(): + expression, num_replaced = re.subn(re.escape(n), k, expression) + + p._expr = expression + class Parameter: """ From 067857eda237fe8ed26afc12955c3542e80695e7 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 19 Sep 2023 12:22:33 +0200 Subject: [PATCH 21/23] minor fixes --- src/gui_qt/data/container.py | 5 +- .../data/signaledit/editsignalwidget.py | 3 +- src/gui_qt/fit/fit_parameter.py | 2 +- src/gui_qt/graphs/drawings.py | 2 - src/gui_qt/lib/mdiarea.py | 1 - src/gui_qt/lib/randpok.py | 110 ------------------ src/gui_qt/main/management.py | 1 - src/nmreval/fit/result.py | 26 +++-- 8 files changed, 21 insertions(+), 129 deletions(-) delete mode 100644 src/gui_qt/lib/randpok.py diff --git a/src/gui_qt/data/container.py b/src/gui_qt/data/container.py index 913ad7e..106eee3 100644 --- a/src/gui_qt/data/container.py +++ b/src/gui_qt/data/container.py @@ -8,6 +8,7 @@ from pyqtgraph import mkPen from nmreval.data.points import Points from nmreval.data.signals import Signal +from nmreval.lib.logger import logger from nmreval.utils.text import convert from nmreval.data.bds import BDS from nmreval.data.dsc import DSC @@ -356,7 +357,7 @@ class ExperimentContainer(QtCore.QObject): elif mode in ['imag', 'all'] and self.plot_imag is not None: self.plot_imag.set_symbol(symbol=symbol, size=size, color=color) else: - print('Updating symbol failed for ' + str(self.id)) + logger.warning(f'Updating symbol failed for {self.id}') def setLine(self, *, width=None, style=None, color=None, mode='real'): if mode in ['real', 'all']: @@ -368,7 +369,7 @@ class ExperimentContainer(QtCore.QObject): elif mode in ['imag', 'all'] and self.plot_imag is not None: self.plot_imag.set_line(width=width, style=style, color=color) else: - print('Updating line failed for ' + str(self.id)) + logger.warning(f'Updating line failed for {self.id}') def update_property(self, key1: str, key2: str, value: Any): keykey = key2.split() diff --git a/src/gui_qt/data/signaledit/editsignalwidget.py b/src/gui_qt/data/signaledit/editsignalwidget.py index 980ea19..31c4aff 100644 --- a/src/gui_qt/data/signaledit/editsignalwidget.py +++ b/src/gui_qt/data/signaledit/editsignalwidget.py @@ -1,3 +1,4 @@ +from nmreval.lib.logger import logger from nmreval.math import apodization from nmreval.lib.importer import find_models from nmreval.utils.text import convert @@ -67,7 +68,7 @@ class EditSignalWidget(QtWidgets.QWidget, Ui_Form): self.do_something.emit(sender, (ph0, ph1, pvt)) else: - print('You should never reach this by accident.') + logger.warning(f'You should never reach this by accident, invalid sender {sender!r}') @QtCore.pyqtSlot(int, name='on_apodcombobox_currentIndexChanged') def change_apodization(self, index): diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index e6b5a11..30b4b31 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -175,7 +175,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): # disable single parameter if it is set global, enable if global is unset widget = self.sender() idx = self.global_parameter.index(widget) - enable = (widget.global_checkbox.checkState() == QtCore.Qt.Unchecked) and (widget.is_linked is None) + enable = (widget.global_checkbox.checkState() == QtCore.Qt.Unchecked) self.data_parameter[idx].setEnabled(enable) def select_next_preview(self, direction): diff --git a/src/gui_qt/graphs/drawings.py b/src/gui_qt/graphs/drawings.py index cd62db4..e87af0e 100644 --- a/src/gui_qt/graphs/drawings.py +++ b/src/gui_qt/graphs/drawings.py @@ -138,9 +138,7 @@ class DrawingsWidget(QtWidgets.QWidget, Ui_Form): graph_id = self.graph_comboBox.currentData() current_lines = self.lines[graph_id] - print(remove_rows) for i in reversed(remove_rows): - print(i) self.tableWidget.removeRow(i) self.line_deleted.emit(current_lines[i], graph_id) diff --git a/src/gui_qt/lib/mdiarea.py b/src/gui_qt/lib/mdiarea.py index 37f6c27..3dd6bbf 100644 --- a/src/gui_qt/lib/mdiarea.py +++ b/src/gui_qt/lib/mdiarea.py @@ -27,7 +27,6 @@ class MdiAreaTile(QtWidgets.QMdiArea): pos = QtCore.QPoint(0, 0) for win in window_list: - print(win.minimumSize()) win.setGeometry(rect) win.move(pos) diff --git a/src/gui_qt/lib/randpok.py b/src/gui_qt/lib/randpok.py deleted file mode 100644 index 1cbfcf8..0000000 --- a/src/gui_qt/lib/randpok.py +++ /dev/null @@ -1,110 +0,0 @@ -import os.path -import json -import urllib.request -import webbrowser -import random - -from ..Qt import QtGui, QtCore, QtWidgets -from .._py.pokemon import Ui_Dialog - -random.seed() - - -class QPokemon(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, number=None, parent=None): - super().__init__(parent=parent) - self.setupUi(self) - self._js = json.load(open(os.path.join(path_to_module, 'utils', 'pokemon.json'), 'r'), encoding='UTF-8') - self._id = 0 - - if number is not None and number in range(1, len(self._js)+1): - poke_nr = f'{number:03d}' - self._id = number - else: - poke_nr = f'{random.randint(1, len(self._js)):03d}' - self._id = int(poke_nr) - - self._pokemon = None - self.show_pokemon(poke_nr) - self.label_15.linkActivated.connect(lambda x: webbrowser.open(x)) - - self.buttonBox.clicked.connect(self.randomize) - self.next_button.clicked.connect(self.show_next) - self.prev_button.clicked.connect(self.show_prev) - - def show_pokemon(self, nr): - self._pokemon = self._js[nr] - self.setWindowTitle('Pokémon: ' + self._pokemon['Deutsch']) - - for i in range(self.tabWidget.count(), -1, -1): - print('i', self.tabWidget.count(), i) - try: - self.tabWidget.widget(i).deleteLater() - except AttributeError: - pass - - for n, img in self._pokemon['Bilder']: - w = QtWidgets.QWidget() - vl = QtWidgets.QVBoxLayout() - l = QtWidgets.QLabel(self) - l.setAlignment(QtCore.Qt.AlignHCenter) - pixmap = QtGui.QPixmap() - - try: - pixmap.loadFromData(urllib.request.urlopen(img, timeout=0.5).read()) - except IOError: - l.setText(n) - else: - sc_pixmap = pixmap.scaled(256, 256, QtCore.Qt.KeepAspectRatio) - l.setPixmap(sc_pixmap) - - vl.addWidget(l) - w.setLayout(vl) - self.tabWidget.addTab(w, n) - - if len(self._pokemon['Bilder']) <= 1: - self.tabWidget.tabBar().setVisible(False) - else: - self.tabWidget.tabBar().setVisible(True) - self.tabWidget.adjustSize() - - self.name.clear() - keys = ['National-Dex', 'Kategorie', 'Typ', 'Größe', 'Gewicht', 'Farbe', 'Link'] - label_list = [self.pokedex_nr, self.category, self.poketype, self.weight, self.height, self.color, self.info] - for (k, label) in zip(keys, label_list): - v = self._pokemon[k] - if isinstance(v, list): - v = os.path.join('', *v) - - if k == 'Link': - v = '{}'.format(v, v) - - label.setText(v) - - for k in ['Deutsch', 'Japanisch', 'Englisch', 'Französisch']: - v = self._pokemon[k] - self.name.addItem(k + ': ' + v) - - self.adjustSize() - - def randomize(self, idd): - if idd.text() == 'Retry': - new_number = f'{random.randint(1, len(self._js)):03d}' - self._id = int(new_number) - self.show_pokemon(new_number) - else: - self.close() - - def show_next(self): - new_number = self._id + 1 - if new_number > len(self._js): - new_number = 1 - self._id = new_number - self.show_pokemon(f'{new_number:03d}') - - def show_prev(self): - new_number = self._id - 1 - if new_number == 0: - new_number = len(self._js) - self._id = new_number - self.show_pokemon(f'{new_number:03d}') diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 8d4c4ee..67e33cd 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -445,7 +445,6 @@ class UpperManagement(QtCore.QObject): models[model_id] = m m_complex = model_p['complex'] - print(model_p) # sets are not in active order but in order they first appeared in fit dialog # iterate over order of set id in active order and access parameter inside loop diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index 9cf6d5e..d825e14 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -2,6 +2,7 @@ from __future__ import annotations import re from collections import OrderedDict +from io import StringIO from pathlib import Path from typing import Any @@ -223,22 +224,25 @@ class FitResult(Points): 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()) + s = StringIO() + s.write('Fit result:\n') + s.write(f' model : {self.name}\n') + s.write(f' #data : {self.nobs}\n') + s.write(f' #var : {self.nvar}\n') + s.write('\nParameter\n') + s.write(self.parameter_string()) if statistics: - print('Statistics') + s.write('Statistics\n') for k, v in self.statistics.items(): - print(f' {k} : {v:.4f}') + s.write(f' {k} : {v:.4f}') if correlations and self.correlation is not None: - print('\nCorrelation (partial corr.)') - print(self._correlation_string()) - print() + s.write('\nCorrelation (partial corr.)\n') + s.write(self._correlation_string()) + s.write('\n') + + print(s.getvalue()) def parameter_string(self): ret_val = '' From 6ecc4a4126a17b0e4897f2e606783673e82a9e77 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 19 Sep 2023 12:34:44 +0200 Subject: [PATCH 22/23] make final fit parameter values --- src/nmreval/fit/parameter.py | 5 ++++- src/nmreval/fit/result.py | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 502f35f..3c24851 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -186,7 +186,6 @@ class Parameter: @property def value(self) -> float | None: - # TODO first _value, then _expr if self._value is not None: return self._value @@ -239,3 +238,7 @@ class Parameter: return para_copy + def fix(self): + self._value = self.value + self.namespace = {} + diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index d825e14..82f7531 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -187,7 +187,7 @@ class FitResult(Points): nice_name = m.group(1) if func_number in split_funcs: nice_func = split_funcs[func_number] - + pvalue.fix() pvalue.name = nice_name pvalue.function = nice_func parameter_dic[pname] = pvalue @@ -196,6 +196,8 @@ class FitResult(Points): if modelname[0] == '(' and modelname[-1] == ')': modelname = modelname[1:-1] + print(parameter_dic) + return parameter_dic, modelname @property @@ -233,9 +235,9 @@ class FitResult(Points): s.write(self.parameter_string()) if statistics: - s.write('Statistics\n') + s.write('\nStatistics\n') for k, v in self.statistics.items(): - s.write(f' {k} : {v:.4f}') + s.write(f' {k} : {v:.4f}\n') if correlations and self.correlation is not None: s.write('\nCorrelation (partial corr.)\n') From dedb13016375d75d041ba0e1b9f14e9c4a178950 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 19 Sep 2023 12:35:24 +0200 Subject: [PATCH 23/23] make final fit parameter values --- src/nmreval/fit/parameter.py | 3 --- src/nmreval/fit/result.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 3c24851..6db56a3 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -60,9 +60,6 @@ class Parameters(dict): self.update_namespace() def replace_parameter(self, key_out: str, key_in: str, parameter: Parameter): - # print('replace par', key_out, key_in, parameter) - # print('name', parameter.name) - self.add_parameter(key_in, parameter) for k, v in self._mapping.items(): if v == key_out: diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index 82f7531..b460ad8 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -196,8 +196,6 @@ class FitResult(Points): if modelname[0] == '(' and modelname[-1] == ')': modelname = modelname[1:-1] - print(parameter_dic) - return parameter_dic, modelname @property