Copyright (C) 1992, Digital Equipment Corporation
All rights reserved.
See the file COPYRIGHT for a full description.
Last modified on Fri Feb 4 10:40:47 PST 2000 by gnelson
modified on Thu Apr 10 09:47:22 PDT 1997 by heydon
modified on Thu Nov 3 17:56:57 PST 1994 by isard
MODULE RedundantLSolve;
FROM JunoValue IMPORT Real, Sqrt;
IMPORT RowOp;
IMPORT IO;
IMPORT Wr, Fmt, Text;
FROM Thread IMPORT Alerted;
<* FATAL Wr.Failure, Alerted *>
VAR debug := 0;
debug >= 1 => show initial and scaled matrices
debug >= 2 => show row-echelon matrix
debug >= 3 => show column permutation, row-echelon matrix, original &
orthonormal basis, and minimal solution
VAR UseGramSchmidt := TRUE;
VAR UseCompletePivoting := TRUE;
PROCEDURE SetGramSchmidt(flag: BOOLEAN) =
BEGIN UseGramSchmidt := flag END SetGramSchmidt;
TYPE
T = Real;
CardArray = REF ARRAY OF CARDINAL;
CONST
InitRows = 50;
InitCols = 50;
VAR
Indent := 9;
Prec := 3;
FieldWidth := Prec + 8;
VAR
c1 := 0.66;
c2 := 0.5;
The constants c1 and c2 are used in the Veach Heuristic.
PROCEDURE LogT(t: TEXT) =
BEGIN IO.Put(t, logWr) END LogT;
PROCEDURE EtpLogSolveRow(<*UNUSED*> ops: INTEGER) =
BEGIN END EtpLogSolveRow;
PROCEDURE ShowMatrix(m, n: CARDINAL; READONLY a: Matrix; READONLY t: TEXT) =
BEGIN
Wr.PutText(logWr, " " & t & ":\n");
FOR i := 0 TO m - 1 DO
ShowVector(n, a[i]);
END;
Wr.Flush(logWr)
END ShowMatrix;
PROCEDURE ShowVector(n: CARDINAL; READONLY v: Vector) =
BEGIN
Wr.PutText(logWr, Fmt.Pad("", Indent));
FOR j := 0 TO n - 1 DO
Wr.PutText(logWr,
Fmt.Pad(Fmt.Real(v[j], Fmt.Style.Sci, prec := Prec), FieldWidth))
END;
Wr.PutChar(logWr, '\n');
END ShowVector;
VAR
p := NEW(REF Vector, InitCols); (* solution vector (not permuted) *)
temp := NEW(REF Vector, InitCols); (* temporary vector for "SwapRows" *)
basis := NEW(REF Matrix, InitCols, InitCols);
colPerm := NEW(CardArray, InitCols);
rowMax := NEW(CardArray, InitRows);
(* "colPerm[c]" (0 <= c < n) is the index in "x" of the unknown represented
by column "c" of matrix "a".
"rowMax[i]" (0 <= i < m) is the index of the column of row "i" of "A"
with largest absolute value (note that "A" does not include column "n" of
the matrix "a"). *)
PROCEDURE P(
m, n: CARDINAL;
VAR (*INOUT*) a: Matrix;
VAR (*OUT*) x: Vector) =
(* Matrix "A" is stored in "a[0..m-1, 0..n-1]".
Vector "b" is stored in "a[0..m-1, n]".
IMPLEMENTATION: The current implementation reduces "A" to a row-echelon
form in which all of the pivot elements are along "A"'s diagonal. At each
stage, it chooses the element with largest magnitude in the remaining
sub-matrix and swaps rows and columns as necessary to make that element
the next pivot. Once the matrix has been reduced to this row-echelon
form, it may be under-constrained. In that case, the implementation
chooses for the solution the point on the solution space with smallest L2
norm (i.e., the point closest to the origin in Euclidean space).
*)
VAR rc: INTEGER := 0;
(* We only consider pivots along the diagonal of "A", so the current row and
the current column are always the same value, called "rc". *)
PROCEDURE InitColPerm() =
(* Make sure "NUMBER(colPerm^) >= n", and initialize "colPerm[i] = i"
for "i <= 0 < n". *)
BEGIN
(* make sure "colPerm" is large enough *)
IF NUMBER(colPerm^) < n THEN
colPerm := NEW(CardArray, MAX(n, 2 * NUMBER(colPerm^)))
END;
(* initialize to identity permutation *)
FOR i := 0 TO n - 1 DO
colPerm[i] := i
END
END InitColPerm;
PROCEDURE InitRowMax() =
(* Initialize "rowMax" so it satisfies its invariant. Also,
scale the matrix so that the uniform norm of each row
is unity. *)
BEGIN
(* make sure "rowMax" is large enough *)
IF NUMBER(rowMax^) < m THEN
rowMax := NEW(CardArray, MAX(m, 2 * NUMBER(rowMax^)))
END;
(* initialize maximum index for each row *)
FOR i := 0 TO m - 1 DO
WITH currRow = a[i] DO
VAR maxVal: T := ABS(currRow[0]); maxCol := 0; BEGIN
FOR j := 1 TO n - 1 DO
VAR abs := ABS(currRow[j]); BEGIN
IF abs > maxVal THEN
maxVal := abs;
maxCol := j
END
END
END;
rowMax[i] := maxCol;
IF maxVal # 0.0 THEN
(* scale the row so that its uniform norm is unity. *)
FOR i := 0 TO n DO
currRow[i] := currRow[i] / maxVal
END
END
END
END
END
END InitRowMax;
PROCEDURE MaxEntry(r: CARDINAL): CARDINAL =
(* Return the index "x" of "rowMax" that maximizes the value
"ABS(a[x, rowMax[x]])" for "r <= x < m". Requires "r < m". *)
VAR maxVal: T := ABS(a[r, rowMax[r]]); maxR := r; BEGIN
FOR i := r + 1 TO m - 1 DO
VAR val := ABS(a[i, rowMax[i]]); BEGIN
IF val > maxVal THEN
maxR := i;
maxVal := val
END
END
END;
RETURN maxR
END MaxEntry;
PROCEDURE MaxEntry2(r: CARDINAL): CARDINAL =
(* Return the index "x" of "rowMax" that maximizes the value
"ABS(a[x,r)" for "x: r <= x < m". Requires "r < m". *)
VAR maxVal: T := ABS(a[r, r]); maxR := r; BEGIN
FOR i := r + 1 TO m - 1 DO
VAR val := ABS(a[i, r]); BEGIN
IF val > maxVal THEN
maxR := i;
maxVal := val
END
END
END;
RETURN maxR
END MaxEntry2;
PROCEDURE SwapCols(c1, c2: CARDINAL) =
(* Swap columns "c1" and "c2" of the matrix "a", adjusting "colPerm" and
"rowMax" to reflect the swap. *)
BEGIN
(* swap column values *)
FOR i := 0 TO m - 1 DO
WITH currRow = a[i], v1 = currRow[c1], v2 = currRow[c2] DO
VAR t := v1; BEGIN v1 := v2; v2 := t END
END;
WITH currMax = rowMax[i] DO
IF currMax = c1 THEN currMax := c2
ELSIF currMax = c2 THEN currMax := c1
END
END
END;
(* reflect swap in "colPerm" *)
WITH v1 = colPerm[c1], v2 = colPerm[c2] DO
VAR t := v1; BEGIN v1 := v2; v2 := t END
END
END SwapCols;
PROCEDURE SwapRows(r1, r2, c: CARDINAL) =
(* Swap rows "r1" and "r2" of the matrix "a" from column "c" through
column "n", adjusting "rowMax" to reflect the swap. *)
BEGIN
(* swap rows *)
WITH row1 = a[r1], row2 = a[r2], cnt = (n - c) + 1 DO
SUBARRAY(temp^, c, cnt) := SUBARRAY(row1, c, cnt);
SUBARRAY(row1, c, cnt) := SUBARRAY(row2, c, cnt);
SUBARRAY(row2, c, cnt) := SUBARRAY(temp^, c, cnt)
END;
(* swap "rowMax" entries *)
VAR t := rowMax[r1]; BEGIN
rowMax[r1] := rowMax[r2]; rowMax[r2] := t
END
END SwapRows;
PROCEDURE Pivot(r, c: CARDINAL) =
(* Pivots matrix "a" about location "(r, c)". This sets all entries in
column "c" of "a" below row "r" to "0.0" without changing the set of
solutions to the equations represented by "a". It also maintain the
invariant on "rowMax". *)
VAR pivot, factor: T; BEGIN
WITH pivotRow = a[r] DO
pivot := pivotRow[c];
FOR i := r + 1 TO m - 1 DO
WITH currRow = a[i] DO
(* adjust row "i" *)
IF currRow[c] # 0.0 THEN
factor := currRow[c] / pivot;
currRow[c] := 0.0;
EtpLogSolveRow(n-c);
VAR maxCol := c; maxAbs := 0.0; BEGIN
FOR j := c+1 TO n-1 DO
WITH currRowJ = currRow[j] DO
currRowJ := currRowJ - (factor * pivotRow[j]);
WITH abs = ABS(currRowJ) DO
IF abs > maxAbs THEN
maxAbs := abs;
maxCol := j
END
END
END
END;
rowMax[i] := maxCol
END;
currRow[n] := currRow[n] - (factor * pivotRow[n])
END
END
END
END
END Pivot;
PROCEDURE SolveRow(
r: CARDINAL;
c: INTEGER;
READONLY v: Vector;
homog: BOOLEAN): T =
(* Let "a" be matrix in row-echelon form. The entry "a[r, c]" is assumed to
be a pivot element of "a". "SolveRow" returns the value for "v[c]" using
the previously computed values of "v[c+1..n-1]". The value returned for
"v[c]" satisfies:
| (+ i: c <= i < n: a[r, i] * v[i]) = x
where "x = 0" if "homog" is "TRUE" and "x = a[r, n]" if "homog" is
"FALSE". *)
VAR sum: T; BEGIN
WITH rowR = a[r] DO
(* incorporate known values to the right of "c" *)
IF homog
THEN sum := 0.0
ELSE sum := rowR[n]
END;
FOR i := c + 1 TO n - 1 DO
sum := sum - (rowR[i] * v[i])
END;
RETURN sum / rowR[c]
END
END SolveRow;
PROCEDURE BackProp(rc: CARDINAL; VAR (*INOUT*) res: Vector; homog := FALSE) =
(* Sets the values of the solution vector entries "res[0..rc-1]" so as to
satisfy "A * res = b". Assumes that the values of "res[rc..n-1]" are
valid. If "homog" is "TRUE", then solves the system "A * res = 0". *)
BEGIN
FOR i := rc - 1 TO 0 BY -1 DO
res[i] := SolveRow(i, i, res, homog);
END
END BackProp;
PROCEDURE MakeBases(dim: CARDINAL) =
(* Forms "dim" dimensional basis vectors in "basis[0..dim-1]". *)
VAR ndim := n - dim; BEGIN
(* Zero last "dim" entries of each basis vector *)
FOR i := 0 TO dim - 1 DO
WITH currRow = basis[i] DO
FOR j := ndim TO n - 1 DO
currRow[j] := 0.0
END
END
END;
FOR i := 0 TO dim - 1 DO
basis[i, rc + i] := 1.0; (* form i'th basis vector *)
BackProp(rc, basis[i], TRUE);
END
END MakeBases;
PROCEDURE GramSchmidt(dim: CARDINAL; VAR (*INOUT*) b: Matrix) =
(* Convert the "dim" x "n" matrix of "dim" basis vectors "b" into an
orthonormal basis using the Gram-Schmidt algorithm. The basis vectors are
stored in "b[0..dim-1]"; they are assumed to span a space of dimension
"dim". *)
BEGIN
FOR i := 0 TO dim - 1 DO
WITH rowI = b[i] DO
(* orthogonalize "b[i]"; "b[0..i-1]" are orthonormal *)
FOR j := 0 TO i - 1 DO
WITH rowJ = b[j] DO
VAR dot := Dot(rowI, rowJ); BEGIN
FOR k := 0 TO n - 1 DO
rowI[k] := rowI[k] - (dot * rowJ[k])
END
END
END
END;
(* normalize "b[i]" *)
VAR len := L2Norm(rowI); BEGIN
<* ASSERT len > 0.0 *>
FOR k := 0 TO n - 1 DO
rowI[k] := rowI[k] / len
END
END
END
END
END GramSchmidt;
PROCEDURE L2Norm(READONLY v: Vector): T =
VAR sum := Dot(v, v); BEGIN
RETURN Sqrt(sum)
END L2Norm;
PROCEDURE Dot(READONLY v1, v2: Vector): T =
VAR res := 0.0; BEGIN
FOR i := 0 TO n - 1 DO
res := res + (v1[i] * v2[i])
END;
RETURN res
END Dot;
PROCEDURE VeachHeuristic(): CARDINAL =
(* Use Eric Veach's heuristic to compute and return how many rows to use.
On entry, "c1" is the fraction of the L1 norm of the residual
vector that must be accounted for, and "c2" is the additional
shrinkage that will be allowed in the pivot elements of rows
that will be added on for free (relative to the pivot element
of the row that is required in order to get "c1" of the L1 norm
of the residual. The procedure returns the number of rows to
ignore. *)
VAR
residnorm := 0.0;
goal: REAL;
numToUse: CARDINAL;
pivtotal := 0.0;
CONST
Epsilon = 1.0E-6;
BEGIN
FOR j := 0 TO m - 1 DO residnorm := residnorm + ABS(a[j, n]) END;
IF residnorm = 0.0 THEN RETURN 0 END;
goal := residnorm * c1;
(* use at least enough rows to reduce the residual norm by "goal". *)
residnorm := 0.0;
numToUse := 0;
WHILE residnorm < goal AND numToUse < MIN(n,m) AND ABS(a[numToUse,numToUse]) > Epsilon DO
residnorm := residnorm + ABS(a[numToUse, n]);
pivtotal := pivtotal + ABS(a[numToUse, numToUse]);
INC(numToUse)
END;
(* Now the sum of the residuals in the first "numToUse" rows is
at least "goal"; or else "numToUse=m". Use any additional rows
whose pivots are not too small; that is, not smaller than "c2" times
the average magnitude of one of the pivots in use: *)
IF numToUse > 0 THEN
VAR limit := c2 * (pivtotal / FLOAT(numToUse)); BEGIN
WHILE numToUse < MIN(m,n) AND ABS(a[numToUse, numToUse]) >= limit DO
INC(numToUse)
END
END
END;
RETURN numToUse;
END VeachHeuristic;
(* PROCEDURE P *)
BEGIN
(* Check for buggy call *)
IF NUMBER(a) < m OR (m > 0 AND NUMBER(a[0]) < n + 1) OR NUMBER(x) < n THEN
<* ASSERT FALSE *>
END;
(* make sure "temp" is large enough for "SwapRows" *)
IF NUMBER(temp^) < n + 1 THEN
temp := NEW(REF Vector, MAX(n + 1, 2 * NUMBER(temp^)))
END;
IF debug >= 1 THEN ShowMatrix(m, n + 1, a, "Matrix before scaling") END;
(* initialize bookkeeping arrays *)
InitRowMax();
IF debug >= 1 THEN ShowMatrix(m, n+1, a, "Matrix after scaling") END;
InitColPerm();
(* Put Matrix A in Row-Echelon form *)
VAR
maxRow, maxCol: CARDINAL;
CONST
Epsilon = 1.0E-6;
(* Pivots must have magnitude at least "Epsilon". *)
BEGIN
WHILE rc < MIN(m, n) DO
IF UseCompletePivoting THEN
maxRow := MaxEntry(rc);
maxCol := rowMax[maxRow]
ELSE
maxRow := MaxEntry2(rc);
maxCol := rc
END;
IF ABS(a[maxRow, maxCol]) > Epsilon THEN
IF rc # maxCol THEN SwapCols(rc, maxCol) END;
IF rc # maxRow THEN SwapRows(rc, maxRow, rc) END;
Pivot(rc, rc);
END;
INC(rc)
END
END;
IF debug >= 2 THEN
ShowMatrix(m, n+1, a, "After pivoting")
END;
IF debug >= 3 THEN
Wr.PutText(logWr, " Column Permutation:\n");
Wr.PutText(logWr, Fmt.Pad("", Indent));
FOR i := 0 TO n - 1 DO
VAR c := Text.FromChar(VAL(colPerm[i] + ORD('a'), CHAR)); BEGIN
Wr.PutText(logWr, Fmt.Pad(c, FieldWidth))
END
END;
Wr.PutChar(logWr, '\n');
ShowMatrix(m, n + 1, a, "Row-Echelon Matrix");
END;
rc := VeachHeuristic();
IF debug >= 2 THEN
LogT("number of rows to use: ");
IO.PutInt(rc, logWr);
LogT("\n")
END;
IF rc = 0 THEN
FOR i := 0 TO n - 1 DO x[i] := 0.0 END
END;
(* Back-propagate solution values *)
IF n > NUMBER(p^) THEN
p := NEW(REF Vector, MAX(n, 2 * NUMBER(p^)))
END;
FOR i := rc TO n - 1 DO p[i] := 0.0 END;
BackProp(rc, p^);
(* Check if system is under-constrained *)
IF rc < n AND UseGramSchmidt THEN
(* Use Gram-Schmidt to find the solution with smallest L2 norm. *)
IF n > NUMBER(basis^) THEN
VAR new_size := MAX(n, 2 * NUMBER(basis^)); BEGIN
basis := NEW(REF Matrix, new_size, new_size)
END
END;
VAR dim := n - rc; BEGIN
(* compute the basis vectors *)
MakeBases(dim);
IF debug >= 3 THEN
Wr.PutText(logWr, " Particular Solution:\n");
ShowVector(n, p^);
ShowMatrix(dim, n, basis^, "Original Basis")
END;
(* make the basis orthonormal *)
GramSchmidt(dim, basis^);
IF debug >= 3 THEN
ShowMatrix(dim, n, basis^, "Orthonormal Basis")
END;
(* compute the minimal solution in "p" *)
FOR i := 0 TO dim - 1 DO
WITH basisI = basis[i] DO
VAR alpha := Dot(p^, basisI); BEGIN
FOR k := 0 TO n - 1 DO
p[k] := p[k] - (alpha * basisI[k])
END
END
END
END;
IF debug >= 3 THEN
Wr.PutText(logWr, " Minimal Solution:\n");
ShowVector(n, p^)
END
END
END;
(* Permute solution "p" into result "x" *)
FOR i := 0 TO n - 1 DO
x[colPerm[i]] := p[i]
END
END P;
BEGIN END RedundantLSolve.