Skip to content

Commit 8c9e233

Browse files
authored
feat: implement Whitaker smoother and all the utilities to be used outside (#292)
feat: add sparse cholesky solver and cuthill-mckee feat: add utilities to build a weighted least squared fit/smooth of 1D data feat: add xWhitakerSmoother
1 parent fc7dfd6 commit 8c9e233

File tree

13 files changed

+828
-0
lines changed

13 files changed

+828
-0
lines changed

src/__tests__/__snapshots__/index.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ exports[`test existence of exported functions 1`] = `
7070
"xSum",
7171
"xUniqueSorted",
7272
"xVariance",
73+
"xWhitakerSmoother",
7374
"xyAlign",
7475
"xyCheck",
7576
"xyCovariance",
@@ -140,9 +141,11 @@ exports[`test existence of exported functions 1`] = `
140141
"matrixBoxPlot",
141142
"matrixCenterZMean",
142143
"matrixCheck",
144+
"matrixCholeskySolver",
143145
"matrixClone",
144146
"matrixColumnsCorrelation",
145147
"matrixCreateEmpty",
148+
"matrixCuthillMckee",
146149
"matrixHistogram",
147150
"matrixMaxAbsoluteZ",
148151
"matrixMedian",
@@ -167,5 +170,6 @@ exports[`test existence of exported functions 1`] = `
167170
"nextPowerOfTwo",
168171
"recursiveResolve",
169172
"stringify",
173+
"calculateAdaptiveWeights",
170174
]
171175
`;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { NumberArray } from 'cheminfo-types';
2+
import { expect, test } from 'vitest';
3+
4+
import { addWeights } from '../../utils/addWeights';
5+
import { createSystemMatrix } from '../../utils/createSystemMatrix';
6+
import { xSequentialFillFromTo } from '../../x';
7+
import { matrixCholeskySolver } from '../matrixCholeskySolver';
8+
import { matrixCuthillMckee } from '../matrixCuthillMckee';
9+
10+
type Func = (b: NumberArray) => NumberArray;
11+
test('solve a least square system', () => {
12+
const x = xSequentialFillFromTo({ from: 0, to: Math.PI, size: 101 });
13+
const noise = x.map(() => Math.random() * 0.1 - 0.05);
14+
const y = x.map((value, index) => Math.cos(value) + noise[index]);
15+
const weights = new Float64Array(y.length).fill(1);
16+
y[50] = 0.9; // add outliers
17+
18+
const lambda = 20;
19+
const dimension = x.length;
20+
const upperTriangularNonZeros = createSystemMatrix(dimension, lambda);
21+
22+
const weighted = addWeights(upperTriangularNonZeros, y, weights);
23+
24+
const cho = matrixCholeskySolver(weighted.leftHandSide, dimension) as Func;
25+
expect(cho).not.toBeNull();
26+
const smoothed = cho(weighted.rightHandSide);
27+
expect(smoothed[50]).toBeLessThan(0.2);
28+
expect(smoothed[50]).toBeGreaterThan(-0.2);
29+
30+
//ignore the outlier, it implicates the smooth should pass closer to zero.
31+
weights[50] = 0;
32+
const weighted2 = addWeights(upperTriangularNonZeros, y, weights);
33+
const cho2 = matrixCholeskySolver(weighted.leftHandSide, dimension) as Func;
34+
expect(cho2).not.toBeNull();
35+
const smoothed2 = cho2(weighted2.rightHandSide);
36+
expect(smoothed2[50]).toBeLessThan(smoothed[50]);
37+
38+
const permutationEncodedArray = matrixCuthillMckee(
39+
upperTriangularNonZeros,
40+
dimension,
41+
);
42+
const cho3 = matrixCholeskySolver(
43+
weighted2.leftHandSide,
44+
dimension,
45+
permutationEncodedArray,
46+
) as Func;
47+
48+
expect(cho3).not.toBeNull();
49+
const smoothed3 = cho2(weighted2.rightHandSide);
50+
expect(smoothed3[50]).toEqual(smoothed3[50]);
51+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { NumberArray } from 'cheminfo-types';
2+
import { expect, test } from 'vitest';
3+
4+
import { matrixCuthillMckee } from '../matrixCuthillMckee';
5+
6+
test('permutation to reduce bandwidth', () => {
7+
const dimension = 10;
8+
const matrix = [
9+
[2, 2, 1.5],
10+
[1, 1, 1],
11+
[1, 4, 0.02],
12+
[5, 5, 1.2],
13+
[7, 7, 1.6],
14+
[4, 4, 2.6],
15+
[3, 3, 1.1],
16+
[4, 7, 0.09],
17+
[4, 6, 0.16],
18+
[0, 0, 1.7],
19+
[4, 8, 0.52],
20+
[0, 8, 0.13],
21+
[6, 6, 1.3],
22+
[7, 8, 0.11],
23+
[4, 9, 0.53],
24+
[8, 8, 1.4],
25+
[9, 9, 3.1],
26+
[1, 9, 0.01],
27+
[6, 9, 0.56],
28+
];
29+
30+
const permutationEncodedArray = matrixCuthillMckee(matrix, dimension);
31+
32+
const reducedBandwidthMatrix = permut(
33+
matrix,
34+
permutationEncodedArray,
35+
dimension,
36+
);
37+
expect(bandwidth(reducedBandwidthMatrix)).toBeLessThan(bandwidth(matrix));
38+
});
39+
40+
function permut(
41+
nonZerosArray: NumberArray[],
42+
permutationEncoded: NumberArray,
43+
dimension: number,
44+
) {
45+
const pinv = new Array(dimension);
46+
for (let k = 0; k < dimension; k++) {
47+
pinv[permutationEncoded[k]] = k;
48+
}
49+
const mt: NumberArray[] = new Array(nonZerosArray.length);
50+
for (let a = 0; a < nonZerosArray.length; ++a) {
51+
const [r, c, value] = nonZerosArray[a];
52+
const [ar, ac] = [pinv[r], pinv[c]];
53+
mt[a] = ac < ar ? [ac, ar, value] : [ar, ac, value];
54+
}
55+
return mt;
56+
}
57+
58+
function bandwidth(matrix: NumberArray[]) {
59+
let bandwidth = 0;
60+
for (const [i, j] of matrix) {
61+
const distance = Math.abs(i - j);
62+
if (distance > bandwidth) {
63+
bandwidth = distance;
64+
}
65+
}
66+
return bandwidth;
67+
}

src/matrix/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ export * from './matrixAutoCorrelation';
44
export * from './matrixBoxPlot';
55
export * from './matrixCenterZMean';
66
export * from './matrixCheck';
7+
export * from './matrixCholeskySolver';
78
export * from './matrixClone';
89
export * from './matrixColumnsCorrelation';
910
export * from './matrixCreateEmpty';
11+
export * from './matrixCuthillMckee';
1012
export * from './matrixHistogram';
1113
export * from './matrixMaxAbsoluteZ';
1214
export * from './matrixMedian';

0 commit comments

Comments
 (0)