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