Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More

chromatography

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

chromatography - npm Package Compare versions

Comparing version 3.11.0 to 4.0.0-0

@@ -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