Top Up Prev Next Bottom Contents Index Search

4.4 The Matrix Data Types


The primary support for matrix types in Ptolemy is the PtMatrix class. PtMatrix is derived from the Message class, and uses the various kernel support functions for working with the Message data type as described in Section 4.3 on page 4-14. This section discusses the PtMatrix class and how to write stars and programs using this class.

4.4.1 Design philosophy

The PtMatrix class implements two dimensional arrays. There are four key classes derived from PtMatrix: ComplexMatrix, FixMatrix, FloatMatrix, and IntMatrix. (Note that FloatMatrix is a matrix of C++ doubles.) A review of matrix classes implemented by other programmers revealed two main styles of implementation: a vector of vectors, or a simple array. In addition, there are two main formats of storing the entries: column-major ordering, where all the entries in the first column are stored before the entries of the second column, and row-major ordering, where the entries are stored starting with the first row. Column-major ordering is how Fortran stores arrays whereas row-major ordering is how C stores arrays.

The Ptolemy PtMatrix class stores data as a simple C array, and therefore uses row-major ordering. Row-major ordering also seems more natural for operations such as image and video processing, but it might make it more difficult to interface Ptolemy's PtMatrix class with Fortran library calls. The limits of interfacing Ptolemy's PtMatrix class with other software is discussed in Section 4.4.5 on page 4-33.

The design decision to store data entries in a C array rather than as an array of vector objects has a greater effect on performance than the decision whether to use row major or column major ordering. There are a couple of advantages to implementing a matrix class as an array of vector class objects: referencing an entry may be faster, and it is easier to do operations on a whole row or column of the matrix, depending on whether the format is an array of column vectors or an array of row vectors. An entry lookup in an array of row vectors requires two index lookups: one to find the desired row vector in the array and one to find the desired entry of that row. A linear array, in contrast, requires a multiplication to find the location of first element of the desired row and then an index lookup to find the column in that row. For example, A[row][col] is equivalent to looking up &data + (row*numRows + col) if the entries are stored in a C array data[], whereas it is *(&rowArray + row) + col if looking up the entry in an array of vectors format.

Although the array of vectors format has faster lookups, it is also more expensive to create and delete the matrix. Each vector of the array must be created in the matrix constructor, and each vector must also be deleted by the matrix destructor. The array of vectors format also requires more memory to store the data and the extra array of vectors.

With the advantages and disadvantages of the two systems in mind, we chose to implement the PtMatrix class with the data stored in a standard C array. Ptolemy's environment is such that matrices are created and deleted constantly as needed by stars: this negates much of the speedup gained from faster lookups. Also, we felt it was important to keep the design of the class simple and the memory usage efficient because of Ptolemy's increasing size and complexity.

4.4.2 The PtMatrix class

The PtMatrix base class is derived from the Message class so that we can use Ptolemy's Envelope class and message-handling system. However, the MessageParticle class is not used by the PtMatrix class; instead, there are special MatrixEnvParticle classes defined to handle type checking between the various types of matrices. This allows the system to automatically detect when two stars with different matrix type inputs and outputs are incorrectly connected together.1 Also, the MatrixEnvParticle class has some special functions not found in the standard MessageParticle class to allow easier handling of PtMatrix class messages. A discussion of how to pass PtMatrix class objects using the MatrixEnvParticles can be found in a following section.

As explained previously, there are currently four data-specific matrix classes: ComplexMatrix, FixMatrix, FloatMatrix, and IntMatrix. Each of these classes stores its entries in a standard C array named data, which is an array of data objects corresponding to the PtMatrix type: Complex, Fix, double, or int. These four matrix classes implement a common set of operators and functions; in addition, the ComplexMatrix class has a few special methods such as conjugate() and hermitian() and the FixMatrix class has a number of special constructors that allow the user to specify the precision of the entries in the matrix. Generally, all entries of a FixMatrix will have the same precision.

The matrix classes were designed to take full advantage of operator overloading in C++ so that operations on matrix objects can be written much like operations on scalar ones. For example, the two-operand multiply operator * has been defined so that if A and B are matrices, A * B will return a third matrix that is the matrix product of A and B.

4.4.3 Public functions and operators for the PtMatrix class

