Matrix.frink

Download or view Matrix.frink in plain text format


/** This is an incomplete class that implements matrix operations.  Please
    feel free to implement missing functions!  */

class Matrix
{
   /** The array that will actually store the matrix data.  Note that this
       is a raw 2-dimensional Frink array, and the indices are zero-based in
       contrast with most matrix notation which is one-based.  The class should
       try to enforce that this is a rectangular 2-dimensional array, but the
       code doesn't currently enforce that with some constructors. */

   var array

   /** The number of rows in the matrix. */
   var rows

   /** The number of columns in the matrix. */
   var cols

   /** Construct a new Matrix with the specified number of rows and columns
       and the specified default value for each element. */

   new[rowCount, colCount, default=0] :=
   {
      rows = rowCount
      cols = colCount
      array = makeArray[[rows, cols], default]
   }

   /** Construct a new Matrix from a rectangular 2-dimensional array.   This
       currently does not verify that the array is rectangular. */

   new[a is array] :=
   {
      rows = length[a]
      cols = length[a@0]
      array = a
   }

   /** Sets the value of the specified element, using 1-based coordinates, as
       is common in matrix notation.   Stupid mathematicians.  */

   set[row, col, val] := array@(row-1)@(col-1) = val

   
   /** Gets the value of the specified element, using 1-based coordinates, as
       is common in matrix notation. */

   get[row, col] := array@(row-1)@(col-1)

   /** Gets the number of columns. */
   getCols[] := cols

   /** Gets the number of rows. */
   getRows[] := rows

   
   /** Multiply two matrices.  The number of columns in matrix a should equal
       the number of rows in matrix b.  The resulting matrix will have the
       same number of rows as matrix a and the same number of columns as
       matrix b.

       TODO:  Use a faster algorithm for large arrays?  This is O(n^3).

       TODO:  Optimize this for small arrays by precalculating.
   */

   class multiply[a, b] :=
   {
      if a.cols != b.rows
         return undef
      else
         items = a.cols

      resRows = a.rows
      resCols = b.cols
      resultArray = makeArray[[resRows, resCols], 0]

      aa = a.array
      bb = b.array
      
      for row = 0 to resRows-1
         for col = 0 to resCols-1
         {
            sum = aa@row@0 * bb@0@col  // Make units come out right
            for k = 1 to items-1
               sum = sum + aa@row@k * bb@k@col

            resultArray@row@col = sum
         }

      return new Matrix[resultArray]
   }

   
   /** Multiply this matrix by the specified matrix and return the result. */
   multiply[b] := multiply[this, b]

   
   /** Multiplies all elements of a matrix by a scalar. */
   multiplyByScalar[s] :=
   {
      a = makeArray[[rows, cols]]
      
      for rowNum = 0 to rows-1
      {
         row = array@rowNum
         for colNum = 0 to cols-1
            a@rowNum@colNum = row@colNum * s
      }

      return new Matrix[a]
   }

   /** Multiplies a row in place by the specified value. (indices are 1-based.)
   */

   multiplyRowByScalar[row, val] :=
   {
      r = row-1
      array@r = mul[array@r, val]
   }
   
   /** Transposes the Matrix and returns the new transposed Matrix.  This
      uses the built-in array.transpose[] method. 
   */

   transpose[] := new Matrix[array.transpose[]]

   
   /** Returns true if this Matrix is square, false otherwise. */
   isSquare[] :=  rows == cols

   /** Exchange the specified-numbered rows in the matrix.  The rows are
       1-indexed to match standard matrix notation. */

   exchangeRows[row1, row2] :=
   {
      r1 = row1-1
      r2 = row2-1
      [array@r1, array@r2] = [array@r2, array@r1]
   }

   /** Exchange the specified-numbered columns in the matrix.  The columns are
       1-indexed to match standard matrix notation. */

   exchangeCols[c1, c2] :=
   {
      for r = 0 to rows-1
      {
         temp = array@r@(c2-1)
         array@r@(c2-1) = array@r@(c1-1)
         array@r@(c1-1) = temp
      }
   }

   /** Returns a matrix as a string with rows separated with newlines. */
   toString[] :=
   {
      result = ""
      for r = 0 to rows-1
         result = result + " " + array@r + "\n"

      return result
   }

   /** Returns the matrix formatted by formatTable with rows separated with
   newlines. */

   toTable[] :=
   {
      return formatTable[array]
   }

   /** Formats a matrix with Unicode box drawing characters.  The result is a
       single string. */

   formatMatrix[] :=
   {
      return formatMatrix[array]
   }
      
   /** Formats a matrix with Unicode box drawing characters.  The result is
       fairly compact with blank lines separating rows and 2 spaces seperating
       columns.  The result is a single string. */

   formatMatrixCompact[] :=
   {
      return formatMatrixCompact[array]
   }
      
   /** Formats a matrix with Unicode box drawing characters.  The result is
       as compact as possible with no blank lines separating rows and 1 space
       separating columns.  The result is a single string. */

   formatMatrixVeryCompact[] :=
   {
      return formatMatrixVeryCompact[array]
   }
      
   /** Returns the matrix formatted as a matrix with Unicode box drawing
       characters.  The result is an array of lines that can be further
       formatted.
   */

   formatMatrixLines[] :=
   {
      m = formatTableLines[array]
      rows = length[m]
      width = length[m@0]
      result = new array
      result.push["\u250c" + repeat[" ", width] + "\u2510"]
      for n=0 to rows-1
         result.push["\u2502" + m@n + "\u2502"]
      
      result.push["\u2514" + repeat[" ", width] + "\u2518"]
      
      return result
   }

   /** Gets the specified (1-based) column as a row array. */
   getColumnAsArray[col] :=
   {
      return deepCopy[array.getColumn[col-1]]
   }

   /** Gets the specified (1-based) column as a 1-column matrix */
   getColumnAsMatrix[col] :=
   {
      return new Matrix[deepCopy[array.getColumn[col-1].transpose[]]]
   }

   /** Gets the specified (1-based) row as a row array. */
   getRowAsArray[row] :=
   {
      return deepCopy[array@(row-1)]
   }
   
   /** Creates the LU (lower-upper) decomposition of the matrix.
       This creates two matrices, L and U, where L has the value 1 on the
       diagonal from top left to bottom right, like:

            1    0    0 
       L = L21   1    0
           L31  L32   1


           U11  U12  U13
       U =  0   U22  U23
            0    0   U33

   This is basically like solving equations by Gaussian elimination. */

   LUDecompose[] :=
   {
      return LUDecomposeCrout[]
   }

   
   /** This uses Crout's method to decompose a square matrix into an lower and
       upper triangular matrix.

       See:
       https://en.wikipedia.org/wiki/Crout_matrix_decomposition
   
       This creates two matrices, L and U, where U has the value 1 on the
       diagonal from top left to bottom right, like:

           L11   0    0 
       L = L21  L22   0
           L31  L32  L33

            1   U12 U13
       U =  0    1  U23
            0    0   1
   
       returns [L, U]
       The original matrix can be obtained as L.multiply[U]
   
       Note that the Crout algorithm fails on certain matrices, for example
       m = new Matrix[[[1,1,1,-1],[1,1,-1,1],[1,-1,1,1],[-1,1,1,1]]]

       See:
       https://semath.info/src/inverse-cofactor-ex4.html
       for more on that matrix.
       https://en.wikipedia.org/wiki/Crout_matrix_decomposition
   */

   LUDecomposeCrout[] :=
   {
      n = rows
      L = new array[[rows, cols], 0]
      U = new array[[rows, cols], 0]
      sum = 0
      
      for i=0 to n-1
         U@i@i = 1

      for j=0 to n-1
      {
         for i=j to n-1
         {
            sum = 0
            for k=0 to j-1
               sum = sum + L@i@k * U@k@j

            L@i@j = array@i@j - sum
         }

         for i=j to n-1
         {
            sum = 0
            for k=0 to j-1
               sum = sum + L@j@k * U@k@i

            if L@j@j == 0
            {
               println["Matrix.LUDecomposeCrout:  det(L) is zero.  Can't divide by zero."]
               return undef
            }
            
            U@j@i = (array@j@i - sum) / L@j@j
         }
      }
      return [new Matrix[L], new Matrix[U]]
   }

   
   /** Another algorithm for Crout LU decomposition.  See:

       http://www.mosismath.com/Matrix_LU/Martix_LU.html

       This returns [L, U] as matrices which may be transposes of the matrices
       returned by LUDecomposeCrout[] .  See its notes for more about this
       decomposition and its properties.
   */

   LUDecomposeCrout2[] :=
   {
      if ! isSquare[]
      {
         println["Matrix.LUDecomposeCrout2 only works on square arrays."]
         return undef
      }

      n = rows
      L = new array[[n,n], 0]
      U = new array[[n,n], 0]
      a = 0

      for i = 0 to n-1
         L@i@i = 1
    
      for j = 0 to n-1
      {
         for i = 0 to n-1
         {
            if i >= j
            {
               U@j@i = array@j@i
               for k = 0 to j-1
               {
                  U@j@i = U@j@i - U@k@i * L@j@k
               }
            }
            
            if i > j
            {
               L@i@j = array@i@j
               for k = 0 to j-1
                  L@i@j = L@i@j - U@k@j * L@i@k

               diag = U@j@j
               if diag == 0
               {
                  println["Matrix.LUDecomposeCrout2:  det(L) is zero.  Can't divide by zero."]
                  println[formatMatrix[L]]
                  println[formatMatrix[U]]
                  return undef
               }

               L@i@j = L@i@j / diag
            }
         }
      }

      return [new Matrix[L], new Matrix[U]]
   }

   /** This uses the Cholesky-Banachiewicz algorithm to decompose a square
       Hermitian matrix into a lower triangular matrix.  If you transpose the
       lower triangular matrix L by L.transpose[], you get an upper triangular
       matrix which is symmetrical to the lower triangular matrix.
       Multiplying the lower triangular matrix by the upper triangular matrix,
       that is,

       L.multiply[L.transpose[]]
   
       gives you back the original matrix!

       See:  https://en.m.wikipedia.org/wiki/Cholesky_decomposition
   */

   CholeskyB[] :=
   {
      if ! isHermitian[]
      {
         println["Matrix.CholeskyB only works on square Hermitian matrices."]
         return undef
      }

      n = rows
      L = new array[[n,n], 0]

      for i = 0 to n-1
         for j = 0 to i
         {
            sum = 0
            for k = 0 to j-1
               sum = sum + L@i@k * L@j@k

            if i == j
               L@i@j = sqrt[array@i@i - sum]
            else
               L@i@j = (1 / L@j@j * (array@i@j -sum))
         }

      return new Matrix[L]
   }
   
   /** This uses the Cholesky-Crout algorithm to decompose a square Hermitian
       matrix into a lower triangular matrix.  If you transpose the lower
       triangular matrix L by L.transpose[], you get an upper triangular
       matrix which is symmetrical to the lower triangular matrix.
       Multiplying the lower triangular matrix by the upper triangular matrix,
       that is,

       L.multiply[L.transpose[]]
   
       gives you back the original matrix!

       See:  https://en.m.wikipedia.org/wiki/Cholesky_decomposition
   */

   CholeskyCrout[] :=
   {
      if ! isHermitian[]
      {
         println["Matrix.CholeskyCrout only works on square Hermitian matrices."]
         return undef
      }

      n = rows
      L = new array[[n,n], 0]

      for j = 0 to n-1
      {
         sum = 0
         for k = 0 to j-1
            sum = sum + (L@j@k)^2

         L@j@j = sqrt[array@j@j - sum]

         for i = j+1 to n-1
         {
            sum = 0
            for k = 0 to j-1
               sum = sum + L@i@k * L@j@k

            L@i@j = (1 / L@j@j * (array@i@j -sum))
         }
      }

      return new Matrix[L]
   }
   
   /** This uses Doolittle's method to decompose a square matrix into an lower
       and upper triangular matrix.
   
       This creates two matrices, L and U, where L has the value 1 on the
       diagonal from top left to bottom right, like:

            1    0    0 
       L = L21   1    0
           L31  L32   1

           U11  U12 U13
       U =  0   U22 U23
            0    0  U33

       The original matrix can be obtained as L.multiply[U]

       Note that the Doolittle algorithm fails on certain matrices, for example
       m = new Matrix[[[1,1,1,-1],[1,1,-1,1],[1,-1,1,1],[-1,1,1,1]]]

       See:
       https://semath.info/src/inverse-cofactor-ex4.html
       for more on that matrix.

       See:
       https://www.geeksforgeeks.org/doolittle-algorithm-lu-decomposition/
   */

