1# Defining User Q-Functions 2 3An important feature of LibCEED.jl is the ability to define [user 4Q-functions](https://libceed.org/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.org/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