Skip to content

Commit 3823ede

Browse files
authored
feat: add row echelon matrix algorithm (#1454)
* feat: add row echelon matrix algorithm * test: add self-tests for row echelon algorithm * fix: replace rounding with float tolerance * chore: use correct style * fix: use error tolerance and segregate testcases * chore: add necessary explaining comments
1 parent a24450a commit 3823ede

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

Diff for: Maths/RowEchelon.js

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Given a two dimensional matrix, find its row echelon form.
3+
*
4+
* For more info: https://en.wikipedia.org/wiki/Row_echelon_form
5+
*
6+
* @param {number[[]]} matrix - Two dimensional array of rational numbers.
7+
* @returns {number[[]]} - Two dimensional array of rational numbers (row echelon form).
8+
*
9+
* @example
10+
* const matrix = [
11+
* [2,3,4,5,7],
12+
* [9,8,4,0,9],
13+
* [5,7,4,3,9],
14+
* [3,4,0,2,1]
15+
* ]
16+
*
17+
* const result = rowEchelon(matrix)
18+
*
19+
* // The function returns the corresponding row echelon form:
20+
* // result:
21+
* // [
22+
* // [1, 1.5, 2, 2.5, 3.5],
23+
* // [0, 1, 2.54545, 4.09091, 4.09091],
24+
* // [0, 0, 1, 1.57692, 1.36539],
25+
* // [0, 0, 0, 1, -0.25]
26+
* // ]
27+
*/
28+
29+
// Set a tolerance value for floating-point comparisons
30+
const tolerance = 0.000001
31+
32+
// Check if all the rows have same length of elements
33+
const isMatrixValid = (matrix) => {
34+
let numRows = matrix.length
35+
let numCols = matrix[0].length
36+
for (let i = 0; i < numRows; i++) {
37+
if (numCols !== matrix[i].length) {
38+
return false
39+
}
40+
}
41+
42+
// Check for input other than a 2D matrix
43+
if (
44+
!Array.isArray(matrix) ||
45+
matrix.length === 0 ||
46+
!Array.isArray(matrix[0])
47+
) {
48+
return false
49+
}
50+
return true
51+
}
52+
53+
const checkNonZero = (currentRow, currentCol, matrix) => {
54+
let numRows = matrix.length
55+
for (let i = currentRow; i < numRows; i++) {
56+
// Checks if the current element is not very near to zero.
57+
if (!isTolerant(0, matrix[i][currentCol], tolerance)) {
58+
return true
59+
}
60+
}
61+
return false
62+
}
63+
64+
const swapRows = (currentRow, withRow, matrix) => {
65+
let numCols = matrix[0].length
66+
let tempValue = 0
67+
for (let j = 0; j < numCols; j++) {
68+
tempValue = matrix[currentRow][j]
69+
matrix[currentRow][j] = matrix[withRow][j]
70+
matrix[withRow][j] = tempValue
71+
}
72+
}
73+
74+
// Select a pivot element in the current column to facilitate row operations.
75+
// Pivot element is the first non-zero element found from the current row
76+
// down to the last row.
77+
const selectPivot = (currentRow, currentCol, matrix) => {
78+
let numRows = matrix.length
79+
for (let i = currentRow; i < numRows; i++) {
80+
if (matrix[i][currentCol] !== 0) {
81+
swapRows(currentRow, i, matrix)
82+
return
83+
}
84+
}
85+
}
86+
87+
// Multiply each element of the given row with a factor.
88+
const scalarMultiplication = (currentRow, factor, matrix) => {
89+
let numCols = matrix[0].length
90+
for (let j = 0; j < numCols; j++) {
91+
matrix[currentRow][j] *= factor
92+
}
93+
}
94+
95+
// Subtract one row from another row
96+
const subtractRow = (currentRow, fromRow, matrix) => {
97+
let numCols = matrix[0].length
98+
for (let j = 0; j < numCols; j++) {
99+
matrix[fromRow][j] -= matrix[currentRow][j]
100+
}
101+
}
102+
103+
// Check if two numbers are equal within a given tolerance
104+
const isTolerant = (a, b, tolerance) => {
105+
const absoluteDifference = Math.abs(a - b)
106+
return absoluteDifference <= tolerance
107+
}
108+
109+
const rowEchelon = (matrix) => {
110+
// Check if the input matrix is valid; if not, throw an error.
111+
if (!isMatrixValid(matrix)) {
112+
throw new Error('Input is not a valid 2D matrix.')
113+
}
114+
115+
let numRows = matrix.length
116+
let numCols = matrix[0].length
117+
let result = matrix
118+
119+
// Iterate through the rows (i) and columns (j) of the matrix.
120+
for (let i = 0, j = 0; i < numRows && j < numCols; ) {
121+
// If the current column has all zero elements below the current row,
122+
// move to the next column.
123+
if (!checkNonZero(i, j, result)) {
124+
j++
125+
continue
126+
}
127+
128+
// Select a pivot element and normalize the current row.
129+
selectPivot(i, j, result)
130+
let factor = 1 / result[i][j]
131+
scalarMultiplication(i, factor, result)
132+
133+
// Make elements below the pivot element zero by performing
134+
// row operations on subsequent rows.
135+
for (let x = i + 1; x < numRows; x++) {
136+
factor = result[x][j]
137+
if (isTolerant(0, factor, tolerance)) {
138+
continue
139+
}
140+
scalarMultiplication(i, factor, result)
141+
subtractRow(i, x, result)
142+
factor = 1 / factor
143+
scalarMultiplication(i, factor, result)
144+
}
145+
i++
146+
}
147+
return result
148+
}
149+
150+
export { rowEchelon }

Diff for: Maths/test/RowEchelon.test.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { rowEchelon } from '../RowEchelon'
2+
describe('Determinant', () => {
3+
const tolerance = 0.000001
4+
test.each([
5+
[
6+
[
7+
[8, 1, 3, 5],
8+
[4, 6, 8, 2],
9+
[3, 5, 6, 8]
10+
],
11+
[
12+
[1, 0.125, 0.375, 0.625],
13+
[0, 1, 1.18182, -0.09091],
14+
[0, 0, 1, -11.0769]
15+
]
16+
],
17+
[
18+
[
19+
[6, 8, 1, 3, 5],
20+
[1, 4, 6, 8, 2],
21+
[0, 3, 5, 6, 8],
22+
[2, 5, 9, 7, 8],
23+
[5, 5, 7, 0, 1]
24+
],
25+
[
26+
[1, 1.33333, 0.16667, 0.5, 0.83333],
27+
[0, 1, 2.1875, 2.8125, 0.4375],
28+
[0, 0, 1, 1.56, -4.28003],
29+
[0, 0, 0, 1, -3.3595],
30+
[0, 0, 0, 0, 1]
31+
]
32+
],
33+
[
34+
[
35+
[1, 3, 5],
36+
[6, 8, 2],
37+
[5, 6, 8],
38+
[7, 9, 9],
39+
[5, 0, 6]
40+
],
41+
[
42+
[1, 3, 5],
43+
[0, 1, 2.8],
44+
[0, 0, 1],
45+
[0, 0, 0],
46+
[0, 0, 0]
47+
]
48+
],
49+
[
50+
[
51+
[0, 7, 8, 1, 3, 5],
52+
[0, 6, 4, 6, 8, 2],
53+
[0, 7, 3, 5, 6, 8],
54+
[6, 8, 1, 0, 0, 4],
55+
[3, 3, 5, 7, 3, 1],
56+
[1, 2, 1, 0, 9, 7],
57+
[8, 8, 0, 2, 3, 1]
58+
],
59+
[
60+
[1, 1.33333, 0.16667, 0, 0, 0.66667],
61+
[0, 1, 0.66667, 1, 1.33333, 0.33333],
62+
[0, 0, 1, 1.2, 1.99999, -3.4],
63+
[0, 0, 0, 1, 1.3, -1.4],
64+
[0, 0, 0, 0, 1, -2.32854],
65+
[0, 0, 0, 0, 0, 1],
66+
[0, 0, 0, 0, 0, 0]
67+
]
68+
]
69+
])('Should return the matrix in row echelon form.', (matrix, expected) => {
70+
for (let i = 0; i < matrix.length; i++) {
71+
for (let j = 0; j < matrix[i].length; j++) {
72+
expect(rowEchelon(matrix)[i][j]).toBeCloseTo(expected[i][j], tolerance)
73+
}
74+
}
75+
})
76+
77+
test.each([
78+
[
79+
[
80+
[8, 1, 3, 5],
81+
[4, 6, 8, 2, 7],
82+
[3, 5, 6, 8]
83+
],
84+
'Input is not a valid 2D matrix.'
85+
]
86+
])('Should return the error message.', (matrix, expected) => {
87+
expect(() => rowEchelon(matrix)).toThrowError(expected)
88+
})
89+
})

0 commit comments

Comments
 (0)