With this file we have four NPM scripts we can run with Yarn. watchBuildDist will be really convenient when we start developing as it will build the project into dist/ each time we make a change to any of the project files. Once the project is built, we can just refresh our browser.

We can now go ahead and install all of our dependencies so make sure to run the following commands.

cd matrix-inverse-calculator
yarn run installPackages

Inverting a matrix

There is an argument to never invert a matrix—reasons being computational space time efficiency and numerical instability. However, inverting for us is an end-in-itself.

The identity matrix

With matrices we cannot divide a matrix like say you would two numbers 3 / 2 = 3 * 2^(-1) = 3 * (1 / 2) = 3 / 2. We can however multiply a matrix by the inverse of some matrix [[...], ...] * [[...], ...]^(-1) provided the inverse exists. What happens if you take a matrix A and times it by its inverse A^(-1)? You get the identity matrix I = A * A^(-1). Multiply a matrix by the identity matrix and you get back the same matrix A * I = A = A * (A * A^(-1)) = A * I.

Notice the ones going down the diagonal. For every matrix value where the row number equals the column number notice the following.

2*0.5+0+0+0=2* (1 / 2) +0+0+0=1

What if we took our matrix A, the identity matrix I, performed some operations on A to make it look just like I, and did those same operations on I? What would I turn into? I would turn into the inverse A^(-1) and A would turn into I.

Inverse criteria

Before we can find the inverse, we must satisfy the following criteria.

A must be square such that it has as many rows as it does columns

I is always square

A cannot have a row that is all zeros and/or a column that is all zeros

If we notice an all zero row or column, while we turn A into I, we have to stop. Similarly, if find ourselves without a pivot position, as we traverse the rows, we have to stop. We’ll have to report back to the user that the matrix could not be inverted if we cannot satisfy all of the criteria.

Elementary row operations

So what operations can we perform to turn A into I?

Swap two rows

Multiply a row by some non zero number

Take a row, multiply it by some number, and then add that result to another row

Elementary row operations are used in Gaussian elimination to reduce a matrix to row echelon form. They are also used in Gauss-Jordan elimination to further reduce the matrix to reduced row echelon form.
Elementary Matrix - Wikipedia

RREF

Using these operations our goal is to turn every diagonal value in A into a one and every other value into a zero. This is known as reduced row echelon form (RREF).

To be in RREF, your matrix must satisfy the following.

The ones (also known as the “pivots”) have to be the only non-zero number in their column

The pivots have to be in column order

From top to bottom, pivots in lower numbered columns come before pivots in higher numbered columns

From left to right, all values before a pivot in a row have to be zero

Gauss-Jordan elimination

Our algorithm for turning A into I follows the following pseudo code.

do
let A be the input matrix
let I be the identity matrix
if A is not square doreturn error
while A is notinRREFdoif A contains a zero row or column doreturn error
in A andfor each diagonal value doin A in current column andin rows at or below current row, find the row with a one or the largest absolute value
in A swap the found row with the current row
in I swap the same row numbers
in A get current diagonal value
if diagonal value is zero doreturn error
in A multiply pivot row by (1 / diagonal value)
in A overwrite pivot row with this new row
in I multiply pivot row by (1 / diagonal value found in A)
in I overwrite pivot row with this new row
in A and from top to bottom row in current column doif current row is the pivot row
skip
in A get current row column value
in A get current pivot
in A subtract doin A multiply pivot row by current row column value
from doin A multiply current row by pivot
and store this new row in current row
in I subtract doin I multiply current row by pivot found in A
from doin I multiply pivot row by current row column value found in A
and store this new row in current row
return I

Overall our goal is to visit each diagonal value (row number equals column number), turn that value into a one (by dividing the whole row by the diagonal value) and zero out every other value in the column (by subtracting the pivot row multiplied by the target value from the target row multiplied by the pivot value) while also mirroring the same operations in what was initially the identity matrix. Note that we reuse the numbers we find in the input matrix when we perform the mirrored operations in the identity matrix. For example, if we are dividing the current row by 11 (because the diagonal was 11) in the input matrix then we will divide the same row number by 11 in the identity matrix.

