IsoSpecPy 2.5.0__py3-none-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. IsoSpecPy/Advanced.py +32 -0
  2. IsoSpecPy/Distributions.py +93 -0
  3. IsoSpecPy/Formulas.py +71 -0
  4. IsoSpecPy/IsoSpec++/aligned_ptr.h +638 -0
  5. IsoSpecPy/IsoSpec++/allocator.h +69 -0
  6. IsoSpecPy/IsoSpec++/btrd.h +206 -0
  7. IsoSpecPy/IsoSpec++/composition.h +126 -0
  8. IsoSpecPy/IsoSpec++/conf.h +38 -0
  9. IsoSpecPy/IsoSpec++/cwrapper.h +271 -0
  10. IsoSpecPy/IsoSpec++/dirtyAllocator.h +58 -0
  11. IsoSpecPy/IsoSpec++/element_lookup.h +36 -0
  12. IsoSpecPy/IsoSpec++/element_tables.h +50 -0
  13. IsoSpecPy/IsoSpec++/fasta.h +70 -0
  14. IsoSpecPy/IsoSpec++/fasta_mods.h +70 -0
  15. IsoSpecPy/IsoSpec++/fixedEnvelopes.h +418 -0
  16. IsoSpecPy/IsoSpec++/isa_kernel_impl.h +192 -0
  17. IsoSpecPy/IsoSpec++/isa_kernels.h +172 -0
  18. IsoSpecPy/IsoSpec++/isoMath.h +121 -0
  19. IsoSpecPy/IsoSpec++/isoSpec++.h +801 -0
  20. IsoSpecPy/IsoSpec++/iso_simd.h +48 -0
  21. IsoSpecPy/IsoSpec++/marginalTrek++.h +614 -0
  22. IsoSpecPy/IsoSpec++/misc.h +229 -0
  23. IsoSpecPy/IsoSpec++/mman.h +67 -0
  24. IsoSpecPy/IsoSpec++/operators.h +150 -0
  25. IsoSpecPy/IsoSpec++/platform.h +180 -0
  26. IsoSpecPy/IsoSpec++/platform_incl.h +33 -0
  27. IsoSpecPy/IsoSpec++/pod_vector.h +400 -0
  28. IsoSpecPy/IsoSpec++/summator.h +118 -0
  29. IsoSpecPy/IsoSpec++/unimod.h +80 -0
  30. IsoSpecPy/IsoSpec++/unimod_support.h +2169 -0
  31. IsoSpecPy/IsoSpec++/unimod_table_data.h +1027 -0
  32. IsoSpecPy/IsoSpecPy.py +1088 -0
  33. IsoSpecPy/IsoSpecPyOld.py +290 -0
  34. IsoSpecPy/PeriodicTbl.py +51 -0
  35. IsoSpecPy/__init__.py +19 -0
  36. IsoSpecPy/__main__.py +25 -0
  37. IsoSpecPy/approximations.py +122 -0
  38. IsoSpecPy/confs_passthrough.py +16 -0
  39. IsoSpecPy/isoFFI.py +290 -0
  40. bin/IsoSpecCppPy.dll +0 -0
  41. isospecpy-2.5.0.dist-info/METADATA +220 -0
  42. isospecpy-2.5.0.dist-info/RECORD +45 -0
  43. isospecpy-2.5.0.dist-info/WHEEL +5 -0
  44. isospecpy-2.5.0.dist-info/licenses/LICENCE +35 -0
  45. lib/IsoSpecCppPy.lib +0 -0
