MyraMath
Tutorial for /dense routines

Overview

The /dense folder contains basic containers for tabulated numeric data like Matrix and Vector, as well as specialized containers for numeric data with exploitable nonzero structure, like DiagonalMatrix and LowerMatrix. Layered on top of these containers are various Range types, which are views into numeric data that is owned by some other underlying container. Algorithms complete the picture, by performing useful linear algebraic operations (arithmetic, decompositions, linear solution, eigensolution) upon Range's.

This layered approach (containers/ranges/algorithms) imitates the design of C++ Standard Template Library (containers/iterators/algorithms) and is intended to provide a measure of container-agnosticism to algorithms. Note that most of the heavy lifting within /dense is performed by the underlying BLAS and LAPACK libraries.

Below are links to the sections of this page:

Matrix and MatrixRange

Of all the containers in the library, Matrix is the one you will probably use the most. It tabulates numeric data into an IxJ rectangle. The internal storage is column-major to remain backwards compatible with Fortran, LAPACK and the BLAS. Matrix indices always start at zero. In fact, every container in the library uses a 0-based indexing convention. Matrix has many member functions for generating a subview (a MatrixRange) over it. Consider tutorial/dense/range.cpp, listed below, which shows some of them in action:

2 
3 #include <tests/myratest.h>
4 
5 #include <iostream>
6 
7 using namespace myra;
8 
9 ADD_TEST("dense_range","[dense]") // int main()
10  {
11  // Make a Matrix A ..
12  auto A = Matrix<double>::fill_rmajor(3,3,{1,2,3, 4,5,6, 7,8,9});
13  // .. inspect various "views" of A ..
14  std::cout << "A = " << A << std::endl; // all
15  std::cout << "A.row(0) = " << A.row(0) << std::endl; // first row
16  std::cout << "A.column(1) = " << A.column(1) << std::endl; // middle column
17  std::cout << "A.left(2) = " << A.left(2) << std::endl; // left 2 columns
18  std::cout << "A.window(1,3,1,3) = " << A.window(1,3,1,3) << std::endl; // trailing 2x2 submatrix
19  std::cout << "A.top(2) = " << A.top(2) << std::endl; // top 2 rows
20  std::cout << "A.top(2).left(2) = " << A.top(2).left(2) << std::endl; // leading 2x2 submatrix
21  // .. modify A through a range ..
22  A.top(2).left(2)(1,1) = 0;
23  std::cout << "A.top(2).left(2) = " << A.top(2).left(2) << std::endl;
24  std::cout << "A = " << A << std::endl;
25  }
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
CMatrixRange< Number > row(int i) const
Returns a MatrixRange over this(i,:)
Definition: Matrix.cpp:189
Definition: syntax.dox:1
CMatrixRange< Number > window(int i0, int i1, int j0, int j1) const
Returns a MatrixRange over this(i0:i1,j0:j1)
Definition: Matrix.cpp:142
General purpose dense matrix container, O(i*j) storage.
CMatrixRange< Number > left(int j) const
Returns a MatrixRange over the j leftmost columns, this(:,0:j)
Definition: Matrix.cpp:263
CMatrixRange< Number > column(int j) const
Returns a MatrixRange over this(:,j)
Definition: Matrix.cpp:245
CMatrixRange< Number > top(int i) const
Returns a MatrixRange over the i topmost rows, this(0:i,:)
Definition: Matrix.cpp:207
A = size 3 by 3 Matrix of double:
[ 1 2 3 ]
[ 4 5 6 ]
[ 7 8 9 ]
A.row(0) = size 1 by 3 Matrix of double:
[ 1 2 3 ]
A.column(1) = size 3 by 1 Matrix of double:
[ 2 ]
[ 5 ]
[ 8 ]
A.left(2) = size 3 by 2 Matrix of double:
[ 1 2 ]
[ 4 5 ]
[ 7 8 ]
A.window(1,3,1,3) = size 2 by 2 Matrix of double:
[ 5 6 ]
[ 8 9 ]
A.top(2) = size 2 by 3 Matrix of double:
[ 1 2 3 ]
[ 4 5 6 ]
A.top(2).left(2) = size 2 by 2 Matrix of double:
[ 1 2 ]
[ 4 5 ]
A.top(2).left(2) = size 2 by 2 Matrix of double:
[ 1 2 ]
[ 4 0 ]
A = size 3 by 3 Matrix of double:
[ 1 2 3 ]
[ 4 0 6 ]
[ 7 8 9 ]

The last few lines of this example show that:

BLAS1 and BLAS2 Routines

The next container of interest is Vector, which has an associated VectorRange for representing subviews. The usual BLAS1 vector operations like inner products (dense/dot.h), norms (dense/euclidean.h) and addition (dense/axpy.h) are present and delegate directly to the BLAS. Note that all these algorithms accept VectorRange's, so it's easy to use "just a piece" of a Vector as an operand. Of course, any Vector is implicitly convertible to a VectorRange over all of its contents, so algorithms work with regular Vector's too. The listing below, tutorial/dense/axpy.cpp, shows these algorithms in action.

2 #include <myramath/dense/dot.h>
3 #include <myramath/dense/axpy.h>
5 
6 #include <tests/myratest.h>
7 
8 #include <iostream>
9 
10 using namespace myra;
11 
12 ADD_TEST("dense_axpy","[dense]") // int main()
13  {
14  // Make a few vectors ..
15  auto x = Vector<double>::fill({1,2,3});
16  auto y = Vector<double>::fill({4,5,6});
17  // .. try out a few operations ..
18  std::cout << "x = " << x << std::endl;
19  std::cout << "x = " << y << std::endl;
20  std::cout << "euclidean(x) = " << euclidean(x) << std::endl;
21  std::cout << "dot(x,y) = " << dot(x,y) << std::endl;
22  // .. compare axpy to overloaded operators ..
23  auto z = Vector<double>::fill({0,0,0});
24  axpy(2,x,z);
25  axpy(3,y,z);
26  std::cout << "z = " << z << std::endl;
27  std::cout << "2*x+3*y = " << 2*x+3*y << std::endl;
28  // .. try using dot() over ranges ..
29  auto w = Vector<double>::fill({1,2,3,4,5,6});
30  std::cout << "w = " << w << std::endl;
31  std::cout << "w(0:3)'w(3:6) = " << dot(w.first(3),w.last(3)) << std::endl;
32  // .. scale just a part of w ..
33  w.first(4) *= 2;
34  std::cout << "w = " << w << std::endl;
35  }
BLAS1 vector-vector add.
static Vector< Number > fill(int N, Number c)
Generates a Vector of specified size filled with constant c.
Definition: Vector.cpp:288
Routines for computing euclidean norm of a Vector/VectorRange, or normalizing a Vector/VectorRange to...
Definition: syntax.dox:1
CVectorRange< Number > first(int n) const
Returns a VectorRange over the first n entries, this(0:n)
Definition: Vector.cpp:192
Routines for inner products of Vector&#39;s / VectorRange&#39;s.
Container for either a column vector or row vector (depends upon the usage context) ...
x = size 3 Vector of double:
[ 1 2 3 ]
x = size 3 Vector of double:
[ 4 5 6 ]
euclidean(x) = 3.74166
dot(x,y) = 32
z = size 3 Vector of double:
[ 14 19 24 ]
2*x+3*y = size 3 Vector of double:
[ 14 19 24 ]
w = size 6 Vector of double:
[ 1 2 3 4 5 6 ]
w(0:3)'w(3:6) = 32
w = size 6 Vector of double:
[ 2 4 6 8 5 6 ]

Vector's can be initialized from MatrixRange's that have size Nx1 or 1xN, such as the .row(i) and .column(j) subviews of a Matrix. When it comes to the question "is this a row Vector, or a column Vector?", the class is agnostic. It's just an N-vector. The "row-ness" or "column-ness" is only deduced from context. For instance, in the code excerpt below (tutorial/dense/gemv.cpp), the gemv(A,x) call will treat x as a column vector (size 3x1), while gemv(x,A) will treat x as a row vector (size 1x3).

3 #include <myramath/dense/gemv.h>
4 
5 #include <tests/myratest.h>
6 
7 #include <iostream>
8 
9 using namespace myra;
10 
11 ADD_TEST("dense_gemv","[dense]") // int main()
12  {
13  // Make a Matrix A and a Vector x ..
14  auto A = Matrix<double>::fill_rmajor(3,3,{1,4,7, 2,5,8, 3,6,9});
15  Vector<double> x(A.column(2));
16  std::cout << "A = " << A << std::endl;
17  std::cout << "x = " << x << std::endl;
18  //
19  // [1 4 7] [7] [102]
20  // .. multiply A*x = [2 5 8] * [8] = [126]
21  // [3 6 9] [9] [150]
22  //
23  std::cout << "A*x = " << gemv(A,x) << std::endl;
24  //
25  // [1 4 7]
26  // .. multiply x*A = [7 8 9] * [2 5 8] = [50 122 194]
27  // [3 6 9]
28  //
29  std::cout << "x*A = " << gemv(x,A) << std::endl;
30  }
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
Variety of routines all for dense Matrix*Vector multiplies. All just delegate to gemm() ...
Definition: syntax.dox:1
General purpose dense matrix container, O(i*j) storage.
Tabulates a vector of length N, allows random access.
Definition: conjugate.h:21
Container for either a column vector or row vector (depends upon the usage context) ...
A = size 3 by 3 Matrix of double:
[ 1 4 7 ]
[ 2 5 8 ]
[ 3 6 9 ]
x = size 3 Vector of double:
[ 7 8 9 ]
A*x = size 3 Vector of double:
[ 102 126 150 ]
x*A = size 3 Vector of double:
[ 50 122 194 ]

BLAS3 Routines

BLAS3 routines, which perform O(n^3) work upon O(n^2) data, typically provide the highest level of performance on modern CPU's due to improved data reuse from cache. The most well-known routine in this class is dense/gemm.h, which performs a fused multiply-add C=alpha*op(A)*op(B)+beta*C for MatrixRange's C, A and B. To avoid the need for data movement and temporaries, gemm() allows for various modifiers ("operations") to be applied to the A and B inputs. Each is denoted by a single character argument:

For the record, it's worth pointing out that this library uses different conventions from the BLAS for "op" modifiers: the BLAS has no option for just conjugating, and instead uses 'C' to denote conjugate transpose (where as this library calls that op='H', for hermitian). Use care. The example below (tutorial/dense/gemm.cpp) exercises all of these options for complex operands and serves as your reference for expected behavior.

2 #include <myramath/dense/gemm.h>
3 
4 #include <tests/myratest.h>
5 
6 #include <complex>
7 #include <iostream>
8 
9 using namespace myra;
10 
11 ADD_TEST("dense_gemm","[dense]") // int main()
12  {
13  typedef std::complex<double> Z;
14  auto A = Matrix<Z>::fill_cmajor(2,2,{Z(1,2),Z(3,4),Z(5,6),Z(7,8)});
15  auto B = Matrix<Z>::fill_cmajor(2,2,{Z(9,10),Z(11,12),Z(13,14),Z(15,16)});
16  std::cout << "A = " << A << std::endl;
17  std::cout << "B = " << B << std::endl;
18  std::cout << "A*B = " << gemm(A,'N',B,'N') << std::endl;
19  std::cout << "transpose(A)*B = " << gemm(A,'T',B,'N') << std::endl;
20  std::cout << "hermitian(A)*B = " << gemm(A,'H',B,'N') << std::endl;
21  std::cout << "conjugate(A)*B = " << gemm(A,'C',B,'N') << std::endl;
22  }
Definition: syntax.dox:1
General purpose dense matrix container, O(i*j) storage.
static Matrix< Number > fill_cmajor(int I, int J, std::initializer_list< Number > list)
Generates a Matrix of specified size, filled from a std::initializer_list.
Definition: Matrix.cpp:370
Variety of routines all for dense Matrix*Matrix multiplies. Delegates to the BLAS.
A = size 2 by 2 Matrix of std::complex<double>:
[ (1,2) (5,6) ]
[ (3,4) (7,8) ]
B = size 2 by 2 Matrix of std::complex<double>:
[ (9,10) (13,14) ]
[ (11,12) (15,16) ]
A*B = size 2 by 2 Matrix of std::complex<double>:
[ (-28,154) (-36,210) ]
[ (-32,238) (-40,326) ]
transpose(A)*B = size 2 by 2 Matrix of std::complex<double>:
[ (-26,108) (-34,148) ]
[ (-34,276) (-42,380) ]
hermitian(A)*B = size 2 by 2 Matrix of std::complex<double>:
[ (110,-16) (150,-24) ]
[ (278,-8) (382,-16) ]
conjugate(A)*B = size 2 by 2 Matrix of std::complex<double>:
[ (156,-14) (212,-22) ]
[ (240,-10) (328,-18) ]

Another noteworthy BLAS3 routine is dense/syrk.h, which is worth exploring because it leads to a new container type. Short for "symmetric rank-K" update, B=syrk(A) is semantically equivalent to B=gemm(A,A,'T') but performs roughly half the work by exploiting the fact that the output is known to be symmetric. The BLAS-provided syrk() routines write into a square output matrix, but will touch only one of its triangles ('U'pper or 'L'ower) as specified by the caller.

Typically the untouched triangle of syrk()'s output just goes along for the ride as wasted space. However LAPACK working note #199 describes an alternative that is waste-free and BLAS3-friendly called "rectangular packed format", which is encapsulated here as a specialized container named LowerMatrix. The code excerpt below (tutorial/dense/syrk.cpp) explores the relationships between calling (i) gemm(A,A'T') on a Matrix, (ii) syrk() on the 'L'ower triangle of a Matrix, and (iii) syrk() directly on a LowerMatrix:

3 #include <myramath/dense/gemm.h>
4 #include <myramath/dense/syrk.h>
5 
6 #include <tests/myratest.h>
7 
8 #include <iostream>
9 
10 using namespace myra;
11 
12 ADD_TEST("dense_syrk","[dense]") // int main()
13  {
14  // Make a Matrix A and empty outputs B ..
15  auto A = Matrix<double>::fill_rmajor(3,3,{1,4,7, 2,5,8, 3,6,9});
16  auto B1 = Matrix<double>::zeros(3,3);
17  auto B2 = Matrix<double>::zeros(3,3);
18  auto B3 = LowerMatrix<double>::zeros(3);
19  // .. compare gemm_inplace() ..
20  gemm_inplace(B1,A,'N',A,'T');
21  std::cout << "B1 = gemm_inplace(A,'N',A,'T') = " << B1 << std::endl;
22  // .. to syrk_inplace() onto the lower triangle of a Matrix..
23  syrk_inplace(B2,'L',A);
24  std::cout << "B2 = syrk_inplace('L',A) = " << B2 << std::endl;
25  // .. and to syrk_inplace() onto a LowerMatrix ..
26  syrk_inplace(B3,A);
27  std::cout << "B3 = syrk_inplace(A) = " << B3 << std::endl;
28  // Note the return type of (non-inplace) syrk() is a LowerMatrix ..
29  std::cout << "syrk(A) = " << syrk(A) << std::endl;
30  // .. but can expand it back out into a Matrix, both triangles filled.
31  std::cout << "syrk(A).make_Matrix('T') = " << syrk(A).make_Matrix('T') << std::endl;
32  }
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
static Matrix< Number > zeros(int I, int J)
Generates a zeros Matrix of specified size.
Definition: Matrix.cpp:357
Definition: syntax.dox:1
Specialized container for a lower triangular matrix, O(N^2/2) storage. Used by symmetry exploiting ma...
General purpose dense matrix container, O(i*j) storage.
static LowerMatrix< Number > zeros(int N)
Generates a zeros LowerMatrix of specified size.
Definition: LowerMatrix.cpp:253
Routines for symmetric rank-k updates, a specialized form of Matrix*Matrix multiplication.
Variety of routines all for dense Matrix*Matrix multiplies. Delegates to the BLAS.
B1 = gemm_inplace(A,'N',A,'T') = size 3 by 3 Matrix of double:
[ 66 78 90 ]
[ 78 93 108 ]
[ 90 108 126 ]
B2 = syrk_inplace('L',A) = size 3 by 3 Matrix of double:
[ 66 0 0 ]
[ 78 93 0 ]
[ 90 108 126 ]
B3 = syrk_inplace(A) = size 3 by 3 LowerMatrix of double:
[ 66 * * ]
[ 78 93 * ]
[ 90 108 126 ]
syrk(A) = size 3 by 3 LowerMatrix of double:
[ 66 * * ]
[ 78 93 * ]
[ 90 108 126 ]
syrk(A).make_Matrix('T') = size 3 by 3 Matrix of double:
[ 66 78 90 ]
[ 78 93 108 ]
[ 90 108 126 ]

