/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2019 OpenFOAM Foundation
Copyright (C) 2021 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 "comfort.H"
#include "wallFvPatch.H"
#include "addToRunTimeSelectionTable.H"
// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
namespace Foam
{
namespace functionObjects
{
defineTypeNameAndDebug(comfort, 0);
addToRunTimeSelectionTable(functionObject, comfort, dictionary);
}
}
// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
Foam::tmp Foam::functionObjects::comfort::magU() const
{
tmp tmagU = mag(lookupObject("U"));
volScalarField& magU = tmagU.ref();
// Flag to use the averaged velocity field in the domain.
// Consistent with EN ISO 7730 but does not make physical sense
if (meanVelocity_)
{
magU = magU.weightedAverage(mesh_.V());
}
return tmagU;
}
Foam::dimensionedScalar Foam::functionObjects::comfort::Trad() const
{
dimensionedScalar Trad(Trad_);
// The mean radiation is calculated by the mean wall temperatures
// which are summed and divided by the area | only walls are taken into
// account. This approach might be correct for a squared room but will
// defintely be inconsistent for complex room geometries. The norm does
// not provide any information about the calculation of this quantity.
if (!TradSet_)
{
const volScalarField::Boundary& TBf =
lookupObject("T").boundaryField();
scalar areaIntegral = 0;
scalar TareaIntegral = 0;
forAll(TBf, patchi)
{
const fvPatchScalarField& pT = TBf[patchi];
const fvPatch& pTBf = TBf[patchi].patch();
const scalarField& pSf = pTBf.magSf();
if (isType(pTBf))
{
areaIntegral += gSum(pSf);
TareaIntegral += gSum(pSf*pT);
}
}
Trad.value() = TareaIntegral/areaIntegral;
}
// Bounds based on EN ISO 7730
if ((Trad.value() < 283.15) || (Trad.value() > 313.15))
{
WarningInFunction
<< "The calculated mean wall radiation temperature is out of the\n"
<< "bounds specified in EN ISO 7730:2005\n"
<< "Valid range is 10 degC < T < 40 degC\n"
<< "The actual value is: " << Trad - 273.15 << nl << endl;
}
return Trad;
}
Foam::tmp Foam::functionObjects::comfort::pSat() const
{
static const dimensionedScalar kPaToPa(dimPressure, 1000);
static const dimensionedScalar A(dimless, 16.6563);
static const dimensionedScalar B(dimTemperature, 4030.183);
static const dimensionedScalar C(dimTemperature, -38.15);
tmp tpSat = volScalarField::New("pSat", mesh_, pSat_);
// Calculate the saturation pressure if no user input is given
if (pSat_.value() == 0)
{
const auto& T = lookupObject("T");
// Equation based on ISO 7730:2006
tpSat = kPaToPa*exp(A - B/(T + C));
}
return tpSat;
}
Foam::tmp Foam::functionObjects::comfort::Tcloth
(
volScalarField& hc,
const dimensionedScalar& metabolicRateSI,
const dimensionedScalar& extWorkSI,
const volScalarField& T,
const dimensionedScalar& Trad
)
{
const dimensionedScalar factor1(dimTemperature, 308.85);
const dimensionedScalar factor2
(
dimTemperature/metabolicRateSI.dimensions(),
0.028
);
const dimensionedScalar factor3
(
dimMass/pow3(dimTime)/pow4(dimTemperature),
3.96e-8
);
// Heat transfer coefficient based on forced convection [W/m^2/K]
const volScalarField hcForced
(
dimensionedScalar(hc.dimensions()/sqrt(dimVelocity), 12.1)
*sqrt(magU())
);
// Tcl [K] (surface cloth temperature)
tmp tTcl
(
volScalarField::New
(
"Tcl",
T.mesh(),
dimTemperature
)
);
volScalarField& Tcl = tTcl.ref();
// Initial guess
Tcl = T;
label i = 0;
Tcl.storePrevIter();
// Same temperatures as for the radiation
const dimensionedScalar Tmin(dimTemperature, 283.15);
const dimensionedScalar Tmax(dimTemperature, 313.15);
// Iterative solving of equation (2)
do
{
Tcl = (Tcl + Tcl.prevIter())/2;
Tcl.storePrevIter();
// Heat transfer coefficient based on natural convection
volScalarField hcNatural
(
dimensionedScalar(hc.dimensions()/pow025(dimTemperature), 2.38)
*pow025(mag(Tcl - T))
);
// Set heat transfer coefficient based on equation (3)
hc =
pos(hcForced - hcNatural)*hcForced
+ neg0(hcForced - hcNatural)*hcNatural;
// Calculate surface temperature based on equation (2)
Tcl =
factor1
- factor2*(metabolicRateSI - extWorkSI)
- Icl_*factor3*fcl_*(pow4(Tcl) - pow4(Trad))
- Icl_*fcl_*hc*(Tcl - T);
// Make sure that Tcl is in some physical limit (same range as we used
// for the radiative estimation - based on ISO EN 7730:2005)
Tcl.clip(Tmin, Tmax);
} while (!converged(Tcl) && i++ < maxClothIter_);
if (i == maxClothIter_)
{
WarningInFunction
<< "The surface cloth temperature did not converge within " << i
<< " iterations" << nl;
}
return tTcl;
}
bool Foam::functionObjects::comfort::converged
(
const volScalarField& phi
) const
{
return
max(mag(phi.primitiveField() - phi.prevIter().primitiveField()))
< tolerance_;
}
// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
Foam::functionObjects::comfort::comfort
(
const word& name,
const Time& runTime,
const dictionary& dict
)
:
fvMeshFunctionObject(name, runTime, dict),
clothing_("clothing", dimless, 0),
metabolicRate_("metabolicRate", dimMass/pow3(dimTime), 0.8),
extWork_("extWork", dimMass/pow3(dimTime), 0),
Trad_("Trad", dimTemperature, 0),
relHumidity_("relHumidity", dimless, 0.5),
pSat_("pSat", dimPressure, 0),
Icl_("Icl", pow3(dimTime)*dimTemperature/dimMass, 0),
fcl_("fcl", dimless, 0),
tolerance_(1e-4),
maxClothIter_(100),
TradSet_(false),
meanVelocity_(false)
{
read(dict);
}
// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
bool Foam::functionObjects::comfort::read(const dictionary& dict)
{
if (fvMeshFunctionObject::read(dict))
{
clothing_.readIfPresent(dict);
metabolicRate_.readIfPresent(dict);
extWork_.readIfPresent(dict);
pSat_.readIfPresent(dict);
tolerance_ = dict.getOrDefault("tolerance", 1e-4);
maxClothIter_ = dict.getOrDefault("maxClothIter", 100);
meanVelocity_ = dict.getOrDefault("meanVelocity", false);
// Read relative humidity if provided and convert from % to fraction
if (dict.found(relHumidity_.name()))
{
relHumidity_.read(dict);
relHumidity_ /= 100;
}
// Read radiation temperature if provided
if (dict.found(Trad_.name()))
{
TradSet_ = true;
Trad_.read(dict);
}
Icl_ = dimensionedScalar(Icl_.dimensions(), 0.155)*clothing_;
fcl_.value() =
Icl_.value() <= 0.078
? 1.0 + 1.290*Icl_.value()
: 1.05 + 0.645*Icl_.value();
return true;
}
return false;
}
bool Foam::functionObjects::comfort::execute()
{
// Assign and build fields
const dimensionedScalar Trad(this->Trad());
const volScalarField pSat(this->pSat());
const dimensionedScalar metabolicRateSI(58.15*metabolicRate_);
const dimensionedScalar extWorkSI(58.15*extWork_);
const auto& T = lookupObject("T");
// Heat transfer coefficient [W/m^2/K]
// This field is updated in Tcloth()
volScalarField hc
(
IOobject
(
"hc",
mesh_.time().timeName(),
mesh_
),
mesh_,
dimensionedScalar(dimMass/pow3(dimTime)/dimTemperature, 0)
);
// Calculate the surface temperature of the cloth by an iterative
// process using equation (2) from DIN EN ISO 7730 [degC]
const volScalarField Tcloth
(
this->Tcloth
(
hc,
metabolicRateSI,
extWorkSI,
T,
Trad
)
);
// Calculate the PMV quantity
const dimensionedScalar factor1(pow3(dimTime)/dimMass, 0.303);
const dimensionedScalar factor2
(
dimless/metabolicRateSI.dimensions(),
-0.036
);
const dimensionedScalar factor3(factor1.dimensions(), 0.028);
const dimensionedScalar factor4(dimLength/dimTime, 3.05e-3);
const dimensionedScalar factor5(dimPressure, 5733);
const dimensionedScalar factor6(dimTime/dimLength, 6.99);
const dimensionedScalar factor8(metabolicRateSI.dimensions(), 58.15);
const dimensionedScalar factor9(dimless/dimPressure, 1.7e-5);
const dimensionedScalar factor10(dimPressure, 5867);
const dimensionedScalar factor11(dimless/dimTemperature, 0.0014);
const dimensionedScalar factor12(dimTemperature, 307.15);
const dimensionedScalar factor13
(
dimMass/pow3(dimTime)/pow4(dimTemperature),
3.96e-8
);
const scalar factor7
(
// Special treatment of Term4
// if metaRate - extWork < factor8, set to zero
(metabolicRateSI - extWorkSI).value() < factor8.value() ? 0 : 0.42
);
Info<< "Calculating the predicted mean vote (PMV)" << endl;
// Equation (1)
tmp PMV =
(
// Term1: Thermal sensation transfer coefficient
(factor1*exp(factor2*metabolicRateSI) + factor3)
*(
(metabolicRateSI - extWorkSI)
// Term2: Heat loss difference through skin
- factor4
*(
factor5
- factor6*(metabolicRateSI - extWorkSI)
- pSat*relHumidity_
)
// Term3: Heat loss through sweating
- factor7*(metabolicRateSI - extWorkSI - factor8)
// Term4: Heat loss through latent respiration
- factor9*metabolicRateSI*(factor10 - pSat*relHumidity_)
// Term5: Heat loss through dry respiration
- factor11*metabolicRateSI*(factor12 - T)
// Term6: Heat loss through radiation
- factor13*fcl_*(pow4(Tcloth) - pow4(Trad))
// Term7: Heat loss through convection
- fcl_*hc*(Tcloth - T)
)
);
Info<< "Calculating the predicted percentage of dissatisfaction (PPD)"
<< endl;
// Equation (5)
tmp PPD =
100 - 95*exp(-0.03353*pow4(PMV()) - 0.21790*sqr(PMV()));
Info<< "Calculating the draught rating (DR)\n";
const dimensionedScalar Umin(dimVelocity, 0.05);
const dimensionedScalar Umax(dimVelocity, 0.5);
const dimensionedScalar pre(dimless, 0.37);
const dimensionedScalar C1(dimVelocity, 3.14);
// Limit the velocity field to the values given in EN ISO 7733
volScalarField Umag(mag(lookupObject("U")));
Umag.clip(Umin, Umax);
// Calculate the turbulent intensity if turbulent kinetic energy field k
// exists
volScalarField TI
(
IOobject
(
"TI",
mesh_.time().timeName(),
mesh_
),
mesh_,
dimensionedScalar(dimless, 0)
);
if (foundObject("k"))
{
const auto& k = lookupObject("k");
TI = sqrt(2/3*k)/Umag;
}
// For unit correctness
const dimensionedScalar correctUnit
(
dimensionSet(0, -1.62, 1.62, -1, 0, 0, 0),
1
);
// Equation (6)
tmp DR =
correctUnit*(factor12 - T)*pow(Umag - Umin, 0.62)*(pre*Umag*TI + C1);
// Calculate the operative temperature
tmp Top = (T + Trad)/2;
// Workaround
word fieldNamePMV = "PMV";
word fieldNamePPD = "PPD";
word fieldNameDR = "DR";
word fieldNameTop = "Top";
return
store(fieldNamePMV, PMV)
&& store(fieldNamePPD, PPD)
&& store(fieldNameDR, DR)
&& store(fieldNameDR, Top);
}
bool Foam::functionObjects::comfort::write()
{
return
writeObject("PMV")
&& writeObject("PPD")
&& writeObject("DR")
&& writeObject("Top");
}
// ************************************************************************* //