first commit
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user