Skip to content

Commit ac23217

Browse files
authored
fix: update PSG n spectra-fitting (#70)
* fix: add prefix I to interfaces rename options of joinBroadPeaks width -> broadWidth, mask -> broadMask * chore: adapt test case * fix: eslint * chore: update dependencies spectrum-generator, ml-spectra-fitting n PSG
1 parent 842a4e5 commit ac23217

File tree

7 files changed

+78
-88
lines changed

7 files changed

+78
-88
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,16 @@
7272
"nodemon": "^2.0.14",
7373
"prettier": "^2.4.1",
7474
"rimraf": "^3.0.2",
75-
"spectrum-generator": "^6.0.0",
75+
"spectrum-generator": "^6.0.1",
7676
"ts-jest": "^27.0.7",
7777
"typescript": "^4.4.4",
7878
"xy-parser": "^3.2.0"
7979
},
8080
"dependencies": {
8181
"cheminfo-types": "^0.8.0",
82-
"ml-peak-shape-generator": "^3.0.2",
82+
"ml-peak-shape-generator": "^3.0.3",
8383
"ml-savitzky-golay-generalized": "2.0.3",
84-
"ml-spectra-fitting": "^3.0.0",
84+
"ml-spectra-fitting": "^3.0.1",
8585
"ml-spectra-processing": "^6.8.0"
8686
}
8787
}

