def __init__(self, vertices, undirected=True, W=None, sizeHint=1000): """ Create a PySparseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a sparse matrix W which is used as the weight matrix of the graph. Different kinds of sparse matrix can impact the speed of various operations. The currently supported sparse matrix types are: ll_mat. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a square sparse matrix of the same size as the number of vertices, or None to create the default one. :param sizeHint: the expected number of edges in the graph for efficient memory usage. :type sizeHint: :class:`int` """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, spmatrix.LLMatType) and W.shape == (len(self.vList), len(self.vList))): raise ValueError( "Input argument W must be None or spmatrix.ll_mat of size " + str(len(self.vList))) self.undirected = undirected if W == None: #Should use ll_mat_sym for undirected graphs but it has several unimplemented methods self.W = spmatrix.ll_mat(len(self.vList), len(self.vList), sizeHint) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W)
def __init__(self, vertices, undirected=True, W=None, dtype=numpy.float): """ Create a DenseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a numpy array W which is used as the weight matrix of the graph. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a numpy array of the same size as vertices, or None to create the default one. :param dtype: the data type of the weight matrix if W is not specified e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, numpy.ndarray) and W.shape == (len(self.vList), len(self.vList))): raise ValueError( "Input argument W must be None or numpy array of size " + str(len(self.vList))) self.undirected = undirected if W == None: self.W = numpy.zeros((len(self.vList), len(self.vList)), dtype=dtype) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W)
def __init__(self, vertices, undirected=True, dtype=numpy.float): """ Create a sparse graph using sppy csarray with a given AbstractVertexList, and specify whether directed. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param dtype: the data type for the weight matrix, e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) self.W = sppy.csarray( (self.vList.getNumVertices(), self.vList.getNumVertices()), dtype) self.undirected = undirected
def __init__(self, vertices, undirected=True, dtype=numpy.float): """ Create a sparse graph using sppy csarray with a given AbstractVertexList, and specify whether directed. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param dtype: the data type for the weight matrix, e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) self.W = sppy.csarray((self.vList.getNumVertices(), self.vList.getNumVertices()), dtype) self.undirected = undirected
def __init__(self, vertices, undirected=True, W=None, sizeHint=1000): """ Create a PySparseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a sparse matrix W which is used as the weight matrix of the graph. Different kinds of sparse matrix can impact the speed of various operations. The currently supported sparse matrix types are: ll_mat. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a square sparse matrix of the same size as the number of vertices, or None to create the default one. :param sizeHint: the expected number of edges in the graph for efficient memory usage. :type sizeHint: :class:`int` """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, spmatrix.LLMatType) and W.shape == (len(self.vList), len(self.vList))): raise ValueError("Input argument W must be None or spmatrix.ll_mat of size " + str(len(self.vList))) self.undirected = undirected if W == None: #Should use ll_mat_sym for undirected graphs but it has several unimplemented methods self.W = spmatrix.ll_mat(len(self.vList), len(self.vList), sizeHint) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W)
def __init__(self, vertices, undirected=True, W=None, dtype=numpy.float): """ Create a DenseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a numpy array W which is used as the weight matrix of the graph. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a numpy array of the same size as vertices, or None to create the default one. :param dtype: the data type of the weight matrix if W is not specified e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, numpy.ndarray) and W.shape == (len(self.vList), len(self.vList))): raise ValueError("Input argument W must be None or numpy array of size " + str(len(self.vList))) self.undirected = undirected if W == None: self.W = numpy.zeros((len(self.vList), len(self.vList)), dtype=dtype) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W)
class DenseGraph(AbstractMatrixGraph): def __init__(self, vertices, undirected=True, W=None, dtype=numpy.float): """ Create a DenseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a numpy array W which is used as the weight matrix of the graph. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a numpy array of the same size as vertices, or None to create the default one. :param dtype: the data type of the weight matrix if W is not specified e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, numpy.ndarray) and W.shape == (len(self.vList), len(self.vList))): raise ValueError( "Input argument W must be None or numpy array of size " + str(len(self.vList))) self.undirected = undirected if W == None: self.W = numpy.zeros((len(self.vList), len(self.vList)), dtype=dtype) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W) def getNumEdges(self): """ Returns the total number of edges in this graph. """ if self.undirected: return (numpy.flatnonzero(self.W).shape[0] + numpy.flatnonzero(numpy.diag(self.W)).shape[0]) / 2 else: return numpy.flatnonzero(self.W).shape[0] def getNumDirEdges(self): """ Returns the number of edges, taking this graph as a directed graph. """ return numpy.flatnonzero(self.W).shape[0] def getWeightMatrix(self): """ Return the weight matrix as a numpy array. """ return self.W def neighbours(self, vertexIndex): """ Return an array of the indices of the neighbours of the given vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) nonZeroIndices = numpy.nonzero(self.W[vertexIndex, :]) neighbourIndices = nonZeroIndices[0] return neighbourIndices def neighbourOf(self, vertexIndex): """ Return an array of the indices of vertices than have an edge going to the input vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) nonZeroIndices = numpy.nonzero(self.W[:, vertexIndex]) neighbourIndices = nonZeroIndices[0] return neighbourIndices def complement(self): """ Returns a graph with identical vertices (same reference) to the current one, but with the complement of the set of edges. Edges that do not exist have weight 1. """ newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = (self.W == 0).astype(self.W.dtype) return newGraph def outDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = numpy.sum(self.W[i, :] != 0) return degrees def inDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = numpy.sum(self.W[:, i] != 0) return degrees def subgraph(self, vertexIndices): """ Pass in a list or set of vertexIndices and returns the subgraph containing those vertices only, and edges between them. :param vertexIndices: the indices of the subgraph vertices. :type vertexIndices: :class:`list` """ Parameter.checkList(vertexIndices, Parameter.checkIndex, (0, self.getNumVertices())) vertexIndices = numpy.unique(numpy.array(vertexIndices)).tolist() vList = self.vList.subList(vertexIndices) subGraph = DenseGraph(vList, self.undirected, self.W.dtype) subGraph.W = self.W[vertexIndices, :][:, vertexIndices] return subGraph def add(self, graph): """ Add the edge weights of the input graph to the current one. Results in a union of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with same vertex list and addition of edge weights """ Parameter.checkClass(graph, DenseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = self.W + graph.W return newGraph def copy(self): """ Returns a copy of this object, which also has a copy of the VertexList. """ graph = DenseGraph(self.vList.copy(), self.undirected, self.W.dtype) graph.W = self.W.copy() return graph def multiply(self, graph): """ Multiply the edge weights of the input graph to the current one. Results in an intersection of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with edge weights which are multiples of the current and graph """ Parameter.checkClass(graph, DenseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = self.W * graph.W return newGraph def intersect(self, graph): """ Take the intersection of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with the intersection of edges of the current plus graph """ newGraph = self.multiply(graph) newGraph.W = (newGraph.W != 0).astype(newGraph.W.dtype) return newGraph def union(self, graph): """ Take the union of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with the union of edges of the current one. """ newGraph = self.add(graph) newGraph.W = (newGraph.W != 0).astype(newGraph.W.dtype) return newGraph def weightMatrixDType(self): """ :returns: the dtype of the matrix used to store edge weights. """ return self.W.dtype def setDiff(self, graph): """ Find the edges in the current graph which are not present in the input graph. Replaces the edges in the current graph with adjacencies. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: The graph which is the set difference of the edges of this graph and graph. """ Parameter.checkClass(graph, DenseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError( "Both graphs must be either undirected or directed") A1 = self.adjacencyMatrix() A2 = graph.adjacencyMatrix() A1 = A1 - A2 A1 = (A1 + numpy.abs(A1**2)) / 2 newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = A1 return newGraph def getAllDirEdges(self): """ Returns the set of directed edges of the current graph as a matrix in which each row corresponds to an edge. For an undirected graph, there is an edge from v1 to v2 and from v2 to v1 if v2!=v1. :returns: A matrix with 2 columns, and each row corresponding to an edge. """ (rows, cols) = numpy.nonzero(self.W) edges = numpy.c_[rows, cols] return edges @staticmethod def loadMatrix(filename): M = scipy.io.mmread(filename) if scipy.sparse.issparse(M): M = M.todense() return M def saveMatrix(self, W, filename): scipy.io.mmwrite(filename, W) def setWeightMatrix(self, W): """ Set the weight matrix of this graph. Requires as input an ndarray or a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The weight matrix to use. :type W: :class:`ndarray` or :class:`scipy.sparse` matrix """ if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) if self.undirected and type(W) == numpy.ndarray and (W != W.T).any(): raise ValueError( "Weight matrix of undirected graph must be symmetric") if self.undirected and scipy.sparse.issparse( W) and not SparseUtils.equals(W, W.T): raise ValueError( "Weight matrix of undirected graph must be symmetric") if scipy.sparse.issparse(W): W = W.todense() self.W = numpy.array(W) undirected = None vList = None W = None
class CsArrayGraph(AbstractMatrixGraph): def __init__(self, vertices, undirected=True, dtype=numpy.float): """ Create a sparse graph using sppy csarray with a given AbstractVertexList, and specify whether directed. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param dtype: the data type for the weight matrix, e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) self.W = sppy.csarray((self.vList.getNumVertices(), self.vList.getNumVertices()), dtype) self.undirected = undirected def getNumEdges(self): """ Returns the total number of edges in this graph. """ if self.undirected: return (self.W.getnnz() + numpy.flatnonzero(self.W.diag()).shape[0])/2 else: return self.W.getnnz() def getNumDirEdges(self): """ Returns the number of edges, taking this graph as a directed graph. """ return self.W.getnnz() def getWeightMatrix(self): """ Return the weight matrix as a numpy array. """ return self.W.toarray() def neighbours(self, vertexIndex): """ Return an array of the indices of the neighbours of the given vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) neighbourIndices = self.W.rowInds(vertexIndex) return neighbourIndices def neighbourOf(self, vertexIndex): """ Return an array of the indices of vertices than have an edge going to the input vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) nonZeroIndices = numpy.nonzero(self.W[:, vertexIndex]) neighbourIndices = nonZeroIndices[0] return neighbourIndices def complement(self): """ Returns a graph with identical vertices (same reference) to the current one, but with the complement of the set of edges. Edges that do not exist have weight 1. """ newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = sppy.ones((newGraph.W.shape)) newGraph.W[self.W.nonzero()] = 0 newGraph.W.prune() newGraph.W.compress() return newGraph def outDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = self.W[i, :].getnnz() return degrees def inDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = self.W[:, i].getnnz() return degrees def subgraph(self, vertexIndices): """ Pass in a list or set of vertexIndices and returns the subgraph containing those vertices only, and edges between them. :param vertexIndices: the indices of the subgraph vertices. :type vertexIndices: :class:`list` """ Parameter.checkList(vertexIndices, Parameter.checkIndex, (0, self.getNumVertices())) vertexIndices = numpy.unique(numpy.array(vertexIndices)).tolist() vList = self.vList.subList(vertexIndices) subGraph = CsArrayGraph(vList, self.undirected, self.W.dtype) subGraph.W = self.W[vertexIndices, :][:, vertexIndices] return subGraph def add(self, graph): """ Add the edge weights of the input graph to the current one. Results in a union of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with same vertex list and addition of edge weights """ Parameter.checkClass(graph, CsArrayGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = self.W + graph.W return newGraph def copy(self): """ Returns a copy of this object, which also has a copy of the VertexList. """ graph = CsArrayGraph(self.vList.copy(), self.undirected, self.W.dtype) graph.W = self.W.copy() return graph def multiply(self, graph): """ Multiply the edge weights of the input graph to the current one. Results in an intersection of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with edge weights which are multiples of the current and graph """ Parameter.checkClass(graph, CsArrayGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = self.W.hadamard(graph.W) return newGraph def intersect(self, graph): """ Take the intersection of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with the intersection of edges of the current plus graph """ newGraph = self.multiply(graph) newGraph.W[newGraph.W.nonzero()] = 1 return newGraph def union(self, graph): """ Take the union of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with the union of edges of the current one. """ newGraph = self.add(graph) newGraph.W[newGraph.W.nonzero()] = 1 return newGraph def weightMatrixDType(self): """ :returns: the dtype of the matrix used to store edge weights. """ return self.W.dtype def setDiff(self, graph): """ Find the edges in the current graph which are not present in the input graph. Replaces the edges in the current graph with adjacencies. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: The graph which is the set difference of the edges of this graph and graph. """ Parameter.checkClass(graph, CsArrayGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError("Both graphs must be either undirected or directed") A1 = self.adjacencyMatrix() A2 = graph.adjacencyMatrix() A1 = A1 - A2 A1 = (A1 + numpy.abs(A1**2))/2 newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = sppy.csarray(A1) return newGraph def getAllDirEdges(self): """ Returns the set of directed edges of the current graph as a matrix in which each row corresponds to an edge. For an undirected graph, there is an edge from v1 to v2 and from v2 to v1 if v2!=v1. :returns: A matrix with 2 columns, and each row corresponding to an edge. """ (rows, cols) = numpy.nonzero(self.W) edges = numpy.c_[rows, cols] return edges @staticmethod def loadMatrix(filename): M = scipy.io.mmread(filename) if type(M) == numpy.ndarray: M2 = sppy.csarray(M) elif scipy.sparse.issparse(M): M2 = sppy.csarray(M.shape, dtype=M.dtype) M2[M.nonzero()] = M.data return M2 def saveMatrix(self, W, filename): W = W.toScipyCsc() scipy.io.mmwrite(filename, W) def setWeightMatrix(self, W): """ Set the weight matrix of this graph. Requires as input an ndarray or a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The weight matrix to use. :type W: :class:`ndarray` or :class:`scipy.sparse` matrix """ if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) if self.undirected and type(W) == numpy.ndarray and (W != W.T).any(): raise ValueError("Weight matrix of undirected graph must be symmetric") if self.undirected and scipy.sparse.issparse(W) and not SparseUtils.equals(W, W.T): raise ValueError("Weight matrix of undirected graph must be symmetric") if scipy.sparse.issparse(W): W = W.todense() self.W = numpy.array(W) def removeAllEdges(self): """ Removes all edges from this graph. """ self.W.setZero() def setWeightMatrixSparse(self, W): """ Set the weight matrix of this graph. Requires as input a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The scipy sparse weight matrix to use. """ self.W[W.nonzero()] = W.data def addVertices(self, n): """ Adds n vertices to the current graph. This is not an efficient operation as we create a new weight matrix and copy the old one. The old vertices are the first m at the start of the new graph. """ W2 = sppy.csarray((self.W.shape[0]+n, self.W.shape[0]+n), self.W.dtype) W2[self.W.nonzero()] = self.W.values() self.W = W2 self.vList.addVertices(n) undirected = None vList = None W = None
class CsArrayGraph(AbstractMatrixGraph): def __init__(self, vertices, undirected=True, dtype=numpy.float): """ Create a sparse graph using sppy csarray with a given AbstractVertexList, and specify whether directed. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param dtype: the data type for the weight matrix, e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) self.W = sppy.csarray( (self.vList.getNumVertices(), self.vList.getNumVertices()), dtype) self.undirected = undirected def getNumEdges(self): """ Returns the total number of edges in this graph. """ if self.undirected: return (self.W.getnnz() + numpy.flatnonzero(self.W.diag()).shape[0]) / 2 else: return self.W.getnnz() def getNumDirEdges(self): """ Returns the number of edges, taking this graph as a directed graph. """ return self.W.getnnz() def getWeightMatrix(self): """ Return the weight matrix as a numpy array. """ return self.W.toarray() def neighbours(self, vertexIndex): """ Return an array of the indices of the neighbours of the given vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) neighbourIndices = self.W.rowInds(vertexIndex) return neighbourIndices def neighbourOf(self, vertexIndex): """ Return an array of the indices of vertices than have an edge going to the input vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) nonZeroIndices = numpy.nonzero(self.W[:, vertexIndex]) neighbourIndices = nonZeroIndices[0] return neighbourIndices def complement(self): """ Returns a graph with identical vertices (same reference) to the current one, but with the complement of the set of edges. Edges that do not exist have weight 1. """ newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = sppy.ones((newGraph.W.shape)) newGraph.W[self.W.nonzero()] = 0 newGraph.W.prune() newGraph.W.compress() return newGraph def outDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = self.W[i, :].getnnz() return degrees def inDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = self.W[:, i].getnnz() return degrees def subgraph(self, vertexIndices): """ Pass in a list or set of vertexIndices and returns the subgraph containing those vertices only, and edges between them. :param vertexIndices: the indices of the subgraph vertices. :type vertexIndices: :class:`list` """ Parameter.checkList(vertexIndices, Parameter.checkIndex, (0, self.getNumVertices())) vertexIndices = numpy.unique(numpy.array(vertexIndices)).tolist() vList = self.vList.subList(vertexIndices) subGraph = CsArrayGraph(vList, self.undirected, self.W.dtype) subGraph.W = self.W[vertexIndices, :][:, vertexIndices] return subGraph def add(self, graph): """ Add the edge weights of the input graph to the current one. Results in a union of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with same vertex list and addition of edge weights """ Parameter.checkClass(graph, CsArrayGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = self.W + graph.W return newGraph def copy(self): """ Returns a copy of this object, which also has a copy of the VertexList. """ graph = CsArrayGraph(self.vList.copy(), self.undirected, self.W.dtype) graph.W = self.W.copy() return graph def multiply(self, graph): """ Multiply the edge weights of the input graph to the current one. Results in an intersection of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with edge weights which are multiples of the current and graph """ Parameter.checkClass(graph, CsArrayGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = self.W.hadamard(graph.W) return newGraph def intersect(self, graph): """ Take the intersection of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with the intersection of edges of the current plus graph """ newGraph = self.multiply(graph) newGraph.W[newGraph.W.nonzero()] = 1 return newGraph def union(self, graph): """ Take the union of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: A new graph with the union of edges of the current one. """ newGraph = self.add(graph) newGraph.W[newGraph.W.nonzero()] = 1 return newGraph def weightMatrixDType(self): """ :returns: the dtype of the matrix used to store edge weights. """ return self.W.dtype def setDiff(self, graph): """ Find the edges in the current graph which are not present in the input graph. Replaces the edges in the current graph with adjacencies. :param graph: the input graph. :type graph: :class:`apgl.graph.CsArrayGraph` :returns: The graph which is the set difference of the edges of this graph and graph. """ Parameter.checkClass(graph, CsArrayGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError( "Both graphs must be either undirected or directed") A1 = self.adjacencyMatrix() A2 = graph.adjacencyMatrix() A1 = A1 - A2 A1 = (A1 + numpy.abs(A1**2)) / 2 newGraph = CsArrayGraph(self.vList, self.undirected) newGraph.W = sppy.csarray(A1) return newGraph def getAllDirEdges(self): """ Returns the set of directed edges of the current graph as a matrix in which each row corresponds to an edge. For an undirected graph, there is an edge from v1 to v2 and from v2 to v1 if v2!=v1. :returns: A matrix with 2 columns, and each row corresponding to an edge. """ (rows, cols) = numpy.nonzero(self.W) edges = numpy.c_[rows, cols] return edges @staticmethod def loadMatrix(filename): M = scipy.io.mmread(filename) if type(M) == numpy.ndarray: M2 = sppy.csarray(M) elif scipy.sparse.issparse(M): M2 = sppy.csarray(M.shape, dtype=M.dtype) M2[M.nonzero()] = M.data return M2 def saveMatrix(self, W, filename): W = W.toScipyCsc() scipy.io.mmwrite(filename, W) def setWeightMatrix(self, W): """ Set the weight matrix of this graph. Requires as input an ndarray or a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The weight matrix to use. :type W: :class:`ndarray` or :class:`scipy.sparse` matrix """ if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) if self.undirected and type(W) == numpy.ndarray and (W != W.T).any(): raise ValueError( "Weight matrix of undirected graph must be symmetric") if self.undirected and scipy.sparse.issparse( W) and not SparseUtils.equals(W, W.T): raise ValueError( "Weight matrix of undirected graph must be symmetric") if scipy.sparse.issparse(W): W = W.todense() self.W = numpy.array(W) def removeAllEdges(self): """ Removes all edges from this graph. """ self.W.setZero() def setWeightMatrixSparse(self, W): """ Set the weight matrix of this graph. Requires as input a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The scipy sparse weight matrix to use. """ self.W[W.nonzero()] = W.data def addVertices(self, n): """ Adds n vertices to the current graph. This is not an efficient operation as we create a new weight matrix and copy the old one. The old vertices are the first m at the start of the new graph. """ W2 = sppy.csarray((self.W.shape[0] + n, self.W.shape[0] + n), self.W.dtype) W2[self.W.nonzero()] = self.W.values() self.W = W2 self.vList.addVertices(n) undirected = None vList = None W = None
class PySparseGraph(AbstractMatrixGraph): ''' Represents a graph, which can be directed or undirected, and has weights on the edges. Memory usage is efficient for sparse graphs. The list of vertices is immutable (see VertexList), however edges can be added or removed. Only non-zero edges can be added. Uses Pysparse as the underlying matrix representation. ''' def __init__(self, vertices, undirected=True, W=None, sizeHint=1000): """ Create a PySparseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a sparse matrix W which is used as the weight matrix of the graph. Different kinds of sparse matrix can impact the speed of various operations. The currently supported sparse matrix types are: ll_mat. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a square sparse matrix of the same size as the number of vertices, or None to create the default one. :param sizeHint: the expected number of edges in the graph for efficient memory usage. :type sizeHint: :class:`int` """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, spmatrix.LLMatType) and W.shape == (len(self.vList), len(self.vList))): raise ValueError("Input argument W must be None or spmatrix.ll_mat of size " + str(len(self.vList))) self.undirected = undirected if W == None: #Should use ll_mat_sym for undirected graphs but it has several unimplemented methods self.W = spmatrix.ll_mat(len(self.vList), len(self.vList), sizeHint) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W) def neighbours(self, vertexIndex): """ Return an array of the indices of neighbours. In the case of a directed graph it is an array of those vertices connected by an edge from the current one. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` :returns: An array of the indices of all neigbours of the input vertex. """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) neighbours = PySparseUtils.nonzero(self.W[int(vertexIndex), :])[1] return numpy.array(neighbours) def neighbourOf(self, vertexIndex): """ Return an array of the indices of vertices than have an edge going to the input vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` :returns: An array of the indices of all vertices with an edge towards the input vertex. """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) neighbours = PySparseUtils.nonzero(self.W[:, vertexIndex])[0] return numpy.array(neighbours) def addEdge(self, vertexIndex1, vertexIndex2, edge=1): """ Add a non-zero edge between two vertices. :param vertexIndex1: The index of the first vertex. :type vertexIndex1: :class:`int` :param vertexIndex2: The index of the second vertex. :type vertexIndex2: :class:`int` :param edge: The value of the edge. :type edge: :class:`float` """ Parameter.checkIndex(vertexIndex1, 0, self.vList.getNumVertices()) Parameter.checkIndex(vertexIndex2, 0, self.vList.getNumVertices()) vertexIndex1 = int(vertexIndex1) vertexIndex2 = int(vertexIndex2) if edge == 0 or edge == float('inf'): raise ValueError("Cannot add a zero or infinite edge") if self.undirected: self.W[vertexIndex1, vertexIndex2] = edge self.W[vertexIndex2, vertexIndex1] = edge else: self.W[vertexIndex1, vertexIndex2] = edge def getNumEdges(self): """ Returns the total number of edges in this graph. """ if self.getNumVertices()==0: return 0 if self.undirected: inds = numpy.arange(self.getNumVertices()) #d = spmatrix.ll_mat(self.getNumVertices(), 1) d = numpy.zeros(self.getNumVertices()) self.W.take(d, inds, inds) return (self.W.nnz + numpy.sum(d!=0))/2 else: return self.W.nnz def getNumDirEdges(self): """ Returns the number of edges, taking this graph as a directed graph. """ return self.W.nnz def outDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ A = self.nativeAdjacencyMatrix() j = spmatrix.ll_mat(self.vList.getNumVertices(), 1) j[:, 0] = 1 degrees = spmatrix.matrixmultiply(A, j) degrees = PysparseMatrix(matrix=degrees) degrees = numpy.array(degrees.getNumpyArray().ravel(), numpy.int) return degrees def inDegreeSequence(self): """ Return a vector of the (in)degree sequence for each vertex. """ A = self.nativeAdjacencyMatrix() j = spmatrix.ll_mat(self.vList.getNumVertices(), 1) j[:, 0] = 1 degrees = spmatrix.dot(A, j) degrees = PysparseMatrix(matrix=degrees) degrees = numpy.array(degrees.getNumpyArray().ravel(), numpy.int) return degrees def nativeAdjacencyMatrix(self): """ Return the adjacency matrix in sparse format. """ A = spmatrix.ll_mat(self.vList.getNumVertices(), self.vList.getNumVertices()) nonzeros = PySparseUtils.nonzero(self.W) A.put(1, nonzeros[0], nonzeros[1]) return A def subgraph(self, vertexIndices): """ Pass in a list or set of vertexIndices and returns the subgraph containing those vertices only, and edges between them. :param vertexIndices: the indices of the subgraph vertices. :type vertexIndices: :class:`list` :returns: A new PySparseGraph containing only vertices and edges from vertexIndices """ Parameter.checkList(vertexIndices, Parameter.checkIndex, (0, self.getNumVertices())) vertexIndices = numpy.unique(numpy.array(vertexIndices)) vList = self.vList.subList(vertexIndices.tolist()) subGraph = PySparseGraph(vList, self.undirected) if len(vertexIndices) != 0: subGraph.W = self.W[vertexIndices, vertexIndices] return subGraph def getWeightMatrix(self): """ Return the weight matrix in dense format. Warning: should not be used unless sufficient memory is available to store the dense matrix. :returns: A numpy.ndarray weight matrix. """ W = PysparseMatrix(matrix=self.W) return W.getNumpyArray() def getAllDirEdges(self): """ Returns the set of directed edges of the current graph as a matrix in which each row corresponds to an edge. For an undirected graph, there is an edge from v1 to v2 and from v2 to v1 if v2!=v1. :returns: A matrix with 2 columns, and each row corresponding to an edge. """ (rows, cols) = PySparseUtils.nonzero(self.W) edges = numpy.c_[rows, cols] return edges def add(self, graph): """ Add the edge weights of the input graph to the current one. Results in a union of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.PySparseGraph` :returns: A new graph with same vertex list and addition of edge weights """ Parameter.checkClass(graph, PySparseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError("Both graphs must be either undirected or directed") newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W = self.W.copy() newGraph.W.shift(1, graph.W) return newGraph def copy(self): """ Returns a copy of this object, which also has a copy of the VertexList. """ newGraph = PySparseGraph(self.vList.copy(), self.undirected) newGraph.W = self.W.copy() return newGraph def removeAllEdges(self): """ Removes all edges from this graph. """ #Not sure why this doesn't work #self.W.scale(0) self.W = spmatrix.ll_mat(self.getNumVertices(), self.getNumVertices()) def setWeightMatrix(self, W): """ Set the weight matrix of this graph. Requires as input an ndarray with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The name of the file to load. :type W: :class:`ndarray` """ #Parameter.checkClass(W, numpy.ndarray) if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) if self.undirected and type(W) == numpy.ndarray and (W != W.T).any(): raise ValueError("Weight matrix of undirected graph must be symmetric") self.W = spmatrix.ll_mat(self.getNumVertices(), self.getNumVertices()) if type(W) == numpy.ndarray: rowInds, colInds = numpy.nonzero(W) self.W.put(W[(rowInds, colInds)], rowInds, colInds) elif isinstance(W, spmatrix.LLMatType): self.setWeightMatrixSparse(W) else: raise ValueError("Invalid matrix type: " + str(type(W))) def weightMatrixType(self): """ Returns the type of the sparse matrix used to store edge weights. """ return type(self.W) def complement(self): """ Returns a graph with identical vertices (same reference) to the current one, but with the complement of the set of edges. Edges that do not exist have weight 1. This makes a sparse graph dense. :returns: A new graph with edges complmenting the current one. """ newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W[:, :] = 1 A = self.nativeAdjacencyMatrix() newGraph.W.shift(-1, A) return newGraph def multiply(self, graph): """ Multiply the edge weights of the input graph to the current one. Results in an intersection of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.PySparseGraph` :returns: A new graph with edge weights which are multiples of the current and graph """ Parameter.checkClass(graph, PySparseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError("Both graphs must be either undirected or directed") if self.W.nnz < graph.W.nnz: (rows, cols) = PySparseUtils.nonzero(self.W) else: (rows, cols) = PySparseUtils.nonzero(graph.W) arr1 = numpy.zeros(len(rows)) arr2 = numpy.zeros(len(rows)) self.W.take(arr1, rows, cols) graph.W.take(arr2, rows, cols) arr1 = arr1 * arr2 newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W.put(arr1, rows, cols) return newGraph def setDiff(self, graph): """ Find the edges in the current graph which are not present in the input graph. :param graph: the input graph. :type graph: :class:`apgl.graph.PySparseGraph` :returns: A new graph with edges from the current graph and not in the input graph. """ Parameter.checkClass(graph, PySparseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError("Both graphs must be either undirected or directed") A1 = self.nativeAdjacencyMatrix() A2 = graph.nativeAdjacencyMatrix() (rows, cols) = PySparseUtils.nonzero(A1) arr1 = numpy.zeros(len(rows)) arr2 = numpy.zeros(len(rows)) A1.take(arr1, rows, cols) A2.take(arr2, rows, cols) arr1 = arr1 - arr2 A1.put(arr1, rows, cols) newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W = A1 return newGraph def getEdge(self, vertexIndex1, vertexIndex2): """ Get the value of an edge, or None if no edge exists. :param vertexIndex1: The index of the first vertex. :type vertexIndex1: :class:`int` :param vertexIndex2: The index of the second vertex. :type vertexIndex2: :class:`int` :returns: The value of the edge between the given vertex indices. """ Parameter.checkIndex(vertexIndex1, 0, self.vList.getNumVertices()) Parameter.checkIndex(vertexIndex2, 0, self.vList.getNumVertices()) vertexIndex1 = int(vertexIndex1) vertexIndex2 = int(vertexIndex2) if self.W[vertexIndex1, vertexIndex2]==0: return None else: return self.W[vertexIndex1, vertexIndex2] @staticmethod def loadMatrix(filename): return spmatrix.ll_mat_from_mtx(filename) def saveMatrix(self, W, filename): W.export_mtx(filename) def setWeightMatrixSparse(self, W): """ Set the weight matrix of this graph. Requires as input a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The weight matrix to use. """ if not isinstance(W, spmatrix.LLMatType) and not sparse.issparse(W): raise ValueError("Input must be a sparse matrix, not " + str(type(W))) if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) try: self.W = spmatrix.ll_mat(W.shape[0], W.shape[0], W.getnnz()) except AttributeError: self.W = spmatrix.ll_mat(W.shape[0], W.shape[0], W.nnz) if isinstance(W, spmatrix.LLMatType): #Warning: no check for symmetric matrix #if self.undirected: # raise ValueError("Weight matrix of undirected graph must be symmetric") items = W.items() for inds, val in items: self.W[inds[0], inds[1]] = val else: if self.undirected and (W - W.transpose()).nonzero()[0].shape[0]: raise ValueError("Weight matrix of undirected graph must be symmetric") rowInds, colInds = W.nonzero() for i in range(rowInds.shape[0]): self.W[int(rowInds[i]), int(colInds[i])] = W[int(rowInds[i]), int(colInds[i])]
class PySparseGraph(AbstractMatrixGraph): ''' Represents a graph, which can be directed or undirected, and has weights on the edges. Memory usage is efficient for sparse graphs. The list of vertices is immutable (see VertexList), however edges can be added or removed. Only non-zero edges can be added. Uses Pysparse as the underlying matrix representation. ''' def __init__(self, vertices, undirected=True, W=None, sizeHint=1000): """ Create a PySparseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a sparse matrix W which is used as the weight matrix of the graph. Different kinds of sparse matrix can impact the speed of various operations. The currently supported sparse matrix types are: ll_mat. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a square sparse matrix of the same size as the number of vertices, or None to create the default one. :param sizeHint: the expected number of edges in the graph for efficient memory usage. :type sizeHint: :class:`int` """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, spmatrix.LLMatType) and W.shape == (len(self.vList), len(self.vList))): raise ValueError( "Input argument W must be None or spmatrix.ll_mat of size " + str(len(self.vList))) self.undirected = undirected if W == None: #Should use ll_mat_sym for undirected graphs but it has several unimplemented methods self.W = spmatrix.ll_mat(len(self.vList), len(self.vList), sizeHint) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W) def neighbours(self, vertexIndex): """ Return an array of the indices of neighbours. In the case of a directed graph it is an array of those vertices connected by an edge from the current one. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` :returns: An array of the indices of all neigbours of the input vertex. """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) neighbours = PySparseUtils.nonzero(self.W[int(vertexIndex), :])[1] return numpy.array(neighbours) def neighbourOf(self, vertexIndex): """ Return an array of the indices of vertices than have an edge going to the input vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` :returns: An array of the indices of all vertices with an edge towards the input vertex. """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) neighbours = PySparseUtils.nonzero(self.W[:, vertexIndex])[0] return numpy.array(neighbours) def addEdge(self, vertexIndex1, vertexIndex2, edge=1): """ Add a non-zero edge between two vertices. :param vertexIndex1: The index of the first vertex. :type vertexIndex1: :class:`int` :param vertexIndex2: The index of the second vertex. :type vertexIndex2: :class:`int` :param edge: The value of the edge. :type edge: :class:`float` """ Parameter.checkIndex(vertexIndex1, 0, self.vList.getNumVertices()) Parameter.checkIndex(vertexIndex2, 0, self.vList.getNumVertices()) vertexIndex1 = int(vertexIndex1) vertexIndex2 = int(vertexIndex2) if edge == 0 or edge == float('inf'): raise ValueError("Cannot add a zero or infinite edge") if self.undirected: self.W[vertexIndex1, vertexIndex2] = edge self.W[vertexIndex2, vertexIndex1] = edge else: self.W[vertexIndex1, vertexIndex2] = edge def getNumEdges(self): """ Returns the total number of edges in this graph. """ if self.getNumVertices() == 0: return 0 if self.undirected: inds = numpy.arange(self.getNumVertices()) #d = spmatrix.ll_mat(self.getNumVertices(), 1) d = numpy.zeros(self.getNumVertices()) self.W.take(d, inds, inds) return (self.W.nnz + numpy.sum(d != 0)) / 2 else: return self.W.nnz def getNumDirEdges(self): """ Returns the number of edges, taking this graph as a directed graph. """ return self.W.nnz def outDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ A = self.nativeAdjacencyMatrix() j = spmatrix.ll_mat(self.vList.getNumVertices(), 1) j[:, 0] = 1 degrees = spmatrix.matrixmultiply(A, j) degrees = PysparseMatrix(matrix=degrees) degrees = numpy.array(degrees.getNumpyArray().ravel(), numpy.int) return degrees def inDegreeSequence(self): """ Return a vector of the (in)degree sequence for each vertex. """ A = self.nativeAdjacencyMatrix() j = spmatrix.ll_mat(self.vList.getNumVertices(), 1) j[:, 0] = 1 degrees = spmatrix.dot(A, j) degrees = PysparseMatrix(matrix=degrees) degrees = numpy.array(degrees.getNumpyArray().ravel(), numpy.int) return degrees def nativeAdjacencyMatrix(self): """ Return the adjacency matrix in sparse format. """ A = spmatrix.ll_mat(self.vList.getNumVertices(), self.vList.getNumVertices()) nonzeros = PySparseUtils.nonzero(self.W) A.put(1, nonzeros[0], nonzeros[1]) return A def subgraph(self, vertexIndices): """ Pass in a list or set of vertexIndices and returns the subgraph containing those vertices only, and edges between them. :param vertexIndices: the indices of the subgraph vertices. :type vertexIndices: :class:`list` :returns: A new PySparseGraph containing only vertices and edges from vertexIndices """ Parameter.checkList(vertexIndices, Parameter.checkIndex, (0, self.getNumVertices())) vertexIndices = numpy.unique(numpy.array(vertexIndices)) vList = self.vList.subList(vertexIndices.tolist()) subGraph = PySparseGraph(vList, self.undirected) if len(vertexIndices) != 0: subGraph.W = self.W[vertexIndices, vertexIndices] return subGraph def getWeightMatrix(self): """ Return the weight matrix in dense format. Warning: should not be used unless sufficient memory is available to store the dense matrix. :returns: A numpy.ndarray weight matrix. """ W = PysparseMatrix(matrix=self.W) return W.getNumpyArray() def getAllDirEdges(self): """ Returns the set of directed edges of the current graph as a matrix in which each row corresponds to an edge. For an undirected graph, there is an edge from v1 to v2 and from v2 to v1 if v2!=v1. :returns: A matrix with 2 columns, and each row corresponding to an edge. """ (rows, cols) = PySparseUtils.nonzero(self.W) edges = numpy.c_[rows, cols] return edges def add(self, graph): """ Add the edge weights of the input graph to the current one. Results in a union of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.PySparseGraph` :returns: A new graph with same vertex list and addition of edge weights """ Parameter.checkClass(graph, PySparseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError( "Both graphs must be either undirected or directed") newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W = self.W.copy() newGraph.W.shift(1, graph.W) return newGraph def copy(self): """ Returns a copy of this object, which also has a copy of the VertexList. """ newGraph = PySparseGraph(self.vList.copy(), self.undirected) newGraph.W = self.W.copy() return newGraph def removeAllEdges(self): """ Removes all edges from this graph. """ #Not sure why this doesn't work #self.W.scale(0) self.W = spmatrix.ll_mat(self.getNumVertices(), self.getNumVertices()) def setWeightMatrix(self, W): """ Set the weight matrix of this graph. Requires as input an ndarray with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The name of the file to load. :type W: :class:`ndarray` """ #Parameter.checkClass(W, numpy.ndarray) if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) if self.undirected and type(W) == numpy.ndarray and (W != W.T).any(): raise ValueError( "Weight matrix of undirected graph must be symmetric") self.W = spmatrix.ll_mat(self.getNumVertices(), self.getNumVertices()) if type(W) == numpy.ndarray: rowInds, colInds = numpy.nonzero(W) self.W.put(W[(rowInds, colInds)], rowInds, colInds) elif isinstance(W, spmatrix.LLMatType): self.setWeightMatrixSparse(W) else: raise ValueError("Invalid matrix type: " + str(type(W))) def weightMatrixType(self): """ Returns the type of the sparse matrix used to store edge weights. """ return type(self.W) def complement(self): """ Returns a graph with identical vertices (same reference) to the current one, but with the complement of the set of edges. Edges that do not exist have weight 1. This makes a sparse graph dense. :returns: A new graph with edges complmenting the current one. """ newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W[:, :] = 1 A = self.nativeAdjacencyMatrix() newGraph.W.shift(-1, A) return newGraph def multiply(self, graph): """ Multiply the edge weights of the input graph to the current one. Results in an intersection of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.PySparseGraph` :returns: A new graph with edge weights which are multiples of the current and graph """ Parameter.checkClass(graph, PySparseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError( "Both graphs must be either undirected or directed") if self.W.nnz < graph.W.nnz: (rows, cols) = PySparseUtils.nonzero(self.W) else: (rows, cols) = PySparseUtils.nonzero(graph.W) arr1 = numpy.zeros(len(rows)) arr2 = numpy.zeros(len(rows)) self.W.take(arr1, rows, cols) graph.W.take(arr2, rows, cols) arr1 = arr1 * arr2 newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W.put(arr1, rows, cols) return newGraph def setDiff(self, graph): """ Find the edges in the current graph which are not present in the input graph. :param graph: the input graph. :type graph: :class:`apgl.graph.PySparseGraph` :returns: A new graph with edges from the current graph and not in the input graph. """ Parameter.checkClass(graph, PySparseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError( "Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError( "Both graphs must be either undirected or directed") A1 = self.nativeAdjacencyMatrix() A2 = graph.nativeAdjacencyMatrix() (rows, cols) = PySparseUtils.nonzero(A1) arr1 = numpy.zeros(len(rows)) arr2 = numpy.zeros(len(rows)) A1.take(arr1, rows, cols) A2.take(arr2, rows, cols) arr1 = arr1 - arr2 A1.put(arr1, rows, cols) newGraph = PySparseGraph(self.vList, self.undirected) newGraph.W = A1 return newGraph def getEdge(self, vertexIndex1, vertexIndex2): """ Get the value of an edge, or None if no edge exists. :param vertexIndex1: The index of the first vertex. :type vertexIndex1: :class:`int` :param vertexIndex2: The index of the second vertex. :type vertexIndex2: :class:`int` :returns: The value of the edge between the given vertex indices. """ Parameter.checkIndex(vertexIndex1, 0, self.vList.getNumVertices()) Parameter.checkIndex(vertexIndex2, 0, self.vList.getNumVertices()) vertexIndex1 = int(vertexIndex1) vertexIndex2 = int(vertexIndex2) if self.W[vertexIndex1, vertexIndex2] == 0: return None else: return self.W[vertexIndex1, vertexIndex2] @staticmethod def loadMatrix(filename): return spmatrix.ll_mat_from_mtx(filename) def saveMatrix(self, W, filename): W.export_mtx(filename) def setWeightMatrixSparse(self, W): """ Set the weight matrix of this graph. Requires as input a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The weight matrix to use. """ if not isinstance(W, spmatrix.LLMatType) and not sparse.issparse(W): raise ValueError("Input must be a sparse matrix, not " + str(type(W))) if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) try: self.W = spmatrix.ll_mat(W.shape[0], W.shape[0], W.getnnz()) except AttributeError: self.W = spmatrix.ll_mat(W.shape[0], W.shape[0], W.nnz) if isinstance(W, spmatrix.LLMatType): #Warning: no check for symmetric matrix #if self.undirected: # raise ValueError("Weight matrix of undirected graph must be symmetric") items = W.items() for inds, val in items: self.W[inds[0], inds[1]] = val else: if self.undirected and (W - W.transpose()).nonzero()[0].shape[0]: raise ValueError( "Weight matrix of undirected graph must be symmetric") rowInds, colInds = W.nonzero() for i in range(rowInds.shape[0]): self.W[int(rowInds[i]), int(colInds[i])] = W[int(rowInds[i]), int(colInds[i])]
class DenseGraph(AbstractMatrixGraph): def __init__(self, vertices, undirected=True, W=None, dtype=numpy.float): """ Create a DenseGraph with a given AbstractVertexList or number of vertices, and specify whether it is directed. One can optionally pass in a numpy array W which is used as the weight matrix of the graph. :param vertices: the initial set of vertices as a AbstractVertexList object, or an int to specify the number of vertices in which case vertices are stored in a GeneralVertexList. :param undirected: a boolean variable to indicate if the graph is undirected. :type undirected: :class:`boolean` :param W: a numpy array of the same size as vertices, or None to create the default one. :param dtype: the data type of the weight matrix if W is not specified e.g numpy.int8. """ Parameter.checkBoolean(undirected) if isinstance(vertices, AbstractVertexList): self.vList = vertices elif isinstance(vertices, int): self.vList = GeneralVertexList(vertices) else: raise ValueError("Invalid vList parameter: " + str(vertices)) if W != None and not (isinstance(W, numpy.ndarray) and W.shape == (len(self.vList), len(self.vList))): raise ValueError("Input argument W must be None or numpy array of size " + str(len(self.vList))) self.undirected = undirected if W == None: self.W = numpy.zeros((len(self.vList), len(self.vList)), dtype=dtype) else: self.W = W #The next line is for error checking mainly self.setWeightMatrix(W) def getNumEdges(self): """ Returns the total number of edges in this graph. """ if self.undirected: return (numpy.flatnonzero(self.W).shape[0] + numpy.flatnonzero(numpy.diag(self.W)).shape[0])/2 else: return numpy.flatnonzero(self.W).shape[0] def getNumDirEdges(self): """ Returns the number of edges, taking this graph as a directed graph. """ return numpy.flatnonzero(self.W).shape[0] def getWeightMatrix(self): """ Return the weight matrix as a numpy array. """ return self.W def neighbours(self, vertexIndex): """ Return an array of the indices of the neighbours of the given vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) nonZeroIndices = numpy.nonzero(self.W[vertexIndex, :]) neighbourIndices = nonZeroIndices[0] return neighbourIndices def neighbourOf(self, vertexIndex): """ Return an array of the indices of vertices than have an edge going to the input vertex. :param vertexIndex: the index of a vertex. :type vertexIndex: :class:`int` """ Parameter.checkIndex(vertexIndex, 0, self.vList.getNumVertices()) nonZeroIndices = numpy.nonzero(self.W[:, vertexIndex]) neighbourIndices = nonZeroIndices[0] return neighbourIndices def complement(self): """ Returns a graph with identical vertices (same reference) to the current one, but with the complement of the set of edges. Edges that do not exist have weight 1. """ newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = (self.W == 0).astype(self.W.dtype) return newGraph def outDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = numpy.sum(self.W[i, :] != 0) return degrees def inDegreeSequence(self): """ Return a vector of the (out)degree for each vertex. """ degrees = numpy.zeros(self.W.shape[0], dtype=numpy.int32) for i in range(0, self.W.shape[0]): degrees[i] = numpy.sum(self.W[:, i] != 0) return degrees def subgraph(self, vertexIndices): """ Pass in a list or set of vertexIndices and returns the subgraph containing those vertices only, and edges between them. :param vertexIndices: the indices of the subgraph vertices. :type vertexIndices: :class:`list` """ Parameter.checkList(vertexIndices, Parameter.checkIndex, (0, self.getNumVertices())) vertexIndices = numpy.unique(numpy.array(vertexIndices)).tolist() vList = self.vList.subList(vertexIndices) subGraph = DenseGraph(vList, self.undirected, self.W.dtype) subGraph.W = self.W[vertexIndices, :][:, vertexIndices] return subGraph def add(self, graph): """ Add the edge weights of the input graph to the current one. Results in a union of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with same vertex list and addition of edge weights """ Parameter.checkClass(graph, DenseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = self.W + graph.W return newGraph def copy(self): """ Returns a copy of this object, which also has a copy of the VertexList. """ graph = DenseGraph(self.vList.copy(), self.undirected, self.W.dtype) graph.W = self.W.copy() return graph def multiply(self, graph): """ Multiply the edge weights of the input graph to the current one. Results in an intersection of the edges. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with edge weights which are multiples of the current and graph """ Parameter.checkClass(graph, DenseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = self.W * graph.W return newGraph def intersect(self, graph): """ Take the intersection of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with the intersection of edges of the current plus graph """ newGraph = self.multiply(graph) newGraph.W = (newGraph.W != 0).astype(newGraph.W.dtype) return newGraph def union(self, graph): """ Take the union of the edges of this graph and the input graph. Resulting edge weights are ignored and only adjacencies are stored. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: A new graph with the union of edges of the current one. """ newGraph = self.add(graph) newGraph.W = (newGraph.W != 0).astype(newGraph.W.dtype) return newGraph def weightMatrixDType(self): """ :returns: the dtype of the matrix used to store edge weights. """ return self.W.dtype def setDiff(self, graph): """ Find the edges in the current graph which are not present in the input graph. Replaces the edges in the current graph with adjacencies. :param graph: the input graph. :type graph: :class:`apgl.graph.DenseGraph` :returns: The graph which is the set difference of the edges of this graph and graph. """ Parameter.checkClass(graph, DenseGraph) if graph.getNumVertices() != self.getNumVertices(): raise ValueError("Can only add edges from graph with same number of vertices") if self.undirected != graph.undirected: raise ValueError("Both graphs must be either undirected or directed") A1 = self.adjacencyMatrix() A2 = graph.adjacencyMatrix() A1 = A1 - A2 A1 = (A1 + numpy.abs(A1**2))/2 newGraph = DenseGraph(self.vList, self.undirected) newGraph.W = A1 return newGraph def getAllDirEdges(self): """ Returns the set of directed edges of the current graph as a matrix in which each row corresponds to an edge. For an undirected graph, there is an edge from v1 to v2 and from v2 to v1 if v2!=v1. :returns: A matrix with 2 columns, and each row corresponding to an edge. """ (rows, cols) = numpy.nonzero(self.W) edges = numpy.c_[rows, cols] return edges @staticmethod def loadMatrix(filename): M = scipy.io.mmread(filename) if scipy.sparse.issparse(M): M = M.todense() return M def saveMatrix(self, W, filename): scipy.io.mmwrite(filename, W) def setWeightMatrix(self, W): """ Set the weight matrix of this graph. Requires as input an ndarray or a scipy sparse matrix with the same dimensions as the current weight matrix. Edges are represented by non-zero edges. :param W: The weight matrix to use. :type W: :class:`ndarray` or :class:`scipy.sparse` matrix """ if W.shape != (self.vList.getNumVertices(), self.vList.getNumVertices()): raise ValueError("Weight matrix has wrong shape : " + str(W.shape)) if self.undirected and type(W) == numpy.ndarray and (W != W.T).any(): raise ValueError("Weight matrix of undirected graph must be symmetric") if self.undirected and scipy.sparse.issparse(W) and not SparseUtils.equals(W, W.T): raise ValueError("Weight matrix of undirected graph must be symmetric") if scipy.sparse.issparse(W): W = W.todense() self.W = numpy.array(W) undirected = None vList = None W = None
r2 = max((k - r), 0) sigmaSqSum = (sigma[0:r2]**2).sum() bound = gammaSqSum + lmbdaSqSum - 2 * sigmaSqSum print("r=" + str(r)) print("gammaSqSum=" + str(gammaSqSum)) print("lmbdaSqSum=" + str(lmbdaSqSum)) print("sigmaSqSum=" + str(sigmaSqSum)) return bound #Change to work with real Laplancian numRows = 100 graph = SparseGraph(GeneralVertexList(numRows)) p = 0.1 generator = ErdosRenyiGenerator(p) graph = generator.generate(graph) print(graph) AA = graph.normalisedLaplacianSym() p = 0.001 generator.setP(p) graph = generator.generate(graph, requireEmpty=False) AA2 = graph.normalisedLaplacianSym()