249 lines
12 KiB
Python
249 lines
12 KiB
Python
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
|