Data Model

FleCSI provides a data model that integrates with the task and kernel abstractions to provide easy registration and access to various data types with automatic dependency tracking.

Example 1: Global data

Global fields are used to store global variables/objects that can be accessed by any task. For the common case of only one value for each field, it is natural to use the data::single layout.

template<typename T>
using single = field<T, data::single>;
const single<double>::definition<global> gfield;

To create a topology instance, declare a topology slot and then use allocate with an argument appropriate to the topology called a coloring. In general, a coloring describes the structure of a topology and its distribution among colors. The global topology is a special case that does not actually use colors; its “coloring” is simply a count of values for each field. Writing to a global field requires a single task launch.

void
init(double v, single<double>::accessor<wo> gv) {
  gv = v;
}

void
print(single<double>::accessor<ro> gv) {
  flog(trace) << "global value: " << gv << std::endl;
}

void
advance(control_policy &) {
  topo::global::slot gtopo;
  gtopo.allocate(1);
  const auto v = gfield(gtopo);
  execute<init>(42.0, v);
  execute<print>(v);
} // advance()

Example 2: Index data

A field on an index topology stores one value for each color.

using namespace flecsi;

template<typename T>
using single = field<T, data::single>;
const single<std::size_t>::definition<topo::index> ifield;

void
init(single<std::size_t>::accessor<wo> iv) {
  flog(trace) << "initializing value on color " << color() << " of " << colors()
              << std::endl;
  iv = color();
}

void
print(single<std::size_t>::accessor<ro> iv) {
  flog(trace) << "index value: " << iv << " (color " << color() << " of "
              << colors() << ")" << std::endl;
}

void
advance(control_policy &) {
  topo::index::slot custom_topology;
  custom_topology.allocate(4);

  execute<init>(ifield(custom_topology));
  execute<print>(ifield(custom_topology));
} // advance()

Example 3: Dense data

A dense field is a field defined on a dense topology index space. In this example we allocate a pressure field on the cells index space of the canonical topology.

const field<double>::definition<canon, canon::cells> pressure;

One can access the field inside of the FleCSI task by passing topology and field accessors with access permissions (wo/rw/ro). The canonical topology is a very simple specialization of the unstructured core topology. It illustrates the use of the mpi_coloring type, which applies a specialization-defined rule for specifying a coloring. Here, a file is the source of the mesh (for purposes of illustration). The resulting coloring is used to initialize two meshes canonical and cp, and the copy task operates on both of them at once using a low-level accessor. The init and print tasks, by contrast, use a topology accessor as a parameter that provides access to the structure of the mesh via the entities function.

void
init(canon::accessor<ro> t, field<double>::accessor<wo> p) {
  std::size_t off{0};
  for(const auto c : t.cells()) {
    p[c] = (off++) * 2.0;
  } // for
} // init

void
copy(field<double>::accessor<ro> src, field<double>::accessor<wo> dest) {
  auto s = src.span();
  std::copy(s.begin(), s.end(), dest.span().begin());
}

void
print(canon::accessor<ro> t, field<double>::accessor<ro> p) {
  std::size_t off{0};
  for(auto c : t.cells()) {
    flog(info) << "cell " << off++ << " has pressure " << p[c] << std::endl;
  } // for
} // print

void
advance(control_policy &) {
  canon::slot canonical, cp;
  canon::mpi_coloring c("test.txt");
  canonical.allocate(c);
  cp.allocate(c);

  auto pf = pressure(canonical), pf2 = pressure(cp);

  execute<init>(canonical, pf);
  execute<copy>(pf, pf2);
  execute<print>(cp, pf2);
} // advance()

Example 4: Ragged data

A ragged field stores a variable amount of data at each index point. It is defined in much the same way as a dense field, but using it involves additional steps.

using ints = field<int, data::ragged>;
const ints::definition<canon, canon::cells> rag;

Because the total storage required for a ragged field is not determined by the size of the index space, an amount of memory to use for elements must be specified. The field reference for a ragged field has a special interface for managing it. Here we use a simple helper task (called allocate) with that interface to specify a fixed, known total, although typically a heuristic overallocation is required:

// This special field type is predefined for this purpose.
void
allocate(topo::resize::Field::accessor<wo> a) {
  a = 6;
}

After executing a task to store the sizes, they must be applied with resize:

  const auto f = rag(mesh);

  auto & elem = f.get_elements();
  execute<allocate>(elem.sizes());
  elem.resize();

Initializing the field, or changing the number of values stored at any point later, requires a mutator. The interface is closely modeled on std::vector:

// Use wo to initialize any field.
void
init(ints::mutator<wo> m) {
  int i = 0;
  for(auto r : m) {
    r.resize(i, i);
    ++i;
  }
}

Using the field is much like a dense field, with a span at each index point:

// Accessors can modify but not create or destroy values.
int
total(ints::accessor<ro> a) {
  int ret = 0;
  for(auto r : a)
    for(auto i : r)
      ret += i;
  return ret;
}

Tasks using either of these are launched with ordinary field references:

  execute<init>(f);
  if(reduce<total, exec::fold::sum>(f).get() != 14)
    throw control_policy::exception{1};