first commit
This commit is contained in:
0
helix/Repositories/__init__.py
Normal file
0
helix/Repositories/__init__.py
Normal file
42
helix/Repositories/graph_repository.py
Normal file
42
helix/Repositories/graph_repository.py
Normal 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])
|
||||
0
helix/Services/__init__.py
Normal file
0
helix/Services/__init__.py
Normal file
22
helix/Services/doc_gen_service.py
Normal file
22
helix/Services/doc_gen_service.py
Normal 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
1002
helix/Services/dxf_helper.py
Normal file
File diff suppressed because it is too large
Load Diff
94
helix/Services/dxf_service.py
Normal file
94
helix/Services/dxf_service.py
Normal 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
0
helix/__init__.py
Normal file
1
helix/api/__init__.py
Normal file
1
helix/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__author__ = 'pivotal'
|
||||
65
helix/api/api.py
Normal file
65
helix/api/api.py
Normal 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
|
||||
})
|
||||
78
helix/calculated_data_repository.py
Normal file
78
helix/calculated_data_repository.py
Normal 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
|
||||
0
helix/calculators/__init__.py
Normal file
0
helix/calculators/__init__.py
Normal file
248
helix/calculators/ballast_calculator.py
Normal file
248
helix/calculators/ballast_calculator.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from collections import namedtuple, OrderedDict
|
||||
from math import ceil, floor
|
||||
|
||||
from helix.constants.panel_type import PanelType
|
||||
from helix.constants.system_type import SystemType
|
||||
from helix.models.panel import Panel, PanelWarnings
|
||||
|
||||
Result = namedtuple('Result', ['ballast_count', 'link_tray_count', 'cross_tray_count', 'system_weight', 'needs_anchor'])
|
||||
|
||||
|
||||
class BallastCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
self.system_type = user_values.system_type()
|
||||
self.anchor_type = user_values.anchor_type()
|
||||
self.system_constants = self.system_type.system_constants()
|
||||
self.module_constants = user_values.module_system_constants()
|
||||
|
||||
def ballast_and_trays_matrix(self, c_p_matrix, q_z, panels, ballast_block_weight=None):
|
||||
if not ballast_block_weight:
|
||||
ballast_block_weight = self.values.ballast_block_weight()
|
||||
ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight)
|
||||
|
||||
for idx, panel in enumerate(panels):
|
||||
stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone]
|
||||
panels[idx] = stored_panel.merge(panel)
|
||||
return panels
|
||||
|
||||
def update_ballast(self, c_p_matrix, q_z, panels):
|
||||
ballast_block_weight = self.values.ballast_block_weight()
|
||||
ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight)
|
||||
|
||||
seismic_ballast_store = {}
|
||||
|
||||
for panel in panels:
|
||||
seismic_anchors = panel.seismic_anchors if panel.seismic_anchors else 0
|
||||
if seismic_anchors != 0:
|
||||
key = hash(panel.wind_zone) + hash(panel.panel_type) + seismic_anchors + hash(panel.fuzzy_wind_zone) # hack
|
||||
stored_panel = seismic_ballast_store.get(key)
|
||||
if stored_panel:
|
||||
panel.ballast = stored_panel.ballast
|
||||
panel.link_tray = stored_panel.link_tray
|
||||
panel.cross_tray = stored_panel.cross_tray
|
||||
panel.pressure = stored_panel.pressure
|
||||
else:
|
||||
anchors = panel.wind_anchors + seismic_anchors
|
||||
c_p = c_p_matrix[panel.wind_zone, panel.panel_type.index()] * (1.15 if panel.fuzzy_wind_zone else 1)
|
||||
force = self.uplift(c_p, q_z) - anchors * self.anchor_type.uplift_capacity()
|
||||
ballast_and_tray_count = self.ballast_and_tray_count(force, panel.panel_type, ballast_block_weight, anchors)
|
||||
pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight)
|
||||
|
||||
panel.ballast = ballast_and_tray_count.ballast_count
|
||||
panel.link_tray = ballast_and_tray_count.link_tray_count
|
||||
panel.cross_tray = ballast_and_tray_count.cross_tray_count
|
||||
panel.pressure = pressure
|
||||
|
||||
seismic_ballast_store[key] = panel
|
||||
else:
|
||||
stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone]
|
||||
|
||||
panel.ballast = stored_panel.ballast
|
||||
panel.link_tray = stored_panel.link_tray
|
||||
panel.cross_tray = stored_panel.cross_tray
|
||||
panel.pressure = stored_panel.pressure
|
||||
return panels
|
||||
|
||||
def calculate_ballast_store(self, cp_matrix, qz, ballast_block_weight):
|
||||
max_psf = self.values.max_system_pressure()
|
||||
store = {}
|
||||
for panel_type in PanelType.all():
|
||||
sub_store = {}
|
||||
for wind_zone, _ in enumerate(self.values.system_type().system_constants().wind_zones):
|
||||
sub_store[wind_zone] = {}
|
||||
for use_fuzzy in (True, False):
|
||||
sub_store[wind_zone][use_fuzzy] = self.ballast_tray_and_anchor_count(wind_zone=wind_zone,
|
||||
panel_type=panel_type,
|
||||
ballast_block_weight=ballast_block_weight,
|
||||
max_system_pressure=max_psf,
|
||||
c_p_matrix=cp_matrix,
|
||||
q_z=qz,
|
||||
use_fuzzy=use_fuzzy)
|
||||
store[panel_type] = sub_store
|
||||
|
||||
return store
|
||||
|
||||
def summary_table(self, c_p_matrix, q_z):
|
||||
wind_zones = self.system_constants.wind_zones
|
||||
ballast_block_weight = self.values.ballast_block_weight()
|
||||
max_system_pressure = self.values.max_system_pressure()
|
||||
|
||||
table = OrderedDict()
|
||||
for panel_type in PanelType.all():
|
||||
ballast_counts = []
|
||||
anchor_counts = []
|
||||
pressures = []
|
||||
warnings = []
|
||||
for wind_zone_index, _ in enumerate(wind_zones):
|
||||
ballast_tray_anchor_panels = self.ballast_tray_and_anchor_count(wind_zone_index, panel_type,
|
||||
ballast_block_weight, max_system_pressure,
|
||||
c_p_matrix, q_z)
|
||||
anchor_count = ballast_tray_anchor_panels.wind_anchors
|
||||
ballast_count = ballast_tray_anchor_panels.ballast
|
||||
pressure = ballast_tray_anchor_panels.pressure
|
||||
warning = ballast_tray_anchor_panels.warnings
|
||||
|
||||
pressure_as_string = "{0:.2f}".format(pressure)
|
||||
# Because pressure is stored as a floating point number, it is possible, because floats
|
||||
# for pressure to be something like 5.02999999999999. Which is clearly meant to be 5.03.
|
||||
# This represents that as a string, which doesn't have that issue.
|
||||
|
||||
anchor_counts.append(anchor_count)
|
||||
ballast_counts.append(ballast_count)
|
||||
pressures.append(pressure_as_string)
|
||||
warnings.append(warning)
|
||||
|
||||
table[panel_type] = {
|
||||
'ballast blocks': ballast_counts,
|
||||
'anchors': anchor_counts,
|
||||
'pressure': pressures,
|
||||
'warnings': warnings
|
||||
}
|
||||
return table
|
||||
|
||||
def ballast_tray_and_anchor_count(self, wind_zone, panel_type, ballast_block_weight, max_system_pressure,
|
||||
c_p_matrix, q_z, use_fuzzy=False):
|
||||
fuzzy_factor = 1.15 if use_fuzzy else 1
|
||||
c_p = c_p_matrix[wind_zone, panel_type.index()] * fuzzy_factor
|
||||
uplift_force = self.uplift(c_p, q_z)
|
||||
|
||||
warnings = []
|
||||
keep_trying = True
|
||||
anchor_count = 0
|
||||
pressure = 0.
|
||||
tries = 0
|
||||
ballast_and_tray_count = None
|
||||
while keep_trying:
|
||||
remainder_force = uplift_force - anchor_count * self.anchor_type.uplift_capacity()
|
||||
ballast_and_tray_count = self.ballast_and_tray_count(remainder_force, panel_type, ballast_block_weight, anchor_count)
|
||||
pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight)
|
||||
keep_trying = (ballast_and_tray_count.needs_anchor or pressure > max_system_pressure) and ballast_and_tray_count.ballast_count > 0
|
||||
if keep_trying:
|
||||
anchor_count = self.calculate_anchors(panel_type, uplift_force) + tries
|
||||
tries += 1
|
||||
keep_trying &= tries < 100
|
||||
|
||||
if uplift_force / self.module_constants.surface_area >= self.module_constants.max_psf:
|
||||
warnings.append(PanelWarnings.MaxPsf)
|
||||
|
||||
return Panel(wind_zone=wind_zone,
|
||||
panel_type=panel_type,
|
||||
ballast=ballast_and_tray_count.ballast_count,
|
||||
link_tray=self.interpret_tray_count(ballast_and_tray_count.link_tray_count, panel_type),
|
||||
cross_tray=ballast_and_tray_count.cross_tray_count,
|
||||
wind_anchors=anchor_count,
|
||||
pressure=pressure,
|
||||
fuzzy_wind_zone=use_fuzzy,
|
||||
warnings=warnings)
|
||||
|
||||
def ballast_and_tray_count(self, force_to_resist, panel_type, ballast_block_weight, anchor_count):
|
||||
system_weight = self.module_constants.base_weight(panel_type, 0)
|
||||
ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)
|
||||
|
||||
link_tray_count = 0
|
||||
cross_tray_count = 0
|
||||
needs_anchor = False
|
||||
|
||||
keep_trying = True
|
||||
tries = 0
|
||||
|
||||
while keep_trying and tries < 3:
|
||||
tries += 1
|
||||
if ballast_count:
|
||||
new_link_tray_count, _ = self.calculate_trays(ballast_count + 2 * anchor_count,
|
||||
self.module_constants.link_tray_thresholds(panel_type))
|
||||
|
||||
# Recalculate weight given new link trays; recalculate ballast given new weight
|
||||
system_weight = self.module_constants.base_weight(panel_type, new_link_tray_count + cross_tray_count)
|
||||
ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)
|
||||
|
||||
new_cross_tray_count, needs_anchor = self.calculate_trays(ballast_count + 2 * anchor_count,
|
||||
self.module_constants.cross_tray_thresholds(
|
||||
panel_type))
|
||||
system_weight = self.module_constants.base_weight(panel_type, new_cross_tray_count + new_link_tray_count)
|
||||
ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)
|
||||
|
||||
if link_tray_count == new_link_tray_count and cross_tray_count == new_cross_tray_count:
|
||||
keep_trying = False
|
||||
link_tray_count = new_link_tray_count
|
||||
cross_tray_count = new_cross_tray_count
|
||||
else:
|
||||
keep_trying = False
|
||||
|
||||
return Result(ballast_count, link_tray_count=link_tray_count, cross_tray_count=cross_tray_count,
|
||||
system_weight=system_weight, needs_anchor=needs_anchor)
|
||||
|
||||
def uplift(self, c_p, q_z):
|
||||
return q_z * self.module_constants.surface_area * c_p
|
||||
|
||||
def calculate_ballast(self, uplift, non_ballast_weight, ballast_block_weight):
|
||||
if non_ballast_weight > uplift:
|
||||
return 0
|
||||
|
||||
return ceil((uplift - non_ballast_weight) / ballast_block_weight)
|
||||
|
||||
def calculate_trays(self, ballast_count, thresholds):
|
||||
for idx, threshold in enumerate(thresholds):
|
||||
if ballast_count <= threshold:
|
||||
return idx, False
|
||||
return len(thresholds) - 1, True
|
||||
|
||||
def calculate_pressure_on_roof(self, ballast_count, ballast_block_weight, non_ballast_weight):
|
||||
effective_area = self.module_constants.surface_area / self.module_constants.ground_coverage_ratio
|
||||
return (ballast_count * ballast_block_weight + non_ballast_weight) / effective_area
|
||||
|
||||
def interpret_tray_count(self, link_tray_count, panel_type):
|
||||
if self.system_type == SystemType.singleTilt:
|
||||
if panel_type == PanelType.EastWest:
|
||||
return 2
|
||||
elif panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
|
||||
return 0
|
||||
return link_tray_count or 0
|
||||
|
||||
def calculate_anchors(self, panel_type, uplift):
|
||||
base_system_weight = self.module_constants.base_weight(panel_type, 0)
|
||||
anchor_capacity = self.anchor_type.uplift_capacity()
|
||||
return max(floor((uplift - base_system_weight) / anchor_capacity), 1)
|
||||
|
||||
def show_presented_link_trays(self, panels):
|
||||
for panel in panels:
|
||||
panel.presented_link_tray = self.present_link_tray(panel.link_tray, panel.panel_type)
|
||||
return panels
|
||||
|
||||
def present_link_tray(self, link_tray_count, panel_type):
|
||||
if self.system_type == SystemType.singleTilt:
|
||||
link_tray_representation = {
|
||||
PanelType.Corner: 0,
|
||||
PanelType.NorthSouth: 0,
|
||||
PanelType.EastWest: 2,
|
||||
PanelType.Middle: min(1, int(link_tray_count)),
|
||||
}[panel_type]
|
||||
else:
|
||||
link_tray_representation = {
|
||||
PanelType.Corner: 2,
|
||||
PanelType.NorthSouth: 2,
|
||||
PanelType.EastWest: 1,
|
||||
PanelType.Middle: min(1, int(link_tray_count)),
|
||||
}[panel_type]
|
||||
return link_tray_representation
|
||||
58
helix/calculators/bom_calculator.py
Normal file
58
helix/calculators/bom_calculator.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from numpy import array, ceil
|
||||
|
||||
from helix.calculators.bom_helper import add_parts_to_list, apply_package_size_rounding
|
||||
from helix.calculators.ebom_calculator import EbomCalculator
|
||||
from helix.calculators.mechanical_bom_calculator import MechanicalBomCalculator
|
||||
from helix.constants.parts import *
|
||||
|
||||
|
||||
class BomCalculator(object):
|
||||
def __init__(self, values, panels, subarrays, graph_repository):
|
||||
self.values = values
|
||||
self.panels = panels
|
||||
self.subarrays = subarrays
|
||||
self.graph_repository = graph_repository
|
||||
|
||||
def compute_bom(self):
|
||||
output_array = []
|
||||
for part, quantity in self.parts_list().items():
|
||||
if ceil(quantity) <= 0:
|
||||
continue
|
||||
row = list(part)
|
||||
row.append(int(ceil(quantity)))
|
||||
output_array.append(row)
|
||||
|
||||
output_array.sort(key=lambda x: x[0] + x[1])
|
||||
return array(output_array)
|
||||
|
||||
def documentation_bom(self):
|
||||
parts_list = self.parts_list()
|
||||
|
||||
for part in all_parts:
|
||||
if part not in parts_list.keys():
|
||||
parts_list[part] = 0
|
||||
|
||||
output_array = []
|
||||
for part, quantity in parts_list.items():
|
||||
if part == ballast:
|
||||
row = 'ballast'
|
||||
elif part == anchor:
|
||||
row = 'anchors'
|
||||
elif part == module:
|
||||
row = 'modules'
|
||||
else:
|
||||
row = part[0]
|
||||
quantity = max(0, quantity)
|
||||
output_array.append((row, int(ceil(quantity))))
|
||||
return output_array
|
||||
|
||||
def parts_list(self):
|
||||
row_count = sum(subarray.row_count for subarray in self.subarrays)
|
||||
column_count = sum(subarray.column_count for subarray in self.subarrays)
|
||||
parts_list = MechanicalBomCalculator(self.values, self.panels, self.subarrays).mechanical_bom()
|
||||
ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module)).compute_ebom()
|
||||
|
||||
add_parts_to_list(parts_list, ebom_parts_list)
|
||||
|
||||
apply_package_size_rounding(parts_list, package_sizes)
|
||||
return parts_list
|
||||
36
helix/calculators/bom_helper.py
Normal file
36
helix/calculators/bom_helper.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from math import ceil
|
||||
|
||||
from helix.constants.panel_type import PanelType
|
||||
|
||||
|
||||
def add_parts_to_list(parts_list, parts_to_add, multiplier=1):
|
||||
for part, quantity in parts_to_add.items():
|
||||
previous_value = parts_list.get(part) or 0
|
||||
if quantity != 0:
|
||||
parts_list[part] = previous_value + quantity * multiplier
|
||||
|
||||
|
||||
def apply_fudge_factors(parts_list, fudge_factors):
|
||||
for part, quantity in parts_list.items():
|
||||
fudge_factor = fudge_factors.get(part) or 1.0
|
||||
parts_list[part] = quantity * fudge_factor
|
||||
|
||||
|
||||
def apply_package_size_rounding(parts_list, package_sizes):
|
||||
for part, quantity in parts_list.items():
|
||||
package_size = package_sizes.get(part) or 1
|
||||
parts_list[part] = package_size * ceil(quantity / package_size)
|
||||
|
||||
|
||||
def get_panel_type_counts(panels):
|
||||
panel_type_counts = {
|
||||
PanelType.Corner: 0,
|
||||
PanelType.NorthSouth: 0,
|
||||
PanelType.EastWest: 0,
|
||||
PanelType.Middle: 0,
|
||||
}
|
||||
|
||||
for panel in panels:
|
||||
panel_type_counts[panel.panel_type] += 1
|
||||
|
||||
return panel_type_counts
|
||||
169
helix/calculators/calculator.py
Normal file
169
helix/calculators/calculator.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from math import ceil, floor
|
||||
import copy
|
||||
from helix.Repositories.graph_repository import GraphRepository
|
||||
from helix.calculators.ballast_calculator import BallastCalculator
|
||||
from helix.calculators.bom_calculator import BomCalculator
|
||||
from helix.calculators.coordinates_calculator import CoordinatesCalculator
|
||||
from helix.calculators.pressure_coefficient_calculator import PressureCoefficientCalculator
|
||||
from helix.calculators.seismic_calculator import SeismicCalculator
|
||||
from helix.calculators.subarray_helper import get_subarray_sizes_and_rows, extract_subarray
|
||||
from helix.calculators.summary_values_calculator import SummaryValuesCalculator
|
||||
from helix.calculators.wind_pressure_calculator import WindPressureCalculator
|
||||
|
||||
|
||||
class Calculator(object):
|
||||
def __init__(self, user_values, calculate_panel_data=True):
|
||||
self.values = user_values
|
||||
self._q_z = None
|
||||
self._c_p_matrix = None
|
||||
self._L_B = None
|
||||
self._K_z = None
|
||||
self.subarrays = None
|
||||
self.buildings = self.values.buildings_polygons()
|
||||
self.buildings_for_drawing = []
|
||||
|
||||
self.panels = self.values.csv()
|
||||
if calculate_panel_data and self.panels is not None:
|
||||
for idx, panel in enumerate(self.panels):
|
||||
panel.id = idx + 1
|
||||
self.panels.sort(key=lambda x: x.subarray)
|
||||
self.subarrays = get_subarray_sizes_and_rows(self.panels)
|
||||
|
||||
self.__compute_ballast()
|
||||
_,_,self.buildings_for_drawing = self.__transform_coordinates()
|
||||
self.graph_repository = GraphRepository(self.panels, self.subarrays, self.values.system_type())
|
||||
if self.values.user_override_seismic_anchors():
|
||||
user_provided_panels = self.values.get_user_provided_seismic_anchors()
|
||||
for user_panel in user_provided_panels:
|
||||
panel = [panel for panel in self.panels if panel.id == user_panel.id][0]
|
||||
panel.seismic_anchors = user_panel.seismic_anchors
|
||||
self.__compute_seismic_anchors(self.panels) # Update subarrays to include required seismic anchors
|
||||
self.__update_ballast(self.panels)
|
||||
else:
|
||||
# Update subarrays *and panels* to include required seismic anchors
|
||||
self.panels = self.__compute_seismic_anchors(self.panels)
|
||||
|
||||
def k_z(self):
|
||||
if self._K_z is None:
|
||||
self._K_z = WindPressureCalculator(self.values).K_z()
|
||||
return self._K_z
|
||||
|
||||
def L_B(self):
|
||||
if self._L_B is None:
|
||||
self._L_B = PressureCoefficientCalculator(self.values).L_B()
|
||||
return self._L_B
|
||||
|
||||
def summary_table(self):
|
||||
return BallastCalculator(self.values).summary_table(self.__c_p_matrix(), self.q_z())
|
||||
|
||||
def minimum_array_sizes(self):
|
||||
return PressureCoefficientCalculator(self.values).minimum_array_size(self.L_B())
|
||||
|
||||
# Used in the array summary page - is the table of weight, psf, anchors, ballast, etc. for the entire system
|
||||
def summary_values(self):
|
||||
seismic_anchors = self.subarray_summary()
|
||||
ballast_calculator = BallastCalculator(self.values)
|
||||
seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()
|
||||
|
||||
return SummaryValuesCalculator(self.values).summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
|
||||
self.q_z(), seismic_interval, ballast_calculator)
|
||||
|
||||
def documentation_summary_values(self):
|
||||
seismic_anchors = self.subarray_summary()
|
||||
ballast_calculator = BallastCalculator(self.values)
|
||||
seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()
|
||||
|
||||
return SummaryValuesCalculator(self.values).documentation_summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
|
||||
self.q_z(), seismic_interval, ballast_calculator)
|
||||
|
||||
# used in the array visualization - is parsed into json and displayed using the fancy canvas
|
||||
def get_computed_csv_columns(self):
|
||||
return BallastCalculator(self.values).show_presented_link_trays(self.panels)
|
||||
|
||||
def compute_bom(self):
|
||||
required_seismic_anchors = self.subarray_summary()
|
||||
return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).compute_bom()
|
||||
|
||||
def documentation_bom(self):
|
||||
required_seismic_anchors = self.subarray_summary()
|
||||
return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).documentation_bom()
|
||||
|
||||
# used in the array summary page - is part of the fancy scrolling table of summing up each subarray
|
||||
def subarray_summary(self):
|
||||
summary_values_calculator = SummaryValuesCalculator(self.values)
|
||||
for subarray in self.subarrays:
|
||||
panels_for_subarray = extract_subarray(self.panels, subarray.subarray_number)
|
||||
weight, _ = summary_values_calculator.system_weight_and_pressure(panels_for_subarray)
|
||||
subarray.weight = weight
|
||||
|
||||
return self.subarrays
|
||||
|
||||
def q_z(self):
|
||||
if self._q_z is None:
|
||||
self._q_z = WindPressureCalculator(self.values).q_z(self.k_z())
|
||||
return self._q_z
|
||||
|
||||
def __c_p_matrix(self):
|
||||
if self._c_p_matrix is None:
|
||||
self._c_p_matrix = PressureCoefficientCalculator(self.values).c_p_matrix(self.L_B())
|
||||
return self._c_p_matrix
|
||||
|
||||
def __compute_seismic_anchors(self, panels):
|
||||
panels = copy.deepcopy(panels)
|
||||
seismic_calculator = SeismicCalculator(self.values, self.graph_repository)
|
||||
for subarray in self.subarrays:
|
||||
if subarray.required_seismic_anchors is None:
|
||||
subarray.required_seismic_anchors = 0
|
||||
panels = self.__seismic_anchors_for_subarray(panels, subarray, seismic_calculator)
|
||||
return panels
|
||||
|
||||
def __seismic_anchors_for_subarray(self, panels, subarray, seismic_calculator):
|
||||
# do first estimation to obtain upper bound
|
||||
required_seismic = seismic_calculator.required_force_seismic_anchors(subarray.subarray_number, panels)
|
||||
|
||||
test_value = required_seismic
|
||||
tried_acceptable_values = []
|
||||
|
||||
def assign_seismic_anchors(count):
|
||||
subarray.required_seismic_anchors = count
|
||||
seismic_calculator.assign_seismic_anchors(subarray, panels)
|
||||
self.__update_ballast(panels)
|
||||
|
||||
assigned = sum([panel.seismic_anchors for panel in panels if panel.seismic_anchors is not None])
|
||||
|
||||
return assigned
|
||||
|
||||
step = max(1, test_value // 2)
|
||||
|
||||
while True:
|
||||
seismic_anchors_assigned = assign_seismic_anchors(test_value)
|
||||
|
||||
provided_force = seismic_calculator.compute_provided_lateral_capacity(subarray.subarray_number, panels)
|
||||
required_seismic_force = seismic_calculator.required_force_seismic_demand(subarray.subarray_number, panels)
|
||||
|
||||
if seismic_anchors_assigned == 0:
|
||||
# anchors were not assigned propably because self.graph_repository.subarray_graph(subarray.subarray_number) is empty
|
||||
# which may be because of test construction
|
||||
return panels
|
||||
|
||||
if provided_force < required_seismic_force:
|
||||
test_value += step
|
||||
else:
|
||||
if (test_value in tried_acceptable_values) or (required_seismic_force == 0):
|
||||
return panels
|
||||
|
||||
tried_acceptable_values.append(test_value)
|
||||
test_value = max(0, test_value - step)
|
||||
|
||||
step = max(1, step // 2)
|
||||
|
||||
def __transform_coordinates(self):
|
||||
return CoordinatesCalculator(self.values).transform_coordinates(self.panels, self.subarrays, self.buildings)
|
||||
|
||||
def __compute_ballast(self):
|
||||
ballast_calculator = BallastCalculator(self.values)
|
||||
changed_panels = ballast_calculator.ballast_and_trays_matrix(self.__c_p_matrix(), self.q_z(), self.panels)
|
||||
self.panels = [panel.merge(changed_panels[idx]) for idx, panel in enumerate(self.panels)]
|
||||
|
||||
def __update_ballast(self, panels):
|
||||
BallastCalculator(self.values).update_ballast(self.__c_p_matrix(), self.q_z(), panels)
|
||||
104
helix/calculators/coordinates_calculator.py
Normal file
104
helix/calculators/coordinates_calculator.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from numpy import array, math, dot, vectorize
|
||||
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
from helix.models.coordinate import Coordinate
|
||||
|
||||
|
||||
class CoordinatesCalculator(object):
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
|
||||
def transform_coordinates(self, panels, subarrays, buildings):
|
||||
"""
|
||||
Scales, rotates, and translates the coordinates so that they're all
|
||||
in inches, and in positive unit space.
|
||||
Coordinates are rounded to whole values (used in drawing on the
|
||||
array_summary page
|
||||
|
||||
Parameters:
|
||||
panels (obj): List of panels
|
||||
subarrays (obj): List of subarrays
|
||||
buildings (obj): List of lists of building polygons
|
||||
|
||||
Returns:
|
||||
tupple
|
||||
|
||||
"""
|
||||
|
||||
rotate_all = vectorize(self.rotate)
|
||||
scale_all = vectorize(self.scale)
|
||||
round_all = vectorize(round)
|
||||
neg_translate_all = vectorize(self.neg_translate)
|
||||
origins = []
|
||||
|
||||
|
||||
first_subarray_rotation = None
|
||||
|
||||
for subarray in subarrays:
|
||||
begin, size = (subarray.start_row, subarray.size)
|
||||
extracted_panels = extract_subarray(panels, subarray.subarray_number)
|
||||
|
||||
raw_coordinates = [panel.coordinate for panel in extracted_panels]
|
||||
|
||||
if first_subarray_rotation is None:
|
||||
first_subarray_rotation = raw_coordinates[0].rotation
|
||||
|
||||
rotated_coordinates = rotate_all(raw_coordinates)
|
||||
scaled_coordinates = scale_all(rotated_coordinates)
|
||||
|
||||
origin = self.find_origin(scaled_coordinates)
|
||||
rounded_coordinates = round_all(scaled_coordinates - origin)
|
||||
|
||||
for idx, val in enumerate(rounded_coordinates):
|
||||
panels[begin + idx].coordinate = val
|
||||
origins.append(origin)
|
||||
|
||||
|
||||
prepared_buildings = self.prepare_buildings(buildings, first_subarray_rotation)
|
||||
rotated_buildings = list(map(lambda building: rotate_all(building), prepared_buildings ))
|
||||
scaled_buildings = list(map(lambda building: scale_all(building), rotated_buildings))
|
||||
|
||||
all_building_coordinates = [point for sublist in scaled_buildings for point in sublist]
|
||||
global_origin = self.find_origin(all_building_coordinates + origins)
|
||||
origins = array(origins) - global_origin
|
||||
|
||||
for idx, origin in enumerate(origins):
|
||||
subarrays[idx].origin = origin
|
||||
|
||||
translated_buildings = list(map(lambda building: neg_translate_all(building, global_origin), scaled_buildings))
|
||||
# rounded_buildings = list(map(lambda building: round_all(building), translated_buildings))
|
||||
|
||||
return panels, subarrays, translated_buildings
|
||||
|
||||
def rotate(self, coordinate):
|
||||
rotation = math.radians(coordinate.rotation)
|
||||
rotation_matrix = array([[math.cos(rotation), -math.sin(rotation)],
|
||||
[math.sin(rotation), math.cos(rotation)]])
|
||||
vector = (coordinate.x, coordinate.y)
|
||||
rotated_vector = dot(vector, rotation_matrix)
|
||||
return Coordinate(rotated_vector[0], rotated_vector[1])
|
||||
|
||||
def scale(self, coordinate):
|
||||
constants = self.values.module_system_constants()
|
||||
panel_x, panel_y = constants.panel_spacing
|
||||
return coordinate.scale(1. / panel_x, 1. / panel_y)
|
||||
|
||||
def neg_translate(self, coordinate, other):
|
||||
return coordinate.neg_translate(other)
|
||||
|
||||
def find_origin(self, coordinates):
|
||||
if coordinates == []:
|
||||
return Coordinate(0,0)
|
||||
min_x = min(list(map(lambda x: x.x, coordinates)))
|
||||
min_y = min(list(map(lambda x: x.y, coordinates)))
|
||||
return Coordinate(min_x, min_y)
|
||||
|
||||
def prepare_buildings(self, buildings, rotation):
|
||||
return list(map(lambda building: self.prepare_single_building(building, rotation), buildings))
|
||||
|
||||
def prepare_single_building(self, building_array, rotation):
|
||||
return list(map(lambda point: Coordinate(point[0],point[1],rotation), building_array))
|
||||
|
||||
|
||||
|
||||
133
helix/calculators/ebom_calculator.py
Normal file
133
helix/calculators/ebom_calculator.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from math import ceil
|
||||
|
||||
from helix.calculators.bom_helper import add_parts_to_list
|
||||
from helix.constants import ebom_parts
|
||||
from helix.constants.ebom_parts import *
|
||||
from helix.constants.parts import wire_clip_large, cable_support, cable_support_lid, channel_nut, sunshade
|
||||
from helix.constants.system_type import SystemType
|
||||
|
||||
|
||||
class EbomCalculator(object):
|
||||
def __init__(self, user_values, row_count, column_count, modules_count = None):
|
||||
self.values = user_values
|
||||
self.row_count = row_count
|
||||
self.column_count = column_count
|
||||
self.modules_count = modules_count
|
||||
|
||||
def resolve_power_monitor_type(self):
|
||||
module_type = self.values.module_type()
|
||||
|
||||
thresholds = {
|
||||
ModuleType.Cell96: 306,
|
||||
ModuleType.Cell128: 230,
|
||||
ModuleType.PSeries: 286
|
||||
}
|
||||
|
||||
if (not self.modules_count) or self.modules_count >= thresholds[module_type]:
|
||||
return monitor_controller_480_v
|
||||
else:
|
||||
return monitor_controller_240_v
|
||||
|
||||
def compute_ebom(self):
|
||||
part_list = {}
|
||||
|
||||
power_stations = self.values.power_stations()
|
||||
standalone_inverters = self.values.standalone_inverters()
|
||||
monitors = self.values.power_monitors()
|
||||
module_type = self.values.module_type()
|
||||
system_type = self.values.system_type()
|
||||
inverter_count = 0
|
||||
total_ac_run_length = 0
|
||||
panel_board_counts = [0, 0]
|
||||
proper_monitor_controller = self.resolve_power_monitor_type()
|
||||
|
||||
for power_station in power_stations:
|
||||
power_station_count = power_station['power_station_quantity']
|
||||
total_ac_run_length += power_station['ac_run_length']
|
||||
inverter_quantity = power_station['inverter_quantity'] + self.get_standalone_inverters(power_station)
|
||||
if inverter_quantity <= 2:
|
||||
panel_board_counts[0] += power_station_count
|
||||
else:
|
||||
panel_board_counts[1] += power_station_count
|
||||
|
||||
if self.power_station_has_monitor(power_station, monitors):
|
||||
panel_board_parts_to_use = panel_board_parts_with_monitor(inverter_quantity, proper_monitor_controller)
|
||||
else:
|
||||
panel_board_parts_to_use = panel_board_parts(inverter_quantity, with_aux=False)
|
||||
add_parts_to_list(part_list, panel_board_parts_to_use, power_station_count)
|
||||
add_parts_to_list(part_list, shared_panel_board_parts(module_type, system_type), power_station_count)
|
||||
add_parts_to_list(part_list, {channel_nut: 4}, power_station_count)
|
||||
|
||||
for inverter in power_station['inverters']:
|
||||
inverter_count += power_station_count
|
||||
self.add_parts_for_inverter(part_list, inverter, power_station_count)
|
||||
add_parts_to_list(part_list, inverter_parts(inverter, module_type), power_station_count)
|
||||
|
||||
for inverter in standalone_inverters:
|
||||
inverter_count += 1
|
||||
total_ac_run_length += inverter['ac_run_length']
|
||||
self.add_parts_for_inverter(part_list, inverter)
|
||||
add_parts_to_list(part_list, standalone_inverter_parts(inverter, system_type, module_type), 1)
|
||||
add_parts_to_list(part_list, inverter_parts(inverter, module_type), 1)
|
||||
if inverter['attachment_point'][1]:
|
||||
add_parts_to_list(part_list, standalone_inverter_attached_to_panel_board_parts, 1)
|
||||
|
||||
for monitor in monitors:
|
||||
if monitor['power_source'][0] == 'Switch Gear/External':
|
||||
add_parts_to_list(part_list, {proper_monitor_controller: 1}, 1)
|
||||
|
||||
add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count)
|
||||
|
||||
add_parts_to_list(part_list, {stump: 1}, ceil(total_ac_run_length / 4.0))
|
||||
cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters))
|
||||
add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports)
|
||||
add_parts_to_list(part_list, {rear_skirt: -1}, ceil(cable_supports*.38))
|
||||
|
||||
dependent_part_list = {}
|
||||
for part, quantity in part_list.items():
|
||||
dependent_parts = ebom_parts.dependent_parts(module_type, system_type).get(part)
|
||||
if dependent_parts:
|
||||
add_parts_to_list(dependent_part_list, dependent_parts, quantity)
|
||||
|
||||
add_parts_to_list(part_list, dependent_part_list)
|
||||
|
||||
return part_list
|
||||
|
||||
def add_parts_for_inverter(self, part_list, inverter, multiplier=1):
|
||||
strings_per_inverter = inverter_strings_parts.get(inverter['strings_per_inverter'], {})
|
||||
add_parts_to_list(part_list, inverter_model_parts[inverter['model']], multiplier)
|
||||
add_parts_to_list(part_list, strings_per_inverter, multiplier)
|
||||
if inverter['sunshade']:
|
||||
add_parts_to_list(part_list, {sunshade: 1, sunshade_bolt: 2, sunshade_washer: 2}, multiplier)
|
||||
if inverter['dc_switch']:
|
||||
add_parts_to_list(part_list, dc_switch_parts, multiplier)
|
||||
|
||||
def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count):
|
||||
if sum(panel_board_counts) == 0:
|
||||
return 0
|
||||
|
||||
if self.values.system_type() == SystemType.dualTilt:
|
||||
dimension1 = self.column_count
|
||||
dimension2 = self.row_count
|
||||
else:
|
||||
dimension1 = self.row_count
|
||||
dimension2 = self.column_count
|
||||
|
||||
result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts)
|
||||
result *= dimension1 * max(dimension1 / dimension2, 1)
|
||||
result += dimension1
|
||||
return ceil(result)
|
||||
|
||||
def get_standalone_inverters(self, power_station):
|
||||
standalone_inverters = self.values.standalone_inverters()
|
||||
count = 0
|
||||
for inverter in standalone_inverters:
|
||||
if inverter['attachment_point'][1] == power_station['power_station_id']:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def power_station_has_monitor(self, power_station, monitors):
|
||||
for monitor in monitors:
|
||||
if monitor['power_source'][1] == power_station['power_station_id']:
|
||||
return True
|
||||
return False
|
||||
148
helix/calculators/mechanical_bom_calculator.py
Normal file
148
helix/calculators/mechanical_bom_calculator.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from math import ceil, floor
|
||||
|
||||
from helix.calculators.bom_helper import add_parts_to_list, apply_fudge_factors, \
|
||||
get_panel_type_counts
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
from helix.constants.module_type import ModuleType
|
||||
from helix.constants.panel_type import PanelType
|
||||
from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1, leading_tray
|
||||
from helix.constants.system_type import SystemType
|
||||
|
||||
|
||||
class MechanicalBomCalculator(object):
|
||||
def __init__(self, values, panels, subarrays):
|
||||
self.values = values
|
||||
self.panels = panels
|
||||
self.subarrays = subarrays
|
||||
|
||||
def mechanical_bom(self):
|
||||
module_type = self.values.module_type()
|
||||
system_type = self.values.system_type()
|
||||
system_parts = system_type.parts(module_type)
|
||||
combined_parts_list = {}
|
||||
|
||||
for subarray in self.subarrays:
|
||||
panels = extract_subarray(self.panels, subarray.subarray_number)
|
||||
|
||||
ballast_count = sum(panel.ballast for panel in panels)
|
||||
cross_count = sum(panel.cross_tray for panel in panels)
|
||||
assigned_seismic_anchors_count = sum(panel.seismic_anchors for panel in panels)
|
||||
required_seismic_anchors_count = subarray.required_seismic_anchors
|
||||
seismic_anchors_count = max(assigned_seismic_anchors_count, required_seismic_anchors_count)
|
||||
required_wind_anchors_count = sum(panel.wind_anchors for panel in panels)
|
||||
anchor_count = required_wind_anchors_count + seismic_anchors_count
|
||||
panel_type_counts = get_panel_type_counts(panels)
|
||||
|
||||
subarray_parts_list = {}
|
||||
|
||||
for index, panel_type_parts in enumerate(system_parts.parts_per_panel_type()):
|
||||
add_parts_to_list(subarray_parts_list, panel_type_parts, panel_type_counts[PanelType.from_index(index)])
|
||||
|
||||
add_parts_to_list(subarray_parts_list, self.values.anchor_type().parts().parts, anchor_count)
|
||||
|
||||
link_count = self.link_count(panel_type_counts, panels, subarray)
|
||||
|
||||
add_parts_to_list(subarray_parts_list, {link_tray: 1}, link_count)
|
||||
cross_tray_parts = cross_tray if self.values.module_type() == ModuleType.Cell96 else cross_tray_1_1
|
||||
add_parts_to_list(subarray_parts_list, {cross_tray_parts: 1}, cross_count)
|
||||
add_parts_to_list(subarray_parts_list, {ballast: 1}, ballast_count)
|
||||
|
||||
add_parts_to_list(subarray_parts_list, system_parts.row_parts(module_type), subarray.row_count)
|
||||
add_parts_to_list(subarray_parts_list, system_parts.column_parts(module_type), subarray.column_count)
|
||||
|
||||
add_parts_to_list(subarray_parts_list, system_parts.sub_array_parts, 1)
|
||||
|
||||
apply_fudge_factors(subarray_parts_list, system_parts.fudge_factors(not subarray.row_counted_geometrically))
|
||||
|
||||
dependent_parts_list = {}
|
||||
for part, quantity in subarray_parts_list.items():
|
||||
dependent_parts = system_parts.dependent_parts(module_type).get(part)
|
||||
if dependent_parts:
|
||||
add_parts_to_list(dependent_parts_list, dependent_parts, quantity)
|
||||
|
||||
subarray_parts_list.update(dependent_parts_list)
|
||||
add_parts_to_list(combined_parts_list, subarray_parts_list)
|
||||
|
||||
return combined_parts_list
|
||||
|
||||
def link_count(self, panel_type_counts, panels, subarray):
|
||||
if self.values.system_type() == SystemType.dualTilt:
|
||||
|
||||
# check if info about position of panels is available
|
||||
coordinates_available = all(p.coordinate for p in panels) \
|
||||
and not all(p.coordinate.x == 0 and p.coordinate.y == 0 for p in panels)
|
||||
|
||||
if coordinates_available:
|
||||
# initially every C, NS, EW panels has 2 link trays attached
|
||||
panel_types = [PanelType.Corner, PanelType.NorthSouth, PanelType.EastWest]
|
||||
layout = dict([((p.coordinate.x, p.coordinate.y), 2) for p in panels if p.panel_type in panel_types])
|
||||
|
||||
row_count = ceil(subarray.row_count)
|
||||
column_count = ceil(subarray.column_count)
|
||||
|
||||
# reduce number of link trays between every two vertically adjoining panels
|
||||
for y in range(row_count):
|
||||
for x in range(column_count):
|
||||
|
||||
if (x, y) not in layout:
|
||||
continue
|
||||
|
||||
if (x, y + 1) in layout:
|
||||
layout[(x, y + 1)] = 1
|
||||
|
||||
# count link trays located on perimeter
|
||||
link_count = sum([layout[p] for p in layout])
|
||||
|
||||
# subtract places reserved for leading trays
|
||||
link_count -= subarray.row_count + 1
|
||||
|
||||
# add link trays for panels of type Middle
|
||||
link_count += sum([1 for panel in panels if panel.link_tray != 0 and panel.panel_type == PanelType.Middle])
|
||||
|
||||
return max(link_count, 0)
|
||||
else:
|
||||
total_possible_link_trays = len(panels) + subarray.column_count
|
||||
link_count = total_possible_link_trays
|
||||
|
||||
for panel in panels:
|
||||
if panel.link_tray == 0 and panel.panel_type == PanelType.Middle:
|
||||
link_count -= 1
|
||||
|
||||
link_count -= floor(subarray.row_count)
|
||||
|
||||
return link_count
|
||||
else:
|
||||
return sum([self.compute_link_count_single_tilt(panel_type, panel_type_counts, panels) for panel_type in PanelType.all()])
|
||||
|
||||
def compute_link_count_single_tilt(self, panel_type, panel_type_counts, panels):
|
||||
if panel_type == PanelType.Corner:
|
||||
return 0
|
||||
elif panel_type == PanelType.NorthSouth:
|
||||
return 0
|
||||
elif panel_type == PanelType.EastWest:
|
||||
return panel_type_counts[panel_type] * 2
|
||||
elif panel_type == PanelType.Middle:
|
||||
return self.get_panel_type_middle_link_trays_single_tilt(panels)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_panel_type_middle_link_trays_single_tilt(self, panels):
|
||||
wind_zones = self.values.system_type().system_constants().wind_zones
|
||||
|
||||
middle_panels_per_wind_zone = [0 for _ in wind_zones]
|
||||
middle_link_trays_per_wind_zone = [0 for _ in wind_zones]
|
||||
|
||||
for panel in panels:
|
||||
if panel.panel_type != PanelType.Middle:
|
||||
continue
|
||||
|
||||
wind_zone = panel.wind_zone
|
||||
middle_panels_per_wind_zone[wind_zone] += 1
|
||||
middle_link_trays_per_wind_zone[wind_zone] += panel.link_tray # use calculated number of link trays to see if it is non-zero
|
||||
|
||||
total_link_trays_required = 0
|
||||
for wind_zone, panel_count in enumerate(middle_panels_per_wind_zone):
|
||||
if middle_link_trays_per_wind_zone[wind_zone] > 0: # if any middle panels in this wind zone need link trays
|
||||
total_link_trays_required += ceil(panel_count * 1.05)
|
||||
return total_link_trays_required
|
||||
|
||||
93
helix/calculators/pressure_coefficient_calculator.py
Normal file
93
helix/calculators/pressure_coefficient_calculator.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from math import sqrt, log
|
||||
import math
|
||||
|
||||
from helix.constants.global_constants import parapet_coefficients, parapet_factor_max
|
||||
from numpy import array
|
||||
from numpy.ma import maximum
|
||||
|
||||
from helix.constants.panel_type import PanelType
|
||||
|
||||
|
||||
class PressureCoefficientCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
self.system_constants = self.values.system_type().system_constants()
|
||||
self.module_constants = self.values.module_system_constants()
|
||||
|
||||
def c_p_matrix(self, L_B):
|
||||
parapet = self.parapet_factor()
|
||||
return self.compute_c_p_matrix(L_B, parapet)
|
||||
|
||||
def L_B(self):
|
||||
""" Building scaling factor """
|
||||
height = max(15, self.values.building_height())
|
||||
|
||||
length = self.values.building_length()
|
||||
width = self.values.building_width()
|
||||
|
||||
longest_side = max(width, length)
|
||||
|
||||
return min(height, 0.4 * sqrt(height * max(1, longest_side)))
|
||||
|
||||
def minimum_array_size(self, L_B):
|
||||
panel_area = self.module_constants.panel_area
|
||||
module_count = self.system_constants.module_count
|
||||
minimum_array_size = []
|
||||
for minimum_A_n in self.minimum_A_n(L_B):
|
||||
if minimum_A_n is None:
|
||||
value = 6
|
||||
else:
|
||||
value = int(math.ceil((minimum_A_n * L_B ** 2) / (panel_area * 1000) / module_count))
|
||||
minimum_array_size.append(value)
|
||||
return minimum_array_size
|
||||
|
||||
# Normalized area, scales the tributary area by the building scaling factor and panel area
|
||||
def A_n(self, L_B):
|
||||
return self.module_constants.tributary_area * (self.module_constants.panel_area * 1000. / L_B ** 2)
|
||||
|
||||
def compute_c_p_matrix(self, L_B, parapet):
|
||||
A_n = self.A_n(L_B)
|
||||
wind_zones = self.system_constants.wind_zones
|
||||
return array([self.c_p_row(A_n, wind_zone, parapet) for wind_zone in wind_zones])
|
||||
|
||||
def c_p_row(self, A_n_row, wind_zone, parapet_factor):
|
||||
c_p_lower_bound = self.module_constants.c_p_lower_bound()
|
||||
|
||||
if wind_zone == self.system_constants.wind_zones[-1]:
|
||||
return c_p_lower_bound
|
||||
computed_row = []
|
||||
for index, A_n in enumerate(A_n_row):
|
||||
edge_factor = self.module_constants.edge_factor(wind_zone, PanelType.from_index(index))
|
||||
computed_row.append(self.c_p(A_n, wind_zone, parapet_factor, edge_factor))
|
||||
|
||||
return maximum(array(computed_row), c_p_lower_bound)
|
||||
|
||||
def c_p(self, A_n, wind_zone, parapet_factor, edge_factor):
|
||||
c0, c1 = self.module_constants.c_p_constants(A_n, wind_zone)
|
||||
return max(0., c0 * log(A_n) + c1) * parapet_factor * edge_factor
|
||||
|
||||
def parapet_factor(self):
|
||||
height = max(15, self.values.building_height())
|
||||
parapet_height = max(0, self.values.building_parapet_height())
|
||||
|
||||
factor = parapet_height / height
|
||||
c0, c1 = parapet_coefficients
|
||||
return min(parapet_factor_max, c0 + c1 * factor)
|
||||
|
||||
def ideal_subarray_average_uplift_c_p(self, L_B):
|
||||
c_p_matrix = self.compute_c_p_matrix(L_B, 1)
|
||||
return [self.module_constants.weighted_average_c_p(c, n, e, m) for c, n, e, m in c_p_matrix]
|
||||
|
||||
def minimum_A_n(self, L_B):
|
||||
uplift_c_p = self.ideal_subarray_average_uplift_c_p(L_B)
|
||||
minimum_A_n = []
|
||||
for idx, wind_zone in enumerate(self.system_constants.wind_zones):
|
||||
wind_zone_uplift = uplift_c_p[idx]
|
||||
coefficients = self.module_constants.minimum_a_n_coefficients(wind_zone_uplift, wind_zone)
|
||||
if not coefficients:
|
||||
minimum_A_n.append(None)
|
||||
continue
|
||||
|
||||
value = math.exp((coefficients[0] - wind_zone_uplift) / coefficients[1])
|
||||
minimum_A_n.append(value)
|
||||
return minimum_A_n
|
||||
184
helix/calculators/seismic_calculator.py
Normal file
184
helix/calculators/seismic_calculator.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from math import ceil, floor
|
||||
|
||||
from helix.calculators.subarray_graph import SubarrayGraph
|
||||
from helix.calculators.subarray_helper import extract_subarray
|
||||
|
||||
from helix.constants.global_constants import minimum_racking_capacity
|
||||
from helix.constants.panel_type import PanelType
|
||||
from helix.models.subarray import Subarray
|
||||
|
||||
|
||||
class SeismicCalculator(object):
|
||||
def __init__(self, values, graph_repository):
|
||||
self.values = values
|
||||
self.system_type = values.system_type()
|
||||
self.system_constants = values.module_system_constants()
|
||||
self.anchor_type = values.anchor_type()
|
||||
self.graph_repository = graph_repository
|
||||
|
||||
def assign_seismic_anchors(self, subarray, panels):
|
||||
panel_data_for_subarray = extract_subarray(panels, subarray.subarray_number)
|
||||
self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
|
||||
return panels
|
||||
|
||||
def assign_anchors_to_subarray(self, panels, subarray):
|
||||
sds = self.values.spectral_response()
|
||||
wind_anchors = sum([panel.wind_anchors for panel in panels])
|
||||
for panel in panels:
|
||||
if panel.seismic_anchors is None:
|
||||
panel.seismic_anchors = 0
|
||||
required_anchors = subarray.required_seismic_anchors
|
||||
if required_anchors == 0 and (wind_anchors == 0 or sds < 1):
|
||||
return panels
|
||||
|
||||
graph = self.graph_repository.subarray_graph(subarray.subarray_number)
|
||||
if len(graph.nodes) == 0:
|
||||
return panels
|
||||
|
||||
more_anchors_needed = True
|
||||
perimeter_covered = sds < 1.0
|
||||
anchor_threshold = 0
|
||||
while more_anchors_needed:
|
||||
rung = graph.pop_rung()
|
||||
interval = int(self.seismic_anchor_interval())
|
||||
nodes_since_last_anchor = interval
|
||||
if len(rung) == 0:
|
||||
graph.reset()
|
||||
anchor_threshold += 1
|
||||
continue
|
||||
while more_anchors_needed and interval >= 0:
|
||||
for node in rung:
|
||||
nodes_since_last_anchor += 1
|
||||
if node.wind_anchor + node.seismic_anchor > anchor_threshold:
|
||||
nodes_since_last_anchor = 0
|
||||
|
||||
if nodes_since_last_anchor > interval:
|
||||
node.assign_seismic_anchor()
|
||||
required_anchors -= 1
|
||||
nodes_since_last_anchor = 0
|
||||
|
||||
more_anchors_needed = (not perimeter_covered) or required_anchors > 0
|
||||
if not more_anchors_needed:
|
||||
break
|
||||
|
||||
perimeter_covered = True
|
||||
if interval <= 1:
|
||||
interval -= 1
|
||||
else:
|
||||
interval /= 2
|
||||
|
||||
for idx, node in enumerate(graph.nodes):
|
||||
panels[idx].seismic_anchors = node.seismic_anchor
|
||||
|
||||
return panels
|
||||
|
||||
def compute_provided_lateral_capacity(self, subarray_number, panels):
|
||||
subarray_panels = extract_subarray(panels, subarray_number)
|
||||
|
||||
anchors = {PanelType.Corner: 0, PanelType.NorthSouth: 0, PanelType.EastWest: 0, PanelType.Middle: 0}
|
||||
|
||||
for panel in subarray_panels:
|
||||
if panel.seismic_anchors is not None:
|
||||
anchors[panel.panel_type] += panel.seismic_anchors
|
||||
|
||||
return self.anchors_shear_capacity(anchors[PanelType.Corner], anchors[PanelType.NorthSouth],
|
||||
anchors[PanelType.EastWest], anchors[PanelType.Middle])
|
||||
|
||||
def seismic_anchors_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
|
||||
seismic_anchors, corner_anchors,
|
||||
north_south_anchors, east_west_anchors, middle_anchors, shear_capacity):
|
||||
demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response, friction_coefficient,
|
||||
seismic_anchors, corner_anchors, north_south_anchors,
|
||||
east_west_anchors, middle_anchors)
|
||||
return ceil(demand / shear_capacity)
|
||||
|
||||
def anchors_shear_capacity(self, corner_anchors, north_south_anchors, east_west_anchors, middle_anchors):
|
||||
|
||||
anchor_shear_capacity = self.anchor_type.shear_capacity()
|
||||
panel_racking_capacity = self.system_constants.racking_capacity
|
||||
|
||||
corner_capacity = corner_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Corner))
|
||||
north_south_capacity = north_south_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.NorthSouth))
|
||||
east_west_capacity = east_west_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.EastWest))
|
||||
middle_capacity = middle_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Middle))
|
||||
|
||||
return corner_capacity + north_south_capacity + east_west_capacity + middle_capacity
|
||||
|
||||
def seismic_demand_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
|
||||
seismic_anchors, corner_anchors,
|
||||
north_south_anchors, east_west_anchors, middle_anchors):
|
||||
if (corner_anchors + north_south_anchors + east_west_anchors + middle_anchors == 0) and seismic_anchors == 0:
|
||||
return 0
|
||||
|
||||
existing_shear_resistance = self.anchors_shear_capacity(corner_anchors, north_south_anchors, east_west_anchors,
|
||||
middle_anchors)
|
||||
|
||||
shear_force = 0.7 * F_p * subarray_weight - (
|
||||
(0.6 - 0.14 * spectral_response) * 0.7 * friction_coefficient * subarray_weight) - existing_shear_resistance
|
||||
|
||||
return max(shear_force, 0)
|
||||
|
||||
def required_force_seismic_anchors(self, subarray_number, panels):
|
||||
|
||||
demand = self.required_force_seismic_demand(subarray_number, panels)
|
||||
|
||||
system_shear_capacity = min(self.anchor_type.shear_capacity(),
|
||||
minimum_racking_capacity)
|
||||
|
||||
return ceil(demand / system_shear_capacity)
|
||||
|
||||
def required_force_seismic_demand(self, subarray_number, panels):
|
||||
subarray_panels = extract_subarray(panels, subarray_number)
|
||||
|
||||
importance_factor = self.values.importance_factor()
|
||||
spectral_response = self.values.spectral_response()
|
||||
F_p = 1.2 * spectral_response / (1.5 / importance_factor)
|
||||
|
||||
# number of wind anchors by panel type
|
||||
anchors = {PanelType.Corner: 0,
|
||||
PanelType.NorthSouth: 0,
|
||||
PanelType.EastWest: 0,
|
||||
PanelType.Middle: 0}
|
||||
|
||||
# total weight
|
||||
subarray_weight = 0
|
||||
|
||||
# total number of seismic anchors
|
||||
seismic_anchors = 0
|
||||
|
||||
for panel in subarray_panels:
|
||||
if panel.seismic_anchors is not None:
|
||||
seismic_anchors += panel.seismic_anchors
|
||||
|
||||
# it could be calculated before the loop to avoid redundant calculations
|
||||
effective_area = self.system_constants.surface_area / self.system_constants.ground_coverage_ratio
|
||||
weight = panel.pressure * effective_area
|
||||
|
||||
anchors[panel.panel_type] += panel.wind_anchors
|
||||
subarray_weight += weight
|
||||
|
||||
force_required_demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response,
|
||||
self.system_constants.friction_coefficient,
|
||||
seismic_anchors,
|
||||
anchors[PanelType.Corner],
|
||||
anchors[PanelType.NorthSouth],
|
||||
anchors[PanelType.EastWest],
|
||||
anchors[PanelType.Middle])
|
||||
return force_required_demand
|
||||
|
||||
def required_geometric_seismic_anchors(self, subarray_number, panels):
|
||||
if panels[0].coordinate is None or self.values.spectral_response() < 1:
|
||||
return 0
|
||||
panel_data_for_subarray = extract_subarray(panels, subarray_number)
|
||||
subarray = Subarray(required_seismic_anchors=0, subarray_number=subarray_number)
|
||||
anchors_for_subarray = self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
|
||||
return sum(panel.seismic_anchors for panel in anchors_for_subarray if panel.seismic_anchors)
|
||||
|
||||
def seismic_anchor_interval(self):
|
||||
sds = self.values.spectral_response()
|
||||
importance_factor = self.values.importance_factor()
|
||||
interval_constant, interval_multiplier = self.system_constants.seismic_anchor_interval_constants
|
||||
denom = (22.96 * importance_factor * sds - interval_constant + interval_multiplier * sds)
|
||||
if denom <= 0:
|
||||
return 15
|
||||
return floor(minimum_racking_capacity / denom)
|
||||
246
helix/calculators/subarray_graph.py
Normal file
246
helix/calculators/subarray_graph.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import copy
|
||||
from enum import Enum
|
||||
|
||||
from helix.constants.system_type import SystemType
|
||||
from helix.models.coordinate import Coordinate
|
||||
from helix.models.dxf.graph_node_store import GraphNodeStore
|
||||
|
||||
|
||||
class Direction(Enum):
|
||||
North = Coordinate(0, -1)
|
||||
South = Coordinate(0, 1)
|
||||
East = Coordinate(-1, 0)
|
||||
West = Coordinate(1, 0)
|
||||
|
||||
def opposite_direction(self):
|
||||
if self == Direction.North:
|
||||
return Direction.South
|
||||
elif self == Direction.South:
|
||||
return Direction.North
|
||||
elif self == Direction.East:
|
||||
return Direction.West
|
||||
else:
|
||||
return Direction.East
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
return [
|
||||
cls.North,
|
||||
cls.West,
|
||||
cls.South,
|
||||
cls.East,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def all_coordinates(cls):
|
||||
return [d.value for d in cls.all()]
|
||||
|
||||
def directions_to_try(self):
|
||||
return {
|
||||
Direction.North: [Direction.East, Direction.North, Direction.West, Direction.South],
|
||||
Direction.East: [Direction.South, Direction.East, Direction.North, Direction.West],
|
||||
Direction.South: [Direction.West, Direction.South, Direction.East, Direction.North],
|
||||
Direction.West: [Direction.North, Direction.West, Direction.South, Direction.East],
|
||||
}[self]
|
||||
|
||||
|
||||
class SubarrayGraphNode(object):
|
||||
"""
|
||||
A node that contains the panel, it's neighbors to the four cardinal
|
||||
directions (N, S, E, W), and a
|
||||
count of the seismic anchors attached.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, panel):
|
||||
self.neighbors = {}
|
||||
self.panel = panel
|
||||
self.seismic_anchor = 0
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.panel.coordinate
|
||||
|
||||
@property
|
||||
def coordinate(self): # GraphNodeStore expects a coordinate property
|
||||
return self.panel.coordinate
|
||||
|
||||
@property
|
||||
def wind_anchor(self):
|
||||
return self.panel.wind_anchors
|
||||
|
||||
def add_neighbor(self, neighbor, direction):
|
||||
self.neighbors[direction] = neighbor
|
||||
neighbor.neighbors[direction.opposite_direction()] = self
|
||||
|
||||
def remove_neighbor_references(self):
|
||||
for direction, neighbor in self.neighbors.items():
|
||||
neighbor.neighbors.pop(direction.opposite_direction(), None)
|
||||
|
||||
def assign_seismic_anchor(self):
|
||||
self.seismic_anchor += 1
|
||||
|
||||
def step(self, direction):
|
||||
return self.neighbors.get(direction)
|
||||
|
||||
def __repr__(self):
|
||||
val = "(" + str(self.location) + ":\n"
|
||||
for direction, neighbor in self.neighbors.items():
|
||||
val += "\t%s: %s\n" % (str(direction), str(neighbor.location))
|
||||
return val + ")"
|
||||
|
||||
def __eq__(self, other):
|
||||
if self is other:
|
||||
return True
|
||||
us = self.panel.coordinate
|
||||
them = other.panel.coordinate
|
||||
# Quick and dirty, inline equality makes for a slightly faster equality check
|
||||
# Also don't bother with floating point equality - it only slows us down. :(
|
||||
return us.x == them.x and us.y == them.y and us.rotation == them.rotation
|
||||
|
||||
def __hash__(self):
|
||||
return self.panel.coordinate.__hash__()
|
||||
|
||||
|
||||
class SubarrayGraph(object):
|
||||
def __init__(self, panels, system_type):
|
||||
self.nodes = []
|
||||
self.system_type = system_type
|
||||
self.node_store = GraphNodeStore()
|
||||
|
||||
self.nodes = []
|
||||
if len(panels) == 0 or panels[0].coordinate == panels[-1].coordinate:
|
||||
self.nodes = []
|
||||
else:
|
||||
for panel in panels:
|
||||
node = SubarrayGraphNode(panel)
|
||||
self.nodes.append(node)
|
||||
self.node_store.add_node(node)
|
||||
|
||||
self.graph = list(self.nodes)
|
||||
self.rungs = []
|
||||
self.current_rung = 0
|
||||
self.assemble_graph()
|
||||
|
||||
def __deepcopy__(self, _):
|
||||
panels = [node.panel for node in self.nodes]
|
||||
graph = SubarrayGraph(panels, self.system_type)
|
||||
return graph
|
||||
|
||||
def assemble_graph(self):
|
||||
for node in self.nodes:
|
||||
if len(node.neighbors) == 4:
|
||||
continue
|
||||
for direction in Direction.all():
|
||||
coordinate = node.location + direction.value
|
||||
neighbor = self.node_store.find_coordinate(coordinate)
|
||||
if neighbor:
|
||||
node.add_neighbor(neighbor, direction.opposite_direction())
|
||||
if len(node.neighbors) == 4:
|
||||
break
|
||||
|
||||
def reset(self):
|
||||
self.graph = list(self.nodes)
|
||||
self.current_rung = 0
|
||||
|
||||
def find_disconnected_subgraphs(self):
|
||||
graph = list(self.graph)
|
||||
subgraphs = []
|
||||
while len(graph) > 0:
|
||||
node = self.lower_left_node(graph)
|
||||
subgraph = self.add_all_neighbors(node)
|
||||
for subgraph_node in subgraph:
|
||||
try:
|
||||
graph.remove(subgraph_node)
|
||||
except:
|
||||
continue
|
||||
subgraphs.append(subgraph)
|
||||
return subgraphs
|
||||
|
||||
def find_node(self, coordinate):
|
||||
if coordinate.x < 0 or coordinate.y < 0:
|
||||
return None
|
||||
for node in self.graph:
|
||||
if node.location == coordinate:
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def add_all_neighbors(node):
|
||||
seen = {node}
|
||||
visited = set()
|
||||
visited_list = [] # apparently, order matters!
|
||||
while len(seen) > 0:
|
||||
node = seen.pop()
|
||||
if node in visited:
|
||||
continue
|
||||
visited.add(node)
|
||||
visited_list.append(node)
|
||||
for neighbor in node.neighbors.values():
|
||||
if neighbor not in visited and neighbor not in seen:
|
||||
seen.add(neighbor)
|
||||
return visited_list
|
||||
|
||||
@staticmethod
|
||||
def lower_left_node(graph):
|
||||
lower_left_node = None
|
||||
lower_left_location = Coordinate(float('inf'), float('inf'))
|
||||
for node in graph:
|
||||
node_location = node.location
|
||||
if node_location.x <= lower_left_location.x and node_location.y <= lower_left_location.y:
|
||||
lower_left_node = node
|
||||
lower_left_location = lower_left_node.location
|
||||
return lower_left_node
|
||||
|
||||
def pop_rung(self):
|
||||
if self.current_rung < len(self.rungs):
|
||||
rung = self.rungs[self.current_rung]
|
||||
self.current_rung += 1
|
||||
return rung
|
||||
rung = []
|
||||
|
||||
def assemble_rung_callback(node, next_direction, previous_direction):
|
||||
rung.append(node)
|
||||
if self.system_type == SystemType.dualTilt:
|
||||
east_west = [Direction.East, Direction.West]
|
||||
if next_direction in east_west or previous_direction in east_west:
|
||||
rung.append(node)
|
||||
|
||||
subgraphs = self.find_disconnected_subgraphs()
|
||||
for subgraph in subgraphs:
|
||||
try:
|
||||
start_node = self.lower_left_node(subgraph)
|
||||
except:
|
||||
break
|
||||
self.walk_graph_perimeter(start_node, assemble_rung_callback)
|
||||
|
||||
for node in rung:
|
||||
node.remove_neighbor_references()
|
||||
try:
|
||||
self.graph.remove(node)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.rungs.append(rung)
|
||||
self.current_rung += 1
|
||||
return rung
|
||||
|
||||
def walk_graph_perimeter(self, start_node, fn, repeat_steps=True):
|
||||
total_nodes = len(self.nodes)
|
||||
steps = 0
|
||||
node = start_node
|
||||
direction = Direction.East
|
||||
while True:
|
||||
next_node = None
|
||||
directions_to_try = list(direction.directions_to_try())
|
||||
last_direction = direction
|
||||
while next_node is None and len(directions_to_try) > 0:
|
||||
direction = directions_to_try.pop(0)
|
||||
next_node = node.step(direction)
|
||||
if next_node is None:
|
||||
break
|
||||
|
||||
fn(node, direction, last_direction)
|
||||
node = next_node
|
||||
steps += 1
|
||||
if node == start_node or (steps > total_nodes and repeat_steps):
|
||||
break
|
||||
19
helix/calculators/subarray_helper.py
Normal file
19
helix/calculators/subarray_helper.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from helix.models.subarray import Subarray
|
||||
|
||||
|
||||
def get_subarray_sizes_and_rows(panels):
|
||||
subarrays = []
|
||||
last_subarray = None
|
||||
for index, panel in enumerate(panels):
|
||||
if last_subarray != panel.subarray:
|
||||
subarray = Subarray(subarray_number=panel.subarray, start_row=index, size=0)
|
||||
|
||||
subarrays.append(subarray)
|
||||
last_subarray = panel.subarray
|
||||
subarrays[-1].size += 1
|
||||
|
||||
return subarrays
|
||||
|
||||
|
||||
def extract_subarray(panels, subarray_number):
|
||||
return [panel for panel in panels if panel.subarray == subarray_number]
|
||||
85
helix/calculators/summary_values_calculator.py
Normal file
85
helix/calculators/summary_values_calculator.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from collections import namedtuple
|
||||
|
||||
SummaryValues = namedtuple('SummaryValues', ['total_weight', 'max_psf', 'avg_psf', 'anchors', 'ballast', 'max_weight', 'ballast_weight'])
|
||||
|
||||
|
||||
class SummaryValuesCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
self.constants = self.values.module_system_constants()
|
||||
|
||||
def summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator):
|
||||
summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator)
|
||||
|
||||
return [
|
||||
{'label': 'Total System Weight (lbs)', 'value': summary_values.total_weight},
|
||||
{'label': 'Max PSF', 'value': summary_values.max_psf},
|
||||
{'label': 'Avg PSF', 'value': summary_values.avg_psf},
|
||||
{'label': 'Total Anchors', 'value': summary_values.anchors},
|
||||
{'label': 'Total Ballast', 'value': summary_values.ballast},
|
||||
{'label': 'Max Possible System Weight', 'value': summary_values.max_weight},
|
||||
{'label': 'Max System Weight Ballast Block', 'value': summary_values.ballast_weight},
|
||||
{'label': 'Seismic Anchor Max. Spacing', 'value': seismic_interval}
|
||||
]
|
||||
|
||||
def documentation_summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator):
|
||||
summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator)
|
||||
|
||||
return {
|
||||
'total_system_weight': summary_values.total_weight,
|
||||
'max_psf': summary_values.max_psf,
|
||||
'ave_psf': summary_values.avg_psf,
|
||||
'total_anchors': summary_values.anchors,
|
||||
'total_ballast': summary_values.ballast,
|
||||
'max_possible_system_weight': summary_values.max_weight,
|
||||
'max_system_weight_ballast_block': summary_values.ballast_weight,
|
||||
'seismic_anchor_max_spacing': seismic_interval
|
||||
}
|
||||
|
||||
def compute_summary(self, panels, subarrays, c_p_matrix, q_z, ballast_calculator):
|
||||
total_weight, avg_psf = self.system_weight_and_pressure(panels)
|
||||
|
||||
max_psf = 0
|
||||
wind_anchors = 0
|
||||
total_ballast = 0
|
||||
for panel in panels:
|
||||
max_psf = panel.pressure if panel.pressure > max_psf else max_psf
|
||||
wind_anchors += panel.wind_anchors
|
||||
total_ballast += panel.ballast
|
||||
|
||||
required_seismic_anchors = sum(subarray.required_seismic_anchors for subarray in subarrays)
|
||||
|
||||
total_anchors = int(wind_anchors + required_seismic_anchors)
|
||||
|
||||
max_weight, ballast_weight = self.find_max_system_weight(panels, c_p_matrix, q_z, ballast_calculator)
|
||||
|
||||
return SummaryValues(
|
||||
total_weight=round(total_weight),
|
||||
max_psf=round(max_psf, 2),
|
||||
avg_psf=round(avg_psf, 2),
|
||||
anchors=total_anchors,
|
||||
ballast=int(total_ballast),
|
||||
max_weight=round(max_weight, 0),
|
||||
ballast_weight=ballast_weight
|
||||
)
|
||||
|
||||
def system_weight_and_pressure(self, panels):
|
||||
constants = self.values.module_system_constants()
|
||||
effective_area = constants.surface_area / constants.ground_coverage_ratio
|
||||
|
||||
psf_sum = sum(panel.pressure for panel in panels)
|
||||
return psf_sum * effective_area, psf_sum / len(panels)
|
||||
|
||||
def find_max_system_weight(self, panels, c_p_matrix, q_z, ballast_calculator):
|
||||
copied_panels = list(panels)
|
||||
max_weight = 0
|
||||
ballast_block_weight_for_max_weight = 0
|
||||
for weight in range(12, 19):
|
||||
ballast_matrix = ballast_calculator.ballast_and_trays_matrix(c_p_matrix, q_z, copied_panels,
|
||||
ballast_block_weight=weight)
|
||||
|
||||
total_weight, _ = self.system_weight_and_pressure(ballast_matrix)
|
||||
if total_weight > max_weight:
|
||||
max_weight = total_weight
|
||||
ballast_block_weight_for_max_weight = weight
|
||||
return max_weight, ballast_block_weight_for_max_weight
|
||||
56
helix/calculators/wind_pressure_calculator.py
Normal file
56
helix/calculators/wind_pressure_calculator.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from math import log
|
||||
|
||||
from helix.constants.exposure_category import ExposureCategory
|
||||
from helix.constants.global_constants import k_zt, k_d
|
||||
|
||||
|
||||
class WindPressureCalculator(object):
|
||||
def __init__(self, user_values):
|
||||
self.values = user_values
|
||||
|
||||
def q_z(self, k_z):
|
||||
v = self.values.wind_speed()
|
||||
return 0.00256 * k_z * k_zt * k_d * v ** 2
|
||||
|
||||
def K_z(self):
|
||||
height = self.values.building_height()
|
||||
exposure = self.values.exposure_category()
|
||||
transition_distance = self.values.exposure_category_transition_distance()
|
||||
return self.calculate_k_z(height, exposure, transition_distance)
|
||||
|
||||
def calculate_k_z(self, h, exp, transition_distance):
|
||||
if exp == ExposureCategory.B and h < 30:
|
||||
return 0.7
|
||||
elif exp == ExposureCategory.B_C or exp == ExposureCategory.C_B:
|
||||
k_zd = self.k_zd(exp, h)
|
||||
return k_zd + self.delta_k(exp, transition_distance, k_zd)
|
||||
else:
|
||||
return 2.01 * (self.h_eff(h, exp) / exp.z_g()) ** (2 / exp.alpha())
|
||||
|
||||
def h_eff(self, h, exposure):
|
||||
if (exposure == ExposureCategory.C or exposure == ExposureCategory.D) and h < 15:
|
||||
return 15.
|
||||
else:
|
||||
return h
|
||||
|
||||
def k_zd(self, exp, h):
|
||||
if exp == ExposureCategory.B_C:
|
||||
h_eff = max(15, h)
|
||||
return self.calculate_expression(h_eff, exp.z_g()[1], exp.alpha()[1])
|
||||
elif exp == ExposureCategory.C_B and h < 30:
|
||||
return 0.7
|
||||
else:
|
||||
return self.calculate_expression(h, exp.z_g()[1], exp.alpha()[1])
|
||||
|
||||
def delta_k(self, exp, transition_distance, k_zd):
|
||||
k_upwind = self.calculate_expression(33, exp.z_g()[0], exp.alpha()[0])
|
||||
k_downwind = self.calculate_expression(33, exp.z_g()[1], exp.alpha()[1])
|
||||
return (k_upwind - k_downwind) * (k_zd / k_downwind) * self.dk_x(k_upwind, k_downwind, transition_distance / 5280.)
|
||||
|
||||
def dk_x(self, k_upwind, k_downwind, transition_distance_in_miles):
|
||||
x0 = 0.621 * 10 ** (-1 * ((k_upwind - k_downwind) ** 2) - 2.3)
|
||||
x1 = 6.21 if k_upwind > k_downwind else 62.1
|
||||
return log(x1 / transition_distance_in_miles, x1 / x0)
|
||||
|
||||
def calculate_expression(self, height, z_g, alpha):
|
||||
return 2.01 * (height / z_g) ** (2 / alpha)
|
||||
0
helix/constants/__init__.py
Normal file
0
helix/constants/__init__.py
Normal file
23
helix/constants/anchor_parts.py
Normal file
23
helix/constants/anchor_parts.py
Normal 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
|
||||
}
|
||||
29
helix/constants/anchor_type.py
Normal file
29
helix/constants/anchor_type.py
Normal 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
|
||||
4
helix/constants/bom_constants.py
Normal file
4
helix/constants/bom_constants.py
Normal 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
11
helix/constants/color.py
Normal 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"
|
||||
89
helix/constants/dual_tilt_parts.py
Normal file
89
helix/constants/dual_tilt_parts.py
Normal 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,
|
||||
}
|
||||
7
helix/constants/dxf_validation.py
Normal file
7
helix/constants/dxf_validation.py
Normal 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."
|
||||
281
helix/constants/ebom_parts.py
Normal file
281
helix/constants/ebom_parts.py
Normal 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,
|
||||
}
|
||||
}
|
||||
34
helix/constants/exposure_category.py
Normal file
34
helix/constants/exposure_category.py
Normal 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
|
||||
50
helix/constants/file_validation_error.py
Normal file
50
helix/constants/file_validation_error.py
Normal 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
|
||||
9
helix/constants/global_constants.py
Normal file
9
helix/constants/global_constants.py
Normal 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
|
||||
25
helix/constants/inverter_brand.py
Normal file
25
helix/constants/inverter_brand.py
Normal 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}
|
||||
94
helix/constants/inverter_type.py
Normal file
94
helix/constants/inverter_type.py
Normal 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()
|
||||
11
helix/constants/module_type.py
Normal file
11
helix/constants/module_type.py
Normal 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
|
||||
0
helix/constants/module_type_constants/__init__.py
Normal file
0
helix/constants/module_type_constants/__init__.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
46
helix/constants/panel_type.py
Normal file
46
helix/constants/panel_type.py
Normal 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
241
helix/constants/parts.py
Normal 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
|
||||
]
|
||||
7
helix/constants/redis_constant.py
Normal file
7
helix/constants/redis_constant.py
Normal 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)
|
||||
5
helix/constants/seismic_anchor_validation_error.py
Normal file
5
helix/constants/seismic_anchor_validation_error.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SeismicAnchorValidationError(Enum):
|
||||
TooFewAnchors = 'There are too few anchors in one or more subarrays'
|
||||
94
helix/constants/single_tilt_parts.py
Normal file
94
helix/constants/single_tilt_parts.py
Normal 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
|
||||
}
|
||||
6
helix/constants/sql_constant.py
Normal file
6
helix/constants/sql_constant.py
Normal 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'))
|
||||
6
helix/constants/subarray.py
Normal file
6
helix/constants/subarray.py
Normal 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'."
|
||||
60
helix/constants/system_type.py
Normal file
60
helix/constants/system_type.py
Normal 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]
|
||||
0
helix/constants/system_type_constants/__init__.py
Normal file
0
helix/constants/system_type_constants/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class DualTiltConstants(object):
|
||||
wind_zones = ['A', 'B', 'C', 'D', 'E']
|
||||
module_count = 2
|
||||
minimum_corner_module_count = 2
|
||||
@@ -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
|
||||
7
helix/constants/version.py
Normal file
7
helix/constants/version.py
Normal 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
107
helix/csv_builder.py
Normal 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
0
helix/db/__init__.py
Normal file
17
helix/db/redis_manager.py
Normal file
17
helix/db/redis_manager.py
Normal 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
26
helix/db/sql_manager.py
Normal 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)
|
||||
166
helix/doc_gen_params_builder.py
Normal file
166
helix/doc_gen_params_builder.py
Normal 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
0
helix/forms/__init__.py
Normal file
17
helix/forms/conditional_validator.py
Normal file
17
helix/forms/conditional_validator.py
Normal 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
183
helix/forms/ebom_form.py
Normal 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)
|
||||
6
helix/forms/grouped_form.py
Normal file
6
helix/forms/grouped_form.py
Normal 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
95
helix/forms/input_form.py
Normal 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
4
helix/functions.py
Normal file
@@ -0,0 +1,4 @@
|
||||
def fequal(x, y, delta=1e-6):
|
||||
if x == y:
|
||||
return True
|
||||
return abs(x - y) < delta
|
||||
0
helix/helpers/__init__.py
Normal file
0
helix/helpers/__init__.py
Normal file
152
helix/helpers/nodequadtree.py
Normal file
152
helix/helpers/nodequadtree.py
Normal 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)
|
||||
97
helix/helpers/polygon_helper.py
Normal file
97
helix/helpers/polygon_helper.py
Normal 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]
|
||||
206
helix/javascript/array_summary/array_visualization.js
Normal file
206
helix/javascript/array_summary/array_visualization.js
Normal 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;
|
||||
23
helix/javascript/array_summary/auto_upload.js
Normal file
23
helix/javascript/array_summary/auto_upload.js
Normal 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;
|
||||
13
helix/javascript/array_summary/colors.js
Normal file
13
helix/javascript/array_summary/colors.js
Normal 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
|
||||
20
helix/javascript/array_summary/index.js
Normal file
20
helix/javascript/array_summary/index.js
Normal 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;
|
||||
|
||||
});
|
||||
43
helix/javascript/array_summary/overlay_control.js
Normal file
43
helix/javascript/array_summary/overlay_control.js
Normal 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;
|
||||
169
helix/javascript/array_summary/panel.js
Normal file
169
helix/javascript/array_summary/panel.js
Normal 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};
|
||||
73
helix/javascript/array_summary/seismic_control.js
Normal file
73
helix/javascript/array_summary/seismic_control.js
Normal 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;
|
||||
49
helix/javascript/array_summary/subarray_display.js
Normal file
49
helix/javascript/array_summary/subarray_display.js
Normal 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;
|
||||
48
helix/javascript/array_summary/zoom_control.js
Normal file
48
helix/javascript/array_summary/zoom_control.js
Normal 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
470
helix/main.py
Normal 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
0
helix/models/__init__.py
Normal file
87
helix/models/coordinate.py
Normal file
87
helix/models/coordinate.py
Normal 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)
|
||||
1
helix/models/dxf/__init__.py
Normal file
1
helix/models/dxf/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__author__ = 'pivotal'
|
||||
8
helix/models/dxf/dxf_error.py
Normal file
8
helix/models/dxf/dxf_error.py
Normal 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
|
||||
47
helix/models/dxf/graph_direction.py
Normal file
47
helix/models/dxf/graph_direction.py
Normal 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]
|
||||
|
||||
68
helix/models/dxf/graph_node.py
Normal file
68
helix/models/dxf/graph_node.py
Normal 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
|
||||
41
helix/models/dxf/graph_node_store.py
Normal file
41
helix/models/dxf/graph_node_store.py
Normal 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
117
helix/models/dxf/polygon.py
Normal 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
130
helix/models/panel.py
Normal 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)
|
||||
0
helix/models/sql/__init__.py
Normal file
0
helix/models/sql/__init__.py
Normal file
14
helix/models/sql/inverter_brands.py
Normal file
14
helix/models/sql/inverter_brands.py
Normal 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,
|
||||
}
|
||||
29
helix/models/sql/inverters.py
Normal file
29
helix/models/sql/inverters.py
Normal 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,
|
||||
}
|
||||
21
helix/models/sql/power_monitors.py
Normal file
21
helix/models/sql/power_monitors.py
Normal 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
|
||||
}
|
||||
24
helix/models/sql/power_stations.py
Normal file
24
helix/models/sql/power_stations.py
Normal 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]
|
||||
}
|
||||
3
helix/models/sql/shared_sql_base.py
Normal file
3
helix/models/sql/shared_sql_base.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
41
helix/models/sql/sites.py
Normal file
41
helix/models/sql/sites.py
Normal 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")
|
||||
29
helix/models/sql/standalone_inverters.py
Normal file
29
helix/models/sql/standalone_inverters.py
Normal 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()) }
|
||||
9
helix/models/sql/users.py
Normal file
9
helix/models/sql/users.py
Normal 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
67
helix/models/subarray.py
Normal 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
Reference in New Issue
Block a user