from collections import namedtuple, OrderedDict from math import ceil, floor from helix.constants.panel_type import PanelType from helix.constants.system_type import SystemType from helix.models.panel import Panel, PanelWarnings Result = namedtuple('Result', ['ballast_count', 'link_tray_count', 'cross_tray_count', 'system_weight', 'needs_anchor']) class BallastCalculator(object): def __init__(self, user_values): self.values = user_values self.system_type = user_values.system_type() self.anchor_type = user_values.anchor_type() self.system_constants = self.system_type.system_constants() self.module_constants = user_values.module_system_constants() def ballast_and_trays_matrix(self, c_p_matrix, q_z, panels, ballast_block_weight=None): if not ballast_block_weight: ballast_block_weight = self.values.ballast_block_weight() ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight) for idx, panel in enumerate(panels): stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone] panels[idx] = stored_panel.merge(panel) return panels def update_ballast(self, c_p_matrix, q_z, panels): ballast_block_weight = self.values.ballast_block_weight() ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight) seismic_ballast_store = {} for panel in panels: seismic_anchors = panel.seismic_anchors if panel.seismic_anchors else 0 if seismic_anchors != 0: key = hash(panel.wind_zone) + hash(panel.panel_type) + seismic_anchors + hash(panel.fuzzy_wind_zone) # hack stored_panel = seismic_ballast_store.get(key) if stored_panel: panel.ballast = stored_panel.ballast panel.link_tray = stored_panel.link_tray panel.cross_tray = stored_panel.cross_tray panel.pressure = stored_panel.pressure else: anchors = panel.wind_anchors + seismic_anchors c_p = c_p_matrix[panel.wind_zone, panel.panel_type.index()] * (1.15 if panel.fuzzy_wind_zone else 1) force = self.uplift(c_p, q_z) - anchors * self.anchor_type.uplift_capacity() ballast_and_tray_count = self.ballast_and_tray_count(force, panel.panel_type, ballast_block_weight, anchors) pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight) panel.ballast = ballast_and_tray_count.ballast_count panel.link_tray = ballast_and_tray_count.link_tray_count panel.cross_tray = ballast_and_tray_count.cross_tray_count panel.pressure = pressure seismic_ballast_store[key] = panel else: stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone] panel.ballast = stored_panel.ballast panel.link_tray = stored_panel.link_tray panel.cross_tray = stored_panel.cross_tray panel.pressure = stored_panel.pressure return panels def calculate_ballast_store(self, cp_matrix, qz, ballast_block_weight): max_psf = self.values.max_system_pressure() store = {} for panel_type in PanelType.all(): sub_store = {} for wind_zone, _ in enumerate(self.values.system_type().system_constants().wind_zones): sub_store[wind_zone] = {} for use_fuzzy in (True, False): sub_store[wind_zone][use_fuzzy] = self.ballast_tray_and_anchor_count(wind_zone=wind_zone, panel_type=panel_type, ballast_block_weight=ballast_block_weight, max_system_pressure=max_psf, c_p_matrix=cp_matrix, q_z=qz, use_fuzzy=use_fuzzy) store[panel_type] = sub_store return store def summary_table(self, c_p_matrix, q_z): wind_zones = self.system_constants.wind_zones ballast_block_weight = self.values.ballast_block_weight() max_system_pressure = self.values.max_system_pressure() table = OrderedDict() for panel_type in PanelType.all(): ballast_counts = [] anchor_counts = [] pressures = [] warnings = [] for wind_zone_index, _ in enumerate(wind_zones): ballast_tray_anchor_panels = self.ballast_tray_and_anchor_count(wind_zone_index, panel_type, ballast_block_weight, max_system_pressure, c_p_matrix, q_z) anchor_count = ballast_tray_anchor_panels.wind_anchors ballast_count = ballast_tray_anchor_panels.ballast pressure = ballast_tray_anchor_panels.pressure warning = ballast_tray_anchor_panels.warnings pressure_as_string = "{0:.2f}".format(pressure) # Because pressure is stored as a floating point number, it is possible, because floats # for pressure to be something like 5.02999999999999. Which is clearly meant to be 5.03. # This represents that as a string, which doesn't have that issue. anchor_counts.append(anchor_count) ballast_counts.append(ballast_count) pressures.append(pressure_as_string) warnings.append(warning) table[panel_type] = { 'ballast blocks': ballast_counts, 'anchors': anchor_counts, 'pressure': pressures, 'warnings': warnings } return table def ballast_tray_and_anchor_count(self, wind_zone, panel_type, ballast_block_weight, max_system_pressure, c_p_matrix, q_z, use_fuzzy=False): fuzzy_factor = 1.15 if use_fuzzy else 1 c_p = c_p_matrix[wind_zone, panel_type.index()] * fuzzy_factor uplift_force = self.uplift(c_p, q_z) warnings = [] keep_trying = True anchor_count = 0 pressure = 0. tries = 0 ballast_and_tray_count = None while keep_trying: remainder_force = uplift_force - anchor_count * self.anchor_type.uplift_capacity() ballast_and_tray_count = self.ballast_and_tray_count(remainder_force, panel_type, ballast_block_weight, anchor_count) pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight) keep_trying = (ballast_and_tray_count.needs_anchor or pressure > max_system_pressure) and ballast_and_tray_count.ballast_count > 0 if keep_trying: anchor_count = self.calculate_anchors(panel_type, uplift_force) + tries tries += 1 keep_trying &= tries < 100 if uplift_force / self.module_constants.surface_area >= self.module_constants.max_psf: warnings.append(PanelWarnings.MaxPsf) return Panel(wind_zone=wind_zone, panel_type=panel_type, ballast=ballast_and_tray_count.ballast_count, link_tray=self.interpret_tray_count(ballast_and_tray_count.link_tray_count, panel_type), cross_tray=ballast_and_tray_count.cross_tray_count, wind_anchors=anchor_count, pressure=pressure, fuzzy_wind_zone=use_fuzzy, warnings=warnings) def ballast_and_tray_count(self, force_to_resist, panel_type, ballast_block_weight, anchor_count): system_weight = self.module_constants.base_weight(panel_type, 0) ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight) link_tray_count = 0 cross_tray_count = 0 needs_anchor = False keep_trying = True tries = 0 while keep_trying and tries < 3: tries += 1 if ballast_count: new_link_tray_count, _ = self.calculate_trays(ballast_count + 2 * anchor_count, self.module_constants.link_tray_thresholds(panel_type)) # Recalculate weight given new link trays; recalculate ballast given new weight system_weight = self.module_constants.base_weight(panel_type, new_link_tray_count + cross_tray_count) ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight) new_cross_tray_count, needs_anchor = self.calculate_trays(ballast_count + 2 * anchor_count, self.module_constants.cross_tray_thresholds( panel_type)) system_weight = self.module_constants.base_weight(panel_type, new_cross_tray_count + new_link_tray_count) ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight) if link_tray_count == new_link_tray_count and cross_tray_count == new_cross_tray_count: keep_trying = False link_tray_count = new_link_tray_count cross_tray_count = new_cross_tray_count else: keep_trying = False return Result(ballast_count, link_tray_count=link_tray_count, cross_tray_count=cross_tray_count, system_weight=system_weight, needs_anchor=needs_anchor) def uplift(self, c_p, q_z): return q_z * self.module_constants.surface_area * c_p def calculate_ballast(self, uplift, non_ballast_weight, ballast_block_weight): if non_ballast_weight > uplift: return 0 return ceil((uplift - non_ballast_weight) / ballast_block_weight) def calculate_trays(self, ballast_count, thresholds): for idx, threshold in enumerate(thresholds): if ballast_count <= threshold: return idx, False return len(thresholds) - 1, True def calculate_pressure_on_roof(self, ballast_count, ballast_block_weight, non_ballast_weight): effective_area = self.module_constants.surface_area / self.module_constants.ground_coverage_ratio return (ballast_count * ballast_block_weight + non_ballast_weight) / effective_area def interpret_tray_count(self, link_tray_count, panel_type): if self.system_type == SystemType.singleTilt: if panel_type == PanelType.EastWest: return 2 elif panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: return 0 return link_tray_count or 0 def calculate_anchors(self, panel_type, uplift): base_system_weight = self.module_constants.base_weight(panel_type, 0) anchor_capacity = self.anchor_type.uplift_capacity() return max(floor((uplift - base_system_weight) / anchor_capacity), 1) def show_presented_link_trays(self, panels): for panel in panels: panel.presented_link_tray = self.present_link_tray(panel.link_tray, panel.panel_type) return panels def present_link_tray(self, link_tray_count, panel_type): if self.system_type == SystemType.singleTilt: link_tray_representation = { PanelType.Corner: 0, PanelType.NorthSouth: 0, PanelType.EastWest: 2, PanelType.Middle: min(1, int(link_tray_count)), }[panel_type] else: link_tray_representation = { PanelType.Corner: 2, PanelType.NorthSouth: 2, PanelType.EastWest: 1, PanelType.Middle: min(1, int(link_tray_count)), }[panel_type] return link_tray_representation