UNPKG

sequaljs

Version:

JavaScript/TypeScript library for parsing and manipulating ProForma peptide sequence notation

1,111 lines (1,110 loc) 45.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModificationMap = exports.GlobalModification = exports.Modification = exports.ModificationValue = void 0; const resources_1 = require("./resources"); const base_block_1 = require("./base_block"); class PipeValue { constructor(value, valueType, originalValue = null) { this.crosslinkId = null; this.isBranch = false; this.isBranchRef = false; this.isCrosslinkRef = false; this.ambiguityGroup = null; this.isAmbiguityRef = false; this.localizationScore = null; this.source = null; this.mass = null; this.observedMass = null; this.isValidGlycan = false; this.isValidFormula = false; this.assignedTypes = []; this.value = value; this._type = valueType; this.originalValue = originalValue; this._extractProperties(); } _extractProperties() { if (this.type === PipeValue.CROSSLINK && this.value.includes("#")) { const parts = this.value.split("#", 2); if (parts[1] === "BRANCH") { this.isBranch = true; } else { this.crosslinkId = parts[1]; } } else if (this.type === PipeValue.AMBIGUITY && this.value.includes("#")) { const parts = this.value.split("#", 2); this.ambiguityGroup = parts[1]; if (this.ambiguityGroup.includes("(") && this.ambiguityGroup.includes(")")) { const match = this.ambiguityGroup.match(/\(([\d.]+)\)/); if (match) { this.localizationScore = parseFloat(match[1]); } } } } toString() { return this.value; } get type() { return this._type; } set type(value) { this._type = value; if (this.assignedTypes.length > 0) { this.assignedTypes[0] = value; } else { this.assignType(value); } } assignType(value) { if (!this.assignedTypes.includes(value)) { this.assignedTypes.push(value); } } } PipeValue.SYNONYM = "synonym"; PipeValue.INFO_TAG = "info_tag"; PipeValue.MASS = "mass"; PipeValue.OBSERVED_MASS = "observed_mass"; PipeValue.CROSSLINK = "crosslink"; PipeValue.BRANCH = "branch"; PipeValue.AMBIGUITY = "ambiguity"; PipeValue.GLYCAN = "glycan"; PipeValue.GAP = "gap"; PipeValue.FORMULA = "formula"; class ModificationValue { constructor(value, mass = null) { this._primaryValue = ""; this._source = null; this._pipeValues = []; this._mass = mass; this._parseValue(value); } static validateGlycan(glycan) { return ModificationValue._validateGlycan(glycan); } static validateFormula(formula) { return ModificationValue._validateFormula(formula); } get localizationScore() { return this._pipeValues .filter((pv) => pv.type === PipeValue.AMBIGUITY) .map((pv) => pv.localizationScore)[0] || null; } _parseValue(value) { if (value.includes("|")) { const components = value.split("|"); this._processPrimaryValue(components[0]); for (let i = 1; i < components.length; i++) { this._processPipeComponent(components[i]); } } else { this._processPrimaryValue(value); } } _processPrimaryValue(value) { if (value === "#BRANCH") { this._primaryValue = ""; const pipeVal = new PipeValue(value, PipeValue.BRANCH, value); pipeVal.isBranchRef = true; pipeVal.isBranch = true; this._pipeValues.push(pipeVal); return; } else if (value.startsWith("#")) { this._primaryValue = ""; const pipeValType = value.substring(1).startsWith("XL") ? PipeValue.CROSSLINK : PipeValue.AMBIGUITY; const pipeVal = new PipeValue(value, pipeValType, value); pipeVal.isCrosslinkRef = pipeValType === PipeValue.CROSSLINK; pipeVal.isAmbiguityRef = pipeValType === PipeValue.AMBIGUITY; pipeVal.crosslinkId = pipeVal.isCrosslinkRef ? value.substring(1) : null; pipeVal.ambiguityGroup = pipeVal.isAmbiguityRef ? value.substring(1) : null; if (pipeVal.ambiguityGroup) { const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } this._pipeValues.push(pipeVal); return; } // Handle source prefix if (value.includes(":")) { const parts = value.split(":", 2); if (ModificationValue.KNOWN_SOURCES.has(parts[0])) { this._source = parts[0]; this._primaryValue = parts[1]; let isValidGlycan = false; let isValidFormula = false; if (this._source.toUpperCase() === "FORMULA") { isValidFormula = ModificationValue._validateFormula(this._primaryValue); } else if (this._source.toUpperCase() === "GLYCAN") { isValidGlycan = ModificationValue._validateGlycan(this._primaryValue); } if (this._primaryValue.includes("#")) { const pvParts = this._primaryValue.split("#", 2); this._primaryValue = pvParts[0]; let pipeVal; if (["XL", "XLMOD", "XL-MOD", "X"].includes(this._source)) { pipeVal = new PipeValue(`${this._primaryValue}`, PipeValue.CROSSLINK, value); pipeVal.source = this._source; pipeVal.crosslinkId = pvParts[1]; } else if (pvParts[1] === "BRANCH") { pipeVal = new PipeValue(pvParts[0], PipeValue.BRANCH); pipeVal.source = this._source; pipeVal.isBranch = true; } else { pipeVal = new PipeValue(`${this._primaryValue}`, PipeValue.AMBIGUITY, value); if (isValidGlycan) { pipeVal.isValidGlycan = isValidGlycan; pipeVal.assignedTypes.push("glycan"); } else if (isValidFormula) { pipeVal.isValidFormula = isValidFormula; pipeVal.assignedTypes.push("formula"); } if (this._source.toUpperCase() === "GNO" || this._source.toUpperCase() === "G") { pipeVal.isValidGlycan = true; pipeVal.assignedTypes.push("glycan"); } pipeVal.source = this._source; pipeVal.ambiguityGroup = pvParts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } this._pipeValues.push(pipeVal); } else { let pipeVal; if (this._source.toUpperCase() === "INFO") { pipeVal = new PipeValue(parts[1], PipeValue.INFO_TAG, value); } else if (this._source.toUpperCase() === "OBS") { pipeVal = new PipeValue(parts[1], PipeValue.OBSERVED_MASS, value); pipeVal.observedMass = parseFloat(parts[1]); } else if (this._source.toUpperCase() === "GLYCAN") { pipeVal = new PipeValue(parts[1], PipeValue.GLYCAN, value); pipeVal.isValidGlycan = isValidGlycan; } else if (this._source.toUpperCase() === "GNO" || this._source.toUpperCase() === "G") { pipeVal = new PipeValue(parts[1], PipeValue.GAP, value); pipeVal.isValidGlycan = true; } else if (this._source.toUpperCase() === "FORMULA") { pipeVal = new PipeValue(parts[1], PipeValue.FORMULA, value); pipeVal.isValidFormula = isValidFormula; } else { pipeVal = new PipeValue(parts[1], PipeValue.SYNONYM, value); } pipeVal.source = this._source; this._pipeValues.push(pipeVal); } } else if (parts[0].toUpperCase() === "MASS") { this._primaryValue = value; try { this._mass = parseFloat(parts[1]); const pipeVal = new PipeValue(parts[1], PipeValue.MASS, value); pipeVal.mass = this._mass; if (parts[1].includes("#")) { const pvParts = this._primaryValue.split("#", 2); this._primaryValue = pvParts[0]; pipeVal.value = pvParts[0]; if (pvParts[1] === "BRANCH") { pipeVal.isBranch = true; pipeVal.type = PipeValue.BRANCH; } else if (pvParts[1].startsWith("XL")) { pipeVal.crosslinkId = pvParts[1]; pipeVal.type = PipeValue.CROSSLINK; } else { pipeVal.ambiguityGroup = pvParts[1]; pipeVal.type = PipeValue.AMBIGUITY; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } pipeVal.assignedTypes.push(PipeValue.MASS); } this._pipeValues.push(pipeVal); } catch (e) { // Parse error handling } } else { this._primaryValue = value; const pipeVal = new PipeValue(value, PipeValue.SYNONYM, value); this._pipeValues.push(pipeVal); } } else { if (value.includes("#")) { const parts = value.split("#", 2); this._primaryValue = parts[0]; let pipeVal; if (parts[1] === "BRANCH") { pipeVal = new PipeValue(`${parts[0]}`, PipeValue.BRANCH, value); pipeVal.isBranch = true; } else if (parts[1].startsWith("XL")) { pipeVal = new PipeValue(`${parts[0]}`, PipeValue.CROSSLINK, value); pipeVal.crosslinkId = parts[1]; } else { pipeVal = new PipeValue(`${parts[0]}`, PipeValue.AMBIGUITY, value); pipeVal.ambiguityGroup = parts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } if (parts[0].startsWith("+") || parts[0].startsWith("-")) { try { this._mass = parseFloat(parts[0]); pipeVal.mass = this._mass; pipeVal.assignedTypes.push(PipeValue.MASS); } catch (e) { // Parse error handling } } else { pipeVal.assignedTypes.push(PipeValue.SYNONYM); } this._pipeValues.push(pipeVal); } else { this._primaryValue = value; if ((this._primaryValue.startsWith("+") || this._primaryValue.startsWith("-")) && /\d/.test(this._primaryValue)) { try { this._mass = parseFloat(this._primaryValue); const pipeVal = new PipeValue(this._primaryValue, PipeValue.MASS, value); pipeVal.mass = this._mass; this._pipeValues.push(pipeVal); } catch (e) { const pipeVal = new PipeValue(value, PipeValue.SYNONYM, value); this._pipeValues.push(pipeVal); } } else { const pipeVal = new PipeValue(value, PipeValue.SYNONYM, value); this._pipeValues.push(pipeVal); } } } } static _validateGlycan(glycan) { const glycanClean = glycan.replace(/\s/g, ""); const sortedMonos = [...resources_1.monosaccharides].sort((a, b) => b.length - a.length); const escapedMonos = sortedMonos.map(m => m.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')); const monoPatternString = "(" + escapedMonos.join("|") + ")(\\((\\d+)\\))?"; const monoPattern = new RegExp(monoPatternString); let i = 0; while (i < glycanClean.length) { const match = glycanClean.substring(i).match(monoPattern); if (!match) { return false; } i += match[0].length; } return i === glycanClean.length; } static _validateFormula(formula) { if (!formula.trim()) { return false; } // Check for balanced brackets if ((formula.match(/\[/g) || []).length !== (formula.match(/\]/g) || []).length) { return false; } const formulaNoSpaces = formula.replace(/\s/g, ""); let i = 0; while (i < formulaNoSpaces.length) { if (formulaNoSpaces[i] === "[") { const endBracket = formulaNoSpaces.indexOf("]", i); if (endBracket === -1) { return false; } const isotopePart = formulaNoSpaces.substring(i + 1, endBracket); if (!isotopePart.match(/\d+[A-Z][a-z]?(-?\d+)?/)) { return false; } i = endBracket + 1; if (i < formulaNoSpaces.length && (formulaNoSpaces[i] === '-' || /\d/.test(formulaNoSpaces[i]))) { let start = i; if (formulaNoSpaces[i] === '-') { i++; } while (i < formulaNoSpaces.length && /\d/.test(formulaNoSpaces[i])) { i++; } if (parseInt(formulaNoSpaces.substring(start, i)) === 0) { return false; } } } else if (/[A-Z]/.test(formulaNoSpaces[i])) { if (i + 1 < formulaNoSpaces.length && /[a-z]/.test(formulaNoSpaces[i + 1])) { i += 2; } else { i++; } if (i < formulaNoSpaces.length && (formulaNoSpaces[i] === '-' || /\d/.test(formulaNoSpaces[i]))) { let start = i; if (formulaNoSpaces[i] === '-') { i++; } while (i < formulaNoSpaces.length && /\d/.test(formulaNoSpaces[i])) { i++; } if (parseInt(formulaNoSpaces.substring(start, i)) === 0) { return false; } } } else { // Unexpected character return false; } } return true; } _processPipeComponent(component) { if (component === "#BRANCH") { const pipeVal = new PipeValue(component, PipeValue.BRANCH, component); pipeVal.isBranchRef = true; pipeVal.isBranch = true; this._pipeValues.push(pipeVal); return; } else if (component.startsWith("#")) { const pipeValType = component.substring(1).startsWith("XL") ? PipeValue.CROSSLINK : PipeValue.AMBIGUITY; const pipeVal = new PipeValue(component, pipeValType, component); pipeVal.isCrosslinkRef = pipeValType === PipeValue.CROSSLINK; pipeVal.isAmbiguityRef = pipeValType === PipeValue.AMBIGUITY; pipeVal.crosslinkId = pipeVal.isCrosslinkRef ? component.substring(1) : null; pipeVal.ambiguityGroup = pipeVal.isAmbiguityRef ? component.substring(1) : null; if (pipeVal.ambiguityGroup) { const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } this._pipeValues.push(pipeVal); return; } // Handle source prefix if (component.includes(":")) { const parts = component.split(":", 2); if (ModificationValue.KNOWN_SOURCES.has(parts[0])) { const source = parts[0]; const value = parts[1]; // Handle crosslinks or ambiguity in value if (value.includes("#")) { const pvParts = value.split("#", 2); const componentValue = pvParts[0]; let isValidGlycan = false; let isValidFormula = false; if (source.toUpperCase() === "FORMULA") { isValidFormula = ModificationValue._validateFormula(componentValue); } else if (source.toUpperCase() === "GLYCAN") { isValidGlycan = ModificationValue._validateGlycan(componentValue); } let pipeVal; if (source === "XL" || source === "XLMOD" || source === "XL-MOD" || source === "X") { pipeVal = new PipeValue(componentValue, PipeValue.CROSSLINK); pipeVal.crosslinkId = pvParts[1]; } else if (pvParts[1] === "BRANCH") { pipeVal = new PipeValue(componentValue, PipeValue.BRANCH); pipeVal.isBranch = true; } else if (source.toUpperCase() === "GLYCAN") { pipeVal = new PipeValue(componentValue, PipeValue.GLYCAN); pipeVal.isValidGlycan = isValidGlycan; } else if (source.toUpperCase() === "GNO" || source.toUpperCase() === "G") { pipeVal = new PipeValue(componentValue, PipeValue.GAP); pipeVal.isValidGlycan = true; } else if (source.toUpperCase() === "FORMULA") { pipeVal = new PipeValue(componentValue, PipeValue.FORMULA); pipeVal.isValidFormula = isValidFormula; } else { pipeVal = new PipeValue(componentValue, PipeValue.AMBIGUITY); pipeVal.ambiguityGroup = pvParts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } pipeVal.source = source; this._pipeValues.push(pipeVal); } else { let pipeVal; if (source.toUpperCase() === "INFO") { pipeVal = new PipeValue(value, PipeValue.INFO_TAG, component); } else if (source.toUpperCase() === "OBS") { pipeVal = new PipeValue(value, PipeValue.OBSERVED_MASS, component); pipeVal.observedMass = parseFloat(value); } else if (source.toUpperCase() === "GLYCAN") { pipeVal = new PipeValue(value, PipeValue.GLYCAN, component); pipeVal.isValidGlycan = ModificationValue._validateGlycan(value); } else if (source.toUpperCase() === "GNO" || source.toUpperCase() === "G") { pipeVal = new PipeValue(value, PipeValue.GAP, component); pipeVal.isValidGlycan = true; } else if (source.toUpperCase() === "FORMULA") { pipeVal = new PipeValue(value, PipeValue.FORMULA, component); pipeVal.isValidFormula = ModificationValue._validateFormula(value); } else { pipeVal = new PipeValue(value, PipeValue.SYNONYM, component); } pipeVal.source = source; this._pipeValues.push(pipeVal); } } else if (parts[0].toUpperCase() === "MASS") { try { const mass = parseFloat(parts[1]); const pipeVal = new PipeValue(parts[1], PipeValue.MASS, component); pipeVal.mass = mass; if (parts[1].includes("#")) { const hashParts = parts[1].split("#", 2); if (hashParts[1] === "BRANCH") { pipeVal.isBranch = true; pipeVal.type = PipeValue.BRANCH; } else if (hashParts[1].startsWith("XL")) { pipeVal.crosslinkId = hashParts[1]; pipeVal.type = PipeValue.CROSSLINK; } else { pipeVal.ambiguityGroup = hashParts[1]; pipeVal.type = PipeValue.AMBIGUITY; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } } this._pipeValues.push(pipeVal); } catch (e) { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } else { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } else { // Handle crosslink ID or ambiguity for values without source prefix if (component.includes("#")) { const parts = component.split("#", 2); const value = parts[0]; let pipeVal; if (parts[1] === "BRANCH") { pipeVal = new PipeValue(value, PipeValue.BRANCH, component); pipeVal.isBranch = true; } else if (parts[1].startsWith("XL")) { pipeVal = new PipeValue(value, PipeValue.CROSSLINK, component); pipeVal.crosslinkId = parts[1]; } else { pipeVal = new PipeValue(value, PipeValue.AMBIGUITY, component); pipeVal.ambiguityGroup = parts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } if ((value.startsWith("+") || value.startsWith("-")) && /\d/.test(value)) { try { const mass = parseFloat(value); pipeVal.mass = mass; pipeVal.assignedTypes.push(PipeValue.MASS); } catch (e) { // Parse error handling } } else { pipeVal.assignedTypes.push(PipeValue.SYNONYM); } this._pipeValues.push(pipeVal); } else { // Handle mass shifts if ((component.startsWith("+") || component.startsWith("-")) && /\d/.test(component)) { try { const mass = parseFloat(component); const pipeVal = new PipeValue(component, PipeValue.MASS, component); pipeVal.mass = mass; this._pipeValues.push(pipeVal); } catch (e) { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } else { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } } } get source() { return this._source; } get primaryValue() { return this._primaryValue; } get mass() { return this._mass; } get pipeValues() { return this._pipeValues; } get infoTags() { return this._pipeValues .filter((pv) => pv.type === PipeValue.INFO_TAG) .map((pv) => pv.value); } get synonyms() { return this._pipeValues .filter((pv) => pv.type === PipeValue.SYNONYM) .map((pv) => pv.value); } get observedMass() { return this._pipeValues .filter((pv) => pv.type === PipeValue.OBSERVED_MASS) .map((pv) => pv.observedMass)[0] || null; } get ambiguityGroup() { return this._pipeValues .filter((pv) => pv.type === PipeValue.AMBIGUITY) .map((pv) => pv.ambiguityGroup)[0] || null; } get isAmbiguityRef() { return this._pipeValues .filter((pv) => pv.type === PipeValue.AMBIGUITY) .map((pv) => pv.isAmbiguityRef)[0] || false; } get isCrosslinkRef() { return this._pipeValues .filter((pv) => pv.type === PipeValue.CROSSLINK) .map((pv) => pv.isCrosslinkRef)[0] || false; } get isBranchRef() { return this._pipeValues .filter((pv) => pv.type === PipeValue.BRANCH) .map((pv) => pv.isBranchRef)[0] || false; } get isBranch() { return this._pipeValues .filter((pv) => pv.type === PipeValue.BRANCH) .map((pv) => pv.isBranch)[0] || false; } get crossLinkId() { return this._pipeValues .filter((pv) => pv.type === PipeValue.CROSSLINK) .map((pv) => pv.crosslinkId)[0] || null; } } exports.ModificationValue = ModificationValue; ModificationValue.KNOWN_SOURCES = new Set([ "Unimod", "U", "PSI-MOD", "M", "RESID", "R", "XL-MOD", "X", "XLMOD", "GNO", "G", "MOD", "Obs", "Formula", "FORMULA", "GLYCAN", "Glycan", "Info", "INFO", "OBS", "XL" ]); class Modification extends base_block_1.BaseBlock { constructor(value, position, regexPattern, fullName, modType = "static", labile = false, labilNumber = 0, mass = 0.0, allFilled = false, crosslinkId, isCrosslinkRef = false, isBranchRef = false, isBranch = false, ambiguityGroup, isAmbiguityRef = false, inRange = false, rangeStart, rangeEnd, localizationScore, modValue) { // Initialize parameters for superclass let processedValue = value; if (value.startsWith("#") && isCrosslinkRef) { crosslinkId = value.substring(1); processedValue = "#" + crosslinkId; } // Call BaseBlock constructor super(processedValue, position, true, mass); // Initialize Modification specific properties this._source = null; this._originalValue = value; this._crosslinkId = crosslinkId || null; this._isCrosslinkRef = isCrosslinkRef; this._isBranchRef = isBranchRef; this._isBranch = isBranch; this._isAmbiguityRef = isAmbiguityRef; this._ambiguityGroup = ambiguityGroup || null; this.inRange = inRange; this.rangeStart = rangeStart || null; this.rangeEnd = rangeEnd || null; this.localizationScore = localizationScore || null; this._modValue = modValue || new ModificationValue(value, mass); const validModTypes = new Set([ "static", "variable", "terminal", "ambiguous", "crosslink", "branch", "gap", "labile", "unknown_position", "global" ]); if ((crosslinkId || isCrosslinkRef) && modType !== "crosslink") { modType = "crosslink"; } if (!validModTypes.has(modType)) { throw new Error(`mod_type must be one of: ${Array.from(validModTypes).join(', ')}`); } this._regex = regexPattern ? new RegExp(regexPattern) : null; this._modType = modType; this._labile = labile; this._labileNumber = labilNumber; this._fullName = fullName || null; this._allFilled = allFilled; if (modType === "labile") { this._labile = true; } if (this.inRange) { this._modType = "ambiguous"; } } get value() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.primaryValue) || super.value; } set value(val) { super.value = val; } get mass() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.mass) || super.mass || 0.0; } set mass(val) { super.mass = val; } get observedMass() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.observedMass) || null; } get ambiguityGroup() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.ambiguityGroup) || this._ambiguityGroup; } get isAmbiguityRef() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.isAmbiguityRef) || this._isAmbiguityRef; } get synonyms() { return this._modValue.synonyms; } get modValue() { return this._modValue; } set modValue(val) { this._modValue = val; } get infoTags() { return this._modValue.infoTags; } get crosslinkId() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.crossLinkId) || this._crosslinkId; } get isCrosslinkRef() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.isCrosslinkRef) || this._isCrosslinkRef; } get source() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.source) || this._source; } get originalValue() { return this._originalValue; } get regex() { return this._regex; } get modType() { return this._modType; } get labile() { return this._labile; } get labileNumber() { return this._labileNumber; } get fullName() { return this._fullName; } get allFilled() { return this._allFilled; } // Methods *findPositions(seq) { if (!this._regex) { throw new Error(`No regex pattern defined for modification '${this.value}'`); } let match; let regex = new RegExp(this._regex); if (!regex.global) { regex = new RegExp(this._regex, 'g'); } while ((match = regex.exec(seq)) !== null) { const groups = match.length > 1 ? match.slice(1) : []; if (groups.length > 0) { for (let groupIdx = 0; groupIdx < groups.length; groupIdx++) { if (groups[groupIdx]) { const start = match.index + match[0].indexOf(match[groupIdx + 1]); yield [start, start + match[groupIdx + 1].length]; } } } else { yield [match.index, match.index + match[0].length]; } } } toDict() { var _a, _b; const baseDict = super.toDict(); return Object.assign(Object.assign({}, baseDict), { source: this._source, original_value: this._originalValue, regex_pattern: (_b = (_a = this._regex) === null || _a === void 0 ? void 0 : _a.source) !== null && _b !== void 0 ? _b : null, full_name: this._fullName, mod_type: this._modType, labile: this._labile, labile_number: this._labileNumber, all_filled: this._allFilled, crosslink_id: this._crosslinkId, is_crosslink_ref: this._isCrosslinkRef }); } equals(other) { if (!super.equals(other)) { return false; } if (!(other instanceof Modification)) { return false; } return (this._modType === other.modType && this._labile === other.labile && this._labileNumber === other.labileNumber); } hashCode() { var _a; const baseHash = super.hashCode(); return baseHash ^ ((((_a = this._modType) === null || _a === void 0 ? void 0 : _a.length) || 0) * 17) ^ (this._labile ? 1 : 0) ^ this._labileNumber; } toString() { if (this._isCrosslinkRef && this._crosslinkId) { return `#${this._crosslinkId}`; } if (this._isBranchRef) { return "#BRANCH"; } let result = this._modValue.toString(); if (this._crosslinkId && !this._isCrosslinkRef) { result += `#${this._crosslinkId}`; } if (this._isBranch && !this._isBranchRef) { result += "#BRANCH"; } if (this._labile) { result += `${this._labileNumber}`; } return result; } static validateGlycan(glycan) { return ModificationValue.validateGlycan(glycan); } static validateFormula(formula) { return ModificationValue.validateFormula(formula); } get isBranchRef() { return this.modValue.isBranchRef || this._isBranchRef; } get isBranch() { return this.modValue.isBranch || this._isBranch; } toProforma() { const parts = []; if (this.modValue) { const seen = new Set(); for (const pv of this.modValue.pipeValues) { let mod_part = ""; if (pv.source) { mod_part = `${pv.source}:`; if (pv.mass) { if (pv.mass > 0) { mod_part += `+${pv.mass}`; seen.add(`+${pv.mass}`); } else if (pv.mass < 0) { mod_part += `-${pv.mass}`; seen.add(`${pv.mass}`); } } else { mod_part += `${pv.value}`; } } else { if (pv.mass) { if (pv.mass > 0) { mod_part = `+${pv.mass}`; } else if (pv.mass < 0) { mod_part = `${pv.mass}`; } } else if (pv.type === PipeValue.SYNONYM) { mod_part = `${pv.value}`; } else { if (!pv.value.includes("#")) { mod_part = `${pv.value}`; } } } if (pv.type === PipeValue.CROSSLINK && pv.crosslinkId) { mod_part += `#${pv.crosslinkId}`; } else if (pv.type === PipeValue.BRANCH && pv.isBranch) { mod_part += `#BRANCH`; } else if (pv.type === PipeValue.AMBIGUITY && pv.ambiguityGroup) { const score_str = pv.localizationScore !== null ? `(${pv.localizationScore.toFixed(2)})` : ""; mod_part += `#${pv.ambiguityGroup}${score_str}`; } if (seen.has(mod_part)) { continue; } parts.push(mod_part); seen.add(mod_part); } return parts.join("|"); } else { if (this.mass !== null && (this.value.startsWith("+") || this.value.startsWith("-"))) { return String(this.mass); } return this.value; } } get hasAmbiguity() { return this.modValue.pipeValues.some(pv => pv.type === PipeValue.AMBIGUITY); } } exports.Modification = Modification; Modification.KNOWN_SOURCES = new Set([ "Unimod", "U", "PSI-MOD", "M", "RESID", "R", "XL-MOD", "X", "XLMOD", "GNO", "G", "MOD", "Obs", "Formula", "Glycan" ]); class GlobalModification extends Modification { constructor(value, target_residues = null, mod_type = "isotope") { if (mod_type !== "isotope" && mod_type !== "fixed") { throw new Error("Global modification type must be 'isotope' or 'fixed'"); } super(value, undefined, // position undefined, // regex_pattern undefined, // full_name "global" // mod_type ); this.modValue = new ModificationValue(value); this.targetResidues = target_residues; this.globalModType = mod_type; } toProforma() { if (this.globalModType === "isotope") { return `<${super.toProforma()}>`; } else { const mod_value = super.toProforma(); let mod_str; if (!mod_value.startsWith("[")) { mod_str = `[${mod_value}]`; } else { mod_str = mod_value; } const targets = this.targetResidues ? this.targetResidues.join(",") : ""; return `<${mod_str}@${targets}>`; } } toString() { return this.toProforma(); } } exports.GlobalModification = GlobalModification; class ModificationMap { constructor(seq, mods = [], ignore_positions = new Set(), parse_position = true, mod_position_dict) { this.modDictByName = {}; this.positionToMods = new Map(); this.seq = seq; this.ignorePositions = ignore_positions; this.modPositionDict = mod_position_dict || {}; this._buildMappings(mods, parse_position); } _buildMappings(mods, parse_position) { for (const mod of mods) { const mod_name = String(mod); this.modDictByName[mod_name] = mod; if (mod.position !== undefined && !parse_position) { const position = mod.position; if (position) { if (!this.positionToMods.has(position)) { this.positionToMods.set(position, []); } this.positionToMods.get(position).push(mod); const mod_value = mod.value; if (!this.modPositionDict[mod_value]) { this.modPositionDict[mod_value] = []; } this.modPositionDict[mod_value].push(position); } } else if (parse_position && mod.regex) { const positions = []; try { for (const [p_start, _] of mod.findPositions(this.seq)) { if (!this.ignorePositions.has(p_start)) { positions.push(p_start); if (!this.positionToMods.has(p_start)) { this.positionToMods.set(p_start, []); } this.positionToMods.get(p_start).push(mod); } } } catch (error) { // No regex pattern defined, skip position parsing } this.modPositionDict[mod_name] = positions; } } for (const mod_name in this.modPositionDict) { if (this.modPositionDict.hasOwnProperty(mod_name)) { this.modPositionDict[mod_name].sort((a, b) => a - b); } } } getModPositions(mod_name) { return this.modPositionDict[mod_name] || null; } getMod(mod_name) { return this.modDictByName[mod_name] || null; } getModsAtPosition(position) { return this.positionToMods.get(position) || []; } hasModAtPosition(position, mod_name) { const mods = this.getModsAtPosition(position); if (!mods.length) { return false; } if (mod_name === undefined) { return true; } return mods.some(mod => String(mod) === mod_name); } toDict() { return { sequence: this.seq, ignore_positions: Array.from(this.ignorePositions), position_to_mods: Object.fromEntries(Array.from(this.positionToMods.entries()) .map(([pos, mods]) => [ pos.toString(), mods.map(mod => mod.toDict()) ])) }; } } exports.ModificationMap = ModificationMap;