Source code for ladybug.datatype.base

# 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)])