Filling Fields
In the previous section it was shown how expressions from the configuration file could contain independent, read only parameters and which could be evaluated at a later point, after the configuration file has been processed. This can be used to evaluate the same expression for any number of different values of the independent parameters. A useful and common application for this feature is to fill numerical grids with position dependent values using an expression that contains the space coordinates x
, y
, and z
depending, of course, on the dimensionality of the simulation. Because this is a common use case, Schnek provides a mechanism for filling numerical grids. To make use of this function, one needs to define a Field
object. The Field
class extends the Grid
class with information about the physical dimensions of the numerical grid and the grid stagger. More information about the Field
class can be found here.
In the following example a Field
will be initialised from a configuration file. This includes defining the number of grid points on the grid as well as the physical dimensions of the domain that the grid represents.
class SimulationBlock : public Block { private: Array<schnek::pParameter, 2> x_parameters; Array<int, 2> N; Array<double, 2> L; Array<double, 2> x; double fieldInit; pParameter paramField; pParametersGroup spaceVars; protected: void initParameters(BlockParameters ¶meters) { parameters.addArrayParameter("N", N); parameters.addArrayParameter("L", L); x_parameters = parameters.addArrayParameter("", x, BlockParameters::readonly); paramField = parameters.addParameter("F", &fieldInit, 0.0); spaceVars = pParametersGroup(new ParametersGroup()); spaceVars->addArray(x_parameters); } };
The array N
will be used to define the number of grid points. By using addArrayParameter()
to register the two dimensional array, the setup file will recognise the parameters Nx
and Ny
. Similarly, the array L
represents the physical dimensions of the grid. The setup file is instructed to recognise the parameters Lx
and Ly
.
In order to register the space coordinates to be used in expressions in the configuration file the array x
is registered with the BlockParameters::readonly
flag. Note that the name of the array is an empty string. This means that the full names in the configuration file will simply be x
and y
. The return value of the addArrayParameter
call is stored in an array of Parameter
objects, called x_parameters
. These parameters are stored in a ParametersGroup
called spaceVars
. This parameter group will later be needed to set up the independent parameters when evaluating the expressions in the configuration file.
Finally, a helper variable is needed which will take the place of the field that should be filled. In the example above this helper variable is called fieldInit
and the name in the configuration file is simple F
. The Parameter
object that is generated is stored in the paramField
variable.
In a separate method which will be called after initialisation a Field
is created and filled with values.
void fillValues() { // create Field Range<double, 2> range(Array<double, 2>(0,0), L); Array<bool,2> stagger(false, true); Field<double, 2> dataField(N, range, stagger, 1); // set up dependency map and updater pBlockVariables blockVars = getVariables(); pDependencyMap depMap(new DependencyMap(blockVars)); DependencyUpdater updater(depMap); updater.addIndependentArray(x_parameters); // fill field with values fill_field(dataField, x, fieldInit, updater, paramField); }
The first three lines of code in the fillValues
method simply set up a field called dataField
. See here for more information about Field
s. Note that we are using the L
and N
arrays to specify the physical and numerical dimensions of the grid.
The next block of code sets up the dependency map and the updater. This follows exactly the same pattern as explained in the section about evaluating expressions. Note however that no dependent variables are added to the updater. The independent variables are added in the form of an array using addIndependentArray
.
The last line of code calls a utility function called fill_field
. This function takes the Field
object that should be filled, the independent variable array x
, the dependent helper variable fieldInit
, the updater
that was just created, and the parameter paramField
that was generated when registering the helper variable. After a call to fill_field
, dataField
will be filled with values calculated from the expression in the configuration file.
A simple double loop can write out the values of dataField
.
for (int i=0; i<=N[0]; ++i) { for (int j=0; j<=N[1]; ++j) { double px = dataField.indexToPosition(0,i); double py = dataField.indexToPosition(1,j); std::cout << px << " " << py << " " << dataField(i,j) << std::endl; } std::cout << std::endl; }
Note here that indexToPosition
is used to calculate the physical coordinates from the grid indices.
Now we are ready to write out configuration file.
Nx = 50; Ny = 50; Lx = 20; Ly = 20; float radius = sqrt(x^2 + y^2); float decay = exp(-radius/10); F = decay*sin(radius);
The numbers Nx
and Ny
define the number of grid points, Lx
and Ly
specify the physical extent of the simulation domain. Two helper variables are defined radius
and decay
. The value of radius
depends on x
and y
. These are the read-only independent variables that specify the position of the grid points. decay
indirectly depends on x
and y
through radius
. Finally the field variable F
is specified using radius
and decay
.
The dependency updater will resolve the dependency of F
on the position variables x
and y
. The fill_field
function will iterate over all grid points of the field and set the value according to the formula specified in the configuration file. The resulting field data is shown on the right.
The code for this example can be found here and the setup file is located here.