Files
controller-wisetopdataserver/python/biopro/devlib/parameter.py
T

880 lines
26 KiB
Python
Raw Normal View History

2021-12-20 14:52:55 +08:00
"""Device Library/Parameter
For the number or data structure used in this library, we can group them into there category:
1. raw instruction value (P value)
The value store in the device parameter and used in the instruction building.
#. raw parameter value (V value)
The value store in the device configuration which has a physical meaning.
It will be saved into the recording meta file if has property *record_meta*.
#. human-readable (H value)
The value or text just to present to human and used in view module.
"""
from . import *
from .domain import *
class PropertySetter(JsonSerialize):
"""
**json format**
::
PropertySetter
= "exact"
| "near"
| "floor"
| "ceil"
| PropertySetterObject
PropertySetterObject = {
"variable"? :variable_table
"parameter" :parameter_table
}
variable_table = {
# with scope from caller
variable_name = Expression[T]
}
parameter_table = {
# with scope from caller plus variable_table
parameter_name = Expression[T]
}
"""
CAST_EXACT = "exact"
CAST_NEAR = "near"
CAST_FLOOR = "floor"
CAST_CEIL = "ceil"
CAST_NT = "NT" # XXX rename
__slots__ = ('_parameter', '_variable')
def __init__(self, parameter: Dict[str, Expression[Any]], variable: Dict[str, Expression[Any]] = None):
self._parameter = parameter
self._variable = variable
def cast_value(self, value: Any, context: Scope) -> Dict[str, Any]:
"""
:param value: V value
:param context: general case root scope
:return: dict with parameter name map to P value
"""
scope = self._resolve_variable(value, context)
ret = {}
for par, exp in self._parameter.items():
ret[par] = exp.value(scope)
return ret
def _resolve_variable(self, value: Any, context: Scope) -> Scope:
scope = context.child(VALUE=value)
if self._variable is not None:
var_set = set(self._variable.keys())
var_set_size = len(var_set)
while var_set_size != 0:
for var, exp in self._variable.items():
if var not in var_set:
try:
val = exp.value(scope)
except NameError:
pass
else:
scope.set_local(var, val)
var_set.discard(var)
if len(var_set) == var_set_size:
raise RuntimeError('recursive variable define : ' + ' '.join(var_set))
var_set_size = len(var_set)
return scope
def as_json(self):
ret = {
'parameter': {k: v.as_json() for k, v in self._parameter.items()}
}
if self._variable is not None:
ret['variable'] = {k: v.as_json() for k, v in self._variable.items()}
return ret
@classmethod
def parse(cls, json: JSON_OBJECT) -> 'PropertySetter':
par = _get(json, 'parameter', dict, 'value_set')
var = _get(json, 'variable', dict, 'value_set', nullable=True)
par = _dict(par, lambda k, v: Expression.parse(v))
if var is not None:
var = _dict(var, lambda k, v: Expression.parse(v))
return PropertySetter(par, var)
@classmethod
def find_root(cls, strategy: str, f: Callable[[int], float], v: float, s: Tuple[int, int]) -> Optional[int]:
"""
:param strategy: strategy method
:param f: function (P) -> V
:param v: V value
:param s: seed range
:return: P value, None if fail
"""
if strategy == cls.CAST_EXACT:
return cls.exact_root(f, v, s)
elif strategy == cls.CAST_NEAR:
return cls.near_root(f, v, s)
elif strategy == cls.CAST_FLOOR:
return cls.ceil_root(f, v, s)
elif strategy == cls.CAST_CEIL:
return cls.floor_root(f, v, s)
elif strategy == cls.CAST_NT:
return cls.nt_root(f, v, s)
else:
raise RuntimeError('unknown cast strategy : ' + strategy)
@staticmethod
def find_identify(f: Callable[[int], str], value: str, seed_range: Tuple[int, int]) -> Optional[int]:
for p in range(*seed_range):
if f(p) == value:
return p
return None
@staticmethod
def exact_root(f: Callable[[int], float], value: float, seed_range: Tuple[int, int]) -> Optional[int]:
# TODO implement binary search algo
for p in range(*seed_range):
if f(p) == value:
return p
return None
@staticmethod
def near_root(f: Callable[[int], float], value: float, seed_range: Tuple[int, int]) -> Optional[int]:
ret = None
min_d = None
for p in range(*seed_range):
v = f(p)
d = abs(value - v)
if min_d is None or d < min_d:
ret = p
min_d = d
return ret
@staticmethod
def ceil_root(f: Callable[[int], float], value: float, seed_range: Tuple[int, int]) -> Optional[int]:
ret = None
for p in range(seed_range[1], seed_range[0], -1):
if value < f(p):
ret = p
return ret
@staticmethod
def floor_root(f: Callable[[int], float], value: float, seed_range: Tuple[int, int]) -> Optional[int]:
ret = None
for p in range(*seed_range):
if f(p) < value:
ret = p
return ret
@staticmethod
def nt_root(f: Callable[[int], float], value: float, seed: Tuple[int, int] = None) -> int:
if seed is None:
s = (0, 100)
else:
s = (seed[0], seed[1])
v = [f(s[0]), f(s[1])]
k = (v[1] - v[0]) / (s[1] - s[0])
a = (s[1] - s[0]) // 2
while True:
b = f(a)
c = value - b
if c == 0:
return a
d = int(c / k)
if d == 0:
return a - 1
a += d
class ParameterInfo(JsonSerialize):
"""
**json format**
::
ParameterInfo = {
"alias"? :str | list[str]
"description"? :str
"record_meta"? :bool
"private"? :bool
"domain"? :ParameterDomain
"initial"? :int|Expression[T]
"guard"? :GuardExpression|ListGuardExpression
"value"? :Expression[T]
"value_set"? :PropertySetter
"on_change"? :str = instruction_name
}
*record_meta* this parameter will be present in the device configuration by default and
will be record in the recording meta file.
Required parameters are always *record_meta*.
*private* this parameter will not present in the device configuration.
*record_meta* parameter cannot be *private*.
*domain* parameter P value domain. Either *domain* or *value* should be existed.
If only *value* present, *domain* would be considered :class:`ParameterConstantRangeDomain`
with value ``[0, len(value)]``.
If *domain* is "property", either *value* or *value_set* must present.
If *domain* is "action", neither *value* nor *value_set* present.
If parameter is ``CHANNEL``, the *domain* only allow "property" or set domain.
*guard* parameter guarder when parameter setting and modifying, the property *raise* can be ignored.
*value* V value getter,
*value_set* V value setter. If *domain* is not a property, it mean the strategy of casting V value to P value.
omitted default to "exact". If *domain* is a property, it should be either ``null`` or a :class:`PropertySetter`.
*initial* initial P value for this parameter. It can be a expression which will be resolved at loading time with
constant scope. If *domain* is :class:`ParameterCollectionDomain`, *initial* will be ignored.
**variable scope search chain**
* used when initialize
1. constant
* used when value evaluation
1. general case root (:class:`biopro.devlib.library.DeviceLibrary`)
"""
SAMPLE_RATE = 'SAMPLE_RATE'
"""required parameter with type int"""
CHANNEL = 'CHANNEL'
"""required parameter with type Set[int]"""
AMP_GAIN = 'AMP_GAIN'
"""required parameter with type int"""
REQUIRED = (SAMPLE_RATE, CHANNEL, AMP_GAIN)
"""required parameters"""
__slots__ = ('_name', '_domain', '_initial', '_value', '_value_set', '_guard', '_on_change',
'_alias', '_description', '_record_meta', '_private')
def __init__(self,
name: str,
domain: ParameterDomain,
initial: Optional[Any] = None,
guard: Optional[ListGuardExpression] = None,
value: Expression[Any] = None,
value_set: Union[None, str, PropertySetter] = None,
on_change: Optional[str] = None,
alias: Union[str, List[str]] = None,
description: Optional[str] = None,
record_meta=False,
private=False):
"""
:param name: parameter name
:param domain: parameter domain
:param initial: initial P value
:param guard: parameter guarded
:param value: parameter present value
:param value_set: parameter setter
:param on_change: instruction to call when parameter changed
:param alias: parameter name alias
:param description: parameter description
:param record_meta: record this parameter in recording meta file
"""
if domain is None:
raise TypeError('NoneType domain for parameter : ' + name)
elif domain == ParameterConstantDomain:
if initial is None:
initial = 0
elif domain == ParameterPropertyDomain:
if value is None and value_set is None:
raise RuntimeError('parameter %s with property domain without value/value_set expression' % name)
if value_set is not None and not isinstance(value_set, PropertySetter):
raise TypeError('value_set of the parameter %s not a PropertySetter' % name)
elif domain == ParameterActionDomain:
if value is not None or value_set is not None:
raise RuntimeError('parameter %s with action domain with value/value_set expression' % name)
record_meta = False
private = True
if name == self.CHANNEL:
if domain == ParameterPropertyDomain:
pass
elif isinstance(domain, ParameterSetDomain):
if not isinstance(domain.element_domain, ParameterValueDomain):
raise RuntimeError('parameter CHANNEL not a set of value domain')
else:
raise RuntimeError('parameter CHANNEL not a set or property domain')
if isinstance(alias, str):
alias = [alias]
self._name = name
self._domain: ParameterDomain = domain
self._initial: Optional[int] = initial
self._value: Optional[Expression[Any]] = value
self._value_set = value_set
self._on_change = on_change
self._guard = guard
self._alias: List[str] = list(alias) if alias is not None else []
self._description = description
# force record meta for required parameter
self._record_meta = record_meta or name in self.REQUIRED
# record_meta parameter cannot be private
self._private = private and not self._record_meta
@property
def name(self) -> str:
"""parameter name """
return self._name
@property
def domain(self) -> ParameterDomain:
"""parameter domain."""
return self._domain
@property
def is_readable(self) -> bool:
if self._domain == ParameterPropertyDomain and self._value is None:
return False
elif self._domain == ParameterActionDomain:
return False
else:
return True
@property
def is_writable(self) -> bool:
if self._domain == ParameterConstantDomain:
return False
elif self._domain == ParameterPropertyDomain and self._value_set is None:
return False
else:
return True
@property
def element_domain(self) -> ParameterDomain:
"""parameter domain."""
if isinstance(self._domain, ParameterCollectionDomain):
return self._domain.element_domain
else:
return self._domain
@property
def description(self) -> Optional[str]:
return self._description
@property
def record_meta(self) -> bool:
return self._record_meta
@property
def private(self) -> bool:
return self._private
@property
def alias(self) -> List[str]:
return list(self._alias)
@property
def on_change(self) -> Optional[str]:
return self._on_change
def init_para(self) -> Any:
"""
:return: initial P value
:raises value: illegal initial value
:raises PropertyDomainError: parameter is a property
:raises ActionDomainError: parameter is a action
"""
if self._domain == ParameterConstantDomain:
return int(self._initial)
elif self._domain == ParameterPropertyDomain:
raise PropertyDomainError(self._name)
elif self._domain == ParameterActionDomain:
raise ActionDomainError(self._name)
else:
return self._domain.init_para(self._initial)
def ensure_guard(self, context: 'ParameterTableScope'):
"""guarder ensure value in *context* is validated.
:param context: general case root scope
:raises IllegalParameterValueError: guarder raise error
"""
if self._guard is None:
return
guard = self._guard.test(context)
if guard is not None:
raise IllegalParameterValueError(self._name, guard.error_message)
def get_value(self, value: Any, context: 'ParameterTableScope') -> Any:
"""get the parameter present value.
:param value: P value
:param context: general case root scope
:return: V value
"""
if self._domain == ParameterActionDomain:
raise ActionDomainError(self._name)
if self._domain == ParameterConstantDomain:
# overwrite value if constant domain
value = int(self._initial)
if self._value is None:
# parameter no getter, which V value equals to P value. return directly
return value
else:
try:
return self._get_value(value, context)
except BaseException as e:
raise DeviceParameterError(self._name, 'get_value error : ' + str(e)) from e
def _get_value(self, value: Any, context: 'ParameterTableScope') -> Any:
if isinstance(self._domain, ParameterArrayDomain):
if isinstance(value, list):
size = self._domain.size
index_para = self._domain.index_parameter
# keep old index_para value
try:
index_value_old = context.get_local(index_para)
except KeyError:
index_value_old = None
ret = []
for i in range(size):
# print(self._value.as_json(), 'VALUE=' + str(v))
context.set_local(index_para, i)
context.set_local('VALUE', value[i])
ret.append(self._value.value(context))
# restore old index_para value
if index_value_old is not None:
context.set_local(index_para, index_value_old)
else:
context.del_local(index_para)
return ret
else:
return self._value.value_local(context, VALUE=value)
elif isinstance(self._domain, ParameterCollectionDomain):
if isinstance(value, (list, set)):
ret = []
for v in value:
# print(self._value.as_json(), 'VALUE=' + str(v))
ret.append(self._value.value_local(context, VALUE=v))
if isinstance(value, set):
return set(ret)
else:
return ret
else:
return self._value.value_local(context, VALUE=value)
elif isinstance(self._value, ConstantExpression):
return self._value.constant
elif isinstance(self._value, ArrayExpression):
return self._value[int(value)]
else:
return self._value.value_local(context, VALUE=value)
def cast_value(self, value: Any, context: 'ParameterTableScope') -> Any:
"""get the parameter according to the present value. The parameter not always fit the present value.
:param value: V value
:param context: general case root scope
:return: P value
"""
if self._domain == ParameterActionDomain:
raise ActionDomainError(self._name)
setter: PropertySetter = self._value_set
if self._domain == ParameterConstantDomain:
# constant parameter
return int(self._initial)
elif isinstance(setter, PropertySetter):
# raise an interrupt to indicate caller it is a special operation.
raise PropertySetInterrupt(self._name, setter.cast_value(value, context))
elif self._domain == ParameterPropertyDomain:
if setter is None:
raise PropertyDomainError(self._name, 'not a setter')
else:
assert not isinstance(setter, PropertySetter)
raise TypeError('parameter %s setter not a PropertySetter' % self._name)
elif self._value is None:
# parameter no getter, which V value equals to P value. just return
return value
elif isinstance(self._domain, ParameterArrayDomain):
domain = self._domain.element_domain
size = self._domain.size
index_para = self._domain.index_parameter
assert index_para in context
ret = []
for i in range(size):
context.set_local(index_para, i)
ret.append(self._cast_value(value[i], domain, context))
return ret
elif isinstance(self._domain, ParameterCollectionDomain):
domain = self._domain.element_domain
ret = []
for v in value:
ret.append(self._cast_value(v, domain, context))
if isinstance(value, list):
return ret
elif isinstance(value, set):
return set(ret)
else:
raise RuntimeError('unknown type for parameter ' + self._name + ' : ' + str(type(value)))
else:
return self._cast_value(value, self._domain, context)
def cast_value_dependency(self) -> List[str]:
if self._value is None:
return []
elif isinstance(self._value, ComplexExpression):
return self._value.dependency
else:
return []
@staticmethod
def _cast_bool(value: Any) -> bool:
if value is None:
return False
elif isinstance(value, bool):
return value
elif isinstance(value, int):
return value > 0
elif isinstance(value, float):
return value > 0
elif isinstance(value, str):
value = value.lower()
return value == '1' or value == 't' or value == 'true'
return False
def _cast_value(self, value: Any, domain: ParameterDomain, context: Scope) -> Any:
if domain == ParameterBoolDomain:
return self._cast_bool(value)
else:
expr = self._value
assert expr is not None
def _f(_value):
return expr.value_local(context, VALUE=_value)
strategy = self._value_set
if strategy is None:
strategy = PropertySetter.CAST_EXACT
assert isinstance(strategy, str)
ret = None
if domain == ParameterIntDomain:
seed_range = (int(value / 2), int(value * 3 / 2))
ret = PropertySetter.find_root(PropertySetter.CAST_NT, _f, value, seed_range)
elif isinstance(domain, ParameterValueDomain):
domain_range = domain.range
test = _f(domain_range[0])
if isinstance(test, int):
# XXX binary search with [floor, exact, ceil]
ret = PropertySetter.find_root(strategy, _f, int(value), domain_range)
elif isinstance(test, float):
# XXX nt root
ret = PropertySetter.find_root(strategy, _f, float(value), domain_range)
elif isinstance(test, str):
try:
value = int(value)
if domain_range[0] <= value < domain_range[1]:
# return directory
return value
except ValueError:
pass
ret = PropertySetter.find_identify(_f, str(value), domain_range)
else:
raise DeviceParameterError(self._name,
'unsupported cast value for domain : ' + domain.__class__.__name__)
if ret is None:
raise DeviceParameterError(self._name, 'cannot cast value : ' + str(value))
# print(self.name, ret)
return ret
def as_json(self):
ret = {
'domain': self.domain.as_json(),
'record_meta': self._record_meta,
}
if len(self._alias) == 1:
ret['alias'] = self._alias[0]
elif len(self._alias) > 1:
ret['alias'] = self._alias
if self._description is not None:
ret['description'] = self._description
if self._initial is not None:
ret['initial'] = self._initial
if self._value is not None:
ret['value'] = self._value.as_json()
if self._value_set is not None:
ret['value'] = JsonSerialize.to_json(self._value_set)
if self._guard is not None:
ret['guard'] = self._guard.as_json()
return ret
class ParameterTable(DictNode[ParameterInfo]):
"""
**json format**
::
ParameterTable = {
parameter_name :ParameterInfo
}
**required parameter**
defined in :class:`biopro.devlib.device.DeviceParameter`
**variable scope search chain**
* used when parameter initializing.
1. "constant"
* used when parameter set/add guarding.
1. general case root (:class:`biopro.devlib.library.DeviceLibrary`)
#. {VALUE=new value}
* used when parameter transform to present value
1. general case root (:class:`biopro.devlib.library.DeviceLibrary`)
#. {VALUE=parameter value}
relate function :func:`ParameterInfo.get_value`, :func:`ParameterInfo.cast_value`
"""
__slots__ = ()
def __init__(self, parameters: Optional[Dict[str, ParameterInfo]] = None):
super().__init__(parameters)
def keys_all(self, show_private=False) -> Iterable[str]:
r = set()
for para in self.values():
if show_private or not para.private:
r.add(para.name)
r.update(para.alias)
return r
def __contains__(self, key: Any) -> bool:
if key in self._dict:
return True
for para in self.values():
if key in para.alias:
return True
return False
def __missing__(self, key: str) -> ParameterInfo:
for para in self.values():
if key in para.alias:
return para
raise KeyError(key)
def get_value(self, name: Union[str, ParameterInfo], value: Any, context: Scope) -> Any:
"""get parameter V value
:param name: parameter name or info
:param value: P value
:param context:
:return: V value
:raises KeyError: parameter name not found
"""
if isinstance(name, str):
name = self[name]
return name.get_value(value, context.chain(ParameterTableScope(self)))
def cast_value(self, name: Union[str, ParameterInfo], value: Any, context: Scope) -> Optional[int]:
if isinstance(name, str):
name = self[name]
return name.cast_value(value, context.chain(ParameterTableScope(self)))
def ensure_guard(self, name: Union[str, ParameterInfo], context: Scope):
"""invoke parameter *name*'s guarder ensure its value in *context* is validated.
:param name: parameter or its name
:param context: general case root scope
:raises IllegalParameterValueError: guarder raise error
"""
if isinstance(name, str):
name = self[name]
return name.ensure_guard(context.chain(ParameterTableScope(self)))
class ParameterTableScope(Scope):
"""parameter table scope, make user access the parameter value through the context scope.
"""
__slots__ = '_table', '_overwrite'
def __init__(self, table: ParameterTable):
super().__init__()
self._table = table
self._overwrite = {}
def locals(self) -> Iterable[str]:
ret = list(self._overwrite.keys())
ret.append('_value')
return ret
def contain_local(self, key: str) -> bool:
return key == '_value' or key in self._overwrite
def get_local(self, key: str) -> Any:
if key == '_value':
return self._value
else:
return self._overwrite[key]
def set_local(self, key: str, value: Any):
self._overwrite[key] = value
def del_local(self, key: str):
del self._overwrite[key]
def clear(self):
self._overwrite.clear()
def _value(self, name: str):
para = self._table[name]
domain = para.domain
ret = para.get_value(self[name], self)
# print('_value(%s)' % name, '=', ret)
if isinstance(domain, ParameterArrayDomain):
ret = domain.get_para(ret, self)
return ret