From 207ee5bffde3baabdd271dfa77f099fb8f5e0484 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 27 Sep 2023 13:55:37 +0200 Subject: [PATCH 1/2] 1/p bounds added --- src/nmreval/fit/parameter.py | 118 ++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 8 deletions(-) diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 6db56a3..7ae525e 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ast import re from itertools import count from io import StringIO @@ -39,10 +40,11 @@ class Parameters(dict): value: str | float | int = None, *, var: bool = True, - lb: float = -np.inf, ub: float = np.inf) -> Parameter: + lb: str | float = -np.inf, + ub: str | float = np.inf) -> Parameter: par = Parameter(name=name, value=value, var=var, lb=lb, ub=ub) - key = f'p{next(Parameters.parameter_counter)}' + key = f'_p{next(Parameters.parameter_counter)}' self.add_parameter(key, par) @@ -99,6 +101,91 @@ class Parameters(dict): p._expr = expression + def prepare_bounds(self): + original_values = list(self.values()) + for param in original_values: + already_with_expression = False + for mode, value in (('lower', param.lb), ('upper', param.ub)): + if already_with_expression: + raise ValueError('Only one boundary can be an expression') + already_with_expression = self.parse(param, value, bnd=mode) + + def parse(self, parameter, expression: str, bnd: str = 'lower') -> bool: + try: + float(expression) + return False + + except ValueError: + pp = ast.parse(expression) + expr_syms = pp.body[0].value + + if isinstance(expr_syms, ast.BinOp): + left, op, right = expr_syms.left, expr_syms.op, expr_syms.right + + # check for sign in numerator + sign = 1 + if isinstance(left, ast.UnaryOp): + if isinstance(left.op, (ast.Not, ast.Invert)): + raise ValueError('Only `+` and `-` are supported for signs') + if isinstance(left.op, ast.USub): + sign = -1 + left = left.operand + + # is expression number / parameter? + if not (isinstance(left, ast.Constant) and isinstance(op, ast.Div) and isinstance(right, ast.Name)): + raise ValueError('Only simple division `const/parameter` are possible') + + result = self.make_proxy_div(parameter, left.value*sign, right.id, bnd=bnd) + + return result + + else: + raise ValueError('I cannot work under these conditions') + + def make_proxy_div(self, param, num: str | float, denom: str | float, bnd: str = 'upper') -> bool: + other_param = self[denom] + other_lb, other_ub = other_param.lb, other_param.ub + + proxy = {'name': f'{param.name}*{other_param.name}', 'value': other_param.value*param.value, 'lb': None, 'ub': None} + + # switching signs is bad for inequalities + if other_lb < 0 < other_ub: + raise ValueError('Parameter in expression changes sign') + + if bnd == 'upper': + # this whole schlamassel is only working for some values as lower bound + if param.lb not in [-np.inf, 0]: + raise ValueError('Invalid lower bounds') + + if other_ub < 0 or num < 0: + # upper bound is always negative, switch boundaries + proxy['lb'] = num + proxy['ub'] = param.lb if param.lb == 0 else np.inf + else: + # upper bound is always positive + proxy['lb'] = param.lb + proxy['ub'] = num + + elif bnd == 'lower': + if param.ub not in [np.inf, 0]: + raise ValueError('Invalid upper bound') + + if other_ub <= 0 or num < 0: + proxy['lb'] = param.lb if param.lb == 0 else np.inf + proxy['ub'] = num + else: + proxy['lb'] = num + proxy['ub'] = param.ub + + else: + raise ValueError(f'unknown bound {bnd!r}, use `upper`or `lower`') + + param.set_expression(f'{num}/{denom}') + self.add(**proxy) + self.update_namespace() + + return True + class Parameter: """ @@ -106,15 +193,21 @@ class 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): + def __init__(self, + name: str, + value: float | str, + var: bool = True, + lb: str | float = -np.inf, + ub: str | float = np.inf, + ): 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.lb: str | None | float = lb if lb is not None else -np.inf + self.ub: str | float | None = ub if ub is not None else np.inf self.namespace: dict = {} self.eval_allowed: bool = True self._expr: None | str = None @@ -126,10 +219,13 @@ class Parameter: self._expr = value self.var = False else: - if self.lb <= value <= self.ub: - self._value = value + if isinstance(self.lb, (int, float)) and isinstance(self.ub, (int, float)): + if self.lb <= value <= self.ub: + self._value = value + else: + raise ValueError('Value of parameter is outside bounds') else: - raise ValueError('Value of parameter is outside bounds') + self._value = value self.init_val = value @@ -191,6 +287,12 @@ class Parameter: return + def set_expression(self, expr: str): + self._value = None + self._expr_disp = expr + self._expr = expr + self.var = False + @property def scaled_error(self) -> None | float: if self.error is not None: From a97c31325f6779e1c76aa1237d6557870278f0b5 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 27 Sep 2023 15:32:19 +0200 Subject: [PATCH 2/2] 1/p bounds in ui --- src/gui_qt/fit/fit_forms.py | 22 ++++++++++++++-------- src/nmreval/fit/minimizer.py | 9 +++++++-- src/nmreval/fit/parameter.py | 2 ++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 67f27d2..6f2a424 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -86,15 +86,21 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): p = self.parameter_line.text().replace(',', '.') if self.checkBox.isChecked(): - try: - lb = float(self.lineEdit.text().replace(',', '.')) - except ValueError: - lb = None + lb_text = self.lineEdit.text() + lb = None + if lb_text: + try: + lb = float(lb_text.replace(',', '.')) + except ValueError: + lb = lb_text - try: - rb = float(self.lineEdit_2.text().replace(',', '.')) - except ValueError: - rb = None + ub_text = self.lineEdit_2.text() + rb = None + if ub_text: + try: + rb = float(ub_text.replace(',', '.')) + except ValueError: + rb = ub_text else: lb = rb = None diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 5594fd1..2b8bf54 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -278,10 +278,12 @@ class FitRoutine(object): data._model = self.fit_model self._no_own_model.append(data) + data.parameter.prepare_bounds() + return self._prep_parameter(data.parameter) @staticmethod - def _prep_parameter(parameter): + def _prep_parameter(parameter: Parameters): vals = [] var_pars = [] for p_k, v_k in parameter.items(): @@ -293,7 +295,8 @@ class FitRoutine(object): return pp, lb, ub, var_pars - def _prep_global(self, data_group, linked): + @staticmethod + def _prep_global(data_group: list[Data], linked): p0 = [] lb = [] ub = [] @@ -306,6 +309,8 @@ class FitRoutine(object): for k, v in data.model.parameter.items(): data.replace_parameter(k, v) + data.parameter.prepare_bounds() + actual_pars = [] for i, p_k in enumerate(data.para_keys): p_k_used = p_k diff --git a/src/nmreval/fit/parameter.py b/src/nmreval/fit/parameter.py index 7ae525e..c68b8d0 100644 --- a/src/nmreval/fit/parameter.py +++ b/src/nmreval/fit/parameter.py @@ -102,10 +102,12 @@ class Parameters(dict): p._expr = expression def prepare_bounds(self): + print('prepare_bounds') original_values = list(self.values()) for param in original_values: already_with_expression = False for mode, value in (('lower', param.lb), ('upper', param.ub)): + print(mode, value) if already_with_expression: raise ValueError('Only one boundary can be an expression') already_with_expression = self.parse(param, value, bnd=mode)