Source code for ladybug_comfort.pet

# coding=utf-8
"""Utility functions for calculating Physiologic Equivalent Temperature (PET).

PET uses the Munich Energy Balance Model (MEMI), which is arguably the most
detailed 3-node human energy balance model in common use today. It can account
for various physiological features of the human subject, including age, sex,
height, and body mass, making it one of the only models that is suitable for
forecasting the thermal experience of a specific individual. This also makes
it one of the better models for estimating core body temperature and whether
a given set of conditions is likely to induce hypothermia or hyperthermia in
a specific individual.

PET is, itself, a "feels like" temperature and is defined as the operative
temperature of a reference environment that would cause the same physiological
response in the human subject as the environment under study. That is, the
same skin temperature and core body temperature.
"""
from __future__ import division
import math

from ladybug.rootfinding import secant_three_var

# constants for standard conditions
TC_SET = 36.6  # standard core body temperature
TSK_SET = 34  # standard skin temperature
TBODY_SET = 0.1 * TSK_SET + 0.9 * TC_SET  # weighted average for full-body temperature
# universal constants
P_REF = 101325  # sea-level barometric pressure [Pa]
C_AIR = 1010  # air specific heat capacity [J/kg-K]
L_VAP = 2420000  # latent heat of evaporation [J/Kg]
C_B = 3640  # blood specific heat [J/kg-K]
EM_SK = 0.99  # human skin emissivity
EM_CL = 0.95  # clothing emissivity
SIGM = 5.67 * (10 ** -8)  # Stefan-Boltzmann constant [W/(m2-K^(-4))]


