"""archetypal UmiBase module."""
import itertools
import math
import re
from collections.abc import Hashable, MutableSet
import numpy as np
from validator_collection import validators
from archetypal.utils import lcm
def _resolve_combined_names(predecessors):
"""Creates a unique name from the list of :class:`UmiBase` objects
(predecessors)
Args:
predecessors (MetaData):
"""
# all_names = [obj.Name for obj in predecessors]
class_ = list(set([obj.__class__.__name__ for obj in predecessors]))[0]
return "Combined_%s_%s" % (
class_,
str(hash((pre.Name for pre in predecessors))).strip("-"),
)
def _shorten_name(long_name):
"""Check if name is longer than 300 characters, and return truncated version
Args:
long_name (str): A long name (300 char+) to shorten.
"""
if len(long_name) > 300:
# shorten name if longer than 300 characters (limit set by
# EnergyPlus)
return long_name[:148] + (long_name[148:] and " .. ")
else:
return long_name
[docs]class UmiBase(object):
"""Base class for template objects."""
__slots__ = (
"_id",
"_datasource",
"_predecessors",
"_name",
"_category",
"_comments",
"_allow_duplicates",
"_unit_number",
)
CREATED_OBJECTS = []
_ids = itertools.count(0) # unique id for each class instance
def __init__(
self,
Name,
Category="Uncategorized",
Comments="",
DataSource=None,
allow_duplicates=False,
**kwargs,
):
"""The UmiBase class handles common properties to all Template objects.
Args:
Name (str): Unique, the name of the object.
Category (str): Group objects by assigning the same category
identifier. Thies can be any string.
Comments (str): A comment displayed in the UmiTemplateLibrary.
DataSource (str): A description of the datasource of the object.
This helps identify from which data is the current object
created.
allow_duplicates (bool): If True, this object can be equal to another one
if it has a different name.
**kwargs:
"""
self.Name = Name
self.Category = Category
self.Comments = Comments
self.DataSource = DataSource
self.id = kwargs.get("id", None)
self.allow_duplicates = allow_duplicates
self.unit_number = next(self._ids)
self.predecessors = None
UmiBase.CREATED_OBJECTS.append(self)
@property
def Name(self):
"""Get or set the name of the object."""
return self._name
@Name.setter
def Name(self, value):
self._name = validators.string(value, coerce_value=True)
@property
def id(self):
"""Get or set the id."""
return self._id
@id.setter
def id(self, value):
if value is None:
value = id(self)
self._id = validators.string(value, coerce_value=True)
@property
def DataSource(self):
"""Get or set the datasource of the object."""
return self._datasource
@DataSource.setter
def DataSource(self, value):
self._datasource = validators.string(value, coerce_value=True, allow_empty=True)
@property
def Category(self):
"""Get or set the Category attribute."""
return self._category
@Category.setter
def Category(self, value):
value = validators.string(value, coerce_value=True, allow_empty=True)
if value is None:
value = ""
self._category = value
@property
def Comments(self):
"""Get or set the object comments."""
return self._comments
@Comments.setter
def Comments(self, value):
value = validators.string(value, coerce_value=True, allow_empty=True)
if value is None:
value = ""
self._comments = value
@property
def allow_duplicates(self):
"""Get or set the use of duplicates [bool]."""
return self._allow_duplicates
@allow_duplicates.setter
def allow_duplicates(self, value):
assert isinstance(value, bool), value
self._allow_duplicates = value
@property
def unit_number(self):
return self._unit_number
@unit_number.setter
def unit_number(self, value):
self._unit_number = validators.integer(value)
@property
def predecessors(self):
"""Get or set the predecessors of self.
Of which objects is self made of. If from nothing else then self,
return self.
"""
if self._predecessors is None:
self._predecessors = MetaData([self])
return self._predecessors
@predecessors.setter
def predecessors(self, value):
self._predecessors = value
[docs] def duplicate(self):
"""Get copy of self."""
return self.__copy__()
def _get_predecessors_meta(self, other):
"""get predecessor objects to self and other
Args:
other (UmiBase): The other object.
"""
predecessors = self.predecessors + other.predecessors
meta = self.combine_meta(predecessors)
return meta
def combine_meta(self, predecessors):
return {
"Name": _resolve_combined_names(predecessors),
"Comments": (
"Object composed of a combination of these "
"objects:\n{}".format(
"\n- ".join(set(obj.Name for obj in predecessors))
)
),
"Category": ", ".join(
set(
itertools.chain(*[obj.Category.split(", ") for obj in predecessors])
)
),
"DataSource": ", ".join(
set(
itertools.chain(
*[
obj.DataSource.split(", ")
for obj in predecessors
if obj.DataSource is not None
]
)
)
),
}
def combine(self, other, allow_duplicates=False):
pass
[docs] def rename(self, name):
"""renames self as well as the cached object
Args:
name (str): the name.
"""
self.Name = name
[docs] def to_dict(self):
"""Return UmiBase dictionary representation."""
return {"$id": "{}".format(self.id), "Name": "{}".format(self.Name)}
@classmethod
def get_classref(cls, ref):
return next(
iter(
[value for value in UmiBase.CREATED_OBJECTS if value.id == ref["$ref"]]
),
None,
)
def get_ref(self, ref):
pass
def __hash__(self):
"""Return the hash value of self."""
return hash((self.__class__.mro()[0].__name__, self.Name))
def __repr__(self):
"""Return a representation of self."""
return ":".join([str(self.id), str(self.Name)])
def __str__(self):
"""string representation of the object as id:Name"""
return self.__repr__()
def __iter__(self):
"""Iterate over attributes. Yields tuple of (keys, value)."""
for attr, value in self.mapping().items():
yield attr, value
def __copy__(self):
"""Create a copy of self."""
return self.__class__(**self.mapping(validate=False))
[docs] def to_ref(self):
"""Return a ref pointer to self."""
return {"$ref": str(self.id)}
[docs] def float_mean(self, other, attr, weights=None):
"""Calculates the average attribute value of two floats. Can provide
weights.
Args:
other (UmiBase): The other UmiBase object to calculate average value
with.
attr (str): The attribute of the UmiBase object.
weights (iterable, optional): Weights of [self, other] to calculate
weighted average.
"""
if getattr(self, attr) is None:
return getattr(other, attr)
if getattr(other, attr) is None:
return getattr(self, attr)
# If weights is a list of zeros
if not np.array(weights).any():
weights = [1, 1]
if not isinstance(getattr(self, attr), list) and not isinstance(
getattr(other, attr), list
):
if math.isnan(getattr(self, attr)):
return getattr(other, attr)
elif math.isnan(getattr(other, attr)):
return getattr(self, attr)
elif math.isnan(getattr(self, attr)) and math.isnan(getattr(other, attr)):
raise ValueError("Both values for self and other are Not A Number.")
else:
return float(
np.average(
[getattr(self, attr), getattr(other, attr)], weights=weights
)
)
elif getattr(self, attr) is None and getattr(other, attr) is None:
return None
else:
# handle arrays by finding the least common multiple of the two arrays and
# tiling to the full length; then, apply average
self_attr_ = np.array(getattr(self, attr))
other_attr_ = np.array(getattr(other, attr))
l_ = lcm(len(self_attr_), len(other_attr_))
self_attr_ = np.tile(self_attr_, int(l_ / len(self_attr_)))
other_attr_ = np.tile(other_attr_, int(l_ / len(other_attr_)))
return np.average([self_attr_, other_attr_], weights=weights, axis=0)
def _str_mean(self, other, attr, append=False):
"""Returns the combined string attributes
Args:
other (UmiBase): The other UmiBase object to calculate combined
string.
attr (str): The attribute of the UmiBase object.
append (bool): Whether or not the attributes should be combined
together. If False, the attribute of self will is used (other is
ignored).
"""
if self is None:
return other
if other is None:
return self
# if self has info, but other is none, use self
if getattr(self, attr) is not None and getattr(other, attr) is None:
return getattr(self, attr)
# if self is none, but other is not none, use other
elif getattr(self, attr) is None and getattr(other, attr) is not None:
return getattr(other, attr)
# if both are not note, impose self
elif getattr(self, attr) and getattr(other, attr):
if append:
return getattr(self, attr) + getattr(other, attr)
else:
return getattr(self, attr)
# if both are None, return None
else:
return None
def __iadd__(self, other):
"""Overload += to implement self.extend.
Args:
other:
"""
return UmiBase.extend(self, other, allow_duplicates=True)
[docs] def extend(self, other, allow_duplicates):
"""Append other to self. Modify and return self.
Args:
other (UmiBase):
Returns:
UmiBase: self
"""
if self is None:
return other
if other is None:
return self
self.CREATED_OBJECTS.remove(self)
id = self.id
new_obj = self.combine(other, allow_duplicates=allow_duplicates)
new_obj.id = id
for key in self.mapping(validate=False):
setattr(self, key, getattr(new_obj, key))
return self
[docs] def validate(self):
"""Validate UmiObjects and fills in missing values."""
return self
[docs] def mapping(self, validate=True):
"""Get a dict based on the object properties, useful for dict repr.
Args:
validate (bool): If True, try to validate object before returning the
mapping.
"""
if validate:
self.validate()
return dict(
id=self.id,
Name=self.Name,
Category=self.Category,
Comments=self.Comments,
DataSource=self.DataSource,
)
[docs] def get_unique(self):
"""Return first object matching equality in the list of instantiated objects."""
if self.allow_duplicates:
# We want to return the first similar object (equality) that has this name.
obj = next(
iter(
sorted(
(
x
for x in UmiBase.CREATED_OBJECTS
if x == self
and x.Name == self.Name
and type(x) == type(self)
),
key=lambda x: x.unit_number,
)
),
self,
)
else:
# We want to return the first similar object (equality) regardless of the
# name.
obj = next(
iter(
sorted(
(
x
for x in UmiBase.CREATED_OBJECTS
if x == self and type(x) == type(self)
),
key=lambda x: x.unit_number,
)
),
self,
)
return obj
class UserSet(Hashable, MutableSet):
"""UserSet class."""
__hash__ = MutableSet._hash
def __init__(self, iterable=()):
"""Initialize object."""
self.data = set(iterable)
def __contains__(self, value):
"""Assert value is in self.data."""
return value in self.data
def __iter__(self):
"""Iterate over self.data."""
return iter(self.data)
def __len__(self):
"""return len of self."""
return len(self.data)
def __repr__(self):
"""Return a representation of self."""
return repr(self.data)
def __add__(self, other):
"""Add other to self."""
self.data.update(other.data)
return self
def update(self, other):
"""Update self with other."""
self.data.update(other.data)
return self
def add(self, item):
"""Add an item."""
self.data.add(item)
def discard(self, item):
"""Remove a class if it is currently present."""
self.data.discard(item)
class MetaData(UserSet):
"""Handles data of combined objects such as Name, Comments and other."""
@property
def Name(self):
"""Get object name."""
return "+".join([obj.Name for obj in self])
@property
def comments(self):
"""Get object comments."""
return "Object composed of a combination of these objects:\n{}".format(
set(obj.Name for obj in self)
)
class UniqueName(str):
"""Attribute unique user-defined names for :class:`UmiBase`."""
existing = set()
def __new__(cls, content):
"""Pick a name. Will increment the name if already used."""
return str.__new__(cls, cls.create_unique(content))
@classmethod
def create_unique(cls, name):
"""Check if name has already been used.
If so, try to increment until not used.
Args:
name:
"""
if not name:
return None
if name not in cls.existing:
cls.existing.add(name)
return name
else:
match = re.match(r"^(.*?)(\D*)(\d+)$", name)
if match:
groups = list(match.groups())
pad = len(groups[-1])
groups[-1] = int(groups[-1])
groups[-1] += 1
groups[-1] = str(groups[-1]).zfill(pad)
name = "".join(map(str, groups))
return cls.create_unique(name)
else:
return cls.create_unique(name + "_1")