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.