Skip to content

Commit d8d646a

Browse files
committed
feat: reintroduce an improved noiseLevel option
1 parent 2967ef5 commit d8d646a

File tree

6 files changed

+194
-7
lines changed

6 files changed

+194
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Preprocessing of the data involves the following parameters
1313

1414
- `maxCriteria`: search either for maxima or minima. We will invert the data and the results if searching for a minima
15+
- `noiseLevel`: specifies the noise level. All the peaks bellow this value (or above in case of maxCriteria=false) are ignored. By default the noiseLevel will be set to the median + 3 x sd. This is a good value when not too many peaks are present in the spectrum.
1516
- `sgOptions`: Savitzky-Golay filter that is used to smooth the data for the calculation of the derivatives
1617
- `smoothY`: If this value is true the SG filter is not only applied during the calculation of the derivatives but also on the original data
1718

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,16 @@
6363
"cheminfo-build": "^1.1.11",
6464
"eslint": "^8.9.0",
6565
"eslint-config-cheminfo-typescript": "^10.3.0",
66-
"eslint-plugin-jest": "^26.1.0",
66+
"eslint-plugin-jest": "^26.1.1",
6767
"esm": "^3.2.25",
6868
"jest": "^27.5.1",
6969
"jest-matcher-deep-close-to": "^3.0.2",
70-
"mf-global": "^1.4.20",
70+
"mf-global": "^1.4.22",
7171
"ml-stat": "^1.3.3",
7272
"nodemon": "^2.0.15",
7373
"prettier": "^2.5.1",
7474
"rimraf": "^3.0.2",
75-
"spectrum-generator": "^6.0.4",
75+
"spectrum-generator": "^7.0.0",
7676
"ts-jest": "^27.1.3",
7777
"typescript": "^4.5.5",
7878
"xy-parser": "^4.0.1"
@@ -82,6 +82,6 @@
8282
"ml-peak-shape-generator": "^4.0.3",
8383
"ml-savitzky-golay-generalized": "4.0.0",
8484
"ml-spectra-fitting": "^3.0.4",
85-
"ml-spectra-processing": "^10.0.0"
85+
"ml-spectra-processing": "^10.1.2"
8686
}
8787
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { DataXY } from 'cheminfo-types';
2+
import { toBeDeepCloseTo, toMatchCloseTo } from 'jest-matcher-deep-close-to';
3+
4+
import { gsd } from '../gsd';
5+
6+
expect.extend({ toBeDeepCloseTo, toMatchCloseTo });
7+
8+
// eslint-disable-next-line @typescript-eslint/no-var-requires
9+
const { generateSpectrum } = require('spectrum-generator');
10+
11+
describe('smooth:false option', () => {
12+
const peaks = [
13+
{ x: -0.5, y: 1, width: 0.05 },
14+
{ x: 0.5, y: 1, width: 0.05 },
15+
];
16+
17+
const data: DataXY = generateSpectrum(peaks, {
18+
generator: {
19+
from: -1,
20+
to: 1,
21+
nbPoints: 101,
22+
},
23+
peaks: {
24+
factor: 6,
25+
},
26+
noise: {
27+
percent: 5,
28+
},
29+
});
30+
31+
it('positive maxima peaks', () => {
32+
let peakList = gsd(data);
33+
expect(peakList).toMatchCloseTo([
34+
{ x: -0.5, y: 1.131 },
35+
{ x: 0.5, y: 1.05 },
36+
]);
37+
});
38+
39+
it('negative maxima peaks', () => {
40+
let peakList = gsd({ x: data.x, y: data.y.map((value) => value - 2) }, {});
41+
expect(peakList).toMatchCloseTo([
42+
{ x: -0.5, y: -0.868 },
43+
{ x: 0.5, y: -0.95 },
44+
]);
45+
});
46+
47+
it('Negative peaks', () => {
48+
// we check negative peaks
49+
let peakList = gsd(
50+
{ x: data.x, y: data.y.map((value) => -value) },
51+
{ maxCriteria: false },
52+
);
53+
expect(peakList).toMatchCloseTo([
54+
{ x: -0.5, y: -1.131 },
55+
{ x: 0.5, y: -1.05 },
56+
]);
57+
});
58+
59+
it('minima peaks', () => {
60+
// we check negative peaks
61+
let peakList = gsd(
62+
{ x: data.x, y: data.y.map((value) => 1 - value) },
63+
{ maxCriteria: false },
64+
);
65+
expect(peakList).toMatchCloseTo([
66+
{ x: -0.5, y: -0.131 },
67+
{ x: 0.5, y: -0.05 },
68+
]);
69+
});
70+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { DataXY } from 'cheminfo-types';
2+
import { toBeDeepCloseTo, toMatchCloseTo } from 'jest-matcher-deep-close-to';
3+
4+
import { gsd } from '../gsd';
5+
6+
expect.extend({ toBeDeepCloseTo, toMatchCloseTo });
7+
8+
// eslint-disable-next-line @typescript-eslint/no-var-requires
9+
const { generateSpectrum } = require('spectrum-generator');
10+
11+
describe('smooth:false option', () => {
12+
const peaks = [
13+
{ x: -0.5, y: 1, width: 0.05 },
14+
{ x: 0.5, y: 1, width: 0.05 },
15+
];
16+
17+
const data: DataXY = generateSpectrum(peaks, {
18+
generator: {
19+
from: -1,
20+
to: 1,
21+
nbPoints: 101,
22+
},
23+
peaks: {
24+
factor: 6,
25+
},
26+
noise: {
27+
percent: 5,
28+
},
29+
});
30+
31+
it('positive maxima peaks but noiseLevel over the peaks', () => {
32+
let peakList = gsd(data, { noiseLevel: 1.2 });
33+
expect(peakList).toStrictEqual([]);
34+
});
35+
36+
it('positive maxima peaks', () => {
37+
let peakList = gsd(data, { noiseLevel: 0.5 });
38+
expect(peakList).toMatchCloseTo([
39+
{ x: -0.5, y: 1.131 },
40+
{ x: 0.5, y: 1.05 },
41+
]);
42+
});
43+
44+
it('negative maxima peaks', () => {
45+
let peakList = gsd(
46+
{ x: data.x, y: data.y.map((value) => value - 2) },
47+
{ noiseLevel: -1.5 },
48+
);
49+
expect(peakList).toMatchCloseTo([
50+
{ x: -0.5, y: -0.868 },
51+
{ x: 0.5, y: -0.95 },
52+
]);
53+
});
54+
55+
it('Negative peaks', () => {
56+
// we check negative peaks
57+
let peakList = gsd(
58+
{ x: data.x, y: data.y.map((value) => -value) },
59+
{ maxCriteria: false, noiseLevel: -0.5 },
60+
);
61+
expect(peakList).toMatchCloseTo([
62+
{ x: -0.5, y: -1.131 },
63+
{ x: 0.5, y: -1.05 },
64+
]);
65+
});
66+
67+
it('Negative peaks with noiseLevel too low', () => {
68+
// we check negative peaks
69+
let peakList = gsd(
70+
{ x: data.x, y: data.y.map((value) => -value) },
71+
{ maxCriteria: false, noiseLevel: -1.2 },
72+
);
73+
expect(peakList).toStrictEqual([]);
74+
});
75+
76+
it('minima peaks', () => {
77+
// we check negative peaks
78+
let peakList = gsd(
79+
{ x: data.x, y: data.y.map((value) => 1 - value) },
80+
{ maxCriteria: false, noiseLevel: 0.5 },
81+
);
82+
expect(peakList).toMatchCloseTo([
83+
{ x: -0.5, y: -0.131 },
84+
{ x: 0.5, y: -0.05 },
85+
]);
86+
});
87+
});

src/__tests__/zero.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('Simple test cases', () => {
66
let y = [];
77
expect(() => {
88
gsd({ x, y });
9-
}).toThrow('Window size is higher than the data length 9>0');
9+
}).toThrow('input must not be empty');
1010
});
1111

1212
it('length = 2', () => {

src/gsd.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
xIsMonotoneIncreasing,
66
xMinValue,
77
xMaxValue,
8+
xNoiseStandardDeviation,
89
} from 'ml-spectra-processing';
910

1011
import { GSDPeak } from './GSDPeak';
@@ -30,11 +31,14 @@ export interface GSDOptions {
3031
*/
3132
maxCriteria?: boolean;
3233
/**
33-
* Threshold to determine if a given peak should be considered as a noise
34+
* Peak under the noiseLevel (or over in case of maxCriteria=false) are ignored.
35+
*/
36+
noiseLevel?: number;
37+
/**
38+
* Minimal height of small peaks based on the ratio between min and max
3439
* @default 0.00025
3540
*/
3641
minMaxRatio?: number;
37-
3842
/**
3943
* Use a quadratic optimizations with the peak and its 3 closest neighbors
4044
* @default false
@@ -56,6 +60,7 @@ export function gsd(data: DataXY, options: GSDOptions = {}): GSDPeak[] {
5660
windowSize: 9,
5761
polynomial: 3,
5862
},
63+
noiseLevel,
5964
smoothY = false,
6065
maxCriteria = true,
6166
minMaxRatio = 0.00025,
@@ -72,11 +77,35 @@ export function gsd(data: DataXY, options: GSDOptions = {}): GSDPeak[] {
7277
// we can assume it to be equally spaced variable
7378
let equallySpaced = xIsEquallySpaced(x);
7479

80+
if (noiseLevel === undefined) {
81+
if (equallySpaced) {
82+
const noiseInfo = xNoiseStandardDeviation(y);
83+
if (maxCriteria) {
84+
noiseLevel = noiseInfo.median + 1.5 * noiseInfo.sd;
85+
} else {
86+
noiseLevel = -noiseInfo.median + 1.5 * noiseInfo.sd;
87+
}
88+
} else {
89+
noiseLevel = 0;
90+
}
91+
} else {
92+
if (maxCriteria === false) {
93+
noiseLevel *= -1;
94+
}
95+
}
96+
7597
if (maxCriteria === false) {
7698
for (let i = 0; i < y.length; i++) {
7799
y[i] *= -1;
78100
}
79101
}
102+
if (noiseLevel) {
103+
for (let i = 0; i < y.length; i++) {
104+
if (y[i] < noiseLevel) {
105+
y[i] = noiseLevel;
106+
}
107+
}
108+
}
80109

81110
let yData = y;
82111
let dY, ddY;

0 commit comments

Comments
 (0)