src/__tests__/broadNMR.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('Global spectra deconvolution NMR spectra', () => {
2525
{ x: spectrum[0], y: spectrum[1] },
2626
result,
2727
{
28-
width: 0.25,
28+
broadWidth: 0.25,
2929
broadRatio: 0.0025,
3030
shape: { kind: 'lorentzian' },
3131
sgOptions: {

src/__tests__/massPeakPicking.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ describe('Check the peak picking of a simulated mass spectrum', () => {
4242
kind: 'gaussian',
4343
},
4444
});
45+
const gaussian = new Gaussian();
4546
expect(result[0].x).toBeCloseTo(69.938, 1);
4647
expect(result[0].y).toBeCloseTo(max, 2);
4748
expect(result[0].fwhm).toBeCloseTo(0.01, 4);
48-
expect(result[0].width).toBeCloseTo(Gaussian.fwhmToWidth(0.01), 4);
49+
expect(result[0].width).toBeCloseTo(gaussian.fwhmToWidth(0.01), 4);
4950

5051
expect(result[1].x).toBeCloseTo(71.935, 2);
5152
expect(result[1].y).toBeCloseTo((63.99155 * max) / 100, 3);

src/gsd.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@ import SG from 'ml-savitzky-golay-generalized';
2626
*/
2727

2828
export interface Peak1D {
29-
index?: number;
3029
x: number;
3130
y: number;
3231
width: number;
3332
fwhm?: number;
3433
shape?: Shape1D;
3534
}
36-
export interface LastType {
35+
36+
interface LastType {
3737
x: number;
3838
index: number;
3939
}
4040

41-
export interface GSDOptions {
41+
export interface IGSDOptions {
4242
noiseLevel?: number;
4343
sgOptions?: {
4444
windowSize: number;
@@ -54,7 +54,7 @@ export interface GSDOptions {
5454
factor?: number;
5555
}
5656

57-
export function gsd(data: DataXY, options: GSDOptions = {}): Peak1D[] {
57+
export function gsd(data: DataXY, options: IGSDOptions = {}): Peak1D[] {
5858
let {
5959
noiseLevel,
6060
sgOptions = {
@@ -195,16 +195,17 @@ export function gsd(data: DataXY, options: GSDOptions = {}): Peak1D[] {
195195
}
196196
}
197197

198-
let widthProcessor = getShape1D(shape).widthToFWHM;
198+
let widthToFWHM = getShape1D(shape).widthToFWHM;
199199

200-
let signals: Peak1D[] = [];
201200
let lastK = -1;
202-
let possible: number,
203-
frequency: number,
204-
distanceJ: number,
205-
minDistance: number,
206-
gettingCloser: boolean;
201+
let possible: number;
202+
let frequency: number;
203+
let distanceJ: number;
204+
let minDistance: number;
205+
let gettingCloser: boolean;
207206

207+
const peaks: Peak1D[] = [];
208+
const indexes: number[] = [];
208209
for (const minddYIndex of minddY) {
209210
frequency = xData[minddYIndex];
210211
possible = -1;
@@ -231,29 +232,29 @@ export function gsd(data: DataXY, options: GSDOptions = {}): Peak1D[] {
231232
if (possible !== -1) {
232233
if (Math.abs(yData[minddYIndex]) > minMaxRatio * maxY) {
233234
let width = Math.abs(intervalR[possible].x - intervalL[possible].x);
234-
signals.push({
235-
index: minddYIndex,
235+
indexes.push(minddYIndex);
236+
peaks.push({
236237
x: frequency,
237238
y: maxCriteria
238239
? yData[minddYIndex] + noiseLevel
239240
: -yData[minddYIndex] - noiseLevel,
240241
width: width,
241-
fwhm: widthProcessor(width),
242+
fwhm: widthToFWHM(width),
242243
shape,
243244
});
244245
}
245246
}
246247
}
247248

248249
if (realTopDetection) {
249-
determineRealTop(signals, xData, yData);
250+
determineRealTop({ peaks, x: xData, y: yData, indexes });
250251
}
251252

252-
signals.sort((a, b) => {
253+
peaks.sort((a, b) => {
253254
return a.x - b.x;
254255
});
255256

256-
return signals;
257+
return peaks;
257258
}
258259

259260
const isEqualSpaced = (x: DoubleArray): boolean => {
@@ -297,18 +298,20 @@ const getNoiseLevel = (y: DoubleArray): number => {
297298

298299
return stddev;
299300
};
300-
const determineRealTop = (
301-
peakList: Peak1D[],
302-
x: DoubleArray,
303-
y: DoubleArray,
304-
): void => {
305-
let alpha: number,
306-
beta: number,
307-
gamma: number,
308-
p: number,
309-
currentPoint: number;
310-
for (const peak of peakList) {
311-
currentPoint = peak.index as number; // peakList[j][2];
301+
const determineRealTop = (options: {
302+
peaks: Peak1D[];
303+
x: DoubleArray;
304+
y: DoubleArray;
305+
indexes: number[];
306+
}): void => {
307+
const { peaks, x, y, indexes } = options;
308+
let alpha: number;
309+
let beta: number;
310+
let gamma: number;
311+
let p: number;
312+
for (let i = 0; i < peaks.length; i++) {
313+
const peak = peaks[i];
314+
let currentPoint = indexes[i];
312315
// The detected peak could be moved 1 or 2 units to left or right.
313316
if (
314317
y[currentPoint - 1] >= y[currentPoint - 2] &&
@@ -350,8 +353,6 @@ const determineRealTop = (
350353
beta = 20 * Math.log10(y[currentPoint]);
351354
gamma = 20 * Math.log10(y[currentPoint + 1]);
352355
p = (0.5 * (alpha - gamma)) / (alpha - 2 * beta + gamma);
353-
// console.log(alpha, beta, gamma, `p: ${p}`);
354-
// console.log(x[currentPoint]+" "+tmp+" "+currentPoint);
355356
peak.x = x[currentPoint] + (x[currentPoint] - x[currentPoint - 1]) * p;
356357
peak.y =
357358
y[currentPoint] -

src/post/broadenPeaks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Peak1D } from '../gsd';
55
* Because peaks may not be symmetric after we add 2 properties, from and to.
66
* @return {Array} peakList
77
*/
8-
interface BroadenPeaksOptions {
8+
export interface IBroadenPeaksOptions {
99
/**
1010
* @default 2
1111
*/
@@ -23,7 +23,7 @@ interface InternPeak1D extends Peak1D {
2323
}
2424
export function broadenPeaks(
2525
peakList: Peak1D[],
26-
options: BroadenPeaksOptions = {},
26+
options: IBroadenPeaksOptions = {},
2727
): Peak1D[] {
2828
const { factor = 2, overlap = false } = options;
2929

src/post/joinBroadPeaks.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,77 @@ import type { DataXY } from 'cheminfo-types';
22
import type { Shape1D } from 'ml-peak-shape-generator';
33
import SG from 'ml-savitzky-golay-generalized';
44
import { optimize } from 'ml-spectra-fitting';
5+
import type { IOptimizationOptions } from 'ml-spectra-fitting';
56
import { xFindClosestIndex } from 'ml-spectra-processing';
67

78
import type { Peak1D } from '../gsd';
89

910
/**
1011
* This function try to join the peaks that seems to belong to a broad signal in a single broad peak.
11-
* @param {Array} peaks - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}].
12-
* @param {object} [options = {}] - options
13-
* @param {number} [options.width=0.25] - width limit to join peaks.
14-
* @param {object} [options.shape={}] - it's specify the kind of shape used to fitting.
15-
* @param {string} [options.shape.kind = 'gaussian'] - kind of shape; lorentzian, gaussian and pseudovoigt are supported.
16-
* @param {object} [options.optimization = {}] - it's specify the kind and options of the algorithm use to optimize parameters.
17-
* @param {string} [options.optimization.kind = 'lm'] - kind of algorithm. By default it's levenberg-marquardt.
18-
* @param {number} [options.optimization.options.timeout = 10] - maximum time running before break in seconds.
19-
* @param {object} [options.optimization.options = {}] - options for the specific kind of algorithm.
12+
* @param peaks - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}].
2013
*/
2114

2215
interface GetSoftMaskOptions {
2316
sgOptions: {
2417
windowSize: number;
2518
polynomial: number;
2619
};
20+
/**
21+
* broadRatio
22+
* @default 0.0025
23+
*/
2724
broadRatio: number;
2825
}
2926

30-
interface OptionsType extends Partial<GetSoftMaskOptions> {
31-
width?: number;
27+
export interface IJoinBroadPeaksOptions extends Partial<GetSoftMaskOptions> {
28+
/**
29+
* width limit to join peaks.
30+
* @default 0.25
31+
*/
32+
broadWidth?: number;
33+
/**
34+
* it's specify the kind of shape used to fitting.
35+
*/
3236
shape?: Shape1D;
33-
optimization?: { kind: string; timeout: number };
34-
mask?: boolean[];
37+
/**
38+
* it's specify the kind and options of the algorithm use to optimize parameters.
39+
*/
40+
optimization?: IOptimizationOptions;
41+
broadMask?: boolean[];
3542
}
3643

3744
export function joinBroadPeaks(
3845
data: DataXY,
3946
peakList: Peak1D[],
40-
options: OptionsType = {},
47+
options: IJoinBroadPeaksOptions = {},
4148
): Peak1D[] {
4249
let {
43-
mask,
50+
broadMask,
4451
shape = { kind: 'gaussian' },
45-
optimization = { kind: 'lm', timeout: 10 },
52+
optimization = { kind: 'lm', options: { timeout: 10 } },
4653
sgOptions = {
4754
windowSize: 9,
4855
polynomial: 3,
4956
},
5057
broadRatio = 0.0025,
58+
broadWidth = 0.25,
5159
} = options;
52-
let { width = 0.25 } = options;
5360

5461
let max = 0;
5562
let maxI = 0;
5663
let count = 1;
5764
const broadLines: Peak1D[] = [];
5865
const peaks: Peak1D[] = JSON.parse(JSON.stringify(peakList));
59-
const broadMask = !mask
66+
const mask = !broadMask
6067
? getSoftMask(data, peaks, { sgOptions, broadRatio })
61-
: mask;
68+
: broadMask;
6269

63-
if (broadMask.length !== peaks.length) {
70+
if (mask.length !== peaks.length) {
6471
throw new Error('mask length does not match the length of peaksList');
6572
}
6673

6774
for (let i: number = peaks.length - 1; i >= 0; i--) {
68-
if (broadMask[i]) {
75+
if (mask[i]) {
6976
broadLines.push(peaks.splice(i, 1)[0]);
7077
}
7178
}
@@ -79,7 +86,7 @@ export function joinBroadPeaks(
7986
};
8087
let indexes: number[] = [0];
8188
for (let i = 1; i < broadLines.length; i++) {
82-
if (Math.abs(broadLines[i - 1].x - broadLines[i].x) < width) {
89+
if (Math.abs(broadLines[i - 1].x - broadLines[i].x) < broadWidth) {
8390
candidates.x.push(broadLines[i].x);
8491
candidates.y.push(broadLines[i].y);
8592
if (broadLines[i].y > max) {

src/post/optimizePeaks.ts

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DataXY } from 'cheminfo-types';
22
import type { Shape1D } from 'ml-peak-shape-generator';
33
import { getShape1D } from 'ml-peak-shape-generator';
44
import { optimize } from 'ml-spectra-fitting';
5+
import type { IOptimizationOptions } from 'ml-spectra-fitting';
56
import { xGetFromToIndex } from 'ml-spectra-processing';
67

78
import type { Peak1D } from '../gsd';
@@ -14,7 +15,7 @@ import { groupPeaks } from './groupPeaks';
1415
* @param data - An object containing the x and y data to be fitted.
1516
* @param peakList - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}].
1617
*/
17-
interface OptimizePeaksOptions {
18+
export interface IOptimizePeaksOptions {
1819
/**
1920
* times of width to group peaks.
2021
* @default 1
@@ -32,29 +33,13 @@ interface OptimizePeaksOptions {
3233
/**
3334
* it's specify the kind and options of the algorithm use to optimize parameters.
3435
*/
35-
optimization?: {
36-
/**
37-
* kind of algorithm. By default it's levenberg-marquardt.
38-
* @default 'lm'
39-
*/
40-
kind: string;
41-
/**
42-
* options for the specific kind of algorithm.
43-
*/
44-
options: {
45-
/**
46-
* maximum time running before break in seconds.
47-
* @default 10
48-
*/
49-
timeout: number;
50-
};
51-
};
36+
optimization?: IOptimizationOptions;
5237
}
5338

5439
export function optimizePeaks(
5540
data: DataXY,
5641
peakList: Peak1D[],
57-
options: OptimizePeaksOptions = {},
42+
options: IOptimizePeaksOptions = {},
5843
): Peak1D[] {
5944
const {
6045
factorWidth = 1,
@@ -68,7 +53,7 @@ export function optimizePeaks(
6853
timeout: 10,
6954
},
7055
},
71-
}: OptimizePeaksOptions = options;
56+
}: IOptimizePeaksOptions = options;
7257

7358
if (data.x[0] > data.x[1]) {
7459
data.x.reverse();
@@ -108,14 +93,10 @@ export function optimizePeaks(
10893
y: data.y.slice(fromIndex, toIndex),
10994
};
11095
if (currentRange.x.length > 5) {
111-
let { peaks: optimizedPeaks }: { peaks: Peak1D[] } = optimize(
112-
currentRange,
113-
peaks,
114-
{
115-
shape,
116-
optimization,
117-
},
118-
);
96+
let { peaks: optimizedPeaks } = optimize(currentRange, peaks, {
97+
shape,
98+
optimization,
99+
});
119100
results = results.concat(optimizedPeaks);
120101
// eslint-disable-next-line curly
121102
} else results = results.concat(peaks);

0 commit comments

Comments
 (0)