Typically one will generate an augmented matrix by appending the identity matrix to the input matrix and then perform the elementary operations on this augmented matrix. By doing it this way you perform the mirrored operations implicitly. For our implementation though we will keep the two matrices separate while we perform the inversion calculation.

The top section defines the module and imports all of the necessary dependencies. The PureScript compiler will warn us if we are importing something we don’t need. It will also throw an error if we are using something we didn’t explicitly import. This can get tedious as PureScript’s package ecosystem is very granular.

defaultMatrix inputs a number and outputs a square matrix of the given size using the buildMatrix function. Every value in the matrix will be 0.0 as given by defaultMatrixValue.

The identityMatrix function takes a size and outputs a square matrix that is zeros everywhere except when the row and col numbers match. You can see that logic in the where clause.

buildMatrix takes a size, a function that takes two integers and outputs a number, and outputs a matrix. The input function is used to fill each value in the matrix being built. We map over the rows and map over the columns and for each row and column we fill the value by passing the row and column number to the function that was passed to buildMatrix. Since we only care about building square matrices, we use the same range for both rows and columns.

Here we define some error checking functions. You’ll remember that our input matrix must be square and have no rows and/or columns that contain all zeros.

isSquareMatrix checks that every row of the matrix has the same number of elements as the number of rows in the matrix. If one or more rows are longer or shorter than the total amount of rows, we output false. Otherwise, if they have all the same size, we output true.

containsZeroRow reduces (foldl for fold from the left) the input matrix down to a single Boolean (true or false) value. For every row, we check if all the values contain zero and if so, we return true for that row. As we reduce each row down to a Boolean, we or (||) these together and output true or false. If one or more rows have all zeros, our output will be true and otherwise it will be false.

containsZeroCol transposes the input matrix (turns every column into a row) and then uses containsZeroRow to check for any row (previously a column) that contains all zeros.

Recall that once we visit a new diagonal, we have to find the best row to pivot off of. The best is a row (in the same column as the diagonal) with a one but if we cannot find that then we pick the row that has the largest absolute value among the column of values. Note that the only row column values we can look at are the values at or below the current row number of the diagonal we are at. We do not want to swap with any rows above the current since we already pivoted off of those rows.

C1C2C3C4
[ 1231 ] R1
[ 0 (0) 11 ] R2
[ 0211 ] R3
[ 0111 ] R4

Let’s say we are at the R2 C2 diagonal.

In this case the first valid row would be R4 since it contains a one in the same column as the diagonal. We would end up swapping R2 with R4.

C1C2C3C4
[ 1231 ] R1
[ 0 (1) 11 ] R2
[ 0811 ] R3
[ 0911 ] R4

In this case the first valid row would be R2 since it contains a one in the same column as the diagonal. We would end up swapping R2 with R2.

C1C2C3C4
[ 1231 ] R1
[ 0 (2) 11 ] R2
[ 0-411 ] R3
[ 0311 ] R4

In this case the first valid row would be R3 since it contains a negative four in the same column as the diagonal. We would end up swapping R2 with R3.

Looking at the function you can see how we go about doing this. We input our matrix, the row and column we can start searching from, and we Maybe return an integer. The Maybe signifies that we may not find any valid row forcing us to return Nothing.

This function looks verbose but really the meat of it is in the helper function folder. We scan the values from atRowOrBelow in the column inCol and search for a one or the abs largest value. If found, we return Just the row number and Just the row column value for the first valid row.

Looking closer at folder, it takes a record, a tuple containing an integer and a real number, and returns a record that holds maybe an integer (index) and maybe a real number (value). There a three input scenarios folderpattern matches on.

The input record has nothing

Return a record containing just what the input tuple had

The input record has values

Return the input record if its value is 1, otherwise

return the input record if its value is greater than the value of the input tuple or

return the tuple as a record if its value is greater than that of the input record

For everything else

return a record containing Nothing

