An introductory example¶
As an introduction to the core concepts in Sidre and how they work, here is an example where we construct the Sidre data hierarchy structure shown in the following figure:
The diagram represents a datastore container, holding all Sidre objects and
provides the main interface to access those objects.
Rounded rectangles represent Sidre Group
objects. Each Group
has a
name and one parent group, except for the root group (i.e. “/”), which
has no parent. A datastore provides one root group (i.e. “/”) that is
created when a DataStore
object is constructed; thus, an application does
not create the root. A Group
may have zero or more child groups
(indicated by an arrow from the parent to each child). Each group also owns
zero or more View
objects, which are shown as rectangles. An arrow points
from a group to each view it owns.
A Sidre View
object has a name and, typically, some data associated with it.
This example shows various types of data that can be described by a view,
including scalars, strings, and data in arrays (both externally allocated
and owned by Sidre Buffer
objects). Each array view has a data pointer
and describes data in terms of data type, number of elements, offset, and
stride. Data pointers held by array view are shown as dashed arrows.
A DataStore
also contains a collection of Buffer
objects, shown as
segmented rectangles.
A DataStore
contains a list of Attribute
objects. Each Attribute
is outlined with a hexagon and defines a metadata label and a default value
associated with that label. In the example, the datastore has “vis” (with
default value 0) and “restart” (with default value 1) attributes.
Default attributes apply to all views unless explicitly set for individual
views. In the example, the “temp” and “rho” views have the “vis” attribute
set to 1.
Various aspects of Sidre usage are illustrated in the C++ code shown next. Sidre provides full C and Fortran APIs that can also be used to generate the same result.
First, we create a DataStore
object, define some Attribute
objects
along with their default values, and add some child Group
objects to the
root Group
.
// Create Sidre datastore object and get root group
auto ds = std::unique_ptr<sidre::DataStore> {new sidre::DataStore()};
sidre::Group* root = ds->getRoot();
// Create two attributes
ds->createAttributeScalar("vis", 0);
ds->createAttributeScalar("restart", 1);
// Create group children of root group
sidre::Group* state = root->createGroup("state");
sidre::Group* nodes = root->createGroup("nodes");
sidre::Group* fields = root->createGroup("fields");
The createViewScalar()
method lets an application store scalar
values in a view owned by the group object the method is called on.
// Populate "state" group
state->createViewScalar("cycle", 25);
state->createViewScalar("time", 1.2562e-2);
state->createViewString("name", "sample_20171206_a");
This example stores (x, y, z) node position data in one array.
The array is managed by a Buffer
object and three View
objects
point into it. C++ Sidre operations that create a Buffer
, a Group
, and
a View
, as shown in the following code, return a pointer to the object
that is created. This allows chaining operations. (Chaining is supported in
the C++ API but not in C or Fortran.)
int N = 16;
int nodecount = N * N * N;
int eltcount = (N - 1) * (N - 1) * (N - 1);
// Populate "nodes" group
//
// "x", "y", and "z" are three views into a shared Sidre buffer object that
// holds 3 * nodecount doubles. These views might describe the location of
// each node in a 16 x 16 x 16 hexahedron mesh. Each view is described by
// number of elements, offset, and stride into that data.
sidre::Buffer* buff =
ds->createBuffer(sidre::DOUBLE_ID, 3 * nodecount)->allocate();
nodes->createView("x", buff)->apply(sidre::DOUBLE_ID, nodecount, 0, 3);
nodes->createView("y", buff)->apply(sidre::DOUBLE_ID, nodecount, 1, 3);
nodes->createView("z", buff)->apply(sidre::DOUBLE_ID, nodecount, 2, 3);
The last two integral arguments to the createView()
method specify the
offset from the beginning of the array and the stride of the data. Thus,
the x, y, z values for each position are stored contiguously with the
x values, y values, and z values each offset from each other by a stride of
three in the array.
The next snippet creates two views (“temp” and “rho”) and allocates
each of their data as an array of type double with length ‘eltcount’. Then,
it sets an attribute (“vis”) on each view with a value of 1. Lastly, it
creates a group (“ext”) that has a view holding an external pointer
(“region”). The apply()
method describes the view data as an array of
integer type and length ‘eltcount’. Note that it is the responsibility of
the caller to ensure that the allocation to which the “region” pointer
references is adequate to contain that data description.
// Populate "fields" group
//
// "temp" is a view into a buffer that is not shared with another View.
// In this case, the data Buffer is allocated directly through the View
// object. Likewise with "rho." Both Views have the default offset (0)
// and stride (1). These Views could point to data associated with
// each of the 15 x 15 x 15 hexahedron elements defined by the nodes above.
sidre::View* temp =
fields->createViewAndAllocate("temp", sidre::DOUBLE_ID, eltcount);
sidre::View* rho =
fields->createViewAndAllocate("rho", sidre::DOUBLE_ID, eltcount);
// Explicitly set values for the "vis" Attribute on the "temp" and "rho"
// buffers.
temp->setAttributeScalar("vis", 1);
rho->setAttributeScalar("vis", 1);
// The "fields" Group also contains a child Group "ext" which holds a pointer
// to an externally owned integer array. Although Sidre does not own the
// data, the data can still be described to Sidre.
sidre::Group* ext = fields->createGroup("ext");
// int * region has been passed in as a function argument. As with "temp"
// and "rho", view "region" has default offset and stride.
ext->createView("region", region)->apply(sidre::INT_ID, eltcount);
The next code example shows various methods to retrieve a group object and data from a view in the group hierarchy.
// Retrieve Group pointers
sidre::Group* root = ds->getRoot();
sidre::Group* state = root->getGroup("state");
sidre::Group* nodes = root->getGroup("nodes");
sidre::Group* fields = root->getGroup("fields");
// Accessing a Group that is not there gives a null pointer
// Requesting a nonexistent View also gives a null pointer
sidre::Group* goofy = root->getGroup("goofy");
if(goofy == nullptr)
{
std::cout << "no such group: goofy" << std::endl;
}
else
{
std::cout << "Something is very wrong!" << std::endl;
}
// Access items in "state" group
int cycle = state->getView("cycle")->getScalar();
double time = state->getView("time")->getScalar();
const char* name = state->getView("name")->getString();
// Access some items in "nodes" and "fields" groups
double* y = nodes->getView("y")->getArray();
int ystride = nodes->getView("y")->getStride();
double* temp = fields->getView("temp")->getArray();
int* region = fields->getView("ext/region")->getArray();
// Nudge the 3rd node, adjust temp and region of the 3rd element
y[2 * ystride] += 0.0032;
temp[2] *= 1.0021;
region[2] = 6;
In the last section, the code accesses the arrays associated with the views
“y”, “temp”, and “region”. While “temp” and “region” have the default offset
(0) and stride (1), “y” has offset 1 and stride 3 (as described earlier).
The pointer returned by View::getPointer()
always points to the first
data element described by the view (the view takes care of the offset),
but use of a stride other than 1 must be done by the code itself.
Unix-style path syntax using the slash (“/”) delimiter is supported for
traversing Sidre group and view hierarchies and accessing their
contents. However, ‘..’ and ‘.’ syntax (up-directory and current directory)
is not supported. This usage is shown in the last call to getView()
in
the code example above. The method call retrieves the view named “region”
in the group named “ext” that is a child of the “fields” group. Character
sequences before the first slash and between two consecutive slashes are
assumed to be group names (describing parent-child relationships). For
this method, and others dealing with view objects, the sequence following
the last slash is assumed to be the name of a view. Similar path syntax
can be used to retrieve a group, create a group and a view, a
nd so forth.