/// <summary> /// Symbolic ordering and analysis for QR. /// </summary> /// <param name="A">Matrix to factorize.</param> /// <param name="p">Permutation.</param> protected void SymbolicAnalysis(CompressedColumnStorage <T> A, int[] p, bool natural) { int m = A.RowCount; int n = A.ColumnCount; var sym = this.S = new SymbolicFactorization(); // Fill-reducing ordering sym.q = p; var C = natural ? SymbolicColumnStorage.Create(A) : Permute(A, null, sym.q); // etree of C'*C, where C=A(:,q) sym.parent = GraphHelper.EliminationTree(m, n, C.ColumnPointers, C.RowIndices, true); int[] post = GraphHelper.TreePostorder(sym.parent, n); sym.cp = GraphHelper.ColumnCounts(C, sym.parent, post, true); // col counts chol(C'*C) bool ok = C != null && sym.parent != null && sym.cp != null && CountV(C, sym); if (ok) { sym.unz = 0; for (int k = 0; k < n; k++) { sym.unz += sym.cp[k]; } } }
/// <summary> /// Permutes a sparse matrix, C = PAQ. /// </summary> /// <param name="A">m-by-n, column-compressed matrix</param> /// <param name="pinv">a permutation vector of length m</param> /// <param name="q">a permutation vector of length n</param> /// <returns>C = PAQ, null on error</returns> private SymbolicColumnStorage Permute(CompressedColumnStorage <T> A, int[] pinv, int[] q) { int t, j, k, nz = 0; int m = A.RowCount; int n = A.ColumnCount; var ap = A.ColumnPointers; var ai = A.RowIndices; var result = SymbolicColumnStorage.Create(A); var cp = result.ColumnPointers; var ci = result.RowIndices; for (k = 0; k < n; k++) { // Column k of C is column q[k] of A cp[k] = nz; j = q != null ? (q[k]) : k; for (t = ap[j]; t < ap[j + 1]; t++) { // Row i of A is row pinv[i] of C ci[nz++] = pinv != null ? (pinv[ai[t]]) : ai[t]; } } // Finalize the last column of C cp[n] = nz; return(result); }
/// <summary> /// Symbolic ordering and analysis for QR. /// </summary> private void SymbolicAnalysis(ColumnOrdering order, CompressedColumnStorage <double> A) { int m = A.RowCount; int n = A.ColumnCount; var sym = this.symFactor = new SymbolicFactorization(); // Fill-reducing ordering sym.q = AMD.Generate(A, order); var C = order > 0 ? Permute(A, null, sym.q) : SymbolicColumnStorage.Create(A); // etree of C'*C, where C=A(:,q) sym.parent = GraphHelper.EliminationTree(m, n, C.ColumnPointers, C.RowIndices, true); int[] post = GraphHelper.TreePostorder(sym.parent, n); sym.cp = GraphHelper.ColumnCounts(C, sym.parent, post, true); // col counts chol(C'*C) bool ok = C != null && sym.parent != null && sym.cp != null && CountV(C); if (ok) { sym.unz = 0; for (int k = 0; k < n; k++) { sym.unz += sym.cp[k]; } } }
/// <summary> /// Ordering and symbolic analysis for a Cholesky factorization. /// </summary> /// <param name="A">Matrix to factorize.</param> /// <param name="p">Permutation.</param> private void SymbolicAnalysis(CompressedColumnStorage <Complex> A, int[] p) { int n = A.ColumnCount; var sym = this.S = new SymbolicFactorization(); // Find inverse permutation. sym.pinv = Permutation.Invert(p); // C = spones(triu(A(P,P))) var C = PermuteSym(A, sym.pinv, false); // Find etree of C. sym.parent = GraphHelper.EliminationTree(n, n, C.ColumnPointers, C.RowIndices, false); // Postorder the etree. var post = GraphHelper.TreePostorder(sym.parent, n); // Find column counts of chol(C) var c = GraphHelper.ColumnCounts(SymbolicColumnStorage.Create(C, false), sym.parent, post, false); sym.cp = new int[n + 1]; // Find column pointers for L sym.unz = sym.lnz = Helper.CumulativeSum(sym.cp, c, n); }
/// <summary> /// Compute the Numeric Cholesky factorization, L = chol (A, [pinv parent cp]). /// </summary> /// <returns>Numeric Cholesky factorization</returns> private void Factorize(CompressedColumnStorage <Complex> A, IProgress <double> progress) { Complex d, lki; int top, i, p, k, cci; int n = A.ColumnCount; // Allocate workspace. var c = new int[n]; var s = new int[n]; var x = this.temp; var colp = S.cp; var pinv = S.pinv; var parent = S.parent; var C = pinv != null?PermuteSym(A, pinv, true) : A; var cp = C.ColumnPointers; var ci = C.RowIndices; var cx = C.Values; this.L = CompressedColumnStorage <Complex> .Create(n, n, colp[n]); var lp = L.ColumnPointers; var li = L.RowIndices; var lx = L.Values; for (k = 0; k < n; k++) { lp[k] = c[k] = colp[k]; } double current = 0.0; double step = n / 100.0; for (k = 0; k < n; k++) // compute L(k,:) for L*L' = C { // Progress reporting. if (k >= current) { current += step; if (progress != null) { progress.Report(k / (double)n); } } // Find nonzero pattern of L(k,:) top = GraphHelper.EtreeReach(SymbolicColumnStorage.Create(C, false), k, parent, s, c); x[k] = 0; // x (0:k) is now zero for (p = cp[k]; p < cp[k + 1]; p++) // x = full(triu(C(:,k))) { if (ci[p] <= k) { x[ci[p]] = cx[p]; } } d = x[k]; // d = C(k,k) x[k] = 0; // clear x for k+1st iteration // Triangular solve for (; top < n; top++) // solve L(0:k-1,0:k-1) * x = C(:,k) { i = s[top]; // s [top..n-1] is pattern of L(k,:) lki = x[i] / lx[lp[i]]; // L(k,i) = x (i) / L(i,i) x[i] = 0; // clear x for k+1st iteration cci = c[i]; for (p = lp[i] + 1; p < cci; p++) { x[li[p]] -= lx[p] * lki; } d -= lki * Complex.Conjugate(lki); // d = d - L(k,i)*L(k,i) p = c[i]++; li[p] = k; // store L(k,i) in column i lx[p] = Complex.Conjugate(lki); } // Compute L(k,k) if (d.Real <= 0 || d.Imaginary != 0) { throw new Exception(Resources.MatrixSymmetricPositiveDefinite); } p = c[k]++; li[p] = k; // store L(k,k) = sqrt (d) in column k lx[p] = Complex.Sqrt(d); } lp[n] = colp[n]; // finalize L }
/// <summary> /// Compute the Numeric Cholesky factorization, L = chol (A, [pinv parent cp]). /// </summary> /// <returns>Numeric Cholesky factorization</returns> private void Factorize(CompressedColumnStorage <double> A) { double d, lki; int top, i, p, k; int n = A.ColumnCount; // Allocate workspace. var c = new int[n]; var s = new int[n]; var x = new double[n]; var colp = symFactor.cp; var pinv = symFactor.pinv; var parent = symFactor.parent; var C = pinv != null?PermuteSym(A, pinv, true) : A; var cp = C.ColumnPointers; var ci = C.RowIndices; var cx = C.Values; this.L = CompressedColumnStorage <double> .Create(n, n, colp[n]); var lp = L.ColumnPointers; var li = L.RowIndices; var lx = L.Values; //var lst = new List<int>(); var percent = 0; for (k = 0; k < n; k++) { lp[k] = c[k] = colp[k]; } for (k = 0; k < n; k++) // compute L(k,:) for L*L' = C { if (100 * k / n != percent) { Progress = percent = (100 * k) / n; //Console.WriteLine("{0}% solve", percent); } // Find nonzero pattern of L(k,:) top = GraphHelper.EtreeReach(SymbolicColumnStorage.Create(C, false), k, parent, s, c); x[k] = 0; // x (0:k) is now zero var tmp = cp[k + 1]; for (p = cp[k]; p < tmp; p++) // x = full(triu(C(:,k))) { if (ci[p] <= k) { x[ci[p]] = cx[p]; } } d = x[k]; // d = C(k,k) x[k] = 0; // clear x for k+1st iteration // Triangular solve for (; top < n; top++) // solve L(0:k-1,0:k-1) * x = C(:,k) { i = s[top]; // s [top..n-1] is pattern of L(k,:) lki = x[i] / lx[lp[i]]; // L(k,i) = x (i) / L(i,i) x[i] = 0; // clear x for k+1st iteration p = lp[i] + 1; var cci = c[i]; for (p = lp[i] + 1; p < cci; p++) { x[li[p]] -= lx[p] * lki; } d -= lki * lki; // d = d - L(k,i)*L(k,i) p = c[i]++; li[p] = k; // store L(k,i) in column i lx[p] = lki; } // Compute L(k,k) if (d <= 0) { throw new NotPositiveDefiniteException("not pos def"); // TODO: ex } p = c[k]++; li[p] = k; // store L(k,k) = sqrt (d) in column k lx[p] = Math.Sqrt(d); } lp[n] = colp[n]; // finalize L }
/// <summary> /// Compute coarse and then fine Dulmage-Mendelsohn decomposition. seed /// optionally selects a randomized algorithm. /// </summary> /// <param name="matrix">column-compressed matrix</param> /// <param name="seed">0: natural, -1: reverse, random order otherwise</param> /// <returns>Dulmage-Mendelsohn analysis</returns> public static DulmageMendelsohn Generate <T>(CompressedColumnStorage <T> matrix, int seed = 0) where T : struct, IEquatable <T>, IFormattable { int i, j, k, cnz, nc, nb1, nb2; int[] Cp, ps, rs; bool ok; // We are not interested in the actual matrix values. var A = SymbolicColumnStorage.Create(matrix); // Maximum matching int m = A.RowCount; int n = A.ColumnCount; var result = new DulmageMendelsohn(m, n); // allocate result int[] p = result.p; int[] q = result.q; int[] r = result.r; int[] s = result.s; int[] cc = result.cc; int[] rr = result.rr; int[] jimatch = MaximumMatching.Generate(A, seed); // max transversal if (jimatch == null) { return(null); } // Coarse decomposition for (j = 0; j < n; j++) { s[j] = -1; // unmark all cols for bfs } for (i = 0; i < m; i++) { r[i] = -1; // unmark all rows for bfs } result.BreadthFirstSearch(A, n, r, s, q, jimatch, m, 0, 1); // find C1, R1 from C0*/ ok = result.BreadthFirstSearch(A, m, s, r, p, jimatch, 0, m, 3); // find R3, C3 from R0*/ if (!ok) { return(null); } result.Unmatched(n, s, q, cc, 0); // unmatched set C0 result.Matched(n, s, jimatch, m, p, q, cc, rr, 1, 1); // set R1 and C1 result.Matched(n, s, jimatch, m, p, q, cc, rr, 2, -1); // set R2 and C2 result.Matched(n, s, jimatch, m, p, q, cc, rr, 3, 3); // set R3 and C3 result.Unmatched(m, r, p, rr, 3); // unmatched set R0 // Fine decomposition int[] pinv = Permutation.Invert(p); // pinv=p' var C = SymbolicColumnStorage.Create(matrix); A.Permute(pinv, q, C); // C=A(p,q) (it will hold A(R2,C2)) Cp = C.ColumnPointers; nc = cc[3] - cc[2]; // delete cols C0, C1, and C3 from C if (cc[2] > 0) { for (j = cc[2]; j <= cc[3]; j++) { Cp[j - cc[2]] = Cp[j]; } } C.Reshape(-1, nc); if (rr[2] - rr[1] < m) // delete rows R0, R1, and R3 from C { RowPrune(C, nc, rr); cnz = Cp[nc]; int[] Ci = C.RowIndices; if (rr[1] > 0) { for (k = 0; k < cnz; k++) { Ci[k] -= rr[1]; } } } C.Reshape(nc, -1); var scc = FindScc(C, nc); // find strongly connected components of C*/ // Combine coarse and fine decompositions ps = scc.p; // C(ps,ps) is the permuted matrix rs = scc.r; // kth block is rs[k]..rs[k+1]-1 nb1 = scc.nb; // # of blocks of A(R2,C2) for (k = 0; k < nc; k++) { s[k] = q[ps[k] + cc[2]]; } for (k = 0; k < nc; k++) { q[k + cc[2]] = s[k]; } for (k = 0; k < nc; k++) { r[k] = p[ps[k] + rr[1]]; } for (k = 0; k < nc; k++) { p[k + rr[1]] = r[k]; } nb2 = 0; // create the fine block partitions r[0] = s[0] = 0; if (cc[2] > 0) { nb2++; // leading coarse block A (R1, [C0 C1]) } for (k = 0; k < nb1; k++) // coarse block A (R2,C2) { r[nb2] = rs[k] + rr[1]; // A (R2,C2) splits into nb1 fine blocks s[nb2] = rs[k] + cc[2]; nb2++; } if (rr[2] < m) { r[nb2] = rr[2]; // trailing coarse block A ([R3 R0], C3) s[nb2] = cc[3]; nb2++; } r[nb2] = m; s[nb2] = n; result.nb = nb2; // Remove unused space Array.Resize(ref result.r, nb2 + 1); Array.Resize(ref result.s, nb2 + 1); return(result); }
/// <summary> /// Generate minimum degree ordering of A+A' (if A is symmetric) or A'A. /// </summary> /// <param name="A">Column-compressed matrix</param> /// <param name="order">Column ordering method</param> /// <returns>amd(A+A') if A is symmetric, or amd(A'A) otherwise, null on /// error or for natural ordering</returns> /// <remarks> /// See Chapter 7.1 (Fill-reducing orderings: Minimum degree ordering) in /// "Direct Methods for Sparse Linear Systems" by Tim Davis. /// </remarks> public static int[] Generate <T>(CompressedColumnStorage <T> A, ColumnOrdering order) where T : struct, IEquatable <T>, IFormattable { int[] Cp, Ci, P, W, nv, next, head, elen, degree, w, hhead; int d, dk, dext, lemax = 0, e, elenk, eln, i, j, k, k1, k2, k3, jlast, ln, dense, nzmax, mindeg = 0, nvi, nvj, nvk, mark, wnvi, cnz, nel = 0, p, p1, p2, p3, p4, pj, pk, pk1, pk2, pn, q, n; bool ok; int h; n = A.ColumnCount; if (order == ColumnOrdering.Natural) { // TODO: return null here? return(Permutation.Create(n)); } var C = ConstructMatrix(SymbolicColumnStorage.Create(A), order); Cp = C.ColumnPointers; cnz = Cp[n]; // Find dense threshold dense = Math.Max(16, 10 * (int)Math.Sqrt(n)); dense = Math.Min(n - 2, dense); // add elbow room to C if (!C.Resize(cnz + cnz / 5 + 2 * n)) { return(null); } P = new int[n + 1]; // allocate result W = new int[n + 1]; // get workspace w = new int[n + 1]; degree = new int[n + 1]; elen = new int[n + 1]; // Initialized to 0's // Initialize quotient graph for (k = 0; k < n; k++) { W[k] = Cp[k + 1] - Cp[k]; } W[n] = 0; nzmax = C.RowIndices.Length; Ci = C.RowIndices; for (i = 0; i <= n; i++) { P[i] = -1; w[i] = 1; // node i is alive degree[i] = W[i]; // degree of node i } next = new int[n + 1]; hhead = new int[n + 1]; head = new int[n + 1]; nv = new int[n + 1]; Array.Copy(P, next, n + 1); Array.Copy(P, head, n + 1); // degree list i is empty Array.Copy(P, hhead, n + 1); // hash list i is empty Array.Copy(w, nv, n + 1); // node i is just one node mark = Clear(0, 0, w, n); // clear w elen[n] = -2; // n is a dead element Cp[n] = -1; // n is a root of assembly tree w[n] = 0; // n is a dead element // Initialize degree lists for (i = 0; i < n; i++) { d = degree[i]; if (d == 0) // node i is empty { elen[i] = -2; // element i is dead nel++; Cp[i] = -1; // i is a root of assembly tree w[i] = 0; } else if (d > dense) // node i is dense { nv[i] = 0; // absorb i into element n elen[i] = -1; // node i is dead nel++; Cp[i] = -(n + 2); // FLIP(n) nv[n]++; } else { if (head[d] != -1) { P[head[d]] = i; } next[i] = head[d]; // put node i in degree list d head[d] = i; } } while (nel < n) // while (selecting pivots) do { // Select node of minimum approximate degree for (k = -1; mindeg < n && (k = head[mindeg]) == -1; mindeg++) { ; } if (next[k] != -1) { P[next[k]] = -1; } head[mindeg] = next[k]; // remove k from degree list elenk = elen[k]; // elenk = |Ek| nvk = nv[k]; // # of nodes k represents nel += nvk; // nv[k] nodes of A eliminated // Garbage collection if (elenk > 0 && cnz + mindeg >= nzmax) { for (j = 0; j < n; j++) { if ((p = Cp[j]) >= 0) // j is a live node or element { Cp[j] = Ci[p]; // save first entry of object Ci[p] = -(j + 2); // first entry is now CS_FLIP(j) } } for (q = 0, p = 0; p < cnz;) // scan all of memory { if ((j = FLIP(Ci[p++])) >= 0) // found object j { Ci[q] = Cp[j]; // restore first entry of object Cp[j] = q++; // new pointer to object j for (k3 = 0; k3 < W[j] - 1; k3++) { Ci[q++] = Ci[p++]; } } } cnz = q; // Ci [cnz...nzmax-1] now free } // Construct new element dk = 0; nv[k] = -nvk; // flag k as in Lk p = Cp[k]; pk1 = (elenk == 0) ? p : cnz; // do in place if elen[k] == 0 pk2 = pk1; for (k1 = 1; k1 <= elenk + 1; k1++) { if (k1 > elenk) { e = k; // search the nodes in k pj = p; // list of nodes starts at Ci[pj]*/ ln = W[k] - elenk; // length of list of nodes in k } else { e = Ci[p++]; // search the nodes in e pj = Cp[e]; ln = W[e]; // length of list of nodes in e } for (k2 = 1; k2 <= ln; k2++) { i = Ci[pj++]; if ((nvi = nv[i]) <= 0) { continue; // node i dead, or seen } dk += nvi; // degree[Lk] += size of node i nv[i] = -nvi; // negate nv[i] to denote i in Lk Ci[pk2++] = i; // place i in Lk if (next[i] != -1) { P[next[i]] = P[i]; } if (P[i] != -1) // remove i from degree list { next[P[i]] = next[i]; } else { head[degree[i]] = next[i]; } } if (e != k) { Cp[e] = -(k + 2); // absorb e into k // FLIP(k) w[e] = 0; // e is now a dead element } } if (elenk != 0) { cnz = pk2; // Ci [cnz...nzmax] is free } degree[k] = dk; // external degree of k - |Lk\i| Cp[k] = pk1; // element k is in Ci[pk1..pk2-1] W[k] = pk2 - pk1; elen[k] = -2; // k is now an element // Find set differences mark = Clear(mark, lemax, w, n); // clear w if necessary for (pk = pk1; pk < pk2; pk++) // scan 1: find |Le\Lk| { i = Ci[pk]; if ((eln = elen[i]) <= 0) { continue; // skip if elen[i] empty } nvi = -nv[i]; // nv [i] was negated wnvi = mark - nvi; for (p = Cp[i]; p <= Cp[i] + eln - 1; p++) // scan Ei { e = Ci[p]; if (w[e] >= mark) { w[e] -= nvi; // decrement |Le\Lk| } else if (w[e] != 0) // ensure e is a live element { w[e] = degree[e] + wnvi; // 1st time e seen in scan 1 } } } // Degree update for (pk = pk1; pk < pk2; pk++) // scan2: degree update { i = Ci[pk]; // consider node i in Lk p1 = Cp[i]; p2 = p1 + elen[i] - 1; pn = p1; for (h = 0, d = 0, p = p1; p <= p2; p++) // scan Ei { e = Ci[p]; if (w[e] != 0) // e is an unabsorbed element { dext = w[e] - mark; // dext = |Le\Lk| if (dext > 0) { d += dext; // sum up the set differences Ci[pn++] = e; // keep e in Ei h += e; // compute the hash of node i } else { Cp[e] = -(k + 2); // aggressive absorb. e.k // FLIP(k) w[e] = 0; // e is a dead element } } } elen[i] = pn - p1 + 1; // elen[i] = |Ei| p3 = pn; p4 = p1 + W[i]; for (p = p2 + 1; p < p4; p++) // prune edges in Ai { j = Ci[p]; if ((nvj = nv[j]) <= 0) { continue; // node j dead or in Lk } d += nvj; // degree(i) += |j| Ci[pn++] = j; // place j in node list of i h += j; // compute hash for node i } if (d == 0) // check for mass elimination { Cp[i] = -(k + 2); // absorb i into k // FLIP(k) nvi = -nv[i]; dk -= nvi; // |Lk| -= |i| nvk += nvi; // |k| += nv[i] nel += nvi; nv[i] = 0; elen[i] = -1; // node i is dead } else { degree[i] = Math.Min(degree[i], d); // update degree(i) Ci[pn] = Ci[p3]; // move first node to end Ci[p3] = Ci[p1]; // move 1st el. to end of Ei Ci[p1] = k; // add k as 1st element in of Ei W[i] = pn - p1 + 1; // new len of adj. list of node i h = ((h < 0) ? (-h) : h) % n; // finalize hash of i next[i] = hhead[h]; // place i in hash bucket hhead[h] = i; P[i] = h; // save hash of i in last[i] } } // scan2 is done degree[k] = dk; // finalize |Lk| lemax = Math.Max(lemax, dk); mark = Clear(mark + lemax, lemax, w, n); // clear w // Supernode detection for (pk = pk1; pk < pk2; pk++) { i = Ci[pk]; if (nv[i] >= 0) { continue; // skip if i is dead } h = P[i]; // scan hash bucket of node i i = hhead[h]; hhead[h] = -1; // hash bucket will be empty for (; i != -1 && next[i] != -1; i = next[i], mark++) { ln = W[i]; eln = elen[i]; for (p = Cp[i] + 1; p <= Cp[i] + ln - 1; p++) { w[Ci[p]] = mark; } jlast = i; for (j = next[i]; j != -1;) // compare i with all j { ok = (W[j] == ln) && (elen[j] == eln); for (p = Cp[j] + 1; ok && p <= Cp[j] + ln - 1; p++) { if (w[Ci[p]] != mark) { ok = false; // compare i and j } } if (ok) // i and j are identical { Cp[j] = -(i + 2); // absorb j into i // FLIP(i) nv[i] += nv[j]; nv[j] = 0; elen[j] = -1; // node j is dead j = next[j]; // delete j from hash bucket next[jlast] = j; } else { jlast = j; // j and i are different j = next[j]; } } } } // Finalize new element for (p = pk1, pk = pk1; pk < pk2; pk++) // finalize Lk { i = Ci[pk]; if ((nvi = -nv[i]) <= 0) { continue; // skip if i is dead } nv[i] = nvi; // restore nv[i] d = degree[i] + dk - nvi; // compute external degree(i) d = Math.Min(d, n - nel - nvi); if (head[d] != -1) { P[head[d]] = i; } next[i] = head[d]; // put i back in degree list P[i] = -1; head[d] = i; mindeg = Math.Min(mindeg, d); // find new minimum degree degree[i] = d; Ci[p++] = i; // place i in Lk } nv[k] = nvk; // # nodes absorbed into k if ((W[k] = p - pk1) == 0) // length of adj list of element k { Cp[k] = -1; // k is a root of the tree w[k] = 0; // k is now a dead element } if (elenk != 0) { cnz = p; // free unused space in Lk } } // Postordering for (i = 0; i < n; i++) { Cp[i] = -(Cp[i] + 2); // fix assembly tree // FLIP(Cp[i]) } for (j = 0; j <= n; j++) { head[j] = -1; } for (j = n; j >= 0; j--) // place unordered nodes in lists { if (nv[j] > 0) { continue; // skip if j is an element } next[j] = head[Cp[j]]; // place j in list of its parent head[Cp[j]] = j; } for (e = n; e >= 0; e--) // place elements in lists { if (nv[e] <= 0) { continue; // skip unless e is an element } if (Cp[e] != -1) { next[e] = head[Cp[e]]; // place e in list of its parent head[Cp[e]] = e; } } for (k = 0, i = 0; i <= n; i++) // postorder the assembly tree { if (Cp[i] == -1) { k = GraphHelper.TreeDepthFirstSearch(i, k, head, next, P, w); } } return(P); }
/// <summary> /// Compute strongly connected components of matrix. /// </summary> /// <param name="matrix">column-compressed matrix</param> /// <returns>Strongly connected components</returns> public static StronglyConnectedComponents Generate <T>(CompressedColumnStorage <T> matrix) where T : struct, IEquatable <T>, IFormattable { return(Generate(SymbolicColumnStorage.Create(matrix), matrix.ColumnCount)); }