   LUDecomposeDoolittle[] :=
   {
      if ! isSquare[]
      {
         println["Matrix.LUDecomposeDoolittle only works on square arrays."]
         return undef
      }
      
      n = rows
      L = new array[[n,n], 0]
      U = new array[[n,n], 0]
      
      // Decomposing matrix into Upper and Lower
      // triangular matrix
      for i = 0 to n-1
      {
         // Upper Triangular
         for k = i to n-1
         {
            // Summation of L(i, j) * U(j, k)
            sum = 0
            for j = 0 to i-1
               sum = sum + (L@i@j * U@j@k)
            
            // Evaluating U(i, k)
            U@i@k = array@i@k - sum
         }
         
         // Lower Triangular
         for k = i to n-1
         {
            if i == k
               L@i@i = 1 // Diagonal as 1
            else
            {
               // Summation of L(k, j) * U(j, i)
               sum = 0
               for j = 0 to i-1
                  sum = sum + L@k@j * U@j@i

               diag = U@i@i
               if (diag == 0)
               {
                  println["Matrix.LUDecomposeDoolittle:  U@$i@$i is zero when solving for L@$k@$i.  Can't divide by zero."]
                  println[formatMatrix[L]]
                  println[formatMatrix[U]]
                  return undef
               }
               // Evaluating L(k, i)
               L@k@i = (array@k@i - sum) / diag
            }
         }
      }

      return [new Matrix[L], new Matrix[U]]
   }

   
   /** Returns the determinant of a square matrix. */
   det[] :=
   {
      if ! isSquare[]
      {
         println["Determinants are only defined for square matrices!  Size was $rows x $cols"]
         return undef
      }

      // It's much faster to use precalculated determinant formulas for small
      // matrices than, to, say LUDecompose the things.
      if rows == 1
         return array@0@0
      
      if rows == 2
         return array@0@0 array@1@1 - array@0@1 array@1@0  // ad - bc

      if rows == 3
         return -array@0@2 array@1@1 array@2@0 +
                 array@0@1 array@1@2 array@2@0 +
                 array@0@2 array@1@0 array@2@1 - 
                 array@0@0 array@1@2 array@2@1 -
                 array@0@1 array@1@0 array@2@2 +
                 array@0@0 array@1@1 array@2@2

      if rows == 4
         return array@0@1 array@1@3 array@2@2 array@3@0 -
                array@0@1 array@1@2 array@2@3 array@3@0 -
                array@0@0 array@1@3 array@2@2 array@3@1 +
                array@0@0 array@1@2 array@2@3 array@3@1 -
                array@0@1 array@1@3 array@2@0 array@3@2 +
                array@0@0 array@1@3 array@2@1 array@3@2 + 
                array@0@1 array@1@0 array@2@3 array@3@2 -
                array@0@0 array@1@1 array@2@3 array@3@2 + 
                array@0@3 (array@1@2 array@2@1 array@3@0 -
                           array@1@1 array@2@2 array@3@0 -
                           array@1@2 array@2@0 array@3@1 +
                           array@1@0 array@2@2 array@3@1 +
                           array@1@1 array@2@0 array@3@2 -
                           array@1@0 array@2@1 array@3@2) +
               (array@0@1 array@1@2 array@2@0 -
                array@0@0 array@1@2 array@2@1 -
                array@0@1 array@1@0 array@2@2 +
                array@0@0 array@1@1 array@2@2) array@3@3 +
                array@0@2 (-array@1@3 array@2@1 array@3@0 +
                           array@1@1 array@2@3 array@3@0 +
                           array@1@3 array@2@0 array@3@1 -
                           array@1@0 array@2@3 array@3@1 -
                           array@1@1 array@2@0 array@3@3 +
                           array@1@0 array@2@1 array@3@3)

      if rows == 5
         return array@0@2 array@1@4 array@2@3 array@3@1 array@4@0 -
                array@0@2 array@1@3 array@2@4 array@3@1 array@4@0 - 
                array@0@1 array@1@4 array@2@3 array@3@2 array@4@0 +
                array@0@1 array@1@3 array@2@4 array@3@2 array@4@0 - 
                array@0@2 array@1@4 array@2@1 array@3@3 array@4@0 +
                array@0@1 array@1@4 array@2@2 array@3@3 array@4@0 + 
      array@0@2 array@1@1 array@2@4 array@3@3 array@4@0 -
      array@0@1 array@1@2 array@2@4 array@3@3 array@4@0 + 
      array@0@2 array@1@3 array@2@1 array@3@4 array@4@0 -
      array@0@1 array@1@3 array@2@2 array@3@4 array@4@0 - 
      array@0@2 array@1@1 array@2@3 array@3@4 array@4@0 +
      array@0@1 array@1@2 array@2@3 array@3@4 array@4@0 - 
      array@0@2 array@1@4 array@2@3 array@3@0 array@4@1 +
      array@0@2 array@1@3 array@2@4 array@3@0 array@4@1 + 
      array@0@0 array@1@4 array@2@3 array@3@2 array@4@1 -
      array@0@0 array@1@3 array@2@4 array@3@2 array@4@1 + 
      array@0@2 array@1@4 array@2@0 array@3@3 array@4@1 -
      array@0@0 array@1@4 array@2@2 array@3@3 array@4@1 - 
      array@0@2 array@1@0 array@2@4 array@3@3 array@4@1 +
      array@0@0 array@1@2 array@2@4 array@3@3 array@4@1 - 
      array@0@2 array@1@3 array@2@0 array@3@4 array@4@1 +
      array@0@0 array@1@3 array@2@2 array@3@4 array@4@1 + 
      array@0@2 array@1@0 array@2@3 array@3@4 array@4@1 -
      array@0@0 array@1@2 array@2@3 array@3@4 array@4@1 + 
      array@0@1 array@1@4 array@2@3 array@3@0 array@4@2 -
      array@0@1 array@1@3 array@2@4 array@3@0 array@4@2 - 
      array@0@0 array@1@4 array@2@3 array@3@1 array@4@2 +
      array@0@0 array@1@3 array@2@4 array@3@1 array@4@2 - 
      array@0@1 array@1@4 array@2@0 array@3@3 array@4@2 +
      array@0@0 array@1@4 array@2@1 array@3@3 array@4@2 + 
      array@0@1 array@1@0 array@2@4 array@3@3 array@4@2 -
      array@0@0 array@1@1 array@2@4 array@3@3 array@4@2 + 
      array@0@1 array@1@3 array@2@0 array@3@4 array@4@2 -
      array@0@0 array@1@3 array@2@1 array@3@4 array@4@2 - 
      array@0@1 array@1@0 array@2@3 array@3@4 array@4@2 +
      array@0@0 array@1@1 array@2@3 array@3@4 array@4@2 + 
      array@0@2 array@1@4 array@2@1 array@3@0 array@4@3 -
      array@0@1 array@1@4 array@2@2 array@3@0 array@4@3 - 
      array@0@2 array@1@1 array@2@4 array@3@0 array@4@3 +
      array@0@1 array@1@2 array@2@4 array@3@0 array@4@3 - 
      array@0@2 array@1@4 array@2@0 array@3@1 array@4@3 +
      array@0@0 array@1@4 array@2@2 array@3@1 array@4@3 + 
      array@0@2 array@1@0 array@2@4 array@3@1 array@4@3 -
      array@0@0 array@1@2 array@2@4 array@3@1 array@4@3 + 
      array@0@1 array@1@4 array@2@0 array@3@2 array@4@3 -
      array@0@0 array@1@4 array@2@1 array@3@2 array@4@3 - 
      array@0@1 array@1@0 array@2@4 array@3@2 array@4@3 +
      array@0@0 array@1@1 array@2@4 array@3@2 array@4@3 + 
      array@0@2 array@1@1 array@2@0 array@3@4 array@4@3 -
      array@0@1 array@1@2 array@2@0 array@3@4 array@4@3 - 
      array@0@2 array@1@0 array@2@1 array@3@4 array@4@3 +
      array@0@0 array@1@2 array@2@1 array@3@4 array@4@3 + 
      array@0@1 array@1@0 array@2@2 array@3@4 array@4@3 -
      array@0@0 array@1@1 array@2@2 array@3@4 array@4@3 + 
      array@0@4 (array@1@1 array@2@3 array@3@2 array@4@0 -
      array@1@1 array@2@2 array@3@3 array@4@0 - 
      array@1@0 array@2@3 array@3@2 array@4@1 +
      array@1@0 array@2@2 array@3@3 array@4@1 - 
      array@1@1 array@2@3 array@3@0 array@4@2 +
      array@1@0 array@2@3 array@3@1 array@4@2 + 
      array@1@1 array@2@0 array@3@3 array@4@2 -
      array@1@0 array@2@1 array@3@3 array@4@2 + 
      array@1@3 (array@2@2 array@3@1 array@4@0 -
      array@2@1 array@3@2 array@4@0 -
      array@2@2 array@3@0 array@4@1 +
      array@2@0 array@3@2 array@4@1 +
      array@2@1 array@3@0 array@4@2 - 
      array@2@0 array@3@1 array@4@2) +
      (array@1@1 array@2@2 array@3@0 -
      array@1@0 array@2@2 array@3@1 - 
      array@1@1 array@2@0 array@3@2 +
      array@1@0 array@2@1 array@3@2) array@4@3 + 
      array@1@2 (-array@2@3 array@3@1 array@4@0 +
      array@2@1 array@3@3 array@4@0 + 
      array@2@3 array@3@0 array@4@1 -
      array@2@0 array@3@3 array@4@1 -
      array@2@1 array@3@0 array@4@3 + 
      array@2@0 array@3@1 array@4@3)) +
      (array@0@2 (-array@1@3 array@2@1 array@3@0 + 
      array@1@1 array@2@3 array@3@0 +
      array@1@3 array@2@0 array@3@1 -
      array@1@0 array@2@3 array@3@1 - 
      array@1@1 array@2@0 array@3@3 +
      array@1@0 array@2@1 array@3@3) + 
      array@0@1 (array@1@3 array@2@2 array@3@0 -
      array@1@2 array@2@3 array@3@0 -
      array@1@3 array@2@0 array@3@2 +
      array@1@0 array@2@3 array@3@2 +
      array@1@2 array@2@0 array@3@3 -
      array@1@0 array@2@2 array@3@3) + 
      array@0@0 (-array@1@3 array@2@2 array@3@1 +
      array@1@2 array@2@3 array@3@1 + 
      array@1@3 array@2@1 array@3@2 -
      array@1@1 array@2@3 array@3@2 -
      array@1@2 array@2@1 array@3@3 + 
       array@1@1 array@2@2 array@3@3)) array@4@4 + 
      array@0@3 (-array@1@1 array@2@4 array@3@2 array@4@0 +
      array@1@1 array@2@2 array@3@4 array@4@0 + 
      array@1@0 array@2@4 array@3@2 array@4@1 -
      array@1@0 array@2@2 array@3@4 array@4@1 + 
      array@1@1 array@2@4 array@3@0 array@4@2 -
      array@1@0 array@2@4 array@3@1 array@4@2 - 
      array@1@1 array@2@0 array@3@4 array@4@2 +
      array@1@0 array@2@1 array@3@4 array@4@2 + 
      array@1@4 (-array@2@2 array@3@1 array@4@0 +
      array@2@1 array@3@2 array@4@0 + 
      array@2@2 array@3@0 array@4@1 -
      array@2@0 array@3@2 array@4@1 -
      array@2@1 array@3@0 array@4@2 + 
      array@2@0 array@3@1 array@4@2) -
      array@1@1 array@2@2 array@3@0 array@4@4 + 
      array@1@0 array@2@2 array@3@1 array@4@4 +
      array@1@1 array@2@0 array@3@2 array@4@4 - 
      array@1@0 array@2@1 array@3@2 array@4@4 + 
      array@1@2 (array@2@4 array@3@1 array@4@0 -
      array@2@1 array@3@4 array@4@0 -
      array@2@4 array@3@0 array@4@1 +
      array@2@0 array@3@4 array@4@1 +
      array@2@1 array@3@0 array@4@4 -
      array@2@0 array@3@1 array@4@4))

      // If we fell through to here, we have a larger matrix and have to try
      // to find its determinant through more inefficent methods.

      // TODO: Calculate determinant in terms of other determinants instead of
      // using LUDecompose when LUDecompose doesn't work.
      
      product = 1
      [L, U] = LUDecomposeDoolittle[]

      // This will happen if the matrix is singular.
      if U == undef
         return undef

      // Multiply diagonals of the lower triangular matrix.  The
      // upper triangular matrix has all ones on the diagonal.
      // Should there be a permutation matrix here to get the signs
      // right?
      for i=0 to rows-1
         product = product * (U.array)@i@i

      return product
   }

