Skip to content

Commit

Permalink
feat: add support for empty matrices (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
holub008 authored Jan 4, 2021
1 parent 1647dd8 commit 211de6e
Show file tree
Hide file tree
Showing 24 changed files with 568 additions and 33 deletions.
5 changes: 5 additions & 0 deletions matrix.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ export abstract class AbstractMatrix {
*/
isSquare(): boolean;

/**
* Returns whether the number of rows or columns (or both) is zero.
*/
isEmpty(): boolean;

/**
* Returns whether the matrix is square and has the same values on both sides of the diagonal.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/decompositions/cholesky.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ describe('Cholesky decomposition', () => {
expect(choAtA.isPositiveDefinite()).toStrictEqual(false);
expect(() => choAtA.solve(b)).toThrow('Matrix is not positive definite');
});
it('should handle empty matrices', () => {
const decomp = new CHO([]);
expect(decomp.lowerTriangularMatrix.to2DArray()).toStrictEqual([]);
});
});

function checkTriangular(matrix) {
Expand Down
5 changes: 5 additions & 0 deletions src/__tests__/decompositions/evd.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ describe('Eigenvalue decomposition', () => {
[0, 3],
]);
});

it('empty matrix', () => {
const matrix = new Matrix([]);
expect(() => new EVD(matrix)).toThrow('Matrix must be non-empty');
});
});
9 changes: 9 additions & 0 deletions src/__tests__/decompositions/lu.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ describe('LU decomposition', () => {
]).determinant,
).toThrow('Matrix must be square');
});

it('should handle empty matrices', () => {
const matrix = new Matrix([]);
const decomp = new LU(matrix);
expect(decomp.lowerTriangularMatrix.to2DArray()).toStrictEqual([]);
expect(decomp.upperTriangularMatrix.to2DArray()).toStrictEqual([]);
// https://en.wikipedia.org/wiki/Matrix_(mathematics)#Empty_matrices
expect(decomp.determinant).toStrictEqual(1);
});
});

function checkTriangular(matrix) {
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/decompositions/qr.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,12 @@ describe('Qr decomposition', () => {
const qR = Q.mmul(R);
expect(qR.to2DArray()).toBeDeepCloseTo(A, 4);
});

it('should work with empty matrices', () => {
const matrix = new Matrix([]);
const decomp = new QR(matrix);
expect(decomp.upperTriangularMatrix.to2DArray()).toStrictEqual([]);
expect(decomp.orthogonalMatrix.to2DArray()).toStrictEqual([]);
expect(decomp.isFullRank()).toStrictEqual(true);
});
});
7 changes: 7 additions & 0 deletions src/__tests__/decompositions/svd.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,11 @@ describe('Singular value decomposition', () => {
expect(actual).toBeDeepCloseTo(output, 8);
});
});

describe('empty matrix', () => {
it('should throw for an empty matrix', () => {
const matrix = new Matrix([]);
expect(() => new SVD(matrix)).toThrow('Matrix must be non-empty');
});
});
});
26 changes: 19 additions & 7 deletions src/__tests__/matrix/creation.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,29 @@ describe('Matrix creation', () => {
expect(matrix).toStrictEqual(original);
});

it('should create an empty matrix', () => {
it('should create a zero matrix', () => {
let matrix = new Matrix(3, 9);
expect(matrix.rows).toBe(3);
expect(matrix.columns).toBe(9);
expect(matrix.get(0, 0)).toBe(0);
});

it('should create an empty matrix', () => {
const matrix00 = new Matrix(0, 0);
expect(matrix00.rows).toBe(0);
expect(matrix00.columns).toBe(0);
const matrix01 = new Matrix(0, 1);
expect(matrix01.rows).toBe(0);
expect(matrix01.columns).toBe(1);
const matrix00FromArray = new Matrix([[]]);
expect(matrix00FromArray.rows).toBe(1);
expect(matrix00FromArray.columns).toBe(0);
});

it('should throw with wrong arguments', () => {
expect(() => new Matrix(6, -1)).toThrow(
/^nColumns must be a positive integer/,
);
expect(() => new Matrix(0, 0)).toThrow(
/^First argument must be a positive number or an array$/,
);
expect(() => new Matrix([[]])).toThrow(
/^Data must be a 2D array with at least one element$/,
);
expect(() => new Matrix([0, 1, 2, 3])).toThrow(/^Data must be a 2D array/);
expect(
() =>
Expand Down Expand Up @@ -137,6 +143,9 @@ describe('Matrix creation', () => {
[0, 1],
[0, 0],
]);

let eye0 = Matrix.eye(0, 0);
expect(eye0.to2DArray()).toStrictEqual([]);
});

it('eye with other value than 1', () => {
Expand Down Expand Up @@ -172,6 +181,9 @@ describe('Matrix creation', () => {
[0, 0, 3, 0],
[0, 0, 0, 0],
]);
expect(Matrix.diag([], 0, 0).to2DArray()).toStrictEqual([]);
expect(Matrix.diag([[]], 1, 0).to2DArray()).toStrictEqual([[]]);
expect(Matrix.diag([], 0, 2).to2DArray()).toStrictEqual([]);
});

it('views should return new instances of Matrix', () => {
Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/matrix/flip.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ test('flip rows', () => {
]);
});

test('flip rows of 0 row matrix', () => {
const matrix = new Matrix([]);
const result = matrix.flipRows();
expect(result).toBe(matrix);
expect(result.to2DArray()).toStrictEqual([]);
});

test('flip columns', () => {
const matrix = new Matrix([
[1, 2, 3],
Expand All @@ -25,3 +32,11 @@ test('flip columns', () => {
[1, 2, 3],
]);
});

test('flip columns of 0 row matrix', () => {
const matrix = new Matrix(0, 5);
const result = matrix.flipColumns();
expect(result).toBe(matrix);
expect(result.rows).toBe(0);
expect(result.columns).toBe(5);
});
19 changes: 19 additions & 0 deletions src/__tests__/matrix/kronecker.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,23 @@ describe('Kronecker product', () => {
[18, 21, 24, 28],
]);
});

it('should compute on empty matrices', () => {
const matrix1 = new Matrix([[]]);
const matrix2 = new Matrix(0, 3);
const matrix3 = new Matrix([
[0, 5],
[6, 7],
]);
const product12 = matrix1.kroneckerProduct(matrix2);
const product13 = matrix1.kroneckerProduct(matrix3);
const product23 = matrix2.kroneckerProduct(matrix3);

expect(product12.rows).toBe(0);
expect(product12.columns).toBe(0);
expect(product13.rows).toBe(2);
expect(product13.columns).toBe(0);
expect(product23.rows).toBe(0);
expect(product23.columns).toBe(6);
});
});
14 changes: 14 additions & 0 deletions src/__tests__/matrix/mean.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ describe('mean by row and columns', () => {
[1, 2, 3],
[4, 5, 6],
]);
const zeroRowMatrix = new Matrix(0, 2);
const zeroColumnMatrix = new Matrix(1, 0);
it('mean by row', () => {
expect(matrix.mean('row')).toStrictEqual([2, 5]);
});
Expand All @@ -16,4 +18,16 @@ describe('mean by row and columns', () => {
it('mean all', () => {
expect(matrix.mean()).toBe(3.5);
});

it('means of 0 row matrix', () => {
expect(zeroRowMatrix.mean('row')).toStrictEqual([]);
expect(zeroRowMatrix.mean('column')).toStrictEqual([NaN, NaN]);
expect(zeroRowMatrix.mean()).toStrictEqual(NaN);
});

it('means of 0 column matrix', () => {
expect(zeroColumnMatrix.mean('row')).toStrictEqual([NaN]);
expect(zeroColumnMatrix.mean('column')).toStrictEqual([]);
expect(zeroColumnMatrix.mean()).toStrictEqual(NaN);
});
});
111 changes: 110 additions & 1 deletion src/__tests__/matrix/minMax.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Matrix } from '../..';
import { getSquareMatrix } from '../../../testUtils';

describe('min - max', () => {
describe('elementwise min - max', () => {
const matrix1 = new Matrix([
[0, 1, 2],
[3, 4, 5],
Expand All @@ -10,6 +11,9 @@ describe('min - max', () => {
[-6, 2, 12],
]);

const empty1 = new Matrix(2, 0);
const empty2 = new Matrix(2, 0);

it('min', () => {
expect(Matrix.min(matrix1, matrix2).to2DArray()).toStrictEqual([
[0, 0, 2],
Expand All @@ -23,4 +27,109 @@ describe('min - max', () => {
[3, 4, 12],
]);
});

it('empty matrix max', () => {
expect(Matrix.max(empty1, empty2).to2DArray()).toStrictEqual([[], []]);
});

it('empty matrix min', () => {
expect(Matrix.min(empty1, empty2).to2DArray()).toStrictEqual([[], []]);
});
});

describe('matrix min/max', () => {
it('empty matrix', () => {
const emptyMatrix = new Matrix(0, 3);
const min = emptyMatrix.min();
const max = emptyMatrix.max();

expect(min).toBe(NaN);
expect(max).toBe(NaN);

expect(() => emptyMatrix.maxIndex()).toThrow(
'Empty matrix has no elements to index',
);
expect(() => emptyMatrix.minIndex()).toThrow(
'Empty matrix has no elements to index',
);
});

it('3x2 matrix', () => {
const mat = new Matrix([
[1, 2],
[7, 3],
[-1, 5],
]);
const min = mat.min();
const max = mat.max();
const minIndex = mat.minIndex();
const maxIndex = mat.maxIndex();
expect(min).toBe(-1);
expect(max).toBe(7);
expect(minIndex).toStrictEqual([2, 0]);
expect(maxIndex).toStrictEqual([1, 0]);
});
});

describe('vector min/max', () => {
const emptyMatrix = new Matrix(0, 0);
const zeroRowMatrix = new Matrix(0, 2);
const zeroColumnMatrix = new Matrix(3, 0);
const squareMatrix = getSquareMatrix();

it('maxRowIndex', () => {
expect(() => emptyMatrix.maxRowIndex(0)).toThrow('Row index out of range');
expect(() => zeroColumnMatrix.maxRowIndex(0)).toThrow(
'Empty matrix has no elements to index',
);
expect(squareMatrix.maxRowIndex(0)).toStrictEqual([0, 1]);
});

it('minRowIndex', () => {
expect(() => emptyMatrix.minRowIndex(0)).toThrow('Row index out of range');
expect(() => zeroColumnMatrix.minRowIndex(0)).toThrow(
'Empty matrix has no elements to index',
);
expect(squareMatrix.minRowIndex(0)).toStrictEqual([0, 2]);
});

it('maxColumnIndex', () => {
expect(() => emptyMatrix.maxColumnIndex(0)).toThrow(
'Column index out of range',
);
expect(() => zeroRowMatrix.maxColumnIndex(0)).toThrow(
'Empty matrix has no elements to index',
);
expect(squareMatrix.maxColumnIndex(2)).toStrictEqual([1, 2]);
});

it('minColumnIndex', () => {
expect(() => emptyMatrix.minColumnIndex(0)).toThrow(
'Column index out of range',
);
expect(() => zeroRowMatrix.minColumnIndex(0)).toThrow(
'Empty matrix has no elements to index',
);
expect(squareMatrix.minColumnIndex(2)).toStrictEqual([2, 2]);
});

it('maxRow', () => {
expect(zeroColumnMatrix.maxRow(0)).toBe(NaN);
expect(squareMatrix.maxRow(1)).toBe(11);
});

it('minRow', () => {
expect(zeroColumnMatrix.minRow(0)).toBe(NaN);
expect(squareMatrix.minRow(1)).toBe(1);
});

it('maxColumn', () => {
expect(zeroRowMatrix.maxColumn(0)).toBe(NaN);
expect(squareMatrix.maxColumn(0)).toBe(9);
});

it('minColumn', () => {
expect(zeroRowMatrix.minColumn(0)).toBe(NaN);
expect(squareMatrix.minColumn(0)).toBe(1);
});
});
20 changes: 20 additions & 0 deletions src/__tests__/matrix/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ describe('product by row and columns', () => {
[1, 2, 3],
[4, 5, 6],
]);
const emptyMatrix = new Matrix(0, 0);
const zeroRowMatrix = new Matrix(0, 2);
const zeroColumnMatrix = new Matrix(3, 0);

it('product by row', () => {
expect(matrix.product('row')).toStrictEqual([6, 120]);
});
Expand All @@ -16,4 +20,20 @@ describe('product by row and columns', () => {
it('product all', () => {
expect(matrix.product()).toBe(720);
});

it('product by row of empty matrix', () => {
expect(emptyMatrix.product('row')).toStrictEqual([]);
});

it('product by column of empty matrix', () => {
expect(emptyMatrix.product('column')).toStrictEqual([]);
});

it('product by column of 0 row matrix', () => {
expect(zeroRowMatrix.product('column')).toStrictEqual([1, 1]);
});

it('product by row of 0 column matrix', () => {
expect(zeroColumnMatrix.product('row')).toStrictEqual([1, 1, 1]);
});
});
Loading

0 comments on commit 211de6e

Please sign in to comment.