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