pyhydronic 2.0.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hydronic/comfort.py ADDED
@@ -0,0 +1,820 @@
1
+ #
2
+ # Hydronic -- calculations for modeling heated structures
3
+ #
4
+ # Copyright (c) 2006-2026, Perry Kundert
5
+ #
6
+ # Hydronic is free software: you can redistribute it and/or modify it under the
7
+ # terms of the GNU General Public License as published by the Free Software
8
+ # Foundation, either version 3 of the License, or (at your option) any later
9
+ # version. See the LICENSE file at the top of the source tree.
10
+ #
11
+ # Hydronic is distributed in the hope that it will be useful, but WITHOUT ANY
12
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14
+ #
15
+
16
+ """Fanger's Comfort Equation: Predicted Mean Vote (PMV) and Percentage Dissatisfied (PPD).
17
+
18
+ All units are SI: temperatures in degrees C, air velocity in m/s, vapour pressure in Pa,
19
+ metabolism in met (or W/m^2) and clothing in clo (or m^2K/W).
20
+
21
+ """
22
+
23
+ from __future__ import absolute_import, division, print_function
24
+
25
+ import math
26
+
27
+ from .misc import Error, scale
28
+
29
+
30
+ class fanger( object ):
31
+ """Implements Fanger's Basic Comfort Equation.
32
+
33
+ A good discussion of its applicability is:
34
+
35
+ http://www.nrc-cnrc.gc.ca/obj/irc/doc/pubs/rr/rr162/rr162.pdf, now:
36
+ https://nrc-publications.canada.ca/eng/view/ft/?id=7525d344-a508-4fdc-9c04-d9d3a9767bdb
37
+
38
+ A derivation of this equation is detailed (somewhat incompletely) in:
39
+
40
+ http://www.lumasenseinc.com/uploads/News/Articles/English/Technical_Review_No_2-1982_-Thermal_Comfort.pdf, now:
41
+ https://www.bksv.com/media/doc/TechnicalReview1982-2.pdf, "Thermal Comfort", page 3
42
+
43
+ The equation depends on humidity, air temperature and velocity, average radiant temperature,
44
+ clothing level and metabolic level. Metabolic rate is described in either 'met', or raw W/m^2:
45
+
46
+ Metabolism, M (met): (W/m^2)
47
+ Activity met M
48
+ -------- --- -----
49
+ Lying down 0.8 47
50
+ Seated quietly 1.0 58
51
+ Sedentary activity (office, school) 1.2 70
52
+ Standing, relaxed 1.2 70
53
+ Light activity, standing (shopping, lab, lt. ind.) 1.6 93
54
+ Medium activity, standing (shop asst., dom. work) 2.0 117
55
+ High activity (heavy machine, garage work) 3.0 175
56
+ Running 9.0 522
57
+
58
+ The insulating (and radiating) value of clothing is expressed in 'clo', or raw m^2K/W:
59
+
60
+ Clothing, I_cl (clo): (m^2K/W)
61
+ Clothing Combination clo I_cl
62
+ -------------------- --- ------
63
+ Naked 0 0
64
+ Shorts 0.1 0.016
65
+ Tropical 0.3 0.047
66
+ Light Summer (short sleeves, light pants) 0.5 0.078
67
+ Working Clothes (cotton shirt + jeans) 0.8 0.124
68
+ Typical indoor winter (shirt + sweater) 1.0 0.155
69
+ Heavy business suit 1.5 0.233
70
+
71
+ Fanger's Comfort Equation:
72
+
73
+ (M-W)
74
+ - 3.05 x 10^-3 x {5733 - 6.99 x (M-W) - P_a}
75
+ - 0.42 { (M-W) - 58.15 }
76
+ - 1.7 x 10^-5 x M x (5867 - P_a)
77
+ - 0.0014 x M x (34 - t_a)
78
+ = 3.96 x 10^-8 x f_cl { (t_cl + 273.15)^4 - (t_r + 273.15)^4 }
79
+ + C
80
+ where:
81
+ M: Metabolism, in met (converted to W/m^2)
82
+ W: External work performed (subtracted from M, usually 0.)
83
+ t_a: Air temperature (dec. C)
84
+ t_s: Temperature of skin (deg. C)
85
+ t_s = 35.7 - 0.028 (M-W)
86
+ E_sw: Sweat loss (W/m^2)
87
+ E_sw = 0.42 { (M-W) - 58.15 } #
88
+ E_res= 0.0014 M(34 - t_a) # Heat loss from respiration
89
+
90
+ C: heat exchange by convection (W/m^2)
91
+ C = f_cl x h_c x (t_cl - t_a)
92
+ t_cl =
93
+ t_s - 0.155 x clo [
94
+ (M-W)
95
+ - 3.05 x 10^-3 x {5733 - 6.99 x (M-W) - p_a}
96
+ - E_sw
97
+ - 1.7 x 10^-5 x M (5867 - p_a)
98
+ - 0.0014 x M x (34 - t_a) ]
99
+
100
+ h_c: Convective heat transfer (W/M^2 K)
101
+ h_c = A) 2.38 x (t_cl - t_a) ^ 0.25 # (for A > B); free convection
102
+ h_c = B) 12.1 x sqrt( v_ar ) # (for A < B); forced convection
103
+
104
+ f_cl: Clothing area factor
105
+ f_cl = A) 1.00 + 0.2 x clo # for clo <= 0.5
106
+ f_cl = B) 1.05 + 0.1 x clo # for clo > 0.5
107
+
108
+ From B. A. McIntyre's Bioengineering, thermal physiology, and comfort, Volume 10, pp.198, there
109
+ are simplifications of the Fanger equations that work within certain constraints:
110
+
111
+ For M < 150 W/m^2 (met ~2.5), and I < 0.233 m^2 K/W (clo ~1.5), this simplification
112
+ (for a target effective temperature) yielded error < 0.5K.
113
+
114
+ t_sub = 33.5 - 3 x I_cl - (0.08 + 0.05 x I_cl) x M
115
+
116
+ and, where t_a and t_r are different, for v < 0.1 m/s:
117
+
118
+ t_sub = 0.56 x t_a + 0.44 x t_r
119
+
120
+ for v > 0.1 m/s (high air velocity), a much more complex equation:
121
+
122
+ t_sub = 0.44 x t_r + (5 - (10 x v)^0.5 x (5 - t_a))/(0.44 + 0.56 x (10 x v)^0.5)
123
+
124
+ So for a typical residence, the lower complexity formula will suffice. Later in
125
+ B. A. McIntyre's Bioengineering book, on pp. 210, he also reveals that once the perceived
126
+ temperature is correct, changes in humidity do little to affect comfort. In fact, "...a change
127
+ in relative humidity from 20% to 75% would reduce the preferred temperature by 1K." So, using
128
+ the simplified equation (and ignoring humidity) is probably going to be adequate for an effective
129
+ radiant and air temperature control strategy.
130
+
131
+
132
+ This simplification might be useful, in concert with an "adaptive model" for thermal neutrality.
133
+ From:
134
+
135
+ http://nceub.commoncense.info/uploads/Saberi.pdf
136
+
137
+ it appears that the "neutral" temperature (at which a person feels thermally neutral,
138
+ "comfortable") is related to the average outdoor temperature, by the equation:
139
+
140
+ t_n = 11.9 + 0.534 x t_m (from Humphreys)
141
+ or:
142
+ t_n = 17.6 + 0.314 x t_m (from Auliciemes)
143
+
144
+ where:
145
+ t_n = neutral temp (in C)
146
+ t_m = Mean outdoor temp (in C)
147
+
148
+ From the data underlying these equations, it appears that Fanger et.al. computed their comfort
149
+ equation temperatures in an environment where the outdoor temperature was around 25 C. This
150
+ means that we must adjust the target temperatures using these equations. If we adjust the
151
+ actual t_a and t_r upward or downward by the difference between the 25 C, and the computed t_n
152
+ for the outdoor temperature, before computing the Fanger L,
153
+
154
+ This result is probably due to the fact that humans adjust their food intake and metabolic rate
155
+ upwards when it is cold outside. This suggests that it might be possible to simply add to the
156
+ metabolic rate input to Fanger's equation, the colder the outside temperature is...
157
+
158
+ Further neutral temperature equations from: http://www.eurojournals.com/ejsr_27_2_06.pdf
159
+ t_n_o = 17 + 0.38 x t_o (Humphreys MA, 1976)
160
+ t_n_i = 2.6 + 0.831 x t_i (Orosa JA, 2008)
161
+ t_n_o = 11.9 + 0.534 x t_o (Orosa JA, 2008)
162
+ t_n_i = 5.41 + 0.731 x t_i
163
+ t_n_o = 17.6 + 0.31 x t_o
164
+ t_n_i_o = 9.22 + 0.48 x t_i + 0.14 x t_o
165
+ t_c = 17.8 + 0.31 x t_o (ASHRAE)
166
+ where:
167
+ t_o = outdoor temperature
168
+ t_i = mean indoor temperature
169
+ t_c = the comfort temperature
170
+ t_n_o = neutral temperature based on mean outdoor temperature
171
+ t_n_i = neutral temperature based on mean indoor temperature
172
+
173
+ All these assume near sedentary activity, and freely adaptable clothing.
174
+
175
+
176
+ Another description of Fanger's equation is from:
177
+
178
+ http://ergo.human.cornell.edu/studentdownloads/DEA3500notes/Thermal/thcomnotes2.html
179
+
180
+ Fanger's Comfort Equation This equation contains terms which relate to:
181
+
182
+ functions of clothing:
183
+ Icl = clothing insulation in clothes
184
+ fcr = ratio of clothed/nude surface area
185
+ functions of activity:
186
+ H = Metabolic heat production (w/m2)
187
+ M = Metabolic free energy production (external work)(w/m2)
188
+ Environmental variables:
189
+ Ta = Air temperature (C)
190
+ Tr = Mean radiant temperature (C)
191
+ v = Relative air speed (m/s)
192
+ Pa = Vapor pressure of water vapor (mb)
193
+
194
+ Fanger's Equation
195
+ (hc = convective transfer coefficient w/m2 K)
196
+ H - 0.31(57.4 - 0.07H - Pa) - 0.42(H-58) - 0.0017M(58.7 - Pa) - 0.0014M(34 - Ta) =
197
+ 3.9 x 10-8fcl {(Tcl + 273)4 - (Tr + 273)4} + fcl hc (Tcl - Ta)
198
+
199
+ Where the clothing surface temperature, Tcl, is given by
200
+ Tcl = 35.7 - 0.0275H + 0.155Iclo {H - 0.31(57.4 - 0.07H - Pa) - 0.42(H - 58) - 0.0017M(58.7 - Pa) - 0.0014M(34 - Ta)}.
201
+
202
+ In addition to this, discomfort may occur when the skin is wetted (sweat, water, etc.) and
203
+ Fanger has produced an equation for skin wettedness which can be used as a test to exclude
204
+ conditions which satisfy the comfort equation:
205
+
206
+ Predicted Mean Vote (PMV)
207
+
208
+ The problem with Fanger's equation is that when people are not satisfied, this is not a measure
209
+ of how uncomfortable deviation is; therefore Fanger developed PMV = mean vote on ASHRAE scale
210
+ (Hot / warm / slightly warm / neutral / slightly cool / cool / cold). PMV can be predicted from
211
+ Fanger's equation thus:
212
+
213
+ PMV = 4 + (0.303 exp(-0.036H) + 0.0275) x {6.57 + 0.46H + 0.31Pa + 0.0017HPa + 0.0014HTa - 4.13 fcl (1 + 0.01dT) (Tcl - Tr) - hcfcl (Tcl - Ta)}
214
+
215
+ where Tcl (surface temperature of clothed body) =
216
+ 35.7 - 0.0275H + 0.155 Iclofcl (4.13 (1 + 0.Old Temp) 1 + 0.155 Iclofcl (4.13 (1 + 0.Old Temp)thc
217
+
218
+ where hc = 2.4(Tcl - Ta)0.25 or 12.1 square root of v (air speed) which is greater
219
+ and dT = Tr - 22.
220
+
221
+
222
+ From the definitive source, the ISO 7730 standard (2005-11-15):
223
+
224
+ http://www.ntm.ru/UserFiles/File/document/Microklimat/Norm/ISO_7730_2005.pdf
225
+
226
+ (with corrections (re: HL6) from http://www.naun.org/journals/mechanics/17-105.pdf)
227
+
228
+ PMV = (0.303 x e^(-0.036 x M) + 0.028) x L
229
+
230
+ L = (M-W)
231
+ - 3.05 x 10^-3 x [5733 - 6.99 x (M-W) - P_a]
232
+ - 0.42 x [(M-W) - 58.15]
233
+ - 1.7 x 10^-5 x M x (5867 - P_a)
234
+ - 1.4 x 10^-3 x M x (34 - t_a)
235
+ - 3.96 x 10^-8 x f_cl x [(t_cl +273)^4 - (t_r + 273)^4]
236
+ - f_cl x h_c x (t_cl - t_a)
237
+ t_cl = 35.7 - 0.028 x (M-W)
238
+ - I_cl
239
+ x (3.96 x 10^-8 x f_cl
240
+ x [(t_cl + 273)^4 - (t_r + 273)^4]
241
+ + f_cl x h_c x (t_cl - t_a)]
242
+
243
+ h_c: Convective heat transfer (W/M^2 K)
244
+ h_c = A) 2.38 x (t_cl - t_a) ^ 0.25 # (for A > B); free convection
245
+ h_c = B) 12.1 x sqrt( v_ar ) # (for A < B); forced convection
246
+
247
+ f_cl: Clothing area factor
248
+ f_cl = A) 1.00 + 1.290 x I_cl # for I_cl <= 0.078 m^2K/W
249
+ f_cl = B) 1.05 + 0.645 x I_cl # for I_cl > 0.078 m^2K/W
250
+ where:
251
+ M is the metabolic rate, in watts per square metre (W/m2);
252
+ W is the effective mechanical power, in watts per square metre (W/m^2);
253
+ I_cl is the clothing insulation, in square metres kelvin per watt (m^2 K/W);
254
+ f_cl is the clothing surface area factor;
255
+ t_a is the air temperature, in degrees Celsius (C);
256
+ t_r is the mean radiant temperature, in degrees Celsius (C);
257
+ v is the relative air velocity, in metres per second (m/s);
258
+ p_a is the water vapour partial pressure, in pascals (Pa);
259
+ h_c is the convective heat transfer coefficient, in watts per square metre kelvin [W/(m^2K)];
260
+ t_cl is the clothing surface temperature, in degrees Celsius (C).
261
+ 1 metabolic unit = 1 met = 58.2 W/m2
262
+ 1 clothing unit = 1 clo = 0.155 m^2K/W
263
+
264
+ PMV should only be used between -2 and 2, and if the 6 main variables are in the ranges:
265
+
266
+ M 46 W/m^2 to 232 W/m^2 (0.8 met to 4 met);
267
+ I_cl 0 m^2K/W to 0.310 m^2K/W (0 clo to 2 clo);
268
+ t_a 10 C to 30 C;
269
+ t_r 10 C to 40 C;
270
+ v 0 m/s to 1 m/s;
271
+ p_a 0 Pa to 2700 Pa.
272
+
273
+ A BASIC program implement PMV is provided:
274
+ 10 Computer program (BASIC) for calculation of
275
+ 20 Predicted Mean Vote (PMV) and Predicted Percentage of Dissatisfied (PPD)
276
+ 30 in accordance with International Standard, ISO 7730
277
+ 40 CLS: PRINT "DATA ENTRY" data entry
278
+ 50 INPUT " Clothing (clo) "; CLO
279
+ 60 INPUT " Metabolic rate (met) " MET
280
+ 70 INPUT " External work, normally around 0 (met) " WME
281
+ 80 INPUT " Air temperature (C) " TA
282
+ 90 INPUT " Mean radiant temperature (C) " TR
283
+ 100 INPUT " Relative air velocity (m/s) " VEL
284
+ 110 INPUT " ENTER EITHER RH OR WATER VAPOUR PRESSURE BUT NOT BOTH"
285
+ 120 INPUT " Relative humidity (%) " RH
286
+ 130 INPUT " Water vapour pressure (Pa) " PA
287
+ 140 DEF FNPS (T) = EXP (16.6536-4030.183/T+235)) : saturated vapour pressure, kPa
288
+ 150 IF PA = 0 THEN PA = RH * 10 * FNPS (TA) : water vapour pressure, Pa
289
+ 160 ICL = .155 * CLO : thermal insulation of the clothing in m^2K/W
290
+ 170 M = MET * 58.15 : metabolic rate in W/m2
291
+ 180 W = WME * 58.15 : external work in W/m2
292
+ 190 MW = M - W : internal heat production in the human body
293
+ 200 IF ICL < .078
294
+ THEN FCL = 1 + 1.29 * ICL
295
+ ELSE FCL = 1.05 + 0.645 * ICL : clothing area factor
296
+ 210 HCF = 12.1 * SQR (VEL) : heat transf. coeff. by forced convection
297
+
298
+ 220 TAA = TA + 273 : air temperature in Kelvin
299
+ 230 TRA = TR + 273 : mean radiant temperature in Kelvin
300
+ 240 -----CALCULATE SURFACE TEMPERATURE OF CLOTHING BY ITERATION ---
301
+ 250 TCLA = TAA + (35.5-TA) / (3.5 * ICL + .1) : first guess for surface temperature of clothing
302
+ 260 P1 = ICL * FCL : calculation term
303
+ 270 P2 = P1 * 3.96 : calculation term
304
+ 280 P3 = P1 * 100 : calculation term
305
+ 290 P4 = P1 * TAA : calculation term
306
+ 300 P5 = 308.7 - .028 * MW + P2 * (TRA/100) * 4
307
+ 310 XN = TLCA / 100
308
+ 320 XF = XN
309
+ 330 N = 0 : N: number of iterations
310
+ 340 EPS = .00015 : stop criteria in iteration
311
+ 350 XF = (XF + XN)/2
312
+ 360 HCN =2.38 * ABS (100 * XF - TAA) ^ .25 : heat transf. coeff. by natural convection
313
+ 370 IF HCF>HCN THEN HC = HCF ELSE HC = HCN
314
+ 380 XN = (P5 + P4 * HC - P2 * XF ^ 4) / (100 + P3 * HC)
315
+ 390 N = N + 1
316
+ 400 IF N > 150 THEN GOTO 550
317
+ 410 IF ABS (XN - XF) > EPS GOTO 350
318
+ 420 TCL = 100 * XN - 273 : surface temperature of the clothing
319
+ 430 --------------------------------HEAT LOSS COMPONENTS -----------------------------------
320
+ 440 HL1 = 3.05 * .001 (5733-6.99 * MW-PA) : heat loss diff. through skin
321
+ 450 IF MW > 58.15 THEN HL2 = .42 * (MW - 58.15)
322
+ ELSE HL2 = 0! : heat loss by sweating (comfort)
323
+ 460 HL3 = 1.7 * .00001 * m * (5867-PA) : latent respiration heat loss
324
+ 470 HL4 = .0014 * m * (34 - TA) : dry respiration heat loss
325
+ 480 HL5 = 3.96 * FCL * (XN^4 - (TRA/100^4) : heat loss by radiation
326
+ 500 --------------------------------CALCULATE PMV AND PPD -----------------------------------
327
+ 510 TS = .303 * EXP (- .036 * m) + .028 : thermal sensation trans coeff
328
+ 520 PMV = TS * (MW - HL1 - HL2 - HL3 - HL4 - HL5 - HL6) : predicted mean vote
329
+ 530 PPD = 100 - 95 * EXP (- .03353 * PMV ^ 4 - .2179 * PMV^ 2) : predicted percentage dissat.
330
+ 540 GOTO 570
331
+ 550 PMV = 999999!
332
+ 560 PPD = 100
333
+ 570 PRINT:PRINT "OUTPUT" : output
334
+ 580 PRINT " Predicted Mean Vote (PMV): "
335
+ :PRINT USING "# # . #": PMV
336
+
337
+
338
+ From:
339
+
340
+ http://listserv.repp.org/pipermail/greenbuilding_listserv.repp.org/2009-April/011178.html (gone, 20261110)
341
+
342
+ we have a seemingly corrected version:
343
+
344
+ 10 CLO = 1'clothing insulation (clo)
345
+ 20 MET=1.1'metabolic rate (met)
346
+ 30 WME=0'external work (met)
347
+ 40 VEL=.1'air velocity
348
+ 50 RH=0'relative humidity (%)
349
+ 60 DATA 70,69,.008,0.05,1
350
+ 70 DATA 69.15,70,.008,0.05,1
351
+ 80 FOR CASE = 1 TO 2
352
+ 90 READ TAF,TRF,WA,VEL,CLO
353
+ 100 TA=(TAF-32)/1.8'air temp (C)
354
+ 110 TR=(TRF-32)/1.8'mean radiant temp (C)
355
+ 120 PA=29.921*3377.2/(1+.62198/WA)'water vapor ressure (Pa)
356
+ 130 DEF FNPS(T)=EXP(16.6536-4030.183/(TA+235))'sat vapor pressure, kPa
357
+ 140 IF PA=0 THEN PA=RH*10*FNPS(TA)'water vapor pressure, Pa
358
+ 150 ICL=.155*CLO'clothing resistance (m^2K/W)
359
+ 160 M=MET*58.15'metabolic rate (W/m^2)
360
+ 170 W=WME*58.15'external work in (W/m^2)
361
+ 180 MW=M-W'internal heat production
362
+ 190 IF ICL<.078 THEN FCL=1+1.29*ICL ELSE FCL=1.05+.645*ICL'clothing
363
+ factor
364
+ 200 HCF=12.1*SQR(VEL)'forced convection conductance
365
+ 210 TAA=TA+273'air temp (K)
366
+ 220 TRA=TR+273'mean radiant temp (K)
367
+ 230 TCLA=TAA+(35.5-TA)/(3.5*(6.45*ICL+.1))'est clothing temp
368
+ 240 P1=ICL*FCL:P2=P1*3.96:P3=P1*100:P4=P1*TAA'intermediate values
369
+ 250 P5=308.7-.028*MW+P2*(TRA/100)^4
370
+ 260 XN=TCLA/100
371
+ 270 XF=XN
372
+ 280 N=0'number of iterations
373
+ 290 EPS=.00015'stop iteration when met
374
+ 300 XF=(XF+XN)/2'natural convection conductance
375
+ 310 HCN=2.38*ABS(100*XF-TAA)^.25
376
+ 320 IF HCF>HCN THEN HC=HCF ELSE HC=HCN
377
+ 330 XN=(P5+P4*HC-P2*XF^4)/(100+P3*HC)
378
+ 340 N=N+1
379
+ 350 IF N>150 GOTO 470
380
+ 360 IF ABS(XN-XF)>EPS GOTO 300
381
+ 370 TCL=100*XN-273'clothing surface temp (C)
382
+ 380 HL1=.00305*(5733-6.99*MW-PA)'heat loss diff through skin
383
+ 390 IF MW>58.15 THEN HL2=.42*(MW-58.15) ELSE HL2=0'heat loss by sweating
384
+ 400 HL3=.000017*M*(5867-PA)'latent respiration heat loss
385
+ 410 HL4=.0014*M*(34-TA)'dry respiration heat loss
386
+ 420 HL5=3.96*FCL*(XN^4-(TRA/100)^4)'heat loss by radiation
387
+ 430 HL6=FCL*HC*(TCL-TA)'heat loss by convection
388
+ 440 TS=.303*EXP(-.036*M)+.028'thermal sensation transfer coefficient
389
+ 450 PMV=TS*(MW-HL1-HL2-HL3-HL4-HL5-HL6)'predicted mean vote
390
+ 460 GOTO 480
391
+ 470 PMV=99999!:PPD=100
392
+ 480 PRINT TAF,TRF,PMV
393
+ 490 NEXT CASE
394
+ 500 LIST 70
395
+
396
+
397
+ From Truong (2010): http://hdl.handle.net/1721.1/61509, we get the following updates to Fanger's
398
+ equations, especially focussed on better predicting comfort in Naturally Ventilated (NV)
399
+ buildings:
400
+
401
+ Fanger underestimated the increase in clothing surface area, leading to too high
402
+ comfort temperatures with higher clo values (for all building types):
403
+
404
+ f_cl = 1 + 0.30 * clo
405
+
406
+ """
407
+ MET = 58.0 # W/m^2 per met (ISO 7730 uses 58.15 or 58.2)
408
+ CLO = 0.155 # m^2K/W per clo
409
+
410
+ def __init__( self, met=None, M=None, clo=None, I_cl=None, v=0.0,
411
+ t_a=None, t_r=None, hum=None, P_w=None, W=0.0, mit=True ):
412
+ """Defaults are sedentary activity or standing (met 1.2), working clothes (clo 0.8), no air
413
+ velocity, no net work output. Optional arguments remembered are:
414
+
415
+ met/M Metabolic rate in either met or M (W/m^2); both not allowed
416
+ clo/I_cl Clothing in either clo or I_cl (m^2 K/W) ; both not allowed
417
+ v Air velocity, in m/s
418
+ t_a, t_r Air and average radiant temperatures (degrees C)
419
+ hum/P_w Relative humidity (0,1), or vapour pressure of water in ambient air (Pa)
420
+ W External work performed (W/m^2), usually 0.
421
+ mit Use Truong (2010)'s revised clothing area factor f_cl
422
+
423
+ If you specify humidity in terms of relative humidity (hum, domain (0.0,1.0)), the vapour
424
+ pressure is computed from the air temperature t_a in effect at the time of each computation.
425
+
426
+ """
427
+ if met is not None and M is not None:
428
+ raise Error( "Specify metabolism as met or M, not both" )
429
+ if clo is not None and I_cl is not None:
430
+ raise Error( "Specify clothing as clo or I_cl, not both" )
431
+ if hum is not None and P_w is not None:
432
+ raise Error( "Specify humidity as hum or P_w, not both" )
433
+ self._M = None
434
+ self._M = self.M( met=1.2 if met is None and M is None else met, M=M )
435
+ self._I_cl = None
436
+ self._I_cl = self.I_cl( clo=0.8 if clo is None and I_cl is None else clo, I_cl=I_cl )
437
+ self.v = v
438
+ self.t_a = t_a
439
+ self.t_r = t_r
440
+ self.hum = hum
441
+ self._P_w = P_w
442
+ if hum is not None and t_a is not None:
443
+ self._P_w = self.P_w( t_a=t_a, hum=hum )
444
+ self.W = W
445
+ self.mit = mit
446
+ self._t_cl = None
447
+
448
+ def metabolism( self, rate=None ):
449
+ """Returns (rate, met, "description") of the current metabolic rate. Optionally, set the 'rate',
450
+ using a value in the domain [0,1] --> (0.8,3.0) met.
451
+
452
+ """
453
+ if rate is None:
454
+ M = self.M()
455
+ else:
456
+ M = self._M = self.M( met=scale( rate, (0., 1.), (0.8, 3.0) ))
457
+ if M is None:
458
+ return ( float( 'nan' ), float( 'nan' ), "unknown" )
459
+ met = M / self.MET
460
+ if rate is None:
461
+ rate = scale( met, (0.8, 3.0), (0., 1.) )
462
+
463
+ for lim, desc in ( ( ( 0.8 + 1.0 ) / 2, "lying" ),
464
+ ( ( 1.0 + 1.2 ) / 2, "sitting" ),
465
+ ( ( 1.2 + 1.6 ) / 2, "standing" ),
466
+ ( ( 1.6 + 2.0 ) / 2, "light" ),
467
+ ( ( 2.0 + 3.0 ) / 2, "medium" ),
468
+ ( ( 3.0 + 4.0 ) / 2, "hard" )):
469
+ if met < lim:
470
+ return ( rate, met, desc )
471
+ return ( rate, met, "heavy" )
472
+
473
+ def M( self, met=None, M=None ):
474
+ """Support specifying M as either met or M, or from precomputed _M attribute"""
475
+ if met is not None:
476
+ if not 0.0 <= met <= 9.0:
477
+ raise Error( "Metabolism %r met outside (0,9)" % ( met, ))
478
+ return met * self.MET
479
+ if M is not None:
480
+ return M
481
+ return self._M
482
+
483
+ def clothing( self, amount=None ):
484
+ """Returns (amount, clo, "description"), where amount is in the range [0,1] --> (0,3) clo. 1.0
485
+ clo is defined as the amount of clothes required for an average person to be indefinitely
486
+ comfortable at 20C, 50%RH, 0m/s. 4.0 is the maximum possible amount of clothing; basically
487
+ "Eskimo" clothing; full furs. Optionally pass amount to adjust clothing level.
488
+
489
+ """
490
+ if amount is None:
491
+ I_cl = self.I_cl()
492
+ else:
493
+ I_cl = self._I_cl = self.I_cl( clo=scale( amount, (0., 1.), (0., 3.) ))
494
+ if I_cl is None:
495
+ return ( float( 'nan' ), float( 'nan' ), "unknown" )
496
+ clo = I_cl / self.CLO
497
+ if amount is None:
498
+ amount = scale( clo, (0., 3.), (0., 1.) )
499
+
500
+ for lim, desc in ( ( ( 0.0 + 0.1 ) / 2, "naked" ),
501
+ ( ( 0.1 + 0.3 ) / 2, "shorts" ),
502
+ ( ( 0.3 + 0.5 ) / 2, "tropical" ),
503
+ ( ( 0.5 + 0.8 ) / 2, "summer" ),
504
+ ( ( 0.8 + 1.0 ) / 2, "casual" ),
505
+ ( ( 1.0 + 1.5 ) / 2, "indoor" ),
506
+ ( ( 1.5 + 2.0 ) / 2, "winter" ),
507
+ ( ( 2.0 + 3.0 ) / 2, "heavy" )):
508
+ if clo < lim:
509
+ return ( amount, clo, desc )
510
+ return ( amount, clo, "blankets" )
511
+
512
+ def I_cl( self, clo=None, I_cl=None ):
513
+ """Support specifying I_cl either directly or from clo, or from a precomputed self._I_cl."""
514
+ if clo is not None:
515
+ if not 0.0 <= clo <= 3.0:
516
+ raise Error( "Clothing %r clo outside (0,3)" % ( clo, ))
517
+ return clo * self.CLO
518
+ if I_cl is not None:
519
+ return I_cl
520
+ return self._I_cl
521
+
522
+ def f_cl( self, I_cl ):
523
+ """The clothing area factor. The updated f_cl = 1 + 0.3 x clo equation is used, if 'mit' mode is
524
+ true.
525
+
526
+ """
527
+ if self.mit:
528
+ return 1.00 + 0.3 * I_cl / self.CLO
529
+ if I_cl < 0.078:
530
+ return 1.00 + 0.2 * I_cl / self.CLO
531
+ return 1.05 + 0.1 * I_cl / self.CLO
532
+
533
+ def t_cl_h_c( self, M, W, I_cl, f_cl, v, t_a, t_r, error=1e-6, allow=1e-4,
534
+ reps=150, advance=5 ):
535
+ """Produce an refined estimate of t_cl and h_c to within 'err' of 1e-4. Since this algorithm
536
+ deals in 1/100th Kelvins, the error will be amplified by 100 in the result, so we should try
537
+ to solve to 1e-6. This should avoid getting too close to the limits of the underlying
538
+ floating point format, and usually take around 10 iterations. However, degenerate cases in
539
+ prior implementations have been observed; for example, the loop oscillates between two
540
+ values, never approaching closer than an error of 0.00000614 after 37 iterations, settling
541
+ into an oscillation between two values:
542
+
543
+ t_cl ( 37): 2.91945054 (err 0.00000614)
544
+ t_cl ( 74): 2.91944133 (err 0.00000614)
545
+ t_cl (111): 2.91945054 (err 0.00000614)
546
+ ...
547
+ t_cl (141): 2.91945054 (err 0.00000614)
548
+ ...
549
+ t_cl (146): 2.91944133 (err 0.00000614)
550
+ t_cl (147): 2.91945054 (err 0.00000614)
551
+ t_cl (148): 2.91944133 (err 0.00000614)
552
+ t_cl (149): 2.91945054 (err 0.00000614)
553
+
554
+ 'xn': 2.9194505366000816,
555
+ 'xf': 2.919444401018496,
556
+ 'taa': 291.94444544017557,
557
+ 'I_cl': 0.46499999999999997,
558
+ 't_a': 18.79444544017561,
559
+ 'hcf': 0.0,
560
+ 't_r': 14.251083402047804,
561
+ 'hcn': 0.11440058061771223,
562
+ 'f_cl': 1.9,
563
+ 'M': 46.400000000000006,
564
+ 'reps': 150,
565
+ 'W': 0.0,
566
+ 'hc': 0.11440058061771223,
567
+ 'p2': 3.4986599999999997,
568
+ 'p3': 88.35,
569
+ 'p1': 0.8835,
570
+ 'p4': 257.9329175463951,
571
+ 'err': 1e-07,
572
+ 'p5': 546.1024074023035,
573
+ 'tra': 287.4010834020478,
574
+ 'n': 149,
575
+ 'mw': 46.400000000000006,
576
+ 'v': 0.0
577
+
578
+ Or, worse yet, not even within 1e-6:
579
+ 'xf': 2.9814100885428747,
580
+ 't_a': 24.989128226382643,
581
+ 't_r': 22.458774905050937,
582
+
583
+ Instead of a fixed number of iterations, we'll simply iterate 'til we cease advancing the
584
+ convergence, or we hit our goal 'error' limit, and only failing if we hit our ultimate rep
585
+ limit, or cease converging without getting within our maximum 'allow' error limit...
586
+
587
+ Uses the self._t_cl as the last estimate, assuming that subsequent calls will change input
588
+ terms only slightly. Pretty much straight from the ISO 7730 BASIC program.
589
+
590
+ """
591
+ mw = M - W
592
+ hcf = 12.1 * math.sqrt( v )
593
+ taa = t_a + 273.15 # Air temp, K
594
+ tra = t_r + 273.15 # Radiant temp, K
595
+ p1 = I_cl * f_cl
596
+ p2 = p1 * 3.96
597
+ p3 = p1 * 100
598
+ p4 = p1 * taa
599
+ p5 = 308.7 - .028 * mw + p2 * math.pow( tra / 100, 4 )
600
+ if self._t_cl is None:
601
+ self._t_cl = t_a + ( 35.5 - t_a ) / ( 3.5 * ( 6.45 * I_cl + .1 ))
602
+ xf = xn = ( self._t_cl + 273.15 ) / 100
603
+
604
+ converged = False
605
+ n = 0
606
+ least = {}
607
+ while not converged:
608
+ xf = ( xf + xn ) / 2
609
+ hcn = 2.38 * math.pow( abs( 100 * xf - taa ), .25 )
610
+ hc = max( hcf, hcn )
611
+ xn = ( p5 + p4 * hc - p2 * math.pow( xf, 4 )) / ( 100 + p3 * hc )
612
+ diff = abs( xf - xn )
613
+ converged = n > 5 and diff <= error * abs( xn )
614
+ n += 1
615
+ if not least or diff < least['diff']:
616
+ least['n'] = n
617
+ least['diff'] = diff
618
+ least['xn'] = xn
619
+ if n > least['n'] + advance or n > reps:
620
+ break
621
+
622
+ if not converged and diff > allow:
623
+ # Well, we didn't converge xf/xn within 'error'; if we didn't even get within
624
+ # 'allow', we'll have to raise an exception.
625
+ raise Error( "Couldn't refine t_cl to within %g, in %d iterations: locals:\n%r" % (
626
+ allow, n, locals() ))
627
+
628
+ self._t_cl = ( 100 * least['xn'] ) - 273.15
629
+ return self._t_cl, hc
630
+
631
+ def t_d( self, e ):
632
+ """Return dewpoint temperature (degrees C), from vapour pressure e (in Pa). Equation is for
633
+ millibars (mb), which are Pa/100. See:
634
+
635
+ http://facstaff.unca.edu/cgodfrey/courses/advcomp/cgi.shtml
636
+
637
+ """
638
+ e_ln = math.log( e / 100. / 6.112 )
639
+ return ( 243.5 * e_ln ) / ( 17.67 - e_ln )
640
+
641
+ def p_sat( self, t_a ):
642
+ """Saturated Vapour Pressure, in Pa, for a given air temperature t_a. From ISO 7730 BASIC
643
+ program, and:
644
+
645
+ http://www.dtic.mil/cgi-bin/GetTRDoc?AD=ADA305730
646
+
647
+ """
648
+ return math.exp( 16.6536 - ( 4030.183 / ( t_a + 235. ))) * 1000.
649
+
650
+ def P_w( self, t_a=None, hum=None, P_w=None ):
651
+ """Compute actual vapour pressure P_w in Pascals (Pa), from Relative Humidity (hum, ~(0., 1.)),
652
+ and air temperature t_a (C). If neither hum nor P_w is supplied, returns the precomputed
653
+ self._P_w.
654
+
655
+ RH: Relative Humidity
656
+ RH = e / e_s
657
+ or:
658
+ e = RH x e_s (in mb)
659
+ P_w = e x 100. (in Pa)
660
+ where:
661
+ P_w: Vapour pressure (Pa)
662
+ e: Vapour pressure (millibars, mb)
663
+
664
+ e_s(t_a): Saturation Vapour Pressure (mb), from air temperature (degress C)
665
+ (From: http://facstaff.unca.edu/cgodfrey/courses/advcomp/cgi.shtml; WRONG)
666
+ e_s(t_a) = 6.112 x 10^((17.67 x t_a) / (t_a + 243.5))
667
+ or:
668
+ (From: http://www.theweathernetwork.com/glossary/humidex)
669
+ e_s(t_a) = 6.112 x 10^(7.5 x t_a/(237.7 + T))
670
+ 1 Bar = 100,000 Pa
671
+ 1 mb = 100 Pa
672
+
673
+ We use the ISO 7730 BASIC program's saturation vapour pressure; see p_sat.
674
+ """
675
+ if P_w is not None:
676
+ return P_w
677
+ if hum is not None:
678
+ # No P_w, but we have RH%; compute vapour pressure (Pa) using air temperature (C)
679
+ if t_a is None or not -100. < t_a < 100.:
680
+ raise Error( "Relative humidity requires an air temperature in (-100,100) C; not %r" % (
681
+ t_a, ))
682
+ if not 0.0 <= hum <= 1.0:
683
+ raise Error( "Relative humidity %r outside (0,1)" % ( hum, ))
684
+ return hum * self.p_sat( t_a=t_a )
685
+ return self._P_w
686
+
687
+ def L( self, met=None, M=None, clo=None, I_cl=None, v=None,
688
+ t_a=None, t_r=None, hum=None, P_w=None, W=None ):
689
+ """Fanger's Comfort Equation. Computes the net (+/-) thermal load (L) on a typical human body,
690
+ subject to the specified parameters, and returns it scaled to the Predicted Mean Vote (PMV)
691
+ thermal sensation scale (see PMV). Any terms not defined must have been specified at the
692
+ time of object creation, or result will be an exception.
693
+
694
+ """
695
+ # Metabolism, clothing
696
+ M = self.M( met=met, M=M )
697
+ I_cl = self.I_cl( clo=clo, I_cl=I_cl )
698
+ f_cl = self.f_cl( I_cl=I_cl )
699
+
700
+ # External Work
701
+ W = W if W is not None else self.W
702
+
703
+ # Environmental conditions. A relative humidity (if specified) tracks the air temperature.
704
+ v = v if v is not None else self.v
705
+ t_a = t_a if t_a is not None else self.t_a
706
+ t_r = t_r if t_r is not None else self.t_r
707
+ if t_a is None or t_r is None:
708
+ raise Error( "Fanger's equation requires air and radiant temperatures t_a and t_r" )
709
+ P_w = self.P_w( t_a=t_a, hum=hum if hum is not None or P_w is not None else self.hum,
710
+ P_w=P_w )
711
+ if P_w is None:
712
+ raise Error( "Fanger's equation requires humidity hum or vapour pressure P_w" )
713
+
714
+ # Compute clothing temperature t_cl, iteratively, since the formula itself depends on
715
+ # t_cl. We'll iterate 'til the computed t_cl is within ~10^-4 of the last sample, and save
716
+ # the last computed value in self._t_cl (for the next time).
717
+ t_cl, h_c = self.t_cl_h_c( M=M, W=W, I_cl=I_cl, f_cl=f_cl, v=v, t_a=t_a, t_r=t_r )
718
+
719
+ # Compute all heat losses (W/m^2)
720
+ hl1 = .00305 * ( 5733 - 6.99 * ( M - W ) - P_w ) # Through skin
721
+ hl2 = max( 0.42 * ( ( M - W ) - 58.15 ), 0. ) # From sweating
722
+ hl3 = .000017 * M * ( 5867 - P_w ) # Latent respiration
723
+ hl4 = .0014 * M * ( 34 - t_a ) # Dry respiration
724
+ hl5 = 3.96 * f_cl * ( math.pow( ( t_cl + 273.15 ) / 100, 4 ) # Radiation
725
+ - math.pow( ( t_r + 273.15 ) / 100, 4 ))
726
+ hl6 = f_cl * h_c * ( t_cl - t_a ) # Convection
727
+ balance = ( M - W ) - hl1 - hl2 - hl3 - hl4 - hl5 - hl6
728
+ ts = .303 * math.exp( -.036 * M ) + .028 # Thermal sensation coefficient
729
+ return ts * balance
730
+
731
+ def L_simple( self, met=None, M=None, clo=None, I_cl=None, v=None,
732
+ t_a=None, t_r=None, hum=None, P_w=None, W=None ):
733
+ """A simplified thermal loading calculation, that operates using an estimate of perceived
734
+ temperatures. It simply returns the difference between desired and perceived temperatures,
735
+ for different levels of clothing and metabolic rate. Result is in degrees C difference
736
+ between perceived and desired temperatures. Humidity and external work are ignored.
737
+
738
+ """
739
+ M = self.M( met=met, M=M )
740
+ I_cl = self.I_cl( clo=clo, I_cl=I_cl )
741
+ v = v if v is not None else self.v
742
+ t_a = t_a if t_a is not None else self.t_a
743
+ t_r = t_r if t_r is not None else self.t_r
744
+ desired, perceived = self.t_sub( M=M, I_cl=I_cl, t_a=t_a, t_r=t_r, v=v )
745
+ return perceived - desired
746
+
747
+ def t_sub( self, M, I_cl, t_a, t_r, v ):
748
+ """McIntyre's simplified desired and perceived temperatures (C); see class docstring."""
749
+ t_sub_desired = 33.5 - 3 * I_cl - ( 0.08 + 0.05 * I_cl ) * M
750
+ if v > 0.1:
751
+ t_sub_perceived = 0.44 * t_r + ( 5. - math.pow( 10 * v, 0.5 ) * ( 5. - t_a )) / (
752
+ 0.44 + 0.56 * math.pow( 10 * v, 0.5 ))
753
+ else:
754
+ t_sub_perceived = 0.56 * t_a + 0.44 * t_r
755
+ return t_sub_desired, t_sub_perceived
756
+
757
+ def PMV( self, **kwargs ):
758
+ """Compute Predicted Mean Vote on the ASHRAE thermal sensation scale (-3.,+3):
759
+
760
+ +3 hot
761
+ +2 warm
762
+ +1 slightly warm
763
+ 0 neutral
764
+ -1 slightly cool
765
+ -2 cool
766
+ -3 cold
767
+
768
+ PMV = (0.303 x e^(-0.036 x M) + 0.028) x L
769
+ where:
770
+ M: Metabolic rate (W/m^2)
771
+ L: thermal load; difference between internal heat production and heat lost to environment
772
+
773
+ This is exactly what L() computes (and has always returned), so PMV is an alias for L,
774
+ accepting all the same keyword args.
775
+
776
+ """
777
+ return self.L( **kwargs )
778
+
779
+ def PPD( self, pmv=None, **kwargs ):
780
+ """Predicted Percentage of Dissatisfied (5% to 100%), from the PMV (ISO 7730, eq. 4):
781
+
782
+ PPD = 100 - 95 x e^(-0.03353 x PMV^4 - 0.2179 x PMV^2)
783
+
784
+ Even at a neutral PMV of 0, 5% of people are dissatisfied; at PMV +/-0.5, ~10%. Supply a
785
+ precomputed 'pmv', or the same keyword args as PMV.
786
+ """
787
+ if pmv is None:
788
+ pmv = self.PMV( **kwargs )
789
+ return 100 - 95 * math.exp( -0.03353 * pmv ** 4 - 0.2179 * pmv ** 2 )
790
+
791
+ def setpoint( self, pmv=0.0, lo=0.0, hi=40.0, error=1e-3, **kwargs ):
792
+ """Air temperature t_a (C) within (lo,hi) yielding the target 'pmv' (default: neutral), given
793
+ all other conditions (eg. the mean radiant temperature t_r). This is the air temperature a
794
+ comfort-seeking thermostat should target. PMV rises monotonically with t_a, so bisect.
795
+
796
+ """
797
+ if 't_a' in kwargs:
798
+ raise Error( "setpoint computes t_a; do not specify it" )
799
+ pmv_lo, pmv_hi = self.PMV( t_a=lo, **kwargs ), self.PMV( t_a=hi, **kwargs )
800
+ if not pmv_lo <= pmv <= pmv_hi:
801
+ raise Error( "PMV %+.2f not attainable in t_a (%g,%g)C; PMV ranges (%+.2f,%+.2f)" % (
802
+ pmv, lo, hi, pmv_lo, pmv_hi ))
803
+ while hi - lo > error:
804
+ mid = ( lo + hi ) / 2
805
+ if self.PMV( t_a=mid, **kwargs ) < pmv:
806
+ lo = mid
807
+ else:
808
+ hi = mid
809
+ return ( lo + hi ) / 2
810
+
811
+ def feels( self, **kwargs ):
812
+ """A description of the PMV: cold, cool, coolish, ok, warmish, warm or hot"""
813
+ pmv = self.PMV( **kwargs )
814
+ for lim, desc in ( ( -3, "cold" ), ( -2, "cool" ), ( -1, "coolish" )):
815
+ if pmv <= lim:
816
+ return desc
817
+ for lim, desc in ( ( +3, "hot" ), ( +2, "warm" ), ( +1, "warmish" )):
818
+ if pmv >= lim:
819
+ return desc
820
+ return "ok"