first commit
This commit is contained in:
248
helix/calculators/ballast_calculator.py
Normal file
248
helix/calculators/ballast_calculator.py
Normal file
@@ -0,0 +1,248 @@
|
||||
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
|
||||
Reference in New Issue
Block a user