UNPKG

ml-spectra-fitting

Version:

Fit spectra using gaussian or lorentzian

184 lines (161 loc) 6.35 kB
import type { DataXY } from 'cheminfo-types'; import type { Shape1D } from 'ml-peak-shape-generator'; import { xMinMaxValues } from 'ml-spectra-processing'; import { getSumOfShapes } from './shapes/getSumOfShapes.ts'; import { getInternalPeaks } from './util/internalPeaks/getInternalPeaks.ts'; import { selectMethod } from './util/selectMethod.ts'; import type { InternalDirectOptimizationOptions } from './util/wrappers/directOptimization.js'; export interface InitialParameter { init?: OptimizationParameter; /** 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. */ min?: OptimizationParameter; /** 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. */ max?: OptimizationParameter; /** 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. */ gradientDifference?: OptimizationParameter; } export interface Peak { id?: string; x: number; y: number; shape?: Shape1D; parameters?: Record< string, { init?: number; min?: number; max?: number; gradientDifference?: number } >; } export interface OptimizedPeak { x: number; y: number; shape: Shape1D; } type OptimizedPeakIDOrNot<T extends Peak> = T extends { id: string } ? OptimizedPeak & { id: string } : OptimizedPeak; type OptimizationParameter = number | ((peak: Peak) => number); interface GeneralAlgorithmOptions { /** number of max iterations * @default 100 */ maxIterations?: number; } export interface LMOptimizationOptions extends GeneralAlgorithmOptions { /** maximum time running before break in seconds */ timeout?: number; /** damping factor * @default 1.5 */ damping?: number; /** error tolerance * @default 1e-8 */ errorTolerance?: number; } export interface DirectOptimizationOptions extends GeneralAlgorithmOptions, InternalDirectOptimizationOptions {} export interface OptimizationOptions { /** * kind of algorithm. By default it's levenberg-marquardt */ kind?: 'lm' | 'levenbergMarquardt' | 'direct'; /** options for the specific kind of algorithm */ options?: DirectOptimizationOptions | LMOptimizationOptions; } export interface OptimizeOptions { /** * baseline value to shift the intensity of data and peak */ baseline?: number; /** * Kind of shape used for fitting. **/ shape?: Shape1D; /** * options of each parameter to be optimized e.g. For a pseudovoigt shape * it could have x, y, fwhm and mu 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 */ parameters?: Record<string, InitialParameter>; /** * The kind and options of the algorithm use to optimize parameters. */ optimization?: OptimizationOptions; } /** * 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 peaks - A list of initial parameters to be optimized. e.g. coming from a peak picking [{x, y, width}]. * @param options - Options for optimize * @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. */ export function optimize<T extends Peak>( data: DataXY, peaks: T[], options: OptimizeOptions = {}, ): { error: number; peaks: Array<OptimizedPeakIDOrNot<T>>; iterations: number; } { // rescale data const temp = xMinMaxValues(data.y); const minMaxY = { ...temp, range: temp.max - temp.min }; const internalPeaks = getInternalPeaks(peaks, minMaxY, options); // need to rescale what is related to Y const { baseline: shiftValue = minMaxY.min } = options; const normalizedY = new Float64Array(data.y.length); for (let i = 0; i < data.y.length; i++) { normalizedY[i] = (data.y[i] - shiftValue) / minMaxY.range; } const nbParams = internalPeaks[internalPeaks.length - 1].toIndex + 1; const minValues = new Float64Array(nbParams); const maxValues = new Float64Array(nbParams); const initialValues = new Float64Array(nbParams); const gradientDifferences = new Float64Array(nbParams); let index = 0; for (const peak of internalPeaks) { for (let i = 0; i < peak.parameters.length; i++) { minValues[index] = peak.propertiesValues.min[i]; maxValues[index] = peak.propertiesValues.max[i]; initialValues[index] = peak.propertiesValues.init[i]; gradientDifferences[index] = peak.propertiesValues.gradientDifference[i]; index++; } } const { algorithm, optimizationOptions } = selectMethod(options.optimization); const sumOfShapes = getSumOfShapes(internalPeaks); const fitted = algorithm({ x: data.x, y: normalizedY }, sumOfShapes, { minValues, maxValues, initialValues, gradientDifference: gradientDifferences, ...optimizationOptions, }); const fittedValues = fitted.parameterValues; const newPeaks = []; for (const peak of internalPeaks) { const { id, shape, parameters, fromIndex } = peak; let newPeak = { x: 0, y: 0, shape } as OptimizedPeakIDOrNot<T>; if (id) { newPeak = { ...newPeak, id } as OptimizedPeakIDOrNot<T>; } newPeak.x = fittedValues[fromIndex]; newPeak.y = fittedValues[fromIndex + 1] * minMaxY.range + shiftValue; for (let i = 2; i < parameters.length; i++) { //@ts-expect-error should be fixed once newPeak.shape[parameters[i]] = fittedValues[fromIndex + i]; } newPeaks.push(newPeak); } return { error: fitted.parameterError, iterations: fitted.iterations, peaks: newPeaks, }; }