In many cases the expressions specified in the configuration file are
evaluated right after the parsing has finished. A value is calculated
from the right hand side and assigned to the variable on the left hand
side of the assignment. In some cases it is desirable to delay the
evaluation of an expression until later. This is the case when the
values on the right hand side are not known straight away or if the
expression should be evaluated for a number of different values of a
variable appearing on the right hand side. This is often the case when
specifying a quantity for different locations in space or at different
times. Imagine, as an example, you want to define the value of a
boundary condition of the simulation as a function of time. The boundary
condition must be evaluated at each time step of the simulation. This
means that the expression will contain an independent variable that
specifies the point in time at which the boundary condition is
calculated. Let’s call this independent variable t
. We can add an
independent variable to the The Schnek parser can be instructed to treat
a variable as read only. This means that the user will not be able to
assign a value to that variable in the configuration file. Instead, the
variable can be used on the right hand side of expressions, without
first being initialised. A read only variable can be added to the
BlockParameters
simply by specifying the readonly
attribute. The
following example shows how this is done.
class SimulationBlock : public Block {
private:
double t;
pParameter paramT;
double value;
pParameter paramValue;
protected:
void initParameters(BlockParameters ¶meters) {
paramT = parameters.addParameter("t", &t, BlockParameters::readonly);
paramValue = parameters.addParameter("value", &value);
}
};
In this piece of code t
is added as read only variable to
parameters
in the initParameters
method. This means that t
can be used in expressions in the configuration file straight away. When
using independent variables in the input deck you should keep the object
returned by the addParameter
method. This object is a smart pointer
to an object of type Parameter
. This object stores information about
the mathematical expression and about any dependencies of a parameter.
The Parameter
objects are needed to define independent and dependent
variables in a deferred evaluation. Reading the configuration file takes
the usual form.
BlockClasses blocks;
blocks.registerBlock("sim").setClass<SimulationBlock>();
std::ifstream in("example_setup_evaluate.setup");
Parser P("my_simulation", "sim", blocks);
registerCMath(P.getFunctionRegistry());
pBlock application = P.parse(in);
SimulationBlock &mysim = dynamic_cast<SimulationBlock&>(*application);
mysim.evaluateParameters();
We can now write a method that iterates through values of t
and
evaluates the expression supplied in the configuration file for each
value of t
. The following function inside the SimulationBlock
class does exactly this.
class SimulationBlock : public Block {
...
public:
void printValues() {
pBlockVariables blockVars = getVariables();
pDependencyMap depMap(new DependencyMap(blockVars));
DependencyUpdater updater(depMap);
updater.addIndependent(paramT);
updater.addDependent(paramValue);
for (int i=0; i<=20; ++i) {
t = 0.5*i;
updater.update();
std::cout << t << " " << value << std::endl;
}
}
};
Let’s look at this code line by line.
pBlockVariables blockVars = getVariables();
The BlockVariables
class holds all the information about the
variables, any expressions that they depend on and that haven’t yet been
evaluated. The Block::getVariables
method returns a shared pointer
to the global BlockVariables
object.
pDependencyMap depMap(new DependencyMap(blockVars));
A DependencyMap
analyses the expressions stored in the
BlockVariables
object. Internally it will create a data structure
that stores the dependencies of each variable. We create a new shared
pointer to a DependencyMap
by passing the blockVars
pointer.
DependencyUpdater updater(depMap);
Finally the DependencyUpdater
class can be used to create an ordered
sequence of evaluations of expressions. It will use the dependency map
to determine which expressions have to be evaluated first and which
expressions don’t have to be evaluated at all in order to evaluate a
given set of dependent variables.
updater.addIndependent(paramT);
updater.addDependent(paramValue);
For the DependencyUpdater
to do its job it needs to know which
variables are independent and which are dependent. This information is
supplied by calling the addIndependent
and the addDependent
methods. Here we pass the Parameter
objects which we obtained when
adding the parameters to the BlockParameters
object.
for (int i=0; i<=20; ++i) {
t = 0.5*i;
updater.update();
std::cout << t << " " << value << std::endl;
}
Once the information has been set up we can change the value of t
and then call update
on the DependencyUpdater
. This will
evaluete all the expressions needed to calculate the dependent variables
and store the result in the memory locations of these dependants. This
means that after a call to update
the value of the value
variable will have been updated. For example, we can write the following
expression in the example_setup_evaluate.setup
file.
value = exp(-t/5);
This will create the following output.
0 1
0.5 0.904837
1 0.818731
1.5 0.740818
2 0.67032
2.5 0.606531
3 0.548812
3.5 0.496585
4 0.449329
4.5 0.40657
5 0.367879
5.5 0.332871
6 0.301194
6.5 0.272532
7 0.246597
7.5 0.22313
8 0.201897
8.5 0.182684
9 0.165299
9.5 0.149569
10 0.135335
In this example only one expression is being evaluated. But the updater does not care how many steps need to be taken to arrive at the result. Consider the following input file.
float decay = exp(-t/5);
float phase = 2*t;
float oscillation = sin(phase);
value = oscillation*decay;
The DependencyUpdater
makes sure that decay
and phase
are
evaluated first. oscillation
depends on phase
and so it will be
evaluated only after phase
has been updated. Finally value
is
calculated from the updated values of oscillation
and decay
. The
result is as follows.
0 0
0.5 0.761394
1 0.74447
1.5 0.104544
2 -0.5073
2.5 -0.581617
3 -0.153346
3.5 0.32625
4 0.444547
4.5 0.167555
5 -0.200134
5.5 -0.332868
6 -0.161613
6.5 0.114509
7 0.244281
7.5 0.145099
8 -0.0581267
8.5 -0.175631
9 -0.124137
9.5 0.0224169
10 0.123554
The code for this example can be downloaded here. The setup file can be found under example_setup_evaluate.setup.