IsoSpecPy/IsoSpecPy.py ADDED
@@ -0,0 +1,1088 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2015-2020 Mateusz Łącki and Michał Startek.
4
+ #
5
+ # This file is part of IsoSpec.
6
+ #
7
+ # IsoSpec is free software: you can redistribute it and/or modify
8
+ # it under the terms of the Simplified ("2-clause") BSD licence.
9
+ #
10
+ # IsoSpec is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
+ #
14
+ # You should have received a copy of the Simplified BSD Licence
15
+ # along with IsoSpec. If not, see <https://opensource.org/licenses/BSD-2-Clause>.
16
+ #
17
+
18
+ from .isoFFI import isoFFI
19
+ import re
20
+ import types
21
+ from . import PeriodicTbl
22
+ from .confs_passthrough import ConfsPassthrough
23
+ from collections import namedtuple, OrderedDict
24
+ import math
25
+
26
+ regex_pattern = re.compile('([A-Z][a-z]?)(-?[0-9]*)')
27
+ ParsedFormula = namedtuple('ParsedFormula', 'atomCounts masses probs elems')
28
+
29
+
30
+
31
+
32
+ def ParseFormula(formula):
33
+ """Parse a chemical formula.
34
+
35
+ Args:
36
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O"
37
+
38
+ Returns:
39
+ A tuple containing element symbols and atomCounts of elements in the parsed formula.
40
+ """
41
+ global regex_pattern
42
+
43
+ ret = OrderedDict()
44
+
45
+ last = 0
46
+ for match in re.finditer(regex_pattern, formula):
47
+ elem, cnt = match.groups()
48
+ if elem in ret:
49
+ raise ValueError("""Invalid formula: {} (repeating element: "{}")""".format(formula, elem))
50
+ ret[elem] = int(cnt) if cnt != '' else 1
51
+ if last!=match.start():
52
+ raise ValueError("""Invalid formula: {} (garbage inside: "{}")""".format(formula, formula[last:match.start()]))
53
+ if elem not in PeriodicTbl.symbol_to_masses:
54
+ raise ValueError("""Invalid formula: {} (unknown element symbol: "{}")""".format(formula, elem))
55
+ last = match.end()
56
+
57
+ if len(formula) != last:
58
+ raise ValueError('''Invalid formula: {} (trailing garbage: "{}")'''.format(formula, formula[last:]))
59
+
60
+ if len(ret) == 0:
61
+ raise ValueError("Invalid formula (empty)")
62
+
63
+ return ret
64
+
65
+ def ParsePeptideSequence(sequence, unimod_db_path=None):
66
+ """Parse a peptide sequence, recognizing SAGE-style [UNIMOD:<id>]
67
+ modification brackets: `[UNIMOD:<id>]-SEQUENCE` for an N-terminal mod,
68
+ `X[UNIMOD:<id>]` for an internal one (immediately after the modified
69
+ residue), `SEQUENCE-[UNIMOD:<id>]` for a C-terminal one -- exactly the
70
+ notation/placement this monorepo's SAGE fork writes
71
+ (crates/sage/src/peptide.rs). Everything parse_fasta_c already tolerated
72
+ (spacers, whitespace, indeterminate-formula codes) is still silently
73
+ ignored; only '[' is now meaningful, and must open a well-formed bracket.
74
+
75
+ Despite this function's name being the honest one -- unlike ParseFASTA
76
+ below (kept only for backward compatibility), this has never parsed an
77
+ actual FASTA file (no '>' header, no multi-record support); it has only
78
+ ever accepted a bare peptide sequence string. Co-author Michał Startek
79
+ has completely no idea how things are supposed to be called; see
80
+ docs/ai/unimod.md.
81
+
82
+ Args:
83
+ sequence (str): a peptide sequence, optionally annotated with
84
+ [UNIMOD:<id>] modification brackets.
85
+ unimod_db_path (str, optional): path to an override Unimod
86
+ composition-delta table (same CSV shape as data/unimod.csv).
87
+ None (default) uses the table embedded at compile time.
88
+
89
+ Returns:
90
+ An OrderedDict of element symbol -> atom count, e.g. {"C": 42, "H":
91
+ 66, "N": 10, "O": 17, "S": 2} for a Carbamidomethyl-modified
92
+ sequence -- general over whatever elements the sequence's
93
+ modifications touch (not just CHNOSSe, unlike the plain
94
+ amino-acid-only path this supersedes).
95
+
96
+ Two properties of that dict are worth knowing before you rely on
97
+ them. Only elements with a non-zero net count appear: a sequence with
98
+ no sulphur has no "S" key at all, rather than "S": 0. And key order
99
+ differs between the two internal paths -- a bracket-free sequence
100
+ comes back in CHNOSSe order, one carrying a [UNIMOD:<id>] bracket in
101
+ element-table (atomic number) order, since the general path
102
+ accumulates over the whole periodic table. Treat the result as a
103
+ mapping, not as a sequence of pairs in a promised order.
104
+
105
+ Like parse_fasta_c before it, this returns the *residue* composition:
106
+ the terminal H2O of a free peptide is NOT included, so the masses of
107
+ an Iso built from it are 18.0106 Da lighter than the neutral peptide.
108
+ Add water explicitly if you want the real molecule -- e.g.
109
+ IsoSpecPy.IsoTotalProb(0.99, peptide_sequence=seq, formula="H2O1").
110
+ (The C++ Iso::FromFASTAWithMods takes an add_water argument and
111
+ defaults it to True, so the two differ by a water unless you say so.)
112
+
113
+ Counts are signed: a modification whose delta removes more atoms of
114
+ an element than the bare sequence provides gives a negative count
115
+ (ParsePeptideSequence("G[UNIMOD:11]") -> H: -1, S: -1). That is
116
+ rejected later, when a composition is turned into an Iso, not here.
117
+
118
+ Raises:
119
+ ValueError: the sequence contains a malformed [UNIMOD:<id>] bracket,
120
+ or references an id not present in the active table (covers both
121
+ a genuinely unknown id and one this fork's shipped table
122
+ deliberately excludes -- isotope-labeled or glycan/
123
+ derivatization "brick" modifications, see docs/ai/unimod.md --
124
+ alike; both are simply not resolvable to an atom-count delta).
125
+ """
126
+ if isinstance(sequence, str):
127
+ sequence = sequence.encode("ascii")
128
+ db_path = isoFFI.ffi.NULL if unimod_db_path is None else str(unimod_db_path).encode("ascii")
129
+
130
+ composition = isoFFI.clib.parseFastaWithModsC(sequence, db_path)
131
+ if composition == isoFFI.ffi.NULL:
132
+ raise ValueError(
133
+ "Invalid peptide sequence or unresolvable [UNIMOD:<id>] reference: {}".format(sequence)
134
+ )
135
+ try:
136
+ size = isoFFI.clib.compositionSizeC(composition)
137
+ symbols = isoFFI.clib.compositionSymbolsC(composition)
138
+ counts = isoFFI.clib.compositionCountsC(composition)
139
+ od = OrderedDict()
140
+ for i in range(size):
141
+ symbol = isoFFI.ffi.string(symbols[i]).decode("ascii")
142
+ od[symbol] = counts[i]
143
+ return od
144
+ finally:
145
+ isoFFI.clib.deleteCompositionC(composition)
146
+
147
+
148
+ def ParseFASTA(fasta, unimod_db_path=None):
149
+ """Compatibility alias for ParsePeptideSequence -- see its docstring for
150
+ the naming note. Prior to this version, this function ignored '[',
151
+ digits, and ':' in its input entirely, silently mis-parsing any
152
+ UNIMOD-bracket-containing string as extra phantom amino acids (every
153
+ letter in "UNIMOD" happens to be a valid 1-letter amino-acid code); that
154
+ was a bug, not a feature, and is now fixed by routing through the same
155
+ mods-aware parser as ParsePeptideSequence.
156
+
157
+ A bracket-free sequence yields the same counts, in the same CHNOSSe
158
+ order, as it always did -- with one difference: elements whose count is
159
+ zero are no longer emitted. This function used to return "C", "H", "N",
160
+ "O" and "S" keys unconditionally (plus "Se" when non-zero), so
161
+ ParseFASTA("A")["S"] was 0 and is now a KeyError. Use .get(sym, 0).
162
+ """
163
+ return ParsePeptideSequence(fasta, unimod_db_path=unimod_db_path)
164
+
165
+
166
+ # (symbol, use_nominal_masses) -> (masses, probs), each a tuple over that
167
+ # element's isotopes. Filled lazily from expandCompositionC, so every value in
168
+ # here is what the C++ tables said -- this is a memo of the C++ resolution, not
169
+ # a second copy of it. Without it, every IsoParamsFromDict call paid for the
170
+ # cffi round trip (per-symbol ffi.new, the accessor calls, three ffi.unpack):
171
+ # 24.7us per call for a 5-element formula against 3.3us for the plain dict
172
+ # lookups it replaced, of which the C++ expansion itself is only 2.6us.
173
+ # Unknown symbols raise and are never stored, so they fail on every call.
174
+ _isotopes_by_symbol = {}
175
+
176
+
177
+ def _resolve_symbol(symbol, use_nominal_masses):
178
+ ffi = isoFFI.ffi
179
+ try:
180
+ symbol_buf = ffi.new("char[]", symbol.encode("ascii"))
181
+ except (UnicodeEncodeError, AttributeError):
182
+ raise ValueError("Invalid formula (element symbols must be ASCII strings)")
183
+ # The "char[]" belongs to cffi and must outlive the call: the "char*[]"
184
+ # holds a borrowed pointer into it.
185
+ symbol_ptrs = ffi.new("char*[]", [symbol_buf])
186
+ # The count is irrelevant to the isotopes: expand_composition_into copies
187
+ # it through untouched.
188
+ count_buf = ffi.new("int[]", [1])
189
+
190
+ expanded = isoFFI.clib.expandCompositionC(symbol_ptrs, count_buf, 1, use_nominal_masses)
191
+ if expanded == ffi.NULL:
192
+ raise ValueError("Invalid formula")
193
+ try:
194
+ # ffi.unpack, not ffi.cast("double[N]", ...): N varies with the
195
+ # element and cffi's type cache is keyed on the exact type string, so
196
+ # embedding a runtime size in one defeats that cache on every call.
197
+ n = isoFFI.clib.expandedIsotopeNumbersC(expanded)[0]
198
+ masses = tuple(ffi.unpack(isoFFI.clib.expandedIsotopeMassesC(expanded), n))
199
+ probs = tuple(ffi.unpack(isoFFI.clib.expandedIsotopeProbabilitiesC(expanded), n))
200
+ finally:
201
+ isoFFI.clib.deleteExpandedCompositionC(expanded)
202
+
203
+ return masses, probs
204
+
205
+
206
+ def IsoParamsFromDict(formula, use_nominal_masses = False):
207
+ """Produces a set of IsoSpec parameters from a chemical formula.
208
+
209
+ The symbol-to-isotopes resolution happens in C++ (cwrapper.h's
210
+ expandCompositionC -> composition.h's expand_composition_into), not here.
211
+ This function used to walk PeriodicTbl's dicts itself, which made it a
212
+ second implementation of a rule the C++ core already owned -- and it
213
+ existed only because the C ABI had no entry point between setupIso (which
214
+ demands the caller already know every isotope mass) and the
215
+ sequence-only isoFromFasta. It has one now. Each symbol is resolved
216
+ through it once and memoized (_isotopes_by_symbol).
217
+
218
+ Args:
219
+ formula (dict): a parsed chemical formula, e.g. {"C": 2, "H": 6, "O": 1}
220
+ use_nominal_masses (boolean): use masses of elements rounded to integer numbers (nominal masses)
221
+
222
+ Returns:
223
+ ParsedFormula: a tuple containing atomCounts, masses and marginal probabilities of elements in the parsed formula.
224
+ Element order follows the dict's own iteration order, as it always did.
225
+
226
+ Raises:
227
+ ValueError: an element symbol the library's tables don't know.
228
+ """
229
+
230
+ use_nominal_masses = bool(use_nominal_masses)
231
+ symbols, atomCounts, masses, probs = [], [], [], []
232
+ for symbol, atomCount in formula.items():
233
+ key = (symbol, use_nominal_masses)
234
+ resolved = _isotopes_by_symbol.get(key)
235
+ if resolved is None:
236
+ resolved = _resolve_symbol(symbol, use_nominal_masses)
237
+ _isotopes_by_symbol[key] = resolved
238
+ symbols.append(symbol)
239
+ atomCounts.append(atomCount)
240
+ masses.append(resolved[0])
241
+ probs.append(resolved[1])
242
+
243
+ return ParsedFormula(atomCounts, masses, probs, symbols)
244
+
245
+
246
+ def IsoParamsFromFormula(formula, use_nominal_masses = False):
247
+ """Produces a set of IsoSpec parameters from a chemical formula.
248
+
249
+ Args:
250
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O"
251
+ use_nominal_masses (boolean): use masses of elements rounded to integer numbers (nominal masses)
252
+
253
+ Returns:
254
+ ParsedFormula: a tuple containing atomCounts, masses and marginal probabilities of elements in the parsed formula.
255
+ """
256
+ parsed = ParseFormula(formula)
257
+ return IsoParamsFromDict(parsed, use_nominal_masses = use_nominal_masses)
258
+
259
+
260
+ class Iso(object):
261
+ """Virtual class representing an isotopic distribution."""
262
+ def __init__(self, formula="",
263
+ get_confs=False,
264
+ atomCounts=None,
265
+ isotopeMasses=None,
266
+ isotopeProbabilities=None,
267
+ use_nominal_masses = False,
268
+ fasta = "",
269
+ charge = 1.0,
270
+ # New arguments go after charge, never between fasta and it:
271
+ # this signature's positional order is public, and inserting
272
+ # here would silently shift charge for anyone passing it
273
+ # positionally.
274
+ peptide_sequence = "",
275
+ unimod_db_path = None):
276
+ """Initialize Iso.
277
+
278
+ Args:
279
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
280
+ get_confs (boolean): should we report counts of isotopologues?
281
+ atomCounts (list): a list of atom counts (alternative to 'formula').
282
+ isotopeMasses (list): a list of lists of masses of elements with counts in 'atomCounts'.
283
+ isotopeProbabilities (list): a list of lists of probabilities of elements with counts in 'atomCounts'.
284
+ use_nominal_masses (boolean): should the masses be rounded to the closest integer values.
285
+ fasta (str): a peptide sequence, optionally with [UNIMOD:<id>] modification
286
+ brackets -- see ParsePeptideSequence's docstring (this is the same argument
287
+ as peptide_sequence, kept under its original name for backward compatibility;
288
+ despite the name, this has never accepted an actual FASTA file).
289
+ charge (float): charge state of the molecule: all masses will be divided by this value to obtain the m/z values.
290
+ peptide_sequence (str): the honestly-named alias of 'fasta' -- pass one or the
291
+ other, not both.
292
+ unimod_db_path (str, optional): override Unimod composition-delta table for
293
+ resolving [UNIMOD:<id>] references in 'fasta'/'peptide_sequence' (same CSV
294
+ shape as data/unimod.csv). None (default) uses the compile-time embedded table.
295
+
296
+ Note that neither 'fasta' nor 'peptide_sequence' adds the terminal H2O of a free
297
+ peptide -- see ParsePeptideSequence's docstring. Pass formula="H2O1" alongside the
298
+ sequence for the neutral molecule's masses.
299
+ """
300
+
301
+ self.iso = None
302
+
303
+ if len(fasta) > 0 and len(peptide_sequence) > 0:
304
+ raise ValueError(
305
+ "'fasta' and 'peptide_sequence' are the same argument under two names -- pass only one"
306
+ )
307
+ sequence = peptide_sequence if len(peptide_sequence) > 0 else fasta
308
+
309
+ if len(sequence) > 0:
310
+ molecule = ParsePeptideSequence(sequence, unimod_db_path=unimod_db_path)
311
+ else:
312
+ molecule = OrderedDict()
313
+
314
+ if len(formula) > 0:
315
+ if isinstance(formula, dict):
316
+ df = formula
317
+ else:
318
+ df = ParseFormula(formula)
319
+ for symbol, count in df.items():
320
+ molecule[symbol] = molecule.get(symbol, 0) + count
321
+
322
+ for sym, cnt in molecule.items():
323
+ if cnt < 0:
324
+ raise Exception("Negative count of element " + sym + ": " + str(cnt))
325
+
326
+ if len(molecule) == 0 and not all([atomCounts, isotopeMasses, isotopeProbabilities]):
327
+ raise Exception("Either formula, fasta or ALL of: atomCounts, isotopeMasses, isotopeProbabilities must not be None")
328
+
329
+ if len(molecule) > 0:
330
+ self.atomCounts, self.isotopeMasses, self.isotopeProbabilities, _ = IsoParamsFromDict(molecule, use_nominal_masses = use_nominal_masses)
331
+ else:
332
+ self.atomCounts, self.isotopeMasses, self.isotopeProbabilities = [], [], []
333
+
334
+ if not (atomCounts is None):
335
+ self.atomCounts.extend(atomCounts)
336
+
337
+ if not (isotopeMasses is None):
338
+ self.isotopeMasses.extend(isotopeMasses)
339
+
340
+ if not (isotopeProbabilities is None):
341
+ self.isotopeProbabilities.extend(isotopeProbabilities)
342
+
343
+ for sublist in self.isotopeProbabilities:
344
+ for prob in sublist:
345
+ if not (0.0 < prob <= 1.0):
346
+ raise ValueError("All isotope probabilities p must fulfill: 0.0 < p <= 1.0")
347
+
348
+ self.isotopeNumbers = tuple(map(len, self.isotopeMasses))
349
+ if self.isotopeNumbers != tuple(map(len, self.isotopeProbabilities)):
350
+ raise ValueError("isotopeMasses and isotopeProbabilities must have the same number of isotopes per element")
351
+ if len(self.atomCounts) != len(self.isotopeNumbers) or len(self.isotopeNumbers) != len(self.isotopeProbabilities):
352
+ raise ValueError("atomCounts, isotopeMasses, and isotopeProbabilities must all have the same length")
353
+
354
+ self.dimNumber = len(self.isotopeNumbers)
355
+
356
+ self.get_confs = get_confs
357
+ self.ffi = isoFFI.clib
358
+
359
+ offsets = []
360
+
361
+ i = 0
362
+ for j in range(self.dimNumber):
363
+ newl = []
364
+ for k in range(self.isotopeNumbers[j]):
365
+ newl.append(i)
366
+ i += 1
367
+ offsets.append(tuple(newl))
368
+ self.offsets = tuple(offsets)
369
+
370
+ # "int[]" (open array) + a separate count keeps this call to a single,
371
+ # fixed type string cached after its first use, instead of parsing a
372
+ # fresh "int[N]" through cffi's C parser on every single Iso() built
373
+ # -- N here varies with the formula and cffi's type cache is keyed on
374
+ # the exact string, so embedding N in it defeats that cache entirely.
375
+ self.conf_space = isoFFI.ffi.new("int[]", sum(self.isotopeNumbers))
376
+
377
+ self.iso = self.ffi.setupIso(self.dimNumber, self.isotopeNumbers,
378
+ self.atomCounts,
379
+ [i/charge for s in self.isotopeMasses for i in s],
380
+ [i for s in self.isotopeProbabilities for i in s])
381
+
382
+ def __del__(self):
383
+ try:
384
+ if self.iso is not None:
385
+ self.ffi.deleteIso(self.iso)
386
+ self.iso = None
387
+ except AttributeError:
388
+ pass
389
+
390
+ def getLightestPeakMass(self):
391
+ """Get the lightest peak in the isotopic distribution."""
392
+ return self.ffi.getLightestPeakMassIso(self.iso)
393
+
394
+ def getLightestPeakLProb(self):
395
+ """Get the log probability of the lightest peak in the isotopic distribution."""
396
+ return self.ffi.getLightestPeakLProbIso(self.iso)
397
+
398
+ def getLightestPeakConf(self):
399
+ """Get the isotopic configuration of the lightest peak in the isotopic distribution."""
400
+ self.ffi.getLightestPeakSignature(self.iso, self.conf_space)
401
+ return self.parse_conf(self.conf_space)
402
+
403
+ def getHeaviestPeakMass(self):
404
+ """Get the heaviest peak in the isotopic distribution."""
405
+ return self.ffi.getHeaviestPeakMassIso(self.iso)
406
+
407
+ def getHeaviestPeakLProb(self):
408
+ """Get the log probability of the heaviest peak in the isotopic distribution."""
409
+ return self.ffi.getHeaviestPeakLProbIso(self.iso)
410
+
411
+ def getHeaviestPeakConf(self):
412
+ """Get the isotopic configuration of the heaviest peak in the isotopic distribution."""
413
+ self.ffi.getHeaviestPeakSignature(self.iso, self.conf_space)
414
+ return self.parse_conf(self.conf_space)
415
+
416
+ def getMonoisotopicPeakMass(self):
417
+ """Get the monoisotopic mass of the peak."""
418
+ return self.ffi.getMonoisotopicPeakMassIso(self.iso)
419
+
420
+ def getMonoisotopicPeakLProb(self):
421
+ """Get the log probability of the monoisotopic peak in the isotopic distribution."""
422
+ return self.ffi.getMonoisotopicPeakLProbIso(self.iso)
423
+
424
+ def getMonoisotopicPeakConf(self):
425
+ """Get the isotopic configuration of the monoisotopic peak in the isotopic distribution."""
426
+ self.ffi.getMonoisotopicPeakSignature(self.iso, self.conf_space)
427
+ return self.parse_conf(self.conf_space)
428
+
429
+ def getModeLProb(self):
430
+ """Get the log probability of the most probable peak(s) in the isotopic distribution."""
431
+ return self.ffi.getModeLProbIso(self.iso)
432
+
433
+ def getModeMass(self):
434
+ """Get the mass of the most probable peak.
435
+
436
+ If there are more, return only the mass of one of them."""
437
+ return self.ffi.getModeMassIso(self.iso)
438
+
439
+ def getTheoreticalAverageMass(self):
440
+ return self.ffi.getTheoreticalAverageMassIso(self.iso)
441
+
442
+ def variance(self):
443
+ return self.ffi.getIsoVariance(self.iso)
444
+
445
+ def stddev(self):
446
+ return self.ffi.getIsoStddev(self.iso)
447
+
448
+ def getMarginalLogSizeEstimates(self, prob):
449
+ cbuf = isoFFI.clib.getMarginalLogSizeEstimates(self.iso, prob)
450
+ # cbuf is already "double *" (cdef'd below): index it directly rather
451
+ # than casting through a fresh "double[N]" string, which would need a
452
+ # full, uncached cffi/pycparser parse for every distinct dimNumber.
453
+ ret = [cbuf[i] for i in range(self.dimNumber)]
454
+ isoFFI.clib.freeReleasedArray(cbuf)
455
+ return ret
456
+
457
+ def parse_conf(self, cptr, starting_with = 0):
458
+ return tuple(tuple(cptr[i+starting_with] for i in o) for o in self.offsets)
459
+
460
+ def _get_parse_conf_fun(self):
461
+ # Can't just use the above function as lambda, as the entire class instance will be in closure
462
+ offsets = self.offsets
463
+ def pc(cptr, starting_with = 0):
464
+ return tuple(tuple(cptr[i+starting_with] for i in o) for o in offsets)
465
+ return pc
466
+
467
+
468
+ class IsoDistribution(object):
469
+ """Isotopoic distribution with precomputed vector of masses and probabilities."""
470
+ def np_masses(self):
471
+ """Return computed masses as a numpy array."""
472
+ try:
473
+ import numpy as np
474
+ except ImportError as e:
475
+ raise Exception(str(e) + "\nThis requires numpy to be installed.")
476
+ return np.frombuffer(isoFFI.ffi.buffer(self.masses))
477
+
478
+ def np_probs(self):
479
+ """Return computed probabilities as a numpy array."""
480
+ try:
481
+ import numpy as np
482
+ except ImportError as e:
483
+ raise Exception(str(e) + "\nThis requires numpy to be installed.")
484
+ return np.frombuffer(isoFFI.ffi.buffer(self.probs))
485
+
486
+ def __iter__(self):
487
+ if hasattr(self, "confs") and self.confs is not None:
488
+ for i in range(self.size):
489
+ yield(self.masses[i], self.probs[i], self.confs[i])
490
+ else:
491
+ for i in range(self.size):
492
+ yield (self.masses[i], self.probs[i])
493
+
494
+ def __getitem__(self, idx):
495
+ try:
496
+ return (self.masses[idx], self.probs[idx], self.confs[idx])
497
+ except (AttributeError, TypeError):
498
+ return (self.masses[idx], self.probs[idx])
499
+
500
+ def __len__(self):
501
+ """Get the number of calculated peaks."""
502
+ return self.size
503
+
504
+ def _get_conf(self, idx):
505
+ return self.parse_conf(self.raw_confs, starting_with = self.sum_isotope_numbers * idx)
506
+
507
+ def __del__(self):
508
+ pass
509
+
510
+ def __init__(self, cobject = None, probs = None, masses = None, get_confs = False, iso = None):
511
+ self.mass_sorted = False
512
+ self.prob_sorted = False
513
+ self._total_prob = float('nan')
514
+
515
+ if cobject is not None:
516
+ self.size = isoFFI.clib.confs_noFixedEnvelope(cobject)
517
+
518
+ # The *WithDeleter getters hand the array over without copying it:
519
+ # the library's own buffers are SIMD-aligned, and a large one is
520
+ # mapped straight from the OS, so plain free() is not necessarily the
521
+ # right way to give them back. Each getter therefore writes out the
522
+ # (size, deleter) pair that releases this particular array, and we
523
+ # bind both into the ffi.gc destructor.
524
+ def wrap(typename, getter, attrname, mult = 1):
525
+ size_out = isoFFI.ffi.new("size_t*")
526
+ deleter_out = isoFFI.ffi.new("IsoSpecArrayDeleter*")
527
+ what = getter(cobject, size_out, deleter_out)
528
+ # Reinterpreting `what` as an array of exactly the right length
529
+ # would normally mean asking cffi to parse "typename[N]" -- but
530
+ # N varies on every call, so cffi's per-string type cache never
531
+ # hits and each call falls through to a full, uncached parse.
532
+ # Route the *length* through a plain byte buffer instead: the
533
+ # "typename *" cast and the "typename[]" open-array type are
534
+ # both fixed strings, parsed (and cached) once for the life of
535
+ # the process, no matter how many distinct N show up here.
536
+ n = self.size * mult
537
+ ptr = isoFFI.ffi.cast(typename + ' *', what)
538
+ if what != isoFFI.ffi.NULL:
539
+ # An empty envelope owns nothing, and has nothing to release.
540
+ # The destructor is attached to the plain pointer, not to
541
+ # the sized array below: cffi's from_buffer() cdata does
542
+ # not support ffi.gc() directly (it corrupts the array's
543
+ # length -- verified separately). Attaching it here
544
+ # instead works because the array keeps its backing
545
+ # buffer alive, and the buffer keeps this pointer alive,
546
+ # so the destructor still fires exactly when the array
547
+ # itself is finally dropped.
548
+ nbytes = size_out[0]
549
+ deleter = deleter_out[0]
550
+ ptr = isoFFI.ffi.gc(ptr, lambda p: isoFFI.clib.freeReleasedArrayWithDeleter(p, nbytes, deleter))
551
+ x = isoFFI.ffi.from_buffer(
552
+ typename + '[]',
553
+ isoFFI.ffi.buffer(ptr, n * isoFFI.ffi.sizeof(typename)))
554
+ setattr(self, attrname, x)
555
+
556
+ wrap("double", isoFFI.clib.massesFixedEnvelopeWithDeleter, "masses")
557
+ wrap("double", isoFFI.clib.probsFixedEnvelopeWithDeleter, "probs")
558
+
559
+ if get_confs:
560
+ # Must also be a subclass of Iso...
561
+ self.sum_isotope_numbers = sum(iso.isotopeNumbers)
562
+ wrap("int", isoFFI.clib.confsFixedEnvelopeWithDeleter, "raw_confs", mult = self.sum_isotope_numbers)
563
+ self.confs = ConfsPassthrough(lambda idx: self._get_conf(idx), self.size)
564
+ self.parse_conf = iso._get_parse_conf_fun()
565
+
566
+ elif probs is not None or masses is not None:
567
+ if probs is None or masses is None:
568
+ raise ValueError("masses and probs must both be provided together")
569
+ if len(probs) != len(masses):
570
+ raise ValueError("masses and probs must have the same length")
571
+ self.size = len(probs)
572
+ # "double[]" (open array) infers its length from the initializer,
573
+ # so this is one fixed, cacheable type string regardless of size
574
+ # -- see the matching note on conf_space above.
575
+ self.probs = isoFFI.ffi.new("double[]", probs)
576
+ self.masses = isoFFI.ffi.new("double[]", masses)
577
+ else:
578
+ raise ValueError("IsoDistribution requires at least one of: cobject, or masses+probs")
579
+
580
+ def _get_cobject(self):
581
+ if hasattr(self, 'raw_confs') and self.raw_confs is not None:
582
+ return isoFFI.clib.setupFixedEnvelopeWithConfs(self.masses, self.probs, self.raw_confs, len(self.masses), self.sum_isotope_numbers, self.mass_sorted, self.prob_sorted, self._total_prob)
583
+ else:
584
+ return isoFFI.clib.setupFixedEnvelope(self.masses, self.probs, len(self.masses), self.mass_sorted, self.prob_sorted, self._total_prob)
585
+
586
+ def copy(self):
587
+ x = self._get_cobject()
588
+ c = isoFFI.clib.copyFixedEnvelope(x)
589
+ isoFFI.clib.deleteFixedEnvelope(x, True)
590
+ ret = IsoDistribution(cobject = c)
591
+ ret._total_prob = self._total_prob
592
+ ret.mass_sorted = self.mass_sorted
593
+ ret.prob_sorted = self.prob_sorted
594
+ isoFFI.clib.deleteFixedEnvelope(c, False)
595
+ return ret
596
+
597
+ def __add__(self, other):
598
+ x = self._get_cobject()
599
+ y = other._get_cobject()
600
+ cobject = isoFFI.clib.addEnvelopes(x, y)
601
+ isoFFI.clib.deleteFixedEnvelope(x, True)
602
+ isoFFI.clib.deleteFixedEnvelope(y, True)
603
+ ret = IsoDistribution(cobject = cobject)
604
+ isoFFI.clib.deleteFixedEnvelope(cobject, False)
605
+ return ret
606
+
607
+ def __mul__(self, other):
608
+ x = self._get_cobject()
609
+ y = other._get_cobject()
610
+ cobject = isoFFI.clib.convolveEnvelopes(x, y)
611
+ isoFFI.clib.deleteFixedEnvelope(x, True)
612
+ isoFFI.clib.deleteFixedEnvelope(y, True)
613
+ ret = IsoDistribution(cobject = cobject)
614
+ isoFFI.clib.deleteFixedEnvelope(cobject, False)
615
+ return ret
616
+
617
+ def total_prob(self):
618
+ if math.isnan(self._total_prob):
619
+ co = self._get_cobject()
620
+ self._total_prob = isoFFI.clib.getTotalProbOfEnvelope(co)
621
+ isoFFI.clib.deleteFixedEnvelope(co, True)
622
+ return self._total_prob
623
+
624
+ def normalize(self):
625
+ co = self._get_cobject()
626
+ isoFFI.clib.normalizeEnvelope(co)
627
+ isoFFI.clib.deleteFixedEnvelope(co, True)
628
+ self._total_prob = 1.0
629
+
630
+ def normalized(self):
631
+ ret = self.copy()
632
+ ret.normalize()
633
+ return ret
634
+
635
+ def add_mass(self, d_mass):
636
+ isoFFI.clib.array_add(self.masses, self.size, d_mass)
637
+
638
+ def mul_mass(self, d_mass):
639
+ isoFFI.clib.array_mul(self.masses, self.size, d_mass)
640
+
641
+ def add_mul_mass(self, add, mul):
642
+ isoFFI.clib.array_fma(self.masses, self.size, mul, add*mul)
643
+
644
+ def mul_add_mass(self, mul, add):
645
+ isoFFI.clib.array_fma(self.masses, self.size, mul, add)
646
+
647
+ def scale(self, factor):
648
+ '''Multiplies the pribabilities of spectrum by factor. Works in place.'''
649
+ co = self._get_cobject()
650
+ isoFFI.clib.scaleEnvelope(co, factor)
651
+ isoFFI.clib.deleteFixedEnvelope(co, True)
652
+ self._total_prob *= factor
653
+
654
+ def scaled(self, factor):
655
+ '''Returns a copy of the spectrum where each probability was multiplied by factor.'''
656
+ ret = self.copy()
657
+ ret.scale(factor)
658
+ return ret
659
+
660
+ def resample(self, ionic_current, beta_bias=1.0):
661
+ co = self._get_cobject()
662
+ isoFFI.clib.resampleEnvelope(co, ionic_current, beta_bias)
663
+ isoFFI.clib.deleteFixedEnvelope(co, True)
664
+ self._total_prob = float(ionic_current)
665
+ self.prob_sorted = False
666
+
667
+ def sort_by_prob(self):
668
+ if not self.prob_sorted:
669
+ co = self._get_cobject()
670
+ isoFFI.clib.sortEnvelopeByProb(co)
671
+ isoFFI.clib.deleteFixedEnvelope(co, True)
672
+ self.mass_sorted = False
673
+ self.prob_sorted = True
674
+
675
+ def sort_by_mass(self):
676
+ if not self.mass_sorted:
677
+ co = self._get_cobject()
678
+ isoFFI.clib.sortEnvelopeByMass(co)
679
+ isoFFI.clib.deleteFixedEnvelope(co, True)
680
+ self.mass_sorted = True
681
+ self.prob_sorted = False
682
+
683
+ def _recalculate_everything(self):
684
+ self._total_prob = float('nan')
685
+ self.mass_sorted = False
686
+ self.prob_sorted = False
687
+
688
+ def empiric_average_mass(self):
689
+ co = self._get_cobject()
690
+ ret = isoFFI.clib.empiricAverageMass(co)
691
+ isoFFI.clib.deleteFixedEnvelope(co, True)
692
+ return ret
693
+
694
+ def empiric_variance(self):
695
+ co = self._get_cobject()
696
+ ret = isoFFI.clib.empiricVariance(co)
697
+ isoFFI.clib.deleteFixedEnvelope(co, True)
698
+ return ret
699
+
700
+ def empiric_stddev(self):
701
+ co = self._get_cobject()
702
+ ret = isoFFI.clib.empiricStddev(co)
703
+ isoFFI.clib.deleteFixedEnvelope(co, True)
704
+ return ret
705
+
706
+ def wassersteinDistance(self, other):
707
+ x = self._get_cobject()
708
+ y = other._get_cobject()
709
+ ret = isoFFI.clib.wassersteinDistance(x, y)
710
+ isoFFI.clib.deleteFixedEnvelope(x, True)
711
+ isoFFI.clib.deleteFixedEnvelope(y, True)
712
+ self.mass_sorted = True
713
+ self.prob_sorted = False
714
+ other.mass_sorted = True
715
+ other.prob_sorted = False
716
+ if math.isnan(ret):
717
+ raise ValueError("Both spectra must be normalized before Wasserstein distance can be computed.")
718
+ return ret
719
+
720
+ def orientedWassersteinDistance(self, other):
721
+ x = self._get_cobject()
722
+ y = other._get_cobject()
723
+ ret = isoFFI.clib.orientedWassersteinDistance(x, y)
724
+ isoFFI.clib.deleteFixedEnvelope(x, True)
725
+ isoFFI.clib.deleteFixedEnvelope(y, True)
726
+ self.mass_sorted = True
727
+ self.prob_sorted = False
728
+ other.mass_sorted = True
729
+ other.prob_sorted = False
730
+ if math.isnan(ret):
731
+ raise ValueError("Both spectra must be normalized before Wasserstein distance can be computed.")
732
+ return ret
733
+
734
+ def abyssalWassersteinDistance(self, other, abyss_depth, other_scale = 1.0):
735
+ x = self._get_cobject()
736
+ y = other._get_cobject()
737
+ ret = isoFFI.clib.abyssalWassersteinDistance(x, y, abyss_depth, other_scale)
738
+ isoFFI.clib.deleteFixedEnvelope(x, True)
739
+ isoFFI.clib.deleteFixedEnvelope(y, True)
740
+ self.mass_sorted = True
741
+ self.prob_sorted = False
742
+ other.mass_sorted = True
743
+ other.prob_sorted = False
744
+ return ret
745
+
746
+ def abyssalWassersteinDistanceGrad(self, others, scales, grad, abyss_depth_exp, abyss_depth_the):
747
+ assert len(others) + 1 == len(scales) == len(grad)
748
+ cobjs = [self._get_cobject()]
749
+ cobjs.extend(other._get_cobject() for other in others)
750
+ ret = isoFFI.clib.abyssalWassersteinDistanceGrad(cobjs, scales, grad, len(others), abyss_depth_exp, abyss_depth_the)
751
+ for cobj in cobjs:
752
+ isoFFI.clib.deleteFixedEnvelope(cobj, True)
753
+ self.mass_sorted = True
754
+ self.prob_sorted = False
755
+ for other in others:
756
+ other.mass_sorted = True
757
+ other.prob_sorted = False
758
+ return ret
759
+
760
+ def wassersteinMatch(self, other, flow_dist, other_scale = 1.0):
761
+ x = self._get_cobject()
762
+ y = other._get_cobject()
763
+ ret = isoFFI.clib.wassersteinMatch(x, y, flow_dist, other_scale)
764
+ isoFFI.clib.deleteFixedEnvelope(x, True)
765
+ isoFFI.clib.deleteFixedEnvelope(y, True)
766
+ self.mass_sorted = True
767
+ self.prob_sorted = False
768
+ other.mass_sorted = True
769
+ other.prob_sorted = False
770
+ return (ret.res1, ret.res2, ret.flow)
771
+
772
+ def binned(self, width = 1.0, middle = 0.0):
773
+ co = self._get_cobject()
774
+ cbo = isoFFI.clib.binnedEnvelope(co, width, middle)
775
+ isoFFI.clib.deleteFixedEnvelope(co, True)
776
+ ret = IsoDistribution(cobject = cbo)
777
+ isoFFI.clib.deleteFixedEnvelope(cbo, False)
778
+ self.mass_sorted = True
779
+ self.prob_sorted = False
780
+ ret.mass_sorted = True
781
+ ret.prob_sorted = False
782
+ return ret
783
+
784
+
785
+ @staticmethod
786
+ def LinearCombination(envelopes, intensities):
787
+ envelope_objs = [x._get_cobject() for x in envelopes]
788
+ if type(intensities) != list:
789
+ intensities = list(intensities)
790
+ cobject = isoFFI.clib.linearCombination(envelope_objs, intensities, len(envelope_objs))
791
+ for x in envelope_objs:
792
+ isoFFI.clib.deleteFixedEnvelope(x, True)
793
+ ret = IsoDistribution(cobject = cobject)
794
+ isoFFI.clib.deleteFixedEnvelope(cobject, False)
795
+ return ret
796
+
797
+
798
+ def plot(self, plot_type = "bars", show = True, **matplotlib_args):
799
+ """Plot the isotopic distribution.
800
+
801
+ Args:
802
+ **matplotlib_args: arguments for matplotlib plot.
803
+ """
804
+ try:
805
+ from matplotlib import pyplot as plt
806
+ except ImportError as e:
807
+ raise ImportError(str(e) + "\nPlotting spectra requires matplotlib to be installed.")
808
+ if plot_type == "bars":
809
+ if "linewidth" not in matplotlib_args:
810
+ matplotlib_args['linewidth'] = 1.0
811
+ plt.vlines(list(self.masses), [0], list(self.probs), **matplotlib_args)
812
+ elif plot_type == "profile":
813
+ self.sort_by_mass()
814
+ plt.plot(list(self.masses), list(self.probs), **matplotlib_args)
815
+ plt.xlabel("Mass (Da)")
816
+ plt.ylabel("Intensity (relative)")
817
+ if show:
818
+ plt.show()
819
+
820
+
821
+
822
+
823
+ def IsoThreshold(threshold,
824
+ formula="",
825
+ absolute=False,
826
+ get_confs=False,
827
+ **kwargs):
828
+ """Initialize the IsoDistribution isotopic distribution by threshold.
829
+
830
+ Args:
831
+ threshold (float): value of the absolute or relative threshold.
832
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
833
+ absolute (boolean): should we report peaks with probabilities above an absolute probability threshold, or above a relative threshold amounting to a given proportion of the most probable peak?
834
+ get_confs (boolean): should we report counts of isotopologues?
835
+ **kwds: named arguments to IsoSpectrum.
836
+ """
837
+ iso = Iso(formula = formula, get_confs = get_confs, **kwargs)
838
+ tabulator = isoFFI.clib.setupThresholdFixedEnvelope(iso.iso, threshold, absolute, get_confs)
839
+ ido = IsoDistribution(cobject = tabulator, get_confs = get_confs, iso = iso)
840
+ isoFFI.clib.deleteFixedEnvelope(tabulator, False)
841
+ return ido
842
+
843
+
844
+ def IsoTotalProb(prob_to_cover,
845
+ formula="",
846
+ get_minimal_pset=True,
847
+ get_confs=False,
848
+ **kwargs):
849
+ """Initialize the IsoDistribution isotopic distribution by total probability.
850
+
851
+ Args:
852
+ prob_to_cover (float): minimal total probability of the reported peaks.
853
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
854
+ get_minimal_pset (boolean): should we trim the last calculated layer of isotopologues so that the reported result is as small as possible?
855
+ get_confs (boolean): should we report the counts of isotopologues?
856
+ **kwargs: named arguments to the superclass.
857
+ """
858
+ iso = Iso(formula=formula, get_confs=get_confs, **kwargs)
859
+ tabulator = isoFFI.clib.setupTotalProbFixedEnvelope(iso.iso, prob_to_cover, get_minimal_pset, get_confs)
860
+ ido = IsoDistribution(cobject = tabulator, get_confs = get_confs, iso = iso)
861
+ isoFFI.clib.deleteFixedEnvelope(tabulator, False)
862
+ return ido
863
+
864
+
865
+ def IsoStochastic(no_molecules,
866
+ formula="",
867
+ precision=0.9999,
868
+ beta_bias=5.0,
869
+ get_confs=False,
870
+ **kwargs):
871
+ """Initialize the isotopic distribution by stochastic sampling, simulating a measured mass spectrum
872
+ with a given number of ions. Each configuration is drawn independently according to its isotopic
873
+ probability, producing integer ion counts rather than exact probabilities.
874
+
875
+ Args:
876
+ no_molecules (uint): number of ions to simulate (ionic current).
877
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
878
+ precision (float): total probability of the envelope used for sampling. Between 0.0 and 1.0.
879
+ beta_bias (float, nonnegative): fiddling with this parameter does not change the result, but might make computations slightly faster (or likely, much, much slower if you screw it up...)
880
+ get_confs (boolean): should we report the counts of isotopologues?
881
+ **kwargs: named arguments to the superclass.
882
+ """
883
+ iso = Iso(formula=formula, get_confs=get_confs, **kwargs)
884
+ tabulator = isoFFI.clib.setupStochasticFixedEnvelope(iso.iso, no_molecules, precision, beta_bias, get_confs)
885
+ ido = IsoDistribution(cobject = tabulator, get_confs = get_confs, iso = iso)
886
+ isoFFI.clib.deleteFixedEnvelope(tabulator, False)
887
+ return ido
888
+
889
+
890
+ def IsoBinned(bin_width,
891
+ formula="",
892
+ target_total_prob=0.9999,
893
+ bin_middle=0.0,
894
+ **kwargs):
895
+ """Initialize the isotopic distribution as a binned (histogram) envelope.
896
+ Configurations are grouped into bins of equal width and their probabilities summed.
897
+
898
+ Args:
899
+ bin_width (float): width of each bin in Da.
900
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
901
+ target_total_prob (float): total probability of configurations to include before binning. Between 0.0 and 1.0.
902
+ bin_middle (float): offset of bin centres from zero.
903
+ **kwargs: named arguments to the superclass.
904
+ """
905
+ iso = Iso(formula=formula, get_confs=False, **kwargs)
906
+ tabulator = isoFFI.clib.setupBinnedFixedEnvelope(iso.iso, target_total_prob, bin_width, bin_middle)
907
+ ido = IsoDistribution(cobject = tabulator, get_confs = False, iso = iso)
908
+ isoFFI.clib.deleteFixedEnvelope(tabulator, False)
909
+ return ido
910
+
911
+
912
+ class IsoGenerator(Iso):
913
+ """Virtual class alowing memory-efficient iteration over the isotopic distribution.
914
+
915
+ This iterator will stop only after enumerating all isotopologues.
916
+ """
917
+ def __init__(self, formula="", get_confs=False, **kwargs):
918
+ """Initialize the IsoGenerator.
919
+
920
+ Args:
921
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
922
+ get_confs (boolean): should we report the counts of isotopologues?
923
+ **kwargs: named arguments to the superclass.
924
+ """
925
+ self.cgen = None
926
+ super(IsoGenerator, self).__init__(formula=formula, get_confs=get_confs, **kwargs)
927
+ self.firstuse = True
928
+
929
+ def __iter__(self):
930
+ if not self.firstuse:
931
+ raise NotImplementedError("Multiple iterations through the same IsoGenerator object are not supported. Either create a new (identical) generator for a second loop-through, or use one of the non-generator classes, which do support being re-used.")
932
+ self.firstuse = False
933
+ cgen = self.cgen
934
+ if self.get_confs:
935
+ while self.advancer(cgen):
936
+ self.conf_getter(cgen, self.conf_space)
937
+ yield (self.mass_getter(cgen), self.xprob_getter(cgen), self.parse_conf(self.conf_space))
938
+ else:
939
+ while self.advancer(cgen):
940
+ yield (self.mass_getter(cgen), self.xprob_getter(cgen))
941
+
942
+ def __del__(self):
943
+ super(IsoGenerator, self).__del__()
944
+
945
+
946
+ class IsoThresholdGenerator(IsoGenerator):
947
+ """Class alowing memory-efficient iteration over the isotopic distribution up till some probability threshold.
948
+
949
+ This iterator will stop only after reaching a probability threshold.
950
+ """
951
+ def __init__(self, threshold, formula="", absolute=False, get_confs=False, reorder_marginals = True, **kwargs):
952
+ """Initialize IsoThresholdGenerator.
953
+
954
+ Args:
955
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
956
+ absolute (boolean): should we report peaks with probabilities above an absolute probability threshold, or above a relative threshold amounting to a given proportion of the most probable peak?
957
+ get_confs (boolean): should we report the counts of isotopologues?
958
+ **kwargs: named arguments to the superclass.
959
+ """
960
+ super(IsoThresholdGenerator, self).__init__(formula=formula, get_confs=get_confs, **kwargs)
961
+ self.threshold = threshold
962
+ self.absolute = absolute
963
+ self.cgen = self.ffi.setupIsoThresholdGenerator(self.iso,
964
+ threshold,
965
+ absolute,
966
+ 1000,
967
+ 1000,
968
+ reorder_marginals)
969
+ self.advancer = self.ffi.advanceToNextConfigurationIsoThresholdGenerator
970
+ self.xprob_getter = self.ffi.probIsoThresholdGenerator
971
+ self.mass_getter = self.ffi.massIsoThresholdGenerator
972
+ self.conf_getter = self.ffi.get_conf_signatureIsoThresholdGenerator
973
+
974
+ def __del__(self):
975
+ """Destructor."""
976
+ try:
977
+ if self.cgen is not None:
978
+ self.ffi.deleteIsoThresholdGenerator(self.cgen)
979
+ self.cgen = None
980
+ except AttributeError:
981
+ pass
982
+ super(IsoThresholdGenerator, self).__del__()
983
+
984
+
985
+ class IsoLayeredGenerator(IsoGenerator):
986
+ """Class alowing memory-efficient iteration over the isotopic distribution up till some joint probability of the reported peaks."""
987
+ def __init__(self, formula="", get_confs=False, reorder_marginals = True, t_prob_hint = 0.99, **kwargs):
988
+ """Initialize IsoLayeredGenerator.
989
+
990
+ Args:
991
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
992
+ get_confs (boolean): should we report the counts of isotopologues?
993
+ reorder_marginals (boolean): should the marginal distributions be reordered for efficiency?
994
+ t_prob_hint (float): hint for the target total probability, used to size internal buffers.
995
+ **kwargs: named arguments to the superclass.
996
+ """
997
+ super(IsoLayeredGenerator, self).__init__(formula=formula, get_confs=get_confs, **kwargs)
998
+ self.cgen = self.ffi.setupIsoLayeredGenerator(self.iso, 1000, 1000, reorder_marginals, t_prob_hint)
999
+ self.advancer = self.ffi.advanceToNextConfigurationIsoLayeredGenerator
1000
+ self.xprob_getter = self.ffi.probIsoLayeredGenerator
1001
+ self.mass_getter = self.ffi.massIsoLayeredGenerator
1002
+ self.conf_getter = self.ffi.get_conf_signatureIsoLayeredGenerator
1003
+
1004
+ def __del__(self):
1005
+ try:
1006
+ if self.cgen is not None:
1007
+ self.ffi.deleteIsoLayeredGenerator(self.cgen)
1008
+ self.cgen = None
1009
+ except AttributeError:
1010
+ pass
1011
+ super(IsoLayeredGenerator, self).__del__()
1012
+
1013
+ class IsoOrderedGenerator(IsoGenerator):
1014
+ """Class representing an isotopic distribution with peaks ordered by descending probability.
1015
+
1016
+ This generator return probilities ordered with descending probability.
1017
+ It it not optimal to do so, but it might be useful.
1018
+
1019
+ WARNING! This algorithm work in O(N*log(N)) vs O(N) of the threshold and layered algorithms.
1020
+ Also, the order of descending probability will most likely not reflect the order of ascending masses.
1021
+ """
1022
+ def __init__(self, formula="", get_confs=False, **kwargs):
1023
+ """Initialize IsoOrderedGenerator.
1024
+
1025
+ Args:
1026
+ formula (str): a chemical formula, e.g. "C2H6O1" or "C2H6O".
1027
+ get_confs (boolean): should we report the counts of isotopologues?
1028
+ **kwargs: named arguments to the superclass.
1029
+ """
1030
+ super(IsoOrderedGenerator, self).__init__(formula=formula, get_confs=get_confs, **kwargs)
1031
+ self.cgen = self.ffi.setupIsoOrderedGenerator(self.iso, 1000, 1000)
1032
+ self.advancer = self.ffi.advanceToNextConfigurationIsoOrderedGenerator
1033
+ self.xprob_getter = self.ffi.probIsoOrderedGenerator
1034
+ self.mass_getter = self.ffi.massIsoOrderedGenerator
1035
+ self.conf_getter = self.ffi.get_conf_signatureIsoOrderedGenerator
1036
+
1037
+ def __del__(self):
1038
+ try:
1039
+ if self.cgen is not None:
1040
+ self.ffi.deleteIsoOrderedGenerator(self.cgen)
1041
+ self.cgen = None
1042
+ except AttributeError:
1043
+ pass
1044
+ super(IsoOrderedGenerator, self).__del__()
1045
+
1046
+
1047
+ class IsoStochasticGenerator(IsoGenerator):
1048
+ def __init__(self, no_molecules, formula="", precision=0.9999, beta_bias = 1.0, get_confs=False, **kwargs):
1049
+ super(IsoStochasticGenerator, self).__init__(formula=formula, get_confs=get_confs, **kwargs)
1050
+ self.threshold = precision
1051
+ self.no_molecules = no_molecules
1052
+ self.cgen = self.ffi.setupIsoStochasticGenerator(self.iso,
1053
+ no_molecules,
1054
+ precision,
1055
+ beta_bias)
1056
+ self.advancer = self.ffi.advanceToNextConfigurationIsoStochasticGenerator
1057
+ self.xprob_getter = self.ffi.probIsoStochasticGenerator
1058
+ self.mass_getter = self.ffi.massIsoStochasticGenerator
1059
+ self.conf_getter = self.ffi.get_conf_signatureIsoStochasticGenerator
1060
+
1061
+ def __del__(self):
1062
+ """Destructor."""
1063
+ try:
1064
+ if self.cgen is not None:
1065
+ self.ffi.deleteIsoStochasticGenerator(self.cgen)
1066
+ self.cgen = None
1067
+ except AttributeError:
1068
+ pass
1069
+ super(IsoStochasticGenerator, self).__del__()
1070
+
1071
+
1072
+ def get_simd_level():
1073
+ """Report what the bundled C++ library's vectorised kernels are running as.
1074
+
1075
+ The wheels carry several builds of the inner loops and choose one from the
1076
+ CPU at import time, so this is a property of the machine as much as of the
1077
+ build. Use it to tell an installation that vectorises from one that silently
1078
+ does not -- on Windows, for instance, where the standard library provides no
1079
+ <experimental/simd> and everything falls back to the scalar path.
1080
+
1081
+ Returns:
1082
+ str: one of "scalar" (not vectorised at all), "sse2", "avx", "avx2",
1083
+ "avx512", "neon", or "simd" for a vector unit the library has no
1084
+ name for. New tokens may appear as instruction sets are added, so
1085
+ treat an unfamiliar one as "some vector unit" rather than an error.
1086
+ """
1087
+ return isoFFI.ffi.string(isoFFI.clib.activeSimdLevel()).decode("ascii")
1088
+