247 lines
7.8 KiB
Python
247 lines
7.8 KiB
Python
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
|