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