The functions and operators listed below are implemented by all matrix classes (ComplexMatrix, FixMatrix, FloatMatrix, and IntMatrix) unless otherwise noted. The symbols used are:

Functions and Operators to access entries of the Matrix:


xxx& entry(int i)

Example: A.entry(i)
Return the ith entry of the matrix when its data storage is considered to be a linear array. This is useful for quick operations on every entry of the matrix without regard for the specific (row,column) position of that entry. The total number of entries in the matrix is defined to be numRows() * numCols(), with indices ranging from 0 to numRows() * numCols() - 1. This function returns a reference to the actual entry in the matrix so that assignments can be made to that entry. In general, functions that wish to linearly reference each entry of a matrix A should use this function instead of the expression A.data[i] because classes which are derived from PtMatrix can then overload the entry() method and reuse the same functions.


xxx* operator [] (int row)

Example: A[row][column]
Return a pointer to the start of the row in the matrix's data storage. (This operation is different to matrix classes defined as arrays of vectors, in which the [] operator returns the vector representing the desired row.) This operator is generally not used alone but with the [] operator defined on C arrays, so that A[i][j] will give you the entry of the matrix in the ith row and jth column of the data storage. The range of rows is from 0 to numRows()-1 and the range of columns is from 0 to numCols()-1.

Constructors:


XXXMatrix()

Example: IntMatrix A;
Create an uninitialized matrix. The number of rows and columns are set to zero and no memory is allocated for the storage of data.


XXXMatrix(int numRow, int numCol)

Example: FloatMatrix A(3,2);
Create a matrix with dimensions numRow by numCol. Memory is allocated for the data storage but the entries are uninitialized.


XXXMatrix(int numRow, int numCol, PortHole& p)

Example: ComplexMatrix(3,3,myPortHole)
Create a matrix of the given dimensions and initialize the entries by assigning to them values taken from the porthole myPortHole. The entries are assigned in a rasterized sequence so that the value of the first particle removed from the porthole is assigned to entry (0,0), the second particle's value to entry (0,1), etc. It is assumed that the porthole has enough particles in its buffer to fill all the entries of the new matrix.


XXXMatrix(int numRow, int numCol, XXXArrayState& dataArray)

Example: IntMatrix A(2,2,myIntArrayState);
Create a matrix with the given dimensions and initialize the entries to the values in the given ArrayState. The values of the ArrayState fill the matrix in rasterized sequence so that entry (0,0) of the matrix is the first entry of the ArrayState, entry (0,1) of the matrix is the second, etc. An error is generated if the ArrayState does not have enough values to initialize the whole matrix.


XXXMatrix(const XXXMatrix& src)

Example: FixMatrix A(B);
This is the copy constructor. A new matrix is formed with the same dimensions as the source matrix and the data values are copied from the source.


XXXMatrix(const XXXMatrix& src, int startRow, int startCol, int numRow, int numCol)

Example: IntMatrix A(B,2,2,3,3);
This special "submatrix" constructor creates a new matrix whose values come from a submatrix of the source. The arguments startRow and startCols specify the starting row and column of the source matrix. The values numRow and numCol specify the dimensions of the new matrix. The sum startRow + numRow must not be greater than the maximum number of rows in the source matrix; similarly, startCol + numCol must not be greater than the maximum number of columns in the source. For example, if B is a matrix with dimension (4,4), then A(B,1,1,2,2) would create a new matrix A that is a (2,2) matrix with data values from the center quadrant of matrix B, so that A[0][0] == B[1][1], A[0][1] == B[1][2], A[1][0] == B[2][1], and A[1][1] == B[2][2].

The following are special constructors for the FixMatrix class that allow the programmer to specify the precision of the entries of the FixMatrix.


FixMatrix(int numRow, int numCol, int length, int intBits)

Example: FixMatrix A(2,2,14,4);
Create a FixMatrix with the given dimensions such that each entry is a fixed-point number with precision as given by the length and intBits inputs.


FixMatrix(int numRow, int numCol, int length, int intBits, PortHole& myPortHole)

Example: FixMatrix A(2,2,14,4);
Create a FixMatrix with the given dimensions such that each entry is a fixed-point number with precision as given by the length and intBits inputs and initialized with the values that are read from the particles contained in the porthole myPortHole.


