IsoSpecPy 2.5.0__tar.gz → 2.6.0__tar.gz

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 (72) hide show
  1. {isospecpy-2.5.0 → isospecpy-2.6.0}/PKG-INFO +55 -6
  2. {isospecpy-2.5.0 → isospecpy-2.6.0}/README.md +54 -5
  3. isospecpy-2.6.0/VERSION +1 -0
  4. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/aligned_ptr.h +59 -9
  5. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/allocator.h +8 -1
  6. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/composition.h +9 -0
  7. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/cwrapper.cpp +84 -19
  8. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/cwrapper.h +20 -1
  9. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/dirtyAllocator.h +2 -1
  10. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/fasta.cpp +28 -1
  11. isospecpy-2.6.0/src/IsoSpec++/fasta.h +120 -0
  12. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/fasta_mods.cpp +62 -30
  13. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/fasta_mods.h +7 -0
  14. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/fixedEnvelopes.cpp +573 -216
  15. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/fixedEnvelopes.h +24 -8
  16. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_dispatch.cpp +90 -32
  17. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_kernel_impl.h +17 -6
  18. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_kernels.h +11 -2
  19. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isoMath.cpp +71 -12
  20. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isoMath.h +72 -4
  21. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isoSpec++.cpp +192 -117
  22. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isoSpec++.h +221 -34
  23. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/marginalTrek++.cpp +139 -45
  24. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/marginalTrek++.h +25 -11
  25. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/misc.cpp +2 -7
  26. isospecpy-2.6.0/src/IsoSpec++/operators.h +64 -0
  27. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/platform.h +14 -0
  28. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/unimod.cpp +24 -16
  29. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/unimod.h +4 -5
  30. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/unity-build.cpp +0 -1
  31. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/IsoSpecPy.py +131 -51
  32. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/isoFFI.py +13 -1
  33. isospecpy-2.5.0/VERSION +0 -1
  34. isospecpy-2.5.0/src/IsoSpec++/fasta.h +0 -70
  35. isospecpy-2.5.0/src/IsoSpec++/operators.cpp +0 -38
  36. isospecpy-2.5.0/src/IsoSpec++/operators.h +0 -150
  37. {isospecpy-2.5.0 → isospecpy-2.6.0}/LICENCE +0 -0
  38. {isospecpy-2.5.0 → isospecpy-2.6.0}/pyproject.toml +0 -0
  39. {isospecpy-2.5.0 → isospecpy-2.6.0}/skbuild/CMakeLists.txt +0 -0
  40. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/IsaDispatch.cmake +0 -0
  41. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/allocator.cpp +0 -0
  42. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/btrd.h +0 -0
  43. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/composition.cpp +0 -0
  44. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/conf.h +0 -0
  45. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/dirtyAllocator.cpp +0 -0
  46. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/element_lookup.cpp +0 -0
  47. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/element_lookup.h +0 -0
  48. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/element_tables.cpp +0 -0
  49. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/element_tables.h +0 -0
  50. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_kernels_avx.cpp +0 -0
  51. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_kernels_baseline.cpp +0 -0
  52. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_kernels_v3.cpp +0 -0
  53. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/isa_kernels_v4.cpp +0 -0
  54. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/iso_simd.h +0 -0
  55. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/misc.h +0 -0
  56. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/mman.cpp +0 -0
  57. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/mman.h +0 -0
  58. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/platform_incl.h +0 -0
  59. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/pod_vector.h +0 -0
  60. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/python-build.cpp +0 -0
  61. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/summator.h +0 -0
  62. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/unimod_support.h +0 -0
  63. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpec++/unimod_table_data.h +0 -0
  64. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/Advanced.py +0 -0
  65. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/Distributions.py +0 -0
  66. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/Formulas.py +0 -0
  67. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/IsoSpecPyOld.py +0 -0
  68. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/PeriodicTbl.py +0 -0
  69. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/__init__.py +0 -0
  70. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/__main__.py +0 -0
  71. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/approximations.py +0 -0
  72. {isospecpy-2.5.0 → isospecpy-2.6.0}/src/IsoSpecPy/confs_passthrough.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: IsoSpecPy
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: IsoSpecPy is a Python library for computing isotopic distributions of molecules.
5
5
  Keywords: isotopic distribution,mass spectrometry,chemistry,isotopic envelope,isotopologues
6
6
  Author: Michał Startek, Mateusz Łącki
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  IsoSpec is a fine-structure isotopic distribution calculator for chemical formulas — given a molecule, it computes the masses and probabilities of its isotopologues. It is fast enough to handle large molecules (proteins, oligonucleotides) where the full distribution has astronomically many configurations, and it returns provably-optimal subsets that cover a chosen fraction of the total probability mass.
34
34
 
35
- IsoSpec is primarily used as a library by mass spectrometry software. It is implemented in C++ (`src/IsoSpec++/`) and shipped with first-class bindings for Python (`IsoSpecPy`, on PyPI) and R (`IsoSpecR`, on CRAN).
35
+ IsoSpec is primarily used as a library by mass spectrometry software. It is implemented in C++ (`src/IsoSpec++/`) and shipped with first-class bindings for Python (`IsoSpecPy`, on PyPI), R (`IsoSpecR`, on CRAN) and Rust (`isospec`, the crate in `rust/`).
36
36
 
37
37
  ## Installation
38
38
 
@@ -54,6 +54,15 @@ install.packages("IsoSpecR")
54
54
 
55
55
  From source: `cd src && R CMD build IsoSpecR && R CMD INSTALL IsoSpecR_*.tar.gz`.
56
56
 
