Skip to content

Commit ff4143f

Browse files
committed
feat: split peakpicking into differents algorithms
1 parent 71eb0ae commit ff4143f

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

src/XIndex.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface XIndex {
2+
x: number;
3+
index: number;
4+
}

src/algorithms/firstDerivative.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { NumberArray } from 'cheminfo-types';
2+
3+
import type { XIndex } from '../XIndex.ts';
4+
import type { GSDPeakID } from '../gsd.ts';
5+
6+
export function firstDerivative(input: {
7+
x: NumberArray;
8+
y: NumberArray;
9+
yData: NumberArray;
10+
dY: NumberArray;
11+
ddY: NumberArray;
12+
yThreshold: number;
13+
dX: number;
14+
}) {
15+
const { x, y, yData, dY, ddY, dX, yThreshold } = input;
16+
17+
let lastMax: XIndex | null = null;
18+
let lastMin: XIndex | null = null;
19+
const crossDy: number[] = [];
20+
const intervalL: XIndex[] = [];
21+
const intervalR: XIndex[] = [];
22+
for (let i = 1; i < y.length - 1; ++i) {
23+
if (
24+
(dY[i] < dY[i - 1] && dY[i] <= dY[i + 1]) ||
25+
(dY[i] <= dY[i - 1] && dY[i] < dY[i + 1])
26+
) {
27+
lastMin = {
28+
x: x[i],
29+
index: i,
30+
};
31+
if (dX > 0 && lastMax !== null) {
32+
intervalL.push(lastMax);
33+
intervalR.push(lastMin);
34+
}
35+
}
36+
37+
// Maximum in first derivative
38+
if (
39+
(dY[i] >= dY[i - 1] && dY[i] > dY[i + 1]) ||
40+
(dY[i] > dY[i - 1] && dY[i] >= dY[i + 1])
41+
) {
42+
lastMax = {
43+
x: x[i],
44+
index: i,
45+
};
46+
if (dX < 0 && lastMin !== null) {
47+
intervalL.push(lastMax);
48+
intervalR.push(lastMin);
49+
}
50+
}
51+
if ((dY[i] < 0 && dY[i + 1] > 0) || (dY[i] > 0 && dY[i + 1] < 0)) {
52+
// push the index of the element closer to zero
53+
crossDy.push(Math.abs(dY[i]) < Math.abs(dY[i + 1]) ? i : i + 1);
54+
}
55+
// Handle exact zero
56+
if (dY[i] === 0) {
57+
crossDy.push(i);
58+
}
59+
}
60+
61+
let lastK = -1;
62+
const peaks: GSDPeakID[] = [];
63+
for (let i = 0; i < intervalL.length; i++) {
64+
let minDistance = Number.POSITIVE_INFINITY;
65+
const intervalWidth = (intervalR[i].x - intervalL[i].x) / 3;
66+
67+
let possible = -1;
68+
for (let k = lastK + 1; k < crossDy.length; k++) {
69+
const crossDyIndex = crossDy[k];
70+
if (yData[crossDyIndex] < yThreshold) {
71+
continue;
72+
}
73+
74+
const deltaX = x[crossDyIndex];
75+
const currentDistance = Math.abs(
76+
deltaX - (intervalL[i].x + intervalR[i].x) / 2,
77+
);
78+
79+
if (currentDistance < intervalWidth) {
80+
if (currentDistance < minDistance) {
81+
possible = k;
82+
}
83+
lastK = k;
84+
}
85+
86+
if (currentDistance >= minDistance) break;
87+
minDistance = currentDistance;
88+
}
89+
90+
if (possible !== -1) {
91+
const crossdYIndex = crossDy[possible];
92+
const width = Math.abs(intervalR[i].x - intervalL[i].x);
93+
peaks.push({
94+
id: crypto.randomUUID(),
95+
x: x[crossdYIndex],
96+
y: y[crossdYIndex],
97+
width,
98+
index: crossdYIndex,
99+
ddY: ddY[crossdYIndex],
100+
inflectionPoints: {
101+
from: intervalL[i],
102+
to: intervalR[i],
103+
},
104+
});
105+
}
106+
}
107+
108+
return peaks;
109+
}

