def index_map_to_region_map( hom_mat, reg2gen ): """ hom_mat : numpy matrix representing a map on generators (index map). reg2gen : dictionary mapping region -> generator(s). (I.e, which regions support which generators) Returns a DiGraph object of the map on regions in phase space. """ H = hom_mat R = reg2gen Rinv = invert_dictionary( R ) G = DiGraph() # find where region k maps to based the index map for k in R.keys(): # find generator connections if hasattr( H, 'nnz' ): if len( R[k] ) == 0: continue gen_conns, _J, _V = sparse.find( H[:,R[k]] ) else: # dense matrix case gen_conns = np.where( H[:,R[k]] != 0 )[0] gen_conns = gen_conns.tolist()[0] # fix matrix formatting for edge in gen_conns: for glist in Rinv.keys(): if edge in glist: G.add_edge( k, Rinv[glist][0] ) # return the graph so that we have access to the nodes labels that # correspond directly to regions with generators. return G
def condensation( G, components ): """Return the condensation of G with respect to the given components. Given components C_1 .. C_k which partition the nodes of G, the condensation graph cG has nodes <C_1> .. <C_k> and has edge (<C_i>,<C_j>) iff there was an edge in G from a node in C_i to a node in C_j. Note: the given components can be arbitrary, but if they are the strongly connected components (in the graph theory sense, not the dynamics sense) of G, then cG corresponds to the directed acyclic graph between strongly connected components. Parameters ---------- G : DiGraph components : list of lists of component nodes. Returns ------- cG : DiGraph The condensed graph. """ mapping = dict([(n,c) for c in range(len(components)) for n in components[c]]) cG = DiGraph() for u in mapping: cG.add_node(mapping[u]) for _,v in G.graph.edges_iter(u, data=False): if v not in components[mapping[u]]: cG.add_edge(mapping[u], mapping[v]) return cG
def condensation( G, components, loops=False ): """Return the condensation of G with respect to the given components. Given components C_1 .. C_k which partition the nodes of G, the condensation graph cG has nodes <C_1> .. <C_k> and has edge (<C_i>,<C_j>) iff there was an edge in G from a node in C_i to a node in C_j. Note: the given components can be arbitrary, but if they are the strongly connected components (in the graph theory sense, not the dynamics sense) of G, then cG corresponds to the directed acyclic graph between strongly connected components. Parameters ---------- G : DiGraph components : list of lists of component nodes, or dictionary of component label -> component nodes loops : whether to allow condensed nodes to have self loops (default: False) Returns ------- cG : DiGraph The condensed graph. """ # convert list to dict if isinstance(components,list): components = {c:components[c] for c in range(len(components))} mapping = {n:c for c in components for n in components[c]} cG = DiGraph() for u in mapping.keys(): cG.add_node(mapping[u]) for v in G.successors(u): # if v~u and u,v are in the same component, don't add the loop # (unless we allow loops) if loops or v not in components[mapping[u]]: cG.add_edge(mapping[u], mapping[v]) return cG
class FiniteRepresentation( object ): """ """ def __init__( self, data, tree, noise, expansion=1. ): """ Created a finite representation of the dynamics of a system from a collection of temporally ordered data points. data -- list or array of points in R^d. Index order specifies temporal order. tree -- BoxTree object initialized with the compact region containing the data. (See BoxTree class.) noise -- array of tuples or real numbers specifying the noise in each dimension of each data point. Hence, if data contains points in R^2, data[0] <--> noise[0] == ((a1,a2),(b1,b2)) or (a1, b1) if data[0] is centered within the error box determined bythe noise. Note: This scheme assume (for the time being) that noise if uniform about each data point. """ self.data = np.asarray( data, dtype=float ) self.noise = noise self.tree = tree self.dim = data.ndim # x_i |--> {grid elements} mapping self.data_hash = {} self.expansion = expansion # constant expansion rate (very # crude approximation of dynamics) def _add_box( self, box ): """ box -- lower left anchor in box[0], widths across dimensions in box[1]. data point center in noise box, and width in j'th dimension specified by 2*noise[j]. """ # shift center to lower left corner anchor bx = self._make_box( box ) self.tree.insert( bx ) def _add_boxes_tuple( self ): """ TODO """ print "NOT IMPLEMENTED!" def _make_box( self, b ): """ b -- tuple or array of """ anchor = b[0] - (b[1] / 2.) if type( b ) == tuple: width = self.dim * [ b[1] ] else: width = b[1] bx = np.array( [ anchor, width ] ) return bx def _expand_equal( self, idx ): """ Expand box dimension equally by self.expansion in all directions. Since boxes are anchored in lower left corner, shift each coordinate by (self.expansion * w - w )/2, where w is the width of the box in each dimension. box : array( [[ x1,...,xd ], [ w1,...,wd ]] ) Returns expanded and shifted box """ B = self.tree.boxes() anchors = B.corners[ idx ] width = B.width new_width = self.expansion * width shift = ( new_width - width ) / 2. new_anchors = anchors - shift new_box = np.array( [ new_anchors, new_width ] ) print new_box print "" return new_box def add_error_boxes( self ): """ Intersect each noise box containing a data point with the grid contained in Tree. This is used to construct the grid on the phase space without a MVM. the MVM function perform the same operation during the construction of the map. """ if hasattr( self.noise[0], '__len__' ): self._add_boxes_tuple() # TODO else: errors = izip( self.data, self.noise ) for box in errors: self._add_box( box ) def boxes( self ): """ Return a list of all boxes in the tree. """ return self.tree.boxes() def construct_mvm_simple( self ): """ Construct a directed graph on the boxes in self.tree. Boxes intersecting error_box[i] are mapped to boxes intersecting error_box[i+1]. [No expansion estimates are used.] """ # store the finite representation self.mvm = DiGraph() # generator to save memory error_boxes = izip( self.data, self.noise ) # pull first time step off the top data_idx = 0 pred = self._make_box( error_boxes.next() ) pred_ids = self.tree.search( pred ) self.data_hash[ data_idx ] = pred_ids # iteration starts at second element for succ in error_boxes: bx = self._make_box( succ ) self.tree.insert( bx ) succ_ids = self.tree.search( bx ) # loop over boxes in predecessor region and create edge to # those in successor regions for u in pred_ids: for v in succ_ids: self.mvm.add_edge( u, v ) # update predecessor for next time step pred = succ pred_ids = succ_ids data_idx += 1 self.data_hash[ data_idx ] = pred_ids def construct_mvm_expansion( self ): """ Construct a directed graph on the boxes in self.tree using. Boxes G_i intersecting error_box[i] are mapped to boxes G_{i+1} intersecting error_box[i+1] with expansion rate C. Thus the image boxes are expanded equally in all directions by a factor C > 1. This image is intersected with boxes in the tree and the image is updated. """ # store the finite representation self.mvm = DiGraph() # generator to save memory error_boxes = izip( self.data, self.noise ) # pull first time step off the top pred = self._make_box( error_boxes.next() ) pred_ids = self.tree.search( pred ) # loop optimizations maker = self._make_box expander = self._expand_equal tree_insert = self.tree.insert tree_search = self.tree.search # iteration starts at second element for succ in error_boxes: # error in 'box' form bx = self._make_box( succ ) # intersect with subdivision self.tree.insert( bx ) succ_ids = self.tree.search( bx ) # apply expansion to image ex_box = self._expand_equal( succ_ids ) # DANGER! THIS MIGHT DOUBLE INSERT BOXES self.tree.insert( ex_box ) # loop over boxes in predecessor region and connect to # those in successor regions for u in pred_ids: for v in succ_ids: self.mvm.add_edge( u, v ) # update predecessor for next time step pred = succ pred_ids = succ_ids def graph_mis( self ): self.mis = alg.graph_mis( self.mvm ) def trim_graph( self, copy=False ): """ Trim the MVM graph to include only nodes from the maximal invariant set (i.e. the strongly connected component == SCC). copy : boolean. Default False. If True, copy original MVM to self.full_mvm. In either case, self.mvm is replaced by the SCC. """ nbunch = set( self.mvm.nodes() ) scc = set( self.mis ) non_scc = nbunch - scc if copy: self.full_mvm = DiGraph() # copy returns NX.DiGraph() self.full_mvm.graph = self.mvm.copy() self.mvm.remove_nodes_from( non_scc ) def pickle_tree( self, fname, tree=None ): """ Extact necessary information to create a persistent object. """ if not tree: tree = self.tree b = tree.boxes() tree_dict = { 'corners' : b.corners, 'width' : b.width, 'dim' : b.dim, 'size' : b.size } with open( fname, 'wb' ) as fh: pkl.dump( tree_dict, fh ) def write_mvm( self, fname, stype='dot' ): """ A wrapper around NX's graph writers. fname : full path to save graph to type : 'pkl' or 'dot' (default). 'pkl' => pickle the graph. 'dot' => save graph in dot format (more portable) """ if stype == 'pkl': write_gpickle( self.mvm.graph, fname ) else: write_dot( self.mvm.graph, fname ) def show_boxes( self, color='b', alpha=0.6 ): """ """ fig = gfx.show_uboxes( self.boxes(), col=color ) return fig def show_error_boxes( self, box=None, color='r', alpha=0.6, fig=None ): """ """ error_boxes = izip( self.data, self.noise ) # boxes = [ self._make_box( b ) for b in error_boxes ] # gfx.show_boxes( error_boxes, S=range( len(boxes) ), col=color, # alpha=alpha, fig=fig ) for b in error_boxes: # shift center to lower left corner anchor bx = self._make_box( b ) fig = gfx.show_box( bx, col=color, alpha=alpha, fig=fig ) return fig