Skip to content

Commit

Permalink
Merge pull request #33 from cheminfo-js/massIntegration
Browse files Browse the repository at this point in the history
Mass integration

closes #23
  • Loading branch information
maasencioh authored Jul 5, 2017
2 parents d09ec09 + 4932752 commit 8f77d96
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 106 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"ml-regression-polynomial": "^1.0.2",
"mzmjs": "^0.2.0",
"netcdf-gcms": "^1.0.1",
"num-sort": "^1.0.0",
"xy-parser": "^1.5.0"
}
}
6 changes: 6 additions & 0 deletions src/__tests__/serieFromArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {serieFromArray} from '../serieFromArray';

test('Errors', () => {
expect(() => serieFromArray(1)).toThrow('Serie.fromArray requires as parameter an array of numbers or array');
expect(() => serieFromArray(['a'])).toThrow('Serie.fromArray requires as parameter an array of numbers or array');
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import {integrate, Chromatogram} from '../..';
import {integrate1D} from '../../util/integrate1D';
import {chromato} from '../data/examples';

var chrom = new Chromatogram([1, 2, 3, 4], {tic: [2, 4, 6, 8]});

test('Integrate a tic', () => {

/*
time: [1, 2, 3, 5, 6],
tic: [10, 20, 30, 40, 50]
*/

// var result = integrate(chromato, [1.5, 5.5] );
// expect(result).toEqual( {"tic": [125] } );

var result = integrate(chromato, [1.8, 5.5]);
expect(result).toEqual({tic: [{
integral: 120.05,
Expand All @@ -21,27 +13,12 @@ test('Integrate a tic', () => {
end: {height: 0, time: 5.5}
}
}]});

//
// var result = integrate(chromato, [2, 5] );
// expect(result).toEqual( {"tic": [95] } );
//
// var result = integrate(chromato, [3, 3] );
// expect(result).toEqual( {"tic": [0] } );
//
// var result = integrate(chromato, [2.5, 2.5] );
// expect(result).toEqual( {"tic": [0] } );
//
// var result = integrate(chromato, [ [2.5, 2.5], [3.5, 3.5] ] );
// expect(result).toEqual( {"tic": [0,0] } );
//
// chromato.addSerie('tac', [100,200,300,400,500]);
// var result = integrate(chromato, [ [2, 3], [3, 5] ] );
// expect(result).toEqual( {"tic": [25,70], "tac": [250,700] } );
});

test('Errors', () => {
expect(() => integrate(chromato, 123)).toThrow('fromTo must be an array of type [from,to]');
expect(() => integrate1D([0], [])).toThrow('The serie is not of dimension 1');
expect(integrate1D([0], {dimension: 1})).toBe(0);
});

describe('Applies baseline correction', () => {
Expand Down
39 changes: 39 additions & 0 deletions src/__tests__/util/integrate2D.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {integrate, Chromatogram} from '../..';
import {integrate2D} from '../../util/integrate2D';
import {simple} from '../data/examples';

const highResolution = new Chromatogram(
[1, 2], {
ms: [
[[100.002, 200.02, 300.0002], [10, 20, 30]],
[[100.001, 200.01, 300.0001], [11, 21, 31]],
]
}
);

test('Low resolution', () => {
var result = integrate(simple, [1, 2]);
expect(result).toEqual({ms: [
[100, 101, 200, 201, 300, 301],
[10, 11, 20, 21, 30, 31]
]});
});

test('High resolution', () => {
expect(integrate(highResolution, [1, 2])).toEqual({ms: [
[100, 200, 300],
[21, 41, 61]
]});

expect(integrate(highResolution, [1, 2], {
slot: 0.01
})).toEqual({ms: [
[100.00, 200.01, 200.02, 300.00],
[21, 21, 20, 61]
]});
});

test('Errors', () => {
expect(() => integrate2D([])).toThrow('The serie is not of dimension 2');
expect(integrate2D({dimension: 2})).toEqual([]);
});
58 changes: 29 additions & 29 deletions src/massFilter.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,3 @@
/**
* 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
*/
export 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;
}

/**
* Filters a mass object
* @param {object} massXYObject - Object with x and y data
Expand Down Expand Up @@ -79,3 +50,32 @@ export function massFilter(massXYObject, options = {}) {

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
*/
export 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;
}
34 changes: 6 additions & 28 deletions src/massInPeaks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {massFilter} from './massFilter';
import {integrate2D} from './util/integrate2D';

/**
* Integrate MS spectra of a peak list
Expand All @@ -13,36 +14,13 @@ import {massFilter} from './massFilter';
export function massInPeaks(peakList, sampleMS, options = {}) {
// integrate MS
for (let i = 0; i < peakList.length; ++i) {
let massDictionary = {};
let max = -1;
for (let j = peakList[i].left.index; j <= peakList[i].right.index; ++j) {
for (let k = 0; k < sampleMS[j][0].length; ++k) {
// round the mass value
let mass = Math.round(sampleMS[j][0][k]);

// add the mass value to the dictionary
if (massDictionary[mass]) {
massDictionary[mass] += sampleMS[j][1][k];
} else {
massDictionary[mass] = sampleMS[j][1][k];
}

if (massDictionary[mass] > max) {
max = massDictionary[mass];
}
}
}
const massList = Object.keys(massDictionary);
let msSum = {
x: new Array(massList.length),
y: new Array(massList.length)
var serie = {dimension: 2, data: sampleMS};
var integral = integrate2D(serie, peakList[i].left.index, peakList[i].right.index, 1);
var msSum = {
x: integral[0],
y: integral[1]
};

for (let j = 0; j < massList.length; ++j) {
msSum.x[j] = Number(massList[j]);
msSum.y[j] = massDictionary[massList[j]];
}

if (options.maxNumberPeaks || options.thresholdFactor || options.groupWidth) {
msSum = massFilter(msSum, options);
}
Expand Down
2 changes: 0 additions & 2 deletions src/serieFromArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export function serieFromArray(array) {
if (!Array.isArray(array)) {
throw new TypeError('Serie.fromArray requires as parameter an array of numbers or array');
}
// TODO we could really check if all the elements are either number of array and
// at one specific level of the same kind !

if (array.length === 0 || typeof array[0] === 'number') {
return new Serie1D(array);
Expand Down
4 changes: 2 additions & 2 deletions src/util/integrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {integrate2D} from './integrate2D';
* @param {Chromatogram} chromatogram
* @param {number|Array<number>} ranges - [from, to] or [ [from1, to1], [from2, to2], ...]
* @param {object} [options = {}] - Options object
* @param {number} [options.slot = 2] - Define when 2 peaks will be combined
* @param {number} [options.slot = 1]
* @param {string} [options.name] - Name of the serie to integrate, by default all the series are integrated
* @param {string|boolean} [options.baseline] - Applies baseline correction
* @return {{serieName: []}}
Expand Down Expand Up @@ -52,7 +52,7 @@ export function integrate(chromatogram, ranges, options = {}) {
results[serieName].push(integrate1D(time, serie, from, to, fromIndex, toIndex, baseline));
break;
case 2:
results[serieName].push(integrate2D(time, serie, from, to, fromIndex, toIndex, {slot}));
results[serieName] = integrate2D(serie, fromIndex, toIndex, slot);
break;
default:
throw new Error('Serie dimension unrecognized');
Expand Down
2 changes: 1 addition & 1 deletion src/util/integrate1D.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {baselineCorrection} from './baselineCorrection';

export function integrate1D(time, serie, from, to, fromIndex, toIndex, baseline) {
if (serie.dimension !== 1) throw new Error('The serie name is not of dimension 1');
if (serie.dimension !== 1) throw new Error('The serie is not of dimension 1');
if (!serie.data) return 0;

let total = 0;
Expand Down
45 changes: 27 additions & 18 deletions src/util/integrate2D.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
export function integrate2D(time, serie, from, to, fromIndex, toIndex) {
if (serie.dimension !== 1) throw new Error('The serie is not of dimension 2');
import {asc} from 'num-sort';

export function integrate2D(serie, fromIndex, toIndex, slot) {
if (serie.dimension !== 2) throw new Error('The serie is not of dimension 2');
if (!serie.data) return [];

let data = [];
for (let i = fromIndex; i <= toIndex; i++) {
let massDictionary = {};

// TODO
/*
for (var j = 0; j < ms[i][0].length; j++) {
// search possible position
var position = {
index: -1,
distance: Number.MAX_VALUE
};
for (var k = 0; k < masses.length; k++) {
// TODO work in progress
}
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;

// check if merge needed
// 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(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 data;
return integral;
}
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4613,6 +4613,12 @@ npmlog@^4.0.2:
gauge "~2.7.3"
set-blocking "~2.0.0"

num-sort@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/num-sort/-/num-sort-1.0.0.tgz#cabec1fd5f4da4aca995af90b7a0f379944e1dbd"
dependencies:
number-is-nan "^1.0.0"

number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
Expand Down Expand Up @@ -5477,6 +5483,10 @@ rollup@^0.42.0:
dependencies:
source-map-support "^0.4.0"

round-to@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/round-to/-/round-to-2.0.0.tgz#bcef4f2bcafd9480902c2142150b28c897f03e37"

run-async@^2.0.0, run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
Expand Down

0 comments on commit 8f77d96

Please sign in to comment.