ml-spectra-fitting
Advanced tools
Comparing version 3.0.4 to 3.1.0
442
lib/index.js
@@ -1,390 +0,68 @@ | ||
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var getMaxValue = require('ml-array-max'); | ||
var mlPeakShapeGenerator = require('ml-peak-shape-generator'); | ||
var mlLevenbergMarquardt = require('ml-levenberg-marquardt'); | ||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
var getMaxValue__default = /*#__PURE__*/_interopDefaultLegacy(getMaxValue); | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.optimize = void 0; | ||
const checkInput_1 = require("./util/checkInput"); | ||
const selectMethod_1 = require("./util/selectMethod"); | ||
/** | ||
* 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 | ||
* @returns {*} | ||
* Fits a set of points to the sum of a set of bell functions. | ||
* | ||
* @param data - An object containing the x and y data to be fitted. | ||
* @param peakList - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. | ||
* @param options - Options. | ||
* @returns - An 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. | ||
*/ | ||
// const pseudoVoigtFct = PseudoVoigt.fct; | ||
function sumOfGaussianLorentzians(p) { | ||
const pseudoVoigt = new mlPeakShapeGenerator.PseudoVoigt(); | ||
return (t) => { | ||
let nL = p.length / 4; | ||
let result = 0; | ||
for (let i = 0; i < nL; i++) { | ||
pseudoVoigt.fwhm = p[i + nL * 2]; | ||
pseudoVoigt.mu = p[i + nL * 3]; | ||
result += p[i + nL] * pseudoVoigt.fct(t - p[i]); | ||
function optimize(data, peakList, options = {}) { | ||
if (!options.shape) { | ||
options = { ...options, ...{ shape: { kind: 'gaussian' } } }; | ||
} | ||
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: widths; | ||
* @param t Ordinate values | ||
* @param p Gaussian parameters | ||
* @returns {*} | ||
*/ | ||
function sumOfGaussians(p) { | ||
const nL = p.length / 3; | ||
const gaussian = new mlPeakShapeGenerator.Gaussian(); | ||
return (t) => { | ||
let result = 0; | ||
for (let i = 0; i < nL; i++) { | ||
gaussian.fwhm = p[i + nL * 2]; | ||
result += p[i + nL] * gaussian.fct(t - p[i]); | ||
const { y, x, maxY, minY, peaks, paramsFunc, optimization } = (0, checkInput_1.checkInput)(data, peakList, options); | ||
let parameters = optimization.parameters; | ||
let nbShapes = peaks.length; | ||
let parameterKey = Object.keys(parameters); | ||
let nbParams = nbShapes * parameterKey.length; | ||
let pMin = new Float64Array(nbParams); | ||
let pMax = new Float64Array(nbParams); | ||
let pInit = new Float64Array(nbParams); | ||
let gradientDifference = new Float64Array(nbParams); | ||
for (let i = 0; i < nbShapes; i++) { | ||
let peak = peaks[i]; | ||
for (let k = 0; k < parameterKey.length; k++) { | ||
let key = parameterKey[k]; | ||
let init = parameters[key].init; | ||
let min = parameters[key].min; | ||
let max = parameters[key].max; | ||
let gradientDifferenceValue = parameters[key].gradientDifference; | ||
pInit[i + k * nbShapes] = init[i % init.length](peak); | ||
pMin[i + k * nbShapes] = min[i % min.length](peak); | ||
pMax[i + k * nbShapes] = max[i % max.length](peak); | ||
gradientDifference[i + k * nbShapes] = | ||
gradientDifferenceValue[i % gradientDifferenceValue.length](peak); | ||
} | ||
} | ||
return result; | ||
}; | ||
} | ||
/** | ||
* 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 | ||
* @param p Lorentzian parameters | ||
* @returns {*} | ||
*/ | ||
// const lorentzianFct = Lorentzian.fct; | ||
function sumOfLorentzians(p) { | ||
const lorentzian = new mlPeakShapeGenerator.Lorentzian(); | ||
return (t) => { | ||
let nL = p.length / 3; | ||
let result = 0; | ||
for (let i = 0; i < nL; i++) { | ||
lorentzian.fwhm = p[i + nL * 2]; | ||
result += p[i + nL] * lorentzian.fct(t - p[i]); | ||
let { algorithm, optimizationOptions } = (0, selectMethod_1.selectMethod)(optimization); | ||
optimizationOptions.minValues = pMin; | ||
optimizationOptions.maxValues = pMax; | ||
optimizationOptions.initialValues = pInit; | ||
optimizationOptions.gradientDifference = gradientDifference; | ||
let pFit = algorithm({ x, y }, paramsFunc, optimizationOptions); | ||
let { parameterError: error, iterations } = pFit; | ||
let result = { error, iterations, peaks }; | ||
for (let i = 0; i < nbShapes; i++) { | ||
for (let k = 0; k < parameterKey.length; k++) { | ||
const key = parameterKey[k]; | ||
const value = pFit.parameterValues[i + k * nbShapes]; | ||
if (key === 'x' || key === 'fwhm') { | ||
peaks[i][key] = value; | ||
} | ||
else if (key === 'y') { | ||
peaks[i][key] = value * maxY + minY; | ||
} | ||
else { | ||
peaks[i].shape[key] = value; | ||
} | ||
} | ||
} | ||
return result; | ||
}; | ||
} | ||
const isValidKey = (key) => { | ||
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; | ||
}; | ||
const isObject = (val) => { | ||
return typeof val === 'object'; | ||
}; | ||
const isPrimitive = (val) => { | ||
return typeof val === 'object' ? val === null : typeof val !== 'function'; | ||
}; | ||
function assignDeep(target, ...args) { | ||
let index = 0; | ||
if (isPrimitive(target)) target = args[index++]; | ||
if (!target) target = {}; | ||
for (; index < args.length; index++) { | ||
if (!isObject(args[index])) continue; | ||
for (const key in args[index]) { | ||
if (!isValidKey(key)) continue; | ||
if (isObject(target[key]) && isObject(args[index][key])) { | ||
assignDeep(target[key], args[index][key]); | ||
} else { | ||
target[key] = args[index][key]; | ||
} | ||
} | ||
} | ||
return target; | ||
} | ||
function checkInput(data, peakList, options) { | ||
let { | ||
shape = { kind: 'gaussian' }, | ||
optimization = { | ||
kind: 'lm', | ||
}, | ||
} = options; | ||
let peaks = JSON.parse(JSON.stringify(peakList)); | ||
if (typeof shape.kind !== 'string') { | ||
throw new Error('kind should be a string'); | ||
} | ||
let kind = shape.kind.toLowerCase().replace(/[^a-z]/g, ''); | ||
let paramsFunc; | ||
let defaultParameters; | ||
switch (kind) { | ||
case 'gaussian': | ||
paramsFunc = sumOfGaussians; | ||
defaultParameters = { | ||
x: { | ||
init: (peak) => peak.x, | ||
max: (peak) => peak.x + peak.fwhm * 2, | ||
min: (peak) => peak.x - peak.fwhm * 2, | ||
gradientDifference: (peak) => peak.fwhm * 2e-3, | ||
}, | ||
y: { | ||
init: (peak) => peak.y, | ||
max: () => 1.5, | ||
min: () => 0, | ||
gradientDifference: () => 1e-3, | ||
}, | ||
fwhm: { | ||
init: (peak) => peak.fwhm, | ||
max: (peak) => peak.fwhm * 4, | ||
min: (peak) => peak.fwhm * 0.25, | ||
gradientDifference: (peak) => peak.fwhm * 2e-3, | ||
}, | ||
}; | ||
break; | ||
case 'lorentzian': | ||
paramsFunc = sumOfLorentzians; | ||
defaultParameters = { | ||
x: { | ||
init: (peak) => peak.x, | ||
max: (peak) => peak.x + peak.fwhm * 2, | ||
min: (peak) => peak.x - peak.fwhm * 2, | ||
gradientDifference: (peak) => peak.fwhm * 2e-3, | ||
}, | ||
y: { | ||
init: (peak) => peak.y, | ||
max: () => 1.5, | ||
min: () => 0, | ||
gradientDifference: () => 1e-3, | ||
}, | ||
fwhm: { | ||
init: (peak) => peak.fwhm, | ||
max: (peak) => peak.fwhm * 4, | ||
min: (peak) => peak.fwhm * 0.25, | ||
gradientDifference: (peak) => peak.fwhm * 2e-3, | ||
}, | ||
}; | ||
break; | ||
case 'pseudovoigt': | ||
paramsFunc = sumOfGaussianLorentzians; | ||
defaultParameters = { | ||
x: { | ||
init: (peak) => peak.x, | ||
max: (peak) => peak.x + peak.fwhm * 2, | ||
min: (peak) => peak.x - peak.fwhm * 2, | ||
gradientDifference: (peak) => peak.fwhm * 2e-3, | ||
}, | ||
y: { | ||
init: (peak) => peak.y, | ||
max: () => 1.5, | ||
min: () => 0, | ||
gradientDifference: () => 1e-3, | ||
}, | ||
fwhm: { | ||
init: (peak) => peak.fwhm, | ||
max: (peak) => peak.fwhm * 4, | ||
min: (peak) => peak.fwhm * 0.25, | ||
gradientDifference: (peak) => peak.fwhm * 2e-3, | ||
}, | ||
mu: { | ||
init: (peak) => | ||
peak.shape && peak.shape.mu !== undefined ? peak.shape.mu : 0.5, | ||
min: () => 0, | ||
max: () => 1, | ||
gradientDifference: () => 0.01, | ||
}, | ||
}; | ||
break; | ||
default: | ||
throw new Error('kind of shape is not supported'); | ||
} | ||
let x = data.x; | ||
let maxY = getMaxValue__default["default"](data.y); | ||
let y = new Array(x.length); | ||
for (let i = 0; i < x.length; i++) { | ||
y[i] = data.y[i] / maxY; | ||
} | ||
for (let i = 0; i < peaks.length; i++) { | ||
peaks[i].y /= maxY; | ||
peaks[i].shape = { | ||
kind: shape.kind, | ||
...peaks[i].shape, | ||
}; | ||
} | ||
let parameters = assignDeep({}, defaultParameters, optimization.parameters); | ||
for (let key in parameters) { | ||
for (let par in parameters[key]) { | ||
if (!Array.isArray(parameters[key][par])) { | ||
parameters[key][par] = [parameters[key][par]]; | ||
} | ||
if ( | ||
parameters[key][par].length !== 1 && | ||
parameters[key][par].length !== peaks.length | ||
) { | ||
throw new Error(`The length of ${key}-${par} is not correct`); | ||
} | ||
for (let index = 0; index < parameters[key][par].length; index++) { | ||
if (typeof parameters[key][par][index] === 'number') { | ||
let value = parameters[key][par][index]; | ||
parameters[key][par][index] = () => value; | ||
} | ||
} | ||
} | ||
} | ||
optimization.parameters = parameters; | ||
return { | ||
y, | ||
x, | ||
maxY, | ||
peaks, | ||
paramsFunc, | ||
optimization, | ||
}; | ||
} | ||
const LEVENBERG_MARQUARDT = 1; | ||
function selectMethod(optimizationOptions = {}) { | ||
let { kind, options } = optimizationOptions; | ||
kind = getKind(kind); | ||
switch (kind) { | ||
case LEVENBERG_MARQUARDT: | ||
return { | ||
algorithm: mlLevenbergMarquardt.levenbergMarquardt, | ||
optimizationOptions: checkOptions(kind, options), | ||
}; | ||
default: | ||
throw new Error(`Unknown kind algorithm`); | ||
} | ||
} | ||
function checkOptions(kind, options = {}) { | ||
// eslint-disable-next-line default-case | ||
switch (kind) { | ||
case LEVENBERG_MARQUARDT: | ||
return Object.assign({}, lmOptions, options); | ||
} | ||
} | ||
function getKind(kind) { | ||
if (typeof kind !== 'string') return kind; | ||
switch (kind.toLowerCase().replace(/[^a-z]/g, '')) { | ||
case 'lm': | ||
case 'levenbergmarquardt': | ||
return LEVENBERG_MARQUARDT; | ||
default: | ||
throw new Error(`Unknown kind algorithm`); | ||
} | ||
} | ||
const lmOptions = { | ||
damping: 1.5, | ||
maxIterations: 100, | ||
errorTolerance: 1e-8, | ||
}; | ||
/** | ||
* Fits a set of points to the sum of a set of bell functions. | ||
* @param {object} data - An object containing the x and y data to be fitted. | ||
* @param {array} peaks - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. | ||
* @param {object} [options = {}] | ||
* @param {object} [options.shape={}] - it's specify the kind of shape used to fitting. | ||
* @param {string} [options.shape.kind = 'gaussian'] - kind of shape; lorentzian, gaussian and pseudovoigt are supported. | ||
* @param {object} [options.optimization = {}] - it's specify the kind and options of the algorithm use to optimize parameters. | ||
* @param {object} [options.optimization.kind = 'lm'] - kind of algorithm. By default it's levenberg-marquardt. | ||
* @param {object} [options.optimization.parameters] - options of each parameter to be optimized e.g. For a gaussian shape | ||
* it could have x, y and with properties, each of which could contain init, min, max and gradientDifference, those options will define the guess, | ||
* the min and max value of the parameter (search space) and the step size to approximate the jacobian matrix respectively. Those options could be a number, | ||
* array of numbers, callback, or array of callbacks. Each kind of shape has default parameters so it could be undefined. | ||
* @param {object} [options.optimization.parameters.x] - options for x parameter. | ||
* @param {number|callback|array<number|callback>} [options.optimization.parameters.x.init] - definition of the starting point of the parameter (the guess), | ||
* if it is a callback the method pass the peak as the unique input, if it is an array the first element define the guess of the first peak and so on. | ||
* @param {number|callback|array<number|callback>} [options.optimization.parameters.x.min] - definition of the lower limit of the parameter, | ||
* if it is a callback the method pass the peak as the unique input, if it is an array the first element define the min of the first peak and so on. | ||
* @param {number|callback|array<number|callback>} [options.optimization.parameters.x.max] - definition of the upper limit of the parameter, | ||
* if it is a callback the method pass the peak as the unique input, if it is an array the first element define the max of the first peak and so on. | ||
* @param {number|callback|array<number|callback>} [options.optimization.parameters.x.gradientDifference] - definition of the step size to approximate the jacobian matrix of the parameter, | ||
* if it is a callback the method pass the peak as the unique input, if it is an array the first element define the gradientDifference of the first peak and so on. | ||
* @param {object} [options.optimization.options = {}] - options for the specific kind of algorithm. | ||
* @param {number} [options.optimization.options.timeout] - maximum time running before break in seconds. | ||
* @param {number} [options.optimization.options.damping=1.5] | ||
* @param {number} [options.optimization.options.maxIterations=100] | ||
* @param {number} [options.optimization.options.errorTolerance=1e-8] | ||
* @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 optimize(data, peakList, options = {}) { | ||
const { y, x, maxY, peaks, paramsFunc, optimization } = checkInput( | ||
data, | ||
peakList, | ||
options, | ||
); | ||
let parameters = optimization.parameters; | ||
let nbShapes = peaks.length; | ||
let parameterKey = Object.keys(parameters); | ||
let nbParams = nbShapes * parameterKey.length; | ||
let pMin = new Float64Array(nbParams); | ||
let pMax = new Float64Array(nbParams); | ||
let pInit = new Float64Array(nbParams); | ||
let gradientDifference = new Float64Array(nbParams); | ||
for (let i = 0; i < nbShapes; i++) { | ||
let peak = peaks[i]; | ||
for (let k = 0; k < parameterKey.length; k++) { | ||
let key = parameterKey[k]; | ||
let init = parameters[key].init; | ||
let min = parameters[key].min; | ||
let max = parameters[key].max; | ||
let gradientDifferenceValue = parameters[key].gradientDifference; | ||
pInit[i + k * nbShapes] = init[i % init.length](peak); | ||
pMin[i + k * nbShapes] = min[i % min.length](peak); | ||
pMax[i + k * nbShapes] = max[i % max.length](peak); | ||
gradientDifference[i + k * nbShapes] = | ||
gradientDifferenceValue[i % gradientDifferenceValue.length](peak); | ||
} | ||
} | ||
let { algorithm, optimizationOptions } = selectMethod(optimization); | ||
optimizationOptions.minValues = pMin; | ||
optimizationOptions.maxValues = pMax; | ||
optimizationOptions.initialValues = pInit; | ||
optimizationOptions.gradientDifference = gradientDifference; | ||
let pFit = algorithm({ x, y }, paramsFunc, optimizationOptions); | ||
let { parameterError: error, iterations } = pFit; | ||
let result = { error, iterations, peaks }; | ||
for (let i = 0; i < nbShapes; i++) { | ||
for (let k = 0; k < parameterKey.length; k++) { | ||
const key = parameterKey[k]; | ||
const value = pFit.parameterValues[i + k * nbShapes]; | ||
// we modify the optimized parameters | ||
if (key === 'x' || key === 'fwhm') { | ||
peaks[i][parameterKey[k]] = value; | ||
} else if (key === 'y') { | ||
peaks[i][parameterKey[k]] = value * maxY; | ||
} else { | ||
peaks[i].shape[parameterKey[k]] = value; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
exports.optimize = optimize; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "ml-spectra-fitting", | ||
"version": "3.0.4", | ||
"version": "3.1.0", | ||
"description": "Fit spectra using gaussian or lorentzian", | ||
"main": "lib/index.js", | ||
"module": "src/index.js", | ||
"types": "src/spectra-fitting.d.ts", | ||
"main": "./lib/index.js", | ||
"module": "./lib-esm/index.js", | ||
"types": "./lib/index.d.ts", | ||
"files": [ | ||
"lib", | ||
"src" | ||
"src", | ||
"lib-esm" | ||
], | ||
"scripts": { | ||
"build": "cheminfo-build --entry src/index.js --root SpectraFitting", | ||
"build": "npm run tsc && cheminfo-build --entry lib-esm/index.js --root SpectraFitting", | ||
"check-types": "tsc --noEmit", | ||
"clean": "rimraf lib lib-esm", | ||
"eslint": "eslint src", | ||
"eslint-fix": "npm run eslint -- --fix", | ||
"compile": "rollup -c", | ||
"prepublishOnly": "npm run compile", | ||
"test": "npm run test-coverage && npm run eslint", | ||
"prepack": "npm run tsc", | ||
"test": "npm run test-only && npm run eslint && npm run check-types", | ||
"prettier": "prettier --check src", | ||
"prettier-write": "prettier --write src", | ||
"test-coverage": "jest --coverage", | ||
"test-only": "jest", | ||
"test-only": "jest --coverage", | ||
"tsc": "npm run clean && npm run tsc-cjs && npm run tsc-esm", | ||
"tsc-cjs": "tsc --project tsconfig.cjs.json", | ||
"tsc-esm": "tsc --project tsconfig.esm.json", | ||
"debug": "npm run prepublishOnly && node src/debug.js" | ||
@@ -44,23 +49,23 @@ }, | ||
"homepage": "https://github.com/mljs/spectra-fitting", | ||
"jest": { | ||
"testEnvironment": "node" | ||
}, | ||
"devDependencies": { | ||
"@babel/plugin-transform-modules-commonjs": "^7.16.8", | ||
"@types/jest": "^27.4.0", | ||
"@types/jest": "^27.4.1", | ||
"cheminfo-build": "^1.1.11", | ||
"eslint": "^8.7.0", | ||
"eslint-config-cheminfo": "^7.2.1", | ||
"eslint": "^8.9.0", | ||
"eslint-config-cheminfo": "^7.2.2", | ||
"eslint-config-cheminfo-typescript": "^10.3.0", | ||
"esm": "^3.2.25", | ||
"jest": "^27.4.7", | ||
"jest": "^27.5.1", | ||
"jest-matcher-deep-close-to": "^3.0.2", | ||
"prettier": "^2.5.1", | ||
"rollup": "^2.66.0", | ||
"spectrum-generator": "^6.0.2" | ||
"spectrum-generator": "^7.0.1", | ||
"ts-jest": "^27.1.3", | ||
"typescript": "^4.5.5" | ||
}, | ||
"dependencies": { | ||
"cheminfo-types": "^0.9.1", | ||
"cheminfo-types": "^1.0.0", | ||
"ml-array-max": "^1.2.4", | ||
"ml-levenberg-marquardt": "^4.0.0", | ||
"ml-peak-shape-generator": "^4.0.1" | ||
"ml-peak-shape-generator": "^4.1.1" | ||
} | ||
} |
@@ -134,3 +134,3 @@ # ml-spectra-fitting | ||
y: 0.19995307937326137, | ||
width: 0.10007670374735196, | ||
fwhm: 0.10007670374735196, | ||
mu: 0.004731136777288483 | ||
@@ -141,3 +141,3 @@ }, | ||
y: 0.19960010175400406, | ||
width: 0.19935932346969124, | ||
fwhm: 0.19935932346969124, | ||
mu: 1 | ||
@@ -144,0 +144,0 @@ } |
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
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
103522
78
1895
13
1
- Removedcheminfo-types@0.9.1(transitive)
Updatedcheminfo-types@^1.0.0