57
+ ### Rust
58
+
59
+ ```toml
60
+ [dependencies]
61
+ isospec = "0.1"
62
+ ```
63
+
64
+ The build script compiles the bundled C++ library; a C++20 compiler is all that is needed. See `rust/README.md`.
65
+
57
66
  ### C++ library
58
67
 
59
68
  ```bash
@@ -160,6 +169,27 @@ clang++ -std=c++20 water.cpp src/IsoSpec++/unity-build.cpp -o water
160
169
 
161
170
  (`unity-build.cpp` is a single translation unit that `#include`s every source file — see `Examples/C++/COMPILING`.)
162
171
 
172
+ ### Rust
173
+
174
+ ```rust
175
+ use isospec::{Iso, PeptideOptions};
176
+
177
+ fn main() -> Result<(), isospec::Error> {
178
+ let iso = Iso::from_formula("H2O1")?;
179
+ for (mass, prob) in iso.envelope().total_prob(0.999)?.iter() {
180
+ println!("{mass}\t{prob}");
181
+ }
182
+
183
+ // A modified peptide; unlike Python's peptide_sequence=, the default
184
+ // here includes the terminal water.
185
+ let peptide = Iso::from_peptide("PEPTC[UNIMOD:4]DEK", &PeptideOptions::default())?;
186
+ println!("{}", peptide.monoisotopic_mass());
187
+ Ok(())
188
+ }
189
+ ```
190
+
191
+ See `rust/examples/` for binned spectra, generators with configurations, and labelled elements.
192
+
163
193
  ### R
164
194
 