FixMatrix(int numRow, int numCol, int length, int intBits, FixArrayState& dataArray)

Example: FixMatrix A(2,2,14,4);
Create a FixMatrix with the given dimensions such that each entry is a fixed-point number with precision as given by the length and intBits inputs and initialized with the values in the given FixArrayState.

There are also special copy constructors for the FixMatrix class that allow the programmer to specify the precision of the entries of the FixMatrix as they are copied from the sources. These copy constructors are usually used for easy conversion between the other matrix types. The last argument specifies the type of masking function (truncate, rounding, etc.) to be used when doing the conversion.


FixMatrix(const XXXMatrix& src, int length, int intBits,
int round)

Example: FixMatrix A(CxMatrix,4,14,TRUE);
Create a FixMatrix with the given dimensions such that each entry is a fixed-point number with precision as given by the length and intBits arguments. Each entry of the new matrix is copied from the corresponding entry of the src matrix and converted as specified by the round argument.

Comparison operators:


int operator == (const XXXMatrix& src)

Example: if(A == B) then ...
Return TRUE if the two matrices have the same dimensions and every entry in A is equal to the corresponding entry in B. Return FALSE otherwise.


int operator != (const XXXMatrix& src)

Example: if(A != B) then ...
Return TRUE if the two matrices have different dimensions or if any entry of A differs from the corresponding entry in B. Return FALSE otherwise.

Conversion operators:

Each matrix class has a conversion operator so that the programmer can explicitly cast one type of matrix to another (this casting is done by copying). It would have been possible to make conversions occur automatically when needed, but because these conversions can be quite expensive for large matrices, and because unexpected results might occur if the user did not intend for a conversion to occur, we chose to require that these conversions be used explicitly.


operator XXXMatrix () const

Example: FloatMatrix C = A * (FloatMatrix)B;
Convert a matrix of one type into another. These conversions allow the various arithmetic operators, such as * and +, to be used on matrices of different type. For example, if A in the example above is a (3,3) FloatMatrix and B is a (3,2) IntMatrix, then C is a FloatMatrix with dimensions (3,2).

Destructive replacement operators:

These operators are member functions that modify the current value of the object. In the following examples, A is usually the lvalue (*this). All operators return *this:


XXXMatrix& operator = (const XXXMatrix& src)

Example: A = B;
This is the assignment operator: make A into a matrix that is a copy of B. If A already has allocated data storage, then the size of this data storage is compared to the size of B. If they are equal, then the dimensions of A are simply set to those of B and the entries copied. If they are not equal, the data storage is freed and reallocated before copying.


XXXMatrix& operator = (xxx value)

Example: A = value;
Assign each entry of A to have the given value. Memory management is handled as in the previous operator.
Note: this operator is targeted for deletion. Do not use it.


XXXMatrix& operator += (const XXXMatrix& src)

Example: A += B;
Perform the operation A.entry(i) += B.entry(i) for each entry in A. A and B must have the same dimensions.


XXXMatrix& operator += (xxx value)

Example: A += value;
Add the scalar value to each entry in the matrix.


XXXMatrix& operator -= (const XXXMatrix& src)

Example: A -= B;
Perform the operation A.entry(i) -= B.entry(i) for each entry in A. A and B must have the same dimensions.


XXXMatrix& operator -= (xxx value)

Example: A -= value;
Subtract the scalar value from each entry in the matrix.


XXXMatrix& operator *= (const XXXMatrix& src)

Example: A *= B;
Perform the operation A.entry(i) *= B.entry(i) for each entry in A. A and B must have the same dimensions. Note: this is an elementwise operation and is not equivalent to A = A * B.


XXXMatrix& operator *= (xxx value)

Example: A *= value;
Multiply each entry of the matrix by the scalar value.


XXXMatrix& operator /= (const XXXMatrix& src)

Example: A /= B;
Perform the operation A.entry(i) /= B.entry(i) for each entry in A. A and B must have the same dimensions.


XXXMatrix& operator /= (xxx value)

Example: A /= value
Divide each entry of the matrix by the scalar value. The scalar value must be non-zero.


XXXMatrix& operator identity()

Example: A.identity();
Change A to be an identity matrix so that each entry on the diagonal is 1 and all off-diagonal entries are 0.

Non-destructive operators (these return a new matrix):


XXXMatrix operator - ()

Example: B = -A;
Return a new matrix such that each element is the negative of the element of the source.


XXXMatrix operator ~ ()

Example: B = ~A;
Return a new matrix that is the transpose of the source.


XXXMatrix operator ! ()

Example: B = !A;
Return a new matrix which is the inverse of the source.


XXXMatrix operator ^ (int exponent)

Example: B = A^2;
Return a new matrix which is the source matrix to the given exponent power. The exponent can be negative, in which case the exponent is first treated as a positive number and the final result is then inverted. So A^2 == A*A and A^(-3) == !(A*A*A).


XXXMatrix transpose()

Example: B = A.transpose();
This is the same as the ~ operator but called by a function name instead of as an operator.


XXXMatrix inverse()

Example: B = A.inverse();
This is the same as the ! operator but called by a function name instead of as an operator.


ComplexMatrix conjugate()

Example: ComplexMatrix B = A.conjugate();
Return a new matrix such that each element is the complex conjugate of the source. This function is defined for the ComplexMatrix class only.


ComplexMatrix hermitian()

Example: ComplexMatrix B = A.hermitian();
Return a new matrix which is the Hermitian Transpose (conjugate transpose) of the source. This function is defined for the ComplexMatrix class only.

Non-member binary operators:


XXXMatrix operator + (const XXXMatrix& left, const XXXMatrix& right)

Example: A = B + C;
Return a new matrix which is the sum of the first two. The left and right source matrices must have the same dimensions.


XXXMatrix operator + (const xxx& scalar, const XXXMatrix& matrix)

Example: A = 5 + B;
Return a new matrix that has entries of the source matrix added to a scalar value.


XXXMatrix operator + (const XXXMatrix& matrix, const xxx& scalar)

Example: A = B + 5;
Return a new matrix that has entries of the source matrix added to a
scalar value. (This is the same as the previous operator but with the scalar on the right.)


XXXMatrix operator - (const XXXMatrix& left, const XXXMatrix& right)

Example: A = B - C;
Return a new matrix which is the difference of the first two. The left and right source matrices must have the same dimensions.


XXXMatrix operator - (const xxx& scalar, const XXXMatrix& matrix)

Example: A = 5 - B;
Return a new matrix that has the negative of the entries of the source matrix added to a scalar value.


XXXMatrix operator - (const XXXMatrix& matrix, const xxx& scalar)

Example: A = B - 5;
Return a new matrix such that each entry is the corresponding entry of the source matrix minus the scalar value.


XXXMatrix operator * (const XXXMatrix& left, const XXXMatrix& right)

