6.1 Mixing MTBDDs and Arrays
6.1.1 A First Algorithm
Our approach will be to emulate the matrix-vector multiplication algorithm for sparse matrices given in Figure 3.13. This multiplies a matrix A, stored in arrays row , col and val , by a vector, stored in an array b, and places the result in an array res. Crucially, we observe that the algorithm uses each element of A exactly once during this process.
In a sparse matrix setting, these elements can be obtained by simply reading along the three arrays, row , col and val . In this case, the matrix entries are obtained in order, row by row, but this is not actually required for matrix-vector multiplication. The same result can be achieved by initially setting every element of res to zero, and then performing the operation res[r] := res[r] + v × b[c] for all entries “(r, c) = v” of A in any order.
by an MTBDD, can be extracted via a depth-first traversal of the data structure, i.e. an exhaustive exploration of all paths through the MTBDD which lead to a non-zero terminal. The entries will not be produced in any meaningful order, but this represents the quickest way of extracting them all in a single pass.
Figure 6.1 shows the algorithm TraverseMtbdd, which performs this depth-first traversal. It does so by recursively splitting the problem into the traversal of four smaller
MTBDDs. This corresponds to the decomposition of the matrix represented by the
MTBDD into four submatrices, as previously illustrated in Figure 3.19.
The recursion bottoms out either when the algorithm comes across a submatrix con- taining no non-zero entries, represented by the zero terminal, or when it locates a ma-
trix entry, i.e. when it reaches a non-zero terminal. In the latter case, the function
UseMatrixEntry(r, c, v) is called, where r, c and v correspond to the row index, column index and value of the matrix entry found. For matrix-vector multiplication, UseMatrixEntry(r, c, v) would perform the operation res[r] := res[r] + v × b[c]. We use this generic function to indicate that the traversal algorithm could just as easily be used for any operation which requires an explicit list of all matrix entries.
In a call to the algorithm TraverseMtbdd(m, i, r, c), m is the current MTBDD node, i is the current level of recursion and r and c are used to keep track of row and column indices. For a matrix represented by an MTBDD M, the algorithm is called at the top level as TraverseMtbdd(M, 1, 0, 0). Recall from Section 3.7 that we consider an MTBDD and the node which represents it (its root node) to be interchangeable.
The current level of recursion, i, has to be tracked explicitly in order to deal with skipped levels in the MTBDD. This corresponds to our observation in Section 3.7 that a single path through an MTBDD can correspond to several minterms and hence to several matrix entries. We assume that the matrix being represented by M uses row variables x = (x1, . . . , xn) and column variables y = (y1, . . . , yn). Skipped levels can be detected by
comparing the positions of the variable for the current node, var (m), and that of either
xi or yi in the MTBDD variable ordering.
The key part of the algorithm is the calculation of the row and column indices. In
an MTBDD, each index is associated with its Boolean encoding, i.e. an element of IBn.
An explicit representation such as an array is indexed by integers. We need a way to convert one to the other. We will assume that the rows and columns of the matrix are
indexed from 0 to 2n− 1 and are encoded using the standard binary representation for
integers. By noting the path that we have taken through the MTBDD and by summing the appropriate powers of two when we take a then edge, we can compute the indices as required. This is exactly what TraverseMtbdd does. Note that the computation of row and column indices is performed independently, using variables r and c, respectively.
TraverseMtbdd(m, i, r, c) 1. if (m = Const(0)) then 2. return 3. else if (i = n + 1) then 4. UseMatrixEntry(r, c, val (m)) 5. return 6. endif 7. if (var (m) > xi) then 8. e := t := m 9. else 10. e := else(m) 11. t := then(m) 12. endif
13. if (var (e) > yi) then 14. ee := et := e 15. else 16. ee := else(e) 17. et := then(e) 18. endif 19. if (var (t) > yi) then 20. te := tt := t 21. else 22. te := else(t) 23. tt := then(t) 24. endif 25. TraverseMtbdd(ee, i + 1, r, c) 26. TraverseMtbdd(et, i + 1, r, c + 2n−i) 27. TraverseMtbdd(te, i + 1, r + 2n−i, c) 28. TraverseMtbdd(tt, i + 1, r + 2n−i, c + 2n−i)
When a then edge is followed from a node at the ith level of recursion, 2n−i is added to
the appropriate variable, r or c.
Alternatively, we can consider TraverseMtbdd to function as follows. Each call to TraverseMtbdd(m, i, r, c) extracts the submatrix represented by node m and computes the corresponding local indices of its elements, relative to that submatrix. The actual row and column indices are calculated by adding the offsets r and c, respectively, to the local indices. Since, at this level of recursion, the algorithm splits a square matrix of size 2n−i+1
into four submatrices of size 2n−i, the offsets, r and c, for the next level are computed by
adding 2n−i where appropriate.
A simple example should demonstrate this process more clearly. Figure 6.2 shows a 4 × 4 matrix M and the MTBDD M which represents it. Note that M was derived from a model with an unreachable state and the corresponding row and column have been filled with zeros. For clarity, we mark these as ‘−’s, not ‘0’s. This issue is irrelevant now, but will be important when we reuse the example later.
Figure 6.3 gives a table, explaining how the TraverseMtbdd algorithm works on this example. Each row of the table corresponds to a single matrix entry. These are listed in the order in which TraverseMtbdd would have extracted them. The first five columns of the table describe the path taken through the MTBDD, i.e. the value of each MTBDD variable and of the terminal reached. The next four columns give the components of the sums to compute the row and column indices. The final column shows the resulting matrix entry.
The table also illustrates why the methods we present here are only applicable to
iterative methods which can be implemented with matrix-vector multiplication. The
order in which the matrix entries are extracted is determined by the interleaving of the row and column variables in the MTBDD. The entire top-left quadrant of the matrix will be extracted before any entries of the top-right quadrant are. Hence, we generally do not obtain a whole row at a time. Notice, for example, in Figure 6.3, that entry (1, 1) is extracted before entry (0, 3). Iterative methods which rely on utilising rows or columns one at a time, such as Gauss-Seidel, could not be implemented in this way.
One way to resolve this would be to opt for a non-interleaved MTBDD variable or- dering such as x1 < · · · < xn < y1 < · · · < yn but, as we saw in Section 4.1.2, this is not
feasible because the size of the MTBDD becomes unmanageable. Alternatively, we could extract every row or column individually, performing one traversal of the MTBDD for each. It seems likely, though, that this would result in a considerable slow-down.
There remains one crucial problem with the approach outlined in this section. Recall from Chapter 4 that our MTBDD encoding of a model typically results in the inclusion of unreachable states. Hence, the matrix represented by the MTBDD will have empty
M = 0 3 − 6 0 3 − 4 − − − − 5 0 − 4 3 6 4 5 x1 y1 x2 y2 M
Figure 6.2: A matrix M and the MTBDD M which represents it
Path Indices Entry of M x1 y1 x2 y2 fM x1 y1 x2 y2 0 0 0 1 3 - - - 1 (0, 1) = 3 0 0 1 1 3 - - 1 1 (1, 1) = 3 0 1 0 1 6 - 2 - 1 (0, 3) = 6 0 1 1 1 4 - 2 1 1 (1, 3) = 4 1 0 1 0 5 2 - 1 - (3, 0) = 5 1 1 1 1 4 2 2 1 1 (3, 3) = 4
rows and columns corresponding to these states. The number of such states is potentially large and, for some of our case studies, is orders of magnitude larger than the number of reachable states. The traversal algorithm presented above takes no account of this, and so effectively we are dealing with a much larger matrix than necessary.
Normally, we are happy to do this since it results in a compact MTBDD representation. The difficulty here, though, is that vectors, and hence the arrays storing them, need to be of the same size. Since these are stored explicitly, this puts unacceptable, practical limits on the size of problems with which we can deal. Note that this is not a problem with sparse matrices, because we assume that the sparse matrix (and hence the solution vector) is only over reachable states.