Skip to content

Commit

Permalink
feat(datatype): Add Data Types for Thermal Comfort
Browse files Browse the repository at this point in the history
feat(datatype): Add Data Types for Thermal Comfort
  • Loading branch information
Chris Mackey authored Mar 3, 2019
2 parents 77c2541 + c63e407 commit ee68bb9
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 27 deletions.
76 changes: 72 additions & 4 deletions ladybug/_datacollectionbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(self, header, values, datetimes):
assert isinstance(header, Header), \
'header must be a Ladybug Header object. Got {}'.format(type(header))
assert isinstance(datetimes, Iterable) \
and not isinstance(datetimes, (str, dict)), \
and not isinstance(datetimes, (str, dict, bytes, bytearray)), \
'datetimes should be a list or tuple. Got {}'.format(type(datetimes))
datetimes = list(datetimes)

Expand Down Expand Up @@ -76,7 +76,8 @@ def values(self):

@values.setter
def values(self, values):
assert isinstance(values, Iterable) and not isinstance(values, (str, dict)), \
assert isinstance(values, Iterable) and not \
isinstance(values, (str, dict, bytes, bytearray)), \
'values should be a list or tuple. Got {}'.format(type(values))
values = list(values)
assert len(values) == len(self.datetimes), \
Expand Down Expand Up @@ -289,7 +290,8 @@ def get_aligned_collection(self, value=0, data_type=None, unit=None):
number of values and have matching datetimes.
Args:
value: The value to be repeated in the aliged collection values.
value: A value to be repeated in the aliged collection values or
A list of values that has the same length as this collection.
Default: 0.
data_type: The data type of the aligned collection. Default is to
use the data type of this collection.
Expand All @@ -305,7 +307,14 @@ def get_aligned_collection(self, value=0, data_type=None, unit=None):
else:
data_type = self.header.data_type
unit = unit or self.header.unit
values = [value] * len(self._values)
if isinstance(value, Iterable) and not isinstance(
value, (str, dict, bytes, bytearray)):
assert len(value) == len(self._values), "Length of value ({}) must match "\
"the length of this collection's values ({})".format(
len(value), len(self._values))
values = value
else:
values = [value] * len(self._values)
header = Header(data_type, unit, self.header.analysis_period,
self.header.metadata)
collection = self.__class__(header, values, self.datetimes)
Expand Down Expand Up @@ -407,6 +416,65 @@ def are_collections_aligned(data_collections, raise_exception=True):
return False
return True

@staticmethod
def compute_function_aligned(funct, data_collections, data_type, unit):
"""Compute a function with a list of aligned data collections or individual values.
Args:
funct: A function with a single numerical value as output and one or
more numerical values as input.
data_collections: A list with a length equal to the number of arguments
for the function. Items of the list can be either Data Collections
or individual values to be used at each datetime of other collections.
data_type: An instance of a Ladybug data type that describes the results
of the funct.
unit: The units of the funct results.
Return:
A Data Collection with the results function. If all items in this list of
data_collections are individual values, only a single value will be returned.
Usage:
from ladybug.datacollection import HourlyContinuousCollection
from ladybug.epw import EPW
from ladybug.psychrometrics import humid_ratio_from_db_rh
from ladybug.datatype.percentage import HumidityRatio
epw_file_path = './epws/denver.epw'
denver_epw = EPW(epw_file_path)
pressure_at_denver = 85000
hr_inputs = [denver_epw.dry_bulb_temperature,
denver_epw.relative_humidity,
pressure_at_denver]
humid_ratio = HourlyContinuousCollection.compute_function_aligned(
humid_ratio_from_db_rh, hr_inputs, HumidityRatio(), 'fraction')
# humid_ratio will be a Data Colleciton of humidity ratios at Denver
"""
# check that all inputs are either data collections or floats
data_colls = []
for i, func_input in enumerate(data_collections):
if isinstance(func_input, BaseCollection):
data_colls.append(func_input)
else:
try:
data_collections[i] = float(func_input)
except ValueError:
raise TypeError('Expected a number or a Data Colleciton. '
'Got {}'.format(type(func_input)))

