Comparing version 5.0.2 to 6.0.0
179
lib/index.js
@@ -6,3 +6,3 @@ 'use strict'; | ||
var SG = require('ml-savitzky-golay-generalized'); | ||
var mlOptimizeLorentzian = require('ml-optimize-lorentzian'); | ||
var mlSpectraFitting = require('ml-spectra-fitting'); | ||
@@ -15,5 +15,6 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
* Global spectra deconvolution | ||
* @param {Array<number>} x - Independent variable | ||
* @param {Array<number>} yIn - Dependent variable | ||
* @param {object} [options] - Options object | ||
* @param {object} data - Object data with x and y arrays | ||
* @param {Array<number>} [data.x] - Independent variable | ||
* @param {Array<number>} [data.y] - Dependent variable | ||
* @param {object} [options={}] - Options object | ||
* @param {object} [options.sgOptions] - Options object for Savitzky-Golay filter. See https://github.com/mljs/savitzky-golay-generalized#options | ||
@@ -34,3 +35,3 @@ * @param {number} [options.sgOptions.windowSize = 9] - points to use in the approximations | ||
*/ | ||
function gsd(x, yIn, options = {}) { | ||
function gsd(data, options = {}) { | ||
let { | ||
@@ -51,2 +52,4 @@ noiseLevel, | ||
let { y: yIn, x } = data; | ||
const y = yIn.slice(); | ||
@@ -355,6 +358,20 @@ let equalSpaced = isEqualSpaced(x); | ||
function optimizePeaks(peakList, x, y, options = {}) { | ||
/** | ||
* Optimize the position (x), max intensity (y), full width at half maximum (width) | ||
* and the ratio of gaussian contribution (mu) if it's required. It supports three kind of shapes: gaussian, lorentzian and pseudovoigt | ||
* @param {object} data - An object containing the x and y data to be fitted. | ||
* @param {Array} peakList - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. | ||
* @param {object} [options = {}] - | ||
* @param {string} [options.kind = 'gaussian'] - kind of shape used to fitting, lorentzian, gaussian and pseudovoigt are supported. | ||
* @param {number} [options.factorWidth = 4] - times of width to group peaks. | ||
* @param {object} [options.joinPeaks = true] - if true the peaks could be grouped if the separation between them are inside of a range of factorWidth * width | ||
* @param {object} [options.optimizationOptions] - options of ml-levenberg-marquardt optimization package. | ||
*/ | ||
const kindSupported = ['gaussian', 'lorentzian', 'pseudovoigt']; | ||
function optimizePeaks(data, peakList, options = {}) { | ||
const { | ||
functionName = 'gaussian', | ||
factorWidth = 4, | ||
joinPeaks = true, | ||
optimizationOptions = { | ||
@@ -367,9 +384,10 @@ damping: 1.5, | ||
let { x, y } = data; | ||
checkFuncName(functionName, optimizationOptions); | ||
let lastIndex = [0]; | ||
let groups = groupPeaks(peakList, factorWidth); | ||
let groups = groupPeaks(peakList, factorWidth, joinPeaks); | ||
let result = []; | ||
let factor = 1; | ||
if (functionName === 'gaussian') { | ||
factor = 1.17741; | ||
} // From https://en.wikipedia.org/wiki/Gaussian_function#Properties | ||
let sampling; | ||
@@ -387,24 +405,10 @@ for (let i = 0; i < groups.length; i++) { | ||
); | ||
if (sampling[0].length > 5) { | ||
let optPeaks = []; | ||
if (functionName === 'gaussian') { | ||
optPeaks = mlOptimizeLorentzian.optimizeGaussianSum(sampling, peaks, optimizationOptions); | ||
} else { | ||
if (functionName === 'lorentzian') { | ||
optPeaks = mlOptimizeLorentzian.optimizeLorentzianSum( | ||
sampling, | ||
peaks, | ||
optimizationOptions, | ||
); | ||
} | ||
} | ||
if (sampling.x.length > 5) { | ||
let { peaks: optPeaks } = mlSpectraFitting.optimize(sampling, peaks, { | ||
kind: functionName, | ||
lmOptions: optimizationOptions, | ||
}); | ||
for (let j = 0; j < optPeaks.length; j++) { | ||
let { parameters } = optPeaks[j]; | ||
result.push({ | ||
x: parameters[0], | ||
y: parameters[1], | ||
width: parameters[2] * factor, | ||
index: peaks[j].index, | ||
}); | ||
optPeaks[j].index = peaks.index; | ||
result.push(optPeaks[j]); | ||
} | ||
@@ -423,27 +427,10 @@ } | ||
if (sampling[0].length > 5) { | ||
let fitResult = []; | ||
if (functionName === 'gaussian') { | ||
fitResult = mlOptimizeLorentzian.optimizeSingleGaussian( | ||
[sampling[0], sampling[1]], | ||
peaks, | ||
optimizationOptions, | ||
); | ||
} else { | ||
if (functionName === 'lorentzian') { | ||
fitResult = mlOptimizeLorentzian.optimizeSingleLorentzian( | ||
[sampling[0], sampling[1]], | ||
peaks, | ||
optimizationOptions, | ||
); | ||
} | ||
} | ||
let { parameters } = fitResult; | ||
result.push({ | ||
x: parameters[0], | ||
y: parameters[1], | ||
width: parameters[2] * factor, | ||
index: peaks.index, | ||
}); // From https://en.wikipedia.org/wiki/Gaussian_function#Properties} | ||
if (sampling.x.length > 5) { | ||
let fitResult = mlSpectraFitting.optimize(sampling, [peaks], { | ||
kind: functionName, | ||
lmOptions: optimizationOptions, | ||
}); | ||
let { peaks: optPeaks } = fitResult; | ||
optPeaks[0].index = peaks.index; | ||
result.push(optPeaks[0]); | ||
} | ||
@@ -485,6 +472,6 @@ } | ||
lastIndex[0] = index; | ||
return [sampleX, sampleY]; | ||
return { x: sampleX, y: sampleY }; | ||
} | ||
function groupPeaks(peakList, nL) { | ||
function groupPeaks(peakList, nL, joinPeaks) { | ||
let group = []; | ||
@@ -498,4 +485,4 @@ let groups = []; | ||
if ( | ||
Math.abs(peakList[i].x - limits[0]) < | ||
nL * peakList[i].width + limits[1] | ||
joinPeaks && | ||
Math.abs(peakList[i].x - limits[0]) < nL * peakList[i].width + limits[1] | ||
) { | ||
@@ -519,3 +506,2 @@ // Add the peak to the group | ||
groups.push({ limits: limits, group: group }); | ||
// var optmimalPeak = fitSpectrum(group,limits,spectrum); | ||
group = [peakList[i]]; | ||
@@ -556,6 +542,21 @@ limits = [peakList[i].x, nL * peakList[i].width]; | ||
function checkFuncName(functionName, optimizationOptions) { | ||
let kind = functionName.toLowerCase().replace(/[^a-z]/g, ''); | ||
let isSupported = kindSupported.some((ks) => ks === kind); | ||
if (isSupported) { | ||
optimizationOptions.kind = kind; | ||
} else { | ||
throw new Error( | ||
`Kind of function unsupported. Just these kind are supported: ${kindSupported.join( | ||
', ', | ||
)}`, | ||
); | ||
} | ||
} | ||
/** | ||
* This function try to join the peaks that seems to belong to a broad signal in a single broad peak. | ||
* @param peakList | ||
* @param options | ||
* @param {Array} peakList - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. | ||
* @param {object} [options = {}] - | ||
* @param {number} options.width - width limit to join peaks. | ||
*/ | ||
@@ -579,9 +580,8 @@ function joinBroadPeaks(peakList, options = {}) { | ||
let candidates = [[broadLines[0].x, broadLines[0].y]]; | ||
let indexes = [broadLines[0].index]; | ||
let candidates = { x: [broadLines[0].x], y: [broadLines[0].y] }; | ||
let indexes = [0]; | ||
for (let i = 1; i < broadLines.length; i++) { | ||
// console.log(broadLines[i-1].x+" "+broadLines[i].x); | ||
if (Math.abs(broadLines[i - 1].x - broadLines[i].x) < width) { | ||
candidates.push([broadLines[i].x, broadLines[i].y]); | ||
candidates.x.push(broadLines[i].x); | ||
candidates.y.push(broadLines[i].y); | ||
if (broadLines[i].y > max) { | ||
@@ -591,23 +591,25 @@ max = broadLines[i].y; | ||
} | ||
indexes.push(broadLines[i].index); | ||
indexes.push(i); | ||
count++; | ||
} else { | ||
if (count > 2) { | ||
let fitted = mlOptimizeLorentzian.optimizeSingleLorentzian(candidates, { | ||
x: broadLines[maxI].x, | ||
y: max, | ||
width: Math.abs( | ||
candidates[0][0] - candidates[candidates.length - 1][0], | ||
), | ||
}); | ||
let { parameters } = fitted; | ||
peakList.push({ | ||
x: parameters[0], | ||
y: parameters[1], | ||
width: parameters[2], | ||
index: Math.floor( | ||
indexes.reduce((a, b) => a + b, 0) / indexes.length, | ||
), | ||
soft: false, | ||
}); | ||
let fitted = mlSpectraFitting.optimize( | ||
candidates, | ||
[ | ||
{ | ||
x: broadLines[maxI].x, | ||
y: max, | ||
width: Math.abs( | ||
candidates.x[0] - candidates.x[candidates.x.length - 1], | ||
), | ||
}, | ||
], | ||
{ kind: 'lorentzian' }, | ||
); | ||
let { peaks: peak } = fitted; | ||
peak[0].index = Math.floor( | ||
indexes.reduce((a, b) => a + b, 0) / indexes.length, | ||
); | ||
peak[0].soft = false; | ||
peakList.push(peak[0]); | ||
} else { | ||
@@ -619,3 +621,3 @@ // Put back the candidates to the signals list | ||
} | ||
candidates = [[broadLines[i].x, broadLines[i].y]]; | ||
candidates = { x: [broadLines[i].x], y: [broadLines[i].y] }; | ||
indexes = [i]; | ||
@@ -627,3 +629,2 @@ max = broadLines[i].y; | ||
} | ||
peakList.sort(function (a, b) { | ||
@@ -630,0 +631,0 @@ return a.x - b.x; |
{ | ||
"name": "ml-gsd", | ||
"version": "5.0.2", | ||
"version": "6.0.0", | ||
"description": "Global Spectra Deconvolution", | ||
@@ -15,3 +15,3 @@ "main": "lib/index.js", | ||
"scripts": { | ||
"build": "cheminfo-build --entry src/index.js --root GSD", | ||
"build": "rollup -c && cheminfo-build --entry src/index.js --root GSD", | ||
"eslint": "eslint src --cache", | ||
@@ -44,26 +44,34 @@ "eslint-fix": "npm run eslint -- --fix", | ||
"homepage": "https://github.com/mljs/global-spectral-deconvolution", | ||
"jest": { | ||
"testEnvironment": "node" | ||
}, | ||
"prettier": { | ||
"arrowParens": "always", | ||
"semi": true, | ||
"singleQuote": true, | ||
"tabWidth": 2, | ||
"trailingComma": "all" | ||
}, | ||
"devDependencies": { | ||
"@babel/plugin-transform-modules-commonjs": "^7.10.4", | ||
"@babel/plugin-transform-modules-commonjs": "^7.12.1", | ||
"chemcalc": "^3.4.1", | ||
"cheminfo-tools": "^1.23.3", | ||
"eslint": "^7.9.0", | ||
"eslint-config-cheminfo": "^3.0.0", | ||
"eslint-plugin-import": "^2.22.0", | ||
"eslint-plugin-jest": "^23.20.0", | ||
"cheminfo-build": "^1.1.8", | ||
"eslint": "^7.12.1", | ||
"eslint-config-cheminfo": "^5.2.2", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-jest": "^24.1.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"jest": "^26.4.2", | ||
"esm": "^3.2.25", | ||
"jest": "^26.6.3", | ||
"mf-global": "^1.3.0", | ||
"ml-stat": "^1.3.3", | ||
"prettier": "^2.1.2", | ||
"rollup": "^2.28.2", | ||
"spectrum-generator": "^4.0.2", | ||
"rollup": "^2.33.1", | ||
"spectrum-generator": "^4.4.0", | ||
"xy-parser": "^3.0.0" | ||
}, | ||
"dependencies": { | ||
"ml-optimize-lorentzian": "^0.2.0", | ||
"ml-savitzky-golay-generalized": "2.0.2" | ||
}, | ||
"jest": { | ||
"verbose": true, | ||
"testURL": "http://localhost/" | ||
"ml-savitzky-golay-generalized": "2.0.2", | ||
"ml-spectra-fitting": "^0.5.0" | ||
} | ||
} |
# global-spectral-deconvolution | ||
[![NPM version][npm-image]][npm-url] | ||
[![build status][travis-image]][travis-url] | ||
[![David deps][david-image]][david-url] | ||
@@ -10,2 +9,6 @@ [![npm download][download-image]][download-url] | ||
`gsd`is using an algorithm that is searching for inflection points to determine the position of peaks and the width of the peaks are between the 2 inflection points. The result of GSD yield to an array of object containing {x, y and width}. However this width is based on the inflection point and may be different from the 'fwhm' (Full Width Half Maximum). | ||
The second algorithm (`optimize`) will optimize the width as a FWHM to match the original peak. After optimization the width with therefore be always FWHM whichever is the function used. | ||
## [API documentation](http://mljs.github.io/global-spectral-deconvolution/) | ||
@@ -53,3 +56,3 @@ | ||
### GSD.post.broadenPeaks(peakList, {factor=2, overlap=false}) | ||
### GSD.broadenPeaks(peakList, {factor=2, overlap=false}) | ||
@@ -59,5 +62,5 @@ We enlarge the peaks and add the properties from and to. | ||
### GSD.post.joinBroadPeaks | ||
### GSD.joinBroadPeaks | ||
### GSD.post.optimizePeaks | ||
### GSD.optimizePeaks | ||
@@ -67,31 +70,24 @@ ## Example | ||
```js | ||
var CC = require('chemcalc'); | ||
var Stat = require('ml-stat'); | ||
var peakPicking = require('ml-gsd'); | ||
import { IsotopicDistribution } from 'mf-global'; | ||
import { gsd, optimizePeaks } from '../src'; | ||
var spectrum = CC.analyseMF('Cl2.Br2', { | ||
isotopomers: 'arrayXXYY', | ||
fwhm: 0.01, | ||
gaussianWidth: 11 | ||
// generate a sample spectrum of the form {x:[], y:[]} | ||
const data = new IsotopicDistribution('C').getGaussian(); | ||
let peaks = gsd(data, { | ||
noiseLevel: 0, | ||
minMaxRatio: 0.00025, // Threshold to determine if a given peak should be considered as a noise | ||
realTopDetection: true, | ||
maxCriteria: true, // inverted:false | ||
smoothY: false, | ||
sgOptions: { windowSize: 7, polynomial: 3 }, | ||
}); | ||
var xy = spectrum.arrayXXYY; | ||
var x = xy[0]; | ||
var y = xy[1]; | ||
//Just a fake noiseLevel | ||
var noiseLevel = | ||
Stat.array.median( | ||
y.filter(function(a) { | ||
return a > 0; | ||
}) | ||
) * 3; | ||
console.log(peaks); // array of peaks {x,y,width}, width = distance between inflection points | ||
// GSD | ||
var options = { | ||
noiseLevel: noiseLevel, | ||
minMaxRatio: 0, | ||
broadRatio: 0, | ||
smoothY: false, | ||
realTopDetection: true | ||
}; | ||
var result = peakPicking.gsd(x, y, options); | ||
result = peakPicking.post.optimizePeaks(result, x, y, 1, 'gaussian'); | ||
let optimized = optimizePeaks(data, peaks); | ||
console.log(optimized); // array of peaks {x,y,width}, width = FWHM | ||
``` | ||
@@ -105,4 +101,2 @@ | ||
[npm-url]: https://npmjs.org/package/ml-gsd | ||
[travis-image]: https://img.shields.io/travis/mljs/global-spectral-deconvolution/master.svg?style=flat-square | ||
[travis-url]: https://travis-ci.org/mljs/global-spectral-deconvolution | ||
[david-image]: https://img.shields.io/david/mljs/global-spectral-deconvolution.svg?style=flat-square | ||
@@ -109,0 +103,0 @@ [david-url]: https://david-dm.org/mljs/global-spectral-deconvolution |
@@ -16,8 +16,11 @@ /** | ||
let pp = gsd(spectrum[0], spectrum[1], { | ||
noiseLevel: 57000.21889405926, // 1049200.537996172/2, | ||
minMaxRatio: 0.01, | ||
broadRatio: 0.0025, | ||
sgOptions: { windowSize: 13, polynomial: 3 }, | ||
}); | ||
let pp = gsd( | ||
{ x: spectrum[0], y: spectrum[1] }, | ||
{ | ||
noiseLevel: 57000.21889405926, // 1049200.537996172/2, | ||
minMaxRatio: 0.01, | ||
broadRatio: 0.0025, | ||
sgOptions: { windowSize: 13, polynomial: 3 }, | ||
}, | ||
); | ||
@@ -24,0 +27,0 @@ joinBroadPeaks(pp, { width: 0.25 }); |
@@ -12,11 +12,14 @@ import { readFileSync } from 'fs'; | ||
); | ||
let result = gsd(spectrum[0], spectrum[1], { | ||
noiseLevel: 1049200.537996172 / 2, | ||
minMaxRatio: 0.01, | ||
broadRatio: 0.0025, | ||
sgOptions: { | ||
windowSize: 9, | ||
polynomial: 3, | ||
let result = gsd( | ||
{ x: spectrum[0], y: spectrum[1] }, | ||
{ | ||
noiseLevel: 1049200.537996172 / 2, | ||
minMaxRatio: 0.01, | ||
broadRatio: 0.0025, | ||
sgOptions: { | ||
windowSize: 9, | ||
polynomial: 3, | ||
}, | ||
}, | ||
}); | ||
); | ||
joinBroadPeaks(result, { width: 0.25 }); | ||
@@ -23,0 +26,0 @@ expect(result).toHaveLength(14); |
@@ -11,13 +11,14 @@ import { readFileSync } from 'fs'; | ||
); | ||
let result = gsd(spectrum[0], spectrum[1], { | ||
// noiseLevel: 1049200.537996172, | ||
minMaxRatio: 0.03, | ||
smoothY: false, | ||
realTopDetection: true, | ||
sgOptions: { windowSize: 5, polynomial: 3 }, | ||
}); | ||
// console.log(spectrum[1][13223]); | ||
// console.log(result); | ||
let result = gsd( | ||
{ x: spectrum[0], y: spectrum[1] }, | ||
{ | ||
// noiseLevel: 1049200.537996172, | ||
minMaxRatio: 0.03, | ||
smoothY: false, | ||
realTopDetection: true, | ||
sgOptions: { windowSize: 5, polynomial: 3 }, | ||
}, | ||
); | ||
expect(result).toHaveLength(21); | ||
}); | ||
}); |
@@ -11,3 +11,3 @@ import { readFileSync } from 'fs'; | ||
); | ||
gsd(spectrum.x, spectrum.y, { | ||
gsd(spectrum, { | ||
noiseLevel: 32, | ||
@@ -14,0 +14,0 @@ minMaxRatio: 0.03, |
@@ -28,10 +28,13 @@ import { gsd, optimizePeaks } from '..'; | ||
it('Check result', () => { | ||
let result = gsd(x, y, { | ||
noiseLevel: noiseLevel, | ||
minMaxRatio: 0, | ||
broadRatio: 0, | ||
smoothY: false, | ||
realTopDetection: true, | ||
}); | ||
result = optimizePeaks(result, x, y, { | ||
let result = gsd( | ||
{ x, y }, | ||
{ | ||
noiseLevel: noiseLevel, | ||
minMaxRatio: 0, | ||
broadRatio: 0, | ||
smoothY: false, | ||
realTopDetection: true, | ||
}, | ||
); | ||
result = optimizePeaks({ x, y }, result, { | ||
factorWidth: 1, | ||
@@ -38,0 +41,0 @@ functionName: 'gaussian', |
import { gsd } from '..'; | ||
describe('Simple test cases', () => { | ||
let X = []; | ||
let Y = []; | ||
let x = []; | ||
let y = []; | ||
for (let i = 0; i < 10; i++) { | ||
X.push(X.length); | ||
Y.push(0); | ||
x.push(x.length); | ||
y.push(0); | ||
} | ||
for (let i = 0; i <= 10; i++) { | ||
X.push(X.length); | ||
Y.push(i > 5 ? 10 - i : i); | ||
x.push(x.length); | ||
y.push(i > 5 ? 10 - i : i); | ||
} | ||
for (let i = 0; i < 10; i++) { | ||
X.push(X.length); | ||
Y.push(0); | ||
x.push(x.length); | ||
y.push(0); | ||
} | ||
it('gsd not realtop', () => { | ||
let peaks = gsd(X, Y, { | ||
realTopDetection: false, | ||
smoothY: true, | ||
sgOptions: { | ||
windowSize: 5, | ||
polynomial: 3, | ||
let peaks = gsd( | ||
{ x, y }, | ||
{ | ||
realTopDetection: false, | ||
smoothY: true, | ||
sgOptions: { | ||
windowSize: 5, | ||
polynomial: 3, | ||
}, | ||
}, | ||
}); | ||
); | ||
@@ -35,12 +38,15 @@ expect(peaks[0].y).toBeCloseTo(4.657, 3); | ||
it('gsd not realtop asymetric', () => { | ||
let Y2 = Y.slice(0); | ||
let Y2 = y.slice(0); | ||
Y2[14] = 5; | ||
let peaks = gsd(X, Y2, { | ||
realTopDetection: false, | ||
smoothY: true, | ||
sgOptions: { | ||
windowSize: 5, | ||
polynomial: 3, | ||
let peaks = gsd( | ||
{ x, y: Y2 }, | ||
{ | ||
realTopDetection: false, | ||
smoothY: true, | ||
sgOptions: { | ||
windowSize: 5, | ||
polynomial: 3, | ||
}, | ||
}, | ||
}); | ||
); | ||
@@ -68,12 +74,15 @@ expect(peaks).toStrictEqual([ | ||
it('gsd realtop', () => { | ||
let Y2 = Y.slice(); | ||
let Y2 = y.slice(); | ||
Y2[14] = 5; | ||
let peaks = gsd(X, Y2, { | ||
realTopDetection: true, | ||
smoothY: false, | ||
sgOptions: { | ||
windowSize: 5, | ||
polynomial: 3, | ||
let peaks = gsd( | ||
{ x, y: Y2 }, | ||
{ | ||
realTopDetection: true, | ||
smoothY: false, | ||
sgOptions: { | ||
windowSize: 5, | ||
polynomial: 3, | ||
}, | ||
}, | ||
}); | ||
); | ||
expect(peaks).toStrictEqual([ | ||
@@ -80,0 +89,0 @@ { |
@@ -1,4 +0,4 @@ | ||
import { gsd } from '..'; | ||
import { gsd, optimizePeaks } from '..'; | ||
const { SpectrumGenerator } = require('spectrum-generator'); | ||
const { generateSpectrum } = require('spectrum-generator'); | ||
@@ -8,11 +8,10 @@ describe('Global spectra deconvolution with simulated spectra', () => { | ||
it('Should provide the right result ...', () => { | ||
const sg = new SpectrumGenerator({ start: 0, end: 100, pointsPerUnit: 10 }); | ||
const peaks = [ | ||
{ x: 0.1, y: 0.2, width: 0.1 }, | ||
{ x: -0.1, y: 0.2, width: 0.3 }, | ||
]; | ||
sg.addPeak([20, 100], { width: 5 }); | ||
sg.addPeak([50, 50], { width: 5 }); | ||
sg.addPeak([70, 20], { width: 5 }); | ||
const data = generateSpectrum(peaks, { from: -1, to: 1, nbPoints: 101 }); | ||
let spectrum = sg.getSpectrum(); | ||
let result = gsd(spectrum.x, spectrum.y, { | ||
let peakList = gsd(data, { | ||
minMaxRatio: 0, | ||
@@ -23,6 +22,11 @@ realTopDetection: false, | ||
expect(result[0]).toMatchObject({ x: 20, y: 100 }); | ||
expect(result[1]).toMatchObject({ x: 50, y: 50 }); | ||
expect(result[2]).toMatchObject({ x: 70, y: 20 }); | ||
let optPeaks = optimizePeaks(data, peakList, {}); | ||
expect(optPeaks[0].x).toBeCloseTo(-0.1, 2); | ||
expect(optPeaks[0].y).toBeCloseTo(0.2, 2); | ||
expect(optPeaks[0].width).toBeCloseTo(0.3, 2); | ||
expect(optPeaks[1].x).toBeCloseTo(0.1, 2); | ||
expect(optPeaks[1].y).toBeCloseTo(0.2, 2); | ||
expect(optPeaks[1].width).toBeCloseTo(0.1, 2); | ||
}); | ||
}); |
@@ -18,8 +18,11 @@ import { readFileSync } from 'fs'; | ||
); | ||
let result = gsd(spectrum[0], spectrum[1], { | ||
// noiseLevel: 0.001, | ||
minMaxRatio: 0, | ||
realTopDetection: true, | ||
smoothY: false, | ||
}); | ||
let result = gsd( | ||
{ x: spectrum[0], y: spectrum[1] }, | ||
{ | ||
// noiseLevel: 0.001, | ||
minMaxRatio: 0, | ||
realTopDetection: true, | ||
smoothY: false, | ||
}, | ||
); | ||
@@ -53,8 +56,11 @@ expect(result[0].x).toBeCloseTo(24, 2); | ||
} | ||
let ans = gsd(times, tic, { | ||
noiseLevel: 0, | ||
realTopDetection: false, | ||
smoothY: false, | ||
sgOptions: { windowSize: 5, polynomial: 3 }, | ||
}); | ||
let ans = gsd( | ||
{ x: times, y: tic }, | ||
{ | ||
noiseLevel: 0, | ||
realTopDetection: false, | ||
smoothY: false, | ||
sgOptions: { windowSize: 5, polynomial: 3 }, | ||
}, | ||
); | ||
@@ -61,0 +67,0 @@ expect(ans).toHaveLength(10); |
@@ -21,3 +21,3 @@ import { readFileSync } from 'fs'; | ||
let result = gsd(spectrum.x, spectrum.y, { | ||
let result = gsd(spectrum, { | ||
noiseLevel: noiseLevel, | ||
@@ -24,0 +24,0 @@ minMaxRatio: 0.0, |
import { gsd } from '..'; | ||
test('Simple test cases', () => { | ||
let X = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; | ||
let Y = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]; | ||
let peaks = gsd(X, Y, { | ||
noiseLevel: 0, | ||
minMaxRatio: 0.00025, // Threshold to determine if a given peak should be considered as a noise | ||
realTopDetection: true, | ||
maxCriteria: true, // inverted:false | ||
smoothY: false, | ||
sgOptions: { windowSize: 7, polynomial: 3 }, | ||
}); | ||
let x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; | ||
let y = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]; | ||
let peaks = gsd( | ||
{ x, y }, | ||
{ | ||
noiseLevel: 0, | ||
minMaxRatio: 0.00025, // Threshold to determine if a given peak should be considered as a noise | ||
realTopDetection: true, | ||
maxCriteria: true, // inverted:false | ||
smoothY: false, | ||
sgOptions: { windowSize: 7, polynomial: 3 }, | ||
}, | ||
); | ||
expect(peaks[0].x).toBe(8); | ||
}); |
@@ -5,5 +5,6 @@ import SG from 'ml-savitzky-golay-generalized'; | ||
* Global spectra deconvolution | ||
* @param {Array<number>} x - Independent variable | ||
* @param {Array<number>} yIn - Dependent variable | ||
* @param {object} [options] - Options object | ||
* @param {object} data - Object data with x and y arrays | ||
* @param {Array<number>} [data.x] - Independent variable | ||
* @param {Array<number>} [data.y] - Dependent variable | ||
* @param {object} [options={}] - Options object | ||
* @param {object} [options.sgOptions] - Options object for Savitzky-Golay filter. See https://github.com/mljs/savitzky-golay-generalized#options | ||
@@ -24,3 +25,3 @@ * @param {number} [options.sgOptions.windowSize = 9] - points to use in the approximations | ||
*/ | ||
export function gsd(x, yIn, options = {}) { | ||
export function gsd(data, options = {}) { | ||
let { | ||
@@ -41,2 +42,4 @@ noiseLevel, | ||
let { y: yIn, x } = data; | ||
const y = yIn.slice(); | ||
@@ -43,0 +46,0 @@ let equalSpaced = isEqualSpaced(x); |
@@ -1,7 +0,8 @@ | ||
import { optimizeSingleLorentzian } from 'ml-optimize-lorentzian'; | ||
import { optimize } from 'ml-spectra-fitting'; | ||
/** | ||
* This function try to join the peaks that seems to belong to a broad signal in a single broad peak. | ||
* @param peakList | ||
* @param options | ||
* @param {Array} peakList - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. | ||
* @param {object} [options = {}] - | ||
* @param {number} options.width - width limit to join peaks. | ||
*/ | ||
@@ -25,9 +26,8 @@ export function joinBroadPeaks(peakList, options = {}) { | ||
let candidates = [[broadLines[0].x, broadLines[0].y]]; | ||
let indexes = [broadLines[0].index]; | ||
let candidates = { x: [broadLines[0].x], y: [broadLines[0].y] }; | ||
let indexes = [0]; | ||
for (let i = 1; i < broadLines.length; i++) { | ||
// console.log(broadLines[i-1].x+" "+broadLines[i].x); | ||
if (Math.abs(broadLines[i - 1].x - broadLines[i].x) < width) { | ||
candidates.push([broadLines[i].x, broadLines[i].y]); | ||
candidates.x.push(broadLines[i].x); | ||
candidates.y.push(broadLines[i].y); | ||
if (broadLines[i].y > max) { | ||
@@ -37,23 +37,25 @@ max = broadLines[i].y; | ||
} | ||
indexes.push(broadLines[i].index); | ||
indexes.push(i); | ||
count++; | ||
} else { | ||
if (count > 2) { | ||
let fitted = optimizeSingleLorentzian(candidates, { | ||
x: broadLines[maxI].x, | ||
y: max, | ||
width: Math.abs( | ||
candidates[0][0] - candidates[candidates.length - 1][0], | ||
), | ||
}); | ||
let { parameters } = fitted; | ||
peakList.push({ | ||
x: parameters[0], | ||
y: parameters[1], | ||
width: parameters[2], | ||
index: Math.floor( | ||
indexes.reduce((a, b) => a + b, 0) / indexes.length, | ||
), | ||
soft: false, | ||
}); | ||
let fitted = optimize( | ||
candidates, | ||
[ | ||
{ | ||
x: broadLines[maxI].x, | ||
y: max, | ||
width: Math.abs( | ||
candidates.x[0] - candidates.x[candidates.x.length - 1], | ||
), | ||
}, | ||
], | ||
{ kind: 'lorentzian' }, | ||
); | ||
let { peaks: peak } = fitted; | ||
peak[0].index = Math.floor( | ||
indexes.reduce((a, b) => a + b, 0) / indexes.length, | ||
); | ||
peak[0].soft = false; | ||
peakList.push(peak[0]); | ||
} else { | ||
@@ -65,3 +67,3 @@ // Put back the candidates to the signals list | ||
} | ||
candidates = [[broadLines[i].x, broadLines[i].y]]; | ||
candidates = { x: [broadLines[i].x], y: [broadLines[i].y] }; | ||
indexes = [i]; | ||
@@ -73,3 +75,2 @@ max = broadLines[i].y; | ||
} | ||
peakList.sort(function (a, b) { | ||
@@ -76,0 +77,0 @@ return a.x - b.x; |
@@ -1,12 +0,21 @@ | ||
import { | ||
optimizeGaussianSum, | ||
optimizeLorentzianSum, | ||
optimizeSingleGaussian, | ||
optimizeSingleLorentzian, | ||
} from 'ml-optimize-lorentzian'; | ||
import { optimize } from 'ml-spectra-fitting'; | ||
export function optimizePeaks(peakList, x, y, options = {}) { | ||
/** | ||
* Optimize the position (x), max intensity (y), full width at half maximum (width) | ||
* and the ratio of gaussian contribution (mu) if it's required. It supports three kind of shapes: gaussian, lorentzian and pseudovoigt | ||
* @param {object} data - An object containing the x and y data to be fitted. | ||
* @param {Array} peakList - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. | ||
* @param {object} [options = {}] - | ||
* @param {string} [options.kind = 'gaussian'] - kind of shape used to fitting, lorentzian, gaussian and pseudovoigt are supported. | ||
* @param {number} [options.factorWidth = 4] - times of width to group peaks. | ||
* @param {object} [options.joinPeaks = true] - if true the peaks could be grouped if the separation between them are inside of a range of factorWidth * width | ||
* @param {object} [options.optimizationOptions] - options of ml-levenberg-marquardt optimization package. | ||
*/ | ||
const kindSupported = ['gaussian', 'lorentzian', 'pseudovoigt']; | ||
export function optimizePeaks(data, peakList, options = {}) { | ||
const { | ||
functionName = 'gaussian', | ||
factorWidth = 4, | ||
joinPeaks = true, | ||
optimizationOptions = { | ||
@@ -19,9 +28,10 @@ damping: 1.5, | ||
let { x, y } = data; | ||
checkFuncName(functionName, optimizationOptions); | ||
let lastIndex = [0]; | ||
let groups = groupPeaks(peakList, factorWidth); | ||
let groups = groupPeaks(peakList, factorWidth, joinPeaks); | ||
let result = []; | ||
let factor = 1; | ||
if (functionName === 'gaussian') { | ||
factor = 1.17741; | ||
} // From https://en.wikipedia.org/wiki/Gaussian_function#Properties | ||
let sampling; | ||
@@ -39,24 +49,10 @@ for (let i = 0; i < groups.length; i++) { | ||
); | ||
if (sampling[0].length > 5) { | ||
let optPeaks = []; | ||
if (functionName === 'gaussian') { | ||
optPeaks = optimizeGaussianSum(sampling, peaks, optimizationOptions); | ||
} else { | ||
if (functionName === 'lorentzian') { | ||
optPeaks = optimizeLorentzianSum( | ||
sampling, | ||
peaks, | ||
optimizationOptions, | ||
); | ||
} | ||
} | ||
if (sampling.x.length > 5) { | ||
let { peaks: optPeaks } = optimize(sampling, peaks, { | ||
kind: functionName, | ||
lmOptions: optimizationOptions, | ||
}); | ||
for (let j = 0; j < optPeaks.length; j++) { | ||
let { parameters } = optPeaks[j]; | ||
result.push({ | ||
x: parameters[0], | ||
y: parameters[1], | ||
width: parameters[2] * factor, | ||
index: peaks[j].index, | ||
}); | ||
optPeaks[j].index = peaks.index; | ||
result.push(optPeaks[j]); | ||
} | ||
@@ -75,27 +71,10 @@ } | ||
if (sampling[0].length > 5) { | ||
let fitResult = []; | ||
if (functionName === 'gaussian') { | ||
fitResult = optimizeSingleGaussian( | ||
[sampling[0], sampling[1]], | ||
peaks, | ||
optimizationOptions, | ||
); | ||
} else { | ||
if (functionName === 'lorentzian') { | ||
fitResult = optimizeSingleLorentzian( | ||
[sampling[0], sampling[1]], | ||
peaks, | ||
optimizationOptions, | ||
); | ||
} | ||
} | ||
let { parameters } = fitResult; | ||
result.push({ | ||
x: parameters[0], | ||
y: parameters[1], | ||
width: parameters[2] * factor, | ||
index: peaks.index, | ||
}); // From https://en.wikipedia.org/wiki/Gaussian_function#Properties} | ||
if (sampling.x.length > 5) { | ||
let fitResult = optimize(sampling, [peaks], { | ||
kind: functionName, | ||
lmOptions: optimizationOptions, | ||
}); | ||
let { peaks: optPeaks } = fitResult; | ||
optPeaks[0].index = peaks.index; | ||
result.push(optPeaks[0]); | ||
} | ||
@@ -137,6 +116,6 @@ } | ||
lastIndex[0] = index; | ||
return [sampleX, sampleY]; | ||
return { x: sampleX, y: sampleY }; | ||
} | ||
function groupPeaks(peakList, nL) { | ||
function groupPeaks(peakList, nL, joinPeaks) { | ||
let group = []; | ||
@@ -150,4 +129,4 @@ let groups = []; | ||
if ( | ||
Math.abs(peakList[i].x - limits[0]) < | ||
nL * peakList[i].width + limits[1] | ||
joinPeaks && | ||
Math.abs(peakList[i].x - limits[0]) < nL * peakList[i].width + limits[1] | ||
) { | ||
@@ -171,3 +150,2 @@ // Add the peak to the group | ||
groups.push({ limits: limits, group: group }); | ||
// var optmimalPeak = fitSpectrum(group,limits,spectrum); | ||
group = [peakList[i]]; | ||
@@ -207,1 +185,15 @@ limits = [peakList[i].x, nL * peakList[i].width]; | ||
} | ||
function checkFuncName(functionName, optimizationOptions) { | ||
let kind = functionName.toLowerCase().replace(/[^a-z]/g, ''); | ||
let isSupported = kindSupported.some((ks) => ks === kind); | ||
if (isSupported) { | ||
optimizationOptions.kind = kind; | ||
} else { | ||
throw new Error( | ||
`Kind of function unsupported. Just these kind are supported: ${kindSupported.join( | ||
', ', | ||
)}`, | ||
); | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5859086
29
1754
16
101
1
+ Addedml-spectra-fitting@^0.5.0
+ Addedd3-random@2.2.2(transitive)
+ Addedesm@3.2.25(transitive)
+ Addedml-peak-shape-generator@0.7.01.0.0(transitive)
+ Addedml-spectra-fitting@0.5.0(transitive)
+ Addedml-xsadd@2.0.0(transitive)
+ Addedobject-hash@2.2.0(transitive)
+ Addedspectrum-generator@4.8.1(transitive)
- Removedml-optimize-lorentzian@^0.2.0
- Removedml-optimize-lorentzian@0.2.0(transitive)