165
195
  ```r
@@ -179,7 +209,19 @@ IsoSpecify(molecule = composition, stopCondition = 0.999)
179
209
 
180
210
  Like the Python `ParsePeptideSequence`, this returns the residue composition without a free peptide's terminal H2O -- add `c(H = 2, O = 1)` to it for the neutral molecule. A modification whose delta removes more atoms of an element than the bare sequence provides gives a negative count (`"G[UNIMOD:11]"` nets `H -1`, `S -1`); `IsoSpecify` rejects such a vector rather than quietly dropping those elements.
181
211
 
182
- See `Examples/R/` for radiolabelling, full-spectrum extraction, and Unimod-annotated sequences.
212
+ The rest of the API takes the same named vector, and treats an envelope as what `IsoSpecify` returns, a matrix with columns `mass` and `prob`:
213
+
214
+ ```r
215
+ insulin <- c(C = 254, H = 377, N = 65, O = 75, S = 6)
216
+ IsoProperties(insulin)$averageMass # also mono/lightest/heaviest/mode peaks, variance
217
+ IsoBinned(insulin, binWidth = 1) # unit-resolution distribution, computed directly
218
+ IsoStochastic(insulin, ionicCurrent = 1e4, seed = 1) # simulated, reproducible measurement
219
+ wassersteinDistance(IsoSpecify(insulin, 0.999), IsoSpecify(c(C = 254, H = 377, N = 65, O = 74, S = 7), 0.999))
220
+ ```
221
+
222
+ with `envelopeConvolve`, `envelopeLinearCombination`, `envelopeBin` and `envelopeResample` for envelope arithmetic, and `orientedWassersteinDistance`, `abyssalWassersteinDistance` and `wassersteinMatch` beside `wassersteinDistance`.
223
+
224
+ See `Examples/R/` for radiolabelling, full-spectrum extraction, Unimod-annotated sequences, and binned, simulated and compared envelopes.
183
225
 
184
226
  ## Advanced features
185
227
 
@@ -189,9 +231,11 @@ See `Examples/R/` for radiolabelling, full-spectrum extraction, and Unimod-annot
189
231
  - `IsoBinned` — histogram-style envelope at a chosen bin width.
190
232
  - **FASTA/peptide-sequence support** — build an `Iso` directly from an amino-acid sequence, with `[UNIMOD:<id>]` modification tags recognized inline; optionally include the N/C-terminal water.
191
233
  - **Custom isotopic tables** — override natural abundances per element (e.g. for radio- or stable-isotope labelling). See `Examples/*/radiolabelling.*`.
192
- - **Fixed-envelope arithmetic** — addition, normalization, convolution, Wasserstein distance.
234
+ - **Fixed-envelope arithmetic** — addition, normalization, convolution, binning, resampling, Wasserstein distances.
235
+ - **Reproducible sampling** — stochastic envelopes and resampling take a seed (`seed=` in Python and R, `seeded_rng()` in C++, `with_seed` in Rust); a seed gives the same counts on every platform and in every binding.
193
236
  - **Nominal-mass mode** — compute distributions over nucleon counts instead of real masses.
194
- - **Streaming generators (for performance)** — the tabulated functions above materialize the full envelope into arrays. For large molecules where that uses too much memory, or for pipelines that consume one configuration at a time (e.g. binning on the fly), use the generator classes instead: `IsoThresholdGenerator`, `IsoLayeredGenerator`, `IsoOrderedGenerator`, `IsoStochasticGenerator`. They yield one isotopologue per call to `advanceToNextConfiguration()` without ever storing the full set. `IsoOrderedGenerator` additionally streams in strict order of decreasing probability.
237
+ - **Thread safety** — the library never parallelises its own work, but independent molecules and envelopes may be used from any number of threads, and a shared molecule or envelope may be read from several at once.
238
+ - **Streaming generators (for performance)** — the tabulated functions above materialize the full envelope into arrays. For large molecules where that uses too much memory, or for pipelines that consume one configuration at a time (e.g. binning on the fly), use the generator classes instead: `IsoThresholdGenerator`, `IsoLayeredGenerator`, `IsoOrderedGenerator`, `IsoStochasticGenerator`. They yield one isotopologue per call to `advanceToNextConfiguration()` without ever storing the full set. `IsoOrderedGenerator` additionally streams in strict order of decreasing probability. Generators are in C++, Python and Rust; R has the materialized functions only.
195
239
 
196
240
  Algorithmic details are in the papers cited below; the Supporting Information of each is the better starting point for implementation specifics.
197
241
 
@@ -201,11 +245,16 @@ Algorithmic details are in the papers cited below; the Supporting Information of
201
245
  src/IsoSpec++/ C++ core library (the algorithms live here)
202
246
  src/IsoSpecPy/ Python binding, loaded via cffi
203
247
  src/IsoSpecR/ R binding, via Rcpp
248
+ rust/ Rust crate (Cargo.toml at the repository root), via cxx
204
249
  Examples/ Working examples in each language
205
- tests/ C, C++, and Python test suites
250
+ tests/ C, C++, and Python test suites (R's are in src/IsoSpecR/tests, Rust's in rust/tests)
206
251
  skbuild/ scikit-build-core entry point used when installing the Python wheel
207
252
  ```
208
253
 
254
+ ## Changes
255
+
256
+ `CHANGELOG` lists the major changes in each release (the Rust crate keeps its own, `rust/CHANGELOG.md`).
257
+
209
258
  ## Citation
210
259
 
211
260
  If IsoSpec is useful in your research, please cite:
@@ -2,7 +2,7 @@
2
2
 
3
3
  IsoSpec is a fine-structure isotopic distribution calculator for chemical formulas — given a molecule, it computes the masses and probabilities of its isotopologues. It is fast enough to handle large molecules (proteins, oligonucleotides) where the full distribution has astronomically many configurations, and it returns provably-optimal subsets that cover a chosen fraction of the total probability mass.
4
4
 
5
- IsoSpec is primarily used as a library by mass spectrometry software. It is implemented in C++ (`src/IsoSpec++/`) and shipped with first-class bindings for Python (`IsoSpecPy`, on PyPI) and R (`IsoSpecR`, on CRAN).
5
+ IsoSpec is primarily used as a library by mass spectrometry software. It is implemented in C++ (`src/IsoSpec++/`) and shipped with first-class bindings for Python (`IsoSpecPy`, on PyPI), R (`IsoSpecR`, on CRAN) and Rust (`isospec`, the crate in `rust/`).
6
6
 
7
7
  ## Installation
8
8
 
@@ -24,6 +24,15 @@ install.packages("IsoSpecR")
24
24
 
25
25
  From source: `cd src && R CMD build IsoSpecR && R CMD INSTALL IsoSpecR_*.tar.gz`.
26
26
 
27
+ ### Rust
28
+
29
+ ```toml
30
+ [dependencies]
31
+ isospec = "0.1"
32
+ ```
33
+
34
+ The build script compiles the bundled C++ library; a C++20 compiler is all that is needed. See `rust/README.md`.
35
+
27
36
  ### C++ library
28
37
 
29
38
  ```bash
@@ -130,6 +139,27 @@ clang++ -std=c++20 water.cpp src/IsoSpec++/unity-build.cpp -o water
130
139
 
131
140
  (`unity-build.cpp` is a single translation unit that `#include`s every source file — see `Examples/C++/COMPILING`.)
132
141
 
142
+ ### Rust
143
+
144
+ ```rust
145
+ use isospec::{Iso, PeptideOptions};
146
+
147
+ fn main() -> Result<(), isospec::Error> {
148
+ let iso = Iso::from_formula("H2O1")?;
149
+ for (mass, prob) in iso.envelope().total_prob(0.999)?.iter() {
150
+ println!("{mass}\t{prob}");
151
+ }
152
+
153
+ // A modified peptide; unlike Python's peptide_sequence=, the default
154
+ // here includes the terminal water.
155
+ let peptide = Iso::from_peptide("PEPTC[UNIMOD:4]DEK", &PeptideOptions::default())?;
156
+ println!("{}", peptide.monoisotopic_mass());
157
+ Ok(())
158
+ }
159
+ ```
160
+
161
+ See `rust/examples/` for binned spectra, generators with configurations, and labelled elements.
162
+
133
163
  ### R
134
164
 
135
165
  ```r
@@ -149,7 +179,19 @@ IsoSpecify(molecule = composition, stopCondition = 0.999)
149
179
 
150
180
  Like the Python `ParsePeptideSequence`, this returns the residue composition without a free peptide's terminal H2O -- add `c(H = 2, O = 1)` to it for the neutral molecule. A modification whose delta removes more atoms of an element than the bare sequence provides gives a negative count (`"G[UNIMOD:11]"` nets `H -1`, `S -1`); `IsoSpecify` rejects such a vector rather than quietly dropping those elements.
151
181
 
152
- See `Examples/R/` for radiolabelling, full-spectrum extraction, and Unimod-annotated sequences.
182
+ The rest of the API takes the same named vector, and treats an envelope as what `IsoSpecify` returns, a matrix with columns `mass` and `prob`:
183
+
184
+ ```r
185
+ insulin <- c(C = 254, H = 377, N = 65, O = 75, S = 6)
186
+ IsoProperties(insulin)$averageMass # also mono/lightest/heaviest/mode peaks, variance
187
+ IsoBinned(insulin, binWidth = 1) # unit-resolution distribution, computed directly
188
+ IsoStochastic(insulin, ionicCurrent = 1e4, seed = 1) # simulated, reproducible measurement
189
+ wassersteinDistance(IsoSpecify(insulin, 0.999), IsoSpecify(c(C = 254, H = 377, N = 65, O = 74, S = 7), 0.999))
190
+ ```
191
+
192
+ with `envelopeConvolve`, `envelopeLinearCombination`, `envelopeBin` and `envelopeResample` for envelope arithmetic, and `orientedWassersteinDistance`, `abyssalWassersteinDistance` and `wassersteinMatch` beside `wassersteinDistance`.
193
+
194
+ See `Examples/R/` for radiolabelling, full-spectrum extraction, Unimod-annotated sequences, and binned, simulated and compared envelopes.
153
195
 
154
196
  ## Advanced features
155
197
 
@@ -159,9 +201,11 @@ See `Examples/R/` for radiolabelling, full-spectrum extraction, and Unimod-annot
159
201
  - `IsoBinned` — histogram-style envelope at a chosen bin width.
160
202
  - **FASTA/peptide-sequence support** — build an `Iso` directly from an amino-acid sequence, with `[UNIMOD:<id>]` modification tags recognized inline; optionally include the N/C-terminal water.
161
203
  - **Custom isotopic tables** — override natural abundances per element (e.g. for radio- or stable-isotope labelling). See `Examples/*/radiolabelling.*`.
162
- - **Fixed-envelope arithmetic** — addition, normalization, convolution, Wasserstein distance.
204
+ - **Fixed-envelope arithmetic** — addition, normalization, convolution, binning, resampling, Wasserstein distances.
205
+ - **Reproducible sampling** — stochastic envelopes and resampling take a seed (`seed=` in Python and R, `seeded_rng()` in C++, `with_seed` in Rust); a seed gives the same counts on every platform and in every binding.
163
206
  - **Nominal-mass mode** — compute distributions over nucleon counts instead of real masses.
164
- - **Streaming generators (for performance)** — the tabulated functions above materialize the full envelope into arrays. For large molecules where that uses too much memory, or for pipelines that consume one configuration at a time (e.g. binning on the fly), use the generator classes instead: `IsoThresholdGenerator`, `IsoLayeredGenerator`, `IsoOrderedGenerator`, `IsoStochasticGenerator`. They yield one isotopologue per call to `advanceToNextConfiguration()` without ever storing the full set. `IsoOrderedGenerator` additionally streams in strict order of decreasing probability.
207
+ - **Thread safety** — the library never parallelises its own work, but independent molecules and envelopes may be used from any number of threads, and a shared molecule or envelope may be read from several at once.
208
+ - **Streaming generators (for performance)** — the tabulated functions above materialize the full envelope into arrays. For large molecules where that uses too much memory, or for pipelines that consume one configuration at a time (e.g. binning on the fly), use the generator classes instead: `IsoThresholdGenerator`, `IsoLayeredGenerator`, `IsoOrderedGenerator`, `IsoStochasticGenerator`. They yield one isotopologue per call to `advanceToNextConfiguration()` without ever storing the full set. `IsoOrderedGenerator` additionally streams in strict order of decreasing probability. Generators are in C++, Python and Rust; R has the materialized functions only.
165
209
 
166
210
  Algorithmic details are in the papers cited below; the Supporting Information of each is the better starting point for implementation specifics.
167
211
 
@@ -171,11 +215,16 @@ Algorithmic details are in the papers cited below; the Supporting Information of
171
215
  src/IsoSpec++/ C++ core library (the algorithms live here)
172
216
  src/IsoSpecPy/ Python binding, loaded via cffi
173
217
  src/IsoSpecR/ R binding, via Rcpp
218
+ rust/ Rust crate (Cargo.toml at the repository root), via cxx
174
219
  Examples/ Working examples in each language
175
- tests/ C, C++, and Python test suites
220
+ tests/ C, C++, and Python test suites (R's are in src/IsoSpecR/tests, Rust's in rust/tests)
176
221
  skbuild/ scikit-build-core entry point used when installing the Python wheel
177
222
  ```
