Source code for ladybug_comfort.chart.adaptive

# coding=utf-8
"""Object for plotting a Adaptive Comfort Chart."""
from __future__ import division

from ladybug_geometry.geometry2d import Point2D, Vector2D, LineSegment2D, \
    Polyline2D, Polygon2D, Mesh2D
from ladybug_geometry.geometry3d import Point3D, Vector3D

from ladybug.datacollection import DailyCollection, MonthlyCollection
from ladybug.graphic import GraphicContainer
from ladybug.datatype.time import Time
from ladybug.datatype.temperaturedelta import TemperatureDelta
from ladybug.datatype.temperature import Temperature, OperativeTemperature

from ..adaptive import neutral_temperature_ashrae55, neutral_temperature_en15251, \
    neutral_temperature_conditioned_function, cooling_effect_ashrae55, \
    cooling_effect_en15251, t_operative
from ..collection.base import BaseCollection
from ..collection.adaptive import Adaptive


[docs] class AdaptiveChart(object): """Adaptive comfort DataCollection object. Args: outdoor_temperature: Either one of the following inputs are acceptable: * A Data Collection of prevailing outdoor temperature values in C. Such a Data Collection must align with the operative_temperature input and bear the PrevailingOutdoorTemperature data type in its header. * A single prevailing outdoor temperature value in C to be used for all of the operative_temperature inputs below. * A Data Collection of actual outdoor temperatures recorded over the entire year. This Data Collection must be continuous and must either be an Hourly Collection or Daily Collection. In the event that the input comfort_parameter has a prevailing_temperature_method of 'Monthly', Monthly collections are also acceptable here. Note that, because an annual input is required, this input collection does not have to align with the operative_temperature input. operative_temperature: Data Collection of operative temperature (To) values in degrees Celsius. air_speed: A number for the air speed values in m/s. If None, a low air speed of 0.1 m/s wil be used. (Default: None). comfort_parameter: Optional AdaptiveParameter object to specify parameters under which conditions are considered acceptable. If None, default will assume ASHRAE-55 criteria. legend_parameters: An optional LegendParameter object to change the display of the AdaptiveChart. (Default: None). base_point: A Point2D to be used as a starting point to generate the geometry of the plot. (Default: (0, 0)). x_dim: A number to set the X dimension of each degree of temperature on the chart. (Default: 1). y_dim: A number to set the Y dimension of each degree of temperature on the chart. (Default: 1). Note that most maximum humidity ratios are around 0.03. (Default: 1500). min_prevailing: An integer for the minimum prevailing temperature on the chart in degrees. This should be celsius if use_ip is False and fahrenheit if use_ip is True. (Default: 10; suitable for celsius). max_prevailing: An integer for the maximum prevailing temperature on the chart in degrees. This should be celsius if use_ip is False and fahrenheit if use_ip is True. (Default: 33; suitable for celsius). min_operative: An integer for the minimum indoor operative temperature on the chart in degrees. This should be celsius if use_ip is False and fahrenheit if use_ip is True. (Default: 14; suitable for celsius). max_operative: An integer for the maximum indoor operative temperature on the chart in degrees. This should be celsius if use_ip is False and fahrenheit if use_ip is True. (Default: 40; suitable for celsius). use_ip: Boolean to note whether temperature values should be plotted in Fahrenheit instead of Celsius. (Default: False). Properties: * collection * prevailing_outdoor_temperature * operative_temperature * air_speed * comfort_parameter * legend_parameters * base_point * x_dim * y_dim * min_prevailing * max_prevailing * min_operative * max_operative * use_ip * chart_border * prevailing_labels * prevailing_label_points * prevailing_lines * operative_labels * operative_label_points * operative_lines * neutral_temperature * degrees_from_neutral * neutral_polyline * comfort_polygon * is_comfortable * thermal_condition * percent_comfortable * percent_uncomfortable * percent_neutral * percent_hot * percent_cold * title_text * title_location * x_axis_text * x_axis_location * y_axis_text * y_axis_location * data_points * time_matrix * hour_values * colored_mesh * legend * container """ __slots__ = ( '_collection', '_prevailing_outdoor_temperature', '_operative_temperature', '_neutral_temperature', '_degrees_from_neutral', '_base_point', '_x_dim', '_y_dim', '_min_prevailing', '_max_prevailing', '_min_operative', '_max_operative', '_use_ip', '_tp_category', '_to_category', '_prevail_range', '_op_range', '_x_range', '_y_range', '_time_multiplier', '_time_matrix', '_hour_values', '_remove_pattern', '_container', '_chart_border', '_data_points', '_colored_mesh' ) TEMP_TYPE = Temperature() DT_TYPE = TemperatureDelta() def __init__( self, outdoor_temperature, operative_temperature, air_speed=None, comfort_parameter=None, legend_parameters=None, base_point=Point2D(), x_dim=1, y_dim=1, min_prevailing=10, max_prevailing=33, min_operative=14, max_operative=40, use_ip=False ): # check inputs that determine other inputs assert air_speed is None or isinstance(air_speed, (float, int)), \ 'Expected number or None for air_speed. Got {}.'.format(type(air_speed)) self._use_ip = bool(use_ip) # build an adaptive comfort collection from the data self._collection = Adaptive(outdoor_temperature, operative_temperature, air_speed, comfort_parameter) # ensue all temperatures are in the correct units if self._use_ip: # convert everything to Fahrenheit self._prevailing_outdoor_temperature = \ self._collection.prevailing_outdoor_temperature.to_ip() self._operative_temperature = \ self._collection.operative_temperature.to_ip() self._neutral_temperature = \ self._collection.neutral_temperature.to_ip() self._degrees_from_neutral = \ self._collection.degrees_from_neutral.to_ip() else: self._prevailing_outdoor_temperature = \ self._collection.prevailing_outdoor_temperature self._operative_temperature = \ self._collection.operative_temperature self._neutral_temperature = self._collection.neutral_temperature self._degrees_from_neutral = self._collection.degrees_from_neutral # extract the timestep from the data collections if isinstance(self._operative_temperature, MonthlyCollection): self._time_multiplier = 30 * 24 elif isinstance(self._operative_temperature, DailyCollection): self._time_multiplier = 24 else: # it's an hourly or sub-hourly collection self._time_multiplier = \ 1 / self._operative_temperature.header.analysis_period.timestep # assign the inputs as properties of the chart assert isinstance(base_point, Point2D), 'Expected Point2D for ' \ 'PsychrometricChart base point. Got {}.'.format(type(base_point)) self._base_point = base_point self._x_dim = self._check_number(x_dim, 'x_dim') self._y_dim = self._check_number(y_dim, 'y_dim') assert max_prevailing - min_prevailing >= 10, 'Adaptive chart ' \ 'max_prevailing and min_prevailing difference must be at least 10.' assert max_operative - min_operative >= 10, 'Adaptive chart ' \ 'max_operative and min_operative difference must be at least 10.' self._max_prevailing = int(max_prevailing) self._min_prevailing = int(min_prevailing) self._max_operative = int(max_operative) self._min_operative = int(min_operative) if self._use_ip: assert self._max_prevailing > 50, 'max_prevailing must be greater than ' \ '50F. Got {}.'.format(self._max_prevailing) assert self._min_prevailing < 86, 'min_prevailing must be less than ' \ '86F. Got {}.'.format(self._min_prevailing) else: assert self._max_prevailing > 10, 'max_prevailing must be greater than ' \ '10C. Got {}.'.format(self._max_prevailing) assert self._min_prevailing < 30, 'min_prevailing must be less than ' \ '30C. Got {}.'.format(self._min_prevailing) # create the graphic container if self._use_ip: # categorize based on every 1.66 fahrenheit self._tp_category, self._to_category = [], [] current_t, max_t = self._min_prevailing, self._max_prevailing + 1.75 while current_t < max_t: current_t += (5 / 3) self._tp_category.append(current_t) current_t, max_t = self._min_operative, self._max_operative + 1.75 while current_t < max_t: current_t += (5 / 3) self._to_category.append(current_t) else: # categorize based on every degree celsius self._tp_category = list(range(self._min_prevailing + 1, self._max_prevailing + 1)) self._to_category = list(range(self._min_operative + 1, self._max_operative + 1)) self._time_matrix, self._hour_values, self._remove_pattern = \ self._compute_hour_values() assert len(self._hour_values) > 0, \ 'No data was found to lie on the adaptive chart.' max_x = base_point.x + (self._max_prevailing - self._min_prevailing + 1) \ * self._x_dim max_y = base_point.y + (self._max_operative - self._min_operative + 1) \ * self._y_dim max_pt = Point3D(max_x, max_y, 0) min_pt = Point3D(base_point.x, base_point.y, 0) self._container = GraphicContainer( self._hour_values, min_pt, max_pt, legend_parameters, Time(), 'hr') self._process_legend_default(self._container.legend_parameters) # create global attributes used by several of the geometry properties self._prevail_range = list(range(self._min_prevailing, self._max_prevailing, 2)) \ + [self._max_prevailing] self._x_range = [self.tp_x_value(t) for t in self._prevail_range] self._op_range = list(range(self._min_operative, self._max_operative, 2)) \ + [self._max_operative] self._y_range = [self.to_y_value(t) for t in self._op_range] if use_ip: # ensure that _temp_range is always in celsius self._prevail_range = self.TEMP_TYPE.to_unit(self._prevail_range, 'C', 'F') self._op_range = self.TEMP_TYPE.to_unit(self._op_range, 'C', 'F') # set null values for properties that are optional self._chart_border = None self._data_points = None self._colored_mesh = None
[docs] @classmethod def from_air_and_rad_temp( cls, outdoor_temperature, air_temperature, rad_temperature=None, air_speed=None, comfort_parameter=None, legend_parameters=None, base_point=Point2D(), x_dim=1, y_dim=1, min_prevailing=10, max_prevailing=33, min_operative=14, max_operative=40, use_ip=False ): """Initialize an AdaptiveChart from air and radiant temperature.""" if rad_temperature is None: to = air_temperature else: to = BaseCollection.compute_function_aligned( t_operative, [air_temperature, rad_temperature], OperativeTemperature(), 'C') return cls( outdoor_temperature, to, air_speed, comfort_parameter, legend_parameters, base_point, x_dim, y_dim, min_prevailing, max_prevailing, min_operative, max_operative, use_ip )
@property def prevailing_outdoor_temperature(self): """Data Collection of prevailing outdoor temperature. This will be in in degrees C if use_ip is False and degrees F if use_ip is True. """ return self._prevailing_outdoor_temperature @property def operative_temperature(self): """Data Collection of indoor operative temperature. This will be in in degrees C if use_ip is False and degrees F if use_ip is True. """ return self._operative_temperature @property def air_speed(self): """Value for air speed in m/s.""" return self._collection.air_speed[0] @property def comfort_parameter(self): """Adaptive comfort parameters that are assigned to this object.""" return self._collection.comfort_parameter @property def legend_parameters(self): """The legend parameters customizing this adaptive chart.""" return self._container.legend_parameters @property def base_point(self): """Point3D for the base point of this adaptive chart.""" return self._base_point @property def x_dim(self): """The X dimension for each degree of prevailing temperature on the chart.""" return self._x_dim @property def y_dim(self): """The Y dimension for each degree of operative temperature on the chart.""" return self._y_dim @property def min_prevailing(self): """An integer for the minimum prevailing outdoor temperature on the chart. Will be in celsius if use_ip is False and fahrenheit if use_ip is True. """ return self._min_prevailing @property def max_prevailing(self): """An integer for the maximum prevailing outdoor temperature on the chart. Will be in celsius if use_ip is False and fahrenheit if use_ip is True. """ return self._max_prevailing @property def min_operative(self): """An integer for the minimum indoor operative temperature on the chart. Will be in celsius if use_ip is False and fahrenheit if use_ip is True. """ return self._min_operative @property def max_operative(self): """An integer for the maximum indoor operative temperature on the chart. Will be in celsius if use_ip is False and fahrenheit if use_ip is True. """ return self._max_operative @property def use_ip(self): """Boolean for whether temperature should be in Fahrenheit or Celsius.""" return self._use_ip @property def chart_border(self): """Get a Polygon2D for the border of the chart.""" if self._chart_border is None: self._chart_border = self._compute_border() return self._chart_border @property def prevailing_labels(self): """Get a tuple of text for the prevailing temperature labels on the chart.""" if self.use_ip: temp_range = tuple(range(self._min_prevailing, self._max_prevailing, 2)) \ + (self._max_prevailing,) return tuple(str(val) for val in temp_range) return tuple(str(val) for val in self._prevail_range) @property def prevailing_label_points(self): """Get a tuple of Point2Ds for the prevailing temperature labels on the chart.""" y_val = self._base_point.y - self.legend_parameters.text_height * 0.5 return tuple(Point2D(x_val, y_val) for x_val in self._x_range) @property def prevailing_lines(self): """Get a tuple of LineSegment2Ds for the prevailing temperature lines.""" t_lines = [] # create the array of line segments y_vec = Vector2D(0, self._y_range[-1] - self._y_range[0]) for x_val in self._x_range: l_seg = LineSegment2D(Point2D(x_val, self._base_point.y), y_vec) t_lines.append(l_seg) return t_lines @property def operative_labels(self): """Get a tuple of text for the operative temperature labels on the chart.""" if self.use_ip: temp_range = tuple(range(self._min_operative, self._max_operative, 2)) \ + (self._max_operative,) return tuple(str(val) for val in temp_range) return tuple(str(val) for val in self._op_range) @property def operative_label_points(self): """Get a tuple of Point2Ds for the operative temperature labels on the chart.""" x_val = self._base_point.x - self.legend_parameters.text_height * 2.5 return tuple(Point2D(x_val, y_val) for y_val in self._y_range) @property def operative_lines(self): """Get a tuple of LineSegment2Ds for the operative temperature lines.""" t_lines = [] # create the array of line segments x_vec = Vector2D(self._x_range[-1] - self._x_range[0], 0) for y_val in self._y_range: l_seg = LineSegment2D(Point2D(self._base_point.x, y_val), x_vec) t_lines.append(l_seg) return t_lines @property def neutral_polyline(self): """Get a LineSegment2D or Polyline2D noting the neutral temperature on the chart. """ # get properties that are used to compute the neutral temperature tp_c_min, tp_c_max = self._prevail_range[0], self._prevail_range[-1] pl_pts = [] if self.comfort_parameter.conditioning != 0: neutral_func = neutral_temperature_conditioned_function( self.comfort_parameter.conditioning, self.comfort_parameter.standard ) elif self.comfort_parameter.ashrae_or_en: neutral_func = neutral_temperature_ashrae55 else: neutral_func =neutral_temperature_en15251 # get the beginning points x_val1 = self._x_range[0] if tp_c_min < 10: n_temp = neutral_func(10) y_val = self.to_y_value(n_temp) if not self.use_ip else \ self.to_y_value(self.TEMP_TYPE.to_unit([n_temp], 'F', 'C')[0]) pl_pts.append(Point2D(x_val1, y_val)) x_val2 = self.tp_x_value(50) if self.use_ip else self.tp_x_value(10) pl_pts.append(Point2D(x_val2, y_val)) else: n_temp = neutral_func(tp_c_min) y_val = self.to_y_value(n_temp) if not self.use_ip else \ self.to_y_value(self.TEMP_TYPE.to_unit([n_temp], 'F', 'C')[0]) pl_pts.append(Point2D(x_val1, y_val)) # get the ending points x_val_end = self._x_range[-1] if self.comfort_parameter.ashrae_or_en: if tp_c_max > 33.5: n_temp = neutral_func(33.5) y_val = self.to_y_value(n_temp) if not self.use_ip else \ self.to_y_value(self.TEMP_TYPE.to_unit([n_temp], 'F', 'C')[0]) x_vali = self.tp_x_value(92.3) if self.use_ip else self.tp_x_value(33.5) pl_pts.append(Point2D(x_vali, y_val)) pl_pts.append(Point2D(x_val_end, y_val)) else: n_temp = neutral_func(tp_c_max) y_val = self.to_y_value(n_temp) if not self.use_ip else \ self.to_y_value(self.TEMP_TYPE.to_unit([n_temp], 'F', 'C')[0]) pl_pts.append(Point2D(x_val_end, y_val)) else: if tp_c_max > 30: n_temp = neutral_func(30) y_val = self.to_y_value(n_temp) if not self.use_ip else \ self.to_y_value(self.TEMP_TYPE.to_unit([n_temp], 'F', 'C')[0]) x_vali = self.tp_x_value(86) if self.use_ip else self.tp_x_value(30) pl_pts.append(Point2D(x_vali, y_val)) pl_pts.append(Point2D(x_val_end, y_val)) else: n_temp = neutral_func(tp_c_max) y_val = self.to_y_value(n_temp) if not self.use_ip else \ self.to_y_value(self.TEMP_TYPE.to_unit([n_temp], 'F', 'C')[0]) pl_pts.append(Point2D(x_val_end, y_val)) # return the neutral line return Polyline2D(pl_pts) if len(pl_pts) > 2 else \ LineSegment2D.from_end_points(pl_pts[0], pl_pts[1]) @property def comfort_polygon(self): """Get a Polygon2D for the comfort range on the chart.""" # start off with the neutral polyline and move it based on the offset neutral_line = self.neutral_polyline offset_t_up = self.comfort_parameter.neutral_offset # lower threshold of EN-16798 is 1 degree cooler than upper threshold offset_t_low = -self.comfort_parameter.neutral_offset \ if self.comfort_parameter.standard == 'ASHRAE-55' else \ -self.comfort_parameter.neutral_offset - 1 offset_t_up = offset_t_up if not self.use_ip else \ self.DT_TYPE.to_unit([offset_t_up], 'dF', 'dC')[0] offset_t_low = offset_t_low if not self.use_ip else \ self.DT_TYPE.to_unit([offset_t_low], 'dF', 'dC')[0] offset_dist_up = self.y_dim * offset_t_up offset_dist_low = self.y_dim * offset_t_low upper_line = neutral_line.move(Vector2D(0, offset_dist_up)) lower_line = neutral_line.move(Vector2D(0, offset_dist_low)) # trim the bottom of the polygon if there is a cold_prevail_temp_limit if self.comfort_parameter.cold_prevail_temp_limit > 10: limit_tc = self.comfort_parameter.cold_prevail_temp_limit limit_t = limit_tc if not self.use_ip else \ self.TEMP_TYPE.to_unit([limit_tc], 'F', 'C')[0] limit_x = self.tp_x_value(limit_t) int_lin = LineSegment2D.from_end_points(Point2D(limit_x, self._y_range[0]), Point2D(limit_x, self._y_range[-1])) i_pts = lower_line.intersect_line_ray(int_lin) if i_pts is not None and (len(i_pts) == 1 or isinstance(i_pts, Point2D)): int_pt = i_pts if isinstance(i_pts, Point2D) else i_pts[0] new_low_pts, int_passed = [], False for pt in lower_line.vertices: if pt.x < int_pt.x: new_low_pts.append(Point2D(pt.x, int_pt.y)) elif not int_passed: new_low_pts.append(int_pt) new_low_pts.append(pt) int_passed = True else: new_low_pts.append(pt) lower_line = Polyline2D(new_low_pts) if len(new_low_pts) > 2 else \ LineSegment2D.from_end_points(new_low_pts[0], new_low_pts[1]) # determine if there is a cooling effect if self.comfort_parameter.discrete_or_continuous_air_speed is True: cooling_func = cooling_effect_ashrae55 else: cooling_func = cooling_effect_en15251 ce = cooling_func(self.air_speed, self._prevail_range[-1]) if ce == 0: # we can build the polygon from upper/lower lines return Polygon2D(lower_line.vertices + tuple(reversed(upper_line.vertices))) # adjust the upper line to account for the cooling effect ce_t = ce if not self.use_ip else self.DT_TYPE.to_unit([ce], 'dF', 'dC')[0] ce_dist = self.y_dim * ce_t ce_vec = Vector2D(0, ce_dist) switch_tc = 12 if self.comfort_parameter.ashrae_or_en else 12.73 switch_t = switch_tc if not self.use_ip else \ self.TEMP_TYPE.to_unit([switch_tc], 'F', 'C')[0] switch_x = self.tp_x_value(switch_t) if upper_line.vertices[0].x >= switch_x: new_up_pts = [pt.move(ce_vec) for pt in upper_line.vertices] else: new_up_pts, switch_occurred = [], False for i, pt in enumerate(upper_line.vertices): if pt.x <= switch_x: new_up_pts.append(pt) else: if switch_occurred: new_up_pts.append(pt.move(ce_vec)) else: int_line1 = LineSegment2D.from_end_points( Point2D(switch_x, self._y_range[0]), Point2D(switch_x, self._y_range[-1])) int_line2 = LineSegment2D.from_end_points( upper_line.vertices[i - 1], pt) int_pt = int_line1.intersect_line_ray(int_line2) new_up_pts.append(int_pt) new_up_pts.append(int_pt.move(ce_vec)) new_up_pts.append(pt.move(ce_vec)) switch_occurred = True return Polygon2D(lower_line.vertices + tuple(reversed(new_up_pts))) @property def neutral_temperature(self): """Data Collection of the desired neutral temperature in degrees C.""" return self._neutral_temperature @property def degrees_from_neutral(self): """Data Collection of the degrees from desired neutral temperature in C.""" return self._degrees_from_neutral @property def is_comfortable(self): """Data Collection of integers noting whether the input conditions are acceptable according to the assigned comfort_parameter. Values are one of the following: * 0 = uncomfortable * 1 = comfortable """ return self._collection.is_comfortable @property def thermal_condition(self): """Data Collection of integers noting the thermal status of a subject according to the assigned comfort_parameter. Values are one of the following: * -1 = cold * 0 = neutral * +1 = hot """ return self._collection.thermal_condition @property def percent_comfortable(self): """The percent of time comfortable given by the assigned comfort_parameter.""" return self._collection.percent_comfortable @property def percent_uncomfortable(self): """The percent of time uncomfortable given by the assigned comfort_parameter.""" return self._collection.percent_uncomfortable @property def percent_neutral(self): """The percent of time that the thermal_condition is neutral.""" self._collection.percent_neutral @property def percent_cold(self): """The percent of time that the thermal_condition is cold.""" self._collection.percent_cold @property def percent_hot(self): """The percent of time that the thermal_condition is hot.""" self._collection.percent_hot @property def title_text(self): """Get text for the title of the chart.""" title_items = ['Adaptive Chart', 'Time [hr]'] extra_data = self.operative_temperature.header.metadata.items() if len(extra_data) == 0: extra_data = self.prevailing_outdoor_temperature.header.metadata.items() return '\n'.join(title_items + ['{}: {}'.format(k, v) for k, v in extra_data]) @property def title_location(self): """Get a Point2D for the title of the chart.""" origin = self.container.lower_title_location.o.move( Vector3D(0, -self.legend_parameters.text_height * 4)) return Point2D(origin.x, origin.y) @property def x_axis_text(self): """Get text for the X-axis label of the chart.""" unit = 'C' if not self.use_ip else 'F' return 'Prevailing Outdoor Temperature [{}]'.format(unit) \ if self.comfort_parameter.avg_month_or_running_mean else \ 'Outdoor Running Mean Temperature [{}]'.format(unit) @property def x_axis_location(self): """Get a Point2D for the X-axis label of the chart.""" y_val = self._base_point.y - self.legend_parameters.text_height * 2.5 x_val = (self._x_range[0] + self._x_range[-1]) / 2 return Point2D(x_val, y_val) @property def y_axis_text(self): """Get text for the Y-axis label of the chart.""" unit = 'C' if not self.use_ip else 'F' if 'type' in self.operative_temperature.header.metadata: return '{} [{}]'.format( self.operative_temperature.header.metadata['type'], unit) return '{} [{}]'.format(self.operative_temperature.header.data_type, unit) @property def y_axis_location(self): """Get a Point2D for the Y-axis label of the chart.""" x_val = self._base_point.x - self.legend_parameters.text_height * 5.5 y_val = (self._y_range[0] + self._y_range[-1]) / 2 return Point2D(x_val, y_val) @property def data_points(self): """Get a tuple of Point2Ds for each of the temperature values.""" if self._data_points is None: zip_o = zip(self.prevailing_outdoor_temperature, self.operative_temperature) self._data_points = tuple( Point2D(self.tp_x_value(tp), self.to_y_value(to)) for tp, to in zip_o ) return self._data_points @property def time_matrix(self): """Get a tuple of of tuples where each sub-tuple is a row of the mesh. Each value in the resulting matrix corresponds to the number of prevailing/ operative temperature points in a given cell of the mesh. """ return tuple(tuple(row) for row in self._time_matrix) @property def hour_values(self): """Get a tuple for the number of hours associated with each colored_mesh face.""" return self._hour_values @property def colored_mesh(self): """Get a colored mesh for the number of hours for each part of the chart.""" if self._colored_mesh is None: self._colored_mesh = self._generate_mesh() return self._colored_mesh @property def legend(self): """The legend assigned to this graphic.""" return self._container._legend @property def container(self): """Get the GraphicContainer for the colored mesh.""" return self._container
[docs] def data_mesh(self, data_collection, legend_parameters=None): """Get a colored mesh for a data_collection aligned with the chart's data. Args: data_collection: A data collection that is aligned with the prevailing and operative temperature values of the chart. legend_parameters: Optional legend parameters to customize the legend and look of the resulting mesh. Returns: A tuple with two values. - mesh: A colored Mesh2D similar to the chart's colored_mesh property but where each face is colored with the average value of the input data_collection. - container: A GraphicContainer object for the mesh, which possesses a legend that corresponds to the mesh. """ # check to be sure the data collection aligns data_vals = data_collection.values _tp_values = self.prevailing_outdoor_temperature.values _to_values = self.operative_temperature.values assert len(data_vals) == len(self.operative_temperature.values), \ 'Number of data collection values ' \ 'must match those of the prevailing and operative temperature.' # create a matrix with a tally of the hours for all the data base_mtx = [[[] for val in self._tp_category] for rh in self._to_category] for tp, to, val in zip(_tp_values, _to_values, data_vals): if tp < self._min_prevailing or tp > self._max_prevailing: continue # temperature value does not currently fit on the chart if to < self._min_operative or to > self._max_operative: continue # temperature value does not currently fit on the chart for y, to_cat in enumerate(self._to_category): if to < to_cat: break for x, tp_cat in enumerate(self._tp_category): if tp < tp_cat: break base_mtx[y][x].append(val) # compute average values avg_values = [sum(val_list) / len(val_list) for tp_l in base_mtx for val_list in tp_l if len(val_list) != 0] # create the colored mesh and graphic container base_contain = self.container container = GraphicContainer( avg_values, base_contain.min_point, base_contain.max_point, legend_parameters, data_collection.header.data_type, data_collection.header.unit) self._process_legend_default(container.legend_parameters) mesh = self.colored_mesh.duplicate() # start with hour mesh as a base mesh.colors = container.value_colors return mesh, container
[docs] def plot_point(self, prevailing, operative): """Get a Point2D for a given prevailing and operative temperature on the chart. Args: prevailing: A prevailing temperature value, which should be in Celsius if use_ip is False and Fahrenheit is use_ip is True. operative: An operative temperature value, which should be in Celsius if use_ip is False and Fahrenheit is use_ip is True. """ return Point2D(self.tp_x_value(prevailing), self.to_y_value(operative))
[docs] def to_y_value(self, temperature): """Get the Y-coordinate for a certain operative temperature on the chart. Args: temperature: A temperature value, which should be in Celsius if use_ip is False and Fahrenheit is use_ip is True. """ return self.base_point.y + self._y_dim * (temperature - self._min_operative)
[docs] def tp_x_value(self, temperature): """Get the X-coordinate for a certain prevailing temperature on the chart. Args: temperature: A temperature value, which should be in Celsius if use_ip is False and Fahrenheit is use_ip is True. """ return self._base_point.x + self._x_dim * (temperature - self._min_prevailing)
def _compute_hour_values(self): """Compute the matrix of binned time values based on the chart inputs. Returns: A tuple with three values. - base_mtx: A full matrix with counts of values for each degree temperature and 5% RH of the chart. - mesh_values: A list of numbers for the values of the mesh. - remove_pattern: A list of booleans for which faces of the full mesh should be removed. """ # create a matrix with a tally of the hours for all the data base_mtx = [[0 for tp in self._tp_category] for to in self._to_category] zip_obj = zip(self.prevailing_outdoor_temperature, self.operative_temperature) for tp, to in zip_obj: if tp < self._min_prevailing or tp > self._max_prevailing: continue # temperature value does not currently fit on the chart if to < self._min_operative or to > self._max_operative: continue # temperature value does not currently fit on the chart for y, to_cat in enumerate(self._to_category): if to < to_cat: break for x, tp_cat in enumerate(self._tp_category): if tp < tp_cat: break base_mtx[y][x] += 1 # flatten the matrix and create a pattern to remove faces flat_values = [tc * self._time_multiplier for to_l in base_mtx for tc in to_l] remove_pattern = [val != 0 for val in flat_values] mesh_values = tuple(val for val in flat_values if val != 0) return base_mtx, mesh_values, remove_pattern def _generate_mesh(self): """Get the colored mesh from this object's hour values.""" # global properties used in the generation of the mesh t_per_row = [self._min_prevailing] + self._tp_category x_per_row = [self.tp_x_value(t) for t in t_per_row] # loop through RH rows and create mesh vertices and faces vertices = [Point2D(x, self._base_point.y) for x in x_per_row] faces, vert_count, row_len = [], 0, len(t_per_row) for to in self._to_category: vert_count += row_len y1 = self.to_y_value(to) vertices.append(Point2D(x_per_row[0], y1)) for i, t in enumerate(x_per_row[1:]): vertices.append(Point2D(x_per_row[i + 1], y1)) v1 = vert_count - row_len + i v2 = v1 + 1 v3 = vert_count + i + 1 v4 = v3 - 1 faces.append((v1, v2, v3, v4)) # create the Mesh2D, remove unused faces, and assign the colors mesh = Mesh2D(vertices, faces) mesh = mesh.remove_faces_only(self._remove_pattern) mesh.colors = self._container.value_colors return mesh def _compute_border(self): """Compute a Polygon2D for the outer border of the chart.""" # get properties used to establish the border of the chart bpt = self.base_point x_max = bpt.x + (self.max_prevailing - self.min_prevailing) * self._x_dim y_max = bpt.y + (self.max_operative - self.min_operative) * self._y_dim # get the points and build the polyline return Polygon2D( (bpt, Point2D(x_max, bpt.y), Point2D(x_max, y_max), Point2D(bpt.x, y_max)) ) def _process_legend_default(self, l_par): """Override the dimensions of the legend to ensure it fits the chart.""" min_pt, max_pt = self.container.min_point, self.container.max_point if l_par.vertical and l_par.is_segment_height_default: l_par.properties_3d.segment_height = (max_pt.y - min_pt.y) / 20 l_par.properties_3d._is_segment_height_default = True elif l_par.vertical and l_par.is_segment_height_default: l_par.properties_3d.segment_width = (max_pt.x - min_pt.x) / 20 l_par.properties_3d._is_segment_width_default = True @staticmethod def _check_number(value, value_name): """Check a given value for a dimension input.""" try: value = float(value) except (ValueError, TypeError): raise TypeError('Expected number for Psychrometric Chart {}. ' 'Got {}.'.format(value_name, type(value))) assert value > 0, 'Psychrometric Chart {} must be greater than 0. ' \ 'Got {}.'.format(value_name, value) return value def __len__(self): """Return length of values on the object.""" return len(self.operative_temperature._values) def __getitem__(self, key): """Return a tuple of temperature and humidity.""" return self.prevailing_outdoor_temperature._values[key], \ self.operative_temperature._values[key] def __iter__(self): """Iterate through the values.""" return zip( self.prevailing_outdoor_temperature._values, self.operative_temperature._values )
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __repr__(self): """Adaptive Chart representation.""" return 'Adaptive Chart: {} values'.format(len(self))