Files
old-krovovi-kalkulator/helix/calculators/subarray_graph.py
2017-11-07 09:23:57 +01:00

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