Calculated Variables
Overview and end-user documentation
Preface
Rationale
the system already provides information which is expressed in quantities different than requested by users. Example: an OPC UA server is built for a device which measures period of a repetitive process, but the users prefer frequency instead. Solution: a Calculated Variable might be added to the server with a simple reciprocal (y=1/x) formula to compute frequency in terms of period.
the system already provides information but in another unit.
the system provides raw (e.g. uncalibrated) information. Example: an OPC UA server publishes data from Analog to Digital Converter. This is raw information though. Solution: a Calculated Variable might be added to provide for gain and offset calibration.
similarly to the example above, the ADC might have the value from a conversion of e.g. a temperature sensor. A Calculated Variable might be attached with a formula that recomputes the ADC value (in volts) into temperature.
Design-based variables vs CalculatedVariables
Aspect |
Design-based variable (cache-variable or source-variable) |
Calculated Variable |
---|---|---|
Instantiation |
The variable always belongs to an object of a quasar class which is defined by the model (Design). Therefore it’s the design which determines its type, behaviour, limitations etc. |
The variable is defined in the configuration file which is loaded at runtime. |
Source of information |
Comes from inside of the server, typically from user supplied Device logic. Any method available by C++ programming can be used. |
Is a result of an analytical expression evaluation. The inputs to the expression can be:
|
Output type |
Any supported by quasar, including arrays. |
Double or boolean scalar. |
Adding new variable per quasar class requires recompilation? |
Yes |
No |
Feature list
Manual Update and Auto Update Control
Method |
Description |
---|---|
setAutoUpdate(bool) |
This function allows the automatic update of a calculated variable to be enabled or disabled. When called with true, the calculated variable will automatically update whenever any of its dependencies change. When called with false, the calculated variable will not update automatically, and manual triggering of recalculation is required. The default value is true. |
triggerRecalculation() |
This function manually triggers the recalculation of a calculated variable. It is useful when automatic updates are disabled and a recalculation is needed. When this function is called, the calculated variable will recompute its value based on the current values of its dependencies. |
Operators and built-in functions
Name |
Argc. |
Explanation |
|
1 |
sine function |
|
1 |
cosine function |
|
1 |
tangens function |
|
1 |
arcus sine function |
|
1 |
arcus cosine function |
|
1 |
arcus tangens function |
|
1 |
hyperbolic sine function |
|
1 |
hyperbolic cosine |
|
1 |
hyperbolic tangens function |
|
1 |
hyperbolic arcus sine function |
|
1 |
hyperbolic arcus tangens function |
|
1 |
hyperbolic arcur tangens function |
|
1 |
logarithm to the base 2 |
|
1 |
logarithm to the base 10 |
|
1 |
logarithm to base e (2.71828…) |
|
1 |
logarithm to base e (2.71828…) |
|
1 |
e raised to the power of x |
|
1 |
square root of a value |
|
1 |
sign function -1 if x<0; 1 if x>0 |
|
1 |
round to nearest integer |
|
1 |
absolute value |
|
var. |
min of all arguments |
|
var. |
max of all arguments |
|
var. |
sum of all arguments |
|
var. |
mean value of all arguments |
|
2 |
x^y |
Operator |
Description |
Priority |
---|---|---|
|
assignement |
-1 |
|
logical and |
1 |
|
logical or |
2 |
|
less or equal |
4 |
|
greater or equal |
4 |
|
not equal |
4 |
|
equal |
4 |
|
greater than |
4 |
|
less than |
4 |
|
addition |
5 |
|
subtraction |
5 |
|
multiplication |
6 |
|
division |
6 |
|
raise x to the power of y |
7 |
Configuration file schema regarding Calculated Variables
**Name ** |
**Obligatory? ** |
XSD Type |
**Meaning ** |
---|---|---|---|
name |
Yes |
xs:string |
Name of this calculated variable. Note that the full address that this variable obtains will be the name prefixed by the address of position in the Address Space where the variable gets instantiated. |
value |
Yes |
xs:string |
Value formula, that is: an analytical expression used to evaluate value of this variable. Some examples will be given below. |
initialValue |
No |
xs:double |
Initial value, i.e. the value that this variable will hold BEFORE first evaluation happens (which normally is when all formula ingredients receive the initial update). If initialValue is not given then the variable will hold NULL along BadWaitin gForInitialData status. |
isBoolean |
No |
xs:boolean |
Evaluate and present as boolean. The final result will be OpcUa_True if the calculation result is non-zero. |
status |
No |
xs:string |
Status formula, that is: an analytical expression used to evaluate OPC UA status-code of this variable. The status-code will be OpcUa_Good if the formula evaluates to non-zero otherwise OpcUa_Bad. If status formula is not used then by default the variable is OpcUa_Good when all input arguments are in good status, or OpcUa_Bad otherwise |
Meta-functions and meta-operators (dollar signs in the formulas)
$thisObjectAddress
$thisObjectAddress
evaluates to the string address of the object under
which the calculated variable was instantiated. It finds a very
practical application to build generalized formulas, which can be
applied “under” multiple places in the address-space, so
$thisObjectAddress
serves as the relative pointer to the object
address.<TestClass name="tc">
<CalculatedVariable name="test_var_multiplied" value="$thisObjectAddress.testVariable * 1000" />
</TestClass>
$_
$_
is an abbreviation for $thisObjectAddress
which comes practical for
long, complex formulas.
$parentObjectAddress(numLevelsUp=N)
$parentObjectAddress
is a generalization of $thisObjectAddress
. For
N=0 it evaluates to $thisObjectAddress
, for N=1 to its parent object
and so on.<TestClass name="tc">
<TestSubClass name="tsc">
<CalculatedVariable name="test_var_multiplied" value="$parentObjectAddress(numLevelsUp=1).testVariable * 1000" />
</TestSubClass>
</TestClass>
$applyGenericFormula(formula)
$applyGenericFormula
is used in the context of generalized function
templates and documented there.Generalized formula templates
defining the generalized formula at the top of the configuration file using the CalculatedVariableGenericFormula XML element
applying the formula at the point of use using
$applyGenericFormula
meta-function.
<CalculatedVariableGenericFormula name="thermistorTemperature"
formula="1/( 3.3540154*10^(-3)+(2.5627725*10^(-4)*log(1000*$thisObjectAddress.value/500))+(2.0829210*10^(-6)*(log(1000*$thisObjectAddress.value/500))^2)+(7.3003206*10^(-8)*(log(1000*$thisObjectAddress.value/500))^3)) -273.15"/>
<AnalogInput id="0" name="GBTX1_TEMP" enableCurrentSource="true" > <CalculatedVariable name="temperature" value="$applyGenericFormula(thermistorTemperature)" /> </AnalogInput>
<AnalogInput id="1" name="GBTX2_TEMP" enableCurrentSource="true" > <CalculatedVariable name="temperature" value="$applyGenericFormula(thermistorTemperature)" /> </AnalogInput>
CalculatedVariables logging and tracing
CalcVars
.<StandardMetaData>
<Log>
<ComponentLogLevels>
<ComponentLogLevel componentName="CalcVars" logLevel="TRC" />
</ComponentLogLevels>
</Log>
</StandardMetaData>
Escaping variable names containing dashes (“-”) and slashes (“/”)
name
attribute of XML elements in the
configuration files) with names containing dashes or slashes.<MyDevice name="Bus1/Device2-A">
<CalculatedVariable name="calibrationConstant" value="2.35"/>
</MyDevice>
<CalculatedVariable name="voltage" value="X - Bus1/Device2-A.calibrationConstant"/>
<CalculatedVariable name="voltage" value="X - Bus1\/Device2\-A.calibrationConstant"/>
Examples
NTC sensors: converting resistance into temperature in Celsius and Fahrenheit degrees
<CalculatedVariable name="T0" value="298.15"/>
<CalculatedVariable name="B" value="3977"/>
<CalculatedVariable name="R0" value="10E3"/>
<CalculatedVariable name="temperatureK" value="T0*B/(T0*ln(NTC1.resistance/R0)+B)" />
<CalculatedVariable name="temperatureC" value="temperatureK-273.15"/>
<CalculatedVariable name="temperatureF" value="temperatureC*1.8+32"/>
isBoolean
attribute:<CalculatedVariable name="isWarmEnough" value="temperatureC > 20" isBoolean="true" />
CalculatedVariable attached to multiple different quasar entities
<TestClass name="tc" configentry="125"/>
<FreeVariable name="free_variable" type="Double"/>
<CalculatedVariable name="a_calc_var" value="500" />
<CalculatedVariable
name="sum_of_free_cache_variables_and_configentry"
value="free_variable + tc.testVariable + tc.configentry - a_calc_var" />
Counter-examples
Place no white-space between unary operation (e.g. a function) and the parenthesis around its operand
<CalculatedVariable name="V300" value="cos(x + 1.4)"/>
<CalculatedVariable name="V300" value="cos (x + 1.4)"/>
Advanced documentation for quasar developers
Selection of expression parser
ExprTk It made an excellent overall impression. However, due to very intensive use of templates, the compilation time has been significant (i.e. its inclusion would triple(!!) the whole compilation time of a simple quasar server). That unfavourable property has made the quasar team to look for another solution.
ATMSP The initial code review has shown that the parser uses setjmp()/longjmp() which has been considered unfavourable for quasar servers.
muParser muParser demonstrated decent performance while it has all the features required by the Calculated Variables feature.
Overview of feature implementation
Classes rationale
ChangeNotifyingVariable - can emit notifications whenever the variable changed value. Applicable to any data type. Can be used with multiple notification receivers. Can be used for applications different than CalculatedVariables.
ParserVariable - stores current numeric variable value as a plain double type, and therefore can be coupled as a mu::Parser variable. (Sidenote: mu::Parser doesn’t know anything about OPC UA and without such arrangement it wouldn’t know how to access a double from UaVariant, neither to know whether the value is correct, etc.).
notifyingVariable - is the pointer to a ChangeNotifyingVariable which notifies this particular ParserVariable on change,
notifiedVariables - the list of all CalculatedVariables that use this particular ParserVariable in formulas.
CalculatedVariable - it’s the OPC UA variable defined by a formula. It’s a subclass of ChangeNotifyingVariable because its output can in turn be used as an input to another Calculated Variable (so it must be able to emit notifications on change).
Engine - puts all things together. It supplies methods for usage in Configuration module:
instantiateCalculatedVariable - called whenerver CalculatedVariable() entry is found in the config file,
registerVariableForCalculatedVariables - called whenever any cache-variable of suitable design properties (numeric and scalar) is inserted into the OPC UA address-space
Overview of information flow
All cache-variables instantiated by quasar Configuration module are of ChangeNotifyingVariable type or its subclasses.
When quasar Configuration determines that given cache-variable variable looks suitable to be used as a formula input (i.e. is numeric and it’s scalar), it would add a ChangeListener and a corresponding ParserVariable. The ChangeListener will (once potentially invoked in future) call setValue() on given ParserVariable.
When device logic or an OPC UA client writes to a suitable cache-variable, the setValue() of ParserVariable bound to the cache-variable will be called. It will store the new value and status in corresponding fields and then call update() on relevant (i.e. those which use given parser variable as an input) CalculatedVariable variables.
Synchronization, re-entrance, multi-threading
sampling threads which sample current values of cache-variables to which any client subscribes. Those threads are run by chosen OPC UA backend and their number is highly dependent on backend’s configuration (i.e. ServerConfig.xml) as well as possibly on number of connected clients and the set of data they subscribe to.
server’s OPC UA requests processing threads. Those threads are run by chosen OPC UA backend and similarly to sampling threads, their number depends on many factors. Those threads process e.g. Write service requests, so that an OPC UA client can write to given variable.
device logic (or other user threads). Those threads are instantiated by server developers and configured by end-users. They typically push data to the address-space.
possible calls to variable setters of the same variable coming from different threads. The worries here are the following:
there might be a clash in storage of value and status, as both of them are necessary to perform the calculation and (to author’s knowledge) such an assignment is never atomic by default. So a recalculation might take value stored by one thread and status from another, or on a 32-bit machine (since double is 64-bits) even take partially stored value.
it’s not entirely clear if calls to mu::Parser::Eval are re-entrant.
possible concurrent calls from different threads to variable setters of different variables which are used in the same formula. The worries here are the following:
the parser might attempt to use the value when it is being assigned to (and that is not atomic)
it’s not entirely clear if calls to mu::Parser::Eval are re-entrant.
Case 1: ignore CV4 (violet node and arrows)
1st one, which will provide exclusive access to setters of PV1, PV2 and PV3 (e.g. if any thread would enter setter of any of {PV1, PV2, PV3} all other threads willing to do the same would need to wait)
2nd one, which will provide exclusive access to setter of PV4
Case 2: CV4 is added
Supplementary notes on certain design decisions
Why constants from config entries propagate into ParserVariables rather than being declared using muParser::DefineConst?
Benchmarks
Aspect
quasar w/o Calculated Variables support |
quasar w Calculated Variables support (note: no Calc Vars declared!) |
Diff |
|
Build time of a simple, one-class server |
55s 56s 54s AVG = 55s |
1m15s 1m3s 59s AVG = 65s |
18% longer |
Build time of a complex server (here: SCA) |
4m15s |
4m25s |
3.9% longer |
Time to publish 100M random doubles via a cache-variable |
32793 ms 32892 ms 32623 ms AVG = 32768 ms |
32917 ms 33313 ms 33460 ms AVG = 33230 ms |
1.4% more overhead |
Valgrind info (publishing 1M random doubles) |
==6591== HEAP SUMMARY: ==6591== in use at exit: 27,753 bytes in 209 blocks ==6591== total heap usage: 1,031,151 allocs, 1,030,942 frees, 72,543,037 bytes allocated |
==5861== HEAP SUMMARY: ==5861== in use at exit: 28,458 bytes in 213 blocks ==5861== total heap usage: 1,032,466 allocs, 1,032,253 frees, 75,191,342 bytes allocated |
0.1% more allocs note “bytes allocated” has no relation to the actual size of RSS memory of a running process! |