Contributor Guide

Guidelines and practical information for contributing to XCALibre.jl

Introduction


In time, our ambition is to document the internal API completely, however, this will take some time and it is an ongoing process. Since XCALibre.jl uses a modular approach and the current functionality covers a good portion of the CFD stack, those interested in working with the internal API should be able to work from existing implementations. In this page we provide key information for those users who want to customise, refine, improve, or extend XCALibre.jl.

From its humble beginning as a 1D diffusion example code to explore the features of the Julia programming language (and speed claims which turned out to be true 😄), XCALibre.jl was meant to be shared and used to help students understand the Finite Volume Method and as an entry point to explore the implementation details underpinning CFD. As the code base has grown, this original purpose remains. However, XCALibre.jl is now a more complete CFD software stack which can be used by both researchers and students to test out new ideas, even on complex geometry with acceptable performance. XCALibre.jl will hopefully continue to grow and offer more functionality as it has been the case since work on XCALibre.jl started. In the sharing spirit of open-source software we also welcome code contributions, and we hope to make this process as simple as possible. However, we ask contributors to follow a few guidelines to ensure that we grow XCALibre.jl in a sustainable and maintainable manner.

Some guidelines


To help use keep the codebase consistent and allow us to merge future Pull Requests (PRs) more easily, we kindly request that contributors adhere to some basic guidelines. We are trying to strike a balance between consistency and ease of contribution by aiming not to be overly demanding on contributors. A minimum set of guidelines is provided below (subject to review as the codebase evolves), in no particular order:

Code style

  • Follow the style guide recommended in the official Julia documentation
  • Use camel case format for custom types e.g. MyType
  • We prefer easy-to-read function names e.g. use calculate_flux over calf or similar. All in lower case and words separated with an underscore
  • For internal variables feel free to use Unicode symbols, or camel case identifiers. However, please refrain from doing this for any top-level or user-facing API variables.
  • Although Julia allows some impressive one-liners, please avoid. These can be hard to reason sometimes, aim to strike a balance between succinctness and clarity.

Code contribution

  • Please open a PR for any code contributions against our main branch if your are contributing top level functionality that builds on existing code e.g. a new turbulence model or a new boundary condition (these contributions will typically be included inside one of the existing sub modules)
  • For contributions that may require code reorganisation please do get in touch to ensure this aligns with any planned changes (open an issue). PRs will likely be requested against the current dev branch
  • Ideally, all contributions will also include basic documentation and tests.

Help wanted

  • If you have specific expertise in MPI or Multi-GPU implementation and wish to get involved please get in touch.

Module organisation


ModuleDescription
XCALibre.MeshThis module defines all types required to construct the mesh object used by the flow solvers. Two main mesh types are used Mesh2 and Mesh3 used for 2D and 3D simulations, respectively. Some access functions are also included in this module.
XCALibre.FieldsThis module defines the fields used to hold and represent the flow variable. Scalar, vector and tensor fields are defined e.g. ScalarField, etc. Information is stored at cell centres. These fields also have face variant where information is stored at face centres e.g. FaceVectorField. These fields are generally used to store fluxes. A limited set of field operations are also defined e.g. getfield to allow indexing field object directly.
XCALibre.ModelFrameworkThis module provides the framework used to define scalar and vector model equations whilst storing information about the operators used. The data structure also defines sparse matrices used to store discretisation information.
XCALibre.DiscretiseThis module defines the various operators needed to represent each terms in a model equation and the main discretisation loop that linearises each term according to the schemes available. Boundary conditions are also implemented in this module.
XCALibre.SolveThis module includes all functions and logic needed to solve the linear system of equations that follows the equation discretisation. The internal API to solve these systems of equations is included in this module.
XCALibre.CalculateImplementation of functions use to carry out calculations essential to the implementation of flow solvers is included in this module. This includes interpolation of variables from cell centroid to cell faces, gradient calculation, surface normals, etc.
XCALibre.ModelPhysicsThis module includes the implementations of all the physical models i.e. fluid, turbulence and energy models.
XCALibre.SimulateThis model contains information needed to set up a simulation, including the Configuration type used by all flow solvers.
XCALibre.SolversImplementations of the SIMPLE and PISO flow solvers from steady and unsteady solutions, including their compressible variant.
XCALibre.PostprocessA limited set of functions for postprocessing are implemented in this module.
XCALibre.IOFormatsFunctionality to write simulation results to VTK or OpenFOAM output files are implemented in this module.
XCALibre.FoamMeshStand alone module to parse, process (geometry calculation) and import OpenFOAM mesh files into XCALibre.jl
XCALibre.UNV3Stand alone module to parse, process (geometry calculation) and import UNV (3D) mesh files into XCALibre.jl
XCALibre.UNV2Stand alone module to parse, process (geometry calculation) and import UNV (2D) mesh files into XCALibre.jl

