From 37415e840f970c07e52d9604faebd9d131ed42ed Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 29 Jul 2026 14:35:45 +0200 Subject: [PATCH 1/3] chore: update dependencies to latest Bump devDependencies to their latest versions and raise the ml-matrix requirement to ^6.14.0, which adds Matrix.transposeMultiply(). TypeScript stays on ^6: typescript-eslint does not support the TS 7 API yet (typescript-eslint/typescript-eslint#10940), so `npm run eslint` fails outright under TS 7.0. Also add .DS_Store to src/.npmignore. Assisted-By: Claude Opus 5 (1M context) --- package.json | 10 +++++----- src/.npmignore | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e3849d2..101be1d 100644 --- a/package.json +++ b/package.json @@ -40,17 +40,17 @@ }, "homepage": "https://github.com/mljs/fcnnls#readme", "dependencies": { - "ml-matrix": "^6.13.0" + "ml-matrix": "^6.14.0" }, "devDependencies": { - "@types/node": "^26.0.0", - "@vitest/coverage-istanbul": "^4.1.9", + "@types/node": "^26.1.2", + "@vitest/coverage-istanbul": "^4.1.10", "@zakodium/tsconfig": "^1.0.5", "eslint": "^9.39.4", "eslint-config-cheminfo-typescript": "^22.1.0", - "prettier": "^3.8.4", + "prettier": "^3.9.6", "rimraf": "^6.1.3", "typescript": "^6.0.3", - "vitest": "^4.1.9" + "vitest": "^4.1.10" } } diff --git a/src/.npmignore b/src/.npmignore index d7a3323..76cf446 100644 --- a/src/.npmignore +++ b/src/.npmignore @@ -1,2 +1,3 @@ __tests__ +.DS_Store .npmignore From 8b574e4cce2a8970eb04008f56a7c139e405f7c3 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 29 Jul 2026 14:35:54 +0200 Subject: [PATCH 2/3] perf: compute XtY with Matrix.transposeMultiply X.transpose().mmul(Y) materialized the transpose and multiplied densely. transposeMultiply() (new in ml-matrix 6.14.0) streams each shared row as a rank-1 update and skips zero entries, so both operands stay row-major and sparse predictors cost far less. The result is identical. Measured on Node 26, median of 5-40 runs, seeded inputs: XtY alone fcnnls() end to end dense X 300x40 Y 300x30 0.39 -> 0.27 ms 4.44 -> 4.40 ms sparse X 1000x60 Y 1000x50 3.19 -> 0.34 ms 14.53 -> 10.83 ms (1.34x) sparse X 2000x80 Y 2000x60 10.3 -> 0.73 ms 35.22 -> 25.95 ms (1.36x) The dense case barely moves because XtY is a small share of the total work there; on the sparse matrices typical of deconvolution it is around a quarter of the runtime. Assisted-By: Claude Opus 5 (1M context) --- src/fcnnls.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fcnnls.ts b/src/fcnnls.ts index 2a0f063..bf01727 100644 --- a/src/fcnnls.ts +++ b/src/fcnnls.ts @@ -70,10 +70,10 @@ export function fcnnls( // pre-computes part of pseudo-inverse. XtX is the symmetric Gram matrix XᵀX; // gram() computes this bit-identically to Xt.mmul(X) but only fills the upper // triangle, skips zeros and never materializes the transpose, so it is much - // faster on the sparse predictor matrices common here. - const Xt = X.transpose(); + // faster on the sparse predictor matrices common here. transposeMultiply() + // applies the same trick to XᵀY. const XtX = X.gram(); - const XtY = Xt.mmul(Y); + const XtY = X.transposeMultiply(Y); const { columns: nColsY, rows: nRowsY } = Y; const { columns: nColsX, rows: nRowsX } = X; From b0bf02e29a5785369bef103c183697353c693ce2 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Wed, 29 Jul 2026 14:35:54 +0200 Subject: [PATCH 3/3] docs: document all options and the info result interceptAtZero was never documented, and the options example assigned the whole result to `K` even though `info: true` returns `{ K, info }`. Add an options table with defaults and describe the info shape. Assisted-By: Claude Opus 5 (1M context) --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb890dd..5ea69c7 100644 --- a/README.md +++ b/README.md @@ -76,14 +76,30 @@ K = Matrix([ 3. Using the options ```js -const K = fcnnls(X, Y, { +const { K, info } = fcnnls(X, Y, { info: true, // returns the error/iteration. maxIterations: 5, gradientTolerance: 0, }); -/* same result than 2*/ +/* K is the same result as in 2 */ +/* info = { rse: [[...], [...], [...]], iterations: 3 } */ ``` +### Options + +Both `fcnnls` and `fcnnlsVector` accept the same options. + +| Option | Type | Default | Description | +| ------------------- | --------- | --------------- | ------------------------------------------------------------------------------------------------------- | +| `maxIterations` | `number` | `3 * X.columns` | Maximum number of iterations of the active-set loop. | +| `gradientTolerance` | `number` | `1e-5` | Larger values (like `1e-4`) can help when the iteration limit is exceeded. | +| `info` | `boolean` | `false` | When `true`, also returns `info` with the root squared error per column of `Y` and the iteration count. | +| `interceptAtZero` | `boolean` | `true` | Set to `false` to add a column of ones to the left of `X`, fitting an intercept. | + +### Result + +`K` is always a `Matrix` of non-negative coefficients. When `info: true`, the result also carries `info.rse` (root squared error, one row per computation of `K`) and `info.iterations`. + ## [API Documentation](https://mljs.github.io/fcnnls/) ## License