chromatography
Advanced tools
Comparing version 3.11.0 to 4.0.0-0
1894
lib/index.js
@@ -7,10 +7,13 @@ 'use strict'; | ||
var isAnyArray = _interopDefault(require('is-any-array')); | ||
var mlSpectraProcessing = require('ml-spectra-processing'); | ||
var mlGsd = require('ml-gsd'); | ||
var arrayMax = _interopDefault(require('ml-array-max')); | ||
var median = _interopDefault(require('ml-array-median')); | ||
var sum = _interopDefault(require('ml-array-sum')); | ||
var max = _interopDefault(require('ml-array-max')); | ||
var IsotopicDistribution = _interopDefault(require('isotopic-distribution')); | ||
var binarySearch = _interopDefault(require('binary-search')); | ||
var numSort = require('num-sort'); | ||
var mfParser = require('mf-parser'); | ||
var arrayMean = _interopDefault(require('ml-array-mean')); | ||
var Regression = _interopDefault(require('ml-regression-polynomial')); | ||
var binarySearch = _interopDefault(require('binary-search')); | ||
var jcampconverter = require('jcampconverter'); | ||
@@ -21,31 +24,9 @@ var xyParser = require('xy-parser'); | ||
/** | ||
* Modifies the time applying the conversion function | ||
* @param {Array<number>} originalTime - Original list of time values | ||
* @param {function(number)} conversionFunction - Function that given a number computes the new value | ||
* @return {Array<number>} - Modified list of time values | ||
*/ | ||
function rescaleTime(originalTime, conversionFunction) { | ||
return originalTime.map(conversionFunction); | ||
} | ||
/** | ||
* Filter the chromatogram based on a callback | ||
* The callback will take a time | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {function(number, number)} callback | ||
* @param {object} [options] - options object | ||
* @param {boolean} [options.copy = false] - return a copy of the original object | ||
* @return {Chromatogram} - Modified chromatogram | ||
*/ | ||
function filter(chromatogram, callback, options = {}) { | ||
const { copy = false } = options; | ||
let chrom; | ||
if (copy) { | ||
chrom = chromatogram.copy(); | ||
} else { | ||
chrom = chromatogram; | ||
chromatogram = chromatogram.copy(); | ||
} | ||
let times = chrom.getTimes(); | ||
let times = chromatogram.getTimes(); | ||
let newTimes = []; | ||
@@ -59,10 +40,10 @@ let indexToKeep = []; | ||
} | ||
chrom.setTimes(newTimes); | ||
chromatogram.times = newTimes; | ||
for (let key of chrom.getSerieNames()) { | ||
let serie = chrom.getSerie(key); | ||
serie.keep(indexToKeep); | ||
for (let key of chromatogram.getSeriesNames()) { | ||
const series = chromatogram.getSeries(key); | ||
series.keep(indexToKeep); | ||
} | ||
return chrom; | ||
return chromatogram; | ||
} | ||
@@ -73,7 +54,7 @@ | ||
*/ | ||
class Serie { | ||
class Series { | ||
constructor(array, dimension, options = {}) { | ||
let { meta = {} } = options; | ||
if (new.target === Serie) { | ||
throw new Error('You need to create either a 1D or 2D serie'); | ||
if (new.target === Series) { | ||
throw new Error('You need to create either a 1D or 2D series'); | ||
} | ||
@@ -97,3 +78,3 @@ this.data = array; | ||
data: this.data, | ||
meta: this.meta | ||
meta: this.meta, | ||
}; | ||
@@ -107,3 +88,3 @@ } | ||
keep(array) { | ||
let newData = []; | ||
const newData = []; | ||
for (let i of array) { | ||
@@ -113,2 +94,3 @@ newData.push(this.data[i]); | ||
this.data = newData; | ||
return this; | ||
} | ||
@@ -120,3 +102,3 @@ } | ||
*/ | ||
class Serie1D extends Serie { | ||
class Series1D extends Series { | ||
constructor(array) { | ||
@@ -130,3 +112,3 @@ super(array, 1); | ||
*/ | ||
class Serie2D extends Serie { | ||
class Series2D extends Series { | ||
constructor(array) { | ||
@@ -137,11 +119,7 @@ super(array, 2); | ||
function isArray(object) { | ||
return toString.call(object).endsWith('Array]'); | ||
} | ||
function serieFromArray(array) { | ||
function seriesFromArray(array) { | ||
// need to check if it is a 1D or 2D array (or 3D ?) | ||
if (!isArray(array)) { | ||
if (!isAnyArray(array)) { | ||
throw new TypeError( | ||
'Serie.fromArray requires as parameter an array of numbers or array' | ||
'seriesFromArray requires as parameter an array of numbers or array', | ||
); | ||
@@ -151,61 +129,22 @@ } | ||
if (array.length === 0 || typeof array[0] === 'number') { | ||
return new Serie1D(array); | ||
return new Series1D(array); | ||
} | ||
if (!isArray(array[0])) { | ||
if (!isAnyArray(array[0])) { | ||
throw new TypeError( | ||
'Serie.fromArray requires as parameter an array of numbers or array' | ||
'seriesFromArray requires as parameter an array of numbers or array', | ||
); | ||
} | ||
return new Serie2D(array); | ||
return new Series2D(array); | ||
} | ||
/** | ||
* Parse from a JSON element to a new Chromatogram | ||
* @param {object} json - Result from the toJSON method | ||
* @return {Chromatogram} - New parsed Chromatogram | ||
*/ | ||
function fromJSON(json) { | ||
let series = json.series; | ||
let times = json.times; | ||
let chromatogram = new Chromatogram(times); | ||
if (Array.isArray(series)) { | ||
for (let i = 0; i < series.length; i++) { | ||
chromatogram.addSerie(series[i].name, series[i].data); | ||
} | ||
} else { | ||
for (let key of Object.keys(series)) { | ||
chromatogram.addSerie(key, series[key].data, { | ||
meta: series[key].meta | ||
}); | ||
} | ||
} | ||
return chromatogram; | ||
} | ||
/** | ||
* Apply the GSD peak picking algorithm | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {object} [options] - Options object | ||
* @param {number} [options.heightFilter = 2] - Filter all objects that are bellow `heightFilter` times the median of the height | ||
* @param {string} [options.serieName = 'tic'] - Serie to do the peak picking | ||
* @param {object} [options.broadenPeaks = {}] - Options to broadenPeaks | ||
* @param {number} [options.broadenPeaks.factor = 1] - factor to enlarge | ||
* @param {boolean} [options.broadenPeaks.overlap = false] - Prevent overlap kf false | ||
* @return {Array<object>} - List of GSD objects | ||
*/ | ||
function getPeaks(chromatogram, options = {}) { | ||
const { heightFilter = 2, serieName = 'tic', broadenPeaks = {} } = options; | ||
const { heightFilter = 2, seriesName = 'tic', broadenPeaks = {} } = options; | ||
let serie = chromatogram.getSerie(serieName); | ||
if (!serie) { | ||
throw new Error(`"${serieName}" serie not founded`); | ||
} | ||
serie = serie.data; | ||
let times = chromatogram.getTimes(); | ||
const series = chromatogram.getSeries(seriesName).data; | ||
const times = chromatogram.getTimes(); | ||
// first peak selection | ||
let peakList = mlGsd.gsd(times, serie, { | ||
let peakList = mlGsd.gsd(times, series, { | ||
noiseLevel: 0, | ||
@@ -216,60 +155,40 @@ realTopDetection: false, | ||
heightFactor: 2, | ||
boundaries: true | ||
boundaries: true, | ||
}); | ||
peakList.sort( | ||
(a, b) => a.right.index - a.left.index - (b.right.index - b.left.index) | ||
); | ||
let medianDotsWidth = peakList[Math.floor((peakList.length - 1) / 2)]; | ||
medianDotsWidth = medianDotsWidth.right.index - medianDotsWidth.left.index; | ||
// filter height by factor | ||
let medianHeight = median(series); | ||
// reset parameters | ||
if (medianDotsWidth < 5) { | ||
medianDotsWidth = 5; | ||
} | ||
if (medianDotsWidth % 2 === 0) { | ||
medianDotsWidth -= 1; | ||
} | ||
// second peak selection | ||
peakList = mlGsd.gsd(times, serie, { | ||
noiseLevel: 0, | ||
realTopDetection: false, | ||
smoothY: true, | ||
sgOptions: { windowSize: medianDotsWidth, polynomial: 2 }, | ||
heightFactor: 2, | ||
boundaries: true | ||
}); | ||
peakList.sort((a, b) => a.height - b.height); | ||
// filter height by factor | ||
let medianHeight = peakList[Math.floor((peakList.length - 1) / 2)].height; | ||
peakList = peakList.filter((val) => val.height > medianHeight * heightFilter); | ||
var factor = broadenPeaks.factor === undefined ? 1 : broadenPeaks.factor; | ||
var overlap = | ||
broadenPeaks.overlap === undefined ? false : broadenPeaks.overlap; | ||
let { factor = 1, overlap = false } = broadenPeaks; | ||
mlGsd.post.broadenPeaks(peakList, { | ||
factor, | ||
overlap | ||
overlap, | ||
}); | ||
return peakList; | ||
peakList.sort((a, b) => a.x - b.x); | ||
return peakList.map((peak) => ({ | ||
from: Math.min(peak.from, peak.to), | ||
to: Math.max(peak.from, peak.to), | ||
fromIndex: Math.min(peak.left.index, peak.right.index), | ||
toIndex: Math.max(peak.left.index, peak.right.index), | ||
x: peak.x, | ||
y: peak.y, | ||
index: peak.index, | ||
})); | ||
} | ||
/** | ||
* Calculate tic | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @return {Array} - Calculated tic | ||
*/ | ||
function calculateTic(chromatogram) { | ||
let ms = chromatogram.getSerie('ms'); | ||
if (!ms) { | ||
throw new Error('The mass serie must be defined'); | ||
const ms = chromatogram.getSeries('ms'); | ||
const massSpectra = ms.data; | ||
const tic = []; | ||
for (const massSpectrum of massSpectra) { | ||
if (massSpectrum[1].length > 0) { | ||
tic.push(sum(massSpectrum[1])); | ||
} else { | ||
tic.push(0); | ||
} | ||
} | ||
var massSpectra = ms.data; | ||
var tic = []; | ||
for (var massSpectrum of massSpectra) { | ||
tic.push(massSpectrum[1].reduce((a, b) => a + b, 0)); | ||
} | ||
@@ -279,12 +198,6 @@ return tic; | ||
/** | ||
* Calculate the number of points of each related information | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {string} serieName - name of the serie | ||
* @return {Array} - Calculated length of the 2D array | ||
*/ | ||
function calculateLength(chromatogram, serieName) { | ||
let serie2D = chromatogram.getSerie(serieName); | ||
var spectra = serie2D.data; | ||
var length = spectra.map((spectrum) => spectrum[0].length); | ||
function calculateLength(chromatogram, seriesName) { | ||
const series2D = chromatogram.getSeries(seriesName); | ||
const spectra = series2D.data; | ||
const length = spectra.map((spectrum) => spectrum[0].length); | ||
return length; | ||
@@ -295,15 +208,12 @@ } | ||
* Calculate bpc | ||
* @param {Chromatogram} chrom - GC/MS chromatogram where make the peak picking | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @return {Array} - Calculated bpc | ||
*/ | ||
function calculateBpc(chrom) { | ||
let ms = chrom.getSerie('ms'); | ||
if (!ms) { | ||
throw new Error('The mass serie must be defined'); | ||
} | ||
var massSpectra = ms.data; | ||
var bpc = []; | ||
for (var massSpectrum of massSpectra) { | ||
function calculateBpc(chromatogram) { | ||
const ms = chromatogram.getSeries('ms'); | ||
const massSpectra = ms.data; | ||
const bpc = []; | ||
for (const massSpectrum of massSpectra) { | ||
if (massSpectrum[1].length > 0) { | ||
bpc.push(arrayMax(massSpectrum[1])); | ||
bpc.push(max(massSpectrum[1])); | ||
} else { | ||
@@ -317,25 +227,21 @@ bpc.push(0); | ||
/** | ||
* Calculate tic | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {number} targetMass - mass for which to extract the spectrum | ||
* @param {object} [options={}] | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMass | ||
* @return {Array} - Calculated mass for targetMass | ||
*/ | ||
function calculateForMass(chromatogram, targetMass, options = {}) { | ||
function calculateEic(chromatogram, targetMass, options = {}) { | ||
if (isNaN(targetMass)) { | ||
throw Error('calculateForMass: targetMass must be defined and a number'); | ||
throw new Error('targetMass must be defined and a number'); | ||
} | ||
const { error = 0.5 } = options; | ||
let ms = chromatogram.getSerie('ms'); | ||
if (!ms) { | ||
throw Error('calculateForMass: the mass serie must be defined'); | ||
if (!isNaN(targetMass)) { | ||
targetMass = [targetMass]; | ||
} else if (typeof targetMass === 'string') { | ||
targetMass = targetMass.split(/[ ,;\r\n\t]+/).map((value) => Number(value)); | ||
} | ||
var massSpectra = ms.data; | ||
var result = new Array(massSpectra.length).fill(0); | ||
const { slotWidth = 1 } = options; | ||
const halfWidth = slotWidth / 2; | ||
const ms = chromatogram.getSeries('ms'); | ||
const massSpectra = ms.data; | ||
const result = new Array(massSpectra.length).fill(0); | ||
for (let i = 0; i < massSpectra.length; i++) { | ||
let massSpectrum = massSpectra[i]; | ||
for (let j = 0; j < massSpectrum[0].length; j++) { | ||
if (Math.abs(massSpectrum[0][j] - targetMass) <= error) { | ||
if (Math.abs(massSpectrum[0][j] - targetMass) <= halfWidth) { | ||
result[i] += massSpectrum[1][j]; | ||
@@ -350,7 +256,10 @@ } | ||
/** | ||
* Calculate tic | ||
* Calculate tic for specific molecular formula and ionizations | ||
* | ||
* The system will take all the peaks with an intensity over 5% (default value) | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {string} targetMF - mass for which to extract the spectrum | ||
* @param {object} [options={}] | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMF | ||
* @param {number} [options.slotWidth=1] - Width of the slot around the mass of targetMF | ||
* @param {number} [options.threshold=0.05] - Minimal height for peaks | ||
* @param {number} [options.ionizations='H+'] - List of allowed ionisation | ||
@@ -361,19 +270,26 @@ * @return {Array} - Calculated mass for targetMass | ||
if (typeof targetMF !== 'string') { | ||
throw Error('calculateForMF: targetMF must be defined and a string'); | ||
throw Error('targetMF must be defined and a string'); | ||
} | ||
const { error = 0.5 } = options; | ||
const { threshold = 0.05, slotWidth = 1, ionizations = 'H+' } = options; | ||
let ms = chromatogram.getSerie('ms'); | ||
if (!ms) { | ||
throw Error('calculateForMF: the mass serie must be defined'); | ||
} | ||
const halfWidth = slotWidth / 2; | ||
var masses = new IsotopicDistribution(targetMF, { | ||
ionizations: options.ionizations.replace(/ /g, '') | ||
}) | ||
.getParts() | ||
.map((entry) => entry.ms.em); | ||
const ms = chromatogram.getSeries('ms'); | ||
var massSpectra = ms.data; | ||
var result = new Array(massSpectra.length).fill(0); | ||
let isotopicDistribution = new IsotopicDistribution(targetMF, { | ||
ionizations, | ||
}); | ||
// we add isotopicDistribution in all the parts | ||
isotopicDistribution.getDistribution(); | ||
let parts = isotopicDistribution.getParts(); | ||
let masses = [].concat(...parts.map((part) => part.isotopicDistribution)); | ||
masses.sort((a, b) => a.x - b.x); | ||
masses = mlSpectraProcessing.XYObject.slotX(masses, { slotWidth }).filter( | ||
(mass) => mass.y > threshold, | ||
); | ||
let massSpectra = ms.data; | ||
let result = new Array(massSpectra.length).fill(0); | ||
for (let targetMass of masses) { | ||
@@ -383,3 +299,3 @@ for (let i = 0; i < massSpectra.length; i++) { | ||
for (let j = 0; j < massSpectrum[0].length; j++) { | ||
if (Math.abs(massSpectrum[0][j] - targetMass) <= error) { | ||
if (Math.abs(massSpectrum[0][j] - targetMass.x) <= halfWidth) { | ||
result[i] += massSpectrum[1][j]; | ||
@@ -393,126 +309,57 @@ } | ||
const ascValue = (a, b) => (a - b); | ||
/** | ||
* Returns information for the closest time | ||
* @param {number} time - Retention time | ||
* @param {Array<number>} times - Time array | ||
* @return {{index: number, timeBefore: number, timeAfter: number, timeClosest: number, safeIndexBefore: number, safeIndexAfter: number}} | ||
*/ | ||
function getClosestTime(time, times) { | ||
let position = binarySearch(times, time, ascValue); | ||
if (position < 0) { | ||
// the value doesn't exists in the array | ||
position = -position - 1; | ||
let safeIndexBefore = position === 0 ? 0 : position - 1; | ||
if (position > (times.length - 1)) { | ||
position = times.length - 1; | ||
safeIndexBefore = times.length - 1; | ||
} | ||
const safeIndexAfter = position; | ||
let difference = times[position] - time; | ||
if (difference > 0.5) { | ||
position = safeIndexBefore; | ||
} | ||
return { | ||
index: position, | ||
timeBefore: times[safeIndexBefore], | ||
timeAfter: times[safeIndexAfter], | ||
timeClosest: times[position], | ||
safeIndexBefore: safeIndexBefore, | ||
safeIndexAfter: safeIndexAfter | ||
}; | ||
} else { | ||
// the value exists in the array | ||
return { | ||
index: position, | ||
timeBefore: times[position], | ||
timeAfter: times[position], | ||
timeClosest: times[position], | ||
safeIndexBefore: position, | ||
safeIndexAfter: position | ||
}; | ||
} | ||
} | ||
function baselineCorrection(total, base, kind) { | ||
if (total === 0) { | ||
return { | ||
integral: 0, | ||
base: { | ||
start: { height: 0 }, | ||
end: { height: 0 } | ||
} | ||
}; | ||
} | ||
function baselineCorrection(points, fromTo, kind) { | ||
const deltaTime = points.x[fromTo.toIndex] - points.x[fromTo.fromIndex]; | ||
const fromHeight = points.y[fromTo.fromIndex]; | ||
const toHeight = points.y[fromTo.toIndex]; | ||
let baseline = 0; | ||
let from = 0; | ||
let to = 0; | ||
switch (kind) { | ||
case 'trapezoid': | ||
return { | ||
integral: total - ((base.end.time - base.start.time) * (base.end.height + base.start.height) / 2), | ||
base | ||
}; | ||
baseline = (deltaTime * (fromHeight + toHeight)) / 2; | ||
from = fromHeight; | ||
to = toHeight; | ||
break; | ||
case 'min': | ||
if (base.end.height > base.start.height) { | ||
base.end.height = base.start.height; | ||
return { | ||
integral: total - ((base.end.time - base.start.time) * base.start.height), | ||
base | ||
}; | ||
} else { | ||
base.start.height = base.end.height; | ||
return { | ||
integral: total - ((base.end.time - base.start.time) * base.end.height), | ||
base | ||
}; | ||
} | ||
from = Math.min(fromHeight, toHeight); | ||
to = from; | ||
baseline = deltaTime * from; | ||
break; | ||
default: | ||
throw new Error(`Unknown baseline method "${kind}"`); | ||
} | ||
return { | ||
value: baseline, | ||
from, | ||
to, | ||
}; | ||
} | ||
/** | ||
* Returns a mass spectrum that is the integration of all the spectra in a specific range of time | ||
* @param {Chromatogram} chromatogram | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {string|boolean} [options.baseline] - Applies baseline correction (trapezoid, min) | ||
* @return {[]} | ||
*/ | ||
function integrate(chromatogram, name, ranges, options = {}) { | ||
const { | ||
baseline = false | ||
} = options; | ||
function integrate(chromatogram, ranges, options = {}) { | ||
const { baseline, seriesName = 'tic' } = options; | ||
if (!Array.isArray(ranges)) { | ||
throw new Error('ranges must be an array of type [[from,to]]'); | ||
throw new Error('Ranges must be an array of type [{from,to}]'); | ||
} | ||
if (ranges.length === 0) { | ||
return undefined; | ||
return []; | ||
} | ||
if (!Array.isArray(ranges[0]) || ranges[0].length !== 2) { | ||
throw new Error('ranges must be an array of type [[from,to]]'); | ||
chromatogram.requiresSeries(seriesName); | ||
let series = chromatogram.series[seriesName]; | ||
if (series.dimension !== 1) { | ||
throw new Error(`The series "${seriesName}" is not of dimension 1`); | ||
} | ||
chromatogram.requiresSerie(name); | ||
let serie = chromatogram.series[name]; | ||
if (serie.dimension !== 1) { | ||
throw new Error('the serie is not of dimension 1'); | ||
} | ||
const time = chromatogram.getTimes(); | ||
let results = []; | ||
for (let fromTo of ranges) { | ||
let from = fromTo[0]; | ||
let to = fromTo[1]; | ||
let fromIndex = getClosestTime(from, time).safeIndexBefore; | ||
let toIndex = getClosestTime(to, time).safeIndexAfter; | ||
results.push(_integrate(time, serie, from, to, fromIndex, toIndex, baseline)); | ||
for (let range of ranges) { | ||
const fromTo = mlSpectraProcessing.X.getFromToIndex(time, range); | ||
const integral = integrateRange( | ||
{ x: time, y: series.data }, | ||
fromTo, | ||
baseline, | ||
); | ||
results.push(integral); | ||
} | ||
@@ -523,52 +370,33 @@ | ||
function _integrate(time, serie, from, to, fromIndex, toIndex, baseline) { | ||
let total = 0; | ||
let base = {}; | ||
for (let i = fromIndex; i < toIndex; i++) { | ||
let timeStart = time[i]; | ||
let timeEnd = time[i + 1]; | ||
let heightStart = serie.data[i]; | ||
if (i === fromIndex) { // need to check the exact starting point | ||
heightStart = serie.data[i] + (serie.data[i + 1] - serie.data[i]) * (from - timeStart) / (timeEnd - timeStart); | ||
base.start = { height: heightStart, time: from }; | ||
timeStart = from; | ||
} | ||
function integrateRange(points, fromTo, baseline) { | ||
let integration = mlSpectraProcessing.XY.integration(points, fromTo); | ||
let heightEnd = serie.data[i + 1]; | ||
if (i === toIndex - 1) { | ||
heightEnd = serie.data[i] + (serie.data[i + 1] - serie.data[i]) * (to - timeStart) / (timeEnd - timeStart); | ||
base.end = { height: heightEnd, time: to }; | ||
timeEnd = to; | ||
} | ||
total += (timeEnd - timeStart) * (heightStart + heightEnd) / 2; | ||
} | ||
if (baseline) { | ||
var ans = baselineCorrection(total, base, baseline); | ||
let correction = baselineCorrection(points, fromTo, baseline); | ||
return { | ||
integral: ans.integral, | ||
integration: integration - correction.value, | ||
from: { | ||
time: from, | ||
index: fromIndex, | ||
baseline: ans.base.start.height | ||
time: points.x[fromTo.fromIndex], | ||
index: fromTo.fromIndex, | ||
baseline: correction.from, | ||
}, | ||
to: { | ||
time: to, | ||
index: toIndex, | ||
baseline: ans.base.end.height | ||
} | ||
time: points.x[fromTo.toIndex], | ||
index: fromTo.toIndex, | ||
baseline: correction.to, | ||
}, | ||
}; | ||
} else { | ||
return { | ||
integral: total, | ||
integration, | ||
from: { | ||
time: from, | ||
index: fromIndex, | ||
baseline: 0 | ||
time: points.x[fromTo.fromIndex], | ||
index: fromTo.fromIndex, | ||
baseline: 0, | ||
}, | ||
to: { | ||
time: to, | ||
index: toIndex, | ||
baseline: 0 | ||
} | ||
time: points.x[fromTo.toIndex], | ||
index: fromTo.toIndex, | ||
baseline: 0, | ||
}, | ||
}; | ||
@@ -578,483 +406,54 @@ } | ||
/** | ||
* Returns a mass spectrum that is the integration of all the spectra in a specific range of time | ||
* @param {Chromatogram} chromatogram | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {object} [options.algorithm = 'slot'] - Decision for merging the peaks | ||
* @param {object} [options.delta = 1] - Parameter for merging the peaks | ||
* @return {[]} | ||
*/ | ||
function merge(chromatogram, name, ranges, options = {}) { | ||
const { | ||
algorithm = 'slot', | ||
delta = 1 | ||
} = options; | ||
function merge(chromatogram, options = {}) { | ||
const time = chromatogram.getTimes(); | ||
let { mergeThreshold = 0.3, seriesName = 'ms', range = {} } = options; | ||
if (!Array.isArray(ranges) || !Array.isArray(ranges[0])) { | ||
throw new Error('ranges must be an array of type [[from,to]]'); | ||
chromatogram.requiresSeries(seriesName); | ||
let series = chromatogram.series[seriesName]; | ||
if (series.dimension !== 2) { | ||
throw new Error(`The series "${seriesName}" is not of dimension 2`); | ||
} | ||
chromatogram.requiresSerie(name); | ||
let serie = chromatogram.series[name]; | ||
if (serie.dimension !== 2) { | ||
throw new Error('The serie is not of dimension 2'); | ||
if (!range || range.from > time[time.length - 1] || range.to < time[0]) { | ||
return { x: [], y: [] }; | ||
} | ||
const time = chromatogram.getTimes(); | ||
let results = []; | ||
for (let fromTo of ranges) { | ||
let from = fromTo[0]; | ||
let to = fromTo[1]; | ||
let fromIndex = getClosestTime(from, time).safeIndexBefore; | ||
let toIndex = getClosestTime(to, time).safeIndexAfter; | ||
results.push({ | ||
serie: _merge(serie, fromIndex, toIndex, delta, algorithm), | ||
from: { | ||
time: from, | ||
index: fromIndex | ||
}, | ||
to: { | ||
time: to, | ||
index: toIndex | ||
} | ||
let { fromIndex, toIndex } = mlSpectraProcessing.X.getFromToIndex(time, range); | ||
let result = mlSpectraProcessing.XY.toXYObject({ | ||
x: series.data[fromIndex][0], | ||
y: series.data[fromIndex][1], | ||
}); | ||
for (let i = fromIndex + 1; i <= toIndex; i++) { | ||
let newData = mlSpectraProcessing.XY.toXYObject({ | ||
x: series.data[i][0], | ||
y: series.data[i][1], | ||
}); | ||
result = result.concat(newData); | ||
result = mlSpectraProcessing.XYObject.sortX(result); | ||
result = mlSpectraProcessing.XYObject.joinX(result, { xError: mergeThreshold }); | ||
} | ||
return results; | ||
} | ||
function _merge(serie, fromIndex, toIndex, delta, algorithm) { | ||
switch (algorithm) { | ||
case 'slot': | ||
return slot(fromIndex, toIndex, serie, delta); | ||
case 'centroid': | ||
return centroid(fromIndex, toIndex, serie, delta); | ||
default: | ||
throw new Error(`Unknown algorithm "${algorithm}"`); | ||
} | ||
} | ||
function slot(fromIndex, toIndex, serie, slot) { | ||
let massDictionary = {}; | ||
for (var i = fromIndex; i <= toIndex; i++) { | ||
for (var j = 0; j < serie.data[i][0].length; j++) { | ||
// round the mass value | ||
var x = serie.data[i][0][j]; | ||
let mass = x + slot / 2 - (x + slot / 2) % slot; | ||
// add the mass value to the dictionary | ||
if (massDictionary[mass]) { | ||
massDictionary[mass] += serie.data[i][1][j]; | ||
} else { | ||
massDictionary[mass] = serie.data[i][1][j]; | ||
} | ||
} | ||
} | ||
const massList = Object.keys(massDictionary).map((val) => Number(val)).sort(numSort.asc); | ||
let integral = [ | ||
new Array(massList.length), | ||
new Array(massList.length) | ||
]; | ||
for (var k = 0; k < massList.length; k++) { | ||
integral[0][k] = Number(massList[k]); | ||
integral[1][k] = massDictionary[massList[k]]; | ||
} | ||
return integral; | ||
} | ||
function centroid(fromIndex, toIndex, serie, slot) { | ||
var integral = [[], []]; | ||
for (var i = fromIndex; i <= toIndex; i++) { | ||
integral = mergeCentroids(integral, serie.data[i], slot); | ||
} | ||
return integral; | ||
} | ||
function mergeCentroids(previous, data, slot) { | ||
var leftIndex = 0; | ||
var rightIndex = 0; | ||
var merged = [[], []]; | ||
var weightedMass = [[], []]; | ||
var size = 0; | ||
while ((leftIndex < previous[0].length) && (rightIndex < data[0].length)) { | ||
if (previous[0][leftIndex] <= data[0][rightIndex]) { | ||
// append first(left) to result | ||
if ((size === 0) || (previous[0][leftIndex] - merged[0][size - 1] > slot)) { | ||
weightedMass[0].push(previous[0][leftIndex] * previous[1][leftIndex]); | ||
weightedMass[1].push(previous[1][leftIndex]); | ||
merged[0].push(previous[0][leftIndex]); | ||
merged[1].push(previous[1][leftIndex++]); | ||
size++; | ||
} else { | ||
weightedMass[0][size - 1] += previous[0][leftIndex] * previous[1][leftIndex]; | ||
weightedMass[1][size - 1] += previous[1][leftIndex]; | ||
merged[0][size - 1] = previous[0][leftIndex]; | ||
merged[1][size - 1] += previous[1][leftIndex++]; | ||
} | ||
} else { | ||
// append first(right) to result | ||
if ((size === 0) || (data[0][rightIndex] - merged[0][size - 1] > slot)) { | ||
weightedMass[0].push(data[0][rightIndex] * data[1][rightIndex]); | ||
weightedMass[1].push(data[1][rightIndex]); | ||
merged[0].push(data[0][rightIndex]); | ||
merged[1].push(data[1][rightIndex++]); | ||
size++; | ||
} else { | ||
weightedMass[0][size - 1] += data[0][rightIndex] * data[1][rightIndex]; | ||
weightedMass[1][size - 1] += data[1][rightIndex]; | ||
merged[0][size - 1] = data[0][rightIndex]; | ||
merged[1][size - 1] += data[1][rightIndex++]; | ||
} | ||
} | ||
} | ||
while (leftIndex < previous[0].length) { | ||
// append first(left) to result | ||
if ((size === 0) || (previous[0][leftIndex] - merged[0][size - 1] > slot)) { | ||
weightedMass[0].push(previous[0][leftIndex] * previous[1][leftIndex]); | ||
weightedMass[1].push(previous[1][leftIndex]); | ||
merged[0].push(previous[0][leftIndex]); | ||
merged[1].push(previous[1][leftIndex++]); | ||
size++; | ||
} else { | ||
weightedMass[0][size - 1] += previous[0][leftIndex] * previous[1][leftIndex]; | ||
weightedMass[1][size - 1] += previous[1][leftIndex]; | ||
merged[0][size - 1] = previous[0][leftIndex]; | ||
merged[1][size - 1] += previous[1][leftIndex++]; | ||
} | ||
} | ||
while (rightIndex < data[0].length) { | ||
// append first(right) to result | ||
if ((size === 0) || (data[0][rightIndex] - merged[0][size - 1] > slot)) { | ||
weightedMass[0].push(data[0][rightIndex] * data[1][rightIndex]); | ||
weightedMass[1].push(data[1][rightIndex]); | ||
merged[0].push(data[0][rightIndex]); | ||
merged[1].push(data[1][rightIndex++]); | ||
size++; | ||
} else { | ||
weightedMass[0][size - 1] += data[0][rightIndex] * data[1][rightIndex]; | ||
weightedMass[1][size - 1] += data[1][rightIndex]; | ||
merged[0][size - 1] = data[0][rightIndex]; | ||
merged[1][size - 1] += data[1][rightIndex++]; | ||
} | ||
} | ||
for (var i = 0; i < merged[0].length; i++) { | ||
merged[0][i] = weightedMass[0][i] / weightedMass[1][i]; | ||
} | ||
return merged; | ||
} | ||
/** | ||
* Calculates the Kovats retention index for a mass spectra of a n-alkane | ||
* @param {object} ms - An mass spectra object | ||
* @param {Array<number>} ms.x - Array of masses | ||
* @param {Array<number>} ms.y - Array of intensities | ||
* @return {number} - Kovats retention index | ||
*/ | ||
function kovats(ms) { | ||
let mass = ms.x; | ||
let massMol = []; | ||
const targets = [43, 57, 71, 85]; | ||
for (let i = 0; i < mass.length; i++) { | ||
if ((mass[i] - 2) % 14 === 0) { | ||
massMol.push(mass[i]); | ||
} | ||
} | ||
if (massMol.length === 0) { | ||
return 0; | ||
} | ||
let kovatsIndex = 0; | ||
for (var m = 0; m < massMol.length; m++) { | ||
let candidate = true; | ||
for (var t = 0; t < targets.length; t++) { | ||
candidate = candidate | ||
&& (mass.indexOf(targets[t]) !== -1) | ||
&& ((mass.indexOf(massMol[m] - targets[t]) !== -1) | ||
|| (mass.indexOf(massMol[m] - targets[t] + 1) !== -1) | ||
|| (mass.indexOf(massMol[m] - targets[t] - 1) !== -1)); | ||
} | ||
if (candidate) { | ||
kovatsIndex = 100 * (massMol[m] - 2) / 14; | ||
} | ||
} | ||
return kovatsIndex; | ||
} | ||
/** | ||
* Filters a mass object | ||
* @param {object} massXYObject - Object with x and y data | ||
* @param {Array<number>} massXYObject.x - Array of mass values | ||
* @param {Array<number>} massXYObject.y - Array of abundance values | ||
* @param {object} options - Options for the integral filtering | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @return {object} - Object with filtered x and y data | ||
*/ | ||
function massFilter(massXYObject, options = {}) { | ||
const { thresholdFactor = 0, maxNumberPeaks = Number.MAX_VALUE, groupWidth = 0 } = options; | ||
let max = -1; | ||
let massList = new Array(massXYObject.x.length); | ||
for (let i = 0; i < massXYObject.x.length; ++i) { | ||
massList[i] = { | ||
x: massXYObject.x[i], | ||
y: massXYObject.y[i] | ||
}; | ||
if (massXYObject.y[i] > max) { | ||
max = massXYObject.y[i]; | ||
} | ||
} | ||
// filters based in thresholdFactor | ||
max *= thresholdFactor; | ||
let filteredList = massList.filter((val) => val.y > max); | ||
// filters based in maxNumberPeaks | ||
if (filteredList.length > maxNumberPeaks || groupWidth !== 0) { | ||
filteredList.sort((a, b) => b.y - a.y); | ||
// filters based in groupWidth | ||
filteredList = moreDistinct(filteredList, maxNumberPeaks, groupWidth); | ||
filteredList.sort((a, b) => a.x - b.x); | ||
} | ||
let ans = { | ||
x: new Array(filteredList.length), | ||
y: new Array(filteredList.length) | ||
result = { | ||
...mlSpectraProcessing.XYObject.toXY(result), | ||
from: { | ||
index: fromIndex, | ||
time: time[fromIndex], | ||
}, | ||
to: { | ||
index: toIndex, | ||
time: time[toIndex], | ||
}, | ||
}; | ||
for (let i = 0; i < filteredList.length; ++i) { | ||
ans.x[i] = filteredList[i].x; | ||
ans.y[i] = filteredList[i].y; | ||
} | ||
return ans; | ||
return result; | ||
} | ||
/** | ||
* Filters based in groupWidth | ||
* @ignore | ||
* @param {Array<object>} list - Sorted list of XY-objects to be filtered | ||
* @param {number} maxNumberPeaks - Maximum number of peaks for each mass spectra | ||
* @param {number} groupWidth - When find a max can't be another max in a radius of this size | ||
* @return {Array<object>} - List of XY-objects filtered | ||
*/ | ||
function moreDistinct(list, maxNumberPeaks, groupWidth) { | ||
let len = 0; | ||
if (maxNumberPeaks > list.length) { | ||
maxNumberPeaks = list.length; | ||
} | ||
let filteredList = new Array(maxNumberPeaks); | ||
for (let i = 0; (i < list.length) && (len < maxNumberPeaks); ++i) { | ||
let outRange = true; | ||
for (let j = 0; j < len && outRange; ++j) { | ||
outRange = outRange && !((list[i].x > (filteredList[j].x - groupWidth)) && (list[i].x < (filteredList[j].x + groupWidth))); | ||
} | ||
if (outRange) { | ||
filteredList[len++] = list[i]; | ||
} | ||
} | ||
filteredList.length = len; | ||
return filteredList; | ||
} | ||
/** | ||
* Integrate MS spectra of a peak list | ||
* @param {Array<object>} peakList - List of GSD objects | ||
* @param {Array<object>} sampleMS - MS array of GC spectra | ||
* @param {object} options - Options for the integral filtering | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @param {number} [options.slot = 1] - When to merge two mass values intensities | ||
* @param {number} [options.method = 'slot'] - Mass combination method | ||
* @return {Array<object>} - List of GSD objects with an extra 'ms' field with the integrated MS spectra | ||
*/ | ||
function massInPeaks(peakList, sampleMS, options = {}) { | ||
const { | ||
thresholdFactor = 0, | ||
maxNumberPeaks = Number.MAX_VALUE, | ||
groupWidth = 0, | ||
slot = 1, | ||
method = 'slot' | ||
} = options; | ||
// integrate MS | ||
for (let i = 0; i < peakList.length; ++i) { | ||
var serie = { dimension: 2, data: sampleMS }; | ||
var integral = _merge(serie, peakList[i].left.index, peakList[i].right.index, slot, method); | ||
var msSum = { | ||
x: integral[0], | ||
y: integral[1] | ||
}; | ||
if (maxNumberPeaks || thresholdFactor || groupWidth) { | ||
msSum = massFilter(msSum, { maxNumberPeaks, thresholdFactor, groupWidth }); | ||
} | ||
peakList[i].ms = msSum; | ||
} | ||
return peakList; | ||
} | ||
/** | ||
* Calculates the table of Kovats indexes for the reference spectra | ||
* @param {Chromatogram} reference - Reference spectra | ||
* @param {object} [options = {}] - Options object | ||
* @param {number} [options.heightFilter = 100] - Filter all objects that are below heightFilter times the median of the height | ||
* @param {number} [options.thresholdFactor = 0.005] - Every peak that is below the main peak times this factor will be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = 40] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 5] - When find a max can't be another max in a radius of this size | ||
* @return {{kovatsIndexes:Array<object>,peaks:Array<object>}} - Time and value for the Kovats index | ||
*/ | ||
function getKovatsTable(reference, options = {}) { | ||
const { | ||
heightFilter = 100, | ||
thresholdFactor = 0.005, | ||
maxNumberPeaks = 40, | ||
groupWidth = 5 | ||
} = options; | ||
// Peak picking | ||
let peaks = getPeaks(reference, { heightFilter }); | ||
/* istanbul ignore next */ | ||
peaks = peaks.sort((a, b) => a.index - b.index); | ||
// integrate mass in the peaks | ||
let ms = reference.getSerie('ms').data; | ||
let integratedMs = massInPeaks(peaks, ms, { thresholdFactor, maxNumberPeaks, groupWidth }); | ||
var kovatsIndexes = new Array(integratedMs.length); | ||
for (var i = 0; i < integratedMs.length; i++) { | ||
kovatsIndexes[i] = { | ||
time: integratedMs[i].x, | ||
value: kovats(integratedMs[i].ms) | ||
}; | ||
} | ||
return { | ||
kovatsIndexes, | ||
peaks | ||
}; | ||
} | ||
const ascValue$1 = (a, b) => (a.value - b.value); | ||
const ascTime = (a, b) => (a.time - b.time); | ||
/** | ||
* Returns a function that allows to convert from time to Kovats or from Kovats to time | ||
* @param {Array<object>} kovatsConversionTable - List of time-kovats from the reference | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.revert = false] - True for convert from Kovats to time, false otherwise | ||
* @return {function(number)} - One parameter function that convert to one dimension to the other | ||
*/ | ||
function kovatsConversionFunction(kovatsConversionTable, options = {}) { | ||
const { revert = false } = options; | ||
if (revert) { | ||
const values = kovatsConversionTable.sort(ascValue$1); | ||
return (index) => { | ||
let position = binarySearch(values, { value: index }, ascValue$1); | ||
if (position < 0) { | ||
position = ~position; | ||
// handle extreme cases | ||
if (position === 0 || position === values.length) { | ||
return 0; | ||
} | ||
let smallerAlcane = values[position - 1].time; | ||
let largerAlcane = values[position].time; | ||
return (index - values[position - 1].value) * (largerAlcane - smallerAlcane) / 100 | ||
+ smallerAlcane; | ||
} else { | ||
return values[position].time; | ||
} | ||
}; | ||
} else { | ||
const times = kovatsConversionTable.sort(ascTime); | ||
return (time) => { | ||
let position = binarySearch(times, { time }, ascTime); | ||
if (position < 0) { | ||
position = ~position; | ||
// handle extreme cases | ||
if (position === 0 || position === times.length) { | ||
return 0; | ||
} | ||
let smallerAlcane = times[position - 1].time; | ||
let largerAlcane = times[position].time; | ||
return 100 * (time - smallerAlcane) / (largerAlcane - smallerAlcane) | ||
+ times[position - 1].value; | ||
} else { | ||
return times[position].value; | ||
} | ||
}; | ||
} | ||
} | ||
/** | ||
* Calculates the table of Kovats indexes for the reference spectra | ||
* @param {Chromatogram} reference - Reference spectra | ||
* @param {object} [options = {}] - Options object | ||
* @param {number} [options.heightFilter = 100] - Filter all objects that are below heightFilter times the median of the height | ||
* @param {number} [options.thresholdFactor = 0.005] - Every peak that is below the main peak times this factor will be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = 40] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 5] - When find a max can't be another max in a radius of this size | ||
* @param {boolean} [options.revert = false] - True for convert from Kovats to time, false otherwise | ||
* @return {{conversionFunction:function(number),kovatsIndexes:Array<object>,peaks:Array<object>}} - Time and value for the Kovats index | ||
*/ | ||
function getKovatsRescale(reference, options) { | ||
let kovatsTable = getKovatsTable(reference, options); | ||
let conversionFunction = kovatsConversionFunction(kovatsTable.kovatsIndexes, { revert: options.revert }); | ||
return { | ||
conversionFunction: conversionFunction, | ||
kovatsIndexes: kovatsTable.kovatsIndexes, | ||
peaks: kovatsTable.peaks | ||
}; | ||
} | ||
/** | ||
* Recalculates series for GC/MS with lock mass | ||
* @param {string|Array<string>} mf - Reference molecular formula(s) | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.oddReference = true] - Mass reference it's in the odd position | ||
* @param {number} [options.maxShift = 0.1] - Maximum allowed shift | ||
* @param {boolean} [options.usePreviousIfNotFound = true] - If not found we use the previous value | ||
* @return {object} this | ||
*/ | ||
function applyLockMass(mf, options = {}) { | ||
function applyLockMass(chromatogram, mfs, options = {}) { | ||
const { oddReference = true, maxShift = 0.1 } = options; | ||
// allows mf as string or array | ||
if (typeof mf === 'string') { | ||
mf = [mf]; | ||
if (typeof mfs === 'string') { | ||
mfs = [mfs]; | ||
} | ||
// calculate the mass reference values | ||
const referenceMass = mf.map((mf) => { | ||
const referenceMass = mfs.map((mf) => { | ||
let info = new mfParser.MF(mf).getInfo(); | ||
@@ -1064,13 +463,9 @@ return info.observedMonoisotopicMass || info.monoisotopicMass; | ||
var ms = this.getSerie('ms'); | ||
if (!ms) { | ||
throw new Error('The "ms" serie must be defined'); | ||
} | ||
ms = ms.data; | ||
const ms = chromatogram.getSeries('ms').data; | ||
// check where is the reference values | ||
var referenceIndexShift = Number(oddReference); | ||
var msIndexShift = Number(!oddReference); | ||
let referenceIndexShift = Number(oddReference); | ||
let msIndexShift = Number(!oddReference); | ||
const newSize = ms.length >> 1; | ||
var referencesCount = new Array(referenceMass.length).fill(0); | ||
let referencesCount = new Array(referenceMass.length).fill(0); | ||
@@ -1080,11 +475,11 @@ // applying the changes for all the spectra | ||
let usingPreviousValidDifference = false; | ||
for (var i = 0; i < newSize; i++) { | ||
var massIndex = 2 * i + msIndexShift; | ||
var referenceIndex = 2 * i + referenceIndexShift; | ||
for (let i = 0; i < newSize; i++) { | ||
let massIndex = 2 * i + msIndexShift; | ||
let referenceIndex = 2 * i + referenceIndexShift; | ||
// calculate the difference between theory and experimental (the smallest) | ||
var difference = Number.MAX_VALUE; | ||
var closestIndex = -1; | ||
for (var j = 0; j < referenceMass.length; j++) { | ||
for (var k = 0; k < ms[referenceIndex][0].length; k++) { | ||
let difference = Number.MAX_VALUE; | ||
let closestIndex = -1; | ||
for (let j = 0; j < referenceMass.length; j++) { | ||
for (let k = 0; k < ms[referenceIndex][0].length; k++) { | ||
if ( | ||
@@ -1116,3 +511,3 @@ Math.abs(difference) > | ||
} | ||
for (var m = 0; m < ms[massIndex][0].length; m++) { | ||
for (let m = 0; m < ms[massIndex][0].length; m++) { | ||
ms[massIndex][0][m] += difference; | ||
@@ -1123,8 +518,10 @@ } | ||
var referenceUsed = { | ||
const referenceUsed = { | ||
total: newSize, | ||
totalFound: referencesCount.reduce((prev, current) => current + prev, 0) | ||
totalFound: referencesCount.reduce((prev, current) => current + prev, 0), | ||
mfs: {}, | ||
percent: 0, | ||
}; | ||
for (var r = 0; r < referenceMass.length; r++) { | ||
referenceUsed[mf[r]] = referencesCount[r]; | ||
for (let r = 0; r < referenceMass.length; r++) { | ||
referenceUsed.mfs[mfs[r]] = referencesCount[r]; | ||
} | ||
@@ -1135,27 +532,27 @@ referenceUsed.percent = | ||
// remove the time and the mass spectra that contains the reference | ||
this.filter((index) => index % 2 !== referenceIndexShift); | ||
chromatogram.filter((index) => index % 2 !== referenceIndexShift); | ||
return { chrom: this, referenceUsed }; | ||
return referenceUsed; | ||
} | ||
function meanFilter(chromatogram, serieName, options = {}) { | ||
function meanFilter(chromatogram, seriesName, options = {}) { | ||
const { factor = 2 } = options; | ||
var serie = chromatogram.getSerie(serieName); | ||
var filtered = []; | ||
for (var i = 0; i < serie.data.length; i++) { | ||
filtered.push(applyFilter(serie.data[i], factor)); | ||
let series = chromatogram.getSeries(seriesName); | ||
let filtered = []; | ||
for (let i = 0; i < series.data.length; i++) { | ||
filtered.push(applyFilter(series.data[i], factor)); | ||
} | ||
return serieFromArray(filtered); | ||
return seriesFromArray(filtered); | ||
} | ||
function applyFilter(serie, factor) { | ||
var filtered = [[], []]; | ||
if (serie[1].length === 0) return filtered; | ||
const meanIntensity = factor * arrayMean(serie[1]); | ||
for (var i = 0; i < serie[0].length; i++) { | ||
if (serie[1][i] > meanIntensity) { | ||
filtered[0].push(serie[0][i]); | ||
filtered[1].push(serie[1][i]); | ||
function applyFilter(series, factor) { | ||
let filtered = [[], []]; | ||
if (series[1].length === 0) return filtered; | ||
const meanIntensity = factor * arrayMean(series[1]); | ||
for (let i = 0; i < series[0].length; i++) { | ||
if (series[1][i] > meanIntensity) { | ||
filtered[0].push(series[0][i]); | ||
filtered[1].push(series[1][i]); | ||
} | ||
@@ -1166,29 +563,27 @@ } | ||
function percentageFilter(chromatogram, serieName, options = {}) { | ||
const { | ||
percentage = 0.1 | ||
} = options; | ||
function percentageFilter(chromatogram, seriesName, options = {}) { | ||
const { percentage = 0.1 } = options; | ||
var serie = chromatogram.getSerie(serieName); | ||
var filtered = []; | ||
let series = chromatogram.getSeries(seriesName); | ||
let filtered = []; | ||
for (var i = 0; i < serie.data.length; i++) { | ||
filtered.push(applyFilter$1(serie.data[i], percentage)); | ||
for (let i = 0; i < series.data.length; i++) { | ||
filtered.push(applyFilter$1(series.data[i], percentage)); | ||
} | ||
return serieFromArray(filtered); | ||
return seriesFromArray(filtered); | ||
} | ||
function applyFilter$1(serie, percentage) { | ||
var basePeak; | ||
function applyFilter$1(series, percentage) { | ||
let basePeak; | ||
try { | ||
basePeak = arrayMax(serie[1]); | ||
basePeak = max(series[1]); | ||
} catch (e) { | ||
basePeak = 0; | ||
} | ||
var filtered = [[], []]; | ||
for (var i = 0; i < serie[0].length; i++) { | ||
if (serie[1][i] > percentage * basePeak) { | ||
filtered[0].push(serie[0][i]); | ||
filtered[1].push(serie[1][i]); | ||
let filtered = [[], []]; | ||
for (let i = 0; i < series[0].length; i++) { | ||
if (series[1][i] > percentage * basePeak) { | ||
filtered[0].push(series[0][i]); | ||
filtered[1].push(series[1][i]); | ||
} | ||
@@ -1199,36 +594,13 @@ } | ||
/** | ||
* Create an object of Chromatogram | ||
* @return {object} | ||
*/ | ||
function toJSON() { | ||
function getClosestData(chromatogram, time, options = {}) { | ||
const { seriesName = 'ms' } = options; | ||
chromatogram.requiresSeries(seriesName); | ||
let closest = chromatogram.getClosestTime(time); | ||
return { | ||
times: this.times, | ||
series: this.series | ||
rt: chromatogram.getTimes()[closest], | ||
index: closest, | ||
data: chromatogram.getSeries(seriesName).data[closest], | ||
}; | ||
} | ||
/** | ||
* Returns the closest mass spectrum to a specific retention time | ||
* @param {string} name - Serie name | ||
* @param {number} rt - Retention time | ||
* @return {{rt: number, index: number, data: Array}} | ||
*/ | ||
function getClosestData(name, rt) { | ||
this.requiresSerie(name); | ||
let closest = this.getClosestTime(rt); | ||
return { | ||
rt: closest.timeClosest, | ||
index: closest.index, | ||
data: this.getSerie(name).data[closest.index] | ||
}; | ||
} | ||
/** | ||
* Class allowing to store time / ms (ms) series | ||
* It allows also to store simple time a trace | ||
* @class Chromatogram | ||
* @param {Array<number>} times - Time serie | ||
* @param {object} series - A map of series with name and the Serie object | ||
*/ | ||
class Chromatogram { | ||
@@ -1238,12 +610,10 @@ constructor(times, series) { | ||
this.times = []; | ||
if (times) { | ||
if (!isArray(times)) { | ||
throw new TypeError('Times must be an array'); | ||
} | ||
this.times = times; | ||
} else { | ||
throw new Error('The time serie is mandatory'); | ||
if (!isAnyArray(times)) { | ||
throw new TypeError('times must be an array'); | ||
} | ||
this.times = times; | ||
if (series) { | ||
this.addSeries(series); | ||
for (const [name, value] of Object.entries(series)) { | ||
this.addSeries(name, value); | ||
} | ||
} | ||
@@ -1256,12 +626,8 @@ } | ||
/** | ||
* Find the serie giving the name | ||
* @param {string} name - name of the serie | ||
* @return {object} - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
*/ | ||
getSerie(name) { | ||
return this.series[name]; | ||
getSeries(seriesName) { | ||
this.requiresSeries(seriesName); | ||
return this.series[seriesName]; | ||
} | ||
getSerieNames() { | ||
getSeriesNames() { | ||
return Object.keys(this.series); | ||
@@ -1271,72 +637,33 @@ } | ||
hasMass() { | ||
return this.hasSerie('ms'); | ||
return this.hasSeries('ms'); | ||
} | ||
/** | ||
* Delete a serie | ||
* @param {string} name - Name of the serie | ||
*/ | ||
deleteSerie(name) { | ||
if (!this.hasSerie(name)) { | ||
throw new Error(`The serie "${name}" does not exist`); | ||
} else { | ||
delete this.series[name]; | ||
} | ||
deleteSeries(seriesName) { | ||
this.requiresSeries(seriesName); | ||
delete this.series[seriesName]; | ||
return this; | ||
} | ||
/** | ||
* Add new series | ||
* @param {object} series - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
* @param {object} [options = {}] - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
*/ | ||
addSeries(series, options = {}) { | ||
if (typeof series !== 'object' || Array.isArray(series)) { | ||
throw new TypeError('data must be an object containing arrays of series'); | ||
addSeries(seriesName, array, options = {}) { | ||
if (this.hasSeries(seriesName) && !options.force) { | ||
throw new Error(`A series with name "${seriesName}" already exists`); | ||
} | ||
for (const key of Object.keys(series)) { | ||
this.addSerie(key, series[key], options); | ||
} | ||
} | ||
/** | ||
* Add a new serie | ||
* @param {string} name - Name of the serie to add | ||
* @param {Array} array - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force replacement of existing serie | ||
*/ | ||
addSerie(name, array, options = {}) { | ||
if (this.hasSerie(name) && !options.force) { | ||
throw new Error(`A serie with name "${name}" already exists`); | ||
} | ||
if (this.times.length !== array.length) { | ||
throw new Error('The array size is not the same as the time size'); | ||
throw new Error(`The series size is not the same as the times size`); | ||
} | ||
this.series[name] = serieFromArray(array); | ||
this.series[name].name = name; | ||
this.series[seriesName] = seriesFromArray(array); | ||
this.series[seriesName].name = seriesName; | ||
return this; | ||
} | ||
/** | ||
* Returns true if the serie name exists | ||
* @param {string} name - Name of the serie to check | ||
* @return {boolean} | ||
*/ | ||
hasSerie(name) { | ||
return typeof this.series[name] !== 'undefined'; | ||
hasSeries(seriesName) { | ||
return typeof this.series[seriesName] !== 'undefined'; | ||
} | ||
/** | ||
* Throws if the serie does not exists | ||
* @param {string} name - Name of the serie to check | ||
*/ | ||
requiresSerie(name) { | ||
if (!this.hasSerie(name)) { | ||
throw new Error(`The serie "${name}" does not exist`); | ||
requiresSeries(seriesName) { | ||
if (!this.hasSeries(seriesName)) { | ||
throw new Error(`The series "${seriesName}" does not exist`); | ||
} | ||
} | ||
/** | ||
* Returns the first time value | ||
* @return {number} - First time value | ||
*/ | ||
get firstTime() { | ||
@@ -1346,6 +673,2 @@ return this.times[0]; | ||
/** | ||
* Returns the last time value | ||
* @return {number} - Last time value | ||
*/ | ||
get lastTime() { | ||
@@ -1355,6 +678,2 @@ return this.times[this.length - 1]; | ||
/** | ||
* Returns the time values | ||
* @return {Array<number>} - Time values | ||
*/ | ||
getTimes() { | ||
@@ -1364,28 +683,14 @@ return this.times; | ||
/** | ||
* Assign the time values | ||
* @param {Array<number>} times - New time values | ||
*/ | ||
setTimes(times) { | ||
if (times.length !== this.times.length) { | ||
throw new Error('New times must have the same length as the old ones'); | ||
} | ||
this.times = times; | ||
} | ||
/** | ||
* Modifies the time applying the conversion function | ||
* @param {function(number)} conversionFunction | ||
* @return {Chromatogram} | ||
*/ | ||
rescaleTime(conversionFunction) { | ||
this.times = rescaleTime(this.times, conversionFunction); | ||
this.times = this.times.map((time) => conversionFunction(time)); | ||
return this; | ||
} | ||
/** | ||
* Will filter the entries based on the time | ||
* You can either use the index of the actual time | ||
* @param {function(number, number)} callback | ||
* @param {object} [options] - options object | ||
* @param {boolean} [options.copy = false] - return a copy of the original object | ||
* @return {Chromatogram} | ||
*/ | ||
filter(callback, options) { | ||
@@ -1395,8 +700,2 @@ return filter(this, callback, options); | ||
/** | ||
* Apply the GSD peak picking algorithm | ||
* @param {object} [options] - Options object | ||
* @param {object} [options.heightFilter = 2] - Filter all objects that are bellow `heightFilter` times the median of the height | ||
* @return {Array<object>} - List of GSD objects | ||
*/ | ||
getPeaks(options) { | ||
@@ -1406,135 +705,66 @@ return getPeaks(this, options); | ||
/** | ||
* Calculate tic | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force the calculation it it exists | ||
*/ | ||
calculateTic(options = {}) { | ||
if (!this.getSerie('tic') || options.force) { | ||
let tic = calculateTic(this); | ||
this.addSerie('tic', tic, options); | ||
if (!this.hasSeries('tic') || options.force) { | ||
const tic = calculateTic(this); | ||
this.addSeries('tic', tic, { force: true }); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Calculate length and save it in the 'length' serie | ||
* @param {string} serieName - Name of the serie to make calculation | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force the calculation it it exists | ||
*/ | ||
calculateLength(serieName, options = {}) { | ||
if (!this.getSerie('length') || options.force) { | ||
let length = calculateLength(this, serieName); | ||
this.addSerie('length', length, options); | ||
calculateLength(seriesName, options = {}) { | ||
if (!this.hasSeries('length') || options.force) { | ||
const length = calculateLength(this, seriesName); | ||
this.addSeries('length', length, { force: true }); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Calculate bpc | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force the calculation it it exists | ||
*/ | ||
calculateBpc(options = {}) { | ||
if (!this.getSerie('bpc') || options.force) { | ||
let bpc = calculateBpc(this); | ||
this.addSerie('bpc', bpc, options); | ||
if (!this.hasSeries('bpc') || options.force) { | ||
const bpc = calculateBpc(this); | ||
this.addSeries('bpc', bpc, { force: true }); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Calculate mass spectrum by filtering for a specific mass | ||
* @param {number} targetMass - mass for which to extract the spectrum | ||
* @param {object} [options = {}] - Options object | ||
* @param {string} [options.serieName='ms'+targetMass] - Name of the serie to make calculation | ||
* @param {boolean} [options.cache = false] - Retrieve from cache if exists | ||
* @param {boolean} [options.force = false] - Force replacement of existing serie | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMass | ||
* @return {Serie} | ||
*/ | ||
calculateForMass(targetMass, options = {}) { | ||
calculateEic(targetMass, options = {}) { | ||
const { | ||
serieName = `ms${targetMass}-${options.error || 0.5}`, | ||
cache = false | ||
seriesName = `ms${targetMass}±${options.slotWidth / 2 || 0.5}`, | ||
cache = false, | ||
} = options; | ||
if (cache && this.hasSerie(serieName)) return this.getSerie(serieName); | ||
let result = calculateForMass(this, targetMass, options); | ||
this.addSerie(serieName, result, options); | ||
return this.getSerie(serieName); | ||
if (cache && this.hasSeries(seriesName)) return this.getSeries(seriesName); | ||
const result = calculateEic(this, targetMass, options); | ||
this.addSeries(seriesName, result, options); | ||
return this.getSeries(seriesName); | ||
} | ||
/** | ||
* Calculate mass spectrum by filtering for a specific mass | ||
* @param {string} targetMF - mass for which to extract the spectrum | ||
* @param {object} [options = {}] - Options object | ||
* @param {string} [options.serieName='ms'+targetMass] - Name of the serie to make calculation | ||
* @param {boolean} [options.force = false] - Force replacement of existing serie | ||
* @param {boolean} [options.cache = false] - Retrieve from cache if exists | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMass | ||
* @param {number} [options.ionizations='H+'] - List of allowed ionisation | ||
* @return {Serie} | ||
*/ | ||
calculateForMF(targetMF, options = {}) { | ||
const { | ||
serieName = `ms${targetMF}-${options.ionizations || | ||
'H+'}-${options.error || 0.5}`, | ||
cache = false | ||
seriesName = `ms ${targetMF} ${options.ionizations || | ||
'H+'} (${options.slotWidth || 1}, ${options.threshold || 0.05})`, | ||
cache = false, | ||
} = options; | ||
if (cache && this.hasSerie(serieName)) return this.getSerie(serieName); | ||
let result = calculateForMF(this, targetMF, options); | ||
this.addSerie(serieName, result, options); | ||
return this.getSerie(serieName); | ||
if (cache && this.hasSeries(seriesName)) return this.getSeries(seriesName); | ||
const result = calculateForMF(this, targetMF, options); | ||
this.addSeries(seriesName, result, options); | ||
return this.getSeries(seriesName); | ||
} | ||
/** | ||
* Calculates the table of Kovats indexes for the reference spectra | ||
* @param {object} [options = {}] - Options object | ||
* @param {number} [options.heightFilter = 100] - Filter all objects that are below heightFilter times the median of the height | ||
* @param {number} [options.thresholdFactor = 0.005] - Every peak that is below the main peak times this factor will be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = 40] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 5] - When find a max can't be another max in a radius of this size | ||
* @param {boolean} [options.revert = false] - True for convert from Kovats to time, false otherwise | ||
* @return {{conversionFunction:function(number),kovatsIndexes:Array<object>,peaks:Array<object>}} - Time and value for the Kovats index | ||
*/ | ||
getKovatsRescale(options) { | ||
return getKovatsRescale(this, options); | ||
integrate(ranges, options) { | ||
return integrate(this, ranges, options); | ||
} | ||
/** | ||
* Returns an object with the result of the integrations | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {string|boolean} [options.baseline] - Applies baseline correction | ||
* @return {[]} | ||
*/ | ||
integrate(name, ranges, options) { | ||
return integrate(this, name, ranges, options); | ||
merge(options) { | ||
return merge(this, options); | ||
} | ||
/** | ||
* Retuns an object with the result of the integrations | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {object} [options.algorithm = 'slot'] - Decision for merging the peaks | ||
* @param {object} [options.delta = 1] - Parameter for merging the peaks | ||
* @return {[]} | ||
*/ | ||
merge(name, ranges, options) { | ||
return merge(this, name, ranges, options); | ||
getClosestTime(time) { | ||
return mlSpectraProcessing.X.findClosestIndex(this.getTimes(), time); | ||
} | ||
/** | ||
* Returns information for the closest time | ||
* @param {number} time - Retention time | ||
* @return {{index: number, timeBefore: number, timeAfter: number, timeClosest: number, safeIndexBefore: number, safeIndexAfter: number}} | ||
*/ | ||
getClosestTime(time) { | ||
return getClosestTime(time, this.getTimes()); | ||
getClosestData(time, options = {}) { | ||
return getClosestData(this, time, options); | ||
} | ||
/** | ||
* Return a copy of the chromatogram | ||
* @return {Chromatogram} | ||
*/ | ||
copy() { | ||
@@ -1545,55 +775,166 @@ const json = JSON.parse(JSON.stringify(this)); | ||
/** | ||
* Filter the given serie2D based on it's median value | ||
* @param {string} serieName | ||
* @param {object} [options] | ||
* @param {string} [options.serieName = 'msMedian'] - Name of the new serie | ||
* @param {number} [options.factor = 2] - The values under the median times this factor are removed | ||
*/ | ||
meanFilter(serieName, options = {}) { | ||
var serie = meanFilter(this, serieName, options); | ||
if (options.serieName) { | ||
this.series[options.serieName] = serie; | ||
} else { | ||
this.series.msMedian = serie; | ||
meanFilter(seriesName, options = {}) { | ||
const { seriesName: newSeriesName = 'msMedian' } = options; | ||
if (this.hasSeries(newSeriesName) && !options.force) { | ||
throw new Error(`A series with name "${seriesName}" already exists`); | ||
} | ||
const newSeries = meanFilter(this, seriesName, options); | ||
this.series[newSeriesName] = newSeries; | ||
return newSeries; | ||
} | ||
/** | ||
* Filter the given serie2D based on the percentage of the highest value | ||
* @param {string} serieName | ||
* @param {object} [options] | ||
* @param {string} [options.serieName = 'msPercentage'] - Name of the new serie | ||
* @param {number} [options.percentage = 0.1] - The values under the median times this factor are removed | ||
*/ | ||
percentageFilter(serieName, options = {}) { | ||
var serie = percentageFilter(this, serieName, options); | ||
if (options.serieName) { | ||
this.series[options.serieName] = serie; | ||
} else { | ||
this.series.msPercentage = serie; | ||
percentageFilter(seriesName, options = {}) { | ||
const { seriesName: newSeriesName = 'msPercentage' } = options; | ||
if (this.hasSeries(newSeriesName) && !options.force) { | ||
throw new Error(`A series with name "${seriesName}" already exists`); | ||
} | ||
const newSeries = percentageFilter(this, seriesName, options); | ||
this.series[newSeriesName] = newSeries; | ||
return newSeries; | ||
} | ||
applyLockMass(mfs, options) { | ||
return applyLockMass(this, mfs, options); | ||
} | ||
toJSON() { | ||
return { | ||
times: this.times, | ||
series: this.series, | ||
}; | ||
} | ||
} | ||
Chromatogram.prototype.applyLockMass = applyLockMass; | ||
Chromatogram.prototype.toJSON = toJSON; | ||
Chromatogram.prototype.getClosestData = getClosestData; | ||
function fromJSON(json) { | ||
let series = json.series; | ||
let times = json.times; | ||
let chromatogram = new Chromatogram(times); | ||
if (Array.isArray(series)) { | ||
for (let i = 0; i < series.length; i++) { | ||
chromatogram.addSeries(series[i].name, series[i].data); | ||
} | ||
} else { | ||
for (let key of Object.keys(series)) { | ||
chromatogram.addSeries(key, series[key].data, { | ||
meta: series[key].meta, | ||
}); | ||
} | ||
} | ||
return chromatogram; | ||
} | ||
/** | ||
* Given a list of GSD objects returns the weighted mass times abundance | ||
* @param {Array<object>} peakList - List of GSD objects | ||
* @param {object} options - Options for the integral filtering | ||
* @param {number} [options.massPower = 3] - Power applied to the mass values | ||
* @param {number} [options.intPower = 0.6] - Power applied to the abundance values | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @return {Array<object>} - List of mass and weighted mass times abundance objects | ||
* Append MS spectra to a list of peaks | ||
* @param {Chromatogram} chromatogram | ||
* @param {Array<object>} peaks - Array of range {from:, to:} | ||
* @param {object} [options={}] - Options for the integral filtering | ||
* @param {number} [options.mergeThreshold=0.3] - Peaks that are under this value (in Da) will be merged | ||
* @param {number} [options.seriesName='ms'] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @return {Array<object>} - A copy of ranges with ms appended | ||
*/ | ||
function vectorify(peakList, options = {}) { | ||
function appendMass(chromatogram, peaks, options = {}) { | ||
const { mergeThreshold = 0.3, seriesName = 'ms' } = options; | ||
const result = []; | ||
// integrate MS | ||
for (const peak of peaks) { | ||
const massSpectrum = merge(chromatogram, { | ||
mergeThreshold, | ||
seriesName, | ||
range: peak, | ||
}); | ||
result.push({ | ||
...peak, | ||
ms: massSpectrum, | ||
}); | ||
} | ||
return result; | ||
} | ||
function massFilter(massXYObject, options = {}) { | ||
const { | ||
thresholdFactor = 0, | ||
maxNumberPeaks = Number.MAX_VALUE, | ||
groupWidth = 0, | ||
} = options; | ||
let max = -1; | ||
let massList = new Array(massXYObject.x.length); | ||
for (let i = 0; i < massXYObject.x.length; ++i) { | ||
massList[i] = { | ||
x: massXYObject.x[i], | ||
y: massXYObject.y[i], | ||
}; | ||
if (massXYObject.y[i] > max) { | ||
max = massXYObject.y[i]; | ||
} | ||
} | ||
// filters based in thresholdFactor | ||
max *= thresholdFactor; | ||
let filteredList = massList.filter((val) => val.y > max); | ||
// filters based in maxNumberPeaks | ||
if (filteredList.length > maxNumberPeaks || groupWidth !== 0) { | ||
filteredList.sort((a, b) => b.y - a.y); | ||
// filters based in groupWidth | ||
filteredList = moreDistinct(filteredList, maxNumberPeaks, groupWidth); | ||
filteredList.sort((a, b) => a.x - b.x); | ||
} | ||
let ans = { | ||
x: new Array(filteredList.length), | ||
y: new Array(filteredList.length), | ||
}; | ||
for (let i = 0; i < filteredList.length; ++i) { | ||
ans.x[i] = filteredList[i].x; | ||
ans.y[i] = filteredList[i].y; | ||
} | ||
return ans; | ||
} | ||
/** | ||
* Filters based in groupWidth | ||
* @ignore | ||
* @param {Array<object>} list - Sorted list of XY-objects to be filtered | ||
* @param {number} maxNumberPeaks - Maximum number of peaks for each mass spectra | ||
* @param {number} groupWidth - When find a max can't be another max in a radius of this size | ||
* @return {Array<object>} - List of XY-objects filtered | ||
*/ | ||
function moreDistinct(list, maxNumberPeaks, groupWidth) { | ||
let len = 0; | ||
if (maxNumberPeaks > list.length) { | ||
maxNumberPeaks = list.length; | ||
} | ||
let filteredList = new Array(maxNumberPeaks); | ||
for (let i = 0; i < list.length && len < maxNumberPeaks; ++i) { | ||
let outRange = true; | ||
for (let j = 0; j < len && outRange; ++j) { | ||
outRange = | ||
outRange && | ||
!( | ||
list[i].x > filteredList[j].x - groupWidth && | ||
list[i].x < filteredList[j].x + groupWidth | ||
); | ||
} | ||
if (outRange) { | ||
filteredList[len++] = list[i]; | ||
} | ||
} | ||
filteredList.length = len; | ||
return filteredList; | ||
} | ||
function vectorify(ranges, options = {}) { | ||
const { massPower = 3, intPower = 0.6 } = options; | ||
let filter = (options.thresholdFactor || options.maxNumberPeaks || options.groupWidth); | ||
let filter = | ||
options.thresholdFactor || options.maxNumberPeaks || options.groupWidth; | ||
let vector = new Array(peakList.length); | ||
let vector = new Array(ranges.length); | ||
if (filter) { | ||
@@ -1603,13 +944,15 @@ const filterOptions = { | ||
maxNumberPeaks: options.maxNumberPeaks, | ||
groupWidth: options.groupWidth | ||
groupWidth: options.groupWidth, | ||
}; | ||
for (let i = 0; i < peakList.length; ++i) { | ||
let len = peakList[i].ms.x.length; | ||
for (let i = 0; i < ranges.length; ++i) { | ||
let len = ranges[i].ms.x.length; | ||
vector[i] = { | ||
x: peakList[i].ms.x, | ||
y: new Array(len) | ||
x: ranges[i].ms.x, | ||
y: new Array(len), | ||
}; | ||
for (let j = 0; j < len; ++j) { | ||
vector[i].y[j] = Math.pow(peakList[i].ms.x[j], massPower) * Math.pow(peakList[i].ms.y[j], intPower); | ||
vector[i].y[j] = | ||
Math.pow(ranges[i].ms.x[j], massPower) * | ||
Math.pow(ranges[i].ms.y[j], intPower); | ||
} | ||
@@ -1620,10 +963,12 @@ | ||
} else { | ||
for (let i = 0; i < peakList.length; ++i) { | ||
let len = peakList[i].ms.x.length; | ||
for (let i = 0; i < ranges.length; ++i) { | ||
let len = ranges[i].ms.x.length; | ||
vector[i] = { | ||
x: peakList[i].ms.x, | ||
y: new Array(len) | ||
x: ranges[i].ms.x, | ||
y: new Array(len), | ||
}; | ||
for (let j = 0; j < len; ++j) { | ||
vector[i].y[j] = Math.pow(peakList[i].ms.x[j], massPower) * Math.pow(peakList[i].ms.y[j], intPower); | ||
vector[i].y[j] = | ||
Math.pow(ranges[i].ms.x[j], massPower) * | ||
Math.pow(ranges[i].ms.y[j], intPower); | ||
} | ||
@@ -1636,11 +981,3 @@ } | ||
/** | ||
* Cosine similarity between two MS spectra | ||
* @param {Array<number>} ms1x - Array of mass values for the first spectra | ||
* @param {Array<number>} ms1y - Array of weighted abundance values for the first spectra | ||
* @param {Array<number>} ms2x - Array of mass values for the second spectra | ||
* @param {Array<number>} ms2y - Array of weighted abundance values for the second spectra | ||
* @return {number} - Similarity between two MS spectra | ||
*/ | ||
function cosine(ms1x, ms1y, ms2x, ms2y) { | ||
function cosineSimilarity(ms1x, ms1y, ms2x, ms2y) { | ||
let index1 = 0; | ||
@@ -1653,3 +990,3 @@ let index2 = 0; | ||
while ((index1 < ms1x.length) || (index2 < ms2x.length)) { | ||
while (index1 < ms1x.length || index2 < ms2x.length) { | ||
let w1 = ms1y[index1]; | ||
@@ -1672,7 +1009,7 @@ let w2 = ms2y[index2]; | ||
var norm1Norm2 = norm1 * norm2; | ||
let norm1Norm2 = norm1 * norm2; | ||
if (norm1Norm2 === 0) { | ||
return 0; | ||
} else { | ||
return (product * product) / (norm1Norm2); | ||
return (product * product) / norm1Norm2; | ||
} | ||
@@ -1684,14 +1021,13 @@ } | ||
* @ignore | ||
* @param {Chromatogram} chromatography - Chromatogram to process | ||
* @param {Chromatogram} chromatogram - Chromatogram to process | ||
* @param {object} [options] - Options object (same as spectraComparison) | ||
* @return {{peaks: Array<object>, integratedMs: Array<object>, vector: Array<object>}} - Array of peaks, integrated mass spectra and weighted mass spectra | ||
*/ | ||
function preprocessing(chromatography, options) { | ||
function preprocessing(chromatogram, options) { | ||
// peak picking | ||
let peaks = getPeaks(chromatography, options); | ||
let peaks = getPeaks(chromatogram, options); | ||
peaks = peaks.sort((a, b) => a.index - b.index); | ||
// integrate mass in the peaks | ||
let ms = chromatography.getSerie('ms').data; | ||
let integratedMs = massInPeaks(peaks, ms, options); | ||
let integratedMs = appendMass(chromatogram, peaks, options); | ||
let vector = vectorify(integratedMs, options); | ||
@@ -1702,7 +1038,7 @@ | ||
integratedMs, | ||
vector | ||
vector, | ||
}; | ||
} | ||
const defaultOption = { | ||
const defaultOptions = { | ||
thresholdFactor: 0, | ||
@@ -1714,24 +1050,7 @@ maxNumberPeaks: Number.MAX_VALUE, | ||
intPower: 0.6, | ||
similarityThreshold: 0.7 | ||
similarityThreshold: 0.7, | ||
}; | ||
/** | ||
* Returns the most similar peaks between two GC/MS and their similarities | ||
* @param {Chromatogram} chrom1 - First chromatogram | ||
* @param {Chromatogram} chrom2 - Second chromatogram | ||
* @param {object} [options] - Options object | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @param {number} [options.heightFilter = 2] - Filter all objects that are bellow `heightFilter` times the median of the height | ||
* @param {number} [options.massPower = 3] - Power applied to the mass values | ||
* @param {number} [options.intPower = 0.6] - Power applied to the abundance values | ||
* @param {number} [options.similarityThreshold = 0.7] - Minimum similarity value to consider them similar | ||
* @return {object} - Most similar peaks and their similarities: | ||
* * `peaksFirst`: Array of peaks, integrated mass spectra and weighted mass spectra for the first chromatogram | ||
* * `peaksSecond`: Array of peaks, integrated mass spectra and weighted mass spectra for the second chromatogram | ||
* * `peaksSimilarity`: Array of similarities (number) | ||
*/ | ||
function spectraComparison(chrom1, chrom2, options) { | ||
options = Object.assign({}, defaultOption, options); | ||
function spectraComparison(chrom1, chrom2, options = {}) { | ||
options = Object.assign({}, defaultOptions, options); | ||
@@ -1747,3 +1066,3 @@ // peak picking | ||
chrom2: new Array(len), | ||
similarity: new Array(len) | ||
similarity: new Array(len), | ||
}; | ||
@@ -1757,3 +1076,8 @@ let similarLen = 0; | ||
for (let j = 0; j < reference.peaks.length; ++j) { | ||
let sim = cosine(sample.vector[i].x, sample.vector[i].y, reference.vector[j].x, reference.vector[j].y); | ||
let sim = cosineSimilarity( | ||
sample.vector[i].x, | ||
sample.vector[i].y, | ||
reference.vector[j].x, | ||
reference.vector[j].y, | ||
); | ||
@@ -1764,3 +1088,3 @@ if (sim > options.similarityThreshold && sim > max.similarity) { | ||
chrom1: reference.peaks[j], | ||
chrom2: sample.peaks[i] | ||
chrom2: sample.peaks[i], | ||
}; | ||
@@ -1783,3 +1107,3 @@ } | ||
for (let i = 0; i < similarLen; ++i) { | ||
if ({}.hasOwnProperty.call(duplicates, similarityPeaks.chrom1[i].x)) { | ||
if (duplicates[similarityPeaks.chrom1[i].x]) { | ||
duplicates[similarityPeaks.chrom1[i].x].push(i); | ||
@@ -1809,27 +1133,15 @@ } else { | ||
/** | ||
* Aligns the time of the sample based on the regression with his reference value | ||
* @param {Array<object>} reference - Array of peaks, integrated mass spectra and weighted mass spectra for the reference chromatogram | ||
* @param {Array<object>} sample - Array of peaks, integrated mass spectra and weighted mass spectra for the sample chromatogram | ||
* @param {object} [options] - Options object | ||
* @param {boolean} [options.computeQuality = false] - Calculate the quality of the regression | ||
* @param {number} [options.polynomialDegree = 3] - Degree of the polynomial regression | ||
* @return {object} - The scaled spectra: | ||
* * `scaleRegression`: The regression function to make the regression | ||
* * `stringFormula`: Regression equation | ||
* * `r2`: R2 quality number | ||
* * `error`: Vector of the difference between the spected value and the actual shift value | ||
*/ | ||
function scaleAlignment(reference, sample, options = {}) { | ||
const { | ||
computeQuality = false, | ||
polynomialDegree = 3 | ||
} = options; | ||
const { computeQuality = false, polynomialDegree = 3 } = options; | ||
let referenceTime = reference.map((val) => val.x); | ||
let sampleTime = sample.map((val) => val.x); | ||
const regression = new Regression(sampleTime, referenceTime, polynomialDegree); | ||
const regression = new Regression( | ||
sampleTime, | ||
referenceTime, | ||
polynomialDegree, | ||
); | ||
let error = new Array(sample.length); | ||
for (var i = 0; i < sample.length; i++) { | ||
for (let i = 0; i < sample.length; i++) { | ||
error[i] = reference[i].x - regression.predict(sample[i].x); | ||
@@ -1839,3 +1151,3 @@ } | ||
let ans = { | ||
scaleRegression: regression | ||
scaleRegression: regression, | ||
}; | ||
@@ -1852,6 +1164,117 @@ | ||
/** | ||
* Creates a new Chromatogram element based in a JCAMP string | ||
* @param {string} jcamp - String containing the JCAMP data | ||
* @return {Chromatogram} - New class element with the given data | ||
* Calculates the Kovats retention index for a mass spectra of a n-alkane | ||
* @param {object} ms - A mass spectra object | ||
* @param {Array<number>} ms.x - Array of masses | ||
* @param {Array<number>} ms.y - Array of intensities | ||
* @return {number} - Kovats retention index | ||
*/ | ||
function kovats(ms, options = {}) { | ||
const { threshold = 0.01 } = options; | ||
// we normalize the data and filter them | ||
let maxY = max(ms.y); | ||
let masses = []; | ||
let intensities = []; | ||
for (let i = 0; i < ms.x.length; i++) { | ||
if (ms.y[i] / maxY > threshold) { | ||
masses.push(ms.x[i]); | ||
intensities.push(ms.y[i] / maxY); | ||
} | ||
} | ||
// we find candidates | ||
let nAlcaneMasses = []; | ||
let fragmentMasses = []; | ||
for (let i = 0; i < masses.length; i++) { | ||
if ((masses[i] - 2) % 14 === 0) { | ||
nAlcaneMasses.push(masses[i]); | ||
} | ||
if ((masses[i] - 1) % 14 === 0) { | ||
fragmentMasses.push(masses[i]); | ||
} | ||
} | ||
if (nAlcaneMasses.length === 0) return {}; | ||
let biggestMass = nAlcaneMasses.sort((a, b) => b - a)[0]; | ||
fragmentMasses = fragmentMasses.filter((mass) => mass < biggestMass); | ||
return { | ||
index: (100 * (biggestMass - 2)) / 14, | ||
numberFragments: fragmentMasses.length, | ||
percentFragments: fragmentMasses.length / ((biggestMass - 2) / 14), | ||
}; | ||
} | ||
function appendKovats(peaks) { | ||
const result = []; | ||
for (let peak of peaks) { | ||
result.push({ ...peak, kovats: kovats(peak.ms) }); | ||
} | ||
return result; | ||
} | ||
const ascValue = (a, b) => a.index - b.index; | ||
const ascTime = (a, b) => a.time - b.time; | ||
function getKovatsConversionFunction(peaks, options = {}) { | ||
const { revert = false } = options; | ||
const kovatsConversionTable = peaks.map((peak) => ({ | ||
time: peak.x, | ||
index: peak.kovats.index, | ||
})); | ||
if (revert) { | ||
const indexes = kovatsConversionTable.sort(ascValue); | ||
return (index) => { | ||
let position = binarySearch(indexes, { index }, ascValue); | ||
if (position < 0) { | ||
position = ~position; | ||
// handle extreme cases | ||
if (position === 0 || position === indexes.length) { | ||
return 0; | ||
} | ||
let smallerAlcane = indexes[position - 1].time; | ||
let largerAlcane = indexes[position].time; | ||
return ( | ||
((index - indexes[position - 1].index) * | ||
(largerAlcane - smallerAlcane)) / | ||
100 + | ||
smallerAlcane | ||
); | ||
} else { | ||
return indexes[position].time; | ||
} | ||
}; | ||
} else { | ||
const times = kovatsConversionTable.sort(ascTime); | ||
return (time) => { | ||
let position = binarySearch(times, { time }, ascTime); | ||
if (position < 0) { | ||
position = ~position; | ||
// handle extreme cases | ||
if (position === 0 || position === times.length) { | ||
return 0; | ||
} | ||
let smallerAlcane = times[position - 1].time; | ||
let largerAlcane = times[position].time; | ||
return ( | ||
(100 * (time - smallerAlcane)) / (largerAlcane - smallerAlcane) + | ||
times[position - 1].index | ||
); | ||
} else { | ||
return times[position].index; | ||
} | ||
}; | ||
} | ||
} | ||
function fromJcamp(jcamp) { | ||
@@ -1862,15 +1285,8 @@ const data = jcampconverter.convert(jcamp, { chromatogram: true }).chromatogram; | ||
/** | ||
* Creates a new Chromatogram element based in a Txt string | ||
* @param {string} text - String containing the data as CSV or TSV | ||
* @param {object} [options] - Options object for the parser | ||
* @return {Chromatogram} - New class element with the given data | ||
*/ | ||
function fromText(text, options) { | ||
options = Object.assign({}, options, { arrayType: 'xxyy' }); | ||
function fromText(text, options = {}) { | ||
const data = xyParser.parseXY(text, options); | ||
const time = data[0]; | ||
const time = data.x; | ||
let series = { | ||
intensity: data[1] | ||
intensity: data.y, | ||
}; | ||
@@ -1885,32 +1301,20 @@ | ||
function fromMzML(xml, kind = 'mzData') { | ||
switch (kind) { | ||
case 'mzData': | ||
return fromJSON(mzData(xml)); | ||
default: | ||
throw new Error(`Unable to parse from "${kind}" format`); | ||
} | ||
function fromXML(xml) { | ||
return fromJSON(mzData(xml)); | ||
} | ||
exports.Chromatogram = Chromatogram; | ||
exports.massInPeaks = massInPeaks; | ||
exports.vectorify = vectorify; | ||
exports.appendKovats = appendKovats; | ||
exports.appendMass = appendMass; | ||
exports.cosineSimilarity = cosineSimilarity; | ||
exports.fromJSON = fromJSON; | ||
exports.fromJcamp = fromJcamp; | ||
exports.fromNetCDF = fromNetCDF; | ||
exports.fromText = fromText; | ||
exports.fromXML = fromXML; | ||
exports.getKovatsConversionFunction = getKovatsConversionFunction; | ||
exports.kovats = kovats; | ||
exports.massFilter = massFilter; | ||
exports.scaleAlignment = scaleAlignment; | ||
exports.spectraComparison = spectraComparison; | ||
exports.scaleAlignment = scaleAlignment; | ||
exports.kovats = kovats; | ||
exports.getKovatsTable = getKovatsTable; | ||
exports.kovatsConversionFunction = kovatsConversionFunction; | ||
exports.rescaleTime = rescaleTime; | ||
exports.getKovatsRescale = getKovatsRescale; | ||
exports.cosine = cosine; | ||
exports.meanFilter = meanFilter; | ||
exports.percentageFilter = percentageFilter; | ||
exports.fromJcamp = fromJcamp; | ||
exports.fromJSON = fromJSON; | ||
exports.fromText = fromText; | ||
exports.fromNetCDF = fromNetCDF; | ||
exports.fromMzML = fromMzML; | ||
exports.integrate = integrate; | ||
exports.merge = merge; | ||
exports.getPeaks = getPeaks; | ||
exports.vectorify = vectorify; |
{ | ||
"name": "chromatography", | ||
"version": "3.11.0", | ||
"description": "Tools for storing, search and analize GC/MS spectra", | ||
"version": "4.0.0-0", | ||
"description": "Tools for storing, searching and analyzing GC/MS data", | ||
"main": "lib/index.js", | ||
"module": "src/index.js", | ||
"types": "chromatography.d.ts", | ||
"sideEffects": false, | ||
"files": [ | ||
"chromatography.d.ts", | ||
"lib", | ||
@@ -12,15 +15,14 @@ "src" | ||
"scripts": { | ||
"build": "rollup -c && cheminfo build --root Chromatography --no-uglify", | ||
"build-doc": "cheminfo doc", | ||
"publish-doc": "cheminfo doc --publish", | ||
"eslint": "eslint src", | ||
"build": "cheminfo-build --entry src/index.js --root Chromatography", | ||
"build-docs": "typedoc --out docs --name \"Chromatography\" --mode file --includeDeclarations --excludeExternals --hideGenerator --excludePrivate --moduleResolution node --target ESNext chromatography.d.ts", | ||
"eslint": "eslint src --cache", | ||
"eslint-fix": "npm run eslint -- --fix", | ||
"prepublish": "rollup -c", | ||
"test": "run-s testonly eslint", | ||
"testonly": "jest", | ||
"test-travis": "jest --coverage && codecov" | ||
"prepublishOnly": "rollup -c", | ||
"test": "npm run test-coverage && npm run eslint", | ||
"test-only": "jest", | ||
"test-coverage": "jest --coverage" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/cheminfo-js/chromatography.git" | ||
"url": "git+https://github.com/cheminfo/chromatography.git" | ||
}, | ||
@@ -36,38 +38,39 @@ "jest": { | ||
"bugs": { | ||
"url": "https://github.com/cheminfo-js/chromatography/issues" | ||
"url": "https://github.com/cheminfo/chromatography/issues" | ||
}, | ||
"homepage": "https://github.com/cheminfo-js/chromatography#readme", | ||
"homepage": "https://github.com/cheminfo/chromatography#readme", | ||
"devDependencies": { | ||
"babel-jest": "^24.5.0", | ||
"babel-loader": "^8.0.5", | ||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", | ||
"babel-preset-env": "^1.7.0", | ||
"cheminfo-tools": "^1.22.4", | ||
"codecov": "^3.2.0", | ||
"documentation": "^9.3.1", | ||
"eslint": "^5.15.1", | ||
"eslint-config-cheminfo": "^1.20.0", | ||
"eslint-plugin-import": "^2.16.0", | ||
"eslint-plugin-jest": "^22.3.2", | ||
"eslint-plugin-no-only-tests": "^2.1.0", | ||
"jest": "^24.5.0", | ||
"@babel/plugin-transform-modules-commonjs": "^7.8.3", | ||
"@types/jest": "^25.1.3", | ||
"cheminfo-build": "^1.0.7", | ||
"eslint": "^6.8.0", | ||
"eslint-config-cheminfo": "^2.0.4", | ||
"eslint-plugin-import": "^2.20.1", | ||
"eslint-plugin-jest": "^23.8.0", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"esm": "^3.2.25", | ||
"jest": "^25.1.0", | ||
"jest-matcher-deep-close-to": "^1.3.0", | ||
"npm-run-all": "^4.1.5", | ||
"rollup": "^1.6.0" | ||
"prettier": "^1.19.1", | ||
"rollup": "^1.31.1", | ||
"spectrum-generator": "^3.1.3", | ||
"typedoc": "^0.16.10" | ||
}, | ||
"dependencies": { | ||
"binary-search": "^1.3.5", | ||
"isotopic-distribution": "^0.9.1", | ||
"jcampconverter": "^3.0.0", | ||
"mf-parser": "^0.9.1", | ||
"ml-array-max": "^1.1.1", | ||
"ml-array-mean": "^1.1.1", | ||
"ml-gsd": "^2.0.5", | ||
"ml-regression-polynomial": "^1.0.3", | ||
"binary-search": "^1.3.6", | ||
"is-any-array": "0.0.3", | ||
"isotopic-distribution": "^0.13.0", | ||
"jcampconverter": "^4.1.0", | ||
"mf-parser": "^0.14.0", | ||
"ml-array-max": "^1.1.2", | ||
"ml-array-mean": "^1.1.2", | ||
"ml-array-median": "^1.1.2", | ||
"ml-array-sum": "^1.1.2", | ||
"ml-gsd": "^2.0.7", | ||
"ml-regression-polynomial": "^2.1.0", | ||
"ml-spectra-processing": "^2.0.0", | ||
"mzdata": "^1.1.0", | ||
"mzmjs": "^0.2.0", | ||
"netcdf-gcms": "^1.3.1", | ||
"num-sort": "^1.0.0", | ||
"xy-parser": "^2.2.2" | ||
"xy-parser": "^3.0.0" | ||
} | ||
} |
# chromatography | ||
[![NPM version][npm-image]][npm-url] | ||
[![build status][travis-image]][travis-url] | ||
[![Test coverage][codecov-image]][codecov-url] | ||
[![npm download][download-image]][download-url] | ||
[![NPM version][npm-image]][npm-url] | ||
[![build status][ci-image]][ci-url] | ||
[![npm download][download-image]][download-url] | ||
Tools for storing, search and analyze GC/MS spectra. | ||
Tools for storing, searching and analyzing GC/MS data. | ||
## Installation | ||
`$ npm install --save chromatography` | ||
`$ npm i chromatography` | ||
@@ -18,8 +17,10 @@ ## Usage | ||
import * as GCMS from 'chromatography'; | ||
// const GCMS = require('chromatography'); | ||
let gcms = GCMS.fromJcamp(jcampReferenceMixture); | ||
let kovatsConversionTable = GCMS.getKovatsTable(gcms); // [{time, value}] | ||
let conversionFunction = GCMS.kovatsConversionFunction(kovatsConversionTable, {}); | ||
let kovatsConversionTable = GCMS.appendKovats(gcms); // [{time, value}] | ||
let conversionFunction = GCMS.getKovatsConversionFunction( | ||
kovatsConversionTable, | ||
{}, | ||
); | ||
@@ -29,3 +30,2 @@ let diesel = GCMS.fromJcamp(jcampOfDiesel); | ||
diesel.setTimes(times); | ||
// diesel.rescaleTime(conversionFunction); | ||
@@ -37,8 +37,11 @@ let peaks = GCMS.getPeaks(diesel, options); | ||
// get a spectrum in another reference model | ||
let revertConversionFunction = GCMS.kovatsConversionFunction(kovatsConversionTable, {revert: true}); | ||
// Get a spectrum in another reference model | ||
let revertConversionFunction = GCMS.getKovatsConversionFunction( | ||
kovatsConversionTable, | ||
{ revert: true }, | ||
); | ||
let mySpectrumInAnotherReference = revertConversionFunction(mySpectrum); | ||
``` | ||
## [API Documentation](https://cheminfo-js.github.io/chromatography/) | ||
## [API Documentation](https://cheminfo.github.io/chromatography/) | ||
@@ -49,11 +52,9 @@ [API discussion](https://docs.google.com/document/d/1Jg2l6wKjFCYBSqdVWBSujSkqMhsEV6ZMyxeI9RSLhn0/edit#heading=h.8gjgl6jygt0s) | ||
[MIT](./LICENSE) | ||
[MIT](./LICENSE) | ||
[npm-image]: https://img.shields.io/npm/v/chromatography.svg?style=flat-square | ||
[npm-image]: https://img.shields.io/npm/v/chromatography.svg | ||
[npm-url]: https://npmjs.org/package/chromatography | ||
[travis-image]: https://img.shields.io/travis/cheminfo-js/chromatography/master.svg?style=flat-square | ||
[travis-url]: https://travis-ci.org/cheminfo-js/chromatography | ||
[codecov-image]: https://img.shields.io/codecov/c/github/cheminfo-js/chromatography.svg?style=flat-square | ||
[codecov-url]: https://codecov.io/github/cheminfo-js/chromatography | ||
[download-image]: https://img.shields.io/npm/dm/chromatography.svg?style=flat-square | ||
[ci-image]: https://github.com/mljs/matrix/workflows/Node.js%20CI/badge.svg?branch=master | ||
[ci-url]: https://github.com/mljs/matrix/actions?query=workflow%3A%22Node.js+CI%22 | ||
[download-image]: https://img.shields.io/npm/dm/chromatography.svg | ||
[download-url]: https://npmjs.org/package/chromatography |
@@ -1,28 +0,19 @@ | ||
import { rescaleTime } from './rescaleTime'; | ||
import isAnyArray from 'is-any-array'; | ||
import { X } from 'ml-spectra-processing'; | ||
import { filter } from './util/filter'; | ||
import { serieFromArray } from './serieFromArray'; | ||
import { fromJSON } from './from/json'; | ||
import { getPeaks } from './util/getPeaks'; | ||
import { seriesFromArray } from './seriesFromArray'; | ||
import { getPeaks } from './peaks/getPeaks'; | ||
import { calculateTic } from './ms/calculateTic'; | ||
import { calculateLength } from './ms/calculateLength'; | ||
import { calculateBpc } from './ms/calculateBpc'; | ||
import { calculateForMass } from './ms/calculateForMass'; | ||
import { calculateEic } from './ms/calculateEic'; | ||
import { calculateForMF } from './ms/calculateForMF'; | ||
import { integrate } from './util/integrate'; | ||
import { merge } from './util/merge'; | ||
import { getKovatsRescale } from './getKovatsRescale'; | ||
import { getClosestTime } from './util/getClosestTime'; | ||
import { merge } from './ms/merge'; | ||
import { applyLockMass } from './ms/applyLockMass'; | ||
import { meanFilter } from './filter/meanFilter'; | ||
import { percentageFilter } from './filter/percentageFilter'; | ||
import { toJSON } from './to/json'; | ||
import { getClosestData } from './util/getClosestData'; | ||
import { isArray } from './util/isArray'; | ||
/** | ||
* Class allowing to store time / ms (ms) series | ||
* It allows also to store simple time a trace | ||
* @class Chromatogram | ||
* @param {Array<number>} times - Time serie | ||
* @param {object} series - A map of series with name and the Serie object | ||
*/ | ||
export class Chromatogram { | ||
@@ -32,12 +23,10 @@ constructor(times, series) { | ||
this.times = []; | ||
if (times) { | ||
if (!isArray(times)) { | ||
throw new TypeError('Times must be an array'); | ||
} | ||
this.times = times; | ||
} else { | ||
throw new Error('The time serie is mandatory'); | ||
if (!isAnyArray(times)) { | ||
throw new TypeError('times must be an array'); | ||
} | ||
this.times = times; | ||
if (series) { | ||
this.addSeries(series); | ||
for (const [name, value] of Object.entries(series)) { | ||
this.addSeries(name, value); | ||
} | ||
} | ||
@@ -50,12 +39,8 @@ } | ||
/** | ||
* Find the serie giving the name | ||
* @param {string} name - name of the serie | ||
* @return {object} - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
*/ | ||
getSerie(name) { | ||
return this.series[name]; | ||
getSeries(seriesName) { | ||
this.requiresSeries(seriesName); | ||
return this.series[seriesName]; | ||
} | ||
getSerieNames() { | ||
getSeriesNames() { | ||
return Object.keys(this.series); | ||
@@ -65,72 +50,33 @@ } | ||
hasMass() { | ||
return this.hasSerie('ms'); | ||
return this.hasSeries('ms'); | ||
} | ||
/** | ||
* Delete a serie | ||
* @param {string} name - Name of the serie | ||
*/ | ||
deleteSerie(name) { | ||
if (!this.hasSerie(name)) { | ||
throw new Error(`The serie "${name}" does not exist`); | ||
} else { | ||
delete this.series[name]; | ||
} | ||
deleteSeries(seriesName) { | ||
this.requiresSeries(seriesName); | ||
delete this.series[seriesName]; | ||
return this; | ||
} | ||
/** | ||
* Add new series | ||
* @param {object} series - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
* @param {object} [options = {}] - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
*/ | ||
addSeries(series, options = {}) { | ||
if (typeof series !== 'object' || Array.isArray(series)) { | ||
throw new TypeError('data must be an object containing arrays of series'); | ||
addSeries(seriesName, array, options = {}) { | ||
if (this.hasSeries(seriesName) && !options.force) { | ||
throw new Error(`A series with name "${seriesName}" already exists`); | ||
} | ||
for (const key of Object.keys(series)) { | ||
this.addSerie(key, series[key], options); | ||
} | ||
} | ||
/** | ||
* Add a new serie | ||
* @param {string} name - Name of the serie to add | ||
* @param {Array} array - Object with an array of data, dimensions of the elements in the array and name of the serie | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force replacement of existing serie | ||
*/ | ||
addSerie(name, array, options = {}) { | ||
if (this.hasSerie(name) && !options.force) { | ||
throw new Error(`A serie with name "${name}" already exists`); | ||
} | ||
if (this.times.length !== array.length) { | ||
throw new Error('The array size is not the same as the time size'); | ||
throw new Error(`The series size is not the same as the times size`); | ||
} | ||
this.series[name] = serieFromArray(array); | ||
this.series[name].name = name; | ||
this.series[seriesName] = seriesFromArray(array); | ||
this.series[seriesName].name = seriesName; | ||
return this; | ||
} | ||
/** | ||
* Returns true if the serie name exists | ||
* @param {string} name - Name of the serie to check | ||
* @return {boolean} | ||
*/ | ||
hasSerie(name) { | ||
return typeof this.series[name] !== 'undefined'; | ||
hasSeries(seriesName) { | ||
return typeof this.series[seriesName] !== 'undefined'; | ||
} | ||
/** | ||
* Throws if the serie does not exists | ||
* @param {string} name - Name of the serie to check | ||
*/ | ||
requiresSerie(name) { | ||
if (!this.hasSerie(name)) { | ||
throw new Error(`The serie "${name}" does not exist`); | ||
requiresSeries(seriesName) { | ||
if (!this.hasSeries(seriesName)) { | ||
throw new Error(`The series "${seriesName}" does not exist`); | ||
} | ||
} | ||
/** | ||
* Returns the first time value | ||
* @return {number} - First time value | ||
*/ | ||
get firstTime() { | ||
@@ -140,6 +86,2 @@ return this.times[0]; | ||
/** | ||
* Returns the last time value | ||
* @return {number} - Last time value | ||
*/ | ||
get lastTime() { | ||
@@ -149,6 +91,2 @@ return this.times[this.length - 1]; | ||
/** | ||
* Returns the time values | ||
* @return {Array<number>} - Time values | ||
*/ | ||
getTimes() { | ||
@@ -158,28 +96,14 @@ return this.times; | ||
/** | ||
* Assign the time values | ||
* @param {Array<number>} times - New time values | ||
*/ | ||
setTimes(times) { | ||
if (times.length !== this.times.length) { | ||
throw new Error('New times must have the same length as the old ones'); | ||
} | ||
this.times = times; | ||
} | ||
/** | ||
* Modifies the time applying the conversion function | ||
* @param {function(number)} conversionFunction | ||
* @return {Chromatogram} | ||
*/ | ||
rescaleTime(conversionFunction) { | ||
this.times = rescaleTime(this.times, conversionFunction); | ||
this.times = this.times.map((time) => conversionFunction(time)); | ||
return this; | ||
} | ||
/** | ||
* Will filter the entries based on the time | ||
* You can either use the index of the actual time | ||
* @param {function(number, number)} callback | ||
* @param {object} [options] - options object | ||
* @param {boolean} [options.copy = false] - return a copy of the original object | ||
* @return {Chromatogram} | ||
*/ | ||
filter(callback, options) { | ||
@@ -189,8 +113,2 @@ return filter(this, callback, options); | ||
/** | ||
* Apply the GSD peak picking algorithm | ||
* @param {object} [options] - Options object | ||
* @param {object} [options.heightFilter = 2] - Filter all objects that are bellow `heightFilter` times the median of the height | ||
* @return {Array<object>} - List of GSD objects | ||
*/ | ||
getPeaks(options) { | ||
@@ -200,135 +118,66 @@ return getPeaks(this, options); | ||
/** | ||
* Calculate tic | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force the calculation it it exists | ||
*/ | ||
calculateTic(options = {}) { | ||
if (!this.getSerie('tic') || options.force) { | ||
let tic = calculateTic(this); | ||
this.addSerie('tic', tic, options); | ||
if (!this.hasSeries('tic') || options.force) { | ||
const tic = calculateTic(this); | ||
this.addSeries('tic', tic, { force: true }); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Calculate length and save it in the 'length' serie | ||
* @param {string} serieName - Name of the serie to make calculation | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force the calculation it it exists | ||
*/ | ||
calculateLength(serieName, options = {}) { | ||
if (!this.getSerie('length') || options.force) { | ||
let length = calculateLength(this, serieName); | ||
this.addSerie('length', length, options); | ||
calculateLength(seriesName, options = {}) { | ||
if (!this.hasSeries('length') || options.force) { | ||
const length = calculateLength(this, seriesName); | ||
this.addSeries('length', length, { force: true }); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Calculate bpc | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.force = false] - Force the calculation it it exists | ||
*/ | ||
calculateBpc(options = {}) { | ||
if (!this.getSerie('bpc') || options.force) { | ||
let bpc = calculateBpc(this); | ||
this.addSerie('bpc', bpc, options); | ||
if (!this.hasSeries('bpc') || options.force) { | ||
const bpc = calculateBpc(this); | ||
this.addSeries('bpc', bpc, { force: true }); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Calculate mass spectrum by filtering for a specific mass | ||
* @param {number} targetMass - mass for which to extract the spectrum | ||
* @param {object} [options = {}] - Options object | ||
* @param {string} [options.serieName='ms'+targetMass] - Name of the serie to make calculation | ||
* @param {boolean} [options.cache = false] - Retrieve from cache if exists | ||
* @param {boolean} [options.force = false] - Force replacement of existing serie | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMass | ||
* @return {Serie} | ||
*/ | ||
calculateForMass(targetMass, options = {}) { | ||
calculateEic(targetMass, options = {}) { | ||
const { | ||
serieName = `ms${targetMass}-${options.error || 0.5}`, | ||
cache = false | ||
seriesName = `ms${targetMass}±${options.slotWidth / 2 || 0.5}`, | ||
cache = false, | ||
} = options; | ||
if (cache && this.hasSerie(serieName)) return this.getSerie(serieName); | ||
let result = calculateForMass(this, targetMass, options); | ||
this.addSerie(serieName, result, options); | ||
return this.getSerie(serieName); | ||
if (cache && this.hasSeries(seriesName)) return this.getSeries(seriesName); | ||
const result = calculateEic(this, targetMass, options); | ||
this.addSeries(seriesName, result, options); | ||
return this.getSeries(seriesName); | ||
} | ||
/** | ||
* Calculate mass spectrum by filtering for a specific mass | ||
* @param {string} targetMF - mass for which to extract the spectrum | ||
* @param {object} [options = {}] - Options object | ||
* @param {string} [options.serieName='ms'+targetMass] - Name of the serie to make calculation | ||
* @param {boolean} [options.force = false] - Force replacement of existing serie | ||
* @param {boolean} [options.cache = false] - Retrieve from cache if exists | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMass | ||
* @param {number} [options.ionizations='H+'] - List of allowed ionisation | ||
* @return {Serie} | ||
*/ | ||
calculateForMF(targetMF, options = {}) { | ||
const { | ||
serieName = `ms${targetMF}-${options.ionizations || | ||
'H+'}-${options.error || 0.5}`, | ||
cache = false | ||
seriesName = `ms ${targetMF} ${options.ionizations || | ||
'H+'} (${options.slotWidth || 1}, ${options.threshold || 0.05})`, | ||
cache = false, | ||
} = options; | ||
if (cache && this.hasSerie(serieName)) return this.getSerie(serieName); | ||
let result = calculateForMF(this, targetMF, options); | ||
this.addSerie(serieName, result, options); | ||
return this.getSerie(serieName); | ||
if (cache && this.hasSeries(seriesName)) return this.getSeries(seriesName); | ||
const result = calculateForMF(this, targetMF, options); | ||
this.addSeries(seriesName, result, options); | ||
return this.getSeries(seriesName); | ||
} | ||
/** | ||
* Calculates the table of Kovats indexes for the reference spectra | ||
* @param {object} [options = {}] - Options object | ||
* @param {number} [options.heightFilter = 100] - Filter all objects that are below heightFilter times the median of the height | ||
* @param {number} [options.thresholdFactor = 0.005] - Every peak that is below the main peak times this factor will be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = 40] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 5] - When find a max can't be another max in a radius of this size | ||
* @param {boolean} [options.revert = false] - True for convert from Kovats to time, false otherwise | ||
* @return {{conversionFunction:function(number),kovatsIndexes:Array<object>,peaks:Array<object>}} - Time and value for the Kovats index | ||
*/ | ||
getKovatsRescale(options) { | ||
return getKovatsRescale(this, options); | ||
integrate(ranges, options) { | ||
return integrate(this, ranges, options); | ||
} | ||
/** | ||
* Returns an object with the result of the integrations | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {string|boolean} [options.baseline] - Applies baseline correction | ||
* @return {[]} | ||
*/ | ||
integrate(name, ranges, options) { | ||
return integrate(this, name, ranges, options); | ||
merge(options) { | ||
return merge(this, options); | ||
} | ||
/** | ||
* Retuns an object with the result of the integrations | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {object} [options.algorithm = 'slot'] - Decision for merging the peaks | ||
* @param {object} [options.delta = 1] - Parameter for merging the peaks | ||
* @return {[]} | ||
*/ | ||
merge(name, ranges, options) { | ||
return merge(this, name, ranges, options); | ||
getClosestTime(time) { | ||
return X.findClosestIndex(this.getTimes(), time); | ||
} | ||
/** | ||
* Returns information for the closest time | ||
* @param {number} time - Retention time | ||
* @return {{index: number, timeBefore: number, timeAfter: number, timeClosest: number, safeIndexBefore: number, safeIndexAfter: number}} | ||
*/ | ||
getClosestTime(time) { | ||
return getClosestTime(time, this.getTimes()); | ||
getClosestData(time, options = {}) { | ||
return getClosestData(this, time, options); | ||
} | ||
/** | ||
* Return a copy of the chromatogram | ||
* @return {Chromatogram} | ||
*/ | ||
copy() { | ||
@@ -339,37 +188,51 @@ const json = JSON.parse(JSON.stringify(this)); | ||
/** | ||
* Filter the given serie2D based on it's median value | ||
* @param {string} serieName | ||
* @param {object} [options] | ||
* @param {string} [options.serieName = 'msMedian'] - Name of the new serie | ||
* @param {number} [options.factor = 2] - The values under the median times this factor are removed | ||
*/ | ||
meanFilter(serieName, options = {}) { | ||
var serie = meanFilter(this, serieName, options); | ||
if (options.serieName) { | ||
this.series[options.serieName] = serie; | ||
} else { | ||
this.series.msMedian = serie; | ||
meanFilter(seriesName, options = {}) { | ||
const { seriesName: newSeriesName = 'msMedian' } = options; | ||
if (this.hasSeries(newSeriesName) && !options.force) { | ||
throw new Error(`A series with name "${seriesName}" already exists`); | ||
} | ||
const newSeries = meanFilter(this, seriesName, options); | ||
this.series[newSeriesName] = newSeries; | ||
return newSeries; | ||
} | ||
/** | ||
* Filter the given serie2D based on the percentage of the highest value | ||
* @param {string} serieName | ||
* @param {object} [options] | ||
* @param {string} [options.serieName = 'msPercentage'] - Name of the new serie | ||
* @param {number} [options.percentage = 0.1] - The values under the median times this factor are removed | ||
*/ | ||
percentageFilter(serieName, options = {}) { | ||
var serie = percentageFilter(this, serieName, options); | ||
if (options.serieName) { | ||
this.series[options.serieName] = serie; | ||
} else { | ||
this.series.msPercentage = serie; | ||
percentageFilter(seriesName, options = {}) { | ||
const { seriesName: newSeriesName = 'msPercentage' } = options; | ||
if (this.hasSeries(newSeriesName) && !options.force) { | ||
throw new Error(`A series with name "${seriesName}" already exists`); | ||
} | ||
const newSeries = percentageFilter(this, seriesName, options); | ||
this.series[newSeriesName] = newSeries; | ||
return newSeries; | ||
} | ||
applyLockMass(mfs, options) { | ||
return applyLockMass(this, mfs, options); | ||
} | ||
toJSON() { | ||
return { | ||
times: this.times, | ||
series: this.series, | ||
}; | ||
} | ||
} | ||
Chromatogram.prototype.applyLockMass = applyLockMass; | ||
Chromatogram.prototype.toJSON = toJSON; | ||
Chromatogram.prototype.getClosestData = getClosestData; | ||
export function fromJSON(json) { | ||
let series = json.series; | ||
let times = json.times; | ||
let chromatogram = new Chromatogram(times); | ||
if (Array.isArray(series)) { | ||
for (let i = 0; i < series.length; i++) { | ||
chromatogram.addSeries(series[i].name, series[i].data); | ||
} | ||
} else { | ||
for (let key of Object.keys(series)) { | ||
chromatogram.addSeries(key, series[key].data, { | ||
meta: series[key].meta, | ||
}); | ||
} | ||
} | ||
return chromatogram; | ||
} |
import arrayMean from 'ml-array-mean'; | ||
import { serieFromArray } from '../serieFromArray'; | ||
import { seriesFromArray } from '../seriesFromArray'; | ||
export function meanFilter(chromatogram, serieName, options = {}) { | ||
export function meanFilter(chromatogram, seriesName, options = {}) { | ||
const { factor = 2 } = options; | ||
var serie = chromatogram.getSerie(serieName); | ||
var filtered = []; | ||
for (var i = 0; i < serie.data.length; i++) { | ||
filtered.push(applyFilter(serie.data[i], factor)); | ||
let series = chromatogram.getSeries(seriesName); | ||
let filtered = []; | ||
for (let i = 0; i < series.data.length; i++) { | ||
filtered.push(applyFilter(series.data[i], factor)); | ||
} | ||
return serieFromArray(filtered); | ||
return seriesFromArray(filtered); | ||
} | ||
function applyFilter(serie, factor) { | ||
var filtered = [[], []]; | ||
if (serie[1].length === 0) return filtered; | ||
const meanIntensity = factor * arrayMean(serie[1]); | ||
for (var i = 0; i < serie[0].length; i++) { | ||
if (serie[1][i] > meanIntensity) { | ||
filtered[0].push(serie[0][i]); | ||
filtered[1].push(serie[1][i]); | ||
function applyFilter(series, factor) { | ||
let filtered = [[], []]; | ||
if (series[1].length === 0) return filtered; | ||
const meanIntensity = factor * arrayMean(series[1]); | ||
for (let i = 0; i < series[0].length; i++) { | ||
if (series[1][i] > meanIntensity) { | ||
filtered[0].push(series[0][i]); | ||
filtered[1].push(series[1][i]); | ||
} | ||
@@ -26,0 +26,0 @@ } |
import arrayMax from 'ml-array-max'; | ||
import { serieFromArray } from '../serieFromArray'; | ||
import { seriesFromArray } from '../seriesFromArray'; | ||
export function percentageFilter(chromatogram, serieName, options = {}) { | ||
const { | ||
percentage = 0.1 | ||
} = options; | ||
export function percentageFilter(chromatogram, seriesName, options = {}) { | ||
const { percentage = 0.1 } = options; | ||
var serie = chromatogram.getSerie(serieName); | ||
var filtered = []; | ||
let series = chromatogram.getSeries(seriesName); | ||
let filtered = []; | ||
for (var i = 0; i < serie.data.length; i++) { | ||
filtered.push(applyFilter(serie.data[i], percentage)); | ||
for (let i = 0; i < series.data.length; i++) { | ||
filtered.push(applyFilter(series.data[i], percentage)); | ||
} | ||
return serieFromArray(filtered); | ||
return seriesFromArray(filtered); | ||
} | ||
function applyFilter(serie, percentage) { | ||
var basePeak; | ||
function applyFilter(series, percentage) { | ||
let basePeak; | ||
try { | ||
basePeak = arrayMax(serie[1]); | ||
basePeak = arrayMax(series[1]); | ||
} catch (e) { | ||
basePeak = 0; | ||
} | ||
var filtered = [[], []]; | ||
for (var i = 0; i < serie[0].length; i++) { | ||
if (serie[1][i] > percentage * basePeak) { | ||
filtered[0].push(serie[0][i]); | ||
filtered[1].push(serie[1][i]); | ||
let filtered = [[], []]; | ||
for (let i = 0; i < series[0].length; i++) { | ||
if (series[1][i] > percentage * basePeak) { | ||
filtered[0].push(series[0][i]); | ||
filtered[1].push(series[1][i]); | ||
} | ||
@@ -33,0 +31,0 @@ } |
import { convert as converter } from 'jcampconverter'; | ||
import { fromJSON } from './json'; | ||
import { fromJSON } from '../Chromatogram'; | ||
/** | ||
* Creates a new Chromatogram element based in a JCAMP string | ||
* @param {string} jcamp - String containing the JCAMP data | ||
* @return {Chromatogram} - New class element with the given data | ||
*/ | ||
export function fromJcamp(jcamp) { | ||
@@ -11,0 +6,0 @@ const data = converter(jcamp, { chromatogram: true }).chromatogram; |
import netcdfJSON from 'netcdf-gcms'; | ||
import { fromJSON } from './json'; | ||
import { fromJSON } from '../Chromatogram'; | ||
@@ -5,0 +5,0 @@ export function fromNetCDF(netcdf) { |
@@ -5,16 +5,8 @@ import { parseXY } from 'xy-parser'; | ||
/** | ||
* Creates a new Chromatogram element based in a Txt string | ||
* @param {string} text - String containing the data as CSV or TSV | ||
* @param {object} [options] - Options object for the parser | ||
* @return {Chromatogram} - New class element with the given data | ||
*/ | ||
export function fromText(text, options) { | ||
options = Object.assign({}, options, { arrayType: 'xxyy' }); | ||
export function fromText(text, options = {}) { | ||
const data = parseXY(text, options); | ||
const time = data[0]; | ||
const time = data.x; | ||
let series = { | ||
intensity: data[1] | ||
intensity: data.y, | ||
}; | ||
@@ -21,0 +13,0 @@ |
@@ -1,4 +0,4 @@ | ||
export { Chromatogram } from './Chromatogram'; | ||
export { Chromatogram, fromJSON } from './Chromatogram'; | ||
export { massInPeaks } from './massInPeaks'; | ||
export { appendMass } from './peaks/appendMass'; | ||
export { vectorify } from './vectorify'; | ||
@@ -9,20 +9,10 @@ export { massFilter } from './massFilter'; | ||
export { kovats } from './kovats'; | ||
export { getKovatsTable } from './getKovatsTable'; | ||
export { kovatsConversionFunction } from './kovatsConversionFunction'; | ||
export { rescaleTime } from './rescaleTime'; | ||
export { getKovatsRescale } from './getKovatsRescale'; | ||
export { appendKovats } from './peaks/appendKovats'; | ||
export { getKovatsConversionFunction } from './getKovatsConversionFunction'; | ||
export { cosine } from './ms/cosine'; | ||
export { cosineSimilarity } from './ms/cosineSimilarity'; | ||
export { meanFilter } from './filter/meanFilter'; | ||
export { percentageFilter } from './filter/percentageFilter'; | ||
export { fromJcamp } from './from/jcamp'; | ||
export { fromJSON } from './from/json'; | ||
export { fromText } from './from/text'; | ||
export { fromNetCDF } from './from/netcdf'; | ||
export { fromMzML } from './from/mzml'; | ||
export { integrate } from './util/integrate'; | ||
export { merge } from './util/merge'; | ||
export { getPeaks } from './util/getPeaks'; | ||
export { fromXML } from './from/xml'; |
@@ -0,4 +1,6 @@ | ||
import max from 'ml-array-max'; | ||
/** | ||
* Calculates the Kovats retention index for a mass spectra of a n-alkane | ||
* @param {object} ms - An mass spectra object | ||
* @param {object} ms - A mass spectra object | ||
* @param {Array<number>} ms.x - Array of masses | ||
@@ -8,31 +10,38 @@ * @param {Array<number>} ms.y - Array of intensities | ||
*/ | ||
export function kovats(ms) { | ||
let mass = ms.x; | ||
let massMol = []; | ||
const targets = [43, 57, 71, 85]; | ||
for (let i = 0; i < mass.length; i++) { | ||
if ((mass[i] - 2) % 14 === 0) { | ||
massMol.push(mass[i]); | ||
export function kovats(ms, options = {}) { | ||
const { threshold = 0.01 } = options; | ||
// we normalize the data and filter them | ||
let maxY = max(ms.y); | ||
let masses = []; | ||
let intensities = []; | ||
for (let i = 0; i < ms.x.length; i++) { | ||
if (ms.y[i] / maxY > threshold) { | ||
masses.push(ms.x[i]); | ||
intensities.push(ms.y[i] / maxY); | ||
} | ||
} | ||
if (massMol.length === 0) { | ||
return 0; | ||
} | ||
let kovatsIndex = 0; | ||
for (var m = 0; m < massMol.length; m++) { | ||
let candidate = true; | ||
for (var t = 0; t < targets.length; t++) { | ||
candidate = candidate | ||
&& (mass.indexOf(targets[t]) !== -1) | ||
&& ((mass.indexOf(massMol[m] - targets[t]) !== -1) | ||
|| (mass.indexOf(massMol[m] - targets[t] + 1) !== -1) | ||
|| (mass.indexOf(massMol[m] - targets[t] - 1) !== -1)); | ||
// we find candidates | ||
let nAlcaneMasses = []; | ||
let fragmentMasses = []; | ||
for (let i = 0; i < masses.length; i++) { | ||
if ((masses[i] - 2) % 14 === 0) { | ||
nAlcaneMasses.push(masses[i]); | ||
} | ||
if (candidate) { | ||
kovatsIndex = 100 * (massMol[m] - 2) / 14; | ||
if ((masses[i] - 1) % 14 === 0) { | ||
fragmentMasses.push(masses[i]); | ||
} | ||
} | ||
return kovatsIndex; | ||
if (nAlcaneMasses.length === 0) return {}; | ||
let biggestMass = nAlcaneMasses.sort((a, b) => b - a)[0]; | ||
fragmentMasses = fragmentMasses.filter((mass) => mass < biggestMass); | ||
return { | ||
index: (100 * (biggestMass - 2)) / 14, | ||
numberFragments: fragmentMasses.length, | ||
percentFragments: fragmentMasses.length / ((biggestMass - 2) / 14), | ||
}; | ||
} |
@@ -1,14 +0,7 @@ | ||
/** | ||
* Filters a mass object | ||
* @param {object} massXYObject - Object with x and y data | ||
* @param {Array<number>} massXYObject.x - Array of mass values | ||
* @param {Array<number>} massXYObject.y - Array of abundance values | ||
* @param {object} options - Options for the integral filtering | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @return {object} - Object with filtered x and y data | ||
*/ | ||
export function massFilter(massXYObject, options = {}) { | ||
const { thresholdFactor = 0, maxNumberPeaks = Number.MAX_VALUE, groupWidth = 0 } = options; | ||
const { | ||
thresholdFactor = 0, | ||
maxNumberPeaks = Number.MAX_VALUE, | ||
groupWidth = 0, | ||
} = options; | ||
@@ -20,3 +13,3 @@ let max = -1; | ||
x: massXYObject.x[i], | ||
y: massXYObject.y[i] | ||
y: massXYObject.y[i], | ||
}; | ||
@@ -45,3 +38,3 @@ | ||
x: new Array(filteredList.length), | ||
y: new Array(filteredList.length) | ||
y: new Array(filteredList.length), | ||
}; | ||
@@ -71,6 +64,11 @@ for (let i = 0; i < filteredList.length; ++i) { | ||
for (let i = 0; (i < list.length) && (len < maxNumberPeaks); ++i) { | ||
for (let i = 0; i < list.length && len < maxNumberPeaks; ++i) { | ||
let outRange = true; | ||
for (let j = 0; j < len && outRange; ++j) { | ||
outRange = outRange && !((list[i].x > (filteredList[j].x - groupWidth)) && (list[i].x < (filteredList[j].x + groupWidth))); | ||
outRange = | ||
outRange && | ||
!( | ||
list[i].x > filteredList[j].x - groupWidth && | ||
list[i].x < filteredList[j].x + groupWidth | ||
); | ||
} | ||
@@ -77,0 +75,0 @@ if (outRange) { |
import { MF } from 'mf-parser'; | ||
/** | ||
* Recalculates series for GC/MS with lock mass | ||
* @param {string|Array<string>} mf - Reference molecular formula(s) | ||
* @param {object} [options = {}] - Options object | ||
* @param {boolean} [options.oddReference = true] - Mass reference it's in the odd position | ||
* @param {number} [options.maxShift = 0.1] - Maximum allowed shift | ||
* @param {boolean} [options.usePreviousIfNotFound = true] - If not found we use the previous value | ||
* @return {object} this | ||
*/ | ||
export function applyLockMass(mf, options = {}) { | ||
export function applyLockMass(chromatogram, mfs, options = {}) { | ||
const { oddReference = true, maxShift = 0.1 } = options; | ||
// allows mf as string or array | ||
if (typeof mf === 'string') { | ||
mf = [mf]; | ||
if (typeof mfs === 'string') { | ||
mfs = [mfs]; | ||
} | ||
// calculate the mass reference values | ||
const referenceMass = mf.map((mf) => { | ||
const referenceMass = mfs.map((mf) => { | ||
let info = new MF(mf).getInfo(); | ||
@@ -26,13 +17,9 @@ return info.observedMonoisotopicMass || info.monoisotopicMass; | ||
var ms = this.getSerie('ms'); | ||
if (!ms) { | ||
throw new Error('The "ms" serie must be defined'); | ||
} | ||
ms = ms.data; | ||
const ms = chromatogram.getSeries('ms').data; | ||
// check where is the reference values | ||
var referenceIndexShift = Number(oddReference); | ||
var msIndexShift = Number(!oddReference); | ||
let referenceIndexShift = Number(oddReference); | ||
let msIndexShift = Number(!oddReference); | ||
const newSize = ms.length >> 1; | ||
var referencesCount = new Array(referenceMass.length).fill(0); | ||
let referencesCount = new Array(referenceMass.length).fill(0); | ||
@@ -42,11 +29,11 @@ // applying the changes for all the spectra | ||
let usingPreviousValidDifference = false; | ||
for (var i = 0; i < newSize; i++) { | ||
var massIndex = 2 * i + msIndexShift; | ||
var referenceIndex = 2 * i + referenceIndexShift; | ||
for (let i = 0; i < newSize; i++) { | ||
let massIndex = 2 * i + msIndexShift; | ||
let referenceIndex = 2 * i + referenceIndexShift; | ||
// calculate the difference between theory and experimental (the smallest) | ||
var difference = Number.MAX_VALUE; | ||
var closestIndex = -1; | ||
for (var j = 0; j < referenceMass.length; j++) { | ||
for (var k = 0; k < ms[referenceIndex][0].length; k++) { | ||
let difference = Number.MAX_VALUE; | ||
let closestIndex = -1; | ||
for (let j = 0; j < referenceMass.length; j++) { | ||
for (let k = 0; k < ms[referenceIndex][0].length; k++) { | ||
if ( | ||
@@ -78,3 +65,3 @@ Math.abs(difference) > | ||
} | ||
for (var m = 0; m < ms[massIndex][0].length; m++) { | ||
for (let m = 0; m < ms[massIndex][0].length; m++) { | ||
ms[massIndex][0][m] += difference; | ||
@@ -85,8 +72,10 @@ } | ||
var referenceUsed = { | ||
const referenceUsed = { | ||
total: newSize, | ||
totalFound: referencesCount.reduce((prev, current) => current + prev, 0) | ||
totalFound: referencesCount.reduce((prev, current) => current + prev, 0), | ||
mfs: {}, | ||
percent: 0, | ||
}; | ||
for (var r = 0; r < referenceMass.length; r++) { | ||
referenceUsed[mf[r]] = referencesCount[r]; | ||
for (let r = 0; r < referenceMass.length; r++) { | ||
referenceUsed.mfs[mfs[r]] = referencesCount[r]; | ||
} | ||
@@ -97,5 +86,5 @@ referenceUsed.percent = | ||
// remove the time and the mass spectra that contains the reference | ||
this.filter((index) => index % 2 !== referenceIndexShift); | ||
chromatogram.filter((index) => index % 2 !== referenceIndexShift); | ||
return { chrom: this, referenceUsed }; | ||
return referenceUsed; | ||
} |
@@ -5,13 +5,10 @@ import max from 'ml-array-max'; | ||
* Calculate bpc | ||
* @param {Chromatogram} chrom - GC/MS chromatogram where make the peak picking | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @return {Array} - Calculated bpc | ||
*/ | ||
export function calculateBpc(chrom) { | ||
let ms = chrom.getSerie('ms'); | ||
if (!ms) { | ||
throw new Error('The mass serie must be defined'); | ||
} | ||
var massSpectra = ms.data; | ||
var bpc = []; | ||
for (var massSpectrum of massSpectra) { | ||
export function calculateBpc(chromatogram) { | ||
const ms = chromatogram.getSeries('ms'); | ||
const massSpectra = ms.data; | ||
const bpc = []; | ||
for (const massSpectrum of massSpectra) { | ||
if (massSpectrum[1].length > 0) { | ||
@@ -18,0 +15,0 @@ bpc.push(max(massSpectrum[1])); |
import IsotopicDistribution from 'isotopic-distribution'; | ||
import { XYObject } from 'ml-spectra-processing'; | ||
/** | ||
* Calculate tic | ||
* Calculate tic for specific molecular formula and ionizations | ||
* | ||
* The system will take all the peaks with an intensity over 5% (default value) | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {string} targetMF - mass for which to extract the spectrum | ||
* @param {object} [options={}] | ||
* @param {number} [options.error=0.5] - Allowed error around the targetMF | ||
* @param {number} [options.slotWidth=1] - Width of the slot around the mass of targetMF | ||
* @param {number} [options.threshold=0.05] - Minimal height for peaks | ||
* @param {number} [options.ionizations='H+'] - List of allowed ionisation | ||
@@ -14,19 +17,26 @@ * @return {Array} - Calculated mass for targetMass | ||
if (typeof targetMF !== 'string') { | ||
throw Error('calculateForMF: targetMF must be defined and a string'); | ||
throw Error('targetMF must be defined and a string'); | ||
} | ||
const { error = 0.5 } = options; | ||
const { threshold = 0.05, slotWidth = 1, ionizations = 'H+' } = options; | ||
let ms = chromatogram.getSerie('ms'); | ||
if (!ms) { | ||
throw Error('calculateForMF: the mass serie must be defined'); | ||
} | ||
const halfWidth = slotWidth / 2; | ||
var masses = new IsotopicDistribution(targetMF, { | ||
ionizations: options.ionizations.replace(/ /g, '') | ||
}) | ||
.getParts() | ||
.map((entry) => entry.ms.em); | ||
const ms = chromatogram.getSeries('ms'); | ||
var massSpectra = ms.data; | ||
var result = new Array(massSpectra.length).fill(0); | ||
let isotopicDistribution = new IsotopicDistribution(targetMF, { | ||
ionizations, | ||
}); | ||
// we add isotopicDistribution in all the parts | ||
isotopicDistribution.getDistribution(); | ||
let parts = isotopicDistribution.getParts(); | ||
let masses = [].concat(...parts.map((part) => part.isotopicDistribution)); | ||
masses.sort((a, b) => a.x - b.x); | ||
masses = XYObject.slotX(masses, { slotWidth }).filter( | ||
(mass) => mass.y > threshold, | ||
); | ||
let massSpectra = ms.data; | ||
let result = new Array(massSpectra.length).fill(0); | ||
for (let targetMass of masses) { | ||
@@ -36,3 +46,3 @@ for (let i = 0; i < massSpectra.length; i++) { | ||
for (let j = 0; j < massSpectrum[0].length; j++) { | ||
if (Math.abs(massSpectrum[0][j] - targetMass) <= error) { | ||
if (Math.abs(massSpectrum[0][j] - targetMass.x) <= halfWidth) { | ||
result[i] += massSpectrum[1][j]; | ||
@@ -39,0 +49,0 @@ } |
@@ -1,12 +0,6 @@ | ||
/** | ||
* Calculate the number of points of each related information | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {string} serieName - name of the serie | ||
* @return {Array} - Calculated length of the 2D array | ||
*/ | ||
export function calculateLength(chromatogram, serieName) { | ||
let serie2D = chromatogram.getSerie(serieName); | ||
var spectra = serie2D.data; | ||
var length = spectra.map((spectrum) => spectrum[0].length); | ||
export function calculateLength(chromatogram, seriesName) { | ||
const series2D = chromatogram.getSeries(seriesName); | ||
const spectra = series2D.data; | ||
const length = spectra.map((spectrum) => spectrum[0].length); | ||
return length; | ||
} |
@@ -1,18 +0,16 @@ | ||
/** | ||
* Calculate tic | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @return {Array} - Calculated tic | ||
*/ | ||
import sum from 'ml-array-sum'; | ||
export function calculateTic(chromatogram) { | ||
let ms = chromatogram.getSerie('ms'); | ||
if (!ms) { | ||
throw new Error('The mass serie must be defined'); | ||
const ms = chromatogram.getSeries('ms'); | ||
const massSpectra = ms.data; | ||
const tic = []; | ||
for (const massSpectrum of massSpectra) { | ||
if (massSpectrum[1].length > 0) { | ||
tic.push(sum(massSpectrum[1])); | ||
} else { | ||
tic.push(0); | ||
} | ||
} | ||
var massSpectra = ms.data; | ||
var tic = []; | ||
for (var massSpectrum of massSpectra) { | ||
tic.push(massSpectrum[1].reduce((a, b) => a + b, 0)); | ||
} | ||
return tic; | ||
} |
import Regression from 'ml-regression-polynomial'; | ||
/** | ||
* Aligns the time of the sample based on the regression with his reference value | ||
* @param {Array<object>} reference - Array of peaks, integrated mass spectra and weighted mass spectra for the reference chromatogram | ||
* @param {Array<object>} sample - Array of peaks, integrated mass spectra and weighted mass spectra for the sample chromatogram | ||
* @param {object} [options] - Options object | ||
* @param {boolean} [options.computeQuality = false] - Calculate the quality of the regression | ||
* @param {number} [options.polynomialDegree = 3] - Degree of the polynomial regression | ||
* @return {object} - The scaled spectra: | ||
* * `scaleRegression`: The regression function to make the regression | ||
* * `stringFormula`: Regression equation | ||
* * `r2`: R2 quality number | ||
* * `error`: Vector of the difference between the spected value and the actual shift value | ||
*/ | ||
export function scaleAlignment(reference, sample, options = {}) { | ||
const { | ||
computeQuality = false, | ||
polynomialDegree = 3 | ||
} = options; | ||
const { computeQuality = false, polynomialDegree = 3 } = options; | ||
let referenceTime = reference.map((val) => val.x); | ||
let sampleTime = sample.map((val) => val.x); | ||
const regression = new Regression(sampleTime, referenceTime, polynomialDegree); | ||
const regression = new Regression( | ||
sampleTime, | ||
referenceTime, | ||
polynomialDegree, | ||
); | ||
let error = new Array(sample.length); | ||
for (var i = 0; i < sample.length; i++) { | ||
for (let i = 0; i < sample.length; i++) { | ||
error[i] = reference[i].x - regression.predict(sample[i].x); | ||
@@ -32,3 +20,3 @@ } | ||
let ans = { | ||
scaleRegression: regression | ||
scaleRegression: regression, | ||
}; | ||
@@ -35,0 +23,0 @@ |
@@ -1,5 +0,5 @@ | ||
import { getPeaks } from './util/getPeaks'; | ||
import { massInPeaks } from './massInPeaks'; | ||
import { getPeaks } from './peaks/getPeaks'; | ||
import { appendMass } from './peaks/appendMass'; | ||
import { vectorify } from './vectorify'; | ||
import { cosine } from './ms/cosine'; | ||
import { cosineSimilarity } from './ms/cosineSimilarity'; | ||
@@ -9,14 +9,13 @@ /** | ||
* @ignore | ||
* @param {Chromatogram} chromatography - Chromatogram to process | ||
* @param {Chromatogram} chromatogram - Chromatogram to process | ||
* @param {object} [options] - Options object (same as spectraComparison) | ||
* @return {{peaks: Array<object>, integratedMs: Array<object>, vector: Array<object>}} - Array of peaks, integrated mass spectra and weighted mass spectra | ||
*/ | ||
function preprocessing(chromatography, options) { | ||
function preprocessing(chromatogram, options) { | ||
// peak picking | ||
let peaks = getPeaks(chromatography, options); | ||
let peaks = getPeaks(chromatogram, options); | ||
peaks = peaks.sort((a, b) => a.index - b.index); | ||
// integrate mass in the peaks | ||
let ms = chromatography.getSerie('ms').data; | ||
let integratedMs = massInPeaks(peaks, ms, options); | ||
let integratedMs = appendMass(chromatogram, peaks, options); | ||
let vector = vectorify(integratedMs, options); | ||
@@ -27,7 +26,7 @@ | ||
integratedMs, | ||
vector | ||
vector, | ||
}; | ||
} | ||
const defaultOption = { | ||
const defaultOptions = { | ||
thresholdFactor: 0, | ||
@@ -39,24 +38,7 @@ maxNumberPeaks: Number.MAX_VALUE, | ||
intPower: 0.6, | ||
similarityThreshold: 0.7 | ||
similarityThreshold: 0.7, | ||
}; | ||
/** | ||
* Returns the most similar peaks between two GC/MS and their similarities | ||
* @param {Chromatogram} chrom1 - First chromatogram | ||
* @param {Chromatogram} chrom2 - Second chromatogram | ||
* @param {object} [options] - Options object | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @param {number} [options.heightFilter = 2] - Filter all objects that are bellow `heightFilter` times the median of the height | ||
* @param {number} [options.massPower = 3] - Power applied to the mass values | ||
* @param {number} [options.intPower = 0.6] - Power applied to the abundance values | ||
* @param {number} [options.similarityThreshold = 0.7] - Minimum similarity value to consider them similar | ||
* @return {object} - Most similar peaks and their similarities: | ||
* * `peaksFirst`: Array of peaks, integrated mass spectra and weighted mass spectra for the first chromatogram | ||
* * `peaksSecond`: Array of peaks, integrated mass spectra and weighted mass spectra for the second chromatogram | ||
* * `peaksSimilarity`: Array of similarities (number) | ||
*/ | ||
export function spectraComparison(chrom1, chrom2, options) { | ||
options = Object.assign({}, defaultOption, options); | ||
export function spectraComparison(chrom1, chrom2, options = {}) { | ||
options = Object.assign({}, defaultOptions, options); | ||
@@ -72,3 +54,3 @@ // peak picking | ||
chrom2: new Array(len), | ||
similarity: new Array(len) | ||
similarity: new Array(len), | ||
}; | ||
@@ -82,3 +64,8 @@ let similarLen = 0; | ||
for (let j = 0; j < reference.peaks.length; ++j) { | ||
let sim = cosine(sample.vector[i].x, sample.vector[i].y, reference.vector[j].x, reference.vector[j].y); | ||
let sim = cosineSimilarity( | ||
sample.vector[i].x, | ||
sample.vector[i].y, | ||
reference.vector[j].x, | ||
reference.vector[j].y, | ||
); | ||
@@ -89,3 +76,3 @@ if (sim > options.similarityThreshold && sim > max.similarity) { | ||
chrom1: reference.peaks[j], | ||
chrom2: sample.peaks[i] | ||
chrom2: sample.peaks[i], | ||
}; | ||
@@ -108,3 +95,3 @@ } | ||
for (let i = 0; i < similarLen; ++i) { | ||
if ({}.hasOwnProperty.call(duplicates, similarityPeaks.chrom1[i].x)) { | ||
if (duplicates[similarityPeaks.chrom1[i].x]) { | ||
duplicates[similarityPeaks.chrom1[i].x].push(i); | ||
@@ -111,0 +98,0 @@ } else { |
@@ -1,34 +0,27 @@ | ||
export function baselineCorrection(total, base, kind) { | ||
if (total === 0) { | ||
return { | ||
integral: 0, | ||
base: { | ||
start: { height: 0 }, | ||
end: { height: 0 } | ||
} | ||
}; | ||
} | ||
export function baselineCorrection(points, fromTo, kind) { | ||
const deltaTime = points.x[fromTo.toIndex] - points.x[fromTo.fromIndex]; | ||
const fromHeight = points.y[fromTo.fromIndex]; | ||
const toHeight = points.y[fromTo.toIndex]; | ||
let baseline = 0; | ||
let from = 0; | ||
let to = 0; | ||
switch (kind) { | ||
case 'trapezoid': | ||
return { | ||
integral: total - ((base.end.time - base.start.time) * (base.end.height + base.start.height) / 2), | ||
base | ||
}; | ||
baseline = (deltaTime * (fromHeight + toHeight)) / 2; | ||
from = fromHeight; | ||
to = toHeight; | ||
break; | ||
case 'min': | ||
if (base.end.height > base.start.height) { | ||
base.end.height = base.start.height; | ||
return { | ||
integral: total - ((base.end.time - base.start.time) * base.start.height), | ||
base | ||
}; | ||
} else { | ||
base.start.height = base.end.height; | ||
return { | ||
integral: total - ((base.end.time - base.start.time) * base.end.height), | ||
base | ||
}; | ||
} | ||
from = Math.min(fromHeight, toHeight); | ||
to = from; | ||
baseline = deltaTime * from; | ||
break; | ||
default: | ||
throw new Error(`Unknown baseline method "${kind}"`); | ||
} | ||
return { | ||
value: baseline, | ||
from, | ||
to, | ||
}; | ||
} |
@@ -1,20 +0,8 @@ | ||
/** | ||
* Filter the chromatogram based on a callback | ||
* The callback will take a time | ||
* @param {Chromatogram} chromatogram - GC/MS chromatogram where make the peak picking | ||
* @param {function(number, number)} callback | ||
* @param {object} [options] - options object | ||
* @param {boolean} [options.copy = false] - return a copy of the original object | ||
* @return {Chromatogram} - Modified chromatogram | ||
*/ | ||
export function filter(chromatogram, callback, options = {}) { | ||
const { copy = false } = options; | ||
let chrom; | ||
if (copy) { | ||
chrom = chromatogram.copy(); | ||
} else { | ||
chrom = chromatogram; | ||
chromatogram = chromatogram.copy(); | ||
} | ||
let times = chrom.getTimes(); | ||
let times = chromatogram.getTimes(); | ||
let newTimes = []; | ||
@@ -28,10 +16,10 @@ let indexToKeep = []; | ||
} | ||
chrom.setTimes(newTimes); | ||
chromatogram.times = newTimes; | ||
for (let key of chrom.getSerieNames()) { | ||
let serie = chrom.getSerie(key); | ||
serie.keep(indexToKeep); | ||
for (let key of chromatogram.getSeriesNames()) { | ||
const series = chromatogram.getSeries(key); | ||
series.keep(indexToKeep); | ||
} | ||
return chrom; | ||
return chromatogram; | ||
} |
@@ -1,15 +0,10 @@ | ||
/** | ||
* Returns the closest mass spectrum to a specific retention time | ||
* @param {string} name - Serie name | ||
* @param {number} rt - Retention time | ||
* @return {{rt: number, index: number, data: Array}} | ||
*/ | ||
export function getClosestData(name, rt) { | ||
this.requiresSerie(name); | ||
let closest = this.getClosestTime(rt); | ||
export function getClosestData(chromatogram, time, options = {}) { | ||
const { seriesName = 'ms' } = options; | ||
chromatogram.requiresSeries(seriesName); | ||
let closest = chromatogram.getClosestTime(time); | ||
return { | ||
rt: closest.timeClosest, | ||
index: closest.index, | ||
data: this.getSerie(name).data[closest.index] | ||
rt: chromatogram.getTimes()[closest], | ||
index: closest, | ||
data: chromatogram.getSeries(seriesName).data[closest], | ||
}; | ||
} |
@@ -1,45 +0,32 @@ | ||
import { getClosestTime } from './getClosestTime'; | ||
import { X, XY } from 'ml-spectra-processing'; | ||
import { baselineCorrection } from './baselineCorrection'; | ||
/** | ||
* Returns a mass spectrum that is the integration of all the spectra in a specific range of time | ||
* @param {Chromatogram} chromatogram | ||
* @param {string} name - Name of the serie to integrate | ||
* @param {Array<Array<number>>} ranges - [[from1, to1], [from2, to2], ...] | ||
* @param {object} [options = {}] - Options object | ||
* @param {string|boolean} [options.baseline] - Applies baseline correction (trapezoid, min) | ||
* @return {[]} | ||
*/ | ||
export function integrate(chromatogram, name, ranges, options = {}) { | ||
const { | ||
baseline = false | ||
} = options; | ||
export function integrate(chromatogram, ranges, options = {}) { | ||
const { baseline, seriesName = 'tic' } = options; | ||
if (!Array.isArray(ranges)) { | ||
throw new Error('ranges must be an array of type [[from,to]]'); | ||
throw new Error('Ranges must be an array of type [{from,to}]'); | ||
} | ||
if (ranges.length === 0) { | ||
return undefined; | ||
return []; | ||
} | ||
if (!Array.isArray(ranges[0]) || ranges[0].length !== 2) { | ||
throw new Error('ranges must be an array of type [[from,to]]'); | ||
chromatogram.requiresSeries(seriesName); | ||
let series = chromatogram.series[seriesName]; | ||
if (series.dimension !== 1) { | ||
throw new Error(`The series "${seriesName}" is not of dimension 1`); | ||
} | ||
chromatogram.requiresSerie(name); | ||
let serie = chromatogram.series[name]; | ||
if (serie.dimension !== 1) { | ||
throw new Error('the serie is not of dimension 1'); | ||
} | ||
const time = chromatogram.getTimes(); | ||
let results = []; | ||
for (let fromTo of ranges) { | ||
let from = fromTo[0]; | ||
let to = fromTo[1]; | ||
let fromIndex = getClosestTime(from, time).safeIndexBefore; | ||
let toIndex = getClosestTime(to, time).safeIndexAfter; | ||
results.push(_integrate(time, serie, from, to, fromIndex, toIndex, baseline)); | ||
for (let range of ranges) { | ||
const fromTo = X.getFromToIndex(time, range); | ||
const integral = integrateRange( | ||
{ x: time, y: series.data }, | ||
fromTo, | ||
baseline, | ||
); | ||
results.push(integral); | ||
} | ||
@@ -50,54 +37,35 @@ | ||
function _integrate(time, serie, from, to, fromIndex, toIndex, baseline) { | ||
let total = 0; | ||
let base = {}; | ||
for (let i = fromIndex; i < toIndex; i++) { | ||
let timeStart = time[i]; | ||
let timeEnd = time[i + 1]; | ||
let heightStart = serie.data[i]; | ||
if (i === fromIndex) { // need to check the exact starting point | ||
heightStart = serie.data[i] + (serie.data[i + 1] - serie.data[i]) * (from - timeStart) / (timeEnd - timeStart); | ||
base.start = { height: heightStart, time: from }; | ||
timeStart = from; | ||
} | ||
function integrateRange(points, fromTo, baseline) { | ||
let integration = XY.integration(points, fromTo); | ||
let heightEnd = serie.data[i + 1]; | ||
if (i === toIndex - 1) { | ||
heightEnd = serie.data[i] + (serie.data[i + 1] - serie.data[i]) * (to - timeStart) / (timeEnd - timeStart); | ||
base.end = { height: heightEnd, time: to }; | ||
timeEnd = to; | ||
} | ||
total += (timeEnd - timeStart) * (heightStart + heightEnd) / 2; | ||
} | ||
if (baseline) { | ||
var ans = baselineCorrection(total, base, baseline); | ||
let correction = baselineCorrection(points, fromTo, baseline); | ||
return { | ||
integral: ans.integral, | ||
integration: integration - correction.value, | ||
from: { | ||
time: from, | ||
index: fromIndex, | ||
baseline: ans.base.start.height | ||
time: points.x[fromTo.fromIndex], | ||
index: fromTo.fromIndex, | ||
baseline: correction.from, | ||
}, | ||
to: { | ||
time: to, | ||
index: toIndex, | ||
baseline: ans.base.end.height | ||
} | ||
time: points.x[fromTo.toIndex], | ||
index: fromTo.toIndex, | ||
baseline: correction.to, | ||
}, | ||
}; | ||
} else { | ||
return { | ||
integral: total, | ||
integration, | ||
from: { | ||
time: from, | ||
index: fromIndex, | ||
baseline: 0 | ||
time: points.x[fromTo.fromIndex], | ||
index: fromTo.fromIndex, | ||
baseline: 0, | ||
}, | ||
to: { | ||
time: to, | ||
index: toIndex, | ||
baseline: 0 | ||
} | ||
time: points.x[fromTo.toIndex], | ||
index: fromTo.toIndex, | ||
baseline: 0, | ||
}, | ||
}; | ||
} | ||
} |
import { massFilter } from './massFilter'; | ||
/** | ||
* Given a list of GSD objects returns the weighted mass times abundance | ||
* @param {Array<object>} peakList - List of GSD objects | ||
* @param {object} options - Options for the integral filtering | ||
* @param {number} [options.massPower = 3] - Power applied to the mass values | ||
* @param {number} [options.intPower = 0.6] - Power applied to the abundance values | ||
* @param {number} [options.thresholdFactor = 0] - Every peak that it's bellow the main peak times this factor fill be removed (when is 0 there's no filter) | ||
* @param {number} [options.maxNumberPeaks = Number.MAX_VALUE] - Maximum number of peaks for each mass spectra (when is Number.MAX_VALUE there's no filter) | ||
* @param {number} [options.groupWidth = 0] - When find a max can't be another max in a radius of this size | ||
* @return {Array<object>} - List of mass and weighted mass times abundance objects | ||
*/ | ||
export function vectorify(peakList, options = {}) { | ||
export function vectorify(ranges, options = {}) { | ||
const { massPower = 3, intPower = 0.6 } = options; | ||
let filter = (options.thresholdFactor || options.maxNumberPeaks || options.groupWidth); | ||
let filter = | ||
options.thresholdFactor || options.maxNumberPeaks || options.groupWidth; | ||
let vector = new Array(peakList.length); | ||
let vector = new Array(ranges.length); | ||
if (filter) { | ||
@@ -23,13 +13,15 @@ const filterOptions = { | ||
maxNumberPeaks: options.maxNumberPeaks, | ||
groupWidth: options.groupWidth | ||
groupWidth: options.groupWidth, | ||
}; | ||
for (let i = 0; i < peakList.length; ++i) { | ||
let len = peakList[i].ms.x.length; | ||
for (let i = 0; i < ranges.length; ++i) { | ||
let len = ranges[i].ms.x.length; | ||
vector[i] = { | ||
x: peakList[i].ms.x, | ||
y: new Array(len) | ||
x: ranges[i].ms.x, | ||
y: new Array(len), | ||
}; | ||
for (let j = 0; j < len; ++j) { | ||
vector[i].y[j] = Math.pow(peakList[i].ms.x[j], massPower) * Math.pow(peakList[i].ms.y[j], intPower); | ||
vector[i].y[j] = | ||
Math.pow(ranges[i].ms.x[j], massPower) * | ||
Math.pow(ranges[i].ms.y[j], intPower); | ||
} | ||
@@ -40,10 +32,12 @@ | ||
} else { | ||
for (let i = 0; i < peakList.length; ++i) { | ||
let len = peakList[i].ms.x.length; | ||
for (let i = 0; i < ranges.length; ++i) { | ||
let len = ranges[i].ms.x.length; | ||
vector[i] = { | ||
x: peakList[i].ms.x, | ||
y: new Array(len) | ||
x: ranges[i].ms.x, | ||
y: new Array(len), | ||
}; | ||
for (let j = 0; j < len; ++j) { | ||
vector[i].y[j] = Math.pow(peakList[i].ms.x[j], massPower) * Math.pow(peakList[i].ms.y[j], intPower); | ||
vector[i].y[j] = | ||
Math.pow(ranges[i].ms.x[j], massPower) * | ||
Math.pow(ranges[i].ms.y[j], intPower); | ||
} | ||
@@ -50,0 +44,0 @@ } |
Sorry, the diff of this file is not supported yet
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
15
57
93137
15
39
2941
1
4
+ Addedis-any-array@0.0.3
+ Addedml-array-median@^1.1.2
+ Addedml-array-sum@^1.1.2
+ Addedml-spectra-processing@^2.0.0
+ Added@types/estree@1.0.6(transitive)
+ Added@types/node@22.10.3(transitive)
+ Addedacorn@7.4.1(transitive)
+ Addedassign-deep@1.0.1(transitive)
+ Addedassign-symbols@2.0.2(transitive)
+ Addedatom-sorter@0.12.0(transitive)
+ Addedchemical-elements@0.12.10.13.0(transitive)
+ Addedchemical-groups@0.12.10.13.0(transitive)
+ Addedcheminfo-types@0.5.0(transitive)
+ Addedd3-array@0.7.1(transitive)
+ Addedd3-random@2.2.2(transitive)
+ Addedensure-string@0.1.1(transitive)
+ Addedfft.js@4.0.4(transitive)
+ Addedis-any-array@0.0.30.1.01.0.1(transitive)
+ Addedisotopic-distribution@0.13.0(transitive)
+ Addedjcampconverter@4.1.1(transitive)
+ Addedmedian-quickselect@1.0.1(transitive)
+ Addedmf-parser@0.12.00.14.0(transitive)
+ Addedmf-utilities@0.13.0(transitive)
+ Addedml-array-median@1.1.6(transitive)
+ Addedml-array-normed@1.3.7(transitive)
+ Addedml-array-sequential-fill@1.1.8(transitive)
+ Addedml-array-standard-deviation@1.1.8(transitive)
+ Addedml-array-variance@1.1.8(transitive)
+ Addedml-arrayxy-uniquex@1.0.2(transitive)
+ Addedml-gsd@6.9.2(transitive)
+ Addedml-levenberg-marquardt@3.1.1(transitive)
+ Addedml-matrix@6.12.0(transitive)
+ Addedml-peak-shape-generator@0.4.01.0.02.0.2(transitive)
+ Addedml-regression-base@2.1.6(transitive)
+ Addedml-regression-polynomial@2.2.0(transitive)
+ Addedml-savitzky-golay-generalized@2.0.3(transitive)
+ Addedml-spectra-fitting@1.0.0(transitive)
+ Addedml-spectra-processing@2.4.06.8.0(transitive)
+ Addedml-xsadd@2.0.0(transitive)
+ Addedpapaparse@5.4.1(transitive)
+ Addedrollup@1.32.1(transitive)
+ Addedspectrum-generator@3.2.2(transitive)
+ Addedspline-interpolator@1.0.0(transitive)
+ Addedundici-types@6.20.0(transitive)
+ Addedxy-parser@3.2.0(transitive)
- Removedmzmjs@^0.2.0
- Removednum-sort@^1.0.0
- Removedatom-sorter@0.9.1(transitive)
- Removedchemical-elements@0.9.1(transitive)
- Removedchemical-groups@0.9.1(transitive)
- Removedisotopic-distribution@0.9.1(transitive)
- Removedjcampconverter@3.0.4(transitive)
- Removedmf-parser@0.9.1(transitive)
- Removedmf-utilities@0.9.1(transitive)
- Removedml-arrayxy-uniquex@0.0.2(transitive)
- Removedml-matrix@5.3.0(transitive)
- Removedml-regression-base@1.2.1(transitive)
- Removedml-regression-polynomial@1.0.3(transitive)
- Removedmzmjs@0.2.0(transitive)
- Removednum-sort@1.0.0(transitive)
- Removednumber-is-nan@1.0.1(transitive)
- Removedpapaparse@4.6.3(transitive)
- Removedsax@1.4.1(transitive)
- Removedspectrum-generator@1.1.0(transitive)
- Removedxy-parser@2.2.2(transitive)
Updatedbinary-search@^1.3.6
Updatedjcampconverter@^4.1.0
Updatedmf-parser@^0.14.0
Updatedml-array-max@^1.1.2
Updatedml-array-mean@^1.1.2
Updatedml-gsd@^2.0.7
Updatedxy-parser@^3.0.0