def graph(self, new_graph): if isinstance(new_graph, GraphLib): self._graph = GraphObject.to_graph_object(new_graph) elif isinstance(new_graph, GraphObject): self._graph = new_graph else: raise TypeError("The object passed is not a \ GraphObject but a {}".format(new_graph.__class__.__name__))
def __init__(self, nodes=0, name="Graph", weighted=True, directed=True, libgraph=None, **kwargs): ''' Initialize Graph instance Parameters ---------- nodes : int, optional (default: 0) Number of nodes in the graph. name : string, optional (default: "Graph") The name of this :class:`Graph` instance. weighted : bool, optional (default: True) Whether the graph edges have weight properties. directed : bool, optional (default: True) Whether the graph is directed or undirected. libgraph : :class:`~nngt.core.GraphObject`, optional An optional :class:`~nngt.core.GraphObject` to serve as base. Returns ------- self : :class:`~nggt.core.Graph` ''' self.__id = self.__class__.__max_id self._name = name self._directed = directed self._edges = [] # create the graphlib graph if libgraph is not None: self._graph = GraphObject.to_graph_object(libgraph) else: self._graph = GraphObject(nodes=nodes, directed=directed) # take care of the weights @todo: use those of the libgraph if weighted: if "weight_prop" in kwargs.keys(): self._w = kwargs["weight_prop"] else: self._w = {"distrib": "constant"} self.set_weights() # update the counters self.__class__.__num_graphs += 1 self.__class__.__max_id += 1
class Graph(object): """ The basic class that contains a :class:`graph_tool.Graph` and some of is properties or methods to easily access them. :ivar id: :class:`int` unique id that identifies the instance. :ivar graph: :class:`~nngt.core.GraphObject` main attribute of the class instance. """ #-------------------------------------------------------------------------# # Class properties __num_graphs = 0 __max_id = 0 #~ __di_property_func = { #~ "reciprocity": reciprocity, "clustering": clustering, #~ "assortativity": assortativity, "diameter": diameter, #~ "scc": num_scc, "wcc": num_wcc, "radius": spectral_radius, #~ "num_iedges": num_iedges } #~ __properties = __di_property_func.keys() @classmethod def num_graphs(cls): ''' Returns the number of alive instances. ''' return cls.__num_graphs @classmethod def from_matrix(cls, matrix, weighted=True, directed=True): ''' Creates a :class:`~nngt.Graph` from a :class:`scipy.sparse` matrix or a dense matrix. Parameters ---------- matrix : :class:`scipy.sparse` matrix or :class:`numpy.array` Adjacency matrix. weighted : bool, optional (default: True) Whether the graph edges have weight properties. directed : bool, optional (default: True) Whether the graph is directed or undirected. Returns ------- :class:`~nngt.Graph` ''' shape = matrix.shape if shape[0] != shape[1]: raise InvalidArgument('A square matrix is required') nodes = shape[0] if not directed: if issubclass(matrix.__class__, ssp.spmatrix): if not (matrix.T != matrix).nnz == 0: raise InvalidArgument('Incompatible directed=False option \ with non symmetric matrix provided.') else: if not (matrix.T == matrix).all(): raise InvalidArgument('Incompatible directed=False option \ with non symmetric matrix provided.') edges = np.array(matrix.nonzero()).T graph = cls(nodes,name='FromNpArray_{}'.format(cls.__num_graphs), weighted=weighted, directed=directed) graph.add_edges(edges) if weighted: weights = None if issubclass(matrix.__class__, ssp.spmatrix): weights = np.array(matrix[edges[:,0],edges[:,1]])[0] else: weights = matrix[dges[:,0],dges[:,1]] graph.set_weights(elist=edges, wlist=weights) return graph #-------------------------------------------------------------------------# # Constructor/destructor and properties def __init__(self, nodes=0, name="Graph", weighted=True, directed=True, libgraph=None, **kwargs): ''' Initialize Graph instance Parameters ---------- nodes : int, optional (default: 0) Number of nodes in the graph. name : string, optional (default: "Graph") The name of this :class:`Graph` instance. weighted : bool, optional (default: True) Whether the graph edges have weight properties. directed : bool, optional (default: True) Whether the graph is directed or undirected. libgraph : :class:`~nngt.core.GraphObject`, optional An optional :class:`~nngt.core.GraphObject` to serve as base. Returns ------- self : :class:`~nggt.core.Graph` ''' self.__id = self.__class__.__max_id self._name = name self._directed = directed self._edges = [] # create the graphlib graph if libgraph is not None: self._graph = GraphObject.to_graph_object(libgraph) else: self._graph = GraphObject(nodes=nodes, directed=directed) # take care of the weights @todo: use those of the libgraph if weighted: if "weight_prop" in kwargs.keys(): self._w = kwargs["weight_prop"] else: self._w = {"distrib": "constant"} self.set_weights() # update the counters self.__class__.__num_graphs += 1 self.__class__.__max_id += 1 def __del__(self): self.__class__.__num_graphs -= 1 @property def id(self): ''' unique :class:`int` identifying the instance ''' return self.__id @property def graph(self): ''' :class:`graph_tool.Graph` attribute of the instance ''' return self._graph @graph.setter def graph(self, new_graph): if isinstance(new_graph, GraphLib): self._graph = GraphObject.to_graph_object(new_graph) elif isinstance(new_graph, GraphObject): self._graph = new_graph else: raise TypeError("The object passed is not a \ GraphObject but a {}".format(new_graph.__class__.__name__)) @property def name(self): ''' name of the graph ''' return self._name @property def edges(self): return self._edges #-------------------------------------------------------------------------# # Graph actions def copy(self): ''' Returns a deepcopy of the current :class:`~nngt.core.Graph` instance ''' gc_instance = Graph(name=self._name+'_copy', weighted=self.is_weighted(), graph=self._graph.copy()) return gc_instance def add_edges(self, lst_edges): ''' Add a list of edges to the graph. Parameters ---------- lst_edges : list of 2-tuples or np.array of shape (edge_nb, 2) List of the edges that should be added as tuples (source, target) @todo: add example, check the edges for self-loops and multiple edges ''' self._graph.new_edges(lst_edges) self._edges.extend(lst_edges) def inhibitory_subgraph(self): ''' Create a :class:`~nngt.core.Graph` instance which graph contains only the inhibitory edges of the current instance's :class:`graph_tool.Graph` ''' eprop_b_type = self._graph.new_edge_property( "bool",-self._graph.edge_properties[TYPE].a+1) self._graph.set_edge_filter(eprop_b_type) inhib_graph = Graph( name=self._name + '_inhib', weighted=self.is_weighted(), graph=GraphObject(self._graph,prune=True) ) self._graph.clear_filters() return inhib_graph def excitatory_subgraph(self): ''' create a :class:`~nngt.core.Graph` instance which graph contains only the excitatory edges of the current instance's :class:`GraphObject` ''' eprop_b_type = self._graph.new_edge_property( "bool",self._graph.edge_properties[TYPE].a+1) self._graph.set_edge_filter(eprop_b_type) exc_graph = Graph( name=self._name + '_exc', weighted=self.is_weighted(), graph=GraphObject(self._graph,prune=True) ) self._graph.clear_filters() return exc_graph def adjacency_matrix(self, typed=True, weighted=True): ''' Returns the adjacency matrix of the graph as a :class:`scipy.sparse.csr_matrix`. Parameters ---------- weighted : bool or string, optional (default: True) If True, each entry ``adj[i,j] = w_ij`` where ``w_ij`` is the strength of the connection from `i` to `j`, if False, ``adj[i,j] = 0. or 1.``. Weighted can also be a string describing an edge attribute (e.g. if "distance" refers to an edge attribute ``dist``, then ``ajacency_matrix("distance")`` will return ``adj[i,i] = dist_ij``). Returns ------- adj : :class:`scipy.sparse.csr_matrix` The adjacency matrix of the graph. ''' return na.adjacency_matrix(self, typed=typed, weighted=weighted) def clear_edges(self): ''' Remove all the edges in the graph. ''' self._graph.clear_edges() def clear_edges(self): ''' Remove all the edges in the graph. ''' self._graph.clear_edges() n = self.node_nb() #-------------------------------------------------------------------------# # Setters def set_name(self, name=""): ''' set graph name ''' if name != "": self._name = name else: self._name = "Graph_" + str(self.__id) def set_edge_attribute(self, attribute, values=None, val=None, value_type=None): if attribute not in self.attributes(): self._graph.new_edge_attribute(attribute, value_type, values, val) else: num_edges = self.edge_nb() if values is None: if val is not None: values = np.repeat(val,num_edges) else: raise InvalidArgument("At least one of the `values` and \ `val` arguments should not be ``None``.") self._graph._eattr[attribute] = values def set_weights(self, elist=None, wlist=None, distrib=None, distrib_prop=None, correl=None, noise_scale=None): ''' Set the synaptic weights. @todo: take elist into account in Connections.weights Parameters ---------- elist : class:`numpy.array`, optional (default: None) List of the edges (for user defined weights). wlist : class:`numpy.array`, optional (default: None) List of the weights (for user defined weights). distrib : class:`string`, optional (default: None) Type of distribution (choose among "constant", "uniform", "gaussian", "lognormal", "lin_corr", "log_corr"). distrib_prop : dict, optional (default: {}) Dictoinary containing the properties of the weight distribution. correl : class:`string`, optional (default: None) Property to which the weights should be correlated. noise_scale : class:`int`, optional (default: None) Scale of the multiplicative Gaussian noise that should be applied on the weights. ''' if distrib is None: distrib = self._w["distrib"] if distrib_prop is None: distrib_prop = (self._w["distrib_prop"] if "distrib_prop" in self._w.keys() else {}) if correl is None: correl = self._w["correl"] if "correl" in self._w.keys() else None Connections.weights(self, elist=elist, wlist=wlist, distrib=distrib, correl=correl, distrib_prop=distrib_prop, noise_scale=noise_scale) #-------------------------------------------------------------------------# # Getters def attributes(self): ''' List of the graph's attributes (synaptic weights, delays...) ''' return self._graph._eattr.keys() def get_name(self): ''' Get the name of the graph ''' return self.__di_prop["name"] def node_nb(self): ''' Number of nodes in the graph ''' return self._graph.node_nb() def edge_nb(self): ''' Number of edges in the graph ''' return self._graph.edge_nb() def get_density(self): ''' Density of the graph: :math:`\\frac{E}{N^2}`, where `E` is the number of edges and `N` the number of nodes. ''' return self._graph.edge_nb()/float(self._graph.node_nb()**2) def is_weighted(self): ''' Whether the edges have weights ''' return "weight" in self.attributes() def is_directed(self): ''' Whether the graph is directed or not ''' return self._directed #~ def get_property(self, s_property): #~ ''' Return the desired property or None for an incorrect one. ''' #~ if s_property in Graph.__properties: #~ return Graph.__di_property_func[s_property](self._graph) #~ else: #~ warnings.warn("Ignoring request for unknown property \ #~ '{}'".format(s_property)) #~ return None #~ def get_properties(self, a_properties): #~ ''' #~ Return a dictionary containing the desired properties #~ #~ Parameters #~ ---------- #~ a_properties : sequence #~ List or tuple of strings of the property names. #~ #~ Returns #~ ------- #~ di_result : dict #~ A dictionary of values with the property names as keys. #~ ''' #~ di_result = { prop: self.get_property(prop) for prop in a_properties } #~ return di_result def get_degrees(self, node_list=None, deg_type="total", use_weights=True): ''' Degree sequence of all the nodes. Parameters ---------- node_list : list, optional (default: None) List of the nodes which degree should be returned deg_type : string, optional (default: "total") Degree type (among 'in', 'out' or 'total'). use_weights : bool, optional (default: True) Whether to use weighted (True) or simple degrees (False). Returns ------- :class:`numpy.array` or None (if an invalid type is asked). ''' valid_types = ("in", "out", "total") if deg_type in valid_types: return self._graph.degree_list(node_list, deg_type, use_weights) else: warnings.warn("Ignoring invalid degree type '{}'".format(strType)) return None def get_betweenness(self, use_weights=True): ''' Betweenness centrality sequence of all nodes and edges. Parameters ---------- use_weights : bool, optional (default: True) Whether to use weighted (True) or simple degrees (False). Returns ------- node_betweenness : :class:`numpy.array` Betweenness of the nodes. edge_betweenness : :class:`numpy.array` Betweenness of the edges. ''' return self._graph.betweenness(use_weights) def get_edge_types(self): if TYPE in self._graph.edge_properties.keys(): return self._graph.edge_properties[TYPE].a else: return repeat(1, self._graph.edge_nb()) def get_weights(self): ''' Returns the weighted adjacency matrix as a :class:`scipy.sparse.lil_matrix`. ''' return self._graph.eproperties["weight"] def is_spatial(self): ''' Whether the graph is embedded in space (has a :class:`~nngt.Shape` attribute). ''' return True if issubclass(self.__class__, SpatialGraph) else False