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

isotopic-distribution

Package Overview
Dependencies
Maintainers
0
Versions
88
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

isotopic-distribution - npm Package Compare versions

Comparing version 3.1.3 to 3.2.0

lib/Distribution.d.ts

768

lib/index.js

@@ -1,753 +0,17 @@

'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var chemicalElements = require('chemical-elements');
var mfParser = require('mf-parser');
var mfUtilities = require('mf-utilities');
var spectrumGenerator = require('spectrum-generator');
function closestPointX(array, target) {
let low = 0;
let high = array.length - 1;
let middle = 0;
while (high - low > 1) {
middle = low + ((high - low) >> 1);
if (array[middle].x < target) {
low = middle;
} else if (array[middle].x > target) {
high = middle;
} else {
return array[middle];
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
}
if (low < array.length - 1) {
if (Math.abs(target - array[low].x) < Math.abs(array[low + 1].x - target)) {
return array[low];
} else {
return array[low + 1];
}
} else {
return array[low];
}
}
/**
* Join x values if there are similar
*/
function joinX(self, threshold = Number.EPSILON) {
// when we join we will use the center of mass
if (self.array.length === 0) return [];
self.sortX();
let current = self.array[0];
let result = [current];
for (let i = 1; i < self.array.length; i++) {
const item = self.array[i];
if (item.x - current.x <= threshold) {
// weighted sum
current.x =
(item.y / (current.y + item.y)) * (item.x - current.x) + current.x;
current.y += item.y;
} else {
current = {
x: item.x,
y: item.y,
};
if (item.composition) current.composition = item.composition;
result.push(current);
}
}
self.array = result;
self.ySorted = false;
return self;
}
function multiply(a, b, options = {}) {
const { minY = 1e-8, maxLines = 5000, deltaX = 1e-2 } = options;
const result = new a.constructor();
a.sortY();
b.sortY();
for (let entryA of a.array) {
for (let entryB of b.array) {
let y = entryA.y * entryB.y;
if (y > minY) {
const composition = calculateComposition(entryA, entryB);
if (composition) {
result.push({ x: entryA.x + entryB.x, y, composition });
} else {
result.push({ x: entryA.x + entryB.x, y });
}
}
if (result.length > maxLines * 2) {
result.joinX(deltaX);
result.topY(maxLines);
}
}
}
result.joinX(deltaX);
result.topY(maxLines);
a.move(result);
return a;
}
function calculateComposition(entryA, entryB) {
if (!entryA.composition || !entryB.composition) return;
let toReturn = {};
const keys = [
...new Set(
Object.keys(entryA.composition).concat(Object.keys(entryB.composition)),
),
];
for (let key of keys) {
toReturn[key] =
(entryA.composition[key] || 0) + (entryB.composition[key] || 0);
}
return toReturn;
}
// https://en.wikipedia.org/wiki/Exponentiation_by_squaring
function power(array, p, options = {}) {
if (p <= 0) throw new Error('power must be larger than 0');
if (p === 1) return array;
if (p === 2) {
return array.square();
}
p--;
let base = array.copy(); // linear time
while (p !== 0) {
if ((p & 1) !== 0) {
array.multiply(base, options); // executed <= log2(p) times
}
p >>= 1;
if (p !== 0) base.square(options); // executed <= log2(p) times
}
return array;
}
/**
* Internal class to deal with isotopic distribution calculations
*/
class Distribution {
constructor(array = []) {
this.array = array;
this.cache = getEmptyCache();
}
emptyCache() {
if (this.cache.isEmpty) return;
this.cache = getEmptyCache();
}
get length() {
return this.array.length;
}
get xs() {
return this.array.map((p) => p.x);
}
get ys() {
return this.array.map((p) => p.y);
}
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 (!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 (!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 (!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 (!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) {
for (const item of this.array) {
item.y *= value;
}
}
setArray(array) {
this.array = array;
this.emptyCache();
}
move(other) {
this.array = other.array;
this.emptyCache();
}
push(point) {
this.array.push(point);
this.emptyCache();
}
/**
* Sort by ASCENDING x values
* @returns {Distribution}
*/
sortX() {
this.cache.ySorted = false;
if (this.cache.xSorted) return this;
this.array.sort((a, b) => a.x - b.x);
this.cache.xSorted = true;
this.cache.isEmpty = false;
return this;
}
/**
* Sort by DESCENDING y values
* @returns {Distribution}
*/
sortY() {
this.cache.xSorted = false;
if (this.cache.ySorted) return this;
this.array.sort((a, b) => b.y - a.y);
this.cache.ySorted = true;
this.cache.isEmpty = false;
return this;
}
normalize() {
const sum = this.sumY;
for (let item of this.array) {
item.y /= sum;
}
return this;
}
/**
* Only keep a defined number of peaks
* @param {number} limit
* @returns
*/
topY(limit) {
if (!limit) return this;
if (this.array.length <= limit) return this;
this.sortY();
this.array.splice(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 = {}) {
return this.multiply(this, options);
}
multiply(b, options) {
return multiply(this, b, options);
}
power(p, options) {
return power(this, p, options);
}
copy() {
let distCopy = new Distribution();
distCopy.cache = { ...this.cache };
distCopy.array = JSON.parse(JSON.stringify(this.array));
return distCopy;
}
maxToOne() {
if (this.array.length === 0) return this;
let currentMax = this.maxY;
for (let item of this.array) {
item.y /= currentMax;
}
return this;
}
joinX(threshold) {
return joinX(this, threshold);
}
append(distribution) {
for (let item of distribution.array) {
this.array.push(item);
}
this.emptyCache();
}
closestPointX(target) {
this.sortX();
return closestPointX(this.array, target);
}
}
function getEmptyCache() {
return {
isEmpty: true,
xSorted: false,
ySorted: false,
minX: NaN,
maxX: NaN,
minY: NaN,
maxY: NaN,
sumY: NaN,
};
}
function getDerivedCompositionInfo(composition) {
const shortComposition = {};
let label = '';
let shortLabel = '';
for (let key in composition) {
let isotopeLabel = '';
for (let i = 0; i < key.length; i++) {
if (mfParser.superscript[key[i]]) {
isotopeLabel += mfParser.superscript[key[i]];
} else {
isotopeLabel += key[i];
}
}
if (composition[key] > 1) {
const number = String(composition[key]);
for (let i = 0; i < number.length; i++) {
isotopeLabel += mfParser.subscript[number[i]];
}
}
label += isotopeLabel;
if (chemicalElements.stableIsotopesObject[key].mostAbundant) continue;
shortLabel += isotopeLabel;
shortComposition[key] = composition[key];
}
return { label, shortComposition, shortLabel };
}
const MINIMAL_FWHM = 1e-8;
/**
* An object containing two arrays
* @typedef {object} XY
* @property {number[]} x - The x array
* @property {number[]} y - The y array
*/
/**
* A class that allows to manage isotopic distribution
*/
class IsotopicDistribution {
/**
* Class that manage isotopic distribution
* @param {string|array} value - Molecular formula or an array of parts
* @param {object} [options={}]
* @param {string} [options.ionizations=''] - string containing a comma separated list of modifications
* @param {number} [options.fwhm=0.01] - Amount of Dalton under which 2 peaks are joined
* @param {number} [options.maxLines=5000] - Maximal number of lines during calculations
* @param {number} [options.minY=1e-8] - Minimal signal height during calculations
* @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)) {
this.parts = JSON.parse(JSON.stringify(value));
for (let part of this.parts) {
part.confidence = 0;
part.isotopesInfo = new mfParser.MF(
`${part.mf}(${part.ionization.mf})`,
).getIsotopesInfo();
}
} else {
let mf = new mfParser.MF(value, { ensureCase: options.ensureCase });
let mfInfo = mf.getInfo();
let ionizations = mfUtilities.preprocessIonizations(options.ionizations);
let parts = mfInfo.parts || [mfInfo];
this.parts = [];
for (let partOriginal of parts) {
// we calculate information for each part
for (const ionization of ionizations) {
let part = JSON.parse(JSON.stringify(partOriginal));
part.em = part.monoisotopicMass; // TODO: To remove !!! we change the name !?
part.isotopesInfo = new mfParser.MF(
`${part.mf}(${ionization.mf})`,
).getIsotopesInfo();
part.confidence = 0;
let msInfo = mfUtilities.getMsInfo(part, {
ionization,
});
part.ionization = msInfo.ionization;
part.ms = msInfo.ms;
this.parts.push(part);
}
}
}
this.cachedDistribution = undefined;
this.fwhm = options.fwhm === undefined ? 0.01 : options.fwhm;
// if fwhm is under 1e-8 there are some artifacts in the spectra
if (this.fwhm < MINIMAL_FWHM) this.fwhm = MINIMAL_FWHM;
this.minY = options.minY === undefined ? MINIMAL_FWHM : options.minY;
this.maxLines = options.maxLines || 5000;
this.allowNeutral =
options.allowNeutral === undefined ? true : options.allowNeutral;
}
getParts() {
return this.parts;
}
/**
* @return {Distribution} returns the total distribution (for all parts)
*/
getDistribution() {
if (this.cachedDistribution) return this.cachedDistribution;
let options = {
maxLines: this.maxLines,
minY: this.minY,
deltaX: this.fwhm,
};
let finalDistribution = new Distribution();
this.confidence = 0;
// TODO need to cache each part without ionization
// in case of many ionization we don't need to recalculate everything !
for (let part of this.parts) {
let totalDistribution = new Distribution([
{
x: 0,
y: 1,
composition: this.fwhm === MINIMAL_FWHM ? {} : undefined,
},
]);
let charge = part.ms.charge;
let absoluteCharge = Math.abs(charge);
if (charge || this.allowNeutral) {
for (let isotope of part.isotopesInfo.isotopes) {
if (isotope.number < 0) return { array: [] };
if (isotope.number > 0) {
const newDistribution = JSON.parse(
JSON.stringify(isotope.distribution),
);
if (this.fwhm === MINIMAL_FWHM) {
// add composition
for (const entry of newDistribution) {
entry.composition = { [Math.round(entry.x) + isotope.atom]: 1 };
}
}
let distribution = new Distribution(newDistribution);
distribution.power(isotope.number, options);
totalDistribution.multiply(distribution, options);
}
}
this.confidence = 0;
for (const item of totalDistribution.array) {
this.confidence += item.y;
}
// we finally deal with the charge
if (charge) {
totalDistribution.array.forEach((e) => {
e.x = (e.x - chemicalElements.ELECTRON_MASS * charge) / absoluteCharge;
});
}
if (totalDistribution.array && totalDistribution.array.length > 0) {
totalDistribution.sortX();
part.fromX = totalDistribution.array[0].x;
part.toX =
totalDistribution.array[totalDistribution.array.length - 1].x;
}
if (part?.ms.similarity?.factor) {
totalDistribution.multiplyY(part.ms.similarity.factor);
} else if (
part.ms?.target?.intensity &&
part.ms?.target?.intensity !== 1
) {
// intensity is the value of the monoisotopic mass !
// need to find the intensity of the peak corresponding
// to the monoisotopic mass
if (part.ms.target.mass) {
let target = totalDistribution.closestPointX(part.ms.target.mass);
totalDistribution.multiplyY(part.ms.target.intensity / target.y);
} else {
totalDistribution.multiplyY(part.ms.target.intensity);
}
} else if (part?.intensity && part?.intensity !== 1) {
totalDistribution.multiplyY(part.intensity);
}
part.isotopicDistribution = totalDistribution.array;
if (finalDistribution.array.length === 0) {
finalDistribution = totalDistribution;
} else {
finalDistribution.append(totalDistribution);
}
}
}
if (finalDistribution) finalDistribution.joinX(this.fwhm);
// 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) {
if (!entry.composition) continue;
Object.assign(entry, getDerivedCompositionInfo(entry.composition));
}
this.confidence /= this.parts.length;
this.cachedDistribution = finalDistribution;
return finalDistribution;
}
getCSV() {
return this.getText({ delimiter: ', ' });
}
getTSV() {
return this.getText({ delimiter: '\t' });
}
getTable(options = {}) {
const { maxValue, xLabel = 'x', yLabel = 'y' } = options;
let points = this.getDistribution().array;
let factor = 1;
if (maxValue) {
let maxY = this.getMaxY(points);
factor = maxValue / maxY;
}
return points.map((point) => {
let newPoint = {};
newPoint[xLabel] = point.x;
newPoint[yLabel] = point.y * factor;
return newPoint;
});
}
getText(options = {}) {
const { delimiter = '\t', numberDecimals = 3 } = options;
let points = this.getDistribution().array;
let csv = [];
for (let point of points) {
csv.push(
`${point.x.toFixed(5)}${delimiter}${(point.y * 100).toFixed(
numberDecimals,
)}`,
);
}
return csv.join('\n');
}
getMaxY(points) {
let maxY = points[0].y;
for (let point of points) {
if (point.y > maxY) maxY = point.y;
}
return maxY;
}
getSumY(points) {
let sumY = 0;
for (let point of points) {
sumY += point.y;
}
return sumY;
}
/**
* Returns the isotopic distribution as an array of peaks
* @param {object} [options={}]
* @param {number} [options.maxValue=100]
* @param {number} [options.sumValue] // if sumValue is defined, maxValue is ignored
* @return {Array<any>} an object containing at least the 2 properties: x:[] and y:[]
*/
getPeaks(options = {}) {
const { maxValue = 100, sumValue } = options;
let peaks = this.getDistribution().array;
if (peaks.length === 0) return [];
let factor = 1;
if (sumValue) {
let sumY = this.getSumY(peaks);
factor = sumY / sumValue;
} else if (maxValue) {
let maxY = this.getMaxY(peaks);
factor = maxY / maxValue;
}
if (factor !== 1) {
// we need to copy the array because we prefer no side effects
peaks = JSON.parse(JSON.stringify(peaks));
for (const peak of peaks) {
peak.y = peak.y / factor;
}
}
return peaks;
}
/**
* Returns the isotopic distirubtion
* @param {object} [options={}]
* @param {number} [options.maxValue=100]
* @param {number} [options.sumValue] // if sumValue is defined, maxValue is ignored
* @return {XY} an object containing at least the 2 properties: x:[] and y:[]
*/
getXY(options = {}) {
let peaks = this.getPeaks(options);
if (peaks.length === 0) {
return { x: [], y: [] };
}
const result = {
x: peaks.map((a) => a.x),
y: peaks.map((a) => a.y),
};
for (let key of Object.keys(peaks[0]).filter(
(k) => k !== 'x' && k !== 'y',
)) {
result[key] = peaks.map((a) => a[key]);
}
return result;
}
/**
* Returns the isotopic distirubtion
* @param {object} [options={}]
* @param {number} [options.maxValue=100]
* @param {number} [options.sumValue] // if sumValue is defined, maxValue is ignored
* @return {import('cheminfo-types').MeasurementXYVariables} an object containing at least the 2 properties: x:[] and y:[]
*/
getVariables(options = {}) {
const xy = this.getXY(options);
return {
x: { data: xy.x, label: 'm/z', units: 'u' },
y: { data: xy.y, label: 'Relative intensity', units: '%' },
};
}
/**
* Returns the isotopic distribution as the sum of gaussian
* @param {object} [options={}]
* @param {number} [options.gaussianWidth=10]
* @param {number} [options.threshold=0.00001] // minimal height to return point
* @param {number} [options.maxLength=1e6] // minimal height to return point
* @param {number} [options.maxValue] // rescale Y to reach maxValue
* @param {function} [options.peakWidthFct=(mz)=>(this.fwhm)]
* @return {XY} isotopic distribution as an object containing 2 properties: x:[] and y:[]
*/
getGaussian(options = {}) {
const {
peakWidthFct = () => this.fwhm,
threshold = 0.00001,
gaussianWidth = 10,
maxValue,
maxLength = 1e6,
} = options;
let points = this.getTable({ maxValue });
if (points.length === 0) return { x: [], y: [] };
const from = Math.floor(options.from || points[0].x - 2);
const to = Math.ceil(options.to || points[points.length - 1].x + 2);
const nbPoints = Math.round(((to - from) * gaussianWidth) / this.fwhm + 1);
if (nbPoints > maxLength) {
throw Error(
`Number of points is over the maxLength: ${nbPoints}>${maxLength}`,
);
}
let gaussianOptions = {
from,
to,
nbPoints,
peakWidthFct,
};
let spectrumGenerator$1 = new spectrumGenerator.SpectrumGenerator(gaussianOptions);
for (let point of points) {
spectrumGenerator$1.addPeak([point.x, point.y]);
}
let spectrum = spectrumGenerator$1.getSpectrum({ threshold });
return spectrum;
}
}
exports.IsotopicDistribution = IsotopicDistribution;
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./IsotopicDistribution"), exports);
{
"name": "isotopic-distribution",
"version": "3.1.3",
"version": "3.2.0",
"description": "Calculate the isotopic distribution of a molecular formula",

@@ -23,5 +23,5 @@ "main": "lib/index.js",

"dependencies": {
"chemical-elements": "^2.0.4",
"mf-parser": "^3.1.1",
"mf-utilities": "^3.1.1",
"chemical-elements": "^2.1.0",
"mf-parser": "^3.2.0",
"mf-utilities": "^3.2.0",
"spectrum-generator": "^8.0.11"

@@ -32,3 +32,3 @@ },

},
"gitHead": "b9f99ec6f05ce6b0034d35f4ae0452ae653f90c2"
"gitHead": "28dae91d3b42556a23097ee08acfe4061f276ed0"
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc