comfort.C
Go to the documentation of this file.
1 /*---------------------------------------------------------------------------*\
2  ========= |
3  \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
4  \\ / O peration |
5  \\ / A nd | www.openfoam.com
6  \\/ M anipulation |
7 -------------------------------------------------------------------------------
8  Copyright (C) 2019 OpenFOAM Foundation
9  Copyright (C) 2021-2025 OpenCFD Ltd.
10 -------------------------------------------------------------------------------
11 License
12  This file is part of OpenFOAM.
13 
14  OpenFOAM is free software: you can redistribute it and/or modify it
15  under the terms of the GNU General Public License as published by
16  the Free Software Foundation, either version 3 of the License, or
17  (at your option) any later version.
18 
19  OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
20  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
21  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
22  for more details.
23 
24  You should have received a copy of the GNU General Public License
25  along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
26 
27 \*---------------------------------------------------------------------------*/
28 
29 #include "comfort.H"
30 #include "wallFvPatch.H"
32 
33 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
34 
35 namespace Foam
36 {
37 namespace functionObjects
38 {
39  defineTypeNameAndDebug(comfort, 0);
40  addToRunTimeSelectionTable(functionObject, comfort, dictionary);
41 }
42 }
43 
44 
45 // Temperature bounds based on EN ISO 7730 (10 - 40 degC)
46 static const Foam::scalarMinMax Tbounds(283.15, 313.15);
47 
48 
49 // * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
50 
51 Foam::tmp<Foam::volScalarField> Foam::functionObjects::comfort::magU() const
52 {
53  tmp<volScalarField> tmagU = mag(lookupObject<volVectorField>("U"));
54  volScalarField& magU = tmagU.ref();
55 
56  // Flag to use the averaged velocity field in the domain.
57  // Consistent with EN ISO 7730 but does not make physical sense
58  if (meanVelocity_)
59  {
60  magU = magU.weightedAverage(mesh_.V());
61  }
62 
63  return tmagU;
64 }
65 
66 
67 Foam::dimensionedScalar Foam::functionObjects::comfort::Trad() const
68 {
69  dimensionedScalar Trad(Trad_);
70 
71  // The mean radiation is calculated by the mean wall temperatures
72  // which are summed and divided by the area | only walls are taken into
73  // account. This approach might be correct for a squared room but will
74  // defintely be inconsistent for complex room geometries. The norm does
75  // not provide any information about the calculation of this quantity.
76  if (!TradSet_)
77  {
78  const volScalarField::Boundary& TBf =
79  lookupObject<volScalarField>("T").boundaryField();
80 
81  scalar areaIntegral = 0;
82  scalar TareaIntegral = 0;
83 
84  forAll(TBf, patchi)
85  {
86  const fvPatchScalarField& pT = TBf[patchi];
87  const fvPatch& pTBf = TBf[patchi].patch();
88  const scalarField& pSf = pTBf.magSf();
89 
90  if (isType<wallFvPatch>(pTBf))
91  {
92  areaIntegral += sum(pSf);
93  TareaIntegral += weightedSum(pSf, pT);
94  }
95  }
96 
97  reduce(TareaIntegral, sumOp<scalar>());
98  reduce(areaIntegral, sumOp<scalar>());
99 
100  Trad.value() = TareaIntegral/(areaIntegral + VSMALL);
101  }
102 
103  // Bounds based on EN ISO 7730
104  if (!Tbounds.contains(Trad.value()))
105  {
107  << "The calculated mean wall radiation temperature is out of the\n"
108  << "bounds specified in EN ISO 7730:2005\n"
109  << "Valid range is 10 degC < T < 40 degC\n"
110  << "The actual value is: " << (Trad.value() - 273.15) << nl << endl;
111  }
112 
113  return Trad;
114 }
115 
116 
117 Foam::tmp<Foam::volScalarField> Foam::functionObjects::comfort::pSat() const
118 {
119  static const dimensionedScalar kPaToPa(dimPressure, 1000);
120  static const dimensionedScalar A(dimless, 16.6563);
121  static const dimensionedScalar B(dimTemperature, 4030.183);
122  static const dimensionedScalar C(dimTemperature, -38.15);
123 
124  tmp<volScalarField> tpSat = volScalarField::New("pSat", mesh_, pSat_);
125 
126  // Calculate the saturation pressure if no user input is given
127  if (pSat_.value() == 0)
128  {
129  const auto& T = lookupObject<volScalarField>("T");
130 
131  // Equation based on ISO 7730:2006
132  tpSat = kPaToPa*exp(A - B/(T + C));
133  }
134 
135  return tpSat;
136 }
137 
138 
139 Foam::tmp<Foam::volScalarField> Foam::functionObjects::comfort::Tcloth
140 (
141  volScalarField& hc,
142  const dimensionedScalar& metabolicRateSI,
143  const dimensionedScalar& extWorkSI,
144  const volScalarField& T,
145  const dimensionedScalar& Trad
146 )
147 {
148  const dimensionedScalar factor1(dimTemperature, 308.85);
149 
150  const dimensionedScalar factor2
151  (
152  dimTemperature/metabolicRateSI.dimensions(),
153  0.028
154  );
155 
156  const dimensionedScalar factor3
157  (
159  3.96e-8
160  );
161 
162  // Heat transfer coefficient based on forced convection [W/m^2/K]
163  const volScalarField hcForced
164  (
165  dimensionedScalar(hc.dimensions()/sqrt(dimVelocity), 12.1)
166  *sqrt(magU())
167  );
168 
169  // Tcl [K] (surface cloth temperature)
170  tmp<volScalarField> tTcl
171  (
173  (
174  "Tcl",
175  T.mesh(),
177  )
178  );
179  volScalarField& Tcl = tTcl.ref();
180 
181  // Initial guess
182  Tcl = T;
183 
184  label i = 0;
185 
186  Tcl.storePrevIter();
187 
188 
189  // Iterative solving of equation (2)
190  do
191  {
192  Tcl = (Tcl + Tcl.prevIter())/2;
193  Tcl.storePrevIter();
194 
195  // Heat transfer coefficient based on natural convection
196  volScalarField hcNatural
197  (
198  dimensionedScalar(hc.dimensions()/pow025(dimTemperature), 2.38)
199  *pow025(mag(Tcl - T))
200  );
201 
202  // Set heat transfer coefficient based on equation (3)
203  hc =
204  pos(hcForced - hcNatural)*hcForced
205  + neg0(hcForced - hcNatural)*hcNatural;
206 
207  // Calculate surface temperature based on equation (2)
208  Tcl =
209  factor1
210  - factor2*(metabolicRateSI - extWorkSI)
211  - Icl_*factor3*fcl_*(pow4(Tcl) - pow4(Trad))
212  - Icl_*fcl_*hc*(Tcl - T);
213 
214  // Make sure that Tcl is in some physical limit (same range as we used
215  // for the radiative estimation - based on ISO EN 7730:2005)
216  Tcl.clamp_range(Tbounds);
217 
218  } while (!converged(Tcl) && i++ < maxClothIter_);
219 
220  if (i == maxClothIter_)
221  {
223  << "The surface cloth temperature did not converge within " << i
224  << " iterations" << nl;
225  }
226 
227  return tTcl;
228 }
229 
230 
231 bool Foam::functionObjects::comfort::converged
232 (
233  const volScalarField& phi
234 ) const
235 {
236  return
237  max(mag(phi.primitiveField() - phi.prevIter().primitiveField()))
238  < tolerance_;
239 }
240 
241 
242 // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
243 
245 (
246  const word& name,
247  const Time& runTime,
248  const dictionary& dict
249 )
250 :
252  clothing_("clothing", dimless, 0),
253  metabolicRate_("metabolicRate", dimMass/pow3(dimTime), 0.8),
254  extWork_("extWork", dimMass/pow3(dimTime), 0),
255  Trad_("Trad", dimTemperature, 0),
256  relHumidity_("relHumidity", dimless, 0.5),
257  pSat_("pSat", dimPressure, 0),
258  Icl_("Icl", pow3(dimTime)*dimTemperature/dimMass, 0),
259  fcl_("fcl", dimless, 0),
260  tolerance_(1e-4),
261  maxClothIter_(100),
262  TradSet_(false),
263  meanVelocity_(false)
264 {
265  read(dict);
266 }
267 
268 
269 // * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
270 
272 {
274  {
275  clothing_.readIfPresent(dict);
276  metabolicRate_.readIfPresent(dict);
277  extWork_.readIfPresent(dict);
278  pSat_.readIfPresent(dict);
279  tolerance_ = dict.getOrDefault("tolerance", 1e-4);
280  maxClothIter_ = dict.getOrDefault("maxClothIter", 100);
281  meanVelocity_ = dict.getOrDefault<bool>("meanVelocity", false);
282 
283  // Read relative humidity if provided and convert from % to fraction
284  if (dict.found(relHumidity_.name()))
285  {
286  relHumidity_.read(dict);
287  relHumidity_ /= 100;
288  }
289 
290  // Read radiation temperature if provided
291  if (dict.found(Trad_.name()))
292  {
293  TradSet_ = true;
294  Trad_.read(dict);
295  }
296 
297  Icl_ = dimensionedScalar(Icl_.dimensions(), 0.155)*clothing_;
298 
299  fcl_.value() =
300  Icl_.value() <= 0.078
301  ? 1.0 + 1.290*Icl_.value()
302  : 1.05 + 0.645*Icl_.value();
303 
304  return true;
305  }
306 
307  return false;
308 }
309 
310 
312 {
313  // Assign and build fields
314  const dimensionedScalar Trad(this->Trad());
315  const volScalarField pSat(this->pSat());
316 
317  const dimensionedScalar metabolicRateSI(58.15*metabolicRate_);
318  const dimensionedScalar extWorkSI(58.15*extWork_);
319 
320  const auto& T = lookupObject<volScalarField>("T");
321 
322  // Heat transfer coefficient [W/m^2/K]
323  // This field is updated in Tcloth()
324  volScalarField hc
325  (
326  IOobject
327  (
328  "hc",
329  mesh_.time().timeName(),
330  mesh_
331  ),
332  mesh_,
334  );
335 
336  // Calculate the surface temperature of the cloth by an iterative
337  // process using equation (2) from DIN EN ISO 7730 [degC]
338  const volScalarField Tcloth
339  (
340  this->Tcloth
341  (
342  hc,
343  metabolicRateSI,
344  extWorkSI,
345  T,
346  Trad
347  )
348  );
349 
350  // Calculate the PMV quantity
351  const dimensionedScalar factor1(pow3(dimTime)/dimMass, 0.303);
352  const dimensionedScalar factor2
353  (
354  dimless/metabolicRateSI.dimensions(),
355  -0.036
356  );
357  const dimensionedScalar factor3(factor1.dimensions(), 0.028);
358  const dimensionedScalar factor4(dimLength/dimTime, 3.05e-3);
359  const dimensionedScalar factor5(dimPressure, 5733);
360  const dimensionedScalar factor6(dimTime/dimLength, 6.99);
361  const dimensionedScalar factor8(metabolicRateSI.dimensions(), 58.15);
362  const dimensionedScalar factor9(dimless/dimPressure, 1.7e-5);
363  const dimensionedScalar factor10(dimPressure, 5867);
364  const dimensionedScalar factor11(dimless/dimTemperature, 0.0014);
365  const dimensionedScalar factor12(dimTemperature, 307.15);
366  const dimensionedScalar factor13
367  (
369  3.96e-8
370  );
371 
372  const scalar factor7
373  (
374  // Special treatment of Term4
375  // if metaRate - extWork < factor8, set to zero
376  (metabolicRateSI - extWorkSI).value() < factor8.value() ? 0 : 0.42
377  );
378 
379  Info<< "Calculating the predicted mean vote (PMV)" << endl;
380 
381  // Equation (1)
382  tmp<volScalarField> PMV =
383  (
384  // Term1: Thermal sensation transfer coefficient
385  (factor1*exp(factor2*metabolicRateSI) + factor3)
386  *(
387  (metabolicRateSI - extWorkSI)
388 
389  // Term2: Heat loss difference through skin
390  - factor4
391  *(
392  factor5
393  - factor6*(metabolicRateSI - extWorkSI)
394  - pSat*relHumidity_
395  )
396 
397  // Term3: Heat loss through sweating
398  - factor7*(metabolicRateSI - extWorkSI - factor8)
399 
400  // Term4: Heat loss through latent respiration
401  - factor9*metabolicRateSI*(factor10 - pSat*relHumidity_)
402 
403  // Term5: Heat loss through dry respiration
404  - factor11*metabolicRateSI*(factor12 - T)
405 
406  // Term6: Heat loss through radiation
407  - factor13*fcl_*(pow4(Tcloth) - pow4(Trad))
408 
409  // Term7: Heat loss through convection
410  - fcl_*hc*(Tcloth - T)
411  )
412  );
413 
414  Info<< "Calculating the predicted percentage of dissatisfaction (PPD)"
415  << endl;
416 
417  // Equation (5)
418  tmp<volScalarField> PPD =
419  100 - 95*exp(-0.03353*pow4(PMV()) - 0.21790*sqr(PMV()));
420 
421  Info<< "Calculating the draught rating (DR)\n";
422 
423  const dimensionedScalar Umin(dimVelocity, 0.05);
424  const dimensionedScalar Umax(dimVelocity, 0.5);
425  const dimensionedScalar pre(dimless, 0.37);
426  const dimensionedScalar C1(dimVelocity, 3.14);
427 
428  // Limit the velocity field to the values given in EN ISO 7733
429  volScalarField Umag(mag(lookupObject<volVectorField>("U")));
430  Umag.clamp_range(Umin, Umax);
431 
432  // Calculate the turbulent intensity if turbulent kinetic energy field k
433  // exists
434  volScalarField TI
435  (
436  IOobject
437  (
438  "TI",
439  mesh_.time().timeName(),
440  mesh_
441  ),
442  mesh_,
444  );
445 
446  if (foundObject<volScalarField>("k"))
447  {
448  const auto& k = lookupObject<volScalarField>("k");
449  TI = sqrt(2/3*k)/Umag;
450  }
451 
452  // For unit correctness
453  const dimensionedScalar correctUnit
454  (
455  dimensionSet(0, -1.62, 1.62, -1, 0, 0, 0),
456  1
457  );
458 
459  // Equation (6)
460  tmp<volScalarField> DR =
461  correctUnit*(factor12 - T)*pow(Umag - Umin, 0.62)*(pre*Umag*TI + C1);
462 
463  // Calculate the operative temperature
464  tmp<volScalarField> Top = 0.5*(T + Trad);
465 
466  // Need modifiable field names:
467  word fieldNamePMV = "PMV";
468  word fieldNamePPD = "PPD";
469  word fieldNameDR = "DR";
470  word fieldNameTop = "Top";
471 
472  return
473  (
474  store(fieldNamePMV, PMV)
475  && store(fieldNamePPD, PPD)
476  && store(fieldNameDR, DR)
477  && store(fieldNameTop, Top)
478  );
479 }
480 
481 
483 {
484  return
485  (
486  writeObject("PMV")
487  && writeObject("PPD")
488  && writeObject("DR")
489  && writeObject("Top")
490  );
491 }
492 
493 
494 // ************************************************************************* //
void clamp_range(const dimensioned< MinMax< Type >> &range)
Clamp field values (in-place) to the specified range.
const Type & value() const noexcept
Return const reference to value.
dictionary dict
defineTypeNameAndDebug(ObukhovLength, 0)
dimensioned< typename typeOfMag< Type >::type > mag(const dimensioned< Type > &dt)
Type weightedSum(const UList< scalar > &weights, const UList< Type > &fld)
The local weighted sum (integral) of a field, using the mag() of the weights.
A list of keyword definitions, which are a keyword followed by a number of values (eg...
Definition: dictionary.H:129
const fvPatch & patch() const noexcept
Return the patch.
Definition: fvPatchField.H:250
dimensioned< Type > weightedAverage(const DimensionedField< scalar, GeoMesh > &weights, const label comm=UPstream::worldComm) const
Return the global weighted average.
label max(const labelHashSet &set, label maxValue=labelMin)
Find the max value in labelHashSet, optionally limited by second argument.
Definition: hashSets.C:40
dimensionedSymmTensor sqr(const dimensionedVector &dv)
constexpr char nl
The newline &#39;\n&#39; character (0x0a)
Definition: Ostream.H:50
engineTime & runTime
dimensionedScalar sqrt(const dimensionedScalar &ds)
Ostream & endl(Ostream &os)
Add newline and flush stream.
Definition: Ostream.H:529
dimensionedScalar pow025(const dimensionedScalar &ds)
GeometricBoundaryField< scalar, fvPatchField, volMesh > Boundary
Type of boundary fields.
label k
Boltzmann constant.
const dimensionSet dimless
Dimensionless.
const DimensionedField< scalar, volMesh > & V() const
Return cell volumes.
dimensioned< Type > sum(const DimensionedField< Type, GeoMesh > &f1, const label comm)
Class to control time during OpenFOAM simulations that is also the top-level objectRegistry.
Definition: Time.H:69
Macros for easy insertion into run-time selection tables.
bool read(const char *buf, int32_t &val)
Same as readInt32.
Definition: int32.H:127
void reduce(T &value, [[maybe_unused]] BinaryOp bop, [[maybe_unused]] const int tag=UPstream::msgType(), const int communicator=UPstream::worldComm)
Reduce inplace (cf. MPI Allreduce)
#define forAll(list, i)
Loop across all elements in list.
Definition: stdFoam.H:286
GeometricField< scalar, fvPatchField, volMesh > volScalarField
Definition: volFieldsFwd.H:72
dimensionedScalar pos(const dimensionedScalar &ds)
const dimensionedScalar e
Elementary charge.
Definition: createFields.H:11
word name(const expressions::valueTypeCode typeCode)
A word representation of a valueTypeCode. Empty for expressions::valueTypeCode::INVALID.
Definition: exprTraits.C:127
dimensionedScalar exp(const dimensionedScalar &ds)
fvPatchField< scalar > fvPatchScalarField
A class for handling words, derived from Foam::string.
Definition: word.H:63
Field< scalar > scalarField
Specialisation of Field<T> for scalar.
const dimensionSet dimTemperature(0, 0, 0, 1, 0, 0, 0)
Definition: dimensionSets.H:52
comfort(const word &name, const Time &runTime, const dictionary &dict)
Construct from Time and dictionary.
Definition: comfort.C:238
dimensionedScalar neg0(const dimensionedScalar &ds)
virtual bool execute()
Calculate the predicted mean vote (PMV) and predicted percentage dissatisfaction (PPD) fields...
Definition: comfort.C:304
virtual bool read(const dictionary &)
Read the data needed for the comfort calculation.
Definition: comfort.C:264
const dimensionSet & dimensions() const noexcept
Return const reference to dimensions.
const dimensionSet dimPressure
static tmp< GeometricField< scalar, fvPatchField, volMesh > > New(const word &name, IOobjectOption::registerOption regOpt, const Mesh &mesh, const dimensionSet &dims, const word &patchFieldType=fvPatchField< scalar >::calculatedType())
Return tmp field (NO_READ, NO_WRITE) from name, mesh, dimensions and patch type. [Takes current timeN...
volScalarField & C
addToRunTimeSelectionTable(functionObject, ObukhovLength, dictionary)
void T(FieldField< Field, Type > &f1, const FieldField< Field, Type > &f2)
dimensionedScalar pow(const dimensionedScalar &ds, const dimensionedScalar &expt)
dimensionedScalar pow3(const dimensionedScalar &ds)
#define WarningInFunction
Report a warning using Foam::Warning.
const dimensionSet dimLength(0, 1, 0, 0, 0, 0, 0)
Definition: dimensionSets.H:50
dimensioned< scalar > dimensionedScalar
Dimensioned scalar obtained from generic dimensioned type.
dimensionedScalar pow4(const dimensionedScalar &ds)
static const Foam::scalarMinMax Tbounds(283.15, 313.15)
const dimensionSet dimTime(0, 0, 1, 0, 0, 0, 0)
Definition: dimensionSets.H:51
messageStream Info
Information stream (stdout output on master, null elsewhere)
Internal & ref(const bool updateAccessTime=true)
Same as internalFieldRef()
virtual bool read(const dictionary &dict)
Read optional controls.
Specialization of Foam::functionObject for an Foam::fvMesh, providing a reference to the Foam::fvMesh...
const dimensionSet dimMass(1, 0, 0, 0, 0, 0, 0)
Definition: dimensionSets.H:49
A class for managing temporary objects.
Definition: HashPtrTable.H:50
virtual bool write()
Write the PPD and PMV fields.
Definition: comfort.C:475
static const Foam::dimensionedScalar A("", Foam::dimPressure, 611.21)
Defines the attributes of an object for which implicit objectRegistry management is supported...
Definition: IOobject.H:180
bool contains(const T &val) const
True if the value is within the range (inclusive check)
Definition: MinMaxI.H:220
const fvMesh & mesh_
Reference to the fvMesh.
static const Foam::dimensionedScalar B("", Foam::dimless, 18.678)
Namespace for OpenFOAM.
static constexpr const zero Zero
Global zero (0)
Definition: zero.H:127
const dimensionSet dimVelocity