UNPKG

sequaljs

Version:

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

1,034 lines (1,033 loc) 39.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModdedSequenceGenerator = exports.Sequence = void 0; exports.countUniqueElements = countUniqueElements; exports.variablePositionPlacementGenerator = variablePositionPlacementGenerator; exports.orderedSerializePositionDict = orderedSerializePositionDict; exports.splitChimericProforma = splitChimericProforma; const modification_1 = require("./modification"); const amino_acid_1 = require("./amino_acid"); const proforma_1 = require("./proforma"); /** * Count unique elements in a sequence. * * @param seq - The sequence of blocks to count elements from. Each element should * have a `value` attribute and optionally a `mods` attribute. * @returns Dictionary where keys are element values and values are their counts. */ function countUniqueElements(seq) { const elements = {}; for (const item of seq) { // Count the amino acid elements[item.value] = (elements[item.value] || 0) + 1; // Count modifications if present if ('mods' in item && item.mods) { // Ensure mods is an array of Modification objects if (!Array.isArray(item.mods) || !item.mods.every(mod => mod instanceof modification_1.Modification)) { throw new Error('Invalid mods array'); } for (const mod of item.mods) { elements[mod.value] = (elements[mod.value] || 0) + 1; } } } return elements; } /** * Generate all possible position combinations for modifications. * * This function creates all possible subsets of positions, including * the empty set and the full set. * * @param positions - List of positions where modifications could be applied. * @yields Each possible combination of positions. */ function* variablePositionPlacementGenerator(positions) { if (!positions.length) { yield []; return; } // Sort positions for consistent output const sortedPositions = [...positions].sort((a, b) => a - b); // Generate all possible combinations (2^n possibilities) const n = sortedPositions.length; // Iterate through all possible binary masks (0 to 2^n - 1) for (let i = 0; i < Math.pow(2, n); i++) { const result = []; for (let j = 0; j < n; j++) { // Check if bit j is set in i if ((i & (1 << j)) !== 0) { result.push(sortedPositions[j]); } } yield result; } } /** * Serialize a dictionary of positions with consistent ordering. * * @param positions - Dictionary mapping positions to modifications or other data. * @returns JSON string with sorted keys for consistent serialization. * @throws Error if the dictionary contains values that cannot be serialized to JSON. */ function orderedSerializePositionDict(positions) { try { // Get all keys, sort them, and build a new object with sorted keys const sortedKeys = Object.keys(positions).map(Number).sort((a, b) => a - b); const sortedObj = {}; for (const key of sortedKeys) { sortedObj[key] = positions[key]; } return JSON.stringify(sortedObj); } catch (e) { throw new Error(`Could not serialize positions dictionary: ${e}`); } } class Sequence { /** * Create a sequence object. */ constructor(seq, encoder = amino_acid_1.AminoAcid, mods = {}, parse = true, parserIgnore = [], modPosition = 'right', chains = [], globalMods = [], sequenceAmbiguities = [], charge = null, ionicSpecies = null) { var _a; this.isChimeric = false; this.currentIterCount = 0; this.encoder = encoder; this.parserIgnore = parserIgnore || []; this.seq = []; this.chains = chains || [this]; this.isMultiChain = false; this.mods = new Map(); this.globalMods = globalMods || []; this.sequenceAmbiguities = sequenceAmbiguities || []; this.charge = charge; this.ionicSpecies = ionicSpecies; this.peptidoforms = []; if (seq instanceof Sequence) { // Copy attributes from existing sequence for (const [key, value] of Object.entries(seq)) { if (key !== 'mods') { this[key] = this.deepCopy(value); } } if (seq.mods instanceof Map) { for (const [pos, modList] of seq.mods.entries()) { this.mods.set(pos, this.deepCopy(modList)); } } } else { // Initialize with provided modifications if (mods) { for (const [posStr, modItems] of Object.entries(mods)) { const pos = parseInt(posStr); if (Array.isArray(modItems)) { this.mods.set(pos, this.deepCopy(modItems)); } else { if (!this.mods.has(pos)) { this.mods.set(pos, []); } (_a = this.mods.get(pos)) === null || _a === void 0 ? void 0 : _a.push(this.deepCopy(modItems)); } } } if (parse) { this._parseSequence(seq, modPosition); } } this.seqLength = this.seq.length; } /** * Create a Sequence object from a ProForma string with multi-chain support. */ static fromProforma(proformaStr) { var _a, _b, _c, _d; if (proformaStr.includes("//")) { const chains = proformaStr.split("//"); const mainSeq = Sequence.fromProforma(chains[0]); mainSeq.isMultiChain = true; mainSeq.chains = [mainSeq]; for (const chainStr of chains.slice(1)) { const chain = Sequence.fromProforma(chainStr); mainSeq.chains.push(chain); } return mainSeq; } const peptidoforms = splitChimericProforma(proformaStr); if (peptidoforms.length > 1) { const mainSeq = Sequence.fromProforma(peptidoforms[0]); mainSeq.isChimeric = true; mainSeq.peptidoforms = [mainSeq]; for (const pep of peptidoforms.slice(1)) { const peptidoform = this.fromProforma(pep); peptidoform.isChimeric = true; mainSeq.peptidoforms.push(peptidoform); } for (const chainStr of peptidoforms.slice(1)) { const chain = Sequence.fromProforma(chainStr); mainSeq.chains.push(chain); } return mainSeq; } const [baseSequence, modifications, globalMods, sequenceAmbiquities, chargeInfo] = proforma_1.ProFormaParser.parse(proformaStr); const charge = chargeInfo ? chargeInfo[0] : null; const species = chargeInfo ? chargeInfo[1] : null; const seq = new Sequence(baseSequence, amino_acid_1.AminoAcid, {}, true, [], 'right', [], globalMods, sequenceAmbiquities, charge, species); if (charge) { seq.isChimeric = true; } for (const [posStr, mods] of Object.entries(modifications)) { const pos = parseInt(posStr); for (const mod of mods) { if (pos === -1) { if (!seq.mods.has(-1)) seq.mods.set(-1, []); (_a = seq.mods.get(-1)) === null || _a === void 0 ? void 0 : _a.push(mod); } else if (pos === -2) { if (!seq.mods.has(-2)) seq.mods.set(-2, []); (_b = seq.mods.get(-2)) === null || _b === void 0 ? void 0 : _b.push(mod); } else if (pos === -3) { if (!seq.mods.has(-3)) seq.mods.set(-3, []); (_c = seq.mods.get(-3)) === null || _c === void 0 ? void 0 : _c.push(mod); } else if (pos === -4) { if (!seq.mods.has(-4)) seq.mods.set(-4, []); (_d = seq.mods.get(-4)) === null || _d === void 0 ? void 0 : _d.push(mod); } else { seq.seq[pos].addModification(mod); } } } seq.peptidoforms = [seq]; return seq; } /** * Parse the input sequence into a list of BaseBlock objects. */ _parseSequence(seq, modPosition) { let currentMod = []; let currentPosition = 0; if (modPosition !== 'left' && modPosition !== 'right') { throw new Error("modPosition must be either 'left' or 'right'"); } for (const [block, isMod] of this._sequenceIterator(seq)) { if (!isMod) { // Handle an amino acid/residue if (modPosition === 'left') { // Handle left-positioned modifications let currentUnit; if (block instanceof this.encoder) { currentUnit = block; currentUnit.position = currentPosition; } else { currentUnit = new this.encoder(block, currentPosition); } // Apply pending modifications this._applyModifications(currentUnit, currentPosition, currentMod); this.seq.push(this.deepCopy(currentUnit)); currentMod = []; } else { // modPosition === "right" // Apply modifications to previous residue if (currentMod.length > 0 && currentPosition > 0) { if (Array.isArray(this.seq) && this.seq.every(item => item instanceof amino_acid_1.AminoAcid)) { for (const mod of currentMod) { // check if this.seq array is an amino acid array this.seq[currentPosition - 1].addModification(mod); } } } // Create new residue let currentUnit; if (block instanceof this.encoder) { currentUnit = block; currentUnit.position = currentPosition; } else { currentUnit = new this.encoder(block, currentPosition); } // Apply configured modifications if (this.mods.has(currentPosition)) { const modsToApply = this.mods.get(currentPosition); if (modsToApply) { if (currentUnit instanceof amino_acid_1.AminoAcid) { for (const mod of modsToApply) { currentUnit.addModification(mod); } } } } this.seq.push(this.deepCopy(currentUnit)); currentMod = []; } currentPosition += 1; } else { // isMod is true // Handle a modification if (this.mods.size === 0) { // Only if not using predefined mods dict // Extract mod string and create Modification object const modValue = this._extractModValue(block); const modObj = new modification_1.Modification(modValue); if (modPosition === 'right' && currentPosition > 0) { // Apply directly to previous residue for right positioning if (Array.isArray(this.seq) && this.seq.every(item => item instanceof amino_acid_1.AminoAcid)) { this.seq[currentPosition - 1].addModification(modObj); } } else { // Store for later application with left positioning currentMod.push(modObj); } } } } } /** * Apply modifications to a block. */ _applyModifications(block, position, pendingMods) { // Apply pending modifications for (const mod of pendingMods) { if (block instanceof amino_acid_1.AminoAcid) { block.addModification(mod); } } // Apply configured modifications if (this.mods.has(position)) { const modsToApply = this.mods.get(position); if (modsToApply) { if (block instanceof amino_acid_1.AminoAcid) { for (const mod of modsToApply) { block.addModification(mod); } } } } } /** * Extract modification value from a string. */ _extractModValue(modStr) { if (modStr[0] in Sequence._MOD_ENCLOSURE_START && modStr[modStr.length - 1] in Sequence._MOD_ENCLOSURE_END) { return modStr.substring(1, modStr.length - 1); } return modStr; } /** * Iterate through sequence elements, identifying blocks and modifications. */ *_sequenceIterator(seq) { let modOpen = 0; let block = ''; let isMod = false; if (typeof seq === 'string') { // Convert string to array of characters seq = Array.from(seq); } for (const item of seq) { if (typeof item === 'string') { if (Sequence._MOD_ENCLOSURE_START.has(item)) { isMod = true; modOpen += 1; } else if (Sequence._MOD_ENCLOSURE_END.has(item)) { modOpen -= 1; } block += item; } else if (item instanceof this.encoder) { block = item.value; } else if (Array.isArray(item)) { // Recursively handle nested iterables yield* this._sequenceIterator(item); continue; } if (modOpen === 0 && block) { yield [block, isMod]; isMod = false; block = ''; } } } /** * Deep copy an object or array. */ deepCopy(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.deepCopy(item)); } const copy = Object.create(Object.getPrototypeOf(obj)); for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { copy[key] = this.deepCopy(obj[key]); } } return copy; } /** * Convert the sequence to ProForma format. */ toProforma() { if (this.isMultiChain) { return this.chains.map(chain => this._chainToProforma(chain)).join("//"); } else { if (this.isChimeric) { if (this.peptidoforms.length > 0) { return this.peptidoforms.map(pep => this._chainToProforma(pep)).join("+"); } } return this._chainToProforma(this); } } /** * Convert a chain to ProForma format. */ _chainToProforma(chain) { let result = ""; // Add global modifications for (const mod of this.globalMods) { result += mod.toProforma(); } // Handle position ranges and modifications const ranges = []; for (let i = 0; i < this.seq.length; i++) { const aa = this.seq[i]; if (aa instanceof amino_acid_1.AminoAcid) { for (const mod of aa.mods) { if (mod.inRange && mod.rangeStart !== undefined && mod.rangeStart !== null && mod.rangeEnd !== undefined && mod.rangeEnd !== null) { ranges.push([mod.rangeStart, mod.rangeEnd, mod]); } } } } ranges.sort((a, b) => a[0] - b[0]); // Handle unknown position modifications if (chain.mods.has(-4)) { const unknownModsByValue = new Map(); const chainMods = chain.mods.get(-4); if (chainMods) { for (const mod of chainMods) { const modProforma = mod.toProforma(); unknownModsByValue.set(modProforma, (unknownModsByValue.get(modProforma) || 0) + 1); } } for (const [modValue, count] of unknownModsByValue.entries()) { let ambiguityStr = ""; if (count > 1) { ambiguityStr += `[${modValue}]`; ambiguityStr += `^${count}?`; } else { ambiguityStr = `[${modValue}]`; ambiguityStr += `?`; } result += ambiguityStr; } } // Handle labile modifications const labileChainMods = chain.mods.get(-3); if (labileChainMods) { for (const mod of labileChainMods) { if (mod.modType === "labile") { result += `{${mod.toProforma()}}`; } } } // Handle N-terminal modifications const nChainMod = chain.mods.get(-1); if (nChainMod) { let nModStr = ""; for (const mod of nChainMod) { const modStr = `[${mod.toProforma()}]`; nModStr += modStr; } if (nModStr) { result += nModStr + "-"; } } // Handle sequence ambiguities const sortedAmbiguities = [...this.sequenceAmbiguities].sort((a, b) => a.position - b.position); let ambiguityIndex = 0; // Process each amino acid in the sequence let rangeStart = false; for (let i = 0; i < chain.seq.length; i++) { // Check for ambiguity at this position if (ambiguityIndex < sortedAmbiguities.length && sortedAmbiguities[ambiguityIndex].position === i) { const ambiguity = sortedAmbiguities[ambiguityIndex]; result += `(?${ambiguity.value})`; ambiguityIndex++; } // Check for range start if (!rangeStart) { for (const [start, end, mod] of ranges) { if (i === start) { result += "("; rangeStart = true; break; } } } // Add amino acid value result += chain.seq[i].value; // Add modifications for this position let modStrData = ""; if (chain.seq[i].mods && chain.seq[i].mods.length > 0) { const crosslinkRefsAdded = new Set(); let branchRefsAdded = false; for (const mod of chain.seq[i].mods) { let thisModStr = mod.toProforma(); if (rangeStart && mod.inRange) { continue; } if (mod.modType === "ambiguous") { if (!mod.hasAmbiguity) { if (mod.inRange) { thisModStr = `[${thisModStr}]`; } else { thisModStr = `{${thisModStr}}`; } } else { thisModStr = `[${thisModStr}]`; } } else { thisModStr = `[${thisModStr}]`; } modStrData += thisModStr; } result += modStrData; } // Check for range end if (rangeStart) { for (const [start, end, mod] of ranges) { if (i === end) { result += ")"; rangeStart = false; break; } } if (!rangeStart) { result = this.getModAndAddToString(chain.seq[i], result); } } } // Handle C-terminal modifications const cChainMod = chain.mods.get(-2); if (cChainMod) { let nModStr = ""; for (const mod of cChainMod) { const modStr = `[${mod.toProforma()}]`; nModStr += modStr; } if (nModStr) { result += "-" + nModStr; } } if (chain.charge) { result += `/${chain.charge}`; if (chain.ionicSpecies) { result += `[${chain.ionicSpecies}]`; } } return result; } /** * Get modifications and add to string. */ getModAndAddToString(aa, result) { let modStr = ""; if (aa.mods && aa.mods.length > 0) { for (const mod of aa.mods) { let thisModStr = mod.toProforma(); if (mod.modType === "ambiguous") { if (!mod.hasAmbiguity) { if (mod.inRange) { thisModStr = `[${thisModStr}]`; } else { thisModStr = `{${thisModStr}}`; } } else { thisModStr = `[${thisModStr}]`; } } else { thisModStr = `[${thisModStr}]`; } modStr += thisModStr; } result += modStr; } return result; } /** * Add info tags to the result string based on the modification. */ _addInfoTags(result, mod) { let infoStr = ""; const addedInfo = new Set(); let strippedResult = result.startsWith("[") ? result.substring(1) : result; strippedResult = strippedResult.endsWith("]") ? strippedResult.substring(0, strippedResult.length - 1) : strippedResult; addedInfo.add(strippedResult); for (const i of strippedResult.split(":", 2)) { const iSplitted = i.split("#", 2); if (iSplitted.length > 1) { for (const i2 of iSplitted) { if (i2 && !addedInfo.has(i2)) { addedInfo.add(i2); } } } addedInfo.add(i); } if (mod.mass !== undefined) { const massStr = mod.mass > 0 ? `+${mod.mass}` : `${mod.mass}`; if (!addedInfo.has(massStr)) { infoStr += `|${massStr}`; addedInfo.add(massStr); } } if (mod.synonyms) { for (const synonym of mod.synonyms) { if (!addedInfo.has(synonym)) { infoStr += `|${synonym}`; addedInfo.add(synonym); } } } if (mod.observedMass) { const obsStr = mod.observedMass > 0 ? `Obs:+${mod.observedMass}` : `Obs:${mod.observedMass}`; if (!addedInfo.has(obsStr)) { infoStr += `|${obsStr}`; addedInfo.add(obsStr); } } if (mod.infoTags) { for (const tag of mod.infoTags) { if (!addedInfo.has(tag)) { infoStr += `|${tag}`; addedInfo.add(tag); } } } if (infoStr) { if (result.endsWith("]")) { result = result.substring(0, result.length - 1) + infoStr + "]"; } else { result += infoStr; } } return result; } /** * Get item or slice from sequence. */ getItem(key) { if (Array.isArray(key)) { const [start, end] = key; const newSeq = new Sequence(this.seq, this.encoder, {}, false); newSeq.seq = this.seq.slice(start, end); newSeq.seqLength = newSeq.seq.length; return newSeq; } return this.seq[key]; } /** * Get length of sequence. */ get length() { return this.seqLength; } /** * String representation of sequence. */ toString() { return this.seq.map(block => block.toString()).join(""); } /** * Programmatic representation of sequence. */ toRepr() { return `Sequence('${this.toString()}')`; } /** * Make the sequence iterable. */ [Symbol.iterator]() { let index = 0; const seq = this.seq; return { next() { if (index < seq.length) { return { value: seq[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } /** * Check if two sequences are equal. */ equals(other) { if (!(other instanceof Sequence)) { return false; } if (this.seqLength !== other.seqLength) { return false; } for (let i = 0; i < this.seqLength; i++) { if (!this.seq[i].equals(other.seq[i])) { return false; } } return true; } /** * Add modifications to residues at specified positions. */ addModifications(modDict) { for (const aa of this.seq) { if (aa.position && aa instanceof amino_acid_1.AminoAcid) { if (aa.position in modDict) { for (const mod of modDict[aa.position]) { aa.addModification(mod); } } } } } /** * Return the sequence as a string without any modification annotations. */ toStrippedString() { return this.seq.map(block => block.value).join(""); } /** * Customize the sequence string with annotations. */ toStringCustomize(data, annotationPlacement = 'right', blockSeparator = "", annotationEncloseCharacters = ["[", "]"], individualAnnotationEnclose = false, individualAnnotationEncloseCharacters = ["[", "]"], individualAnnotationSeparator = "") { if (annotationPlacement !== 'left' && annotationPlacement !== 'right') { throw new Error("annotationPlacement must be either 'left' or 'right'"); } const elements = []; for (let i = 0; i < this.seq.length; i++) { // Add annotation before residue if placement is left if (annotationPlacement === 'left' && i in data) { const annotation = this._formatAnnotation(data[i], individualAnnotationEnclose, individualAnnotationEncloseCharacters, individualAnnotationSeparator, annotationEncloseCharacters); elements.push(annotation); } // Add residue elements.push(this.seq[i].value); // Add annotation after residue if placement is right if (annotationPlacement === 'right' && i in data) { const annotation = this._formatAnnotation(data[i], individualAnnotationEnclose, individualAnnotationEncloseCharacters, individualAnnotationSeparator, annotationEncloseCharacters); elements.push(annotation); } } return elements.join(blockSeparator); } /** * Format annotation strings. */ _formatAnnotation(annotations, individualEnclose, individualEncloseChars, separator, groupEncloseChars) { let annText; if (typeof annotations === 'string') { annText = annotations; } else { if (individualEnclose) { const enclosedAnnotations = annotations.map(item => `${individualEncloseChars[0]}${item}${individualEncloseChars[1]}`); annText = enclosedAnnotations.join(separator); } else { annText = annotations.join(separator); } } if (groupEncloseChars) { return `${groupEncloseChars[0]}${annText}${groupEncloseChars[1]}`; } return annText; } /** * Find positions in the sequence that match a given regex motif. */ *findWithRegex(motif, ignore) { const pattern = new RegExp(motif, 'g'); let seqStr; if (ignore) { // Build string excluding ignored positions seqStr = this.seq .filter((_, i) => i < ignore.length && !ignore[i]) .map(aa => aa.value) .join(''); } else { seqStr = this.toStrippedString(); } let match; while ((match = pattern.exec(seqStr)) !== null) { if (!match.groups || Object.keys(match.groups).length === 0) { yield [match.index, match.index + match[0].length]; } else { for (let i = 1; i < match.length; i++) { if (match[i]) { const groupStart = seqStr.indexOf(match[i], match.index); yield [groupStart, groupStart + match[i].length]; } } } } } /** * Identify gaps in the sequence. */ gaps() { return this.seq.map(block => block.value === "-"); } /** * Count occurrences of a character in a range. */ count(char, start, end) { const subStr = this.toStrippedString().substring(start, end); return (subStr.match(new RegExp(char, 'g')) || []).length; } /** * Convert the sequence to a dictionary representation. */ toDict() { // Collect all modifications by position const modsByPosition = {}; for (let i = 0; i < this.seq.length; i++) { const aa = this.seq[i]; if (aa instanceof amino_acid_1.AminoAcid) { if (aa.mods && aa.mods.length > 0) { modsByPosition[i] = aa.mods.map(mod => mod.toDict()); } } } return { sequence: this.toStrippedString(), modifications: modsByPosition }; } } exports.Sequence = Sequence; // Regular expression patterns for parsing Sequence._MOD_PATTERN = /[\(|\[]+([^\)]+)[\)|\]]+/; Sequence._MOD_ENCLOSURE_START = new Set(['(', '[', '{']); Sequence._MOD_ENCLOSURE_END = new Set([')', ']', '}']); /** * Generator for sequences with different modification combinations. * * This class creates all possible modified sequences by applying combinations * of static and variable modifications to a base sequence. */ class ModdedSequenceGenerator { /** * Initialize a ModdedSequenceGenerator object. * * @param seq - The base sequence to modify * @param variableMods - List of variable modifications to apply * @param staticMods - List of static modifications to apply * @param usedScenarios - Set of serialized modification scenarios to avoid duplicates * @param parseModPosition - Whether to parse positions using modification regex patterns * @param modPositionDict - Pre-computed positions for modifications * @param ignorePosition - Set of positions to ignore when applying modifications */ constructor(seq, variableMods = [], staticMods = [], usedScenarios = new Set(), parseModPosition = true, modPositionDict = {}, ignorePosition = new Set()) { this.seq = seq; this.staticMods = staticMods || []; this.variableMods = variableMods || []; this.usedScenariosSet = usedScenarios || new Set(); this.ignorePosition = ignorePosition || new Set(); this.staticModPositionDict = {}; this.variableMapScenarios = {}; // Initialize modification maps and position dictionaries if (this.staticMods.length > 0) { this.staticMap = new modification_1.ModificationMap(seq, this.staticMods, new Set(), // Initially no ignored positions for static mods parseModPosition, modPositionDict); this.staticModPositionDict = this._generateStaticModPositions(); // Update ignore positions with static mod positions for (const pos of Object.keys(this.staticModPositionDict).map(Number)) { this.ignorePosition.add(pos); } } if (this.variableMods.length > 0) { // Create variable modification map, considering ignored positions this.variableMap = new modification_1.ModificationMap(seq, this.variableMods, this.ignorePosition, parseModPosition, modPositionDict); } } /** * Generate all possible modification combinations. */ *generate() { if (this.variableMods.length === 0) { // If only static mods, yield the single scenario if not already used const serialized = orderedSerializePositionDict(this.staticModPositionDict); if (!this.usedScenariosSet.has(serialized)) { this.usedScenariosSet.add(serialized); yield this.staticModPositionDict; } return; } // Generate all variable mod scenarios this._generateVariableModScenarios(); // Explore all combinations for (const variableScenario of this._exploreScenarios()) { // Combine static and variable modifications const combinedScenario = Object.assign({}, this.staticModPositionDict); // Update with variable modifications for (const [posStr, mods] of Object.entries(variableScenario)) { const pos = Number(posStr); if (pos in combinedScenario) { combinedScenario[pos] = [...combinedScenario[pos], ...mods]; } else { combinedScenario[pos] = [...mods]; } } // Serialize to check for duplicates const serialized = orderedSerializePositionDict(combinedScenario); if (!this.usedScenariosSet.has(serialized)) { this.usedScenariosSet.add(serialized); yield combinedScenario; } } } /** * Generate dictionary of positions for static modifications. */ _generateStaticModPositions() { const positionDict = {}; for (const mod of this.staticMods) { if (!this.staticMap) continue; const positions = this.staticMap.getModPositions(String(mod.value)); if (positions && positions.length > 0) { for (const position of positions) { if (!(position in positionDict)) { positionDict[position] = []; } positionDict[position].push(mod); } } } return positionDict; } /** * Generate all possible position combinations for variable modifications. */ _generateVariableModScenarios() { this.variableMapScenarios = {}; for (const mod of this.variableMods) { if (!this.variableMap) continue; const positions = this.variableMap.getModPositions(String(mod.value)) || []; if (!mod.allFilled) { // Generate all possible subsets of positions const scenarios = Array.from(variablePositionPlacementGenerator(positions)); this.variableMapScenarios[mod.value] = scenarios; } else { // For allFilled mods, only empty list or all positions are valid this.variableMapScenarios[mod.value] = [[], positions]; } } } /** * Recursively explore all possible modification scenarios. * * @param currentModIdx - Index of the current modification being processed * @param currentScenario - Current scenario being built */ *_exploreScenarios(currentModIdx = 0, currentScenario = {}) { // Base case: processed all modifications if (currentModIdx >= this.variableMods.length) { yield Object.assign({}, currentScenario); return; } const currentMod = this.variableMods[currentModIdx]; const positionCombinations = this.variableMapScenarios[currentMod.value] || [[]]; for (const positions of positionCombinations) { // Create a copy of the current scenario const scenarioCopy = this._deepCopyScenario(currentScenario); // Add current modification to positions for (const pos of positions) { if (!(pos in scenarioCopy)) { scenarioCopy[pos] = []; } // Create a deep copy of the modification scenarioCopy[pos].push(this._deepCopyModification(currentMod)); } // Recursively continue with next modification yield* this._exploreScenarios(currentModIdx + 1, scenarioCopy); } } /** * Create a deep copy of a modification scenario. */ _deepCopyScenario(scenario) { const copy = {}; for (const [pos, mods] of Object.entries(scenario)) { copy[Number(pos)] = mods.map(mod => this._deepCopyModification(mod)); } return copy; } /** * Create a deep copy of a modification. */ _deepCopyModification(mod) { return Object.assign(Object.create(Object.getPrototypeOf(mod)), JSON.parse(JSON.stringify(mod))); } } exports.ModdedSequenceGenerator = ModdedSequenceGenerator; function splitChimericProforma(proformaStr) { const parts = []; let currentPartStart = 0; let bracketLevel = 0; for (let i = 0; i < proformaStr.length; i++) { const char = proformaStr[i]; if (char === '[' || char === '{' || char === '(') { bracketLevel++; } else if (char === ']' || char === '}' || char === ')') { // Ensure bracket level doesn't go below zero due to malformed strings bracketLevel = Math.max(0, bracketLevel - 1); } else if (char === '+' && bracketLevel === 0) { // Found a separator '+' outside of any brackets parts.push(proformaStr.substring(currentPartStart, i).trim()); currentPartStart = i + 1; } } // Add the last part of the string (or the only part if no '+' was found) parts.push(proformaStr.substring(currentPartStart).trim()); // Filter out any empty strings return parts.filter(part => part.length > 0); }