src/algorithms/secondDerivative.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { NumberArray } from 'cheminfo-types';
2+
3+
import type { XIndex } from '../XIndex.ts';
4+
import type { GSDPeakID } from '../gsd.ts';
5+
6+
export function secondDerivative(input: {
7+
x: NumberArray;
8+
y: NumberArray;
9+
dY: NumberArray;
10+
ddY: NumberArray;
11+
yThreshold: number;
12+
dX: number;
13+
}) {
14+
const { x, y, yData, dY, ddY, dX, yThreshold } = input;
15+
let lastMax: XIndex | null = null;
16+
let lastMin: XIndex | null = null;
17+
const minddY: number[] = [];
18+
const intervalL: XIndex[] = [];
19+
const intervalR: XIndex[] = [];
20+
21+
// By the intermediate value theorem We cannot find 2 consecutive maximum or minimum
22+
for (let i = 1; i < y.length - 1; ++i) {
23+
if (
24+
(dY[i] < dY[i - 1] && dY[i] <= dY[i + 1]) ||
25+
(dY[i] <= dY[i - 1] && dY[i] < dY[i + 1])
26+
) {
27+
lastMin = {
28+
x: x[i],
29+
index: i,
30+
};
31+
if (dX > 0 && lastMax !== null) {
32+
intervalL.push(lastMax);
33+
intervalR.push(lastMin);
34+
}
35+
}
36+
37+
// Maximum in first derivative
38+
if (
39+
(dY[i] >= dY[i - 1] && dY[i] > dY[i + 1]) ||
40+
(dY[i] > dY[i - 1] && dY[i] >= dY[i + 1])
41+
) {
42+
lastMax = {
43+
x: x[i],
44+
index: i,
45+
};
46+
if (dX < 0 && lastMin !== null) {
47+
intervalL.push(lastMax);
48+
intervalR.push(lastMin);
49+
}
50+
}
51+
52+
// Minimum in second derivative
53+
if (ddY[i] < ddY[i - 1] && ddY[i] < ddY[i + 1]) {
54+
minddY.push(i);
55+
}
56+
}
57+
let lastK = -1;
58+
const peaks: GSDPeakID[] = [];
59+
for (let i = 0; i < intervalL.length; i++) {
60+
let minDistance = Number.POSITIVE_INFINITY;
61+
const intervalWidth = (intervalR[i].x - intervalL[i].x) / 3;
62+
63+
let possible = -1;
64+
for (let k = lastK + 1; k < minddY.length; k++) {
65+
const minddYIndex = minddY[k];
66+
if (yData[minddYIndex] < yThreshold) {
67+
continue;
68+
}
69+
70+
const deltaX = x[minddYIndex];
71+
const currentDistance = Math.abs(
72+
deltaX - (intervalL[i].x + intervalR[i].x) / 2,
73+
);
74+
75+
if (currentDistance < intervalWidth) {
76+
if (currentDistance < minDistance) {
77+
possible = k;
78+
}
79+
lastK = k;
80+
}
81+
82+
if (currentDistance >= minDistance) break;
83+
minDistance = currentDistance;
84+
}
85+
86+
if (possible !== -1) {
87+
const minddYIndex = minddY[possible];
88+
const width = Math.abs(intervalR[i].x - intervalL[i].x);
89+
peaks.push({
90+
id: crypto.randomUUID(),
91+
x: x[minddYIndex],
92+
y: yData[minddYIndex],
93+
width,
94+
index: minddYIndex,
95+
ddY: ddY[minddYIndex],
96+
inflectionPoints: {
97+
from: intervalL[i],
98+
to: intervalR[i],
99+
},
100+
});
101+
}
102+
}
103+
104+
return peaks;
105+
}

0 commit comments

Comments
 (0)