1
0
forked from IPKM/nmreval

1/p bounds added

This commit is contained in:
Dominik Demuth 2023-09-27 13:55:37 +02:00
parent 0046d04683
commit 207ee5bffd

View File

@ -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: