Skip to content

Commit

Permalink
feat: add optimizePeaksWithLogs to be able to debug
Browse files Browse the repository at this point in the history
  • Loading branch information
lpatiny committed May 3, 2022
1 parent 9b8e0b6 commit 4e7848b
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 65 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './gsd';
export * from './post/optimizePeaks';
export * from './post/optimizePeaksWithLogs';
export * from './post/joinBroadPeaks';
export * from './post/broadenPeaks';
export * from './utils/appendShapeAndFWHM';
Expand Down
63 changes: 63 additions & 0 deletions src/post/__tests__/optimizePeaksWithLogs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { toMatchCloseTo } from 'jest-matcher-deep-close-to';
import { generateSpectrum } from 'spectrum-generator';

import { optimizePeaksWithLogs } from '../optimizePeaksWithLogs';

expect.extend({ toMatchCloseTo });

describe('optimizePeaksWithLogs', () => {
it('Should throw because execution time is over timeout', () => {
const peaks = [{ x: 0, y: 1, width: 0.12 }];

const data = generateSpectrum(peaks, {
generator: {
from: -0.5,
to: 0.5,
nbPoints: 101,
shape: {
kind: 'gaussian',
},
},
});

let result = optimizePeaksWithLogs(data, [
{
x: 0.01,
y: 0.9,
width: 0.11,
},
]);
expect(result.logs).toMatchObject([
{
iterations: 100,
error: 0.000017852930772995625,
parameters: { kind: 'lm', options: { timeout: 10 } },
message: 'optimization successful',
groupSize: 1,
},
]);
expect(result.optimizedPeaks).toMatchCloseTo([
{
x: 0,
y: 1,
width: 0.12,
shape: {
kind: 'gaussian',
fwhm: 0.14128970668640126,
},
},
]);

const options = {
optimization: {
kind: 'lm' as const,
options: {
timeout: 0,
},
},
};
expect(() =>
optimizePeaksWithLogs(data, [{ x: 0.1, y: 0.9, width: 0.11 }], options),
).toThrow('The execution time is over to 0 seconds');
});
});
69 changes: 4 additions & 65 deletions src/post/optimizePeaks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type { DataXY, PeakXYWidth } from 'cheminfo-types';
import { getShape1D, Shape1D } from 'ml-peak-shape-generator';
import { optimize } from 'ml-spectra-fitting';
import { Shape1D } from 'ml-peak-shape-generator';
import type { OptimizationOptions } from 'ml-spectra-fitting';
import { xGetFromToIndex } from 'ml-spectra-processing';

import { GSDPeakOptimized } from '../GSDPeakOptimized';
import { appendShapeAndFWHM } from '../utils/appendShapeAndFWHM';
import { groupPeaks } from '../utils/groupPeaks';

import { optimizePeaksWithLogs } from './optimizePeaksWithLogs';

export interface OptimizePeaksOptions {
/**
Expand Down Expand Up @@ -41,64 +39,5 @@ export function optimizePeaks(
peakList: PeakXYWidth[],
options: OptimizePeaksOptions = {},
): GSDPeakOptimized[] {
const {
shape = { kind: 'gaussian' },
groupingFactor = 1,
factorLimits = 2,
optimization = {
kind: 'lm',
options: {
timeout: 10,
},
},
}: OptimizePeaksOptions = options;

/*
The optimization algorithm will take some group of peaks.
We can not simply optimize everything because there would be too many variables to optimize
and it would be too time consuming.
*/
let groups = groupPeaks(peakList, { factor: groupingFactor });

let results: GSDPeakOptimized[] = [];

groups.forEach((peakGroup) => {
// In order to make optimization we will add fwhm and shape on all the peaks
const peaks = appendShapeAndFWHM(peakGroup, { shape });

const firstPeak = peaks[0];
const lastPeak = peaks[peaks.length - 1];

const from = firstPeak.x - firstPeak.width * factorLimits;
const to = lastPeak.x + lastPeak.width * factorLimits;
const { fromIndex, toIndex } = xGetFromToIndex(data.x, { from, to });

const x =
data.x instanceof Float64Array
? data.x.subarray(fromIndex, toIndex)
: data.x.slice(fromIndex, toIndex);
const y =
data.y instanceof Float64Array
? data.y.subarray(fromIndex, toIndex)
: data.y.slice(fromIndex, toIndex);

if (x.length > 5) {
let { peaks: optimizedPeaks } = optimize({ x, y }, peaks, {
shape,
optimization,
});
for (let i = 0; i < peaks.length; i++) {
results.push({
...optimizedPeaks[i],
width: getShape1D(peaks[i].shape).fwhmToWidth(
optimizedPeaks[i].shape.fwhm,
),
});
}
} else {
results = results.concat(peaks);
}
});

return results;
return optimizePeaksWithLogs(data, peakList, options).optimizedPeaks;
}
103 changes: 103 additions & 0 deletions src/post/optimizePeaksWithLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { DataXY, PeakXYWidth } from 'cheminfo-types';
import { getShape1D } from 'ml-peak-shape-generator';
import { optimize } from 'ml-spectra-fitting';
import { xGetFromToIndex } from 'ml-spectra-processing';

