"""Module for coloring OpenDSS geometry with attributes."""
from ladybug_geometry.geometry2d import Point2D
from ladybug_geometry.geometry3d import Point3D
from ladybug.datacollection import HourlyContinuousCollection, \
HourlyDiscontinuousCollection
from ladybug.graphic import GraphicContainer
from ladybug.legend import LegendParameters
from honeybee.typing import int_in_range
from honeybee.colorobj import _ColorObject
from .network import ElectricalNetwork
[docs]
class ColorNetwork(_ColorObject):
"""Object for visualizing ElectricalNetwork attributes.
Args:
network: An ElectricalNetwork object, which will be colored with the attribute.
attr_name: A text string of an attribute that the input network equipment has.
This can have '.' that separate the nested attributes from one another.
For example, 'properties.kva' for the kva rating of transformers.
legend_parameters: An optional LegendParameter object to change the display
of the results (Default: None).
Properties:
* network
* attr_name
* legend_parameters
* attr_name_end
* attributes
* attributes_unique
* attributes_original
* geometries
* graphic_container
* min_point
* max_point
"""
__slots__ = ('_network', '_geometries')
def __init__(self, network, attr_name, legend_parameters=None):
"""Initialize ColorNetwork."""
assert isinstance(network, ElectricalNetwork), 'Expected ElectricalNetwork for' \
' ColorNetwork. Got {}.'.format(type(network))
self._network = network
all_obj = (network.substation,) + network.transformers + network.connectors
self._geometries = tuple(obj.geometry for obj in all_obj)
self._min_point, self._max_point = _calculate_min_max(all_obj)
# assign the legend parameters of this object
self.legend_parameters = legend_parameters
# get the attributes of the input equipment
self._process_attribute_name(attr_name)
self._process_attributes(all_obj)
@property
def network(self):
"""Get the ElectricalNetwork associated with this object."""
return self._network
@property
def geometries(self):
"""A tuple of Polygon2D, Polyline2D and LineSegment2D aligned with attributes.
"""
return self._geometries
@property
def min_point(self):
"""Get a Point3D for the minimum of the box around the objects."""
return Point3D(self._min_point.x, self._min_point.y, 0)
@property
def max_point(self):
"""Get a Point3D for the maximum of the box around the objects."""
return Point3D(self._max_point.x, self._max_point.y, 0)
def __repr__(self):
"""Color ElectricalNetwork representation."""
return 'Color Network: {} [{}]'.format(
self.network.display_name, self.attr_name_end)
[docs]
class ColorNetworkResults(object):
"""Class for coloring ElectricalNetwork geometry with simulation results.
Args:
data_collections: An array of data collections of the same data type,
which will be used to color the network with simulation results. Data
collections should all have headers with metadata dictionaries with 'type'
and 'name' keys. These keys will be used to match the data in the collections
to the input electrical network.
network: An ElectricalNetwork object, which will be colored with the attribute.
legend_parameters: An optional LegendParameter object to change the display
of the ColorNetworkResults (Default: None).
attribute: Text to note the attribute of the data collections with which the
network geometry should be colored. Typical values are max, min, average,
median, or total. This input can also be an integer (greater than or equal
to 0) to select a specific step of the data collections for which result
values will be generated. (Default: "Max" to color with the peak value).
Properties:
* data_collections
* network
* legend_parameters
* attribute
* matched_geometries
* matched_data
* matched_values
* graphic_container
* title_text
* data_type_text
* data_type
* unit
* analysis_period
* min_point
* max_point
"""
__slots__ = (
'_data_collections', '_network', '_legend_parameters', '_attribute',
'_matched_geometries', '_matched_data', '_matched_values',
'_base_collection', '_base_type', '_base_unit', '_min_point', '_max_point')
def __init__(self, data_collections, network,
legend_parameters=None, attribute='max'):
"""Initialize ColorNetworkResults."""
# check the input collections
accept_cols = (HourlyContinuousCollection, HourlyDiscontinuousCollection)
try:
data_collections = list(data_collections)
except TypeError:
raise TypeError('Input data_collections must be an array. Got {}.'.format(
type(data_collections)))
assert len(data_collections) > 0, \
'ColorNetworkResults must have at least one data_collection.'
for i, coll in enumerate(data_collections):
assert isinstance(coll, accept_cols), 'Expected hourly ' \
'data collection for ColorNetworkResults. Got {}.'.format(type(coll))
self._base_collection = data_collections[0]
self._base_type = self._base_collection.header.data_type
self._base_unit = self._base_collection.header.unit
for coll in data_collections[1:]:
assert coll.header.unit == self._base_unit, \
'ColorNetworkResults data_collections must all have matching units. ' \
'{} != {}.'.format(coll.header.unit, self._base_unit)
assert len(coll.values) == len(self._base_collection.values), \
'ColorNetworkResults data_collections must be aligned with one another' \
'.{} != {}'.format(len(coll.values), len(self._base_collection.values))
self._data_collections = data_collections
# process the input electrical network
assert isinstance(network, ElectricalNetwork), 'Expected ElectricalNetwork for' \
' ColorNetworkResults. Got {}.'.format(type(network))
self._network = network
all_obj = (network.substation,) + network.transformers + network.connectors
self._min_point, self._max_point = _calculate_min_max(all_obj)
geo_dict = {obj.identifier.lower().replace(':', ''): obj.geometry
for obj in all_obj}
self._matched_geometries, self._matched_data = [], []
for dat in self._data_collections:
try:
self._matched_geometries.append(
geo_dict[dat.header.metadata['name'].replace(':', '')])
self._matched_data.append(dat)
except KeyError: # data could not be matched
pass
# assign the other properties of this object
self.legend_parameters = legend_parameters
self.attribute = attribute
@property
def data_collections(self):
"""Get a tuple of data collections assigned to this object."""
return tuple(self._data_collections)
@property
def network(self):
"""Get the ElectricalNetwork associated with this object."""
return self._network
@property
def geometries(self):
"""A tuple of Polygon2D, Polyline2D and LineSegment2D aligned with attributes.
"""
return self._geometries
@property
def legend_parameters(self):
"""Get or set the legend parameters."""
return self._legend_parameters
@legend_parameters.setter
def legend_parameters(self, value):
if value is not None:
assert isinstance(value, LegendParameters), \
'Expected LegendParameters. Got {}.'.format(type(value))
self._legend_parameters = value.duplicate()
else:
self._legend_parameters = LegendParameters()
@property
def attribute(self):
"""Get or set text for the data attribute or an integer a specific data step."""
return self._attribute
@attribute.setter
def attribute(self, value):
if not hasattr(self._base_collection, value):
value = int_in_range(
value, 0, len(self._base_collection) - 1, 'simulation step')
self._attribute = value
@property
def matched_geometries(self):
"""Get a tuple of geometries that were matched with the data collections."""
return tuple(self._matched_geometries)
@property
def matched_data(self):
"""Get a tuple of data collections aligned with the matched_geometries."""
return tuple(self._matched_data)
@property
def matched_values(self):
"""Get an array of numbers that correspond to the matched_geometries.
These values are derived from the data_collections but they will be
averaged/totaled or for a specific time step depending on the
other inputs to this object.
"""
if isinstance(self.attribute, int): # specific index from all collections
return tuple(data[self._attribute] for data in self._matched_data)
else: # data collection property
return tuple(getattr(data, self._attribute)
for data in self._matched_data)
@property
def graphic_container(self):
"""Get a ladybug GraphicContainer that relates to this object.
The GraphicContainer possesses almost all things needed to visualize the
object including the legend, value_colors, lower_title_location,
upper_title_location, etc.
"""
return GraphicContainer(
self.matched_values, self.min_point, self.max_point,
self.legend_parameters, self.data_type, str(self.unit))
@property
def title_text(self):
"""Text string for the title of the object."""
d_type_text = self.data_type
if isinstance(self.attribute, int): # specific index from all collections
time_text = self.time_interval_text(self.attribute)
else: # average or total the data
time_text = str(self.analysis_period).split('@')[0]
d_type_text = '{} {}'.format(self.attribute.capitalize(), d_type_text)
return '{}\n{}'.format('{} ({})'.format(d_type_text, self.unit), time_text)
@property
def data_type(self):
"""Text for the data type."""
return self._base_type
@property
def unit(self):
"""The unit of this object's data collections."""
return self._base_unit
@property
def analysis_period(self):
"""The analysis_period of this object's data collections."""
return self._base_collection.header.analysis_period
@property
def min_point(self):
"""Get a Point3D for the minimum of the box around the objects."""
return Point3D(self._min_point.x, self._min_point.y, 0)
@property
def max_point(self):
"""Get a Point3D for the maximum of the box around the objects."""
return Point3D(self._max_point.x, self._max_point.y, 0)
[docs]
def time_interval_text(self, simulation_step):
"""Get text for a specific time simulation_step of the data collections.
Args:
simulation_step: An integer for the step of simulation for which
text should be generated.
"""
return str(self._base_collection.datetimes[simulation_step])
[docs]
def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
def __repr__(self):
"""Color Network representation."""
return 'Color Network results: {} [{} results]'.format(
self.network.display_name, len(self._data_collections))
def _calculate_min_max(net_objs):
"""Calculate maximum and minimum Point3D for a set of objects."""
st_rm_min, st_rm_max = net_objs[0].geometry.min, net_objs[0].geometry.max
min_pt = [st_rm_min.x, st_rm_min.y]
max_pt = [st_rm_max.x, st_rm_max.y]
for obj in net_objs[1:]:
rm_min, rm_max = obj.geometry.min, obj.geometry.max
if rm_min.x < min_pt[0]:
min_pt[0] = rm_min.x
if rm_min.y < min_pt[1]:
min_pt[1] = rm_min.y
if rm_max.x > max_pt[0]:
max_pt[0] = rm_max.x
if rm_max.y > max_pt[1]:
max_pt[1] = rm_max.y
return Point2D(min_pt[0], min_pt[1]), Point2D(max_pt[0], max_pt[1])