# coding=utf-8
"""Base data type."""
from __future__ import division
import os
import importlib
import re
[docs]
class DataTypeBase(object):
"""Base class for data types.
Args:
name: Optional name for the type. Default is derived from the class name.
Properties:
* name
* units
* si_units
* ip_units
* min
* max
* abbreviation
* unit_descr
* point_in_time
* cumulative
* normalized_type
* time_aggregated_type
* time_aggregated_factor
"""
_units = [None]
_si_units = [None]
_ip_units = [None]
_min = float('-inf')
_max = float('+inf')
_abbreviation = ''
_unit_descr = None
_point_in_time = True
_cumulative = False
_normalized_type = None
_time_aggregated_type = None
_time_aggregated_factor = None
_type_enumeration = None
def __init__(self, name=None):
"""Initialize DataType.
"""
self._name = name
[docs]
@classmethod
def from_dict(cls, data):
"""Create a data type from a dictionary.
Args:
data: A python dictionary in the following format
.. code-block:: python
{
"name": "" # data type name of the data type as a string
"data_type": "" # the class name of the data type as a string
}
"""
# load up all of the types if they aren't already loaded
assert 'name' in data, 'Required keyword "name" is missing!'
assert 'data_type' in data, 'Required keyword "data_type" is missing!'
if cls._type_enumeration is None:
cls._type_enumeration = _DataTypeEnumeration(import_modules=False)
# load the data type from the dictionary
if data['data_type'] == 'GenericType':
assert 'base_unit' in data, \
'Keyword "base_unit" is missing and is required for GenericType.'
d_min = data['min'] if 'min' in data else float('-inf')
d_max = data['max'] if 'max' in data else float('+inf')
abbr = data['abbreviation'] if 'abbreviation' in data else None
unit_descr = data['unit_descr'] if 'unit_descr' in data else None
point_in_time = data['point_in_time'] if 'point_in_time' in data else True
cumulative = data['cumulative'] if 'cumulative' in data else False
return cls._type_enumeration._GENERICTYPE(
data['name'], data['base_unit'], d_min, d_max, abbr,
unit_descr, point_in_time, cumulative)
elif data['data_type'] in cls._type_enumeration._TYPES:
clss = cls._type_enumeration._TYPES[data['data_type']]
if data['data_type'] == data['name'].title().replace(' ', ''):
return clss()
else:
instance = clss()
instance._name = data['name']
return instance
else:
raise ValueError(
'Data Type {} could not be recognized'.format(data['data_type']))
[docs]
@classmethod
def from_string(cls, data_type_string):
"""Create a data type from a string.
Args:
data: A data type string.
"""
# load up all of the types if they aren't already loaded
if cls._type_enumeration is None:
cls._type_enumeration = _DataTypeEnumeration(import_modules=False)
# first, see if it is a standard data type
d_type_class = data_type_string.title().replace(' ', '')
if d_type_class in cls._type_enumeration._TYPES:
clss = cls._type_enumeration._TYPES[d_type_class]
return clss()
# assume that it's a Generic data type
return cls._type_enumeration._GENERICTYPE.from_string(data_type_string)
[docs]
def is_unit_acceptable(self, unit, raise_exception=True):
"""Check if a certain unit is acceptable for the data type.
Args:
unit: A text string representing the abbreviated unit.
raise_exception: Set to True to raise an exception if not acceptable.
"""
_is_acceptable = unit in self.units
if _is_acceptable or not raise_exception:
return _is_acceptable
else:
raise ValueError(
'{0} is not an acceptable unit type for {1}. '
'Choose from the following: {2}'.format(
unit, self.__class__.__name__, self.units
)
)
[docs]
def to_unit(self, values, unit, from_unit=None):
"""Return values converted to the unit given the input from_unit."""
raise NotImplementedError(
'to_unit is not implemented on %s' % self.__class__.__name__
)
[docs]
def to_ip(self, values, from_unit=None):
"""Return values in IP and the units to which the values have been converted."""
raise NotImplementedError(
'to_ip is not implemented on %s' % self.__class__.__name__
)
[docs]
def to_si(self, values, from_unit=None):
"""Return values in SI and the units to which the values have been converted."""
raise NotImplementedError(
'to_si is not implemented on %s' % self.__class__.__name__
)
[docs]
def is_in_range(self, values, unit=None, raise_exception=True):
"""Check if a list of values is within physically/mathematically possible range.
Args:
values: A list of values.
unit: The unit of the values. If not specified, the default metric
unit will be assumed.
raise_exception: Set to True to raise an exception if not in range.
"""
self._is_numeric(values)
if unit is None or unit == self.units[0]:
minimum = self.min
maximum = self.max
else:
namespace = {'self': self}
self.is_unit_acceptable(unit, True)
min_statement = "self._{}_to_{}(self.min)".format(
self._clean(self.units[0]), self._clean(unit))
max_statement = "self._{}_to_{}(self.max)".format(
self._clean(self.units[0]), self._clean(unit))
minimum = eval(min_statement, namespace)
maximum = eval(max_statement, namespace)
for value in values:
if value < minimum or value > maximum:
if not raise_exception:
return False
else:
raise ValueError(
'{0} should be between {1} and {2}. Got {3}'.format(
self.__class__.__name__, self.min, self.max, value
)
)
return True
[docs]
def duplicate(self):
"""Return a copy of the data type."""
return self.__class__(self.name)
[docs]
def to_dict(self):
"""Get data type as a dictionary."""
return {
'name': self.name,
'data_type': self.__class__.__name__,
'type': 'DataType'
}
[docs]
def to_string(self):
"""Get data type as a string."""
return self.name
def _is_numeric(self, values):
"""Check to be sure values are numbers before doing numerical operations."""
if len(values) > 0:
assert isinstance(values[0], (float, int)), \
"values must be numbers to perform math operations. Got {}".format(
type(values[0]))
return True
def _to_unit_base(self, base_unit, values, unit, from_unit):
"""Return values in a given unit given the input from_unit."""
self._is_numeric(values)
namespace = {'self': self, 'values': values}
if not from_unit == base_unit:
self.is_unit_acceptable(from_unit, True)
statement = '[self._{}_to_{}(val) for val in values]'.format(
self._clean(from_unit), self._clean(base_unit))
values = eval(statement, namespace)
namespace['values'] = values
if not unit == base_unit:
self.is_unit_acceptable(unit, True)
statement = '[self._{}_to_{}(val) for val in values]'.format(
self._clean(base_unit), self._clean(unit))
values = eval(statement, namespace)
return values
def _clean(self, unit):
"""Clean out special characters from unit abbreviations."""
return unit.replace(
'/', '_').replace(
'-', '').replace(
' ', '').replace(
'%', 'pct')
@property
def name(self):
"""The full name of the data type as a string."""
if self._name is None:
return re.sub(r"(?<=\w)([A-Z])", r" \1", self.__class__.__name__)
else:
return self._name
@property
def units(self):
"""A tuple of all acceptable units of the data type as abbreviated text.
The first item of the list should be the standard SI unit.
The second item of the list should be the standard IP unit (if it exists).
The rest of the list can be any other acceptable units.
(eg. [C, F, K])
"""
return self._units
@property
def si_units(self):
"""A tuple of acceptable si_units for the data type."""
return self._si_units
@property
def ip_units(self):
"""A tuple of acceptable ip_units for the data type."""
return self._ip_units
@property
def min(self):
"""Number for the lower limit for the data type.
Values below this limit should be physically or mathematically impossible.
"""
return self._min
@property
def max(self):
"""Number for the upper limit for the data type.
Values above this limit should be physically or mathematically impossible.
"""
return self._max
@property
def abbreviation(self):
"""An optional abbreviation for the data type as text.
(eg. 'UTCI' for Universal Thermal Climate Index).
This can also be a letter that represents the data type in a formula.
(eg. 'A' for Area; 'P' for Pressure)
"""
return self._abbreviation
@property
def unit_descr(self):
"""A dictionary that matches numerical values to text categories.
This will be None if there are no text categories that the data type can be
mapped to. (eg. -1 = Cold, 0 = Neutral, +1 = Hot) (eg. 0 = False, 1 = True).
"""
return self._unit_descr
@property
def point_in_time(self):
"""Boolean to note whether data type is for a single instant in time.
If False, the data type is meant to represent an average or accumulation
over time whenever found in an array of time series data.
(True Examples: Temperature, WindSpeed)
(False Examples: Energy, Radiation, Illuminance)
"""
return self._point_in_time
@property
def cumulative(self):
"""Boolean to note if data type can be summed over time to yield meaningful data.
If False, this data type can only be averaged over time to be meaningful.
Note that cumulative cannot be True when point_in_time is also True.
(False Examples: Temperature, Irradiance, Illuminance)
(True Examples: Energy, Radiation)
"""
return self._cumulative
@property
def normalized_type(self):
"""A data type object representing the area-normalized version of this data type.
This will be None if the data type cannot be normalized per unit area to
yield a meaningful data type.
"""
return self._normalized_type
@property
def time_aggregated_type(self):
"""A data type object representing the time-aggregated version of this data type.
This will be None if the data type cannot be aggregated per unit time to
yield a meaningful data type.
"""
return self._time_aggregated_type
@property
def time_aggregated_factor(self):
"""A number to convert to the base unit of the type to the time aggregated unit.
The factor assumes that the data is aggregated over one hour. This will be
None if the data type cannot be aggregated per unit time to yield a
meaningful data type.
"""
return self._time_aggregated_factor
[docs]
def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __key(self):
return (
self._name, self._units, self._si_units, self._ip_units, self._min,
self._max, self._abbreviation, self._unit_descr, self._point_in_time,
self._cumulative, self._normalized_type, self._time_aggregated_type
)
def __eq__(self, other):
return isinstance(other, DataTypeBase) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
"""Return Ladybug data type as a string."""
return self.name
class _DataTypeEnumeration(object):
"""Enumerates all data types, base types, and units."""
_TYPES = {}
_BASETYPES = {}
_UNITS = {}
_GENERICTYPE = None
def __init__(self, import_modules=True):
if import_modules:
self._import_modules()
for clss in DataTypeBase.__subclasses__():
if clss.__name__ != 'GenericType':
self._TYPES[clss.__name__] = clss
self._BASETYPES[clss.__name__] = clss
self._UNITS[clss.__name__] = clss._units
for subclss in self._all_subclasses(clss):
self._TYPES[subclss.__name__] = subclss
else:
self._GENERICTYPE = clss
@property
def types(self):
"""A tuple indicating all currently supported data types."""
return tuple(sorted(self._TYPES.keys()))
@property
def base_types(self):
"""A tuple indicating all base types.
Base types are the data types on which unit systems are defined.
"""
return tuple(sorted(self._BASETYPES.keys()))
@property
def units(self):
"""A dictionary containing all currently supported units.
The keys of this dictionary are the base types (eg. 'Temperature').
"""
return self._UNITS
@property
def types_dict(self):
"""A dictionary containing pointers to the classes of each data type.
The keys of this dictionary are the data types.
"""
return self._TYPES
def _import_modules(self):
root_dir = os.path.dirname(__file__)
modules = os.listdir(os.path.dirname(__file__))
modules = [os.path.join(root_dir, mod) for mod in modules]
importable = ['.{}'.format(os.path.basename(f)[:-3]) for f in modules
if os.path.isfile(f) and f.endswith('.py')
and not f.endswith('__init__.py')
and not f.endswith('base.py')]
for mod in importable:
importlib.import_module(mod, 'ladybug.datatype')
def _all_subclasses(self, clss):
return set(clss.__subclasses__()).union(
[s for c in clss.__subclasses__() for s in self._all_subclasses(c)])