The BLAS3 routine dense/trsm.h is very relevant for linear system solution using LU-like decompositions. It performs triangular system backsolution across multiple right hand sides. Just like syrk(), trsm() can operate upon either the 'U'pper/'L'ower triangle of a Matrix, or a LowerMatrix directly. Note that trsm() is highly reconfigurable, being able to solve L*X=B "from the left", X*L=B "from the right", and can also apply an op modifier ('N','T','H' or 'C') to L during the process.

The remaining BLAS3 routines are:

LAPACK Routines

Building on the high performance basic blocks of the BLAS, LAPACK provides a collection of matrix decompositions that can be used for (i) linear system solution, (ii) least squares solution and (iii) spectral representations. LAPACK provides a huge amount of functionality, this library really only exposes the routines that are relevant for sparse linear system solution. Many of the routines operate "in-place" and overwrite their input data. The example program below (tutorial/dense/potrf.cpp) uses potrf_inplace() ("positive triangular factorization", the Cholesky algorithm) to solve an A*X=B system. LowerMatrix plays a large role in this example because the system A is symmetric.

3 #include <myramath/dense/syrk.h>
4 #include <myramath/dense/symm.h>
5 #include <myramath/dense/potrf.h>
6 #include <myramath/dense/trsm.h>
8 
9 #include <tests/myratest.h>
10 
11 #include <iostream>
12 
13 using namespace myra;
14 
15 ADD_TEST("dense_potrf","[dense]") // int main()
16  {
17  // Make arbitrary G ..
18  auto G = Matrix<double>::fill_rmajor(3,3,{1,1,1, 1,2,3, 1,3,6});
19  std::cout << "G = " << G << std::endl;
20  // .. form a symmetric positive A using A=G*G' (uses syrk)..
21  LowerMatrix<double> A = syrk(G);
22  std::cout << "A = syrk(G) = " << A << std::endl;
23  // .. form arbitrary solution X ..
24  auto X = Matrix<double>::fill_rmajor(3,2,{4,6, 3,2, 5,1});
25  std::cout << "X = " << X << std::endl;
26  // .. form the right hand sides B using B=A*X (uses symm)..
27  Matrix<double> B = symm('L',A,X);
28  std::cout << "B = symm(A,X) = " << B << std::endl;
29  // .. factor A = L*L', overwriting A with L (uses potrf) ..
30  potrf_inplace(A);
31  std::cout << "L = potrf(A) = " << A << std::endl;
32  // .. solve L*L'*X = B, overwriting B with X (uses trsm, twice) ..
33  trsm_inplace('L','N',A,B);
34  trsm_inplace('L','T',A,B);
35  // .. check result
36  std::cout << "A\\B = L\\L'\\B = " << B << std::endl;
37  std::cout << "frobenius(A\\B-X) = " << frobenius(B-X) << std::endl;
38  }
Cholesky factorization routines for positive definite matrices.
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
Routines for computing Frobenius norms of various algebraic containers.
Definition: syntax.dox:1
Routines for backsolving by a triangular Matrix or LowerMatrix.
Specialized container for a lower triangular matrix, O(N^2/2) storage. Used by symmetry exploiting ma...
General purpose dense matrix container, O(i*j) storage.
Stores a lower triangular matrix in rectangular packed format.
Definition: conjugate.h:22
Routines for symmetric Matrix * dense Matrix multiplication.
Routines for symmetric rank-k updates, a specialized form of Matrix*Matrix multiplication.
G = size 3 by 3 Matrix of double:
[ 1 1 1 ]
[ 1 2 3 ]
[ 1 3 6 ]
A = syrk(G) = size 3 by 3 LowerMatrix of double:
[ 3 * * ]
[ 6 14 * ]
[ 10 25 46 ]
X = size 3 by 2 Matrix of double:
[ 4 6 ]
[ 3 2 ]
[ 5 1 ]
B = symm(A,X) = size 3 by 2 Matrix of double:
[ 80 40 ]
[ 191 89 ]
[ 345 156 ]
L = potrf(A) = size 3 by 3 LowerMatrix of double:
[ 1.73205 * * ]
[ 3.4641 1.41421 * ]
[ 5.7735 3.53553 0.408248 ]
A\B = L\L'\B = size 3 by 2 Matrix of double:
[ 4 6 ]
[ 3 2 ]
[ 5 1 ]
frobenius(A\B-X) = 1.42081e-12

