A quasar-based OPC-UA server is based on a model of a system it
is made for. This model is called a design. The model explains (in
terms of quasar classes, variables etc.) what sources and sinks of
information the system can publish or digest.
Often it is practical to add supplementary sources of information
- like Calculated Variables - on top of what the model already
provides. The reasons for doing this are often the following:
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 |
State |
Supports all quasar numerical types |
Yes |
Formulas with N inputs |
Yes |
Good/Bad/WaitingForInitialData support |
Yes |
Separate formula for status evaluation |
Yes |
Support for delegated cache-variables |
Yes |
Tracing in separate LogIt component |
Yes |
open62541 compatibility |
Yes |
Initial value support |
Yes |
Evaluation as boolean |
Yes |
Atomic passing of value and status, thread
safety |
Yes |
Optimizing out variables not used in any
expression |
Yes |
Formula templates |
Yes |
A summary of functions, operators and constants which mu::Parser
supports is pasted here for reference.
Built-in functions
Name | Argc. | Explanation |
sin |
1 | sine function |
cos |
1 | cosine function |
tan |
1 | tangens function |
asin |
1 | arcus sine function |
acos |
1 | arcus cosine function |
atan |
1 | arcus tangens function |
sinh |
1 | hyperbolic sine function |
cosh |
1 | hyperbolic cosine |
tanh |
1 | hyperbolic tangens function |
asinh |
1 | hyperbolic arcus sine function |
acosh |
1 | hyperbolic arcus tangens function |
atanh |
1 | hyperbolic arcur tangens function |
log2 |
1 | logarithm to the base 2 |
log10 |
1 | logarithm to the base 10 |
log |
1 | logarithm to base e (2.71828...) |
ln |
1 | logarithm to base e (2.71828...) |
exp |
1 | e raised to the power of x |
sqrt |
1 | square root of a value |
sign |
1 | sign function -1 if x<0; 1 if x>0 |
rint |
1 | round to nearest integer |
abs |
1 | absolute value |
min |
var. | min of all arguments |
max |
var. | max of all arguments |
sum |
var. | sum of all arguments |
avg |
var. | mean value of all arguments |
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 |
Common mathematical constants
_pi, _e
The XML element type is called CalculatedVariable and it has the
following attributes:
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 BadWaitingForInitialData 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 |
The XML element CalculatedVariable can be attached under any
quasar object declaration as well as on global scope.
It is often practical to perform some sort of elaboration of
configured formulas before they are given to be compiled by the
formula parser. Such elaboration steps are achieved by placing
dollar-sign operators and functions in the formulas. For all
examples below, the following quasar design diagram is used:
<TestClass name="tc">
<CalculatedVariable name="test_var_multiplied" value="$thisObjectAddress.testVariable * 1000" />
</TestClass>
<TestClass name="tc">
<TestSubClass name="tsc">
<CalculatedVariable name="test_var_multiplied" value="$parentObjectAddress(numLevelsUp=1).testVariable * 1000" />
</TestSubClass>
</TestClass>
$applyGenericFormula is used in the context of generalized
function templates and documented there.
Technically, the job done by quasar for applying the formula at
the point of use boils down to pasting the formula in place of the
meta-function. In the future, extending this operation by optional
arguments, might be considered.
An example of the generalized formula template from a real system
(CERN - ATLAS DCS - New Small Wheel project, courtesy of P.
Tzanis) is given. The generalized formula is put at the top of the
configuration file:
<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>
<StandardMetaData>
<Log>
<ComponentLogLevels>
<ComponentLogLevel componentName="CalcVars" logLevel="TRC" />
</ComponentLogLevels>
</Log>
</StandardMetaData>
Users of quasar-based servers sometimes choose to name their
quasar objects (i.e. the name attribute of XML elements in
the configuration files) with names containing dashes or slashes.
This is legit in the quasar world. However, it poses some problems
if CalculatedVariables inputs connect to such named objects (i.e.
its variables).
Imagine the following config file:
<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"/>
Imagine that a device can measure resistance of a connected
resistor. If the resistor happens to be a NTC temperature probe,
then one can find the temperature in function of resistance:
T = T0 * B / (T0 * ln(R/R0) + B)
where T0 is typically 298.15K (that is, +25 deg C in Kelvin
degrees), B is the so called B constant of a NTC probe (often
3977K) and R0 is the resistance at T0.
The variable in the example is R, and that is the cache-variable
that gets updated by your OPC-UA server device logic.
Let's assume that the OPC-UA address of the variable is
NTC1.resistance
Therefore, anywhere below NTC1 declaration in your config file,
you can instantiate a CalculatedVariable that will recompute the
measured resistance into temperature expressed in Kelvin degrees.
In the example below we also add some CalculatedVariables to hold
B, T0 and R0 constants.
<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)" />
We also add two Calculated Variables that will recomputer Kelvins
into Celsius degrees and Fahrenheit degrees:
<CalculatedVariable name="temperatureC" value="temperatureK-273.15"/>
<CalculatedVariable name="temperatureF" value="temperatureC*1.8+32"/>
In addition, we can add a boolean variable which subjectively
indicates whether it's warm enough. It's an example of usage of
logical operators as well as isBoolean attribute:
<CalculatedVariable name="isWarmEnough" value="temperatureC > 20" isBoolean="true" />
This example shall illustrate that a CalculatedVariable can be
attached (i.e. its inputs might be) different quasar entities such
as: cache-variables, free-variables, other calculated-variables
and even config-entries (if they are of some numeric data-type).
<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" />
As can be seen, the last calculated variable is a function
computed of values of many different quasar entities which all
corresponds to address-space variables.
Note that it is illegal (i.e. will be refused at configuration
loading) to put any whitespace between unary operation (function?)
and the operands, e.g. this is legal:
<CalculatedVariable name="V300" value="cos(x + 1.4)"/>
<CalculatedVariable name="V300" value="cos (x + 1.4)"/>
There exist many open-source parsers potentially suitable for the
feature. At the time of writing, a good overview was present at https://github.com/ArashPartow/math-parser-benchmark-project
.
The author has evaluated three parsers from the list:
An UML class diagram is presented below.
The CalculatedVariables module is closely tied to the
AddressSpace of a quasar-based server.
For instance, the recalculation of an associated calculated
variable is done within the call to a setter of a variable that it
depends on.
It must be emphasized that AddressSpace is brutally
multi-threaded. At the same time, the following thread families
would be doing work on AddressSpace objects:
Having analyzed the problem and trying to propose a guaranteed
dead-lock free solution, the author proposes to form disjoint
subgraphs of the calculation graph and synchronize per each
subgraph.
Let's look at an example for which the calculation graph is like
in the picture below.
PV5 would not get a synchronizer because it's output is not used
by anything; in fact PV5 would be optimized out after the
configuration process is finished.
Some benchmarks have been performed. The base has been pre-1.3.1
release of quasar. The benchmarks have been performed with UA-SDK
1.5.5 as the OPC-UA backend.
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! |
The muParser is distributed along quasar in an amalgamated way.
In quasar repo, you can go to:
CalculatedVariables/ext_components/
where you will find a script "clone_and_amalgamate_muparser.sh"
which will perform cloning of muParser and then amalgamation.
Note that the particular version of muParser as well as
accompanying amalgamation utility is fixed so there is no reason
to run the script without changing the version.