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,42 @@
import copy
from helix.calculators.subarray_graph import SubarrayGraph, Direction
from helix.calculators.subarray_helper import extract_subarray
from helix.constants.panel_type import PanelType
class GraphRepository(object):
def __init__(self, panels, subarrays, system_type):
self.graphs = {}
for subarray in subarrays:
subarray_panels = extract_subarray(panels, subarray.subarray_number)
self.graphs[subarray.subarray_number] = SubarrayGraph(panels=subarray_panels, system_type=system_type)
def walk_callback(_, next_direction, direction_to_get_here):
if next_direction == Direction.North:
self.rows_perimeter += 1
elif next_direction == Direction.East:
self.columns_perimeter += 1
for subarray in subarrays:
graph = self.graphs[subarray.subarray_number]
subarray_panels = extract_subarray(panels, subarray.subarray_number)
rows_fallback = sum(1 for panel in subarray_panels if panel.panel_type == PanelType.Corner or panel.panel_type == PanelType.EastWest) / 2
columns_fallback = sum(1 for panel in subarray_panels if panel.panel_type == PanelType.Corner or panel.panel_type == PanelType.NorthSouth) / 2
if len(graph.nodes) == 0:
subarray.row_count = rows_fallback
subarray.column_count = columns_fallback
subarray.row_counted_geometrically = False
subarray.column_counted_geometrically = False
continue
self.rows_perimeter = 1
self.columns_perimeter = 1
graph.walk_graph_perimeter(graph.lower_left_node(graph.nodes), walk_callback, repeat_steps=False)
subarray.row_count = max(self.rows_perimeter, rows_fallback)
subarray.row_counted_geometrically = self.rows_perimeter >= rows_fallback
subarray.column_count = max(self.columns_perimeter, columns_fallback)
subarray.column_counted_geometrically = self.columns_perimeter >= columns_fallback
def subarray_graph(self, subarray_number):
return copy.deepcopy(self.graphs[subarray_number])

View File

View File

@@ -0,0 +1,22 @@
import json
class DocGenServiceError(Exception):
pass
class DocGenService(object):
def __init__(self, request_maker, request_builder):
self.request_maker = request_maker
self.request_builder = request_builder
def generate(self):
url = 'https://dcs.us.sunpower.com/ws/docgen/docx/generatePdf'
headers = {'content-type': 'application/json'}
params = json.dumps(self.request_builder.build())
result = self.request_maker.post(url, params, headers=headers)
if result.status_code != 200:
raise DocGenServiceError(result.content)
return result.content

1002
helix/Services/dxf_helper.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
import io
import dxfgrabber
from helix.constants.file_validation_error import FileValidationMessage
from helix.models.dxf.dxf_error import OldDxfFormatException
class DXFService(object):
"""
Takes the contents of a DXF file and creates modules, buildings, panels,
subarrays, and polygons based on the data
"""
# l_b is expected to be in inches, not feet!
def parse(self, dxf_file_contents, module_constants, system_type, l_b, dxf_helper, subarray_validator):
"""Parse will generate the panels, subarrays, buildings
and modules that are present in the dxf file
Arguments:
dxf_file_contents (str) Content of the uploaded file
"""
dxf = dxfgrabber.read(io.StringIO(dxf_file_contents, newline=None))
buildings, modules = dxf_helper.build_polygons(dxf.entities)
"""
A new type of aurora format file was added in the project
The visibile difference if you read the file, is that the
new format contains the string buildings.
The old version doesn't
"""
pair_spacing = None
dxf_helper.is_new_aurora_format()
if hasattr(module_constants, "spacing_size_inches"):
pair_spacing = module_constants.spacing_size_inches
if dxf_helper.should_consolidate_modules(modules, system_type,
module_constants):
modules = dxf_helper.consolidate_dual_tilt_modules(modules,
system_type,
pair_spacing)
translated_buildings, translated_modules = dxf_helper.translate_towards_origin(buildings, modules)
translated_buildings_ccw = dxf_helper.get_polygons_counterclockwise(translated_buildings)
buildings_ccw = dxf_helper.get_polygons_counterclockwise(buildings)
panels = dxf_helper.generate_panels(modules, translated_modules)
node_graph = dxf_helper.build_node_graph(panels, module_constants.panel_spacing)
subarrays = dxf_helper.detect_subarrays(node_graph, panels)
for subarray in subarrays:
subarray_validator.validate_subarray(node_graph, subarray.subarray_number, system_type)
dxf_helper.detect_panel_types(node_graph)
dxf_helper.detect_wind_zones(panels, translated_buildings_ccw, translated_modules, l_b, system_type)
all_points = [p.points for p in translated_buildings_ccw + translated_modules]
points = [point for points_list in all_points for point in points_list]
max_x = max(p[0] for p in points)
max_y = max(p[1] for p in points)
panel_orientation = panels[0].coordinate.rotation
return {
'size': (max_x, max_y), # Used for debugging
'buildings': buildings_ccw,
'modules': translated_modules,
'panels': panels,
'subarrays': subarrays,
'lb_polygons': dxf_helper.l_b_polygons(translated_buildings_ccw, l_b, system_type, panel_orientation),
'is_panel_drawing_inaccurate': self.is_panel_drawing_inaccurate(panels)
}
def is_panel_drawing_inaccurate(self,panels):
'''True if subarrays are not rotated more than allowed tolerance, false otherwise'''
ROTATION_ACCURACY_DELTA_DEGREES = 0.1
if panels is None or panels == []:
return True
first_panel_rotation = panels[0].coordinate.rotation # rotation is in degrees
for panel in panels:
difference = abs(first_panel_rotation - panel.coordinate.rotation)
if difference >= ROTATION_ACCURACY_DELTA_DEGREES:
return True
return False

0
helix/__init__.py Normal file
View File

1
helix/api/__init__.py Normal file
View File

@@ -0,0 +1 @@
__author__ = 'pivotal'

65
helix/api/api.py Normal file
View File

@@ -0,0 +1,65 @@
from flask import Blueprint, request, session, jsonify
from helix.calculators.calculator import Calculator
from helix.presenters.panel_presenter import ProjectPresenter
from helix.session_manager import SessionManager
from helix.constants import redis_constant, sql_constant
from helix.seismic_validator_user_values import SeismicValidatorUserValues
from helix.validators.file_validator import FileValidator
from helix.validators.seismic_anchor_validator import SeismicAnchorValidator
api = Blueprint('api', __name__, template_folder='templates')
@api.route("/panel_data")
def panel_data():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_values = session_manager.user_values()
calculator = Calculator(user_values)
system_type = user_values.system_type()
module_type = user_values.module_type()
data = ProjectPresenter(system_type, module_type).get_panel_data(calculator.get_computed_csv_columns(), calculator.subarrays)
db_session.close()
return jsonify({'panel_data': data})
@api.route("/update_panel_data", methods=['POST'])
def update_panel_data():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_seismic_data = request.get_json()
user_values = SeismicValidatorUserValues(session_manager.user_values(), user_seismic_data)
calculator = Calculator(user_values)
validator = SeismicAnchorValidator(calculator)
validation_result = validator.validate(calculator.panels)
subarrays = calculator.subarray_summary()
subarray_data = []
for subarray in subarrays:
data = {
"subarray": subarray.subarray_number,
"required_seismic_anchors": subarray.required_seismic_anchors,
"weight": round(subarray.weight),
}
subarray_data.append(data)
if not validation_result:
session_manager.save_user_provided_seismic_anchors(user_seismic_data)
panel_data = ProjectPresenter(user_values.system_type(), user_values.module_type()).get_panel_data(calculator.get_computed_csv_columns(),
calculator.subarrays)
db_session.close()
return jsonify({
"status": "success",
"error": None,
"panel_data": panel_data,
"subarray_data": subarray_data
})
else:
db_session.close()
return jsonify({
"status": "error",
"error": validation_result.value,
"panel_data": None,
"subarray_data": subarray_data
})

View File

@@ -0,0 +1,78 @@
import json
from numpy import array
class CalculatedDataRepository(object):
summary_values_key = 'summary_values'
computed_csv_columns_key = 'computed_csv_columns'
compute_bom_key = 'compute_bom'
subarray_summary_key = 'subarray_summary'
def __init__(self, store, calculator):
self.store = store
self.calculator = calculator
def reset_data(self):
pass
def reset_site_data(self):
pass
def reset_panel_data(self):
pass
def reset_subarray_data(self):
self.store.delete(self.summary_values_key)
self.store.delete(self.subarray_summary_key)
def reset_ebom_data(self):
self.store.delete(self.compute_bom_key)
def k_z(self):
return self.calculator.k_z()
def L_B(self):
return self.calculator.L_B()
def summary_table(self):
return self.calculator.summary_table()
def minimum_array_sizes(self):
return self.calculator.minimum_array_sizes()
def summary_values(self):
key = self.summary_values_key
if self.store.exists(key):
summary_values_json = self.store.get(key).decode('utf-8)')
return array(json.loads(summary_values_json))
summary_values = self.calculator.summary_values()
self.store.set(key, json.dumps(list(summary_values)))
return summary_values
def get_computed_csv_columns(self):
return self.calculator.get_computed_csv_columns() # TODO: revisit after getting rid of DataMatrix
# key = self.computed_csv_columns_key
# if self.store.exists(key):
# computed_csv_columns_json = self.store.get(key).decode('utf-8')
# return jsonpickle.decode(computed_csv_columns_json)
# computed_csv_columns = self.calculator.get_computed_csv_columns()
# self.store.set(key, jsonpickle.encode(computed_csv_columns))
# return computed_csv_columns
def compute_bom(self):
key = self.compute_bom_key
if self.store.exists(key):
compute_bom_json = self.store.get(key).decode('utf-8')
return array(json.loads(compute_bom_json))
computed_bom = self.calculator.compute_bom()
self.store.set(key, json.dumps(computed_bom.tolist()))
return computed_bom
def subarray_summary(self):
key = self.subarray_summary_key
if self.store.exists(key):
received_json = self.store.get(key).decode('utf-8')
return array(json.loads(received_json))
data = self.calculator.subarray_summary()
self.store.set(key, json.dumps(data.tolist()))
return data

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)

View File

View File

@@ -0,0 +1,23 @@
from helix.constants.parts import anchor_plate, anchor, anchor_washer
class OmgPowerGripParts(object):
parts = {
anchor_plate: 1,
anchor: 1,
anchor_washer: 1
}
class OmgPowerGripPlusParts(object):
parts = {
anchor_plate: 1,
anchor: 1,
anchor_washer: 1
}
class EcoFastenParts(object):
parts = {
anchor_plate: 1,
anchor: 1,
anchor_washer: 1
}

View File

@@ -0,0 +1,29 @@
from enum import Enum
from helix.constants.anchor_parts import OmgPowerGripParts, OmgPowerGripPlusParts, EcoFastenParts
from helix.constants.global_constants import system_force_capacity
class AnchorType(Enum):
OMG_PowerGrip = 'OMG PowerGrip'
OMG_PowerGrip_Plus = 'OMG PowerGrip Plus'
EcoFasten = 'EcoFasten Eco 65'
def uplift_capacity(self):
return min(system_force_capacity, {AnchorType.OMG_PowerGrip: 305.,
AnchorType.OMG_PowerGrip_Plus: 2000.,
AnchorType.EcoFasten: 1343.}[self])
def shear_capacity(self):
return {AnchorType.OMG_PowerGrip: 142.,
AnchorType.OMG_PowerGrip_Plus: 431.,
AnchorType.EcoFasten: 1008.}[self]
def parts(self):
return {AnchorType.OMG_PowerGrip: OmgPowerGripParts(),
AnchorType.OMG_PowerGrip_Plus: OmgPowerGripPlusParts(),
AnchorType.EcoFasten: EcoFastenParts()}[self]
@classmethod
def default_value(cls):
return cls.OMG_PowerGrip_Plus.value

View File

@@ -0,0 +1,4 @@
dt_chassis_fudge_factor = 1.04
leading_tray_fudge_factor = 1.05
bolts_per_package = 50

11
helix/constants/color.py Normal file
View File

@@ -0,0 +1,11 @@
from enum import Enum
class Color(Enum):
array_background = "white"
seismic_background = "#F1E8A2"
wind_background = "#B8F3E5"
default_panel_background = "#133256"
light_text = "white"
dark_text = "#6490BA"
border = "#537DAA"

View File

@@ -0,0 +1,89 @@
from helix.constants.module_type import ModuleType
from helix.constants.parts import *
class DualTiltParts(object):
east_west_panel_parts = {
dual_tilt_chassis: 1,
module: 2
}
center_panel_parts = {
dual_tilt_chassis: 1,
module: 2
}
sub_array_parts = {
leading_tray: 1
}
def __init__(self, module_type):
if module_type == ModuleType.PSeries:
self.corner_panel_parts = {
left_deflector_1_1: 1,
right_deflector_1_1: 1,
dual_tilt_chassis: 1.5,
module: 2
}
self.north_south_panel_parts = {
left_deflector_1_1: 1,
right_deflector_1_1: 1,
dual_tilt_chassis: 1.5,
module: 2
}
else:
self.corner_panel_parts = {
left_deflector: 1,
right_deflector: 1,
dual_tilt_chassis: 1.5,
module: 2
}
self.north_south_panel_parts = {
left_deflector: 1,
right_deflector: 1,
dual_tilt_chassis: 1.5,
module: 2
}
def row_parts(self, module_type):
if module_type == ModuleType.Cell96:
front_skirt_parts = front_skirt
else:
front_skirt_parts = front_skirt_1_1
return {
front_skirt_parts: 2,
leading_tray: 1
}
def column_parts(self, _):
return {}
def parts_per_panel_type(self):
return [
self.corner_panel_parts,
self.north_south_panel_parts,
self.east_west_panel_parts,
self.center_panel_parts
]
def dependent_parts(self, _):
return {
module: {
wire_clip: 2,
rubber_foot: 0.1
},
dual_tilt_chassis: {
dual_tilt_platform: 1,
platform_bolt: 4
}
}
def fudge_factors(self, used_fallback):
if used_fallback:
return {
dual_tilt_chassis: 1.04,
leading_tray: 1.05
}
return {
dual_tilt_chassis: 1.04,
}

View File

@@ -0,0 +1,7 @@
'''
Created on May 22, 2017
@author: jvazquez
'''
INVALID_DUAL_TILT_DESIGN = "Error - not a dual tilt file, or invalid "\
"dual tilt design."

View File