# run the function and return the result
if len(data_colls) == 0:
return funct(*data_collections)
else:
BaseCollection.are_collections_aligned(data_colls)
val_len = len(data_colls[0].values)
for i, col in enumerate(data_collections):
data_collections[i] = [col] * val_len if isinstance(col, float) else col
result = data_colls[0].get_aligned_collection(data_type=data_type, unit=unit)
for i in xrange(val_len):
result[i] = funct(*[col[i] for col in data_collections])
return result

def is_in_data_type_range(self, raise_exception=True):
"""Check if the Data Collection values are in permissable ranges for the data_type.
Expand Down
23 changes: 16 additions & 7 deletions ladybug/datacollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __init__(self, header, values, datetimes):
assert isinstance(header.analysis_period, AnalysisPeriod), \
'header of {} must have an analysis_period.'.format(self.__class__.__name__)
assert isinstance(datetimes, Iterable) \
and not isinstance(datetimes, (str, dict)), \
and not isinstance(datetimes, (str, dict, bytes, bytearray)), \
'datetimes should be a list or tuple. Got {}'.format(type(datetimes))
datetimes = list(datetimes)

Expand Down Expand Up @@ -611,7 +611,8 @@ def values(self):

@values.setter
def values(self, values):
assert isinstance(values, Iterable) and not isinstance(values, (str, dict)), \
assert isinstance(values, Iterable) and not isinstance(
values, (str, dict, bytes, bytearray)), \
'values should be a list or tuple. Got {}'.format(type(values))
values = list(values)
if self.header.analysis_period.is_annual:
Expand Down Expand Up @@ -867,7 +868,8 @@ def get_aligned_collection(self, value=0, data_type=None, unit=None):
have the same number of values and have matching datetimes.
Args:
value: The value to be repeated in the aliged collection values.
value: A value to be repeated in the aliged collection values or
A list of values that has the same length as this collection.
Default: 0.
data_type: The data type of the aligned collection. Default is to
use the data type of this collection.
Expand All @@ -883,7 +885,14 @@ def get_aligned_collection(self, value=0, data_type=None, unit=None):
else:
data_type = self.header.data_type
unit = unit or self.header.unit
values = [value] * len(self._values)
if isinstance(value, Iterable) and not isinstance(
value, (str, dict, bytes, bytearray)):
assert len(value) == len(self._values), "Length of value ({}) must match "\
"the length of this collection's values ({})".format(
len(value), len(self._values))
values = value
else:
values = [value] * len(self._values)
header = Header(data_type, unit, self.header.analysis_period,
self.header.metadata)
return self.__class__(header, values)
Expand Down Expand Up @@ -991,7 +1000,7 @@ def __init__(self, header, values, datetimes):
assert isinstance(header.analysis_period, AnalysisPeriod), \
'header of {} must have an analysis_period.'.format(self.__class__.__name__)
assert isinstance(datetimes, Iterable) \
and not isinstance(datetimes, (str, dict)), \
and not isinstance(datetimes, (str, dict, bytes, bytearray)), \
'datetimes should be a list or tuple. Got {}'.format(type(datetimes))
datetimes = list(datetimes)

Expand Down Expand Up @@ -1133,7 +1142,7 @@ def __init__(self, header, values, datetimes):
assert isinstance(header.analysis_period, AnalysisPeriod), \
'header of {} must have an analysis_period.'.format(self.__class__.__name__)
assert isinstance(datetimes, Iterable) \
and not isinstance(datetimes, (str, dict)), \
and not isinstance(datetimes, (str, dict, bytes, bytearray)), \
'datetimes should be a list or tuple. Got {}'.format(type(datetimes))
datetimes = list(datetimes)

Expand Down Expand Up @@ -1255,7 +1264,7 @@ def __init__(self, header, values, datetimes):
assert isinstance(header.analysis_period, AnalysisPeriod), \
'header of {} must have an analysis_period.'.format(self.__class__.__name__)
assert isinstance(datetimes, Iterable) \
and not isinstance(datetimes, (str, dict)), \
and not isinstance(datetimes, (str, dict, bytes, bytearray)), \
'datetimes should be a list or tuple. Got {}'.format(type(datetimes))
datetimes = list(datetimes)

Expand Down
6 changes: 6 additions & 0 deletions ladybug/datatype/percentage.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ class RelativeHumidity(Percentage):
_missing_epw = 999


class HumidityRatio(Percentage):
_min = 0
_max = 100
_abbreviation = 'HR'


class TotalSkyCover(Percentage):
# (used if Horizontal IR Intensity missing)
_min = 0
Expand Down
4 changes: 4 additions & 0 deletions ladybug/datatype/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,7 @@ class StandardEffectiveTemperature(Temperature):

class UniversalThermalClimateIndex(Temperature):
_abbreviation = 'UTCI'


class PrevailingOutdoorTemperature(Temperature):
_abbreviation = 'Tprevail'
60 changes: 60 additions & 0 deletions ladybug/datatype/temperaturedelta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# coding=utf-8
"""Temperature Delta data type."""
from __future__ import division

from .base import DataTypeBase


class TemperatureDelta(DataTypeBase):
"""Temperature"""
_units = ('C', 'F', 'K')
_si_units = ('C', 'K')
_ip_units = ('F')
_abbreviation = 'DeltaT'

def _C_to_F(self, value):
return value * 9. / 5.

def _C_to_K(self, value):
return value

def _F_to_C(self, value):
return value * (5. / 9.)

def _K_to_C(self, value):
return value

def to_unit(self, values, unit, from_unit):
"""Return values converted to the unit given the input from_unit."""
return self._to_unit_base('C', values, unit, from_unit)

def to_ip(self, values, from_unit):
"""Return values in IP and the units to which the values have been converted."""
if from_unit == 'F':
return values, from_unit
else:
return self.to_unit(values, 'F', from_unit), 'F'

def to_si(self, values, from_unit):
"""Return values in SI and the units to which the values have been converted."""
if from_unit == 'C' or from_unit == 'K':
return values, from_unit
else:
return self.to_unit(values, 'C', from_unit), 'C'

@property
def isTemperatureDelta(self):
"""Return True."""
return True


class AirTemperatureDelta(TemperatureDelta):
_abbreviation = 'DeltaTair'


class RadiantTemperatureDelta(TemperatureDelta):
_abbreviation = 'DeltaTrad'


class OperativeTemperatureDelta(TemperatureDelta):
_abbreviation = 'DeltaTo'
56 changes: 51 additions & 5 deletions ladybug/datatype/thermalcondition.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,56 @@ class PredictedMeanVote(ThermalCondition):
'+1 = Slightly Warm, +2 = Warm, +3 = Hot'


class UTCICondition(ThermalCondition):
_abbreviation = 'UTCIcond'
_unit_descr = '-4 = Extreme Cold, -3 = Very Strong Cold, '\
class DiscomfortReason(ThermalCondition):
_abbreviation = 'RDiscomf'
_unit_descr = '-2 = Too Dry, -1 = Too Cold, \n' \
'0 = Comfortable, \n' \
'+1 = Too Hot, +2 = Too Humid'


class ThermalConditionFivePoint(ThermalCondition):
_abbreviation = 'Tcond-5'
_unit_descr = '-2 = Strong/Extreme Cold, -1 = Moderate Cold, \n' \
'0 = No Thermal Stress, \n' \
'+1 = Moderate Heat, +2 = Strong/Extreme Heat'


class ThermalConditionSevenPoint(ThermalCondition):
_abbreviation = 'Tcond-7'
_unit_descr = '-3 = Very Strong/Extreme Cold, ' \
'-2 = Strong Cold, -1 = Moderate Cold, \n' \
'0 = No Thermal Stress, \n' \
'+1 = Moderate Heat, +2 = Strong Heat, '\
'+3 = Very Strong Heat, +4 = Extreme Heat'
'+1 = Moderate Heat, +2 = Strong Heat, ' \
'+3 = Very Strong/Extreme Heat'


class ThermalConditionNinePoint(ThermalCondition):
_abbreviation = 'Tcond-9'
_unit_descr = '-4 = Very Strong/Extreme Cold, ' \
'-3 = Strong Cold, -2 = Moderate Cold, -1 = Slight Cold, \n' \
'0 = No Thermal Stress, \n' \
'+1 = Slight Heat, +2 = Moderate Heat, +3 = Strong Heat, '\
'+4 = Very Strong/Extreme Heat'


class ThermalConditionElevenPoint(ThermalCondition):
_abbreviation = 'Tcond-11'
_unit_descr = '-5 = Extreme Cold, -4 = Very Strong Cold, ' \
'-3 = Strong Cold, -2 = Moderate Cold, -1 = Slight Cold, \n' \
'0 = No Thermal Stress, \n' \
'+1 = Slight Heat, +2 = Moderate Heat, +3 = Strong Heat, ' \
'+4 = Very Strong Heat, +5 = Extreme Heat'


class UTCICategory(ThermalCondition):
_abbreviation = 'UTCIcond'
_unit_descr = '0 = extreme cold stress' \
'1 = very strong cold stress' \
'2 = strong cold stress' \
'3 = moderate cold stress' \
'4 = slight cold stress' \
'5 = no thermal stress' \
'6 = moderate heat stress' \
'7 = strong heat stress' \
'8 = strong heat stress' \
'9 = extreme heat stress'
8 changes: 0 additions & 8 deletions ladybug/psychrometrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@
import math


def saturated_vapor_pressure_torr(db_temp):
"""Saturated Vapor Pressure (Torr) at temperature (C)
Used frequently throughtout PMV comfort functions.
"""
return math.exp(18.6686 - 4030.183 / (db_temp + 235.0))


def saturated_vapor_pressure(t_kelvin):
"""Saturated Vapor Pressure (Pa) at t_kelvin (K).
Expand Down
28 changes: 27 additions & 1 deletion tests/datacollection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from ladybug.dt import DateTime
from ladybug.datatype.generic import GenericType
from ladybug.datatype.temperature import Temperature
from ladybug.datatype.percentage import RelativeHumidity
from ladybug.datatype.percentage import RelativeHumidity, HumidityRatio

