Post-processing
Information about postprocessing XCALibre.jl results
ParaView
All solvers in XCALibre.jl will write simulation results in formats that can be loaded directly in ParaView, which is the leading open-source project for scientific visualisation and postprocessing. More information about how to use ParaView can be found in the resources page on their website.
XCALibre.jl can output simulation results to either VTK compliant formats or OpenFOAM format. XCALibre.jl uses two different VTK formats depending on the type of flow solver used. For 2D simulations, the results are written to file using the .vtk file format. 3D simulations are written using the unstructured VTK file format, .vtu.
A limitation of the current VTK writers in XCALibre.jl is that boundary information is stored along with internal mesh cell information, and results are stored at cell centres only. Thus, care must be taken when visualising results at boundary faces. Boundary information for fixedValue boundaries is displayed corrently when the results are saved in the OpenFOAM format.
Available functions
Although ParaView offers considerable flexibility for postprocessing results, users may also wish to carry out more advanced or different analyses on their CFD results. At present XCALibre.jl offers a limited set of built-in runtime postprocessing functions, currently these include time averaging a scalar or vector field over a specified range of iterations, and also the root mean square (RMS) of the fluctuations of a field.
Example: Calculate runtime averaged field
As an example, to average any Scalar or Vector field an instance of FieldAverage must be created. For transient simulations this is a time average and for steady simulations it is an average over iterations.
XCALibre.Postprocess.FieldAverage — Type
FieldAverage(
#required arguments
field;
name::String,
#optional keyword arguments
start::Union{Real,Nothing},
stop::Union{Real,Nothing},
update_interval::Union{Real,Nothing})Constructor to allocate memory to store the time averaged field. Once created, should be passed to the Configuration object as an argument with keyword postprocess
Input arguments
fieldtheVectorFieldorScalarFieldto be averaged, e.g ,model.momentum.U.name::Stringthe name of the field to be averaged, e.g "U_mean", this is used only when exporting to .vtk format
Optional arguments
start::Union{Real,Nothing}optional keyword which specifies the start time/iteration of the averaging window, for steady simulations, this is in iterations, for transient simulations it is in flow time.stop::Union{Real,Nothing}optional keyword which specifies the end iteration/time of the averaging window. Default value is the last iteration/timestep.update_interval::Union{Real,Nothing}optional keyword which specifies how often the time average of the field is updated and stored (default value is 1 i.e average updates every timestep/iteration). Note that the frequency of writing the post-processed fields is specified by thewrite_intervalinConfiguration.
Once created this is simply passed to the Configuration object as an extra argument with the keyword postprocess. For example to average the velocity field over the whole simulation,
postprocess = FieldAverage(model.momentum.U; name = "U_mean")
config = Configuration(solvers=solvers, schemes=schemes, runtime=runtime, hardware=hardware, boundaries=BCs, postprocess=postprocess)The rest of the case would remain exactly the same.
Example: Calculate field RMS
The RMS of a Scalar of Vector field can be obtained in a similar way to the time averaged field, instead an instance of RMS is created which has the following definition.
XCALibre.Postprocess.FieldRMS — Type
FieldRMS(
#required arguments
field;
name::String,
#optional keyword arguments
start::Union{Real,Nothing},
stop::Union{Real,Nothing},
update_interval::Union{Real,Nothing})Constructor to allocate memory to store the root mean square of the fluctuations of a field over the averaging window. Once created, should be passed to the Configuration object as an argument with keyword postprocess
Input arguments
fieldtheVectorFieldorScalarField, e.g ,model.momentum.U.name::Stringthe name/label of the field, e.g "U_rms", this is used only when exporting to .vtk format
Optional arguments
start::Union{Real,Nothing}optional keyword which specifies the start of the RMS calculation window, for steady simulations, this is in iterations, for transient simulations it is in flow time.stop::Union{Real,Nothing}optional keyword which specifies the end iteration/time of the RMS calculation window. Default value is the last iteration/timestep.update_interval::Union{Real,Nothing}optional keyword which specifies how often the RMS of the field is updated and stored (default value is 1 i.e RMS updates every timestep/iteration). Note that the frequency of writing the post-processed fields is specified by thewrite_intervalinConfiguration.
The RMS of the velocity field can be easily calculated by creating an instance of FieldRMS and passing it to the Configuration object with the keyword postprocess, with the rest of the case remaining unchanged.
postprocess = FieldRMS(model.momentum.U; name = "U_rms")
config = Configuration(solvers=solvers, schemes=schemes, runtime=runtime, hardware=hardware, boundaries=BCs, postprocess=postprocess)Example: Calculate the Reynolds Stress Tensor
The Reynolds Stress Tensor can be obtained using the constructor ReynoldsStress, which has to be passed to the Configuration object.
XCALibre.Postprocess.ReynoldsStress — Type
ReynoldsStress(
#required arguments
model.momentum.U;
#optional keyword arguments
start::Union{Real,Nothing},
stop::Union{Real,Nothing},
update_interval::Union{Real,Nothing})Constructor to allocate memory to store the Reynolds Stress Tensor over the calculation window. Once created, should be passed to the Configuration object as an argument with keyword postprocess
Input arguments
field, must bemodel.momentum.U
Optional arguments
start::Union{Real,Nothing}optional keyword which specifies the start of the Reynolds Stress Tensor calculation window, for steady simulations, this is in iterations, for transient simulations it is in flow time.stop::Union{Real,Nothing}optional keyword which specifies the end iteration/time of the Reynolds Stress Tensor calculation window. Default value is the last iteration/timestep.update_interval::Union{Real,Nothing}optional keyword which specifies how often the Reynolds Stress Tensor is updated and stored (default value is 1 i.e Reynolds Stress Tensor updates every timestep/iteration). Note that the frequency of writing the post-processed fields is specified by thewrite_intervalinConfiguration.
Example: Post-process multiple fields
To post-process multiple fields as a time, a vector of objects can be passed instead e.g.
postprocess = [FieldRMS(model.momentum.U; name = "U_rms"), FieldAverage(model.momentum.U; name = "U_mean"), FieldAverage(model.momentum.p; name = "p_mean")]
config = Configuration(solvers=solvers, schemes=schemes, runtime=runtime, hardware=hardware, boundaries=BCs, postprocess=postprocess)If more functionality is required, defining new custom postprocessing functions is reasonably straight-forward since these can be written in pure Julia. In this section, examples of postprocessing functions will be provided as an illustration.
At present the postprocessing functions available in XCALibre.jl shown below will only execute on CPUs and should be considered experimental. Once we settle on a "sensible" (maintainable and extensible) API, we plan to offer a larger selection of postprocessing tools which are likely to include more options for runtime postprocessing.
Example: Calculate boundary average
In this example, a function is shown that can be used to calculate the average on a user-provided boundary.
XCALibre.Postprocess.boundary_average — Function
function boundary_average(patch::Symbol, field, config; time=0)
# Extract mesh object
mesh = field.mesh
# Determine ID (index) of the boundary patch
ID = boundary_index(mesh.boundaries, patch)
@info "calculating average on patch: $patch at index $ID"
boundary = mesh.boundaries[ID]
(; IDs_range) = boundary
# Create face field of same type provided by user (scalar or vector)
sum = nothing
if typeof(field) <: VectorField
faceField = FaceVectorField(mesh)
sum = zeros(_get_float(mesh), 3) # create zero vector
else
faceField = FaceScalarField(mesh)
sum = zero(_get_float(mesh)) # create zero
end
# Interpolate CFD results to boundary
interpolate!(faceField, field, config)
correct_boundaries!(faceField, field, field.BCs, time, config)
# Calculate the average
for fID ∈ IDs_range
sum += faceField[fID]
end
ave = sum/length(IDs_range)
# return average
return ave
endTo calculate pressure and viscous forces, the following functions are available:
XCALibre.Postprocess.pressure_force — Function
pressure_force(patch::Symbol, p::ScalarField, rho)Function to calculate the pressure force acting on a given patch/boundary.
Input arguments
patch::Symbolname of the boundary of interest (as aSymbol)p::ScalarFieldpressure fieldrhodensity. Set to 1 for incompressible solvers
XCALibre.Postprocess.viscous_force — Function
viscous_force(patch::Symbol, U::VectorField, rho, ν, νt, config)Function to calculate the pressure force acting on a given patch/boundary.
Input arguments
patch::Symbolname of the boundary of interest (as aSymbol)U::VectorFieldvelocity fieldrhodensity. Set to 1 for incompressible solversνlaminar viscosity of the fluidνteddy viscosity from turbulence models. Pass ConstantScalar(0) for laminar flowsconfigneed to passConfigurationobject as this contains the boundary conditions