forked from IPKM/nmreval
Merge branch 'fit_bounds'
This commit is contained in:
commit
6f76349932
@ -86,15 +86,21 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter):
|
|||||||
p = self.parameter_line.text().replace(',', '.')
|
p = self.parameter_line.text().replace(',', '.')
|
||||||
|
|
||||||
if self.checkBox.isChecked():
|
if self.checkBox.isChecked():
|
||||||
try:
|
lb_text = self.lineEdit.text()
|
||||||
lb = float(self.lineEdit.text().replace(',', '.'))
|
lb = None
|
||||||
except ValueError:
|
if lb_text:
|
||||||
lb = None
|
try:
|
||||||
|
lb = float(lb_text.replace(',', '.'))
|
||||||
|
except ValueError:
|
||||||
|
lb = lb_text
|
||||||
|
|
||||||
try:
|
ub_text = self.lineEdit_2.text()
|
||||||
rb = float(self.lineEdit_2.text().replace(',', '.'))
|
rb = None
|
||||||
except ValueError:
|
if ub_text:
|
||||||
rb = None
|
try:
|
||||||
|
rb = float(ub_text.replace(',', '.'))
|
||||||
|
except ValueError:
|
||||||
|
rb = ub_text
|
||||||
else:
|
else:
|
||||||
lb = rb = None
|
lb = rb = None
|
||||||
|
|
||||||
|
@ -278,10 +278,12 @@ class FitRoutine(object):
|
|||||||
data._model = self.fit_model
|
data._model = self.fit_model
|
||||||
self._no_own_model.append(data)
|
self._no_own_model.append(data)
|
||||||
|
|
||||||
|
data.parameter.prepare_bounds()
|
||||||
|
|
||||||
return self._prep_parameter(data.parameter)
|
return self._prep_parameter(data.parameter)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _prep_parameter(parameter):
|
def _prep_parameter(parameter: Parameters):
|
||||||
vals = []
|
vals = []
|
||||||
var_pars = []
|
var_pars = []
|
||||||
for p_k, v_k in parameter.items():
|
for p_k, v_k in parameter.items():
|
||||||
@ -293,7 +295,8 @@ class FitRoutine(object):
|
|||||||
|
|
||||||
return pp, lb, ub, var_pars
|
return pp, lb, ub, var_pars
|
||||||
|
|
||||||
def _prep_global(self, data_group, linked):
|
@staticmethod
|
||||||
|
def _prep_global(data_group: list[Data], linked):
|
||||||
p0 = []
|
p0 = []
|
||||||
lb = []
|
lb = []
|
||||||
ub = []
|
ub = []
|
||||||
@ -306,6 +309,8 @@ class FitRoutine(object):
|
|||||||
for k, v in data.model.parameter.items():
|
for k, v in data.model.parameter.items():
|
||||||
data.replace_parameter(k, v)
|
data.replace_parameter(k, v)
|
||||||
|
|
||||||
|
data.parameter.prepare_bounds()
|
||||||
|
|
||||||
actual_pars = []
|
actual_pars = []
|
||||||
for i, p_k in enumerate(data.para_keys):
|
for i, p_k in enumerate(data.para_keys):
|
||||||
p_k_used = p_k
|
p_k_used = p_k
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
import re
|
import re
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
@ -39,10 +40,11 @@ class Parameters(dict):
|
|||||||
value: str | float | int = None,
|
value: str | float | int = None,
|
||||||
*,
|
*,
|
||||||
var: bool = True,
|
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)
|
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)
|
self.add_parameter(key, par)
|
||||||
|
|
||||||
@ -99,6 +101,93 @@ class Parameters(dict):
|
|||||||
|
|
||||||
p._expr = expression
|
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)
|
||||||
|
|
||||||
|
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:
|
class Parameter:
|
||||||
"""
|
"""
|
||||||
@ -106,15 +195,21 @@ class Parameter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO Parameter should know its own key
|
# 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._value: float | None = None
|
||||||
self.var: bool = bool(var) if var is not None else True
|
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.error: None | float = None if self.var is False else 0.0
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.function: str = ""
|
self.function: str = ""
|
||||||
|
|
||||||
self.lb: None | float = lb if lb is not None else -np.inf
|
self.lb: str | 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.ub: str | float | None = ub if ub is not None else np.inf
|
||||||
self.namespace: dict = {}
|
self.namespace: dict = {}
|
||||||
self.eval_allowed: bool = True
|
self.eval_allowed: bool = True
|
||||||
self._expr: None | str = None
|
self._expr: None | str = None
|
||||||
@ -126,10 +221,13 @@ class Parameter:
|
|||||||
self._expr = value
|
self._expr = value
|
||||||
self.var = False
|
self.var = False
|
||||||
else:
|
else:
|
||||||
if self.lb <= value <= self.ub:
|
if isinstance(self.lb, (int, float)) and isinstance(self.ub, (int, float)):
|
||||||
self._value = value
|
if self.lb <= value <= self.ub:
|
||||||
|
self._value = value
|
||||||
|
else:
|
||||||
|
raise ValueError('Value of parameter is outside bounds')
|
||||||
else:
|
else:
|
||||||
raise ValueError('Value of parameter is outside bounds')
|
self._value = value
|
||||||
|
|
||||||
self.init_val = value
|
self.init_val = value
|
||||||
|
|
||||||
@ -191,6 +289,12 @@ class Parameter:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def set_expression(self, expr: str):
|
||||||
|
self._value = None
|
||||||
|
self._expr_disp = expr
|
||||||
|
self._expr = expr
|
||||||
|
self.var = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scaled_error(self) -> None | float:
|
def scaled_error(self) -> None | float:
|
||||||
if self.error is not None:
|
if self.error is not None:
|
||||||
|
Loading…
Reference in New Issue
Block a user