first commit
This commit is contained in:
0
helix/calculators/__init__.py
Normal file
0
helix/calculators/__init__.py
Normal file
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
|
||||
58
helix/calculators/bom_calculator.py
Normal file
58
helix/calculators/bom_calculator.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from numpy import array, ceil
|
||||
|
||||
from helix.calculators.bom_helper import add_parts_to_list, apply_package_size_rounding
|
||||
from helix.calculators.ebom_calculator import EbomCalculator
|
||||
from helix.calculators.mechanical_bom_calculator import MechanicalBomCalculator
|
||||
from helix.constants.parts import *
|
||||
|
||||
|
||||
class BomCalculator(object):
|
||||
def __init__(self, values, panels, subarrays, graph_repository):
|
||||
self.values = values
|
||||
self.panels = panels
|
||||
self.subarrays = subarrays
|
||||
self.graph_repository = graph_repository
|
||||
|
||||
def compute_bom(self):
|
||||
output_array = []
|
||||
for part, quantity in self.parts_list().items():
|
||||
if ceil(quantity) <= 0:
|
||||
continue
|
||||
row = list(part)
|
||||
row.append(int(ceil(quantity)))
|
||||
output_array.append(row)
|
||||
|
||||
output_array.sort(key=lambda x: x[0] + x[1])
|
||||
return array(output_array)
|
||||
|
||||
def documentation_bom(self):
|
||||
parts_list = self.parts_list()
|
||||
|
||||
for part in all_parts:
|
||||
if part not in parts_list.keys():
|
||||
parts_list[part] = 0
|
||||
|
||||
output_array = []
|
||||
for part, quantity in parts_list.items():
|
||||
if part == ballast:
|
||||
row = 'ballast'
|
||||
elif part == anchor:
|
||||
row = 'anchors'
|
||||
elif part == module:
|
||||
row = 'modules'
|
||||
else:
|
||||
row = part[0]
|
||||
quantity = max(0, quantity)
|
||||
output_array.append((row, int(ceil(quantity))))
|
||||
return output_array
|
||||
|
||||
def parts_list(self):
|
||||
row_count = sum(subarray.row_count for subarray in self.subarrays)
|
||||
column_count = sum(subarray.column_count for subarray in self.subarrays)
|
||||
parts_list = MechanicalBomCalculator(self.values, self.panels, self.subarrays).mechanical_bom()
|
||||
ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module)).compute_ebom()
|
||||
|
||||
add_parts_to_list(parts_list, ebom_parts_list)
|
||||
|
||||
apply_package_size_rounding(parts_list, package_sizes)
|
||||
return parts_list
|
||||
36
helix/calculators/bom_helper.py
Normal file
36
helix/calculators/bom_helper.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from math import ceil
|
||||
|
||||
from helix.constants.panel_type import PanelType
|
||||
|
||||
|
||||
def add_parts_to_list(parts_list, parts_to_add, multiplier=1):
|
||||
for part, quantity in parts_to_add.items():
|
||||
previous_value = parts_list.get(part) or 0
|
||||
if quantity != 0:
|
||||
parts_list[part] = previous_value + quantity * multiplier
|
||||
|
||||
|
||||
def apply_fudge_factors(parts_list, fudge_factors):
|
||||
for part, quantity in parts_list.items():
|
||||
fudge_factor = fudge_factors.get(part) or 1.0
|
||||
parts_list[part] = quantity * fudge_factor
|
||||
|
||||
|
||||
def apply_package_size_rounding(parts_list, package_sizes):
|
||||
for part, quantity in parts_list.items():
|
||||
package_size = package_sizes.get(part) or 1
|
||||
parts_list[part] = package_size * ceil(quantity / package_size)
|
||||
|
||||
|
||||
def get_panel_type_counts(panels):
|
||||
panel_type_counts = {
|
||||
PanelType.Corner: 0,
|
||||
PanelType.NorthSouth: 0,
|
||||
PanelType.EastWest: 0,
|
||||
PanelType.Middle: 0,
|
||||
}
|
||||
|
||||
for panel in panels:
|
||||
panel_type_counts[panel.panel_type] += 1
|
||||
|
||||
return panel_type_counts
|
||||
169
helix/calculators/calculator.py
Normal file
169
helix/calculators/calculator.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from math import ceil, floor
|
||||
import copy
|
||||
from helix.Repositories.graph_repository import GraphRepository
|
||||
from helix.calculators.ballast_calculator import BallastCalculator
|
||||
from helix.calculators.bom_calculator import BomCalculator
|
||||
from helix.calculators.coordinates_calculator import CoordinatesCalculator
|
||||
from helix.calculators.pressure_coefficient_calculator import PressureCoefficientCalculator
|
||||
from helix.calculators.seismic_calculator import SeismicCalculator
|
||||
from helix.calculators.subarray_helper import get_subarray_sizes_and_rows, extract_subarray
|
||||
from helix.calculators.summary_values_calculator import SummaryValuesCalculator
|
||||
from helix.calculators.wind_pressure_calculator import WindPressureCalculator
|
||||
|
||||
|
||||
class Calculator(object):
|
||||
def __init__(self, user_values, calculate_panel_data=True):
|
||||
self.values = user_values
|
||||
self._q_z = None
|
||||
self._c_p_matrix = None
|
||||
self._L_B = None
|
||||
self._K_z = None
|
||||
self.subarrays = None
|
||||
self.buildings = self.values.buildings_polygons()
|
||||
self.buildings_for_drawing = []
|
||||
|
||||
self.panels = self.values.csv()
|
||||
if calculate_panel_data and self.panels is not None:
|
||||
for idx, panel in enumerate(self.panels):
|
||||
panel.id = idx + 1
|
||||
self.panels.sort(key=lambda x: x.subarray)
|
||||
self.subarrays = get_subarray_sizes_and_rows(self.panels)
|
||||
|
||||
self.__compute_ballast()
|
||||
_,_,self.buildings_for_drawing = self.__transform_coordinates()
|
||||
self.graph_repository = GraphRepository(self.panels, self.subarrays, self.values.system_type())
|
||||
if self.values.user_override_seismic_anchors():
|
||||
user_provided_panels = self.values.get_user_provided_seismic_anchors()
|
||||
for user_panel in user_provided_panels:
|
||||
panel = [panel for panel in self.panels if panel.id == user_panel.id][0]
|
||||
panel.seismic_anchors = user_panel.seismic_anchors
|
||||
self.__compute_seismic_anchors(self.panels) # Update subarrays to include required seismic anchors
|
||||
self.__update_ballast(self.panels)
|
||||
else:
|
||||
# Update subarrays *and panels* to include required seismic anchors
|
||||
self.panels = self.__compute_seismic_anchors(self.panels)
|
||||
|
||||
def k_z(self):
|
||||
if self._K_z is None:
|
||||
self._K_z = WindPressureCalculator(self.values).K_z()
|
||||
return self._K_z
|
||||
|
||||
def L_B(self):
|
||||
if self._L_B is None:
|
||||
self._L_B = PressureCoefficientCalculator(self.values).L_B()
|
||||
return self._L_B
|
||||
|
||||
def summary_table(self):
|
||||
return BallastCalculator(self.values).summary_table(self.__c_p_matrix(), self.q_z())
|
||||
|
||||
def minimum_array_sizes(self):
|
||||
return PressureCoefficientCalculator(self.values).minimum_array_size(self.L_B())
|
||||
|
||||
# Used in the array summary page - is the table of weight, psf, anchors, ballast, etc. for the entire system
|
||||
def summary_values(self):
|
||||
seismic_anchors = self.subarray_summary()
|
||||
ballast_calculator = BallastCalculator(self.values)
|
||||
seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()
|
||||
|
||||
return SummaryValuesCalculator(self.values).summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
|
||||
self.q_z(), seismic_interval, ballast_calculator)
|
||||
|
||||
def documentation_summary_values(self):
|
||||
seismic_anchors = self.subarray_summary()
|
||||
ballast_calculator = BallastCalculator(self.values)
|
||||
seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()
|
||||
|
||||
return SummaryValuesCalculator(self.values).documentation_summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
|
||||
self.q_z(), seismic_interval, ballast_calculator)
|
||||
|
||||
# used in the array visualization - is parsed into json and displayed using the fancy canvas
|
||||
def get_computed_csv_columns(self):
|
||||
return BallastCalculator(self.values).show_presented_link_trays(self.panels)
|
||||
|
||||
def compute_bom(self):
|
||||
required_seismic_anchors = self.subarray_summary()
|
||||
return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).compute_bom()
|
||||
|
||||
def documentation_bom(self):
|
||||
required_seismic_anchors = self.subarray_summary()
|
||||
return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).documentation_bom()
|
||||
|
||||
# used in the array summary page - is part of the fancy scrolling table of summing up each subarray
|
||||
def subarray_summary(self):
|
||||
summary_values_calculator = SummaryValuesCalculator(self.values)
|
||||
for subarray in self.subarrays:
|
||||
panels_for_subarray = extract_subarray(self.panels, subarray.subarray_number)
|
||||
weight, _ = summary_values_calculator.system_weight_and_pressure(panels_for_subarray)
|
||||
subarray.weight = weight
|
||||
|
||||
return self.subarrays
|
||||
|
||||
def q_z(self):
|
||||
if self._q_z is None:
|
||||
self._q_z = WindPressureCalculator(self.values).q_z(self.k_z())
|
||||
return self._q_z
|
||||
|
||||
def __c_p_matrix(self):
|
||||
if self._c_p_matrix is None:
|
||||
self._c_p_matrix = PressureCoefficientCalculator(self.values).c_p_matrix(self.L_B())
|
||||
return self._c_p_matrix
|
||||
|
||||
def __compute_seismic_anchors(self, panels):
|
||||
panels = copy.deepcopy(panels)
|
||||
seismic_calculator = SeismicCalculator(self.values, self.graph_repository)
|
||||
for subarray in self.subarrays:
|
||||
if subarray.required_seismic_anchors is None:
|
||||
subarray.required_seismic_anchors = 0
|
||||
panels = self.__seismic_anchors_for_subarray(panels, subarray, seismic_calculator)
|
||||
return panels
|
||||
|
||||
def __seismic_anchors_for_subarray(self, panels, subarray, seismic_calculator):
|
||||
# do first estimation to obtain upper bound
|
||||
required_seismic = seismic_calculator.required_force_seismic_anchors(subarray.subarray_number, panels)
|
||||
|
||||
test_value = required_seismic
|
||||
tried_acceptable_values = []
|
||||
|
||||
def assign_seismic_anchors(count):
|
||||
subarray.required_seismic_anchors = count
|
||||
seismic_calculator.assign_seismic_anchors(subarray, panels)
|
||||
self.__update_ballast(panels)
|
||||
|
||||
assigned = sum([panel.seismic_anchors for panel in panels if panel.seismic_anchors is not None])
|
||||
|
||||
return assigned
|
||||
|
||||
step = max(1, test_value // 2)
|
||||
|
||||
while True:
|
||||
seismic_anchors_assigned = assign_seismic_anchors(test_value)
|
||||
|
||||
provided_force = seismic_calculator.compute_provided_lateral_capacity(subarray.subarray_number, panels)
|
||||
required_seismic_force = seismic_calculator.required_force_seismic_demand(subarray.subarray_number, panels)
|
||||
|
||||
if seismic_anchors_assigned == 0:
|
||||
# anchors were not assigned propably because self.graph_repository.subarray_graph(subarray.subarray_number) is empty
|
||||
# which may be because of test construction
|
||||
return panels
|
||||
|
||||
if provided_force < required_seismic_force:
|
||||
test_value += step
|
||||
else:
|
||||
if (test_value in tried_acceptable_values) or (required_seismic_force == 0):
|
||||
return panels
|
||||
|
||||
tried_acceptable_values.append(test_value)
|
||||
test_value = max(0, test_value - step)
|
||||
|
||||
step = max(1, step // 2)
|
||||
|
||||
def __transform_coordinates(self):
|
||||
return CoordinatesCalculator(self.values).transform_coordinates(self.panels, self.subarrays, self.buildings)
|
||||
|
||||
def __compute_ballast(self):
|
||||
ballast_calculator = BallastCalculator(self.values)
|
||||
changed_panels = ballast_calculator.ballast_and_trays_matrix(self.__c_p_matrix(), self.q_z(), self.panels)
|
||||
self.panels = [panel.merge(changed_panels[idx]) for idx, panel in enumerate(self.panels)]
|
||||
|
||||
def __update_ballast(self, panels):
|
||||
BallastCalculator(self.values).update_ballast(self.__c_p_matrix(), self.q_z(), panels)
|
||||
104
helix/calculators/coordinates_calculator.py
Normal file
104
helix/calculators/coordinates_calculator.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from numpy import array, math, dot, vectorize
|
||||
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
from helix.models.coordinate import Coordinate
|
||||
|
||||
|
||||
class CoordinatesCalculator(object):
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
|
||||
def transform_coordinates(self, panels, subarrays, buildings):
|
||||
"""
|
||||
Scales, rotates, and translates the coordinates so that they're all
|
||||
in inches, and in positive unit space.
|
||||
Coordinates are rounded to whole values (used in drawing on the
|
||||
array_summary page
|
||||
|
||||
Parameters:
|
||||
panels (obj): List of panels
|
||||
subarrays (obj): List of subarrays
|
||||
buildings (obj): List of lists of building polygons
|
||||
|
||||
Returns:
|
||||
tupple
|
||||
|
||||
"""
|
||||
|
||||
rotate_all = vectorize(self.rotate)
|
||||
scale_all = vectorize(self.scale)
|
||||
round_all = vectorize(round)
|
||||
neg_translate_all = vectorize(self.neg_translate)
|
||||
origins = []
|
||||
|
||||
|
||||
first_subarray_rotation = None
|
||||
|
||||
for subarray in subarrays:
|
||||
begin, size = (subarray.start_row, subarray.size)
|
||||
extracted_panels = extract_subarray(panels, subarray.subarray_number)
|
||||
|
||||
raw_coordinates = [panel.coordinate for panel in extracted_panels]
|
||||
|
||||
if first_subarray_rotation is None:
|
||||
first_subarray_rotation = raw_coordinates[0].rotation
|
||||
|
||||
rotated_coordinates = rotate_all(raw_coordinates)
|
||||
scaled_coordinates = scale_all(rotated_coordinates)
|
||||
|
||||
origin = self.find_origin(scaled_coordinates)
|
||||
rounded_coordinates = round_all(scaled_coordinates - origin)
|
||||
|
||||
for idx, val in enumerate(rounded_coordinates):
|
||||
panels[begin + idx].coordinate = val
|
||||
origins.append(origin)
|
||||
|
||||
|
||||
prepared_buildings = self.prepare_buildings(buildings, first_subarray_rotation)
|
||||
rotated_buildings = list(map(lambda building: rotate_all(building), prepared_buildings ))
|
||||
scaled_buildings = list(map(lambda building: scale_all(building), rotated_buildings))
|
||||
|
||||
all_building_coordinates = [point for sublist in scaled_buildings for point in sublist]
|
||||
global_origin = self.find_origin(all_building_coordinates + origins)
|
||||
origins = array(origins) - global_origin
|
||||
|
||||
for idx, origin in enumerate(origins):
|
||||
subarrays[idx].origin = origin
|
||||
|
||||
translated_buildings = list(map(lambda building: neg_translate_all(building, global_origin), scaled_buildings))
|
||||
# rounded_buildings = list(map(lambda building: round_all(building), translated_buildings))
|
||||
|
||||
return panels, subarrays, translated_buildings
|
||||
|
||||
def rotate(self, coordinate):
|
||||
rotation = math.radians(coordinate.rotation)
|
||||
rotation_matrix = array([[math.cos(rotation), -math.sin(rotation)],
|
||||
[math.sin(rotation), math.cos(rotation)]])
|
||||
vector = (coordinate.x, coordinate.y)
|
||||
rotated_vector = dot(vector, rotation_matrix)
|
||||
return Coordinate(rotated_vector[0], rotated_vector[1])
|
||||
|
||||
def scale(self, coordinate):
|
||||
constants = self.values.module_system_constants()
|
||||
panel_x, panel_y = constants.panel_spacing
|
||||
return coordinate.scale(1. / panel_x, 1. / panel_y)
|
||||
|
||||
def neg_translate(self, coordinate, other):
|
||||
return coordinate.neg_translate(other)
|
||||
|
||||
def find_origin(self, coordinates):
|
||||
if coordinates == []:
|
||||
return Coordinate(0,0)
|
||||
min_x = min(list(map(lambda x: x.x, coordinates)))
|
||||
min_y = min(list(map(lambda x: x.y, coordinates)))
|
||||
return Coordinate(min_x, min_y)
|
||||
|
||||
def prepare_buildings(self, buildings, rotation):
|
||||
return list(map(lambda building: self.prepare_single_building(building, rotation), buildings))
|
||||
|
||||
def prepare_single_building(self, building_array, rotation):
|
||||
return list(map(lambda point: Coordinate(point[0],point[1],rotation), building_array))
|
||||
|
||||
|
||||
|
||||
133
helix/calculators/ebom_calculator.py
Normal file
133
helix/calculators/ebom_calculator.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from math import ceil
|
||||
|
||||
from helix.calculators.bom_helper import add_parts_to_list
|
||||
from helix.constants import ebom_parts
|
||||
from helix.constants.ebom_parts import *
|
||||
from helix.constants.parts import wire_clip_large, cable_support, cable_support_lid, channel_nut, sunshade
|
||||
from helix.constants.system_type import SystemType
|
||||
|
||||
|
||||
class EbomCalculator(object):
|
||||
def __init__(self, user_values, row_count, column_count, modules_count = None):
|
||||
self.values = user_values
|
||||
self.row_count = row_count
|
||||
self.column_count = column_count
|
||||
self.modules_count = modules_count
|
||||
|
||||
def resolve_power_monitor_type(self):
|
||||
module_type = self.values.module_type()
|
||||
|
||||
thresholds = {
|
||||
ModuleType.Cell96: 306,
|
||||
ModuleType.Cell128: 230,
|
||||
ModuleType.PSeries: 286
|
||||
}
|
||||
|
||||
if (not self.modules_count) or self.modules_count >= thresholds[module_type]:
|
||||
return monitor_controller_480_v
|
||||
else:
|
||||
return monitor_controller_240_v
|
||||
|
||||
def compute_ebom(self):
|
||||
part_list = {}
|
||||
|
||||
power_stations = self.values.power_stations()
|
||||
standalone_inverters = self.values.standalone_inverters()
|
||||
monitors = self.values.power_monitors()
|
||||
module_type = self.values.module_type()
|
||||
system_type = self.values.system_type()
|
||||
inverter_count = 0
|
||||
total_ac_run_length = 0
|
||||
panel_board_counts = [0, 0]
|
||||
proper_monitor_controller = self.resolve_power_monitor_type()
|
||||
|
||||
for power_station in power_stations:
|
||||
power_station_count = power_station['power_station_quantity']
|
||||
total_ac_run_length += power_station['ac_run_length']
|
||||
inverter_quantity = power_station['inverter_quantity'] + self.get_standalone_inverters(power_station)
|
||||
if inverter_quantity <= 2:
|
||||
panel_board_counts[0] += power_station_count
|
||||
else:
|
||||
panel_board_counts[1] += power_station_count
|
||||
|
||||
if self.power_station_has_monitor(power_station, monitors):
|
||||
panel_board_parts_to_use = panel_board_parts_with_monitor(inverter_quantity, proper_monitor_controller)
|
||||
else:
|
||||
panel_board_parts_to_use = panel_board_parts(inverter_quantity, with_aux=False)
|
||||
add_parts_to_list(part_list, panel_board_parts_to_use, power_station_count)
|
||||
add_parts_to_list(part_list, shared_panel_board_parts(module_type, system_type), power_station_count)
|
||||
add_parts_to_list(part_list, {channel_nut: 4}, power_station_count)
|
||||
|
||||
for inverter in power_station['inverters']:
|
||||
inverter_count += power_station_count
|
||||
self.add_parts_for_inverter(part_list, inverter, power_station_count)
|
||||
add_parts_to_list(part_list, inverter_parts(inverter, module_type), power_station_count)
|
||||
|
||||
for inverter in standalone_inverters:
|
||||
inverter_count += 1
|
||||
total_ac_run_length += inverter['ac_run_length']
|
||||
self.add_parts_for_inverter(part_list, inverter)
|
||||
add_parts_to_list(part_list, standalone_inverter_parts(inverter, system_type, module_type), 1)
|
||||
add_parts_to_list(part_list, inverter_parts(inverter, module_type), 1)
|
||||
if inverter['attachment_point'][1]:
|
||||
add_parts_to_list(part_list, standalone_inverter_attached_to_panel_board_parts, 1)
|
||||
|
||||
for monitor in monitors:
|
||||
if monitor['power_source'][0] == 'Switch Gear/External':
|
||||
add_parts_to_list(part_list, {proper_monitor_controller: 1}, 1)
|
||||
|
||||
add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count)
|
||||
|
||||
add_parts_to_list(part_list, {stump: 1}, ceil(total_ac_run_length / 4.0))
|
||||
cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters))
|
||||
add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports)
|
||||
add_parts_to_list(part_list, {rear_skirt: -1}, ceil(cable_supports*.38))
|
||||
|
||||
dependent_part_list = {}
|
||||
for part, quantity in part_list.items():
|
||||
dependent_parts = ebom_parts.dependent_parts(module_type, system_type).get(part)
|
||||
if dependent_parts:
|
||||
add_parts_to_list(dependent_part_list, dependent_parts, quantity)
|
||||
|
||||
add_parts_to_list(part_list, dependent_part_list)
|
||||
|
||||
return part_list
|
||||
|
||||
def add_parts_for_inverter(self, part_list, inverter, multiplier=1):
|
||||
strings_per_inverter = inverter_strings_parts.get(inverter['strings_per_inverter'], {})
|
||||
add_parts_to_list(part_list, inverter_model_parts[inverter['model']], multiplier)
|
||||
add_parts_to_list(part_list, strings_per_inverter, multiplier)
|
||||
if inverter['sunshade']:
|
||||
add_parts_to_list(part_list, {sunshade: 1, sunshade_bolt: 2, sunshade_washer: 2}, multiplier)
|
||||
if inverter['dc_switch']:
|
||||
add_parts_to_list(part_list, dc_switch_parts, multiplier)
|
||||
|
||||
def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count):
|
||||
if sum(panel_board_counts) == 0:
|
||||
return 0
|
||||
|
||||
if self.values.system_type() == SystemType.dualTilt:
|
||||
dimension1 = self.column_count
|
||||
dimension2 = self.row_count
|
||||
else:
|
||||
dimension1 = self.row_count
|
||||
dimension2 = self.column_count
|
||||
|
||||
result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts)
|
||||
result *= dimension1 * max(dimension1 / dimension2, 1)
|
||||
result += dimension1
|
||||
return ceil(result)
|
||||
|
||||
def get_standalone_inverters(self, power_station):
|
||||
standalone_inverters = self.values.standalone_inverters()
|
||||
count = 0
|
||||
for inverter in standalone_inverters:
|
||||
if inverter['attachment_point'][1] == power_station['power_station_id']:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def power_station_has_monitor(self, power_station, monitors):
|
||||
for monitor in monitors:
|
||||
if monitor['power_source'][1] == power_station['power_station_id']:
|
||||
return True
|
||||
return False
|
||||
148
helix/calculators/mechanical_bom_calculator.py
Normal file
148
helix/calculators/mechanical_bom_calculator.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from math import ceil, floor
|
||||
|
||||
from helix.calculators.bom_helper import add_parts_to_list, apply_fudge_factors, \
|
||||
get_panel_type_counts
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
from helix.constants.module_type import ModuleType
|
||||
from helix.constants.panel_type import PanelType
|
||||
from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1, leading_tray
|
||||
from helix.constants.system_type import SystemType
|
||||
|
||||
|
||||
class MechanicalBomCalculator(object):
|
||||
def __init__(self, values, panels, subarrays):
|
||||
self.values = values
|
||||
self.panels = panels
|
||||
self.subarrays = subarrays
|
||||
|
||||
def mechanical_bom(self):
|
||||
module_type = self.values.module_type()
|
||||
system_type = self.values.system_type()
|
||||
system_parts = system_type.parts(module_type)
|
||||
combined_parts_list = {}
|
||||
|
||||
for subarray in self.subarrays:
|
||||
panels = extract_subarray(self.panels, subarray.subarray_number)
|
||||
|
||||
ballast_count = sum(panel.ballast for panel in panels)
|
||||
cross_count = sum(panel.cross_tray for panel in panels)
|
||||
assigned_seismic_anchors_count = sum(panel.seismic_anchors for panel in panels)
|
||||
required_seismic_anchors_count = subarray.required_seismic_anchors
|
||||
seismic_anchors_count = max(assigned_seismic_anchors_count, required_seismic_anchors_count)
|
||||
required_wind_anchors_count = sum(panel.wind_anchors for panel in panels)
|
||||
anchor_count = required_wind_anchors_count + seismic_anchors_count
|
||||
panel_type_counts = get_panel_type_counts(panels)
|
||||
|
||||
subarray_parts_list = {}
|
||||
|
||||
for index, panel_type_parts in enumerate(system_parts.parts_per_panel_type()):
|
||||
add_parts_to_list(subarray_parts_list, panel_type_parts, panel_type_counts[PanelType.from_index(index)])
|
||||
|
||||
add_parts_to_list(subarray_parts_list, self.values.anchor_type().parts().parts, anchor_count)
|
||||
|
||||
link_count = self.link_count(panel_type_counts, panels, subarray)
|
||||
|
||||
add_parts_to_list(subarray_parts_list, {link_tray: 1}, link_count)
|
||||
cross_tray_parts = cross_tray if self.values.module_type() == ModuleType.Cell96 else cross_tray_1_1
|
||||
add_parts_to_list(subarray_parts_list, {cross_tray_parts: 1}, cross_count)
|
||||
add_parts_to_list(subarray_parts_list, {ballast: 1}, ballast_count)
|
||||
|
||||
add_parts_to_list(subarray_parts_list, system_parts.row_parts(module_type), subarray.row_count)
|
||||
add_parts_to_list(subarray_parts_list, system_parts.column_parts(module_type), subarray.column_count)
|
||||
|
||||
add_parts_to_list(subarray_parts_list, system_parts.sub_array_parts, 1)
|
||||
|
||||
apply_fudge_factors(subarray_parts_list, system_parts.fudge_factors(not subarray.row_counted_geometrically))
|
||||
|
||||
dependent_parts_list = {}
|
||||
for part, quantity in subarray_parts_list.items():
|
||||
dependent_parts = system_parts.dependent_parts(module_type).get(part)
|
||||
if dependent_parts:
|
||||
add_parts_to_list(dependent_parts_list, dependent_parts, quantity)
|
||||
|
||||
subarray_parts_list.update(dependent_parts_list)
|
||||
add_parts_to_list(combined_parts_list, subarray_parts_list)
|
||||
|
||||
return combined_parts_list
|
||||
|
||||
def link_count(self, panel_type_counts, panels, subarray):
|
||||
if self.values.system_type() == SystemType.dualTilt:
|
||||
|
||||
# check if info about position of panels is available
|
||||
coordinates_available = all(p.coordinate for p in panels) \
|
||||
and not all(p.coordinate.x == 0 and p.coordinate.y == 0 for p in panels)
|
||||
|
||||
if coordinates_available:
|
||||
# initially every C, NS, EW panels has 2 link trays attached
|
||||
panel_types = [PanelType.Corner, PanelType.NorthSouth, PanelType.EastWest]
|
||||
layout = dict([((p.coordinate.x, p.coordinate.y), 2) for p in panels if p.panel_type in panel_types])
|
||||
|
||||
row_count = ceil(subarray.row_count)
|
||||
column_count = ceil(subarray.column_count)
|
||||
|
||||
# reduce number of link trays between every two vertically adjoining panels
|
||||
for y in range(row_count):
|
||||
for x in range(column_count):
|
||||
|
||||
if (x, y) not in layout:
|
||||
continue
|
||||
|
||||
if (x, y + 1) in layout:
|
||||
layout[(x, y + 1)] = 1
|
||||
|
||||
# count link trays located on perimeter
|
||||
link_count = sum([layout[p] for p in layout])
|
||||
|
||||
# subtract places reserved for leading trays
|
||||
link_count -= subarray.row_count + 1
|
||||
|
||||
# add link trays for panels of type Middle
|
||||
link_count += sum([1 for panel in panels if panel.link_tray != 0 and panel.panel_type == PanelType.Middle])
|
||||
|
||||
return max(link_count, 0)
|
||||
else:
|
||||
total_possible_link_trays = len(panels) + subarray.column_count
|
||||
link_count = total_possible_link_trays
|
||||
|
||||
for panel in panels:
|
||||
if panel.link_tray == 0 and panel.panel_type == PanelType.Middle:
|
||||
link_count -= 1
|
||||
|
||||
link_count -= floor(subarray.row_count)
|
||||
|
||||
return link_count
|
||||
else:
|
||||
return sum([self.compute_link_count_single_tilt(panel_type, panel_type_counts, panels) for panel_type in PanelType.all()])
|
||||
|
||||
def compute_link_count_single_tilt(self, panel_type, panel_type_counts, panels):
|
||||
if panel_type == PanelType.Corner:
|
||||
return 0
|
||||
elif panel_type == PanelType.NorthSouth:
|
||||
return 0
|
||||
elif panel_type == PanelType.EastWest:
|
||||
return panel_type_counts[panel_type] * 2
|
||||
elif panel_type == PanelType.Middle:
|
||||
return self.get_panel_type_middle_link_trays_single_tilt(panels)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_panel_type_middle_link_trays_single_tilt(self, panels):
|
||||
wind_zones = self.values.system_type().system_constants().wind_zones
|
||||
|
||||
middle_panels_per_wind_zone = [0 for _ in wind_zones]
|
||||
middle_link_trays_per_wind_zone = [0 for _ in wind_zones]
|
||||
|
||||
for panel in panels:
|
||||
if panel.panel_type != PanelType.Middle:
|
||||
continue
|
||||
|
||||
wind_zone = panel.wind_zone
|
||||
middle_panels_per_wind_zone[wind_zone] += 1
|
||||
middle_link_trays_per_wind_zone[wind_zone] += panel.link_tray # use calculated number of link trays to see if it is non-zero
|
||||
|
||||
total_link_trays_required = 0
|
||||
for wind_zone, panel_count in enumerate(middle_panels_per_wind_zone):
|
||||
if middle_link_trays_per_wind_zone[wind_zone] > 0: # if any middle panels in this wind zone need link trays
|
||||
total_link_trays_required += ceil(panel_count * 1.05)
|
||||
return total_link_trays_required
|
||||
|
||||
93
helix/calculators/pressure_coefficient_calculator.py
Normal file
93
helix/calculators/pressure_coefficient_calculator.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from math import sqrt, log
|
||||
import math
|
||||
|
||||
from helix.constants.global_constants import parapet_coefficients, parapet_factor_max
|
||||
from numpy import array
|
||||
from numpy.ma import maximum
|
||||
|
||||
from helix.constants.panel_type import PanelType
|
||||
|
||||
|
||||
class PressureCoefficientCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
self.system_constants = self.values.system_type().system_constants()
|
||||
self.module_constants = self.values.module_system_constants()
|
||||
|
||||
def c_p_matrix(self, L_B):
|
||||
parapet = self.parapet_factor()
|
||||
return self.compute_c_p_matrix(L_B, parapet)
|
||||
|
||||
def L_B(self):
|
||||
""" Building scaling factor """
|
||||
height = max(15, self.values.building_height())
|
||||
|
||||
length = self.values.building_length()
|
||||
width = self.values.building_width()
|
||||
|
||||
longest_side = max(width, length)
|
||||
|
||||
return min(height, 0.4 * sqrt(height * max(1, longest_side)))
|
||||
|
||||
def minimum_array_size(self, L_B):
|
||||
panel_area = self.module_constants.panel_area
|
||||
module_count = self.system_constants.module_count
|
||||
minimum_array_size = []
|
||||
for minimum_A_n in self.minimum_A_n(L_B):
|
||||
if minimum_A_n is None:
|
||||
value = 6
|
||||
else:
|
||||
value = int(math.ceil((minimum_A_n * L_B ** 2) / (panel_area * 1000) / module_count))
|
||||
minimum_array_size.append(value)
|
||||
return minimum_array_size
|
||||
|
||||
# Normalized area, scales the tributary area by the building scaling factor and panel area
|
||||
def A_n(self, L_B):
|
||||
return self.module_constants.tributary_area * (self.module_constants.panel_area * 1000. / L_B ** 2)
|
||||
|
||||
def compute_c_p_matrix(self, L_B, parapet):
|
||||
A_n = self.A_n(L_B)
|
||||
wind_zones = self.system_constants.wind_zones
|
||||
return array([self.c_p_row(A_n, wind_zone, parapet) for wind_zone in wind_zones])
|
||||
|
||||
def c_p_row(self, A_n_row, wind_zone, parapet_factor):
|
||||
c_p_lower_bound = self.module_constants.c_p_lower_bound()
|
||||
|
||||
if wind_zone == self.system_constants.wind_zones[-1]:
|
||||
return c_p_lower_bound
|
||||
computed_row = []
|
||||
for index, A_n in enumerate(A_n_row):
|
||||
edge_factor = self.module_constants.edge_factor(wind_zone, PanelType.from_index(index))
|
||||
computed_row.append(self.c_p(A_n, wind_zone, parapet_factor, edge_factor))
|
||||
|
||||
return maximum(array(computed_row), c_p_lower_bound)
|
||||
|
||||
def c_p(self, A_n, wind_zone, parapet_factor, edge_factor):
|
||||
c0, c1 = self.module_constants.c_p_constants(A_n, wind_zone)
|
||||
return max(0., c0 * log(A_n) + c1) * parapet_factor * edge_factor
|
||||
|
||||
def parapet_factor(self):
|
||||
height = max(15, self.values.building_height())
|
||||
parapet_height = max(0, self.values.building_parapet_height())
|
||||
|
||||
factor = parapet_height / height
|
||||
c0, c1 = parapet_coefficients
|
||||
return min(parapet_factor_max, c0 + c1 * factor)
|
||||
|
||||
def ideal_subarray_average_uplift_c_p(self, L_B):
|
||||
c_p_matrix = self.compute_c_p_matrix(L_B, 1)
|
||||
return [self.module_constants.weighted_average_c_p(c, n, e, m) for c, n, e, m in c_p_matrix]
|
||||
|
||||
def minimum_A_n(self, L_B):
|
||||
uplift_c_p = self.ideal_subarray_average_uplift_c_p(L_B)
|
||||
minimum_A_n = []
|
||||
for idx, wind_zone in enumerate(self.system_constants.wind_zones):
|
||||
wind_zone_uplift = uplift_c_p[idx]
|
||||
coefficients = self.module_constants.minimum_a_n_coefficients(wind_zone_uplift, wind_zone)
|
||||
if not coefficients:
|
||||
minimum_A_n.append(None)
|
||||
continue
|
||||
|
||||
value = math.exp((coefficients[0] - wind_zone_uplift) / coefficients[1])
|
||||
minimum_A_n.append(value)
|
||||
return minimum_A_n
|
||||
184
helix/calculators/seismic_calculator.py
Normal file
184
helix/calculators/seismic_calculator.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from math import ceil, floor
|
||||
|
||||
from helix.calculators.subarray_graph import SubarrayGraph
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
|
||||
from helix.constants.global_constants import minimum_racking_capacity
|
||||
from helix.constants.panel_type import PanelType
|
||||
from helix.models.subarray import Subarray
|
||||
|
||||
|
||||
class SeismicCalculator(object):
|
||||
def __init__(self, values, graph_repository):
|
||||
self.values = values
|
||||
self.system_type = values.system_type()
|
||||
self.system_constants = values.module_system_constants()
|
||||
self.anchor_type = values.anchor_type()
|
||||
self.graph_repository = graph_repository
|
||||
|
||||
def assign_seismic_anchors(self, subarray, panels):
|
||||
panel_data_for_subarray = extract_subarray(panels, subarray.subarray_number)
|
||||
self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
|
||||
return panels
|
||||
|
||||
def assign_anchors_to_subarray(self, panels, subarray):
|
||||
sds = self.values.spectral_response()
|
||||
wind_anchors = sum([panel.wind_anchors for panel in panels])
|
||||
for panel in panels:
|
||||
if panel.seismic_anchors is None:
|
||||
panel.seismic_anchors = 0
|
||||
required_anchors = subarray.required_seismic_anchors
|
||||
if required_anchors == 0 and (wind_anchors == 0 or sds < 1):
|
||||
return panels
|
||||
|
||||
graph = self.graph_repository.subarray_graph(subarray.subarray_number)
|
||||
if len(graph.nodes) == 0:
|
||||
return panels
|
||||
|
||||
more_anchors_needed = True
|
||||
perimeter_covered = sds < 1.0
|
||||
anchor_threshold = 0
|
||||
while more_anchors_needed:
|
||||
rung = graph.pop_rung()
|
||||
interval = int(self.seismic_anchor_interval())
|
||||
nodes_since_last_anchor = interval
|
||||
if len(rung) == 0:
|
||||
graph.reset()
|
||||
anchor_threshold += 1
|
||||
continue
|
||||
while more_anchors_needed and interval >= 0:
|
||||
for node in rung:
|
||||
nodes_since_last_anchor += 1
|
||||
if node.wind_anchor + node.seismic_anchor > anchor_threshold:
|
||||
nodes_since_last_anchor = 0
|
||||
|
||||
if nodes_since_last_anchor > interval:
|
||||
node.assign_seismic_anchor()
|
||||
required_anchors -= 1
|
||||
nodes_since_last_anchor = 0
|
||||
|
||||
more_anchors_needed = (not perimeter_covered) or required_anchors > 0
|
||||
if not more_anchors_needed:
|
||||
break
|
||||
|
||||
perimeter_covered = True
|
||||
if interval <= 1:
|
||||
interval -= 1
|
||||
else:
|
||||
interval /= 2
|
||||
|
||||
for idx, node in enumerate(graph.nodes):
|
||||
panels[idx].seismic_anchors = node.seismic_anchor
|
||||
|
||||
return panels
|
||||
|
||||
def compute_provided_lateral_capacity(self, subarray_number, panels):
|
||||
subarray_panels = extract_subarray(panels, subarray_number)
|
||||
|
||||
anchors = {PanelType.Corner: 0, PanelType.NorthSouth: 0, PanelType.EastWest: 0, PanelType.Middle: 0}
|
||||
|
||||
for panel in subarray_panels:
|
||||
if panel.seismic_anchors is not None:
|
||||
anchors[panel.panel_type] += panel.seismic_anchors
|
||||
|
||||
return self.anchors_shear_capacity(anchors[PanelType.Corner], anchors[PanelType.NorthSouth],
|
||||
anchors[PanelType.EastWest], anchors[PanelType.Middle])
|
||||
|
||||
def seismic_anchors_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
|
||||
seismic_anchors, corner_anchors,
|
||||
north_south_anchors, east_west_anchors, middle_anchors, shear_capacity):
|
||||
demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response, friction_coefficient,
|
||||
seismic_anchors, corner_anchors, north_south_anchors,
|
||||
east_west_anchors, middle_anchors)
|
||||
return ceil(demand / shear_capacity)
|
||||
|
||||
def anchors_shear_capacity(self, corner_anchors, north_south_anchors, east_west_anchors, middle_anchors):
|
||||
|
||||
anchor_shear_capacity = self.anchor_type.shear_capacity()
|
||||
panel_racking_capacity = self.system_constants.racking_capacity
|
||||
|
||||
corner_capacity = corner_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Corner))
|
||||
north_south_capacity = north_south_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.NorthSouth))
|
||||
east_west_capacity = east_west_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.EastWest))
|
||||
middle_capacity = middle_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Middle))
|
||||
|
||||
return corner_capacity + north_south_capacity + east_west_capacity + middle_capacity
|
||||
|
||||
def seismic_demand_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
|
||||
seismic_anchors, corner_anchors,
|
||||
north_south_anchors, east_west_anchors, middle_anchors):
|
||||
if (corner_anchors + north_south_anchors + east_west_anchors + middle_anchors == 0) and seismic_anchors == 0:
|
||||
return 0
|
||||
|
||||
existing_shear_resistance = self.anchors_shear_capacity(corner_anchors, north_south_anchors, east_west_anchors,
|
||||
middle_anchors)
|
||||
|
||||
shear_force = 0.7 * F_p * subarray_weight - (
|
||||
(0.6 - 0.14 * spectral_response) * 0.7 * friction_coefficient * subarray_weight) - existing_shear_resistance
|
||||
|
||||
return max(shear_force, 0)
|
||||
|
||||
def required_force_seismic_anchors(self, subarray_number, panels):
|
||||
|
||||
demand = self.required_force_seismic_demand(subarray_number, panels)
|
||||
|
||||
system_shear_capacity = min(self.anchor_type.shear_capacity(),
|
||||
minimum_racking_capacity)
|
||||
|
||||
return ceil(demand / system_shear_capacity)
|
||||
|
||||
def required_force_seismic_demand(self, subarray_number, panels):
|
||||
subarray_panels = extract_subarray(panels, subarray_number)
|
||||
|
||||
importance_factor = self.values.importance_factor()
|
||||
spectral_response = self.values.spectral_response()
|
||||
F_p = 1.2 * spectral_response / (1.5 / importance_factor)
|
||||
|
||||
# number of wind anchors by panel type
|
||||
anchors = {PanelType.Corner: 0,
|
||||
PanelType.NorthSouth: 0,
|
||||
PanelType.EastWest: 0,
|
||||
PanelType.Middle: 0}
|
||||
|
||||
# total weight
|
||||
subarray_weight = 0
|
||||
|
||||
# total number of seismic anchors
|
||||
seismic_anchors = 0
|
||||
|
||||
for panel in subarray_panels:
|
||||
if panel.seismic_anchors is not None:
|
||||
seismic_anchors += panel.seismic_anchors
|
||||
|
||||
# it could be calculated before the loop to avoid redundant calculations
|
||||
effective_area = self.system_constants.surface_area / self.system_constants.ground_coverage_ratio
|
||||
weight = panel.pressure * effective_area
|
||||
|
||||
anchors[panel.panel_type] += panel.wind_anchors
|
||||
subarray_weight += weight
|
||||
|
||||
force_required_demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response,
|
||||
self.system_constants.friction_coefficient,
|
||||
seismic_anchors,
|
||||
anchors[PanelType.Corner],
|
||||
anchors[PanelType.NorthSouth],
|
||||
anchors[PanelType.EastWest],
|
||||
anchors[PanelType.Middle])
|
||||
return force_required_demand
|
||||
|
||||
def required_geometric_seismic_anchors(self, subarray_number, panels):
|
||||
if panels[0].coordinate is None or self.values.spectral_response() < 1:
|
||||
return 0
|
||||
panel_data_for_subarray = extract_subarray(panels, subarray_number)
|
||||
subarray = Subarray(required_seismic_anchors=0, subarray_number=subarray_number)
|
||||
anchors_for_subarray = self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
|
||||
return sum(panel.seismic_anchors for panel in anchors_for_subarray if panel.seismic_anchors)
|
||||
|
||||
def seismic_anchor_interval(self):
|
||||
sds = self.values.spectral_response()
|
||||
importance_factor = self.values.importance_factor()
|
||||
interval_constant, interval_multiplier = self.system_constants.seismic_anchor_interval_constants
|
||||
denom = (22.96 * importance_factor * sds - interval_constant + interval_multiplier * sds)
|
||||
if denom <= 0:
|
||||
return 15
|
||||
return floor(minimum_racking_capacity / denom)
|
||||
246
helix/calculators/subarray_graph.py
Normal file
246
helix/calculators/subarray_graph.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import copy
|
||||
from enum import Enum
|
||||
|
||||
from helix.constants.system_type import SystemType
|
||||
from helix.models.coordinate import Coordinate
|
||||
from helix.models.dxf.graph_node_store import GraphNodeStore
|
||||
|
||||
|
||||
class Direction(Enum):
|
||||
North = Coordinate(0, -1)
|
||||
South = Coordinate(0, 1)
|
||||
East = Coordinate(-1, 0)
|
||||
West = Coordinate(1, 0)
|
||||
|
||||
def opposite_direction(self):
|
||||
if self == Direction.North:
|
||||
return Direction.South
|
||||
elif self == Direction.South:
|
||||
return Direction.North
|
||||
elif self == Direction.East:
|
||||
return Direction.West
|
||||
else:
|
||||
return Direction.East
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
return [
|
||||
cls.North,
|
||||
cls.West,
|
||||
cls.South,
|
||||
cls.East,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def all_coordinates(cls):
|
||||
return [d.value for d in cls.all()]
|
||||
|
||||
def directions_to_try(self):
|
||||
return {
|
||||
Direction.North: [Direction.East, Direction.North, Direction.West, Direction.South],
|
||||
Direction.East: [Direction.South, Direction.East, Direction.North, Direction.West],
|
||||
Direction.South: [Direction.West, Direction.South, Direction.East, Direction.North],
|
||||
Direction.West: [Direction.North, Direction.West, Direction.South, Direction.East],
|
||||
}[self]
|
||||
|
||||
|
||||
class SubarrayGraphNode(object):
|
||||
"""
|
||||
A node that contains the panel, it's neighbors to the four cardinal
|
||||
directions (N, S, E, W), and a
|
||||
count of the seismic anchors attached.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, panel):
|
||||
self.neighbors = {}
|
||||
self.panel = panel
|
||||
self.seismic_anchor = 0
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.panel.coordinate
|
||||
|
||||
@property
|
||||
def coordinate(self): # GraphNodeStore expects a coordinate property
|
||||
return self.panel.coordinate
|
||||
|
||||
@property
|
||||
def wind_anchor(self):
|
||||
return self.panel.wind_anchors
|
||||
|
||||
def add_neighbor(self, neighbor, direction):
|
||||
self.neighbors[direction] = neighbor
|
||||
neighbor.neighbors[direction.opposite_direction()] = self
|
||||
|
||||
def remove_neighbor_references(self):
|
||||
for direction, neighbor in self.neighbors.items():
|
||||
neighbor.neighbors.pop(direction.opposite_direction(), None)
|
||||
|
||||
def assign_seismic_anchor(self):
|
||||
self.seismic_anchor += 1
|
||||
|
||||
def step(self, direction):
|
||||
return self.neighbors.get(direction)
|
||||
|
||||
def __repr__(self):
|
||||
val = "(" + str(self.location) + ":\n"
|
||||
for direction, neighbor in self.neighbors.items():
|
||||
val += "\t%s: %s\n" % (str(direction), str(neighbor.location))
|
||||
return val + ")"
|
||||
|
||||
def __eq__(self, other):
|
||||
if self is other:
|
||||
return True
|
||||
us = self.panel.coordinate
|
||||
them = other.panel.coordinate
|
||||
# Quick and dirty, inline equality makes for a slightly faster equality check
|
||||
# Also don't bother with floating point equality - it only slows us down. :(
|
||||
return us.x == them.x and us.y == them.y and us.rotation == them.rotation
|
||||
|
||||
def __hash__(self):
|
||||
return self.panel.coordinate.__hash__()
|
||||
|
||||
|
||||
class SubarrayGraph(object):
|
||||
def __init__(self, panels, system_type):
|
||||
self.nodes = []
|
||||
self.system_type = system_type
|
||||
self.node_store = GraphNodeStore()
|
||||
|
||||
self.nodes = []
|
||||
if len(panels) == 0 or panels[0].coordinate == panels[-1].coordinate:
|
||||
self.nodes = []
|
||||
else:
|
||||
for panel in panels:
|
||||
node = SubarrayGraphNode(panel)
|
||||
self.nodes.append(node)
|
||||
self.node_store.add_node(node)
|
||||
|
||||
self.graph = list(self.nodes)
|
||||
self.rungs = []
|
||||
self.current_rung = 0
|
||||
self.assemble_graph()
|
||||
|
||||
def __deepcopy__(self, _):
|
||||
panels = [node.panel for node in self.nodes]
|
||||
graph = SubarrayGraph(panels, self.system_type)
|
||||
return graph
|
||||
|
||||
def assemble_graph(self):
|
||||
for node in self.nodes:
|
||||
if len(node.neighbors) == 4:
|
||||
continue
|
||||
for direction in Direction.all():
|
||||
coordinate = node.location + direction.value
|
||||
neighbor = self.node_store.find_coordinate(coordinate)
|
||||
if neighbor:
|
||||
node.add_neighbor(neighbor, direction.opposite_direction())
|
||||
if len(node.neighbors) == 4:
|
||||
break
|
||||
|
||||
def reset(self):
|
||||
self.graph = list(self.nodes)
|
||||
self.current_rung = 0
|
||||
|
||||
def find_disconnected_subgraphs(self):
|
||||
graph = list(self.graph)
|
||||
subgraphs = []
|
||||
while len(graph) > 0:
|
||||
node = self.lower_left_node(graph)
|
||||
subgraph = self.add_all_neighbors(node)
|
||||
for subgraph_node in subgraph:
|
||||
try:
|
||||
graph.remove(subgraph_node)
|
||||
except:
|
||||
continue
|
||||
subgraphs.append(subgraph)
|
||||
return subgraphs
|
||||
|
||||
def find_node(self, coordinate):
|
||||
if coordinate.x < 0 or coordinate.y < 0:
|
||||
return None
|
||||
for node in self.graph:
|
||||
if node.location == coordinate:
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def add_all_neighbors(node):
|
||||
seen = {node}
|
||||
visited = set()
|
||||
visited_list = [] # apparently, order matters!
|
||||
while len(seen) > 0:
|
||||
node = seen.pop()
|
||||
if node in visited:
|
||||
continue
|
||||
visited.add(node)
|
||||
visited_list.append(node)
|
||||
for neighbor in node.neighbors.values():
|
||||
if neighbor not in visited and neighbor not in seen:
|
||||
seen.add(neighbor)
|
||||
return visited_list
|
||||
|
||||
@staticmethod
|
||||
def lower_left_node(graph):
|
||||
lower_left_node = None
|
||||
lower_left_location = Coordinate(float('inf'), float('inf'))
|
||||
for node in graph:
|
||||
node_location = node.location
|
||||
if node_location.x <= lower_left_location.x and node_location.y <= lower_left_location.y:
|
||||
lower_left_node = node
|
||||
lower_left_location = lower_left_node.location
|
||||
return lower_left_node
|
||||
|
||||
def pop_rung(self):
|
||||
if self.current_rung < len(self.rungs):
|
||||
rung = self.rungs[self.current_rung]
|
||||
self.current_rung += 1
|
||||
return rung
|
||||
rung = []
|
||||
|
||||
def assemble_rung_callback(node, next_direction, previous_direction):
|
||||
rung.append(node)
|
||||
if self.system_type == SystemType.dualTilt:
|
||||
east_west = [Direction.East, Direction.West]
|
||||
if next_direction in east_west or previous_direction in east_west:
|
||||
rung.append(node)
|
||||
|
||||
subgraphs = self.find_disconnected_subgraphs()
|
||||
for subgraph in subgraphs:
|
||||
try:
|
||||
start_node = self.lower_left_node(subgraph)
|
||||
except:
|
||||
break
|
||||
self.walk_graph_perimeter(start_node, assemble_rung_callback)
|
||||
|
||||
for node in rung:
|
||||
node.remove_neighbor_references()
|
||||
try:
|
||||
self.graph.remove(node)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.rungs.append(rung)
|
||||
self.current_rung += 1
|
||||
return rung
|
||||
|
||||
def walk_graph_perimeter(self, start_node, fn, repeat_steps=True):
|
||||
total_nodes = len(self.nodes)
|
||||
steps = 0
|
||||
node = start_node
|
||||
direction = Direction.East
|
||||
while True:
|
||||
next_node = None
|
||||
directions_to_try = list(direction.directions_to_try())
|
||||
last_direction = direction
|
||||
while next_node is None and len(directions_to_try) > 0:
|
||||
direction = directions_to_try.pop(0)
|
||||
next_node = node.step(direction)
|
||||
if next_node is None:
|
||||
break
|
||||
|
||||
fn(node, direction, last_direction)
|
||||
node = next_node
|
||||
steps += 1
|
||||
if node == start_node or (steps > total_nodes and repeat_steps):
|
||||
break
|
||||
19
helix/calculators/subarray_helper.py
Normal file
19
helix/calculators/subarray_helper.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from helix.models.subarray import Subarray
|
||||
|
||||
|
||||
def get_subarray_sizes_and_rows(panels):
|
||||
subarrays = []
|
||||
last_subarray = None
|
||||
for index, panel in enumerate(panels):
|
||||
if last_subarray != panel.subarray:
|
||||
subarray = Subarray(subarray_number=panel.subarray, start_row=index, size=0)
|
||||
|
||||
subarrays.append(subarray)
|
||||
last_subarray = panel.subarray
|
||||
subarrays[-1].size += 1
|
||||
|
||||
return subarrays
|
||||
|
||||
|
||||
def extract_subarray(panels, subarray_number):
|
||||
return [panel for panel in panels if panel.subarray == subarray_number]
|
||||
85
helix/calculators/summary_values_calculator.py
Normal file
85
helix/calculators/summary_values_calculator.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from collections import namedtuple
|
||||
|
||||
SummaryValues = namedtuple('SummaryValues', ['total_weight', 'max_psf', 'avg_psf', 'anchors', 'ballast', 'max_weight', 'ballast_weight'])
|
||||
|
||||
|
||||
class SummaryValuesCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
self.constants = self.values.module_system_constants()
|
||||
|
||||
def summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator):
|
||||
summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator)
|
||||
|
||||
return [
|
||||
{'label': 'Total System Weight (lbs)', 'value': summary_values.total_weight},
|
||||
{'label': 'Max PSF', 'value': summary_values.max_psf},
|
||||
{'label': 'Avg PSF', 'value': summary_values.avg_psf},
|
||||
{'label': 'Total Anchors', 'value': summary_values.anchors},
|
||||
{'label': 'Total Ballast', 'value': summary_values.ballast},
|
||||
{'label': 'Max Possible System Weight', 'value': summary_values.max_weight},
|
||||
{'label': 'Max System Weight Ballast Block', 'value': summary_values.ballast_weight},
|
||||
{'label': 'Seismic Anchor Max. Spacing', 'value': seismic_interval}
|
||||
]
|
||||
|
||||
def documentation_summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator):
|
||||
summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator)
|
||||
|
||||
return {
|
||||
'total_system_weight': summary_values.total_weight,
|
||||
'max_psf': summary_values.max_psf,
|
||||
'ave_psf': summary_values.avg_psf,
|
||||
'total_anchors': summary_values.anchors,
|
||||
'total_ballast': summary_values.ballast,
|
||||
'max_possible_system_weight': summary_values.max_weight,
|
||||
'max_system_weight_ballast_block': summary_values.ballast_weight,
|
||||
'seismic_anchor_max_spacing': seismic_interval
|
||||
}
|
||||
|
||||
def compute_summary(self, panels, subarrays, c_p_matrix, q_z, ballast_calculator):
|
||||
total_weight, avg_psf = self.system_weight_and_pressure(panels)
|
||||
|
||||
max_psf = 0
|
||||
wind_anchors = 0
|
||||
total_ballast = 0
|
||||
for panel in panels:
|
||||
max_psf = panel.pressure if panel.pressure > max_psf else max_psf
|
||||
wind_anchors += panel.wind_anchors
|
||||
total_ballast += panel.ballast
|
||||
|
||||
required_seismic_anchors = sum(subarray.required_seismic_anchors for subarray in subarrays)
|
||||
|
||||
total_anchors = int(wind_anchors + required_seismic_anchors)
|
||||
|
||||
max_weight, ballast_weight = self.find_max_system_weight(panels, c_p_matrix, q_z, ballast_calculator)
|
||||
|
||||
return SummaryValues(
|
||||
total_weight=round(total_weight),
|
||||
max_psf=round(max_psf, 2),
|
||||
avg_psf=round(avg_psf, 2),
|
||||
anchors=total_anchors,
|
||||
ballast=int(total_ballast),
|
||||
max_weight=round(max_weight, 0),
|
||||
ballast_weight=ballast_weight
|
||||
)
|
||||
|
||||
def system_weight_and_pressure(self, panels):
|
||||
constants = self.values.module_system_constants()
|
||||
effective_area = constants.surface_area / constants.ground_coverage_ratio
|
||||
|
||||
psf_sum = sum(panel.pressure for panel in panels)
|
||||
return psf_sum * effective_area, psf_sum / len(panels)
|
||||
|
||||
def find_max_system_weight(self, panels, c_p_matrix, q_z, ballast_calculator):
|
||||
copied_panels = list(panels)
|
||||
max_weight = 0
|
||||
ballast_block_weight_for_max_weight = 0
|
||||
for weight in range(12, 19):
|
||||
ballast_matrix = ballast_calculator.ballast_and_trays_matrix(c_p_matrix, q_z, copied_panels,
|
||||
ballast_block_weight=weight)
|
||||
|
||||
total_weight, _ = self.system_weight_and_pressure(ballast_matrix)
|
||||
if total_weight > max_weight:
|
||||
max_weight = total_weight
|
||||
ballast_block_weight_for_max_weight = weight
|
||||
return max_weight, ballast_block_weight_for_max_weight
|
||||
56
helix/calculators/wind_pressure_calculator.py
Normal file
56
helix/calculators/wind_pressure_calculator.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from math import log
|
||||
|
||||
from helix.constants.exposure_category import ExposureCategory
|
||||
from helix.constants.global_constants import k_zt, k_d
|
||||
|
||||
|
||||
class WindPressureCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
|
||||
def q_z(self, k_z):
|
||||
v = self.values.wind_speed()
|
||||
return 0.00256 * k_z * k_zt * k_d * v ** 2
|
||||
|
||||
def K_z(self):
|
||||
height = self.values.building_height()
|
||||
exposure = self.values.exposure_category()
|
||||
transition_distance = self.values.exposure_category_transition_distance()
|
||||
return self.calculate_k_z(height, exposure, transition_distance)
|
||||
|
||||
def calculate_k_z(self, h, exp, transition_distance):
|
||||
if exp == ExposureCategory.B and h < 30:
|
||||
return 0.7
|
||||
elif exp == ExposureCategory.B_C or exp == ExposureCategory.C_B:
|
||||
k_zd = self.k_zd(exp, h)
|
||||
return k_zd + self.delta_k(exp, transition_distance, k_zd)
|
||||
else:
|
||||
return 2.01 * (self.h_eff(h, exp) / exp.z_g()) ** (2 / exp.alpha())
|
||||
|
||||
def h_eff(self, h, exposure):
|
||||
if (exposure == ExposureCategory.C or exposure == ExposureCategory.D) and h < 15:
|
||||
return 15.
|
||||
else:
|
||||
return h
|
||||
|
||||
def k_zd(self, exp, h):
|
||||
if exp == ExposureCategory.B_C:
|
||||
h_eff = max(15, h)
|
||||
return self.calculate_expression(h_eff, exp.z_g()[1], exp.alpha()[1])
|
||||
elif exp == ExposureCategory.C_B and h < 30:
|
||||
return 0.7
|
||||
else:
|
||||
return self.calculate_expression(h, exp.z_g()[1], exp.alpha()[1])
|
||||
|
||||
def delta_k(self, exp, transition_distance, k_zd):
|
||||
k_upwind = self.calculate_expression(33, exp.z_g()[0], exp.alpha()[0])
|
||||
k_downwind = self.calculate_expression(33, exp.z_g()[1], exp.alpha()[1])
|
||||
return (k_upwind - k_downwind) * (k_zd / k_downwind) * self.dk_x(k_upwind, k_downwind, transition_distance / 5280.)
|
||||
|
||||
def dk_x(self, k_upwind, k_downwind, transition_distance_in_miles):
|
||||
x0 = 0.621 * 10 ** (-1 * ((k_upwind - k_downwind) ** 2) - 2.3)
|
||||
x1 = 6.21 if k_upwind > k_downwind else 62.1
|
||||
return log(x1 / transition_distance_in_miles, x1 / x0)
|
||||
|
||||
def calculate_expression(self, height, z_g, alpha):
|
||||
return 2.01 * (height / z_g) ** (2 / alpha)
|
||||
Reference in New Issue
Block a user