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.

Note

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.FieldAverageType
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

  • field the VectorField or ScalarField to be averaged, e.g , model.momentum.U.
  • name::String the 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 the write_interval in Configuration.
source

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.FieldRMSType
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

  • field the VectorField or ScalarField, e.g , model.momentum.U.
  • name::String the 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 the write_interval in Configuration.
source

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.ReynoldsStressType
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 be model.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 the write_interval in Configuration.
source

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.

Note

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_averageFunction
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
end
source

To calculate pressure and viscous forces, the following functions are available:

XCALibre.Postprocess.pressure_forceFunction
pressure_force(patch::Symbol, p::ScalarField, rho)

Function to calculate the pressure force acting on a given patch/boundary.

Input arguments

  • patch::Symbol name of the boundary of interest (as a Symbol)
  • p::ScalarField pressure field
  • rho density. Set to 1 for incompressible solvers
source
XCALibre.Postprocess.viscous_forceFunction
viscous_force(patch::Symbol, U::VectorField, rho, ν, νt, config)

Function to calculate the pressure force acting on a given patch/boundary.

Input arguments

  • patch::Symbol name of the boundary of interest (as a Symbol)
  • U::VectorField velocity field
  • rho density. Set to 1 for incompressible solvers
  • ν laminar viscosity of the fluid
  • νt eddy viscosity from turbulence models. Pass ConstantScalar(0) for laminar flows
  • config need to pass Configuration object as this contains the boundary conditions
source