from ladybug.epw import EPW
from ladybug.psychrometrics import humid_ratio_from_db_rh

import unittest
import pytest
Expand Down Expand Up @@ -982,6 +985,29 @@ def test_get_aligned_collection_continuous(self):
assert dc3.header.data_type.name == 'Relative Humidity'
assert dc3.header.unit == '%'

def test_compute_function_aligned(self):
"""Test the method for computing funtions with aligned collections."""
epw_file_path = './tests/epw/chicago.epw'
chicago_epw = EPW(epw_file_path)
pressure_at_chicago = 95000
hr_inputs = [chicago_epw.dry_bulb_temperature,
chicago_epw.relative_humidity,
pressure_at_chicago]
humid_ratio = HourlyContinuousCollection.compute_function_aligned(
humid_ratio_from_db_rh, hr_inputs, HumidityRatio(), 'fraction')
assert isinstance(humid_ratio, HourlyContinuousCollection)
assert len(humid_ratio.values) == 8760
for i, val in enumerate(humid_ratio.values):
assert val == humid_ratio_from_db_rh(chicago_epw.dry_bulb_temperature[i],
chicago_epw.relative_humidity[i],
pressure_at_chicago)

hr_inputs = [20, 70, pressure_at_chicago]
humid_ratio = HourlyContinuousCollection.compute_function_aligned(
humid_ratio_from_db_rh, hr_inputs, HumidityRatio(), 'fraction')
assert isinstance(humid_ratio, float)
assert humid_ratio == humid_ratio_from_db_rh(20, 70, pressure_at_chicago)

def test_duplicate(self):
"""Test the duplicate method on the discontinuous collections."""
header = Header(Temperature(), 'C', AnalysisPeriod(end_month=1, end_day=1))
Expand Down
Loading

0 comments on commit ee68bb9

Please sign in to comment.