   /** Create a square identity matrix with the specified number of rows and
       columns.  The elements on the diagonal will be set to 1, the rest to
       zero.  This requires Frink 2020-04-19 or later. */

   class makeIdentity[dimension] :=
   {
      return new Matrix[makeArray[[dimension, dimension], {|a,b| a==b ? 1 : 0}]]
   }

   /** Makes a diagonal matrix.  This is passed in an array of elements (e.g.
       [1,2,3] that will make up the diagonal elements.  This requires Frink
       2020-04-22 or later. 
   */

   class makeDiagonal[arr] :=
   {
      d = length[arr]
      return new Matrix[makeArray[[d,d], {|a,b,data| a==b ? data@a : 0}, arr]]
   }

   /** Makes a new one-column Matrix from an array. */
   class makeColumn[arr] :=
   {
      return new Matrix[arr.transpose[]]
   }

   /** Returns a matrix with the specified row and column removed.   Row and
       column numbers are 1-indexed.   This is used in many matrix operations
       including calculation of determinant or of the adjugate/adjoint matrix.
   */

   removeRowColumn[rowToRemove, colToRemove] :=
   {
      a  = makeArray[[rows-1, cols-1]]

      newRow = 0
      ROW:
      for origRow = 0 to rows-1
      {
         if origRow == rowToRemove-1
            next ROW

         newCol = 0
         
         COL:
         for origCol = 0 to rows-1
         {
            if origCol == colToRemove-1
               next COL

            a@newRow@newCol = array@origRow@origCol
            newCol = newCol + 1
         }
            
         newRow = newRow + 1  
      }

      return new Matrix[a]
   }