@@ -0,0 +1,281 @@
from helix.constants.inverter_type import InverterType
from helix.constants.module_type import ModuleType
from helix.constants.parts import *
from helix.constants.system_type import SystemType
inverter_model_parts = {
InverterType.SMA.MODEL_12KW: {sma_12kw_inverter: 1},
InverterType.SMA.MODEL_15KW: {sma_15kw_inverter: 1},
InverterType.SMA.MODEL_20KW: {sma_20kw_inverter: 1},
InverterType.SMA.MODEL_24KW: {sma_24kw_inverter: 1},
InverterType.DELTA.MODEL_36KW: {delta_36kw_inverter: 1},
InverterType.DELTA.MODEL_42KW: {delta_42kw_inverter: 1},
InverterType.DELTA.MODEL_60KW: {delta_60kw_inverter: 1},
InverterType.DELTA.MODEL_80KW: {delta_80kw_inverter: 1},
}
inverter_strings_parts = {
0: {},
2: {
harness_2_string_mf: 1,
harness_2_string_fm: 1
},
3: {
harness_3_string_mf: 1,
harness_3_string_fm: 1
},
4: {
harness_2_string_mf: 2,
harness_2_string_fm: 2
},
5: {
harness_2_string_mf: 1,
harness_2_string_fm: 1,
harness_3_string_mf: 1,
harness_3_string_fm: 1
},
6: {
harness_3_string_mf: 2,
harness_3_string_fm: 2
},
7: {
harness_3_string_mf: 1,
harness_3_string_fm: 1,
harness_4_string_mf: 1,
harness_4_string_fm: 1
},
8: {
harness_4_string_mf: 2,
harness_4_string_fm: 2
},
}
def shared_panel_board_parts(module_type, system_type):
v1_1_inverter_links = 0
if system_type == SystemType.singleTilt and (module_type == ModuleType.PSeries or module_type == ModuleType.Cell128):
v1_1_inverter_links = 1
return {
front_legs: 1,
back_legs: 1,
inverter_link: 2,
inverter_rail: 1,
rubber_foot: 3,
mounting_back_plate: 1,
ethernet_plug: 1.8,
flat_washer: 4,
hex_nut_three_eighths_16: 2,
hex_bolt_1_2: 9,
inverter_link_long: v1_1_inverter_links,
inverter_link_short: v1_1_inverter_links,
}
dc_switch_parts = {
dc_switch_bracket: 1,
dc_switch_box: 1,
hex_bolt_3_4: 4,
hex_bolt_quarter_20: 4,
hex_nut_three_eighths_16: 4,
hex_nut_quarter_20: 4,
flat_washer_quarter_inch: 4,
}
def panel_board_parts(inverter_quantity, with_aux):
if with_aux:
parts = {
1: {
panel_board_2_aux: 1,
},
2: {
panel_board_2_aux: 1,
},
3: {
panel_board_3_aux: 1,
},
4: {
panel_board_4_aux: 1,
}
}
else:
parts = {
1: {
panel_board_2: 1,
},
2: {
panel_board_2: 1,
},
3: {
panel_board_3: 1,
},
4: {
panel_board_4: 1,
}
}
return parts[inverter_quantity]
def panel_board_parts_with_monitor(inverter_quantity, monitor_controller_type):
parts = panel_board_parts(inverter_quantity, monitor_controller_type != monitor_controller_240_v)
parts[monitor_controller_type] = 1
return parts
def standalone_inverter_parts(inverter, system_type, module_type):
multiplier = 1
v1_1_inverter_links = 0
if inverter['model'] in InverterType.DELTA.all():
parts = {}
if system_type == SystemType.singleTilt:
parts = {**parts, delta_kit_inverter_mount: 1}
else:
parts = {**parts, delta_kit_inverter_mount_dt: 1}
if inverter['splice_box']:
parts = {**parts, delta_splice_box: 1}
return parts
if system_type == SystemType.singleTilt:
multiplier = 2
v1_1_inverter_links = 1 if module_type == ModuleType.PSeries or module_type == ModuleType.Cell128 else 0
return {
ac_switch: 1,
star_washer: 4,
phillips_screw: 4 * multiplier,
ac_inverter_bracket: 1 * multiplier,
hex_bolt_1_2: 4,
flat_washer_6: 4,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
whip_tray: 1
}
standalone_inverter_attached_to_panel_board_parts = {
ac_splice_box: 1,
}
def inverter_parts(inverter, module_type):
if inverter['model'] in InverterType.DELTA.all():
return {
delta_inverter_leg: 3,
rubber_foot: 3,
}
else:
if module_type == ModuleType.PSeries:
return {
flat_washer: 8,
hex_nut_three_eighths_16: 6,
hex_bolt_3_4: 5,
hex_bolt_1_2: 18,
front_legs: 1,
back_legs: 1,
mounting_back_plate: 1,
rubber_foot: 3,
inverter_link: 2,
inverter_rail: 1,
stump: 6,
fuseshade: 1,
screw_12_24x1_25: 2,
fuseshade_brace: 1,
}
else:
return {
flat_washer: 4,
hex_nut_three_eighths_16: 4,
hex_bolt_3_4: 2,
hex_bolt_1_2: 18,
front_legs: 1,
back_legs: 1,
mounting_back_plate: 1,
rubber_foot: 3,
inverter_link: 2,
inverter_rail: 1,
stump: 6,
}
def dependent_parts(module_type, system_type):
v1_1_inverter_links = 0
if system_type == SystemType.singleTilt and (module_type == ModuleType.Cell128 or module_type == ModuleType.PSeries):
v1_1_inverter_links = 1
return {
panel_board_2: {
harness_ac_inner: 2,
whip_tray: 2,
comm_cable: 1
},
panel_board_2_aux: {
harness_ac_inner: 2,
whip_tray: 2,
comm_cable: 2
},
panel_board_3: {
harness_ac_inner: 2,
harness_ac_outer: 1,
whip_tray: 3,
comm_cable: 2,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
panel_board_3_aux: {
harness_ac_inner: 2,
harness_ac_outer: 1,
whip_tray: 3,
comm_cable: 3,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
panel_board_4: {
harness_ac_inner: 2,
harness_ac_outer: 2,
whip_tray: 4,
comm_cable: 3,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
panel_board_4_aux: {
harness_ac_inner: 2,
harness_ac_outer: 2,
whip_tray: 4,
comm_cable: 4,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
monitor_controller_480_v: {
monitor_power_plug: 1,
flat_washer: 4,
channel_nut: 4,
hex_nut_three_eighths_16: 2,
front_legs: 1,
back_legs: 1,
inverter_link: 2,
inverter_rail: 1,
rubber_foot: 3,
hex_bolt_1_2: 9,
mounting_back_plate: 1,
},
monitor_controller_240_v: {},
ac_splice_box: {
hex_bolt_1_2: 4,
hex_bolt_3_4: 4,
hex_nut_three_eighths_16: 8,
flat_washer: 8,
back_legs: 1,
front_legs: 1,
rubber_foot: 3,
inverter_rail: 1,
inverter_link: 2,
mounting_back_plate: 1,
},
inverter_link_long: {
inverter_link: -1,
},
inverter_link_short: {
inverter_link: -1,
}
}

View File

@@ -0,0 +1,34 @@
from enum import Enum
class ExposureCategory(Enum):
B = 'B'
C = 'C'
D = 'D'
B_C = 'B to C'
C_B = 'C to B'
def z_g(self):
return {
ExposureCategory.B: 1200.,
ExposureCategory.C: 900.,
ExposureCategory.D: 700.,
ExposureCategory.B_C: (1273., 906.),
ExposureCategory.C_B: (906., 1273.)
}[self]
def alpha(self):
return {
ExposureCategory.B: 7.,
ExposureCategory.C: 9.5,
ExposureCategory.D: 11.5,
ExposureCategory.B_C: (6.62, 9.5),
ExposureCategory.C_B: (9.5, 6.62)
}[self]
def is_interpolated(self):
return self in [ExposureCategory.B_C, ExposureCategory.C_B]
@classmethod
def default_value(cls):
return cls.C.value

View File

@@ -0,0 +1,50 @@
from enum import Enum
from helix.constants.system_type import SystemType
class FileValidationMessage(Enum):
Generic = 'Input file has invalid data'
InvalidHeaders = 'Input file has less than 5 headers'
InvalidRowCount = 'Input file has no data'
DualTiltWindZone = 'Invalid wind zone for Dual Tilt System'
SingleTiltWindZone = 'Invalid wind zone for Single Tilt System'
PanelTypeOutOfBounds = 'Invalid Panel Type (Not in 1-4)'
PanelTypeTooFewCornersSingleTilt = 'Input file must have at least 4 corner modules per subarray'
PanelTypeTooFewCornersDualTilt = 'Input file must have at least 2 corner modules per subarray'
UnknownFileUploaded = 'Please upload a valid .txt or .dxf file'
ExpectedTxtFile = 'Invalid file uploaded. Please upload a valid .txt file.'
ExpectedDxfFile = 'Invalid file uploaded. Please upload a valid .dxf file.'
OldDxfFormat = 'Invalid Aurora format uploaded. Please upload a new format.'
@classmethod
def invalid_wind_zones(cls, system_type):
if system_type == SystemType.singleTilt:
return cls.SingleTiltWindZone
else:
return cls.DualTiltWindZone
@classmethod
def panel_type_too_few_corners(cls, system_type):
return {
SystemType.singleTilt: cls.PanelTypeTooFewCornersSingleTilt,
SystemType.dualTilt: cls.PanelTypeTooFewCornersDualTilt,
}[system_type]
class FileValidationError(object):
def __init__(self, validation_message, row_number):
self.row_number = row_number
self.validation_message = validation_message
def format_error_message(self):
if self.row_number:
return self.validation_message.value + ' on line ' + str(self.row_number)
return self.validation_message.value
def __repr__(self):
return "<ValidationError: " + self.format_error_message() + ">"
def __eq__(self, other):
if self.__class__ != other.__class__:
return False
return self.row_number == other.row_number and self.validation_message == other.validation_message

View File

@@ -0,0 +1,9 @@
k_d = 0.85
k_zt = 1.
parapet_factor_max = 1.12
parapet_coefficients = 0.88, 1.2
system_force_capacity = 418.
minimum_racking_capacity = 226

View File

@@ -0,0 +1,25 @@
from enum import IntEnum
class InverterBrand(IntEnum):
SMA = 1
DELTA = 2
@property
def label(self):
return {
self.SMA: 'SMA',
self.DELTA: 'Delta',
}[self]
@classmethod
def default_value(cls):
return cls.SMA.value
@classmethod
def all(cls):
return [cls.SMA, cls.DELTA]
@classmethod
def dict(cls):
return {cls.SMA.label: cls.SMA.value, cls.DELTA.label: cls.DELTA.value}

View File

@@ -0,0 +1,94 @@
from enum import IntEnum
class InverterTypeSMA(IntEnum):
MODEL_12KW = 4
MODEL_15KW = 5
MODEL_20KW = 6
MODEL_24KW = 8
@property
def default_string(self):
return {
self.MODEL_12KW: 4,
self.MODEL_15KW: 5,
self.MODEL_20KW: 6,
self.MODEL_24KW: 8,
}[self]
@property
def label(self):
return {
self.MODEL_12KW: '12kW SMA Tripower - 514686',
self.MODEL_15KW: '15kW SMA Tripower - 514687',
self.MODEL_20KW: '20kW SMA Tripower - 512676',
self.MODEL_24KW: '24kW SMA Tripower - 514685',
}[self]
@property
def valid_string_ranges(self):
return {
self.MODEL_12KW: range(2, 9),
self.MODEL_15KW: range(2, 9),
self.MODEL_20KW: range(2, 9),
self.MODEL_24KW: range(2, 9),
}[self]
@classmethod
def default_value(cls):
return cls.MODEL_24KW.value
@classmethod
def all(cls):
return [cls.MODEL_12KW, cls.MODEL_15KW, cls.MODEL_20KW, cls.MODEL_24KW]
class InverterTypeDelta(IntEnum):
MODEL_36KW = 9
MODEL_42KW = 10
MODEL_60KW = 11
MODEL_80KW = 12
@property
def label(self):
return {
self.MODEL_36KW: '36kW Delta - 524952',
self.MODEL_42KW: '42kW Delta - 524969',
self.MODEL_60KW: '60kW Delta - 524954',
self.MODEL_80KW: '80kW Delta - 524955',
}[self]
@property
def default_string(self):
return {
self.MODEL_36KW: None,
self.MODEL_42KW: None,
self.MODEL_60KW: None,
self.MODEL_80KW: 14,
}[self]
@property
def valid_string_ranges(self):
return {
self.MODEL_36KW: None,
self.MODEL_42KW: None,
self.MODEL_60KW: None,
self.MODEL_80KW: range(14, 25),
}[self]
@classmethod
def default_value(cls):
return cls.MODEL_60KW.value
@classmethod
def all(cls):
return [cls.MODEL_36KW, cls.MODEL_42KW, cls.MODEL_60KW, cls.MODEL_80KW]
class InverterType:
SMA = InverterTypeSMA
DELTA = InverterTypeDelta
@classmethod
def all(cls):
return cls.SMA.all() + cls.DELTA.all()

View File

@@ -0,0 +1,11 @@
from enum import Enum
class ModuleType(Enum):
Cell96 = '96 Cell'
Cell128 = '128 Cell'
PSeries = 'P-Series'
@classmethod
def default_value(cls):
return cls.Cell96.value

View File

@@ -0,0 +1,113 @@
from numpy import array
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
class DualTilt128CellConstants(object):
panel_spacing = (88.24, 82.0) # inches
spacing_size_inches = 5.12 # This is inches
presenter_spacing = (1.5, 1.5)
panel_area = 23.29
surface_area = 46.58 # aka tent area
tributary_area = array([3.23, 6.84, 6.54, 13.57])
ground_coverage_ratio = 0.91
friction_coefficient = 0.59
seismic_anchor_interval_constants = (13.8768, 3.23792)
max_psf = 32
def c_p_lower_bound(self):
return array([0.06, 0.05, 0.052, 0.039])
def edge_factor(self, wind_zone, panel_type):
return 1
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 825:
return -0.144, 1.1451
else:
return -0.058, 0.5651
elif wind_zone == 'B':
if A_n < 465:
return -0.117, 0.8824
elif A_n < 825:
return -0.087, 0.6981
else:
return -0.033, 0.3336
elif wind_zone == 'C':
if A_n < 465:
return -0.073, 0.5479
elif A_n < 825:
return -0.035, 0.3143
else:
return -0.015, 0.184
elif wind_zone == 'D':
if A_n < 465:
return -0.039, 0.303
elif A_n < 825:
return -0.023, 0.2023
else:
return -0.008, 0.102
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [108.66, 110.96, 112.11, 116.44, 119.62, 122.80, 125.98][tray_count]
elif panel_type == PanelType.NorthSouth:
return [107.58, 109.88, 111.03, 114.21, 117.39, 120.57, 123.75][tray_count]
elif panel_type == PanelType.EastWest:
return [103.19, 105.49, 105.49, 108.67, 111.85, 115.03, 118.21][tray_count]
else:
return [102.11, 104.41, 104.41, 107.59, 110.77, 113.95, 117.13][tray_count]
def link_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [7.5, 10, 15]
else:
return [5, 7.5, 10]
def cross_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [15, 23, 31, 39, 47]
else:
return [10, 18, 26, 34, 42]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.275:
return 1.5305, 0.187
else:
return 0.9252, 0.097
elif wind_zone == 'B':
if uplift_c_p > 0.2292:
return 1.2058, 0.159
elif uplift_c_p > 0.1534:
return 1.0334, 0.131
else:
return 0.4411, 0.043
elif wind_zone == 'C':
if uplift_c_p > 0.151:
return 0.7899, 0.104
elif uplift_c_p > 0.108:
return 0.5785, 0.07
else:
return 0.2921, 0.027
elif wind_zone == 'D':
if uplift_c_p > 0.0929:
return 0.3939, 0.049
elif uplift_c_p > 0.069:
return 0.3043, 0.035
else:
return 0.122, 0.008
return None

View File

@@ -0,0 +1,121 @@
from numpy import array
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
class DualTilt96CellConstants(object):
panel_spacing = (88.24, 62.0) # inches
spacing_size_inches = 5.12 # This is inches
presenter_spacing = (1.5, 1)
panel_area = 17.58
surface_area = 35.14 # aka tent area
tributary_area = array([3.32, 6.58, 7.29, 21.74]) # for each panel type
ground_coverage_ratio = 0.91
friction_coefficient = 0.59
seismic_anchor_interval_constants = (10.1598, 2.3706)
max_psf = 48
def c_p_lower_bound(self):
return array([0.06, 0.05, 0.052, 0.039])
def edge_factor(self, wind_zone, panel_type):
return 1
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 825:
return -0.144, 1.1451
else:
return -0.058, 0.5651
elif wind_zone == 'B':
if A_n < 465:
return -0.117, 0.8824
elif A_n < 825:
return -0.087, 0.6981
else:
return -0.033, 0.3336
elif wind_zone == 'C':
if A_n < 465:
return -0.073, 0.5479
elif A_n < 825:
return -0.035, 0.3143
else:
return -0.015, 0.184
elif wind_zone == 'D':
if A_n < 465:
return -0.039, 0.303
elif A_n < 825:
return -0.023, 0.2023
else:
return -0.008, 0.102
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [92.58,
94.31,
96.03,
98.33,
100.63,
102.93,
105.23][tray_count]
else:
return [87.11,
88.84,
89.41,
91.71,
94.01,
96.31,
98.61][tray_count]
def link_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [7.5, 10, 15]
else:
return [5, 7.5, 10]
def cross_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [15, 24, 33, 42, 51]
else:
return [10, 19, 28, 37, 46]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.275:
return 1.5305, 0.187
else:
return 0.9252, 0.097
elif wind_zone == 'B':
if uplift_c_p > 0.2292:
return 1.2058, 0.159
elif uplift_c_p > 0.1537:
return 1.0334, 0.131
else:
return 0.4411, 0.043
elif wind_zone == 'C':
if uplift_c_p > 0.151:
return 0.7899, 0.104
elif uplift_c_p > 0.108:
return 0.5785, 0.07
else:
return 0.2921, 0.027
elif wind_zone == 'D':
if uplift_c_p > 0.093:
return 0.3939, 0.049
elif uplift_c_p > 0.069:
return 0.3043, 0.035
else:
return 0.122, 0.008
return None

View File

@@ -0,0 +1,113 @@
from numpy import array
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
class DualTiltPSeriesConstants(object):
panel_spacing = (88.24, 82.0) # inches
spacing_size_inches = 8.8503937 # This is inches
presenter_spacing = (1.5, 1.5)
panel_area = 22.22
surface_area = 44.44 # aka tent area
tributary_area = array([3.23, 6.84, 6.54, 13.57])
ground_coverage_ratio = 0.91
friction_coefficient = 0.59
seismic_anchor_interval_constants = (12.6378, 2.94882)
max_psf = 32
def c_p_lower_bound(self):
return array([0.06, 0.05, 0.052, 0.039])
def edge_factor(self, wind_zone, panel_type):
return 1
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 825:
return -0.144, 1.1451
else:
return -0.058, 0.5651
elif wind_zone == 'B':
if A_n < 465:
return -0.117, 0.8824
elif A_n < 825:
return -0.087, 0.6981
else:
return -0.033, 0.3336
elif wind_zone == 'C':
if A_n < 465:
return -0.073, 0.5479
elif A_n < 825:
return -0.035, 0.3143
else:
return -0.015, 0.184
elif wind_zone == 'D':
if A_n < 465:
return -0.039, 0.303
elif A_n < 825:
return -0.023, 0.2023
else:
return -0.008, 0.102
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [103.66, 105.96, 107.11, 111.44, 114.62, 117.80, 120.98][tray_count]
elif panel_type == PanelType.NorthSouth:
return [102.58, 104.88, 106.03, 109.21, 112.39, 115.57, 118.75][tray_count]
elif panel_type == PanelType.EastWest:
return [98.19, 100.49, 100.49, 103.67, 106.85, 110.03, 113.21][tray_count]
else:
return [97.11, 99.41, 99.41, 102.59, 105.77, 108.95, 112.13][tray_count]
def link_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [7.5, 10, 15]
else:
return [5, 7.5, 10]
def cross_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [15, 23, 31, 39, 47]
else:
return [10, 18, 26, 34, 42]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.275:
return 1.5305, 0.187
else:
return 0.9252, 0.097
elif wind_zone == 'B':
if uplift_c_p > 0.2292:
return 1.2058, 0.159
elif uplift_c_p > 0.1537:
return 1.0334, 0.131
else:
return 0.4411, 0.043
elif wind_zone == 'C':
if uplift_c_p > 0.151:
return 0.7899, 0.104
elif uplift_c_p > 0.108:
return 0.5785, 0.07
else:
return 0.2921, 0.027
elif wind_zone == 'D':
if uplift_c_p > 0.093:
return 0.3939, 0.049
elif uplift_c_p > 0.069:
return 0.3043, 0.035
else:
return 0.122, 0.008
return None

View File

@@ -0,0 +1,236 @@
from numpy import array
from numpy.ma import log
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SingleTilt128CellConstants(object):
panel_spacing = (82.0, 60.0) # inches
presenter_spacing = (1.5, 1)
panel_area = 23.29
surface_area = 23.29
tributary_area = array([1.87, 3.24, 3.24, 3.82])
ground_coverage_ratio = 0.67
friction_coefficient = 0.42
seismic_anchor_interval_constants = (9.8784, 2.30496)
system_constants = SingleTiltConstants()
max_psf = 32
def c_p_lower_bound(self):
c_p_lower_bound = []
for index, area in enumerate(self.tributary_area):
if area < 9:
c_p = -0.0273 * log(area) + 0.1401
else:
c_p = -0.0146 * log(area) + 0.112
edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index))
c_p_lower_bound.append(c_p * edge_factor)
return array(c_p_lower_bound)
def edge_factor(self, wind_zone, panel_type):
# corner, north/south, east/west, middle
edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0],
'B': [1.2, 1.2, 1.0, 1.0],
'C': [1.4, 1.4, 1.0, 1.0],
'D': [1.4, 1.4, 1.0, 1.0],
'E': [1.5, 1.0, 1.5, 1.0],
'F': [1.3, 1.3, 1.0, 1.0],
'G': [1.0, 1.0, 1.0, 1.0],
'H': [1.0, 1.0, 1.0, 1.0],
'I': [1.0, 1.0, 1.0, 1.0],
'J': [2.0, 2.0, 1.0, 1.0],
'K': [1.4, 1.4, 1.0, 1.0]}
return edge_matrix[wind_zone][panel_type.index()]
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.EastWest:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 421.5:
return -0.2245, 1.717
elif A_n < 2294.7:
return -0.1298, 1.1446
else:
return -0.0308, 0.3787
elif wind_zone == 'B':
if A_n < 421.5:
return -0.1818, 1.3585
elif A_n < 2294.7:
return -0.0708, 0.688
else:
return -0.0308, 0.3787
elif wind_zone == 'C':
if A_n < 421.5:
return -0.0735, 0.6443
elif A_n < 2294.7:
return -0.059, 0.5566
else:
return -0.021, 0.2627
elif wind_zone == 'D':
if A_n < 187.3:
return -0.1007, 0.6868
elif A_n < 421.5:
return -0.049, 0.4181
else:
return -0.0183, 0.2304
elif wind_zone == 'E':
if A_n < 187.3:
return -0.058, 0.4236
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'F':
if A_n < 187.3:
return -0.0655, 0.4682
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'G':
if A_n < 187.3:
return -0.0341, 0.2786
elif A_n < 421.5:
return -0.0271, 0.242
else:
return -0.0125, 0.1533
elif wind_zone == 'H':
if A_n < 187.3:
return -0.1161, 0.7872
elif A_n < 421.5:
return -0.074, 0.5672
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'I':
if A_n < 187.3:
return -0.3856, 2.258
elif A_n < 421.5:
return -0.148, 1.0143
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'J':
if A_n < 187.3:
return -0.1024, 0.6557
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [71.91, 71.91, 75.09, 78.27][tray_count]
elif panel_type == PanelType.NorthSouth:
return [65.8, 65.8, 68.98, 72.16][tray_count]
elif panel_type == PanelType.EastWest:
return [69.75, 72.05, 75.23, 78.41][tray_count]
else:
return [65.08, 67.38, 70.56, 73.74][tray_count]
def link_tray_thresholds(self, panel_type):
return [[0, 13.0],
[0, 10.00],
[7, 14.5],
[6, 11.0]][panel_type.index()]
def cross_tray_thresholds(self, panel_type):
return [[13.0, 21.0, 29.0],
[10.00, 18.0, 26.0],
[14.5, 22.5, 30.5],
[11.0, 19.0, 27.0]][panel_type.index()]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
""" Based on a 30 by 28 rectangular array """
return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.360:
return 3.1178, 0.3769
elif uplift_c_p > 0.180:
return 2.4967, 0.274
else:
return 0.9691, 0.0685
elif wind_zone == 'B':
if uplift_c_p > 0.260:
return 2.3762, 0.2807
elif uplift_c_p > 0.160:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'C':
if uplift_c_p > 0.200:
return 1.7251, 0.215
elif uplift_c_p > 0.100:
return 1.2668, 0.1274
else:
return 0.4655, 0.0196
elif wind_zone == 'D':
if uplift_c_p > 0.120:
return 2.3762, 0.2807
elif uplift_c_p > 0.094:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'E':
if uplift_c_p > 0.078:
return 0.8096, 0.0976
elif uplift_c_p > 0.060:
return 0.438, 0.0361
else:
return 0.3227, 0.0206
elif wind_zone == 'F':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'G':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'H':
if uplift_c_p > 0.180:
return 1.8215, 0.2525
elif uplift_c_p > 0.120:
return 1.4679, 0.185
elif uplift_c_p > 0.060:
return 1.13479, 0.1298
else:
return 0.3227, 0.0206
elif wind_zone == 'I':
if uplift_c_p > 0.240:
return 4.3609, 0.6996
elif uplift_c_p > 0.120:
return 2.639, 0.3699
elif uplift_c_p > 0.060:
return 1.4027, 0.1659
else:
return 0.3277, 0.0206
elif wind_zone == 'J':
if uplift_c_p > 0.120:
return 0.7131, 0.0891
elif uplift_c_p > 0.078:
return 0.5503, 0.058
else:
return 0.328, 0.0212
elif wind_zone == 'K':
if uplift_c_p > 0.080:
return 0.3, 0.0455
else:
return 0.2465, 0.0212
else:
return 1, 1

