Skip to content

Commit 05b0925

Browse files
authored
fix: first derivative algorithm to detect peaks (#135)
* chore: extract cross zero points checking to unit test * chore: add failling test case * fix: check than back and next index is less and greater than zero * chore: fix eslint * fix: threshold would be the max between noiseLevel and minMaxRatio * chore: add documentation to xGetCrossZeroPoints
1 parent f3fbf78 commit 05b0925

File tree

7 files changed

+57
-22
lines changed

7 files changed

+57
-22
lines changed

src/__tests__/gaussian.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { generateSpectrum } from 'spectrum-generator';
33
import { describe, expect, it } from 'vitest';
44

55
import type { GSDPeak } from '../GSDPeak.js';
6-
import { gsd } from '../gsd.ts';
76
import type { GSDPeakID } from '../gsd.ts';
7+
import { gsd } from '../gsd.ts';
88

99
describe('smooth:false option', () => {
1010
const peaks = [
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect, test } from 'vitest';
2+
3+
import { xGetCrossZeroPoints } from '../algorithms/xGetCrossZeroPoints.ts';
4+
5+
test('cross with exact zero', () => {
6+
const y = [1, 1, 1];
7+
const dY = [1e-10, 0, 1e-8];
8+
9+
const result = xGetCrossZeroPoints({
10+
y,
11+
dY,
12+
});
13+
14+
expect(result).toHaveLength(0);
15+
});

src/algorithms/firstDerivative.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,13 @@
11
import type { PeakData } from './PeakData.ts';
22
import { getMinMaxIntervalsDy } from './getMinMaxIntervals.ts';
33
import { getPeakFromIntervals } from './getPeaksFromIntervals.ts';
4+
import { xGetCrossZeroPoints } from './xGetCrossZeroPoints.ts';
45

56
export function firstDerivative(input: PeakData) {
6-
const { x, y, yData, dY, ddY, dX, yThreshold } = input;
7-
8-
const crossDy: number[] = [];
7+
const { y, x, dY, dX, yData, yThreshold, ddY } = input;
8+
const crossDy = xGetCrossZeroPoints(input);
99
const { intervalL, intervalR } = getMinMaxIntervalsDy(y, x, dY, dX);
1010

11-
for (let i = 1; i < y.length - 1; ++i) {
12-
if ((dY[i] < 0 && dY[i + 1] > 0) || (dY[i] > 0 && dY[i + 1] < 0)) {
13-
// push the index of the element closer to zero
14-
crossDy.push(Math.abs(dY[i]) < Math.abs(dY[i + 1]) ? i : i + 1);
15-
}
16-
// Handle exact zero
17-
if (
18-
dY[i] === 0 &&
19-
dY[i] < Math.abs(dY[i + 1]) &&
20-
dY[i] < Math.abs(dY[i - 1])
21-
) {
22-
crossDy.push(i);
23-
}
24-
}
25-
2611
return getPeakFromIntervals({
2712
minData: crossDy,
2813
intervalL,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { PeakData } from './PeakData.ts';
2+
3+
export type XGetCrossZeroPointsInput = Pick<PeakData, 'y' | 'dY'>;
4+
5+
/**
6+
* Finds the indices where the first derivative crosses zero (sign change),
7+
* which are potential peak positions. This function does not detect zero-crossings
8+
* in regions with consecutive zero values in the derivative (flat regions).
9+
*
10+
* @param input - Object containing the y values and their first derivative (dY).
11+
* @returns Array of indices where the first derivative crosses zero (excluding consecutive zeros).
12+
*/
13+
export function xGetCrossZeroPoints(input: XGetCrossZeroPointsInput) {
14+
const { y, dY } = input;
15+
16+
const crossDy: number[] = [];
17+
18+
for (let i = 1; i < y.length - 1; ++i) {
19+
if (isLessAndGreaterThanZero(dY[i], dY[i + 1])) {
20+
// push the index of the element closer to zero
21+
crossDy.push(Math.abs(dY[i]) < Math.abs(dY[i + 1]) ? i : i + 1);
22+
} else if (
23+
// Handle exact zero
24+
dY[i] === 0 &&
25+
isLessAndGreaterThanZero(dY[i - 1], dY[i + 1])
26+
) {
27+
crossDy.push(i);
28+
}
29+
}
30+
return crossDy;
31+
}
32+
33+
function isLessAndGreaterThanZero(back: number, next: number) {
34+
return (back < 0 && next > 0) || (back > 0 && next < 0);
35+
}

src/gsd.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export function gsd(data: DataXY, options: GSDOptions = {}): GSDPeakID[] {
145145
derivative: 2,
146146
});
147147

148-
const yThreshold = minY + (maxY - minY) * minMaxRatio;
148+
const yThreshold = Math.max(noiseLevel, minY + (maxY - minY) * minMaxRatio);
149149

150150
const dX = x[1] - x[0];
151151

src/post/optimizePeaks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { DataXY, FromTo } from 'cheminfo-types';
22
import type { Shape1D } from 'ml-peak-shape-generator';
33
import type { OptimizationOptions } from 'ml-spectra-fitting';
44

5-
import { optimizePeaksWithLogs } from './optimizePeaksWithLogs.ts';
65
import type { Peak } from './optimizePeaksWithLogs.ts';
6+
import { optimizePeaksWithLogs } from './optimizePeaksWithLogs.ts';
77

88
export interface OptimizePeaksOptions {
99
/**

src/utils/groupPeaks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function groupPeaks<T extends { x: number; width: number }>(
1414
factor?: number;
1515
} = {},
1616
): T[][] {
17-
if (peaks && peaks.length === 0) return [];
17+
if (peaks?.length === 0) return [];
1818

1919
const { factor = 1 } = options;
2020

0 commit comments

Comments
 (0)