first commit

This commit is contained in:
Senad Uka
2017-11-07 09:23:57 +01:00
commit 0eee92660a
356 changed files with 747259 additions and 0 deletions

View File

View 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

View 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

View 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

View 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)

View 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))

View 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

View 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

View 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

View 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)

View 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

View 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]

View 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

View 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)