def maxcardsearch(A, ve=None): """ Maximum cardinality search ordering of a sparse chordal matrix. Returns the maximum cardinality search ordering of a symmetric chordal matrix :math:`A`. Only the lower triangular part of :math:`A` is accessed. The maximum cardinality search ordering is a perfect elimination ordering in the factorization :math:`PAP^T = LL^T`. The optional argument `ve` is the index of the last vertex to be eliminated (the default value is n-1). :param A: :py:class:`spmatrix` :param ve: integer between 0 and `A.size[0]`-1 (optional) """ n = A.size[0] assert A.size[1] == n, "A must be a square matrix" assert type(A) is spmatrix, "A must be a sparse matrix" if ve is None: ve = n - 1 else: assert type(ve) is int and 0<=ve<n,\ "ve must be an integer between 0 and A.size[0]-1" As = symmetrize(A) cp, ri, _ = As.CCS # permutation vector p = matrix(0, (n, 1)) # weight array w = matrix(0, (n, 1)) max_w = 0 S = [list(range(ve)) + list(range(ve + 1, n)) + [ve] ] + [[] for i in range(n - 1)] for i in range(n - 1, -1, -1): while True: if len(S[max_w]) > 0: v = S[max_w].pop() if w[v] >= 0: break else: max_w -= 1 p[i] = v w[v] = -1 # set w[v] = -1 to mark that node v has been numbered # increase weights for all unnumbered neighbors for r in ri[cp[v]:cp[v + 1]]: if w[r] >= 0: w[r] += 1 S[w[r]].append(r) # bump r up to S[w[r]] max_w = max(max_w, w[r]) return p
def maxcardsearch(A, ve = None): """ Maximum cardinality search ordering of a sparse chordal matrix. Returns the maximum cardinality search ordering of a symmetric chordal matrix :math:`A`. Only the lower triangular part of :math:`A` is accessed. The maximum cardinality search ordering is a perfect elimination ordering in the factorization :math:`PAP^T = LL^T`. The optional argument `ve` is the index of the last vertex to be eliminated (the default value is n-1). :param A: :py:class:`spmatrix` :param ve: integer between 0 and `A.size[0]`-1 (optional) """ n = A.size[0] assert A.size[1] == n, "A must be a square matrix" assert type(A) is spmatrix, "A must be a sparse matrix" if ve is None: ve = n-1 else: assert type(ve) is int and 0<=ve<n,\ "ve must be an integer between 0 and A.size[0]-1" As = symmetrize(A) cp,ri,_ = As.CCS # permutation vector p = matrix(0,(n,1)) # weight array w = matrix(0,(n,1)) max_w = 0 S = [list(range(ve))+list(range(ve+1,n))+[ve]] + [[] for i in range(n-1)] for i in range(n-1,-1,-1): while True: if len(S[max_w]) > 0: v = S[max_w].pop() if w[v] >= 0: break else: max_w -= 1 p[i] = v w[v] = -1 # set w[v] = -1 to mark that node v has been numbered # increase weights for all unnumbered neighbors for r in ri[cp[v]:cp[v+1]]: if w[r] >= 0: w[r] += 1 S[w[r]].append(r) # bump r up to S[w[r]] max_w = max(max_w,w[r]) return p
def peo(A, p): """ Checks whether an ordering is a perfect elmimination order. Returns `True` if the permutation :math:`p` is a perfect elimination order for a Cholesky factorization :math:`PAP^T = LL^T`. Only the lower triangular part of :math:`A` is accessed. :param A: :py:class:`spmatrix` :param p: :py:class:`matrix` or :class:`list` of length `A.size[0]` """ n = A.size[0] assert type(A) == spmatrix, "A must be a sparse matrix" assert A.size[1] == n, "A must be a square matrix" assert len(p) == n, "length of p must be equal to the order of A" if isinstance(p, list): p = matrix(p) As = symmetrize(A) cp,ri,_ = As.CCS # compute inverse permutation array ip = matrix(0,(n,1)) ip[p] = matrix(range(n),(n,1)) # test set inclusion for k in range(n): v = p[k] # next vertex to be eliminated # indices of neighbors that correspond to strictly lower triangular elements in reordered pattern r = set([rj for rj in ri[cp[v]:cp[v+1]] if ip[rj] > k]) for rj in r: if not r.issubset(set(ri[cp[rj]:cp[rj+1]])): return False return True
def _iadd_spmatrix(self, X, alpha = 1.0): """ Add a sparse matrix :math:`X` to :py:class:`cspmatrix`. """ assert self.is_factor is False, "cannot add spmatrix to a cspmatrix factor" n = self.symb.n snptr = self.symb.snptr snode = self.symb.snode relptr = self.symb.relptr snrowidx = self.symb.snrowidx sncolptr = self.symb.sncolptr blkptr = self.symb.blkptr blkval = self.blkval if self.symb.p is not None: Xp = tril(perm(symmetrize(X),self.symb.p)) else: Xp = tril(X) cp, ri, val = Xp.CCS # for each block ... for k in range(self.symb.Nsn): nn = snptr[k+1]-snptr[k] na = relptr[k+1]-relptr[k] nj = nn + na r = list(snrowidx[sncolptr[k]:sncolptr[k+1]]) # copy cols from A to block for i in range(nn): j = snode[snptr[k]+i] offset = blkptr[k] + nj*i # extract correct indices and add values I = [offset + r.index(idx) for idx in ri[cp[j]:cp[j+1]]] blkval[I] += alpha*val[cp[j]:cp[j+1]] return
def peo(A, p): """ Checks whether an ordering is a perfect elmimination order. Returns `True` if the permutation :math:`p` is a perfect elimination order for a Cholesky factorization :math:`PAP^T = LL^T`. Only the lower triangular part of :math:`A` is accessed. :param A: :py:class:`spmatrix` :param p: :py:class:`matrix` or :class:`list` of length `A.size[0]` """ n = A.size[0] assert type(A) == spmatrix, "A must be a sparse matrix" assert A.size[1] == n, "A must be a square matrix" assert len(p) == n, "length of p must be equal to the order of A" if isinstance(p, list): p = matrix(p) As = symmetrize(A) cp, ri, _ = As.CCS # compute inverse permutation array ip = matrix(0, (n, 1)) ip[p] = matrix(range(n), (n, 1)) # test set inclusion for k in range(n): v = p[k] # next vertex to be eliminated # indices of neighbors that correspond to strictly lower triangular elements in reordered pattern r = set([rj for rj in ri[cp[v]:cp[v + 1]] if ip[rj] > k]) for rj in r: if not r.issubset(set(ri[cp[rj]:cp[rj + 1]])): return False return True
def _iadd_spmatrix(self, X, alpha=1.0): """ Add a sparse matrix :math:`X` to :py:class:`cspmatrix`. """ assert self.is_factor is False, "cannot add spmatrix to a cspmatrix factor" n = self.symb.n snptr = self.symb.snptr snode = self.symb.snode relptr = self.symb.relptr snrowidx = self.symb.snrowidx sncolptr = self.symb.sncolptr blkptr = self.symb.blkptr blkval = self.blkval if self.symb.p is not None: Xp = tril(perm(symmetrize(X), self.symb.p)) else: Xp = tril(X) cp, ri, val = Xp.CCS # for each block ... for k in range(self.symb.Nsn): nn = snptr[k + 1] - snptr[k] na = relptr[k + 1] - relptr[k] nj = nn + na r = list(snrowidx[sncolptr[k]:sncolptr[k + 1]]) # copy cols from A to block for i in range(nn): j = snode[snptr[k] + i] offset = blkptr[k] + nj * i # extract correct indices and add values I = [offset + r.index(idx) for idx in ri[cp[j]:cp[j + 1]]] blkval[I] += alpha * val[cp[j]:cp[j + 1]] return
def convert_block(G, h, dim, **kwargs): r""" Applies the clique conversion method to a single positive semidefinite block of a cone linear program .. math:: \begin{array}{ll} \mbox{maximize} & -h^T z \\ \mbox{subject to} & G^T z + c = 0 \\ & \mathbf{smat}(z)\ \ \text{psd completable} \end{array} After conversion, the above problem is converted to a block-diagonal one .. math:: \begin{array}{ll} \mbox{maximize} & -h_b^T z_b \\ \mbox{subject to} & G_b^T z_b + c = 0 \\ & G_c^T z_b = 0 \\ & \mathbf{smat}(z_b)\ \ \text{psd block-diagonal} \end{array} where :math:`z_b` is a vector representation of a block-diagonal matrix. The constraint :math:`G_b^T z_b + c = 0` corresponds to the original constraint :math:`G'z + c = 0`, and the constraint :math:`G_c^T z_b = 0` is a coupling constraint. :param G: :py:class:`spmatrix` :param h: :py:class:`matrix` :param dim: integer :param merge_function: routine that implements a merge heuristic (optional) :param coupling: mode of conversion (optional) :param max_density: float (default: 0.4) The following example illustrates how to apply the conversion method to a one-block SDP: .. code-block:: python block = (G, h, dim) blockc, blk2sparse, symb = convert_block(*block) The return value `blk2sparse` is a 4-tuple (`blki,I,J,n`) that defines a mapping between the sparse matrix representation and the converted block-diagonal representation. If `blkvec` represents a block-diagonal matrix, then .. code-block:: python S = spmatrix(blkvec[blki], I, J) maps `blkvec` into is a sparse matrix representation of the matrix. Similarly, a sparse matrix `S` can be converted to the block-diagonal matrix representation using the code .. code-block:: python blkvec = matrix(0.0, (len(S),1), tc=S.typecode) blkvec[blki] = S.V The optional argument `max_density` controls whether or not to perform conversion based on the aggregate sparsity of the block. Specifically, conversion is performed whenever the number of lower triangular nonzeros in the aggregate sparsity pattern is less than or equal to `max_density*dim`. The optional argument `coupling` controls the introduction of equality constraints in the conversion. Possible values are *full* (default), *sparse*, *sparse+tri*, and any nonnegative integer. Full coupling results in a conversion in which all coupling constraints are kept, and hence the converted problem is equivalent to the original problem. Sparse coupling yeilds a conversion in which only the coupling constraints corresponding to nonzero entries in the aggregate sparsity pattern are kept, and sparse-plus-tridiagonal (*sparse+tri*) yeilds a conversion with tridiagonal coupling in addition to coupling constraints corresponding to nonzero entries in the aggregate sparsity pattern. Setting `coupling` to a nonnegative integer *k* yields a conversion with coupling constraints corresponding to entries in a band with half-bandwidth *k*. .. seealso:: M. S. Andersen, A. Hansson, and L. Vandenberghe, `Reduced-Complexity Semidefinite Relaxations of Optimal Power Flow Problems <http://dx.doi.org/10.1109/TPWRS.2013.2294479>`_, IEEE Transactions on Power Systems, 2014. """ merge_function = kwargs.get('merge_function', None) coupling = kwargs.get('coupling', 'full') tskip = kwargs.get('max_density', 0.4) tc = G.typecode ### ### Find filled pattern, compute symbolic factorization using AMD ### ordering, and do "symbolic conversion" ### # find aggregate sparsity pattern h = sparse(h) LIa = matrix(list(set(G.I).union(set(h.I)))) Ia = [i % dim for i in LIa] Ja = [j // dim for j in LIa] Va = spmatrix(1., Ia, Ja, (dim, dim)) # find permutation, symmetrize, and permute Va = symmetrize(tril(Va)) # if not very sparse, skip decomposition if float(len(Va)) / Va.size[0]**2 > tskip: return (G, h, None, [dim]), None, None # compute symbolic factorization F = symbolic(Va, merge_function=merge_function, p=amd.order) p = F.p ip = F.ip Va = F.sparsity_pattern(reordered=True, symmetric=True) # symbolic conversion if coupling == 'sparse': coupling = tril(Va) elif coupling == 'sparse+tri': coupling = tril(Va) coupling += spmatrix(1.0,[i for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+2))],\ [j for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+2))],Va.size) elif type(coupling) is int: assert coupling >= 0 bw = +coupling coupling = spmatrix(1.0,[i for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+bw+1))],\ [j for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+bw+1))],Va.size) dims, sparse_to_block, constraints = symb_to_block(F, coupling=coupling) # dimension of block-diagonal representation N = sum([d**2 for d in dims]) ### ### Convert problem data ### m = G.size[1] # cols in G cp, ri, val = G.CCS IV = [] # list of m (row, value) tuples J = [] for j in range(m): iv = [] for i in range(cp[j + 1] - cp[j]): row = ri[cp[j] + i] % dim col = ri[cp[j] + i] // dim if row < col: continue # ignore upper triangular entries k1 = ip[row] k2 = ip[col] blk_idx = sparse_to_block[min(k1, k2) * dim + max(k1, k2)] if k1 == k2: iv.append((blk_idx[0], val[cp[j] + i])) elif k1 > k2: iv.append((blk_idx[0], val[cp[j] + i])) iv.append((blk_idx[1], val[cp[j] + i].conjugate())) else: iv.append((blk_idx[0], val[cp[j] + i].conjugate())) iv.append((blk_idx[1], val[cp[j] + i])) iv.sort(key=lambda x: x[0]) IV.extend(iv) J.extend(len(iv) * [j]) # build G_converted I, V = zip(*IV) G_converted = spmatrix(V, I, J, (N, m), tc=tc) # convert and build new h _, ri, val = h.CCS iv = [] for i in range(len(ri)): row = ri[i] % dim col = ri[i] // dim if row < col: continue # ignore upper triangular entries k1 = ip[row] k2 = ip[col] blk_idx = sparse_to_block[min(k1, k2) * dim + max(k1, k2)] if k1 == k2: iv.append((blk_idx[0], val[i])) elif k1 > k2: iv.append((blk_idx[0], val[i])) iv.append((blk_idx[1], val[i].conjugate())) else: iv.append((blk_idx[0], val[i].conjugate())) iv.append((blk_idx[1], val[i])) iv.sort(key=lambda x: x[0]) if iv: I, V = zip(*iv) else: I, V = [], [] h_converted = spmatrix(V, I, len(I) * [0], (N, 1), tc=tc) ### ### Build matrix representation of coupling constraints ### IV = [] # list of (row, value) tuples J = [] ncon = 0 for j in range(len(constraints)): iv = [] if len(constraints[j]) == 2: ii, jj = constraints[j] iv = sorted([(ii, 1.0), (jj, -1.0)], key=lambda x: x[0]) jl = 2 * [ncon] ncon += 1 elif len(constraints[j]) == 4: i1, j1, i2, j2 = constraints[j] iv = sorted([(i1, 1.0), (i2, 1.0), (j1, -1.0), (j2, -1.0)], key=lambda x: x[0]) jl = 4 * [ncon] ncon += 1 if tc == 'z': iv.extend( sorted([(i1, complex(0.0, 1.0)), (i2, complex(0.0, -1.0)), (j1, complex(0.0, -1.0)), (j2, complex(0.0, 1.0))], key=lambda x: x[0])) jl.extend(4 * [ncon]) ncon += 1 IV.extend(iv) J.extend(jl) # build G_converted if IV: I, V = zip(*IV) else: I, V = [], [] G_coupling = spmatrix(V, I, J, (N, ncon), tc=tc) # generate indices for reverse mapping (block_to_sparse) idx = [] for k in sparse_to_block.keys(): k1 = p[k % dim] k2 = p[k // dim] idx.append((min(k1, k2) * dim + max(k1, k2), sparse_to_block[k][0])) idx.sort() idx, blki = zip(*idx) blki = matrix(blki) I = [v % dim for v in idx] J = [v // dim for v in idx] n = sum([di**2 for di in dims]) return (G_converted, h_converted, G_coupling, dims), (blki, I, J, n), F
def __init__(self, A, p = None, merge_function = None, **kwargs): assert isinstance(A,spmatrix), "A must be a sparse matrix" assert A.size[0] == A.size[1], "A must be a square matrix" supernodal = kwargs.get('supernodal',True) # Symmetrize A Ap = symmetrize(A) nnz_Ap = (len(Ap)+Ap.size[0])/2 # number of nonzeros in lower triangle assert len([i for i,j in zip(Ap.I,Ap.J) if i==j]) == A.size[0], "the sparsity pattern of A must include diagonal elements" # Permute if permutation vector p or ordering routine is specified if p is not None: if isinstance(p, BuiltinFunctionType) or isinstance(p, FunctionType): p = p(Ap) elif isinstance(p, list): p = matrix(p) assert len(p) == A.size[0], "length of permutation vector must be equal to the order of A" Ap = perm(Ap,p) # Symbolic factorization par = etree(Ap) post = post_order(par) colcount = counts(Ap, par, post) nnz_Ae = sum(colcount) if supernodal: snode, snptr, snpar = supernodes(par, post, colcount) snpost = post_order(snpar) else: snpar = par snpost = post snode = matrix(range(A.size[0])) snptr = matrix(range(A.size[0]+1)) if merge_function: colcount, snode, snptr, snpar, snpost = amalgamate(colcount, snode, snptr, snpar, snpost, merge_function) # Post order nodes such that supernodes have consecutively numbered nodes pp = matrix([snode[snptr[snpost[k]]:snptr[snpost[k]+1]] for k in range(len(snpar))]) snptr2 = matrix(0,(len(snptr),1)) for k in range(len(snpar)): snptr2[k+1] = snptr2[k] + snptr[snpost[k]+1]-snptr[snpost[k]] colcount = colcount[pp] snposti = matrix(0,(len(snpost),1)) snposti[snpost] = matrix(range(len(snpost))) snpar = matrix([snposti[snpar[snpost[k]]] for k in range(len(snpar)) ]) snode = matrix(range(len(snode))) snpost = matrix(range(len(snpost))) snptr = snptr2 # Permute A and store effective permutation and its inverse Ap = perm(Ap,pp) if p is None: self.__p = pp else: self.__p = p[pp] self.__ip = matrix(0,(len(self.__p),1)) self.__ip[self.__p] = matrix(range(len(self.__p))) # Compute embedding and relative indices sncolptr, snrowidx = embed(Ap, colcount, snode, snptr, snpar, snpost) relptr, relidx = relative_idx(sncolptr, snrowidx, snptr, snpar) # build chptr chptr = matrix(0, (len(snpar)+1,1)) for j in snpost: if snpar[j] != j: chptr[snpar[j]+1] += 1 for j in range(1,len(chptr)): chptr[j] += chptr[j-1] # build chidx tmp = +chptr chidx = matrix(0,(chptr[-1],1)) for j in snpost: if snpar[j] != j: chidx[tmp[snpar[j]]] = j tmp[snpar[j]] += 1 del tmp # compute stack size stack_size = 0 stack_mem = 0 stack_max = 0 frontal_max = 0 stack = [] for k in snpost: nn = snptr[k+1]-snptr[k] na = relptr[k+1]-relptr[k] nj = na + nn frontal_max = max(frontal_max, nj**2) for j in range(chptr[k+1]-1,chptr[k]-1,-1): v = stack.pop() stack_mem -= v**2 if (na > 0): stack.append(na) stack_mem += na**2 stack_max = max(stack_max,stack_mem) stack_size = max(stack_size,len(stack)) self.frontal_len = frontal_max self.stack_len = stack_max self.stack_size = stack_size # build blkptr blkptr = matrix(0, (len(snpar)+1,1)) for i in range(len(snpar)): blkptr[i+1] = blkptr[i] + (snptr[i+1]-snptr[i])*(sncolptr[i+1]-sncolptr[i]) # compute storage requirements stack = [] stack_depth = 0 stack_mem = 0 stack_tmp = 0 cln = 0 stack_solve = 0 stack_stmp = 0 for k in snpost: nn = snptr[k+1]-snptr[k] # |Nk| na = relptr[k+1]-relptr[k] # |Ak| nj = na + nn cln = max(cln,nj) # this is the clique number for i in range(chptr[k+1]-1,chptr[k]-1,-1): na_ch = stack.pop() stack_tmp -= na_ch**2 stack_stmp -= na_ch if na > 0: stack.append(na) stack_tmp += na**2 stack_mem = max(stack_tmp,stack_mem) stack_stmp += na stack_solve = max(stack_stmp,stack_solve) stack_depth = max(stack_depth,len(stack)) self.__clique_number = cln self.__n = len(snode) self.__Nsn = len(snpar) self.__snode = snode self.__snptr = snptr self.__chptr = chptr self.__chidx = chidx self.__snpar = snpar self.__snpost = snpost self.__relptr = relptr self.__relidx = relidx self.__sncolptr = sncolptr self.__snrowidx = snrowidx self.__blkptr = blkptr self.__fill = (nnz_Ae-nnz_Ap,self.nnz-nnz_Ae) self.__memory = {'stack_depth':stack_depth, 'stack_mem':stack_mem, 'frontal_mem':cln**2, 'stack_solve':stack_solve} return
def spmatrix(self, reordered = True, symmetric = False): """ Converts the :py:class:`cspmatrix` :math:`A` to a sparse matrix. A reordered matrix is returned if the optional argument `reordered` is `True` (default), and otherwise the inverse permutation is applied. Only the default options are allowed if the :py:class:`cspmatrix` :math:`A` represents a Cholesky factor. :param reordered: boolean (default: True) :param symmetric: boolean (default: False) """ n = self.symb.n snptr = self.symb.snptr snode = self.symb.snode relptr = self.symb.relptr snrowidx = self.symb.snrowidx sncolptr = self.symb.sncolptr blkptr = self.symb.blkptr blkval = self.blkval if self.is_factor: if symmetric: raise ValueError("'symmetric = True' not implemented for Cholesky factors") if not reordered: raise ValueError("'reordered = False' not implemented for Cholesky factors") snpost = self.symb.snpost blkval = +blkval for k in snpost: j = snode[snptr[k]] # representative vertex nn = snptr[k+1]-snptr[k] # |Nk| na = relptr[k+1]-relptr[k] # |Ak| if na == 0: continue nj = na + nn if nn == 1: blas.scal(blkval[blkptr[k]],blkval,offset = blkptr[k]+1,n=na) else: blas.trmm(blkval,blkval, transA = "N", diag = "N", side = "R",uplo = "L", \ m = na, n = nn, ldA = nj, ldB = nj, \ offsetA = blkptr[k],offsetB = blkptr[k] + nn) cc = matrix(0,(n,1)) # count number of nonzeros in each col for k in range(self.symb.Nsn): nn = snptr[k+1]-snptr[k] na = relptr[k+1]-relptr[k] nj = nn + na for i in range(nn): j = snode[snptr[k]+i] cc[j] = nj - i # build col. ptr cp = [0] for i in range(n): cp.append(cp[-1] + cc[i]) cp = matrix(cp) # copy data and row indices val = matrix(0.0, (cp[-1],1)) ri = matrix(0, (cp[-1],1)) for k in range(self.symb.Nsn): nn = snptr[k+1]-snptr[k] na = relptr[k+1]-relptr[k] nj = nn + na for i in range(nn): j = snode[snptr[k]+i] blas.copy(blkval, val, offsetx = blkptr[k]+nj*i+i, offsety = cp[j], n = nj-i) ri[cp[j]:cp[j+1]] = snrowidx[sncolptr[k]+i:sncolptr[k+1]] I = []; J = [] for i in range(n): I += list(ri[cp[i]:cp[i+1]]) J += (cp[i+1]-cp[i])*[i] tmp = spmatrix(val, I, J, (n,n)) # tmp is reordered and lower tril. if reordered or self.symb.p is None: # reordered matrix (do not apply inverse permutation) if not symmetric: return tmp else: return symmetrize(tmp) else: # apply inverse permutation tmp = perm(symmetrize(tmp), self.symb.ip) if symmetric: return tmp else: return tril(tmp)
def __init__(self, A, p=None, merge_function=None, **kwargs): assert isinstance(A, spmatrix), "A must be a sparse matrix" assert A.size[0] == A.size[1], "A must be a square matrix" supernodal = kwargs.get('supernodal', True) # Symmetrize A Ap = symmetrize(A) nnz_Ap = (len(Ap) + Ap.size[0]) / 2 # number of nonzeros in lower triangle assert len([i for i, j in zip(Ap.I, Ap.J) if i == j]) == A.size[ 0], "the sparsity pattern of A must include diagonal elements" # Permute if permutation vector p or ordering routine is specified if p is not None: if isinstance(p, BuiltinFunctionType) or isinstance( p, FunctionType): p = p(Ap) elif isinstance(p, list): p = matrix(p) assert len(p) == A.size[ 0], "length of permutation vector must be equal to the order of A" Ap = perm(Ap, p) # Symbolic factorization par = etree(Ap) post = post_order(par) colcount = counts(Ap, par, post) nnz_Ae = sum(colcount) if supernodal: snode, snptr, snpar = supernodes(par, post, colcount) snpost = post_order(snpar) else: snpar = par snpost = post snode = matrix(range(A.size[0])) snptr = matrix(range(A.size[0] + 1)) if merge_function: colcount, snode, snptr, snpar, snpost = amalgamate( colcount, snode, snptr, snpar, snpost, merge_function) # Post order nodes such that supernodes have consecutively numbered nodes pp = matrix([ snode[snptr[snpost[k]]:snptr[snpost[k] + 1]] for k in range(len(snpar)) ]) snptr2 = matrix(0, (len(snptr), 1)) for k in range(len(snpar)): snptr2[k + 1] = snptr2[k] + snptr[snpost[k] + 1] - snptr[snpost[k]] colcount = colcount[pp] snposti = matrix(0, (len(snpost), 1)) snposti[snpost] = matrix(range(len(snpost))) snpar = matrix([snposti[snpar[snpost[k]]] for k in range(len(snpar))]) snode = matrix(range(len(snode))) snpost = matrix(range(len(snpost))) snptr = snptr2 # Permute A and store effective permutation and its inverse Ap = perm(Ap, pp) if p is None: self.__p = pp else: self.__p = p[pp] self.__ip = matrix(0, (len(self.__p), 1)) self.__ip[self.__p] = matrix(range(len(self.__p))) # Compute embedding and relative indices sncolptr, snrowidx = embed(Ap, colcount, snode, snptr, snpar, snpost) relptr, relidx = relative_idx(sncolptr, snrowidx, snptr, snpar) # build chptr chptr = matrix(0, (len(snpar) + 1, 1)) for j in snpost: if snpar[j] != j: chptr[snpar[j] + 1] += 1 for j in range(1, len(chptr)): chptr[j] += chptr[j - 1] # build chidx tmp = +chptr chidx = matrix(0, (chptr[-1], 1)) for j in snpost: if snpar[j] != j: chidx[tmp[snpar[j]]] = j tmp[snpar[j]] += 1 del tmp # compute stack size stack_size = 0 stack_mem = 0 stack_max = 0 frontal_max = 0 stack = [] for k in snpost: nn = snptr[k + 1] - snptr[k] na = relptr[k + 1] - relptr[k] nj = na + nn frontal_max = max(frontal_max, nj**2) for j in range(chptr[k + 1] - 1, chptr[k] - 1, -1): v = stack.pop() stack_mem -= v**2 if (na > 0): stack.append(na) stack_mem += na**2 stack_max = max(stack_max, stack_mem) stack_size = max(stack_size, len(stack)) self.frontal_len = frontal_max self.stack_len = stack_max self.stack_size = stack_size # build blkptr blkptr = matrix(0, (len(snpar) + 1, 1)) for i in range(len(snpar)): blkptr[i + 1] = blkptr[i] + (snptr[i + 1] - snptr[i]) * ( sncolptr[i + 1] - sncolptr[i]) # compute storage requirements stack = [] stack_depth = 0 stack_mem = 0 stack_tmp = 0 cln = 0 stack_solve = 0 stack_stmp = 0 for k in snpost: nn = snptr[k + 1] - snptr[k] # |Nk| na = relptr[k + 1] - relptr[k] # |Ak| nj = na + nn cln = max(cln, nj) # this is the clique number for i in range(chptr[k + 1] - 1, chptr[k] - 1, -1): na_ch = stack.pop() stack_tmp -= na_ch**2 stack_stmp -= na_ch if na > 0: stack.append(na) stack_tmp += na**2 stack_mem = max(stack_tmp, stack_mem) stack_stmp += na stack_solve = max(stack_stmp, stack_solve) stack_depth = max(stack_depth, len(stack)) self.__clique_number = cln self.__n = len(snode) self.__Nsn = len(snpar) self.__snode = snode self.__snptr = snptr self.__chptr = chptr self.__chidx = chidx self.__snpar = snpar self.__snpost = snpost self.__relptr = relptr self.__relidx = relidx self.__sncolptr = sncolptr self.__snrowidx = snrowidx self.__blkptr = blkptr self.__fill = (nnz_Ae - nnz_Ap, self.nnz - nnz_Ae) self.__memory = { 'stack_depth': stack_depth, 'stack_mem': stack_mem, 'frontal_mem': cln**2, 'stack_solve': stack_solve } return
def spmatrix(self, reordered=True, symmetric=False): """ Converts the :py:class:`cspmatrix` :math:`A` to a sparse matrix. A reordered matrix is returned if the optional argument `reordered` is `True` (default), and otherwise the inverse permutation is applied. Only the default options are allowed if the :py:class:`cspmatrix` :math:`A` represents a Cholesky factor. :param reordered: boolean (default: True) :param symmetric: boolean (default: False) """ n = self.symb.n snptr = self.symb.snptr snode = self.symb.snode relptr = self.symb.relptr snrowidx = self.symb.snrowidx sncolptr = self.symb.sncolptr blkptr = self.symb.blkptr blkval = self.blkval if self.is_factor: if symmetric: raise ValueError( "'symmetric = True' not implemented for Cholesky factors") if not reordered: raise ValueError( "'reordered = False' not implemented for Cholesky factors") snpost = self.symb.snpost blkval = +blkval for k in snpost: j = snode[snptr[k]] # representative vertex nn = snptr[k + 1] - snptr[k] # |Nk| na = relptr[k + 1] - relptr[k] # |Ak| if na == 0: continue nj = na + nn if nn == 1: blas.scal(blkval[blkptr[k]], blkval, offset=blkptr[k] + 1, n=na) else: blas.trmm(blkval,blkval, transA = "N", diag = "N", side = "R",uplo = "L", \ m = na, n = nn, ldA = nj, ldB = nj, \ offsetA = blkptr[k],offsetB = blkptr[k] + nn) cc = matrix(0, (n, 1)) # count number of nonzeros in each col for k in range(self.symb.Nsn): nn = snptr[k + 1] - snptr[k] na = relptr[k + 1] - relptr[k] nj = nn + na for i in range(nn): j = snode[snptr[k] + i] cc[j] = nj - i # build col. ptr cp = [0] for i in range(n): cp.append(cp[-1] + cc[i]) cp = matrix(cp) # copy data and row indices val = matrix(0.0, (cp[-1], 1)) ri = matrix(0, (cp[-1], 1)) for k in range(self.symb.Nsn): nn = snptr[k + 1] - snptr[k] na = relptr[k + 1] - relptr[k] nj = nn + na for i in range(nn): j = snode[snptr[k] + i] blas.copy(blkval, val, offsetx=blkptr[k] + nj * i + i, offsety=cp[j], n=nj - i) ri[cp[j]:cp[j + 1]] = snrowidx[sncolptr[k] + i:sncolptr[k + 1]] I = [] J = [] for i in range(n): I += list(ri[cp[i]:cp[i + 1]]) J += (cp[i + 1] - cp[i]) * [i] tmp = spmatrix(val, I, J, (n, n)) # tmp is reordered and lower tril. if reordered or self.symb.p is None: # reordered matrix (do not apply inverse permutation) if not symmetric: return tmp else: return symmetrize(tmp) else: # apply inverse permutation tmp = perm(symmetrize(tmp), self.symb.ip) if symmetric: return tmp else: return tril(tmp)
def convert_block(G, h, dim, **kwargs): r""" Applies the clique conversion method to a single positive semidefinite block of a cone linear program .. math:: \begin{array}{ll} \mbox{maximize} & -h^T z \\ \mbox{subject to} & G^T z + c = 0 \\ & \mathbf{smat}(z)\ \ \text{psd completable} \end{array} After conversion, the above problem is converted to a block-diagonal one .. math:: \begin{array}{ll} \mbox{maximize} & -h_b^T z_b \\ \mbox{subject to} & G_b^T z_b + c = 0 \\ & G_c^T z_b = 0 \\ & \mathbf{smat}(z_b)\ \ \text{psd block-diagonal} \end{array} where :math:`z_b` is a vector representation of a block-diagonal matrix. The constraint :math:`G_b^T z_b + c = 0` corresponds to the original constraint :math:`G'z + c = 0`, and the constraint :math:`G_c^T z_b = 0` is a coupling constraint. :param G: :py:class:`spmatrix` :param h: :py:class:`matrix` :param dim: integer :param merge_function: routine that implements a merge heuristic (optional) :param coupling: mode of conversion (optional) :param max_density: float (default: 0.4) The following example illustrates how to apply the conversion method to a one-block SDP: .. code-block:: python block = (G, h, dim) blockc, blk2sparse, symb = convert_block(*block) The return value `blk2sparse` is a 4-tuple (`blki,I,J,n`) that defines a mapping between the sparse matrix representation and the converted block-diagonal representation. If `blkvec` represents a block-diagonal matrix, then .. code-block:: python S = spmatrix(blkvec[blki], I, J) maps `blkvec` into is a sparse matrix representation of the matrix. Similarly, a sparse matrix `S` can be converted to the block-diagonal matrix representation using the code .. code-block:: python blkvec = matrix(0.0, (len(S),1), tc=S.typecode) blkvec[blki] = S.V The optional argument `max_density` controls whether or not to perform conversion based on the aggregate sparsity of the block. Specifically, conversion is performed whenever the number of lower triangular nonzeros in the aggregate sparsity pattern is less than or equal to `max_density*dim`. The optional argument `coupling` controls the introduction of equality constraints in the conversion. Possible values are *full* (default), *sparse*, *sparse+tri*, and any nonnegative integer. Full coupling results in a conversion in which all coupling constraints are kept, and hence the converted problem is equivalent to the original problem. Sparse coupling yeilds a conversion in which only the coupling constraints corresponding to nonzero entries in the aggregate sparsity pattern are kept, and sparse-plus-tridiagonal (*sparse+tri*) yeilds a conversion with tridiagonal coupling in addition to coupling constraints corresponding to nonzero entries in the aggregate sparsity pattern. Setting `coupling` to a nonnegative integer *k* yields a conversion with coupling constraints corresponding to entries in a band with half-bandwidth *k*. .. seealso:: M. S. Andersen, A. Hansson, and L. Vandenberghe, `Reduced-Complexity Semidefinite Relaxations of Optimal Power Flow Problems <http://dx.doi.org/10.1109/TPWRS.2013.2294479>`_, IEEE Transactions on Power Systems, 2014. """ merge_function = kwargs.get('merge_function', None) coupling = kwargs.get('coupling', 'full') tskip = kwargs.get('max_density',0.4) tc = G.typecode ### ### Find filled pattern, compute symbolic factorization using AMD ### ordering, and do "symbolic conversion" ### # find aggregate sparsity pattern h = sparse(h) LIa = matrix(list(set(G.I).union(set(h.I)))) Ia = [i%dim for i in LIa] Ja = [j//dim for j in LIa] Va = spmatrix(1.,Ia,Ja,(dim,dim)) # find permutation, symmetrize, and permute Va = symmetrize(tril(Va)) # if not very sparse, skip decomposition if float(len(Va))/Va.size[0]**2 > tskip: return (G, h, None, [dim]), None, None # compute symbolic factorization F = symbolic(Va, merge_function = merge_function, p = amd.order) p = F.p ip = F.ip Va = F.sparsity_pattern(reordered = True, symmetric = True) # symbolic conversion if coupling == 'sparse': coupling = tril(Va) elif coupling == 'sparse+tri': coupling = tril(Va) coupling += spmatrix(1.0,[i for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+2))],\ [j for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+2))],Va.size) elif type(coupling) is int: assert coupling >= 0 bw = +coupling coupling = spmatrix(1.0,[i for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+bw+1))],\ [j for j in range(Va.size[0]) for i in range(j,min(Va.size[0],j+bw+1))],Va.size) dims, sparse_to_block, constraints = symb_to_block(F, coupling = coupling) # dimension of block-diagonal representation N = sum([d**2 for d in dims]) ### ### Convert problem data ### m = G.size[1] # cols in G cp, ri, val = G.CCS IV = [] # list of m (row, value) tuples J = [] for j in range(m): iv = [] for i in range(cp[j+1]-cp[j]): row = ri[cp[j]+i]%dim col = ri[cp[j]+i]//dim if row < col: continue # ignore upper triangular entries k1 = ip[row] k2 = ip[col] blk_idx = sparse_to_block[min(k1,k2)*dim + max(k1,k2)] if k1 == k2: iv.append((blk_idx[0], val[cp[j]+i])) elif k1 > k2: iv.append((blk_idx[0], val[cp[j]+i])) iv.append((blk_idx[1], val[cp[j]+i].conjugate())) else: iv.append((blk_idx[0], val[cp[j]+i].conjugate())) iv.append((blk_idx[1], val[cp[j]+i])) iv.sort(key=lambda x: x[0]) IV.extend(iv) J.extend(len(iv)*[j]) # build G_converted I, V = zip(*IV) G_converted = spmatrix(V, I, J, (N, m), tc = tc) # convert and build new h _, ri, val = h.CCS iv = [] for i in range(len(ri)): row = ri[i]%dim col = ri[i]//dim if row < col: continue # ignore upper triangular entries k1 = ip[row] k2 = ip[col] blk_idx = sparse_to_block[min(k1,k2)*dim + max(k1,k2)] if k1 == k2: iv.append((blk_idx[0], val[i])) elif k1 > k2: iv.append((blk_idx[0], val[i])) iv.append((blk_idx[1], val[i].conjugate())) else: iv.append((blk_idx[0], val[i].conjugate())) iv.append((blk_idx[1], val[i])) iv.sort(key=lambda x: x[0]) if iv: I, V = zip(*iv) else: I, V = [], [] h_converted = spmatrix(V, I, len(I)*[0], (N, 1), tc = tc) ### ### Build matrix representation of coupling constraints ### IV = [] # list of (row, value) tuples J = [] ncon = 0 for j in range(len(constraints)): iv = [] if len(constraints[j]) == 2: ii, jj = constraints[j] iv = sorted([(ii, 1.0), (jj, -1.0)],key=lambda x: x[0]) jl = 2*[ncon] ncon += 1 elif len(constraints[j]) == 4: i1,j1,i2,j2 = constraints[j] iv = sorted([(i1, 1.0), (i2, 1.0), (j1, -1.0), (j2, -1.0)],key=lambda x: x[0]) jl = 4*[ncon] ncon += 1 if tc == 'z': iv.extend(sorted([(i1, complex(0.0,1.0)), (i2, complex(0.0,-1.0)), (j1, complex(0.0,-1.0)), (j2, complex(0.0,1.0))],key=lambda x: x[0])) jl.extend(4*[ncon]) ncon += 1 IV.extend(iv) J.extend(jl) # build G_converted if IV: I, V = zip(*IV) else: I, V = [], [] G_coupling = spmatrix(V, I, J, (N, ncon), tc = tc) # generate indices for reverse mapping (block_to_sparse) idx = [] for k in sparse_to_block.keys(): k1 = p[k%dim] k2 = p[k//dim] idx.append((min(k1,k2)*dim + max(k1,k2), sparse_to_block[k][0])) idx.sort() idx, blki = zip(*idx) blki = matrix(blki) I = [v%dim for v in idx] J = [v//dim for v in idx] n = sum([di**2 for di in dims]) return (G_converted, h_converted, G_coupling, dims), (blki, I, J, n), F
def maxchord(A, ve=None): """ Maximal chordal subgraph of sparsity graph. Returns a lower triangular sparse matrix which is the projection of :math:`A` on a maximal chordal subgraph and a perfect elimination order :math:`p`. Only the lower triangular part of :math:`A` is accessed. The optional argument `ve` is the index of the last vertex to be eliminated (the default value is `n-1`). If :math:`A` is chordal, then the matrix returned is equal to :math:`A`. :param A: :py:class:`spmatrix` :param ve: integer between 0 and `A.size[0]`-1 (optional) .. seealso:: P. M. Dearing, D. R. Shier, D. D. Warner, `Maximal chordal subgraphs <http://dx.doi.org/10.1016/0166-218X(88)90075-3>`_, Discrete Applied Mathematics, 20:3, 1988, pp. 181-190. """ n = A.size[0] assert A.size[1] == n, "A must be a square matrix" assert type(A) is spmatrix, "A must be a sparse matrix" if ve is None: ve = n - 1 else: assert type(ve) is int and 0<=ve<n,\ "ve must be an integer between 0 and A.size[0]-1" As = symmetrize(A) cp, ri, val = As.CCS # permutation vector p = matrix(0, (n, 1)) # weight array w = matrix(0, (n, 1)) max_w = 0 S = [list(range(ve)) + list(range(ve + 1, n)) + [ve] ] + [[] for i in range(n - 1)] C = [set() for i in range(n)] E = [[] for i in range(n)] # edge list V = [[] for i in range(n)] # num. values for i in range(n - 1, -1, -1): # select next node to number while True: if len(S[max_w]) > 0: v = S[max_w].pop() if w[v] >= 0: break else: max_w -= 1 p[i] = v w[v] = -1 # set w[v] = -1 to mark that node v has been numbered # loop over unnumbered neighbors of node v for ii in range(cp[v], cp[v + 1]): u = ri[ii] d = val[ii] if w[u] >= 0: if C[u].issubset(C[v]): C[u].update([v]) w[u] += 1 S[w[u]].append(u) # bump up u to S[w[u]] max_w = max(max_w, w[u]) # update max deg. E[min(u, v)].append(max(u, v)) V[min(u, v)].append(d) elif u == v: E[u].append(u) V[u].append(d) # build adjacency matrix of reordered max. chordal subgraph Am = spmatrix([d for d in chain.from_iterable(V)],[i for i in chain.from_iterable(E)],\ [i for i in chain.from_iterable([len(Ej)*[j] for j,Ej in enumerate(E)])],(n,n)) return Am, p
def maxchord(A, ve = None): """ Maximal chordal subgraph of sparsity graph. Returns a lower triangular sparse matrix which is the projection of :math:`A` on a maximal chordal subgraph and a perfect elimination order :math:`p`. Only the lower triangular part of :math:`A` is accessed. The optional argument `ve` is the index of the last vertex to be eliminated (the default value is `n-1`). If :math:`A` is chordal, then the matrix returned is equal to :math:`A`. :param A: :py:class:`spmatrix` :param ve: integer between 0 and `A.size[0]`-1 (optional) .. seealso:: P. M. Dearing, D. R. Shier, D. D. Warner, `Maximal chordal subgraphs <http://dx.doi.org/10.1016/0166-218X(88)90075-3>`_, Discrete Applied Mathematics, 20:3, 1988, pp. 181-190. """ n = A.size[0] assert A.size[1] == n, "A must be a square matrix" assert type(A) is spmatrix, "A must be a sparse matrix" if ve is None: ve = n-1 else: assert type(ve) is int and 0<=ve<n,\ "ve must be an integer between 0 and A.size[0]-1" As = symmetrize(A) cp,ri,val = As.CCS # permutation vector p = matrix(0,(n,1)) # weight array w = matrix(0,(n,1)) max_w = 0 S = [list(range(ve))+list(range(ve+1,n))+[ve]] + [[] for i in range(n-1)] C = [set() for i in range(n)] E = [[] for i in range(n)] # edge list V = [[] for i in range(n)] # num. values for i in range(n-1,-1,-1): # select next node to number while True: if len(S[max_w]) > 0: v = S[max_w].pop() if w[v] >= 0: break else: max_w -= 1 p[i] = v w[v] = -1 # set w[v] = -1 to mark that node v has been numbered # loop over unnumbered neighbors of node v for ii in range(cp[v],cp[v+1]): u = ri[ii] d = val[ii] if w[u] >= 0: if C[u].issubset(C[v]): C[u].update([v]) w[u] += 1 S[w[u]].append(u) # bump up u to S[w[u]] max_w = max(max_w,w[u]) # update max deg. E[min(u,v)].append(max(u,v)) V[min(u,v)].append(d) elif u == v: E[u].append(u) V[u].append(d) # build adjacency matrix of reordered max. chordal subgraph Am = spmatrix([d for d in chain.from_iterable(V)],[i for i in chain.from_iterable(E)],\ [i for i in chain.from_iterable([len(Ej)*[j] for j,Ej in enumerate(E)])],(n,n)) return Am,p