   /** Returns the adjugate or adjoint matrix.  See:
       https://semath.info/src/inverse-cofactor-ex4.html

       TODO:  Calculate hardcoded adjugate matrices for small matrices because
       this is expensive
   */

   adjugate[] :=
   {
      a = makeArray[[rows,cols]]
      for i = 0 to rows-1
         for j = 0 to cols-1
            a@i@j = (-1)^((i+1)+(j+1)) *  removeRowColumn[j+1, i+1].det[]

      return new Matrix[a]       
   }


   /** Returns the inverse of a matrix.    See:
       https://semath.info/src/inverse-cofactor-ex4.html

       TODO:  Calculate hardcoded inverse matrices for small matrices because
       this is expensive
   */

   inverse[] :=
   {
      if ! isSquare[]
      {
         println["Matrix.inverse only works on square arrays."]
         return undef
      }

      // It's much faster to calculate inverse matrices with hard-coded
      // equations for small matrices.
      if rows == 1
         return 1 / array@0@0

      if rows == 2
      {
         invdet = 1/det[]

         // [  d  -b ]
         // [ -c   a ] / det
         return (new Matrix[[[array@1@1, -array@0@1],
                             [-array@1@0, array@0@0]]]).multiplyByScalar[invdet]   
      }

      if rows == 3
      {
         invdet = 1/det[]

         return (new Matrix[[[-array@1@2 array@2@1 + array@1@1 array@2@2, 
                               array@0@2 array@2@1 - array@0@1 array@2@2,
                              -array@0@2 array@1@1 + array@0@1 array@1@2],
                             [ array@1@2 array@2@0 - array@1@0 array@2@2,
                              -array@0@2 array@2@0 + array@0@0 array@2@2,  
                               array@0@2 array@1@0 - array@0@0 array@1@2],
                             [-array@1@1 array@2@0 + array@1@0 array@2@1,
                               array@0@1 array@2@0 - array@0@0 array@2@1,
                              -array@0@1 array@1@0 + array@0@0 array@1@1]]]).multiplyByScalar[invdet]   
      }

      if rows == 4
      {
         invdet = 1/det[]

         return (new Matrix[[[-array@1@3 array@2@2 array@3@1 +
         array@1@2 array@2@3 array@3@1 +
         array@1@3 array@2@1 array@3@2 - 
         array@1@1 array@2@3 array@3@2 - 
         array@1@2 array@2@1 array@3@3 +
         array@1@1 array@2@2 array@3@3, 

         array@0@3 array@2@2 array@3@1 - 
         array@0@2 array@2@3 array@3@1 - 
         array@0@3 array@2@1 array@3@2 +
         array@0@1 array@2@3 array@3@2 +
         array@0@2 array@2@1 array@3@3 - 
         array@0@1 array@2@2 array@3@3,

         -array@0@3 array@1@2 array@3@1 +
         array@0@2 array@1@3 array@3@1 +
         array@0@3 array@1@1 array@3@2 - 
         array@0@1 array@1@3 array@3@2 - 
         array@0@2 array@1@1 array@3@3 +
         array@0@1 array@1@2 array@3@3, 

         array@0@3 array@1@2 array@2@1 - 
         array@0@2 array@1@3 array@2@1 - 
         array@0@3 array@1@1 array@2@2 +
         array@0@1 array@1@3 array@2@2 +
         array@0@2 array@1@1 array@2@3 - 
         array@0@1 array@1@2 array@2@3],

         [array@1@3 array@2@2 array@3@0 - 
         array@1@2 array@2@3 array@3@0 - 
         array@1@3 array@2@0 array@3@2 +
         array@1@0 array@2@3 array@3@2 +
         array@1@2 array@2@0 array@3@3 - 

         array@1@0 array@2@2 array@3@3,

         -array@0@3 array@2@2 array@3@0 +
         array@0@2 array@2@3 array@3@0 +
         array@0@3 array@2@0 array@3@2 - 
         array@0@0 array@2@3 array@3@2 - 
         array@0@2 array@2@0 array@3@3 +
         array@0@0 array@2@2 array@3@3,
         
         array@0@3 array@1@2 array@3@0 - 
         array@0@2 array@1@3 array@3@0 - 
         array@0@3 array@1@0 array@3@2 +
         array@0@0 array@1@3 array@3@2 +
         array@0@2 array@1@0 array@3@3 - 
         array@0@0 array@1@2 array@3@3,

         -array@0@3 array@1@2 array@2@0 +
         array@0@2 array@1@3 array@2@0 +
         array@0@3 array@1@0 array@2@2 - 
         array@0@0 array@1@3 array@2@2 - 
         array@0@2 array@1@0 array@2@3 +
         array@0@0 array@1@2 array@2@3],

         [-array@1@3 array@2@1 array@3@0 +
         array@1@1 array@2@3 array@3@0 +
         array@1@3 array@2@0 array@3@1 - 
         array@1@0 array@2@3 array@3@1 - 
         array@1@1 array@2@0 array@3@3 +
         array@1@0 array@2@1 array@3@3,
         
         array@0@3 array@2@1 array@3@0 - 
         array@0@1 array@2@3 array@3@0 - 
         array@0@3 array@2@0 array@3@1 +
         array@0@0 array@2@3 array@3@1 +
         array@0@1 array@2@0 array@3@3 - 
         array@0@0 array@2@1 array@3@3,

         -array@0@3 array@1@1 array@3@0 +
         array@0@1 array@1@3 array@3@0 +
         array@0@3 array@1@0 array@3@1 - 
         array@0@0 array@1@3 array@3@1 - 
         array@0@1 array@1@0 array@3@3 +
         array@0@0 array@1@1 array@3@3,
         
         array@0@3 array@1@1 array@2@0 - 
         array@0@1 array@1@3 array@2@0 - 
         array@0@3 array@1@0 array@2@1 +
         array@0@0 array@1@3 array@2@1 +
         array@0@1 array@1@0 array@2@3 - 
         array@0@0 array@1@1 array@2@3],

         [array@1@2 array@2@1 array@3@0 - 
         array@1@1 array@2@2 array@3@0 - 
         array@1@2 array@2@0 array@3@1 +
         array@1@0 array@2@2 array@3@1 +
         array@1@1 array@2@0 array@3@2 - 
         array@1@0 array@2@1 array@3@2,

         -array@0@2 array@2@1 array@3@0 +
         array@0@1 array@2@2 array@3@0 +
         array@0@2 array@2@0 array@3@1 - 
         array@0@0 array@2@2 array@3@1 - 
         array@0@1 array@2@0 array@3@2 +
         array@0@0 array@2@1 array@3@2,
         
         array@0@2 array@1@1 array@3@0 - 
         array@0@1 array@1@2 array@3@0 - 
         array@0@2 array@1@0 array@3@1 +
         array@0@0 array@1@2 array@3@1 +
         array@0@1 array@1@0 array@3@2 - 
         array@0@0 array@1@1 array@3@2,

         -array@0@2 array@1@1 array@2@0 +
         array@0@1 array@1@2 array@2@0 +
         array@0@2 array@1@0 array@2@1 - 
         array@0@0 array@1@2 array@2@1 - 
         array@0@1 array@1@0 array@2@2 +
         array@0@0 array@1@1 array@2@2]]]).multiplyByScalar[invdet]
      }

      // Otherwise use the adjugate function.
      return adjugate[].multiplyByScalar[1/det[]]
   }

