Getting started with LELA


Table of Contents

Downloading and installing
Basic usage
Generalities
Selecting the ring
Declaring and filling vectors and matrices
Vector- and matrix-arithmetic
Using higher level solutions
Input and output of matrices and vectors
The commentator and debugging-support
Dealing with compiler-errors
Extending the library
Creating new rings
Creating new vector- and matrix-types
Extending the BLAS-implementations

Downloading and installing

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

Basic usage

In this section you will learn how to use LELA for basic calculations.

Generalities

LELA is a library in C++ which is intended for exact calculations in linear algebra. It is flexible in two respects:

  • the choice of ring over which vectors and matrices are defined, and
  • the representation of those vectors or matrices, e.g. dense versus sparse.

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:

  • Rings, which provide ring-arithmetic;
  • Ring-elements, the type of which is defined by the ring;
  • Vectors in a free module over a ring;
  • Matrices over a ring;
  • Context-objects, which allow the matrix-arithmetic to be configured;
  • Solution-objects, which provide a simple interface to the more sophisticated algorithms, such as computing the reduced row-echelon form of a matrix.

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.

Selecting the ring

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:

  • Integers, for integers (actually a wrapper for GMP)
  • Rationals, for rational numbers (also a wrapper for GMP)
  • Modular, for integers modulo n,
  • GF2 (for fast computations over GF(2)), and
  • TypeWrapperRing (a wrapper around any type which implements the basic arithmetic-operations +, -, *, /, and %)

Integers

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.

Rationals

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.

Modular

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-typeSizeIntegral vs. FPMaximum allowed modulusMaximum recommended modulus
uint81 byteIntegral255 (28-1)255
uint162 bytesIntegral65535 (216-1)65535
uint324 bytesIntegral4294967295 (232-1)4294967295
float4 bytesFloating point4095 (212-1)127
double8 bytesFloating point67108863 (226-1)2097151
integerVariableIntegralUnlimitedUnlimited

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.

GF2

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.

TypeWrapperRing

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.

Ring-arithmetic

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);
}

Declaring and filling vectors and matrices

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.

Types of vectors

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.

Representation-types for general rings

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:

  • Initialise the entries directly using array-notation or an iterator, as above;
  • Copy another vector to the just declared vector;
  • 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);
}
Representation-types for GF(2)

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
}

Types of matrices

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]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]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
	...
}

Declaring subvectors, submatrices, and transposed matrices

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:

  • Mutable (i.e. non-const) subvectors of sparse vectors are not permitted;
  • Arbitrary mutable subvectors of hybrid vectors are not permitted.

[Caution]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 x_1 and x_2 of a vector x. If you append an entry to x_1 it will appear correctly in x, but x_2 will be left in an invalid state:

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]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.

Vector- and matrix-arithmetic

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]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]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.

Using higher level solutions

Higher level functionality, such as computing the row echelon form of a matrix, is provided by solutions. Each solution is encapsulated in a class.

Computing the row-echelon form

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 of matrices and vectors

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 user
  • FORMAT_DUMAS, suitable for the storage of sparse matrices
  • FORMAT_MAPLE, for interacting with Maple
  • FORMAT_MATLAB, for interacting with Matlab
  • FORMAT_SAGE, for interacting with Sage
  • FORMAT_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.

The commentator and debugging-support

Enabling and disabling precondition-checks

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.

Using and configuring the commentator

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.

Disabling the commentator entirely

To disable the commentator entirely, define the macro DISABLE_COMMENTATOR. Then all commentator-lines compile to nothing.

Dealing with compiler-errors

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.

  • Check carefully that the template-arguments are of the correct type and in the correct order. Since there is no direct type-checking of template-arguments in C++, using the wrong types will result in unpredictable errors during instantiation.
  • 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
  • Check that you are not using a forbidden combination of types for BLAS-routines (see section on matrix- and vector-arithmetic above). This type of error will usually manifest itself when compiling as "No matching function for call to <function>_impl", where <function> is the name of the offending BLAS-routine.
  • 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.

  • If you are really stuck, send an email to the mailing list at lela-users@googlegroups.com.

Extending the library

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.

Creating new rings

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 vector- and matrix-types

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.

  • For matrices, see lela/matrix/interface.h
  • For dense vectors, see lela/vector/dense-interface.h
  • For sparse vectors, see lela/vector/sparse-interface.h
  • For dense 0-1 vectors, see lela/vector/dense01-interface.h
  • For sparse 0-1 vectors, see lela/vector/sparse01-interface.h
  • For hybrid 0-1 vectors, see lela/vector/hybrid01-interface.h

Extending the BLAS-implementations

LELA 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:

  • AllModules<GF2>
  • M4RIModules, which acts as a wrapper for M4RI
  • GenericModule, which provides the remaining functionality not provided by M4RI

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

  • a Module structure,
  • a specialisation of the implementation-class for each function being implemented in that module

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) {}
};