* Implementation of symbolic matrices */
/*
- * GiNaC Copyright (C) 1999-2016 Johannes Gutenberg University Mainz, Germany
+ * GiNaC Copyright (C) 1999-2019 Johannes Gutenberg University Mainz, Germany
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
}
+/** Inverse of this matrix, with automatic algorithm selection. */
+matrix matrix::inverse() const
+{
+ return inverse(solve_algo::automatic);
+}
+
/** Inverse of this matrix.
*
+ * @param algo selects the algorithm (one of solve_algo)
* @return the inverted matrix
* @exception logic_error (matrix not square)
* @exception runtime_error (singular matrix) */
-matrix matrix::inverse() const
+matrix matrix::inverse(unsigned algo) const
{
if (row != col)
throw (std::logic_error("matrix::inverse(): matrix not square"));
matrix sol(row,col);
try {
- sol = this->solve(vars,identity);
+ sol = this->solve(vars, identity, algo);
} catch (const std::runtime_error & e) {
if (e.what()==std::string("matrix::solve(): inconsistent linear system"))
throw (std::runtime_error("matrix::inverse(): singular matrix"));
/** Solve a linear system consisting of a m x n matrix and a m x p right hand
* side by applying an elimination scheme to the augmented matrix.
*
- * @param vars n x p matrix, all elements must be symbols
+ * @param vars n x p matrix, all elements must be symbols
* @param rhs m x p matrix
* @param algo selects the solving algorithm
* @return n x p solution matrix
const unsigned n = this->cols();
const unsigned p = rhs.cols();
- // syntax checks
- if ((rhs.rows() != m) || (vars.rows() != n) || (vars.col != p))
+ // syntax checks
+ if ((rhs.rows() != m) || (vars.rows() != n) || (vars.cols() != p))
throw (std::logic_error("matrix::solve(): incompatible matrices"));
for (unsigned ro=0; ro<n; ++ro)
for (unsigned co=0; co<p; ++co)
for (unsigned c=0; c<p; ++c)
aug.m[r*(n+p)+c+n] = rhs.m[r*p+c];
}
-
- // Gather some statistical information about the augmented matrix:
- bool numeric_flag = true;
- for (auto & r : aug.m) {
- if (!r.info(info_flags::numeric)) {
- numeric_flag = false;
- break;
- }
- }
-
- // Here is the heuristics in case this routine has to decide:
- if (algo == solve_algo::automatic) {
- // Bareiss (fraction-free) elimination is generally a good guess:
- algo = solve_algo::bareiss;
- // For m<3, Bareiss elimination is equivalent to division free
- // elimination but has more logistic overhead
- if (m<3)
- algo = solve_algo::divfree;
- // This overrides any prior decisions.
- if (numeric_flag)
- algo = solve_algo::gauss;
- }
-
+
// Eliminate the augmented matrix:
- switch(algo) {
- case solve_algo::gauss:
- aug.gauss_elimination();
- break;
- case solve_algo::divfree:
- aug.division_free_elimination();
- break;
- case solve_algo::bareiss:
- default:
- aug.fraction_free_elimination();
- }
+ auto colid = aug.echelon_form(algo, n);
// assemble the solution matrix:
matrix sol(n,p);
unsigned last_assigned_sol = n+1;
for (int r=m-1; r>=0; --r) {
unsigned fnz = 1; // first non-zero in row
- while ((fnz<=n) && (aug.m[r*(n+p)+(fnz-1)].is_zero()))
+ while ((fnz<=n) && (aug.m[r*(n+p)+(fnz-1)].normal().is_zero()))
++fnz;
if (fnz>n) {
// row consists only of zeros, corresponding rhs must be 0, too
- if (!aug.m[r*(n+p)+n+co].is_zero()) {
+ if (!aug.m[r*(n+p)+n+co].normal().is_zero()) {
throw (std::runtime_error("matrix::solve(): inconsistent linear system"));
}
} else {
// assign solutions for vars between fnz+1 and
// last_assigned_sol-1: free parameters
for (unsigned c=fnz; c<last_assigned_sol-1; ++c)
- sol(c,co) = vars.m[c*p+co];
+ sol(colid[c],co) = vars.m[colid[c]*p+co];
ex e = aug.m[r*(n+p)+n+co];
for (unsigned c=fnz; c<n; ++c)
- e -= aug.m[r*(n+p)+c]*sol.m[c*p+co];
- sol(fnz-1,co) = (e/(aug.m[r*(n+p)+(fnz-1)])).normal();
+ e -= aug.m[r*(n+p)+c]*sol.m[colid[c]*p+co];
+ sol(colid[fnz-1],co) = (e/(aug.m[r*(n+p)+fnz-1])).normal();
last_assigned_sol = fnz;
}
}
// assign solutions for vars between 1 and
// last_assigned_sol-1: free parameters
for (unsigned ro=0; ro<last_assigned_sol-1; ++ro)
- sol(ro,co) = vars(ro,co);
+ sol(colid[ro],co) = vars(colid[ro],co);
}
return sol;
}
-
/** Compute the rank of this matrix. */
unsigned matrix::rank() const
+{
+ return rank(solve_algo::automatic);
+}
+
+/** Compute the rank of this matrix using the given algorithm,
+ * which should be a member of enum solve_algo. */
+unsigned matrix::rank(unsigned solve_algo) const
{
// Method:
// Transform this matrix into upper echelon form and then count the
// number of non-zero rows.
-
GINAC_ASSERT(row*col==m.capacity());
- // Actually, any elimination scheme will do since we are only
- // interested in the echelon matrix' zeros.
matrix to_eliminate = *this;
- to_eliminate.fraction_free_elimination();
+ to_eliminate.echelon_form(solve_algo, col);
unsigned r = row*col; // index of last non-zero element
while (r--) {
* @see matrix::determinant() */
ex matrix::determinant_minor() const
{
- // for small matrices the algorithm does not make any sense:
const unsigned n = this->cols();
- if (n==1)
- return m[0].expand();
- if (n==2)
- return (m[0]*m[3]-m[2]*m[1]).expand();
- if (n==3)
- return (m[0]*m[4]*m[8]-m[0]*m[5]*m[7]-
- m[1]*m[3]*m[8]+m[2]*m[3]*m[7]+
- m[1]*m[5]*m[6]-m[2]*m[4]*m[6]).expand();
-
+
// This algorithm can best be understood by looking at a naive
// implementation of Laplace-expansion, like this one:
// ex det;
// calculated in step c-1. We therefore only have to store at most
// 2*binomial(n,n/2) minors.
- // Unique flipper counter for partitioning into minors
- std::vector<unsigned> Pkey;
- Pkey.reserve(n);
- // key for minor determinant (a subpartition of Pkey)
- std::vector<unsigned> Mkey;
+ // we store the minors in maps, keyed by the rows they arise from
+ typedef std::vector<unsigned> keyseq;
+ typedef std::map<keyseq, ex> Rmap;
+
+ Rmap M, N; // minors used in current and next column, respectively
+ // populate M with dummy unit, to be used as factor in rightmost column
+ M[keyseq{}] = _ex1;
+
+ // keys to identify minor of M and N (Mkey is a subsequence of Nkey)
+ keyseq Mkey, Nkey;
Mkey.reserve(n-1);
- // we store our subminors in maps, keys being the rows they arise from
- typedef std::map<std::vector<unsigned>,class ex> Rmap;
- typedef std::map<std::vector<unsigned>,class ex>::value_type Rmap_value;
- Rmap A;
- Rmap B;
+ Nkey.reserve(n);
+
ex det;
- // initialize A with last column:
- for (unsigned r=0; r<n; ++r) {
- Pkey.erase(Pkey.begin(),Pkey.end());
- Pkey.push_back(r);
- A.insert(Rmap_value(Pkey,m[n*(r+1)-1]));
- }
// proceed from right to left through matrix
- for (int c=n-2; c>=0; --c) {
- Pkey.erase(Pkey.begin(),Pkey.end()); // don't change capacity
- Mkey.erase(Mkey.begin(),Mkey.end());
+ for (int c=n-1; c>=0; --c) {
+ Nkey.clear();
+ Mkey.clear();
for (unsigned i=0; i<n-c; ++i)
- Pkey.push_back(i);
- unsigned fc = 0; // controls logic for our strange flipper counter
+ Nkey.push_back(i);
+ unsigned fc = 0; // controls logic for minor key generator
do {
det = _ex0;
for (unsigned r=0; r<n-c; ++r) {
// maybe there is nothing to do?
- if (m[Pkey[r]*n+c].is_zero())
+ if (m[Nkey[r]*n+c].is_zero())
continue;
- // create the sorted key for all possible minors
- Mkey.erase(Mkey.begin(),Mkey.end());
- for (unsigned i=0; i<n-c; ++i)
- if (i!=r)
- Mkey.push_back(Pkey[i]);
- // Fetch the minors and compute the new determinant
+ // Mkey is same as Nkey, but with element r removed
+ Mkey.clear();
+ Mkey.insert(Mkey.begin(), Nkey.begin(), Nkey.begin() + r);
+ Mkey.insert(Mkey.end(), Nkey.begin() + r + 1, Nkey.end());
+ // add product of matrix element and minor M to determinant
if (r%2)
- det -= m[Pkey[r]*n+c]*A[Mkey];
+ det -= m[Nkey[r]*n+c]*M[Mkey];
else
- det += m[Pkey[r]*n+c]*A[Mkey];
+ det += m[Nkey[r]*n+c]*M[Mkey];
}
- // prevent build-up of deep nesting of expressions saves time:
+ // prevent nested expressions to save time
det = det.expand();
- // store the new determinant at its place in B:
+ // if the next computed minor is zero, don't store it in N:
+ // (if key is not found, operator[] will just return a zero ex)
if (!det.is_zero())
- B.insert(Rmap_value(Pkey,det));
- // increment our strange flipper counter
+ N[Nkey] = det;
+ // compute next minor key
for (fc=n-c; fc>0; --fc) {
- ++Pkey[fc-1];
- if (Pkey[fc-1]<fc+c)
+ ++Nkey[fc-1];
+ if (Nkey[fc-1]<fc+c)
break;
}
if (fc<n-c && fc>0)
for (unsigned j=fc; j<n-c; ++j)
- Pkey[j] = Pkey[j-1]+1;
+ Nkey[j] = Nkey[j-1]+1;
} while(fc);
- // next column, clear B and change the role of A and B:
- A = std::move(B);
+ // if N contains no minors, then they all vanished
+ if (N.empty())
+ return _ex0;
+
+ // proceed to next column: switch roles of M and N, clear N
+ M = std::move(N);
}
return det;
}
+std::vector<unsigned>
+matrix::echelon_form(unsigned algo, int n)
+{
+ // Here is the heuristics in case this routine has to decide:
+ if (algo == solve_algo::automatic) {
+ // Gather some statistical information about the augmented matrix:
+ bool numeric_flag = true;
+ for (const auto & r : m) {
+ if (!r.info(info_flags::numeric)) {
+ numeric_flag = false;
+ break;
+ }
+ }
+ unsigned density = 0;
+ for (const auto & r : m) {
+ density += !r.is_zero();
+ }
+ unsigned ncells = col*row;
+ if (numeric_flag) {
+ // For numerical matrices Gauss is good, but Markowitz becomes
+ // better for large sparse matrices.
+ if ((ncells > 200) && (density < ncells/2)) {
+ algo = solve_algo::markowitz;
+ } else {
+ algo = solve_algo::gauss;
+ }
+ } else {
+ // For symbolic matrices Markowitz is good, but Bareiss/Divfree
+ // is better for small and dense matrices.
+ if ((ncells < 120) && (density*5 > ncells*3)) {
+ if (ncells <= 12) {
+ algo = solve_algo::divfree;
+ } else {
+ algo = solve_algo::bareiss;
+ }
+ } else {
+ algo = solve_algo::markowitz;
+ }
+ }
+ }
+ // Eliminate the augmented matrix:
+ std::vector<unsigned> colid(col);
+ for (unsigned c = 0; c < col; c++) {
+ colid[c] = c;
+ }
+ switch(algo) {
+ case solve_algo::gauss:
+ gauss_elimination();
+ break;
+ case solve_algo::divfree:
+ division_free_elimination();
+ break;
+ case solve_algo::bareiss:
+ fraction_free_elimination();
+ break;
+ case solve_algo::markowitz:
+ colid = markowitz_elimination(n);
+ break;
+ default:
+ throw std::invalid_argument("matrix::echelon_form(): 'algo' is not one of the solve_algo enum");
+ }
+ return colid;
+}
/** Perform the steps of an ordinary Gaussian elimination to bring the m x n
* matrix into an upper echelon form. The algorithm is ok for matrices
return sign;
}
+/* Perform Markowitz-ordered Gaussian elimination (with full
+ * pivoting) on a matrix, constraining the choice of pivots to
+ * the first n columns (this simplifies handling of augmented
+ * matrices). Return the column id vector v, such that v[column]
+ * is the original number of the column before shuffling (v[i]==i
+ * for i >= n). */
+std::vector<unsigned>
+matrix::markowitz_elimination(unsigned n)
+{
+ GINAC_ASSERT(n <= col);
+ std::vector<int> rowcnt(row, 0);
+ std::vector<int> colcnt(col, 0);
+ // Normalize everything before start. We'll keep all the
+ // cells normalized throughout the algorithm to properly
+ // handle unnormal zeros.
+ for (unsigned r = 0; r < row; r++) {
+ for (unsigned c = 0; c < col; c++) {
+ if (!m[r*col + c].is_zero()) {
+ m[r*col + c] = m[r*col + c].normal();
+ rowcnt[r]++;
+ colcnt[c]++;
+ }
+ }
+ }
+ std::vector<unsigned> colid(col);
+ for (unsigned c = 0; c < col; c++) {
+ colid[c] = c;
+ }
+ exvector ab(row);
+ for (unsigned k = 0; (k < col) && (k < row - 1); k++) {
+ // Find the pivot that minimizes (rowcnt[r]-1)*(colcnt[c]-1).
+ unsigned pivot_r = row + 1;
+ unsigned pivot_c = col + 1;
+ int pivot_m = row*col;
+ for (unsigned r = k; r < row; r++) {
+ for (unsigned c = k; c < n; c++) {
+ const ex &mrc = m[r*col + c];
+ if (mrc.is_zero())
+ continue;
+ GINAC_ASSERT(rowcnt[r] > 0);
+ GINAC_ASSERT(colcnt[c] > 0);
+ int measure = (rowcnt[r] - 1)*(colcnt[c] - 1);
+ if (measure < pivot_m) {
+ pivot_m = measure;
+ pivot_r = r;
+ pivot_c = c;
+ }
+ }
+ }
+ if (pivot_m == row*col) {
+ // The rest of the matrix is zero.
+ break;
+ }
+ GINAC_ASSERT(k <= pivot_r && pivot_r < row);
+ GINAC_ASSERT(k <= pivot_c && pivot_c < col);
+ // Swap the pivot into (k, k).
+ if (pivot_c != k) {
+ for (unsigned r = 0; r < row; r++) {
+ m[r*col + pivot_c].swap(m[r*col + k]);
+ }
+ std::swap(colid[pivot_c], colid[k]);
+ std::swap(colcnt[pivot_c], colcnt[k]);
+ }
+ if (pivot_r != k) {
+ for (unsigned c = k; c < col; c++) {
+ m[pivot_r*col + c].swap(m[k*col + c]);
+ }
+ std::swap(rowcnt[pivot_r], rowcnt[k]);
+ }
+ // No normalization before is_zero() here, because
+ // we maintain the matrix normalized throughout the
+ // algorithm.
+ ex a = m[k*col + k];
+ GINAC_ASSERT(!a.is_zero());
+ // Subtract the pivot row KJI-style (so: loop by pivot, then
+ // column, then row) to maximally exploit pivot row zeros (at
+ // the expense of the pivot column zeros). The speedup compared
+ // to the usual KIJ order is not really significant though...
+ for (unsigned r = k + 1; r < row; r++) {
+ const ex &b = m[r*col + k];
+ if (!b.is_zero()) {
+ ab[r] = b/a;
+ rowcnt[r]--;
+ }
+ }
+ colcnt[k] = rowcnt[k] = 0;
+ for (unsigned c = k + 1; c < col; c++) {
+ const ex &mr0c = m[k*col + c];
+ if (mr0c.is_zero())
+ continue;
+ colcnt[c]--;
+ for (unsigned r = k + 1; r < row; r++) {
+ if (ab[r].is_zero())
+ continue;
+ bool waszero = m[r*col + c].is_zero();
+ m[r*col + c] = (m[r*col + c] - ab[r]*mr0c).normal();
+ bool iszero = m[r*col + c].is_zero();
+ if (waszero && !iszero) {
+ rowcnt[r]++;
+ colcnt[c]++;
+ }
+ if (!waszero && iszero) {
+ rowcnt[r]--;
+ colcnt[c]--;
+ }
+ }
+ }
+ for (unsigned r = k + 1; r < row; r++) {
+ ab[r] = m[r*col + k] = _ex0;
+ }
+ }
+ return colid;
+}
/** Perform the steps of division free elimination to bring the m x n matrix
* into an upper echelon form.
sign = -sign;
for (unsigned r2=r0+1; r2<m; ++r2) {
for (unsigned c=c0+1; c<n; ++c)
- this->m[r2*n+c] = (this->m[r0*n+c0]*this->m[r2*n+c] - this->m[r2*n+c0]*this->m[r0*n+c]).expand();
+ this->m[r2*n+c] = (this->m[r0*n+c0]*this->m[r2*n+c] - this->m[r2*n+c0]*this->m[r0*n+c]).normal();
// fill up left hand side with zeros
for (unsigned c=r0; c<=c0; ++c)
this->m[r2*n+c] = _ex0;