178
223
 
224
+ ## Changes
225
+
226
+ `CHANGELOG` lists the major changes in each release (the Rust crate keeps its own, `rust/CHANGELOG.md`).
227
+
179
228
  ## Citation
180
229
 
181
230
  If IsoSpec is useful in your research, please cite:
@@ -0,0 +1 @@
1
+ 2.6.0
@@ -36,7 +36,9 @@
36
36
  // elsewhere for plain huge-page mmap): that layer has no remap concept at
37
37
  // all on Windows and no equivalent of mremap on Darwin, so each platform's
38
38
  // real growth primitive is used directly instead:
39
- // - Linux / other real-mman UNIX: mmap + mremap(MREMAP_MAYMOVE) + munmap.
39
+ // - Linux (any libc with mremap): mmap + mremap(MREMAP_MAYMOVE) + munmap.
40
+ // - Other UNIX (FreeBSD, ...: no mremap): growth maps a fresh region and
41
+ // copies, shrink unmaps the tail in place.
40
42
  // - Apple: mmap/munmap for create/destroy (Darwin has no mremap); growth
41
43
  // re-wires pages into a fresh mapping via mach_vm_remap instead of
42
44
  // copying, shrink just munmaps the tail in place.
@@ -57,25 +59,41 @@
57
59
 
58
60
  // The small-allocation backend needs an aligned allocator that can also be
59
61
  // deallocated with a plain std::free() (release() promises exactly that).
60
- // glibc/libc++'s std::aligned_alloc satisfies both at once. MSVC's CRT has
62
+ // glibc/libc++'s std::aligned_alloc satisfies both at once. Windows' CRT
63
+ // (MSVC's, and the msvcrt/UCRT that MinGW, i.e. R's Rtools, links against) has
61
64
  // no C11 aligned_alloc at all, only _aligned_malloc/_aligned_free -- and
62
65
  // memory from those two is NOT free()-compatible (the CRT stores its own
63
66
  // offset bookkeeping ahead of the returned pointer, which plain free() does
64
- // not know how to unwind). So on MSVC the small backend uses
67
+ // not know how to unwind). So on Windows the small backend uses
65
68
  // _aligned_malloc/_aligned_free for its own storage (still genuinely
66
69
  // Alignment-aligned, as marginalTrek++.h's use of this class relies on),
67
70
  // and release() falls back to materialising a fresh plain-malloc()'d copy
68
71
  // to keep its free()-compatibility promise -- the same thing it already
69
72
  // does to escape a VM-backed region.
70
- #if defined(_MSC_VER)
73
+ #if defined(_WIN32)
71
74
  #define ISOSPEC_ALIGNED_PTR_SMALL_BACKEND_FREE_COMPATIBLE 0
72
75
  #else
73
76
  #define ISOSPEC_ALIGNED_PTR_SMALL_BACKEND_FREE_COMPATIBLE 1
74
77
  #endif
75
78
 
76
79
  #if ISOSPEC_WE_ARE_ON_WINDOWS
80
+ // Without NOMINMAX, <windows.h> defines min and max as macros, which break
81
+ // every later std::min(...) and numeric_limits<T>::max() in the including
82
+ // translation unit. Defined only around this include, so that the macro
83
+ // itself does not leak into the caller's code (though a later <windows.h>
84
+ // there is then a no-op, without the macros); a caller who included
85
+ // <windows.h> first, macros and all, is covered by the parenthesised
86
+ // (std::min)(...) calls in the public headers.
87
+ #ifndef NOMINMAX
88
+ #define NOMINMAX
89
+ #define ISOSPEC_UNDEF_NOMINMAX
90
+ #endif
77
91
  #include <windows.h>
78
- #if defined(_MSC_VER)
92
+ #ifdef ISOSPEC_UNDEF_NOMINMAX
93
+ #undef NOMINMAX
94
+ #undef ISOSPEC_UNDEF_NOMINMAX
95
+ #endif
96
+ #if defined(_WIN32)
79
97
  #include <malloc.h> // _aligned_malloc / _aligned_free
80
98
  #endif
81
99
  #elif ISOSPEC_WE_ARE_ON_UNIX_YAY
@@ -303,7 +321,26 @@ inline void vm_resize(vm_region& r, std::size_t new_bytes)
303
321
  r.size = new_size;
304
322
  }
305
323
 
306
- #else // Linux / other real-mman UNIX with an actual mremap()
324
+ #else // Other UNIX: Linux with mremap(), the rest (FreeBSD, ...) without
325
+
326
+ // ThreadSanitizer intercepts mmap() and munmap() but not mremap(), so a region
327
+ // that mremap() moves away from is freed behind its back and keeps its old
328
+ // shadow state. When another thread's mremap() later lands on those
329
+ // addresses, TSan reports its writes as racing with the previous owner's --
330
+ // pure address reuse, reproduced with two threads and nothing else. Under
331
+ // TSan, growth therefore goes through mmap + copy + munmap, which it does see.
332
+ #if defined(__SANITIZE_THREAD__)
333
+ #define ISOSPEC_ALIGNED_PTR_UNDER_TSAN 1
334
+ #elif defined(__has_feature)
335
+ #if __has_feature(thread_sanitizer)
336
+ #define ISOSPEC_ALIGNED_PTR_UNDER_TSAN 1
337
+ #endif
338
+ #endif
339
+
340
+ // mremap() is Linux's alone; MREMAP_MAYMOVE, from <sys/mman.h>, is the test.
341
+ #if defined(MREMAP_MAYMOVE) && !defined(ISOSPEC_ALIGNED_PTR_UNDER_TSAN)
342
+ #define ISOSPEC_ALIGNED_PTR_USE_MREMAP 1
343
+ #endif
307
344
 
308
345
  inline void vm_resize(vm_region& r, std::size_t new_bytes)
309
346
  {
@@ -311,11 +348,24 @@ inline void vm_resize(vm_region& r, std::size_t new_bytes)
311
348
  if (new_size == r.size)
312
349
  return;
313
350
 
351
+ #if !defined(ISOSPEC_ALIGNED_PTR_USE_MREMAP)
352
+ if (new_size < r.size) {
353
+ // Shrink in place: base address is untouched, just drop the tail.
354
+ ::munmap(static_cast<char*>(r.base) + new_size, r.size - new_size);
355
+ r.size = new_size;
356
+ return;
357
+ }
358
+ vm_region moved = vm_create(new_bytes);
359
+ std::memcpy(moved.base, r.base, (std::min)(r.size, moved.size));
360
+ vm_destroy(r);
361
+ r = moved;
362
+ #else
314
363
  void* p = ::mremap(r.base, r.size, new_size, MREMAP_MAYMOVE);
315
364
  if (p == MAP_FAILED)
316
365
  throw std::bad_alloc();
317
366
  r.base = p;
318
367
  r.size = new_size;
368
+ #endif
319
369
  }
320
370
 
321
371
  #endif // __APPLE__
