IsoSpecPy 2.5.0__py3-none-win_arm64.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
@@ -0,0 +1,290 @@
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
+
19
+ '''
20
+ Bunch of deprecated functions for 1.0.X compatibility.
21
+ Avoid using them: there is a considerable overhead associated
22
+ with using the old interface... The backward compatibility module
23
+ is also very rudimentary, somewhat incomplete and not very well
24
+ tested too...
25
+
26
+ The current API is implemented in __init__.py, use that instead
27
+ '''
28
+
29
+
30
+ import re
31
+ from .IsoSpecPy import IsoTotalProb, IsoThreshold
32
+
33
+ DIGIT_PATTERN = re.compile(r"\d+")
34
+ NON_DIGIT_PATTERN = re.compile(r"\D+")
35
+
36
+ class IsoSpec():
37
+ def __init__(
38
+ self,
39
+ _atomCounts,
40
+ _isotopeMasses,
41
+ _isotopeProbabilities,
42
+ _stopCondition,
43
+ tabSize = 1000, # ignored
44
+ hashSize = 1000, # ignored
45
+ step = 0.3, # ignored
46
+ trim = True, # True not supported yet, treated as False anyway
47
+ method = 'layered'
48
+ ):
49
+
50
+ isoargs = {
51
+ "formula" : None,
52
+ "get_confs" : True,
53
+ "atomCounts" : _atomCounts,
54
+ "isotopeMasses" : _isotopeMasses,
55
+ "isotopeProbabilities" : _isotopeProbabilities,
56
+ }
57
+
58
+
59
+ self.dimNumber = len(_atomCounts)
60
+ self._isotopeNumbers = [len(x) for x in _isotopeMasses]
61
+ self.allIsotopeNumber = sum(self._isotopeNumbers)
62
+ self._atomCounts = _atomCounts
63
+ self._isotopeMasses = _isotopeMasses
64
+ self._isotopeProbabilities = _isotopeProbabilities
65
+ self._stopCondition = _stopCondition
66
+
67
+ from .__init__ import IsoThreshold
68
+
69
+ try:
70
+ algo = { 'layered' : lambda total_prob: IsoTotalProb(total_prob, **isoargs),
71
+ 'ordered' : lambda total_prob: IsoTotalProb(total_prob, **isoargs),
72
+ 'threshold_absolute' : lambda threshold: IsoThreshold(threshold, True, **isoargs),
73
+ 'threshold_relative' : lambda threshold: IsoThreshold(threshold, False, **isoargs),
74
+ 'layered_estimating' : lambda total_prob: IsoTotalProb(total_prob, **isoargs)
75
+ }[method]
76
+ except KeyError:
77
+ raise Exception("Invalid ISO method")
78
+
79
+ # Reference to iso needs to be held in this object: it will deallocate masses/lprobs/etc arrays on C++ side if we
80
+ # allow GC to collect it prematurely
81
+ self.iso = algo(_stopCondition)
82
+
83
+ self.masses = self.iso.masses
84
+ self.lprobs = self.iso.lprobs
85
+ self.probs = self.iso.probs
86
+ self.confs = self.iso.confs
87
+ self.size = self.iso.size
88
+
89
+ if method == 'ordered' and len(self.masses) > 0:
90
+ L = sorted(zip(self.masses, self.lprobs, self.probs, self.confs), key = lambda x: -x[1])
91
+ self.masses, self.lprobs, self.probs, self.confs = zip(*L)
92
+
93
+ @staticmethod
94
+ def IsoFromFormula(formula, cutoff, tabSize = 1000, hashSize = 1000, classId = None, method = 'threshold_relative', step = 0.25, trim = True):
95
+ # It's much easier to just parse it in python than to use the C parsing function
96
+ # and retrieve back into Python the relevant object sizes
97
+ symbols = re.findall(NON_DIGIT_PATTERN, formula)
98
+ atom_counts = [int(x) for x in re.findall(DIGIT_PATTERN, formula)]
99
+
100
+ if not len(symbols) == len(atom_counts):
101
+ raise ValueError("Invalid formula")
102
+
103
+ from .PeriodicTbl import symbol_to_masses, symbol_to_probs
104
+ try:
105
+ masses = tuple(symbol_to_masses[symbol] for symbol in symbols)
106
+ probs = tuple(symbol_to_probs[symbol] for symbol in symbols)
107
+ except KeyError:
108
+ raise ValueError("Invalid formula")
109
+
110
+ return IsoSpec(atom_counts, masses, probs, cutoff, tabSize, hashSize, step, trim, method)
111
+
112
+
113
+
114
+ def __len__(self):
115
+ return self.size
116
+
117
+ def getConfsRaw(self):
118
+ return (self.masses, self.lprobs, self.confs)
119
+
120
+ # def get_conf_by_no(self, clist, idx):
121
+ # idx *= self.allIsotopeNumber
122
+ # ret = []
123
+ # for ison in self._isotopeNumbers:
124
+ # ret.append(tuple(clist[idx:idx+ison]))
125
+ # idx += ison
126
+ # return tuple(ret)
127
+
128
+
129
+ def getConfs(self):
130
+ masses, logProbs, isoCounts = self.getConfsRaw()
131
+ rows_no = len(masses)
132
+ masses = list(masses)
133
+ logProbs= list(logProbs)
134
+ confs = []
135
+ for i in range(rows_no):
136
+ confs.append([x for sublist in isoCounts[i] for x in sublist])
137
+ return masses, logProbs, confs
138
+
139
+
140
+ def getConfsNumpy(self):
141
+ import numpy as np
142
+ masses, logProbs, configurations = self.getConfsRaw()
143
+ rows_no = len(masses)
144
+ if rows_no == 0:
145
+ return np.array([])
146
+ cols_no = len(configurations)//len(masses)
147
+ masses = np.array(list(masses))
148
+ logProbs= np.array(list(logProbs))
149
+ configurations = np.array(list(configurations)).reshape((rows_no,cols_no))
150
+ return masses, logProbs, configurations
151
+
152
+
153
+ def splitConf(self, l, offset = 0):
154
+ conf = []
155
+ idx = self.allIsotopeNumber * offset
156
+ for i in range(self.dimNumber):
157
+ conf.append(tuple(l[idx:idx+self._isotopeNumbers[i]]))
158
+ idx += self._isotopeNumbers[i]
159
+ return tuple(conf)
160
+
161
+ def confStr(self, conf):
162
+ return '\t'.join([' '.join([str(x) for x in y]) for y in conf])
163
+
164
+ def printConfs(self):
165
+ masses, logProbs, isoCounts = self.getConfsRaw()
166
+ confs = []
167
+ step = sum(self._isotopeNumbers)
168
+ for i in range(len(masses)):
169
+ confs.append((masses[i], logProbs[i], self.splitConf(isoCounts, i)))
170
+
171
+ for conf in confs:
172
+ print(("Mass = {0}\t and log-prob = {1} and prob = {2}\t and configuration"\
173
+ "=\t{3}").format(conf[0], conf[1], math.exp(conf[1]), self.confStr(conf[2])))
174
+
175
+
176
+
177
+ class IsoPlot(dict):
178
+ def __init__(self, iso, bin_w):
179
+ self.iso = iso
180
+ self.bin_w = bin_w
181
+ masses, logProbs, _isoCounts = iso.getConfsRaw()
182
+ dd = defaultdict(Summator)
183
+ for i in range(len(masses)):
184
+ dd[float(int(masses[i]/bin_w))*bin_w].add(math.exp(logProbs[i]))
185
+ for key, val in dd.items():
186
+ self[key] = val.get()
187
+
188
+
189
+ def IsoSpecify( formula,
190
+ cutoff,
191
+ method= 'layered',
192
+ output_format = 'numpy_arrays',
193
+ trim = True,
194
+ _step = 0.25,
195
+ _trim = True,
196
+ _tabSize = 1000,
197
+ _hashSize = 1000 ):
198
+ """
199
+ Call IsoSpec on a formula with a given cutoff.
200
+
201
+ This function wraps around the IsoSpec class.
202
+
203
+ Parameters
204
+ ----------
205
+ formula : char
206
+ a string of a form '< Element Tag 1 >< Count 1 > ... ',
207
+ e.g. 'C100H202'. Using IUPAC conventions to name elements.
208
+
209
+ cutoff : float
210
+ The cutoff value. See description of the method argument.
211
+
212
+ method : char
213
+ Can take one of the following values: 'layered',
214
+ 'layered_estimating', 'threshold_absolute',
215
+ 'threshold_relative', 'ordered'.
216
+
217
+ The threshold versions of the algorithm rely on user
218
+ providing a precise lower bound on the reported peak heights.
219
+ This can be specified in absolute terms ('threshold_absolute'),
220
+ i.e. in terms of the limiting probability of the isotopologue,
221
+ or as a percentage of the heighest peak ('threshold_relative').
222
+
223
+ The layered versions of the algorithm rely on calculating
224
+ consecutive values of peak thresholds on flight.
225
+ The ultimate goal is to reach a peak probability that assures
226
+ that the sum of probabilities of the more probable isotopologues
227
+ exceeds the provided cutoff value.
228
+ The sequence of consecutive thresholds can be generated in
229
+ two ways. The default way, 'layered_estimating', estimates
230
+ the threshold to joint probability function by a progressive
231
+ linear spline, check Anal Chem. 2017 Mar 21;89(6):3272-3277.
232
+ doi: 10.1021/acs.analchem.6b01459. Epub 2017 Mar 8.
233
+ The other way, 'layered', estimates a threshold as a 30%%
234
+ quantile of the probabilities gathered in the fringe set, i.e.
235
+ isotopologues that are direct neighbours of the previously
236
+ accepted layer. Finally, choosing the 'ordered' version will
237
+ provide a loglinear version of the algorithm that relies on
238
+ the priority queue. This version automatically sorts
239
+ the configurations by their probability.
240
+
241
+ trim
242
+ while using a layered method, should one discard superfluously
243
+ obtained isotopologues, i.e. such that without them the set of
244
+ reported isotopologues already is an optimal p-set.
245
+
246
+ output_format
247
+ Should the output be presented as lists ('lists'),
248
+ or as numpy arrays ('numpy_arrays').
249
+
250
+ Returns
251
+ -------
252
+ masses
253
+ masses of isotopologues, either a list or a numpy array.
254
+
255
+ logProbs
256
+ logarithms of probabilities (theoretical heights) of isotopologues,
257
+ either a list or a numpy array.
258
+
259
+ confs
260
+ counts of isotopologues (extended chemical formulas that
261
+ include counts of isotopes of elements)
262
+ """
263
+
264
+
265
+ assert output_format in ('lists', 'numpy_arrays'), "Wrong value of output_format. Should be either 'lists' or 'numpy_arrays'."
266
+
267
+ assert method in ('layered', 'ordered', 'threshold_absolute', 'threshold_relative', 'layered_estimating'), "Wrong value of method. Should be among 'layered', 'ordered', 'threshold_absolute', 'threshold_relative', or 'layered_estimating'."
268
+
269
+ assert isinstance(cutoff, float), "Provided cut off ain't a float."
270
+
271
+ assert isinstance(formula, str), "Provided formula off ain't a string."
272
+
273
+ iso = IsoSpec.IsoFromFormula( formula,
274
+ cutoff,
275
+ tabSize = 1000,
276
+ hashSize = 1000,
277
+ classId = None,
278
+ method = method,
279
+ step = 0.25,
280
+ trim = trim )
281
+
282
+ if output_format == 'lists':
283
+ masses, logProbs, confs = iso.getConfs()
284
+ else:
285
+ masses, logProbs, confs = iso.getConfsNumpy()
286
+
287
+ # print 'Rev Startek is a silly old chump and his mother dresses up silly.'
288
+ return masses, logProbs, confs
289
+
290
+
@@ -0,0 +1,51 @@
1
+ from .isoFFI import isoFFI
2
+ from collections import defaultdict
3
+
4
+
5
+ # There IS a real exported C variable with this exact value,
6
+ # isospec_number_of_isotopic_entries (element_tables.cpp), which would
7
+ # eliminate the hand-duplication below entirely if it were usable -- but
8
+ # cffi's dlopen()-based ABI mode (what isoFFI.py uses; there's no
9
+ # compile-time cffi build step here) cannot read a plain `extern const
10
+ # size_t` global: `NotImplementedError: non-integer constant
11
+ # 'isospec_number_of_isotopic_entries' cannot be accessed from a dlopen()
12
+ # library` (confirmed by trying it directly). Only #define-style integer
13
+ # constants (NUMBER_OF_ISOTOPIC_ENTRIES below) are readable this way, and
14
+ # that one is exactly the hand-typed value that must be kept in sync by hand
15
+ # with ISOSPEC_NUMBER_OF_ISOTOPIC_ENTRIES in element_tables.h -- forgetting
16
+ # to bump it already caused one real bug (the #51/deuterium fix's first
17
+ # pass silently missed "D" here even though the compiled library itself
18
+ # already had it). No independent runtime cross-check is possible in ABI
19
+ # mode, so there's nothing to assert against -- if you add table entries,
20
+ # bump NUMBER_OF_ISOTOPIC_ENTRIES in isoFFI.py's cdef by hand, in the same
21
+ # commit, or elements will silently go missing here again.
22
+ number_of_isotopic_entries = isoFFI.clib.NUMBER_OF_ISOTOPIC_ENTRIES
23
+
24
+ symbol_to_masses = defaultdict(tuple)
25
+ symbol_to_massNo = defaultdict(tuple)
26
+ symbol_to_probs = defaultdict(tuple)
27
+ symbol_to_atomic_number = {}
28
+
29
+ for i in range(number_of_isotopic_entries):
30
+ symbol = isoFFI.ffi.string(isoFFI.clib.elem_table_symbol[i]).decode("ascii")
31
+ symbol_to_masses[symbol] += (isoFFI.clib.elem_table_mass[i],)
32
+ symbol_to_massNo[symbol] += (isoFFI.clib.elem_table_massNo[i],)
33
+ symbol_to_probs[symbol] += (isoFFI.clib.elem_table_probability[i],)
34
+ symbol_to_atomic_number[symbol] = isoFFI.clib.elem_table_atomicNo[i]
35
+
36
+ symbol_to_masses = dict(symbol_to_masses)
37
+ symbol_to_probs = dict(symbol_to_probs)
38
+
39
+ # Several derivative convenience dicts...
40
+ symbol_to_massprob = dict((key, list(zip(symbol_to_masses[key], symbol_to_probs[key]))) for key in symbol_to_probs.keys())
41
+
42
+ def crossprod(l1, l2):
43
+ return sum(x1*x2 for x1, x2 in zip(l1, l2))
44
+
45
+ symbol_to_avg_mass = dict((key, crossprod(symbol_to_masses[key], symbol_to_probs[key])) for key in symbol_to_probs.keys())
46
+
47
+ def maxprod(l1, l2):
48
+ return max(zip(l1, l2), key = lambda x: x[1])[0]
49
+
50
+ symbol_to_monoisotopic_mass = dict((key, maxprod(symbol_to_masses[key], symbol_to_probs[key])) for key in symbol_to_probs.keys())
51
+
IsoSpecPy/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ from .IsoSpecPy import *
2
+ import importlib.metadata
3
+
4
+ __version__ = importlib.metadata.version("IsoSpecPy")
5
+
6
+ # Old, deprecated name, for compatibility with 1.9.X only
7
+ IsoLayered = IsoTotalProb
8
+
9
+ # For backward compatibility with 1.0.X:
10
+ class CompatIsoWrapper(object):
11
+ def __init__(self):
12
+ from .IsoSpecPyOld import IsoSpec, IsoSpecify, IsoPlot
13
+ self.IsoSpec = IsoSpec
14
+ self.IsoSpecify = IsoSpecify
15
+ self.IsoPlot = IsoPlot
16
+
17
+
18
+ IsoSpecPy = CompatIsoWrapper()
19
+
IsoSpecPy/__main__.py ADDED
@@ -0,0 +1,25 @@
1
+ from . import __version__
2
+
3
+ if __name__ == "__main__":
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ parser = argparse.ArgumentParser(description="IsoSpecPy: Python interface to IsoSpec++ library, a fast and memory-efficient library for computing isotopic distributions.")
8
+ parser.add_argument('--version', '-v', action='version', version=__version__)
9
+ parser.add_argument('--libpath', action='store_true',
10
+ help='Print the path to the loaded C++ library and exit.')
11
+ parser.add_argument('--include', action='store_true',
12
+ help='Print the include path for the headers of the C++ library and exit.')
13
+ args = parser.parse_args()
14
+
15
+ if args.libpath:
16
+ try:
17
+ from .isoFFI import IsoFFI
18
+ ffi = IsoFFI()
19
+ print(Path(ffi.libpath).resolve())
20
+ except ImportError as e:
21
+ print(f"Error loading IsoSpecPy: {e}")
22
+ exit(0)
23
+
24
+ if args.include:
25
+ print(Path(__file__).parent.resolve())
@@ -0,0 +1,122 @@
1
+ """Here we store formulas for approximating numbers of isotopologues and subisotopologues."""
2
+ from math import exp, log, lgamma, pi
3
+
4
+ from .IsoSpecPy import IsoParamsFromFormula
5
+
6
+
7
+ def log_multinomial_confs_cnt(n, i):
8
+ """Return the natural logarithm of the number of configurations of the multinomial distribution.
9
+
10
+ There are n independent trials of an experiment that results in one of the i outcomes.
11
+ For each outcome, count how many times it occured.
12
+ These counts then follow the multinomial distribution.
13
+
14
+ Args:
15
+ n (int): The number of trials.
16
+ i (int): The number of possible outcomes.
17
+ """
18
+ return lgamma(n + i) - \
19
+ lgamma(n + 1) - \
20
+ lgamma(i)
21
+
22
+ def test_log_multinomial_confs_cnt():
23
+ x = log_multinomial_confs_cnt(10,3)
24
+ assert abs(x - 4.189655) < 10**(-5)
25
+
26
+
27
+
28
+ def log_V_simplex(n, i):
29
+ r"""Get the natural logarithm of the volume of a simplex {(x_1,..,x_{i-1}): \sum_{j=1}^i = n}.
30
+
31
+ Args:
32
+ n (int): The number of atoms of the element.
33
+ i (int): The number of isotopes of the element.
34
+ """
35
+ return (i-1)*log(n) - lgamma(i)
36
+
37
+
38
+ def test_log_V_simplex():
39
+ x = log_V_simplex(10,3)
40
+ assert abs(x - 3.912023) < 10**(-5)
41
+
42
+
43
+ def V_simplex(n, i):
44
+ r"""Get the volume of a simplex {(x_1,..,x_{i-1}): \sum_{j=1}^i = n}.
45
+
46
+ Args:
47
+ n (int): The number of atoms of the element.
48
+ i (int): The number of isotopes of the element.
49
+ """
50
+ return exp(log_V_simplex(n,i))
51
+
52
+ def log_V_ellipsoid(n, R2, probs):
53
+ """Get the natural logarithm of the volume of the ellipsoid.
54
+
55
+ The ellipsoid is defined by x' W^{-1} x <= R2,
56
+ where W = diag(probs[0:-1]) - probs[0:-1] * probs[0:-1]'
57
+ and R2 is the square of the radius.
58
+ diag(probs) is a matrix with values probs[0:-1] on the diagonal,
59
+ and probs[0:-1] * probs[0:-1]' is a projection on probs[0:-1].
60
+ Since probabilities are nonzero and sum to one, then det W != 0.
61
+ Also, the expression does not really depend upon the choice of one of the
62
+ ommited probability term, e.g. the outcome would stay the same if we remover p[1].
63
+
64
+ Args:
65
+ n (int): The number of atoms of the element.
66
+ R2 (float): The square of the radius of the ellipsoid.
67
+ probs (list): List with the natural frequencies of isotopes.
68
+ """
69
+ i = len(probs)
70
+ log_det = sum(log(p) for p in probs)
71
+ return (log_det + (i-1)*(log(n) + log(R2) + log(pi)))/2.0 - lgamma((i+1)/2.0)
72
+
73
+
74
+ def test_log_V_ellipsoid():
75
+ assert abs(log_V_ellipsoid(100, 10, [.2,.3,.5]) - 6.299206) < 10**(-5)
76
+
77
+
78
+ def V_ellipsoid(n, R2, probs):
79
+ """Get the volume of the ellipsoid."""
80
+ return exp(log_V_ellipsoid(n, R2, probs))
81
+
82
+ def log_subisotopologue_cnt(atoms_cnt, isotope_frequencies, ellipsoid_R2):
83
+ """Get the natural logarithm of the approximate number of subisotopologues.
84
+
85
+ Args:
86
+ atoms_cnt (int): The number of atoms of the given element.
87
+ isotope_frequencies (list): The natural frequencies of isotopes (sum to one).
88
+ ellipsoid_R2 (float): The radius of the ellipsoid used to approximate the optimal P-set.
89
+ """
90
+ isotopes_cnt = len(isotope_frequencies)
91
+ return log_multinomial_confs_cnt(atoms_cnt, isotopes_cnt) + \
92
+ log_V_ellipsoid(atoms_cnt, ellipsoid_R2, isotope_frequencies) - \
93
+ log_V_simplex(atoms_cnt, isotopes_cnt)
94
+
95
+ def test_log_subisotopologue_cnt():
96
+ assert abs(log_subisotopologue_cnt(100, [.2,.3,.5], 10) - 6.328959) < 10**(-5)
97
+
98
+
99
+ def subisotopologue_cnt(atoms_cnt, isotope_frequencies, ellipsoid_R2):
100
+ """Get the approximate number of subisotopologues."""
101
+ return exp(log_subisotopologue_cnt(atoms_cnt, isotope_frequencies, ellipsoid_R2))
102
+
103
+
104
+ def approximate_subisotopologues(molecule, P):
105
+ """Approximate the number of subisotopologues.
106
+
107
+ Args:
108
+ molecule (str): A string with molecule, e.g. 'C100H202'.
109
+ P (float): The joint probability threshold.
110
+ """
111
+ from scipy.stats import chi2
112
+ assert P >= 0 and P <= 1, 'That is not a probability.'
113
+ mol = IsoParamsFromFormula(molecule)
114
+ chi2_df = sum(len(p) for p in mol.probs) - len(mol.probs)
115
+ R2 = chi2.ppf(q=P, df=chi2_df)
116
+ return {e: subisotopologue_cnt(n, p, R2) for n, p, e in \
117
+ zip(mol.atomCounts, mol.probs, mol.elems)}
118
+
119
+
120
+ if __name__ == '__main__':
121
+ print(approximate_subisotopologues('C100H202', .999))
122
+ print(approximate_subisotopologues("C100H100", .999))
@@ -0,0 +1,16 @@
1
+
2
+
3
+
4
+
5
+ class ConfsPassthrough(object):
6
+ def __init__(self, confs_parser, size):
7
+ self.confs_parser = confs_parser
8
+ self.size = size
9
+
10
+ def __len__(self):
11
+ return self.size
12
+
13
+ def __getitem__(self, idx):
14
+ return self.confs_parser(idx)
15
+
16
+