For sake of completeness, a nonsymmetric example (tutorial/dense/getrf.cpp) based on getrf_inplace() (a pivoted LU decomposition) is shown below. The notable changes are (i) the pivoting step results in row swaps that have to accounted for during backsolution (ii) the lack of symmetry of the problem means that the both the 'L'ower and 'U'pper triangles of the decomposition must be stored, leading to different looking calls to trsm_inplace(). Since pivoting requires additional integer-valued storage to record the row swaps, MyraMath introduces another view-like type (an intRange) to represent this parameter.

3 #include <myramath/dense/gemm.h>
4 #include <myramath/dense/getrf.h>
5 #include <myramath/dense/swaps.h>
6 #include <myramath/dense/trsm.h>
8 
9 #include <tests/myratest.h>
10 
11 #include <iostream>
12 
13 using namespace myra;
14 
15 ADD_TEST("dense_getrf","[dense]") // int main()
16  {
17  // Make arbitrary A ..
18  auto A = Matrix<double>::fill_rmajor(3,3,{1,4,9, 3,6,7, 5,8,2});
19  // .. form arbitrary solution X ..
20  auto X = Matrix<double>::fill_rmajor(3,2,{10,13, 11,14, 12,15});
21  std::cout << "A = " << A << std::endl;
22  std::cout << "X = " << X << std::endl;
23  // .. form the right hand sides B using B=A*X (uses gemm)..
24  Matrix<double> B = gemm(A,X);
25  std::cout << "B = A*X = " << B << std::endl;
26  // .. factor A = P'*L*U, overwriting A with L/U and returning P (uses getrf_inplace)
27  std::vector<int> P(3);
28  getrf_inplace(A,P);
29  // solve P'*L*U*X=B, overwriting B with X
30  swap_rows(P,B);
31  trsm_inplace('L','L','N',A,B,'U');
32  trsm_inplace('L','U','N',A,B);
33  // .. check result
34  std::cout << "A\\B = P\\L\\U\\B = " << B << std::endl;
35  std::cout << "frobenius(A\\B-X) = " << frobenius(B-X) << std::endl;
36  }
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
Routines for computing Frobenius norms of various algebraic containers.
Definition: syntax.dox:1
Routines for backsolving by a triangular Matrix or LowerMatrix.
Routines related to swap sequences, often used during pivoting.
General purpose dense matrix container, O(i*j) storage.
Variety of routines all for dense Matrix*Matrix multiplies. Delegates to the BLAS.
General purpose A = P&#39;*L*U decomposition for square Matrix&#39;s.
Interface class for representing subranges of contiguous int&#39;s.
A = size 3 by 3 Matrix of double:
[ 1 4 9 ]
[ 3 6 7 ]
[ 5 8 2 ]
X = size 3 by 2 Matrix of double:
[ 10 13 ]
[ 11 14 ]
[ 12 15 ]
B = A*X = size 3 by 2 Matrix of double:
[ 162 204 ]
[ 180 228 ]
[ 162 207 ]
A\B = P\L\U\B = size 3 by 2 Matrix of double:
[ 10 13 ]
[ 11 14 ]
[ 12 15 ]
frobenius(A\B-X) = 2.7231e-14

The other noteworthy decompositions that are wrapped from LAPACK are:

This list may grow as needs require.

Solver Classes

LAPACK and the BLAS expose a fixed set of functions that can be pipelined together for solving linear algebra problems. That design is (for the most part) imitated by the routines in /dense. However, one area where slight embellishments have been made is with the introduction of "Solver" classes which encapsulate all the steps (factorization, pivot reordering, backsolution) associated with a given decomposition. Consider tutorial/dense/solver.cpp, below. It solves the same linear system as tutorial/dense/getrf.cpp but at a slightly higher level (using LUSolver() instead of getrf_inplace(), trsm_inplace(), etc).

