Operator¶
Overview¶
An Operator is an essential building block to represent expressions in NeoNs dsl. Here is an example of combining multiple Operator to an expression:
dsl::imp::ddt(U) + dsl::imp::div(phi, U) - dsl::imp::laplacian(nu, U)
NeoN’s implements explicit (exp) and implicit (imp) of operator in the dsl namespace. Note that, temporal operators are currently, part of the implicit (imp) namespace eventhough an explicit time integration can be selected at runtime.
An Operator can be instantiated on fields of different value types, e.g. VolumeField<scalar> or VolumeField<Vec3>, thus Operator are templated over the field ValueType. Since the purpose of an Operator is to encode an expression, an Operator merely stores constant references to the field(s) it operates on and provides a connection to kernel which are executed at runtime when an expression is solved and operators are evaluated. Additionally, since expressions are evaluated lazily, a simple instantiation of an operator does not execute any computational kernel.
Implementation¶
Operators are implemented via multiple functions and classes:
dsl/implicit.hpp and dsl/explicit.hpp implement free standing functions such as dsl::imp::div(phi, U) or dsl::exp::grad(p) or which act as factories to construct the operator instance.
The expression stores operators separately either as temporal or spatial operator. Thus, an implementation of both classes can be found in spatialOperator.hpp and temporalOperator.hpp.
The Operator holder classes spatialOperator and temporalOperator implement an interface which expression trigger evaluation of the operator via the explicitOperation and implicitOperation member function. These function forward the operation call to the concrete model.
The concrete model here is constructed at run time via NeoN run time factory mechanism. Thus a base class for the registry is implemented for example in divOperator.hpp for which concrete implementation an its kernels are implemented in gaussGreenDiv.hpp
Details¶
An Operator is either explicit, implicit or temporal, and can be scalable by an additional coefficient, for example a scalar value or a further field. The Operator implementation uses Type Erasure (more details [1] [2] [3]) to achieve polymorphism without inheritance. Consequently, the class needs only to implement the interface which is used in the DSL and which is shown in the below example:
- Example:
NeoN::dsl::Operator<NeoN::scalar> divTerm = Divergence(NeoN::dsl::Operator<NeoN::scalar>::Type::Explicit, exec, ...); NeoN::dsl::Operator<NeoN::scalar> ddtTerm = TimeTerm(NeoN::dsl::Operator<NeoN::scalar>::Type::Temporal, exec, ..);
To fit the specification of the Expression (storage in a vector), the Operator needs to be able to be scaled:
NeoN::Vector<NeoN::scalar> scalingVector(exec, nCells, 2.0);
auto sF = scalingVector.view();
dsl::Operator<NeoN::scalar> customTerm =
CustomTerm(dsl::Operator<NeoN::scalar>::Type::Explicit, exec, nCells, 1.0);
auto constantScaledTerm = 2.0 * customTerm; // A constant scaling factor of 2 for the term.
auto fieldScaledTerm = scalingVector * customTerm; // scalingVector is used to scale the term.
auto multiScaledTerm = (scale + scale + scale + scale) * customTerm;
// Operator also supports the use of a lambda as scaling function to reduce the number of temporaries generated
auto lambdaScaledTerm =
(KOKKOS_LAMBDA(const NeoN::size_t i) { return sF[i] + sF[i] + sF[i] + sF[i]; }) * customTerm;
- To add a user-defined Operator, a new derived class must be created, inheriting from OperatorMixin,
and provide the definitions of the below virtual functions that are required for the Operator interface:
build: build the term
explicitOperation: perform the explicit operation
implicitOperation: perform the implicit operation
getType: get the type of the term
exec: get the executor
volumeVector: get the volume field
An example can be found in test/dsl/operator.cpp.
The required scaling of the term is handled by the Coeff type which can be retrieved by the getCoefficient function of Operator.