Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ var error = Matrix.sub(B, A.mmul(x)); // The error enables to evaluate the solut
```
#### Decompositions

`QrDecomposition` along with `LuDecomposition` need a matrix with at least as many rows as columns. A matrix carrying more columns than rows is rejected with a `RangeError`.

##### QR Decomposition
```js
var A = new Matrix([
Expand Down
6 changes: 6 additions & 0 deletions matrix.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,9 @@ export class CholeskyDecomposition {
export { CholeskyDecomposition as CHO };

/**
* The LU decomposition of a matrix with at least as many rows as columns.
* A matrix carrying more columns than rows is rejected with a `RangeError`.
*
* @link https://github.com/lutzroeder/Mapack/blob/master/Source/LuDecomposition.cs
*/
export class LuDecomposition {
Expand All @@ -1585,6 +1588,9 @@ export class LuDecomposition {
export { LuDecomposition as LU };

/**
* The QR decomposition of a matrix with at least as many rows as columns.
* A matrix carrying more columns than rows is rejected with a `RangeError`.
*
* @link https://github.com/lutzroeder/Mapack/blob/master/Source/QrDecomposition.cs
*/
export class QrDecomposition {
Expand Down
9 changes: 9 additions & 0 deletions src/__tests__/decompositions/lu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ describe('LU decomposition', () => {
new LU([
[0, 1, 2],
[0, 1, 2],
]),
).toThrow('Matrix must have at least as many rows as columns');

expect(
() =>
new LU([
[0, 1],
[0, 1],
[0, 1],
]).determinant,
).toThrow('Matrix must be square');
});
Expand Down
74 changes: 74 additions & 0 deletions src/__tests__/decompositions/shape.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, it, expect } from 'vitest';

import { Matrix, LuDecomposition, QrDecomposition, solve } from '../..';

const message = /^Matrix must have at least as many rows as columns$/;

describe('LU and QR need at least as many rows as columns', () => {
const wide = new Matrix([
[1, 2, 3],
[4, 5, 6],
]);

it('LU rejects a wide matrix', () => {
expect(() => new LuDecomposition(wide)).toThrow(message);
});

it('QR rejects a wide matrix', () => {
expect(() => new QrDecomposition(wide)).toThrow(message);
});

it('LU rejects a wide 2D array', () => {
expect(() => new LuDecomposition([[1, 2, 3, 4]])).toThrow(message);
});

it('QR rejects a wide 2D array', () => {
expect(() => new QrDecomposition([[1, 2, 3, 4]])).toThrow(message);
});

it('solve reports the shape rather than a rank problem', () => {
expect(() => solve(wide, Matrix.columnVector([1, 2]))).toThrow(message);
});
});

describe('LU and QR keep working on the supported shapes', () => {
const tall = new Matrix([
[1, 2],
[3, 4],
[5, 6],
]);
const square = new Matrix([
[4, 3],
[6, 3],
]);

it('QR on a tall matrix rebuilds the input', () => {
const qr = new QrDecomposition(tall);
const product = qr.orthogonalMatrix.mmul(qr.upperTriangularMatrix);
for (let i = 0; i < tall.rows; i++) {
for (let j = 0; j < tall.columns; j++) {
expect(product.get(i, j)).toBeCloseTo(tall.get(i, j), 10);
}
}
});

it('LU on a tall matrix reports a usable triangular pair', () => {
const lu = new LuDecomposition(tall);
expect(lu.lowerTriangularMatrix.rows).toBe(3);
expect(lu.upperTriangularMatrix.columns).toBe(2);
});

it('LU on a square matrix stays solvable', () => {
const lu = new LuDecomposition(square);
expect(lu.isSingular()).toBe(false);
const x = lu.solve(Matrix.columnVector([10, 12]));
const back = square.mmul(x);
expect(back.get(0, 0)).toBeCloseTo(10, 10);
expect(back.get(1, 0)).toBeCloseTo(12, 10);
});

it('an empty matrix is still accepted', () => {
expect(() => new LuDecomposition(new Matrix(0, 0))).not.toThrow();
expect(() => new QrDecomposition(new Matrix(0, 0))).not.toThrow();
});
});
3 changes: 3 additions & 0 deletions src/dc/lu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import WrapperMatrix2D from '../wrap/WrapperMatrix2D';
export default class LuDecomposition {
constructor(matrix) {
matrix = WrapperMatrix2D.checkMatrix(matrix);
if (matrix.rows < matrix.columns) {
throw new RangeError('Matrix must have at least as many rows as columns');
}

let lu = matrix.clone();
let rows = lu.rows;
Expand Down
3 changes: 3 additions & 0 deletions src/dc/qr.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { hypotenuse } from './util';
export default class QrDecomposition {
constructor(value) {
value = WrapperMatrix2D.checkMatrix(value);
if (value.rows < value.columns) {
throw new RangeError('Matrix must have at least as many rows as columns');
}

let qr = value.clone();
let m = value.rows;
Expand Down