   /** Returns the Kronecker product of a and b.

       https://en.wikipedia.org/wiki/Kronecker_product
   */

   class KroneckerProduct[a is Matrix, b is Matrix] :=
   {
      m = a.rows
      n = a.cols
      p = b.rows
      q = b.cols
      newRows = m p
      newCols = n q
      n = new array[[newRows, newCols], 0]
      for i=0 to newRows-1
         for j=0 to newCols-1
            n@i@j = (a.array)@(i div p)@(j div q) * (b.array)@(i mod p)@(j mod q)

      return new Matrix[n]       
   }

   
   /** Return the Kronecker product of this matrix and b. */
   KroneckerProduct[b is Matrix] := KroneckerProduct[this, b]

   
   /** Returns true if both matrices are equal (that is, they have the same
       dimensions and all array elements are equal.)
   */

   equals[other is Matrix] :=
   {
      return rows==other.rows and cols==other.cols and array==other.array
   }

   
   /** Returns a new Matrix where each element is the complex conjugate of
       the corresponding element in original Matrix. */

   conjugate[] :=
   {
      c = makeArray[[rows, cols], {|r,c,data| conjugate[data@r@c]}, array]
      return new Matrix[c]
   }


   /** Returns a new Matrix where the Matrix is first transposed and then
       each element is the complex conjugate of
       the corresponding element in the transposed Matrix. */

   conjugateTranspose[] :=
   {
      t = array.transpose[]
      c = makeArray[[rows, cols], {|r,c,data| conjugate[data@r@c]}, t]
      return new Matrix[c]
   }

   /** Returns true if this matrix is "Hermitian", or "self-adjoint", that is,
       the matrix must be square and equal to its own conjugate transpose.  In
       other words, for all elements, array@i@j == conjugate[array@j@i] where
       conjugate is the complex conjugate of a number.  */

   isHermitian[] :=
   {
      if rows != cols
         return false

      for i=0 to rows-1
         for j = i+1 to rows-1
            if array@i@j != conjugate[array@j@i]
               return false

      return true           
   }

   /** Performs the QR decomposition of a matrix.

       This is based on the (real-only) implementation at:
       https://github.com/fiji/Jama/blob/master/src/main/java/Jama/QRDecomposition.java

       This is an internal implementation, primarily for use by the leastSquares
       method, that returns raw arrays [QR, rdiag].  If you're solving for
       least squares, use that routine directly instead of this.
   */

   QRDecomposeInternal[] :=
   {
      QR = deepCopy[array]   // Copy the original matrix's array
      rdiag = new array[[cols], 0]       // Stores the diagonal

      for k = 0 to cols-1
      {
         // Compute 2-norm of k-th column
         nrm = 0 QR@0@k   // Make units work right
         for i = k to rows-1
            nrm = sqrt[nrm^2 + (QR@i@k)^2]

         if nrm != 0 QR@0@k  // Make units work right
         {
            // Form k-th Householder vector
            if isNegativeUnit[QR@k@k]
               nrm = -nrm

            for i = k to rows-1
               QR@i@k = QR@i@k / nrm

            QR@k@k = QR@k@k + 1

            // Apply transformation to remaining columns
            for j = k+1 to cols-1
            {
               s = undef
               for i=k to rows-1
               {
                  if s == undef
                     s = QR@i@k * QR@i@j
                  else
                     s = s + QR@i@k * QR@i@j
               }

               s = -s / QR@k@k

               for i = k to rows-1
                  QR@i@j = QR@i@j + s * QR@i@k
            }
         }

         rdiag@k = -nrm
      }

      return [QR, rdiag]
   }

