containers

Fields

The Field classes are the central elements for implementing a platform portable CFD framework. Fields are able to perform basic algebraic operations such as addition or subtraction of two fields, or scalar operations like the multiplication of a field with a scalar. The Field classes store the data in a platform-independent way and the executor, which is used to dispatch the operations to the correct device. The Field classes are implemented in the Field.hpp header file and mainly store a pointer to the data, the size of the data, and the executor.

template<typename ValueType>
class Field

A class to contain the data and executors for a field and define some basic operations.

Private Members

size_t size_

Size of the field.

ValueType *data_

Pointer to the field data.

const Executor exec_

Executor associated with the field. (CPU, GPU, openMP, etc.)

To run a function on the GPU, the data and function need to be trivially copyable. This is not the case with the existing OpenFOAM Field class , and it can be viewed as a wrapper around the data. To solve this issue, the NeoFOAM field class has a public member function called field() that returns a span that can be used to access the data on the CPU and GPU.

template<typename ValueType>
class Field

A class to contain the data and executors for a field and define some basic operations.

The following example shows how to use the field function to access the data of a field and set all the values to 1.0. However, the for loop is only executed on the single CPU core and not on the GPU.

NeoFOAM::CPUExecutor cpuExec {};
NeoFOAM::Field<NeoFOAM::scalar> a(cpuExec, size);
std::span<double> sA = a.field();
// for loop
for (int i = 0; i < sA.size(); i++)
{
     sA[i] = 1.0;
}

To run the for loop on the GPU is a bit more complicated and is based on the Kokkos library that simplifies the process and support parallelization strategies for different GPU vendors and OpenMP support for CPU targets. The following example shows how to set all the values of a field to 1.0 on the GPU.

NeoFOAM::GPUExecutor gpuExec {};
NeoFOAM::Field<NeoFOAM::scalar> b(gpuExec, size);
std::span<double> sB = b.field();
Kokkos::parallel_for(
     Kokkos::RangePolicy<gpuExec::exec>(0, sB.size()),
     KOKKOS_LAMBDA(const int i) { sB[i] = 1.0; }
);

Kokkos requires the knowledge of where to run the code and the range of the loop. The range is defined by the size of the data and the executor. The KOKKOS_LAMBDA is required to mark the function so it is also compiled for the GPU. This approach however is not very user-friendly and requires the knowledge of the Kokkos library. To simplify the process, the Field class stores the executor and the field independent of the device can be set to 1.0 with the following code.

NeoFOAM::Field<NeoFOAM::scalar> c(gpuExec, size);
NeoFOAM::fill(b, 10.0);

The fill function uses the std::visit function to call the correct function based on the executor as described in the previous section.

Operation op{};
std::visit([&](const auto& exec)
           { op(exec); },
           exec);

Note

TODO organize the FieldOperation so the can be easily shown here

FieldGraph

The Field can now be used to compose more complex data structures. To solve PDE’s, information about the neighbors is required. This is usually done with the following approach:

int nCells = 3;
std::vector<std::vector<int> > cellToCellStencil(nCells);

cellToCellStencil.push_back({1, 2, 3});
cellToCellStencil.push_back({4, 5, 6});
cellToCellStencil.push_back({7, 8, 9});

for (for auto& cell : cellToCellStencil)
{
     for (auto& neibour : cell)
     {
          std::cout << neibour << " ";
     }
     std::cout << std::endl;
}

Now we can loop over each cell and access the neighbors with a nested for loop. However, this approach is not suited for GPUs. Instead of the vector of vector approach, the neighbor hood is stored with two fields (described with std::vector to simplify the example):

int nCells = 3;
std::vector<int> value = {1, 2, 3, 4, 5, 6, 7, 8, 9};
std::vector<int> offset_ = {0, 3, 6, 9};

for (int i = 0; i < nCells ; i++)
{
     int start = offset_[i];
     int end = offset_[i+1];
     for (int j = start; j < end; j++)
     {
          int neibour = value[j];
          std::cout << neibour << " ";
     }
     std::cout << std::endl;
}

The same approach is used in the FieldGraph class (we had a better name for this but i forgot). That implements the above approach using the Field class.

Note

TODO implement the FieldGraph class

BoundaryFields

The BoundaryFields class is used to store all the boundary conditions of a DomainField. The BoundaryFields class is implemented in the boundaryFields.hpp header file and stores the boundary conditions in a generic container that can be used to represent different boundary conditions: Dirichlet, Neumann, and Robin.

template<typename T>
class BoundaryFields

Represents the boundary fields for a computational domain.

The BoundaryFields class stores the boundary conditions and related information for a computational domain. It provides access to the computed values, reference values, value fractions, reference gradients, boundary types, offsets, and the number of boundaries and boundary faces.

Template Parameters:

ValueType – The type of the underlying field values

Private Members

NeoFOAM::Field<T> value_

The Field storing the computed values from the boundary condition.

NeoFOAM::Field<T> refValue_

The Field storing the Dirichlet boundary values.

NeoFOAM::Field<scalar> valueFraction_

The Field storing the fraction of the boundary value.

NeoFOAM::Field<T> refGrad_

The Field storing the Neumann boundary values.

NeoFOAM::Field<int> boundaryTypes_

The Field storing the boundary types.

NeoFOAM::Field<localIdx> offset_

The Field storing the offsets of each boundary.

int nBoundaries_

The number of boundaries.

int nBoundaryFaces_

The number of boundary faces.

DomainField

The DomainField is a data class which stores both the InternalField and BoundaryFields in a single container. Together, it fully defines a discrete topologically closed physical field, for a given mesh.

template<typename ValueType>
class DomainField

Represents the domain fields for a computational domain.

The DomainField class stores the internal fields and boundary information for a computational domain. It provides access to the computed values, reference values, value fractions, reference gradients, boundary types, offsets, and the number of boundaries and boundary faces.

Template Parameters:

ValueType – The type of the underlying field values

Public Functions

inline DomainField(const Executor &exec)
inline DomainField(const Executor &exec, int nCells, int nBoundaryFaces, int nBoundaries)
inline DomainField(const Executor &exec, const UnstructuredMesh &mesh)
inline DomainField(const ::NeoFOAM::DomainField<ValueType> &rhs)
inline DomainField(DomainField<ValueType> &&rhs)
inline DomainField<ValueType> &operator=(const ::NeoFOAM::DomainField<ValueType> &rhs)
inline DomainField<ValueType> &operator=(DomainField<ValueType> &&rhs)
inline const Field<ValueType> &internalField() const
inline Field<ValueType> &internalField()
inline const BoundaryFields<ValueType> &boundaryField() const
inline BoundaryFields<ValueType> &boundaryField()
inline const Executor &exec() const