xref: /libCEED/julia/LibCEED.jl/docs/src/UserQFunctions.md (revision 80a9ef0545a39c00cdcaab1ca26f8053604f3120)
1# Defining User Q-Functions
2
3An important feature of LibCEED.jl is the ability to define [user
4Q-functions](https://libceed.readthedocs.io/en/latest/libCEEDapi/#gallery-of-qfunctions)
5natively in Julia. These user Q-functions work with both the CPU and CUDA
6backends.
7
8User Q-functions describe the action of the $D$ operator at quadrature points
9(see [libCEED's theoretical
10framework](https://libceed.readthedocs.io/en/latest/libCEEDapi/#theoretical-framework)).
11Since the Q-functions are invoked at every quadrature point, efficiency is
12very important.
13
14## Apply mass Q-function in C
15
16Before describing how to define user Q-functions in Julia, we will briefly given
17an example of a user Q-function defined in C. This is the "apply mass"
18Q-function from `ex1-volume.c`, which computes the action of the mass operator.
19The mass operator on each element can be written as $B^\intercal D B$, where $B$
20is the basis operator, and $D$ represents multiplication by quadrature weights
21and geometric factors (i.e. the determinant of the mesh transformation Jacobian
22at each qudarture point). It is the action of $D$ that the Q-function must
23implement. The C source of the Q-function is:
24
25```c
26/// libCEED Q-function for applying a mass operator
27CEED_QFUNCTION(f_apply_mass)(void *ctx, const CeedInt Q,
28                             const CeedScalar *const *in,
29                             CeedScalar *const *out) {
30  const CeedScalar *u = in[0], *qdata = in[1];
31  CeedScalar *v = out[0];
32  // Quadrature Point Loop
33  CeedPragmaSIMD
34  for (CeedInt i=0; i<Q; i++) {
35    v[i] = qdata[i] * u[i];
36  } // End of Quadrature Point Loop
37  return 0;
38}
39```
40
41From this example, we see that a user Q-function is a C callback that takes a
42"data context" pointer, a number of quadrature points, and two arrays of arrays,
43one for inputs, and one for outputs.
44
45In this example, the first input array is `u`, which is the value of the trial
46function evaluated at each quadrature point. The second input array is `qdata`,
47which contains the precomputed geometric factors. There is only one output
48array, `v`, which will store the pointwise product of `u` and `data`. Given the
49definition of this Q-function, the `CeedQFunction` object is created by
50```c
51CeedQFunctionCreateInterior(ceed, 1, f_apply_mass, f_apply_mass_loc, &apply_qfunc);
52CeedQFunctionAddInput(apply_qfunc, "u", 1, CEED_EVAL_INTERP);
53CeedQFunctionAddInput(apply_qfunc, "qdata", 1, CEED_EVAL_NONE);
54CeedQFunctionAddOutput(apply_qfunc, "v", 1, CEED_EVAL_INTERP);
55```
56When adding the inputs and outputs, `CEED_EVAL_INTERP` indicates that the $B$
57basis operator should be used to interpolate the trial and test functions from
58nodal points to quadrature points, and `CEED_EVAL_NONE` indicates that the
59`qdata` is already precomputed at quadrature points, and no interpolation is
60requried.
61
62## Apply mass Q-function in Julia
63
64We now replicate this Q-function in Julia. The main way of defining user
65Q-functions in Julia is using the [`@interior_qf`](@ref) macro. The above C code
66(both the definition of the Q-function, its creation, and adding the inputs and
67outputs) is analogous to the following Julia code:
68
69```julia
70@interior_qf apply_qfunc = (
71    ceed, Q,
72    (u, :in, EVAL_INTERP, Q), (qdata, :in, EVAL_NONE, Q),
73    (v, :out, EVAL_INTERP, Q),
74    @inbounds @simd for i=1:Q
75        v[i] = qdata[i]*u[i]
76    end
77)
78```
79
80This creates a [`QFunction`](@ref) object named `apply_qfunc`. The Q-function is
81defined by the tuple on the right-hand side. `ceed` is the name of the
82[`Ceed`](@ref) object where the Q-function will be created, and the second
83argument, `Q`, is the name of that variable that will contain the number of
84quadrature points. The next three arguments are specifications of the input and
85output fields:
86```julia
87    (u, :in, EVAL_INTERP, Q),
88    (qdata, :in, EVAL_NONE, Q),
89    (v, :out, EVAL_INTERP, Q),
90```
91Each input or output field specification is a tuple, where the first entry is
92the name of the array, and the second entry is either `:in` or `:out`, according
93to whether the array is an input or output array. The third entry is the
94[`EvalMode`](@ref) of the field. The remaining entries are the dimensions of the
95array. The first dimension is always equal to the number of quadrature points.
96In this case, all the arrays are simply vectors whose size is equal to the
97number of quadrature points, but in more sophisticated examples (e.g. the [apply
98diffusion Q-function](@ref applydiff)) these arrays could consists of vectors or
99matrices at each quadrature point. After providing all of the array
100specifications, the body of the Q-function is provided.
101
102## [Apply diffusion Q-function in Julia](@id applydiff)
103
104For a more sophisticated example of a Q-function, we consider the "apply
105diffusion" Q-function, used in `ex2-surface`. This Q-function computes the
106action of the diffusion operator. When written in the form $B^\intercal D B$, in
107this case $B$ represents the basis gradient matrix, and $D$ represents
108multiplication by $w \det(J) J^{-\intercal} J^{-1}$, where $J$ is the mesh
109transformation Jacobian, and $w$ is the quadrature weight.
110
111This Q-function is implemented in Julia as follows:
112```julia
113@interior_qf apply_qfunc = (
114    ceed, Q, dim=dim,
115    (du, :in, EVAL_GRAD, Q, dim),
116    (qdata, :in, EVAL_NONE, Q, dim*(dim+1)÷2),
117    (dv, :out, EVAL_GRAD, Q, dim),
118    @inbounds @simd for i=1:Q
119        dXdxdXdxT = getvoigt(@view(qdata[i,:]), CeedDim(dim))
120        dui = SVector{dim}(@view(du[i,:]))
121        dv[i,:] .= dXdxdXdxT*dui
122    end
123)
124```
125In contrast to the previous example, before the field specifications, this
126Q-function includes a _constant definition_ `dim=dim`. The
127[`@interior_qf`](@ref) macro allows for any number of constant definitions,
128which make the specified values available within the body of the Q-function as
129compile-time constants.
130
131In this example, `dim` is either 1, 2, or 3 according to the spatial dimension
132of the problem. When the user Q-function is defined, LibCEED.jl will JIT compile
133the body of the Q-function and make it available to libCEED as a C callback. In
134the body of this Q-function, `dim` will be available, and its value will be a
135compile-time constant, allowing for (static) dispatch based on the value of
136`dim`, and eliminating branching.
137
138Note that `dim` is also available for use in the field specifications. In this
139example, the field specifications are slightly more involved that in the
140previous example. The arrays are given by
141```julia
142    (du, :in, EVAL_GRAD, Q, dim),
143    (qdata, :in, EVAL_NONE, Q, dim*(dim+1)÷2),
144    (dv, :out, EVAL_GRAD, Q, dim),
145```
146Note that the input array `du` has [`EvalMode`](@ref) `EVAL_GRAD`, meaning that
147this array stores the gradient of the trial function at each quadrature point.
148Therefore, at each quadrature point, `du` stores a vector of length `dim`, and
149so the shape of `du` is `(Q, dim)`. Similarly, the action of $D$ is given by
150$w \det(J) J^{-\intercal} J^{-1} \nabla u$, which is also a vector of length `dim` at
151each quadrature point. This means that the output array `dv` also has shape
152`(Q, dim)`.
153
154The geometric factors stored in `qdata` represent the symmetric matrix $w
155\det(J) J^{-\intercal} J^{-1}$ evaluated at every quadrature point. In order to
156reduce data usage, instead of storing this data as a $d \times d$ matrix, we use
157the fact that we know it is symmetric to only store $d(d+1)/2$ entries, and the
158remaining entries we infer by symmetry. These entries are stored using the
159[Voigt convention](https://en.wikipedia.org/wiki/Voigt_notation). LibCEED.jl
160provides some [utilities](Misc.md#LibCEED.getvoigt) for storing and extracting
161symmetric matrices stored in this fashion.
162
163After the field specifications, we have the body of the Q-function:
164```julia
165@inbounds @simd for i=1:Q
166    dXdxdXdxT = getvoigt(@view(qdata[i,:]), CeedDim(dim))
167    dui = SVector{dim}(@view(du[i,:]))
168    dv[i,:] .= dXdxdXdxT*dui
169end
170```
171First, the matrix $w \det(J) J^{-\intercal} J^{-1}$ is stored in the variable
172`dXdxdXdxT`. The symmetric entries of this matrix are accesed using
173`@view(qdata[i,:])`, which avoids allocations. [`getvoigt`](@ref) is used to
174convert from Voigt notation to a symmetric matrix, which returns a statically
175sized `SMatrix`. The version for the correct spatial dimension is selected using
176`CeedDim(dim)`, which allows for compile-time dispatch, since `dim` is a
177constant whose value is known as a constant when the Q-function is JIT compiled.
178
179Then, the gradient of $u$ at the given quadrature point is loaded as a
180fixed-size `SVector`. The result is placed into the output array, where the
181[`StaticArrays.jl`](https://github.com/JuliaArrays/StaticArrays.jl) package
182evaluates `dXdxdXdxT*dui` using an optimized matrix-vector product for small
183matrices (since their sizes are known statically).
184
185## GPU Kernels
186
187If the `Ceed` resource uses a CUDA backend, then the user Q-functions defined
188using [`@interior_qf`](@ref) are automatically compiled as CUDA kernels using
189[`CUDA.jl`](https://github.com/JuliaGPU/CUDA.jl). Some Julia features are not
190available in GPU code (for example, dynamic dispatch), so if the Q-function is
191intended to be run on the GPU, the user should take care when defining the body
192of the user Q-function.
193