   /** This performs a QR decomposition of this matrix and returns [Q, R]
       as new matrices.

       Q is the orthogonal factor.
       R is the upper triangular factor.

       If you are doing a least-squares solve, call leastSquares directly
       instead.

       THINK ABOUT:  Should this return the Householder vectors and rdiag?
   */

   QRDecompose[] :=
   {
      // The Q and R matrices are packed into the results of the following.
      [QR, rdiag] = QRDecomposeInternal[]

      // Extract the Q matrix
      Q = new array[[rows, cols], 0]
      for k = cols-1 to 0 step -1
      {
         for i = 0 to rows-1
            Q@i@k = 0

         Q@k@k = 1

         for j = k to cols-1
         {
            if QR@k@k != 0 QR@k@k  // Make units work right
            {
               s = 0 (QR@0@k) * Q@0@j // Make units work right
               for i = k to rows-1
                  s = s + QR@i@k * Q@i@j

               s = -s / QR@k@k
               
               for i = k to rows-1
                  Q@i@j = Q@i@j + s * QR@i@k
            }
         }
      }

      // Extract the R matrix
      R = new array[[cols, cols], 0]
      for i = 0 to cols-1
      {
         for j = 0 to cols-1
         {
            if i < j
               R@i@j = QR@i@j
            else
               if i == j
                  R@i@j = rdiag@i
               else
                  R@i@j = 0
         }
      }

      return [new Matrix[Q], new Matrix[R]]
   }

   /** Return the least-squares solution (called X) of the system
       A * X = B
       where this Matrix is A and parameter B is B.

       This can be performed on an overdetermined system, where there are
       more measurements than equations.

       TODO:  Try to solve exactly using x = A.inverse[] * b when the system
       is not overdetermined?

       See MatrixQRTest.frink for examples.

       This is the best discussion I've seen of least-squares fitting:
        https://www.aleksandrhovhannisyan.com/blog/the-method-of-least-squares/
        https://www.aleksandrhovhannisyan.com/blog/least-squares-fitting/
   */

   leastSquares[B is Matrix] :=
   {
      if rows != B.rows
      {
         println["Matrix.leastSquares:  Matrix row dimensions must agree."]
         return undef
      }
      
      [QR, rdiag] = QRDecomposeInternal[]

      // Check if the matrix is "full rank," that is, that each row is
      // independent (not a multiple of) other rows.
      for j = 0 to cols-1
      {
         if rdiag@j == 0 rdiag@j
         {
            println["Matrix.leastSquares: Row entries are not all independent."]
            return undef
         }
      }

      X = deepCopy[B.array]   // Copy B's array

      // Compute Y = Q.transpose * B
      for k = 0 to cols-1
      {
         for j = 0 to B.cols-1
         {
            s = undef
            for i = k to rows-1
               if s == undef
                  s = QR@i@k * X@i@j
               else
                  s = s + QR@i@k * X@i@j

            s = -s / QR@k@k

            for i = k to rows-1
               X@i@j = X@i@j + s * QR@i@k
         }
      }

      // Solve R * X = y
      for k = cols-1 to 0 step -1
      {
         for j = 0 to B.cols-1
            X@k@j = X@k@j / rdiag@k

         for i = 0 to k-1
            for j = 0 to B.cols-1
               X@i@j = X@i@j - X@k@j * QR@i@k
      }

      X1 = new Matrix[X]
      return X1.getSubMatrix[0, cols-1, 0, B.cols-1]
   }

   /** Gets a submatrix of this matrix. */
   getSubMatrix[fromRow, toRow, fromCol, toCol] :=
   {
      a = new array[[toRow-fromRow+1, toCol-fromCol+1], 0]
      for row = fromRow to toRow
         for col = fromCol to toCol
            a@(row-fromRow)@(col-fromCol) = array@row@col

//      println["a is $a"]
      return new Matrix[a]       
   }

   /** Rounds values to the nearest integer if they are less than the specified
       relative error away from an integer and returns a new array. */

   roundToInt[relerror = 1e-14] :=
   {
      ret = new array[[rows, cols]]
      for r = 0 to rows-1
         for c = 0 to cols-1
         {
            v = array@r@c
            rnd = round[v]
            if (rnd==0 and abs[rnd-v] <= relerror) or abs[(rnd-v)/rnd] <= relerror
               ret@r@c = rnd
            else
               ret@r@c = v
         }

      return new Matrix[ret]
   }

   /** Adds the multiple of row1 to row2, modifying row2 in place.  */
   addMultipleOfRow[row1, mulBy, row2] :=
   {
      r1 = row1-1
      r2 = row2-1
      array@r2 = add[array@r2, mul[array@r1, mulBy]]
   }

   /** Static method to count the number of leading zeroes in a row
      (represented as a one-dimensional array.) */

   class countLeadingZeroes[a] :=
   {
      len = length[a]
      count = 0
      while count < len and a@count == 0
         count = count + 1

      return count
   }

   /** Sorts rows by leading zeros, with elements with more leading zeroes at
       the bottom.  This modifies the matrix in place.*/

   sortByLeadingZeroes[] :=
   {
      sort[array, {|a,b| Matrix.countLeadingZeroes[a] <=> Matrix.countLeadingZeroes[b]}]
   }

   
   /** Solves a matrix equation (this * x = B) for x.
       The result is a one-dimensional array of values for x.

       This matrix should have the same number of rows as matrix B.

       This is a dispatcher method that calls the appropriate method.  This
       even works with units of measure.

       THINK ABOUT:  What should this return for an inconsistent system?

       THINK ABOUT:  If a row is all zeroes, then don't return that row?
   */

   solve[B] :=
   {
      if ! B.hasUnits[]
         return solveDimensionless[B]
      else
         return solveWithUnits[B]
   }

   /** Solves a matrix equation (this * x = B) for x.  This uses Gauss-Jordan
       reduction to solve the equation.  The result is a column Matrix of values
       for x.
   */

   solveToMatrix[B] :=
   {
      tr = solve[B].transpose[]
      return new Matrix[noEval[tr]]
   }

   /** Primary internal matrix solver.
       Solves a matrix equation (this * x = B) for x.  This uses Gauss-Jordan
       reduction to solve the equation.  The result is a one-dimensional array
       of values for x.

       This matrix should have the same number of rows as matrix B.

       THINK ABOUT:  What should this return for an inconsistent system?

       THINK ABOUT:  If a row is all zeroes, then don't return that row?
   */