import { GSDPeakOptimized } from '../GSDPeakOptimized';
import { appendShapeAndFWHM } from '../utils/appendShapeAndFWHM';
import { groupPeaks } from '../utils/groupPeaks';

import { OptimizePeaksOptions } from './optimizePeaks';

/**
* Optimize the position (x), max intensity (y), full width at half maximum (fwhm)
* and the ratio of gaussian contribution (mu) if it's required. It currently supports three kind of shapes: gaussian, lorentzian and pseudovoigt
* @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}].
*/
export function optimizePeaksWithLogs(
data: DataXY,
peakList: PeakXYWidth[],
options: OptimizePeaksOptions = {},
): { logs: any[]; optimizedPeaks: GSDPeakOptimized[] } {
const {
shape = { kind: 'gaussian' },
groupingFactor = 1,
factorLimits = 2,
optimization = {
kind: 'lm',
options: {
timeout: 10,
},
},
}: OptimizePeaksOptions = options;

/*
The optimization algorithm will take some group of peaks.
We can not simply optimize everything because there would be too many variables to optimize
and it would be too time consuming.
*/
let groups = groupPeaks(peakList, { factor: groupingFactor });
let logs: any[] = [];
let results: GSDPeakOptimized[] = [];

groups.forEach((peakGroup) => {
const start = Date.now();
// In order to make optimization we will add fwhm and shape on all the peaks
const peaks = appendShapeAndFWHM(peakGroup, { shape });

const firstPeak = peaks[0];
const lastPeak = peaks[peaks.length - 1];

const from = firstPeak.x - firstPeak.width * factorLimits;
const to = lastPeak.x + lastPeak.width * factorLimits;
const { fromIndex, toIndex } = xGetFromToIndex(data.x, { from, to });

const x =
data.x instanceof Float64Array
? data.x.subarray(fromIndex, toIndex)
: data.x.slice(fromIndex, toIndex);
const y =
data.y instanceof Float64Array
? data.y.subarray(fromIndex, toIndex)
: data.y.slice(fromIndex, toIndex);

if (x.length > 5) {
const {
iterations,
error,
peaks: optimizedPeaks,
} = optimize({ x, y }, peaks, {
shape,
optimization,
});

for (let i = 0; i < peaks.length; i++) {
results.push({
...optimizedPeaks[i],
width: getShape1D(peaks[i].shape).fwhmToWidth(
optimizedPeaks[i].shape.fwhm,
),
});
}
logs.push({
iterations,
error,
parameters: optimization,
message: 'optimization successful',
groupSize: peakGroup.length,
time: `${Date.now() - start}ms`,
});
} else {
results = results.concat(peaks);
logs.push({
iterations: 0,
message: 'x length too small for optimization',
groupSize: peakGroup.length,
time: `${Date.now() - start}ms`,
});
}
});

return { logs, optimizedPeaks: results };
}

0 comments on commit 4e7848b

Please sign in to comment.