from __future__ import annotations
import math
import numpy as np
from pythermalcomfort.classes_input import SolarGainInputs
from pythermalcomfort.classes_return import SolarGain
from pythermalcomfort.utilities import Postures, transpose_sharp_altitude
[docs]
def solar_gain(
sol_altitude: float | list[float],
sharp: float | list[float],
sol_radiation_dir: float | list[float],
sol_transmittance: float | list[float],
f_svv: float | list[float],
f_bes: float | list[float],
asw: float | list[float] = 0.7,
posture: str = Postures.sitting.value,
floor_reflectance: float | list[float] = 0.6,
round_output: bool = True,
) -> SolarGain:
"""Calculate the solar gain to the human body using the Effective Radiant Field
(ERF) [55ASHRAE2023]_. The ERF is a measure of the net energy flux to or from the
human body. ERF is expressed in W over human body surface area [W/m2]. In addition,
it calculates the delta mean radiant temperature. Which is the amount by which the
mean radiant temperature of the space should be increased if no solar radiation is
present.
Parameters
----------
sol_altitude : float or list of floats
Solar altitude, degrees from horizontal [deg]. Ranges between 0 and 90.
sharp : float or list of floats
Solar horizontal angle relative to the front of the person (SHARP) [deg].
Ranges between 0 and 180 and is symmetrical on either side. Zero (0) degrees
represents direct-beam radiation from the front, 90 degrees represents
direct-beam radiation from the side, and 180 degrees represents direct-beam
radiation from the back. SHARP is the angle between the sun and the person
only. Orientation relative to compass or to room is not included in SHARP.
sol_radiation_dir : float or list of floats
Direct-beam solar radiation, [W/m2]. Ranges between 200 and 1000. See Table
C2-3 of ASHRAE 55 2020 [55ASHRAE2023]_.
sol_transmittance : float or list of floats
Total solar transmittance, ranges from 0 to 1. The total solar
transmittance of window systems, including glazing unit, blinds, and other
façade treatments, shall be determined using one of the following methods:
i) Provided by manufacturer or from the National Fenestration Rating
Council approved Lawrence Berkeley National Lab International Glazing
Database.
ii) Glazing unit plus venetian blinds or other complex or unique shades
shall be calculated using National Fenestration Rating Council approved
software or Lawrence Berkeley National Lab Complex Glazing Database.
f_svv : float or list of floats
Fraction of sky-vault view fraction exposed to body, ranges from 0 to 1.
It can be calculated using the function
:py:meth:`pythermalcomfort.utilities.f_svv`.
f_bes : float or list of floats
Fraction of the possible body surface exposed to sun, ranges from 0 to 1.
See Table C2-2 and equation C-7 ASHRAE 55 2020 [55ASHRAE2023]_.
asw : float or list of floats, optional
The average short-wave absorptivity of the occupant. It will range widely,
depending on the color of the skin of the occupant as well as the color and
amount of clothing covering the body. Defaults to 0.7.
.. note::
Short-wave absorptivity typically ranges from 0.57 to 0.84, depending
on skin and clothing color. More information is available in Blum (1945).
posture : str, optional
Default 'sitting' list of available options 'standing', 'supine' or 'sitting'.
floor_reflectance : float or list of floats, optional
Floor reflectance. It is assumed to be constant and equal to 0.6. Defaults to 0.6.
round_output : bool, optional
If True, rounds output value. If False, it does not round it. Defaults to True.
Returns
-------
SolarGain
A dataclass containing the solar gain to the human body and delta mean radiant temperature.
See :py:class:`~pythermalcomfort.classes_return.SolarGain` for more details.
To access the `erf` and `delta_mrt` values, use the corresponding attributes of the returned `SolarGain` instance, e.g., `result.erf`.
Raises
------
ValueError
If the posture is not one of 'standing', 'supine', or 'sitting'.
Examples
--------
.. code-block:: python
from pythermalcomfort.models import solar_gain
result = solar_gain(
sol_altitude=0,
sharp=120,
sol_radiation_dir=800,
sol_transmittance=0.5,
f_svv=0.5,
f_bes=0.5,
asw=0.7,
posture="sitting",
)
print(result.erf) # 42.9
print(result.delta_mrt) # 10.3
Applicability
-------------
This model is applicable under the following conditions:
- Solar altitude (sol_altitude) must be between 0° and 90°.
- Solar horizontal angle (sharp) must be between 0° and 180°.
- Direct-beam solar radiation (sol_radiation_dir) should typically range from 200 to 1000 W/m², as per ASHRAE 55 Table C2-3.
- Solar transmittance (sol_transmittance) must be between 0 and 1.
- Sky-vault view fraction (f_svv) and body surface exposure fraction (f_bes) must be between 0 and 1.
- Average short-wave absorptivity (asw) typically ranges from 0.57 to 0.84, depending on skin and clothing color.
- Posture must be one of 'standing', 'supine', or 'sitting'.
- Floor reflectance (floor_reflectance) is assumed constant at 0.6, but can vary.
- All inputs are in SI units (e.g., angles in degrees, radiation in W/m²).
"""
# Validate inputs using the SolarGainInputs class
SolarGainInputs(
sol_altitude=sol_altitude,
sharp=sharp,
sol_radiation_dir=sol_radiation_dir,
sol_transmittance=sol_transmittance,
f_svv=f_svv,
f_bes=f_bes,
asw=asw,
floor_reflectance=floor_reflectance,
)
sol_altitude = np.asarray(sol_altitude)
sharp = np.asarray(sharp)
sol_radiation_dir = np.asarray(sol_radiation_dir)
sol_transmittance = np.asarray(sol_transmittance)
f_svv = np.asarray(f_svv)
f_bes = np.asarray(f_bes)
asw = np.asarray(asw)
floor_reflectance = np.asarray(floor_reflectance)
posture = posture.lower()
if posture not in [
Postures.standing.value,
Postures.supine.value,
Postures.sitting.value,
]:
error_msg_posture = (
"Posture has to be either 'standing', 'supine' or 'sitting'."
)
raise ValueError(error_msg_posture)
erf, d_mrt = _solar_gain_vectorised(
sol_altitude=sol_altitude,
sharp=sharp,
sol_radiation_dir=sol_radiation_dir,
sol_transmittance=sol_transmittance,
f_svv=f_svv,
f_bes=f_bes,
asw=asw,
floor_reflectance=floor_reflectance,
posture=posture,
)
if round_output:
erf = np.round(erf, 1)
d_mrt = np.round(d_mrt, 1)
return SolarGain(erf=erf, delta_mrt=d_mrt)
@np.vectorize
def _solar_gain_vectorised(
sol_altitude,
sharp,
sol_radiation_dir,
sol_transmittance,
f_svv,
f_bes,
asw,
posture,
floor_reflectance,
):
def find_span(arr, x):
for i in range(len(arr)):
if arr[i + 1] >= x >= arr[i]:
return i
return -1
deg_to_rad = 0.0174532925
hr = 6
i_diff = 0.2 * sol_radiation_dir
# fp is the projected area factor
fp_table = [
[0.35, 0.35, 0.314, 0.258, 0.206, 0.144, 0.082],
[0.342, 0.342, 0.31, 0.252, 0.2, 0.14, 0.082],
[0.33, 0.33, 0.3, 0.244, 0.19, 0.132, 0.082],
[0.31, 0.31, 0.275, 0.228, 0.175, 0.124, 0.082],
[0.283, 0.283, 0.251, 0.208, 0.16, 0.114, 0.082],
[0.252, 0.252, 0.228, 0.188, 0.15, 0.108, 0.082],
[0.23, 0.23, 0.214, 0.18, 0.148, 0.108, 0.082],
[0.242, 0.242, 0.222, 0.18, 0.153, 0.112, 0.082],
[0.274, 0.274, 0.245, 0.203, 0.165, 0.116, 0.082],
[0.304, 0.304, 0.27, 0.22, 0.174, 0.121, 0.082],
[0.328, 0.328, 0.29, 0.234, 0.183, 0.125, 0.082],
[0.344, 0.344, 0.304, 0.244, 0.19, 0.128, 0.082],
[0.347, 0.347, 0.308, 0.246, 0.191, 0.128, 0.082],
]
if posture == Postures.sitting.value:
fp_table = [
[0.29, 0.324, 0.305, 0.303, 0.262, 0.224, 0.177],
[0.292, 0.328, 0.294, 0.288, 0.268, 0.227, 0.177],
[0.288, 0.332, 0.298, 0.29, 0.264, 0.222, 0.177],
[0.274, 0.326, 0.294, 0.289, 0.252, 0.214, 0.177],
[0.254, 0.308, 0.28, 0.276, 0.241, 0.202, 0.177],
[0.23, 0.282, 0.262, 0.26, 0.233, 0.193, 0.177],
[0.216, 0.26, 0.248, 0.244, 0.22, 0.186, 0.177],
[0.234, 0.258, 0.236, 0.227, 0.208, 0.18, 0.177],
[0.262, 0.26, 0.224, 0.208, 0.196, 0.176, 0.177],
[0.28, 0.26, 0.21, 0.192, 0.184, 0.17, 0.177],
[0.298, 0.256, 0.194, 0.174, 0.168, 0.168, 0.177],
[0.306, 0.25, 0.18, 0.156, 0.156, 0.166, 0.177],
[0.3, 0.24, 0.168, 0.152, 0.152, 0.164, 0.177],
]
if posture == Postures.supine.value:
sharp, sol_altitude = transpose_sharp_altitude(sharp, sol_altitude)
alt_range = [0, 15, 30, 45, 60, 75, 90]
az_range = [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180]
alt_i = find_span(alt_range, sol_altitude)
az_i = find_span(az_range, sharp)
fp11 = fp_table[az_i][alt_i]
fp12 = fp_table[az_i][alt_i + 1]
fp21 = fp_table[az_i + 1][alt_i]
fp22 = fp_table[az_i + 1][alt_i + 1]
az1 = az_range[az_i]
az2 = az_range[az_i + 1]
alt1 = alt_range[alt_i]
alt2 = alt_range[alt_i + 1]
fp = fp11 * (az2 - sharp) * (alt2 - sol_altitude)
fp += fp21 * (sharp - az1) * (alt2 - sol_altitude)
fp += fp12 * (az2 - sharp) * (sol_altitude - alt1)
fp += fp22 * (sharp - az1) * (sol_altitude - alt1)
fp /= (az2 - az1) * (alt2 - alt1)
f_eff = 0.725 # fraction of the body surface exposed to environmental radiation
if posture == Postures.sitting.value:
f_eff = 0.696
sw_abs = asw
lw_abs = 0.95
e_diff = f_eff * f_svv * 0.5 * sol_transmittance * i_diff
e_direct = f_eff * fp * sol_transmittance * f_bes * sol_radiation_dir
e_reflected = (
f_eff
* f_svv
* 0.5
* sol_transmittance
* (sol_radiation_dir * math.sin(sol_altitude * deg_to_rad) + i_diff)
* floor_reflectance
)
e_solar = e_diff + e_direct + e_reflected
erf = e_solar * (sw_abs / lw_abs)
d_mrt = erf / (hr * f_eff)
return erf, d_mrt