   solveDimensionless[B] :=
   {
      augment = this.augment[B]   // Make augmented matrix
      augment.reduceRows[]        // Do the actual solve
      augment.simplifySymbolic[]  // Clean up symbolic solutions

      // Check for inconsistent system.
      if augment.getPivotRow[augment.cols] == augment.rows
         println["Matrix.solve:  Warning: inconsistent system."]
         
      col = augment.getColumnAsArray[augment.getCols[]]  // Extract solution col
      return col
   }

   /** Specialized internal solver to work with units of measure.  This
       factors out units of measure and then derives the required units of
       measure and multiplies them back in.  The result is a one-dimensional
       array of values for x. */

   solveWithUnits[B] :=
   {
      [A1, A2] = factorUnits[]
      [B1, B2] = B.factorUnits[]

      x = A1.solveDimensionless[B1]

      // Derive necessary units of measure and multiply them back in
      len = length[x]
      for i = 0 to len-1
         x@i = x@i * (B2.array@i@0 / A2.array@i@0)

      return x
   }


   /** This turns a Matrix into reduced row-echelon form, modifying this matrix
       in place.  This uses Gauss-Jordan reduction.

       This can be used to solve a system of equations in "augmented matrix"
       form.  See the solve function.

       See:
       https://www.cfm.brown.edu/people/dobrush/cs52/Mathematica/Part7/Part1/LinearEquations_rref.html

       https://chrisj.math.gatech.edu/24s/1553/materials/1_2-1_3-full.pdf
   */

   reduceRows[] :=
   {
      r = 0
      for j = 1 to cols
      {
         i = r+1
         while i <= rows and get[i,j] == 0
            i = i + 1

         if i < rows+1
         {
            r = r + 1
            exchangeRows[i,r]
            scaleToOne[r,j]
            ROW:
            for k = 1 to rows
            {
               if k != r and get[k,j] != 0
                  makeZero[k, j, r]
            }
         }
      }
   }

   /** Scales a cell to 1 by multiplying the specified row by the reciprocal
       of the cell.  This modifies the matrix in place. */

   scaleToOne[row, col] :=
   {
      n = get[row, col]
      if n == 0
      {
         println["Cannot scale [$row, $col] to 1 because it's zero"]
         return
      }
      
      if n != 1
         multiplyRowByScalar[row, recip[n]]

      // Force the cell to exactly 1 in case there's some floating-point
      // or symbolic issue
      set[row, col, 1]
   }

   /** Scales a cell to 0 by adding another scaled row to its row. This
       modifies the matrix in place. */

   makeZero[row, col, otherRow] :=
   {
      n = get[row,col]
      other = get[otherRow, col]
      if n != 0                   // Should check if other != 0 ?
         addMultipleOfRow[otherRow, negate[n/other], row]
      
      // Force the cell to exactly 0 in case there's some floating-point
      // or symbolic issue
      set[row, col, 0]
   }

   /** Augments a matrix with another column matrix, returning a new matrix.
       That is, the column matrix is appended to the right of this matrix.
       The matrices should have the same number of rows.
   */

   augment[colMatrix is Matrix] :=
   {
      a1 = deepCopy[array]
      for c = 0 to colMatrix.cols - 1
         a1.setColumn[colMatrix.array.getColumn[c],cols+c]

      return new Matrix[a1]
   }

   /** Given a Matrix in reduced row-echelon form (i.e. one that comes from
       calling reduceRows), returns the index of the pivot row for the
       specified column (1-based).  The to have a pivot in the column, the
       value in that row has to be 1 and all values to the left of that 1 must
       be zero.  If the column does not have a pivot, this returns undef.

       When solving a system of equations, if a column does not have a pivot
       row, it is a free variable.

       An augmented matrix corresponds to an inconsistent sytem of equations if
       and only if the last (i.e. the augmented) column is a pivot column.
   */

   getPivotRow[col] :=
   {
      for r = rows to 1 step -1
         if get[r, col] == 1
         {
            for c = 1 to col-1
               if get[r,c] != 0
                  return undef
               
            return r           
         }

      return undef       
   }

   /** When solving a symbolic matrix with ReduceRows, the reduced matrix may
       contain terms like 1 + 0 r^-1 s which has unreduced multiplicative terms.
       This eliminates those terms, modifying the matrix in place. */

   simplifySymbolic[] :=
   {
      // Check to see if array has unresolved symbols.  This is a relatively
      // quick check.
      if length[getSymbols[array]] > 0
      {
         for r = 1 to rows
         {
            for c = 1 to cols
            {
               before = get[r,c]
               after = substituteExpression[before, noEval[0 _x],   0]
               after = substituteExpression[after,  noEval[0 + _x], noEval[_x]]
               if after != before
               {
                  // println["$before simplified to $after"]
                  set[r, c, after]
               }
            }
         }
      }
   }

   /** Calculate the trace of a matrix.  The trace is the sum of the entries on
       the diagonal. */

   trace[] :=
   {
      if ! isSquare[]
      {
         println["Matrix.trace: matrix is not square!"]
         return undef
      }

      sum = array@0@0  // Make dimensions come out right
      for i = 1 to rows-1
         sum = sum + array@i@i

      return sum
   }

   /** Returns true if any element has units of measure, that is, if any
       element is not dimensionless. */

   hasUnits[] :=
   {
      for r = 0 to rows-1
      {
         row = array@r
         for c = 0 to cols-1
            if ! ((row@c) conforms 0)
               return true
      }

      return false
   }

   /** Factors a matrix into two matrices [A, B] where A contains only
       dimensionless numbers and B contains dimensions.  This is basically
       calling factorUnits on each element.

       returns [A, B]
   */

   factorUnits[] :=
   {
      A = new Matrix[rows, cols]
      B = new Matrix[rows, cols]
      for r = 1 to rows
         for c = 1 to cols
         {
            v = get[r,c]
            if isUnit[v]
               [a, b] = factorUnits[get[r,c]]
            else
               [a, b] = [v, 1]
            
            A.set[r,c,a]
            B.set[r,c,b]
         }

      return [A, B]      
   }
}



"class Matrix loaded OK."


Download or view Matrix.frink in plain text format


This is a program written in the programming language Frink.
For more information, view the Frink Documentation or see More Sample Frink Programs.

Alan Eliasen, eliasen@mindspring.com