4 #include <myramath/dense/gemm.h>
5 
6 #include <tests/myratest.h>
7 
8 #include <iostream>
9 
10 using namespace myra;
11 
12 ADD_TEST("dense_solver","[dense]") // int main()
13  {
14  // Make arbitrary A ..
15  auto A = Matrix<double>::fill_rmajor(3,3,{1,4,9, 3,6,7, 5,8,2});
16  // .. form arbitrary solution X ..
17  auto X = Matrix<double>::fill_rmajor(3,2,{10,13, 11,14, 12,15});
18  std::cout << "A = " << A << std::endl;
19  std::cout << "X = " << X << std::endl;
20  // .. form the right hand sides B using B=A*X (uses gemm)..
21  Matrix<double> B = gemm(A,X);
22  std::cout << "B = A*X = " << B << std::endl;
23  // .. form solver for A ..
24  LUSolver<double> solver(A);
25  // .. solve A*X = B ..
26  solver.solve(B);
27  // .. check result
28  std::cout << "A\\B = " << B << std::endl;
29  std::cout << "frobenius(A\\B-X) = " << frobenius(B-X) << std::endl;
30  }
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
Routines for computing Frobenius norms of various algebraic containers.
Definition: syntax.dox:1
General purpose dense matrix container, O(i*j) storage.
General purpose linear solver, no symmetry/definiteness assumptions upon A (just square) ...
Factors a square matrix A into L*U, presents solve methods.
Definition: LUSolver.h:30
Variety of routines all for dense Matrix*Matrix multiplies. Delegates to the BLAS.
A = size 3 by 3 Matrix of double:
[ 1 4 9 ]
[ 3 6 7 ]
[ 5 8 2 ]
X = size 3 by 2 Matrix of double:
[ 10 13 ]
[ 11 14 ]
[ 12 15 ]
B = A*X = size 3 by 2 Matrix of double:
[ 162 204 ]
[ 180 228 ]
[ 162 207 ]
A\B = size 3 by 2 Matrix of double:
[ 10 13 ]
[ 11 14 ]
[ 12 15 ]
frobenius(A\B-X) = 2.7231e-14

On toy examples the benefits of LUSolver vs getrf_inplace() are admittedly debatable, but it could in principle save you few moments thinking about (i) "what was the sequence of trsm() calls, again?" and (ii) "how do I undo the row interchanges, again?"

The following Solver classes are available:

The real motivation for introducing the Solver concept now is that it sets the stage for all the sparse /multifrontal solvers to come (where there is no universally accepted "LAPACK/BLAS"-like fixed function layer). Each /multifrontal solver encapsulates related functionality (reordering, factoring, solving, refinement, and more) into a single Solver entity.

Appendix: Working with external data

To expand on an idea mentioned earlier, the main purpose of the "iterator" concept in the C++ STL is to decouple std::algorithms from the std::containers upon which they operate. All of the "Range" classes in this library are intended to serve a similar role. They allow algorithms (like gemm() or gemv()) to operate upon data other than our own Matrix and Vector containers, provided that the external data is "layout compatible" with the assumptions of the Range (column major, fixed leading dimension between columns). In practice, this is almost always the case, because it's really BLAS/LAPACK/Fortran interoperability that is sets these layout conventions for everybody. MyraMath just complies with the conventions, and provides MatrixRange to help you get your own compliant data into MyraMath algorithms.

This appendix example (tutorial/dense/external.cpp) shows how to use MatrixRange to feed "external" data into MyraMath's gemm_inplace() wrapper. Here, a C-style array of double[]'s serves as mock "external" data. To instantiate a MatrixRange, you must provide essentially the same parameters that you'd pass into the BLAS anyway: (i) a pointer to a buffer of numbers, (ii) size metadata, (iii) "leading dimension" metadata.

