Skip to content

Commit 1ec8a91

Browse files
authored
feat!: new output and a few features (#21)
refactor!: main functions return the same type refactor!: use named exports in functions feat: intercepting at other than zero feat: log some execution information refactor: destructure fn parameters refactor: increase test coverage refactor: add function overloads
1 parent 16fb211 commit 1ec8a91

33 files changed

+582
-290
lines changed

README.md

+44-33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cnnls
1+
# fcnnls
22

33
[![NPM version][npm-image]][npm-url]
44
[![build status][ci-image]][ci-url]
@@ -8,73 +8,84 @@ cnnls
88

99
Fast Combinatorial Non-negative Least Squares.
1010

11-
Fast algorithm for the solution of large‐scale non‐negativity‐constrained least squares problems from Van Benthem and Keenan ([10.1002/cem.889](http://doi.org/10.1002/cem.889)), based on the active-set method algorithm published by Lawson and Hanson.
11+
As described in the publication by Van Benthem and Keenan ([10.1002/cem.889](http://doi.org/10.1002/cem.889)), which is in turn based on the active-set method algorithm previously published by Lawson and Hanson. The basic active-set method is implemented in the [nnls repository.](https://github.com/mljs/nnls)
1212

13-
It solves the following optimisation problem.
14-
Given $\mathbf{X}$ an $n \times l$ matrix and $\mathbf{Y}$ an $n\times p$, find $$\mathrm{argmin}_K ||\mathbf{XK} -\mathbf{Y}||^2_F$$ subject to $\mathbf{K}\geq 0$, where $\mathbf{K}$ is an $l \times p$ matrix and $||\ldots||_F$ is the Frobenius norm. In fact, $\mathbf{K}$ is the best solution to the equation: $\mathbf{XK}=\mathbf{Y}$, where $\mathbf{K} \geq 0$, it performs the regular Non-negative Least Squares algorithm and finds a vector as a solution to the problem. Also, performing this algorithm when $\mathbf{Y}$ is a matrix is like running the algorithm on each column of $\mathbf{Y}$, it will give the same result but in a much more efficient way.
13+
Given the matrices $\mathbf{X}$ and $\mathbf{Y}$, the code finds the matrix $\mathbf{K}$ that minimises the squared Frobenius norm $$\mathrm{argmin}_K ||\mathbf{XK} -\mathbf{Y}||^2_F$$ subject to $\mathbf{K}\geq 0$.
1514

1615
https://en.wikipedia.org/wiki/Non-negative_least_squares
1716

1817
## Installation
1918

20-
`$ npm i ml-fcnnls`
19+
```bash
20+
npm i ml-fcnnls
21+
```
2122

22-
## [API Documentation](https://mljs.github.io/fcnnls/)
23+
## Usage Example
2324

24-
## Usage
25+
1. Single $y$, using arrays as inputs.
26+
27+
```js
28+
import { fcnnlsVector } from 'ml-fcnnls';
29+
30+
const X = [
31+
[1, 1, 2],
32+
[10, 11, -9],
33+
[-1, 0, 0],
34+
[-5, 6, -7],
35+
];
36+
const y = [-1, 11, 0, 1];
37+
38+
const k = fcnnlsVector(X, y).K.to1DArray();
39+
/* k = [0.4610, 0.5611, 0] */
40+
```
41+
42+
2. Multiple RHS, using `Matrix` instances as inputs.
2543

2644
```js
27-
import { Matrix } from 'ml-matrix';
2845
import { fcnnls } from 'ml-fcnnls';
46+
import { Matrix } from 'ml-matrix'; //npm i ml-matrix
2947

3048
// Example with multiple RHS
3149

32-
let X = new Matrix([
50+
const X = new Matrix([
3351
[1, 1, 2],
3452
[10, 11, -9],
3553
[-1, 0, 0],
3654
[-5, 6, -7],
3755
]);
3856

39-
// Y can either be a Matrix of an array of array
40-
let Y = new Matrix([
57+
// Y can either be a Matrix or an array of arrays
58+
const Y = new Matrix([
4159
[-1, 0, 0, 9],
4260
[11, -20, 103, 5],
4361
[0, 0, 0, 0],
4462
[1, 2, 3, 4],
4563
]);
4664

47-
let K = fcnnls(X, Y);
48-
65+
const K = fcnnls(X, Y).K;
66+
// `K.to2DArray()` converts the matrix to array.
4967
/*
5068
K = Matrix([
51-
[0.461, 0, 4.9714, 0],
69+
[0.4610, 0, 4.9714, 0],
5270
[0.5611, 0, 4.7362, 2.2404],
5371
[0, 1.2388, 0, 1.9136],
54-
])
72+
])
5573
*/
74+
```
5675

57-
import { fcnnlsVector } from 'ml-fcnnls';
58-
59-
// Example with single RHS and same X
60-
// Should be giving a vector with the element of the first column of K in the previous example, since y is the first column of Y
61-
62-
let X = new Matrix([
63-
[1, 1, 2],
64-
[10, 11, -9],
65-
[-1, 0, 0],
66-
[-5, 6, -7],
67-
]);
68-
69-
let y = [-1, 11, 0, 1];
70-
71-
let k = fcnnlsVector(X, y);
76+
3. Using the options
7277

73-
/*
74-
k = [0.461, 0.5611, 0]
75-
*/
78+
```js
79+
const K = fcnnls(X, Y, {
80+
info: true, // returns the error/iteration.
81+
maxIterations: 5,
82+
gradientTolerance: 0,
83+
});
84+
/* same result than 2*/
7685
```
7786

87+
## [API Documentation](https://mljs.github.io/fcnnls/)
88+
7889
## License
7990

8091
[MIT](./LICENSE)

package.json

+7-8
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@
4242
},
4343
"homepage": "https://github.com/mljs/fcnnls#readme",
4444
"devDependencies": {
45-
"@types/jest": "^29.5.3",
46-
"@vitest/coverage-v8": "^0.33.0",
47-
"eslint": "^8.45.0",
45+
"@vitest/coverage-v8": "^0.34.5",
46+
"eslint": "^8.50.0",
4847
"eslint-config-cheminfo-typescript": "^12.0.4",
49-
"prettier": "^3.0.0",
50-
"rimraf": "^5.0.1",
51-
"typescript": "^5.1.6",
52-
"vitest": "^0.33.0"
48+
"prettier": "^3.0.3",
49+
"rimraf": "^5.0.5",
50+
"typescript": "^5.2.2",
51+
"vitest": "^0.34.5"
5352
},
5453
"dependencies": {
55-
"ml-matrix": "^6.10.4"
54+
"ml-matrix": "^6.10.5"
5655
}
5756
}

src/__tests__/assertResult.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
import { type Matrix } from 'ml-matrix';
1+
import { Matrix } from 'ml-matrix';
22
import { expect } from 'vitest';
33

4-
// used for most tests here and in fcnnlsVector.test.ts
5-
export function assertResult(result: Matrix, solution: Matrix, precision = 4) {
4+
import { type FcnnlsOutput } from '../fcnnls';
5+
6+
/**
7+
* We only use the value K from the output.
8+
* @param output - The output of the fcnnls function
9+
* @param solution - The expected solution
10+
* @param precision - Number of digits to match
11+
*/
12+
export function assertResult(
13+
output: FcnnlsOutput,
14+
solution: Matrix,
15+
precision = 4,
16+
) {
17+
const result = output.K;
18+
solution = Matrix.checkMatrix(solution);
619
for (let i = 0; i < result.rows; i++) {
720
for (let j = 0; j < result.columns; j++) {
821
const sol = solution.get(i, j);

src/__tests__/cssls.test.ts

+24-25
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@ import { cssls } from '../cssls';
55
import { initialisation } from '../initialisation';
66

77
import { assertResult } from './assertResult';
8+
import { prepareInput } from './prepareInitInput';
89

10+
function csslsResult(X: Matrix, Y: Matrix, useNull?: boolean) {
11+
const input = prepareInput(X, Y);
12+
const init = initialisation(input);
13+
return cssls({
14+
XtX: input.XtX,
15+
XtY: input.XtY,
16+
Pset: useNull ? null : init.Pset,
17+
nColsX: X.columns,
18+
nColsY: Y.columns,
19+
});
20+
}
921
describe('cssls test', () => {
1022
it('identity X, Y 4x1', () => {
1123
const X = Matrix.eye(4);
1224
const Y = new Matrix([[0], [1], [2], [3]]);
13-
const { l, p, XtX, XtY, Pset } = initialisation(X, Y);
1425
const solution = new Matrix([[0], [1], [2], [3]]);
15-
const result = cssls(XtX, XtY, Pset, l, p);
26+
const result = { K: csslsResult(X, Y) };
1627
assertResult(result, solution);
1728
});
1829
it('identity X, Y 5x3', () => {
@@ -24,15 +35,14 @@ describe('cssls test', () => {
2435
[3, 8, 13],
2536
[4, 9, 14],
2637
]);
27-
const init = initialisation(X, Y);
2838
const solution = new Matrix([
2939
[0, 5, 10],
3040
[1, 6, 11],
3141
[2, 7, 12],
3242
[3, 8, 13],
3343
[4, 9, 14],
3444
]);
35-
const result = cssls(init.XtX, init.XtY, init.Pset, init.l, init.p);
45+
const result = { K: csslsResult(X, Y) };
3646
assertResult(result, solution);
3747
});
3848
it('non-singular square X, Y 3x1', () => {
@@ -42,22 +52,20 @@ describe('cssls test', () => {
4252
[1, 1, 0],
4353
]);
4454
const Y = new Matrix([[-1], [2], [-3]]);
45-
const { l, p, XtX, XtY, Pset } = initialisation(X, Y);
4655
const solution = new Matrix([[-1], [0], [1]]);
47-
const result = cssls(XtX, XtY, Pset, l, p);
56+
const result = { K: csslsResult(X, Y) };
4857
assertResult(result, solution);
4958
});
50-
it('ill-conditionned square X rank 2, Y 3x1', () => {
59+
it('ill-conditioned square X rank 2, Y 3x1', () => {
5160
const X = new Matrix([
5261
[1, 2, 3],
5362
[4, 5, 6],
5463
[7, 8, 9],
5564
]);
5665

5766
const Y = new Matrix([[-1], [0], [10]]);
58-
const { l, p, XtX, XtY, Pset } = initialisation(X, Y);
5967
const solution = new Matrix([[1.0455], [0], [0]]);
60-
const result = cssls(XtX, XtY, Pset, l, p);
68+
const result = { K: csslsResult(X, Y) };
6169
assertResult(result, solution);
6270
});
6371
it('6x3 X full-rank, Y 6x7', () => {
@@ -77,15 +85,13 @@ describe('cssls test', () => {
7785
[1000, 2, 56, 40, 1, 1, 3],
7886
[7, 6, 5, 4, 3, 2, 1],
7987
]);
80-
const { l, p, XtX, XtY, Pset } = initialisation(X, Y);
88+
89+
const result = { K: csslsResult(X, Y) };
8190
const solution = new Matrix([
8291
[203.7567, 0, 0, 0, 0, 0, 0],
8392
[-149.1338, 3.3309, 2.0243, 1.5134, 0, 0, 0],
8493
[0, 0, 0, 0, 1.0827, 0.3911, 0.4738],
8594
]);
86-
const result = Matrix.round(cssls(XtX, XtY, Pset, l, p).mul(10000)).mul(
87-
0.0001,
88-
);
8995
assertResult(result, solution);
9096
});
9197
it('Van Benthem - Keenan example', () => {
@@ -102,23 +108,19 @@ describe('cssls test', () => {
102108
[41, 61, 39],
103109
]);
104110

105-
const { l, p, XtX, XtY, Pset } = initialisation(X, Y);
111+
const result = { K: csslsResult(X, Y) };
106112
const solution = new Matrix([
107113
[0, 0.6873, 0.2836],
108114
[0.6272, 0, 0.2862],
109115
[0.3517, 0.2873, 0.335],
110116
]);
111-
const result = Matrix.round(cssls(XtX, XtY, Pset, l, p).mul(10000)).mul(
112-
0.0001,
113-
);
114117
assertResult(result, solution);
115118
});
116119
it('negative identity X, positive Y', () => {
117120
const X = Matrix.eye(3).mul(-1);
118121
const Y = new Matrix([[1], [2], [3]]);
119-
const init = initialisation(X, Y);
120122
const solution = new Matrix([[-1], [-2], [-3]]);
121-
const result = cssls(init.XtX, init.XtY, null, init.l, init.p);
123+
const result = { K: csslsResult(X, Y, true) };
122124
assertResult(result, solution);
123125
});
124126

@@ -129,9 +131,8 @@ describe('cssls test', () => {
129131
[1, 2, 2, 1],
130132
]);
131133
const Y = new Matrix([[-2], [2], [0]]);
132-
const init = initialisation(X, Y);
133134
const solution = new Matrix([[-2], [0], [0], [2]]);
134-
const result = cssls(init.XtX, init.XtY, null, init.l, init.p);
135+
const result = { K: csslsResult(X, Y, true) };
135136
assertResult(result, solution);
136137
});
137138

@@ -142,9 +143,8 @@ describe('cssls test', () => {
142143
[1, 2, 2, 1],
143144
]);
144145
const Y = new Matrix([[-2], [2], [0]]);
145-
const init = initialisation(X, Y);
146146
const solution = new Matrix([[0], [0], [0], [1]]);
147-
const result = cssls(init.XtX, init.XtY, init.Pset, init.l, init.p);
147+
const result = { K: csslsResult(X, Y) };
148148
assertResult(result, solution);
149149
});
150150

@@ -162,7 +162,6 @@ describe('cssls test', () => {
162162
[0, 23, 23, 23, 0, 0, 0, 23, 23],
163163
]);
164164
const Y = new Matrix([[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]);
165-
const init = initialisation(X, Y);
166165
const solution = new Matrix([
167166
[-3.62e-1],
168167
[1.16712],
@@ -174,7 +173,7 @@ describe('cssls test', () => {
174173
[0.806154],
175174
[-4.54969],
176175
]);
177-
const result = cssls(init.XtX, init.XtY, null, init.l, init.p);
176+
const result = { K: csslsResult(X, Y, true) };
178177
assertResult(result, solution, 3);
179178
});
180179
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//plane shifted 5 units
2+
3+
const X = [
4+
[0, 0],
5+
[1, 3],
6+
[2, 4],
7+
[3, 6],
8+
[4, 8],
9+
[5, 10],
10+
[6, 12],
11+
[7, 14],
12+
[8, 16],
13+
[9, 18],
14+
[10, 20],
15+
[11, 22],
16+
[12, 24],
17+
[13, 26],
18+
[14, 28],
19+
[15, 30],
20+
[16, 32],
21+
[17, 34],
22+
[18, 36],
23+
[19, 38],
24+
[20, 40],
25+
];
26+
const Y = [
27+
0 + 5,
28+
2.01 + 5,
29+
3 + 5,
30+
4.5 + 5,
31+
5.8 + 5,
32+
7.1 + 5,
33+
9.05 + 5,
34+
10.5 + 5,
35+
12 + 5,
36+
13.5 + 5,
37+
15 + 5,
38+
16.5 + 5,
39+
18 + 5,
40+
19.5 + 5,
41+
21.4 + 5,
42+
22.5 + 5,
43+
24 + 5,
44+
25.5 + 5,
45+
27 + 5,
46+
28.5 + 5,
47+
30 + 5,
48+
];
49+
50+
const X5 = X.map((x) => [1, ...x]);
51+
export const data = { X, X5, Y };

0 commit comments

Comments
 (0)