@@ -549,7 +599,7 @@ class aligned_unique_ptr {
549
599
  // still has to copy -- there is nothing yet to remap.
550
600
  aligned_ptr_detail::vm_region r = aligned_ptr_detail::vm_create(new_bytes);
551
601
  if (ptr_) {
552
- std::memcpy(r.base, ptr_, std::min(capacity_bytes_, new_bytes));
602
+ std::memcpy(r.base, ptr_, (std::min)(capacity_bytes_, new_bytes));
553
603
  free_small(ptr_);
554
604
  }
555
605
  region_ = r;
@@ -562,7 +612,7 @@ class aligned_unique_ptr {
562
612
 
563
613
  T* new_ptr = allocate_small(n);
564
614
  if (ptr_) {
565
- std::memcpy(new_ptr, ptr_, std::min(capacity_bytes_, new_bytes));
615
+ std::memcpy(new_ptr, ptr_, (std::min)(capacity_bytes_, new_bytes));
566
616
  free_small(ptr_);
567
617
  }
568
618
  ptr_ = new_ptr;
@@ -573,7 +623,7 @@ class aligned_unique_ptr {
573
623
  // bare std::free() / C free(), regardless of which backend produced it.
574
624
  // Whenever the current backend isn't already free()-compatible on its
575
625
  // own -- a VM-backed region always needs this, and so does the small
576
- // backend on MSVC (see ISOSPEC_ALIGNED_PTR_SMALL_BACKEND_FREE_COMPATIBLE
626
+ // backend on Windows (see ISOSPEC_ALIGNED_PTR_SMALL_BACKEND_FREE_COMPATIBLE
577
627
  // above) -- this materialises a fresh plain-malloc()'d copy instead.
578
628
  // That copy is the whole cost of calling this in those cases;
579
629
  // release_with_deleter() below avoids it when the caller can accept a
@@ -23,6 +23,13 @@
23
23
  namespace IsoSpec
24
24
  {
25
25
 
26
+ // Configurations per allocator slab. Small on purpose: with glibc, a slab of
27
+ // up to 1032 bytes (4 isotopes x 64 ints = 1024) is recycled through the
28
+ // per-thread tcache when the next molecule is built, so many small molecules
29
+ // in a row run 16-20% faster than with 1000 (2000 tryptic peptides, threshold
30
+ // generator, x86-64-v3 and v4). Single large calculations do not notice.
31
+ constexpr int default_tab_size = 64;
32
+
26
33
  template <typename T> inline void copyConf(
27
34
  const T* source, T* destination,
28
35
  int dim
@@ -39,7 +46,7 @@ template <typename T> class Allocator
39
46
  pod_vector<T*> prevTabs;
40
47
 
41
48
  public:
42
- explicit Allocator(const int dim, const int tabSize = 10000);
49
+ explicit Allocator(const int dim, const int tabSize = default_tab_size);
43
50
  ~Allocator();
44
51
 
45
52
  Allocator(const Allocator& other) = delete;
@@ -27,6 +27,7 @@
27
27
 
28
28
  #pragma once
29
29
 
30
+ #include <string_view>
30
31
  #include <vector>
31
32
 
32
33
  #include "isoSpec++.h"
@@ -87,6 +88,14 @@ struct ExpandedComposition {
87
88
  }
88
89
  };
89
90
 
91
+ //! A formula string read as a composition: each element once, in the order
92
+ //! written, counts signed ("H-2O-1" is accepted -- a composition may be a
93
+ //! delta; build_iso_from_composition rejects negatives when one becomes a
94
+ //! molecule). Throws std::invalid_argument for a malformed formula, an unknown
95
+ //! element or one written twice, std::out_of_range for a count past int. The
96
+ //! same tokenizer and checks as parse_formula, which goes through it.
97
+ ElementComposition parse_formula_composition(std::string_view formula);
98
+
90
99
  //! Resolves each element of `composition` to its isotopes' masses and
91
100
  //! probabilities from element_tables.h, filling a caller-owned `out`
92
101
  //! (cleared first).
@@ -300,6 +300,43 @@ void* setupIsoStochasticGenerator(void* iso,
300
300
  beta_bias);
301
301
  });
302
302
  }
303
+
304
+ namespace
305
+ {
306
+ // The random state a seeded generator draws from has to live as long as the
307
+ // generator and be built before it: a base class listed first is both.
308
+ struct OwnedRng
309
+ {
310
+ std::mt19937 owned_rng;
311
+ explicit OwnedRng(uint64_t seed) : owned_rng(seeded_rng(seed)) {}
312
+ };
313
+
314
+ class SeededIsoStochasticGenerator : private OwnedRng, public IsoStochasticGenerator
315
+ {
316
+ public:
317
+ SeededIsoStochasticGenerator(Iso&& iso, size_t no_molecules, double precision, double beta_bias, uint64_t seed) :
318
+ OwnedRng(seed),
319
+ IsoStochasticGenerator(std::move(iso), no_molecules, precision, beta_bias, owned_rng)
320
+ {}
321
+ };
322
+ } // namespace
323
+
324
+ void* setupIsoStochasticGeneratorSeeded(void* iso,
325
+ size_t no_molecules,
326
+ double precision,
327
+ double beta_bias,
328
+ uint64_t seed)
329
+ {
330
+ return c_guard([&]() -> void*
331
+ {
332
+ // Handed out as the base, like the unseeded generator, so every
333
+ // *IsoStochasticGenerator function (delete included, through the
334
+ // virtual destructor) works on it unchanged.
335
+ IsoStochasticGenerator* generator = new SeededIsoStochasticGenerator(
336
+ std::move(*reinterpret_cast<Iso*>(iso)), no_molecules, precision, beta_bias, seed);
337
+ return generator;
338
+ });
339
+ }
303
340
  ISOSPEC_C_FN_CODES(IsoStochasticGenerator)
304
341
 
305
342
  // ______________________________________________________ FixedEnvelopes
@@ -351,6 +388,26 @@ void* setupStochasticFixedEnvelope(void* iso,
351
388
  });
