To understand the how fvDOM model is used and implemented in OpenFOAM, we use tutorial smallPoolFire3D as an example. We have discussed the p1, which is also a radiation model implemented in OpenFOAM. Here, we will discuss another radiation model used in OpenFOAM called fvDOM (Finite Volume Discrete Ordinates Method). The implementation logic of this model in OpenFOAM is quite similar to how we did in P1 model. Here we will not repeat what we have explained in p1 model.
Before starting, the the mathematical form of the fvDOM model in ANSYS FLUENT will be shown:
In OpenFOAM, the last term on the rhs of Eq.(smallPoolFire3D to see how fireFoam uses the fvDOM model. Everything works as same as coalCombustionFoam until we choose the model. So, here I will directly go to the model selection part. In Foam::radiationModel::New, we will search for the models we input in the radiationProperties files:
1 | radiation on; |
From radiationModel fvDOM;, we know that fvDOM model is used. Then we will go to the constructor of class fvDOM in :
1 | Foam::radiationModels::fvDOM::fvDOM(const volScalarField& T) |
In the initialization list, the radiationModel constructor will be called first. typeName will be fvDOM here:
1 | Foam::radiationModel::radiationModel(const word& type, const volScalarField& T) |
The coeffs_ here will be:
1 | { |
Then, we call initialise() to initialize the absorptionEmission_, scatter_ and soot_. This has already been explained in P1 and will not be explained again here. After calling the constructor of the radiationModel class, other members in the fvDOM class will be initialized with a zero value first. nTheta_ which is nPhi_ which is coeffs_ above. In the figure below you will see how
![]()
Let’s go back to the fvDOM constructor, nRay_ means the number of rays we are going to solve. This is determined by how we discretized the RTE using the DOM (discrete-ordinate method). Because theoretically, there should be infinite amount of rays. It means we have to solve infinite number of RTEs, which is indeed impossible. Therefore the situation is simplified to finite number of directions of rays with the same amount of RTEs. nLambda_ is the number of wavelength bands. aLambda_ is the wavelength total absorption coefficient, blackBody_ is the blackbody radiation intensity, IRay_ is the ray radiation intensity.
After the initialization in the fvDOM constructor, the function initialise() will be called.
1 | void Foam::radiationModels::fvDOM::initialise() |
We will only discuss the general 3D scenario here. The number of rays nRay_, is determined number of angles we what to discretize defined by nPhi_ and nTheta_. nPhi_ and nTheta_ are defined in the radiationProperties file. IRay_ is a pointer list which save informations of radiation rays. Here, the size of this pointer list is initialized by the number of rays as setSize(nRay_). Then, the increment variables deltaPhi and deltaTheta are defined. Here as the spherical coordinate system is used, the increment of nPhi is azimuthal angles in nTheta is polar angles in nPhi and nTheta are both equal to one, there will be 1
1 | for (label n = 1; n <= nTheta_; n++) |
Then, inside the for loop, the IRay_ will be initialized using set(). Here i will count for the index of the radiation ray. new radiativeIntensityRay will call the constructor of the radiativeIntensityRay class. The radiativeIntensityRay constructor is very long, we will discuss it in a separate way. Firstly, the parameters the constructor takes and what we input in fvDOM.C:
1 | Foam::radiationModels::radiativeIntensityRay::radiativeIntensityRay |
and
1 | new radiativeIntensityRay |
Then, dom_, mesh_, absorptionEmission_, and blackBody_ will be initialized with the input parameters. I_, qr_, qem_, and qem_ will be initialized with zero value. The radiation ray direction d_ and the average direction vector inside the solid angle dAve are initialized as zero. No more words about the constructor, please see the code below:
1 | d_(Zero), |
To save some time:
1 | scalar sinTheta = Foam::sin(theta); |
Then, the solid angle for “this” radiation ray will be calculated:
1 | omega_ = 2.0*sinTheta*Foam::sin(deltaTheta/2.0)*deltaPhi; |
To explain this, some derivations will be shown here. In spherical coordinate system, the solid angle is defined as:
In one radiation ray direction, to calculate the solid angle, we need to do a integral over its angular range:
Then, the the direction of the ray in a unit sphere is defined:
1 | d_ = vector(sinTheta*sinPhi, sinTheta*cosPhi, cosTheta); |
In OpenFOAM, as we mentioned in radiationProperties, the angle d_ vector is defined as above. The average direction vector inside the solid angle is defined as:
1 | dAve_ = vector |
We will do the derivation here to explain how this vector is shown as this form. To solve the RTE using the fvDOM (discrete ordinate method), we need to do a finite volume integral over the spherical coordinate as what we did in Cartesian system. The RTE we are going to solve is:
As I mentioned before, OpenFOAM does not consider the last term on the rhs. So, the equation we are going to solve in OpenFOAM becomes:
It should be mentioned here, the d_ (ray direction) as defined in the code. Now, the finite volume method will be applied to the RTE:
The last two terms on the rhs becomes:
and
The derivation of the first term on the lhs is a little bit different:
,where
For each term,
After the integration, the
This is defined as dAve_ in the radiativeIntensityRay constructor body:
1 | dAve_ = vector |
After the definition of dAve_, projections to 2D and 1D case are introduced. Here we will only discuss the 3D case. The initialization of ILambda_ will not be discussed here. When the for loop for different rays in the initialise() function is finished, the absorption coefficient for different wave length will be initialised:
1 | // Construct absorption field for each wavelength |
Here, the aLambda_ is initialized by a_, which is initialized in fvDOM constructor as zero value field. The following code will calculate the maximum solid angle omegaMax_:
1 | // Calculate the maximum solid angle |
The following code will print Information about what we have done in the discretization:
1 | Info<< typeName << ": Created " << IRay_.size() << " rays with average " |
Then, we will see how fireFoam call the radiation model. In fireFoam.C, we have #include "YEEqn.H". In YEEqn.H , radiation->correct(); will call the correct() function in the radiation model we choose, which is fvDOM here. But in fvDOM class, there is no function called correct(). But we know, the fvDOM class is a subclass of radiationModel. In class radiationModel, a correct() function exists. After calling the correct() function, a source term radiation->Sh(thermo, he) will be added to the energy equation. Here we will see how we call the correct() function first. in radiationModel.C:
1 | void Foam::radiationModel::correct() |
Firstly, calculate() will be called and then soot_->correct(); will be called. As we know, the calculate() function is a pure virtual function. As the polymorphism in C++, it will called the calculate() function of its subclass. Here the subclass is the class fvDOM. So the calculate() function in fvDOM will be called:
1 | void Foam::radiationModels::fvDOM::calculate() |
Firstly, a correct function is called. Here, the absorptionEmission_ actually points to greyMeanCombustion which is a subclass of greyMean. And greyMean is a subclass of absorptionEmissionModel. But in both grepMean and greyMeanCombustion have no function called correct. Therefore, the correct function in absorptionEmissionModel will be called:
1 | void Foam::radiationModels::absorptionEmissionModel::correct |
The thing is weird here, only the first element of aLambda_ which corresponds to parameter aj has been initialized by a_ corresponding to parameter a. If only one wave length is considered in the simulation, this will not influence the result. Then we call updateBlackBodyEmission() which is defined in fvDOM.C:
1 | void Foam::radiationModels::fvDOM::updateBlackBodyEmission() |
The nLambda_ here is one in this simulation. The blackBody_ is a member of the fvDOM class which has a type of blackBodyEmission. It has been initialized in the constructor of fvDOM as blackBody_(nLambda_, T),. This will call the constructor of the blackBodyEmission:
1 | Foam::radiationModels::blackBodyEmission::blackBodyEmission |
Here the bLambda_ which is the black body emission energy field for each wavelength will be initialized as physicoChemical::sigma*pow4(T), updateBlackBodyEmission() function. The correct function of blackBody_ will be called:
1 | void Foam::radiationModels::blackBodyEmission::correct |
In this correct function, the member bLambda_ of blackBody_ will be assigned with EbDeltaLambdaT(T_, band) at wave length lambdaI. The EbDeltaLambdaT function is defined as:
1 | Foam::tmp<Foam::volScalarField> |
Because here the band we transfer trace from absorptionEmission_->bands(j) in updateBlackBodyEmission function, it means we have to see what band(j) is. In both greyMean and greyMeanCombustion class (greyMean is a subclass of absorptionEmissionModel and greyMeanCombustion is a subclass of greyMean), there is no function called bands, but in absorptionEmissionModel, this bands function exists. Therefore, the bands function called here is the one in the absorptionEmissionModel:
1 | const Foam::Vector2D<Foam::scalar>& |
As the returned value is Vector2D<scalar>::one, the if{} will no be called in the EbDeltaLambdaT. Therefore the return value from the EbDeltaLambdaT function here is the Eb, which is equal to physicoChemical::sigma*pow4(T) (Vector2D<scalar>::one:
1 | Vector2D<scalar>::one // returns (1 1) |
Let’s go back to the calculate function of fvDOM. A list of bool with a size of number of rays is defined for convergence check of each radiation ray. The maximum residual maxResidual and a counter for iterations radIter are also defined. These 2 values will be compared with the tolerance and maxIter we set in the radationPrperties file to see if the calculation is converged.
1 | // Set rays converged false |
In the following do while loop, a condition maxResidual > tolerance_ && radIter < maxIter_ has been set. If the calculation is converged or it reaches the user defined maximum iterator times.
1 | do |
Inside the loop, for each radiation ray IRay_[rayI], the correct function will be called. IRay_ is a pointer list with a type of radiativeIntensityRay. Here, IRay_[rayI] dereference the rayI‘s pointer and call the correct() function of the radiativeIntensityRay class:
1 | Foam::scalar Foam::radiationModels::radiativeIntensityRay::correct() |
The mesh_.Sf() returns the surface area vector of the cell faces. Here, const surfaceScalarField Ji(dAve_ & mesh_.Sf()); calculate the vector dot product of dAve_ and mesh_.Sf() as a result of Ji. Then, solve the RTE for radiation ray in direction rayI and wave length lambdaI. Here, we will translate the code of the equation to mathematic equation:
, where
It can be found that the particle scattering factor correct() function, the maximum residual of the calculation will also be returned in the end to see if the residual convergence is reached. It should be noted that in the calculate function of the fvDOM class, such a loop will select only the unconverged ray equation to solve. If the ray we are solving is converged, in the rayIdConv, its value will become true and will not be solved again. After this do while loop, the last function to call is the updateG():
1 | void Foam::radiationModels::fvDOM::updateG() |
The G_ is the Incident radiation, which is as same as the G_ in P1 model. Before assigning value to the variables, all of them are initialized to zero with certain dimension. Firstly, addIntensity() is called:
1 | void Foam::radiationModels::radiativeIntensityRay::addIntensity() |
Here, the total radiative intensity I_ will be calculated as a summation over all wave length. G_ is calculated as a summation of IRay_[rayI].I()*IRay_[rayI].omega() where I() is defined as:
1 | inline const Foam::volScalarField& |
and omega() is defined as:
1 | inline Foam::scalar Foam::radiationModels::radiativeIntensityRay::omega() const |
Therefore G_ is actually calculated as:
The qr_, qin, andqem are total radiative heat flux on boundary, incident radiative heat flux on boundary, and emitted radiative heat flux on boundary. These values will be updated by a summation over different rays on the boundaries. Till here, the calculate() function in Foam::radiationModel::correct() is finished. Then the radiation->Sh(thermo, he) will be added to the energy equation. As same as we introduced in P1 model previously, the Sh function only exits in the radiationModel class:
1 | Foam::tmp<Foam::fvScalarMatrix> Foam::radiationModel::Sh |
The only difference is how we calculate the Ru() and Rp(). In fvDOM, Ru() is calculated as:
1 | Foam::tmp<Foam::volScalarField> Foam::radiationModels::fvDOM::Rp() const |
The function deltaLambdaT is defined as in class blackBodyEmission:
1 | Foam::tmp<Foam::volScalarField> |
We only have one band, there for deltaLambdaT is one. Rp() is calculated as the following equation:
The definition of Ru is:
1 | Foam::tmp<Foam::DimensionedField<Foam::scalar, Foam::volMesh>> |
The code follows equation:
Then we can substitute the Ru() and Rp() into the Sh and add them to the energy equation as a source term.