-
Configuration File
Mesh File
Restart File
Solver Setup
Physical Definition
Markers and Boundary Conditions
Convective Schemes
Slope Limiters and Shock Resolution
History and Solution Output
Linear Solvers and Preconditioners
Basics of Multi-Zone Computations
Execution
Aeroacoustic analysis of unsteady pressure
Flamelet Solver — Python Wrapper Wall Boundary Conditions
Output Fields Reference
Error Messages
-
Setup Guide: Steady RANS for External Aerodynamics
Setup Guide: Incompressible Flows
Setup Guide: Unsteady Flows and DES
Setup Guide: Laminar-Turbulent Transition
Setup Guide: Turbomachinery
Setup Guide: Heat Transfer and Conjugate Heat Transfer
Setup Guide: Species Transport and Combustion
Setup Guide: Shape Optimization with the Discrete Adjoint
Setup Guide: Non-Ideal Compressible Flows
-
Option Reference
Problem Definition
FluidModel
Data-driven fluid model parameters
Freestream Conditions
Reference Conditions
Boundary Markers
Time-marching
Linear solver definition
Convergence
Multi-grid
Spatial Discretization
Adjoint and Gradient
Sobolev Gradient Solver Parameters
Input/output files and formats
Dynamic mesh definition
Aeroelastic Simulation (Typical Section Model)
Wind Gust
Equivalent Area
Grid deformation
Deformable mesh
FEM flow solver definition
FEA solver
Multizone definition
Radiation solver
Heat solver
Visualize Control Volumes
Inverse design problem
Unsupported options
FFD point inversion
Automatic Differentation options
Python Options
Hybrid_RANSLES Options
Output Options
libROM options
Renamed and Removed Options
-
GIT-ting Started
Developing SU2 on GitHub (Internal Developers)
Running Regression Tests
Code Review
Code Structure
Style Guide
Advanced AD Techniques
Containers for Development
Writing Unit Tests
Integrating Tracy Profiler with SU2
Automatic Differentiation (AD)
Code Reference (Doxygen)
Debugging SU2
Documentation Improvements and Status
Glossary
Docs Check
Adding a Numerical Scheme
This page shows how to add a new convective scheme, upwind or centered, to the finite volume flow solvers of SU2. It also covers, more briefly, how to add a viscous flux or a source term. It follows the code of the compressible solver (CEulerSolver, CNSSolver) and points to the incompressible and NEMO versions.
For the overall layout of the code, see Code Structure. Turbulence, transition and species schemes are covered in other pages: Adding a Turbulence Model, Adding a Transition Model and Adding a Solver.
- Files you will touch
- How the configuration picks the scheme
- Two code paths: classic and vectorized
- The CNumerics interface
- Add a classic upwind scheme
- MUSCL reconstruction and limiters
- Add a vectorized scheme
- Centered schemes
- Jacobians
- Low-Mach and entropy fix options
- Incompressible and NEMO schemes
- Viscous fluxes and source terms
- Discrete adjoint
- Tests
- Checklist
Files you will touch
| File | What goes there |
|---|---|
Common/include/option_structure.hpp |
A value in enum class UPWIND or enum class CENTERED, and its keyword in Upwind_Map or Centered_Map |
Common/src/CConfig.cpp |
Optional: checks in CConfig::SetPostprocessing, the screen summary in CConfig::SetOutput |
config_template.cfg |
The new keyword in the comment above CONV_NUM_METHOD_FLOW |
SU2_CFD/include/numerics/flow/convection/*.hpp, SU2_CFD/src/numerics/flow/convection/*.cpp |
The classic numerics class (one edge at a time) |
SU2_CFD/src/meson.build |
A new .cpp file in the numerics/... list |
SU2_CFD/src/drivers/CDriver.cpp |
The new of the numerics class in CDriver::InitializeNumerics |
SU2_CFD/include/numerics_simd/flow/convection/upwind.hpp or centered.hpp, SU2_CFD/include/numerics_simd/CNumericsSIMD.cpp |
Optional: the vectorized version and its factory entry |
SU2_CFD/src/solvers/CEulerSolver.cpp |
Only if the scheme needs new inputs, new sensors, or the vectorized path |
TestCases/parallel_regression.py, TestCases/serial_regression.py (and the _AD lists) |
A regression test |
How the configuration picks the scheme
The option CONV_NUM_METHOD_FLOW is registered in CConfig::SetConfig_Options (Common/src/CConfig.cpp) with addConvectOption:
addConvectOption("CONV_NUM_METHOD_FLOW", Kind_ConvNumScheme_Flow, Kind_Centered_Flow, Kind_Upwind_Flow);
The parser is COptionConvect in Common/include/option_structure.inl. It looks up the keyword in Centered_Map first, then in Upwind_Map. It sets three values:
Kind_ConvNumScheme_Flow:SPACE_CENTEREDorSPACE_UPWIND(enum ENUM_SPACE). Read it withconfig->GetKind_ConvNumScheme_Flow().Kind_Centered_Flow: aCENTEREDvalue. It isCENTERED::NONEfor upwind schemes. Read it withconfig->GetKind_Centered_Flow().Kind_Upwind_Flow: anUPWINDvalue. It isUPWIND::NONEfor centered schemes. Read it withconfig->GetKind_Upwind_Flow().
The same kind of option is used for CONV_NUM_METHOD_ADJFLOW, CONV_NUM_METHOD_TURB, CONV_NUM_METHOD_ADJTURB, CONV_NUM_METHOD_SPECIES and CONV_NUM_METHOD_HEAT.
The enums and maps are in Common/include/option_structure.hpp. For an upwind scheme, add a value to enum class UPWIND and a line to Upwind_Map:
static const MapType<std::string, UPWIND> Upwind_Map = {
MakePair("NONE", UPWIND::NONE)
MakePair("ROE", UPWIND::ROE)
MakePair("TURKEL_PREC", UPWIND::TURKEL)
// ...
MakePair("LAX-FRIEDRICH", UPWIND::LAX_FRIEDRICH)
MakePair("UDS", UPWIND::UDS)
};
For a centered scheme, do the same with enum class CENTERED and Centered_Map.
Notes:
- Give a new scheme a keyword that is in one map only.
Centered_Mapis searched first, soCONV_NUM_METHOD_FLOWreads a keyword that is in both maps as centered.LAX-FRIEDRICHis such a keyword.UPWIND::LAX_FRIEDRICHis reached only throughRIEMANN_SOLVER_FEM, anaddEnumOptionwithUpwind_Mapthat the DG solver uses. - The comment above the two enums mentions an
ENUM_CONVECTIVE. It no longer exists. The two maps are all you need. - Add the keyword to the list in
config_template.cfgaboveCONV_NUM_METHOD_FLOW. For the general rules on enums and maps, see Adding a Config Option.
At run time, the solver picks the residual function as follows:
- Before each equation system is solved,
CConfig::SetGlobalParamcopies the values of that system into the generic ones (SetKind_ConvNumScheme). CIntegration::Space_Integration(SU2_CFD/src/integration/CIntegration.cpp) then callsCentered_Residualifconfig->GetKind_ConvNumScheme()isSPACE_CENTERED, andUpwind_Residualif it isSPACE_UPWIND.
Two code paths: classic and vectorized
The compressible solver CEulerSolver and its child CNSSolver can compute the convective fluxes on the edges in two ways:
- Classic numerics. A
CNumericsobject computes the flux of one edge. The solver does the MUSCL reconstruction and callsComputeResidual. It then adds the result to the residual and the Jacobian. Files:SU2_CFD/include/numerics/flow/convection/. - Vectorized numerics. A
CNumericsSIMDobject computes the fluxes of several edges at once. It uses SIMD: one CPU instruction acts on several values together. The object also does the reconstruction and the viscous flux, and updates the residual and the Jacobian itself. Files:SU2_CFD/include/numerics_simd/.
The table shows which path is used. See CEulerSolver::Centered_Residual and CEulerSolver::Upwind_Residual in SU2_CFD/src/solvers/CEulerSolver.cpp.
| Case | Path |
|---|---|
Centered schemes JST, JST_KE, JST_MAT, LAX-FRIEDRICH |
Vectorized, always. Works with any fluid model. LOW_MACH_CORR= YES is rejected. Coarse multigrid levels use the Lax-Friedrich scheme. |
Upwind ROE or MSW, FLUID_MODEL = STANDARD_AIR or IDEAL_GAS, LOW_MACH_CORR= NO |
Vectorized |
Any other upwind case (other schemes, other fluid models, LOW_MACH_CORR= YES) |
Classic |
Boundary conditions (conv_bound_term) |
Classic, always |
This is the start of CEulerSolver::Upwind_Residual:
const bool ideal_gas = (config->GetKind_FluidModel() == STANDARD_AIR) ||
(config->GetKind_FluidModel() == IDEAL_GAS);
const bool low_mach_corr = config->Low_Mach_Correction();
/*--- Use vectorization if the scheme supports it. ---*/
if (ideal_gas && !low_mach_corr) {
if (config->GetKind_Upwind_Flow() == UPWIND::ROE || config->GetKind_Upwind_Flow() == UPWIND::MSW) {
EdgeFluxResidual(geometry, solver_container, config);
return;
}
}
A new upwind scheme needs at least a classic version, because the boundary conditions use the CONV_BOUND_TERM numerics. A vectorized version is optional. Write one only for schemes that will be used a lot with an ideal gas.
The CNumerics interface
Every classic flux class derives from CNumerics (SU2_CFD/include/numerics/CNumerics.hpp). A flux class:
- takes
(unsigned short val_nDim, unsigned short val_nVar, const CConfig* config)in its constructor, and passes them toCNumerics(val_nDim, val_nVar, config); - allocates its flux vector and its two Jacobian matrices once, in the constructor, and frees them in the destructor;
- gets the data of the edge through setters. The solver calls them before each call:
SetNormal,SetPrimitive,SetSecondary,SetGridVel,SetCoord,SetSensor,SetDissipation,SetVelocity2_Inf, …; - overrides
ComputeResidual(const CConfig* config), which returns aResidualType<>.
ResidualType is a read-only view of the flux and of the two Jacobians. The data stays inside the numerics object.
template<class Vector_t = const su2double*,
class Matrix_t = const Vector_t*>
struct ResidualType {
const Vector_t residual;
const Matrix_t jacobian_i;
const Matrix_t jacobian_j;
// ...
};
The flow solvers use these conventions:
- The normal (
Normal) points from point i to point j. Its norm is the area of the face. The flux is the flux that leaves the control volume of i, integrated over the face. jacobian_iandjacobian_jare the derivatives of the flux with respect to the conservative variables of i and j.- The primitive vector of the compressible solver is
(T, vx, vy, [vz,] P, rho, h, c, ...).CEulerVariable::CIndicesgives the positions (Pressure(),Density(),Enthalpy(), …). Older numerics useV_i[nDim+1]for the pressure,V_i[nDim+2]for the density andV_i[nDim+3]for the enthalpy. - For a non-ideal fluid model,
S_i[0]is dP/drho at constant e, andS_i[1]is dP/de at constant rho. SeeCEulerVariable::SetSecondaryVar. - Each OpenMP thread has its own numerics object (
numerics[...][CONV_TERM + offset]). So the class can keep work arrays as members.
CNumerics also has older ComputeResidual overloads. They write into arrays passed by the caller, for example ComputeResidual(su2double *val_residual, su2double **val_Jacobian_i, su2double **val_Jacobian_j, CConfig* config). The template classes in SU2_CFD/include/numerics/template.hpp still use them. The flow solvers call the ResidualType<> version, so use that one.
CNumerics has helpers for the Euler flux and its eigensystem: GetInviscidProjFlux, GetInviscidProjJac, GetPMatrix, GetPMatrix_inv. GetRoe_Dissipation serves the low-dissipation Roe variants.
Example: the Roe family
The Roe schemes in SU2_CFD/include/numerics/flow/convection/roe.hpp share an intermediate base class, CUpwRoeBase_Flow. Its ComputeResidual works in two steps:
- It computes the central part of the flux.
- It calls the pure virtual
FinalizeResidual. Each child adds its own dissipation there:CUpwRoe_Flow,CUpwL2Roe_Flow,CUpwLMRoe_Flow.
The AD::...Preacc calls belong to the discrete adjoint. See Discrete adjoint.
Trimmed from SU2_CFD/src/numerics/flow/convection/roe.cpp:
CNumerics::ResidualType<> CUpwRoeBase_Flow::ComputeResidual(const CConfig* config) {
implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT);
unsigned short iVar, jVar, iDim;
su2double ProjGridVel = 0.0, Energy_i, Energy_j;
AD::StartPreacc();
AD::SetPreaccIn(V_i, nDim+4); AD::SetPreaccIn(V_j, nDim+4); AD::SetPreaccIn(Normal, nDim);
// ...
/*--- Face area (norm of the normal vector) and unit normal ---*/
Area = GeometryToolbox::Norm(nDim, Normal);
for (iDim = 0; iDim < nDim; iDim++)
UnitNormal[iDim] = Normal[iDim]/Area;
/*--- Primitive variables at point i ---*/
for (iDim = 0; iDim < nDim; iDim++)
Velocity_i[iDim] = V_i[iDim+1];
Pressure_i = V_i[nDim+1];
Density_i = V_i[nDim+2];
Enthalpy_i = V_i[nDim+3];
Energy_i = Enthalpy_i - Pressure_i/Density_i;
// ...
/*--- Negative RoeSoundSpeed^2, the jump variables is too large, clear fluxes and exit. ---*/
if (RoeSoundSpeed2 <= 0.0) {
// ...
AD::SetPreaccOut(Flux, nVar);
AD::EndPreacc();
return ResidualType<>(Flux, Jacobian_i, Jacobian_j);
}
// ...
/*--- Apply Mavriplis' entropy correction to eigenvalues ---*/
su2double MaxLambda = fabs(ProjVelocity) + RoeSoundSpeed;
for (iVar = 0; iVar < nVar; iVar++)
Lambda[iVar] = max(fabs(Lambda[iVar]), config->GetEntropyFix_Coeff()*MaxLambda);
// ...
/*--- Initialize residual (flux) and Jacobians ---*/
for (iVar = 0; iVar < nVar; iVar++)
Flux[iVar] = 0.5*(ProjFlux_i[iVar]+ProjFlux_j[iVar]);
if (implicit) {
GetInviscidProjJac(Velocity_i, &Energy_i, Normal, 0.5, Jacobian_i);
GetInviscidProjJac(Velocity_j, &Energy_j, Normal, 0.5, Jacobian_j);
}
/*--- Finalize in children class ---*/
FinalizeResidual(Flux, Jacobian_i, Jacobian_j, config);
/*--- Correct for grid motion ---*/
// ...
AD::SetPreaccOut(Flux, nVar);
AD::SetPreaccOut(Jacobian_i, nVar, nVar);
AD::SetPreaccOut(Jacobian_j, nVar, nVar);
AD::EndPreacc();
return ResidualType<>(Flux, Jacobian_i, Jacobian_j);
}
The AUSM+ and SLAU family in ausm_slau.hpp uses the same pattern. CUpwAUSMPLUS_SLAU_Base_Flow::ComputeResidual is final, and the children implement ComputeMassAndPressureFluxes. If your scheme belongs to one of these families, derive from the base class and implement only the part that is specific to your scheme.
Add a classic upwind scheme
Follow these steps to add an upwind scheme that works with every fluid model and on the boundaries.
- Add the enum value and the keyword (see above).
- Declare the class in a header in
SU2_CFD/include/numerics/flow/convection/. Use an existing header or a new one. Derive the class fromCNumericsor from a family base class. OverrideResidualType<> ComputeResidual(const CConfig* config). - Implement it in the matching
.cppinSU2_CFD/src/numerics/flow/convection/. If you add a new.cppfile, add it to thenumerics/...list inSU2_CFD/src/meson.build. - Create the object in
SU2_CFD/src/drivers/CDriver.cpp:- Include the header. The convection headers are included at the top of the file.
- Add a
casein theswitch (config->GetKind_Upwind_Flow())of the compressible block ofCDriver::InitializeNumerics. - Create the object for every multigrid level, both as
conv_termand asconv_bound_term.
For example, the MSW case:
case UPWIND::MSW: for (iMGlevel = 0; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { numerics[iMGlevel][FLOW_SOL][conv_term] = new CUpwMSW_Flow(nDim, nVar_Flow, config); numerics[iMGlevel][FLOW_SOL][conv_bound_term] = new CUpwMSW_Flow(nDim, nVar_Flow, config); } break;InitializeNumericsruns this code once per OpenMP thread (conv_term = CONV_TERM + offset), so each thread gets its own objects.CDriver::FinalizeNumericsdeletes them.If the scheme works only with an ideal gas, follow the
UPWIND::ROEandUPWIND::HLLCcases. They testideal_gas, and for other fluids they createCUpwGeneralRoe_FloworCUpwGeneralHLLC_Flowinstead. - The scheme may need data that
CEulerSolver::Upwind_Residualdoes not set yet. In that case, add theSet...call there, in the edge loop, beforenumerics->ComputeResidual(config). If it needs a new quantity per point (a sensor, for example), compute it inCEulerSolver::CommonPreprocessing, next toSetCentered_Dissipation_SensorandSetRoe_Dissipation. - Optional: print the scheme name in
CConfig::SetOutput, next to the otherKind_Upwind_Flowlines. Add checks inCConfig::SetPostprocessing. For example, non-ideal fluids accept onlyROE,HLLCandJST(search for “Non-Ideal Compressible Fluids” inCConfig.cpp).
Nothing else is needed. The solver gets the object from numerics_container[CONV_TERM + omp_get_thread_num()*MAX_TERMS], calls it on every edge, and adds the result:
auto conv_residual = numerics->ComputeResidual(config);
// ...
if (ReducerStrategy) {
EdgeFluxes.SetBlock(iEdge, conv_residual);
} else {
LinSysRes.AddBlock(iPoint, conv_residual);
LinSysRes.SubtractBlock(jPoint, conv_residual);
}
/*--- Viscous contribution, returns its Jacobians so that the matrix is updated once. ---*/
const auto visc_residual = Viscous_Residual(
iEdge, geometry, solver_container, numerics_container[VISC_TERM + omp_get_thread_num()*MAX_TERMS], config);
if (implicit) UpdateJacobian(iEdge, iPoint, jPoint, conv_residual, visc_residual);
UpdateJacobian (CFVMFlowSolverBase.hpp) updates four blocks of the matrix, together with the viscous Jacobians:
jacobian_iis added to block (i,i) and subtracted from block (j,i);jacobian_jis added to block (i,j) and subtracted from block (j,j).
MUSCL reconstruction and limiters
Your scheme does not reconstruct. The solver reconstructs the left and right states and passes them through SetPrimitive (and SetSecondary). Where this happens:
- Gradients and limiters.
CEulerSolver::Preprocessingcomputes them whenMUSCL_FLOW= YES, on the finest grid. It callsSetPrimitive_Gradient_GGorSetPrimitive_Gradient_LS(optionNUM_METHOD_GRAD_RECON), thenSetPrimitive_Limiter(optionSLOPE_LIMITER_FLOW). The limiter functions are inSU2_CFD/include/limiters/(computeLimiters.hpp,CLimiterDetails.hpp). - Reconstruction. The edge loop of
CEulerSolver::Upwind_ResidualcallsCSolver::MUSCL_Reconstruction(SU2_CFD/include/solvers/CSolver.hpp). It supportsMUSCL_KAPPA_FLOWand the MUSCL ramp (RAMP_MUSCL). WithVAN_ALBADA_EDGE, the limiter is computed per edge (LimiterHelpers<>::vanAlbadaFunction). - Non-ideal fluids and
LOW_MACH_CORR= YES.CEulerSolver::ComputeConsistentExtrapolation(andLowMachPrimitiveCorrection) makes the reconstructed states thermodynamically consistent. - Non-physical states. If the reconstruction gives a negative pressure, density or Roe-averaged speed of sound, the edge falls back to first order (
UpdateNonPhysicalEdgeCounter).
In the vectorized path, reconstructPrimitives in SU2_CFD/include/numerics_simd/flow/convection/common.hpp does the same steps. CUpwindBase::ComputeFlux calls it.
The incompressible solver (CIncEulerSolver::Upwind_Residual) and the NEMO solver (CNEMOEulerSolver::Upwind_Residual) do their own reconstruction in the same way.
Add a vectorized scheme
The framework for vectorized (SIMD) schemes is in SU2_CFD/include/numerics_simd/:
| File | Content |
|---|---|
CNumericsSIMD.hpp |
The interface: the pure virtual ComputeFlux(Int iEdge, ...) and the factory CNumericsSIMD::CreateNumerics |
CNumericsSIMD.cpp |
The factory functions createUpwindIdealNumerics, createUpwindGeneralNumerics (returns nullptr, no real-gas schemes yet), createCenteredNumerics, createNumerics<nDim>. It is compiled through SU2_CFD/src/meson.build. |
flow/convection/upwind.hpp |
CUpwindBase, CRoeScheme, CMSWScheme |
flow/convection/centered.hpp |
CCenteredBase, CJSTScheme, CJSTmatScheme, CJSTkeScheme, CLaxScheme |
flow/convection/common.hpp |
reconstructPrimitives, pMatrix, pMatrixInv, inviscidProjFlux, inviscidProjJac, roeDissipation, … |
flow/diffusion/viscous_fluxes.hpp |
The viscous “decorators” CNoViscousFlux, CCompressibleViscousFlux, CGeneralCompressibleViscousFlux |
flow/variables.hpp |
CCompressiblePrimitives, CCompressibleConservatives, roeAveragedVariables |
The types Double and Int (CNumericsSIMD.hpp) are simd::Arrays. Each operation on them acts on several edges at once. Helpers such as gatherVariables, updateLinearSystem and stopPreacc are in SU2_CFD/include/numerics/util.hpp.
Each scheme is a class template. Its template parameter is a viscous decorator: a class that adds the viscous flux, or no flux for CNoViscousFlux. The work is split this way:
- The base class (
CUpwindBase) does the common work inComputeFlux: geometry, reconstruction, conservative variables, viscous terms, and the update of the linear system. - It then calls
finalizeFluxof the derived class. The call is resolved at compile time through the template parameter, without virtual functions (static polymorphism).
This is the start of CRoeScheme:
template<class Decorator>
class CRoeScheme : public CUpwindBase<CRoeScheme<Decorator>, Decorator> {
private:
using Base = CUpwindBase<CRoeScheme<Decorator>, Decorator>;
// ...
public:
template<class... Ts>
CRoeScheme(const CConfig& config, Ts&... args) : Base(config, args...),
kappa(config.GetRoe_Kappa()),
entropyFix(config.GetEntropyFix_Coeff()),
typeDissip(static_cast<ENUM_ROELOWDISS>(config.GetKind_RoeLowDiss())) {
}
template<class PrimVarType, class ConsVarType, class... Ts>
FORCEINLINE void finalizeFlux(VectorDbl<nVar>& flux,
MatrixDbl<nVar>& jac_i,
MatrixDbl<nVar>& jac_j,
const bool implicit,
const Double& area,
const VectorDbl<nDim>& unitNormal,
const VectorDbl<nDim>& normal,
const CPair<PrimVarType>& V,
const CPair<ConsVarType>& U,
const Int& iPoint,
const Int& jPoint,
const Double& nonPhysical,
const CEulerVariable& solution,
const CGeometry& geometry,
Ts&...) const {
// ...
}
};
To add a vectorized upwind scheme for an ideal gas:
- In
upwind.hpp, writetemplate<class Decorator> class CMyScheme : public CUpwindBase<CMyScheme<Decorator>, Decorator>. Give it a constructor that forwards its arguments to the base, and afinalizeFluxwith the signature above. Write all arithmetic withDouble. Do not useifonDoublevalues, because each one holds the values of several edges. Usefmax,fmin,abs,signand masks instead, asCMSWSchemedoes. -
Add a
caseincreateUpwindIdealNumerics(CNumericsSIMD.cpp):switch (config.GetKind_Upwind_Flow()) { case UPWIND::ROE: obj = new CRoeScheme<ViscousDecorator>(config, iMesh, turbVars); break; case UPWIND::MSW: obj = new CMSWScheme<ViscousDecorator>(config, iMesh, turbVars); break; default: break; } - Add the scheme to the test at the start of
CEulerSolver::Upwind_Residual(shown above). Otherwise the classic path is used. - Keep the classic version in
CDriver::InitializeNumericsforconv_bound_term.
How the solver calls it:
CFVMFlowSolverBase::EdgeFluxResidual(SU2_CFD/include/solvers/CFVMFlowSolverBase.inl) creates the object on first use (CEulerSolver::InstantiateEdgeNumerics).- It loops over the edge colors in chunks of
Double::Sizeedges, and callsedgeNumerics->ComputeFlux(...). Edge colors are groups of edges that threads can process in parallel. - If the factory returns
nullptr, the run stops with “The numerical scheme + gas model in use do not support vectorization.”.
With OpenMP, EDGE_COLORING_GROUP_SIZE must be a multiple of the SIMD length. The SIMD length depends on the build option cpu-arch.
The decorator adds the viscous flux in the same edge loop. So a viscous run with a vectorized scheme does not use CAvgGrad_Flow for the interior edges.
Centered schemes
All compressible centered schemes are vectorized, so a new one goes into the vectorized framework. CEulerSolver::Centered_Residual only calls EdgeFluxResidual. CDriver::InitializeNumerics does not create a conv_term for these schemes. It creates only CUpwRoe_Flow as conv_bound_term.
To add a centered scheme:
- Add the value to
enum class CENTEREDandCentered_Map. - Write the class in
SU2_CFD/include/numerics_simd/flow/convection/centered.hpp, derived fromCCenteredBase<CMyScheme<Decorator>, Decorator>. FollowCJSTSchemeorCLaxScheme. - Add a
caseincreateCenteredNumerics(CNumericsSIMD.cpp). On coarse grids (iMesh != MESH_0) the factory always usesCENTERED::LAX. Theswitchlists everyCENTEREDvalue and has nodefault. So add the new value there even if it is not vectorized. - The scheme may need the pressure sensor, the undivided Laplacian or the spectral radius. In that case, add it to the booleans
center_jst,center_jst_ke,center_jst_matinCEulerSolver::CommonPreprocessing. These booleans trigger the calls toSetCentered_Dissipation_Sensor,SetUndivided_LaplacianandSetMax_Eigenvalue.
The existing centered schemes use these options: JST_SENSOR_COEFF, LAX_SENSOR_COEFF, CENTRAL_JACOBIAN_FIX_FACTOR.
CENTERED::LD2 and CENTERED::CDS exist only for the incompressible solver. For the compressible solver, they make the factory return nullptr.
Jacobians
With TIME_DISCRE_FLOW= EULER_IMPLICIT, the solver needs the Jacobians of the flux. They go into the block matrix Jacobian (CSysMatrix<su2mixedfloat>). ImplicitEuler_Iteration solves this matrix together with the time-step term.
- The Jacobians do not need to be exact. Most schemes use an approximation:
- the Roe schemes use
GetInviscidProjJacand the frozen dissipation matrix; - the AUSM+ and SLAU family uses the Roe Jacobian (
ApproximateJacobian). WithUSE_ACCURATE_FLUX_JACOBIANS= YESit usesAccurateJacobianinstead, which mixes finite differences and manual differentiation.
A more accurate Jacobian usually lets you use a higher CFL number.
- the Roe schemes use
- The matrix stores passive values (
PassiveAssigninCSysMatrix::UpdateBlocks). Passive values are plain numbers that automatic differentiation (AD) does not track. So AD does not differentiate the Jacobians. - Fill the Jacobians only when
implicitis true. This saves time in explicit runs. - With
NEWTON_KRYLOV= YES,CNewtonIntegrationuses matrix-free products with the true Jacobian, computed by finite differences of the residual. It uses this matrix only to build the preconditioner. Expect less accurate finite differences if your flux is not smooth.
Low-Mach and entropy fix options
These options change the convective fluxes. All are in Common/src/CConfig.cpp.
| Option | Getter | Used by |
|---|---|---|
ENTROPY_FIX_COEFF |
GetEntropyFix_Coeff() |
Roe schemes (Mavriplis’ correction of the eigenvalues) |
ROE_KAPPA |
GetRoe_Kappa() |
Roe schemes (weight of the dissipation) |
ROE_LOW_DISSIPATION |
GetKind_RoeLowDiss() |
CUpwRoe_Flow, CUpwSLAU_Flow, CUpwSLAU2_Flow, CRoeScheme; values in RoeLowDiss_Map |
LOW_MACH_CORR |
Low_Mach_Correction() |
Correction of the reconstructed velocity in CEulerSolver::LowMachPrimitiveCorrection; not supported by the vectorized path |
LOW_MACH_PREC |
Low_Mach_Preconditioning() |
Low-Mach preconditioning of the implicit time term in CEulerSolver::PrepareImplicitIteration (always on with TURKEL_PREC) |
MSW_ALPHA |
GetMSW_Alpha() |
MSW scheme |
USE_ACCURATE_FLUX_JACOBIANS |
GetUse_Accurate_Jacobians() |
AUSM+-up, AUSM+-up2, SLAU, SLAU2 |
How ROE_LOW_DISSIPATION works:
- The solver computes the blending in
CEulerSolver::CommonPreprocessing(SetRoe_Dissipation,SetUpwind_Ducros_Sensor). - It passes the result to the numerics with
SetDissipationandSetSensor. - The numerics combine them with
CNumerics::GetRoe_Dissipation.
A new scheme that supports this option takes a bool val_low_dissipation in its constructor, as CUpwRoe_Flow does. CDriver passes roe_low_dissipation for conv_term and false for conv_bound_term.
Incompressible and NEMO schemes
The other finite volume solvers have their own scheme classes.
Incompressible, density based (CIncEulerSolver):
- Schemes: upwind
FDS(CUpwFDSInc_Flow,SU2_CFD/include/numerics/flow/convection/fds.hpp); centeredJST/LD2(CCentJSTInc_Flow) andLAX-FRIEDRICH(CCentLaxInc_Flow) incentered.hpp. - They are created in the
incompressiblepart of the convectiveswitchinCDriver::InitializeNumerics. CIncEulerSolver::Upwind_ResidualandCIncEulerSolver::Centered_Residualcall them.- There is no vectorized path.
Incompressible, pressure based (INCOMP_SYSTEM::PRESSURE_BASED): UDS (CPBConvection_Upwind) and CDS (CPBConvection_Central) in pressure_based.hpp.
NEMO (CNEMOEulerSolver):
- The schemes derive from
CNEMONumericsand are inSU2_CFD/include/numerics/NEMO/convection/:CUpwRoe_NEMO,CUpwAUSM_NEMO,CUpwAUSMPLUSUP2_NEMO,CUpwAUSMPLUSM_NEMO,CUpwMSW_NEMO,CCentLax_NEMO. - They are created in the
NEMO_euler || NEMO_nsblock ofCDriver::InitializeNumerics. - Their constructors also take
nPrimVarandnPrimVarGrad.
FEM (DG): RIEMANN_SOLVER_FEM chooses the Riemann solver. ROE and LAX-FRIEDRICH are hard coded in CFEM_DG_EulerSolver. The others reuse the classic finite volume classes.
Viscous fluxes and source terms
Viscous flux.
- The classic classes are in
SU2_CFD/include/numerics/flow/flow_diffusion.hpp:CAvgGrad_Base,CAvgGrad_Flow(ideal gas),CGeneralAvgGrad_Flow(other fluid models),CAvgGradInc_Flow. - They are created as
visc_termandvisc_bound_term, after the convectiveswitchinCDriver::InitializeNumerics. - The flow solvers call them per edge, from the convective loop, through
Viscous_Residual(iEdge, ...)(CFVMFlowSolverBase::Viscous_Residual_impl). The residual is subtracted from i and added to j. - In the compressible solver, the vectorized path uses the decorators in
numerics_simd/flow/diffusion/viscous_fluxes.hppinstead. So make any change to the viscous flux in both places.
Source term. The flow source terms are in SU2_CFD/include/numerics/flow/flow_sources.hpp. They derive from CSourceBase_Flow, which allocates residual and jacobian. To add one:
- Write the class with
ResidualType<> ComputeResidual(const CConfig* config) override, returningResidualType<>(residual, jacobian, nullptr). The residual is minus the source integrated over the control volume (seeCSourceBodyForce::ComputeResidual). - Create it in
CDriver::InitializeNumerics, in theif/else ifchain forsource_first_termorsource_second_term. Each solver has only these two source slots.CSourceNothing(an alias ofCNumerics) fills an unused slot. - Call it in the
Source_Residualof the solver (CEulerSolver::Source_Residual,CIncEulerSolver::Source_Residual):- loop over the points;
- set the inputs (
SetConservative,SetPrimitive,SetVolume,SetCoord, …); - call
ComputeResidual; - call
LinSysRes.AddBlock(iPoint, residual)and, if implicit,Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i).
Discrete adjoint
A new scheme does not need adjoint code, but you must write it so that AD works. The discrete adjoint drivers record one primal iteration, with all its flux evaluations, on the AD tape. The tape is the record of every operation. Evaluating the tape gives the adjoint. CDiscAdjSolver holds the adjoint variables.
Rules for the scheme:
- Use
su2doublefor every value that depends on the solution or on the mesh.passivedoubleandSU2_TYPE::GetValuecut the derivative. - Use preaccumulation correctly. Preaccumulation condenses the operations of one function into a small Jacobian from its inputs to its outputs, so the tape stays smaller.
- Start with
AD::StartPreacc(). - Declare all inputs with
AD::SetPreaccIn: primitives, normal, grid velocity, sensors, secondary variables, … - End every return path with
AD::SetPreaccOutandAD::EndPreacc(), as in the early return ofCUpwRoeBase_Flow::ComputeResidualabove.
A missing input gives wrong gradients without any error. In the vectorized framework,
gatherVariablesregisters the inputs andstopPreaccends the section. - Start with
- Branches on solution values (
max,fabs,if) are recorded as taken at the converged state. The derivative is the one of the active branch. Avoid branches whose outcome changes between iterations at convergence. - A tape debug build helps to find preaccumulation errors. Build with
-Dcodi-tape=Tagand use the optionsCHECK_TAPE_TYPEandCHECK_TAPE_VARIABLES. See Advanced AD Techniques.
Tests
Add these tests for a new scheme:
- Regression test. Add a short case to
TestCases/parallel_regression.pyandTestCases/serial_regression.py. Existing scheme tests you can copy:wedge(euler/wedge/inv_wedge_HLLC.cfg) andramp_msw(euler/ramp/inv_ramp_msw.cfg). If the scheme is meant for the discrete adjoint, also add a case toparallel_regression_AD.py/serial_regression_AD.py(for example, thedisc_adj_eulercases). See Running Regression Tests. - Order of accuracy. Use the method of manufactured solutions (MMS) cases in
TestCases/mms/. Themms_fvm_*tests inparallel_regression.pyuse them:mms/fvm_euler/inv_mms_vortex.cfg(ROE,KIND_VERIFICATION_SOLUTION= INVISCID_VORTEX)mms/fvm_navierstokes/lam_mms_roe.cfg(ROE,MMS_NS_UNIT_QUAD)mms/fvm_incomp_euler/inv_mms_jst.cfg(JST,MMS_INC_EULER)mms/fvm_incomp_navierstokes/lam_mms_fds.cfg(FDS,MMS_INC_NS)
Change
CONV_NUM_METHOD_FLOWand run them on a sequence of meshes. The exact solutions are inCommon/include/toolboxes/MMS/.CFVMFlowSolverBase::ComputeVerificationErrorcomputes the error norms. - Unit test. You can test numerics alone with Catch2. See
UnitTests/SU2_CFD/numerics/CNumerics_tests.cppand Writing Unit Tests. - OpenMP. If you changed the solver loops, run at least one case with OpenMP threads (
hybrid_regression.py).
Checklist
- Enum value and keyword in
option_structure.hpp, keyword in only one ofCentered_Map/Upwind_Map. - Keyword listed in
config_template.cfg. - Classic class with
ResidualType<> ComputeResidual(const CConfig*), flux and Jacobians w.r.t. conservative variables. - New
.cppinSU2_CFD/src/meson.build. conv_termandconv_bound_termcreated for every multigrid level inCDriver::InitializeNumerics.- Vectorized version (optional): class in
numerics_simd, factorycase, test inCEulerSolver::Upwind_Residual. - AD: all inputs in preaccumulation, every return path closes it.
- Regression test, and MMS runs for the order of accuracy.
Last checked against SU2 develop 11c177b904.