View File

@@ -0,0 +1,231 @@
from numpy import array
from numpy.ma import log
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SingleTilt96CellConstants(object):
panel_spacing = (62.0, 60.0) # inches
presenter_spacing = (1, 1)
panel_area = 17.58
surface_area = 17.57
tributary_area = array([2.07, 4.06, 4.06, 9.79])
ground_coverage_ratio = 0.67
friction_coefficient = 0.42
seismic_anchor_interval_constants = (7.2324, 1.6876)
system_constants = SingleTiltConstants()
max_psf = 48
def c_p_lower_bound(self):
c_p_lower_bound = []
for index, area in enumerate(self.tributary_area):
if area < 9:
c_p = -0.0273 * log(area) + 0.1401
else:
c_p = -0.0146 * log(area) + 0.112
edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index))
c_p_lower_bound.append(c_p * edge_factor)
return array(c_p_lower_bound)
def edge_factor(self, wind_zone, panel_type):
# corner, north/south, east/west, middle
edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0],
'B': [1.2, 1.2, 1.0, 1.0],
'C': [1.4, 1.4, 1.0, 1.0],
'D': [1.4, 1.4, 1.0, 1.0],
'E': [1.5, 1.0, 1.5, 1.0],
'F': [1.3, 1.3, 1.0, 1.0],
'G': [1.0, 1.0, 1.0, 1.0],
'H': [1.0, 1.0, 1.0, 1.0],
'I': [1.0, 1.0, 1.0, 1.0],
'J': [2.0, 2.0, 1.0, 1.0],
'K': [1.4, 1.4, 1.0, 1.0]}
return edge_matrix[wind_zone][panel_type.index()]
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.EastWest:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 421.5:
return -0.2245, 1.717
elif A_n < 2294.7:
return -0.1298, 1.1446
else:
return -0.0308, 0.3787
elif wind_zone == 'B':
if A_n < 421.5:
return -0.1818, 1.3585
elif A_n < 2294.7:
return -0.0708, 0.688
else:
return -0.0308, 0.3787
elif wind_zone == 'C':
if A_n < 421.5:
return -0.0735, 0.6443
elif A_n < 2294.7:
return -0.059, 0.5566
else:
return -0.021, 0.2627
elif wind_zone == 'D':
if A_n < 187.3:
return -0.1007, 0.6868
elif A_n < 421.5:
return -0.049, 0.4181
else:
return -0.0183, 0.2304
elif wind_zone == 'E':
if A_n < 187.3:
return -0.058, 0.4236
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'F':
if A_n < 187.3:
return -0.0655, 0.4682
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'G':
if A_n < 187.3:
return -0.0341, 0.2786
elif A_n < 421.5:
return -0.0271, 0.242
else:
return -0.0125, 0.1533
elif wind_zone == 'H':
if A_n < 187.3:
return -0.1161, 0.7872
elif A_n < 421.5:
return -0.074, 0.5672
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'I':
if A_n < 187.3:
return -0.3856, 2.258
elif A_n < 421.5:
return -0.148, 1.0143
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'J':
if A_n < 187.3:
return -0.1024, 0.6557
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
return [[54.50, 54.50, 56.80, 59.10],
[49.47, 49.47, 51.77, 54.07],
[53.42, 55.72, 58.02, 60.32],
[48.75, 51.05, 53.35, 55.65]][panel_type.index()][tray_count]
def link_tray_thresholds(self, panel_type):
return [[0, 12.0],
[0, 9.00],
[6, 13.5],
[5, 10.0]][panel_type.index()]
def cross_tray_thresholds(self, panel_type):
return [[12.0, 21.0, 30.0],
[9.00, 18.0, 27.0],
[13.5, 22.5, 31.5],
[10.0, 19.0, 28.0]][panel_type.index()]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
""" Based on a 30 by 28 rectangular array """
return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.360:
return 3.1178, 0.3769
elif uplift_c_p > 0.180:
return 2.4967, 0.274
else:
return 0.9691, 0.0685
elif wind_zone == 'B':
if uplift_c_p > 0.260:
return 2.3762, 0.2807
elif uplift_c_p > 0.160:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'C':
if uplift_c_p > 0.200:
return 1.7251, 0.215
elif uplift_c_p > 0.100:
return 1.2668, 0.1274
else:
return 0.4655, 0.0196
elif wind_zone == 'D':
if uplift_c_p > 0.120:
return 2.3762, 0.2807
elif uplift_c_p > 0.094:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'E':
if uplift_c_p > 0.078:
return 0.8096, 0.0976
elif uplift_c_p > 0.060:
return 0.438, 0.0361
else:
return 0.3227, 0.0206
elif wind_zone == 'F':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'G':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'H':
if uplift_c_p > 0.180:
return 1.8215, 0.2525
elif uplift_c_p > 0.120:
return 1.4679, 0.185
elif uplift_c_p > 0.060:
return 1.13479, 0.1298
else:
return 0.3227, 0.0206
elif wind_zone == 'I':
if uplift_c_p > 0.240:
return 4.3609, 0.6996
elif uplift_c_p > 0.120:
return 2.639, 0.3699
elif uplift_c_p > 0.060:
return 1.4027, 0.1659
else:
return 0.3277, 0.0206
elif wind_zone == 'J':
if uplift_c_p > 0.120:
return 0.7131, 0.0891
elif uplift_c_p > 0.078:
return 0.5503, 0.058
else:
return 0.328, 0.0212
elif wind_zone == 'K':
if uplift_c_p > 0.080:
return 0.3, 0.0455
else:
return 0.2465, 0.0212
else:
return 1, 1

View File

@@ -0,0 +1,235 @@
from numpy import array
from numpy.ma import log
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SingleTiltPSeriesConstants(object):
panel_spacing = (82.0, 61.8755) # inches
presenter_spacing = (1.5, 1)
panel_area = 22.22
surface_area = 22.22
tributary_area = array([1.87, 3.24, 3.24, 3.82])
ground_coverage_ratio = 0.67
friction_coefficient = 0.42
seismic_anchor_interval_constants = (8.9964, 2.09916)
max_psf = 32
system_constants = SingleTiltConstants()
def c_p_lower_bound(self):
c_p_lower_bound = []
for index, area in enumerate(self.tributary_area):
if area < 9:
c_p = -0.0273 * log(area) + 0.1401
else:
c_p = -0.0146 * log(area) + 0.112
edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index))
c_p_lower_bound.append(c_p * edge_factor)
return array(c_p_lower_bound)
def edge_factor(self, wind_zone, panel_type):
# corner, north/south, east/west, middle
edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0],
'B': [1.2, 1.2, 1.0, 1.0],
'C': [1.4, 1.4, 1.0, 1.0],
'D': [1.4, 1.4, 1.0, 1.0],
'E': [1.5, 1.0, 1.5, 1.0],
'F': [1.3, 1.3, 1.0, 1.0],
'G': [1.0, 1.0, 1.0, 1.0],
'H': [1.0, 1.0, 1.0, 1.0],
'I': [1.0, 1.0, 1.0, 1.0],
'J': [2.0, 2.0, 1.0, 1.0],
'K': [1.4, 1.4, 1.0, 1.0]}
return edge_matrix[wind_zone][panel_type.index()]
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.EastWest:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 421.5:
return -0.2245, 1.717
elif A_n < 2294.7:
return -0.1298, 1.1446
else:
return -0.0308, 0.3787
elif wind_zone == 'B':
if A_n < 421.5:
return -0.1818, 1.3585
elif A_n < 2294.7:
return -0.0708, 0.688
else:
return -0.0308, 0.3787
elif wind_zone == 'C':
if A_n < 421.5:
return -0.0735, 0.6443
elif A_n < 2294.7:
return -0.059, 0.5566
else:
return -0.021, 0.2627
elif wind_zone == 'D':
if A_n < 187.3:
return -0.1007, 0.6868
elif A_n < 421.5:
return -0.049, 0.4181
else:
return -0.0183, 0.2304
elif wind_zone == 'E':
if A_n < 187.3:
return -0.058, 0.4236
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'F':
if A_n < 187.3:
return -0.0655, 0.4682
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'G':
if A_n < 187.3:
return -0.0341, 0.2786
elif A_n < 421.5:
return -0.0271, 0.242
else:
return -0.0125, 0.1533
elif wind_zone == 'H':
if A_n < 187.3:
return -0.1161, 0.7872
elif A_n < 421.5:
return -0.074, 0.5672
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'I':
if A_n < 187.3:
return -0.3856, 2.258
elif A_n < 421.5:
return -0.148, 1.0143
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'J':
if A_n < 187.3:
return -0.1024, 0.6557
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [66.91, 66.91, 70.09, 73.27][tray_count]
elif panel_type == PanelType.NorthSouth:
return [60.8, 60.8, 63.98, 67.16][tray_count]
elif panel_type == PanelType.EastWest:
return [64.75, 67.05, 70.23, 73.41][tray_count]
else:
return [60.08, 62.38, 65.56, 68.74][tray_count]
def link_tray_thresholds(self, panel_type):
return [[0, 13.0],
[0, 10.00],
[7, 14.5],
[6, 11.0]][panel_type.index()]
def cross_tray_thresholds(self, panel_type):
return [[13.0, 21.0, 29.0],
[10.00, 18.0, 26.0],
[14.5, 22.5, 30.5],
[11.0, 19.0, 27.0]][panel_type.index()]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
""" Based on a 30 by 28 rectangular array """
return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.360:
return 3.1178, 0.3769
elif uplift_c_p > 0.180:
return 2.4967, 0.274
else:
return 0.9691, 0.0685
elif wind_zone == 'B':
if uplift_c_p > 0.260:
return 2.3762, 0.2807
elif uplift_c_p > 0.160:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'C':
if uplift_c_p > 0.200:
return 1.7251, 0.215
elif uplift_c_p > 0.100:
return 1.2668, 0.1274
else:
return 0.4655, 0.0196
elif wind_zone == 'D':
if uplift_c_p > 0.120:
return 2.3762, 0.2807
elif uplift_c_p > 0.094:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'E':
if uplift_c_p > 0.078:
return 0.8096, 0.0976
elif uplift_c_p > 0.060:
return 0.438, 0.0361
else:
return 0.3227, 0.0206
elif wind_zone == 'F':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'G':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'H':
if uplift_c_p > 0.180:
return 1.8215, 0.2525
elif uplift_c_p > 0.120:
return 1.4679, 0.185
elif uplift_c_p > 0.060:
return 1.13479, 0.1298
else:
return 0.3227, 0.0206
elif wind_zone == 'I':
if uplift_c_p > 0.240:
return 4.3609, 0.6996
elif uplift_c_p > 0.120:
return 2.639, 0.3699
elif uplift_c_p > 0.060:
return 1.4027, 0.1659
else:
return 0.3277, 0.0206
elif wind_zone == 'J':
if uplift_c_p > 0.120:
return 0.7131, 0.0891
elif uplift_c_p > 0.078:
return 0.5503, 0.058
else:
return 0.328, 0.0212
elif wind_zone == 'K':
if uplift_c_p > 0.080:
return 0.3, 0.0455
else:
return 0.2465, 0.0212
else:
return 1, 1

View File

@@ -0,0 +1,46 @@
from enum import Enum
class PanelType(Enum):
Corner = 0
NorthSouth = 1
EastWest = 2
Middle = 3
@classmethod
def from_number(cls, number):
if number == 1:
return cls.Corner
elif number == 2:
return cls.NorthSouth
elif number == 3:
return cls.EastWest
elif number == 4:
return cls.Middle
return None
@classmethod
def from_index(cls, index):
return PanelType(index)
@classmethod
def all(cls):
return [cls.Corner, cls.NorthSouth, cls.EastWest, cls.Middle]
def number(self):
return self.value + 1
def index(self):
return self.value
def snake_case(self):
return {
PanelType.Corner: 'corner',
PanelType.NorthSouth: 'north_south',
PanelType.EastWest: 'east_west',
PanelType.Middle: 'middle',
}[self]
def __sub__(self, other): # Used for testing (assert_array_almost_equal requires subtraction)
return self.value - other.value

241
helix/constants/parts.py Normal file
View File

@@ -0,0 +1,241 @@
# Mechanical Parts
single_tilt_chassis = ('513831', 'CHASSIS, SINGLE TILT, HELIX ROOF')
dual_tilt_chassis = ('514056', 'BASE, CHASSIS, DUAL TILT, HELIX ROOF')
dual_tilt_platform = ('514057', 'PLATFORM, CHASSIS, DUAL TILT, HELIX ROOF')
platform_bolt = ('515063', 'SCREW, CAP, SH, M6 X 1 X 12, 18-8 SS (DIN 912)')
left_deflector = ('513841', 'DEFLECTOR, LH, HELIX ROOF')
right_deflector = ('513842', 'DEFLECTOR, RH, HELIX ROOF')
front_skirt = ('515928', 'FRONT SKIRT, HELIX ROOF')
rear_skirt = ('515929', 'REAR SKIRT, HELIX ROOF')
spoiler = ('513836', 'SPOILER, SINGLE TILT, HELIX ROOF')
rear_skirt_1_1 = ('520301', 'REAR SKIRT, HELIX ROOF V1.1')
spoiler_1_1 = ('520302', 'SPOILER, SINGLE TILT, HELIX ROOF V1.1')
front_skirt_1_1 = ('520303', 'FRONT SKIRT, HELIX ROOF V1.1')
cross_tray_1_1 = ('520306', 'TRAY, OPTIONAL BALLAST, HELIX ROOF V1.1')
left_deflector_1_1 = ('521794', 'DEFLECTOR, LH, HELIX ROOF V1.1')
right_deflector_1_1 = ('521795', 'DEFLECTOR, RH, HELIX ROOF V1.1')
leading_tray = ('517871', 'TRAY, LEADING, HELIX ROOF, RIVETED VERSION')
following_tray = ('513832', 'TRAY, FOLLOWING, HELIX ROOF')
link_tray = ('513833', 'TRAY, LINK, HELIX ROOF')
cross_tray = ('513844', 'TRAY, OPTIONAL BALLAST, HELIX ROOF')
anchor_plate = ('513843', 'PLATE, ANCHOR, HELIX ROOF')
anchor = ('TBD', 'Anchors')
anchor_washer = ('518477', 'WASHER, FLAT, 3/8, 1.00 OD, 18-8 SS')
module = ('TBD', 'Modules')
ballast = ('Contractor Supplied', 'Ballast Blocks')
rubber_foot = ('514265', 'FOOT, RECYCLED RUBBER, HELIX ROOF')
# Electrical Parts, per inverter
flat_washer = ('104813', 'WASHER, FLAT, 3/8, .812 OD, 18-8 SS (1500-731)')
channel_nut = ('106925', 'NUT, CHANNEL, 3/8-16, SS, UNISTRUT P3008 (1507-770)')
hex_nut_three_eighths_16 = ('107551', 'NUT, HEX, 3/8-16, 18-8 SS (5100-086)')
hex_bolt_3_4 = ('513007', 'BOLT, HH, 3/8-16 X 3/4, 316 SS')
hex_bolt_1_2 = ('514865', 'BOLT, HH, 3/8-16 X 1/2, 18-8 SS')
front_legs = ('512660', 'FRONT LEG, INVERTER RACK, HELIX ROOF')
back_legs = ('512661', 'BACK LEGS, INVERTER RACK, HELIX ROOF')
harness_2_string_fm = ('514437', 'HARNESS, DC COMBINATION, NO FUSE, 2 STRING, FEMALES TO MALE, HELIX')
harness_2_string_mf = ('514438', 'HARNESS, DC COMBINATION, NO FUSE, 2 STRING, MALES TO FEMALE, HELIX')
harness_3_string_fm = ('514435', 'HARNESS, DC COMBINATION, W/ FUSE, 3 STRING, FEMALES TO MALE, HELIX')
harness_3_string_mf = ('514436', 'HARNESS, DC COMBINATION, W/ FUSE, 3 STRING, MALES TO FEMALE, HELIX')
harness_4_string_fm = ('514439', 'HARNESS, DC COMBINATION, W/ FUSE, 4 STRING, FEMALES TO MALE, HELIX')
harness_4_string_mf = ('514440', 'HARNESS, DC COMBINATION, W/ FUSE, 4 STRING, MALES TO FEMALE, HELIX')
inverter_rail = ('512663', 'RAIL, INVERTER RACK, HELIX ROOF')
inverter_link = ('512662', 'LINK TO ARRAY, INVERTER RACK, HELIX ROOF')
inverter_link_short = ('521798', 'LINK TO ARRAY, LONG, INVERTER RACK, HELIX ROOF V1.1')
inverter_link_long = ('521797', 'LINK TO ARRAY, SHORT, INVERTER RACK, HELIX ROOF V1.1')
mounting_back_plate = ('518331', 'MOUNTING BACK PLATE, INVERTER/PANEL BOARD, HELIX ROOF/TRACKER')
sma_12kw_inverter = ('514686', 'INVERTER, SMA, STP, 12000TL-US-10 (SPR-12000m-3 XXX), AFCI, CONNECTORIZED')
sma_15kw_inverter = ('514687', 'INVERTER, SMA, STP, 15000TL-US-10 (SPR-15000m-3 XXX), AFCI, CONNECTORIZED')
sma_20kw_inverter = ('512676', 'INVERTER, SMA, STP, 20000TL-US-10 (SPR-20000m-3 XXX), AFCI, CONNECTORIZED')
sma_24kw_inverter = ('514685', 'INVERTER, SMA, STP, 24000TL-US-10 (SPR-24000m-3 XXX), AFCI, CONNECTORIZED')
delta_36kw_inverter = ('524952', 'INVERTER, DELTA, M36U_122(MC4), 10INPUT, 36KW, 3PH 480V AC,1000V DC')
delta_42kw_inverter = ('524969', 'INVERTER, DELTA, M42U_122(MC4), 12INPUT, 42KW, 3PH 480V AC,1000V DC')
delta_60kw_inverter = ('524954', 'INVERTER, DELTA, M60U_122 (MC4), 18INPUT, 60KW, 3PH 480V AC,1000V DC')
delta_80kw_inverter = ('524955', '-')
screw_12_24x1_25 = ('507985', 'SCREW, S-D, HWH, #12-24X 1-1/4", #3 PT, BI-METAL')
# Wire management
stump = ('512021', 'STUMP, WIRE MANAGEMENT, 50MM ID, HELIX ROOF')
cable_support = ('512511', 'CABLE SUPPORT, HELIX ROOF')
cable_support_lid = ('512510', 'LID, CABLE SUPPORT, HELIX ROOF')
wire_clip = ('512200', 'CLIP, WIRE FORMED, CABLE MANAGEMENT, INSIDE, 352MM ^ 2')
wire_clip_large = ('512199', 'CLIP, WIRE FORMED, CABLE MANAGEMENT, INSIDE, 1624MM ^ 2')
# Panel Boards
panel_board_4 = ('513299', 'COMBINER BOX, AC, 4 INPUT, NO AUX, W/ CONNECTOR')
panel_board_3 = ('513301', 'COMBINER BOX, AC, 3 INPUT, NO AUX, W/ CONNECTOR')
panel_board_2 = ('513303', 'COMBINER BOX, AC, 2 INPUT, NO AUX, W/ CONNECTOR')
harness_ac_inner = ('514477', 'HARNESS, AC, INNER, 72", HELIX ROOFTOP')
harness_ac_outer = ('514478', 'HARNESS, AC, OUTER, 108", HELIX ROOFTOP')
whip_tray = ('515059', 'ASSY, WHIP TRAY W/FUSE CLIPS, INVERTER, HELIX')
comm_cable = ('514697', 'COMM CABLE, INVERTER DAISY CHAIN, 118", HELIX ROOF')
ethernet_plug = ('518058', 'CONNECTOR, ETHERNET, PLUG, RJ-45, WEATHERPROOF, SHIELDED')
# Aux Plug
panel_board_4_aux = ('513300', 'COMBINER BOX, AC, 4 INPUT, W/ AUX, W/ CONNECTOR')
panel_board_3_aux = ('513302', 'COMBINER BOX, AC, 3 INPUT, W/ AUX, W/ CONNECTOR')
panel_board_2_aux = ('513304', 'COMBINER BOX, AC, 2 INPUT, W/ AUX, W/ CONNECTOR')
# Monitoring
monitor_power_plug = ('519008', 'HARNESS, MONITORING POWER CABLE, SINGLE CONNECTOR, HELIX ROOF')
monitor_controller_480_v = ('518059', 'CONTROLLER, MONITORING, COMMERCIAL, PVS5C BASED, 480VAC, US')
monitor_controller_240_v = ('517463', 'MONITORING SYSTEM, COMMERCIAL, <100KW, PVS5x BASED, 240VAC, US')
# DC Switch related
dc_switch_bracket = ('512575', 'BRACKET, DC SWITCH BOX, HELIX')
dc_switch_box = ('514698', 'DC SWITCH BOX, HELIX')
hex_bolt_quarter_20 = ('114961', 'BOLT, HH, 1/4-20 X 3/4", 18-8SS')
hex_nut_quarter_20 = ('107549', 'NUT, HEX, 1/4-20, 18-8 SS (5100-084)')
flat_washer_quarter_inch = ('107586', 'WASHER, FLAT, 1/4, 0.5 OD, 18-8 SS (5100-144)')
# Standalone Inverter
star_washer = ('105317', 'WASHER, STAR, #6, SS (1501-606)')
flat_washer_6 = ('111147', 'WASHER, FLAT, #6, 18-8 SS (1509-097)')
phillips_screw = ('107538', 'SCREW, PH, 6-32 X 1/2, SS (5100-073)')
ac_splice_box = ('516045', 'AC SPLICE BOX, CONNECTORIZED, HELIX ROOF')
ac_switch = ('516043', 'AC SWITCH, CONNECTORIZED, HELIX ROOF')
ac_inverter_bracket = ('513586', 'BRACKET, INVERTER AC SWITCH, HELIX')
delta_kit_inverter_mount = ('524781', 'KIT, INVERTER MOUNT, DELTA, HELIX ROOF')
delta_kit_inverter_mount_dt = ('525772', 'KIT, INVERTER MOUNT, DELTA, DT, HELIX ROOF')
delta_splice_box = ('525651', 'KIT, AC SPLICE, DELTA, HELIX ROOF')
delta_inverter_leg = ('524783', 'INVERTER LEG, DELTA, HELIX ROOF')
delta_branch_connector = ('TBD', 'Branch connector')
# Other Ebom
sunshade = ('512910', 'SUN SHADE, INVERTER, HELIX')
sunshade_bolt = ('805615', 'SCREW, HEXAGONAL HEAD, M10X20, SS A2')
sunshade_washer = ('521031', 'WASHER, FLAT, M10 X 20MM OD, SS')
fuseshade = ('521363', 'FUSE SHADE, HELIX ROOF')
fuseshade_brace = ('522020', 'BRACE, FUSE SHADE, HELIX ROOF')
# Package Sizes
package_sizes = {
flat_washer: 50,
flat_washer_quarter_inch: 100,
channel_nut: 50,
hex_nut_three_eighths_16: 50,
hex_nut_quarter_20: 100,
hex_bolt_3_4: 50,
hex_bolt_quarter_20: 50,
hex_bolt_1_2: 50,
wire_clip_large: 10,
wire_clip: 30,
platform_bolt: 50,
star_washer: 100,
anchor_washer: 25
}
all_parts = [
single_tilt_chassis,
dual_tilt_chassis,
dual_tilt_platform,
platform_bolt,
left_deflector,
right_deflector,
front_skirt,
rear_skirt,
spoiler,
rear_skirt_1_1,
spoiler_1_1,
front_skirt_1_1,
cross_tray_1_1,
leading_tray,
following_tray,
link_tray,
cross_tray,
anchor_plate,
anchor,
anchor_washer,
module,
ballast,
rubber_foot,
flat_washer,
channel_nut,
hex_nut_three_eighths_16,
hex_bolt_3_4,
hex_bolt_1_2,
front_legs,
back_legs,
harness_2_string_fm,
harness_2_string_mf,
harness_3_string_fm,
harness_3_string_mf,
harness_4_string_fm,
harness_4_string_mf,
inverter_rail,
inverter_link,
mounting_back_plate,
sma_12kw_inverter,
sma_15kw_inverter,
sma_20kw_inverter,
sma_24kw_inverter,
stump,
cable_support,
cable_support_lid,
wire_clip,
wire_clip_large,
panel_board_4,
panel_board_3,
panel_board_2,
harness_ac_inner,
harness_ac_outer,
whip_tray,
comm_cable,
ethernet_plug,
panel_board_4_aux,
panel_board_3_aux,
panel_board_2_aux,
monitor_power_plug,
monitor_controller_480_v,
dc_switch_bracket,
dc_switch_box,
hex_bolt_quarter_20,
hex_nut_quarter_20,
flat_washer_quarter_inch,
star_washer,
flat_washer_6,
phillips_screw,
ac_splice_box,
ac_switch,
ac_inverter_bracket,
sunshade,
sunshade_bolt,
sunshade_washer,
screw_12_24x1_25,
fuseshade_brace,
left_deflector_1_1,
right_deflector_1_1,
inverter_link_short,
inverter_link_long,
fuseshade,
monitor_controller_240_v
]

