From 416d5fac1face2d264f26fc8e6c8b2bde4dc8ec9 Mon Sep 17 00:00:00 2001 From: Theodore Kruczek Date: Sun, 14 Jan 2024 20:34:16 -0500 Subject: [PATCH] test: :white_check_mark: add testing for Matrix --- src/operations/Matrix.ts | 189 ++++++++++-- test/operations/Matrix.test.ts | 277 ++++++++++++++++++ .../__snapshots__/Matrix.test.ts.snap | 161 ++++++++++ 3 files changed, 606 insertions(+), 21 deletions(-) create mode 100644 test/operations/Matrix.test.ts create mode 100644 test/operations/__snapshots__/Matrix.test.ts.snap diff --git a/src/operations/Matrix.ts b/src/operations/Matrix.ts index 7141fd9..368fb70 100644 --- a/src/operations/Matrix.ts +++ b/src/operations/Matrix.ts @@ -1,10 +1,15 @@ +import { Radians } from 'src/main'; import { Vector } from './Vector'; import { Vector3D } from './Vector3D'; +/** + * A matrix is a rectangular array of numbers or other mathematical objects for + * which operations such as addition and multiplication are defined. + */ export class Matrix { - public elements: number[][]; - public readonly rows: number; - public readonly columns: number; + elements: number[][]; + readonly rows: number; + readonly columns: number; constructor(elements: number[][]) { this.elements = elements; @@ -12,10 +17,29 @@ export class Matrix { this.columns = elements[0].length; } + /** + * Creates a matrix with all elements set to zero. + * + * @param rows - The number of rows in the matrix. + * @param columns - The number of columns in the matrix. @returns A matrix + * with all elements set to zero. + */ static allZeros(rows: number, columns: number): Matrix { return this.fill(rows, columns, 0.0); } + /** + * Creates a new Matrix with the specified number of rows and columns, filled + * with the specified value. + * + * @param rows The number of rows in the matrix. + * + * @param columns The number of columns in the matrix. + * + * @param value The value to fill the matrix with. Default is 0.0. + * + * @returns A new Matrix filled with the specified value. + */ static fill(rows: number, columns: number, value = 0.0): Matrix { const elements: number[][] = []; @@ -29,7 +53,13 @@ export class Matrix { return new Matrix(elements); } - public static rotX(theta: number): Matrix { + /** + * Creates a rotation matrix around the X-axis. + * @param theta - The angle of rotation in radians. + * + * @returns The rotation matrix. + */ + static rotX(theta: Radians): Matrix { const cosT = Math.cos(theta); const sinT = Math.sin(theta); const result = Matrix.zero(3, 3); @@ -43,7 +73,13 @@ export class Matrix { return result; } - public static rotY(theta: number): Matrix { + /** + * Creates a rotation matrix around the y-axis. + * @param theta - The angle of rotation in radians. + * + * @returns The rotation matrix. + */ + static rotY(theta: Radians): Matrix { const cosT = Math.cos(theta); const sinT = Math.sin(theta); const result = Matrix.zero(3, 3); @@ -57,7 +93,13 @@ export class Matrix { return result; } - public static rotZ(theta: number): Matrix { + /** + * Creates a rotation matrix around the Z-axis. + * + * @param theta The angle of rotation in radians. + * @returns The rotation matrix. + */ + static rotZ(theta: Radians): Matrix { const cosT = Math.cos(theta); const sinT = Math.sin(theta); const result = Matrix.zero(3, 3); @@ -71,7 +113,15 @@ export class Matrix { return result; } - public static zero(rows: number, columns: number): Matrix { + /** + * Creates a zero matrix with the specified number of rows and columns. + * @param rows The number of rows in the matrix. + * + * @param columns The number of columns in the matrix. + * + * @returns A new Matrix object representing the zero matrix. + */ + static zero(rows: number, columns: number): Matrix { const elements: number[][] = []; for (let i = 0; i < rows; i++) { @@ -84,7 +134,13 @@ export class Matrix { return new Matrix(elements); } - public static identity(dimension: number): Matrix { + /** + * Creates an identity matrix of the specified dimension. + * @param dimension The dimension of the identity matrix. + * + * @returns The identity matrix. + */ + static identity(dimension: number): Matrix { const elements: number[][] = []; for (let i = 0; i < dimension; i++) { @@ -97,7 +153,14 @@ export class Matrix { return new Matrix(elements); } - public static diagonal(d: number[]): Matrix { + /** + * Creates a diagonal matrix with the given diagonal elements. + * + * @param d - An array of diagonal elements. + * + * @returns A new Matrix object representing the diagonal matrix. + */ + static diagonal(d: number[]): Matrix { const dimension = d.length; const elements: number[][] = []; @@ -111,7 +174,13 @@ export class Matrix { return new Matrix(elements); } - public add(m: Matrix): Matrix { + /** + * Adds the elements of another matrix to this matrix and returns the result. + * @param m - The matrix to be added. + * + * @returns The resulting matrix after addition. + */ + add(m: Matrix): Matrix { const result = Matrix.zero(this.rows, this.columns); for (let i = 0; i < this.rows; i++) { @@ -123,7 +192,14 @@ export class Matrix { return result; } - public subtract(m: Matrix): Matrix { + /** + * Subtracts the elements of another matrix from this matrix. + * + * @param m - The matrix to subtract. + * + * @returns A new matrix containing the result of the subtraction. + */ + subtract(m: Matrix): Matrix { const result = Matrix.zero(this.rows, this.columns); for (let i = 0; i < this.rows; i++) { @@ -135,7 +211,13 @@ export class Matrix { return result; } - public scale(n: number): Matrix { + /** + * Scales the matrix by multiplying each element by a scalar value. + * @param n - The scalar value to multiply each element by. + * + * @returns A new Matrix object representing the scaled matrix. + */ + scale(n: number): Matrix { const result = Matrix.zero(this.rows, this.columns); for (let i = 0; i < this.rows; i++) { @@ -147,11 +229,21 @@ export class Matrix { return result; } - public negate(): Matrix { + /** + * Negates the matrix by scaling it by -1. + * @returns The negated matrix. + */ + negate(): Matrix { return this.scale(-1); } - public multiply(m: Matrix): Matrix { + /** + * Multiplies this matrix with another matrix. + * @param m The matrix to multiply with. + * + * @returns The resulting matrix. + */ + multiply(m: Matrix): Matrix { const result = Matrix.zero(this.rows, m.columns); for (let i = 0; i < this.rows; i++) { @@ -165,7 +257,14 @@ export class Matrix { return result; } - public outerProduct(m: Matrix): Matrix { + /** + * Computes the outer product of this matrix with another matrix. + * + * @param m - The matrix to compute the outer product with. + * + * @returns The resulting matrix. + */ + outerProduct(m: Matrix): Matrix { const result = Matrix.zero(this.rows, this.columns); for (let i = 0; i < this.rows; i++) { @@ -177,7 +276,13 @@ export class Matrix { return result; } - public multiplyVector(v: Vector): Vector { + /** + * Multiplies the matrix by a vector. + * @param v The vector to multiply by. + * + * @returns A new vector representing the result of the multiplication. + */ + multiplyVector(v: Vector): Vector { const result: number[] = []; for (let i = 0; i < this.rows; i++) { @@ -192,7 +297,16 @@ export class Matrix { return new Vector(result); } - public multiplyVector3D(v: Vector3D): Vector3D { + /** + * Multiplies a 3D vector by the matrix. + * + * @template T - The type of the vector elements. + * + * @param v - The 3D vector to multiply. + * + * @returns The resulting 3D vector after multiplication. + */ + multiplyVector3D(v: Vector3D): Vector3D { const result: T[] = []; for (let i = 0; i < this.rows; i++) { @@ -219,7 +333,15 @@ export class Matrix { return new Vector3D(result[0], result[1], result[2]); } - public reciprocal(): Matrix { + /** + * Returns a new Matrix object where each element is the reciprocal of the + * corresponding element in the current matrix. If an element in the current + * matrix is zero, the corresponding element in the output matrix will also be + * zero. + * @returns A new Matrix object representing the reciprocal of the current + * matrix. + */ + reciprocal(): Matrix { const output = Matrix.zero(this.rows, this.columns); for (let i = 0; i < this.rows; i++) { @@ -233,7 +355,11 @@ export class Matrix { return output; } - public transpose(): Matrix { + /** + * Transposes the matrix by swapping rows with columns. + * @returns A new Matrix object representing the transposed matrix. + */ + transpose(): Matrix { const result = Matrix.zero(this.columns, this.rows); for (let i = 0; i < this.rows; i++) { @@ -245,7 +371,13 @@ export class Matrix { return result; } - public cholesky(): Matrix { + /** + * Performs the Cholesky decomposition on the matrix. + * + * @returns A new Matrix object representing the Cholesky decomposition of the + * original matrix. + */ + cholesky(): Matrix { const result = Matrix.zero(this.rows, this.rows); for (let i = 0; i < this.rows; i++) { @@ -265,6 +397,12 @@ export class Matrix { return result; } + /** + * Swaps two rows in the matrix. + * + * @param i - The index of the first row. + * @param j - The index of the second row. + */ private _swapRows(i: number, j: number): void { if (i === j) { return; @@ -275,6 +413,10 @@ export class Matrix { this.elements[j] = tmp; } + /** + * Converts the matrix to reduced row echelon form using the Gaussian + * elimination method. This method modifies the matrix in-place. + */ private _toReducedRowEchelonForm(): void { for (let lead = 0, row = 0; row < this.rows && lead < this.columns; ++row, ++lead) { let i = row; @@ -308,7 +450,12 @@ export class Matrix { } } - public inverse(): Matrix { + /** + * Calculates the inverse of the matrix. + * + * @returns The inverse of the matrix. + */ + inverse(): Matrix { const tmp = Matrix.zero(this.rows, this.columns * 2); for (let row = 0; row < this.rows; ++row) { diff --git a/test/operations/Matrix.test.ts b/test/operations/Matrix.test.ts new file mode 100644 index 0000000..03c3fa2 --- /dev/null +++ b/test/operations/Matrix.test.ts @@ -0,0 +1,277 @@ +import { Matrix, Radians, Vector } from '../../src/main'; + +describe('Matrix', () => { + // should create a matrix with the correct number of rows and columns + it('should create a matrix with the correct number of rows and columns', () => { + const elements = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]; + const matrix = new Matrix(elements); + + expect(matrix.elements).toEqual(elements); + expect(matrix.rows).toBe(elements.length); + expect(matrix.columns).toBe(elements[0].length); + }); + + // should add two matrices correctly + it('should add two matrices correctly', () => { + const matrix1 = new Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + const matrix2 = new Matrix([ + [9, 8, 7], + [6, 5, 4], + [3, 2, 1], + ]); + const result = matrix1.add(matrix2); + + expect(result.elements).toEqual([ + [10, 10, 10], + [10, 10, 10], + [10, 10, 10], + ]); + expect(result.rows).toBe(3); + expect(result.columns).toBe(3); + }); + + // should subtract two matrices correctly + it('should subtract two matrices correctly', () => { + const matrix1 = new Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + const matrix2 = new Matrix([ + [9, 8, 7], + [6, 5, 4], + [3, 2, 1], + ]); + const result = matrix1.subtract(matrix2); + + expect(result.elements).toEqual([ + [-8, -6, -4], + [-2, 0, 2], + [4, 6, 8], + ]); + expect(result.rows).toBe(3); + expect(result.columns).toBe(3); + }); + + // should scale a matrix correctly + it('should scale a matrix correctly', () => { + const matrix = new Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + const result = matrix.scale(2); + + expect(result.elements).toEqual([ + [2, 4, 6], + [8, 10, 12], + [14, 16, 18], + ]); + expect(result.rows).toBe(3); + expect(result.columns).toBe(3); + }); + + // should multiply two matrices correctly + it('should multiply two matrices correctly', () => { + const matrix1 = new Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + const matrix2 = new Matrix([ + [9, 8, 7], + [6, 5, 4], + [3, 2, 1], + ]); + const result = matrix1.multiply(matrix2); + + expect(result.elements).toEqual([ + [30, 24, 18], + [84, 69, 54], + [138, 114, 90], + ]); + expect(result.rows).toBe(3); + expect(result.columns).toBe(3); + }); + + // should multiply a matrix and a vector correctly + it('should multiply a matrix and a vector correctly', () => { + const matrix = new Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + const vector = new Vector([1, 2, 3]); + const result = matrix.multiplyVector(vector); + + expect(result.elements).toEqual([14, 32, 50]); + }); + + // should create a matrix with empty rows + it('should create a matrix with empty rows', () => { + const matrix = new Matrix([[], [], []]); + + expect(matrix.elements).toEqual([[], [], []]); + expect(matrix.rows).toBe(3); + expect(matrix.columns).toBe(0); + }); + + // should create a matrix with empty columns + it('should create a matrix with empty columns', () => { + const matrix = new Matrix([[1], [2], [3]]); + + expect(matrix.elements).toEqual([[1], [2], [3]]); + expect(matrix.rows).toBe(3); + expect(matrix.columns).toBe(1); + }); + + // should multiply a matrix and a 3D vector correctly + it('should multiply a matrix and a 3D vector correctly when the matrix and vector are valid', () => { + const matrixElements = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]; + const vectorElements = [1, 2, 3]; + const expectedElements = [14, 32, 50]; + const matrix = new Matrix(matrixElements); + const vector = new Vector(vectorElements); + + const result = matrix.multiplyVector(vector); + + expect(result.elements).toEqual(expectedElements); + }); + + // should calculate the outer product of two matrices correctly + it('should calculate the outer product of two matrices correctly', () => { + const matrix1 = new Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + const matrix2 = new Matrix([ + [9, 8, 7], + [6, 5, 4], + [3, 2, 1], + ]); + const expected = new Matrix([ + [9, 16, 21], + [24, 25, 24], + [21, 16, 9], + ]); + const result = matrix1.outerProduct(matrix2); + + expect(result).toEqual(expected); + }); + + // should calculate the transpose of a matrix correctly + it('should calculate the transpose of a matrix correctly', () => { + const elements = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]; + const matrix = new Matrix(elements); + const transpose = matrix.transpose(); + + expect(transpose.elements).toEqual([ + [1, 4, 7], + [2, 5, 8], + [3, 6, 9], + ]); + expect(transpose.rows).toBe(matrix.columns); + expect(transpose.columns).toBe(matrix.rows); + }); + + // should calculate the inverse of a matrix correctly + it('should calculate the inverse of a matrix correctly', () => { + const elements = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]; + const matrix = new Matrix(elements); + const inverse = matrix.inverse(); + + expect(inverse.elements).toMatchSnapshot(); + }); + + // allZeros + it('should return true if all elements are zero', () => { + const matrix = Matrix.allZeros(3, 3); + + expect(matrix.elements).toEqual([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ]); + }); + + // fill + it('should fill a matrix with the correct value', () => { + const matrix = Matrix.fill(3, 3, 1); + + expect(matrix.elements).toEqual([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ]); + }); + + // rotX + it('should create a rotation matrix around the x-axis', () => { + const matrix = Matrix.rotX((Math.PI / 2) as Radians); + + expect(matrix.elements).toMatchSnapshot(); + }); + + // rotY + it('should create a rotation matrix around the y-axis', () => { + const matrix = Matrix.rotY((Math.PI / 2) as Radians); + + expect(matrix.elements).toMatchSnapshot(); + }); + + // rotZ + it('should create a rotation matrix around the z-axis', () => { + const matrix = Matrix.rotZ((Math.PI / 2) as Radians); + + expect(matrix.elements).toMatchSnapshot(); + }); + + // identity + it('should create an identity matrix', () => { + const matrix = Matrix.identity(3); + + expect(matrix.elements).toMatchSnapshot(); + }); + + // diagonal + it('should create a diagonal matrix', () => { + const matrix = Matrix.diagonal([1, 2, 3]); + + expect(matrix.elements).toMatchSnapshot(); + }); + + // reciprocal + it('should create a reciprocal matrix', () => { + const matrix = Matrix.allZeros(3, 3).reciprocal(); + + expect(matrix.elements).toMatchSnapshot(); + }); + + // cholesky + it('should create a cholesky matrix', () => { + const matrix = Matrix.allZeros(3, 3).cholesky(); + + expect(matrix.elements).toMatchSnapshot(); + }); +}); diff --git a/test/operations/__snapshots__/Matrix.test.ts.snap b/test/operations/__snapshots__/Matrix.test.ts.snap new file mode 100644 index 0000000..8232bb4 --- /dev/null +++ b/test/operations/__snapshots__/Matrix.test.ts.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matrix should calculate the inverse of a matrix correctly 1`] = ` +Array [ + Array [ + 0, + -2.6666666666666665, + 1.6666666666666665, + ], + Array [ + 0, + 2.333333333333333, + -1.3333333333333333, + ], + Array [ + 1, + -2, + 1, + ], +] +`; + +exports[`Matrix should create a cholesky matrix 1`] = ` +Array [ + Array [ + 0, + 0, + 0, + ], + Array [ + NaN, + NaN, + 0, + ], + Array [ + NaN, + NaN, + NaN, + ], +] +`; + +exports[`Matrix should create a diagonal matrix 1`] = ` +Array [ + Array [ + 1, + 0, + 0, + ], + Array [ + 0, + 2, + 0, + ], + Array [ + 0, + 0, + 3, + ], +] +`; + +exports[`Matrix should create a reciprocal matrix 1`] = ` +Array [ + Array [ + 0, + 0, + 0, + ], + Array [ + 0, + 0, + 0, + ], + Array [ + 0, + 0, + 0, + ], +] +`; + +exports[`Matrix should create a rotation matrix around the x-axis 1`] = ` +Array [ + Array [ + 1, + 0, + 0, + ], + Array [ + 0, + 6.123233995736766e-17, + 1, + ], + Array [ + 0, + -1, + 6.123233995736766e-17, + ], +] +`; + +exports[`Matrix should create a rotation matrix around the y-axis 1`] = ` +Array [ + Array [ + 6.123233995736766e-17, + 0, + -1, + ], + Array [ + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 6.123233995736766e-17, + ], +] +`; + +exports[`Matrix should create a rotation matrix around the z-axis 1`] = ` +Array [ + Array [ + 6.123233995736766e-17, + 1, + 0, + ], + Array [ + -1, + 6.123233995736766e-17, + 0, + ], + Array [ + 0, + 0, + 1, + ], +] +`; + +exports[`Matrix should create an identity matrix 1`] = ` +Array [ + Array [ + 1, + 0, + 0, + ], + Array [ + 0, + 1, + 0, + ], + Array [ + 0, + 0, + 1, + ], +] +`;