Table of Contents
The latest release of LELA may be obtained from its website. Developement of LELA occurs at its GIT repository to which anyone may obtain read-only access.
See the file INSTALL in the LELA-distribution for detailed installation-instructions. The basic procedure is
./configure make make install
LELA has one mandatory library-dependency, the GNU Multi-Precision Library (GMP) for calculations with large integers and rational numbers. This is available for nearly all platforms and can usually be obtained from your platform-vendor or, for users of Windows, from Cygwin. See also GMP's website.
In addition, LELA can optionally make use of some libraries to
provide highly tuned implementations of algorithms in specific
cases or to provide additional functionality. Their presence is
detected when you run configure. They are
as follows:
The Basic Linear Algebra Subprograms or BLAS. This can dramatically improve performance over Z/n. LELA can use almost any implementation of BLAS. Such an implementation can be obtained from your vendor or from a project such as ATLAS.
The use of BLAS may be enabled or disabled when
running configure with the
option --with-blas,
respectively --without-blas.
M4RI. This library dramatically improves performance for calculations with dense matrices over GF(2). It can be obtained from its website.
The use of M4RI may be enabled or disabled when
running configure with the
option --with-m4ri,
respectively --without-m4ri.
libpng. This library adds support for reading and writing 0-1 matrices in PNG format, which is extremely useful for large matrices.
The use of libpng may be enabled or disabled when
running configure with the
option --with-png,
respectively --without-png.
For a complete list of options
to configure, run
./configure --help
In this section you will learn how to use LELA for basic calculations.
LELA is a library in C++ which is intended for exact calculations in linear algebra. It is flexible in two respects:
LELA is designed so that it will use the most efficient available method for the above choices. To that end, various algorithms are implemented for different cases and selected automatically at compile-time. There are also wrappers for certain libraries which contain specialised implementations for specific cases, such as M4RI for dense matrices over GF(2).
LELA is a C++ template-library. This means in particular that algorithms and classes which require arithmetic are parametrised by the ring of computation as well as vector- and matrix-types. Many functions are then specialised to provide better performance in specific cases, for example a specific choice of vector-representation over a specific ring. It is possible to extend the functionality of LELA by providing types which meet well-defined interfaces (see Section 3).
All definitions in LELA are in the namespace LELA. A simple program using LELA may include the line using namespace LELA; at the top of the file to avoid unneeded verbage.
The basic types with which the user should be familiar are:
This section is divided as follows. First there are instructions on how to define rings followed by some basics on arithmetic over rings. Then the definition of vectors and matrices is described. Then the facilities for vector- and matrix-arithmetic are introduced. After that, a solution-object for a higher-level interface is briefly described. Then one learns how to input and output vectors and matrices to and from the console and files. Then there is some information on support for reporting, benchmarking, and debugging in LELA. Finally, we present some pointers on how to deal with compiler-errors which sometimes arise.
The first step is to define the ring over which you wish to compute. You must include the ring's corresponding header-file and then declare the ring-object. There are four ring-types built in to LELA:
To work with integers (of arbitrary precision), use the following:
#include <lela/ring/integers.h>
using namespace LELA;
int main ()
{
Integers Z;
...
return 0;
}
The element-type of Integers is called
integer. It is defined in
lela/integer.h.
To work with rational numbers, use the following:
#include <lela/ring/rationals.h>
using namespace LELA;
int main ()
{
Rationals F;
...
return 0;
}
The element-type of Rationals is
called RationalElement. It is defined
in lela/element/rational.h.
Here is an example of the declaration of the field of integers modulo 101:
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
...
return 0;
}The type Modular is parametrised by the element-type, which may in principle be any C++-type which supports basic arithmetic operations (i.e. binary +, -, *, /, and %). The parameter passed to the constructor is the modulus. It may not exceed the maximum value allowed by the type.
The choice of which element-type to use is based on the size of the modulus (since obviously some moduli are too big for some element-types) as well as whether one wishes to use an integral of a floating-point representation. A floating-point representation generally uses more memory for the same modulus but enables the use of highly tuned BLAS-implementations which can greatly improve performance as compared with the integral representation.
While with a given element-type one can use any modulus up to the maximum (see table below), we recommend using a somewhat larger element-type than is necessary, since this can improve performance greatly. Here we present some recommendations.
Table 1. Element-types to be used with Modular
| Element-type | Size | Integral vs. FP | Maximum allowed modulus | Maximum recommended modulus |
|---|---|---|---|---|
| uint8 | 1 byte | Integral | 255 (28-1) | 255 |
| uint16 | 2 bytes | Integral | 65535 (216-1) | 65535 |
| uint32 | 4 bytes | Integral | 4294967295 (232-1) | 4294967295 |
| float | 4 bytes | Floating point | 4095 (212-1) | 127 |
| double | 8 bytes | Floating point | 67108863 (226-1) | 2097151 |
| integer | Variable | Integral | Unlimited | Unlimited |
The definitions of the types above (other than float and
double) are in lela/integer.h. The
header-file lela/ring/modular.h
declares the class Modular.
The following code permits working over GF2:
#include <lela/ring/gf2.h>
using namespace LELA;
int main ()
{
GF2 F;
...
return 0;
}It is also possible to work over GF(2) using the Modular class described above, but the specialised class GF2 is vastly more efficient. Its use is highly recommended.
The element-type of GF2
is bool. The value true
corresponds to the field-element 1
and false to 0.
If desired, it is possible to wrap a C++-type (builtin or user-defined) which supports standard arithmetic into LELA-ring so that it may be used within LELA. For example, wrapping float or double permits that LELA be used for numerical linear algebra. To do this, use the following;
#include <lela/ring/unparametric.h>
using namespace LELA;
int main ()
{
TypeWrapperRing<float> R;
...
return 0;
}The class TypeWrapperRing is parametrised by the type being wrapped, which is then the element-type of the resulting ring.
Once a ring is declared, it may be used to perform arithmetic:
#include <iostream>
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
uint8 a, b, c, d; // Declare ring-elements
F.init (a, -1); // Set a to -1 in the ring
F.init (b, 1); // Set b to 1 in the ring
F.add (c, a, b); // Set c <- a + b
F.sub (c, a, b); // Set c <- a - b
F.mul (c, a, b); // Set c <- a * b
F.neg (c, b); // Set c <- -b
F.axpy (d, a, b, c); // Set d <- a * b + c
if (F.inv (c, a)) // Is a invertible in the ring? If so, set c to the inverse
std::cout << "a is invertible" << std::endl;
else
std::cout << "a is not invertible" << std::endl;
if (F.div (c, a, b)) // Does the quotient a / b exist in the ring? If so, set c to the quotient
std::cout << "a / b exists" << std::endl;
else
std::cout << "a / b does not exist" << std::endl;
return 0;
}All arithmetic operations take a reference to the element in which to write the result as their first parameter. As compared with the seemingly more natural approach of returning the result, this makes things more efficient in the case that the element-type is complicated (such as a GMP-integer).
The method init sets its first argument
to the ring-element which is the image of its second
argument under the canonical map from the integers to the
ring. The second argument may be of any integral type.
All arithmetic-operations have in-place variants:
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
uint8 a, b, c, d; // Declare ring-elements
F.init (a, -1); // Set a to -1 in the ring
F.copy (b, a); // Copy a to b
F.negin (b); // Replace b to -b
F.init (c, 2); // Set c to 2 in the ring
F.addin (a, b); // Set a <- a + b
F.subin (a, b); // Set a <- a - b
F.mulin (a, b); // Set a <- a * b
F.axpyin (a, b, c); // Set a <- a + b * c
if (F.invin (a)) // Is a invertible in the ring? If so, replace it with its inverse
std::cout << "a is invertible" << std::endl;
else
std::cout << "a is not invertible" << std::endl;
if (F.divin (a, b)) // Does the quotient a / b exist in the ring? If so, replace a by a / b
std::cout << "a / b exists" << std::endl;
else
std::cout << "a / b does not exist" << std::endl;
return 0;
}
The method copy makes a copy of its
second argument and stores it in its first argument. In
generic code, it is recommended to use
copy rather than the
assignment-operator, since the latter may result in only a
shallow copy, depending on the element-type. In the case of
Modular<uint8> above, the call
copy (b, a) is entirely equivalent to the
assignment b = a.
Ring-elements may be read from or written to a C++-stream
with the methods read and write:
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
uint8 a;
std::cout << "Enter a value: ";
F.read (std::cin, a);
std::cout << "You entered ";
F.write (std::cout, a) << std::endl;
return 0;
}The precise meaning of these methods depends on the ring. The method Modular::read interprets the input as an integer and maps it to the modular field under the canonical homomorphism.
Equality of elements may be checked with the
method areEqual:
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
uint8 a, b;
F.init (a, 50);
std::cout << "Enter a value: ";
F.read (std::cin, b);
if (F.areEqual (a, b))
std::cout << "You guessed correctly!" << std::endl;
return 0;
}
Similarly, the methods isZero
and isOne determine whether a
ring-element is equal to zero or one, respectively, in the
ring.
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
uint8 a;
std::cout << "Enter a value: ";
F.read (std::cin, a);
if (F.isZero (a))
std::cout << "You entered zero in the ring" << std::endl;
if (F.isOne (a))
std::cout << "You entered one in the ring" << std::endl;
return 0;
}
The
methods zero, one,
and minusOne return read-only
references to respectively the ring's 0, 1, and -1 elements.
#include <lela/integer.h>
#include <lela/ring/modular.h>
using namespace LELA;
int main ()
{
Modular<uint8> F (101);
uint8 a;
std::cout << "Enter a value: ";
F.read (std::cin, a);
if (F.areEqual (a, F.one ()))
std::cout << "You entered one in the ring" << std::endl;
if (F.areEqual (a, F.zero ()))
std::cout << "You entered zero in the ring" << std::endl;
if (F.areEqual (a, F.minusOne ()))
std::cout << "You entered minus one in the ring" << std::endl;
return 0;
}When writing algorithms, it is desireable that the code not depend on operating over a given ring. Thus the relevant function or class should be parametrised on the ring-type. Ring-elements may be declared with typename Ring::Element, as in the following:
template <class Ring>
void MyAlgorithm (const Ring &R, typename Ring::Element &v)
{
typename Ring::Element a, b;
R.init (a, 1);
R.init (b, 103);
R.add (v, a, b);
}Algorithms in LELA are designed to be generic with respect to vector- and matrix-type. They may also work with both sparse and dense representations. There is a variety of vector- and matrix-types to serve these needs.
LELA supports five basic vector representation-types: two for general rings and three over GF(2). Any C++-type which meets the required interface of one of these types can be used in any container or algorithm which accepts that type.
Over general rings the two vector representation-types are dense and sparse.
A dense vector must meet the interface of std::vector. Its size must be the rank of the free module in which it exists. It is an error, for example, to attempt to perform an arithmetic operation using two dense vectors of different sizes.
#include <vector>
template <class Ring>
void MyAlgorithm (const Ring &R, size_t n)
{
std::vector<typename Ring::Element> v (n); // A dense vector in R^n
// Initialise some entries
R.init (v[0], 1);
R.init (v[1], -1);
R.init (v[2], -3);
}The state of the entries of a just declared dense vector is undefined. All entries must be initialised in some fashion before the vector is used. There are several possibilities to do this:
Scale the vector by zero using the BLAS-function. In this special case the entries of the vector are initialised to zero and need not have been previously initialised. For example:
Context<Ring> ctx (R); std::vector<typename Ring::Element> v (n); // A dense vector in R^n BLAS1::scal (ctx, R.zero (), v); // Initialise all entries in v to zero
Initialise the vector from a VectorStream. For example:
RandomDenseStream<Ring, std::vector<typename Ring::Element> > stream (R, n); std::vector<typename Ring::Element> v (n); // A dense vector in R^n stream >> v; // Obtain a random vector in R^n
Alternatively, the type Vector<Ring>::Dense may be used to declare dense vectors. For general rings, this is just an alias for std::vector<typename Ring::Element>.
A sparse vector must appear as an STL-vector of
index-entry (STL-)pairs,
i.e. std::vector<std::pair<unsigned int,
typename Ring::Element> >. There are no
requirements on its size and the module in which it exists
is not defined. Indices must be in strictly increasing
order and the zero-element is not allowed as an entry
(such an entry should be removed from the vector). Indices
which have no corresponding entry in the vector are taken
to be zero. Normally such vectors are built progressively
using push_back.
To declare a sparse vector, use Vector<Ring>::Sparse.
#include <lela/vector/sparse.h>
using namespace LELA;
template <class Ring>
void MyAlgorithm (const Ring &R)
{
typename Vector<Ring>::Sparse v;
// Index zero gets value -1
v.push_back (typename Vector<Ring>::Sparse::value_type (0, Ring::Element ()));
R.init (v.back ().second, -1);
// Index 15 gets value 3
v.push_back (typename Vector<Ring>::Sparse::value_type (15, Ring::Element ()));
R.init (v.back ().second, 3);
// Index 23 gets value 10
v.push_back (typename Vector<Ring>::Sparse::value_type (23, Ring::Element ()));
R.init (v.back ().second, 10);
}Over GF(2), three types are permitted: dense, sparse, and hybrid.
The dense type is in principle similar to that for general rings, but requires additional interfaces to permit parallel computations on words.
#include <lela/ring/gf2.h>
using namespace LELA;
void MyAlgorithm (const GF2 &F, unsigned int n)
{
Vector<GF2>::Dense v (n);
*(v.word_begin ()) = 0xf0f0f0f0f0f0f0f0ULL; // Set all of the first 64 bits of the vector simultaneously
*(v.word_begin () + 1) = 0xaaaaaaaaaaaaaaaaULL; // Set all of the second 64 bits of the vector simultaneously
v.back_word () = 0xeeee5555eeee5555ULL; // Set the final 64 bits of the vector simultaneously
}As with dense vectors over arbitrary rings, the module in which the vector exists is defined by the vector's size.
The sparse type, accessed through Vector<GF2>::Sparse is just an std::vector of indices corresponding to nonzero entries. The indices must be in strictly increasing order.
#include <lela/ring/gf2.h>
using namespace LELA;
void MyAlgorithm (const GF2 &F)
{
Vector<GF2>::Sparse v;
v.push_back (1); // Index 1 gets value one
v.push_back (15); // Index 15 gets value one
v.push_back (23); // Index 23 gets value one
}As with general sparse vectors, nonexistent indices are taken to be zero. The vector need not be of a particular size, and the module in which it exists is not well defined.
The hybrid vector is a mixture of sparse and dense representations. As with a sparse vector, it appears as a vector of pairs. The space of indices is divided into blocks each of size equal to the number of bits in a word (normally 64 bits). The first entry of a pair is the index of the block and the second is the word located at that block. Missing indices correspond to the zero word.
This representation is well suited to vectors over GF(2) with large blocks of zeros as well as dense blocks. It permits fast parallel operations on vectors but, provided the zero-blocks are large enough, saves memory as compared with the dense representation.
To construct a hybrid-vector, use Vector<GF2>::Hybrid. The interface is then similar to that of sparse vectors over general rings.
#include <lela/ring/gf2.h>
using namespace LELA;
void MyAlgorithm (const GF2 &R)
{
typename Vector<GF2>::Hybrid v;
// Append entries to the vector
v.push_back (Vector<GF2>::Hybrid::value_type (0, 0xffff0000ffff0000ULL)); // First 64 indices
v.push_back (Vector<GF2>::Hybrid::value_type (1, 0xf0f0f0f0f0f0f0f0ULL)); // Second 64 indices
v.push_back (Vector<GF2>::Hybrid::value_type (5, 0xaaaa5555aaaa5555ULL)); // Indices 5 * 64 - 6 * 64
}Matrices in LELA are pure containers. They know nothing about the underlying ring or its arithmetic. LELA provides two basic matrix-types: dense and sparse. The interfaces for these matrices are similar.
To declare a dense matrix, use DenseMatrix, which is parametrised by the element-type.
#include <lela/matrix/dense.h>
using namespace LELA;
template <class Ring>
void MyFunction (const Ring &R)
{
DenseMatrix<typename Ring::Element> M (5, 5); // Declare a 5x5 matrix
typename Ring::Element a;
R.init (a, 5);
M.setEntry (1, 2, a); // Set entry (1,2) to a
}Note that row- and column-indices start from zero.
![]() | Caution |
|---|---|
The state of the entries of a newly defined dense matrix is undefined. In particular, all entries should be initialised before the dense matrix is used in any calculation. This can be done:
|
Dense matrices provide three types of iterators: row, column, and raw. The row- and column-iterators iterate over the row- and column-vectors of the matrix, providing them as dense vectors.
#include <lela/matrix/dense.h>
using namespace LELA;
template <class Ring>
void MyFunction (const Ring &R)
{
DenseMatrix<typename Ring::Element> M (5, 5); // Declare a 5x5 matrix
// Iterate through all the rows, setting the entry 2 on each one to 5 in the ring
typename DenseMatrix<typename Ring::Element>::RowIterator i;
for (i = M.rowBegin (); i != M.rowEnd (); ++i)
R.init ((*i)[2], 5);
// Iterate through all the columns, setting the entry 1 on each one to 10 in the ring
typename DenseMatrix<typename Ring::Element>::ColIterator j;
for (j = M.colBegin (); j != M.colEnd (); ++j)
R.init ((*i)[1], 10);
}The raw iterator iterates through all the entries of the matrix in some order. It is useful for applying an operation uniformly to all entries in the matrix.
#include <lela/matrix/dense.h>
using namespace LELA;
template <class Ring>
void MyFunction (const Ring &R, DenseMatrix<typename Ring::Element> &M)
{
// Set all entries in the matrix to 5 in the ring
DenseMatrix<typename Ring::Element>::RawIterator i;
for (i = M.rawBegin (); i != M.rawEnd (); ++i)
R.init (*i, 5);
}Each of the iterators has a const equivalent, the name prefixed by Const.
Over GF(2), the declaration of a dense matrix is analogous: use DenseMatrix<bool>. There is however no support for column-iterators.
![]() | Note |
|---|---|
If LELA is configured with support for M4RI, then DenseMatrix<bool> is a wrapper for the matrix-type in M4RI. |
The sparse matrix stores the rows of the matrix as sparse vectors. Its type is SparseMatrix and it is parametrised by element-type.
#include <lela/matrix/sparse.h>
using namespace LELA;
template <class Ring>
void MyFunction (const Ring &R)
{
SparseMatrix<typename Ring::Element> M (5, 5); // Declare a 5x5 matrix
typename Ring::Element a;
R.init (a, 5);
M.setEntry (1, 2, a); // Set entry (1,2) to a
}Sparse matrices have no support for column-iterators. Row iterators iterate over row-vectors which are of a sparse type. Otherwise the interface is identical to that of DenseMatrix.
In contrast to dense matrices, a newly defined sparse matrix is by definition the zero-matrix.
Sparse matrices are also parameterised by the type of the row-vector. This can be used to define a sparse matrix over GF(2) whose rows are hybrid vectors.
#include <lela/ring/gf2.h>
#include <lela/matrix/sparse.h>
using namespace LELA;
void MyFunction (const GF2 &F)
{
SparseMatrix<bool, Vector<GF2>::Hybrid> M (5, 5); // Declare a 5x5 matrix
...
}LELA provides support for declaring subvectors, submatrices, and transposed matrices which are just windows into their respective containers and therefore require no additional copying. There are, however, some caveats which we will discuss here.
There are two types of subvectors and respectively submatrices: arbitrary and aligned. The starting and possibly ending indices of an aligned subvector or submatrix may be limited to an integral multiple of some value. However, calculations with aligned subvectors and submatrices may be much faster than with arbitrary ones. In addition, the hybrid vector-representation does not permit arbitrary subvectors which can be modified. Aligned subvector and submatrices are useful in recursive algorithms which divide matrices and vectors up and where the decision of where to make the division is open to the implementor of the algorithm. Currently these types are only distinct with vectors over GF(2).
One may declare an arbitrary subvector of a vector of
type Vector by defining an object of the
type VectorTraits<Ring,
Vector>::SubvectorType and passing to its
constructor the parent-vector and the starting and ending
indices of the subvector. Here we define a
subvector w of a previously defined
vector v to span the indices 10 through
100 (not including 100 itself).
Vector v; ... typename VectorTraits<Ring, Vector>::SubvectorType w (v, 10, 100);
If the parent-vector is const, then it is required to use VectorTraits<Ring, Vector>::ConstSubvectorType instead.
const Vector v; ... typename VectorTraits<Ring, Vector>::ConstSubvectorType w (v, 10, 100);
To declare an aligned subvector, use VectorTraits<Ring, Vector>::AlignedSubvectorType, respectively VectorTraits<Ring, Vector>::ConstAlignedSubvectorType. In this case the starting index must be an integral multiple of the static integer VectorTraits<Ring, Vector>::align. The ending index should either be an integral multiple of VectorTraits<Ring, Vector>::align or coincide with the end of the vector.
This basic mechanism works for all vector-types but there are a few limitations:
![]() | Caution |
|---|---|
It is important to be extremely careful with mutable
subvectors of sparse or hybrid vectors. If a subvector is
changed, then other subvectors of the same vector will not
be updated to reflect that change. For example, suppose you
have two subvectors Vector<Ring>::Sparse x; ... typename Vector<Ring>::Sparse::SubvectorType x_1 (x, 0, 10); typename Vector<Ring>::Sparse::SubvectorType x_2 (x, 10, 20); x_1.push_back (5, typename Ring::Element ()); // x_2 now invalid Failure to take this into account may lead to random memory-corruption. This can also be a problem when using the arithmetic-routines described below. It is advised to avoid using two subvectors of a given sparse vector as inputs to the same operation if one of them will be changed, for example: BLAS2::gemv (ctx, a, A, x_2, b, x_1); // DANGER: Using both x_1 and x_2! |
Declaring a submatrix is similar. To create an arbitrary mutable submatrix of a given matrix of type Matrix, use Matrix::SubmatrixType and supply to the constructor the containing matrix, the row- and column-indices of the upper left entry (indexed from 0, as usual), and the row- and column-dimensions. The following creates a 5x5 submatrix starting at index (2,2) of a 10x10 matrix.
Matrix M (10, 10); // M has size 10x10 typename Matrix::SubmatrixType S (M, 2, 2, 5, 5);
Note the difference with subvectors: for submatrices one supplies the dimensions of the submatrix, while for subvectors one supplies instead the ending index.
If the matrix is const, then use Matrix::ConstSubmatrixType.
const Matrix M (10, 10); // M has size 10x10 typename Matrix::ConstSubmatrixType S (M, 2, 2, 5, 5);
One can create aligned submatrices with Matrix::AlignedSubmatrixType and Matrix::ConstAlignedSubmatrixType. In this case the starting row- and column-indices must be integer multiples of Matrix::rowAlign and respectively Matrix::colAlign. The dimensions should be such that either the ending row and column are aligned with an integer multiple of the above values or they coincide with the end of the matrix.
As with subvectors, it is not permitted to take an mutable arbitrary submatrix of a sparse matrix whose rows or columns are hybrid vectors. Unlike subvectors it is okay to construct a mutable submatrix of a mutable submatrix of a sparse matrix.
To construct a transposed matrix, use the
class TransposeMatrix, defined
in lela/matrix/transpose.h. This class
is parametrised by matrix-type.
#include "lela/matrix/transpose.h" ... Matrix M; TransposeMatrix<Matrix> MT (M);
If the matrix is const, then TransposeMatrix should also be instantiated with the const variant.
const Matrix M; TransposeMatrix<const Matrix> MT (M);
![]() | Note |
|---|---|
In LELA there also exist classes Subvector, SparseSubvector, and Submatrix. These are basically for internal use. Do not use them unless you are creating your own vector- or matrix-type and wish to employ them to define the subvector or submatrix-types used above. |
Basic vector- and matrix-arithmetic follow the interface of BLAS.
To use these features, first construct a context object. This object encapsulates the ring over which computations take place, the configuration of algorithms for arithmetic, and temporary storage.
#include <lela/blas/context.h>
using namespace LELA;
template <class Ring>
void MyFunction (const Ring &R)
{
Context<Ring> ctx (R);
...
}![]() | Caution |
|---|---|
Context objects are not intended to be reentrant, since they contain temporary storage. If your program is multithreaded, it is necessary to construct one context object per thread. Context objects can be copy-constructed. |
BLAS-routines are split into three levels: level 1 for vector-vector operations, level 2 for matrix-vector operations, and level 3 for matrix-matrix operations.. The routines in LELA are divided correspondingly into the namespaces BLAS1, BLAS2, and BLAS3.
Use use BLAS-routines at a given level, include the
header-file lela/blas/level<l>.h,
where <l> is respectively 1, 2, or 3. Then declare the
context-object and simply invoke the function. The arguments
generally follow the BLAS-standard.
Here is an example with vectors:
#include <lela/blas/context.h>
#include <lela/blas/level1.h>
using namespace LELA;
template <class Ring, class Vector1, class Vector2>
void MyFunction (const Ring &R, Vector1 &v, Vector2 &w)
{
Context<Ring> ctx (R);
BLAS1::axpy (ctx, R.one (), v, w); // Replace w by v + w
BLAS1::scal (ctx, R.minusOne (), v); // Replace v by -v
typename Ring::Element a;
BLAS1::dot (ctx, a, v, w); // Compute the dot-product of v and w and store it in a
}And another example with level 2 operations:
#include <lela/blas/context.h>
#include <lela/blas/level2.h>
using namespace LELA;
template <class Ring, class Vector, class Matrix>
void MyFunction (const Ring &R, Vector &v, Vector &w, Matrix &A)
{
Context<Ring> ctx (R);
BLAS2::ger (ctx, R.one (), v, w, A); // Replace A by v w^T + A
BLAS2::gemv (ctx, R.one (), A, v, R.minusOne (), w); // Replace w by A v - w
// Replace v by U^-1 v, where U is the upper triangular part of A with ones on the diagonal
BLAS2::trsv (ctx, A, v, UpperTriangular, true);
}
Because matrix- and vector-types in LELA are distinct, several
level 1 operations have in level 3 matrix-equivalents which
are not present in the BLAS standard. These
include copy, scal,
and axpy.
#include <lela/blas/context.h>
#include <lela/blas/level3.h>
using namespace LELA;
template <class Ring, class Matrix1, class Matrix2, class Matrix3>
void MyFunction (const Ring &R, Matrix1 &A, Matrix2 &B, Matrix3 &C)
{
Context<Ring> ctx (R);
BLAS3::axpy (ctx, R.one (), A, B); // Replace B by A + B
BLAS3::scal (ctx, R.minusOne (), A); // Replace A by -A
BLAS3::gemm (ctx, R.one (), A, B, R.one (), C); // Replace C by A * B + C
}There are some limitations in the combinations of types which can be used in some interfaces. See the reference-documentation for more details.
In addition to basic BLAS-functionality there is support for applying permutations to both vectors and matrices as well as input and output of vectors and matrices.
![]() | Note |
|---|---|
Support for symmetric, skew-symmetric, and banded matrices has not been implemented. |
While LELA strives to be as flexible as possible with respect to the inputs to the BLAS-routines, there are some combinations whose support is essentially impossible (or at least far to difficult to implement). They must therefore be avoided. Examples include:
BLAS3::axpy (i.e. aX + Y ->
Y, X and Y matrices) where X and Y not support an
interator-type in common, e.g. if X is a sparse matrix and Y
is the transpose of a sparse matrix;BLAS3::gemm (i.e. aAB + bC
-> C, A, B, and C matrices) where A and B are both sparse
matrices and C is the transpose of a sparse matrix, or where
A and B are both transposed sparse matrices and C is an
ordinary sparse matrix;BLAS2::trmv
and BLAS2::trsv (i.e. Tx -> x,
T-1x -> x, T a triangular
matrix, x a vector) where x is a sparse or hybrid
vector;BLAS2::trmm
and BLAS2::trsm (i.e. TB -> B,
T-1B -> B, T a triangular
matrix, B a matrix) where B is a transposed sparse or hybrid
matrix.Please consult the reference-documentation for a more detailed account.
Higher level functionality, such as computing the row echelon form of a matrix, is provided by solutions. Each solution is encapsulated in a class.
To compute the row echelon form of a matrix, use the
solution EchelonForm. Its header-file
is lela/solutions/echelon-form.h.
The use is quite straightforward:
#include <lela/solutions/echelon-form.h>
using namespace LELA;
template <class Ring, class Matrix>
void ComputeRowEchelonForm (const Ring &R, Matrix &A)
{
Context<Ring> ctx (R);
EchelonForm<Ring> EF (ctx);
echelonize (A); // A now replaced by its row echelon form
}There is also support for computing the reduced row echelon form of a matrix:
#include <lela/solutions/echelon-form.h>
using namespace LELA;
template <class Ring, class Matrix>
void ComputeRowEchelonForm (const Ring &R, Matrix &A)
{
Context<Ring> ctx (R);
EchelonForm<Ring> EF (ctx);
echelonize (A, true); // A now replaced by its reduced row echelon form
}You can also instruct EchelonForm to use a particular method when computing the row-echelon form. For example, if your matrix comes from the F4 algorithm, it is useful to use the method of Faugère-Lachartre:
#include <lela/solutions/echelon-form.h>
using namespace LELA;
template <class Ring, class Matrix>
void ComputeRowEchelonForm (const Ring &R, Matrix &A)
{
Context<Ring> ctx (R);
EchelonForm<Ring> EF (ctx);
echelonize (A, true, METHOD_FAUGERE_LACHARTRE); // A now replaced by its reduced row echelon form
}The supported methods are as follows:
METHOD_STANDARD_GJ, naive
Gaussian elimination. Slow, but available for any type of
matrix in with any configuration.
METHOD_ASYMPTOTICALLY_FAST_GJ,
asymptotically fast Gaussian elimination (using possibly
fast matrix-multiplication). Only available for dense
matrices.
METHOD_M4RI, wrapper for the
method in M4RI. Only available for dense matrices over
GF(2) when M4RI is enabled.
METHOD_FAUGERE_LACHARTRE, method
based on the algorithm of Faugère and Lachartre. Only
available for matrices of the form coming from the F4
algorithm.
Input and output are provided within the BLAS-interface.
The interfaces for inputing respectively outputing vectors
are BLAS1::read
respectively BLAS1::write. They accept as
arguments the C++ stream and the vector in question.
#include <iostream>
#include <lela/blas/level1.h>
using namespace LELA;
template <class Ring, class Vector>
void ReadAndWriteVector (const Ring &R, Vector &v)
{
Context<Ring> ctx (R);
std::cout << "Enter a vector: ";
BLAS1::read (ctx, std::cin, v);
std::cout << "You entered: ";
BLAS1::write (ctx, std::cout, v) << std::endl;
}
The input-format should be a list of numbers which can be
interpreted by Ring::read. The
output-format is a comma-separated list of entries in square
brackets.
Input and output of matrices is provided by similar interfaces
in BLAS3. The parameters are the same with the
addition of a parameter format which
specifies the format for the matrix.
#include <iostream>
#include <lela/blas/level3.h>
using namespace LELA;
template <class Ring, class Matrix>
void ReadAndWriteVector (const Ring &R, Matrix &A, std::istream &is)
{
Context<Ring> ctx (R);
std::cout << "Reading matrix...";
BLAS3::read (ctx, istream, A, FORMAT_DETECT);
std::cout << "Matrix: " << std::endl;
BLAS3::write (ctx, std::cout, A, FORMAT_PRETTY);
}Some examples of formats are:
FORMAT_PRETTY, suitable for output to
the console or a text-file so that the matrix can be examined
by the userFORMAT_DUMAS, suitable for the
storage of sparse matricesFORMAT_MAPLE, for interacting with
MapleFORMAT_MATLAB, for interacting with
MatlabFORMAT_SAGE, for interacting with
SageFORMAT_PNG, good for large matrices
over GF(2). Only available for matrices over GF(2) and only if
LELA is configured with support for libpng.FORMAT_DETECT, try to detect the
format of a matrix. Only makes sense
for read.
The format FORMAT_PRETTY replaces zero
entries with a period to make nonzero entries more visible. It
also attempts to align entries by column so that the matrix
appears correctly in a fixed width typeface.
Throughout LELA there are checks of the form
lela_check (condition);
These check that the input to algorithms meets the required
preconditions. These checks are disabled by default, but can
be enabled by defining the macro DEBUG
when compiling both the library and sources.
LELA has a facility called the commentator for reporting information about computations it performs. This is particularly useful for obtaining benchmarks.
A program-run is divided conceptually
into activities which form a tree. To
mark the beginning and end of an activity, use the
interfaces commentator.start and
respectively commentator.stop.
#include <lela/util/commentator.h>
using namespace LELA;
void MyFunction ()
{
commentator.start ("Description of activity", __FUNCTION__);
...
commentator.stop (MSG_DONE);
}
The second argument
to commentator.start should be the name
of the calling function. The argument
to commentator.stop is a short string
indicating the state at the end of the activity, such as
whether a test passed. The
macros MSG_DONE
and MSG_STATUS provide standardised
strings for this purpose.
One can use the commentator to report information about a
computation with the
interface commentator.report, which
returns an std::ostream.
#include <lela/util/commentator.h>
using namespace LELA;
void MyFunction ()
{
commentator.start ("Description of activity", __FUNCTION__);
std::ostream &report = commentator.report (Commentator::LEVEL_IMPORTANT, INTERNAL_DESCRIPTION);
report << "Some information about the computation." << std::endl;
commentator.stop (MSG_DONE);
}
The first parameter
to commentator.report indicates the
importance of the information; it can be
either Commentator::LEVEL_IMPORTANT, Commentator::LEVEL_NORMAL,
or Commentator::LEVEL_UNIMPORTANT. The
second parameter indicates the general category of the
information. Possible values
include INTERNAL_DESCRIPTION, INTERNAL_WARNING, INTERNAL_ERROR, TIMING_MEASURE,
and PROGRESS_REPORT.
The commentator includes a sophisticated system to select which messages are printed and which are not. Messages to be printed can be selected according to the nesting-depth of the activity, the importance-level, and the classification. See the commentator's documentation for more information.
The commentator can produce both a brief output for the console and a more detailed output which can be directed to a file. See the commentator's documentation for more information.
Since LELA is a template-library, it is possible to encounter difficult compiler-errors when compiling against it. In particular, some errors appear only when a particular class or function is instantiated. The resulting output can be quite difficult to understand. Here we give some tips to help to solve such problems.
If using submatrices or subvectors, check that you have properly declared subobjects of const objects using the corresponding const type. For example:
const Matrix M; typename Matrix::SubmatrixType N (M, 2, 2, 5, 5); // ERROR! typename Matrix::ConstSubmatrixType N (M, 2, 2, 5, 5); // Correct
Many errors of the form "No matching function for call to..." or "No matching constructor..." result from attempts to remove a const from a type. For example, the following code will result in an error:
void MyFunction (const Matrix &M)
{
typename Matrix::RowIterator i = M.rowBegin (); // ERROR!
}
In this case, because M
is const, one must
use ConstRowIterator instead:
void MyFunction (const Matrix &M)
{
typename Matrix::ConstRowIterator i = M.rowBegin (); // Correct
}Consider using the compiler Clang (see their website) rather than GCC. Its error-output is much more readable. To use Clang rather than GCC, use
CXX=clang ./configure
when configuring LELA.
LELA is designed to be flexible and generic so that one can provide one's own types of rings, vectors, and matrices, as well as one's own matrix- and vector-arithmetic, and they will be properly integrated into the library.
There are three ways to create rings:
One can simply define a class meeting the proper interface and then instantiates the remaining parts of the library on that class. When the element-type and arithmetic-operations are simple (such as with Modular), this will typically provide the best performance by avoiding function-calls and enabling more compiler-optimisations. It comes however at the cost of more code-bloat, since practically the entire library must be instantiated for that ring-type.
One can define a class inheriting from RingInterface, which is parametrised by element-type. Then one instantiates all remaining classes and functions on RingInterface<Element>, where Element is one's chosen element-type. This avoids multiple instantiations in the case that one uses the library with multiple rings using the same element-type, but is slower than the method above because it relies on the invocation of virtual methods.
One can define a class inheriting from RingInterface<AbstractElement>. The type AbstractElement allows in principle any element-type to be represented. Thus the entire library is only instantiated once, even when multiple rings with multiple element-types are used. It comes at a cost of both performance and memory-overhead.
To implement a ring with the first method, it is necessary to create a class which exports the same interface as RingInterface, implementing ring-arithmetic I/O, and initialisation of ring-elements. One can use RingInterface as a starting-point. One must define an element-type; any C++ type, builtin or otherwise, may serve as one.
class MyRing
{
public:
typedef int Element;
Element &add (Element &a, const Element &b, const Element &c)
{ a = b + c; return a; } // Implementation
...
};See the section called “Extending the BLAS-implementations” for more details on this mechanism.
To use the second method, create a class which derives from RingInterface<Element>, where Element is the C++-type you have chosen to represent elements. The class RingInterface is an abstract base-class implementing the required ring-interface. It is necessary to override all pure virtual methods in it.
class MyRing : public RingInterface<MyElement>
{
public:
typedef MyElement Element;
Element &add (Element &a, const Element &b, const Element &c)
{
a = b + c;
return a;
}
...
};To use the third method, define a class which inherits from RingInterface<AbstractElement>. The element-type is then AbstractElement. This type contains two members:
ref, an void pointer to element-data, the content of which you are free to define,ring, a pointer to the ring.
Any method in your ring-class which initialises a ring-element
(that is init, copy,
read, and all out-of-place arithmetic
operations) must initialise both
ref and ring.
The class RingInterface implements the virtual
methods ref and
unref, which are called in the
copy-constructor and assignment-operator and in the destructor
of ElementAbstract, respectively (provided
ring is properly initialised).
This mechanism allows a simple memory-model based on reference-counting. For example:
class MyRing
{
public:
typedef AbstractElement Element;
void ref (Element &x)
{ if (x.ref != NULL) ++static_cast<MyElement *> (x.ref).refcount; }
void unref (Element &x)
{
if (x.ref != NULL) {
--static_cast<MyElement *> (x.ref).refcount;
if (static_cast<MyElement *> (x.ref).refcount == 0)
delete static_cast<MyElement *> (x.ref);
}
}
Element &add (Element &a, const Element &b, const Element &c)
{
a.ring = this;
unref (a);
static_cast<MyElement *> (a.ref) = new MyElement; // Assume MyElement::refcount initialised to 1
*static_cast<MyElement *> (a.ref) = *static_cast<MyElement *> (b) + *static_cast<MyElement *> (c);
return a;
}
...
};It is advantageous to define a stack of BLAS-operations for the new ring. Otherwise the generic fallback operations will be used in all cases, reducing performance substantially. See the section the section called “Extending the BLAS-implementations” for more information on this mechanism.
In any event, the header-files defining the BLAS-modules for
your new ring must be included at the end of the header-file
defining your ring. In particular, the
files lela/blas/level1-generic.h, lela/blas/level2-generic.h,
and lela/blas/level3-generic.h must be
included.
Creating new types of vectors and matrices is a matter of creating a class which meets the proper interface. LELA provides example-files which can be used as a starting-point and which have documentation for the required members.
lela/matrix/interface.hlela/vector/dense-interface.hlela/vector/sparse-interface.hlela/vector/dense01-interface.hlela/vector/sparse01-interface.hlela/vector/hybrid01-interface.hLELA provides a flexible mechanism for extending its vector- and matrix-arithmetic system to support new algorithms and to provide wrappers for specialised libraries. Here we explain a bit how this system works then how to extend it.
The BLAS-subsystem is divided into Modules which exist in a hierarchy. Each module takes responsibility for some calculations and passes those which it cannot handle to the next module down in hierarchy. At the bottom is GenericModule which acts as a catch-all for calculations which cannot be performed elsewhere. It should take any reasonable combination of input and produce the correct output, possibly with limited performance.
At the top is AllModules, a type parametrised on the ring whose specialisation to a given ring should provide all available functionality over that ring given the library's configuration. This type merely specifies in a generic way what the highest level of the hierachy is; it provides no functionality of its own.
An example of a hierarchy is that for computations over GF2 in the case that M4RI is installed. We have the following:
Each BLAS-routine has a associated class which has the same
name as the routine with a preceeding underscore. The class is
parametrised by the Ring and by the
module-tag, which is an memberless
structure whose type is obtained from a Module with
typename Module::Tag. This allows the class
to be specialised for both the ring-type and the module of
computation.
Each class should in turn contain a single static public
method op which implements the required
functionality. It may invoke a specialised function based on
the representation-type of the vector-input, the iterator-type
of the matrix-input, and other things. It must be parametrised
by Modules (the computational module-object itself) as well as
any vector- or matrix-types which appear in the input.
As an example, we provide here the class for
gemm.
template <class Ring, class ModulesTag>
class _gemm
{
public:
template <class Modules, class Matrix1, class Matrix2, class Matrix3>
static Matrix3 &op (const Ring &F, Modules &M, const typename Ring::Element &a, const Matrix1 &A, const Matrix2 &B, const typename Ring::Element &b, Matrix3 &C)
{ return _gemm<Ring, typename ModulesTag::Parent>::op (F, M, a, A, B, b, C); }
};This is the default implementation which passes the calculation onto the next module-type, whose tag is given by ModulesTag::Parent. Eventually the generic module is reached which will handle the calculation.
To create a new module it is therefore necessary to create
Here is an example of a Module-structure:
template <class Element>
struct ZpModule<integer> : public GenericModule<Modular<integer> >
{
struct Tag { typedef typename GenericModule<Modular<Element> >::Tag Parent; };
/// Number of times a product of two elements can be added before it is necessary to reduce by the modulus; 0 for unlimited
size_t block_size;
mutable std::vector<typename ModularTraits<Element>::DoubleFatElement> _tmp;
ZpModule (const Modular<Element> &R) : block_size (((typename ModularTraits<Element>::DoubleFatElement) -1LL) / ((R._modulus - 1) * (R._modulus - 1))) {}
};The structure should inherit from its parent-module, in this case GenericModule.
The first member is the Tag, which contains a single typedef giving the parent-module's tag. Note that both this typedef and inheritance of the parent-module are required.
The Module-structure may contain parameters to configure the
computation, such as block_size above, as
well as temporary storage, such as _tmp
above.
A module must contain a constructor which takes a ring. This
allows for example block_size to be
configured based on the ring. So that modules may be
constructed generically, every module must have such a
constructor.
Once the module is defined, the implementation-classes for functionality which the module implements should be specialised to the module's tag and, if necessary, to the ring over which the module operates. As an example, we show the definition of the dot-product with ZpModule:
template <>
class _dot<Modular<uint8>, ZpModule<uint8>::Tag>
{
template <class reference, class Vector1, class Vector2>
static reference &dot_impl (const Modular<uint8> &F, ZpModule<uint8> &M, reference &res, const Vector1 &x, const Vector2 &y,
VectorRepresentationTypes::Dense, VectorRepresentationTypes::Dense);
template <class reference, class Vector1, class Vector2>
static reference &dot_impl (const Modular<uint8> &F, ZpModule<uint8> &M, reference &res, const Vector1 &x, const Vector2 &y,
VectorRepresentationTypes::Sparse, VectorRepresentationTypes::Dense);
template <class reference, class Vector1, class Vector2>
static reference &dot_impl (const Modular<uint8> &F, ZpModule<uint8> &M, reference &res, const Vector1 &x, const Vector2 &y,
VectorRepresentationTypes::Dense, VectorRepresentationTypes::Sparse)
{ return op (F, M, res, y, x); }
template <class reference, class Vector1, class Vector2>
static reference &dot_impl (const Modular<uint8> &F, ZpModule<uint8> &M, reference &res, const Vector1 &x, const Vector2 &y,
VectorRepresentationTypes::Sparse, VectorRepresentationTypes::Sparse);
public:
template <class Modules, class reference, class Vector1, class Vector2>
static reference &op (const Modular<uint8> &F, Modules &M, reference &res, const Vector1 &x, const Vector2 &y)
{ return dot_impl (F, M, res, x, y,
typename VectorTraits<Modular<uint8>, Vector1>::RepresentationType (),
typename VectorTraits<Modular<uint8>, Vector2>::RepresentationType ()); }
};In this case the dot-product is specialised on the representation-types of the input-vectors.
When defining a new ring, it is important to define a module-hierarchy for it. Otherwise the generic module is used for all calculations and this may be quite slow. To do this, simply specialise AllModules to the ring-type and define the next module in the hierachy. Some modules, such as StrassenModule providing fast matrix-multiplication, are in turn parametrised by their intended parent-modules so that one can put them on top of any other module. Here is an example of the construction of a hierarchy for a user-defined ring-type MyRing. This hierarchy will support fast matrix-multiplication but use GenericModule for everything else:
struct AllModules<MyRing> : public StrassenModule<MyRing, GenericModule<MyRing> >
{
struct Tag { typedef typename StrassenModule<MyRing, GenericModule<MyRing> >::Tag Parent; };
AllModules (const MyRing &R) : StrassenModule<MyRing, GenericModule<MyRing> > (R) {}
};