352
389
  }
353
390
 
391
+ void* setupStochasticFixedEnvelopeSeeded(void* iso,
392
+ size_t no_molecules,
393
+ double precision,
394
+ double beta_bias,
395
+ bool get_confs,
396
+ uint64_t seed)
397
+ {
398
+ return c_guard([&]() -> void*
399
+ {
400
+ std::mt19937 rng = seeded_rng(seed);
401
+ return new FixedEnvelope(
402
+ FixedEnvelope::FromStochastic(Iso(*reinterpret_cast<const Iso*>(iso), true),
403
+ no_molecules,
404
+ precision,
405
+ beta_bias,
406
+ get_confs,
407
+ rng));
408
+ });
409
+ }
410
+
354
411
 
355
412
  void* setupBinnedFixedEnvelope(void* iso,
356
413
  double target_total_prob,
@@ -542,6 +599,15 @@ void resampleEnvelope(void* envelope, size_t ionic_current, double beta_bias)
542
599
  return c_guard([&]{ reinterpret_cast<FixedEnvelope*>(envelope)->resample(ionic_current, beta_bias); });
543
600
  }
544
601
 
602
+ void resampleEnvelopeSeeded(void* envelope, size_t ionic_current, double beta_bias, uint64_t seed)
603
+ {
604
+ return c_guard([&]
605
+ {
606
+ std::mt19937 rng = seeded_rng(seed);
607
+ reinterpret_cast<FixedEnvelope*>(envelope)->resample(ionic_current, beta_bias, rng);
608
+ });
609
+ }
610
+
545
611
 
546
612
  void* binnedEnvelope(void* envelope, double width, double middle)