2 #include <myramath/dense/gemm.h>
3 
4 #include <tests/myratest.h>
5 
6 #include <iostream>
7 
8 using namespace myra;
9 
10 ADD_TEST("dense_external","[dense]") // int main()
11  {
12  // Make a random Matrix A
13  auto A = Matrix<double>::fill_rmajor(3,3,{1,3,5,4,6,8,9,7,2});
14  // Someone just gave us some input values B, intended
15  // to represent a 3x3 matrix stored in column-major order.
16  double B_values[9] = {0.1, 1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8, 8.9};
17  // We also have some output values C, it's all zero.
18  double C_values[9] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
19  // Wrap up B_values and C_values into MatrixRange's
20  MatrixRange<double> B(B_values,3,3,3); // (pointer,I,J,LD)
21  MatrixRange<double> C(C_values,3,3,3); // (pointer,I,J,LD)
22  // Examine inputs.
23  std::cout << "A = " << A << std::endl;
24  std::cout << "B = " << B << std::endl;
25  std::cout << "C = " << C << std::endl;
26  // Multiply C = A*B
27  gemm_inplace(C,A,'N',B,'N');
28  // Examine output.
29  std::cout << "C = gemm(A,B) = " << C << std::endl;
30  // Examine C_values, verify that the MatrixRange "writes-through" to it.
31  for (int c = 0; c < 9; ++c)
32  std::cout << "C_values[" << c << "] = " << C_values[c] << std::endl;
33  }
Tabulates an IxJ matrix. Allows random access, has column major layout to be compatible with BLAS/LAP...
Definition: bdsqr.h:20
Definition: syntax.dox:1
Represents a mutable MatrixRange.
Definition: conjugate.h:26
General purpose dense matrix container, O(i*j) storage.
Variety of routines all for dense Matrix*Matrix multiplies. Delegates to the BLAS.
A = size 3 by 3 Matrix of double:
[ 1 3 5 ]
[ 4 6 8 ]
[ 9 7 2 ]
B = size 3 by 3 Matrix of double:
[ 0.1 3.4 6.7 ]
[ 1.2 4.5 7.8 ]
[ 2.3 5.6 8.9 ]
C = size 3 by 3 Matrix of double:
[ 0 0 0 ]
[ 0 0 0 ]
[ 0 0 0 ]
C = gemm(A,B) = size 3 by 3 Matrix of double:
[ 15.2 44.9 74.6 ]
[ 26 85.4 144.8 ]
[ 13.9 73.3 132.7 ]
C_values[0] = 15.2
C_values[1] = 26
C_values[2] = 13.9
C_values[3] = 44.9
C_values[4] = 85.4
C_values[5] = 73.3
C_values[6] = 74.6
C_values[7] = 144.8
C_values[8] = 132.7

If there was no intermediary like MatrixRange, and gemm_inplace() only accepted concrete Matrix objects, this wouldn't have been possible. At least, not without copying to/from temporaries for B and C. Similar idioms are used for getting "external" data into de-facto-standard formats for sparse matrices (like compressed-sparse-column) into MyraMath's /sparse routines and /multifrontal solvers. Furthermore, the /dense MatrixRange's and VectorRange's still have roles to play even in /sparse-land, because they serve as the interface types for right-hand-sides and solutions.

Appendix: A note about const-correctness.

As mentioned earlier, a MatrixRange is a view into data "owned" by some other container (a slice/window of a Matrix for example, or possibly external data). MatrixRange is a "mutable" view which permits modification of the underlying data. For "immutable" (const) views, there is a complementary class called CMatrixRange. This design is similar to the containers from the C++ STL, which present both (mutable) iterator's and (immutable) const_iterator's. Typically both range types are passed around as const&, when reading signatures just remember that the mutability of the underlying data is baked into the type itself (MatrixRange vs. CMatrixRange), not the const& qualifier. As an example, consider the signature of gemm_inplace(), which updates C+=A*B:

1 void gemm_inplace(
3  const MatrixRange<float>& C, // C's data is mutable
4  const CMatrixRange<float>& A, // A's data is const
5  const CMatrixRange<float>& B); // B's data is const

Because C is modified, it must be a (mutable) MatrixRange. But since A and B are passed as (immutable) CMatrixRange's, the routine is not permitted to modify them. Note that a MatrixRange can always be implicitly converted into a CMatrixRange, because it's always safe to "add constness" to a variable. The reverse conversion, "casting away const" from CMatrixRange to MatrixRange, is stylistically discouraged but can be accomplished by explicitly calling the .remove_const() member function of CMatrixRange.

Continue to Tutorial for /sparse routines, or go back to API Tutorials