import warnings
from typing import List, Dict, Tuple
from abc import ABC, abstractmethod
import platypus
import eppySupport
from IO_Objects import Descriptor, IOBase, Selector, DummySelector, AnyValue
from config import range_parameter as conf
import eppy_funcs as ef # incorrectly detected as an unused import by pycharm
from errors import ModeError
# Descriptors
[docs]class RangeParameter(Descriptor):
"""RangeParameter(min_val, max_val)
Represents an aspect of a building that can take on value from an interval.
:param float min_val: Minimum value for the range
:param float max_val: Maximum value for the range
"""
pandas_type = float
rbf_type = 'R'
def __init__(self, min_val=conf.get('min'),
max_val=conf.get('max')):
super().__init__()
if min_val > max_val:
raise ValueError('minimum is larger than maximum')
self.min = min_val
self.max = max_val
self._add_reprs(['min', 'max'])
self.platypus_type = platypus.Real(self.min, self.max)
[docs] def validate(self, val):
min_ = float('-inf') if self.min is None else self.min
max_ = float('inf') if self.max is None else self.max
return min_ <= val <= max_
[docs] def sample(self, value):
"""Transforms a value in [0, 1] into a value in [min, max].
This transformation is uniform.
:param value: a value in the interval [0, 1]
:return: a value from the interval [min, max]
"""
return (self.max - self.min) * value + self.min
def __str__(self):
return f'{self.__class__.__name__} [{self.min}, {self.max}]'
[docs]class Category(platypus.Subset):
"""Category(elements)
:param elements:
"""
def __init__(self, elements):
super().__init__(elements=elements, size=1)
[docs] def encode(self, value):
return [value]
[docs] def decode(self, value):
return value[0]
[docs]class CategoryParameter(Descriptor):
"""CategoryParameter(options, **kwargs)
:param list options: a list of possible value this parameter can be set to
"""
pandas_type = 'category'
def __init__(self, options: list, **kwargs):
super().__init__(**kwargs)
self.options: list = options
self._to_repr['options'] = 'options'
self.platypus_type = Category(self.options)
[docs] def validate(self, value):
return value in self.options
[docs] def sample(self, value):
return self.options[int(len(self.options) * value)]
[docs]class AbstractFieldSelector(Selector, ABC):
"""AbstractFieldSelector(field_name)
Base class for selectors that modify one field in one or more objects in an EnergyPlus building
:param field_name: Name of the field to select
"""
def __init__(self, field_name):
super().__init__()
self.field_name = field_name
self._add_repr('field_name')
[docs] def get(self, building) -> List:
"""Gets the current values of this field from a building
:param building: the building to retrieve values from
:return: a list containing the current values of this selector's fields
"""
mode = ef.get_mode(building)
objects = self.get_objects(building)
if mode == 'idf':
return [getattr(o, ef.convert_format(self.field_name, 'field', mode)) for o in objects]
if mode == 'json':
return [o[self.field_name] for o in objects]
[docs] def set(self, building, value) -> None:
"""Sets this field in the building to the provided value
:param building: the building to modify
:param value: the value to set this field to
:return:
"""
mode = ef.get_mode(building)
objects = self.get_objects(building)
field_name = ef.convert_format(self.field_name, 'field', mode)
if mode == 'idf':
for o in objects:
setattr(o, field_name, value)
if mode == 'json':
for o in objects:
assert field_name in o, f'{field_name} not in {repr(o)}'
o[field_name] = value
[docs] @abstractmethod
def get_objects(self, building) -> List:
"""Returns a list of the object this selector applies to
:param building: the building to search for objects
:return: a list of the objects this selector applies to
"""
pass
[docs]class FilterSelector(AbstractFieldSelector):
"""FilterSelector(get_objects, field_name)
A selector that uses a custom function to find which objects it should modify
:param get_objects: a function that takes a building and returns the objects this selector should modify
:param field_name: the field to modify
"""
def __init__(self, get_objects, field_name):
super().__init__(field_name)
self._get_objects = get_objects
self._add_repr('get_objects', '_get_objects')
[docs] def get_objects(self, building):
return self._get_objects(building)
[docs]class FieldSelector(AbstractFieldSelector):
"""FieldSelector(class_name = None, object_name = None, field_name = None)
A selector that modifies one or more fields in an EnergyPlus building,
based on the class, object and field names
:param class_name: class of the object to modify
ex: 'Material'
:param object_name: name of the object to modify
ex: 'Mass NonRes Wall Insulation'
:param field_name: name of the field to modify
ex: Thickness
"""
def __init__(self, class_name=None, object_name=None, field_name=None):
super().__init__(field_name=field_name)
self.class_name = class_name
self.object_name = object_name
self._add_reprs(['class_name', 'object_name'], check=True)
[docs] def get_objects(self, building) -> List:
"""Retrieves the objects that this selector will affect from the building.
:param building: the building to search
:return: a list of the objects found
"""
mode = ef.get_mode(building)
if mode == 'idf':
if self.class_name is not None:
class_name = ef.convert_format(self.class_name, 'class', 'idf')
else:
class_name = None
if self.object_name == '*':
if class_name is None:
raise TypeError("When object_name='*', class_name must be specified.")
return building.idfobjects[class_name]
if self.object_name and class_name:
# this is probably the most reliable way to select an idfObject.
return [building.getobject(key=class_name, name=self.object_name)]
if self.object_name:
# There should only one object matching the name, assuming the idf is valid
return [eppySupport.get_idfobject_from_name(building, self.object_name)]
if class_name is not None:
# assume that we want the first object matching the key
# TODO: is this specific enough, or should we remove it? our JSON code does not support this
return [building.idfobjects[class_name][0]]
else: # we have neither object_name nor class_name
raise TypeError('Either class_name or object_name must be specified.')
elif mode == 'json':
if self.object_name == '*':
if not self.class_name:
raise TypeError("When object_name='*', class_name must be specified.")
return list(building[self.class_name].values())
if self.object_name and self.class_name:
# this is probably the most reliable way to select an idfObject.
return [building[self.class_name][self.object_name]]
if self.object_name:
# There should only one object matching the name, assuming the building is valid
result = [obj for objs in building.values()
for name, obj in objs.items()
if name == self.object_name]
if len(result) != 1:
warnings.warn(f'found {len(result)} objects with object_name: {self.object_name}, expected 1')
return result
if self.class_name:
result = list(building[self.class_name].items())
if len(result) == 1:
return result
raise ValueError(f'multiple objects with class_name {self.class_name}.'
f'Cannot guarantee a reliable ordering')
else: # we have neither object_name nor class_name
raise TypeError('Either class_name or object_name must be specified.')
raise ModeError(mode)
[docs]class GenericSelector(Selector):
"""GenericSelector(set = None, get = None, setup = None)
A selector that supports custom get/set functions
:param set:
:param get:
:param setup:
"""
def __init__(self, set=None, get=None, setup=None):
super().__init__()
self._set = set
self._get = get
self._setup = setup
for attr in ('set', 'get', 'setup'):
self._add_repr(attr, f'_{attr}', True)
def set(self, building, value):
if self._set is not None:
return self._set(building, value)
raise NotImplementedError
def get(self, building):
if self._get is not None:
return self._get(building)
raise NotImplementedError
[docs] def setup(self, building):
if self._setup is None:
return
self._setup(building)
# Parameters
[docs]class Parameter(IOBase):
"""Parameter(selector = None, value_descriptor = None, **kwargs)
:param selector: a Selector describing how to modify the building
:param value_descriptor: a Descriptor specifying which values to use
:param setup: a setup function to run on the building
"""
def __init__(self, selector: Selector = None, value_descriptor: Descriptor = None, **kwargs):
super().__init__(**kwargs)
self.selector = selector or DummySelector()
self.value_descriptor = value_descriptor or AnyValue()
self._add_reprs(['selector', 'value_descriptor'], True)
[docs] def sample(self, value: float):
"""sample(value)
Takes a value in the range 0-1 and returns a valid value for this parameter
:param float value:
"""
return self.value_descriptor.sample(value)
[docs] def validate(self, value):
"""Checks if value is a valid value for this parameter.
:param float value:
:return: True if the value is valid False otherwise
"""
return self.value_descriptor.validate(value)
def setup(self, building) -> None:
self.selector.setup(building)
@property
def _default_name(self):
"""The name to use for this Parameter if no name was provided"""
if hasattr(self.selector, 'field_name'):
return self.selector.field_name
@property
def platypus_type(self):
"""The platypus equivalent of this parameter"""
return self.value_descriptor.platypus_type
[docs]def wwr(value_descriptor=RangeParameter(0.01, 0.99), **kwargs) -> Parameter:
"""wwr(value_descriptor = RangeParameter(0.01, 0.99), **kwargs) -> Parameter
Makes a window-to-wall-ratio parameter.
:param value_descriptor: a parameter describing the valid window-wall-ratios
:return: Parameter
"""
if isinstance(value_descriptor, RangeParameter):
min_val, max_val = value_descriptor.min, value_descriptor.max
if not (0 < min_val < max_val < 1):
if 0 == min_val:
raise ValueError('min must be strictly greater than 0')
if 1 == max_val:
raise ValueError('max must be strictly less than 1')
raise ValueError('Invalid min and max values. 0 < min < max < 1 must be satisfied.')
else:
warnings.warn(f'wwr is intended to be used with RangeParameter. Your value_descriptor is {value_descriptor}')
def set(building, value):
ef.wwr_all(building, value)
def get():
# This feature has not yet been requested
raise NotImplementedError('Calculation of window to wall ratio is not supported.')
def setup(building):
ef.one_window(building)
selector = GenericSelector(set=set, get=get, setup=setup)
name = kwargs.pop('name', 'Window to Wall Ratio')
return Parameter(selector=selector, value_descriptor=value_descriptor, name=name, **kwargs)
# Simpler ways to get specific transformers
keyFormat = Dict[str, Dict[str, Tuple[float, float]]]
# TODO: Formalize this syntax, maybe add a guaranteed order
[docs]def expand_plist(pList: keyFormat) -> List:
"""expand_plist(pList) -> List
This function expands a nested dictionary of the correct format into a list of inputs.
The dictionary should have the format:
{'idf object name': {'idf object1 property name': (min_value, max_value)}
Both layers of the dictionaries can have as many names as desired
:param keyFormat pList: Nested dictionary
:return: A list of proper inputs
"""
return [Parameter(FieldSelector(object_name=name, field_name=prop),
RangeParameter(min_val=min_, max_val=max_), name=prop)
for name, subProps in pList.items()
for prop, (min_, max_) in subProps.items()]
if __name__ == '__main__':
pass
# a few simple tests of the inputs module
#edit: moved tests to test_parameter.py
#import eppy_funcs as ef
# transformation functions should modify the idf
# TODO: Test transformation functions
#print('tests passed')