ml-spectra-fitting
Advanced tools
Comparing version 0.2.1 to 0.3.0
573
lib/index.js
@@ -5,12 +5,15 @@ 'use strict'; | ||
var getMaxValue = require('ml-array-max'); | ||
var LM = require('ml-levenberg-marquardt'); | ||
var mlPeakShapeGenerator = require('ml-peak-shape-generator'); | ||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
var getMaxValue__default = /*#__PURE__*/_interopDefaultLegacy(getMaxValue); | ||
var LM__default = /*#__PURE__*/_interopDefaultLegacy(LM); | ||
/** | ||
* This function calculates the spectrum as a sum of lorentzian functions. The Lorentzian | ||
* parameters are divided in 3 batches. 1st: centers; 2nd: heights; 3th: widths; | ||
* @param t Ordinate values | ||
* This function calculates the spectrum as a sum of linear combination of gaussian and lorentzian functions. The pseudo voigt | ||
* parameters are divided in 4 batches. 1st: centers; 2nd: heights; 3th: widths; 4th: mu's ; | ||
* @param t Ordinate value | ||
* @param p Lorentzian parameters | ||
@@ -23,26 +26,11 @@ * @returns {*} | ||
let nL = p.length / 4; | ||
let factorG1; | ||
let factorG2; | ||
let factorL; | ||
let rows = t.length; | ||
let p2; | ||
let result = rows === undefined ? 0 : new Float64Array(rows).fill(0); | ||
let result = 0; | ||
for (let i = 0; i < nL; i++) { | ||
let xG = p[i + nL * 3]; | ||
let xL = 1 - xG; | ||
p2 = Math.pow(p[i + nL * 2] / 2, 2); | ||
factorL = xL * p[i + nL] * p2; | ||
factorG1 = p[i + nL * 2] * p[i + nL * 2] * 2; | ||
factorG2 = xG * p[i + nL]; | ||
if (rows === undefined) { | ||
result += | ||
factorG2 * Math.exp(-Math.pow(t - p[i], 2) / factorG1) + | ||
factorL / (Math.pow(t - p[i], 2) + p2); | ||
} else { | ||
for (let j = 0; j < rows; j++) { | ||
result[j] += | ||
factorG2 * Math.exp(-Math.pow(t[j] - p[i], 2) / factorG1) + | ||
factorL / (Math.pow(t[j] - p[i], 2) + p2); | ||
} | ||
} | ||
let func = mlPeakShapeGenerator.pseudovoigtFct({ | ||
x: p[i], | ||
y: p[i + nL], | ||
width: p[i + nL * 2], | ||
mu: p[i + nL * 3], | ||
}); | ||
result += func(t); | ||
} | ||
@@ -53,85 +41,21 @@ return result; | ||
function optimizeGaussianLorentzianSum(xy, group, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
let nL = group.length; | ||
let pInit = new Float64Array(nL * 4); | ||
let pMin = new Float64Array(nL * 4); | ||
let pMax = new Float64Array(nL * 4); | ||
let dt = Math.abs(t[0] - t[1]); | ||
for (let i = 0; i < nL; i++) { | ||
pInit[i] = group[i].x; | ||
pInit[i + nL] = 1; | ||
pInit[i + 2 * nL] = group[i].width; | ||
pInit[i + 3 * nL] = 0.5; | ||
pMin[i] = group[i].x - dt; | ||
pMin[i + nL] = 0; | ||
pMin[i + 2 * nL] = group[i].width / 4; | ||
pMin[i + 3 * nL] = 0; | ||
pMax[i] = group[i].x + dt; | ||
pMax[i + nL] = 1.5; | ||
pMax[i + 2 * nL] = group[i].width * 4; | ||
pMax[i + 3 * nL] = 1; | ||
} | ||
let data = { | ||
x: t, | ||
y: yData, | ||
}; | ||
let result = new Array(nL); | ||
let lmOptions = { | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}; | ||
opts = Object.assign({}, lmOptions, opts); | ||
let pFit = LM__default['default'](data, sumOfGaussianLorentzians, opts); | ||
for (let i = 0; i < nL; i++) { | ||
result[i] = { | ||
parameters: [ | ||
pFit.parameterValues[i], | ||
pFit.parameterValues[i + nL] * maxY, | ||
pFit.parameterValues[i + 2 * nL], | ||
pFit.parameterValues[i + 3 * nL], | ||
], | ||
error: pFit.parameterError, | ||
}; | ||
} | ||
return result; | ||
} | ||
/** | ||
* This function calculates the spectrum as a sum of gaussian functions. The Gaussian | ||
* parameters are divided in 3 batches. 1st: centers; 2nd: height; 3th: std's; | ||
* parameters are divided in 3 batches. 1st: centers; 2nd: height; 3th: widths; | ||
* @param t Ordinate values | ||
* @param p Gaussian parameters | ||
* @param c Constant parameters(Not used) | ||
* @returns {*} | ||
*/ | ||
function sumOfGaussians(p) { | ||
return function (t) { | ||
let nL = p.length / 3; | ||
let factor; | ||
let rows = t.length; | ||
let result = rows === undefined ? 0 : new Float64Array(rows).fill(0); | ||
let result = 0; | ||
for (let i = 0; i < nL; i++) { | ||
factor = Math.pow(p[i + nL * 2], 2) * 2; | ||
if (rows === undefined) { | ||
result += p[i + nL] * Math.exp(-Math.pow(t - p[i], 2) / factor); | ||
} else { | ||
for (let j = 0; j < rows; j++) { | ||
result[j] += p[i + nL] * Math.exp(-Math.pow(t[j] - p[i], 2) / factor); | ||
} | ||
} | ||
let func = mlPeakShapeGenerator.gaussianFct({ | ||
x: p[i], | ||
y: p[i + nL], | ||
width: p[i + nL * 2], | ||
}); | ||
result += func(t); | ||
} | ||
@@ -143,173 +67,2 @@ return result; | ||
/** | ||
* | ||
* @param xy A two column matrix containing the x and y data to be fitted | ||
* @param group A set of initial lorentzian parameters to be optimized [center, heigth, half_width_at_half_height] | ||
* @returns {Array} A set of final lorentzian parameters [center, heigth, hwhh*2] | ||
*/ | ||
function optimizeGaussianSum(xy, group, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
let nL = group.length; | ||
let pInit = new Float64Array(nL * 3); | ||
let pMin = new Float64Array(nL * 3); | ||
let pMax = new Float64Array(nL * 3); | ||
let dt = Math.abs(t[0] - t[1]); | ||
for (let i = 0; i < nL; i++) { | ||
pInit[i] = group[i].x; | ||
pInit[i + nL] = group[i].y / maxY; | ||
pInit[i + 2 * nL] = group[i].width; | ||
pMin[i] = group[i].x - dt; | ||
pMin[i + nL] = 0; | ||
pMin[i + 2 * nL] = group[i].width / 4; | ||
pMax[i] = group[i].x + dt; | ||
pMax[i + nL] = (group[i].y * 1.2) / maxY; | ||
pMax[i + 2 * nL] = group[i].width * 4; | ||
} | ||
let data = { | ||
x: t, | ||
y: yData, | ||
}; | ||
let result = new Array(nL); | ||
let lmOptions = { | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}; | ||
opts = Object.assign({}, lmOptions, opts); | ||
let pFit = LM__default['default'](data, sumOfGaussians, opts); | ||
for (let i = 0; i < nL; i++) { | ||
result[i] = { | ||
parameters: [ | ||
pFit.parameterValues[i], | ||
pFit.parameterValues[i + nL] * maxY, | ||
pFit.parameterValues[i + nL * 2], | ||
], | ||
error: pFit.parameterError, | ||
}; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Single 3 parameter gaussian function | ||
* @param t Ordinate values | ||
* @param p Gaussian parameters [mean, height, std] | ||
* @param c Constant parameters(Not used) | ||
* @returns {*} | ||
*/ | ||
function singleGaussian(p) { | ||
return function (t) { | ||
let factor2 = (p[2] * p[2]) / 2; | ||
let rows = t.length; | ||
if (!rows) return p[1] * Math.exp((-(t - p[0]) * (t - p[0])) / factor2); | ||
let result = new Float64Array(t.length); | ||
for (let i = 0; i < t.length; i++) { | ||
result[i] = p[1] * Math.exp((-(t[i] - p[0]) * (t[i] - p[0])) / factor2); | ||
} | ||
return result; | ||
}; | ||
} | ||
/** | ||
* Fits a set of points to a gaussian bell. Returns the mean of the peak, the std and the height of the signal. | ||
* @param data,[y] | ||
* @returns {*[]} | ||
*/ | ||
function optimizeSingleGaussian(xy, peak, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
let dt = Math.abs(t[0] - t[1]); | ||
let pInit = new Float64Array([peak.x, 1, peak.width]); | ||
let pMin = new Float64Array([peak.x - dt, 0, peak.width / 4]); | ||
let pMax = new Float64Array([peak.x + dt, 1.25, peak.width * 4]); | ||
let data = { | ||
x: t, | ||
y: yData, | ||
}; | ||
let lmOptions = { | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}; | ||
opts = Object.assign({}, lmOptions, opts); | ||
let pFit = LM__default['default'](data, singleGaussian, opts); | ||
return { | ||
parameters: [ | ||
pFit.parameterValues[0], | ||
pFit.parameterValues[1] * maxY, | ||
pFit.parameterValues[2], | ||
], | ||
error: pFit.parameterError, | ||
}; | ||
} | ||
/* | ||
peaks on group should sorted | ||
*/ | ||
function optimizeGaussianTrain(xy, group, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
let currentIndex = 0; | ||
let nbPoints = t.length; | ||
let nextX; | ||
let tI, yI; | ||
let result = []; | ||
let current; | ||
for (let i = 0; i < group.length; i++) { | ||
nextX = group[i].x - group[i].width * 1.5; | ||
while (t[currentIndex++] < nextX && currentIndex < nbPoints); | ||
nextX = group[i].x + group[i].width * 1.5; | ||
tI = []; | ||
yI = []; | ||
while (t[currentIndex] <= nextX && currentIndex < nbPoints) { | ||
tI.push(t[currentIndex]); | ||
yI.push(yData[currentIndex] * maxY); | ||
currentIndex++; | ||
} | ||
current = optimizeSingleGaussian([tI, yI], group[i], opts); | ||
if (current) { | ||
result.push({ | ||
x: current.parameters[0], | ||
y: current.parameters[1], | ||
width: current.parameters[2], | ||
opt: true, | ||
}); | ||
} else { | ||
result.push({ | ||
x: group[i].x, | ||
y: group[i].y, | ||
width: group[i].width, | ||
opt: false, | ||
}); | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* This function calculates the spectrum as a sum of lorentzian functions. The Lorentzian | ||
@@ -325,16 +78,10 @@ * parameters are divided in 3 batches. 1st: centers; 2nd: heights; 3th: widths; | ||
let nL = p.length / 3; | ||
let factor; | ||
let p2; | ||
let rows = t.length; | ||
let result = rows === undefined ? 0 : new Float64Array(rows).fill(0); | ||
let result = 0; | ||
for (let i = 0; i < nL; i++) { | ||
p2 = Math.pow(p[i + nL * 2] / 2, 2); | ||
factor = p[i + nL] * p2; | ||
if (rows === undefined) { | ||
result += factor / (Math.pow(t - p[i], 2) + p2); | ||
} else { | ||
for (let j = 0; j < rows; j++) { | ||
result[j] += factor / (Math.pow(t[j] - p[i], 2) + p2); | ||
} | ||
} | ||
let func = mlPeakShapeGenerator.lorentzianFct({ | ||
x: p[i], | ||
y: p[i + nL], | ||
width: p[i + nL * 2], | ||
}); | ||
result += func(t); | ||
} | ||
@@ -345,63 +92,79 @@ return result; | ||
const STATE_INIT = 0; | ||
const STATE_MIN = 1; | ||
const STATE_MAX = 2; | ||
const keys = ['x', 'y', 'width', 'mu']; | ||
/** | ||
* | ||
* @param xy A two column matrix containing the x and y data to be fitted | ||
* @param group A set of initial lorentzian parameters to be optimized [center, heigth, half_width_at_half_height] | ||
* @returns {Array} A set of final lorentzian parameters [center, heigth, hwhh*2] | ||
* Fits a set of points to the sum of a set of bell functions. | ||
* @param {Object} input - 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 {Object} [options.lmOptions = {}] - options of ml-levenberg-marquardt optimization package. | ||
* @returns {Object} - A object with fitting error and the list of optimized parameters { parameters: [ {x, y, width} ], error } if the kind of shape is pseudoVoigt mu parameter is optimized. | ||
*/ | ||
function optimizeLorentzianSum(xy, group, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
function optimize(data, peakList, options = {}) { | ||
let { kind = 'gaussian', lmOptions = {} } = options; | ||
let nL = group.length; | ||
let pInit = new Float64Array(nL * 3); | ||
let pMin = new Float64Array(nL * 3); | ||
let pMax = new Float64Array(nL * 3); | ||
let dt = Math.abs(t[0] - t[1]); | ||
let maxY = getMaxValue__default['default'](data.y); | ||
data.y.forEach((_, i, arr) => (arr[i] /= maxY)); | ||
for (let i = 0; i < nL; i++) { | ||
pInit[i] = group[i].x; | ||
pInit[i + nL] = 1; | ||
pInit[i + 2 * nL] = group[i].width; | ||
let nbParams; | ||
let paramsFunc; | ||
switch (kind.toLowerCase()) { | ||
case 'gaussian': | ||
nbParams = 3; | ||
paramsFunc = sumOfGaussians; | ||
break; | ||
case 'lorentzian': | ||
nbParams = 3; | ||
paramsFunc = sumOfLorentzians; | ||
break; | ||
case 'pseudovoigt': | ||
nbParams = 4; | ||
paramsFunc = sumOfGaussianLorentzians; | ||
break; | ||
default: | ||
throw new Error('kind of shape is not supported'); | ||
} | ||
pMin[i] = group[i].x - dt; | ||
pMin[i + nL] = 0; | ||
pMin[i + 2 * nL] = group[i].width / 4; | ||
let nL = peakList.length; | ||
let pInit = new Float64Array(nL * nbParams); | ||
let pMin = new Float64Array(nL * nbParams); | ||
let pMax = new Float64Array(nL * nbParams); | ||
let dt = Math.abs(data.x[0] - data.x[1]); | ||
pMax[i] = group[i].x + dt; | ||
pMax[i + nL] = 1.5; | ||
pMax[i + 2 * nL] = group[i].width * 4; | ||
for (let i = 0; i < nL; i++) { | ||
let peak = peakList[i]; | ||
for (let s = 0; s < nbParams; s++) { | ||
pInit[i + s * nL] = getValue(s, peak, STATE_INIT, dt); | ||
pMin[i + s * nL] = getValue(s, peak, STATE_MIN, dt); | ||
pMax[i + s * nL] = getValue(s, peak, STATE_MAX, dt); | ||
} | ||
} | ||
let data = { | ||
x: t, | ||
y: yData, | ||
}; | ||
lmOptions = Object.assign( | ||
{ | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}, | ||
lmOptions, | ||
); | ||
let pFit = LM__default['default'](data, paramsFunc, lmOptions); | ||
let result = new Array(nL); | ||
let lmOptions = { | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}; | ||
opts = Object.assign({}, lmOptions, opts); | ||
let pFit = LM__default['default'](data, sumOfLorentzians, opts); | ||
let { parameterError: error, iterations } = pFit; | ||
let result = { error, iterations, peaks: new Array(nL) }; | ||
for (let i = 0; i < nL; i++) { | ||
result[i] = { | ||
parameters: [ | ||
pFit.parameterValues[i], | ||
pFit.parameterValues[i + nL] * maxY, | ||
pFit.parameterValues[i + nL * 2], | ||
], | ||
error: pFit.parameterError, | ||
}; | ||
let peak = {}; | ||
pFit.parameterValues[i + nL] *= maxY; | ||
for (let s = 0; s < nbParams; s++) { | ||
peak[keys[s]] = pFit.parameterValues[i + s * nL]; | ||
} | ||
result.peaks[i] = peak; | ||
} | ||
@@ -411,120 +174,30 @@ return result; | ||
/** | ||
* Single 4 parameter lorentzian function | ||
* @param t Ordinate values | ||
* @param p Lorentzian parameters | ||
* @param c Constant parameters(Not used) | ||
* @returns {*} | ||
*/ | ||
function singleLorentzian(p) { | ||
return function (t) { | ||
let factor = p[1] * Math.pow(p[2] / 2, 2); | ||
let rows = t.length; | ||
if (!rows) return factor / (Math.pow(t - p[0], 2) + Math.pow(p[2] / 2, 2)); | ||
let result = new Float64Array(rows); | ||
for (let i = 0; i < rows; i++) { | ||
result[i] = factor / (Math.pow(t[i] - p[0], 2) + Math.pow(p[2] / 2, 2)); | ||
} | ||
return result; | ||
}; | ||
} | ||
/** | ||
* * Fits a set of points to a Lorentzian function. Returns the center of the peak, the width at half height, and the height of the signal. | ||
* @param data,[y] | ||
* @returns {*[]} | ||
*/ | ||
function optimizeSingleLorentzian(xy, peak, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
let dt = Math.abs(t[0] - t[1]); | ||
let pInit = new Float64Array([peak.x, 1, peak.width]); | ||
let pMin = new Float64Array([peak.x - dt, 0.75, peak.width / 4]); | ||
let pMax = new Float64Array([peak.x + dt, 1.25, peak.width * 4]); | ||
let data = { | ||
x: t, | ||
y: yData, | ||
}; | ||
let lmOptions = { | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}; | ||
opts = Object.assign({}, lmOptions, opts); | ||
let pFit = LM__default['default'](data, singleLorentzian, opts); | ||
return { | ||
parameters: [ | ||
pFit.parameterValues[0], | ||
pFit.parameterValues[1] * maxY, | ||
pFit.parameterValues[2], | ||
], | ||
error: pFit.parameterError, | ||
}; | ||
} | ||
/* | ||
peaks on group should sorted | ||
*/ | ||
function optimizeLorentzianTrain(xy, group, opts = {}) { | ||
let t = xy[0]; | ||
let yData = xy[1]; | ||
let maxY = Math.max(...yData); | ||
yData.forEach((x, i, arr) => (arr[i] /= maxY)); | ||
let currentIndex = 0; | ||
let nbPoints = t.length; | ||
let nextX; | ||
let tI, yI; | ||
let result = []; | ||
let current; | ||
for (let i = 0; i < group.length; i++) { | ||
nextX = group[i].x - group[i].width * 1.5; | ||
while (t[currentIndex++] < nextX && currentIndex < nbPoints); | ||
nextX = group[i].x + group[i].width * 1.5; | ||
tI = []; | ||
yI = []; | ||
while (t[currentIndex] <= nextX && currentIndex < nbPoints) { | ||
tI.push(t[currentIndex]); | ||
yI.push(yData[currentIndex] * maxY); | ||
currentIndex++; | ||
} | ||
current = optimizeSingleLorentzian([tI, yI], group[i], opts); | ||
if (current) { | ||
result.push({ | ||
x: current.parameters[0], | ||
y: current.parameters[1], | ||
width: current.parameters[2], | ||
opt: true, | ||
}); | ||
} else { | ||
result.push({ | ||
x: group[i].x, | ||
y: group[i].y, | ||
width: group[i].width, | ||
opt: false, | ||
}); | ||
} | ||
function getValue(parameterIndex, peak, key, dt) { | ||
let value; | ||
switch (parameterIndex) { | ||
case 0: | ||
value = | ||
key === STATE_INIT | ||
? peak.x | ||
: key === STATE_MIN | ||
? peak.x - dt | ||
: peak.x + dt; | ||
break; | ||
case 1: | ||
value = key === STATE_INIT ? 1 : key === STATE_MIN ? 0 : 1.5; | ||
break; | ||
case 2: | ||
value = | ||
key === STATE_INIT | ||
? peak.width | ||
: key === STATE_MIN | ||
? peak.width / 4 | ||
: peak.width * 4; | ||
break; | ||
default: | ||
value = key === STATE_INIT ? 0.5 : key === STATE_MIN ? 0 : 1; | ||
} | ||
return result; | ||
return value; | ||
} | ||
exports.optimizeGaussianLorentzianSum = optimizeGaussianLorentzianSum; | ||
exports.optimizeGaussianSum = optimizeGaussianSum; | ||
exports.optimizeGaussianTrain = optimizeGaussianTrain; | ||
exports.optimizeLorentzianSum = optimizeLorentzianSum; | ||
exports.optimizeLorentzianTrain = optimizeLorentzianTrain; | ||
exports.optimizeSingleGaussian = optimizeSingleGaussian; | ||
exports.optimizeSingleLorentzian = optimizeSingleLorentzian; | ||
exports.singleGaussian = singleGaussian; | ||
exports.singleLorentzian = singleLorentzian; | ||
exports.sumOfGaussianLorentzians = sumOfGaussianLorentzians; | ||
exports.sumOfGaussians = sumOfGaussians; | ||
exports.sumOfLorentzians = sumOfLorentzians; | ||
exports.optimize = optimize; |
{ | ||
"name": "ml-spectra-fitting", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "Fit spectra using guassian or lorentzian", | ||
@@ -14,3 +14,4 @@ "main": "lib/index.js", | ||
"eslint-fix": "npm run eslint -- --fix", | ||
"prepublishOnly": "rollup -c", | ||
"compile": "rollup -c", | ||
"prepublishOnly": "npm run compile", | ||
"test": "npm run test-coverage && npm run eslint", | ||
@@ -36,2 +37,12 @@ "test-coverage": "jest --coverage", | ||
"homepage": "https://github.com/mljs/spectra-fitting", | ||
"jest": { | ||
"testEnvironment": "node" | ||
}, | ||
"prettier": { | ||
"arrowParens": "always", | ||
"semi": true, | ||
"singleQuote": true, | ||
"tabWidth": 2, | ||
"trailingComma": "all" | ||
}, | ||
"devDependencies": { | ||
@@ -50,4 +61,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.4", | ||
"dependencies": { | ||
"ml-levenberg-marquardt": "^2.1.1" | ||
"ml-array-max": "^1.2.0", | ||
"ml-levenberg-marquardt": "^2.1.1", | ||
"ml-peak-shape-generator": "^0.6.0", | ||
"spectrum-generator": "^4.2.0" | ||
} | ||
} |
153
README.md
@@ -0,14 +1,25 @@ | ||
[![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![npm download][download-image]][download-url] | ||
# ml-spectra-fitting | ||
[![NPM version][npm-image]][npm-url] | ||
[![build status][travis-image]][travis-url] | ||
[![npm download][download-image]][download-url] | ||
Curve fitting method in javascript. | ||
Bell optmization library. It contains: | ||
This is spectra fitting package optimize the position (x), max intensity (y), full width at half maximum (width) and the percent of gaussian (mu). It supports three kind of shapes: | ||
- `optimizeSingleLorentzian`: Function to fit a curve to a single 3 parameter Lorentzian function | ||
- `optimizeLorentzianSum`: Function to fit a curve to a set of 3 parameters Lorentzian functions | ||
- `optimizeSingleGaussian`: Function to fit a curve to a single 3 parameter Gaussian function | ||
- `optimizeGaussianSum`: Function to fit a curve to a set of 3 parameters Gaussian functions | ||
| Name | Equation | | ||
| ------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | ||
| Gaussian | <img src="https://tex.cheminfo.org/?tex=y%20%5Ccdot%20exp%20%5Cleft%5B%5Cfrac%7B%5Cdelta%7D%7B2%20%5Csigma%5E2%7D%5Cright%5D"/> | | ||
| Lorentzian | <img src="https://tex.cheminfo.org/?tex=y%5Ccdot%5Cfrac%7B%5Cgamma%7D%7B%5Cdelta%20%2B%20%5Cgamma%7D"/> | | ||
| Pseudo Voigt | <img src="https://tex.cheminfo.org/?tex=y%20*%20%5Cleft%5B%5Cmu%20%5Ccdot%20exp%20%5Cleft%5B%5Cfrac%7B%5Cdelta%7D%7B2%20%5Csigma%5E2%7D%5Cright%5D%20%2B%20(1%20-%20%5Cmu)%20%5Ccdot%20%5Cfrac%7B%5Cgamma%7D%7B%5Cdelta%20%2B%20%5Cgamma%7D%20%5Cright%5D%0A"/> | | ||
where | ||
| <img src="https://tex.cheminfo.org/?tex=%5Cdelta%20%3D%20%5Cleft(t%20-%20x%5Cright)%5E2%0A"/> | <img src="https://tex.cheminfo.org/?tex=%5Csigma%20%3D%20%5Cfrac%7Bwidth%7D%7B2%5Csqrt%7B2%20%5Ccdot%20Log(2)%7D%7D"/> | <img src="https://tex.cheminfo.org/?tex=%5Cgamma%3D%5Cleft(width%5Cright)%5E2"/> | | ||
| --------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------- | | ||
It is a wrapper of [ml-levenberg-marquardt](https://github.com/mljs/levenberg-marquardt) | ||
## [API Documentation](https://mljs.github.io/spectra-fitting/) | ||
## Installation | ||
@@ -18,2 +29,128 @@ | ||
## Example | ||
```js | ||
// import library | ||
import { optimizeSum } from 'ml-spectra-fitting'; | ||
import { generateSpectrum } from 'spectrum-generator'; | ||
const peaks = [ | ||
{ x: 0.5, y: 0.2, width: 0.2 }, | ||
{ x: -0.5, y: 0.2, width: 0.3 }, | ||
]; | ||
const data = generateSpectrum(peaks, {from: -1, to: 1, nbPoints: 41}); | ||
//the approximate values to be optimized, It could come from a peak picking with ml-gsd | ||
let peakList = [ | ||
{ | ||
x: -0.5, | ||
y: 0.18, | ||
width: 0.18, | ||
}, | ||
{ | ||
x: 0.52, | ||
y: 0.17, | ||
width: 0.37, | ||
}, | ||
]; | ||
// the function recive a peaklist with {x, y, width} as a guess | ||
// and return a list of objects | ||
let fittedParams = optimize(data, peakList); | ||
console.log(fittedParams); | ||
/** | ||
{ | ||
error: 0.010502794375558983, | ||
iterations: 15, | ||
peaks: [ | ||
{ | ||
x: -0.49999760133593774, | ||
y: 0.1999880261075537, | ||
width: 0.3000369491704072 | ||
}, | ||
{ | ||
x: 0.5000084944744884, | ||
y: 0.20004144804853427, | ||
width: 0.1999731186595336 | ||
} | ||
] | ||
} | ||
*/ | ||
``` | ||
For data with and combination of signals with shapes between gaussian and lorentzians, we could use the kind pseudovoigt to fit the data. | ||
```js | ||
import { optimize } from 'ml-spectra-fitting'; | ||
import { SpectrumGenerator } from 'spectrum-generator'; | ||
const generator = new SpectrumGenerator({ | ||
nbPoints: 101, | ||
from: -1, | ||
to: 1, | ||
}); | ||
// by default the kind of shape is gaussian; | ||
generator.addPeak({ x: 0.5, y: 0.2 }, { width: 0.2 }); | ||
generator.addPeak( | ||
{ x: -0.5, y: 0.2 }, | ||
{ | ||
width: 0.1, | ||
shape: { | ||
kind: 'lorentzian', | ||
options: { | ||
fwhm: 1000, | ||
length: 50001, | ||
factor: 5 | ||
}, | ||
}, | ||
}, | ||
); | ||
//points to fit {x, y}; | ||
let data = generator.getSpectrum(); | ||
console.log(JSON.stringify({x: Array.from(data.x), y: Array.from(data.y)})) | ||
//the approximate values to be optimized, It could coming from a peak picking with ml-gsd | ||
let peakList = [ | ||
{ | ||
x: -0.5, | ||
y: 0.22, | ||
width: 0.25, | ||
}, | ||
{ | ||
x: 0.52, | ||
y: 0.18, | ||
width: 0.18, | ||
}, | ||
]; | ||
// the function recive a peaklist with {x, y, width} as a guess | ||
// and return a list of objects | ||
let fittedParams = optimize(data, peakList, { kind: 'pseudovoigt' }); | ||
console.log(fittedParams); | ||
/** | ||
{ | ||
error: 0.12361588652854476, | ||
iterations: 100, | ||
peaks: [ | ||
{ | ||
x: -0.5000014532421942, | ||
y: 0.19995307937326137, | ||
width: 0.10007670374735196, | ||
mu: 0.004731136777288483 | ||
}, | ||
{ | ||
x: 0.5001051783652894, | ||
y: 0.19960010175400406, | ||
width: 0.19935932346969124, | ||
mu: 1 | ||
} | ||
] | ||
} | ||
*/ | ||
``` | ||
## License | ||
@@ -20,0 +157,0 @@ |
140
src/index.js
@@ -1,27 +0,115 @@ | ||
import { optimizeGaussianLorentzianSum } from './optimizeGaussianLorentzianSum'; | ||
import { optimizeGaussianSum } from './optimizeGaussianSum'; | ||
import { optimizeGaussianTrain } from './optimizeGaussianTrain'; | ||
import { optimizeLorentzianSum } from './optimizeLorentzianSum'; | ||
import { optimizeLorentzianTrain } from './optimizeLorentzianTrain'; | ||
import { optimizeSingleGaussian } from './optimizeSingleGaussian'; | ||
import { optimizeSingleLorentzian } from './optimizeSingleLorentzian'; | ||
import { singleGaussian } from './singleGaussian'; | ||
import { singleLorentzian } from './singleLorentzian'; | ||
import { sumOfGaussianLorentzians } from './sumOfGaussianLorentzians'; | ||
import { sumOfGaussians } from './sumOfGaussians'; | ||
import { sumOfLorentzians } from './sumOfLorentzians'; | ||
import getMaxValue from 'ml-array-max'; | ||
import LM from 'ml-levenberg-marquardt'; | ||
export { | ||
optimizeGaussianLorentzianSum, | ||
optimizeGaussianTrain, | ||
optimizeGaussianSum, | ||
optimizeLorentzianSum, | ||
optimizeLorentzianTrain, | ||
optimizeSingleGaussian, | ||
optimizeSingleLorentzian, | ||
singleGaussian, | ||
singleLorentzian, | ||
sumOfGaussianLorentzians, | ||
sumOfGaussians, | ||
sumOfLorentzians, | ||
}; | ||
import { sumOfGaussianLorentzians } from './shapes/sumOfGaussianLorentzians'; | ||
import { sumOfGaussians } from './shapes/sumOfGaussians'; | ||
import { sumOfLorentzians } from './shapes/sumOfLorentzians'; | ||
const STATE_INIT = 0; | ||
const STATE_MIN = 1; | ||
const STATE_MAX = 2; | ||
const keys = ['x', 'y', 'width', 'mu']; | ||
/** | ||
* Fits a set of points to the sum of a set of bell functions. | ||
* @param {Object} input - 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 {Object} [options.lmOptions = {}] - options of ml-levenberg-marquardt optimization package. | ||
* @returns {Object} - A object with fitting error and the list of optimized parameters { parameters: [ {x, y, width} ], error } if the kind of shape is pseudoVoigt mu parameter is optimized. | ||
*/ | ||
export function optimize(data, peakList, options = {}) { | ||
let { kind = 'gaussian', lmOptions = {} } = options; | ||
let maxY = getMaxValue(data.y); | ||
data.y.forEach((_, i, arr) => (arr[i] /= maxY)); | ||
let nbParams; | ||
let paramsFunc; | ||
switch (kind.toLowerCase()) { | ||
case 'gaussian': | ||
nbParams = 3; | ||
paramsFunc = sumOfGaussians; | ||
break; | ||
case 'lorentzian': | ||
nbParams = 3; | ||
paramsFunc = sumOfLorentzians; | ||
break; | ||
case 'pseudovoigt': | ||
nbParams = 4; | ||
paramsFunc = sumOfGaussianLorentzians; | ||
break; | ||
default: | ||
throw new Error('kind of shape is not supported'); | ||
} | ||
let nL = peakList.length; | ||
let pInit = new Float64Array(nL * nbParams); | ||
let pMin = new Float64Array(nL * nbParams); | ||
let pMax = new Float64Array(nL * nbParams); | ||
let dt = Math.abs(data.x[0] - data.x[1]); | ||
for (let i = 0; i < nL; i++) { | ||
let peak = peakList[i]; | ||
for (let s = 0; s < nbParams; s++) { | ||
pInit[i + s * nL] = getValue(s, peak, STATE_INIT, dt); | ||
pMin[i + s * nL] = getValue(s, peak, STATE_MIN, dt); | ||
pMax[i + s * nL] = getValue(s, peak, STATE_MAX, dt); | ||
} | ||
} | ||
lmOptions = Object.assign( | ||
{ | ||
damping: 1.5, | ||
initialValues: pInit, | ||
minValues: pMin, | ||
maxValues: pMax, | ||
gradientDifference: dt / 10000, | ||
maxIterations: 100, | ||
errorTolerance: 10e-5, | ||
}, | ||
lmOptions, | ||
); | ||
let pFit = LM(data, paramsFunc, lmOptions); | ||
let { parameterError: error, iterations } = pFit; | ||
let result = { error, iterations, peaks: new Array(nL) }; | ||
for (let i = 0; i < nL; i++) { | ||
let peak = {}; | ||
pFit.parameterValues[i + nL] *= maxY; | ||
for (let s = 0; s < nbParams; s++) { | ||
peak[keys[s]] = pFit.parameterValues[i + s * nL]; | ||
} | ||
result.peaks[i] = peak; | ||
} | ||
return result; | ||
} | ||
function getValue(parameterIndex, peak, key, dt) { | ||
let value; | ||
switch (parameterIndex) { | ||
case 0: | ||
value = | ||
key === STATE_INIT | ||
? peak.x | ||
: key === STATE_MIN | ||
? peak.x - dt | ||
: peak.x + dt; | ||
break; | ||
case 1: | ||
value = key === STATE_INIT ? 1 : key === STATE_MIN ? 0 : 1.5; | ||
break; | ||
case 2: | ||
value = | ||
key === STATE_INIT | ||
? peak.width | ||
: key === STATE_MIN | ||
? peak.width / 4 | ||
: peak.width * 4; | ||
break; | ||
default: | ||
value = key === STATE_INIT ? 0.5 : key === STATE_MIN ? 0 : 1; | ||
} | ||
return value; | ||
} |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
165
23116
4
10
434
2
+ Addedml-array-max@^1.2.0
+ Addedspectrum-generator@^4.2.0
+ Addedd3-random@2.2.2(transitive)
+ Addedml-peak-shape-generator@0.6.11.0.0(transitive)
+ Addedml-xsadd@2.0.0(transitive)
+ Addedobject-hash@2.2.0(transitive)
+ Addedspectrum-generator@4.8.1(transitive)