Key types and structures


Mesh type and format

The definitions of the data structures used to define Mesh3 objects is given below. Note that for succinctness only 3D structures are shown since all the 2D structures are identical. The type is only used for dispatching solvers to operate in 2D or 3D.

XCALibre.Mesh.Mesh3Type
struct Mesh3{VC, VI, VF<:AbstractArray{<:Face3D}, VB, VN, SV3, UR} <: AbstractMesh
    cells::VC           # vector of cells
    cell_nodes::VI      # vector of indices to access cell nodes
    cell_faces::VI      # vector of indices to access cell faces
    cell_neighbours::VI # vector of indices to access cell neighbours
    cell_nsign::VI      # vector of indices to with face normal correction (1 or -1 )
    faces::VF           # vector of faces
    face_nodes::VI      # vector of indices to access face nodes
    boundaries::VB      # vector of boundaries
    nodes::VN           # vector of nodes
    node_cells::VI      # vector of indices to access node cells
    get_float::SV3      # store mesh float type
    get_int::UR         # store mesh integer type
    boundary_cellsID::VI # vector of indices of boundary cell IDs
end
source
XCALibre.Mesh.NodeType
struct Node{SV3<:SVector{3,<:AbstractFloat}, UR<:UnitRange{<:Integer}}
    coords::SV3     # node coordinates
    cells_range::UR # range to access neighbour cells in Mesh3.node_cells
end
source
XCALibre.Mesh.Face3DType
struct Face3D{
    F<:AbstractFloat, 
    SV2<:SVector{2,<:Integer},
    SV3<:SVector{3,F}, 
    UR<:UnitRange{<:Integer}
    }
    
    nodes_range::UR # range to access face nodes in Mesh3.face_nodes
    ownerCells::SV2 # IDs of face owner cells (always 2)
    centre::SV3     # coordinates of face centre
    normal::SV3     # face normal unit vector
    e::SV3          # unit vector in the direction between owner cells
    area::F         # face area
    delta::F        # distance between owner cells centres
    weight::F       # linear interpolation weight
end
source
XCALibre.Mesh.CellType
struct Cell{F<:AbstractFloat, SV3<:SVector{3,F},UR<:UnitRange{<:Integer}}
    centre::SV3     # coordinate of cell centroid
    volume::F       # cell volume
    nodes_range::UR # range to access cell nodes in Mesh3.cell_nodes
    faces_range::UR # range to access cell faces info (faces, neighbours cells, etc.)
end
source
XCALibre.Mesh.BoundaryType
struct Boundary{S<:Symbol, UR<:UnitRange{<:Integer}}
    name::S         # Boundary patch name
    IDs_range::UR   # range to access boundary info (faces and boundary_cellsID)
end
source

To fully characterise how mesh information is represented in XCALibre.jl, it is important to highlight the following "contracts" that are exploited throughout:

  • Node, face and cell IDs correspond to the index where they are stored in their corresponding vector in the Mesh3 structure e.g. Mesh3.Faces[10] would return information for the face whose ID is 10. These vectors are 1-indexed as standard in Julia.
  • Face normals at boundary faces is always pointing outside the domain e.g. they point in the direction expected in the FVM
  • Face normals for internal faces is always pointing in the direction from the ownerCell with the smallest ID to the largest. Since the discretisation loop is cell based, for the cell with the highest ID the direction must be reversed. This information is tracked in Mesh3.nsign which stores 1 if the face normal is correctly aligned or -1 if the normal needs to be reversed.
  • Boundary faces (e.g. patches) are stored consecutively in Mesh3.Faces starting at the beginning of the array followed by all the internal faces.
  • Boundary faces are those connected only to 1 Cell, thus, for these faces the entry Face3D.ownerCells is a 2-element vector with a repeated index e.g. [3, 3]
  • Boundary cells only store information for internal faces. This improves performance for the main discretisation loop (cell based) since it can always been assumed that none of the faces will be a boundary face, which are dealt with in a separate loop.

Field types

After the Mesh2 and Mesh3 objects, the most fundamental data structure in XCALibre.jl are fields used to represent the flow variables. In the current implementation, the prime field is the ScalarField for storing information at cell centres, and the corresponding FaceScalarField to store information at cell faces (normally fluxes). Internally, both are identical, therefore, the internal structure of the "Face" variants will not be discussed. ScalarFields have the following definition:

XCALibre.Fields.ScalarFieldType
struct ScalarField{VF,M<:AbstractMesh,BC} <: AbstractScalarField
    values::VF  # scalar values at cell centre
    mesh::M     # reference to mesh
    BCs::BC     # store user-provided boundary conditions
end
source

Vector and tensors are represented internally by their individual components, as an illustration the VectorField type is shown below. Notice that each component of both vector and tensor fields are themselves represented by the same ScalarField type shown above. This has some implementation benefits, i.e. reducing duplication and allowing for rapid development. However, the performance of other means of storing these fields is being investigated. Thus, these internals may change if we identify performance gains.

XCALibre.Fields.VectorFieldType
struct VectorField{S1<:ScalarField,S2,S3,M<:AbstractMesh,BC} <: AbstractVectorField
    x::S1   # x-component is itself a `ScalarField`
    y::S2   # y-component is itself a `ScalarField`
    z::S3   # z-component is itself a `ScalarField`
    mesh::M
    BCs::BC
end
source

All fields behave (mostly) like regular arrays and can be indexed using the standard Julia syntax e.g. say U is a VectorField, then U[3] would return the vector stored in cell with ID = 3 (as a SVector{3} for improved performance).

Note

The implementation of broadcasting operations has been put on hold until a more thorough investigation of alternative data structures for vector and tensor fields has been completed. This is ongoing work.

Boundary conditions


To implement a new boundary condition the following elements are required (see source code for Dirichlet, for example):

  • type definition: a structure containing the fields :ID and :value
  • fixedValue function: used to check that user provided information is suitable for this boundary being implemented
  • Implementation of the boundary face value: functor defining the boundary condition implementation, facilitated by a macro (see details below)
  • Scalar and vector face value interpolation: kernels to specify how to transfer cell information to the boundary.

Developers and contributors are encouraged to explore the source code for examples on existing implementations of boundary conditions. To ease their implementation the XCALibre.Discretise.@define_boundary is provided.

XCALibre.Discretise.@define_boundaryMacro
macro define_boundary(boundary, operator, definition)
    quote
        @inline (bc::$boundary)(
            term::Operator{F,P,I,$operator}, cellID, zcellID, cell, face, fID, i, component, time
            ) where {F,P,I} = $definition
    end |> esc
end

Macro to reduce boilerplate code when defining boundary conditions (implemented as functors) and provides access to key fields needed in the implementation of boundary conditions, such as the boundary cell and face objects (more details below)

Input arguments

  • boundary specifies the boundary type being defined
  • operator specifies the operator to which the condition applies e.g. Laplacian
  • definition provides the implementation details

Available fields

  • term reference to operator on which the boundary applies (gives access to the field and mesh)
  • cellID ID of the corresponding boundary cell
  • zcellID sparse matrix linear index for the cell
  • cell gives access to boundary cell object and corresponding information
  • face gives access to boundary face object and corresponding information
  • fID ID of the boundary face (to index Mesh2.faces vector)
  • i local index of the boundary faces within a kernel or loop
  • component for vectors this specifies the components being evaluated (access as component.value). For scalars component = nothing
  • time provides the current simulation time. This only applies to time dependent boundary implementation defined as functions or neural networks.

Example

Below the use of this macro is illustrated for the implementation of a Dirichlet boundary condition acting on the Laplacian using the Linear scheme:

@define_boundary Dirichlet Laplacian{Linear} begin
    J = term.flux[fID]      # extract operator flux
    (; area, delta) = face  # extract boundary face information
    flux = J*area/delta     # calculate the face flux
    ap = term.sign*(-flux)  # diagonal (cell) matrix coefficient
    ap, ap*bc.value         # return `ap` and `an`
end

When called, this functor will return two values ap and an, where ap is the cell contribution for approximating the boundary face value, and an is the explicit part of the face value approximation i.e. ap contributes to the diagonal of the sparse matrix (left-hand side) and an is the explicit contribution assigned to the solution vector b on the right-hand of the linear system of equations $Ax = b$

source

Implementing new models


The internal API for models is still somewhat experimental, thus, it is more instructive to explore the source code. Although not fully finalised, the implementation is reasonably straight forward to follow thanks to the abstractions that have been adopted, some of which are described above, as well as the use of descriptive names for internal functions. If you need help on any aspect of the internals, for now, it is recommended to contact us by opening an issue.