1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/*
* Copyright (C) 2015-2026 Mateusz Łącki and Michał Startek.
*
* This file is part of IsoSpec.
*
* IsoSpec is free software: you can redistribute it and/or modify
* it under the terms of the Simplified ("2-clause") BSD licence.
*
* IsoSpec is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the Simplified BSD Licence
* along with IsoSpec. If not, see <https://opensource.org/licenses/BSD-2-Clause>.
*/
// The library's vocabulary for "a molecule as element counts", and the one
// implementation of turning that into the isotope masses and probabilities an
// Iso is built from.
//
// This lives in its own header because three unrelated things need it: the
// formula parser (isoSpec++.cpp), the peptide-sequence parsers (fasta_mods.h),
// and the C ABI (cwrapper.cpp). It used to sit inside fasta_mods.h, which
// meant the core formula path could only share the resolution by including a
// peptide-modification header -- backwards, so it didn't, and kept its own
// copy instead. See docs/ai/composition_expansion.md.
#pragma once
#include <string_view>
#include <vector>
#include "isoSpec++.h"
namespace IsoSpec {
//! A resolved elemental composition: parallel arrays of
//! (element_table_first_index, count) pairs, one per element actually
//! present (non-zero net count).
//!
//! Counts are *signed*, and parse_fasta_with_mods/_full really can return a
//! negative one: a modification's composition delta may remove more atoms of
//! some element than the bare sequence provides ("G[UNIMOD:11]" nets H-1
//! S-1). That is a legitimate intermediate -- adding a second modification
//! could bring it back up -- so the parser does not reject it. The
//! non-negativity check belongs at the point a composition becomes a
//! molecule, and lives in build_iso_from_composition; anything else
//! consuming an ElementComposition directly (the C ABI's
//! parseFastaWithModsC, R's RParsePeptideSequence) must do its own check
//! before handing the counts to an Iso.
struct ElementComposition {
std::vector<int> element_first_index;
std::vector<int> count;
//! Empties both vectors without releasing their capacity -- a caller
//! that reuses one ElementComposition across many parse_fasta_with_mods_into
//! calls (e.g. building a cache over millions of peptides) pays for the
//! underlying heap allocation at most a handful of times total (until
//! capacity reaches this feature's real ceiling, ~30 elements), not once
//! per call the way the value-returning parse_fasta_with_mods does.
void clear() {
element_first_index.clear();
count.clear();
}
};
//! A composition expanded to the flat, per-isotope form the generic
//! Iso(dimNumber, isotopeNumbers, atomCounts, masses, probs) constructor
//! takes: one isotopeNumbers/atomCounts entry per element of the
//! composition, and one isotope_masses/isotope_probabilities entry per
//! isotope, element-major, in the composition's own order. Exactly the
//! layout the C ABI's setupIso expects, since that constructor is what it
//! forwards to.
struct ExpandedComposition {
std::vector<int> isotopeNumbers;
std::vector<int> atomCounts;
std::vector<double> isotope_masses;
std::vector<double> isotope_probabilities;
//! Empties all four vectors without releasing their capacity -- same
//! reuse contract, for the same reason, as ElementComposition::clear
//! above.
void clear() {
isotopeNumbers.clear();
atomCounts.clear();
isotope_masses.clear();
isotope_probabilities.clear();
}
};
//! A formula string read as a composition: each element once, in the order
//! written, counts signed ("H-2O-1" is accepted -- a composition may be a
//! delta; build_iso_from_composition rejects negatives when one becomes a
//! molecule). Throws std::invalid_argument for a malformed formula, an unknown
//! element or one written twice, std::out_of_range for a count past int. The
//! same tokenizer and checks as parse_formula, which goes through it.
ElementComposition parse_formula_composition(std::string_view formula);
//! Resolves each element of `composition` to its isotopes' masses and
//! probabilities from element_tables.h, filling a caller-owned `out`
//! (cleared first).
//!
//! The single implementation of that resolution. Everything that turns
//! element counts into an Iso goes through here: parse_formula (the formula
//! string path), build_iso_from_composition (the peptide-sequence path), and
//! cwrapper.h's expandCompositionC -- which exists so that a binding
//! language need not carry its own copy of the periodic table either.
//! IsoSpecPy did carry one (IsoParamsFromDict, walking PeriodicTbl's
//! symbol->isotopes dicts) until that entry point existed.
//!
//! Counts are copied through unchanged, negatives included: expanding a
//! composition is not the same as making a molecule out of one, and the
//! non-negativity check belongs at the latter -- see ElementComposition's
//! note above, and build_iso_from_composition, which does check.
void expand_composition_into(const ElementComposition& composition, ExpandedComposition& out,
bool use_nominal_masses = false);
//! Throws std::invalid_argument if any of `composition`'s counts is
//! negative, naming the offending elements. The check every path from a
//! composition to an Iso has to make: a negative atom count reaching Iso's
//! constructor is not merely a wrong answer but undefined behaviour --
//! Marginal::computeModeConf() sizes its configuration buffer from the count
//! and writeInitialConfiguration then walks off the end of it (ASan catches
//! it as a heap-buffer-overflow).
void reject_negative_counts(const ElementComposition& composition);
//! Resolves a composition (as returned by parse_fasta_with_mods[_full]) into
//! an Iso via the same generic Iso(dimNumber, isotopeNumbers, atomCounts,
//! masses, probs) constructor parse_formula uses. Throws
//! std::invalid_argument if any accumulated element count is negative (a
//! modification set that removes more atoms of some element than the base
//! sequence plus other modifications provide).
Iso build_iso_from_composition(const ElementComposition& composition, bool use_nominal_masses = false);
} // namespace IsoSpec