View File

@@ -0,0 +1,7 @@
import os
from helix.db.redis_manager import RedisManager
vcap_url = os.getenv('VCAP_SERVICES') # pws is best ws.
heroku_url = os.getenv('REDIS_URL')
redis_store = RedisManager.get_redis_connection(vcap_url, heroku_url)

View File

@@ -0,0 +1,5 @@
from enum import Enum
class SeismicAnchorValidationError(Enum):
TooFewAnchors = 'There are too few anchors in one or more subarrays'

View File

@@ -0,0 +1,94 @@
from helix.constants.module_type import ModuleType
from helix.constants.parts import *
class SingleTiltParts(object):
center_panel_parts = {
module: 1,
single_tilt_chassis: 1
}
sub_array_parts = {
leading_tray: 1
}
north_south_panel_parts = {
module: 1,
single_tilt_chassis: 0.5,
}
def __init__(self, module_type):
if module_type == ModuleType.PSeries:
self.corner_panel_parts = {
module: 1,
single_tilt_chassis: 1,
left_deflector_1_1: 0.5,
right_deflector_1_1: 0.5,
}
self.east_west_panel_parts = {
module: 1,
single_tilt_chassis: 1.5,
left_deflector_1_1: 0.5,
right_deflector_1_1: 0.5
}
else:
self.corner_panel_parts = {
module: 1,
single_tilt_chassis: 1,
left_deflector: 0.5,
right_deflector: 0.5
}
self.east_west_panel_parts = {
module: 1,
single_tilt_chassis: 1.5,
left_deflector: 0.5,
right_deflector: 0.5
}
def row_parts(self, _):
return {}
def column_parts(self, module_type):
if module_type == ModuleType.Cell96:
front_skirt_parts = front_skirt
else:
front_skirt_parts = front_skirt_1_1
return {
front_skirt_parts: 1,
leading_tray: 1
}
def parts_per_panel_type(self):
return [
self.corner_panel_parts,
self.north_south_panel_parts,
self.east_west_panel_parts,
self.center_panel_parts
]
def module(self, module_type):
if module_type == ModuleType.Cell96:
rear_skirt_parts = rear_skirt
spoiler_parts = spoiler
else:
rear_skirt_parts = rear_skirt_1_1
spoiler_parts = spoiler_1_1
return {
spoiler_parts: 1,
rear_skirt_parts: 1,
wire_clip: 2,
rubber_foot: 0.1
}
def dependent_parts(self, module_type):
return {
module: self.module(module_type),
leading_tray: {
following_tray: 1
}
}
def fudge_factors(self, _):
return {
single_tilt_chassis: 1.0525
}

View File

@@ -0,0 +1,6 @@
import os
from helix.db.sql_manager import SQLManager
def sql_session_maker():
return SQLManager.get_sql_session_maker(os.getenv('DATABASE_URL'))

View File

@@ -0,0 +1,6 @@
'''
Created on May 22, 2017
@author: jvazquez
'''
SUBARRAY_SIZE_BIG = "Array size is too big. Max is 150' by 150'."

View File

@@ -0,0 +1,60 @@
from enum import Enum
from helix.constants.module_type import ModuleType
from helix.constants.module_type_constants.dual_tilt_128_cell_constants import DualTilt128CellConstants
from helix.constants.module_type_constants.dual_tilt_96_cell_constants import DualTilt96CellConstants
from helix.constants.dual_tilt_parts import DualTiltParts
from helix.constants.module_type_constants.dual_tilt_pseries_constants import DualTiltPSeriesConstants
from helix.constants.module_type_constants.single_tilt_128_cell_constants import SingleTilt128CellConstants
from helix.constants.module_type_constants.single_tilt_96_cell_constants import SingleTilt96CellConstants
from helix.constants.module_type_constants.single_tilt_pseries_constants import SingleTiltPSeriesConstants
from helix.constants.single_tilt_parts import SingleTiltParts
from helix.constants.system_type_constants.dual_tilt_constants import DualTiltConstants
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SystemType(Enum):
singleTilt = '0'
dualTilt = '1'
def system_constants(self):
return {
SystemType.singleTilt: SingleTiltConstants(),
SystemType.dualTilt: DualTiltConstants()
}[self]
def module_constants(self, module_type):
return {
SystemType.singleTilt: self.single_tilt_constants(module_type),
SystemType.dualTilt: self.dual_tilt_constants(module_type)
}[self]
def parts(self, module_type):
return {
SystemType.singleTilt: SingleTiltParts(module_type),
SystemType.dualTilt: DualTiltParts(module_type)
}[self]
def display_name(self):
return {
SystemType.singleTilt: 'Single-Tilt',
SystemType.dualTilt: 'Dual-Tilt'
}[self]
@classmethod
def default_value(cls):
return cls.dualTilt.value
def single_tilt_constants(self, module_type):
return {
ModuleType.Cell96: SingleTilt96CellConstants(),
ModuleType.Cell128: SingleTilt128CellConstants(),
ModuleType.PSeries: SingleTiltPSeriesConstants()
}[module_type]
def dual_tilt_constants(self, module_type):
return {
ModuleType.Cell96: DualTilt96CellConstants(),
ModuleType.Cell128: DualTilt128CellConstants(),
ModuleType.PSeries: DualTiltPSeriesConstants()
}[module_type]

View File

@@ -0,0 +1,4 @@
class DualTiltConstants(object):
wind_zones = ['A', 'B', 'C', 'D', 'E']
module_count = 2
minimum_corner_module_count = 2

View File

@@ -0,0 +1,4 @@
class SingleTiltConstants(object):
wind_zones = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
module_count = 1
minimum_corner_module_count = 4

View File

@@ -0,0 +1,7 @@
import os
def version():
if os.getenv('VERSION'):
return os.getenv('VERSION')
return 'release-9-72-g3e1c132'

107
helix/csv_builder.py Normal file
View File

@@ -0,0 +1,107 @@
import csv
from enum import Enum
from io import StringIO
class PanelDataColumn(Enum):
Handle = 'HANDLE'
Blockname = 'BLOCKNAME'
Subarray = 'SUBARRAY'
PanelType = 'POS'
WindZone = 'WIND'
Ballast = 'BAL'
LinkTray = 'LT_CALCULATED'
CrossTray = 'XTRAY'
WindAnchor = 'ANC'
SeismicAnchor = 'SEISMIC'
Coordinate = 'COORDINATE'
Pressure = 'PSF'
Id = 'ID'
PresentedLinkTray = 'LTRAY'
Xcoord = 'XCOORD'
Ycoord = 'YCOORD'
Rotation = 'ANGLE'
FuzzyWindZone = 'FUZZYWINDZONE'
class CSVDataColumn(Enum):
PresentedAnchors = "ANC"
class CsvBuilder(object):
def build_cad_output(self, panels):
panels.sort(key=lambda x: x.id)
output_columns = [
PanelDataColumn.Handle, PanelDataColumn.Blockname, PanelDataColumn.WindZone, PanelDataColumn.PanelType,
PanelDataColumn.Subarray, PanelDataColumn.Pressure, PanelDataColumn.Ballast,
PanelDataColumn.PresentedLinkTray, PanelDataColumn.CrossTray, CSVDataColumn.PresentedAnchors,
PanelDataColumn.Id, PanelDataColumn.Xcoord, PanelDataColumn.Ycoord, PanelDataColumn.Rotation
]
use_fuzzy_wind_zone = False
for panel in panels:
if panel.fuzzy_wind_zone:
output_columns.append(PanelDataColumn.FuzzyWindZone)
use_fuzzy_wind_zone = True
break
header_row = [col.value for col in output_columns]
matrix = [self.format_panel_for_csv(panel, include_fuzzy_wind_zone=use_fuzzy_wind_zone) for panel in panels]
return self.output_csv(header_row, matrix)
def build_bom_output(self, rows):
headers = ['Part #', 'Description', 'Total']
return self.output_csv(headers, rows)
def output_csv(self, headers, rows):
output = StringIO()
writer = csv.writer(output, dialect='excel-tab', quoting=csv.QUOTE_NONE, quotechar='')
writer.writerow(headers)
writer.writerows(rows)
return output.getvalue()
def format_panel_for_csv(self, panel, include_fuzzy_wind_zone=False):
row = [
panel.handle or "",
panel.blockname or "",
self.int_to_wind_zone(panel.wind_zone),
panel.panel_type.number() if panel.panel_type else "",
panel.subarray,
round(panel.pressure, 2) if panel.pressure else "",
self.present_value(panel.ballast),
self.present_value(panel.presented_link_tray) or '-',
self.present_value(panel.cross_tray) or '-',
self.present_anchors(panel.wind_anchors, panel.seismic_anchors),
panel.id,
round(panel.original_coordinate.x, 14),
round(panel.original_coordinate.y, 14),
round(panel.original_coordinate.rotation, 14)
]
if include_fuzzy_wind_zone:
row.append(int(panel.fuzzy_wind_zone))
return row
def round_two_digits(self, value):
return round(value, 2)
def zero_to_dash(self, value):
return int(value) or '-'
def int_to_wind_zone(self, value):
if value is None:
return ""
return ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"][value]
def present_value(self, value, default=''):
if value is None:
return default
return int(value)
def present_anchors(self, wind, seismic):
if not wind and not seismic:
return '-'
wind_string = str(int(wind)) if wind != 0 else ''
return wind_string + 'S' * int(seismic)

0
helix/db/__init__.py Normal file
View File

17
helix/db/redis_manager.py Normal file
View File

@@ -0,0 +1,17 @@
import json
import redis
class RedisManager(object):
@staticmethod
def get_redis_connection(vcap_env, heroku_redis_url):
if vcap_env:
redis_env = json.loads(vcap_env)['rediscloud'][0]['credentials']
return redis.Redis(host=redis_env['hostname'],
port=int(redis_env['port']),
password=redis_env['password'])
elif heroku_redis_url:
return redis.Redis.from_url(heroku_redis_url)
else:
return redis.Redis(host='localhost', port=6379, db=0)

26
helix/db/sql_manager.py Normal file
View File

@@ -0,0 +1,26 @@
import sqlalchemy
from sqlalchemy.orm import sessionmaker
class SQLManager(object):
# Cache the database connection per application process.
# More properly, this should be kept in thread-local state (threading.local()), but should suffice.
# Each passed connection url will have its own pool.
engines = {}
@classmethod
def get_sql_session_maker(cls, heroku_postgres_url, should_echo=False, cache=True):
if not cache:
return sessionmaker(bind=cls.connect(heroku_postgres_url, should_echo=should_echo))()
else:
engine = cls.engines.setdefault(heroku_postgres_url,
cls.connect(heroku_postgres_url, should_echo=should_echo))
return sessionmaker(bind=engine)()
@staticmethod
def connect(heroku_postgres_url, should_echo=False):
if heroku_postgres_url:
return sqlalchemy.create_engine(heroku_postgres_url, echo=should_echo)
else:
return sqlalchemy.create_engine('postgres://pivotal:@localhost/pivotal', echo=True)

View File

@@ -0,0 +1,166 @@
from base64 import b64encode
import os
from helix.constants.system_type import SystemType
from helix.constants.version import version
class DocGenParamsBuilder(object):
def __init__(self, user_values, system_type, calculator, image_presenter):
self.user_values = user_values
self.system_type = system_type
self.calculator = calculator
self.image_presenter = image_presenter
def build(self):
if self.user_values.system_type() == SystemType.singleTilt:
template_name = 'Helix_Single_Tilt_Template'
else:
template_name = 'Helix_Dual_Tilt_Template'
body = {
**self.site_characterization(),
**self.calculator.documentation_summary_values(),
**self.panel_attributes(),
**self.bom(),
**self.power_stations(),
**self.standalone_inverters(),
**self.subarray_summary(),
}
image_dicts = self.array_image()
list_params = self.convert_dict_params_to_list(body)
images = self.convert_dict_params_to_list(image_dicts, key_name='imageKey', value_name='base64encodedImage')
api_key = os.getenv('SP_DOCGEN_API_KEY', '')
params = {
"apiKey": api_key,
"templateName": template_name,
"nameValuePairs": list_params,
"dynamicImages": images,
}
return params
def site_characterization(self):
panels = self.calculator.get_computed_csv_columns()
return {
'project_name': self.user_values.project_name(),
'building_height': self.user_values.building_height(),
'building_width': self.user_values.building_width(),
'building_length': self.user_values.building_length(),
'parapet_height': self.user_values.building_parapet_height(),
'ballast_block_weight': self.user_values.ballast_block_weight(),
'max_allowable_system_pressure': self.user_values.max_system_pressure(),
'anchor_type': self.user_values.anchor_type().value,
'exposure_category': self.user_values.exposure_category().value,
'exposure_category_transition_distance': self.user_values.exposure_category_transition_distance(),
'wind_speed': self.user_values.wind_speed(),
'spectral_response': self.user_values.spectral_response(),
'seismic_importance_factor': self.user_values.importance_factor(),
'module_type': self.user_values.module_type().value,
'system_type': self.user_values.system_type().display_name(),
'total_modules': len(panels),
'version': version(),
'lb': round(self.calculator.L_B(), 2),
'kz': round(self.calculator.k_z(), 2),
'qz': round(self.calculator.q_z(), 2),
}
def panel_attributes(self):
summary_table = self.calculator.summary_table()
minimum_array_sizes = self.calculator.minimum_array_sizes()
result = {}
for panel_type, values in summary_table.items():
ballast_blocks = values['ballast blocks']
pressure = values['pressure']
anchors = values['anchors']
for wind_zone_index in range(len(ballast_blocks)):
wind_zone = self.system_type.system_constants().wind_zones[wind_zone_index].lower()
prefix = '%d_%s_' % (panel_type.number(), wind_zone)
result[prefix + 'bb'] = ballast_blocks[wind_zone_index]
result[prefix + 'psf'] = pressure[wind_zone_index]
result[prefix + 'anc'] = anchors[wind_zone_index]
for idx, array_size in enumerate(minimum_array_sizes):
wind_zone = self.system_type.system_constants().wind_zones[idx].lower()
result['min_' + wind_zone] = array_size
return result
def bom(self):
bom_values = self.calculator.documentation_bom()
result = {}
for row in bom_values:
result['total_' + row[0]] = row[1]
return result
def power_stations(self):
power_station_string = ""
power_stations = self.user_values.power_stations()
if len(power_stations) > 0:
power_station_string = "\n"
for power_station in self.user_values.power_stations():
power_station_string += "Description: %s\n" % power_station['power_station_description']
power_station_string += "\tQuantity: %s\n" % power_station['power_station_quantity']
power_station_string += "\tAC Run Length: %s\n" % power_station['ac_run_length']
power_station_string += "\tInverters:\n"
for inverter in power_station['inverters']:
power_station_string += "\t\tModel: %s\n" % inverter['model'].label
power_station_string += "\t\tStrings per inverter: %s\n" % inverter['strings_per_inverter']
if inverter.get('sunshade'):
power_station_string += "\t\tSunShade: Yes\n"
if inverter.get('dc_switch'):
power_station_string += "\t\tDC Switch: Yes\n"
power_station_string += "\n"
return {'power_station_string': power_station_string}
def standalone_inverters(self):
inverters_string = ""
for inverter in self.user_values.standalone_inverters():
inverters_string += "\n"
inverters_string += "\tAC Run Length: %s\n" % inverter['ac_run_length']
inverters_string += "\tAttachment Point: %s\n" % inverter['attachment_point'][0]
inverters_string += "\tModel: %s\n" % inverter['model'].label
inverters_string += "\tStrings per inverter: %s\n" % inverter['strings_per_inverter']
if inverter.get('sunshade'):
inverters_string += "\tSunShade: Yes\n"
if inverter.get('dc_switch'):
inverters_string += "\tDC Switch: Yes\n"
return {'standalone_inverter_string': inverters_string}
def subarray_summary(self):
subarray_string = ""
for subarray in self.calculator.subarray_summary():
subarray_string += "\n"
subarray_string += "\tSubarray: %d\n" % subarray.subarray_number
subarray_string += "\tSeismic Anchors: %d\n" % subarray.required_seismic_anchors
subarray_string += "\tWeight: %s lbs\n" % "{:,}".format(round(subarray.weight))
return {'subarrays_string': subarray_string}
def array_image(self):
png_data = self.image_presenter.generate_image(self.calculator.get_computed_csv_columns(),
self.calculator.subarrays)
return {'array_image': b64encode(png_data).decode('utf-8')}
@staticmethod
def convert_list_params_to_dict(list_params):
dict_params = {}
for datum in list_params:
dict_params[datum['name']] = datum['value']
return dict_params
@staticmethod
def convert_dict_params_to_list(dict_params, key_name='name', value_name='value'):
list_params = []
for k, v in dict_params.items():
list_params.append({
key_name: k,
value_name: v
})
return sorted(list_params, key=lambda k: k[key_name])

0
helix/forms/__init__.py Normal file
View File

View File

@@ -0,0 +1,17 @@
from wtforms.validators import Optional
class ConditionalValidator(object):
def __init__(self, other_field_name, other_data_contents, dependent_validator):
self.other_field_name = other_field_name
self.other_data_contents = other_data_contents
self.dependent_validator = dependent_validator
def __call__(self, form, field):
other_field = form._fields.get(self.other_field_name)
if other_field is None:
raise Exception('no field named "%s" in form' % self.other_field_name)
if other_field.data in self.other_data_contents:
self.dependent_validator.__call__(form, field)
else:
Optional().__call__(form, field)

183
helix/forms/ebom_form.py Normal file
View File

@@ -0,0 +1,183 @@
from wtforms import StringField, SelectField, FormField, BooleanField
from wtforms.fields.html5 import IntegerField
from wtforms.validators import NumberRange, DataRequired
from helix.constants.inverter_brand import InverterBrand
from helix.constants.inverter_type import InverterType
from helix.constants.system_type import SystemType
from helix.forms.grouped_form import GroupedForm
def generate_string_choices(from_i, to_i, only_even=False):
step = 2 if only_even else 1
return list(
map(lambda x: (x, "%s" % x), range(from_i, to_i + 1, step))
)
class InverterBrandForm(GroupedForm):
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "inverter_brand_form"})
inverter_brand_id = SelectField('',
choices=[(InverterBrand.SMA.value, InverterBrand.SMA.label),
(InverterBrand.DELTA.value, InverterBrand.DELTA.label)],
coerce=int,
default=InverterBrand.default_value(),
render_kw={'group': 'inverter_brands'},
)
def populate_choices(self, inverter_brands):
if len(inverter_brands) > 0:
self.inverter_brand_id.default = next(
map(lambda x: x['inverter_brand_id'], inverter_brands)
, InverterBrand.default_value())
self.process()
def is_delta(self):
return self.inverter_brand_id.data == InverterBrand.DELTA.value
class InverterFormSMA(GroupedForm):
quantity = IntegerField('Quantity',
default=1,
render_kw={'group': 'quantity', 'row_class': 'quantity'},
validators=[NumberRange(0, None)])
model = SelectField('Model',
choices=[(InverterType.SMA.MODEL_12KW.value, InverterType.SMA.MODEL_12KW.label),
(InverterType.SMA.MODEL_15KW.value, InverterType.SMA.MODEL_15KW.label),
(InverterType.SMA.MODEL_20KW.value, InverterType.SMA.MODEL_20KW.label),
(InverterType.SMA.MODEL_24KW.value, InverterType.SMA.MODEL_24KW.label)],
coerce=int,
default=InverterType.SMA.default_value(),
render_kw={'group': 'non-optional', 'row_class': 'inverter_model'},
)
strings_per_inverter = SelectField('# Strings/Inverter',
coerce=int,
choices=generate_string_choices(2, 8),
default=8,
render_kw={'group': 'non-optional', 'row_class': 'inverter_strings'})
sunshade = BooleanField('Sun Shade', render_kw={'group': 'optional'})
dc_switch = BooleanField('DC Switch', render_kw={'group': 'optional'})
def update_strings(self, system_type):
self.strings_per_inverter.choices = generate_string_choices(
2,
8,
system_type != SystemType.singleTilt
)
class InverterFormDelta(GroupedForm):
quantity = IntegerField('Quantity',
default=1,
render_kw={'group': 'quantity', 'row_class': 'quantity'},
validators=[NumberRange(0, None)])
model = SelectField('Model',
choices=[(InverterType.DELTA.MODEL_36KW.value, InverterType.DELTA.MODEL_36KW.label),
(InverterType.DELTA.MODEL_42KW.value, InverterType.DELTA.MODEL_42KW.label),
(InverterType.DELTA.MODEL_60KW.value, InverterType.DELTA.MODEL_60KW.label),
# (InverterType.DELTA.MODEL_80KW.value, InverterType.DELTA.MODEL_80KW.label),
],
coerce=int,
default=InverterType.DELTA.default_value(),
render_kw={'group': 'non-optional', 'row_class': 'inverter_model'},
)
strings_per_inverter = SelectField('# Strings/Inverter',
coerce=int,
choices=generate_string_choices(0, 24),
default=8,
render_kw={'group': 'non-optional', 'row_class': 'inverter_strings'})
splice_box = BooleanField('Splice Box', default=True, render_kw={'group': 'optional'})
class EbomForm(GroupedForm):
power_station_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': 'power_station_form'})
power_station_description = StringField('Power Station Description', render_kw={'group': 'header'},
validators=[DataRequired(message='Power Station Description is required.')],
default='Power Station 1')
power_station_quantity = IntegerField('Power Station Quantity',
default=1,
validators=[NumberRange(0, None)],
render_kw={'group': 'header'})
ac_run_length = IntegerField('Total AC Run Length for Power Station(s) (ft)',
render_kw={'group': 'header'}, default=0, validators=[NumberRange(0, None)])
monitor_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
inverter_quantity = SelectField('Inverters', choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4")],
coerce=int,
default=4,
render_kw={'group': 'inverter_quantity'})
inverter_1 = FormField(InverterFormSMA, 'Inverter 1', render_kw={'group': 'inverters'})
inverter_2 = FormField(InverterFormSMA, 'Inverter 2', render_kw={'group': 'inverters'})
inverter_3 = FormField(InverterFormSMA, 'Inverter 3', render_kw={'group': 'inverters'})
inverter_4 = FormField(InverterFormSMA, 'Inverter 4', render_kw={'group': 'inverters'})
def update_inverter_strings_choices(self, system_type):
self.inverter_1.update_strings(system_type)
self.inverter_2.update_strings(system_type)
self.inverter_3.update_strings(system_type)
self.inverter_4.update_strings(system_type)
class StandAloneInverterForm(GroupedForm):
standalone_inverter_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "standalone_inverter_form"})
standalone_ac_run_length = IntegerField('AC Run Length for Inverter (ft)',
render_kw={'group': 'power_station'},
default=0, validators=[NumberRange(0, None)])
def update_inverter_strings_choices(self, system_type):
self.inverter.update_strings(system_type)
def populate_choices(self):
pass
class StandAloneInverterFormSMA(StandAloneInverterForm):
inverter = FormField(InverterFormSMA, 'Inverter', render_kw={'group': 'inverters'})
attachment_point = SelectField('Attachment Point',
choices=[],
default='switch_gear',
render_kw={'group': 'power_station'})
def populate_choices(self, power_stations, standalone_inverters):
standalone_inverter_count_per_power_station = {}
for inverter in standalone_inverters:
key = inverter['attachment_point'][1]
standalone_count = standalone_inverter_count_per_power_station.get(key) or 0
standalone_inverter_count_per_power_station[key] = standalone_count + 1
power_stations_with_free_slots = []
for power_station in power_stations:
standalone_count = standalone_inverter_count_per_power_station.get(power_station['power_station_id']) or 0
inverter_count = power_station['inverter_quantity'] + standalone_count
if inverter_count < 4 and power_station['power_station_quantity'] == 1:
power_stations_with_free_slots.append(power_station)
choices = map(lambda x: (str(x['power_station_id']), x['power_station_description']), power_stations_with_free_slots)
self.attachment_point.choices = [('switch_gear', 'Switch Gear')] + list(choices)
class StandAloneInverterFormDelta(StandAloneInverterForm):
inverter = FormField(InverterFormDelta, 'Inverter', render_kw={'group': 'inverters'})
class SupervisorForm(GroupedForm):
monitor_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "supervisor_form"})
power_source = SelectField('Power Source',
render_kw={'group': 'power_source'},
choices=[('switch_gear', 'Switch Gear/External')])
class SupervisorFormSMA(SupervisorForm):
def populate_choices(self, power_stations, supervisors):
supervisor_power_sources = list(map(lambda x: x['power_source'][1], supervisors))
power_stations_without_supervisors = []
for power_station in power_stations:
if power_station['power_station_id'] not in supervisor_power_sources:
power_stations_without_supervisors.append(power_station)
choices = map(lambda x: (str(x['power_station_id']), x['power_station_description']), power_stations_without_supervisors)
self.power_source.choices = self.power_source.choices[:1] + list(choices)

View File

@@ -0,0 +1,6 @@
from flask.ext.wtf import Form
class GroupedForm(Form):
def group(self, label):
return [field for field in self if field.render_kw and field.render_kw['group'] == label]

95
helix/forms/input_form.py Normal file
View File