[docs] def physiologic_equivalent_temperature( ta, tr, vel, rh, met, clo, age=36, sex=0.5, ht=1.65, m_body=62, pos='standing', b_press=101325): """Calculate Physiological Equivalent Temperature (PET). This method is based on Peter Hoeppe's original PET Fortran code, from the VDI Norm 3787, Blatt 2 [1][2], as well as Djordje Spasic's Ladybug Legacy source code. It includes the corrected reference environment and updated vapor transfer model published by Edouard Walther (AREP, France) and Quentin Goestchel (ENS Paris-Saclay, France) [3]. Chris Mackey added support for the specification of metabolic rates in met (instead of Watts above basal metabolism) using a USDA report that relates Resting Metabolic Rate (RMR) (assumed to be 1 met) to Basal Metabolic Rate (BMR) using a Physical Activity Level (PAL) ratio [4]. Note: [1] Hoeppe, Peter. (1999). "The physiological equivalent temperature - A universal index for the biometeorological assessment of the thermal environment." International journal of biometeorology. 43. 71-5. 10.1007/s004840050118. https://link.springer.com/article/10.1007/s004840050118 [2] Hoeppe, Peter. (2008). "Urban climatic map and standards for wind environment - Feasibility study, Technical Input Report No.1", The Chinese University of Hong Kong, Planning Department. [3] Walther, Edouard and Goestchel, Quentin. (2018). "The P.E.T. comfort index: Questioning the model". Building and Environment. 137C. 1-10. 10.1016/j.buildenv.2018.03.054. https://www.sciencedirect.com/science/article/pii/S0360132318301896 [4] "Dietary Reference Intakes for Energy, Carbohydrate, Fiber, Fat, Fatty Acids, Cholesterol, Protein, and Amino Acids (Macronutrients) (2005)". USDA. National Academy of Sciences, Institute of Medicine, Food and Nutrition Board. Archived from the original on 10 March 2016. Chapter 12, page 8. https://web.archive.org/web/20160310031719/http://fnic.nal.usda.gov/\ dietary-guidance/dri-nutrient-reports/energy-carbohydrate-fiber-fat-\ fatty-acids-cholesterol-protein Args: ta: Air temperature [C]. tr: Mean radiant temperature [C]. vel: Relative air velocity [m/s]. rh: Relative humidity [%]. met: Metabolic rate [met]. Note that the original PET model requires that the activity of the human subject be accounted for as additional Watts above the basal metabolism, which is often difficult to estimate. In order to accept an input in [met], it is assumed that 1 met refers to Resting Metabolic Rate (RMR) and this is 1.17 times the male Basal Metabolic Rate (BMR) or 1.22 times the female BMR, where 1.17 = 1 / 0.9 (TEF or food digestion) / 0.95 (basal to resting activity difference) clo: Clothing [clo]. age: The age of the human subject in years. (Default: 36 years for middle age of the average worldwide life expectancy). sex: A value between 0 and 1 to indicate the sex of the human subject, which influences the computation of basal metabolism. 0 indicates male. 1 indicates female and any number in between denotes a weighted average between the two. (Default: 0.5). ht: The height of the human subject in meters. (Default: 1.65m for a worldwide average between male and female height). m_body: The body mass of the human subject in kilograms. (Default: 62 kg for the worldwide average adult human body mass). pos: Text to indicate the posture of the human subject's body. Choose from the following: "standing", "seated", "crouching". (Default: "standing"). b_press: An optional number for the air pressure in which the human subject exists, which determines the rate of sweat evaporation [Pa]. Default is pressure at sea level (101325 Pa). Returns: A dictionary containing results of the PET model with the following keys - pet -- Physiological equivalent temperature (PET) [C] - t_core -- Core body temperature [C] - t_skin -- Skin temperature [C] - t_clo -- Clothing temperature [C] """ epsilon = 0.01 # the acceptable error in the result of the temperatures # compute the constant variables of the MEMI model a_du, a_clo, a_effr, feff, hc, fcl, facl, rcl, htcl, vpa, he, ere = \ _memi_constant_vars(ta, vel, rh, b_press, met, clo, age, sex, ht, m_body, pos) # determine a starting guess for the human temperatures using constant variables t_core_in = 36.6 # normal human body temperature t_env = (ta + tr) / 2 # the operative temperature of the surrounding environment r_body = (1 / htcl) - rcl r_tot = r_body + rcl t_sk_in = t_core_in * (rcl / r_tot) + t_env * (r_body / r_tot) t_clo_in = (ta + tr + t_sk_in) / 3 # average of air, radiant, and skin temperature # find a steady state solution to the MEMI model balance under the input conditions d_args = (ta, tr, a_du, a_clo, a_effr, feff, hc, fcl, facl, rcl, htcl, vpa, he, ere) for i in (1, 2, 3, 4, 5, 6): # progressively widen search range t_min = (t_core_in - (5 * i), t_sk_in - (10 * i), t_clo_in - (10 * i)) t_max = (t_core_in + (5 * i), t_sk_in + (10 * i), t_clo_in + (10 * i)) try: tn = secant_three_var( t_min, t_max, _memi_dynamic_balance, epsilon, other_args=d_args) except OverflowError: # about 1% of the time, the solution diverges tn = None if tn is not None: break # model converged and root was found if tn is None: # if we still don't have convergence, try brute force method starting_guess = (t_core_in, t_sk_in, t_clo_in) increments = (0.001, 0.01, 0.01) # increments with which to adjust body temp err = 0.1 # maximum allowed error in [W] tn = _brute_force_three_var( starting_guess, increments, _memi_dynamic_balance, err, other_args=d_args) # compute the PET using the human subject temperatures using a bisection method def f(tx): """A function with the input variables of the PET reference situation.""" return memi_balance( tn, tx, tx, 0.1, 50, met, 0.9, age, sex, ht, m_body, pos, b_press, False, True) ti = -40 # start of the search interval tf = 60 # end of the search interval pet = 0 while tf - ti > epsilon: # bisection loop if f(ti) * f(pet) < 0: tf = pet else: ti = pet pet = (ti + tf) / 2 # put all of the results into a single dictionary return {'pet': pet, 't_core': tn[0], 't_skin': tn[1], 't_clo': tn[2]}
[docs] def memi_balance( t_human, ta, tr, vel, rh, met, clo, age, sex, ht, m_body, pos, b_press=101325, actual=True, scalar=False): """Perform the human load balance using Munich energy balance model (MEMI). Args: t_human: A list of three values for the temperature of the human subject. * core body temperature [C] * skin temperature [C] * clothing temperature [C] ta: Air temperature [C]. tr: Mean radiant temperature [C]. vel: Relative air velocity [m/s]. rh: Relative humidity [%]. met: Metabolic rate [met]. clo: Clothing [clo]. age: The age of the human subject [years]. sex: A value between 0 and 1 to indicate the sex of the human [0=male, 1=female]. ht: The height of the human subject in [m]. m_body: The body mass of the human subject in [kg]. pos: Text for the posture of the human subject. [standing, seated, crouching]. b_press: The barometric air pressure [Pa]. (Default: 101325 for sea level). actual: A boolean to indicate whether the calculation should be performed in the actual environment (True) or the reference environment (False). scalar: A boolean for whether the result should be returned as a vector of the energy flux across [core, skin, clo] or it should be a single scalar energy flux for the entire human subject. Returns: The energy flux across the entire human subject if scalar is True or a vector with energy flux across [core, skin, clo] if scalar is False. """ # compute constant attributes of the model a_du, a_clo, a_effr, feff, hc, fcl, facl, rcl, htcl, vpa, he, ere = \ _memi_constant_vars( ta, vel, rh, b_press, met, clo, age, sex, ht, m_body, pos, actual) # compute the dynamic load balance of the model return _memi_dynamic_balance( t_human, ta, tr, a_du, a_clo, a_effr, feff, hc, fcl, facl, rcl, htcl, vpa, he, ere, scalar)
def _memi_constant_vars( ta, vel, rh, b_press, met, clo, age, sex, ht, m_body, pos, actual=True): """Compute variables of the MEMI model that do not change with body temperature. This includes things such as the surface are of the human, basal metabolism, vapor pressure, convection coefficients, radiant efficiency, etc. Args: ta: Air temperature [C]. vel: Relative air velocity [m/s]. rh: Relative humidity [%]. b_press: The barometric air pressure [Pa]. (Default: 101325 for sea level). met: Metabolic rate [met]. clo: Clothing [clo]. age: The age of the human subject [years]. sex: A value between 0 and 1 to indicate the sex of the human [0=male, 1=female]. ht: The height of the human subject in [m]. m_body: The body mass of the human subject in [kg]. pos: Text for the posture of the human subject. [standing, seated, crouching]. actual: A boolean to indicate whether the calculation should be performed in the actual environment (True) or the reference environment (False). Returns: A tuple of variables that are constant throughout the computation of the MEMI model energy balance. """ # compute tha area parameters of the body a_du = 0.203 * m_body ** 0.425 * ht ** 0.725 # Dubois surface area of human subject feff = 0.725 if pos in ('standing', 'crouching') else 0.696 # radiant efficiency # increase the Burton surface to account for clothing, k = 0.31 for Hoeppe fcl = 1 + (0.31 * clo) # increase heat exchange surface depending on clothing level facl = (173.51 * clo - 2.36 - 100.76 * clo * clo + 19.28 * clo ** 3.0) / 100 a_clo = a_du * facl + a_du * (fcl - 1.0) a_effr = a_du * feff # effective radiative area derived from posture of the subject # partial pressure of water depending on relative humidity and air temperature if actual: # the calculation of the actual vapor pressure of inputs vpa = rh / 100.0 * 6.105 * math.exp(17.27 * ta / (237.7 + ta)) # [hPa] else: # use reference temperature, humidity and barometric pressure vpa = 12 # [hPa] vapour pressure of the standard environment # convection coefficient depending on air speed and subject posture if pos == 'standing': hc = 2.67 + (6.5 * vel ** 0.67) elif pos == 'seated': hc = 2.26 + (7.42 * vel ** 0.67) elif pos == 'crouching': hc = 8.6 * (vel ** 0.513) # modification of hc with the total air pressure hc = hc * (b_press / P_REF) ** 0.55 # compute base metabolism for men and women in [W] r_fem = ht * 100.0 / m_body ** (1.0 / 3.0) - 42.1 metab_female = 3.19 * m_body ** 0.75 * (1.0 + 0.004 * (30.0 - age) + 0.018 * r_fem) r_mal = ht * 100.0 / m_body ** (1.0 / 3.0) - 43.4 metab_male = 3.45 * m_body ** 0.75 * (1.0 + 0.004 * (30.0 - age) + 0.01 * r_mal) # compute the total metabolism, accounting for the activity level if actual: # actual human subject metabolic rate mec = (metab_male * 1.17 * met) / a_du # [W/m2] fec = (metab_female * 1.22 * met) / a_du # [W/m2] else: # reference human subject metabolic rate assumes 80 W of activity level mec = (80 + metab_male) / a_du # [W/m2] fec = (80 + metab_female) / a_du # [W/m2] # attribution of internal energy depending on the sex of the subject he = ((1 - sex) * mec) + (sex * fec) # [W/m2] # compute the respiratory energy losses from the metabolic rate texp = 0.47 * ta + 21.0 # [degC] dventpulm = he * 1.44 * (10.0 ** -6) # pulmonary flow rate eres = C_AIR * (ta - texp) * dventpulm # sensible heat loss [W/m2] vpexp = 6.11 * 10.0 ** (7.45 * texp / (235.0 + texp)) p_hpa = b_press / 100 # barometric pressure [hPa] erel = 0.623 * L_VAP / p_hpa * (vpa - vpexp) * dventpulm # latent heat loss [W/m2] ere = eres + erel # total respiratory heat loss [W/m2] # compute the clothed fraction of the body and the clothing thickness rcl = clo / 6.45 # convert [clo] to [m2-K/W] if facl > 1.0: # ensure that clothing does not cover more than 100% facl = 1.0 y = 0 # thickness of the clothing layer if clo >= 2.0: y = 1.0 elif clo > 0.6: y = (ht - 0.2) / ht elif clo > 0.3: y = 0.5 elif clo > 0.0: y = 0.1 # compute subject radius depending on the clothing level (6.28 = 2 * pi) r2 = a_du * (fcl - 1.0 + facl) / (6.28 * ht * y) # external radius r1 = facl * a_du / (6.28 * ht * y) # internal radius di = r2 - r1 # compute the equivalent thermal resistance of body tissues htcl = (6.28 * ht * y * di) / (rcl * math.log(r2 / r1) * a_clo) # [W/(m2-K)] # return all of the variables used by the rest of the MEMI calculation return a_du, a_clo, a_effr, feff, hc, fcl, facl, rcl, htcl, vpa, he, ere def _memi_dynamic_balance( t_human, ta, tr, a_du, a_clo, a_effr, feff, hc, fcl, facl, rcl, htcl, vpa, he, ere, scalar=False): """Compute the dynamic load balance of the MEMI model. Args: t_human: A list of three values for the temperature of the human subject. * core body temperature [C] * skin temperature [C] * clothing temperature [C] ta: Air temperature [C]. tr: Mean radiant temperature [C]. a_du: The Dubois surface area of the human subject [m2]. a_clo: The clothing surface area of the human subject [m2]. a_effr: The effective radiant surface area of the human subject [m2]. feff: The fractional radiant efficiency of the human subject. hc: The coefficient of convective thermal transfer [W/m2-k]. fcl: The fractional increase in heat exchange surface depending on clothing. facl: The fraction of the human subject covered in clothing. rcl: The thermal resistance of the clothing layer [m2-K/W]. htcl: The thermal transmittance of all body tissues and clothing [W/(m2-K)]. vpa: The partial vapour pressure of the environment [hPa]. he: The internal heat energy generated by the human subject [W/m2]. ere: The total respiratory heat loss [W/m2]. scalar: A boolean for whether the result should be returned as a vector of the energy flux across [core, skin, clo] or it should be a single scalar energy flux for the entire human subject. Returns: The energy flux across the entire human subject if scalar is True or a vector with energy flux across [core, skin, clo] if scalar is False. """ # unpack the array of temperatures of the human subject and get average body temp t_core, t_sk, t_clo = t_human alpha = 0.1 # constant in steady state model but updates t_body in transient model t_body = alpha * t_sk + (1 - alpha) * t_core # compute sweat losses qmsw = sweat_volume(t_body) # L_VAP / 1000 --> [J/g] ; qwsw / 3600 --> [g/m2-s] esw = (L_VAP / 1000) * (qmsw / 3600) # [W/m2] # saturation vapor pressure at temperature tsk pv_sk = 6.105 * math.exp((17.27 * (t_sk + 273.15) - 4717.03) / (237.7 + t_sk)) # hPa # compute vapour transfer lw = 1.67 # Lewis factor [K/hPa] he_diff = hc * lw # diffusion coefficient of air layer fecl = 1 / (1 + 0.92 * hc * rcl) # Burton efficiency factor emax = he_diff * fecl * (pv_sk - vpa) # maximum diffusion at skin surface w = esw / emax # skin wettedness if w > 1: w = 1 delta = esw - emax if delta < 0: esw = emax if esw < 0: esw = 0 i_m = 0.38 # Woodcock's ratio r_ecl = (1 / (fcl * hc) + rcl) / (lw * i_m) # clothing vapour transfer resistance ediff = (1 - w) * (pv_sk - vpa) / r_ecl # diffusion heat transfer evap = -(ediff + esw) # [W/m2] # compute radiation losses tr_k, tsk_k, tclo_k = tr + 273.15, t_sk + 273.15, t_clo + 273.15 # for bare skin area [W/m2] rbare = a_effr * (1.0 - facl) * EM_SK * SIGM * (tr_k ** 4 - tsk_k ** 4) / a_du # for dressed area [W/m2] rclo = feff * a_clo * EM_CL * SIGM * (tr_k ** 4 - tclo_k ** 4) / a_du # compute convection losses # # for bare skin area: cbare = hc * (ta - t_sk) * a_du * (1.0 - facl) / a_du # [W/m2] # for dressed area: cclo = hc * (ta - t_clo) * a_clo / a_du # [W/m2] # return either the calculated [core, skin, clo] energy flux or the scalar sum if scalar: # return the scalar sum of the energy balance return he + ere + rclo + rbare + cclo + cbare + evap else: # produce a vector of 3 energy fluxes across [core, skin, clo] vaso_ex = (vaso_circulation(t_core, t_sk) / 3600 * C_B + 5.28) * (t_core - t_sk) clo_ex = htcl * (t_sk - t_clo) e_core = he + ere - vaso_ex # core balance [W/m2] e_sk = rbare + cbare + evap + vaso_ex - clo_ex # skin balance [W/m2] e_clo = cclo + rclo + clo_ex # clothes balance [W/m2] return (e_core, e_sk, e_clo)
[docs] def vaso_circulation(t_core, t_skin): """Calculate skin blood flow (vaso-circulation) using core and skin temperatures. Args: t_core: The core temperature of the human subject. [C]. t_skin: The skin temperature of the human subject. [C]. Returns: A number for the volume of blood flow in L/m2-h. """ # set value signals sig_skin = TSK_SET - t_skin sig_core = t_core - TC_SET if sig_core < 0: # t_core < TC_SET --> the blood flow is reduced sig_core = 0 if sig_skin < 0: # t_skin > TSK_SET --> the blood flow is increased sig_skin = 0 # 6.3 L/m2-h is the standard volume of the blood flow qm_blood = (6.3 + 75 * sig_core) / (1 + 0.5 * sig_skin) return qm_blood if qm_blood < 90 else 90 # 90 L/m2-h is the blood flow upper limit
[docs] def sweat_volume(t_body): """Calculate skin sweat volume using average body temperatures. Args: t_body: The average body temperature of the human subject. [C]. Returns: A number for the volume of sweat in g/m2-h. """ sig_body = t_body - TBODY_SET if sig_body < 0: # Tbody < Tbody_set --> The sweat flow is 0 sig_body = 0 qm_sw = 304.94 * 10 ** -3 * sig_body return qm_sw if qm_sw < 500 else 500 # 500 g/m2-h is the upper limit of sweat rate
[docs] def pet_category(pet): """Get the category of heat/cold stress associated PET. This method uses the original categories developed by Matzarakis and Mayer (1996), which are most suitable in temperature climates. Each number (from -4 to +4) represents a certain PET thermal sensation category. * -4 = Extreme Cold * -3 = Strong Cold * -2 = Moderate Cold * -1 = Slight Cold * 0 = Comfortable * 1 = Slight Heat * 2 = Moderate Heat * 3 = Strong Heat * 4 = Extreme Heat Args: pet: Physiological Equivalent Temperature [C]. Returns: category -- An integer indicating the level of heat/cold stress associated with the PET. """ if pet < 4: return -4 elif pet < 8: return -3 elif pet < 13: return -2 elif pet < 18: return -1 elif pet <= 23: return 0 elif pet <= 29: return 1 elif pet <= 35: return 2 elif pet <= 41: return 3 else: return 4
[docs] def pet_category_humid(pet): """Get the category of heat/cold stress associated PET. This method uses the categories by Lin and Matzarakis (2008), which are suitable for tropical and subtropical humid climates. Each number (from -4 to +4) represents a certain PET thermal sensation category. * -4 = Extreme Cold * -3 = Strong Cold * -2 = Moderate Cold * -1 = Slight Cold * 0 = Comfortable * 1 = Slight Heat * 2 = Moderate Heat * 3 = Strong Heat * 4 = Extreme Heat Args: pet: Physiological Equivalent Temperature [C]. Returns: category -- An integer indicating the level of heat/cold stress associated with the PET. """ if pet < 14: return -4 elif pet < 18: return -3 elif pet < 22: return -2 elif pet < 26: return -1 elif pet <= 30: return 0 elif pet <= 34: return 1 elif pet <= 38: return 2 elif pet <= 42: return 3 else: return 4
[docs] def core_temperature_category(t_core): """Get the classification of core body temperature. * -2 = Hypothermia * -1 = Cold * 0 = Normal * 1 = Hot * 2 = Hyperthermia Args: t_core: The core body temperature of the human subject [C]. Returns: category -- An integer indicating the classification of the body temperature. Note: [1] Marx J (2006). Rosen's emergency medicine : concepts and clinical practice (6th ed.). Philadelphia: Mosby/Elsevier. p. 2239. ISBN 978-0-323-02845-5. OCLC 58533794. """ if t_core < 35: return -2 elif t_core < 36.5: return -1 elif t_core < 37.5: return 0 elif t_core < 38.3: return 1 else: return 2
def _brute_force_three_var( starting_guess, increments, fn, epsilon, other_args, max_iter=100000): """Brute force root-finding algorithm designed specifically for solving MEMI models. It is reliable. However, it converges slowly and, for this reason, it is recommended that this only be used after the secant_three_var() method has returned None. Args: starting_guess: A tuple with 3 numbers with a starting guess for the roots. increments: A tuple with 3 numbers indicating the increments with which each value should be adjusted to find the root. fn: A function for which roots are to be solved. That is, where the output of the function is a tuple of three zeros. epsilon: The acceptable error in the output of the function (aka. how far from zero it is allowed to be). other_args: Other input arguments for the fn other than the ones being adjusted to solve the root. max_iter: The maximum number of iterations with which a root will be sought. (Default: 100000). Returns: root -- a tuple of 3 values that return a vector of zeros from the fn. """ curr_i = 0 in_args = (starting_guess,) + other_args en_vec = fn(*in_args) while not all(abs(v) < epsilon for v in en_vec): new_guess = list(starting_guess) for i, hf in enumerate(en_vec): if hf > epsilon: new_guess[i] = new_guess[i] + increments[i] elif hf < -epsilon: new_guess[i] = new_guess[i] - increments[i] starting_guess = new_guess in_args = (starting_guess,) + other_args en_vec = fn(*in_args) curr_i += 1 if curr_i == max_iter: break return starting_guess