/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2024 OpenCFD Ltd.
-------------------------------------------------------------------------------
License
This file is part of OpenFOAM.
OpenFOAM is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with OpenFOAM. If not, see .
\*---------------------------------------------------------------------------*/
#include "FriedrichModel.H"
#include "processorFaPatch.H"
#include "unitConversion.H"
#include "addToRunTimeSelectionTable.H"
// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
namespace Foam
{
namespace filmSeparationModels
{
defineTypeNameAndDebug(FriedrichModel, 0);
addToRunTimeSelectionTable(filmSeparationModel, FriedrichModel, dictionary);
const Foam::Enum
<
FriedrichModel::separationType
>
FriedrichModel::separationTypeNames
({
{ separationType::FULL, "full" },
{ separationType::PARTIAL , "partial" },
});
// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
bitSet FriedrichModel::calcCornerEdges() const
{
bitSet cornerEdges(mesh().nEdges(), false);
const areaVectorField& faceCentres = mesh().areaCentres();
const areaVectorField& faceNormals = mesh().faceAreaNormals();
const labelUList& own = mesh().edgeOwner();
const labelUList& nbr = mesh().edgeNeighbour();
// Check if internal face-normal vectors diverge (no separation)
// or converge (separation may occur)
forAll(nbr, edgei)
{
const label faceO = own[edgei];
const label faceN = nbr[edgei];
cornerEdges[edgei] = isCornerEdgeSharp
(
faceCentres[faceO],
faceCentres[faceN],
faceNormals[faceO],
faceNormals[faceN]
);
}
// Skip the rest of the routine if the simulation is a serial run
if (!Pstream::parRun()) return cornerEdges;
// Check if processor face-normal vectors diverge (no separation)
// or converge (separation may occur)
const faBoundaryMesh& patches = mesh().boundary();
for (const faPatch& fap : patches)
{
if (isA(fap))
{
const label patchi = fap.index();
const auto& edgeFaces = fap.edgeFaces();
const label internalEdgei = fap.start();
const auto& faceCentresp = faceCentres.boundaryField()[patchi];
const auto& faceNormalsp = faceNormals.boundaryField()[patchi];
forAll(faceNormalsp, bndEdgei)
{
const label faceO = edgeFaces[bndEdgei];
const label meshEdgei = internalEdgei + bndEdgei;
cornerEdges[meshEdgei] = isCornerEdgeSharp
(
faceCentres[faceO],
faceCentresp[bndEdgei],
faceNormals[faceO],
faceNormalsp[bndEdgei]
);
}
}
}
return cornerEdges;
}
bool FriedrichModel::isCornerEdgeSharp
(
const vector& faceCentreO,
const vector& faceCentreN,
const vector& faceNormalO,
const vector& faceNormalN
) const
{
// Calculate the relative position of centres of faces sharing an edge
const vector relativePosition(faceCentreN - faceCentreO);
// Calculate the relative normal of faces sharing an edge
const vector relativeNormal(faceNormalN - faceNormalO);
// Return true if the face normals converge, meaning that the edge is sharp
return ((relativeNormal & relativePosition) < -1e-8);
}
scalarList FriedrichModel::calcCornerAngles() const
{
scalarList cornerAngles(mesh().nEdges(), Zero);
const areaVectorField& faceNormals = mesh().faceAreaNormals();
const labelUList& own = mesh().edgeOwner();
const labelUList& nbr = mesh().edgeNeighbour();
// Process internal edges
forAll(nbr, edgei)
{
if (!cornerEdges_[edgei]) continue;
const label faceO = own[edgei];
const label faceN = nbr[edgei];
cornerAngles[edgei] = calcCornerAngle
(
faceNormals[faceO],
faceNormals[faceN]
);
}
// Skip the rest of the routine if the simulation is a serial run
if (!Pstream::parRun()) return cornerAngles;
// Process processor edges
const faBoundaryMesh& patches = mesh().boundary();
for (const faPatch& fap : patches)
{
if (isA(fap))
{
const label patchi = fap.index();
const auto& edgeFaces = fap.edgeFaces();
const label internalEdgei = fap.start();
const auto& faceNormalsp = faceNormals.boundaryField()[patchi];
forAll(faceNormalsp, bndEdgei)
{
const label faceO = edgeFaces[bndEdgei];
const label meshEdgei = internalEdgei + bndEdgei;
if (!cornerEdges_[meshEdgei]) continue;
cornerAngles[meshEdgei] = calcCornerAngle
(
faceNormals[faceO],
faceNormalsp[bndEdgei]
);
}
}
}
return cornerAngles;
}
scalar FriedrichModel::calcCornerAngle
(
const vector& faceNormalO,
const vector& faceNormalN
) const
{
const scalar magFaceNormal = mag(faceNormalO)*mag(faceNormalN);
// Avoid any potential exceptions during the cosine calculations
if (magFaceNormal < SMALL) return 0;
scalar cosAngle = (faceNormalO & faceNormalN)/magFaceNormal;
cosAngle = clamp(cosAngle, -1, 1);
return std::acos(cosAngle);
}
bitSet FriedrichModel::calcSeparationFaces() const
{
bitSet separationFaces(mesh().faces().size(), false);
const edgeScalarField& phis = film().phi2s();
const labelUList& own = mesh().edgeOwner();
const labelUList& nbr = mesh().edgeNeighbour();
// Process internal faces
forAll(nbr, edgei)
{
if (!cornerEdges_[edgei]) continue;
const label faceO = own[edgei];
const label faceN = nbr[edgei];
isSeparationFace
(
separationFaces,
phis[edgei],
faceO,
faceN
);
}
// Skip the rest of the routine if the simulation is a serial run
if (!Pstream::parRun()) return separationFaces;
// Process processor faces
const faBoundaryMesh& patches = mesh().boundary();
for (const faPatch& fap : patches)
{
if (isA(fap))
{
const label patchi = fap.index();
const auto& edgeFaces = fap.edgeFaces();
const label internalEdgei = fap.start();
const auto& phisp = phis.boundaryField()[patchi];
forAll(phisp, bndEdgei)
{
const label faceO = edgeFaces[bndEdgei];
const label meshEdgei(internalEdgei + bndEdgei);
if (!cornerEdges_[meshEdgei]) continue;
isSeparationFace
(
separationFaces,
phisp[bndEdgei],
faceO
);
}
}
}
return separationFaces;
}
void FriedrichModel::isSeparationFace
(
bitSet& separationFaces,
const scalar phiEdge,
const label faceO,
const label faceN
) const
{
const scalar tol = 1e-8;
// Assuming there are no sources/sinks at the edge
if (phiEdge > tol) // From owner to neighbour
{
separationFaces[faceO] = true;
}
else if ((phiEdge < -tol) && (faceN != -1)) // From neighbour to owner
{
separationFaces[faceN] = true;
}
}
scalarList FriedrichModel::calcSeparationAngles
(
const bitSet& separationFaces
) const
{
scalarList separationAngles(mesh().faces().size(), Zero);
const labelUList& own = mesh().edgeOwner();
const labelUList& nbr = mesh().edgeNeighbour();
// Process internal faces
forAll(nbr, edgei)
{
if (!cornerEdges_[edgei]) continue;
const label faceO = own[edgei];
const label faceN = nbr[edgei];
if (separationFaces[faceO])
{
separationAngles[faceO] = cornerAngles_[edgei];
}
if (separationFaces[faceN])
{
separationAngles[faceN] = cornerAngles_[edgei];
}
}
// Skip the rest of the routine if the simulation is a serial run
if (!Pstream::parRun()) return separationAngles;
// Process processor faces
const edgeScalarField& phis = film().phi2s();
const faBoundaryMesh& patches = mesh().boundary();
for (const faPatch& fap : patches)
{
if (isA(fap))
{
const label patchi = fap.index();
const auto& edgeFaces = fap.edgeFaces();
const label internalEdgei = fap.start();
const auto& phisp = phis.boundaryField()[patchi];
forAll(phisp, bndEdgei)
{
const label faceO = edgeFaces[bndEdgei];
const label meshEdgei(internalEdgei + bndEdgei);
if (!cornerEdges_[meshEdgei]) continue;
if (separationFaces[faceO])
{
separationAngles[faceO] = cornerAngles_[meshEdgei];
}
}
}
}
return separationAngles;
}
tmp FriedrichModel::Fratio() const
{
const areaVectorField Up(film().Up());
const areaVectorField& Uf = film().Uf();
const areaScalarField& h = film().h();
const areaScalarField& rho = film().rho();
const areaScalarField& mu = film().mu();
const areaScalarField& sigma = film().sigma();
// Identify the faces where separation may occur
const bitSet separationFaces(calcSeparationFaces());
// Calculate the corner angles corresponding to the separation faces
const scalarList separationAngles(calcSeparationAngles(separationFaces));
// Initialize the force ratio
auto tFratio = tmp::New(mesh().faces().size(), Zero);
auto& Fratio = tFratio.ref();
// Process internal faces
forAll(separationFaces, i)
{
// Skip the routine if the face is not a candidate for separation
if (!separationFaces[i]) continue;
// Calculate the corner-angle trigonometric values
const scalar sinAngle = std::sin(separationAngles[i]);
const scalar cosAngle = std::cos(separationAngles[i]);
// Reynolds number (FLW:Eq. 16)
const scalar Re = h[i]*mag(Uf[i])*rho[i]/mu[i];
// Weber number (FLW:Eq. 17)
const scalar We =
h[i]*rhop_*sqr(mag(Up[i]) - mag(Uf[i]))/(2.0*sigma[i]);
// Characteristic breakup length (FLW:Eq. 15)
const scalar Lb =
0.0388*Foam::sqrt(h[i])*Foam::pow(Re, 0.6)*Foam::pow(We, -0.5);
// Force ratio - denominator (FLW:Eq. 20)
const scalar den =
sigma[i]*(sinAngle + 1.0) + rho[i]*magG_*h[i]*Lb*cosAngle;
if (mag(den) > 0)
{
// Force ratio (FLW:Eq. 20)
Fratio[i] = rho[i]*sqr(mag(Uf[i]))*h[i]*sinAngle/den;
}
}
// Skip the rest of the routine if the simulation is a serial run
if (!Pstream::parRun()) return tFratio;
// Process processor faces
const faBoundaryMesh& patches = mesh().boundary();
for (const faPatch& fap : patches)
{
if (isA(fap))
{
const label patchi = fap.index();
const label internalEdgei = fap.start();
const auto& hp = h.boundaryField()[patchi];
const auto& Ufp = Uf.boundaryField()[patchi];
const auto& Upp = Up.boundaryField()[patchi];
const auto& rhop = rho.boundaryField()[patchi];
const auto& sigmap = sigma.boundaryField()[patchi];
const auto& mup = mu.boundaryField()[patchi];
forAll(hp, i)
{
// Skip the routine if the face is not a candidate for separation
if (!separationFaces[i]) continue;
const label meshEdgei = internalEdgei + i;
// Calculate the corner-angle trigonometric values
const scalar sinAngle = std::sin(cornerAngles_[meshEdgei]);
const scalar cosAngle = std::cos(cornerAngles_[meshEdgei]);
// Reynolds number (FLW:Eq. 16)
const scalar Re = hp[i]*mag(Ufp[i])*rhop[i]/mup[i];
// Weber number (FLW:Eq. 17)
const scalar We =
hp[i]*rhop_*sqr(mag(Upp[i]) - mag(Ufp[i]))/(2.0*sigmap[i]);
// Characteristic breakup length (FLW:Eq. 15)
const scalar Lb =
0.0388*Foam::sqrt(hp[i])
*Foam::pow(Re, 0.6)*Foam::pow(We, -0.5);
// Force ratio - denominator (FLW:Eq. 20)
const scalar den =
sigmap[i]*(sinAngle + 1.0)
+ rhop[i]*magG_*hp[i]*Lb*cosAngle;
if (mag(den) > 0)
{
// Force ratio (FLW:Eq. 20)
Fratio[i] = rhop[i]*sqr(mag(Ufp[i]))*hp[i]*sinAngle/den;
}
}
}
}
return tFratio;
}
// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
FriedrichModel::FriedrichModel
(
const regionModels::areaSurfaceFilmModels::liquidFilmBase& film,
const dictionary& dict
)
:
filmSeparationModel(film, dict),
separation_
(
separationTypeNames.getOrDefault
(
"separationType",
dict,
separationType::FULL
)
),
rhop_(dict.getScalar("rhop")),
magG_(mag(film.g().value())),
C0_(dict.getOrDefault("C0", 0.882)),
C1_(dict.getOrDefault("C1", -1.908)),
C2_(dict.getOrDefault("C2", 1.264)),
cornerEdges_(calcCornerEdges()),
cornerAngles_(calcCornerAngles())
{
if (rhop_ < VSMALL)
{
FatalIOErrorInFunction(dict)
<< "Primary-phase density, rhop: " << rhop_ << " must be non-zero."
<< abort(FatalIOError);
}
if (mag(C2_) < VSMALL)
{
FatalIOErrorInFunction(dict)
<< "Empirical constant, C2 = " << C2_ << "cannot be zero."
<< abort(FatalIOError);
}
}
// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
tmp FriedrichModel::separatedMassRatio() const
{
tmp tFratio = Fratio();
const auto& Fratio = tFratio.cref();
// Initialize the mass ratio of film separation
auto tseparated = tmp::New(mesh().faces().size(), Zero);
auto& separated = tseparated.ref();
switch (separation_)
{
case separationType::FULL:
{
forAll(Fratio, i)
{
if (Fratio[i] > 1)
{
separated[i] = 1;
}
}
break;
}
case separationType::PARTIAL:
{
forAll(Fratio, i)
{
if (Fratio[i] > 1)
{
// (ZJD:Eq. 16)
separated[i] = C0_ + C1_*Foam::exp(-Fratio[i]/C2_);
}
}
break;
}
default:
break; // This should not happen.
}
if (debug && mesh().time().writeTime())
{
{
areaScalarField areaFratio
(
mesh().newIOobject("Fratio"),
mesh(),
dimensionedScalar(dimForce, Zero)
);
areaFratio.primitiveFieldRef() = Fratio;
areaFratio.write();
}
}
return tseparated;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
} // End namespace filmSeparationModels
} // End namespace Foam
// ************************************************************************* //