@@ -0,0 +1,95 @@
from flask.ext.wtf.file import FileField
from helix.constants.anchor_type import AnchorType
from helix.constants.exposure_category import ExposureCategory
from helix.constants.module_type import ModuleType
from helix.constants.system_type import SystemType
from wtforms import SelectField, StringField, BooleanField
from wtforms.fields.html5 import DecimalField, IntegerField
from wtforms.validators import NumberRange, DataRequired
from helix.forms.conditional_validator import ConditionalValidator
from helix.forms.grouped_form import GroupedForm
class InputForm(GroupedForm):
project_name = StringField('Project Name', validators=[DataRequired(message='Project Name is required.')],
render_kw={'group': 'project_info'})
building_height = DecimalField('Building Height (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
building_width = DecimalField('Building Width (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
building_length = DecimalField('Building Length (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
building_parapet_height = DecimalField('Parapet Height (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
wind_speed = IntegerField('Wind Speed (ASCE 7-10) (mph)',
validators=[NumberRange(100, 200)],
render_kw={'group': 'site_info',
'link': {'text': 'Look up',
'href': 'http://windspeed.atcouncil.org/'}})
exposure_category = SelectField('Exposure Category',
choices=[(ExposureCategory.B.value, ExposureCategory.B.value),
(ExposureCategory.B_C.value, "B to C"),
(ExposureCategory.C_B.value, "C to B"),
(ExposureCategory.C.value, ExposureCategory.C.value),
(ExposureCategory.D.value, ExposureCategory.D.value)],
default=ExposureCategory.default_value(),
render_kw={'group': 'site_info',
'link': {'text': 'More info',
'href': '/exposure_categories'}
})
exposure_category_transition_distance = IntegerField('Exposure Transition Distance (ft)',
default=0,
validators=[ConditionalValidator('exposure_category',
['B to C', 'C to B'],
NumberRange(1, None))],
render_kw={'group': 'site_info'})
ballast_block_weight = DecimalField('Ballast Block Weight (lbs)',
validators=[NumberRange(12, 20)],
default=14,
places=1,
render_kw={'group': 'site_info'})
max_system_pressure = DecimalField('Max Allowable System Pressure (psf)',
places=1,
validators=[NumberRange(0, None)],
default=12,
render_kw={'group': 'site_info'})
system_type = SelectField('System Type',
choices=[(SystemType.singleTilt.value, SystemType.singleTilt.display_name()),
(SystemType.dualTilt.value, SystemType.dualTilt.display_name())],
default=SystemType.default_value(),
render_kw={'group': 'project_info'})
module_type = SelectField('Module Type',
choices=[(ModuleType.Cell128.value, ModuleType.Cell128.value),
(ModuleType.PSeries.value, ModuleType.PSeries.value),
(ModuleType.Cell96.value, ModuleType.Cell96.value)],
default=ModuleType.default_value(),
render_kw={'group': 'project_info'})
anchor_type = SelectField('Anchor Type',
choices=[(AnchorType.OMG_PowerGrip.value, AnchorType.OMG_PowerGrip.value),
(AnchorType.OMG_PowerGrip_Plus.value, AnchorType.OMG_PowerGrip_Plus.value),
(AnchorType.EcoFasten.value, AnchorType.EcoFasten.value)],
default=AnchorType.default_value(),
render_kw={'group': 'site_info', 'tooltip': 'OMG anchors are compatible with TPO and PVC roof membranes.<br>EcoFasten anchors are compatible with Built Up Roofing (BUR), Hot Tar, Sips Panels and membrane type roofs.'})
design_spectral_response = DecimalField('Design Spectral Response Acceleration (S<sub>DS</sub>) (g)',
places=1, validators=[NumberRange(0, 5)],
render_kw={'group': 'site_info',
'link': {'text': 'Look up',
'href': 'http://earthquake.usgs.gov/designmaps/us/application.php'
}
})
importance_factor = SelectField('Seismic Importance Factor (I<sub>p</sub>)',
choices=[('1', 1), ('1.5', 1.5)],
default=1,
render_kw={'group': 'site_info', 'tooltip': 'Use 1.5 for essential facilities such as: Hospitals, Police, Fire & Rescue stations & Designated emergency shelters. All other structures should use 1.0.'})
class ArrayForm(GroupedForm):
file_upload = FileField('System Data (txt)', render_kw={'group': 'array_info', 'class': 'system_upload'})
dxf_upload = FileField('Cad File (dxf)', render_kw={'group': 'dxf_file', 'class': 'system_upload'})
class TestDXFForm(GroupedForm):
dxf_upload = FileField('Cad File (dxf)', render_kw={'group': 'array_info', 'class': 'system_upload'})
show_wind_zones = BooleanField('Show Wind Zones', default=True, render_kw={'group': 'array_info'})

4
helix/functions.py Normal file
View File

@@ -0,0 +1,4 @@
def fequal(x, y, delta=1e-6):
if x == y:
return True
return abs(x - y) < delta

View File

View File

@@ -0,0 +1,152 @@
from math import floor, ceil
# A utility class, a rectangle acting as boundaries of the quad tree
class Bounds:
def __init__(self, left, right, bottom, top):
if top < bottom:
top, bottom = bottom, top
if right < left:
right, left = left, right
self.left = left
self.right = right
self.bottom = bottom
self.top = top
self.width = right - left
self.height = top - bottom
def getWidth(self):
return self.width
def getHeight(self):
return self.height
def getLeft(self):
return self.left
def getRight(self):
return self.right
def getBottom(self):
return self.bottom
def getTop(self):
return self.top
# A QuadTree implemented to specifically handle panel Nodes
class NodeQuadTree():
MAX_OBJECTS = 100
MAX_LEVELS = 5
def __init__(self, level, bounds, variance):
if level < 1:
level = 1
self.level = level
self.variance = variance
self.bounds = bounds
if level == 1:
self.bounds = Bounds(floor(bounds.getLeft() - self.variance),
ceil(bounds.getRight() + self.variance),
floor(bounds.getBottom() - self.variance),
ceil(bounds.getTop() + self.variance))
self.nodeList = []
self.quads = [None, None, None, None]
def clear(self):
self.nodeList = []
for quad in self.quads:
if quad is not None:
quad.clear()
self.quads = [None, None, None, None]
def pointInside(self, point):
if point.x - self.variance < self.bounds.getLeft():
return False
if point.x + self.variance > self.bounds.getRight():
return False
if point.y - self.variance < self.bounds.getBottom():
return False
if point.y + self.variance > self.bounds.getTop():
return False
return True
def split(self):
left = self.bounds.getLeft()
right = self.bounds.getRight()
midLR = left + self.bounds.getWidth() / 2
bottom = self.bounds.getBottom()
top = self.bounds.getTop()
midBT = bottom + self.bounds.getHeight() / 2
self.quads[0] = NodeQuadTree(self.level + 1, Bounds(left, midLR, bottom, midBT), self.variance)
self.quads[1] = NodeQuadTree(self.level + 1, Bounds(midLR, right, bottom, midBT), self.variance)
self.quads[2] = NodeQuadTree(self.level + 1, Bounds(left, midLR, midBT, top), self.variance)
self.quads[3] = NodeQuadTree(self.level + 1, Bounds(midLR, right, midBT, top), self.variance)
# Returns which child index the point belongs in, or -1 if it doesn't fit completely within any
def getIndex(self, point):
for i in range(4):
if self.quads[i] is not None:
if self.quads[i].pointInside(point):
return i
return -1
# insert the node into the QuadTree, or one of its children
def insert(self, node):
# add it to our children, if they exist and the point fits
if self.quads[0] is not None:
index = self.getIndex(node.coordinate)
if index != -1:
self.quads[index].insert(node)
return
# else, add it to self
self.nodeList.append(node)
# too big? split into quads
if (len(self.nodeList) > NodeQuadTree.MAX_OBJECTS) and \
(self.level < NodeQuadTree.MAX_LEVELS) and \
(self.quads[0] is None):
self.split()
toKeep = []
for n in self.nodeList:
index = self.getIndex(n.coordinate)
if index != -1:
self.quads[index].insert(n)
else:
toKeep.append(n)
self.nodeList = toKeep
# Return a list of all possible nodes that can be near this point
def retrieve(self, nearPoint):
retNodes = list(self.nodeList)
if self.quads[0] is not None:
index = self.getIndex(nearPoint)
if index != -1:
retNodes += self.quads[index].retrieve(nearPoint)
else:
for quad in self.quads:
retNodes += quad.retrieve(nearPoint)
return retNodes
def report(self, outputArray):
if outputArray is not None:
outputArray.append(len(self.nodeList))
for quad in self.quads:
if quad is not None:
quad.report(outputArray)

View File

@@ -0,0 +1,97 @@
def point_inside_polygon(x, y, points):
n = len(points)
inside = False
# this does a raytracing algorithm that I don't quite understand.
# See http://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778#2922778
# for an attempt at an explanation
p1x, p1y = points[0]
for i in range(n + 1):
p2x, p2y = points[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
def line_segments_intersect(first_line, second_line):
# returns either None or the point where the line segments intersect
x_0_0 = first_line[0][0]
x_0_1 = first_line[1][0]
y_0_0 = first_line[0][1]
y_0_1 = first_line[1][1]
x_1_0 = second_line[0][0]
x_1_1 = second_line[1][0]
y_1_0 = second_line[0][1]
y_1_1 = second_line[1][1]
if max(x_0_0, x_0_1) < min(x_1_0, x_1_1):
return None
rise_0 = y_0_1 - y_0_0
run_0 = x_0_1 - x_0_0
if run_0 == 0:
slope_0 = None
y_intercept_0 = 0
else:
slope_0 = rise_0 / run_0
y_intercept_0 = y_0_0 - (slope_0 * x_0_0)
rise_1 = y_1_1 - y_1_0
run_1 = x_1_1 - x_1_0
if run_1 == 0:
slope_1 = None
y_intercept_1 = 0
else:
slope_1 = rise_1 / run_1
y_intercept_1 = y_1_0 - (slope_1 * x_1_0)
if slope_0 is not None and slope_1 is not None and abs(slope_0 - slope_1) < 1e-3:
return None
if slope_0 is None and slope_1 is None:
return None
if slope_0 is None:
x_point = x_0_0
slope = slope_1
elif slope_1 is None:
x_point = x_1_0
slope = slope_0
else:
x_point = (y_intercept_1 - y_intercept_0) / (slope_0 - slope_1)
slope = slope_0
if (x_point < max(min(x_0_0, x_0_1), min(x_1_0, x_1_1))) and (x_point > min(max(x_0_0, x_0_1), max(x_1_0, x_1_1))):
return None
y_point = slope * x_point + y_intercept_1
return x_point, y_point
def clip_polygon(a, clip_region):
points = []
for x, y in a:
points.append((x, y, point_inside_polygon(x, y, clip_region)))
idx = 0
while idx < len(points):
x0, y0, inside_0 = points[(idx-1) % len(points)]
x1, y1, inside_1 = points[idx]
first_line = [(x0, y0), (x1, y1)]
if inside_0 and not inside_1: # intersected between then and now
for index, vertex in enumerate(clip_region):
second_line = [vertex, clip_region[(index + 1) % len(clip_region)]]
intersection = line_segments_intersect(first_line, second_line)
if intersection:
points[idx] = (intersection[0], intersection[1], False)
break
idx += 1
return [(x, y) for x, y, _ in points]

View File

@@ -0,0 +1,206 @@
import '../../../lib/easeljs.js';
import $ from "jquery";
import {Panel} from './panel';
import Colors from "./colors";
class ArrayVisualization {
constructor(panelData, isDualTilt, subarrayDisplay, buildings) {
this.panelData = panelData;
this.isDualTilt = isDualTilt;
this.subarrayDisplay = subarrayDisplay;
this.buildings = buildings;
this.scale = 10;
}
init() {
this.stage = new createjs.Stage("arrayCanvas");
this.container = new createjs.Container();
let background = new createjs.Shape();
background.graphics.beginFill(Colors.canvas_background).drawRect(0, 0, 850, 850);
this.stage.addChild(background);
this.stage.mouseMoveOutside = true;
this.adjustScale(this.buildings);
this.drawArray(this.container, this.panelData, this.scale);
this.container.x = 0;
this.container.y = 0;
this.stage.addChild(this.container);
this.drawBuildings(this.buildings,this.scale);
this.stage.update();
var lastMove = undefined;
var self = this;
// Panning
this.stage.on("pressmove", function (event) {
let x = event.stageX;
let y = event.stageY;
if (lastMove != undefined) {
let deltaX = x - lastMove.x;
let deltaY = y - lastMove.y;
self.container.x += deltaX;
self.container.y += deltaY;
self.stage.update();
}
lastMove = {x: x, y: y};
});
this.stage.on("pressup", function () {
lastMove = undefined;
});
}
adjustScale(buildings) {
if (!buildings || buildings.length === 0) return; // leave scale as default
// we cannot determine canvas size
// we are in a test or in a very impossible state
// so we better not touch anything
if(!this.stage.canvas) return;
let all_x = ([].concat(...buildings)).map( (building) => building.x * this.scale );
let all_y = ([].concat(...buildings)).map( (building) => building.y * this.scale );
let max_x = Math.max(...all_x);
let max_y = Math.max(...all_y);
let ratio_y = this.stage.canvas.height * 1.0 / max_y;
let ratio_x = this.stage.canvas.width * 1.0 / max_x;
const MARGIN = 0.05;
this.scale = this.scale * Math.min(ratio_y, ratio_x) * (1 - MARGIN);
}
drawBuildings(buildings, scale) {
if (!buildings) {
console.log("No Buildings!");
return;
}
if (!scale) {
console.log("No Scale - don't know how big the buildings should be!");
return;
}
let line = new createjs.Shape();
let color = createjs.Graphics.getRGB(0x010101, 1);
line.graphics.setStrokeStyle(3);
for (let i = 0; i < buildings.length; i++) {
let building = buildings[i];
let firstPoint = building[0];
let nextPoint = null;
line.graphics.beginStroke(color);
line.graphics.moveTo(firstPoint.x * scale, firstPoint.y * scale);
for (let j = 1; j < building.length; j++ ) {
nextPoint = building[j];
line.graphics.lineTo(nextPoint.x * scale ,nextPoint.y * scale);
line.graphics.moveTo(nextPoint.x * scale,nextPoint.y * scale);
}
line.graphics.lineTo(firstPoint.x * scale, firstPoint.y * scale );
line.graphics.endStroke();
}
this.container.addChild(line);
}
refreshPanels(panelData) {
this.panelData = panelData;
for (let i = 0; i < this.panels.length; i++) {
this.container.removeChild(this.panels[i]);
}
this.drawArray(this.container, this.panelData, this.scale);
let selectedPanel = this.selectedPanel;
this.selectedPanel = undefined;
this.selectPanel(selectedPanel);
this.setOverlay(this.overlay);
this.subarrayDisplay.didModifyPanels(panelData);
this.stage.update();
}
drawArray(container, panels) {
this.panels = [];
let treatCoordinatesAsCenterpoints = this.buildings && this.buildings.length > 0;
for (let i = 0; i < panels.length; i++) {
let panel = panels[i];
let box = new Panel(panel, this.isDualTilt, this.scale, treatCoordinatesAsCenterpoints);
container.addChild(box);
this.panels.push(box);
let self = this;
box.on("click", function () {
self.selectPanel(i);
});
}
}
selectPanel(panelIndex) {
let panel = this.panels[panelIndex];
if (this.selectedPanel !== undefined) {
let previousPanel = this.panels[this.selectedPanel];
previousPanel.deselect();
}
if (this.selectedPanel === panelIndex) {
this.selectedPanel = undefined;
} else {
this.selectedPanel = panelIndex;
panel.select();
}
this.stage.update();
}
setZoom(zoomLevel) {
this.container.scaleX = zoomLevel;
this.container.scaleY = zoomLevel;
this.stage.update();
}
setOverlay(overlay) {
this.overlay = overlay;
this.panels.forEach((panel) => {
panel.setOverlay(overlay);
});
this.stage.update();
}
addSeismicAnchor() {
if (this.selectedPanel !== undefined) {
this.panelData[this.selectedPanel].data.seismic_anchors++;
this.panels[this.selectedPanel].redrawOverlays();
this.stage.update();
this.subarrayDisplay.didModifyPanels(this.panelData);
}
}
removeSeismicAnchor() {
if (this.selectedPanel !== undefined) {
let seismicAnchors = this.panelData[this.selectedPanel].data.seismic_anchors;
if (seismicAnchors > 0) {
this.panelData[this.selectedPanel].data.seismic_anchors--;
this.panels[this.selectedPanel].redrawOverlays();
this.stage.update();
this.subarrayDisplay.didModifyPanels(this.panelData);
}
}
}
}
export default ArrayVisualization;

View File

@@ -0,0 +1,23 @@
let AutoUpload = () => {
$("#file_upload").change((e) => {
var ten_megabyte_max_upload = 10000000;
$("#error_container_txt").empty();
if(e.currentTarget.files[0].size < ten_megabyte_max_upload){
e.currentTarget.form.submit();
}else{
$("#error_container_txt").append('<span class="error_message centered_error" id="error_message_txt">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
$("#dxf_upload").change((e) => {
var ten_megabyte_max_upload = 10000000;
$("#error_container_dxf").empty();
if(e.currentTarget.files[0].size < ten_megabyte_max_upload){
e.currentTarget.form.submit();
}else{
$("#error_container_dxf").append('<span class="error_message centered_error" id="error_message_dxf">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
};
export default AutoUpload;

View File

@@ -0,0 +1,13 @@
let Colors = {
canvas_background: '#F9F9F9',
seismic_background: '#F1E8A2',
wind_background: '#B8F3E5',
default_background: '#133256',
selected_background: 'white',
light_text: 'white',
dark_text: '#6490BA',
border: '#537DAA'
};
export default Colors

View File

@@ -0,0 +1,20 @@
"use strict";
import ArrayVisualization from './array_visualization';
import ZoomControl from './zoom_control';
import OverlayControl from './overlay_control';
import SeismicControl from './seismic_control';
import SubarrayDisplay from './subarray_display';
import AutoUpload from './auto_upload';
$(document).ready(function () {
AutoUpload();
let subarrayDisplay = new SubarrayDisplay();
subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data);
let arrayVisualization = new ArrayVisualization(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates);
arrayVisualization.init();
new ZoomControl(arrayVisualization).init($('#zoom_control'));
new OverlayControl(arrayVisualization).init($('#overlay_control'), $('#legend_container'));
new SeismicControl(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save"));
window.arrayVisualization = arrayVisualization;
});

View File

@@ -0,0 +1,43 @@
import $ from "jquery";
class OverlayControl {
constructor(arrayVisualization) {
this.visualization = arrayVisualization;
}
init(overlaySelector, legendSelector) {
let self = this;
this.setOverlay("ANCHOR", overlaySelector, legendSelector);
overlaySelector.find("#anchor_overlay").click(function () {
self.setOverlay("ANCHOR", overlaySelector, legendSelector);
});
overlaySelector.find("#all_overlay").click(function () {
self.setOverlay("ALL", overlaySelector, legendSelector);
});
}
setOverlay(overlay, overlaySelector, legendSelector) {
let selectedButton;
let selectedLegend;
if (overlay == "ANCHOR") {
selectedButton = overlaySelector.find("#anchor_overlay");
selectedLegend = legendSelector.find("img.anchors_mode");
} else if (overlay == "ALL") {
selectedButton = overlaySelector.find("#all_overlay");
selectedLegend = legendSelector.find("img.all_mode");
}
this.visualization.setOverlay(overlay);
overlaySelector.find('.overlay_toggle').removeClass("overlay_active");
selectedButton.addClass("overlay_active");
legendSelector.find('.legend').hide();
selectedLegend.show();
}
}
export default OverlayControl;

View File

@@ -0,0 +1,169 @@
import "../../../lib/easeljs.js";
import Colors from "./colors";
import $ from "jquery";
function Panel(panel, isDualTilt, scale = 10, treatCoordinatesAsCenterpoints = true) {
this.Container_constructor();
this.panel = panel;
this.isDualTilt = isDualTilt;
this.textOverlays = {};
this.selected = false;
this.scale = scale;
this.treatCoordinatesAsCenterpoints = treatCoordinatesAsCenterpoints;
this.setup();
}
const panelMethods = {
setup() {
if (this.treatCoordinatesAsCenterpoints) {
this.x = (this.panel.x - (this.panel.width / 2.0)) * this.scale;
this.y = (this.panel.y - (this.panel.height / 2.0)) * this.scale;
} else {
this.x = this.panel.x * this.scale;
this.y = this.panel.y * this.scale;
}
this.width = this.scale * this.panel.width;
this.height = this.scale * this.panel.height;
this.drawBackground();
},
drawBackground() {
if (this.background === undefined) {
this.background = new createjs.Shape();
} else {
this.removeChild(this.background);
}
let fillColor;
let borderColor;
let textColor;
let secondFillColor;
if (this.panel.data.seismic_anchors > 0 && this.panel.data.wind_anchors > 0) {
fillColor = Colors.seismic_background;
secondFillColor = Colors.wind_background;
textColor = Colors.dark_text;
borderColor = Colors.border;
} else if (this.panel.data.seismic_anchors > 0) {
fillColor = Colors.seismic_background;
textColor = Colors.dark_text;
borderColor = Colors.border;
} else if (this.panel.data.wind_anchors > 0) {
fillColor = Colors.wind_background;
textColor = Colors.dark_text;
borderColor = Colors.border;
} else {
fillColor = Colors.default_background;
textColor = Colors.light_text;
borderColor = fillColor;
}
if (this.selected) {
fillColor = Colors.selected_background;
textColor = Colors.dark_text;
borderColor = Colors.border;
}
this.textColor = textColor;
let width = this.width;
let height = this.height;
let borderWidth = 0.5;
// main background fill rectangle with white border
this.background.graphics.setStrokeStyle(borderWidth)
.beginStroke('white')
.beginFill(fillColor)
.drawRect(0, 0, width, height)
.endFill()
.endStroke();
// diagonal split background color for multiple anchor types
if (secondFillColor !== undefined) {
this.background.graphics.beginFill(secondFillColor)
.moveTo(borderWidth, borderWidth)
.lineTo(width - borderWidth, height - borderWidth)
.lineTo(width - borderWidth, borderWidth)
.closePath()
.endFill()
.endStroke();
}
// inner border for use with light background colors
this.background.graphics.beginStroke(borderColor)
.drawRect(borderWidth, borderWidth, width - 2 * borderWidth, height - 2 * borderWidth)
.endStroke();
// line drawing of dual-tilt indicator (the right-facing triangle)
if (this.isDualTilt) {
this.background.graphics.setStrokeStyle(0.25)
.beginStroke(Colors.border)
.moveTo(width / 2., borderWidth)
.lineTo(width / 2., height - borderWidth)
.lineTo(width - borderWidth, height / 2.)
.closePath()
.endStroke();
}
this.addChildAt(this.background, 0);
},
select() {
this.selected = true;
this.drawBackground();
this.redrawOverlays();
},
deselect() {
this.selected = false;
this.drawBackground();
this.redrawOverlays();
},
setOverlay(overlay) {
let data = this.panel.data;
let self = this;
$.each(this.textOverlays, function (idx, overlay) {
self.removeChild(overlay);
});
this.textOverlays = {};
if (overlay == "ALL") {
this.addOverlay('panel_id', data.panel_id, "2.5px", 0.3, 0.55);
this.addOverlay('wind_zones', data.wind_zones, '2.5px', 0.7, 0.55);
this.addOverlay('ballast', data.ballast, "1.5px", 0.125, 0.25);
this.addOverlay('wind_anchors', data.wind_anchors, "1.5px", 0.125, 0.8);
this.addOverlay('seismic_anchors', "S".repeat(data.seismic_anchors), "1.5px", 0.35, 0.8);
this.addOverlay('cross_trays', data.cross_trays, "1.5px", 0.6, 0.25);
this.addOverlay('link_trays', data.link_trays, "1.5px", 0.85, 0.25);
this.addOverlay('subarray', data.subarray, "1.5px", 0.6, 0.8);
this.addOverlay('psf', data.psf.toPrecision(3), "1.5px", 0.35, 0.25);
this.addOverlay('panel_type', data.panel_type, "1.5px", 0.85, 0.8);
} else if (overlay == 'ANCHOR') {
this.addOverlay('ballast', data.ballast, "2.5px", 0.3, 0.55);
this.addOverlay('anchors', data.seismic_anchors + data.wind_anchors, '2.5px', 0.7, 0.55);
}
this.currentOverlay = overlay;
},
addOverlay(name, text, fontSize, relativeX, relativeY) {
let overlay = new createjs.Text();
overlay.text = text;
overlay.font = fontSize + " sans-serif";
overlay.x = this.width * relativeX;
overlay.y = this.height * relativeY;
overlay.textAlign = "center";
overlay.textBaseline = "middle";
overlay.color = this.textColor;
overlay.maxWidth = this.width * 0.5;
this.textOverlays[name] = overlay;
this.addChild(overlay);
},
redrawOverlays() {
this.setOverlay(this.currentOverlay);
}
};
Object.assign(Panel.prototype, createjs.extend(Panel, createjs.Container));
Object.assign(Panel.prototype, panelMethods);
Panel = createjs.promote(Panel, "Container");
export {Panel};

View File

@@ -0,0 +1,73 @@
import $ from 'jquery';
class SeismicControl {
constructor(arrayVisualization, subarrayDisplay) {
this.visualization = arrayVisualization;
this.subarrayDisplay = subarrayDisplay;
}
init(seismicSelector, bannerSelector) {
let self = this;
seismicSelector.find("#add_seismic").click(function () {
self.visualization.addSeismicAnchor();
});
seismicSelector.find("#remove_seismic").click(function () {
self.visualization.removeSeismicAnchor();
});
seismicSelector.find("#save_seismic_changes").click(function () {
let updatedData = self.visualization.panelData.map((panel) => {
return {
panel_id: panel.data.panel_id,
seismic_anchors: panel.data.seismic_anchors
};
});
$.ajax({
url: "/api/update_panel_data",
data: JSON.stringify(updatedData),
contentType: "application/json; charset=utf-8",
method: 'POST'
})
.then(function (data) {
bannerSelector.removeClass("seismic_error");
bannerSelector.removeClass("seismic_success");
bannerSelector.removeClass("hidden");
let seismicClass;
let bannerText;
bannerSelector.find(".circle").removeClass("icon-ok");
bannerSelector.find(".circle").text("!");
if (data.status !== undefined && data.status == "success") {
seismicClass = "seismic_success";
bannerText = "Changes to the Seismic Anchors have been successfully saved!";
bannerSelector.find(".circle").addClass("icon-ok");
bannerSelector.find(".circle").text("");
self.visualization.refreshPanels(data.panel_data);
self.subarrayDisplay.didUpdateSubarrayData(data.subarray_data);
} else if (data.error !== undefined) {
seismicClass = "seismic_error";
bannerText = data.error;
self.subarrayDisplay.didUpdateSubarrayData(data.subarray_data);
} else {
seismicClass = "seismic_error";
bannerText = "Unknown error, please try again.";
}
bannerSelector.addClass(seismicClass);
bannerSelector.find(".seismic_save_message").text(bannerText);
});
});
bannerSelector.find(".dismiss_button").click(function () {
bannerSelector.addClass("hidden");
bannerSelector.removeClass("seismic_error");
bannerSelector.removeClass("seismic_success");
});
}
}
export default SeismicControl;

View File

@@ -0,0 +1,49 @@
class SubarrayDisplay {
init(currentSeismicCountSelector, neededAnchorSelector, weightSelector, panelData) {
this.currentSeismicCountSelector = currentSeismicCountSelector;
this.neededAnchorSelector = neededAnchorSelector;
this.weightSelector = weightSelector;
this.didModifyPanels(panelData);
}
didModifyPanels(panelData) {
let seismicCount = this.computeSeismicAnchorCounts(panelData);
this.assignSeismicCountItems(seismicCount);
}
didUpdateSubarrayData(subarrayData) {
for (let i = 0; i < subarrayData.length; i++) {
let data = subarrayData[i];
let subarray = i + 2;
let neededAnchors = data.required_seismic_anchors;
let weight = data.weight;
this.neededAnchorSelector.find('td:nth-child(' + subarray + ')').text(neededAnchors);
this.weightSelector.find('td:nth-child(' + subarray + ')').text(weight.toLocaleString());
}
}
computeSeismicAnchorCounts(panelData) {
let seismicCount = {};
for (let i = 0; i < panelData.length; i++) {
let panel = panelData[i].data;
if (seismicCount[panel.subarray] === undefined) {
seismicCount[panel.subarray] = 0;
}
seismicCount[panel.subarray] += panel.seismic_anchors;
}
return seismicCount
}
assignSeismicCountItems(seismicAnchorCounts) {
for (var subarray in seismicAnchorCounts) {
if (seismicAnchorCounts.hasOwnProperty(subarray)) {
const childId = parseInt(Object.keys(seismicAnchorCounts).indexOf(subarray)) + 2;
this.currentSeismicCountSelector.find(`td:nth-child(${childId})`).text(seismicAnchorCounts[subarray]);
}
}
}
}
export default SubarrayDisplay;

View File

@@ -0,0 +1,48 @@
import $ from "jquery";
class ZoomControl {
constructor(arrayVisualization) {
this.visualization = arrayVisualization;
this.zooms = [1, 2, 3, 4, 5, 6, 7];
this.zoomLevel = 0;
}
init(zoomSelector) {
let self = this;
zoomSelector.find("#increase_zoom").click(function () {
if (self.zoomLevel != (self.zooms.length - 1)) {
self.setZoom(self.zoomLevel + 1, zoomSelector);
}
});
zoomSelector.find("#decrease_zoom").click(function () {
if (self.zoomLevel != 0) {
self.setZoom(self.zoomLevel - 1, zoomSelector);
}
});
for (let i = 0; i < self.zooms.length; i++) {
zoomSelector.find("#zoom_indicator_" + i).click(function () {
self.setZoom(i, zoomSelector);
});
}
self.setZoom(0, zoomSelector);
}
setZoom(zoomLevel, zoomSelector) {
const oldZoom = this.zoomLevel;
const totalZooms = this.zooms.length;
this.zoomLevel = zoomLevel % totalZooms;
const zoom = this.zooms[this.zoomLevel];
const zoom_percentage = Math.round((100 / (totalZooms - 1)) * this.zoomLevel);
this.visualization.setZoom(zoom);
zoomSelector.find("#zoom_indicator_" + oldZoom).removeClass("zoom_active");
zoomSelector.find("#zoom_indicator_" + this.zoomLevel).addClass("zoom_active");
zoomSelector.find("#zoom_level").text(zoom_percentage + "%");
}
}
export default ZoomControl;

470
helix/main.py Normal file
View File

@@ -0,0 +1,470 @@
import os
import requests
import rollbar
import rollbar.contrib.flask
from flask import Flask, request, make_response, session, render_template, \
redirect, url_for
from flask import got_request_exception
from flask.ext import assets
from webassets.filter import get_filter
from helix.Services.doc_gen_service import DocGenService
from helix.Services.dxf_helper import DXFHelper
from helix.Services.dxf_service import DXFService
from helix.api.api import api
from helix.calculators.calculator import Calculator
from helix.constants import redis_constant, sql_constant
from helix.constants.inverter_type import InverterType
from helix.constants.system_type import SystemType
from helix.csv_builder import CsvBuilder
from helix.doc_gen_params_builder import DocGenParamsBuilder
from helix.forms.ebom_form import EbomForm, InverterBrandForm, \
SupervisorForm, SupervisorFormSMA, StandAloneInverterFormSMA, \
StandAloneInverterFormDelta
from helix.forms.input_form import InputForm, ArrayForm, TestDXFForm
from helix.models.dxf.dxf_error import DXFError, OldDxfFormatException
from helix.presenters.image_presenter import ImagePresenter
from helix.presenters.panel_presenter import ProjectPresenter
from helix.session_manager import SessionManager
from helix.validators.file_validator import FileValidator, FileType
from helix.validators.subarray_validator import SubarrayValidator
app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api')
app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey')
app.config['PROFILE'] = True
assets_env = assets.Environment(app)
assets_env.init_app(app)
assets_env.load_path = [
os.path.join(os.path.dirname(__file__), 'scss')
]
sass = get_filter('scss')
sass.load_paths = [os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scss')]
assets_env.register(
'main_css',
assets.Bundle(
'*.scss',
filters=(sass,),
output='css/main.css'
)
)
@app.before_first_request
def init_rollbar():
# Do nothing unless Rollbar is configured
if not os.getenv("ROLLBAR_ACCESS_TOKEN"):
return
rollbar.init(os.getenv("ROLLBAR_ACCESS_TOKEN"),
# Setup this var in heroku to distinguish errors from different envs
os.getenv("ROLLBAR_ENV", "development"),
root=os.path.dirname(os.path.realpath(__file__)),
allow_logging_basic_config=False)
got_request_exception.connect(rollbar.contrib.flask.report_exception, app)
@app.route("/")
def index():
return redirect(url_for('site_characterization'))
@app.route("/test_dxf/", methods=['GET', 'POST'])
def test_dxf():
form = TestDXFForm()
if form.validate_on_submit():
file = request.files['dxf_upload']
file_contents = file.read().decode('utf-8')
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_values = session_manager.user_values()
calculator = Calculator(user_values, calculate_panel_data=False)
l_b = calculator.L_B() * 12 # convert from feet to inches
try:
dxf_data = DXFService().parse(file_contents, user_values.module_system_constants(),
user_values.system_type(), l_b, DXFHelper(), SubarrayValidator())
dxf_data['panels'].sort(key=lambda p: p.id)
dxf_data['l_b'] = l_b
if not form.show_wind_zones.data:
dxf_data['lb_polygons'] = []
except DXFError as error:
form.dxf_upload.errors.append(error.message)
dxf_data = {}
else:
dxf_data = {}
dxf_data['colors'] = [
'red',
'orange',
'yellow',
'green',
'blue',
'purple',
]
return render_template('test_dxf.html.jinja', context=dxf_data, form=form)
# wizard steps
@app.route("/site_characterization/", methods=['GET', 'POST'])
def site_characterization():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
site_info_form = InputForm()
context = session_manager.context()
context['current_step'] = 1
context['javascripts'] = ['site_characterization']
if site_info_form.validate_on_submit():
session_manager.save_form_submission(request.form)
return redirect(url_for('summary'))
if request.method != 'POST':
session_manager.fill_saved_values_in_form(site_info_form)
db_session.close()
return render_template('site_characterization.html.jinja',
context=context,
form=site_info_form)
@app.route("/summary/")
def summary():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
context = session_manager.context()
context['current_step'] = 2
if context['site_data_available']:
user_values = session_manager.user_values()
calculator = Calculator(user_values, calculate_panel_data=False)
context['wind_zones'] = user_values.system_type().system_constants().wind_zones
context['summary_table'] = calculator.summary_table()
context['minimum_array_sizes'] = calculator.minimum_array_sizes()
context['l_b'] = round(calculator.L_B(), 2)
context['k_z'] = round(calculator.k_z(), 2)
context['q_z'] = round(calculator.q_z(), 2)
context['warning_messages'] = set()
context['javascripts'] = ['https://cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.js']
for panel_type, values in context['summary_table'].items():
for panel_type_warnings in values['warnings']:
for warning in panel_type_warnings:
context['warning_messages'].add(warning)
else:
context['no_proceed'] = True
db_session.close()
return render_template('site_summary.html.jinja', context=context)
@app.route("/array_summary/", methods=['GET', 'POST'])
def array_summary():
"""This endpoint allows you to upload a file.
The content of the file is parsed, and then
several objects are created that aid in the validation
of the uploaded file.
"""
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
context = session_manager.context()
context['current_step'] = 3
array_form = ArrayForm()
if array_form.validate_on_submit():
if 'file_upload' in request.files and request.files['file_upload'].filename:
validator = FileValidator(session_manager.user_values())
file = request.files['file_upload']
file_contents = validator.obtain_stream(file)
validation_error = validator.validate(file_contents, file,
FileType.Csv)
if not validation_error:
session_manager.save_uploaded_file(file_contents,
cad_file_name=file.filename)
session_manager.save_buildings_polygons([]) # no buildings in the csv file
session_manager.save_is_drawing_inaccurate(False)
return redirect(url_for('array_summary'))
else:
array_form.file_upload.errors.append(validation_error.format_error_message())
elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename:
file = request.files['dxf_upload']
user_values = session_manager.user_values()
calculator = Calculator(user_values, calculate_panel_data=False)
validator = FileValidator(user_values)
file_contents = validator.obtain_stream(file)
validation_error = validator.validate(file_contents, file,
FileType.AuroraDxf)
if validation_error:
error_msg = validation_error.format_error_message()
array_form.dxf_upload.errors.append(error_msg)
else:
try:
module_constants = user_values.module_system_constants()
dxf_data = DXFService().parse(file_contents,
module_constants,
user_values.system_type(),
calculator.L_B() * 12,
DXFHelper(),
SubarrayValidator())
csv = CsvBuilder().build_cad_output(dxf_data['panels'])
session_manager.save_uploaded_file(csv, dxf_file_name=file.filename)
buildings = dxf_data['buildings']
session_manager.save_buildings_polygons(buildings)
session_manager.save_is_drawing_inaccurate(dxf_data['is_panel_drawing_inaccurate'])
return redirect(url_for('array_summary'))
except DXFError as error:
array_form.dxf_upload.errors.append(error.message)
except OldDxfFormatException as error:
array_form.dxf_upload.errors.append(error.message)
elif context['csv_available']:
return redirect(url_for('power_station_configuration'))
else:
array_form.file_upload.errors.append('Please provide a .txt file!')
context['javascripts'] = ['array_summary_bundle']
context['hide_submit'] = True
context['cad_file_name'] = ''
context['dxf_file_name'] = ''
if context['site_data_available'] and context['csv_available']:
user_values = session_manager.user_values()
calculator = Calculator(user_values)
system_type = user_values.system_type()
module_type = user_values.module_type()
project_presenter = ProjectPresenter(system_type, module_type)
context['wind_zones'] = system_type.system_constants().wind_zones
context['summary_table'] = calculator.summary_table()
context['minimum_array_sizes'] = calculator.minimum_array_sizes()
context['seismic_anchors'] = calculator.subarray_summary()
context['summary_values'] = calculator.summary_values()
panels = calculator.get_computed_csv_columns()
context['panel_array'] = project_presenter.get_panel_data(panels,
calculator.subarrays,
project_presenter.get_max_y(
calculator.buildings_for_drawing,
panels))
context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing)
context['override_form'] = True
context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data'
context['dxf_file_name'] = session_manager.site.dxf_file_name or 'Upload System DXF'
context['is_drawing_inaccurate'] = session_manager.user_values().is_panel_drawing_inaccurate()
context['inaccurate_drawing_warning'] = 'The subarrays in this design are not parallel to each other, \
and the graphical representation on this page may not be accurate.'
elif not context['site_data_available']:
context['no_proceed'] = True
db_session.close()
return render_template('array_summary.html.jinja', context=context, form=array_form)
@app.route("/power_station_configuration/", methods=['GET', 'POST'])
def power_station_configuration():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
context = session_manager.context()
if session_manager.site:
system_type = session_manager.user_values().system_type()
else:
system_type = None
inverter_brand_form = InverterBrandForm()
if request.method != 'POST' or request.form['form_id'] != 'inverter_brand_form':
inverter_brand_form.populate_choices(context['inverter_brands'])
is_delta = inverter_brand_form.is_delta()
if is_delta:
ebom_form = EbomForm()
standalone_inverter_form = StandAloneInverterFormDelta()
standalone_inverter_form.populate_choices()
supervisor_form = SupervisorForm()
else:
ebom_form = EbomForm()
ebom_form.update_inverter_strings_choices(system_type)
standalone_inverter_form = StandAloneInverterFormSMA()
standalone_inverter_form.update_inverter_strings_choices(system_type)
standalone_inverter_form.populate_choices(context['power_stations'], context['standalone_inverters'])
supervisor_form = SupervisorFormSMA()
supervisor_form.populate_choices(context['power_stations'], context['power_monitors'])
if request.method == 'POST':
if request.form['form_id'] == 'inverter_brand_form' and inverter_brand_form.validate_on_submit():
session_manager.delete_power_station_config_data()
session_manager.save_or_update_inverter_brands(request.form)
return redirect("/power_station_configuration/")
elif request.form['form_id'] == 'power_station_form' and ebom_form.validate_on_submit():
session_manager.save_or_update_power_station(request.form)
return redirect("/power_station_configuration/")
elif request.form['form_id'] == 'standalone_inverter_form' and standalone_inverter_form.validate_on_submit():
session_manager.save_or_update_standalone_inverter(request.form)
return redirect("/power_station_configuration/")
elif request.form['form_id'] == 'supervisor_form' and supervisor_form.validate_on_submit():
session_manager.save_or_update_supervisor_monitor(request.form)
return redirect("/power_station_configuration")
ebom_form.power_station_description.data = "Power Station " + str(len(context['power_stations']) + 1)
inverter_enum = InverterType.DELTA if is_delta else InverterType.SMA
string_limits = {}
string_defaults = {}
for i_e in inverter_enum.all():
string_limits[i_e.value] = list(i_e.valid_string_ranges) if i_e.valid_string_ranges is not None else []
string_defaults[i_e.value] = i_e.default_string
context['standalone_inverter_string_limits'] = string_limits
context['standalone_inverter_string_defaults'] = string_defaults
context['current_step'] = 4
context['javascripts'] = ['power_station_configuration']
db_session.close()
return render_template('power_station_configuration.html.jinja',
context=context,
ebom_form=ebom_form,
is_delta=is_delta,
inverter_brand_form=inverter_brand_form,
standalone_inverter_form=standalone_inverter_form,
supervisor_monitor_form=supervisor_form)
@app.route("/download/")
def download():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
context = session_manager.context()
context['current_step'] = 5
db_session.close()
return render_template('download.html.jinja', context=context)
@app.route("/delete_power_station/<uuid>")
def delete_power_station(uuid):
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
session_manager.delete_power_station(uuid)
db_session.close()
return redirect(url_for('power_station_configuration'))
@app.route('/delete_standalone_inverter/<uuid>')
def delete_standalone_inverter(uuid):
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
session_manager.delete_standalone_inverter(uuid)
db_session.close()
return redirect(url_for('power_station_configuration'))
@app.route('/delete_supervisor_monitor/<uuid>')
def delete_supervisor_monitor(uuid):
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
session_manager.delete_supervisor_monitor(uuid)
db_session.close()
return redirect(url_for('power_station_configuration'))
@app.route("/result")
def result():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_values = session_manager.user_values()
calculator = Calculator(user_values)
csv = CsvBuilder().build_cad_output(calculator.get_computed_csv_columns())
response = make_response(csv)
response.headers["Content-Disposition"] = "attachment; filename=%s_result.txt" % user_values.project_name_no_spaces()
db_session.close()
return response
@app.route("/documentation")
def documentation():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_values = session_manager.user_values()
calculator = Calculator(user_values)
image_presenter = ImagePresenter(user_values.system_type(), user_values.module_type())
doc_gen_service = DocGenService(requests, DocGenParamsBuilder(user_values, user_values.system_type(), calculator, image_presenter))
document = doc_gen_service.generate()
response = make_response(document)
response.headers["Content-Disposition"] = "attachment; filename=%s_documentation.pdf" % user_values.project_name_no_spaces()
db_session.close()
return response
@app.route("/bom")
def bom():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_values = session_manager.user_values()
calculator = Calculator(user_values)
csv = CsvBuilder().build_bom_output(calculator.compute_bom())
response = make_response(csv)
response.headers["Content-Disposition"] = "attachment; filename=%s_bom.txt" % user_values.project_name_no_spaces()
db_session.close()
return response
@app.route("/exposure_categories")
def exposure_categories():
db_session = sql_constant.sql_session_maker()
context = SessionManager(session, redis_constant.redis_store, db_session).context()
db_session.close()
return render_template('exposure_categories.html.jinja', context=context)
@app.route("/helix_documentation")
def helix_documentation():
db_session = sql_constant.sql_session_maker()
context = SessionManager(session, redis_constant.redis_store, db_session).context()
db_session.close()
return render_template('helix_documentation.jinja', context=context)
@app.template_filter('format_number')
def format_number(number):
return "{:,g}".format(number)
@app.template_filter('is_dual_tilt')
def is_dual_tilt(system_type):
return system_type == SystemType.dualTilt
@app.context_processor
def power_station_has_monitor():
def _power_station_has_monitor(power_station, monitors):
for monitor in monitors:
if monitor['power_source'][1] == power_station['power_station_id']:
return True
return False
return dict(power_station_has_monitor=_power_station_has_monitor)
@app.context_processor
def enum():
def _enum(item):
return enumerate(item)
return dict(enum=_enum)
def main():
host = '0.0.0.0'
port = int(os.getenv('PORT', 5000))
app.run(host=host, port=port, debug=bool(os.getenv("FLASK_DEBUG", False)))
@app.route("/fail-test")
def fail_test():
raise RuntimeError("This is a test failure, ignore it")
if __name__ == "__main__":
main()

0
helix/models/__init__.py Normal file
View File

View File

@@ -0,0 +1,87 @@
import json
from math import hypot
import math
from helix.functions import fequal
class Coordinate(object):
def __init__(self, x, y, rotation=0., calculate_rounding=True):
self.x = x
self.y = y
self.rotation = rotation
if calculate_rounding:
self.__rounded_x = round(x, 3)
self.__rounded_y = round(y, 3)
else:
self.__rounded_x = x
self.__rounded_y = y
def __eq__(self, other):
if self is other:
return True
if not isinstance(other, self.__class__):
return False
if fequal(self.rotation, other.rotation, delta=1e-3):
return fequal(self.x, other.x, delta=1e-3) and fequal(self.y, other.y, delta=1e-3)
return False
@property
def dictionary(self):
return {"x": self.x, "y": self.y, "rotation": self.rotation}
def __repr__(self):
return json.dumps(self.dictionary, sort_keys=True)
def __sub__(self, other):
if fequal(self.rotation, other.rotation, delta=1e-1):
return Coordinate(self.x - other.x, self.y - other.y, self.rotation)
else:
raise ValueError
def __add__(self, other):
if abs(self.rotation - other.rotation) < 1e-3:
return Coordinate(self.x + other.x, self.y + other.y, self.rotation, calculate_rounding=False)
else:
raise ValueError
def __abs__(self):
return self.length()
def __lt__(self, other):
if self == other:
return False
if self.y < other.y:
return True
elif other.y < self.y:
return False
return self.x < other.x
def __round__(self, n=0):
return Coordinate(round(self.x, n), round(self.y, n), self.rotation)
def __hash__(self):
return hash(self.__rounded_x) ^ hash(self.__rounded_y) ^ hash(self.rotation)
# Returns a Coordinate based on self, rotated by self's rotation
def rotate(self):
rotation = math.radians(self.rotation)
x = self.x * math.cos(rotation) - self.y * math.sin(rotation)
y = self.x * math.sin(rotation) + self.y * math.cos(rotation)
return Coordinate(x, y)
# Returns a Coordinate based on self, rotated by self's negative rotation
def unrotate(self):
rotation = math.radians(-self.rotation)
x = self.x * math.cos(rotation) - self.y * math.sin(rotation)
y = self.x * math.sin(rotation) + self.y * math.cos(rotation)
return Coordinate(x, y)
def scale(self, x, y):
return Coordinate(self.x * x, self.y * y, self.rotation)
def neg_translate(self, other):
return Coordinate(self.x - other.x, self.y - other.y, self.rotation)
def length(self):
return hypot(self.x, self.y)

View File

@@ -0,0 +1 @@
__author__ = 'pivotal'

View File

@@ -0,0 +1,8 @@
class DXFError(Exception):
def __init__(self, message):
self.message = message
class OldDxfFormatException(Exception):
def __init__(self, message):
self.message = message

View File

@@ -0,0 +1,47 @@
from enum import Enum
class GraphDirection(Enum):
North = (0, 1)
NorthEast = (1, 1)
East = (1, 0)
SouthEast = (1, -1)
South = (0, -1)
SouthWest = (-1, -1)
West = (-1, 0)
NorthWest = (-1, 1)
@classmethod
def values(cls):
return [
cls.North.value,
cls.NorthEast.value,
cls.East.value,
cls.SouthEast.value,
cls.South.value,
cls.SouthWest.value,
cls.West.value,
cls.NorthWest.value
]
@classmethod
def ordinal_directions(cls):
return [
cls.North,
cls.East,
cls.South,
cls.West
]
def opposite_direction(self):
return {
GraphDirection.North: GraphDirection.South,
GraphDirection.NorthEast: GraphDirection.SouthWest,
GraphDirection.East: GraphDirection.West,
GraphDirection.SouthEast: GraphDirection.NorthWest,
GraphDirection.South: GraphDirection.North,
GraphDirection.SouthWest: GraphDirection.NorthEast,
GraphDirection.West: GraphDirection.East,
GraphDirection.NorthWest: GraphDirection.SouthEast
}[self]

View File

@@ -0,0 +1,68 @@
from helix.models.dxf.graph_direction import GraphDirection
class GraphNode(object):
def __init__(self, panel, x_spacing, y_spacing):
self.neighbors = {}
self.neighbors = {
GraphDirection.North: None,
GraphDirection.NorthEast: None,
GraphDirection.East: None,
GraphDirection.SouthEast: None,
GraphDirection.South: None,
GraphDirection.SouthWest: None,
GraphDirection.West: None,
GraphDirection.NorthWest: None
}
self.panel = panel
self.x_spacing = x_spacing
self.y_spacing = y_spacing
@property
def coordinate(self):
return self.panel.coordinate
def has_existing_neighbor(self, direction):
return self.neighbors.get(direction) is not None
def add_neighbor(self, other_node, direction):
if other_node is not None:
if direction:
self.neighbors[direction] = other_node
opposite_direction = direction.opposite_direction()
other_node.neighbors[opposite_direction] = self
def neighboring_nodes(self):
return [neighbor for neighbor in self.neighbors.values() if neighbor is not None]
def ordinal_neighbors(self):
neighbors = {}
for direction in GraphDirection.ordinal_directions():
if self.neighbors.get(direction):
neighbors[direction] = self.neighbors[direction]
return neighbors
def __hash__(self):
return self.panel.coordinate.__hash__()
def __repr__(self):
neighbors = {}
for key, value in self.neighbors.items():
if value is not None:
neighbors[key] = value.panel
return str(self.panel) + " - " + str(neighbors)
def __eq__(self, other):
if self is other:
return True
if not self.panel == other.panel and self.x_spacing != other.x_spacing and self.y_spacing != other.y_spacing:
return False
for key, value in self.neighbors.items():
other_neighbor = other.neighbors[key]
if value is None and other_neighbor is None:
continue
elif value is None or other_neighbor is None:
return False
elif value.panel != other_neighbor.panel:
return False
return True

View File

@@ -0,0 +1,41 @@
from helix.helpers.nodequadtree import Bounds, NodeQuadTree
class GraphNodeStore(list):
def __init__(self):
super().__init__()
self.variance = 0.2
self.first = True
self.quadTree = None
def add_node(self, node):
self.append(node)
if self.first:
self.left = self.right = node.coordinate.x
self.top = self.bottom = node.coordinate.y
self.first = False
else:
self.left = min(self.left, node.coordinate.x)
self.right = max(self.right, node.coordinate.x)
self.bottom = min(self.bottom, node.coordinate.y)
self.top = max(self.top, node.coordinate.y)
def distance_squared(self, node, coordinate):
dx = node.coordinate.x - coordinate.x
dy = node.coordinate.y - coordinate.y
return dx * dx + dy * dy
def find_coordinate(self, coordinate):
# create and populate the quadtree on first request
if self.quadTree is None:
self.quadTree = NodeQuadTree(1, Bounds(self.left, self.bottom, self.right - self.left, self.top - self.bottom), self.variance)
for node in self:
self.quadTree.insert(node)
del self[:]
possibilities = self.quadTree.retrieve(coordinate)
for node in possibilities:
if self.distance_squared(node, coordinate) <= self.variance ** 2:
return node
else:
return None

117
helix/models/dxf/polygon.py Normal file
View File

@@ -0,0 +1,117 @@
import math
from helix.functions import fequal
class Polygon(object):
def __init__(self, line=None, points=()):
if line:
self.points = [line.start, line.end]
elif points:
self.points = points
else:
self.points = []
def continues_with_line(self, line):
if not self.closed and line.start == self.points[-1]:
return True
return False
@property
def closed(self):
return len(self.points) != 1 and self.points[0] == self.points[-1]
def sorted_points(self):
return sorted(self.points, key=lambda x: x[0])
def determine_orientation(self):
points = self.sorted_points()
p1 = points[0]
other_points = sorted(points[1:], key=lambda x: (x[0] - p1[0]) ** 2 + (x[1] - p1[1]) ** 2)
p2 = other_points[1]
# other_points[0] is the point that (along with p1) defines the short edge of this rectangle
# other_points[1] defines the long edge of this rectangle
# other_points[2] is diagonally across from p1. Not useful for defining edges.
x = p2[0] - p1[0]
y = p2[1] - p1[1]
return math.degrees(math.atan2(y, x))
def __do_something_with_long_edges__(self, module, fn, pair_spacing):
def cmp_point(p1, p2, pair_spacing):
dx = p1[0] - p2[0]
dy = p1[1] - p2[1]
d = math.sqrt(dx * dx + dy * dy)
allowedVariance = 0.1
if pair_spacing is None:
return d < allowedVariance
else:
d = math.fabs(d - pair_spacing)
return d <= allowedVariance
p1 = self.points[0]
other_points = sorted(self.points[1:], key=lambda x: (x[0] - p1[0]) ** 2 + (x[1] - p1[1]) ** 2)
self_long_edges = [sorted([p1, other_points[1]],), sorted([other_points[0], other_points[2]],)]
p2 = module.points[0]
other_points = sorted(module.points[1:], key=lambda x: (x[0] - p2[0]) ** 2 + (x[1] - p2[1]) ** 2)
other_long_edges = [sorted([p2, other_points[1]],), sorted([other_points[0], other_points[2]],)]
for edge in self_long_edges:
for other_edge in other_long_edges:
if cmp_point(edge[0], other_edge[0], pair_spacing) and\
cmp_point(edge[1], other_edge[1], pair_spacing):
fn(self, module, edge, other_edge)
return True
return False
def shares_module_on_long_edge(self, module, pair_spacing):
def on_match_edges(this, other, this_edges, other_edges):
pass
return self.__do_something_with_long_edges__(module, on_match_edges, pair_spacing)
def consolidate_with(self, pair, pair_spacing):
def on_match_edges(this, other, this_edges, other_edges):
this.points.remove(this_edges[0])
this.points.remove(this_edges[1])
other.points.remove(other_edges[0])
other.points.remove(other_edges[1])
this.points += pair.points
self.__do_something_with_long_edges__(pair, on_match_edges,
pair_spacing)
def scale(self, scale_x=1, scale_y=1):
polygon = Polygon()
polygon.points = [(x * scale_x, y * scale_y) for x, y in self.points]
return polygon
def svg_points(self, array_size):
value_string = ""
if len(self.points) == 4:
p0 = self.points[0]
points = sorted(self.points, key=lambda x: (x[0] - p0[0]) ** 2 + (x[1] - p0[1]) ** 2)
sorted_points = [p0, points[1], points[3], points[2]]
else:
sorted_points = self.points
for point in sorted_points:
value_string += "%f,%f " % (point[0], array_size[1] - point[1])
return value_string
def __repr__(self):
return str([(round(p[0], 3), round(p[1], 3)) for p in self.points])
def __eq__(self, other):
if self is other:
return True
if not isinstance(other, self.__class__):
return False
if len(self.points) != len(other.points):
return False
for idx, point in enumerate(self.points):
other_point = other.points[idx]
for variable_index in range(len(point)):
if not fequal(point[variable_index], other_point[variable_index]):
return False
return True

130
helix/models/panel.py Normal file
View File

@@ -0,0 +1,130 @@
from enum import Enum
import json
from helix.constants.panel_type import PanelType
from helix.functions import fequal
from helix.models.coordinate import Coordinate
class PanelData(Enum):
Handle = 'HANDLE'
Blockname = 'BLOCKNAME'
Subarray = 'SUBARRAY'
PanelType = 'POS'
WindZone = 'WIND'
Ballast = 'BAL'
LinkTray = 'LT_CALCULATED'
CrossTray = 'XTRAY'
WindAnchor = 'ANC'
SeismicAnchor = 'SEISMIC'
Coordinate = 'COORDINATE'
Pressure = 'PSF'
Id = 'ID'
PresentedLinkTray = 'LTRAY'
Xcoord = 'XCOORD'
Ycoord = 'YCOORD'
Rotation = 'ANGLE'
FuzzyWindZone = 'FUZZYWINDZONE'
class PanelWarnings(Enum):
MaxPsf = 'The values highlighted are panels that exceed our UL listed load limit. Please do not place panels in these areas to avoid exceeding the listed limit.'
class Panel(object):
def __init__(self, handle=None, blockname=None, subarray=None, panel_type=None, wind_zone=None, ballast=None,
link_tray=None, cross_tray=None, wind_anchors=None, seismic_anchors=None, coordinate=None,
pressure=None, id=None, presented_link_tray=None, original_coordinate=None, fuzzy_wind_zone=False,
warnings=None):
self.handle = handle
self.blockname = blockname
self.subarray = subarray
self.panel_type = panel_type
self.wind_zone = wind_zone
self.ballast = ballast
self.link_tray = link_tray
self.cross_tray = cross_tray
self.wind_anchors = wind_anchors
self.seismic_anchors = seismic_anchors
# this field after DXF parsing and before serialization into CSV contains translated coordinates (all positive)
# and after deserialization from CSV contains original coordinates - same as original_coordinate field
self.coordinate = coordinate
self.original_coordinate = original_coordinate
self.pressure = pressure
self.id = id
self.presented_link_tray = presented_link_tray
self.fuzzy_wind_zone = fuzzy_wind_zone
self.warnings = warnings or []
def merge(self, other):
if not isinstance(other, self.__class__):
return self
d = {}
for key, data in self.__dict__.items():
if data is not None:
d[key] = data
else:
d[key] = other.__dict__.get(key)
panel = Panel()
panel.__dict__.update(d)
return panel
def is_subset(self, other):
if not isinstance(other, self.__class__):
return False
for key, value in self.__dict__.items():
if value is None:
continue
if other.__dict__[key] != value:
return False
return True
def almost_equal(self, other, decimal=6):
if not isinstance(other, self.__class__):
return False
if self.pressure is not None and other.pressure is not None:
if not fequal(self.pressure, other.pressure, delta=(10 ** (-decimal))):
print("Pressures are not equal to within %d decimal places, got %f, expected %f" % (decimal, self.pressure, other.pressure))
return False
elif self.pressure != other.pressure:
return False
for key, value in self.__dict__.items():
if key == 'pressure':
continue
elif other.__dict__.get(key) != value:
print("Expected %s to be equal, got %s, expected %s" % (key, str(value), str(other.__dict__.get(key))))
return False
return True
def __deepcopy__(self, _):
return Panel(handle=self.handle,
blockname=self.blockname,
subarray=self.subarray,
panel_type=self.panel_type,
wind_zone=self.wind_zone,
ballast=self.ballast,
link_tray=self.link_tray,
cross_tray=self.cross_tray,
wind_anchors=self.wind_anchors,
seismic_anchors=self.seismic_anchors,
coordinate=self.coordinate,
original_coordinate=self.original_coordinate,
pressure=self.pressure,
id=self.id,
presented_link_tray=self.presented_link_tray,
fuzzy_wind_zone=self.fuzzy_wind_zone)
def __eq__(self, other):
if self is other:
return True
return self.almost_equal(other, decimal=3)
def __repr__(self):
def json_forcer(x):
if isinstance(x, PanelType):
return x.value
if isinstance(x, Coordinate):
return x.dictionary
return x.__dict__
d = {key: value for (key, value) in self.__dict__.items() if value is not None}
return json.dumps(d, sort_keys=True, default=json_forcer)

View File

View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, ForeignKey
from helix.models.sql.shared_sql_base import Base
class InverterBrand(Base):
__tablename__ = 'inverter_brands'
id = Column(Integer, primary_key=True)
site_id = Column(Integer, ForeignKey('sites.id'), primary_key=True)
def to_json(self):
return {
'inverter_brand_id': self.id,
}

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, Enum, Boolean, ForeignKey, CheckConstraint
from helix.constants.inverter_type import InverterType
from helix.models.sql.shared_sql_base import Base
class Inverter(Base):
__tablename__ = 'inverters'
id = Column(Integer, primary_key=True)
model = Column(Enum(*map(lambda x: str(x.value), InverterType.all()), name='invertertype'), nullable=False)
strings_per_inverter = Column(Integer, nullable=False)
sunshade = Column(Boolean)
dc_switch = Column(Boolean)
splice_box = Column(Boolean)
power_station_id = Column(Integer, ForeignKey('power_stations.id'))
standalone_inverter_id = Column(Integer, ForeignKey('standalone_inverters.id', ondelete='CASCADE'))
__table_args__ = (
CheckConstraint('(power_station_id IS NULL != standalone_inverter_id IS NULL)'),
)
def to_json(self):
inverter_type = InverterType.SMA if int(self.model) in InverterType.SMA.all() else InverterType.DELTA
return {
'model': inverter_type(int(self.model)),
'strings_per_inverter': self.strings_per_inverter,
'sunshade': self.sunshade,
'dc_switch': self.dc_switch,
'splice_box': self.splice_box,
}

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from helix.models.sql.shared_sql_base import Base
class PowerMonitor(Base):
__tablename__ = 'power_monitors'
id = Column(Integer, primary_key=True)
site_id = Column(Integer, ForeignKey('sites.id'))
power_station_id = Column(Integer, ForeignKey('power_stations.id'))
power_station = relationship("PowerStation")
def to_json(self):
if self.power_station:
power_source = (self.power_station.description, self.power_station.id)
else:
power_source = ('Switch Gear/External', None)
return {
'monitor_id': self.id,
'power_source': power_source
}

View File

@@ -0,0 +1,24 @@
from sqlalchemy import Column, Integer, Unicode, ForeignKey
from sqlalchemy.orm import relationship
from helix.models.sql.inverters import Inverter
from helix.models.sql.shared_sql_base import Base
class PowerStation(Base):
__tablename__ = 'power_stations'
id = Column(Integer, primary_key=True)
site_id = Column(Integer, ForeignKey('sites.id'))
quantity = Column(Integer, nullable=False)
ac_run_length = Column(Integer, nullable=False)
description = Column(Unicode, nullable=False)
inverters = relationship(Inverter.__name__, backref="power_stations", cascade="save-update, merge, delete")
def to_json(self):
return {
'inverter_quantity': len(self.inverters),
'power_station_quantity': self.quantity,
'power_station_description': self.description,
'power_station_id': self.id,
'ac_run_length': self.ac_run_length,
'inverters': [inverter.to_json() for inverter in self.inverters]
}

View File

@@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

41
helix/models/sql/sites.py Normal file
View File

@@ -0,0 +1,41 @@
from sqlalchemy import Column, Integer, Unicode, Float, Enum, ForeignKey
from sqlalchemy.orm import relationship
from helix.constants.anchor_type import AnchorType
from helix.constants.module_type import ModuleType
from helix.constants.system_type import SystemType
from helix.models.sql.inverter_brands import InverterBrand
from helix.models.sql.power_monitors import PowerMonitor
from helix.models.sql.power_stations import PowerStation
from helix.models.sql.shared_sql_base import Base
from helix.models.sql.standalone_inverters import StandaloneInverter
class Site(Base):
__tablename__ = 'sites'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
project_name = Column(Unicode, nullable=False)
building_height = Column(Float, nullable=False)
building_width = Column(Float, nullable=False)
building_length = Column(Float, nullable=False)
parapet_height = Column(Float, nullable=False)
wind_speed = Column(Integer, nullable=False)
exposure_category = Column(Unicode, nullable=False)
exposure_transition_distance = Column(Integer)
ballast_block_weight = Column(Integer, nullable=False)
max_psf = Column(Float, nullable=False)
system_type = Column(Enum(SystemType.singleTilt.value, SystemType.dualTilt.value, name='SystemType'), nullable=False)
module_type = Column(Enum(ModuleType.Cell96.value, ModuleType.Cell128.value, ModuleType.PSeries.value, name='ModuleType'), nullable=False)
anchor_type = Column(Enum(AnchorType.OMG_PowerGrip.value, AnchorType.OMG_PowerGrip_Plus.value, AnchorType.EcoFasten.value, name='AnchorType'), nullable=False)
spectral_response = Column(Float, nullable=False)
seismic_importance_factor = Column(Float, nullable=False)
cad_file = Column(Unicode)
cad_file_name = Column(Unicode)
dxf_file = Column(Unicode)
dxf_file_name = Column(Unicode)
inverter_brands = relationship(InverterBrand.__name__, backref="site", cascade="save-update, merge, delete")
power_stations = relationship(PowerStation.__name__, backref="site", cascade="save-update, merge, delete")
standalone_inverters = relationship(StandaloneInverter.__name__, backref="site", cascade="save-update, merge, delete")
power_monitors = relationship(PowerMonitor.__name__, backref="site", cascade="save-update, merge, delete")

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship, backref
from helix.models.sql.inverters import Inverter
from helix.models.sql.power_stations import PowerStation
from helix.models.sql.shared_sql_base import Base
class StandaloneInverter(Base):
__tablename__ = 'standalone_inverters'
id = Column(Integer, primary_key=True)
site_id = Column(Integer, ForeignKey('sites.id'))
ac_run_length = Column(Integer, nullable=False)
inverter = relationship(Inverter.__name__,
backref=backref("standalone_inverters", uselist=False),
cascade="save-update, merge, delete")
attachment_point_id = Column(Integer, ForeignKey('power_stations.id'))
attachment_point = relationship(PowerStation.__name__)
def to_json(self):
if self.attachment_point:
attachment_point = (self.attachment_point.description, self.attachment_point.id)
else:
attachment_point = ('Switch Gear', None)
return { **{
'standalone_inverter_id': self.id,
'ac_run_length': self.ac_run_length,
'attachment_point': attachment_point
}, **(self.inverter[0].to_json()) }

View File

@@ -0,0 +1,9 @@
from sqlalchemy import Column, Integer, Unicode
from helix.models.sql.shared_sql_base import Base
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(Unicode, nullable=False)
password_hash = Column(Unicode, nullable=False)

67
helix/models/subarray.py Normal file
View File

@@ -0,0 +1,67 @@
import json
from helix.functions import fequal
from helix.models.coordinate import Coordinate
class Subarray(object):
def __init__(self, subarray_number=None, origin=None, required_seismic_anchors=None, start_row=None, size=None,
weight=None, row_count=None, row_counted_geometrically=None, column_count=None,
column_counted_geometrically=None):
self.subarray_number = subarray_number
self.origin = origin
self.required_seismic_anchors = required_seismic_anchors
self.start_row = start_row
self.size = size
self.weight = weight
self.row_count = row_count
self.row_counted_geometrically = row_counted_geometrically
self.column_count = column_count
self.column_counted_geometrically = column_counted_geometrically
def filter_data(self, required_data):
required_key_names = map(lambda x: x.subarray_key(), required_data)
d = {key: self.__dict__.get(key) for key in required_key_names}
subarray = Subarray()
subarray.__dict__.update(d)
return subarray
def is_subset(self, other):
if not isinstance(other, self.__class__):
return False
for key, value in self.__dict__.items():
if value is None:
continue
if other.__dict__[key] != value:
return False
return True
def almost_equal(self, other, decimal=6):
if not isinstance(other, self.__class__):
return False
if not fequal(self.weight, other.weight, delta=(10 ** (-decimal))):
print("Weights are not equal to within %d decimal places, got %f, expected %f" % (decimal, self.weight, other.weight))
return False
for key, value in self.__dict__.items():
if key == 'weight':
continue
elif other.__dict__.get(key) != value:
print("Expected %s to be equal, got %s, expected %s" % (key, str(value), str(other.__dict__.get(key))))
return False
return True
def __eq__(self, other):
if self is other:
return True
if not isinstance(other, self.__class__):
return False
return self.__dict__ == other.__dict__
def __repr__(self):
def json_forcer(x):
if isinstance(x, Coordinate):
return x.dictionary
return x.__dict__
d = {key: value for (key, value) in self.__dict__.items() if value is not None}
return json.dumps(d, sort_keys=True, default=json_forcer)

Some files were not shown because too many files have changed in this diff Show More