547
613
  {
@@ -614,27 +680,26 @@ void* parseFastaWithModsC(const char* sequence, const char* unimod_db_path)
614
680
  {
615
681
  return c_guard([&]() -> void*
616
682
  {
617
- // Reused across calls (parse_fasta_with_mods_into only clears it,
618
- // never releases capacity) -- avoids reallocating the scratch
619
- // ElementComposition on every single call from Python/R, which is
620
- // the real hot path this matters for (millions of peptides). Plain
621
- // `static`, not `thread_local`: IsoSpec is single-threaded by design
622
- // (this repo's CLAUDE.md) so they'd behave identically here, and
623
- // thread_local's extra TLS teardown machinery is a real, observed
624
- // crash risk for a dynamically-loaded Python/R extension module
625
- // (segfault during interpreter shutdown, seen in CI on macOS) that
626
- // buys nothing in a library that never has a second thread. Nothing
627
- // below retains a pointer into `scratch` itself: `counts` is copied
628
- // out by value, and `symbols` is built from elem_table_symbol's
629
- // static string-literal pointers, not from `scratch`'s own storage --
630
- // safe to overwrite `scratch` on the next call.
631
- static ElementComposition scratch;
632
- parse_fasta_with_mods_into(sequence, scratch, resolve_unimod_table(unimod_db_path));
683
+ // A fresh composition per call, not a reused `static` one: a static
684
+ // shared by every thread is a data race, and `thread_local` is no way
685
+ // out -- its TLS teardown has crashed a dynamically-loaded Python/R
686
+ // extension module at interpreter shutdown (seen in CI on macOS).
687
+ // Reserving up front makes each vector one allocation rather than a
688
+ // doubling series, and `count` is moved into the handle rather than
689
+ // copied, so the net cost over the static is one malloc/free pair:
690
+ // measured at +7 ns per call (43 -> 50 ns) on unmodified peptides and
691
+ // +2.5% on modified ones, x86-64-v3, gcc -- small next to any
692
+ // binding's own per-call overhead. 16 covers an unmodified peptide
693
+ // (C, H, N, O, S, Se) with room for what a few modifications add.
694
+ ElementComposition composition;
695
+ composition.element_first_index.reserve(16);
696
+ composition.count.reserve(16);
697
+ parse_fasta_with_mods_into(sequence, composition, resolve_unimod_table(unimod_db_path));
633
698
  std::unique_ptr<ElementCompositionHandle> handle(new ElementCompositionHandle());
634
- handle->counts = scratch.count;
635
- handle->symbols.reserve(scratch.element_first_index.size());
636
- for(int idx : scratch.element_first_index)
699
+ handle->symbols.reserve(composition.element_first_index.size());
700
+ for(int idx : composition.element_first_index)
637
701
  handle->symbols.push_back(elem_table_symbol[idx]);
702
+ handle->counts = std::move(composition.count);
638
703
  return handle.release();
639
704
  });
640
705
  }
@@ -16,6 +16,9 @@
16
16
 
17
17
  #pragma once
18
18
 
19
+ #include <stddef.h>
20
+ #include <stdint.h>
21
+
19
22
  #include "platform.h"
20
23
 
21
24
  #define ISOSPEC_ALGO_LAYERED 0
@@ -117,6 +120,15 @@ ISOSPEC_C_API double probIsoStochasticGenerator(void* generator);
117
120
  ISOSPEC_C_API void get_conf_signatureIsoStochasticGenerator(void* generator, int* space);
118
121
  ISOSPEC_C_API bool advanceToNextConfigurationIsoStochasticGenerator(void* generator);
119
122
  ISOSPEC_C_API void deleteIsoStochasticGenerator(void* generator);
123
+ // The *Seeded variants draw from a generator seeded with `seed`, so the same
124
+ // seed reproduces the same sample (also across bindings). The unseeded ones draw
125
+ // from a randomly seeded per-thread generator. A seeded stochastic generator
126
+ // owns its random state and is used and deleted like an unseeded one.
127
+ ISOSPEC_C_API void* setupIsoStochasticGeneratorSeeded(void* iso,
128
+ size_t no_molecules,
129
+ double precision,
130
+ double beta_bias,
131
+ uint64_t seed);
120
132
 
121
133
  ISOSPEC_C_API void* setupThresholdFixedEnvelope(void* iso,
122
134
  double threshold,
@@ -134,6 +146,13 @@ ISOSPEC_C_API void* setupStochasticFixedEnvelope(void* iso,
134
146
  double beta_bias,
135
147
  bool get_confs);
136
148
 
149
+ ISOSPEC_C_API void* setupStochasticFixedEnvelopeSeeded(void* iso,
150
+ size_t no_molecules,
151
+ double precision,
152
+ double beta_bias,
153
+ bool get_confs,
154
+ uint64_t seed);
155
+
137
156
  ISOSPEC_C_API void* setupBinnedFixedEnvelope(void* iso,
138
157
  double target_total_prob,
139
158
  double bin_width,
@@ -185,7 +204,6 @@ ISOSPEC_C_API double empiricStddev(void* tabulator);
185
204
  ISOSPEC_C_API double wassersteinDistance(void* tabulator1, void* tabulator2);
186
205
  ISOSPEC_C_API double orientedWassersteinDistance(void* tabulator1, void* tabulator2);
187
206
  ISOSPEC_C_API double abyssalWassersteinDistance(void* tabulator1, void* tabulator2, double abyss_depth, double other_scale);
188
- // ISOSPEC_C_API double abyssalWassersteinDistanceGrad(void* const* envelopes, const double* scales, double* ret_gradient, size_t N, double abyss_depth_exp, double abyss_depth_the);
189
207
 
190
208
  struct ws_match_res{
191
209
  double res1;
@@ -203,6 +221,7 @@ ISOSPEC_C_API void scaleEnvelope(void* envelope, double factor);
203
221
  ISOSPEC_C_API void normalizeEnvelope(void* envelope);
204
222
  ISOSPEC_C_API void shiftMassEnvelope(void* envelope, double d_mass);
205
223
  ISOSPEC_C_API void resampleEnvelope(void* envelope, size_t ionic_current, double beta_bias);
224
+ ISOSPEC_C_API void resampleEnvelopeSeeded(void* envelope, size_t ionic_current, double beta_bias, uint64_t seed);
206
225
  ISOSPEC_C_API void* binnedEnvelope(void* envelope, double width, double middle);
207
226
  ISOSPEC_C_API void* linearCombination(void* const * const envelopes, const double* intensities, size_t count);
208
227
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  #include <cstring>
20
20
  #include "pod_vector.h"
21
+ #include "allocator.h"
21
22
 
22
23
  namespace IsoSpec
23
24
  {
@@ -33,7 +34,7 @@ class DirtyAllocator
33
34
  pod_vector<void*> prevTabs;
34
35
 
35
36
  public:
36
- explicit DirtyAllocator(const int dim, const int tabSize = 10000);
37
+ explicit DirtyAllocator(const int dim, const int tabSize = default_tab_size);
37
38
  ~DirtyAllocator();
38
39
 
39
40
  DirtyAllocator(const DirtyAllocator& other) = delete;
@@ -54,7 +54,7 @@ const double aa_elem_probabilities[19] = {
54
54
  };
55
55
 
56
56
 
57
- const int aa_symbol_to_elem_counts[256*6] = {
57
+ constexpr int aa_symbol_to_elem_counts[256*6] = {
58
58
  /* Code: 0 unprintable */ 0, 0, 0, 0, 0, 0,
59
59
  /* Code: 1 unprintable */ 0, 0, 0, 0, 0, 0,
60
60
  /* Code: 2 unprintable */ 0, 0, 0, 0, 0, 0,
@@ -312,4 +312,31 @@ const int aa_symbol_to_elem_counts[256*6] = {
312
312
  /* Code: 254 unprintable */ 0, 0, 0, 0, 0, 0
313
313
  };
314
314
 
315
+ namespace {
316
+
317
+ constexpr PackedElemCounts pack_elem_counts()
318
+ {
319
+ PackedElemCounts ret{};
320
+ for(int c = 0; c < 256; c++)
321
+ for(int ii = 0; ii < 6; ii++)
322
+ ret.row[c] |= static_cast<std::uint64_t>(aa_symbol_to_elem_counts[c*6 + ii]) << (AA_PACKED_FIELD_BITS * ii);
323
+ return ret;
324
+ }
325
+
326
+ constexpr bool packed_counts_cannot_overflow()
327
+ {
328
+ for(int ii = 0; ii < 256*6; ii++)
329
+ if(aa_symbol_to_elem_counts[ii] < 0 ||
330
+ aa_symbol_to_elem_counts[ii] * AA_PACKED_FLUSH_EVERY >= (1 << AA_PACKED_FIELD_BITS))
331
+ return false;
332
+ return 6 * AA_PACKED_FIELD_BITS <= 64;
333
+ }
334
+
335
+ static_assert(packed_counts_cannot_overflow(),
336
+ "AA_PACKED_FLUSH_EVERY residues of the largest count must fit an AA_PACKED_FIELD_BITS field");
337
+
338
+ } // namespace
339
+
340
+ constexpr PackedElemCounts aa_symbol_to_packed_elem_counts = pack_elem_counts();
341
+
315
342
  } // namespace IsoSpec