isotopic-distribution
Advanced tools
Comparing version 3.0.1 to 3.1.0
210
lib/index.js
@@ -114,14 +114,14 @@ 'use strict'; | ||
function power(a, p, options = {}) { | ||
function power(array, p, options = {}) { | ||
if (p <= 0) throw new Error('power must be larger than 0'); | ||
if (p === 1) return a; | ||
if (p === 1) return array; | ||
if (p === 2) { | ||
return a.square(); | ||
return array.square(); | ||
} | ||
p--; | ||
let base = a.copy(); // linear time | ||
let base = array.copy(); // linear time | ||
while (p !== 0) { | ||
if ((p & 1) !== 0) { | ||
a.multiply(base, options); // executed <= log2(p) times | ||
array.multiply(base, options); // executed <= log2(p) times | ||
} | ||
@@ -132,3 +132,3 @@ p >>= 1; | ||
return a; | ||
return array; | ||
} | ||
@@ -140,14 +140,12 @@ | ||
class Distribution { | ||
constructor(array) { | ||
if (Array.isArray(array)) { | ||
this.array = array; | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
} else { | ||
this.array = []; | ||
this.xSorted = true; | ||
this.ySorted = true; | ||
} | ||
constructor(array = []) { | ||
this.array = array; | ||
this.cache = getEmptyCache(); | ||
} | ||
emptyCache() { | ||
if (this.cache.isEmpty) return; | ||
this.cache = getEmptyCache(); | ||
} | ||
get length() { | ||
@@ -165,24 +163,69 @@ return this.array.length; | ||
get sumY() { | ||
if (!isNaN(this.cache.sumY)) return this.cache.sumY; | ||
let sumY = 0; | ||
for (let item of this.array) { | ||
sumY += item.y; | ||
} | ||
this.cache.sumY = sumY; | ||
this.cache.isEmpty = false; | ||
return sumY; | ||
} | ||
get minX() { | ||
if (!this.xSorted) this.sortX(); | ||
return this.array[0].x; | ||
if (!isNaN(this.cache.minX)) return this.cache.minX; | ||
let minX = Number.POSITIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.x < minX) { | ||
minX = item.x; | ||
} | ||
} | ||
this.cache.minX = minX; | ||
this.cache.isEmpty = false; | ||
return minX; | ||
} | ||
get maxX() { | ||
if (!this.xSorted) this.sortX(); | ||
return this.array[this.array.length - 1].x; | ||
if (!isNaN(this.cache.maxX)) return this.cache.maxX; | ||
let maxX = Number.NEGATIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.x > maxX) { | ||
maxX = item.x; | ||
} | ||
} | ||
this.cache.maxX = maxX; | ||
this.cache.isEmpty = false; | ||
return maxX; | ||
} | ||
get minY() { | ||
if (!this.ySorted) this.sortY(); | ||
return this.array[0].y; | ||
if (!isNaN(this.cache.minY)) return this.cache.minY; | ||
let minY = Number.POSITIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.y < minY) { | ||
minY = item.y; | ||
} | ||
} | ||
this.cache.minY = minY; | ||
this.cache.isEmpty = false; | ||
return minY; | ||
} | ||
get maxY() { | ||
if (!this.ySorted) this.sortY(); | ||
return this.array[this.array.length - 1]; | ||
if (!isNaN(this.cache.maxY)) return this.cache.maxY; | ||
let maxY = Number.NEGATIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.y > maxY) { | ||
maxY = item.y; | ||
} | ||
} | ||
this.cache.maxY = maxY; | ||
this.cache.isEmpty = false; | ||
return maxY; | ||
} | ||
multiplyY(value) { | ||
this.array.forEach((item) => (item.y *= value)); | ||
for (const item of this.array) { | ||
item.y *= value; | ||
} | ||
} | ||
@@ -192,10 +235,8 @@ | ||
this.array = array; | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
this.emptyCache(); | ||
} | ||
move(other) { | ||
this.xSorted = other.xSorted; | ||
this.ySorted = other.ySorted; | ||
this.array = other.array; | ||
this.emptyCache(); | ||
} | ||
@@ -205,20 +246,28 @@ | ||
this.array.push(point); | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
this.emptyCache(); | ||
} | ||
/** | ||
* Sort by ASCENDING x values | ||
* @returns {Distribution} | ||
*/ | ||
sortX() { | ||
this.ySorted = false; | ||
if (this.xSorted) return this; | ||
this.cache.ySorted = false; | ||
if (this.cache.xSorted) return this; | ||
this.array.sort((a, b) => a.x - b.x); | ||
this.xSorted = true; | ||
this.cache.xSorted = true; | ||
this.cache.isEmpty = false; | ||
return this; | ||
} | ||
/** | ||
* Sort by DESCENDING y values | ||
* @returns {Distribution} | ||
*/ | ||
sortY() { | ||
this.xSorted = false; | ||
if (this.ySorted) return this; | ||
this.cache.xSorted = false; | ||
if (this.cache.ySorted) return this; | ||
this.array.sort((a, b) => b.y - a.y); | ||
this.ySorted = true; | ||
this.cache.ySorted = true; | ||
this.cache.isEmpty = false; | ||
return this; | ||
@@ -228,7 +277,4 @@ } | ||
normalize() { | ||
let sum = 0; | ||
const sum = this.sumY; | ||
for (let item of this.array) { | ||
sum += item.y; | ||
} | ||
for (let item of this.array) { | ||
item.y /= sum; | ||
@@ -239,2 +285,7 @@ } | ||
/** | ||
* Only keep a defined number of peaks | ||
* @param {number} limit | ||
* @returns | ||
*/ | ||
topY(limit) { | ||
@@ -248,2 +299,13 @@ if (!limit) return this; | ||
/** | ||
* remove all the peaks under a defined relative threshold | ||
* @param {number} [relativeValue=0] Should be between 0 and 1. 0 means no peak will be removed, 1 means all peaks will be removed | ||
*/ | ||
threshold(relativeValue = 0) { | ||
if (!relativeValue) return this; | ||
const maxY = this.maxY; | ||
const threshold = maxY * relativeValue; | ||
this.array = this.array.filter((point) => point.y >= threshold); | ||
} | ||
square(options = {}) { | ||
@@ -263,4 +325,3 @@ return this.multiply(this, options); | ||
let distCopy = new Distribution(); | ||
distCopy.xSorted = this.xSorted; | ||
distCopy.ySorted = this.ySorted; | ||
distCopy.cache = { ...this.cache }; | ||
distCopy.array = JSON.parse(JSON.stringify(this.array)); | ||
@@ -272,7 +333,4 @@ return distCopy; | ||
if (this.array.length === 0) return this; | ||
let currentMax = this.array[0].y; | ||
let currentMax = this.maxY; | ||
for (let item of this.array) { | ||
if (item.y > currentMax) currentMax = item.y; | ||
} | ||
for (let item of this.array) { | ||
item.y /= currentMax; | ||
@@ -291,4 +349,3 @@ } | ||
} | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
this.emptyCache(); | ||
} | ||
@@ -302,2 +359,15 @@ | ||
function getEmptyCache() { | ||
return { | ||
isEmpty: true, | ||
xSorted: false, | ||
ySorted: false, | ||
minX: NaN, | ||
maxX: NaN, | ||
minY: NaN, | ||
maxY: NaN, | ||
sumY: NaN, | ||
}; | ||
} | ||
function getDerivedCompositionInfo(composition) { | ||
@@ -352,7 +422,11 @@ const shortComposition = {}; | ||
* @param {number} [options.minY=1e-8] - Minimal signal height during calculations | ||
* @param {number} [options.ensureCase=false] - Ensure uppercase / lowercase | ||
* @param {number} [options.allowNeutral=true] - Should we keep the distribution if the molecule has no charge | ||
* @param {boolean} [options.ensureCase=false] - Ensure uppercase / lowercase | ||
* @param {number} [options.threshold] - We can filter the result based on the relative height of the peaks | ||
* @param {number} [options.limit] - We may define the maximum number of peaks to keep | ||
* @param {boolean} [options.allowNeutral=true] - Should we keep the distribution if the molecule has no charge | ||
*/ | ||
constructor(value, options = {}) { | ||
this.threshold = options.threshold; | ||
this.limit = options.limit; | ||
if (Array.isArray(value)) { | ||
@@ -447,6 +521,6 @@ this.parts = JSON.parse(JSON.stringify(value)); | ||
} | ||
this.confidence += totalDistribution.array.reduce( | ||
(sum, value) => sum + value.y, | ||
0, | ||
); | ||
this.confidence = 0; | ||
for (const item of totalDistribution.array) { | ||
this.confidence += item.y; | ||
} | ||
@@ -468,6 +542,7 @@ // we finally deal with the charge | ||
if ( | ||
part.ms.target && | ||
part.ms.target.intensity && | ||
part.ms.target.intensity !== 1 | ||
if (part?.ms.similarity?.factor) { | ||
totalDistribution.multiplyY(part.ms.similarity.factor); | ||
} else if ( | ||
part.ms?.target?.intensity && | ||
part.ms?.target?.intensity !== 1 | ||
) { | ||
@@ -483,3 +558,3 @@ // intensity is the value of the monoisotopic mass ! | ||
} | ||
} else if (part.intensity && part.intensity !== 1) { | ||
} else if (part?.intensity && part?.intensity !== 1) { | ||
totalDistribution.multiplyY(part.intensity); | ||
@@ -499,2 +574,15 @@ } | ||
// if there is a threshold we will deal with it | ||
// and we will correct the confidence | ||
if (this.threshold || this.limit) { | ||
const sumBefore = finalDistribution.sumY; | ||
if (this.threshold) finalDistribution.threshold(this.threshold); | ||
if (this.limit) { | ||
finalDistribution.topY(this.limit); | ||
finalDistribution.sortX(); | ||
} | ||
const sumAfter = finalDistribution.sumY; | ||
this.confidence = (this.confidence * sumAfter) / sumBefore; | ||
} | ||
for (let entry of finalDistribution.array) { | ||
@@ -501,0 +589,0 @@ if (!entry.composition) continue; |
{ | ||
"name": "isotopic-distribution", | ||
"version": "3.0.1", | ||
"version": "3.1.0", | ||
"description": "Calculate the isotopic distribution of a molecular formula", | ||
@@ -24,4 +24,4 @@ "main": "lib/index.js", | ||
"chemical-elements": "^2.0.3", | ||
"mf-parser": "^3.0.1", | ||
"mf-utilities": "^3.0.1", | ||
"mf-parser": "^3.1.0", | ||
"mf-utilities": "^3.1.0", | ||
"spectrum-generator": "^8.0.8" | ||
@@ -32,3 +32,3 @@ }, | ||
}, | ||
"gitHead": "a731fb03357ed5adbae7f72c791547691333a88c" | ||
"gitHead": "deb282f3f2d6ae966112bc520b28fb75af3c327b" | ||
} |
@@ -61,6 +61,11 @@ import { Distribution } from '../Distribution.js'; | ||
let distribution = new Distribution(); | ||
distribution.push({ x: 2, y: 3 }); | ||
distribution.push({ x: 1, y: 1 }); | ||
distribution.push({ x: 2, y: 3 }); | ||
distribution.push({ x: 2.25, y: 1 }); | ||
expect(distribution.minX).toStrictEqual(1); | ||
expect(distribution.maxX).toStrictEqual(2.25); | ||
expect(distribution.minY).toStrictEqual(1); | ||
expect(distribution.maxY).toStrictEqual(3); | ||
distribution.joinX(1); | ||
expect(distribution.array).toStrictEqual([{ x: 1.85, y: 5 }]); | ||
@@ -81,2 +86,12 @@ }); | ||
}); | ||
it('Check the threshold', () => { | ||
let distribution = new Distribution(); | ||
distribution.push({ x: 1, y: 1 }); | ||
distribution.push({ x: 2, y: 3 }); | ||
distribution.push({ x: 2.25, y: 1 }); | ||
distribution.push({ x: 5, y: 1 }); | ||
distribution.threshold(0.5); | ||
expect(distribution.array).toStrictEqual([{ x: 2, y: 3 }]); | ||
}); | ||
}); |
@@ -0,3 +1,7 @@ | ||
import { toBeDeepCloseTo } from 'jest-matcher-deep-close-to'; | ||
import { IsotopicDistribution } from '..'; | ||
expect.extend({ toBeDeepCloseTo }); | ||
describe('test isotopicDistribution', () => { | ||
@@ -199,2 +203,23 @@ it('create distribution of CH0', () => { | ||
it('create distribution of C10 with threshold', () => { | ||
let isotopicDistribution = new IsotopicDistribution('C10', { | ||
threshold: 0.1, | ||
}); | ||
let xy = isotopicDistribution.getXY({ sumValue: 100 }); | ||
expect(isotopicDistribution.confidence).toBeGreaterThan(0.95); | ||
expect(xy.x[0]).toBe(120); | ||
expect(xy.y.reduce((previous, current) => previous + current, 0)).toBe(100); | ||
}); | ||
it('create distribution of C10 with limit', () => { | ||
let isotopicDistribution = new IsotopicDistribution('C10', { | ||
limit: 2, | ||
}); | ||
let xy = isotopicDistribution.getXY(); | ||
expect(xy).toBeDeepCloseTo({ | ||
x: [120, 121.00335483507], | ||
y: [100, 10.815728292732235], | ||
}); | ||
}); | ||
it('create distribution of C10 and getVariables with maxValue to 100', () => { | ||
@@ -201,0 +226,0 @@ let isotopicDistribution = new IsotopicDistribution('C10'); |
@@ -10,14 +10,12 @@ import { closestPointX } from './utils/closestPointX.js'; | ||
export class Distribution { | ||
constructor(array) { | ||
if (Array.isArray(array)) { | ||
this.array = array; | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
} else { | ||
this.array = []; | ||
this.xSorted = true; | ||
this.ySorted = true; | ||
} | ||
constructor(array = []) { | ||
this.array = array; | ||
this.cache = getEmptyCache(); | ||
} | ||
emptyCache() { | ||
if (this.cache.isEmpty) return; | ||
this.cache = getEmptyCache(); | ||
} | ||
get length() { | ||
@@ -35,24 +33,69 @@ return this.array.length; | ||
get sumY() { | ||
if (!isNaN(this.cache.sumY)) return this.cache.sumY; | ||
let sumY = 0; | ||
for (let item of this.array) { | ||
sumY += item.y; | ||
} | ||
this.cache.sumY = sumY; | ||
this.cache.isEmpty = false; | ||
return sumY; | ||
} | ||
get minX() { | ||
if (!this.xSorted) this.sortX(); | ||
return this.array[0].x; | ||
if (!isNaN(this.cache.minX)) return this.cache.minX; | ||
let minX = Number.POSITIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.x < minX) { | ||
minX = item.x; | ||
} | ||
} | ||
this.cache.minX = minX; | ||
this.cache.isEmpty = false; | ||
return minX; | ||
} | ||
get maxX() { | ||
if (!this.xSorted) this.sortX(); | ||
return this.array[this.array.length - 1].x; | ||
if (!isNaN(this.cache.maxX)) return this.cache.maxX; | ||
let maxX = Number.NEGATIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.x > maxX) { | ||
maxX = item.x; | ||
} | ||
} | ||
this.cache.maxX = maxX; | ||
this.cache.isEmpty = false; | ||
return maxX; | ||
} | ||
get minY() { | ||
if (!this.ySorted) this.sortY(); | ||
return this.array[0].y; | ||
if (!isNaN(this.cache.minY)) return this.cache.minY; | ||
let minY = Number.POSITIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.y < minY) { | ||
minY = item.y; | ||
} | ||
} | ||
this.cache.minY = minY; | ||
this.cache.isEmpty = false; | ||
return minY; | ||
} | ||
get maxY() { | ||
if (!this.ySorted) this.sortY(); | ||
return this.array[this.array.length - 1]; | ||
if (!isNaN(this.cache.maxY)) return this.cache.maxY; | ||
let maxY = Number.NEGATIVE_INFINITY; | ||
for (let item of this.array) { | ||
if (item.y > maxY) { | ||
maxY = item.y; | ||
} | ||
} | ||
this.cache.maxY = maxY; | ||
this.cache.isEmpty = false; | ||
return maxY; | ||
} | ||
multiplyY(value) { | ||
this.array.forEach((item) => (item.y *= value)); | ||
for (const item of this.array) { | ||
item.y *= value; | ||
} | ||
} | ||
@@ -62,10 +105,8 @@ | ||
this.array = array; | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
this.emptyCache(); | ||
} | ||
move(other) { | ||
this.xSorted = other.xSorted; | ||
this.ySorted = other.ySorted; | ||
this.array = other.array; | ||
this.emptyCache(); | ||
} | ||
@@ -75,20 +116,28 @@ | ||
this.array.push(point); | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
this.emptyCache(); | ||
} | ||
/** | ||
* Sort by ASCENDING x values | ||
* @returns {Distribution} | ||
*/ | ||
sortX() { | ||
this.ySorted = false; | ||
if (this.xSorted) return this; | ||
this.cache.ySorted = false; | ||
if (this.cache.xSorted) return this; | ||
this.array.sort((a, b) => a.x - b.x); | ||
this.xSorted = true; | ||
this.cache.xSorted = true; | ||
this.cache.isEmpty = false; | ||
return this; | ||
} | ||
/** | ||
* Sort by DESCENDING y values | ||
* @returns {Distribution} | ||
*/ | ||
sortY() { | ||
this.xSorted = false; | ||
if (this.ySorted) return this; | ||
this.cache.xSorted = false; | ||
if (this.cache.ySorted) return this; | ||
this.array.sort((a, b) => b.y - a.y); | ||
this.ySorted = true; | ||
this.cache.ySorted = true; | ||
this.cache.isEmpty = false; | ||
return this; | ||
@@ -98,7 +147,4 @@ } | ||
normalize() { | ||
let sum = 0; | ||
const sum = this.sumY; | ||
for (let item of this.array) { | ||
sum += item.y; | ||
} | ||
for (let item of this.array) { | ||
item.y /= sum; | ||
@@ -109,2 +155,7 @@ } | ||
/** | ||
* Only keep a defined number of peaks | ||
* @param {number} limit | ||
* @returns | ||
*/ | ||
topY(limit) { | ||
@@ -118,2 +169,13 @@ if (!limit) return this; | ||
/** | ||
* remove all the peaks under a defined relative threshold | ||
* @param {number} [relativeValue=0] Should be between 0 and 1. 0 means no peak will be removed, 1 means all peaks will be removed | ||
*/ | ||
threshold(relativeValue = 0) { | ||
if (!relativeValue) return this; | ||
const maxY = this.maxY; | ||
const threshold = maxY * relativeValue; | ||
this.array = this.array.filter((point) => point.y >= threshold); | ||
} | ||
square(options = {}) { | ||
@@ -133,4 +195,3 @@ return this.multiply(this, options); | ||
let distCopy = new Distribution(); | ||
distCopy.xSorted = this.xSorted; | ||
distCopy.ySorted = this.ySorted; | ||
distCopy.cache = { ...this.cache }; | ||
distCopy.array = JSON.parse(JSON.stringify(this.array)); | ||
@@ -142,7 +203,4 @@ return distCopy; | ||
if (this.array.length === 0) return this; | ||
let currentMax = this.array[0].y; | ||
let currentMax = this.maxY; | ||
for (let item of this.array) { | ||
if (item.y > currentMax) currentMax = item.y; | ||
} | ||
for (let item of this.array) { | ||
item.y /= currentMax; | ||
@@ -161,4 +219,3 @@ } | ||
} | ||
this.xSorted = false; | ||
this.ySorted = false; | ||
this.emptyCache(); | ||
} | ||
@@ -171,1 +228,14 @@ | ||
} | ||
function getEmptyCache() { | ||
return { | ||
isEmpty: true, | ||
xSorted: false, | ||
ySorted: false, | ||
minX: NaN, | ||
maxX: NaN, | ||
minY: NaN, | ||
maxY: NaN, | ||
sumY: NaN, | ||
}; | ||
} |
@@ -30,7 +30,11 @@ import { ELECTRON_MASS } from 'chemical-elements'; | ||
* @param {number} [options.minY=1e-8] - Minimal signal height during calculations | ||
* @param {number} [options.ensureCase=false] - Ensure uppercase / lowercase | ||
* @param {number} [options.allowNeutral=true] - Should we keep the distribution if the molecule has no charge | ||
* @param {boolean} [options.ensureCase=false] - Ensure uppercase / lowercase | ||
* @param {number} [options.threshold] - We can filter the result based on the relative height of the peaks | ||
* @param {number} [options.limit] - We may define the maximum number of peaks to keep | ||
* @param {boolean} [options.allowNeutral=true] - Should we keep the distribution if the molecule has no charge | ||
*/ | ||
constructor(value, options = {}) { | ||
this.threshold = options.threshold; | ||
this.limit = options.limit; | ||
if (Array.isArray(value)) { | ||
@@ -125,6 +129,6 @@ this.parts = JSON.parse(JSON.stringify(value)); | ||
} | ||
this.confidence += totalDistribution.array.reduce( | ||
(sum, value) => sum + value.y, | ||
0, | ||
); | ||
this.confidence = 0; | ||
for (const item of totalDistribution.array) { | ||
this.confidence += item.y; | ||
} | ||
@@ -146,6 +150,7 @@ // we finally deal with the charge | ||
if ( | ||
part.ms.target && | ||
part.ms.target.intensity && | ||
part.ms.target.intensity !== 1 | ||
if (part?.ms.similarity?.factor) { | ||
totalDistribution.multiplyY(part.ms.similarity.factor); | ||
} else if ( | ||
part.ms?.target?.intensity && | ||
part.ms?.target?.intensity !== 1 | ||
) { | ||
@@ -161,3 +166,3 @@ // intensity is the value of the monoisotopic mass ! | ||
} | ||
} else if (part.intensity && part.intensity !== 1) { | ||
} else if (part?.intensity && part?.intensity !== 1) { | ||
totalDistribution.multiplyY(part.intensity); | ||
@@ -177,2 +182,15 @@ } | ||
// if there is a threshold we will deal with it | ||
// and we will correct the confidence | ||
if (this.threshold || this.limit) { | ||
const sumBefore = finalDistribution.sumY; | ||
if (this.threshold) finalDistribution.threshold(this.threshold); | ||
if (this.limit) { | ||
finalDistribution.topY(this.limit); | ||
finalDistribution.sortX(); | ||
} | ||
const sumAfter = finalDistribution.sumY; | ||
this.confidence = (this.confidence * sumAfter) / sumBefore; | ||
} | ||
for (let entry of finalDistribution.array) { | ||
@@ -179,0 +197,0 @@ if (!entry.composition) continue; |
// https://en.wikipedia.org/wiki/Exponentiation_by_squaring | ||
export function power(a, p, options = {}) { | ||
export function power(array, p, options = {}) { | ||
if (p <= 0) throw new Error('power must be larger than 0'); | ||
if (p === 1) return a; | ||
if (p === 1) return array; | ||
if (p === 2) { | ||
return a.square(); | ||
return array.square(); | ||
} | ||
p--; | ||
let base = a.copy(); // linear time | ||
let base = array.copy(); // linear time | ||
while (p !== 0) { | ||
if ((p & 1) !== 0) { | ||
a.multiply(base, options); // executed <= log2(p) times | ||
array.multiply(base, options); // executed <= log2(p) times | ||
} | ||
@@ -20,3 +20,3 @@ p >>= 1; | ||
return a; | ||
return array; | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
76838
27
2207
Updatedmf-parser@^3.1.0
Updatedmf-utilities@^3.1.0