Example: A = B * C;
Return a new matrix which is the matrix product of the first two. The left and right source matrices must have compatible dimensions (i.e. A.numCols() == B.numRows().


XXXMatrix operator * (const xxx& scalar, const XXXMatrix& matrix)

Example: A = 5 * B;
Return a new matrix that has entries of the source matrix multiplied by a scalar value.


XXXMatrix operator * (const XXXMatrix& matrix, const xxx& scalar)

Example: A = B * 5;
Return a new matrix that has entries of the source matrix multiplied
by a scalar value. (This is the same as the previous operator but with the scalar on the right.)

Miscellaneous functions:


int numRows()

Return the number of rows in the matrix.


int numCols()

Return the number of columns in the matrix.


Message* clone()

Example: IntMatrix *B = A.clone();
Return a copy of *this.


StringList print()

Example: A.print()
Return a formatted StringList that can be printed to display the contents of the matrix in a reasonable format.


XXXMatrix& multiply (const XXXMatrix& left, const XXXMatrix& right, XXXMatrix& result)

Example: multiply(A,B,C);
This is a faster 3 operand form of matrix multiply such that the result matrix is passed as an argument so that we avoid the extra copy step that is involved when we write C = A * B.


const char* dataType()

Example: A.dataType()
Return a string that specifies the name of the type of matrix. The strings are "ComplexMatrix", "FixMatrix", "FloatMatrix", and "IntMatrix".


int isA(const char* type)

Example: if(A.isA("FixMatrix")) then ...
Return TRUE if the argument string matches the type string of the matrix.

4.4.4 Writing stars and programs using the PtMatrix class

This section describes how to use the matrix data classes when writing stars. Some examples will be given here but the programmer should refer to the stars in $PTOLEMY/src/domains/sdf/matrix/stars/*.pl and $PTOLEMY/src/domains/sdf/image/stars/*.pl for more examples

Memory management

The most important thing to understand about the use of matrix data classes in the Ptolemy environment is that stars that intend to output the matrix in a particle should allocate memory for the matrix but never delete that matrix. Memory reclamation is done automatically by the reference-counting mechanism of the Message class. Strange errors will occur if the star deletes the matrix before it is used by another star later in the execution sequence.

Naming conventions

Stars that implement general-purpose matrix operations usually have names with the _M suffix to distinguish them from stars that operate on scalar particles. For example, the SDFGain_M star multiplies an input matrix by a scalar value and outputs the resulting matrix. This is in contrast to SDFGain, which multiplies an input value held in a FloatParticle by a double and puts that result in an output FloatParticle.

Include files

For a star to use the PtMatrix classes, it must include the file Matrix.h in either its .h or .cc file. If the star has a matrix data member, then the declaration

hinclude { "Matrix.h" } needs to be in the Star definition. Otherwise, the declaration

ccinclude { "Matrix.h" } is sufficient.

To declare an input porthole that accepts matrices, the following syntax is used:

input {
name { inputPortHole }
type { FLOAT_MATRIX_ENV }
}
The syntax is the same for output portholes. The type field can be COMPLEX_MATRIX_ENV, FLOAT_MATRIX_ENV, FIX_MATRIX_ENV, or INT_MATRIX_ENV. The icons created by Ptolemy will have terminals that are thicker and that have larger arrow points than the terminals for scalar particle types. The colors of the terminals follow the pattern of colors for scalar data types (e.g., blue represents Float and FloatMatrix).

Input portholes

The syntax to extract a matrix from the input porthole is:

Envelope inPkt;
(inputPortHole%0).getMessage(inPkt);
const FloatMatrix& inputMatrix =
*(const FloatMatrix *)inPkt.myData();
The first line declares an Envelope, which is used to access the matrix. Details of the Envelope class are given in "Use of the Envelope class" on page 4-17. The second line fills the envelope with the input matrix. Note that, because of the reference-counting mechanism, this line does not make a copy of the matrix. The last two lines extract a reference to the matrix from the envelope. It is up to the programmer to make sure that the cast agrees with the definition of the input port.

Because multiple envelopes might reference the same matrix, a star is generally not permitted to modify the matrix held by the Envelope. Thus, the function myData() returns a const Message *. We cast that to be a const FloatMatrix * and then de-reference it and assign the value to inputMatrix. It is generally better to handle matrices by reference instead of by pointer because it is clearer to write "A + B" rather than "*A + *B" when working with matrix operations. Stars that wish to modify an input matrix should access it using the writableCopy method, as explained in "Use of the Envelope class" on page 4-17.

Allowing delays on inputs

The cast to (const FloatMatrix *) above is not always safe. Even if the source star is known to provide matrices of the appropriate type, a delay on the arc connecting the two stars can cause problems. In particular, delays in dataflow domains are implemented as initial particles on the arcs. These initial particles are given the value "zero" as defined by the type of particle. For Message particles, a "zero" is an uninitialized Message particle containing a "dummy" data value. This dummy Message will be returned by the myData method in the third line of the above code fragment. The dummy message is not a FloatMatrix, rendering the above cast invalid. A star that expects matrix inputs must have code to handle empty particles. An example is:


if(inPkt.empty()) {
FloatMatrix& result = *(new FloatMatrix(int(numRows),
int(numCols)));
result = 0.0;
output%0 << result;
}
There are many ways that an empty input can be interpreted by a star that operates on matrices. For example, a star multiplying two matrices can simply output a zero matrix if either input is empty. A star adding two matrices can output whichever input is not empty. Note above that we create an output matrix that has the dimensions as set by the state parameters of the star so that any star that uses this output will have valid data.

A possible alternative to outputting a zero matrix is to simply pass that empty MessageParticle along. This approach, however, can lead to counterintuitive results. Suppose that empty message reaches a display star like TkText, which will attempt to call the print() method of the object. An empty message has a print() method that results in a message like

<type>: no print method

This is likely to prove extremely confusing to users, so we strongly recommend that each matrix star handle the empty input in a reasonable way, and produce a non-empty output.

Matrix outputs

To put a matrix into an output porthole, the syntax is:

FloatMatrix& outMatrix =*(new FloatMatrix(someRow,someCol));
// ... do some operations on the outMatrix
outputPortHole%0 << outMatrix;
The last line is similar to outputting a scalar value. This is because we have overloaded the << operator for MatrixEnvParticles to support PtMatrix class inputs. The standard use of the MessageParticle class requires you to put your message into an envelope first and then use << on the envelope (see "Use of the Envelope class" on page 4-17), but we have specialized this so that the extra operation of creating an envelope first is not explicit.

Here is an example of a complete star definition that inputs and outputs matrices:


defstar {
name { Mpy_M }
domain { SDF }
desc {
Does a matrix multiplication of two input Float matrices A and B to
produce matrix C.
Matrix A has dimensions (numRows,X).
Matrix B has dimensions (X,numCols).
Matrix C has dimensions (numRows,numCols).
The user need only specify numRows and numCols. An error will be
generated automatically if the number of columns in A does not match
the number of columns in B.
}
input {
name { Ainput }
type { FLOAT_MATRIX_ENV }
}
input {
name { Binput }
type { FLOAT_MATRIX_ENV }
}
output {
name { output }
type { FLOAT_MATRIX_ENV }
}
defstate {
name { numRows }
type { int }
default { 2 }
desc { The number of rows in Matrix A and Matrix C.}
}
defstate {
name { numCols }
type { int }
default { 2 }
desc { The number of columns in Matrix B and Matrix C}
}
ccinclude { "Matrix.h" }
go {
// get inputs
Envelope Apkt;
(Ainput%0).getMessage(Apkt);
const FloatMatrix& Amatrix =
*(const FloatMatrix *)Apkt.myData();

Envelope Bpkt;
(Binput%0).getMessage(Bpkt);
const FloatMatrix& Bmatrix =
*(const FloatMatrix *)Bpkt.myData();

// check for "null" matrix inputs, which could be
// caused by delays on the input line
if(Apkt.empty() || Bpkt.empty()) {
// if either input is empty, return a zero
// matrix with the state dimensions
FloatMatrix& result =
*(new FloatMatrix(int(numRows),
int(numCols)));
result = 0.0;
output%0 << result;
}
else {
// Amatrix and Bmatrix are both valid
if((Amatrix.numRows() != int(numRows)) ||
(Bmatrix.numCols() != int(numCols))) {
Error::abortRun(*this,
"Dimension size of FloatMatrix inputs do ",
"not match the given state parameters.");
return;
}
// do matrix multiplication
FloatMatrix& result =
*(new FloatMatrix(int(numRows),
int(numCols)));
// we could write
// result = Amatrix * Bmatrix;
// but the following is faster
multiply(Amatrix,Bmatrix,result);

output%0 << result;
}
}
}

4.4.5 Future extensions

After reviewing the libraries of numerical analysis software that is freely available on the Internet, it is clear that it would be beneficial to extend the PtMatrix class by adding those well-tested libraries as callable functions. Unfortunately, many of those libraries are currently only available in Fortran, and there are some incompatibilities with Fortran's column major ordering and C's row major ordering. Those problems would still exist even if the Fortran code was converted to C. There are a few groups which are currently working on C++ ports of the numerical analysis libraries. One notable group is the Lapack++2 project which is developing a flexible matrix class of their own, besides porting the Fortran algorithms of Lapack into C++. This might possibly be incorporated in a future release.



Top Up Prev Next Bottom Contents Index Search

1 We recommend, however, that you do not adapt this method to your own types, but use the standard method of adding new message types described in Section 4.3. The method currently used for the matrix classes may not be supported in future releases.

2 LAPACK++: A Design Overview of Object-Oriented Extensions for High Performance Linear Algebra, by Jack J. Dongarra, Roldan Pozo, and David W. Walker, available on netlib.

Copyright © 1990-1997, University of California. All rights reserved.