The foldl uses folder to reduce the extracted tuples, from the matrix column, down to a single record where we access the index or row number that Maybe an integer if folder was able to find something.

Given the input matrix, we extract out the rest of the column into tuples where the first element is the row number and the second element is the value. We then reduce these tuples to a single record where we return the index attribute.

clearColumnExceptPivot will helps up clear a column to all zeros except the diagonal value. It works on two matrices at a time mirroring the operations on aMat in bMat (mat short for matrix). The majority of the work occurs in clearValue.

clearValue takes a tuple containing two matrices, a tuple of two integers, another tuple of two integers, and returns a tuple containing two matrices. While clearValue may look complicated at first, it is simply performing the Target Row = (Pivot * Target Row) - (Target Value * Pivot Row) update step. This occurs in the multiplyAndSubtractRows function. The rest is mostly accessing row values.

You can see that it takes the input matrix and returns a tuple. The first tuple element is either a string or a matrix and the second element is a matrix.

Users of this function can test the first element of the returned tuple. If it is a string, there was an issue with trying to invert the matrix. However, if it is a matrix, then the inversion was possible and the first element will be the identity matrix and the second element will be the inverse of the input matrix.

These are guard clauses that check if the input matrix satisfies our criteria. Notice that the first three guard clauses return a tuple containing the error message and the identity matrix. If the first three scenarios do not apply then we return the result.

folder pattern matches twice. The first one matches if the first element of the input (the result of the last fold iteration) tuple is Left (which would be a string). In this scenario it just returns the tuple it was given. No need to keep trying as there must have been an error. The last one matches everything else but more specifically that the first element of the input tuple is Right (which would be a matrix). The last scenario checks four different cases before it proceeds.

If any of these are true then it returns a tuple with an error string explaining that it could not invert the matrix and whatever was the second element of the input tuple. However, if they are all false, then it returns a tuple containing the updated matrices.

Our UI state consists of the size of the matrix, two matrices, a status message, and two Booleans indicating if the calculator is running and if it finished the calculation after having run.

dataQuery a =UpdateMatrixSizeString a |UpdateMatrixValueIntIntString a |Run a

Our UI is setup to handle three messages or queries. The first updates the size of the input matrix. The second updates the input matrix with a new value at the specified row and column. The third fires off the inversion attempt then updates the UI component’s state once the calculator comes back with a result.

If the matrix size is valid, we display all of the input boxes that make up the input matrix. Using CSS, we color the diagonal input boxes slightly different. Once a run finishes, we swap the colors of the left and right matrix.

After the user attempts to update the matrix size, we check to see if the value is valid. If valid, we go ahead and create a default and identity matrix. If invalid, we update the status message and clear out both of the state’s matrices.

After clicking the run button, we set the state’s status and running attribute. Next we get the current input matrix and attempt to invert it. If there was an error, we update the status. Otherwise we set matrixA to the identity matrix and set matrixB to the inverse of the input matrix (as returned by invertMatrix).

Make sure to open up your favorite text editor and replace src/UI.purs with the following code.

Taking a closer look at the type alias Effects, we see that it is all of Halogen’s effects (which are asynchronous variables, exceptions, and DOM manipulations) plus sending output to the console.

PureScript’s purescript-eff package defines a monad called Eff, which is used to handle native effects. The goal of the Eff monad is to provide a typed API for effectful computations, while at the same time generating efficient Javascript.
Handling Native Effects with the Eff Monad by Phil Freeman

Building

We have set up and programmed our entire application. At this point we can build the project and finally try out the matrix inverse calculator.

Recap

From the ground up, we implemented a matrix inverse calculator using PureScript and PureScript-Halogen. We discussed using NVM and Yarn to manage our dependencies and automate the building of our project. We looked at the what, why, and how of inverting a matrix. We covered the basic Gauss-Jordan elimination algorithm for turning the input matrix into the identity matrix and the identity matrix into the inverse of the input matrix. We took our pseudo code and turned it into our matrix inverse PureScript module. We defined the dynamically generated HTML and query message handlers that power our user interface. And lastly we compiled and linked our application together getting it ready for distribution.